(特に Git 併用ユーザには是非読んでおいて欲しい) Mercurial における『ブランチ』の概念 〜 その3
※ 2013-10-10: 暗黙の更新反映等、ブックマークの伝搬に関して、記述を補足
当初は『構造的ブランチ』の説明をしようと思っていたのだけれど、『特に Git 併用ユーザには是非読んでおいて欲しい』と冠していることもあり、Git ユーザにとっての『ブランチ』のイメージに近い『ブックマーク』の説明を先に持ってくることに。
以下、各回で説明する主なトピック:
- その1: 名前無しブランチ
- その2: 名前付きブランチ
- その3: ブックマーク(本エントリ)
- その4: 構造的ブランチ
- その5: 名前付きブランチ運用で必要なトピック
- その6: 名前付きブランチに関するちょっと踏み込んだ話
ブックマークの基本的な機能
Mercurial では『ブックマーク』(bookmark)という機能が使用出来る (1.8 版以降は基本機能として、それ以前は拡張機能としてサポート)。
『ブックマーク』では、特定のリビジョンに対して、任意の名前を設定することができる。
$ hg bookmarks -r REV ブックマーク名
設定された『ブックマーク』名は、タグと同様に、"hg log -r REV
" や "hg diff -c REV
" といった、リビジョンを指定する任意の場面において使用することができる。
# "hg update
" への指定は注意が必要(詳細は後述)
以上の機能だけを見ると、タグと同じではないのか?という疑問が出てくることと思うが、『ブックマーク』とタグは以下の点で異なる。
- 新規の『ブックマーク』情報は、明示しない限り他リポジトリに伝播しない: 通常のタグは伝播してしまう
- 『ブックマーク』情報は他リポジトリに伝搬させることができる: ローカルタグは伝播させられない
- 『ブックマーク』情報の追加/削除は履歴管理されていない: タグは .hgtags ファイルの改変として記録が残る
また、後述するように『ブックマーク』は自動的に参照先リビジョンを変更することができる点で、参照先リビジョンが固定されるタグとは決定的に異なる。
# タグも強制的な上書きをすれば参照先の変更ができるけどね……
ブックマークの活性/非活性
『ブックマーク』は、活性(active)/非活性(inactive)という状態を持っていて、以下のいずれかの条件を満たす場合、対象の『ブックマーク』は活性状態になる。
- 作業領域の親リビジョンに対して "
hg bookmarks
" によって『ブックマーク』が追加された- 対象リビジョン指定無しでの "
hg bookmarks
" 実行、または - "
hg bookmarks
" 実行時に明示的に指定したリビジョンが、作業領域の親リビジョン
- 対象リビジョン指定無しでの "
- "
hg update
" の対象リビジョンとして『ブックマーク』が指定された
活性状態にある『ブックマーク』は、"hg bookmarks
" コマンドでの列挙の際の、"*" の有無で識別でき、最大で1つの『ブックマーク』が活性化される。
$ hg bookmarks
BAR 1:c5bafe77abba
* FOO 4:2aa8ea8bef1d
基本的には、以下のように考えておけばよいだろう:
作業領域の親リビジョンと『ブックマーク』の参照先が一致していないと、『ブックマーク』は活性化されない
活性状態の『ブックマーク』の参照先リビジョンは、以下のケースで自動的に更新される。
- 新規リビジョンのコミット: "
hg import
" や "hg qnew
"、"hg qpush
" 等も含む - リビジョンを明示しない "
hg update
": 同一『名前付きブランチ』の最新リビジョンでの更新に相当
前者は、まさに Git ユーザにとっての『ブランチ』と同じ挙動の筈。
共有リポジトリから取り込まれた成果が、『ブックマーク』 対象リビジョンの直系の子孫で無かった (= 『枝分かれ』している) 場合、後者のような、『対象リビジョンを明示しない "hg update
"』は、『ブランチ横断の更新』との判定により中断されるので、挙動としては妥当であろう: 『ブックマーク』を介した共同作業に関しては後述。
先述したように、"hg update
" の対象リビジョンとして『ブックマーク』が指定された場合、当該『ブックマーク』は活性化されてしまうので:
一時的に、作業領域を『ブックマーク』時点の内容に戻したいだけなので、『ブックマーク』を活性化したくない
といった状況では、少々厄介だが "hg update
" への対象リビジョン指定を以下のような revsets 表記で指定する (→ 間接的な指定の場合、活性実施が回避) か:
$ hg update 'bookmark(FOO)'
一旦 "hg update
" した上で、『ブックマーク』を明示的に非活性にすることになる: 『ブックマーク』名省略での --inactive 実施は、親リビジョンのブックマークを非活性化。
$ hg update FOO
$ hg bookmarks --inactive
ブックマーク情報の伝播
先述したように、明示的に指定しない限り、新規の『ブックマーク』は他のリポジトリには伝播しない。
そのため、例えば "BOOKMARK" という『ブックマーク』によって参照されている、以下のような履歴の『枝分かれ』があり:
連携先/ローカルの双方のリポジトリが M1 〜 M3 リビジョンのみを保持する、と仮定した場合、以下の "hg push
" 実行は、単に "BOOKMARK" の指す『枝分かれ』部分 (= B1 〜 B2) を、連携先リポジトリに反映させるだけに過ぎない。
$ hg push -f -r BOOKMARK
※ 本エントリの実行例では、連携先が "[paths]" セクションで "default" として指定されていることを仮定
以下のように、"BOOKMARK の反映" を明示して、初めて "BOOKMARK" という『ブックマーク』の情報が、連携先リポジトリにも格納される。
$ hg push -f -B BOOKMARK
"-B BOOKMARK
" 指定は、暗黙に "-r BOOKMARK
" 指定が含まれるので、BOOKMARK で参照されるリビジョンの祖先しか、反映先への伝搬対象にならない。
また、"-f
" 指定は、B2 リビジョン反映による『ヘッドの複数化』を許可するためのものなので、リビジョン反映が『ヘッドの複数化』につながらない場合は、指定不要: ちなみに、『"-B
" 指定時は "-f
" 指定を不要にしよう!』、という修正提案も上がっているので、本エントリにおける例のようなケースでも、いずれは "-f
" 指定が必要なくなるかも?
なお、『明示的に指定しない限り他のリポジトリに伝播』しないのは、新規の『ブックマーク』である点に注意が必要。
一旦『ブックマーク』を連携先に反映した後は、"-B
" で明示的に指定しない『ブックマーク』に関しても、以下の全ての条件が成立すれば、連携先の同名『ブックマーク』を更新する:
- ローカルでの『ブックマーク』の参照先が、"
hg push
" での反映対象に含まれている - 連携先での『ブックマーク』の参照先が、ローカルリポジトリにも存在する
- ローカルでの参照先が、連携先での参照先の直系の子孫
この条件に合致しない『ブックマーク』を連携先に反映したい場合は、"hg push
" 時に明示的に "-B
" で指定する必要がある: "-B
" は連携先の『ブックマーク』を強制的に更新する、と覚えておけば良いだろう。
さて、明示的に指定しない限り、新規の『ブックマーク』が他のリポジトリに伝播しないのは、"hg push
" 方向の話で、"hg pull
" に関しては必ずしもその限りではない: これは 2.3 版からの挙動改変事項。
連携先リポジトリ上には存在するが、手元のリポジトリには存在しない『ブックマーク』は、"
hg pull
" で自動的に取り込まれる
取り込み対象が (連携先において) 新規に追加された『ブックマーク』に限定されているのがミソで、これにより、同一『ブックマーク』を共有して共同作業しているようなケースでの、予期せぬ『ブックマーク』の更新を回避している。
ブックマーク共有による共同作業
いわゆる "Pull Request" 的な運用において、『枝分かれ』先の参照に使用するだけであれば、先述した説明だけで十分なのだが、複数人で『ブックマーク』を共有して共同作業を行う場合は、もう少し踏み込む必要がある。
"BOOKMARK" という『ブックマーク』を共有して共同作業する場合、共有リポジトリ等からの履歴の取り込みは、以下のように実施される。
$ hg pull -r BOOKMARK
"hg incoming
" や "hg pull
" において、"-r REV
" で指定されたリビジョンの値の解釈は、ローカルリポジトリではなく、連携先リポジトリ側で実施される、というのがミソである: その一方で、"hg outgoing
" や "hg push
" の場合はローカル側で解釈される。
つまり、上記の例では、ローカルと連携先とで "BOOKMARK" の参照するリビジョンが違う場合だけ、連携先からリビジョンの取り込みが行われるのだ。
この『連携先側で解釈』という挙動のため、連携先に『ブックマーク』 "BOOKMARK" 自体の情報を伝播させる以前に "hg incoming
" や "hg pull
" を実施した場合は、以下のようなエラーが発生する。
$ hg incoming -r BOOKMARK
.....
中断: 'BOOKMARK' は未知のリビジョンです!
$
なお、連携先の履歴ツリーが以下のようになっている場合:
連携先における "BOOKMARK" の『枝分かれ』の祖先ではないリビジョン B3 は、取り込み対象にならない。これは、先述したように、BOOKMARK 参照先の解釈が、連携先リポジトリ側で実施されるため。
もっとも、『複数ヘッド禁止』を守って運用していれば、このようなケースは発生しないので、通常は問題にならない筈。
さて、連携先の "BOOKMARK" 参照先が、ローカルのものと異なっているケースには、主に以下の2通りが考えられる:
- 連携先の "BOOKMARK" が、ローカルの "BOOKMARK" の直系の子孫を指す
- それ以外 ⇒ 複数『ブランチヘッド』による『枝分かれ』
前者のケースは、ローカルにおける "BOOKMARK" の値を、連携先のそれで上書きしてしまって問題ない: 『直系の子孫』であることは、関連する成果が "hg pull -r BOOKMARK
" の際に全て取り込まれていることも意味する。
問題なのは後者のケース。
例えば、ローカルリポジトリが:
連携先リポジトリが:
という状況において、"hg pull
" による取り込みを行った際には、以下のようなメッセージが表示される: "@default" 部分は状況に応じて変化有り。
$ hg pull -r BOOKMARK .... 分岐するブックマーク BOOKMARK を BOOKMARK@default として保存 .... $
Mercurial の『ブックマーク』は、Git の『ブランチ』で言うところの "remote" 的な名前空間を持たない代わりに、『ブランチヘッド』の複数化による『枝分かれ』が検出された場合は、上記のような新たな『ブックマーク』を作成することで、お互いの『ブックマーク』の参照先を維持したまま、共同作業ができるようになっているのだ。
後は、両者の成果をマージした上で、連携先の『ブックマーク』上書きを指示する "-B BOOKMARK
" 付きで "hg push
" を実行してやればよい: "BOOKMARK@default" は削除してしまって構わない。
なお、"hg pull
" の段階でうっかり "-B BOOKMARK
" を指定してしまうと、連携先リポジトリでの "BOOKMARK" 参照値により、ローカルの "BOOKMARK" が上書きされてしまう。"hg push
" での "-B
" 指定が、連携先の『ブックマーク』を強制的に更新するのと同様に、"hg pull
" での "-B
" 指定は、ローカルの『ブックマーク』を強制的に更新する。
その際でも "BOOKMARK@default" の取り込みは行われるので、"BOOKMARK" と "BOOKMARK@default" という2つの『ブックマーク』が共に同じリビジョンを指す、という嬉しくない状況になるので、注意が必要。
もしもうっかりやってしまった場合は、慌てず騒がず、"hg rollback
" によって『ブックマーク』情報の更新も含めて取り消しすれば良い。
※ 以下、"その4" に続く