彷徨えるフジワラ

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

(特に Git 併用ユーザには是非読んでおいて欲しい) Mercurial における『ブランチ』の概念 〜 その2

今回のお題は、Git における『ブランチ』と同じものと誤解してGit 併用ユーザが混乱する事案が多発する、Mercurial の『名前付きブランチ』に関して。

以下、各回で説明する主なトピック:

  • その1: 名前無しブランチ
  • その2: 名前付きブランチ(本エントリ)
  • その3: ブックマーク
  • その4: 構造的ブランチ
  • その5: 名前付きブランチ運用で必要なトピック
  • その6: 名前付きブランチに関するちょっと踏み込んだ話



名前付きブランチ

Mercurial では、履歴記録の際に、各リビジョンに対して『所属するグループの名前』を1つだけ指定することができる。この情報は、履歴情報の一部としてハッシュ値算出にも使用され、永続的に記録される。

通常、新規に作成されたリビジョンは:

  • 親リビジョンと同じグループに属する
  • リポジトリで一番最初に記録されるリビジョンは、特に指定の無い場合は "default" に属する

以上のことから "default" というグループに属するのだが、コミット前にグループ名を明示的に指定することで、所属グループの名前を変更することができる: "hg branch 名前"

Mercurial では、"default" 以外の各グループに属するリビジョン群を、『名前付きブランチ』と呼んでいる。


但し、本家が "named branch" という用語を用いているので、『名前付きブランチ』と呼んでいるけれど、多分『リビジョングループ』という概念で捉えた方が理解し易い筈。『名前付きブランチ』の『枝分かれ』っぽく無さに関しては、"その6"でも説明する予定。

その1冒頭で参照した、SCMBootCamp in Nagoya #1 での、稲田氏 (id:methane) の基調講演資料における Mercurial の『ブランチ』に関する記述は、『各リビジョンには、所属グループ名を1つ保持できる』という管理構造に焦点をあてたもの。

そのため、決して間違っているわけではないのだけれど、周辺事情を知らない人が、口頭での捕捉等が無いまま、いきなりあれを見ても、Mercurial の『ブランチ』に関しては、理解できないか誤解しやすい印象がある。

Mercurial では、リビジョンを指定する際に『名前付きブランチ』名を指定することで、その『名前付きブランチ』の最新リビジョンが参照されるようになる: 上記の例では "branch-A" は A3、"branch-B" は B2 の指定に相当。

現時点において存在する全ての『名前付きブランチ』の一覧は "hg branches" で見ることができる。

但し、特定の条件に合致する名前付きブランチを "hg branches" 表示から除外することもできる。

  • 明示的な --closed オプション指定がない場合、閉鎖済みの名前付きブランチは表示されない
  • 明示的に --active オプションを指定した場合構造的ヘッドを持たない名前付きブランチは表示されない

そのため、"hg branches" 表示が増大することを嫌って、『名前付きブランチ』の使用を選択しなかったケースでも、『名前付きブランチ』の使用を改めて検討してみる価値はある。

なお、Mercurial 使用歴が長い人でも、以下のように誤解している場合がある模様:

『名前付きブランチ』に属していない (= "default" に属する) リビジョンにおける『枝分かれ』のことを『名前無しブランチ』と呼ぶ

『名前付きブランチ』の説明前だったために、その1では不完全にならざるを得なかった『名前無しブランチ』の定義も、厳密に定義すると以下のようになる:

  • X の親リビジョン P が、P と同一の『名前付きブランチ』内に、X 以外の子リビジョンを最低1つ持つ
  • リビジョン X と P が、同一『名前付きブランチ』に属する

特定の『名前付きブランチ』上に、多数の『名前無しブランチ』を生成することもできるので、くれぐれも誤解の無いように。

ちなみに、『名前付きブランチ』とは別の観点からのグループ分け機能を提供するエクステンションとして、Group Extension というものも公開されている (僕は使ったことが無いけれど……)。

『枝分かれ』範囲の特定

例えば、『パターンによるソフトウェア構成管理』における『Release Line』のような、比較的寿命の長い『枝分かれ』の場合:

履歴の参照/監査の際に、対象を『枝分かれ』の範囲に限定したい

といった要望は、当然出てくる筈。特にリポジトリの規模が大きくなる程、余計な履歴の走査は避けたいところ。

『ブランチヘッド』ベースでの運用の場合:

どこからどこまでが『枝分かれ』なのか?

という判断は、非常に相対的な話になってしまう。

その1での Alice と Bob の例では:

リビジョン B 側で作業している Bob からすれば、『枝分かれ』は『Alice の作成したリビジョン A』になる。

しかしここで、リビジョン Pn よりも前の時点 P1 で共有リポジトリを複製し、作業を開始していた Carol が、自分のリポジトリに Alice の成果を取り込んだ結果、以下のような履歴になったと仮定すると:

リビジョン C 側で作業している Carol にとっては、P2 〜 A までが『枝分かれ』となる。

つまり、履歴ツリーの比較対象を決定しないことには、『枝分かれ』を厳密に定義することができないのだ。

『枝分かれ』時のリビジョンにタグを付与すれば、『そのタグから先』ということで、範囲を特定可能にできる、という考え方もある: 以下の例では『タグ "TAG" から先』という条件で範囲を特定。

しかしその場合でも、他の『枝分かれ』との間でマージが発生すると、単に『到達可能なリビジョン』を辿るだけでは、期待通りの範囲を特定することはできない。

Git での『ブランチ』や Mercurial の『ブックマーク』(詳細はその3で説明)は、『枝分かれ』の先頭を指すポインタとなるため、『枝分かれ』時リビジョンへのタグ付けと組み合わせることで、『枝分かれ』の範囲を確実に特定することができる。

この場合の該当リビジョン群は、Mercurial では以下のように revsets 表記することができる:

TAG::BOOKMARK

Mercurial の『名前付きブランチ』の場合、リビジョンの持つグループ名 = 『ブランチ名』は、履歴ツリーの構造に関係なく、永続的に記録される情報であるため、特定の『枝分かれ』に属するリビジョン群は、容易に特定できる。

上記の例において、対象リビジョンを revsets 表記する場合:

branch("BRANCH")

でも良いのだが、『名前付きブランチ』の所属判定は、各リビジョンの持つ属性ベースで実施されるので、単純な表記の場合、履歴の全走査が発生してしまう。

そのため、『ブックマーク』での範囲特定と同様に、『枝分かれ』リビジョンにタグを付与した上で、以下のような revsets 表記を用いた方が、実行効率上は良い筈。

(1) TAG:: and branch("BRANCH") または
(2) TAG::BRANCH または
(3) ::BRANCH and branch("BRANCH")

(1) は、BRANCH 内部で『ブランチヘッド』が複数ある場合でも、対象リビジョンを特定可能だが、(2) は単一ヘッド状態を仮定している。単一ヘッドを仮定可能なら (3) を使えば、TAG を打たなくても済む。

※ 以下、"その3" に続く