Edited at

Git guilt によるパッチ管理

More than 1 year has passed since last update.

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


MQ とは

分散バージョン管理システムと言えば有名な Git のほかに Mercurial SCM があります。

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

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


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


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


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


Git におけるパッチ管理

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

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 を題材に 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 remoteguilt が出ないことから、 (ローカルに) guilt/master という名前のブランチがあることが分かります (origin/master はリモート originmaster ブランチです)。

% git remote

origin

ここで、index.html を編集し、保存します。編集した内容は guilt 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 を編集し、保存します。ここでは以下のように編集しました。

% 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 で変更した内容がなくなっていることが分かります。

% 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 appliedguilt unapplied で適用されているパッチと適用されていないパッチが分かります。

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

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

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


パッチを再更新する

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

まずは index.html をそのまま編集します。

ここでは Source Han Code JP を追加しました。

% 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

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

% 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 appliedguilt 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 qguilt に置換するだけです。


ブランチ毎のパッチ管理

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

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


パッチのバージョン管理

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

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


まとめ

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

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