分散バージョン管理システムが優れている主な理由の 1 つは、SVN などの従来のツールよりもマージがはるかに優れていることだと、いくつかの場所で聞いたことがあります。これは実際には 2 つのシステムの動作方法の本質的な違いによるものでしょうか、それともGit/Mercurial などの特定のDVCS 実装のマージ アルゴリズムが SVN よりも優れているだけなのでしょうか。
ベストアンサー1
DVCSのマージがSubversionよりも優れている理由の主張は、少し前のSubversionでのブランチとマージの仕組みに大きく基づいていました。1.5.0ブランチがいつマージされたかに関する情報が保存されていなかったため、マージしたいときには、マージする必要があるリビジョンの範囲を指定する必要がありました。
では、なぜ Subversion のマージはダメだったのでしょうか?
次の例について考えてみましょう。
1 2 4 6 8
trunk o-->o-->o---->o---->o
\
\ 3 5 7
b1 +->o---->o---->o
私たちが望むときマージb1 の変更をトランクに反映するには、トランクがチェックアウトされているフォルダーで次のコマンドを発行します。
svn merge -r 2:7 {link to branch b1}
… ローカルの作業ディレクトリに変更をマージしようとしますb1
。その後、競合を解決して結果をテストした後、変更をコミットします。コミットすると、リビジョン ツリーは次のようになります。
1 2 4 6 8 9
trunk o-->o-->o---->o---->o-->o "the merge commit is at r9"
\
\ 3 5 7
b1 +->o---->o---->o
しかし、リビジョンの範囲を指定するこの方法は、バージョン ツリーが大きくなるとすぐに手に負えなくなります。これは、Subversion には、いつどのリビジョンがマージされたかに関するメタデータがなかったためです。その後に何が起こるかを考えてみましょう。
12 14
trunk …-->o-------->o
"Okay, so when did we merge last time?"
13 15
b1 …----->o-------->o
これは主に Subversion のリポジトリ設計による問題です。ブランチを作成するには、トランクのコピーを格納する新しい仮想ディレクトリをリポジトリ内に作成する必要がありますが、いつ何がマージされたかに関する情報は保存されません。そのため、厄介なマージ競合が発生することがあります。さらに悪いことに、Subversion はデフォルトで双方向マージを使用していましたが、2 つのブランチ ヘッドが共通の祖先と比較されない場合、自動マージに致命的な制限があります。
これを軽減するために、Subversion はブランチとマージのメタデータを保存するようになりました。これですべての問題が解決するのではないでしょうか?
ああ、ところで、Subversion はまだダメだ…
Subversion のような集中型システムでは、仮想ディレクトリは役に立ちません。なぜでしょう? 誰もが仮想ディレクトリを閲覧できるからです... 実験的なゴミディレクトリでさえも。実験したいが、みんなの実験を見たくない場合は、ブランチが便利です。これは深刻な認知ノイズです。ブランチを追加すればするほど、見るべきゴミが増えます。
リポジトリ内のパブリック ブランチの数が増えるほど、すべての異なるブランチを追跡することが難しくなります。そのため、ブランチがまだ開発中なのか、それとも完全に終了しているのかという疑問が生じますが、これは集中型バージョン管理システムでは判断が難しいことです。
私が見てきた限りでは、ほとんどの場合、組織はデフォルトで 1 つの大きなブランチを使用します。これは残念なことです。なぜなら、テスト バージョンとリリース バージョンを追跡することが難しくなり、ブランチ化によって得られるその他の利点も追跡できなくなるからです。
では、なぜ Git、Mercurial、Bazaar などの DVCS は、ブランチ作成やマージにおいて Subversion よりも優れているのでしょうか?
その理由は非常に単純です。ブランチは第一級の概念だからです。設計上、仮想ディレクトリはなく、ブランチは DVCS 内のハード オブジェクトであり、リポジトリの同期 (プッシュとプル)を簡単に行うためには、そのようにする必要があります。
DVCSで作業するときに最初に行うことは、リポジトリ(gitのclone
、hgのclone
そしてbzrのbranch
)。クローン作成は、概念的にはバージョン管理でブランチを作成するのと同じことです。これをフォークまたはブランチ作成と呼ぶ人もいますが (後者は、同じ場所にあるブランチを指すためにもよく使用されます)、同じことです。すべてのユーザーが独自のリポジトリを実行するため、ユーザーごとにブランチが作成されます。
バージョン構造はツリーではなくグラフです。より具体的には、有向非巡回グラフ(DAG は、サイクルのないグラフを意味します)。各コミットに 1 つ以上の親参照 (コミットのベース) があること以外、DAG の詳細を詳しく説明する必要はありません。そのため、次のグラフでは、リビジョン間の矢印が逆に表示されます。
マージの非常に単純な例は次のとおりです。 と呼ばれる中央リポジトリがありorigin
、ユーザー Alice がリポジトリを自分のマシンにクローンしていると想像してください。
a… b… c…
origin o<---o<---o
^master
|
| clone
v
a… b… c…
alice o<---o<---o
^master
^origin/master
クローン中に何が起こるかというと、すべてのリビジョンがそのまま Alice にコピーされ (一意に識別可能なハッシュ ID によって検証されます)、元のブランチがどこにあるかがマークされます。
その後、アリスは自分のリポジトリで作業し、自分のリポジトリにコミットして、変更をプッシュすることにしました。
a… b… c…
origin o<---o<---o
^ master
"what'll happen after a push?"
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
解決策は非常に単純で、origin
リポジトリが行う必要があるのは、すべての新しいリビジョンを取得し、そのブランチを最新のリビジョンに移動することだけです (git ではこれを「高速フォワード」と呼びます)。
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
a… b… c… d… e…
alice o<---o<---o<---o<---o
^master
^origin/master
上で説明したユースケースでは、何もマージする必要すらありません。3 方向マージ アルゴリズムはすべてのバージョン コントロール システム間でほぼ同じであるため、問題はマージ アルゴリズムにあるわけではありません。問題は何よりも構造に関するものです。
では、実際にマージが行われる例を見せていただけますか?
確かに、上記の例は非常に単純な使用例なので、より一般的ではあるものの、より複雑な例を見てみましょう。3origin
つのリビジョンから始まったことを覚えていますか? さて、リビジョンを行った人 (ボブと呼ぶことにします) は、独自に作業し、自分のリポジトリにコミットしました。
a… b… c… f…
bob o<---o<---o<---o
^ master
^ origin/master
"can Bob push his changes?"
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
これで、ボブはリポジトリに直接変更をプッシュできなくなりましたorigin
。システムがこれを検出するためには、ボブのリビジョンが のorigin
リビジョンから直接派生しているかどうかをチェックしますが、この場合はそうではありません。プッシュしようとすると、システムは次のようなメッセージを表示します。うーん...残念だけどそれはできないよボブ」
ボブは変更をプルインしてマージする必要がある(gitのpull
; または hg のpull
そしてmerge
; またはbzrのmerge
)。これは 2 段階のプロセスです。まず、ボブは新しいリビジョンを取得し、リポジトリからそのままコピーする必要がありますorigin
。これで、グラフが発散していることがわかります。
v master
a… b… c… f…
bob o<---o<---o<---o
^
| d… e…
+----o<---o
^ origin/master
a… b… c… d… e…
origin o<---o<---o<---o<---o
^ master
プル プロセスの 2 番目のステップは、分岐したヒントをマージし、結果をコミットすることです。
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
^ origin/master
マージで競合が発生しないことを祈ります(競合が発生することが予想される場合は、gitで2つの手順を手動で実行できます)。fetch
そしてmerge
)。後で必要なのは、これらの変更を に再度プッシュすることです。マージ コミットはリポジトリorigin
内の最新のコミットの直接の子孫であるため、結果として fast-forward マージが行われます。origin
v origin/master
v master
a… b… c… f… 1…
bob o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
v master
a… b… c… f… 1…
origin o<---o<---o<---o<-------o
^ |
| d… e… |
+----o<---o<--+
gitとhgをマージする別の方法として、 rebaseというものがあります。これはボブの変更を最新の変更の後に移動します。この回答をこれ以上冗長にしたくないので、ギット、気まぐれなまたはバザール代わりにそれに関するドキュメントを参照してください。
読者の皆さんの練習として、別のユーザーが関与した場合の仕組みを描いてみてください。これは、上記の Bob の例と同様に行われます。リポジトリ間のマージは、すべてのリビジョン/コミットが一意に識別できるため、思ったより簡単です。
また、各開発者間でパッチを送信するという問題もあります。これは Subversion では大きな問題でしたが、git、hg、bzr では一意に識別可能なリビジョンによって軽減されています。誰かが変更をマージ (つまり、マージコミット) し、それを中央リポジトリにプッシュするかパッチを送信してチームの他の全員に送信すれば、マージはすでに行われているため、心配する必要はありません。Martin Fowler はこの作業方法を次のように呼んでいます。乱交的な統合。
Subversion とは構造が異なり、DAG を採用することで、システムだけでなくユーザーにとってもブランチやマージが簡単に行えるようになります。