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