Mercurial MQ について

巷では git の大ブームだけど,ひさしぶりに Mercurial について書きます。


Mercurial について言及されたブログとか読んでいるとき,たまに MQ という言葉を目にして気になっていた。ながらく気にはとめつつ全然調べていなかったんだけど,ちょっと利用しようかなというケースがあり,ちょこっと触ってみた。


自分の理解では,MQ (Mercurial Queues) とは,誤解を恐れずにいえば Mercurial の changeset と独立して構成される修正履歴(パッチ)のスタックのようなものだ。

(なので今後 MQ の patch queues を Queues という名称と裏腹に「パッチスタック」「パッチ群」などと勝手に呼び称します)

「誤解を恐れずにいえば」と書いたけれど,この直感的な印象は MQ を使っていくうちに――大筋では変わらないものの――ちょっと変わった。それでこの文書を書こうと思った。

さいしょは具体的な利用局面を想定してこの tutorial 的なものを書こうと思ったんだけど,挫折した。挫折したのは,恥ずかしながら Mercurial 自体チームプレイで使ったことがないからだと思う。だから説得力のある例を思いつけなかった。

ということで,(確固たる目標なしに)だらだらと MQ を使いながら書いていきます。

いいわけカコワルイけど,いくつか断り書きを。

  • (当然のことながら)Mercurial 本体の知識(利用歴)があること前提で書いている。もし Mercurial の知識なしにこの文書を読むと,Mercurial ってなんて面倒なんだという感想を抱くかもしれない。でも通常の開発フローで Mercurial (本体)を使うなら,一般の分散型リビジョン管理システムと相違なく使える。これはあくまで MQ の使い方について。
  • 入門Mercurial に MQ についても記載されているらしい。Mercurial 本体も習熟したいのならこの本を買うのもいいだろう。
  • 書き終わってから気づいたけど http://dailyhacking.blogspot.com/2007/08/hg-mq.html のほうが簡潔でわかりやすい。この長いだらだらした文書に眩暈がしたならこちらのほうがおすすめ。
  • 当たり前だけど,自分の手を動かすのが理解を深める一番の手段。

MQ を使い始めるその前に

MQ は(今の)Mercurial distribution についてくるんだけど,extension なので標準では有効になっていない。

~/.hgrc

[extensions]
mq=

を追加しておこう。

そして,MQ を有効にするのなら,ついでに(同様に同梱されている)color extension も追加しておこう。これがあると MQ の出力がわかりやすくなる。しかも hg statushg diff の出力も colorize されるというおまけつき。MQ を使わない人にも超オススメなのです。

イントロダクション

ある日 BOSS に呼び出された。

BOSS ちょっとある店のページを作ることになってさ,おおまかには作ったんだけど,メニューのとこだけやってよ

ある店?ページ?などいくつかの疑問が頭にわいたけど,とりあえず BOSS の Mercurial レポジトリを clone することにした。

$ hg clone /home/boss/saturn mywork
updating working directory
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ cd mywork

ふむ,いっこしかファイルがないらしい。

$ ls -F
index.txt

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==

ふむふむ。Bar Saturn*1 のページね。テキストファイル?まあいいや。この MENU ってとこに追記していけばいいのか。

はじめての MQ パッチ

さて,普段の Mercurial ならがしがし編集していって都度都度 commit という形をとるんだけど,MQ の場合「まず(これからの作業を記録する)パッチを作成し,修正するたびに更新」という形になる。

$ hg qinit

$ hg qnew add_menu

hg qinit というのがこの作業コピー用の MQ の初期化コマンドなんだけど,パッチ群のバージョン管理をおこなわない(後述)のなら実は hg qinit は必要ない((初回の hg qnewhg qimport のときに自動的に qinit される。))。

hg qnew で「これからの作業履歴」を記録するパッチを作成する。名前は簡潔かつわかりやすいもの(そして英数字で構成されており空白を含まないもの……すなわちファイル名みたいなの)をつけたほうがよい。

メニューを追加するのでパッチの名前は add_menu ということにした。

余談: パッチ名についてのちょっと深い話

なんで「簡潔かつわかりやすい名前」なのかって?

MQ では,例の .hg/ 下に patches/ というディレクトリを作り,そこにパッチ群を格納していく。

$ ls -F .hg/
00changelog.i  branchheads.cache  hgrc      requires  undo.branch
branch         dirstate           patches/  store/    undo.dirstate

$ ls -F .hg/patches/
add_menu  series  status

ここにいまさっき名前をつけた add_menu というファイルがある。これが「簡潔かつわかりやすい(かつ空白を含まない英数字)」名前をつけたほうがいい理由。日本語で長ったらしく説明したい,ということであれば,パッチの「メッセージ」(コミットメッセージとほぼ同義)を後からでもつけられるので,そちらを利用する。

なお,この add_menu というファイルは unified diff 形式のパッチファイルそのもの。こいつを直接編集するのはおすすめしないけど((実際どうなんだろ。パッチをバージョン管理する場合には結局直接いじってるみたいなことになるので,問題はおこらない気もする。ただ status ファイルがあるからパッチだけいじるのは問題おこるかも。)),パッチ群をガッと upstream にメールで送りたいときは,こっからもっていってもいいと思う。

ちなみに上記 patches/ ディレクトリの中をみればわかるとおり,パッチ名として series というのと status というのはつけられない。暇なひとは hg qnew series とかやってみよう。


作業をおこなうまえに「簡潔かつわかりやすい」名前を考えるのなんて面倒。でも,パッチ名はあとからでも hg qrename で変更できるので,とりあえず hg qnew p1 とかしといても構わない。あと「作業前にあらかじめ」ってとこに抵抗感がある人もいると思うけど,じっさいはいくらでも対処法があります(後述)。

パッチの「更新」

さーて編集。

$ hg status
# まだなにも編集されていない

$ echo "* cocktail" >> index.txt

$ hg status
M index.txt
$ hg diff
diff -r 90768f851444 index.txt
--- a/index.txt Fri May 15 17:21:19 2009 +0900
+++ b/index.txt Fri May 15 17:31:42 2009 +0900
@@ -4,3 +4,4 @@
 * dayflower
 
 ==MENU==
+* cocktail

ここまでは,いつもの Mercurial と同じ。いつもならここで hg ci するところだけど,今回は MQ。

MQ 的な考えでは「add_menu パッチを,現在の編集結果を反映したパッチに更新しよう」となる。
(ちなみに「更新」する前は hg qnew したばっかなので,パッチの中身はカラ)

$ hg qrefresh

$ hg status

$ hg diff

あたかも commit したかのように,現在は最新版ってことになった。


現在パッチスタックがどのようになっているか調べるには hg qseries というコマンドを使う。

$ hg qseries
add_menu

ミョーに強調されてみえる理由は color extension を有効にしたため。まだ初期段階なのであんまり詳しくは書かないけど,とくに color extension が有効な場合,hg qseries コマンドを一番よく使うことになると思う。

余談: MQ と changeset (1)

「あたかも commit したかのように」って書いたけど,じつはパッチは changeset として生成されている。

$ hg log
changeset:   1:1e4ff0e2e48d
tag:         qtip
tag:         add_menu
tag:         tip
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Fri May 15 17:37:42 2009 +0900
summary:     [mq]: add_menu

changeset:   0:79d8edfc7bda
tag:         qparent
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

いろいろ tag もついてる(後述)んだけど,このように add_menu は changeset としてみなされている。このへんの changeset まわりについては後でも触れる。いまのところあんまり気にしないで。

さらに変更して,さらに qrefresh

カクテルしかメニューにないというのもアレなので,ワインも追加してみる。

$ echo "* win" >> index.txt

$ hg status
M index.txt

$ hg qrefresh

$ hg status

$ hg qseries
add_menu

hg qrefresh で「現在のパッチ(add_menu)」を「更新」したので,hg qseries ででてくるのは,あくまで add_menu だけ。


この「現在のパッチ」がどのようになっているか,確認してみよう。

$ hg diff

おっと。先ほども述べたように(疑似的に)commit された状態なので,hg diff しても何もでてこない。

現在のパッチ(スタックトップのパッチ((厳密にいうと,スタックトップのパッチ,ではない。スタックトップのパッチ+現在の編集内容,だ。qrefresh するとこういう内容のパッチになりますよ,ということだ。)))を確認するには hg qdiff というコマンドを使う((もちろん qdiff しなくても hg diff -r 0:1 しても確認できる。でも専用コマンドのほうが楽だよね。))。

$ hg qdiff
diff -r 79d8edfc7bda index.txt
--- a/index.txt Fri May 15 17:19:26 2009 +0900
+++ b/index.txt Mon May 18 12:56:50 2009 +0900
@@ -4,3 +4,5 @@
 * dayflower
 
 ==MENU==
+* cocktail
+* win

カクテルとワインを追加したという,これまでの過程が,ひとつのパッチにまとまっている。

逆にいうと,それらの過程は,このように qrefresh していくだけだと後から不可分になる。さきほど「あたかも commit したかのように」と書いたけど,qrefreshhg ci の代わりに使うと痛い目を見る。

各作業過程を別個のものとして管理するには,hg qnew で新しいパッチとして新たに始めることが必要になる。んだけど後述。

余談: MQ と changeset (2)

$ hg tip
changeset:   1:fe5fb08c54a6
tag:         qtip
tag:         add_menu
tag:         tip
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Mon May 18 12:54:40 2009 +0900
summary:     [mq]: add_menu

さきほどの changeset ID をおぼえていますか?実は「1:1e4ff0e2e48d」だった。つまり(qrefresh により)今回生成された changeset と revision index はおんなじだけど,ハッシュ ID が違うということになる。

つまり hg qrefresh するたびに,内部的には「commit しなおし」ていることになる。おもしろいね((さらに付け加えると,ドキュメントに変更をまったく加えずに hg qrefresh しても changeset hash ID は変わる。))。

パッチにメッセージをつける

hg qnew で次のステップに進む前に。

いままで add_menu とかいう,人間にはちょっとわかりづらい名前でパッチを扱ってきた。でも,これだけだとどんな変更を加えた・加えているのか自分にもわかんないよね。

なので,パッチには「メッセージ」をつけることができる。hg qrefresh-m オプションをつけると現在作業中のパッチにメッセージをつけられる。

$ hg qrefresh -m "カクテルとワインを追加した"

まるでコミットログみたいだ*2


パッチメッセージは hg log でも確認できるけど,いままで使ってきた hg qseries コマンドに -s オプション(summary の略)をつけると,確認することができる。

$ hg qseries -s
add_menu: カクテルとワインを追加した

これで「いままでどんな変更を加えてきた」「いまどんな変更をしようとしている」のか,わかりやすくなった。


なお,hg qrefresh コマンドは,ファイルに変更がなくても実行することができる。パッチメッセージの変更のためだけにおこなうのも全然 OK。

新しいパッチで作業を始める

BOSS に聞いていた限りでは Bar Saturn で提供される飲み物はカクテルとワインだった。add_menu パッチはほぼ完成したと見ていいだろう。

しかし個人的にはビールも加えたほうがいいと思う。メニューにビールを加えるかどうかだが……最終的に BOSS に意向を聞く必要があるので,add_menu パッチには含めたくない。別個のパッチ(add_beer)として管理しよう。

$ hg qnew -m "ビールを追加した" add_beer

hg qnew でも -m オプションをつかってあらかじめパッチのメッセージを登録できる(そしてもちろん後ほど qrefresh -m MESSAGE で変更できる)。メッセージが過去形の文章になっているのは,のちのち commit した場合のことを考えてそうした。体言止めで書く流儀の人もいるかもしれない。


さて,編集編集。

$ echo "* beer" >> index.txt

$ hg qrefresh

$ hg qseries
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

これでパッチスタックには add_menuadd_beer の二つがたまった。スタックといいつつ,より新しいものが下に記述されるようになっていることに注意。

余談: MQ と changeset (3)

ここで hg log して changeset をみてみる。

$ hg log
changeset:   2:146728d6674f
tag:         qtip
tag:         tip
tag:         add_beer
user:        dayflower <dayflower@example.com>
date:        Mon May 18 13:57:38 2009 +0900
summary:     ビールを追加した

changeset:   1:05b916013804
tag:         add_menu
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Mon May 18 12:54:40 2009 +0900
summary:     カクテルとワインを追加した

changeset:   0:79d8edfc7bda
tag:         qparent
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

「適用済み」のパッチが個別の changeset として commit されているかのようにみえる。つまり MQ のパッチスタックが一つの changeset になるのではなくて,各パッチが一つの changeset となる,ということだ。

あくまで「適用済み」のみ現れるので,後述する hg qpop などでパッチスタックを遡ると,この changeset の数が増減する。

tag にいろいろ興味深いタグが出現している。

qparent
MQ を適用する元となった changeset
qbase
MQ パッチスタックで一番最初に適用された(底の)パッチ
qtip
MQ パッチスタックで適用済みのもののうち最上位

また「パッチ名」もタグになっている。

qpopqpush で作業状態を自由に移動

パッチスタックというくらいだから,スタックの push / pop はお手の物。

スタックを pop(qpop)して前のパッチ編集状況に戻してみる。

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* win
* beer

$ hg qpop
now at: add_menu

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* win

おお。見事に前の編集履歴まで戻ってこれた。

シェルディレクトリスタックの pushd / popd とのアナロジーからすると,pop してしまったパッチの編集内容(add_beer)は失われてしまったように感じるかもしれない。

でも心配ご無用。パッチスタックには pop したパッチもきちんと残っている。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

color extension を有効にしていると,このようにパッチスタックのうち適用されているパッチとそうでないパッチを識別することができる(これが color extension をオススメする理由)。

color extension が有効でない場合は,hg qapplied で適用済みのパッチの一覧を取得すればよい。

$ hg qapplied -s
add_menu: カクテルとワインを追加した

ちなみに hg qunapplied という逆の(適用されていないパッチ一覧を表示する)コマンドもある。

また,現在のスタックトップは hg qtop というコマンドで確認することができる((qtip という名前のほうが整合性がとれる気がするんだけど。))。

$ hg qtop
add_menu

しつこいようだけど,color extension を有効にしていれば qapplied / qunapplied / qtop コマンドを使うことはあまりないと思う。


ということで,qpop で遡ったパッチスタックは qpush で安心して戻ってくることができる。

$ hg qpush
applying add_beer
now at: add_beer

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* win
* beer

$ hg qapplied -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

スタック途中のパッチを修正する

と,この時点でワインのつづりが間違っていることに気づいてしまった。

add_beer パッチを適用したうえで,新しく fix_wine というパッチを作成するという考え方ももちろんアリだろう。でも,今回の例では add_menu パッチの誤りを訂正するという方向で話を進めたい。

まずは add_menu 時点の作業環境まで状況を戻すことにする。

$ hg qgoto add_menu
now at: add_menu

もちろん qpop で戻ってもいいんだけど,今回は一足飛びで指定したスタックトップに飛ぶことのできる qgoto を使ってみた(スタックの段数があまりないので意味ないけど)。

$ vi index.txt      # wine のつづりを修正

$ hg diff
diff -r 553b8522771e index.txt
--- a/index.txt Mon May 18 13:09:19 2009 +0900
+++ b/index.txt Mon May 18 13:47:50 2009 +0900
@@ -5,4 +5,4 @@
 
 ==MENU==
 * cocktail
-* win
+* wine
$ hg qrefresh

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

qrefresh によって,現在のスタックトップ(すなわち add_menu)のパッチを修正することができた。


なお,今回は add_menuqrefresh で修正したけど,ここから qnew で新しいパッチ(fix_wine)を「挿入」することもできる(別パッチとすべきだという方針もありうる)。

12.5.5 Pushing and popping many patches

hg qpush -a で全パッチ適用ということになると、「パッチ適用順序」は神経質になるに値する重要事項ですから、前節での疑問である「パッチスタック途中での hg qnew は、パッチスタックに対してどの位置にパッチを作成するのか?」がますます深刻なものになってきます。

というわけで、「パッチスタック途中での hg qnew は、パッチスタックに対してどの位置にパッチを作成するのか?」を確認してみました。

〜〜〜中略〜〜〜

何のことは無い、hg qnew で生成されるパッチは、最上段の「applied patch」(この場合は 1st.patch)と最下段の「not applied patch」(この場合は 2nd-1.patch)の間に生成されます。「スタック」と言い切っているのですから、確かにそれ以外に作りようが無いですよね。

〜〜〜中略〜〜〜

スタック途中で hg qnew により作成されたパッチは、間違いなくスタック途中に挿入されるようです。

http://inside.ascade.co.jp/node/16

くわしくは自習課題ということで。


さて,qpush でビールの編集に戻ろう。

$ hg qpush
applying add_beer
patching file index.txt
Hunk #1 FAILED at 5
1 out of 1 hunks FAILED -- saving rejects to file index.txt.rej
patch failed, unable to continue (try -v)
patch failed, rejects left in working dir
errors during apply, please fix and refresh add_beer

おっと,パッチ当てに失敗してしまった。

パッチ当てに失敗したスタックを修正する

普通にパッチスタックを構築していれば,このようになることはあんまりないんだけど,せっかくだからこの失敗したパッチを修正してみる。

今どこにいるのかな。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

$ ls
index.txt  index.txt.rej

add_beer まで当たったことになっている。そして失敗結果が index.txt.rej として残っているらしい。

編集結果をみてみよう。

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* wine

$ cat index.txt.rej
--- index.txt
+++ index.txt
@@ -6,3 +6,4 @@
 ==MENU==
 * cocktail
 * win
+* beer   

ああなるほど。パッチの context が一致しないのでうまくパッチ当てがすすまなかったみたいだ。

これくらいなら手で修正可能(beer の行を付け加えるだけだもんね)なので,エディタで編集する。

$ vi index.txt

$ hg diff
diff -r 54f1717b189e index.txt
--- a/index.txt Mon May 18 13:49:15 2009 +0900
+++ b/index.txt Mon May 18 13:51:06 2009 +0900
@@ -6,3 +6,4 @@
 ==MENU==
 * cocktail
 * wine
+* beer

$ rm index.txt.rej

$ hg qrefresh

これでパッチ add_beer が現状に沿う形になった。

qdiff で確認してみる。

$ hg qdiff
diff -r 9b5a198afb12 index.txt
--- a/index.txt Mon May 18 13:48:07 2009 +0900
+++ b/index.txt Mon May 18 13:51:24 2009 +0900
@@ -6,3 +6,4 @@
 ==MENU==
 * cocktail
 * wine   
+* beer

うんうん。正しくなった。

親の修正履歴を pull で取り込む

と,ここまでやったところで BOSS に呼ばれた。

BOSS こないだのアレだけど,ちょっと修正したから内容とりこんどいて

自分の作業がすべておわってから merge するというのも手だけど,今すぐ反映して結果をみたいということもよくある。んで MQ を使うとそういうときでも迷うことがない。

まずは hg pull する前に hg in して,心の準備をする。

$ hg in
comparing with /home/dayflower/mywork
searching for changes
changeset:   1:c4f2797cb6d5
tag:         tip
user:        BOSS <boss@example.com>
date:        Mon May 18 13:53:49 2009 +0900
summary:     STAFF を MENU のあとにもってきた

なんだか大掛かりな変更っぽいな。

ここで,通常の Mercurial フローなら hg pull するところだけど,MQ を使っているのなら pull する前に qpop -a して自分の作業履歴を「リセット」しておこう。

$ hg qpop -a
patch queue now empty

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==

qpop -a-a は all のことで,パッチスタック上の全パッチを未適用状態にするという意味。んで,ファイルをたしかめたところ,たしかに(ローカル的には)一番最初の状態に戻っている。

さて,いよいよ hg pull する。

$ hg pull
pulling from /home/boss/saturn
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
(run 'hg update' to get a working copy)

qpop -a で「もともとの状態」に戻しておいたおかげで,multiple heads とはならなかったみたいだ。

なので安心して?hg update する。

$ hg update
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ cat index.txt
=Bar Saturn=

==MENU==

==STAFF==
* dayflower

なるほど,STAFF の項目が MENU より後ろに来ているね。これくらいの修正なら,いままでのパッチはそのまま当たるかな。


さて,うれしいことに(というか当たり前なんだけど)hg pull / update しても,パッチスタックはきちんと生き残っているのだ。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

全部まだ未適用だけど(qpop -a したから当たり前)。

では,qpop とは逆に qpush -a してパッチスタック上の全パッチ(すなわちこれまでの自分の全修正履歴)を適用してみよう。

$ hg qpush -a
applying add_menu
patching file index.txt
Hunk #1 succeeded at 2 with fuzz 1 (offset -3 lines).
applying add_beer
now at: add_beer

適用箇所でちょっと文句をいわれた((そう,だから今後もこのパッチ群を利用するなら add_menuqrefresh しておいたほうがよい。))けど,無事適用できた。

ファイルの内容をみてみると……

$ cat index.txt
=Bar Saturn=

==MENU==
* cocktail
* wine
* beer

==STAFF==
* dayflower

おお,きちんと BOSS の編集内容も反映されている(STAFF が MENU のあとにきている)し,自分の編集内容も反映されている。

余談: さきに hg pull しちゃっても大丈夫

いまの例だと,hg pull する前に hg qpop -a したけど,実は qpop する前に pull しちゃってもうまく対処できる。

qpop -a する前に hg pull するとこうなる。

$ hg pull
pulling from /home/boss/saturn
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)

自分の編集履歴と独立した履歴なので multiple heads (+1 heads) になってしまった。

hg heads で確認してみると,

$ hg heads
changeset:   3:c4f2797cb6d5
tag:         tip
parent:      0:79d8edfc7bda
user:        BOSS <boss@example.com>
date:        Mon May 18 13:53:49 2009 +0900
summary:     STAFF を MENU のあとにもってきた

changeset:   2:002fd52dec0a
tag:         qtip
tag:         add_beer
user:        dayflower <dayflower@example.com>
date:        Mon May 18 13:51:13 2009 +0900
summary:     ビールを追加した

たしかに multiple heads になっている(BOSS の修正ツリーと自分の修正ツリーが別ブランチとしてみなされている)。


この時点では自分の「作業コピー」はあくまで changeset 2:002fd52dec0a のまま。ということは,ここから qpop -a しても問題なさそうだ。

$ hg qpop -a
saving bundle to /home/dayflower/mywork/.hg/strip-backup/9b5a198afb12-temp
adding branch
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
patch queue now empty

おお,これまでと違って saving bundle ... とかいわれてるけど,問題なく qpop できた!

しかも multiple heads が解消されたっぽい。確認してみると……

$ hg heads
changeset:   1:c4f2797cb6d5
tag:         tip
user:        BOSS <boss@example.com>
date:        Mon May 18 13:53:49 2009 +0900
summary:     STAFF を MENU のあとにもってきた

single head になってる!

ではということで,現在の「作業コピー」に「BOSS の編集結果」を update してみる。

$ hg update
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ cat index.txt
=Bar Saturn=

==MENU==

==STAFF==
* dayflower

問題なく update できた。


あとの手順はさきほどと同じ。

$ hg qpush -a
applying add_menu
patching file index.txt
Hunk #1 succeeded at 2 with fuzz 1 (offset -3 lines).
applying add_beer
now at: add_beer

$ cat index.txt
=Bar Saturn=

==MENU==
* cocktail
* wine
* beer

==STAFF==
* dayflower

めでたしめでたし。

qfinish でパッチスタックによる作業を完了する

さてさて。

これまで MQ のパッチスタックで作業をおこなっていたけれど,BOSS のオッケーもでたので push したい。まずは commit しよう。

$ hg ci
abort: cannot commit over an applied mq patch

おっと。「MQ が当たっている状態だと commit できないよ」と怒られてしまった。

実際にはこれまでも見てきたように MQ のパッチも changeset として管理されているから,ここで commit しても意味はない(ある意味もう commit 済なのだ)。このメッセージは安全弁として考えればいい。


ほんとうに今やりたかったことは,今現在構成されているパッチスタックとかいうもやもやしたものを取り払って,これまでの修正履歴(パッチ)を changeset として記録したい,ということだ。

#とここまで書いてちょっと表現が分かりにくいなと思った。ともあれ実際の実行結果(下記)をみればわかると思う。

これは hg qfinish というコマンドでおこなう。

$ hg qfinish -a

-a というのは,これまでの all じゃなくて,applied の略。すなわち,今現在適用済みの patch を changeset に変換するということになる*3

hg qseries でパッチスタックの状態を見てみると……

$ hg qseries -s

なくなった。

じゃあ changeset log として見てみると……

$ hg log
changeset:   3:ff656f0e765c
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Mon May 18 13:57:38 2009 +0900
summary:     ビールを追加した

changeset:   2:68ad307143ae
user:        dayflower <dayflower@example.com>
date:        Mon May 18 13:57:38 2009 +0900
summary:     カクテルとワインを追加した

changeset:   1:c4f2797cb6d5
user:        BOSS <boss@example.com>
date:        Mon May 18 13:53:49 2009 +0900
summary:     STAFF を MENU のあとにもってきた

changeset:   0:79d8edfc7bda
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

おお。これまでの qなんとか とかいう tag もついていない,通常の changeset として記録された。しかも,これまで律儀に書いてきたパッチメッセージもコミットログとして記録されている。


以上でひととおりの MQ ツアーはおしまい。

これまででてきたコマンドを図にまとめておきます。


qfold で複数のパッチをまとめる

人によっては add_menuadd_beer が別々の changeset として commit されたことに不満を覚えるかもしれない。手元の修正履歴はまとめて一つの changeset として commit したいんやー((SVK での svk push -l のように。))。

MQ では qfold というコマンドを使うことで,他のパッチを畳む――すなわち融合する――ことができる。

パッチスタックが現在下記の状態になっているとしよう。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

今回は add_menuadd_beer をまとめたい。なので,まずは add_menu スタックトップに移動する。

$ hg qgoto add_menu
now at: add_menu

んで,qfold に「畳みたい」未適用のパッチの名前を指定する*4

$ hg qfold add_beer

これで add_beer という修正内容が add_menu に取り込まれた((hg qrefresh する必要もない。というか,逆に patch -p < .hg/patches/add_beer して hg qrefresh して hg qdelete add_beer するというのをひとまとめに実行するのが hg qfold だといえるだろう。))。

パッチスタックはどうなったか。

$ hg qseries -s
add_menu: カクテルとワインを追加した

add_beer が消えた。

じゃあ現在のパッチ(add_menu)はどうなった?

$ hg qdiff
diff -r c4f2797cb6d5 index.txt
--- a/index.txt Mon May 18 13:53:49 2009 +0900
+++ b/index.txt Mon May 18 14:07:27 2009 +0900
@@ -1,6 +1,9 @@
 =Bar Saturn=

 ==MENU== 
+* cocktail
+* wine
+* beer

 ==STAFF==
 * dayflower

いままでの修正(add_menuadd_beer)が一つのパッチになったことがわかる。


このままだとパッチメッセージが add_menu だけのものになっているので,現況を反映したものに変えておこう :)

$ hg qrefresh -m "カクテルとワインとビールを追加した"

あとからパッチ(hg qnew -f 篇)

MQ の場合は「あらかじめパッチを作成して,そのパッチを『更新』していく」という作業手順になっている。

前に

「作業前にあらかじめ」ってとこに抵抗感がある人もいると思うけど,じっさいはいくらでも対処法があります(後述)。

と書いたけど,対処法のその1を書く。


MQ でパッチ管理を始める前の段階(BOSS からファイルをもらってきたところ)から始めたと思ってください。

編集をおこなう。

$ echo "* cocktail" >> index.txt

$ hg status
M index.txt

前回はこのように編集をおこなう前に hg qnew で新しいパッチを作っていたけれど,編集をおこなったあとで qnew すると,その修正内容を新しいパッチとしてつくることができる。

といっても普通に hg qnew すると,

$ hg qnew add_menu
abort: local changes found, refresh first

のように怒られてしまう。通常は「修正」→「更新(qrefresh)」のサイクルで作業をおこなっていくので,このエラーもいうなれば安全弁ですな。

今回は「意図的に」新規パッチを作成したいので,-f オプションをつけて qnew を実行する。

$ hg qnew -f add_menu

$ hg status

$ hg qseries
add_menu

これで(cocktail を追加するという内容の)add_menu という新規パッチとなる。

そうそう,ワインも追加するんだった。

$ echo "* wine" >> index.txt

$ hg qrefresh

今回の編集内容は add_menu に加えてもいいかなと思える内容なので,普通に qrefresh した。


さらに編集を続ける。

$ echo "* beer" >> index.txt

$ hg status
M index.txt

と,ここで「ビールを追加するのは add_menu に含めたくないなぁ」と思ったとする。

そこで hg qnew -f する。

$ hg qnew -f add_beer

$ hg qseries
add_menu
add_beer

いまの編集内容(ビールの追加)が add_beer という新規パッチとなった。

あとからパッチ(hg qimport 篇)

と,以上のように hg qnew -f するとあとからでもパッチ化することができるんだけど,(オプションの名前からすると)無理にまげてやってる感がいなめない。

なので,もっとダイレクトな方法(いままでの Mercurial 作法が通用する方法)を説明する。


まずは,いつもと同じように Mercurial で履歴をとる。

$ echo "* cocktail" >> index.txt
$ echo "* wine"     >> index.txt

$ hg ci -m "カクテルとワインを追加した"

$ echo "* beer" >> index.txt

$ hg ci -m "ビールを追加した"

通常と同じようにバリバリ hg ci してる。

ここまでの履歴をみてみると,

$ hg log
changeset:   2:a3fe0a48ea18
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Mon May 18 14:16:31 2009 +0900
summary:     ビールを追加した

changeset:   1:ac74699bafab
user:        dayflower <dayflower@example.com>
date:        Mon May 18 14:16:16 2009 +0900
summary:     カクテルとワインを追加した

changeset:   0:79d8edfc7bda
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

全部通常の changeset になっている(当たり前)。

んで,MQ のすごいのは,hg qimport で,通常の changeset を MQ パッチスタックに変換できるところ。

$ hg qimport -r 1:2

ここでは上記リビジョンコードの 1 と 2(カクテルとワインの追加,と,ビールの追加)を MQ 化してみた。

MQ のパッチスタックをみてみると……

$ hg qseries -s
1.diff: カクテルとワインを追加した
2.diff: ビールを追加した

1.diff とか 2.diff とか味気ないパッチ名になってるけど,これはまぎれもなくパッチスタックだ。

名前が味気ないので,いままでと同じような名前に qrename しておく(もちろん必須じゃない)。

$ hg qrename 1.diff add_menu

$ hg qrename 2.diff add_beer

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

で,当然のことながら,qpop / qpush でスタックを行き来できる。

$ hg qpop
now at: add_menu

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* wine

パッチスタックの作業がおわったら hg qfinish で通常の changeset に戻すことももちろんできる。



今まで「MQ はパッチスタックだ」と書いてきた。ところどころ顔を出す changeset の呪縛も感じながら。でも呪縛なんかじゃなかった。MQ は(ローカルな)changeset を自在に渡り歩くためのツールなんだ。

なんかすごくない?


しかもこれを応用すると既存の changeset をいじる(変更・削除・挿入)することすらできる。qfold と組み合わせると,複数の changeset をまとめることもできる。

くわしくは下記記事参照。

注意: MQ を利用している作業コピーを pull させてはいけない

MDC の Mercurial basics には以下のようなことが書いてある。

誰かが pull する可能性のあるレポジトリでは Mercurial Queues を使用してはいけません。

Mercurial basics | MDN

これ,どういうことかなと思ったんだけど,実際確かめてみた。

いま現在 MQ のパッチスタックが下記のようになっているとする。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

この状態で hg log をとると,

$ hg log
changeset:   1:ac74699bafab
tag:         qtip
tag:         add_menu
tag:         tip
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Mon May 18 14:16:16 2009 +0900
summary:     カクテルとワインを追加した

changeset:   0:79d8edfc7bda
tag:         qparent
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

add_menu だけ changeset 化されていることがわかる。


んじゃ,いまこの作業コピーを clone してみよう。

$ cd ..

$ hg clone mywork work2
updating working directory
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

別に問題はなさそうだ。

本当に?

clone されたレポジトリを hg log でみてみると……

$ cd work2

$ hg log
changeset:   1:ac74699bafab
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Mon May 18 14:16:16 2009 +0900
summary:     カクテルとワインを追加した

changeset:   0:79d8edfc7bda
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

MQ のつけていたタグは除去され,通常の changeset として記録されている。

もちろん MQ のタグが除去されているのは好ましい。ただ,問題は MQ の適用済みパッチが changeset として import されてしまっているところだ。

clone もとの親が MQ のパッチを qrefresh したり qpush したりするたび,これらの changeset はかわっていく。MQ スタックをいじるたびに changeset の深さやリビジョン hash ID がかわっていく。そして pull するたびにそれは子孫に「違う changeset」として伝播する。

ということは。子孫にとって望まざる mutiple heads が発生しうるということだ。そして子孫がある changeset から修正を加えていったとしても,それは親にとってはあくまで temporalily にいじっていたファイルだったのかもしれない。


結論: MQ は末端(clone / push / pull 系譜上という意味で)の開発者が作業用コピーで十徳ナイフとして使うべきものだ。


補足。MQ が有効な場合,MQ のパッチデータも含めて clone することのできる qclone というコマンドがある。ただ後述するようにパッチをバージョン管理(レポジトリ化)してないとだめっぽいし,MQ の使われ方からすると,複数人の共同作業で使うというよりは個人のちょっとしたテストのための clone として使うものだと思う。

パッチスタックの(適用)順序を変えたい

実はパッチスタックの適用順は .hg/patches/series にかかれている。

$ cat .hg/patches/series
add_menu
add_beer

内容はシンプル。

Mercurial Definitive Guide では

Note

You may sometimes want to edit the series file by hand; for example, to change the sequence in which some patches are applied. However, manually editing the status file is almost always a bad idea, as it's easy to corrupt MQ's idea of what is happening.

Chapter 12. Managing change with Mercurial Queues

と脅されてるけど,.hg/patches/status ファイル「は」いじるなってことかな。series のほうはまあ許容範囲っぽい。

でもちょっと注意。

6.3. Re-ordering patches

This is a very safe way to do it:

  • Execute hg qpop -a to remove all patches from the stack
  • Reorder patches in .hg/patches/series file
  • Execute hg qpush -a or hg qpush for patches that you want to re-apply
MqTutorial - Mercurial

.hg/patches/series を書き換える前に hg qpop -a しとくこと。

qrefresh したけど実は前の内容のほうが正しかった

それは今からではどうしようもない!

でもこれからのことを考えるなら,パッチ群自体も履歴管理するという方法がある。


通常 hg qinit でレポジトリの MQ 的初期化をおこなう*5けど,その際に hg qinit -c とすれば MQ パッチ群をリビジョン管理することができる。

パッチ群をリビジョン管理ってどういうことか?と思うけど,単純にいって .hg/patches/ ディレクトリを Mercurial レポジトリ化するだけのことだ((だから,あとからパッチ群を履歴管理する場合,.hg/patches/ ディレクトリで hg init すればいい。と Chapter 12. Managing change with Mercurial Queues にも書いてある。))。MQ の普段使いでは注意することはなにもない。パッチ群の「作業用コピー」をパッチとして扱うだけだから。

今現在のパッチ群の内容をとっておきたい,と思ったら hg qcommit とするとパッチ群のレポジトリが commit される。


じゃあこれまでのパッチ群の履歴(ログ)を見たいときや,パッチ群を少し前の(commit した)状態に戻したいときはどうすればいいんだろう。

実は MQ にはそのような作業に特化したコマンドは用意されていない。そのようなことをしたいのなら,.hg/patches/ レポジトリで Mercurial のコマンドを発行する必要がある。じっさい Mercurial Definitive Guide には

Finally, as a convenience to manage the patch directory, you can define the alias mq on Unix systems. For example, on Linux systems using the bash shell, you can include the following snippet in your ~/.bashrc.

alias mq=`hg -R $(hg root)/.hg/patches'
Chapter 12. Managing change with Mercurial Queues

なんて書かれている。不便だと思うならこんな(シェル)エイリアスを定義すればいいでしょう,と。うーん。

あまつさえ Mercurial basics | MDN では

Mercurial Queues を使用する場合は作業のバックアップを保存してください。hg qrefresh は古いパッチを新しいもので破壊的に置き換えます! パッチのために別のバックアップレポジトリを作成するには hg qinit -c を使用し、定期的に hg qcommit -m backup を実行してください。

Mercurial basics | MDN

のように書かれてる。あくまでも「バックアップ」,ねぇ。


個人的には「パッチの履歴管理」という目的で hg qinit -c することの必要性は感じない。たしかに qrefresh でそれまでの修正履歴が一新されてしまうけど,それは普通に Mercurial を使っていてもおこりうること(hg ci する前に色々修正して,あー戻せばよかった,と思ったり)。

ただ,MQ は extension(すなわちアドオン)なので本体より動作が安定していない可能性がある。パッチスタックが深くなってくると何かの事故で失われたらどうしようと不安にもなる。それに MQ の操作に慣れていないうちはついつい気軽に qrefresh してしまうかもしれない。なので,保険として――MDC のいうようにバックアップとして―― hg qinit -c するのは悪くない。さいわいにして普通に使っている分には,パッチ群が履歴管理されていることは隠蔽されている(すなわち透過的につかえる)から。

他にどのようなコマンド/フィーチャーがある?

hg help mq とすると MQ extension で追加されるコマンドの一覧と説明が表示される。

なんと,いままでの解説で MQ に用意されているほとんどのコマンドを使ってきた。

説明していないコマンドは下記のとおり。

  • qsave / qrestore (ステート保存系)
  • qguard / qselect (ガード系)
  • strip (レポジトリ操作系)

ステート保存系(qsave / qrestore)は,現在のパッチ群の状態を保存したり戻したりするときに使うらしい。パッチ群をリビジョン管理するまでもないけど,っていうときに使うのかな?あと MQ の内容を 3-way merge するときにも使うらしい?よくわかりません。

ガード系(qguard / qselect)というのは,パッチ群のなかのいくつかのパッチをグルーピングするために使える。たとえば i386 アーキテクチャだと patch1patch2 を当てるけど patch3 は当てない,とか。こんなときは patch1patch2 に positive guard として +i386 を指定して patch3 には negative guard として -i386 を指定しておく。んで qselect i386 とすると,パッチスタックで適用するパッチを制限できる。とあたかも知ってるフリして書いてきたけど,使ったことないのでわかりません。Chapter 13. Advanced uses of Mercurial Queues に書いてある。

strip というのは指定した changeset (とそれに連なる changeset)を(ローカル)レポジトリから削除するもの。MQ とは直接関係ないけど MQ についてる。じっさい,MQ で同じことできるしね。qimport して qpop -a し,qdelete すれば strip と同じことになります。

で,結局どういうときに使えばいいの?

で,どうなんだろう。


「これからは編集をおこなう度に qnew して,ある程度自信がついたら qfinish で changeset 化しよう」というのも一つの考え方ではある。

んが,hg qimport もあとからできるわけだし,最初からこのように張り切って使う必要もないかな,と思う。


個人的に MQ の用途としてぱっと思いついたのはこれくらい。

  • 新規機能の開発時にちょこっと試してみたい作業の記録
  • 既存機能のバグ修正
  • 複数のブランチに merge する機能の開発とテスト適用
  • 3-way merge ではない merge
  • 頻繁に更新される別レポジトリとの協調作業

だいたい内容は想像つくと思うんだけど,「3-way merge ではない merge」について補足。

通常 Mercurial では merge の際 3-way merge をおこなうんだけど,この 3-way merge がいまいち馴染まないという人もいるかと思う*6。そんなとき MQ を使うと,あくまでパッチ単位での適用可否になるのでローカルでの作業はシンプルになる(と思う)。ただし ThreeWayMerge でいうところの OTHER の修正をすべて正として採択した上で,それにパッチをおこなうということになってしまうんだけど。

なお MQ を使ってもパッチ群を 3-way merge することはできます*7


あと最後の「頻繁に更新される別レポジトリとの協調作業」だけど,たとえば今回途中で BOSS の修正内容を取り込んだように,プロジェクト初期段階でどんどん別レポジトリが修正されていく(そして実装機能が変わっていく)場合,最後に merge しようと思っても乖離が大きすぎて時既に遅しってこともあると思う。

そんなときは MQ を使っているとこまめに追従できるよってことだ(もちろんローカルでこまめに merge していってもいいんだけど)。

Mercurial は個人レベルでしか使ってないんだけど MQ 使う意味ある?

あると思います。


くだらない typo など恥ずかしい changeset を削除したい。バラバラに commit した changeset をあとからまとめたい。MQ はそのようなわがままに応えることができる。


『コミット権限が無いプロジェクトに手元で修正を加えている場合,新しい版が出た際に独自の変更分の反映が面倒』というのがあるんだけど,こんなときでも MQ は便利。


ドットファイルなど設定ファイルをレポジトリにつっこんでいる場合,ローカルな環境用の記述を別 branch とかにする代わりにも MQ は使える。

おわりに

MQ は差分をあてたり戻したりという試行錯誤を簡単におこなうことのできる十徳ナイフのようなものだ。まして既存の(ローカル)レポジトリを自在にあとから変更できてしまう。便利さと裏腹に,リビジョン管理システムの存在意義を無に帰するものなのではと警戒する人もいるだろう。

たしかに,ぱっとみ醜く感じるいろいろな試行錯誤の跡もまた履歴管理の花,かもしれない。だが(ローカルなレポジトリだからこそ)「すべての」リビジョン管理を堅苦しく考える・履行する必要はない,ということだと思う。

The huge advantage of MQ

/* 中略 */

Traditional revision control tools make a permanent, irreversible record of everything that you do. While this has great value, it's also somewhat stifling. If you want to perform a wild-eyed experiment, you have to be careful in how you go about it, or you risk leaving unneeded―or worse, misleading or destabilising―traces of your missteps and errors in the permanent revision record.

By contrast, MQ's marriage of distributed revision control with patches makes it much easier to isolate your work. Your patches live on top of normal revision history, and you can make them disappear or reappear at will. If you don't like a patch, you can drop it. If a patch isn't quite as you want it to be, simply fix it―as many times as you need to, until you have refined it into the form you desire.

Chapter 12. Managing change with Mercurial Queues
12.3 The huge advantage of MQ

この節では、(従来の)構成管理ツールは「永続的な記録が残ってしまうのが堅苦しい」と、構成管理の利点をいきなりひっくり返す記述があります。

「それを言っては身も蓋も無い」気がしますが、「上流リポジトリ」が自身の制御の及ばない状況の場合、 Mercurial のようにローカルリポジトリへの自由な commit が出来るからといって、「永続的な記録」=チェンジセットを残してしまうと、その後の「上流リポジトリ」の更新への追従の際には継続的な merge が必要とされます。そのようなことから「永続的な記録が残ってしまうのが堅苦しい」と思ってしまう気持ちはわからないでもありません。

この辺は、制御できない外部要因とのすりあわせが常に要求される OSS (に関わる)開発と、開発プロセスや品質管理/開発分担といったものが制御しやすい閉じた開発とで、意識が全然違ってくるところでしょう。

http://inside.ascade.co.jp/node/11

参考文献

*1:ここ WEB+DB PRESS Vol.50 を読んだ人なら笑うところです。いちおう。

*2:じっさい後述するようにコミット時にコミットログとして使われる。

*3:だからもちろん,パッチ群の一部のみ changeset 化して,残りはパッチスタックとして管理を続けるということもできる。

*4:ここが MQ で唯一非線形にパッチスタックを適用できる部分だと思う。違ってたらすみません。

*5:前述したように,リビジョン管理をしないなら実は必要ない。

*6:何を隠そうわたしのことです。たぶん大規模な分散リビジョン管理開発をおこなったことがないからだと思うな。

*7:http://inside.ascade.co.jp/node/17 の 12.8 Updating your patches when the underlying code changes 項参照。