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 status
や hg 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 qnew
や hg 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 したかのように」と書いたけど,qrefresh
を hg 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_menu
と add_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 パッチスタックで適用済みのもののうち最上位
また「パッチ名」もタグになっている。
qpop
と qpush
で作業状態を自由に移動
パッチスタックというくらいだから,スタックの 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_menu
を qrefresh
で修正したけど,ここから qnew
で新しいパッチ(fix_wine
)を「挿入」することもできる(別パッチとすべきだという方針もありうる)。
http://inside.ascade.co.jp/node/1612.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
により作成されたパッチは、間違いなくスタック途中に挿入されるようです。
くわしくは自習課題ということで。
さて,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_menu
を qrefresh
しておいたほうがよい。))けど,無事適用できた。
ファイルの内容をみてみると……
$ 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 として記録された。しかも,これまで律儀に書いてきたパッチメッセージもコミットログとして記録されている。
これまででてきたコマンドを図にまとめておきます。
qfold
で複数のパッチをまとめる
人によっては add_menu
と add_beer
が別々の changeset として commit されたことに不満を覚えるかもしれない。手元の修正履歴はまとめて一つの changeset として commit したいんやー((SVK での svk push -l
のように。))。
MQ では qfold
というコマンドを使うことで,他のパッチを畳む――すなわち融合する――ことができる。
パッチスタックが現在下記の状態になっているとしよう。
$ hg qseries -s add_menu: カクテルとワインを追加した add_beer: ビールを追加した
今回は add_menu
に add_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_menu
と add_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
内容はシンプル。
Chapter 12. Managing change with Mercurial QueuesNote
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 thestatus
file is almost always a bad idea, as it's easy to corrupt MQ's idea of what is happening.
と脅されてるけど,.hg/patches/status
ファイル「は」いじるなってことかな。series
のほうはまあ許容範囲っぽい。
でもちょっと注意。
MqTutorial - Mercurial6.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
orhg qpush
for patches that you want to re-apply
.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 を使用する場合は作業のバックアップを保存してください。
Mercurial basics | MDNhg qrefresh
は古いパッチを新しいもので破壊的に置き換えます! パッチのために別のバックアップレポジトリを作成するにはhg qinit -c
を使用し、定期的にhg qcommit -m backup
を実行してください。
のように書かれてる。あくまでも「バックアップ」,ねぇ。
個人的には「パッチの履歴管理」という目的で 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 アーキテクチャだと patch1
と patch2
を当てるけど patch3
は当てない,とか。こんなときは patch1
と patch2
に 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 は差分をあてたり戻したりという試行錯誤を簡単におこなうことのできる十徳ナイフのようなものだ。まして既存の(ローカル)レポジトリを自在にあとから変更できてしまう。便利さと裏腹に,リビジョン管理システムの存在意義を無に帰するものなのではと警戒する人もいるだろう。
たしかに,ぱっとみ醜く感じるいろいろな試行錯誤の跡もまた履歴管理の花,かもしれない。だが(ローカルなレポジトリだからこそ)「すべての」リビジョン管理を堅苦しく考える・履行する必要はない,ということだと思う。
Chapter 12. Managing change with Mercurial QueuesThe 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.
http://inside.ascade.co.jp/node/1112.3 The huge advantage of MQ
この節では、(従来の)構成管理ツールは「永続的な記録が残ってしまうのが堅苦しい」と、構成管理の利点をいきなりひっくり返す記述があります。
「それを言っては身も蓋も無い」気がしますが、「上流リポジトリ」が自身の制御の及ばない状況の場合、 Mercurial のようにローカルリポジトリへの自由な commit が出来るからといって、「永続的な記録」=チェンジセットを残してしまうと、その後の「上流リポジトリ」の更新への追従の際には継続的な merge が必要とされます。そのようなことから「永続的な記録が残ってしまうのが堅苦しい」と思ってしまう気持ちはわからないでもありません。
この辺は、制御できない外部要因とのすりあわせが常に要求される OSS (に関わる)開発と、開発プロセスや品質管理/開発分担といったものが制御しやすい閉じた開発とで、意識が全然違ってくるところでしょう。
参考文献
- Mercurial Queues Extension
- MQ の公式ページ
- MQ Tutorial も参照
- Mercurial: The Definitive Guide
- そのうちオライリーから刊行される Mercurial: The Definitive Guide の原稿。MQ に関する章は以下の2つ。
- Chapter 12. Managing change with Mercurial Queues
- Chapter 13. Advanced uses of Mercurial Queues
- もちろん英語なんだけど,けっこう平易に書かれている。
- 入門Mercurial
- まだ買ってない/読んでない。MQ の話も載っているらしい。評判よさげ。
- http://inside.ascade.co.jp/node/11, (2), (3), (4), http://inside.ascade.co.jp/node/25
- 上記 入門Mercurial 著者のアスケイドの藤原さんによる Mercurial: The Definitive Guide の翻訳メモのうち MQ に関する部分
- Mercurialではじめる分散構成管理:特集|gihyo.jp … 技術評論社
- Mercurial basics | MDN
- Mercurial を使う上で気をつけたほうがいいこと(MQ 含む) by MDC
- Mercurial Queue - aodagの日記
- MQ の概要
- http://dailyhacking.blogspot.com/2007/08/hg-mq.html
- MQ の説明(とてもわかりやすい)
- 水星人の運命 (pdf)
- Community Engine の森田さんによるスライド。ust はこちら
- あと steps to phantasien t(2007-01-18): 最近みた Tech Talks: Mercurial Project も。
- [mercurial] - ursmの日記
*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 項参照。