您可以找到description here(另请参阅part 2):
什么时候需要合并递归?
(Git 2.30,2020 年第一季度,将有一个 new merge-ort strategy)
如果我们找到“两个共同的祖先”怎么办?下面的分支浏览器视图显示了一个替代方案,其中有两个可能的“共同祖先”。
请注意:这个例子有点勉强,因为开发人员从变更集 11 合并到 16 而不是从变更集 15 合并(合并点主分支的最新版本)没有充分的理由)。
但是让我们假设它必须这样做是有原因的,比如说,变更集 11 是稳定的,而 13 和 15 当时还不稳定。
重点是:在 15 和 16 之间没有一个唯一的祖先,而是在相同“距离”的两个祖先:12 和 11。
虽然这种情况不会经常发生,但它确实很可能发生在长期存在的分支或复杂的分支拓扑中。 (上面描述的案例是解决“多祖先”问题的最短案例,但在“交叉”合并之间存在多个变更集和分支时也会发生这种情况。
一种解决方案是“选择”其中一个祖先作为合并的有效祖先(这是 Mercurial 采用的选项),但它有很多缺点。
合并递归如何工作?
当找到多个有效祖先时,递归合并策略将创建一个新的唯一“虚拟祖先”,合并最初找到的那些。
下图描述了算法:
一个新的祖先2将作为“祖先”来合并“src”和“dst”。
“合并递归策略”能够找到比我将在下面描述的“选择两者之一”更好的解决方案。
注意:合并递归策略最初是合并“fredrik”策略(参见commit e4cf17c,2005 年 9 月,Git v0.99.7a),在 Fredrik Kuivinen 之后。
它是python script,在commit 720d150 中发起,它说明了原始算法。
有关详细信息,请参阅“Current Concepts in Version Control Systems from Petr Baudiˇs 2009-09-11”,第 17 页。
|B| = 1 : b(B) = B0
|B| = 2 : b(B) = M(LCA(B0, B1), B0, B1)
M(B, x, y) = ∆−1
(b(B), x ∪ y)
m(x, y) = M(LCA(x, y), x, y)
(是的,我也不知道怎么读)
如果发生冲突,算法的主要思想是在将结果用作进一步合并的基础时简单地保留冲突标记。
这意味着早期的冲突会正确传播,新修订版中的冲突更改也会正确传播。
这里指的是revctrl.org/CrissCrossMerge,它描述了交叉合并中递归合并的上下文。
交叉合并是一个祖先图,其中最小的共同祖先不是唯一的。
最简单的标量示例如下:
a
/ \
b1 c1
|\ /|
| X |
|/ \|
b2 c2
这里可以讲的故事是 Bob 和 Claire 独立进行了一些更改,然后各自将更改合并在一起。
他们发生了冲突,鲍勃(当然)认为他的改变更好,而克莱尔(通常)选择了她的版本。
现在,我们需要再次合并。这应该是冲突。
请注意,文本合并同样会发生这种情况——他们各自编辑了文件中的相同位置,并且在解决冲突时,他们各自选择使生成的文本与其原始版本相同(即,他们不不要以某种方式将两个编辑混为一谈,他们只是选择一个获胜)。
所以:
另一种可能的解决方案是首先将'b1'和'c1'合并到一个临时节点(基本上,想象图中的'X'实际上是一个修订,而不仅仅是边缘交叉)和然后将其用作合并“b2”和“c2”的基础。
有趣的部分是当合并 'b1' 和 'c1' 会导致冲突 - 诀窍是在这种情况下,'X' 包含在内部记录的冲突中(例如,使用经典冲突标记)。
由于 'b2' 和 'c2' 都必须解决相同的冲突,如果他们以相同的方式解决它,他们都以相同的方式从 'X' 中删除冲突并且干净合并结果;如果他们以不同的方式解决它,来自 'X' 的冲突会传播到最终的合并结果。
这就是torek 在"git merge: how did I get a conflict in BASE file?" 中描述的“不对称结果”:
“这些不对称结果是无害的,除了定时炸弹本身以及您后来运行递归合并的事实。
你会看到冲突。由你来解决它 - 再次 - 但这次我们/他们的伎俩并不容易,如果这对 C 和 D 的人有效。”
从revctrl.org/CrissCrossMerge恢复:
如果合并会产生两个以上的碱基('b1'、'c1、'd1'),它们会连续合并 - 首先是 'b1' 和 'c1',然后然后是'd1'的结果。
这就是“Git”的“递归合并”策略所做的。
在 Git 2.29(2020 年第四季度)中,为准备新的合并策略后端,确实很好地描述了冲突和递归合并策略的作用:
(同样,Git 2.30,2020 年第一季度,将有一个 new merge-ort strategy)
见commit 1f3c9ba,commit e8eb99d,commit 2a7c16c,commit 1cb5887,commit 6c74948,commit a1d8b01,commit a0601b2,commit 3df4e3b,commit 3df4e3b,commit 3b6eb15,commit bc29dff,@097654344@,@097654344@ 2020 年)Elijah Newren (newren)。
(由 Junio C Hamano -- gitster -- 合并于 commit 36d225c,2020 年 8 月 19 日)
t6425: 更灵活地重命名/删除冲突消息
签字人:Elijah Newren
首先,有一种称为修改/删除的基本冲突类型,即内容冲突。
当一方删除文件但另一方修改文件时会发生这种情况。
还有一个称为重命名/删除的路径冲突。
当一方删除路径,另一方重命名路径时,会发生这种情况。
这不是内容冲突,而是路径冲突。
但是,它通常会与内容冲突一起发生,即修改/删除。
因此,这两者经常结合在一起。
可能存在的另一种类型的冲突是目录/文件冲突。
例如,一侧在某个路径添加一个新文件,而另一侧历史记录在同一路径添加一个目录。
不过,可以通过重命名将“添加”的路径放在那里。
因此,我们有可能单个路径受到修改/删除、重命名/删除和目录/文件冲突的影响。
在某种程度上,这是合并递归设计的自然副产品。
由于它正在执行四路合并,工作树的内容是它必须考虑的第四个因素,因此它的工作树处理遍布整个代码。
它还通过所有其他类型的冲突进行了目录/文件冲突处理。
这种结构的自然产物是冲突消息,它结合了当前代码路径正在考虑的所有不同类型。
但是,如果我们想使不同的冲突类型相互正交并避免重复自己并获得非常脆弱的代码,那么我们需要将来自这些不同冲突类型的消息分开。
此外,试图确定所有可能的排列是一个皇家混乱。
处理重命名/删除/目录/文件冲突输出的代码已经有些难以解析,而且有些脆弱。
但是如果我们真的想走这条路,那么我们就必须对以下类型的组合进行特殊处理:
-
重命名/添加/删除:在没有重命名给定文件的历史记录一侧,删除文件并在重命名的方式放置一个不相关的文件
-
rename/rename(2to1)/mode conflict/delete/delete:将两个不同的文件,一个可执行一个不执行,重命名到同一个位置,每一方删除对方重命名的源文件
-
rename/rename(1to2)/add/add:在历史记录的每一方都以不同的方式重命名文件,每一方都将一个不相关的文件放在另一方的路上
-
重命名/重命名(1to2)/内容冲突/文件位置/(D/F)/(D/F)/:双方以冲突的方式修改文件,都重命名该文件但路径不同, 一方重命名另一方重命名该文件的目录,使其可能需要传递重命名,并且每一方都在另一方的路径中放置一个目录。
让我们远离这条疯狂的道路,通过允许将冲突消息拆分为不同的类型,允许不同类型的冲突由单独的非重复代码片段处理。 (如果多个冲突类型影响单个路径,则可以顺序打印冲突消息。)从这个路径开始,做一个简单的改变:修改这个测试更灵活,并接受合并后端(递归或新的 ort)将产生的输出.
请注意,Git 2.22(2019 年第二季度)将改进递归合并策略,因为 git merge-recursive” 后端最近(Git 2.18)学习了一种新的启发式方法
根据同一目录中其他文件的方式推断文件移动
搬家了。
由于这本质上不如基于文件本身的内容相似性(而不是基于其邻居正在做什么)的启发式方法稳健,因此有时会产生最终用户意想不到的结果。这已被调低以将重命名的路径留在索引中较高/冲突的阶段,因此
用户可以检查并确认结果。
见commit 8c8e5bd、commit e62d112、commit 6d169fd、commit e0612a1、commit 8daec1d、commit e2d563d、commit c336ab8、commit 3f9c92e、commit e9cd1b5、@9876546359@、commit 043622b987654360@ @、commit e3de888、commit 259ccb6、commit 5ec1e72(2019 年 4 月 5 日)Elijah Newren (newren)。
(由 Junio C Hamano -- gitster -- 合并于 commit 96379f0,2019 年 5 月 8 日) p>
merge-recursive: 切换目录重命名检测默认
当x/a、x/b 和 x/c 全部移至 z/a、z/b 和 z/c 时
分支,有一个问题是,当两个分支合并时,添加到不同分支的x/d 应该保留在x/d 还是出现在z/d。
这里有不同的可能观点:
A) 文件放置在 x/d;它与x/ 中的其他文件无关,因此x/ 中的所有文件都移至一个分支上的z/ 并不重要; x/d 仍应保留在 x/d。
B)x/d与x/中的其他文件相关,x/重命名为z/;因此x/d 应该移动到z/d。
由于之前无法检测目录重命名
Git 2.18,无论上下文如何,用户都能体验到(A)。
选择 (B) 是在 Git 2.18 中实现的,没有返回到 (A) 的选项,并且一直在使用。
但是,一位用户报告说合并结果与他们的预期不符,这使得更改默认值存在问题,尤其是因为目录重命名检测移动文件时没有打印通知。
注意这里还有第三种可能:
C) 根据上下文和内容有不同的答案,Git 无法确定,所以这是一个冲突。
使用索引中更高的阶段来记录冲突并通知用户潜在的问题,而不是默默地为他们选择解决方案。
为用户添加一个选项来指定他们是否使用的偏好
目录重命名检测,默认为(C)。
即使启用了目录重命名检测,也会添加有关文件移入新目录的通知消息。
在 Git 2.31(2021 年第一季度)中,“ORT”合并策略(我 presented here)会影响旧的递归策略。
参见commit c5a6f65、commit e2e9dc0、commit 04af187、commit 43c1dcc、commit 1c7873c、commit 101bc5b、commit 6784574(2020 年 12 月 3 日)Elijah Newren (newren)。
(合并Junio C Hamano -- gitster --commit 85cf82f,2021 年 1 月 6 日)
签字人:Elijah Newren
这里的重点是添加path_msg(),它将有关合并的警告/冲突/通知消息排队以供以后处理,并将这些消息存储在pathname -> strbuf 映射中。
这似乎是一个很大的变化,但实际上只是:
- 用一些 cmets 声明必要的地图
- 数据的初始化和记录
- 在打印/空闲时间迭代地图的一堆代码
- 至少有一个调用者,以避免出现关于未使用函数的错误(我们以实现修改/删除冲突处理的形式提供)。
在这个阶段,我可能不清楚我为什么选择延迟输出处理。
有多种原因:
-
如果合并会覆盖脏更改,则应该中止合并
在工作树中。
在进行重命名检测并且完成对重命名条目的完整处理之前,我们无法正确确定是否会覆盖更改。
警告/冲突/通知消息出现在中间代码路径中,因此除非我们希望在合并将被中止时打印虚假的冲突/警告消息,否则我们需要保存这些消息并仅在相关时打印它们。
-
单个路径可以有多条消息,我们希望给定路径的所有消息一起显示,而不是按冲突/警告类型分组。
这已经是merge-recursive.c 的一个问题,但由于1f3c9ba707 的提交消息中讨论的冲突类型的分裂而变得更加重要(“t6425:更灵活地重命名/删除冲突消息”,2020 -08-10,Git 2.29)
-
某些调用者可能希望避免在某些情况下显示输出,例如最终结果是干净的合并。
变基通常会这样做。
-
一些调用者可能不希望输出到 stdout 甚至 stderr,但可能想要完全用它做其他事情。
例如,git show 或 git log -p 的 --remerge-diff 选项可以即时重新合并并针对重新合并的版本比较合并提交,这将受益于未以标准形式写入 stdout/stderr。