彷徨えるフジワラ

年がら年中さまよってます

(特に 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" に続く