web-dev-qa-db-ja.com

「git merge」はどのように詳細に機能しますか?

「git merge」の背後にある正確なアルゴリズム(またはその近く)を知りたいです。少なくともこれらのサブ質問に対する回答は役に立ちます:

  • Gitは、競合しない特定の変更のコンテキストをどのように検出しますか?
  • Gitは、これらの正確な行に矛盾があることをどのように検出しますか?
  • Gitが自動マージするのはどれですか?
  • ブランチをマージするための共通のベースがない場合、gitはどのように動作しますか?
  • ブランチをマージするための複数の共通ベースがある場合、gitはどのように動作しますか?
  • 一度に複数のブランチをマージするとどうなりますか?
  • マージ戦略の違いは何ですか?

しかし、アルゴリズム全体の説明ははるかに優れています。

61
abyss.7

3ウェイマージアルゴリズムの説明を探すのが最善かもしれません。高レベルの説明は次のようになります。

  1. 適切なマージベースB-両方の新しいバージョン(XY)の祖先であるファイルのバージョン、および通常は最新のベースを見つけます。 (さらに戻る必要がある場合もありますが、これはgits default recursive mergeの機能の1つです)
  2. XBおよびYBの差分を実行します。
  3. 2つの差分で識別される変更ブロックを確認します。両側が同じスポットに同じ変更を導入する場合、どちらかを受け入れます。一方が変更を導入し、もう一方がその領域をそのままにする場合、最終版に変更を導入します。両方がスポットに変更を導入しても一致しない場合は、手動で解決する競合をマークします。

完全なアルゴリズムはこれをより詳細に扱っており、ドキュメント(/usr/share/doc/git-doc/technical/trivial-merge.txtとともに、git help XXXページ、XXXはmerge-basemerge-filemergemerge-one-fileおよび場合によっては他のいくつか)。それが十分に深くなければ、常にソースコードがあります...

44
twalberg

ブランチをマージするための複数の共通ベースがある場合、gitはどのように動作しますか?

この記事はとても役に立ちました: http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html (こちらは part 2 ))。

Recursiveはdiff3を再帰的に使用して、祖先として使用される仮想ブランチを生成します。

例えば。:

(A)----(B)----(C)-----(F)
        |      |       |
        |      |   +---+
        |      |   |
        |      +-------+
        |          |   |
        |      +---+   |
        |      |       |
        +-----(D)-----(E)

次に:

git checkout E
git merge F

CDの2つの最良の共通の祖先(他の祖先ではない共通の祖先)があります。 Gitはそれらを新しい仮想ブランチVにマージし、Vをベースとして使用します。

(A)----(B)----(C)--------(F)
        |      |          |
        |      |      +---+
        |      |      |
        |      +----------+
        |      |      |   |
        |      +--(V) |   |
        |          |  |   |
        |      +---+  |   |
        |      |      |   |
        |      +------+   |
        |      |          |
        +-----(D)--------(E)

より良い共通の祖先があれば、GitはVを次の祖先とマージするだけで続行すると思います。

この記事では、仮想ブランチの生成中にマージの競合が発生した場合、Gitは競合マーカーをそのままにして、続行します。

複数のブランチを一度にマージするとどうなりますか?

@Nevik Rehnelが説明したように、それは戦略に依存し、man git-mergeMERGE STRATEGIESセクションでよく説明されています。

octopusours/theirsのみが、複数のブランチを一度にマージすることをサポートします。たとえば、recursiveはサポートしません。

octopusは競合がある場合はマージを拒否し、oursは競合が発生しないように取るに足らないマージです。

これらのコマンドは、新しいコミットを生成し、2つ以上の親を持ちます。

Git 1.8.5で競合なしにmerge -X octopusを1つ実行し、その結果を確認しました。

初期状態:

   +--B
   |
A--+--C
   |
   +--D

アクション:

git checkout B
git merge -Xoctopus C D

新しい状態:

   +--B--+
   |     |
A--+--C--+--E
   |     |
   +--D--+

予想どおり、Eには3つの親があります。

TODO:単一のファイル変更でタコが正確に動作する方法。再帰的な2 x 2の3方向マージ?

ブランチをマージするための共通のベースがない場合、gitはどのように動作しますか?

@Torekは、2.9以降、--allow-unrelated-historiesなしでマージが失敗することに言及しています。

Git 1.8.5で経験的に試してみました:

git init
printf 'a\nc\n' > a
git add .
git commit -m a

git checkout --Orphan b
printf 'a\nb\nc\n' > a
git add .
git commit -m b
git merge master

aの内容:

a
<<<<<<< ours
b
=======
>>>>>>> theirs
c

次に:

git checkout --conflict=diff3 -- .

aの内容:

<<<<<<< ours
a
b
c
||||||| base
=======
a
c
>>>>>>> theirs

解釈:

  • ベースは空です
  • ベースが空の場合、単一ファイルの変更を解決することはできません。解決できるのは、新しいファイルの追加などだけです。上記の競合は、ベースa\nc\nを1行追加する3方向のマージで解決されます。
  • I think基本ファイルなしの3方向マージは2方向マージと呼ばれ、単なる差分です

私も興味があります。答えはわかりませんが、...

動作する複雑なシステムは、動作する単純なシステムから進化したものであることが常に判明しています。

Gitのマージは非常に洗練されており、理解するのは非常に難しいと思いますが、これにアプローチする1つの方法は、その前駆体からであり、懸念の中心に焦点を当てることです。つまり、共通の祖先を持たない2つのファイルが与えられた場合、git mergeはどのようにそれらをマージするのか、競合はどこにあるのでしょうか?

いくつかの前駆体を見つけてみましょう。 git help merge-fileから:

git merge-file is designed to be a minimal clone of RCS merge; that is,
       it implements all of RCS merge's functionality which is needed by
       git(1).

ウィキペディアから: http://en.wikipedia.org/wiki/Git_%28software%29 -> http://en.wikipedia.org/wiki/Three-way_merge#Three -way_merge -> http://en.wikipedia.org/wiki/Diff -> http://www.cis.upenn.edu/~bcpierce/papers/ diff3-short.pdf

最後のリンクは、diff3アルゴリズムを詳細に説明している論文のPDFです。 google pdf-viewer version です。長さはわずか12ページで、アルゴリズムは数ページしかないが、完全な数学的処理です。それは少し形式的すぎるように思えるかもしれませんが、gitのマージを理解したい場合は、最初に単純なバージョンを理解する必要があります。私はまだチェックしていませんが、diff3のような名前では、おそらくdiff( longest common subsequenceを使用する必要があります) アルゴリズム)。ただし、Googleをお持ちの場合は、diff3のより直感的な説明があります。


さて、diff3git merge-fileを比較する実験を行ったところです。同じ3つの入力ファイルversion1 oldversion version2を使用し、<<<<<<< version1=======>>>>>>> version2diff3には||||||| oldversion)もあり、共通の遺産を示しています。

oldversionには空のファイルを使用し、version1およびversion2version2に1行だけ追加します。

結果:git merge-fileは、1つの変更された行を競合として識別しました。ただし、diff3は2つのファイル全体を競合として扱いました。したがって、diff3のように洗練されているため、この最も単純な場合でも、gitのマージはさらに洗練されています。

これが実際の結果です(テキストには@twalbergの回答を使用しました)。必要なオプションに注意してください(それぞれのマンページを参照)。

$ git merge-file -p fun1.txt fun0.txt fun2.txt

You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.
<<<<<<< fun1.txt
=======
THIS IS A BIT DIFFERENT
>>>>>>> fun2.txt

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...

$ diff3 -m fun1.txt fun0.txt fun2.txt

<<<<<<< fun1.txt
You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...
||||||| fun0.txt
=======
You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.
THIS IS A BIT DIFFERENT

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...
>>>>>>> fun2.txt

これに本当に興味があるなら、それはちょっとしたうさぎの穴です。私には、正規表現、最長共通部分列diff、文脈自由文法、または関係代数のアルゴリズムと同じくらい深いようです。あなたがそれの一番下に到達したい場合、私はあなたができると思うが、それはいくつかの断固とした研究が必要になります。

6
13ren

これが元の実装です

http://git.kaarsemaker.net/git/blob/857f26d2f41e16170e48076758d974820af685ff/git-merge-recursive.py

基本的に、2つのコミットの共通の祖先のリストを作成し、それらを再帰的にマージします。それらを高速転送するか、ファイルの3者間マージの基礎に使用される仮想コミットを作成します。

2
aamontal

Gitは、競合しない特定の変更のコンテキストをどのように検出しますか?
これらの正確な行に矛盾があることをgitはどのように検出しますか?

マージの両側で同じ行が変更された場合、それは競合です。そうでない場合、一方からの変更(存在する場合)が受け入れられます。

Gitが自動マージするのはどれですか?

競合しない変更(上記を参照)

ブランチをマージするための複数の共通ベースがある場合、gitはどのように動作しますか?

Git merge-base の定義により、1つのみ(最新の共通の祖先)が存在します。

一度に複数のブランチをマージするとどうなりますか?

それはマージ戦略に依存します(octopusおよびours/theirs戦略のみが3つ以上のブランチのマージをサポートします)。

マージ戦略の違いは何ですか?

これは git merge manpage

1
Nevik Rehnel