Git guilt によるパッチ管理

Git には MQ のようなパッチ管理はできないかと調べてみました。

## MQ とは

分散バージョン管理システムと言えば有名な [Git](https://git-scm.com/) のほかに [Mercurial SCM](https://www.mercurial-scm.org/) があります。

Mercurial には Mercurial Queue (MQ) という拡張機能があり、これを有効化すると、通常のチェンジセット (コミット) とは独立してパッチを管理することができるようになります。

- [JapaneseMqExtension - Mercurial](https://www.mercurial-scm.org/wiki/JapaneseMqExtension)

例えば、以下のような場合に使えます。

-   何かオープンソースソフトウェアの実装を調査したりデバッグする際に、標準にはないログを追加するとき

-   自分や自分のいる組織特有の事情があって、ソフトウェア本体を改変する必要があるが、改変箇所をメインブランチにコミットやマージをしたくない場合

MQ でパッチを管理するようになると、メインブランチのリビジョンが進んでも、それに合わせて自分用のログ機能や改変部分のパッチを適用しなおすことができるのでとても便利です。

## Git におけるパッチ管理

Git にも MQ のようなパッチ管理ツールがないかと調べたら、Guilt というのがありました。

-   [Public Git Hosting - guilt.git/summary](http://repo.or.cz/w/guilt.git)

MQ に強い影響を受けているようです。

> Since guilt is heavily based on Mercurial queues (mq), some of the following is shamelessly stolen from the mq page [[1]].

## Guilt のインストール

macOS にインストールしてみました。

1.  リポジトリからクローン

    ```
    % git clone http://repo.or.cz/guilt.git
    Cloning into 'guilt'...
    remote: Counting objects: 3844, done.
    remote: Total 3844 (delta 0), reused 0 (delta 0)
    Receiving objects: 100% (3844/3844), 624.05 KiB | 152.00 KiB/s, done.
    Resolving deltas: 100% (2864/2864), done.
    ```

2.  インストール

    ```
    % make install
    install -d /usr/local/bin/
    install -m 755 guilt /usr/local/bin/
    install -d /usr/local/lib/guilt/
    install -m 755 guilt-add guilt-applied guilt-branch guilt-commit guilt-delete guilt-diff guilt-export guilt-files guilt-fold guilt-fork guilt-graph guilt-guard guilt-header guilt-help guilt-import guilt-import-commit guilt-init guilt-new guilt-next guilt-patchbomb guilt-pop guilt-prev guilt-push guilt-rebase guilt-refresh guilt-repair guilt-rm guilt-select guilt-series guilt-status guilt-top guilt-unapplied /usr/local/lib/guilt/
    install -m 644 os.Darwin os.FreeBSD os.Linux os.SunOS /usr/local/lib/guilt/
    ```

3.  確認

    ```
    % guilt 
    Guilt v0.36

    Pick a command:
            add      files   import         prev     series
            applied  fold    import-commit  push     status
            branch   fork    init           rebase   top
            commit   graph   new            refresh  unapplied
            delete   guard   next           repair
            diff     header  patchbomb      rm
            export   help    pop            select

    Example:
            guilt push
    ```

インストールできたようです。

## Guilt を使ってみる

### 準備

[hakimel/reveal.js: The HTML Presentation Framework](https://github.com/hakimel/reveal.js/) を題材に Guilt を使ってみます。


まずは reveal.js をクローンします。

```
% git clone https://github.com/hakimel/reveal.js.git
Cloning into 'reveal.js'...
remote: Counting objects: 10252, done.
remote: Total 10252 (delta 0), reused 0 (delta 0), pack-reused 10252
Receiving objects: 100% (10252/10252), 7.68 MiB | 450.00 KiB/s, done.
Resolving deltas: 100% (5648/5648), done.
% cd reveal.js 
% ls -F
.git/                   README.md               lib/
.gitignore              bower.json              package.json
.travis.yml             css/                    plugin/
CONTRIBUTING.md         demo.html               test/
Gruntfile.js            index.html
LICENSE                 js/
```

`.git/` の中は以下のようになっています。普通です。

```
% ls -F .git          
HEAD            description     index           logs/           packed-refs
config          hooks/          info/           objects/        refs/
```

### Guilt の初期化

パッチを管理できるように Guilt を初期化します。

```
$ guilt init
```

`.git/` の中に `patches/` が出来ていました。

```
% ls -F .git      
HEAD            hooks/          logs/           patches/
config          index           objects/        refs/
description     info/           packed-refs
```

MQ を使ったことのある人は分かるかもしれませんが、チェックアウトしているブランチ名のディレクトリがあるほかは、 MQ と同じファイル構成です。

```
% ls -F -R .git/patches
master/

.git/patches/master:
series  status
```

### 最初のパッチを作る

では、reveal.js のソースを少し弄って、 `index.html` にソースコード用のフォントのスタイルを追加してみることにします。

まずはパッチを定義します。ここではパッチ名を `code-font` としました。

```
% guilt new code-font
```

`git status` を実行してみたら、新しいブランチになっていました。

```
% git status
On branch guilt/master
nothing to commit, working tree clean
```

少し紛らわしいのですが、`git remote` で `guilt` が出ないことから、 (ローカルに) `guilt/master` という名前のブランチがあることが分かります (`origin/master` はリモート `origin` の `master` ブランチです)。

```
% git remote
origin
```

ここで、`index.html` を編集し、保存します。編集した内容は `guilt diff` で確認できます

```diff
% guilt diff
diff --git a/index.html b/index.html
index 98accc3..394d1ca 100644
--- a/index.html
+++ b/index.html
@@ -12,6 +12,12 @@
                <!-- Theme used for syntax highlighting of code -->
                <link rel="stylesheet" href="lib/css/zenburn.css">
 
+               <style type="text/css">
+               .reveal code {
+                       font-family: "Source Code Pro", "Courier New", monospace;
+               }
+               </style>
+
                <!-- Printing and PDF exports -->
                <script>
                        var link = document.createElement( 'link' );
```

`git status` で作業ディレクトリの状態を見てみます。

```
% git status
On branch guilt/master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")
```

この変更を `guilt refresh` を使って、パッチ `code-font` に反映させます。

```
% guilt refresh
Patch code-font refreshed
```

### 2つ目のパッチを作る

新しい変更を入れてみます。

今度は reveal.js のスライド表示で、コントロール (矢印) が表示されないようにします。

```
% guilt new hide-controls
```

再び、`index.html` を編集し、保存します。ここでは以下のように編集しました。

```diff
% guilt diff
diff --git a/index.html b/index.html
index 394d1ca..331fee6 100644
--- a/index.html
+++ b/index.html
@@ -43,6 +43,7 @@
                        // - https://github.com/hakimel/reveal.js#configuration
                        // - https://github.com/hakimel/reveal.js#dependencies
                        Reveal.initialize({
+                               controls: false,
                                dependencies: [
                                        { src: 'plugin/markdown/marked.js' },
                                        { src: 'plugin/markdown/markdown.js' },
```

やはり先程と同様に `guilt refresh` を使って、パッチ `hide-controls` に反映させます。

```
% guilt refresh
Patch hide-controls refreshed
```

### パッチキューを確認する

コミットログを見ると、これまでの 2つのパッチがコミットされていることが分かります。

```
% git log master..HEAD --oneline
43af056 (HEAD -> guilt/master, refs/patches/master/hide-controls) patch hide-controls
1838f54 (refs/patches/master/code-font) patch code-font
```

また、`guilt series` で一連のパッチ (パッチキュー、パッチスタック) を確認できます。

```
% guilt series 
code-font
hide-controls
```

### パッチを外す

適用したパッチを外すのは `guilt pop` です。

```
% guilt pop
Now at code-font.
```

`master` ブランチと作業ディレクトリを比較すると、先程の `hide-control` で変更した内容がなくなっていることが分かります。

```diff
% git diff master
diff --git a/index.html b/index.html
index 98accc3..394d1ca 100644
--- a/index.html
+++ b/index.html
@@ -12,6 +12,12 @@
                <!-- Theme used for syntax highlighting of code -->
                <link rel="stylesheet" href="lib/css/zenburn.css">
 
+               <style type="text/css">
+               .reveal code {
+                       font-family: "Source Code Pro", "Courier New", monospace;
+               }
+               </style>
+
                <!-- Printing and PDF exports -->
                <script>
                        var link = document.createElement( 'link' );
```

`guilt applied` と `guilt unapplied` で適用されているパッチと適用されていないパッチが分かります。

```
% guilt applied  # 適用されているパッチ

code-font
% guilt unapplied # 適用されていないパッチ

hide-controls
% guilt series # 全てのパッチ
code-font
hide-controls
```

### パッチを再更新する

この状態で、一度、作成したパッチを修正して、フォントの定義を変更するものとします。

まずは `index.html` をそのまま編集します。
ここでは `Source Han Code JP` を追加しました。

```diff
% git diff     
diff --git a/index.html b/index.html
index 108a274..c31d574 100644
--- a/index.html
+++ b/index.html
@@ -14,7 +14,7 @@
 
                <style type="text/css">
                .reveal code {
-                       font-family: "Source Code Pro", "Courier New", monospace;
+                       font-family: "Source Han Code JP", "Source Code Pro", "Courier New", monospace;
                }
                </style>

```

ファイルを編集したら `guilt refresh` するのは同じです。


```
% guilt refresh
Patch code-font refreshed
```

差分を見ると反映されていることが分かります。

```diff
% git diff HEAD~
diff --git a/index.html b/index.html
index 98accc3..108a274 100644
--- a/index.html
+++ b/index.html
@@ -12,6 +12,12 @@
                <!-- Theme used for syntax highlighting of code -->
                <link rel="stylesheet" href="lib/css/zenburn.css">
 
+               <style type="text/css">
+               .reveal code {
+                       font-family: "Source Han Code JP", "Source Code Pro", "Courier New", monospace;
+               }
+               </style>
+
                <!-- Printing and PDF exports -->
                <script>
                        var link = document.createElement( 'link' );
```

### パッチを当て直す

外したパッチを当て直すのは `guilt push` です。

```
% guilt push    
Applying patch..hide-controls
Patch applied.
```

`guilt applied` と `guilt unapplied` で適用されているパッチを確認してみます。

```
% guilt applied
code-font
hide-controls
% guilt unapplied 

```

### パッチをまとめて当てたり外したり

全てのパッチをまとめて当てたり外したりするときは `-a` または `--all` オプションです。

```
% guilt pop -a  
All patches popped.
% guilt push --all
Applying patch..code-font
Patch applied.
Applying patch..hide-controls
Patch applied.
```

### 別のブランチにパッチを当てる

MQ と異なり、Guilt はパッチがブランチ毎に管理されているので、他のブランチにパッチを当てるのは少し面倒です。

まずは、`guilt branch` で Guilt のパッチをコピーして新しいブランチを作ります。

既に同名のローカルブランチがある場合、`guilt branch` コマンドが失敗するので注意が必要です。


```
% guilt branch dev   
Switched to branch 'dev'
```

パッチがコピーされていることを確認します。

```
% guilt series    
code-font
hide-controls
```

リモートブランチがあるなら追跡ブランチの設定をします。

```
% git branch -u origin/dev 
Branch 'dev' set up to track remote branch 'dev' from 'origin'.
```

ローカルブランチをリモートブランチにリセットします。

```
% git reset --hard origin/dev
HEAD is now at 0c946ae fix missing theme line-height when printing #1967
```

`git pull` だとマージが発生するおそれがあるため、ハードリセットしました。

そして、元のパッチを全て適用します。

```
% guilt push -a
Applying patch..code-font
Patch applied.
Applying patch..hide-controls
Patch applied.
```

## MQ との違い

### コマンド体系

今回、Guilt の基本的な操作を試してみました。

Guilt は MQ ととてもよく似た操作で操作できることが分かります。
コマンドは MQ だと `hg qrefresh` とするところを `guilt refresh` とするように、大体 `hg q` を `guilt ` に置換するだけです。

### ブランチ毎のパッチ管理

前述の通り、Guilt ではブランチ毎にパッチが管理されてます。

MQ ではブランチの区別なくパッチ管理を管理するため、別のブランチへのパッチ適用が簡単です (そもそも Git と Mercurial ではブランチの考え方が少し異なりますが)。

### パッチのバージョン管理

Guilt は MQ と違ってパッチそのものをバージョン管理することはできないようです。

MQ では、`hg init --mq` コマンドでパッチそのものをバージョン管理することができますが、それに相当する Guilt のコマンドは見つけられませんでした。


## まとめ

Git でも MQ のようなパッチ管理できることが分かりました。

しかし、やはり Guilt でもブランチに依存しないパッチ管理やパッチのバージョン管理ができると使いやすかったと思います。