Git使いがMercurial使いに転職するとき設定しておくべきMercurial拡張


2011年 03月 15日

Mercurialは、Merucurial拡張という拡張モジュールを使って、Merucrialの挙動をいろいろ拡張できるようになっています。
デフォルトのままだと使いにくいので、Mercurialを使う上で便利にしてくれる拡張を設定しておきましょう。
デフォルトでバンドルされているMercurial拡張は、Using Mercurial Extensionsにまとめられています。

今回はGit使いがMercurial使いに転職するときに、Gitで実現できたことをMercurialで実現するための、組み込み拡張、および、サードパーティ製の拡張について紹介します。

色づけしよう

ブランチの確認、diff、パッチ等々、色づけされていないとつらいです。
というわけでGit同様に色づけしましょう。
Color Extensionはすでにバンドルされているので、.hgrcに次の記述を加えましょう。

[extensions]
color =

この拡張は、ANSIカラーとWindowsコンソールの両方をサポートしており、
一般的な開発環境で使用する場合、自動でターミナルの情報を認識して設定してくれます。
ただ、任意のモード、たとえばWindowsコンソールモードを使用したい場合、

[color]
# 有効なモードは、win32, ansi, auto, off の4種
mode = win32

と記述します。
これでhg status や hg log、hg branchesなどが色づけされます。
これらの色づけの挙動を変更したい場合は、それぞれの設定を上書きできます。

別名を定義

よくつかうコマンドや、オプションを組み合わせたものを頻繁に使うときはaliasを設定しておくと便利です。
Alias Extensionはすでに、Version1.3からMercurial拡張からコアへと組み込まれました。
そのため、Gitで設定するaliasのように設定できます。

筆者は、

[alias]
l = log
lb = log -b
gl = glog
gll = glob --limit
pr = pull --rebase
sh = shelve
ush = unshelve
qi = qimport
qr = qrefresh
qf = qfinish

などを追加しています。

リポジトリの情報を確認する

Subversionで提供されているsvn infoは便利です。
Gitではgit config –listで確認します。
そこでMercurialでもhg infoを実現するInfo Extensionが作られています。
これはデフォルトでバンドルされていないので、モジュール(info.py)をダウンロードして設定します。

[extensions]
info = /path/to/info.py

拡張モジュールは任意の場所に保存しておきましょう。
.hgrcでそのモジュールのパスを渡すと使用できるようになります。

$ hg info
Repository: /home/yoppi/work/mercurial/sandbox/info [hg root]
Base Hash: ae91f328abaefb69886b34dcb7cb9fe4843c8301 [hg id -r0]
Revisions: 1 [hg tip --template "{rev}"]
Files: 1 [hg manifest | wc -l]
Cloned From: None [hg paths default]

改行コード

はるか昔から議論されているにも関わらず、改行コードにまつわるトラブルは現在も顕在しています。
Gitであれば、設定ファイルにcore.safecrlfやcore.autocrlfなどの設定で改行コードのトラブルを回避できます。
MercurialではEOL Extensionを使ってそんなわずらわしさから解放されましょう。
~/.hgrcに次の設定を加えます。

[extensions]
eol =

プロジェクト内で、改行コードを統一するためにプロジェクトルートに次の.hgeolファイルを設置しておきましょう。
たとえば、Javaで開発しているときに基本的に改行コードをLFで統一したい場合、

[patterns]
**.java = LF
**.jsp = LF
**.tag = LF
**.xml = LF
...

としておくと、たとえば、ローカルでCRLFのファイルをリモートリポジトリにpushするときにLFに変換してくれます。

歴史をグラフで確認する

MercurialのGUIクライアントを使うとログをきれいにグラフで見ることができますが、Graph log extensionを使うとgit log –graph相当のものを実現できます。

[extensions]
graphlog =

と~/.hgrcに記述します。
コマンドは、

$ hg glog [OPTIONS]

でログをグラフ状に確認できます。

また、Graph Log Extensionを有効にすると、hg log コマンドに -G オプションが追加され、同様にコミットの歴史をグラフ表示できます。
Mercurialは、Gitと異なりMultiple Headの概念があることから、常にグラフ形式でコミットログを確認することをお勧めします。

簡単マージ

Git使いがMercurial使いへ転職するときに、git pullの挙動とhg pullの挙動の違いにみなさん驚かれることでしょう。
git pullすると、リモートブランチから取得してワーキングブランチへ取り込みマージまでしてくれるのに対して、hg pullはリモートブランチのチェンジセットをローカルに取り込むだけしかしてくれません。
そこで、Fetch Extensionを使いましょう。
~/.hgrcに次の設定を追加します。

[extensions]
fetch =

これでfetchできるようになります。

hg fetch

hg pull、hg mergeを一度にこなしてくれます。
ただし、Fast Forwardですむ場合でも不要なマージが発生し、歴史を振り返った時に追いかけにくくなってしまうので、マージしたくない場合は次に紹介するrebaseを使いましょう。

歴史に不要なマージを減らす

現在あるブランチで開発していて、いくつかコミットを築いたとします。
分散VSCでは、ローカルで開発し、コミットの歴史はローカルで積み重ねられるので、リモートブランチにpushするタイミングで歴史を取り込み、マージしてからpushすることになります。
Mercurialではhg pull、hg merge、hg updateという流れを踏みます。

しかし、pullするときに本来ならFast ForwardですむのにMultiple Head(無名のブランチ)を作られることにより無駄なマージが発生します。
Gitでは、お互いが編集していない場合では、マージが発生せずFast Forwardで歴史を取り込みます。
Gitはこの仕組みをrebaseと呼んでいます。
この機能をMercurialでも実現したのがRebase Extensionです。
有効にするには、.hgrcに次の設定を加えましょう。

[extensions]
rebase =

では、実際にリモートリポジトリから歴史をローカルに適用してみましょう。
リモートリポジトリにmaintブランチ、featureブランチ、およびdefaultブランチが存在するとします。
開発者Aliceはfeatureブランチで新機能開発を開発者Bobと共同で進めています。
ここでAliceは機能Aを開発しているとしましょう。同様にBobも機能Aを開発しています。

[bob]$ h gl --style compact
@  4[tip]:2   1724a9784c79   2011-03-14 15:36 +0900   bob
|    added foo4
|
| o  3:0   f6b470b38c33   2011-03-14 15:33 +0900   alice
| |    bug fixed hoge
| |
o |  2   296b91be79de   2011-03-14 15:32 +0900   alice
| |    added foo2
| |
o |  1   952cffa88cb6   2011-03-14 15:28 +0900   alice
|/     developping foo
|
o  0   15c785b69dc6   2011-03-14 15:12 +0900   alice
     added hoge
[alice]$ h gl --style compact
@  4[tip]:2   4bfa526da3c5   2011-03-14 15:36 +0900   alice
|    added  foo3
|
| o  3:0   f6b470b38c33   2011-03-14 15:33 +0900   alice
| |    bug fixed hoge
| |
o |  2   296b91be79de   2011-03-14 15:32 +0900   alice
| |    added foo2
| |
o |  1   952cffa88cb6   2011-03-14 15:28 +0900   alice
|/     developping foo
|
o  0   15c785b69dc6   2011-03-14 15:12 +0900   alice
     added hoge

AliceとBobは機能fooに対しそれぞれfoo3とfoo4を追加しています。
これらは別のファイルであり、かつ同一機能なので歴史はFast Fowardでになってほしいわけです。
Bobが先にpushしてAliceがfoo3をpullして取り込むとしましょう。

するとAliceのローカルブランチでは次のような歴史になるはずです。

[alice]$hg glog --style compact
o  5[tip]:2   1724a9784c79   2011-03-14 15:36 +0900   bob
|    added foo4
|
| @  4:2   4bfa526da3c5   2011-03-14 15:36 +0900   alice
|/     added  foo3
|
| o  3:0   f6b470b38c33   2011-03-14 15:33 +0900   alice
| |    bug fixed hoge
| |
o |  2   296b91be79de   2011-03-14 15:32 +0900   alice
| |    added foo2
| |
o |  1   952cffa88cb6   2011-03-14 15:28 +0900   alice
|/     developping foo
|
o  0   15c785b69dc6   2011-03-14 15:12 +0900   alice
     added hoge

Aliceがpullした直後になります。
featureブランチにrevsion5とrevision4がheadとして作成されてしまいます。
いわゆるこれが、MercurialにおけるMultiple Headです。

さてこのままMercurialのお作法に従うと、hg mergeして4をマージします。
すると、featureブランチで同じfoo機能を開発しているにもかかわらず無駄なマージが発生してしまうのです!
これは、不本意なマージです。

[alice]$ hg glog --style compact
@    6[tip]:4,5   93cc2c6a4b9f   2011-03-14 15:55 +0900   alice
|\     merged
| |
| o  5:2   1724a9784c79   2011-03-14 15:36 +0900   bob
| |    added foo4
| |
o |  4:2   4bfa526da3c5   2011-03-14 15:36 +0900   alice
|/     added  foo3
|
| o  3:0   f6b470b38c33   2011-03-14 15:33 +0900   alice
| |    bug fixed hoge
| |
o |  2   296b91be79de   2011-03-14 15:32 +0900   alice
| |    added foo2
| |
o |  1   952cffa88cb6   2011-03-14 15:28 +0900   alice
|/     developping foo
|
o  0   15c785b69dc6   2011-03-14 15:12 +0900   alice
     added hoge

不本意なマージをhg rebaseを使って回避しましょう。
これには2つの方法があります。

hg pullした時点でheadが2つできたところでrebaseする方法と、pullするときに–rebaseオプションを付ける方法です。
RebaseExtensionを有効化すると、hg pullに–rebaseオプションが追加されています。

では、rebaseコマンドを実行する方法を見てみましょう。

$ hg rebase

このコマンドでrebaseされ、Bobのコミット(rev4)のあとに自分のコミット(rev5)を重ねることができます。

[alice]$ h gl --style compact
@  5[tip]   fc163b1b87ca   2011-03-14 15:36 +0900   alice
|    added  foo3
|
o  4:2   1724a9784c79   2011-03-14 15:36 +0900   bob
|    added foo4
|
| o  3:0   f6b470b38c33   2011-03-14 15:33 +0900   alice
| |    bug fixed hoge
| |
o |  2   296b91be79de   2011-03-14 15:32 +0900   alice
| |    added foo2
| |
o |  1   952cffa88cb6   2011-03-14 15:28 +0900   alice
|/     developping foo
|
o  0   15c785b69dc6   2011-03-14 15:12 +0900   alice
     added hoge

またpullするときにhg pull –rebaseすると同様の働きをします。
このようにあとから、歴史を振り返ったときに無駄なマージが発生させないように開発することで、あとからデバッグしやすくなるのでRebaseも使いこなせるようになりましょう。

歴史を整形する

たとえば、コミットメッセージを書き換えたり、コミットを並び替えたりという歴史の整形は、Gitを使っての開発において日常茶飯事です。
Gitでは git rebase を使うと容易に歴史を変更できます。

Mercurialではどうでしょうか。Git使いには残念なお知らせがあります。
Mercurialでコミットしてしまったら、その歴史を書き換えるには rollback するしか道は残されていません。
1つ前であれば、rollbackするだけでよいですが、2つ前にコミットしたものを変更したい場合だと破綻します。

そこで、Mercurial Queue Extensionを使いましょう。
通称MQ拡張です。
これは、Mercurialにおけるパッチ管理システムなのですが、コミットした歴史を書き換えることもできます。
MQを使いこなせばMercurialでの開発が格段に便利になります。
Mercurialにバンドルされているので、

[extensions]
mq =

と.hgrcに設定しておきましょう。
ローカルリポジトリで、あるコミットログがおかしいことに気づきます。さてこのコミットログを書き直したい場合、どうすればいいでしょうか?

その変更したいコミットをパッチ化することで解決します。

$ hg glog --style compact
@  2[tip]   152f67e69b5b   2011-03-14 20:45 +0900   yoppi
|    added hoge2
|
o  1   a74c33e376cc   2011-03-14 20:33 +0900   yoppi
|    added hoge
|
o  0   24fa78ec5dba   2011-03-14 19:36 +0900   yoppi
     initial commit

rev1のコミットは追加ではなくてhoge.txtを変更したものでした。
コミットログを変更しなければ、混乱を招きます。

では、rev1からtip(rev2)までをパッチ化しましょう。

$ hg qimport -r 1:tip
$ hg qseries
1.diff
2.diff
$ hg qtop
2.diff
$ hg glog --style compact
@  2[2.diff,qtip,tip]   152f67e69b5b   2011-03-14 20:45 +0900   yoppi
|    added hoge2
|
o  1[1.diff,qbase]   a74c33e376cc   2011-03-14 20:33 +0900   yoppi
|    added hoge
|
o  0[qparent]   24fa78ec5dba   2011-03-14 19:36 +0900   yoppi
     initial commit

さてこれで、rev1とrev2をパッチ化できました。
hg glogの出力がパッチを意味するものになっていることがわかります。
hg qseriesコマンドは、現在適用されているパッチの一覧で、hg qtopは現在のパッチの先頭を表示します。
変更したいのはrev1です。
rev2のパッチが現在いる場所になるので、rev1に移ります。

$ hg qgoto 1.diff
popping 2.diff
now at: 1.diff
$ hg qtop
1.diff
$ hg glog --style compact
@  1[1.diff,qbase,qtip,tip]   a74c33e376cc   2011-03-14 20:33 +0900   yoppi
|    added hoge
|
o  0[qparent]   24fa78ec5dba   2011-03-14 19:36 +0900   yoppi
      initial commit

qgotoコマンドで編集したいパッチに移ります。
ここで驚かないでほしいのですが、rev2がログから消えてしまいました。
どこにいったのでしょうか?

そうです。qimportしたのでパッチとして残されているのです。
hg qseriesしてみましょう。

$ hg qseries
1.diff
2.diff

ColorExtensionを設定していれば、2.diffがグレーで表示されていることがわかります。
つまり、パッチとしては保存されているけれども、現在のワーキングディレクトリで有効化されていないパッチを意味します。
rev1を修正してから適用すれば問題ないので、しばしこのまま進めましょう。
パッチを適用しなおすコマンドはhg qrefreshコマンドで、コミットログを編集するには-eオプションを付けます。

$ hg qrefresh -e

エディタが起動しコミットログを編集できるようになります。
コミットログを編集したあとは保存して、エディタを終了しましょう。

$ hg glog --style compact
@  1[1.diff,qbase,qtip,tip]   d8f0251ccf0d   2011-03-14 20:33 +0900   yoppi
|    modified hoge
|
o  0[qparent]   24fa78ec5dba   2011-03-14 19:36 +0900   yoppi
     initial commit

さてこのあと、このパッチを有効化するため、qfinishしましょう。

$ hg qfinish -a
$ hg glog -style compact
@  1[tip]   d8f0251ccf0d   2011-03-14 20:33 +0900   yoppi
|    modified hoge
|
o  0   24fa78ec5dba   2011-03-14 19:36 +0900   yoppi
     initial commit

hg glogで確認するとrev1がパッチではなく本来のコミットとして扱われていることがわかるでしょう。
では、横にのけておいたrev2のパッチを再度適用しましょう。

$ hg qapplied
$ hg qpush
$ hg qfinish -a

hg qappliedは現在適用されているパッチのリストを表示するコマンドです。
rev2のパッチは適用されていないので表示されないですね。
qpushして有効化して、qfinishしましょう。rev2は本来何もいじる必要はないので、qrefreshは必要ありません。
これで元通りになります。

ところで、コミットログを変更する方法は他にも方法があります。
直接パッチを編集しても書き換えることが可能です。
.hg/patchesいかにパッチが保存されています。
hg qimport した時点で.hg/patches以下には、1.diffと2.diffのファイルが存在します。

$ hg qimport -r 1:tip
$ ls .hg/patches/
1.diff  2.diff  series  status

そこで1.diffファイルがあるのでエディタで開きましょう。

$ vim .hg/patches/1.diff
# HG changeset patch
# User yoppi
# Date 1300102423 -32400
# Node ID d8f0251ccf0dc2e3e7009b73b91284f2ae56c90c
# Parent  24fa78ec5dbae85de0687bb7d3eaef62d97ab908
modified hoge

diff --git a/hoge.txt b/hoge.txt
--- a/hoge.txt
+++ b/hoge.txt
@@ -0,0 +1,1 @@
+hoge

コミットログやその他の情報も修正できます。
コミットログを修正したら保存して終了しましょう。
まだこの段階では修正したパッチは適用されていないので、qgotoして移動し、qrefreshしてパッチを再適用しましょう。

$ hg qgoto 1.diff
$ hg qrefresh

あとは、どうようにrev2のパッチを有効化しqfinishします。

$ hg qpush
$ hg qfinish -a

今回は、コミットログの修正にMQを使用しました。
MQはいろいろな場面で便利に使える(transplantの例で使用します)ので、マニュアルを読んでいろんな場面で使うようにしてみましょう。

つまみぐい

人間である以上ミスはつきものです。メンテナンスブランチに対してバグ修正しなければならないのに、featureブランチに対して修正するというミスを犯したとしましょう。
Gitであれば、git cherry-pick を使ってその修正のみをメンテナンスブランチに持ってこれます。

MercurialではTransplant Extensionを使います。

[extensions]
transplant =

と同様に記述しておきましょう。
コマンドの定義は、次の通りです。

$ hg transplant [-s REPOSITORY] [-b BRANCH [-a]] [-p REV] [-m REV] [REV]

ブランチmaintに対して、バグ修正しなければならないのに対して、間違ってfeatureブランチで修正してしまった状況を例に挙げましょう。

$ hg glog --style=compact
@  4[tip]:2   e50e22d3998d   2011-03-10 18:30 +0900   yoppi
|    fixed foo bug2
|
| o  3:1   d9ef5a4b5b84   2011-03-10 18:29 +0900   yoppi
| |    fixed foo bug
| |
o |  2   493a5feabaf2   2011-03-10 18:28 +0900   yoppi
|/     added feature in hoge
|
o  1   2832749994fb   2011-03-10 18:26 +0900   yoppi
|    added foo
|
o  0   ed07682b0722   2011-03-10 18:23 +0900   yoppi
     initial commit

ここでコミットしてしまったrev4は本来ならばmaintブランチでコミットしなければなりませんでした。
したがってこのコミットをrev3の後に移植しましょう。
まずは、MQを使ってrev4のコミットをパッチ化しましよう。

$ hg qimport -r 4

これでrev4のコミットがパッチ化されます。

$ hg qseries
4.diff

ではmaintブランチに移って、パッチ化したrev4をtransplantで取り込みましょう。

$ hg update 3
$ hg transplant -b feature 4
$ hg glob --style compact
@  5[tip]:3   ab208c564fc4   2011-03-10 18:30 +0900   yoppi
|    fixed foo bug2
|
| o  4[4.diff,qbase,qtip]:2   e50e22d3998d   2011-03-10 18:30 +0900   yoppi
| |    fixed foo bug2
| |
o |  3:1   d9ef5a4b5b84   2011-03-10 18:29 +0900   yoppi
| |    fixed foo bug
| |
| o  2[qparent]   493a5feabaf2   2011-03-10 18:28 +0900   yoppi
|/     added feature in hoge
|
o  1   2832749994fb   2011-03-10 18:26 +0900   yoppi
|    added foo
|
o  0   ed07682b0722   2011-03-10 18:23 +0900   yoppi
     initial commit

このコマンドは、「featureブランチのリビジョン4を現在のブランチに移植する」ことを意味します。
さて、これでmaintブランチで修正できました。
ここで、glogの出力をよく見てみましょう。featureブランチで間違ったコミットのパッチが残っています。
このコミットはすでにmaintブランチに取り込んだので、もはや不要です。削除してしまいましょう。

$ hg qpop -- 現在有効化されているパッチを取り出す
$ hg qdelete -- パッチを削除する

これで再度hg glogで確認してみましょう。

$ hg glog
@  4[tip]   ab208c564fc4   2011-03-10 18:30 +0900   yoppi
|    fixed foo bug2
|
o  3:1   d9ef5a4b5b84   2011-03-10 18:29 +0900   yoppi
|    fixed foo bug
|
| o  2   493a5feabaf2   2011-03-10 18:28 +0900   yoppi
|/     added feature in hoge
|
o  1   2832749994fb   2011-03-10 18:26 +0900   yoppi
|    added foo
|
o  0   ed07682b0722   2011-03-10 18:23 +0900   yoppi
     initial commit

featureブランチから間違ったコミットが削除されていることがわかると思います。
transplantはこのように歴史の整形にも適用できるので、ぜひマスターしましょう。

部分コミット

一度にファイルを修正して、1ファイルに論理的でない変更を加えてしまったときに、Gitだとgit add -pでコミットする部分を選択できます。
MercurialでもRecord Extensionを使うと、部分的にコミットできます。
~/.hgrcに

[extensions]
record =

を追加しましょう。
これで、細かい単位(hunkと呼びます)でコミットできるようになります。
では、実際にrecordを試してみましょう。

$ hg diff
diff --git a/hoge.rb b/hoge.rb
--- a/hoge.rb
+++ b/hoge.rb
@@ -1,3 +1,12 @@
+class Hoge2
+  def initialize
+  end
+
+  def hoge2
+    "hoge2"
+  end
+end
+
 class Hoge
   def initialize
   end
@@ -7,6 +16,15 @@
   end
 end

+class Hoge3
+  def initialize
+  end
+
+  def hoge3
+    "hoge3"
+  end
+end
+
 if __FILE__ == $0
   Hoge.new.hoge
 end

hoge.rbにまちがって一度にクラスを追加してしまいました。
ここで一度にコミットするのではなくて1クラスずつ追加しないと論理的な歴史を積み重ねられません。
hg recordを使うと、hunk毎にコミットできます。

$ hg record
diff --git a/hoge.rb b/hoge.rb
2 hunks, 18 lines changed
examine changes to 'hoge.rb'? [Ynsfdaq?]

hoge.rbには2個のhunkがあってそれらをコミットするかどうかを尋ねられます。
これらのオプションの詳細はhg help recordを参照してください(その場で?で確認できます)。
ここではhoge.rbのhunkの1つ目をコミットしています。

@@ -1,3 +1,12 @@
+class Hoge2
+  def initialize
+  end
+
+  def hoge2
+    "hoge2"
+  end
+end
+
 class Hoge
   def initialize
   end
record change 1/2 to 'hoge.rb'? [Ynsfdaq?]y
@@ -7,6 +16,15 @@
   end
 end

+class Hoge3
+  def initialize
+  end
+
+  def hoge3
+    "hoge3"
+  end
+end
+
 if __FILE__ == $0
   Hoge.new.hoge
 end
record change 2/2 to 'hoge.rb'? [Ynsfdaq?] n

エディタが起動し、コミットログを記述できます。
これで一つめのhunkのみをコミットしたことになります。

ただGitのようにhunkをさらに細かく分割できません。Feature Requestとして提案されているので、しばし待ちましょう。もちろん自分でパッチ書いて投げることが望ましいです:)

一時避難

ワーキングブランチで一時的に変更したあと、他のブランチに移動してすぐに修正したい場合があります。
しかし、変更が加えられているため他のブランチに移動することはできません。
こういうときには、Gitではgit stashを使って現在のワーキングコピーでの変更を横に退けておけます。

Mercurialでもできないでしょうか?
Shelve Extensionがそれを実現してくれます。
この拡張も、デフォルトではバンドルされていないので、モジュールを取得して設定しましょう。

$ hg clone https://bitbucket.org/tksoh/hgshelve

取得したあと、.hgrcに同様に設定します。hg cloneしたディレクトリにhgshelve.pyが拡張モジュールになります。

[extensions]
hgshelve = /path/to/hgshelve.py

さて、これでMercurialでもstashできるようになりました。
ローカルのチェンジセットでの変更点を回避しておくコマンドは

$ hg shelve

となります。
逆に、横にのけておいたものを元に戻すには

$ hg unshelve

となります。
では、いくつかワーキングディレクトリのファイルに変更を加えてみましょう。

$ hg status
M hoge.rb
$ hg shelve
diff --git a/hoge.rb b/hoge.rb
1 hunks, 9 lines changed
shelve changes to 'hoge.rb'? [Ynsfdaq?]  y

これで編集した箇所を横にのけておけます。
-nオプションをつけることで、shelveに名前を付けることもできます。
このあと、他のブランチで作業し、再度戻ってきて横に退けておいたshelveを元に戻しましょう。

$ hg unshelve

これもRecord Extensionと同様に、現在hunkを細かく分割できません。