Git合并的顺序是否重要?

34

假设我有两个分支,AB。以下属性是否成立?

  • A合并到B中会冲突,当且仅当将B合并到A中会冲突。
  • 在将A合并到B之后,我的文件内容与将B合并到A之后的文件内容相同。

假设没有冲突,合并的内容是相同的。 - Maroun
1
@Maroun 我也有同样的预感,但这是否有文献资料支持呢?或者说,这是否是根据已知的合并算法属性逻辑推导出来的呢? - user4815162342
5个回答

22

cmaster的答案是正确的,但有一些限制条件。让我们先注意以下这些条款/假设:

  • 始终存在一个单独的合并基础提交。我们称此提交为B,即基础。
  • 另外两个输入也是单个提交。我们称它们为左侧/本地(--ours)和右侧/远程(--theirs)的LR

第一个假设不一定成立。如果有多个合并基础候选项,则由合并策略来处理此问题。两种标准的两头合并策略是recursiveresolveresolve策略只是随机选择其中一个。recursive策略每次合并两个合并基础,然后使用生成的提交作为合并基础。由resolve选择的可以受到传递给git merge-basegit merge参数顺序的影响,因此这里有一个限制条件。因为递归策略可以执行多个合并,所以这里有第二个限制条件,但仅适用于存在超过两个合并基础的情况。

第二个假设更为真实,但需要注意的是合并代码可以在部分修改的工作树上运行。在这种情况下,所有可能都不存在,因为工作树既不匹配L也不匹配R。标准的git merge会告诉你必须先提交,所以通常这不是问题。
合并策略很重要。
我们已经注意到了多个合并基础的问题。我们还假设进行了双头合并。
章鱼合并可处理多个头。这也改变了合并基础的计算方式,但通常情况下,章鱼合并将无法处理具有复杂合并问题的情况,并且仅会拒绝在可能顺序很重要的情况下运行。我不会过于强调它;这是另一种对称规则可能失败的情况。 -s ours合并策略完全忽略所有其他提交,因此合并顺序显然非常重要:结果始终是L。(我相当确定-s ours甚至不会费心计算合并基础B。)
你可以编写自己的策略并随心所欲地操作。在这里,你可以使顺序很重要,就像-s ours一样。
高级合并(有一个合并基础):文件名更改
Git现在从这三个快照中有效地计算出两个变更集。
  • L - B,或者git diff --find-renames B L
  • R - B,或者git diff --find-renames B R

这里的重命名检测是独立的——我的意思是它们互不影响;但两者都使用相同的规则。主要问题在于,在B中可能检测到同一文件在两个更改集中被重命名,在这种情况下,我们就会得到我所说的高级冲突,具体来说是重命名/重命名冲突。(我们还可以在重命名/删除和其他几种情况下得到高级冲突。)对于重命名/重命名冲突,Git选择的最终名称是L中的名称,而不是R中的名称。因此,在最终文件名方面,顺序很重要。但这不会影响工作树合并的内容

低级合并

此时,我们应该对Git的内部进行一小段介绍。我们已经将BL中的文件以及BR中的文件匹配起来了,也就是说,我们知道在每个提交中哪些文件是“相同”的。然而,Git存储文件和提交的方式非常有趣。从逻辑上讲,Git没有增量:每个提交都是所有文件的完整快照。然而,每个文件只是一个实体对:路径名P和哈希IDH
换句话说,在这一点上,没有必要遍历从BLR的所有提交。我们知道我们有一些文件F,由多达三个不同的路径名标识(如上所述,Git在大多数情况下将使用L路径,但如果在合并的B-vs-R方面只有一个重命名,则使用R路径)。所有三个文件的完整内容都可以通过直接查找获得:HB表示基本文件内容,HL表示左侧文件,HR表示右侧文件。
两个文件的哈希值完全匹配时才能认为它们完全相同。因此,Git在这一点上只比较哈希ID。如果三个哈希值都匹配,则合并后的文件与左、右和基础文件相同:没有任何工作需要完成。如果L和R匹配,则合并后的文件是L或R内容;由于双方进行了相同的更改,因此基础文件无关紧要。如果B匹配L或R但不匹配另一个文件,则合并后的文件是不匹配哈希的文件。Git只有在存在潜在的低级合并冲突时才需要进行低级合并。

因此,现在Git提取三个内容并执行合并。这是逐行进行的(当多个相邻行发生更改时,将它们分组在一起):

  • 如果左右两侧只改动了不同的源代码行,Git将会接受这两个更改。这显然是对称的。

  • 如果左右两侧同时改动了相同的源代码行,Git将会检查这两个更改是否相同。如果相同,Git将会接受一个更改。这也是对称的。

  • 如果左右两侧改动了相同的代码行,但是做出了不同的更改,Git将会宣布合并冲突。工作区内容将取决于更改的顺序,因为工作区内容有<<<<<<< HEAD ... ||||||| base ... ======= ... other >>>>>>>标记(如果选择了diff3样式,则会出现base部分)。

“相同的代码行”的定义有点棘手。这取决于差异算法(您可以选择),因为某些文件的某些部分可能会重复。但是,Git始终使用单个算法计算L和R,因此这里的顺序不重要。


换句话说,如果你成功地生成了一个“双重文件”——它与某个现有文件具有不同的内容,但相同的哈希值,Git将拒绝将该文件放入存储库。shattered.it PDF就不是这样的文件,因为Git会在文件数据前加上单词“blob”和文件大小,但该原则仍然适用。请注意,将这样的文件放入SVN会破坏SVN——嗯,有点道理

-X选项显然是不对称的

您可以使用-X ours-X theirs来覆盖合并冲突的投诉。这些指令让Git分别在LR更改方面解决冲突。

合并会生成一个合并提交,影响合并基础计算

即使考虑到上述警告,这个对称原则在单个合并中也是可行的。但是,一旦您进行了合并,下一个要运行的合并将使用修改后的提交图来计算新的合并基础。如果您有两个要执行的合并,并且按以下方式执行它们:

git merge one    (and fix conflicts and commit if needed)
git merge two    (fix conflicts and commit if needed)

即使每次合并都是对称的,也不意味着你一定会得到与运行以下命令相同的结果

git merge two
git merge one

无论哪个合并先运行,都会得到一个{{合并提交}},而第二个合并现在会找到另一个不同的合并基础点,这一点特别重要,如果您必须在完成先前的合并之前解决冲突,因为这也会影响第二个{{git merge}}命令的{{L}}输入。它将使用第一个合并的快照作为{{L}},并将新的(可能不同的)合并基础点作为{{B}},用于其三个输入中的两个。这就是我提到 -s recursive 在处理多个合并基础时具有潜在的顺序差异的原因。假设有三个合并基础,Git将合并前两个(无论它们以何种顺序从合并基础计算中弹出),提交结果(即使存在合并冲突,在这种情况下只需提交冲突),然后将该提交与第三个提交合并并提交结果。这里的最终提交然后是输入{{B}}。只有当此过程的所有部分都对称时,最终的{{B}}结果才是无序的。大多数合并都是对称的,但我们在上面看到了所有的警告。

3
谢谢你用这个精确、技术细节的回答来补充我的高层次、不够准确的回答 :-) - cmaster - reinstate monica

15
我认为,如果你无法满足这两个条件,那么你就找到了一个git merge的bug。
原因:在所有方向上同时合并是git构建的根本目的。这就是git从一开始就使用3路合并的原因:这是提供正确合并结果的唯一方法。这种3路合并在数学上是对称的,它基本上基于一个基础提交B,计算出状态R = (A - B) + (C - B) + B,而AC则来自于不同的状态。只有合并顺序的区别才会影响合并提交的父项顺序。
编辑:如果您对更多细节感兴趣,请参阅torek's answer。该回答给出了有关不同合并策略的所有技术细节,并指出了我的回答由于采用高度抽象的方式而不是很准确。

1
问题是,对于任意复杂的合并(例如涉及重命名检测和类似高级功能),是否保证将(C - B)后跟(A - B)应用于B会产生与以相反顺序执行相同的结果。由于Git没有补丁的正式理论,声称所有合并都是对称的,并且其他任何情况都是错误的,应该有文件支持。 - user4815162342
1
@user4815162342:Git不按照那个顺序应用更改。具体来说,Git从三个blob哈希(基础、左、右)开始:如果B=L或B=R,则选择不相等的一个。只有当所有三个都不相等时,我们才会得到一个合并——然后对于任何可能存在不对称的情况,我们都会得到合并冲突。至于重命名,重命名检测器在合并之前运行,在两个差异的树结果上运行,这些结果也不受顺序影响。但是,所有这些仅适用于一个合并。 - torek
这个问题还有更多的内容,所以我会写一个真正的答案。 - torek
@torek 感谢您澄清重命名的问题。只有当三个都不相等时,我们才会得到一个合并 - 然后我们会在任何可能存在不对称性的情况下得到合并冲突 - 这是否有文档记录?或者您是说这是“补丁”(patch)的基本属性,是历史上已经理解并不需要特定文档的?我真的很好奇;“应用补丁”的概念在 Git 中似乎从未被精确定义,并且考虑到补丁允许以某种“模糊性”解释其输入,它们并不完全清楚。 - user4815162342
@user4815162342:合并操作实际上并不应用补丁。请看我的答案(我想现在已经完成了)。有关合并文件的实际代码,请参见https://github.com/git/git/blob/master/xdiff/xmerge.c(以及`xdiff /`中的所有内容)。 - torek
@torek 真是个迷人的答案,感谢你抽出时间写下来。 - user4815162342

1
合并冲突发生在两个人更改同一文件的相同行或者一个人决定删除它而另一个人决定修改它的情况下。
基本上,如果您尝试将B合并到A时出现冲突,则尝试将A合并到B时也会出现冲突。如果没有冲突,将A合并到B和将B合并到A的结果必须相同。
这个问题纯粹是逻辑性的。因此,如果有人认为我的逻辑回答有误或需要改进,请随意纠正我或编辑此内容。

1
我不熟悉算法,但我认为对于这两个问题都是“是”。如果您找到了反例,很高兴能看到它。到目前为止,我还没有意识到任何问题。
我会检查一些模棱两可的情况,例如如果一个文件部分被一个人复制并被另一个人修改。哪个副本需要更改?由于没有单一正确答案,这可能取决于像父级顺序之类的小原因。

0

将A视为主分支,B视为子分支。

  1. 现在创建一个readme.txt文件,并添加一些内容,即“更改1”,并进行暂存和提交。
  2. 现在创建另一个子分支B,在readme.txt中进行一些更改,即将“更改2”附加到readme.txt中,并提交更改。
  3. 切换回主分支A,现在您将无法看到子分支B所做的任何更改。要反映B所做的相同更改,请从主分支即B合并到A
  4. 一旦您在主分支A中,将“更改3”附加到readme.txt文件中,并进行提交。
  5. 现在切换到子分支B,在readme.txt中添加“更改4”,并提交所做的更改
  6. 当你在子分支B中合并主分支A时,会导致合并冲突。

由于您无法在子分支B中看到readme.txt文件中的文本“更改3”,也不会将文本“更改4”附加到readme.txt文件中。相反,您正在覆盖readme.txt,即通过将具有“更改3”的文本readme.txt的内容与文本“更改4”合并。

从上面的例子可以看出,这两个属性都是好的。


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