Mercurialで築いたコミットを並び替える

DVCS(GitやMercurialなど)の利点は、コードを書き散らかしたあとにとりあえずコミットして、あとから論理的な歴史になるようにコミットを並び変えたり、コミットをまとめたりできることだ。
Mercurialでは、これらの歴史の改変はMQ拡張を使って実現できる。
たとえば、次のような歴史を作っていたとしよう。

$ hg glog --style=compact
@  4[tip]   722b0c3ef8f7   2011-06-07 22:34 +0900   yoppi
|    D
|
o  3   e912ff6fd1f1   2011-06-07 22:33 +0900   yoppi
|    B
|
o  2   eaca60210a86   2011-06-07 22:33 +0900   yoppi
|    C
|
o  1   f669da88832f   2011-06-07 22:33 +0900   yoppi
|    A
|
o  0   b3a79f426bdc   2011-06-07 22:32 +0900   yoppi
     initial commit

コミットがA->C->Bとなっており論理的な並びになっていない。
そこでこのコミットを論理的な歴史に修正しよう。
コミットをまずMQでパッチにする。

$ hg qimport -r tip:1
$ hg qseries
1.diff
2.diff
3.diff
4.diff

さて、パッチ化したあとは適用済みになっているので、これらのパッチをキューから解除する。

$ hg qpop --all
popping 4.diff
popping 3.diff
popping 2.diff
popping 1.diff
patch queue now empty

キューから削除することで、各パッチを入れ替えることができる。
言い換えると、キューにenqueueする順序を変えることがコミットの順序を入れ替えることになる。
では、キューにパッチをpushする順番を変更するにはどうすればいいのか?
パッチ化したものは.hg/patches以下に保存されている。

$ ls .hg/patches
1.diff  2.diff  3.diff  4.diff  series  status

パッチの順序はこの'series'ファイルで管理されている。
したがって、ファイルの中身をエディタで書き換えることでコミットの順序を並び替えられる。
コミットの順序を正しくする(A->B->C)にはリビジョン2と3を入れ替える。

$ cat .hg/patches/series
1.diff
2.diff
3.diff
4.diff
$ vim .hg/patches/series
$ cat .hg/patches/series
1.diff
3.diff
2.diff
4.diff

このあとに、パッチをpush(適用)して、パッチをもとのコミットに戻す。

$ hg qpush --all
$ hg qfinish --all
$ hg glog --style=compact

普通はhistedit拡張を使う

MQ拡張で操作すると少し手順が多くなる(慣れると気にならなくなるが)ので、histedit拡張を使うともっと簡単にコミットを並び替えられる。
これはgit rebase -i のように対話的にコミットの歴史を変更できる。
histeditはMercurialにbundleされていないので、bitbucketからcloneして~/.hgrcに設定を書いておこう。 ~/.hgrcに

[extensions]
histedit = /path/to/cloned/histedit/hg_histedit.py

とライブラリのパスを記述しておく。
histedit拡張の使い方はいたって簡単で、コミットの歴史を変更したいチェンジセットのrevsetsを指定するだけでよい。
先ほど、MQ拡張でコミットを並び替えたものを、HistEdit拡張で並び替えてみよう。

$ hg histedit -r tip:1

するとtipからリビジョン1のコミットの情報が1行ごとに書き込まれた状態でエディタが起動する。

pick 52119b06ce60 A
pick d1f359253a3a C
pick 6170a8d1a63b B
pick faf26eeb0bb2 D

# Edit history between 52119b06ce60 and faf26eeb0bb2
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  f, fold = use commit, but fold into previous commit
#  d, drop = remove commit from history
#

Gitとほぼ同じであり、コミットの順序を並び替えたいのであれば、並び替えたいコミット行を入れ替える。
今回はコミット'C'とコミット'B'を入れ替えるので、それぞれの行を入れ替える。
また、左端のカラムはhisteditの操作を意味しており、コミットを並び替える以外の操作も可能だ。

  • e edit: コミットメッセージを変更する
  • f fold: 一つ前の親コミットに含める(エディタ上だと上のものが親にあたる)
  • d delete: そのコミットを破棄する

編集し終わったら保存してエディタを終了してコミットを並び替えが完了する。
histeditが便利なので、普段はこっちを使うけどMQを使ってやってることをラップしているので、より複雑な操作をしたい場合はMQ使うことになるので、MQでの操作も知っておいたほうがいい。