web-dev-qa-db-ja.com

GitでのマージはSVNよりもどのようにおよび/またはなぜ優れているのですか?

分散バージョン管理システムが輝く主な理由の1つは、SVNのような従来のツールよりもはるかに優れていると、いくつかの場所で聞いたことがあります。これは、実際には2つのシステムの動作方法に固有の違いによるものですか、またはspecific Git/MercurialのようなDVCS実装はSVNよりも賢いマージアルゴリズムを持っているだけですか?

397
Mr. Boy

SubversionよりもDVCSの方がマージが優れている理由の主張は、主にSubversionでの分岐とマージの動作方法にかなり基づいていました。 1.5. より前のSubversionは、ブランチがマージされるタイミングに関する情報を保存しなかったため、マージする場合は、マージする必要があるリビジョンの範囲を指定する必要がありました。

では、なぜSubversionはマージsuckしたのですか?

この例を考えてみましょう:

      1   2   4     6     8
trunk o-->o-->o---->o---->o
       \
        \   3     5     7
b1       +->o---->o---->o

merge 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 )。クローン作成は、概念的にはバージョン管理でブランチを作成することと同じです。これをforkingまたはbranchingと呼ぶ人もいます(後者は同じ場所にあるブランチの参照にもよく使用されますが)同じこと。すべてのユーザーは独自のリポジトリを実行します。つまり、ユーザーごとに分岐が行われます。

バージョン構造はtreeではなく、代わりにgraphです。より具体的には、 有向非巡回グラフ (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

クローン中に起こることは、すべてのリビジョンが正確にそのままアリスにコピーされ(一意に識別可能なハッシュIDによって検証される)、Originのブランチがある場所をマークすることです。

その後、アリスは自分のリポジトリで作業し、自分のリポジトリでコミットし、変更をプッシュすることにしました。

         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方向のマージアルゴリズムはすべてのバージョン管理システム間でほとんど同じであるため、問題はマージアルゴリズムには実際にはありません。 問題は何よりも構造に関するものです

では、realマージの例を示してください。

確かに、上記の例は非常に単純なユースケースなので、より一般的な例ではありますが、より多くのツイストを行いましょう。 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リポジトリに直接プッシュできません。システムがこれを検出する方法は、ボブのリビジョンがOriginから直接下降しているかどうかをチェックすることです。この場合はそうではありません。プッシュしようとすると、システムに「 うーん...ボブをやらせることはできないのではないかと思う 」に似たメッセージが表示されます。

したがって、ボブはプルインしてから変更をマージする必要があります(gitの pull ;またはhgの pull および merge ;またはbzrの merge )。これは2段階のプロセスです。最初にBobは新しいリビジョンを取得する必要があります。これにより、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で fetch および merge を使用して2つの手順を手動で実行できます)。後で行う必要があるのは、これらの変更を再度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<--+

rebaseと呼ばれるgitとhgをマージする別のオプションがあります。これは、最新の変更後にBobの変更を移動します。この回答をこれ以上冗長にしたくないので、代わりに gitMercurial または Bazaar のドキュメントを読んでみましょう。

読者の練習として、関係する別のユーザーとどのように機能するかを考えてみてください。上記の例と同様にボブで行われます。リポジトリ間のマージは、すべてのリビジョン/コミットが一意に識別できるため、思っているよりも簡単です。

また、各開発者間でパッチを送信するという問題もあります。これは、Subversionでは大きな問題であり、一意に識別可能なリビジョンによってgit、hg、bzrで緩和されます。誰かが自分の変更をマージ(つまり、マージコミット)し、中央のリポジトリにプッシュするかパッチを送信することで消費するためにチームの他の全員に送信すると、すでに発生しているので、マージについて心配する必要はありません。マーティン・ファウラーは、この作業方法を 無差別統合 と呼びます。

構造がSubversionと異なるため、代わりにDAGを使用することにより、システムだけでなくユーザーにとってもより簡単な方法で分岐とマージを行うことができます。

551
Spoike

歴史的に、Subversionはマージ情報を保存していないため、まっすぐな双方向のマージのみを実行できました。これには、一連の変更を取得してツリーに適用することが含まれます。マージ情報があったとしても、これは依然として最も一般的に使用されているマージ戦略です。

Gitはデフォルトで3方向のマージアルゴリズムを使用します。これには、マージされるヘッドの共通の祖先を見つけ、マージの両側に存在する知識を利用することが含まれます。これにより、Gitはよりインテリジェントに競合を回避できます。

また、Gitには洗練された名前変更検出コードがあり、これも役立ちます。 does n't変更セットの保存または追跡情報の保存-コミットごとにファイルの状態を保存し、ヒューリスティックを使用して、必要に応じて名前の変更とコードの動きを特定します(ディスク上のストレージはこれよりも複雑ですが、それがロジック層に提示するインターフェースは追跡を公開しません)。

29
Andrew Aylett

簡単に言えば、マージの実装は SVN よりも Git の方が優れています。 1.5以前は、SVNはマージアクションを記録しなかったため、SVNが記録しなかった情報を提供する必要があるユーザーの助けなしに、将来のマージを実行できませんでした。 1.5ではより良くなり、実際、SVNストレージモデルはGitのDAGよりもわずかに機能します。しかし、SVNはマージ情報をかなり複雑な形式で保存しているため、Gitよりもマージに非常に長い時間がかかります。実行時間に300の要因があります。

また、SVNは、移動されたファイルのマージを支援するために名前変更を追跡すると主張しています。しかし、実際にはまだコピーと個別の削除アクションとしてそれらを保存し、マージアルゴリズムは変更/名前変更の状況でつまずきます。つまり、あるブランチでファイルが変更され、別のブランチで名前が変更されます。マージされます。このような状況では、依然として誤ったマージの競合が発生し、ディレクトリの名前を変更する場合、変更がサイレントに失われることさえあります。 (SVNの人々は、変更がまだ履歴にあることを指摘する傾向がありますが、それらが表示されるはずのマージ結果にない場合、それはあまり役に立ちません。

一方、Gitは名前の変更も追跡しませんが、事実(マージ時)の後に名前を把握します。

SVNマージ表現にも問題があります。 1.5/1.6では、トランクからブランチに好きなだけ自動的にマージできましたが、他の方向へのマージをアナウンスする必要があり(--reintegrate)、ブランチを使用できない状態のままにしました。かなり後になって、彼らはこれが実際にはそうではなく、a)--reintegratecanが自動的に計算され、b)双方向で繰り返しマージが可能であることを発見しました。

しかし、このすべての後(私見は彼らが何をしているのか理解していないことを示しています)、私は(OK、私は)任意の非自明な分岐シナリオでSVNを使用することを非常に注意し、理想的にはGitの考えマージ結果。

SVNのブランチのグローバルな可視性が強制されているため、回答で指摘された他のポイントは、マージ機能とは関係ありません(ただし、使いやすさのため)。また、「Gitは変更を保存しますが、SVNは(別の何かを保存します)」という点はほとんど意味がありません。 Gitは各コミットを個別のツリー( tar ファイルなど)として概念的に保存し、かなりの経験則を使用して効率的に保存します。 2つのコミット間の変更の計算は、ストレージの実装とは別個のものです。本当のことは、Gitが履歴DAGをSVNがmergeinfoを実行するよりずっと簡単な形式で保存するということです。後者を理解しようとする人は誰でも私が言っていることを知っているでしょう。

簡単に言うと、GitはSVNよりもはるかに単純なデータモデルを使用してリビジョンを保存するため、実際のマージよりも表現をうまく処理しようとするのではなく、実際のマージアルゴリズムに多くのエネルギーを注ぐことができます。

17
Andreas Krey

他の回答で言及されていないことの1つ、DVCSの大きな利点は、変更をプッシュする前にローカルでコミットできることです。 SVNでは、チェックインしたい変更があり、その間に誰かが同じブランチですでにコミットを行っていたため、コミットする前にsvn updateを実行する必要がありました。これは、私の変更と他の人からの変更が一緒に混合されることを意味し、戻るコミットが存在しないため、(git resethg update -Cのように)マージを中止する方法がありません。マージが自明でない場合、これは、マージ結果をクリーンアップする前に機能の作業を続行できないことを意味します。

しかし、それはおそらく、別々のブランチを使用するにはあまりにも愚かな人々にとってのみ利点です(私が正しく覚えていれば、SVNを使用した会社で開発に使用されたブランチは1つしかありませんでした)。

11
daniel kullmann

編集:これは主に質問のこの部分に対処しています:
これは実際に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で根本的に優れた何かを実証するために、SVN /集中型VCでは利用できないGit/Mercurial/DVCで利用可能な4番目の情報を指摘してください。

それは彼らがより良いツールではないと言っているわけではありません!

10
Peter

SVNはファイルを追跡し、Gitは追跡します コンテンツ 変更します。あるクラス/ファイルから別のクラス/ファイルにリファクタリングされたコードブロックを追跡するのに十分です。ソースを追跡するには、2つの完全に異なるアプローチを使用します。

私はまだSVNを頻繁に使用していますが、Gitを使用した数回に非常に満足しています。

時間があれば素敵な読書:Gitを選んだ理由

8
used2could

Joelのブログ(悲しいことに彼の最後のブログ)の記事を読んでください。これはMercurialについてですが、実際にはGitなどの分散VCシステムの利点について説明しています。

分散バージョン管理では、分散部分は実際には最も興味深い部分ではありません。興味深いのは、これらのシステムがバージョンの観点ではなく、変更の観点から考えることです。

記事を読む here

6
rubayeet