`git merge` 是做什么的?

132
我想了解一下关于git merge的确切算法(或接近的算法)。至少对以下子问题的回答将会很有帮助:
- Git如何检测特定非冲突更改的上下文? - Git如何发现这些确切行中的冲突? - Git自动合并哪些内容? - 当合并分支没有共同基础时,Git如何执行? - 当合并分支有多个共同基础时,Git如何执行? - 当我一次合并多个分支时会发生什么? - 合并策略之间有什么区别?
但是,整个算法的描述会更好。

11
我猜你可以用这些答案写一本书…… - Daniel Hilgarth
2
或者你可以直接阅读代码,这需要的时间与“描述整个算法”差不多。 - Nevik Rehnel
5
@DanielHilgarth 我很乐意找出是否已经有这样的书。欢迎提供参考。 - abyss.7
6
可以的。但如果有人已经了解这段代码背后的理论,那么它可能会更容易理解。 - abyss.7
  1. "特定非冲突更改的上下文"是什么?
  2. 第二点和第三点是相同但是否定的,我们可以合并这两个问题吗?
- Ciro Santilli OurBigBook.com
5个回答

105
您最好查找一个三方合并算法的描述。一个高级别的描述可能是这样的:
1. 找到一个合适的合并基础B - 该文件版本是新版本(XY)的祖先,并且通常是最近的这种基础(虽然有时需要进一步回溯,这是git默认递归合并的一些特性之一) 2. 对XBYB进行差异比较 3. 遍历在两个差异中识别出来的更新块。如果两侧在同一位置引入了相同的更改,则接受其中任意一个;如果一侧引入了更改而另一侧保持原样,则在最终结果中引入更改;如果两侧在同一位置都引入了更改,但是它们不匹配,则将其标记为需要手动解决的冲突。
完整的算法会更详细地处理这个问题,并且还有一些文档(例如:https://github.com/git/git/blob/master/Documentation/technical/trivial-merge.txt),以及git help XXX页面,其中XXX可以是merge-basemerge-filemergemerge-one-file,以及可能还有其他一些页面。如果这还不够深入,总是可以查看源代码...

“trivial-merge”文档可在https://git-scm.com/docs/trivial-merge上以格式查看。 - philb

16

当合并分支时,如果有多个共同的基础,Git会如何执行?

这篇文章非常有帮助:http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html(这里是第二部分)。

递归使用diff3递归地生成一个虚拟分支,将被用作祖先。

例如:

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

那么:

git checkout E
git merge F

存在两个最佳共同祖先(不是任何其他祖先的祖先),分别为CD。Git将它们合并成一个新的虚拟分支V,然后将V用作基础。

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

如果有更多的共同祖先,Git将继续使用它来将V与下一个祖先合并。

文章称,如果在生成虚拟分支时出现合并冲突,Git会将冲突标记留在原地并继续执行。

当我同时合并多个分支时会发生什么?

如@Nevik Rehnel所解释的那样,这取决于策略,该策略在man git-merge MERGE STRATEGIES部分中有很好的说明。

只有octopusours/theirs支持一次性合并多个分支,例如,recursive不支持。

octopus将拒绝合并如果存在冲突,则ours是一个平凡的合并,因此不可能存在冲突。

这些命令将生成一个新的提交记录,其中将有超过2个父节点。

我在Git 1.8.5上执行了一个无冲突的merge -X octopus,以查看情况如何。

初始状态:

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

操作:

git checkout B
git merge -Xoctopus C D

新状态:

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

不出所料,E 有三个父节点。

待办事项:octopus 如何对单个文件进行修改?递归两两三路合并?

当合并分支时没有共同基础时,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
这个问题现在有一个新的Stack Overflow链接,所以我浏览了一下这个答案(非常好),并注意到最后一部分最近由于Git版本2.9(提交e379fdf34fee96cd205be83ff4e71699bdc32b18)的变化已经过时了。自从Git版本2.9起,如果没有合并基础,Git现在会拒绝合并,除非你添加—allow-unrelated-histories - torek
1
这是@Ciro发布的文章的后续内容:http://blog.plasticscm.com/2012/01/more-on-recursive-merge-strategy.html - adam0101
除非自上次尝试以来行为有所改变:如果要合并的分支之间没有共同文件路径,则可以省略 --allow-unrelated-histories - Jeremy List
小修正:有 ours 合并策略,但没有 theirs 合并策略。recursive+theirs 策略只能解决两个分支。https://git-scm.com/docs/git-merge#_merge_strategies - nekketsuuu

12

我也很感兴趣。我不知道答案,但是...

一个有效的复杂系统通常都是从一个有效的简单系统演变而来的。

我认为Git的合并功能非常复杂,很难理解 - 但是一种方法是从它的前身入手,并关注你关心的核心问题。也就是说,如果有两个没有共同祖先的文件,Git如何合并它们,并确定冲突在哪里?

让我们尝试找到一些前身。从 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/Diff3 -> http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf

最后一个链接是一篇详细描述diff3算法的论文pdf。这里有一个谷歌pdf阅读器版本。它只有12页,算法只有几页 - 但是全面的数学处理。这可能看起来有点过于正式,但如果你想理解git的合并,你需要先理解简单版本。我还没有检查过,但像diff3这样的名称,你可能也需要了解diff(它使用最长公共子序列算法)。然而,如果你有谷歌,可能会有更直观的diff3解释...


现在,我刚刚做了一个实验,比较了diff3git merge-file。它们使用相同的三个输入文件version1 oldversion version2,并以相同的方式标记冲突,使用<<<<<<< version1=======>>>>>>> version2diff3还有||||||| oldversion),显示它们的共同遗产。
我为oldversion使用了一个空文件,并为version1version2使用几乎相同的文件,只是在version2中添加了一行额外的内容。
结果:git merge-file将单个更改的行视为冲突;但diff3将整个两个文件视为冲突。因此,尽管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的最长公共子序列算法、上下文无关文法或关系代数一样深奥。如果你想深入了解,我认为你可以,但需要一些决心去学习。

3

git如何检测单独的非冲突更改所在的上下文?
git如何发现这些确切行存在冲突?

如果合并的两侧都对同一行进行了修改,就会产生冲突;如果没有修改,则会接受其中一个侧面(如果有的话)的更改。

哪些内容可以被git自动合并?

不冲突的更改(见上文)。

当有多个公共基础用于合并分支时,git会如何表现?

根据Git merge-base的定义,始终只会有一个最新的共同祖先。

当我同时合并多个分支时会发生什么?

这取决于合并策略(只有octopusours/theirs策略支持合并两个以上的分支)。

合并策略有什么区别?

这在git merge手册中有解释。


2
“同一行”是什么意思?如果我在两行之间插入新的非空行并合并,哪些行是相同的?如果我在一个分支中删除了某些行,另一个分支中哪些行是“相同”的? - abyss.7
1
这个问题用文字来回答有点棘手。Git使用diffs来表示两个文件之间(或同一文件的两个版本之间)的差异,它可以通过比较上下文(默认情况下是三行)来检测是否添加或删除了某些行。 "相同的行"意味着根据上下文来确定,同时保留添加和删除的内容。 - Nevik Rehnel
1
你认为“同一行”更改会导致冲突。自动合并引擎是基于行还是基于块的?是否只有一个共同祖先?如果是这样,为什么需要git-merge-recursive存在? - Edward Thomson
1
@EdwardThomson:是的,分辨率是基于行的(块可以被拆分成更小的块,直到只剩下一行)。默认的合并策略使用最新的共同祖先作为参考,但如果你想使用其他策略也是可以的。至于git-merge-recursive我不知道应该是什么(没有man页面,谷歌也没有相关结果)。关于这个问题的更多信息可以在git mergegit merge-base的man页面中找到。 - Nevik Rehnel
1
git-merge手册和您指出的git-merge-base手册讨论了多个共同祖先和递归合并。我认为如果没有讨论这些内容,您的答案是不完整的。 - Edward Thomson
当修改不同的行也会产生冲突时,问题就出现了。 - skan

2

2
链接已经失效。 - Chujun Song

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接