Mercurialではじめる分散構成管理

第2回「マージ」怖くない ~ 分散した成果の集約

前回は、様々な方法で複製したリポジトリにおいて、それぞれ異なる作業成果を"hg commit"し、下図のような状態を構築するところまでを説明しました。

図1 成果の分散
図1 成果の分散

今回は、これら複数の成果を、最終的な成果へと統合する「マージ」について説明します。

成果の集約

成果をマージするためには、マージ作業を行うリポジトリへと成果を集約する必要があります。

成果の集約には"hg pull"を使用します。前回の説明では「リポジトリの複製」に使用した"hg pull"ですが、厳密には「一方の保持していない成果を他方に伝播」する、リポジトリ間連携機能なのです。

myrepo2の成果をmyrepoに取り込む手順を以下に示します。

コマンド1
% cd myrepo
% hg pull ../myrepo2
pulling from ../myrepo2
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)
% 

前回、"hg pull"を使って複製した時と異なり、"(+1 heads)"なる情報が表示がされていますが、これは一体何を表すのでしょう?

以下に示す、myrepo2の成果を取り込んだ直後のリポジトリの模式図を見れば、この表示の意味するところが何となくわかるのではないでしょうか。

図2 リポジトリ内での枝分かれ
図2 リポジトリ内での枝分かれ

そうです。myrepoでの変更履歴を表す木構造が、myrepo2の成果を取り込むことで枝分かれしているのです("2(1)"は、元リポジトリmyrepo2で"1"だったチェンジセットが、現リポジトリmyrepoでは"2"として扱われていることを意味します⁠⁠。

Mercurialでは、あるチェンジセットAが別なチェンジセットBから派生している場合、それぞれをお互いにとってのチェンジセットと呼び、子チェンジセットを持たないチェンジセット、つまり枝分かれの先端のチェンジセットをヘッド(head)と呼びます。

先の"(+1 heads)"表示は、myrepo2からの成果取り込みにより、myrepoのヘッドが増えたことを知らせているのです。

あるリポジトリにおけるヘッドを表示するには、先の実行結果にも表示されていますが、"hg heads"を使用します。

コマンド2
% hg heads
changeset:   2:fe8c0fd86885
tag:         tip
parent:      0:a044b949325f
user:        fujiwara
date:        Tue May 06 19:38:25 2008 +0900
summary:     add evening

changeset:   1:b85af333a085
user:        fujiwara
date:        Mon May 05 21:57:35 2008 +0900
summary:     add morning

% 

マージ作業は、ここで表示されるヘッド同士をマージして、枝分かれを解消することに他なりません。

次節では、実際にマージ作業を行う前に、Mercurialの「マージ」と、CVSやSubversionにおける「マージ」の違いについて、説明したいと思います。

実は日常的に「マージ」しているCVS/Subversion

CVSやSubversionの使用経験のある人がMercurialの概要を聞いた場合、往々にして以下のような感想を漏らす事が多いように思われます。

頻繁にマージをしなければならないようなツールは使いたくない。

こういった感想を持たれる人は、おそらくCVSやSubversionにおける「ブランチのマージ」の面倒くささに懲りているのでしょう。

しかし、ちょっと待ってください。ブランチを使用しなければ、CVSやSubversionは本当に「マージ」が不要なのでしょうか?

答えはNOです。

CVSやSubversionを習得する際には、書籍やオンラインドキュメント等で、何度も何度も以下のような「指導」を受けていると思います。

頻繁にupdateを実施して、リポジトリから最新の成果を取り込み、衝突(conflict)の解消が最小限で済むようにしましょう。

それではCVSやSubversionにおける、日常的な衝突解消の流れを詳細に見てみましょう。

  1. リポジトリからcheckout
  2. ワーキングディレクトリで作業を実施
  3. 他の人が作業成果をcommit
  4. updateで変更の取り込み
  5. 衝突を解消してcommit

これを図にすると、図3のようになります。

図3 未記録成果ベースのマージ
図3 未記録成果ベースのマージ

「updateで変更の取り込み」は、⁠マージ」という用語こそ使用していないものの、 ⁠ワーキングディレクトリの内容」「他の人の成果」「マージ」していることに他なりません。

つまり、CVSやSubversionの日常的なワークフローでも、実は頻繁にマージを行っていたわけです。

以上の事から、⁠マージの頻度」⁠=量)をもって分散リポジトリ形式の不便さを主張することは、意味を持たないことがお分かりでしょう。

Mercurialの「マージ」「commit済み成果ベース」

それでは次に、⁠マージの方式」⁠=質)について見てみましょう。

CVSやSubversionにおける、⁠commmit済み成果」「ワーキングディレクトリの内容」をマージする方式を、ここでは便宜上未記録成果ベースのマージと呼びます。

一方で、Mercurialでの「マージ」の流れは以下のようになります。

  1. ローカルリポジトリからupdate
  2. ワーキングディレクトリで作業を実施
  3. 作業成果をcommit
  4. 他のリポジトリからpullによる成果取り込み
  5. 2つの成果をmerge
  6. 衝突を解消してcommit

これを図にすると、図4のようになります。

図4 commit 済み成果ベースのマージ
図4 commit 済み成果ベースのマージ

「commit 済み成果」同士をマージしているこの方式を、ここでは便宜上commit 済み成果ベースのマージと呼びます(CVSやSubversionのブランチ間マージも、この形式です⁠⁠。

人によっては、以下のように考える方もいるかもしれません。

マージに先立ってcommitしなければならないのは面倒。

しかし、⁠未記録成果ベースのマージ」において、マージ対象となる「ワーキングディレクトリ」中の作業成果は、その時点ではどこにも記録されていません。それは即ち、マージ直前までのワーキングディレクトリ中の作業成果が、マージ作業の最中に霧散消失してしまう危険性と、常に隣り合わせであることを意味しています。

一方の「commit済み成果ベースのマージ」の場合、既にリポジトリに内容が記録されている「commit済み成果」をマージ対象としますから、そういったことは起こりません。

安全性の確保は、作業実施の際の心理的抵抗の低下を促すことでしょう。つまり、⁠マージの方式」⁠=質)では、むしろMercurialの方に利があるのです。

どうしても、次のように思えてしまう方もいるかもしれません。

CVSやSubversionでのブランチのマージは面倒だったので、きっとMercurialのマージも面倒に違いない。

しかし、次ページのMercurialでの実際のマージ手順を見れば、その簡単さに拍子抜けするのではないでしょうか。

マージ元チェンジセットの決定

それでは、Mercurialでマージを行ってみましょう。

Mercurialのマージでは、(1)ワーキングディレクトリが由来するチェンジセットと、(2)"hg merge"コマンドで指定するチェンジセットの、2つをマージします。

もっとも、日常的にマージが発生するワークフローは以下のような形になるでしょう。

  1. 作業成果をcommit
  2. 他のリポジトリからpullによる成果取り込み
  3. pullで増えたheadをワーキングディレクトリとmerge
  4. 衝突を解消してcommit

したがって、直前のcommit成果が対象となるため、特に何もする必要はありません

どうしても特定のチェンジセット同士をマージしたい場合は、チェンジセット識別子を指定して"hg update"を実行し、ワーキングディレクトリの内容を書き換えてください。

ちなみに、ワーキングディレクトリに未commnitの変更が残っている場合や、枝分かれしたチェンジセットの一方から他方へと"hg update"しようとした場合、以下のように表示されます。

コマンド3
abort: crosses branches (use 'hg merge' or 'hg update -C')

ワーキングディレクトリの上書きが問題ないのであれば、"-C"オプションを付けて"hg update"を実行してください。

マージの実施

マージの実施は"hg merge"コマンドで行います。マージ対象チェンジセットを指定して"hg merge"を実行してください。

コマンド4
% hg parents
changeset:   1:b85af333a085
user:        fujiwara
date:        Mon May 05 21:57:35 2008 +0900
summary:     add morning

% hg merge 2
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
% hg parents
changeset:   1:b85af333a085
user:        fujiwara
date:        Mon May 05 21:57:35 2008 +0900
summary:     add morning

changeset:   2:fe8c0fd86885
tag:         tip
parent:      0:a044b949325f
user:        fujiwara
date:        Tue May 06 19:38:25 2008 +0900
summary:     add evening

% 

"hg parents"は、ワーキングディレクトリが当該時点において、どのチェンジセットに由来するものかを表示します。"hg merge"実行によって2つのチェンジセットが表示されるようになるのは、ワーキングディレクトリの内容が、マージ対象となる2つのチェンジセットに由来していることを表します。

上記の実行例では表示されていませんが、マージの際に、同一ファイルの同一箇所に対して、双方のチェンジセットでの変更が検出された場合、衝突(conflict)と見なされ、以下のようなメッセージが表示されます。

コマンド5
merging hello.txt
merging hello.txt failed!
0 files updated, 0 files merged, 0 files removed, 1 files unresolved
There are unresolved merges, you can redo the full merge using:
  hg update -C 1
  hg merge 2

"merging hello.txt failed!"と表示されていますが安心してください。これは、⁠双方のチェンジセットの更新内容の自動的な統合に失敗」した旨のメッセージであって、マージ処理そのものが失敗したわけではありません。数行下に"There are unresolved merges"とあるように、⁠未解決の衝突が残っている」ために"failed"と表示しているのです。

さらに、出力メッセージにもあるように、"-C"付きで"hg update"を実行することで、"hg merge"実行を取り消すことができますから安心してください("hg merge"実行の取り消しは、このメッセージ出力の有無に関わらず可能です⁠⁠。

さて、⁠未解決の衝突が残っている」というのは、以下のような箇所を持つファイルが存在することを意味します(先の実行例では"hello.txt"がそれに該当します⁠⁠。

コマンド6
<<<<<<< local
hello someone
||||||| base
hello
=======
hello everyone
>>>>>>> other

マージの際の最も重要な作業は、どちらのチェンジセットに由来する変更を残すか(あるいは全く新しい内容にするか)を決定し、上記のようになったファイルを修正することです。言い換えれば"衝突さえなければ、マージ作業でやるべきことは、事後のcommitのみ"と言っても差し支えありません。つまりCVSやSubversionでのcommit前updateと同じ感覚です。

衝突の解消が済んだなら、"hg commit"で内容を確定し履歴を記録します。

コマンド7

% hg commit -m 'merge'
% hg parents
changeset:   3:69c713acc830
tag:         tip
parent:      1:b85af333a085
parent:      2:fe8c0fd86885
user:        fujiwara
date:        Mon May 26 10:17:21 2008 +0900
summary:     merge

% 

"hg merge"直後の"hg parents"で出力されていた2つのチェンジセットが、そのまま新規チェンジセットの「親」として記録されています。

マージ成果の"hg commit"後のリポジトリの模式図を以下に示します。

図5 マージによる成果統合
図5 マージによる成果統合

後は、同様な手順を繰り返すことで、他のリポジトリの成果を最終的な成果へと統合することができます。

ちなみに、CVSやSubversionの経験がある人は(特にcommit時にファイル名を几帳面に指定していた人は⁠⁠、マージにおける"hg commit"実行の際に、自分が衝突解消したファイルだけを指定する傾向があるようですが、マージ時の"hg commit"実行は基本的には「ファイル名指定無し⁠⁠、即ち「リポジトリ中の変更済みの全てのファイルが対象」と考えてください。

Mercurialのマージは「枝分かれした他のチェンジセットの成果との統合」ですから、自分が関知していないファイルに対する成果も当然含まれ得ます。"hg commit"の際にファイル名を指定する癖があると、それらの成果をマージし損ねる可能性が増加しますのでご注意ください。

マージツールの選択

頻繁にマージを実施するのであれば、自分が使い慣れたマージツールを使用したい、というのは自然な欲求と言えます。

Mercurialのマージツール設定は、以前も比較的容易でしたが、1.0版になって更に柔軟な設定ができるようになりました。

例えば、Windowsのバイナリ版Mercurialでは、GUIベースのマージツールだけでも、以下のものが使用可能な状態になっています(各ツールのインストールは別途必要です⁠⁠。

また、拡張子によるマージツールの使い分けをすることもできます。例えば、以下のようなケースです。

  • *.jpgや*.pngならペイント系ツール
  • *.docならMS Word
  • *.xlsならMS Excel
  • *.htmlや*.xmlなら専用エディタ
  • それ以外は従来の非対話的な3-way diffマージ

このようなUI選択の柔軟性によって、これまでより格段にマージが楽になるのではないでしょうか。

次回からは、いよいよ実践的な分散リポジトリの活用方法について説明したいと思います。

おすすめ記事

記事・ニュース一覧