了解 git 冲突

Understanding git conflicts

我想我可能对 git 处理事情的方式有一些误解,因此,我面临着一些相当烦人的冲突。

我从 master 开始一个新的分支 A,并开始创建新文件。最终,我从分支 A 创建了分支 B,并开始处理它。一个分支 A 继续开发,B 需要在分支 A 中所做的更改,所以我将 A 合并到 B,并继续在它们两个上工作。

随着时间的推移,分支A继续开发,直到合并到master。在这一点上,我认为,现在它已经合并到 master 中,我可以简单地将 master 合并到 B 中,并从 A 和其他所有人那里获得所有更改。

问题是,现在我正在尝试这样做,我遇到了 "Both added"、"Both modified" 等的多重冲突 - 但我没有更改文件。我完全理解我更改的文件中的冲突 - 我造成了它们,我非常清楚。

认为我的解释令人困惑,我冒险进入 Google 幻灯片并创建了这个 "amazing" 绘图来说明我的场景(其中箭头代表合并,如 "merged master into A",除非它们指向分支中的第一个提交 - 在这种情况下它们表示 "branched off from here";也就是说,除了箭头方向之外,非常标准的 git 表示法 - 此处不确定):


其他人修改的文件,我不明白为什么列为冲突。也就是说,有一个文件在我创建分支 A 时已经更改,当我尝试将 master 合并到 B 时,同一个文件又被更改了。当然,分支 B 仍然有该给定文件的超旧版本,但是,仍然 git 不应该认识到它们在我的提交中没有被更改,而只是覆盖它们,或者它做了什么?我在这里错过了什么?

编辑:澄清一下,我是分支 A 和 B 的唯一贡献者。

shouldn't git recognize that they have not been changed in my commits, and just overwrite them?

没有

没改文件也没关系。 git 的脑子里没有关于 who 创建提交或 who 获得优先权的事情。 masterb 在某种程度上都不是 "better"。

重点只是 master 已知的文件和 b 已知的文件存在无法自动协调的不同之处。

Git 显然可以自动调和一些差异;例如,如果文件在一个分支中因第 1 行已更改而不同,而在另一个分支中因第 1000 行已更改而不同。但有时它只是举手让你解决。那不是因为 更改了文件,而是因为差异的性质。

至于 区别是什么,我认为阅读 会有所帮助 — 这是我读过的关于什么是合并以及如何合并的最好的文章有用。这完全是关于 LCA 以及从它到分支末端的差异。在你的图表中,LCA 是从最左边开始的第二个提交,一直回到 A 第一次分支的点。该提交与 master 末尾之间的所有差异,以及该提交与 b 末尾之间的所有差异都必须考虑在内,以便合并。好吧,git 告诉您对于这个文件它不能这样做。如果您考虑文件从那里到 master 末尾以及从那里到 b 末尾的变化情况,您就会明白为什么。

更改了文件或如何绘制图表都没有关系。有一条路径描述(在您的脑海中)发生的事情的历史并不重要。重要的是三种状态:早期提交时的状态、master 结束时的状态以及 b 结束时的状态.

你的图表很漂亮,但是有点误导。

让我们从一个问题开始:蓝色提交在哪个分支上?

这是一个技巧问题。它们不在 a 分支上,它们在 several branches 上,复数形式。如果你说 他们在分支 A,嗯,这是真的,但他们 也在 master其中大部分 也在 分支 B.

在您的绘图中,每个提交都没有标识符。这让他们很难谈论:我可以说 "the second-from-left grey commit" 或 "the rightmost blue commit",但这有点笨拙,所以让我将图形重新绘制为文本,在每次提交中使用单个大写字母。我也不会在这里使用箭头,因为它们在文本中很难做到。

A--B--C--D--E--F--G--H--I--J--K--L   <-- master
    \        \            /
     M--N--O--P--Q---R---S   <-- branch-A
         \        \
          T--U--V--W--X--Y--Å--Ø--Z   <-- branch-B

也就是说,master提示提交是提交Lbranch-A 提示提交 是提交 Sbranch-B 提示提交 是提交 Z.

在 Git 中,分支 name,如 master,总是指向一个单独的提交。您可以选择哪个提交;你select这种方式的提交是分支的提示提交可从到达的任何较早的提交也在分支上。因此,从 L 开始并向后工作——在这些图中向左——沿着从提交到提交的箭头,1 我们从 LK,然后从 KJ。但是 J 是一个 合并提交: 它有两个 parent,而不是一个。所以从 J 我们去 S I。从这两个,我们转到 H R,然后转到 G QFPE。有两种方法可以到达 E,但我们只访问了一次。从这里我们可以继续ODNCMB以及A。因此,该提交列表是 master.

上的一组提交

(请注意,我们不能从 QW,尽管我们 可以 走另一条路,从 WQ:所有的箭头都是单向的,指向后方。)

branch-A 上的提交集从 S 开始,然后倒退到 R,然后是 Q,然后是 P,然后是 EO 以及 DN 等等。所有这些提交也在 master.

branch-B 上的提交集从 Z 开始,通过 ØÅ 以及 YX 向后移动W,然后选择 PV,然后是 EO 以及 U,依此类推。因此 branch-B 上有四个提交不在任何其他分支上。


1从技术上讲,Git 的所有内部箭头都指向 向后 。因此,您通过向前绘制箭头来向后绘制箭头。出现这种情况是因为提交完全 read-only:parent 无法提前知道其最终 children 可能具有的哈希 ID,但 child 提交确实知道,提前/在创建时,child 具有的 parent 哈希 ID。所以箭头必须指向后面。

(为了向前移动,你告诉 Git 你想要结束的地方,然后它从那里向后移动,以确保它可以从其他起点结束到那里。)


git merge 的工作原理,大大简化了

当你运行:

git checkout master
git merge branch-B

Git 必须找到提交 LZ 合并基础 提交。为此,它像大多数 Git 操作一样向后工作,从这些分支提示提交中找到 最佳共享提交: 从两个分支提示都可以访问的提交,因此在两个分支上,但实际上是 "closest to the tips" 。在这种情况下,虽然可能不是很明显,但这就是提交 Q。从 L 开始,通过 K 返回到 J,然后向下返回到 S,然后是 R,然后是 Q。同时,从 Z 开始,回到 W 直到 Q。提交 PEO 等也在两个分支上,但提交 Q 是 "better",因为它是最后一个这样的提交:它是所有这些提交的后代。

Git 现在将在内部 运行 两个 git diff 命令。这将比较合并基础——提交 Q——与两个分支提示:

git diff --find-renames <hash-of-Q> <hash-of-L>   # what we changed
git diff --find-renames <hash-of-Q> <hash-of-Z>   # what they changed

在这两个 diff 列表中,如果某些文件在 both 分支提示中显示为 newly-created,您将得到该文件的 add/add 冲突.该文件不在 Q 中,但它 L Z 中。

对于所有三个提交中的文件,Git 将尝试合并两组差异中显示的任何更改。如果这些更改不重叠(并且 "touch at the edges" 也不重叠),Git 可以将它们组合起来。它们确实重叠但重叠完全相同的地方——覆盖相同的原始行 iQ,并进行 相同的 更改——Git 只需复制一份更改即可将其合并。所有其他重叠都会导致合并冲突。

此时你的工作是解决所有冲突,以你喜欢的任何方式。例如,在add/add whole-file 冲突中,如果两个文件匹配,则只选择其中一个的内容。如果没有,请以某种方式合并这两个文件。完成后,使用 git add 将每个冲突文件的最终内容写入索引。这将索引冲突标记为 "resolved",您现在可以 运行 git merge --continuegit commit 完成合并。2


2git merge --continue 检查以确保您正在完成合并。如果没有,它会出错。如果你是,它只是 运行s git commit——所以这两种方式都没有真正的区别,除非你实际上没有完成冲突的合并。