Hatena::ブログ(Diary)

彷徨えるフジワラ このページをアンテナに追加 RSSフィード

2012-08-01

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

ここ暫く、Twitter や ML 等で、Mercurial の『ブランチ』に関する質問 (特に Git の『ブランチ』との対比) に答える機会が度々あったので、Mercurial における『ブランチ』の概念に関してまとめてみた。

実のところ、SCMBootCamp in Nagoya #1 での、稲田氏 (id:methane) の基調講演資料における Mercurial のブランチに関する説明 (30ページ目) を見て:

別途口頭での説明が無いと、初学者や他のツールからの移行者にとっては、誤解を招きやすいのではなかろうか?

と思ったのが、このエントリを書く元々の動機だったのだけれど、書き上げるのにかれこれ3ヶ月以上を要してしまったというのは、反省することしきり。

っつーか、ここ暫くは本家に (議論の叩き台としてリジェクト覚悟で) パッチ提案したりと、色々アレだったんで、その辺は勘弁していただけるとありがたい限り …… orz

Mercurial 利用者に対する説明だけで良いなら:

  • 履歴ツリーが枝分かれしたのが『名前無しブランチ』
  • それだけだと使い勝手が悪いので、グループ分けできるようにしたのが『名前付きブランチ
  • Git 的な運用に拘りさえしなければ『ブックマーク』は急いで覚える必要は無い

ってことで、非常に簡単な話で済む筈。

しかし、表題に『特に Git 併用ユーザには是非読んでおいて欲しい』がついていることもあって、このエントリは『初学者向けの容易さ』よりも、『DVCS 経験者向けの正確さ』に主眼を置いていることから、どうしても込み入った話になってしまっている。

そのため、完全な DVCS 初学者が本エントリを読む場合、できれば拙著『入門 Mercurial』や『Mercurialではじめる分散構成管理』などで、ある程度 Mercurial に関する基本的な事柄は理解してから読まれることをお勧めする。

ちなみに、あまり長いエントリは読んでもらえない傾向にあるので(笑)、6回に分割して掲載する。

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

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

名前無しブランチとブランチヘッド

Mercurial における『ブランチ』概念の基本は、『名前無しブランチ』(anonymous branch) であると言える。

Mercurial では、以下の条件を満たす場合、リビジョン X は『名前無しブランチ』とみなされる(※ より厳密な定義は"その2"を参照のこと)。

  • X の親リビジョン P が、X の他に子リビジョンを持つ

図にすると以下のような感じ:

f:id:flying-foozy:20120805234647j:image

Mercurial では、子リビジョンを持たないリビジョンのことを『ヘッド』(head) または『ブランチヘッド』(branch head)、上記のようなヘッドが複数ある状態を『複数ヘッド』(multiple heads) 状態と呼ぶ:『ブランチヘッド』の厳密な定義はその4を参照。

リポジトリ中の『ブランチヘッド』は、"hg heads" で常に列挙することができる。

なお、Mercurial の『ヘッド』は Git の HEAD ポインタとは全くの別物なので、Git 併用ユーザは要注意!

上の図だと:

リビジョン名前無しブランチブランチヘッド
P(*1)×
A
X

(*1) P は親の状況が分からないので、『名前無しブランチ』性は不明。

『名前無しブランチ』の X に対して、子リビジョン Y が作成された場合:

f:id:flying-foozy:20120805234648j:image

Y 自身は、親(= X)が複数の子リビジョンを持たないので:

リビジョン名前無しブランチブランチヘッド
P(*1)×
A
X×
Y×

そこから更に『枝分かれ』した場合:

f:id:flying-foozy:20120805234649j:image

リビジョン名前無しブランチブランチヘッド
P(*1)×
A
X×
Y××
Z1○(*2)
Z2

(*2) 最初に Z1 だけが作成された時点では、Z1 は『名前無しブランチ』性を持たない。Z2 が作成されて、はじめて双方が『名前無しブランチ』性を獲得することになる。

ここで、A と Z1 がマージされると:

f:id:flying-foozy:20120805234954j:image

リビジョン名前無しブランチブランチヘッド
P(*1)×
A×
B×
X×
Y××
Z1×
Z2

『ブランチ』/『ヘッド』の両用語の使用の際には、以下のような使い分けを推奨する: 今後は、メッセージ翻訳等もこの方向で進める予定。

  • ある時点で『枝分かれした』こと(= 確定した話)は『ブランチ』
  • 現時点で『枝分かれしている』こと(= 変動し得る話)は『複数ヘッド』(or 『ブランチヘッド』の複数化)

但し、現状では、『名前無しブランチ』を両者の意味で使用するケースも多々あるため、会話の際には注意が必要。

複数ヘッド化のユースケース

Mercurial において、『ブランチヘッド』が複数作成される典型的なパターンは:

  1. 共有リポジトリ S の最新リビジョンは Pn

f:id:flying-foozy:20120801185106j:image

  1. Alice が、リポジトリ S を複製
  2. Alice が、Pn の子リビジョン A をコミット

f:id:flying-foozy:20120801185107j:image

  1. Bob が、リポジトリ S を複製
  2. Bob が、Pn の子リビジョン B をコミット

f:id:flying-foozy:20120801185108j:image

  1. Alice が、リポジトリ S にリビジョン A を反映

f:id:flying-foozy:20120801185109j:image

  1. Bob が、リポジトリ S にリビジョン B を反映 ⇒ 『ヘッド増加』要因で abort
  2. Bob が、リポジトリ S からリビジョン A を取り込み

f:id:flying-foozy:20120801185110j:image

この時点で Bob のリポジトリは、リビジョン A という新規の『ブランチヘッド』を取り込んだことになる: この後 Bob は、『ブランチヘッド』である2つのリビジョン A/B をマージして、再度リポジトリ S に反映することに。

ちなみに:

Bob の視点で見れば、リビジョン B が『主』であるので、『枝分かれ』したのはリビジョン A 側

ではあるが、Alice が Bob のリビジョン B を取り込むケースでは、当然この関係は逆転する。

『名前無しブランチ』の場合、『枝分かれ』における『主』と『従』の概念は、(少なくとも Mercurial の管理情報上は)存在しない: リビジョン番号はリポジトリ毎の固有情報なので大小関係に意味は無いし、日付情報も単なる『自己申告値』に過ぎない。

そのため、履歴ツリーにおける『幹』とか『枝』の認識は、あくまで主観的な判断しかできないのだが、個人的には:

自分のコミットしたリビジョンは、自分にとっては『主』であり『幹』であるけれど、他の人にとっては『従』であり『枝』である

という、極めて相対的なところが、分散開発における平等性を象徴しているようで、非常に気に入っている。

言い換えるなら、『幹』とか『枝』といった『木』のイメージではなく、お互いに絡まりながら伸びて行く『ツタ』のイメージへのシフト、と言ったところか?

もっとも、『ツタ』にだって『幹』的なものはあるだろうし、ツル同士がくっ付くわけでもないから、あくまで印象レベルでのイメージの話だけど(笑)。

なお、"Mercurial: The Definitive Guide" や "A Guide to Branching in Mercurial" などで、リポジトリを複製する事そのものを『枝分かれ』とみなしているのは:

リポジトリを複製したら、それぞれで新しい変更をコミットするでしょ? たとえその成果を共有リポジトリに反映させなくても、そんな複数のリポジトリを神の視点で1つに重ねて見れば、ほーら、履歴ツリーは『枝分かれ』して見えるじゃない!

という発想からだと思われる。

なので、『リポジトリの複製』による『枝分かれ』は、僕個人としては『名前無しブランチ』による『枝分かれ』の範疇だと考えている。

ブランチヘッドの永続性

Mercurial の基本方針が『記録された履歴は永続的なもの』であるため、例え複数の『ブランチヘッド』が存在していようとも、『ブランチヘッド』そのもの、およびそこに至るまでのリビジョン群は、そのまま放置しておいても履歴が刈り取られたりすることは無い。

その一方で、Git で言う『ブランチ』が:

  1. 『枝分かれ』先に名前をつける ("git branch ブランチ名")
  2. 変更対象を『枝分かれ』先に変更する ("git checkout ブランチ名")
  3. 変更成果をコミット ("git commit")

という手順を踏む必要があるのは:

『ブランチ』というポインタで『枝分かれ』の先を参照しておかないと、(1) 履歴が容易には参照できなくなるのと、(2) 参照されていない『枝分かれ』の先の履歴が、リポジトリのゴミ集め (garbage collection) の際に刈り取られてしまう

という理由があるため (と僕は理解している)。

obsolete 機能の説明における比較も参照のこと

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

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

リンク元