分散バージョン管理システムが優れている主な理由の1つは、SVNのような従来のツールよりもはるかに優れていると聞いたことがあります。これは、実際には2つのシステムの動作方法に固有の違いが原因ですか、それともGit / Mercurialなどの特定の DVCS実装には、SVNよりも巧妙なマージアルゴリズムしかありませんか?
分散バージョン管理システムが優れている主な理由の1つは、SVNのような従来のツールよりもはるかに優れていると聞いたことがあります。これは、実際には2つのシステムの動作方法に固有の違いが原因ですか、それともGit / Mercurialなどの特定の DVCS実装には、SVNよりも巧妙なマージアルゴリズムしかありませんか?
回答:
なぜSubversionよりもDVCSの方がマージが優れているという主張は、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
ただし、リビジョンの範囲を指定するこの方法は、バージョンツリーが大きくなるとすぐに手に負えなくなります。サブバージョンには、いつどのリビジョンがマージされるかに関するメタデータがないためです。後で何が起こるかについて熟考する:
12 14
trunk …-->o-------->o
"Okay, so when did we merge last time?"
13 15
b1 …----->o-------->o
これは、Subversionが持っているリポジトリ設計による主な問題です。ブランチを作成するには、トランクのコピーを格納するリポジトリに新しい仮想ディレクトリを作成する必要がありますが、いつ、何に関する情報を保存しません。マージされた状態に戻ります。これにより、厄介なマージの競合が発生することがあります。さらに悪いことに、Subversionはデフォルトで双方向のマージを使用していたため、2つのブランチヘッドが共通の祖先と比較されない場合の自動マージにはいくつかの不自由な制限があります。
このSubversionを軽減するために、ブランチとマージのメタデータが保存されるようになりました。それですべての問題は解決しますか?
一元化されたシステムでは、Subversionのように、仮想ディレクトリは最低です。どうして?誰もがそれらを表示するためのアクセス権を持っているからです…ごみの実験的なものでさえです。実験したいが、全員とその叔母の実験を見たくない場合は、分岐が適しています。これは深刻な認知ノイズです。追加するブランチが多いほど、より多くのがらくたが表示されます。
リポジトリ内に公開ブランチが多いほど、さまざまなブランチすべてを追跡することが難しくなります。したがって、問題はブランチがまだ開発中であるか、集中管理されたバージョン管理システムではわかりにくいかどうかです。
ほとんどの場合、私が見た限りでは、組織はデフォルトで1つの大きなブランチを使用することになります。これは残念なことです。それは、テストやリリースのバージョンを追跡することが困難になるためです。
非常に単純な理由があります。分岐はファーストクラスのコンセプトです。設計上、仮想ディレクトリはなく、ブランチはDVCSのハードオブジェクトであり、リポジトリの同期(つまり、pushとpull)を単純に処理するためには、ハードオブジェクトである必要があります。
DVCSで作業するときに最初に行うことは、リポジトリ(git clone
、hg clone
、bzr branch
)のクローンを作成することです。クローン作成は、バージョン管理でブランチを作成することと概念的に同じです。一部の人はこれをフォークまたはブランチングと呼びます(後者は同じ場所に配置されたブランチを参照するために使用されることもあります)が、それは同じことです。すべてのユーザーが独自のリポジトリを実行します。つまり、ユーザーごとのブランチが行われます。
バージョン構造はツリーではなく、グラフです。より具体的には、有向非巡回グラフ(DAG、つまりサイクルのないグラフを意味します)。各コミットに1つ以上の親参照(コミットの基になっているもの)があることを除いて、DAGの詳細を詳しく知る必要はありません。そのため、次のグラフでは、これによりリビジョン間の矢印が逆に表示されます。
マージの非常に簡単な例はこれです。と呼ばれる中央リポジトリorigin
と、ユーザーのアリスが自分のマシンにリポジトリを複製するとします。
a… b… c…
origin o<---o<---o
^master
|
| clone
v
a… b… c…
alice o<---o<---o
^master
^origin/master
クローン中に発生することは、すべてのリビジョンがそれらとまったく同じように(一意に識別可能なハッシュIDによって検証される)Aliceにコピーされ、オリジンのブランチがどこにあるかをマークすることです。
次に、アリスは自分のリポジトリで作業し、自分のリポジトリにコミットして、変更をプッシュすることを決定します。
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者間マージアルゴリズムはすべてのバージョン管理システム間でほぼ同じであるため、問題はマージアルゴリズムにはありません。問題は何よりも構造についてです。
確かに上記の例は非常に単純なユースケースなので、より一般的なものではありますが、より多くのねじれたものを実行してみましょう。origin
3つのリビジョンから始まったことを覚えていますか?まあ、それらをやった人は彼をBobと呼んで、自分で作業していて、自分のリポジトリでコミットしています:
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
リポジトリに直接プッシュできなくなりました。システムがこれを検出する方法は、Bobのリビジョンがから直接下降しているかどうかをチェックすることです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
うまくいけば、マージが競合に遭遇しないことを期待します(それらが予想される場合は、fetch
とでgitで2つの手順を手動で実行できますmerge
)。後で行う必要があるのは、これらの変更を再びにプッシュorigin
することです。マージコミットは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の変更を最新の変更の後に移動します。この回答をこれ以上冗長にしたくないので、代わりにgit、mercurial、またはbazaarのドキュメントを読んでもらいます。
読者のための練習として、関係する別のユーザーとどのようにうまくいくかを引き出してみてください。上記の例と同様にボブを使用して行います。リポジトリ間のマージは、すべてのリビジョン/コミットが一意に識別できるため、思ったより簡単です。
また、各開発者間でパッチを送信するという問題もあります。これはSubversionで大きな問題であり、git、hg、bzrで一意に識別可能なリビジョンによって軽減されています。誰かが自分の変更をマージして(つまり、マージコミットを作成して)、中央リポジトリにプッシュするか、パッチを送信することで、チームの他の全員が使用できるように送信すると、すでに発生しているため、マージについて心配する必要はありません。 。マーティン・ファウラーは、このような無差別な統合の仕組みを呼び出しています。
Subversionとは構造が異なるため、DAGを採用することで、システムだけでなくユーザーにとっても簡単に分岐とマージを行うことができます。
歴史的に、Subversionはマージ情報を保存していなかったため、ストレート双方向のマージしか実行できませんでした。これには、一連の変更を取得してツリーに適用することが含まれます。マージ情報があっても、これは依然として最も一般的に使用されるマージ戦略です。
Gitはデフォルトで3ウェイマージアルゴリズムを使用します。これには、マージされるヘッドの共通の祖先を見つけ、マージの両側に存在する知識を利用することが含まれます。これにより、Gitは競合を回避する際によりインテリジェントになります。
Gitには、洗練された名前変更検索コードもいくつかあり、これも役立ちます。変更セットや追跡情報は保存されません -コミットごとのファイルの状態を保存し、ヒューリスティックを使用して必要に応じて名前変更とコードの移動を特定します(ディスク上のストレージはこれよりも複雑ですが、インターフェイスロジック層に表示され、追跡は行われません)。
簡単に言うと、マージの実装はSVNよりもGitの方が優れています。。1.5より前のバージョンでは、SVNはマージアクションを記録しなかったため、SVNが記録しなかった情報を提供する必要があるユーザーの助けなしに、将来のマージを実行することはできませんでした。1.5でより良くなり、実際、SVNストレージモデルはGitのDAGよりもわずかに優れています。しかし、SVNはマージ情報をかなり複雑な形式で保存しました。これにより、マージはGitよりも大幅に時間がかかります。実行時間には300のファクターが観察されました。
また、SVNは移動されたファイルのマージを支援するために名前変更を追跡すると主張しています。しかし、実際には、それらはまだそれらをコピーおよび個別の削除アクションとして保存し、マージアルゴリズムは、変更/名前変更の状況でそれらにつまずきます。マージされます。このような状況でも、疑似マージの競合が発生し、ディレクトリの名前を変更した場合、変更内容が失われることもあります。(SVNの人々は、変更がまだ履歴にあることを指摘する傾向がありますが、それらが表示されるはずのマージ結果に含まれていない場合はあまり役に立ちません。
一方、Gitは名前の変更さえ追跡しませんが、(マージ時に)名前変更後にそれらを把握し、非常に魔法のように実行します。
SVNマージ表現にも問題があります。1.5 / 1.6では、好きなだけ頻繁にトランクからブランチに自動的にマージできましたが、反対方向のマージはアナウンスする必要があり(--reintegrate
)、ブランチを使用できない状態のままにしました。ずっと後に、これは実際にはそうではなく、a)自動的に計算--reintegrate
できること、およびb)双方向で繰り返しマージが可能であることを発見しました。
しかし、これらすべて(IMHOが彼らのしていることに対する理解の欠如を示しています)の後で、私は(OK、私は)SVNを重要でない分岐シナリオで使用することに非常に注意を払い、Gitが考えていることを確認しようとするのが理想的ですマージ結果。
SVNでブランチを強制的にグローバルに可視化するなど、回答で行われた他のポイントは、マージ機能には関係ありません(ただし、使いやすさは)。また、「Gitは変更を保存しますが、SVNは(何か違うもの)を保存します」はほとんど問題になっています。Gitは概念的には、各コミットを個別のツリー(tarファイルなど)として格納し、かなりのヒューリスティックを使用して効率的に格納します。2つのコミット間の変更の計算は、ストレージの実装とは別です。真実は、Gitが履歴DAGをSVNがmergeinfoを実行するよりもはるかに簡単な形式で保存することです。後者を理解しようとする人は誰でも私が何を意味するか知っています。
一言で言えば、GitはSVNよりもはるかに単純なデータモデルを使用してリビジョンを保存するため、表現に対応しようとするのではなく、実際のマージアルゴリズムに多くのエネルギーを費やすことができます。
他の回答で言及されていないことの1つは、DVCSの大きな利点であり、変更をプッシュする前にローカルでコミットできることです。SVNでは、チェックインしたい変更があり、その間に誰かがすでに同じブランチでコミットを行っていたため、コミットするsvn update
前に実行する必要がありました。これは、私の変更と他の人からの変更が混在していることを意味します。元に戻すコミットがないため、(git reset
またはのようにhg update -C
)マージを中止する方法はありません。マージが重要でない場合、これは、マージ結果をクリーンアップするまで、機能の作業を続行できないことを意味します。
しかし、多分それは、別々のブランチを使用するのが難しい人にとってはメリットにすぎないでしょう(私が正しく覚えていれば、SVNを使用していた会社で開発に使用されたブランチは1つしかありませんでした)。
編集:これは主に質問のこの部分に対処しています:これは
実際には2つのシステムの動作方法の固有の違いによるものですか、それともGit / Mercurialのような特定のDVCS実装にはSVNよりも巧妙なマージアルゴリズムがあるだけですか?
TL; DR-これらの特定のツールには、より優れたアルゴリズムがあります。分散されていることはワークフローにいくつかの利点がありますが、マージの利点とは関係ありません。
編集を終了
受け入れた回答を読みます。それはただの間違いです。
SVNのマージは面倒な場合があり、扱いにくい場合もあります。ただし、実際の動作を1分間無視してください。Gitが保持または導出できる情報には、SVNも保持または導出できない情報はありません。さらに重要なことに、バージョン管理システムの個別の(場合によっては部分的な)コピーを保持することで、より実際的な情報が提供される理由はありません。2つの構造は完全に同等です。
あなたが「何か賢いこと」をしたいとしましょう。Gitは「得意」です。そして、あなたはSVNにチェックインされています。
SVNを同等のGitフォームに変換し、Gitで実行してから、おそらく複数のコミットを使用して、いくつかの追加のブランチで結果を確認します。SVNの問題をGitの問題に変える自動化された方法を想像できる場合、Gitには基本的な利点はありません。
結局のところ、どのバージョンコントロールシステムでも
1. Generate a set of objects at a given branch/revision.
2. Provide the difference between a parent child branch/revisions.
さらに、マージするために知っておくことも役立ちます(または重要です)
3. The set of changes have been merged into a given branch/revision.
Mercurial、Git、およびSubversion(現在はネイティブで、以前はsvnmerge.pyを使用していた)はすべて、3つの情報すべてを提供できます。DVCで根本的に優れたものを示すために、Git / Mercurial / DVCで利用できる4番目の情報を指摘してください。SVN/集中型VCでは利用できません。
それは彼らがより良いツールではないと言っているのではありません!
git merge-base
です。gitでは、「ブランチaとbがリビジョンxで分割されている」と言うことができます。しかし、svnは「ファイルがfooからbarにコピーされました」を格納するため、ヒューリスティックを使用して、プロジェクト内でファイルをコピーするのではなく、barへのコピーが新しいブランチを作成していたことを突き止める必要があります。コツは、svnのリビジョンがリビジョン番号とベースパスによって定義されることです。ほとんどの場合「トランク」であると想定することは可能ですが、実際にブランチがある場合は噛み付きます。
SVNはファイルを追跡し、Gitはコンテンツの変更を追跡します。あるクラス/ファイルから別のクラス/ファイルにリファクタリングされたコードのブロックを追跡するのに十分なほど賢いです。彼らはソースを追跡するために2つの完全に異なるアプローチを使用します。
私はまだSVNを頻繁に使用していますが、Gitを使用したことが何度かあることに非常に満足しています。
時間があれば読みやすい:なぜGitを選んだのか
Joelのブログの記事を読んでください(悲しいことに彼の最後の記事です)。これはMercurialに関するものですが、実際にはGitなどの分散型VCシステムの利点について述べています。
分散バージョン管理では、分散部分は実際には最も興味深い部分ではありません。興味深いのは、これらのシステムがバージョンではなく変更の観点から考えていることです。