git merge原理(递归三路合并算法)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了git merge原理(递归三路合并算法)相关的知识,希望对你有一定的参考价值。

参考技术A 我们知道git 合并文件是以行为单位进行一行一行进行合并的,但是有些时候并不是两行内容不一样git就会报冲突,因为smart git 会帮我们自动帮我们进行取舍,分析出那个结果才是我们所期望的,如果smart git 都无法进行取舍时候才会报冲突,这个时候才需要我们进行人工干预。那git 是如何帮我们进行Smart 操作的呢?

Mine 代表你本地修改...
Theirs 代表其他人修改...
假设对于同一个文件,出现你和其他人一起修改,此时如果git来进行合并,git就懵逼了,因为Git既不敢得罪你(Mine),也不能得罪他们(Theirs) 无理无据,git只能让你自己搞了,但是这种情况太多了而且也没有必要…

三路合并就是先找出一个基准,然后以基准为Base 进行合并,如果2个文件相对基准(base)都发生了改变 那git 就报冲突,然后让你人工决断。否则,git将取相对于基准(base)变化的那个为最终结果。

下面面的合并就是我们常见的分支graph,结合具体分析:

git merge-base –all commit_id1(Yours/Theirs) commit_id2(Yours/Theirs) 就能找出公共祖先的commitId(Base)

图虽然复杂 但是核心原理是不变的,下面我们看 另外一个稍微高级一点的核心原理”递归三路合并” 也是我们很常见看到 git merge 输出的 recursive strategy

简短描述下 如何会出现上面的图:

从上面我们DAG图可以知道公共祖先有③和④,那到底选择哪个呢,我们分别来看:

最终期待的结果是什么?

所以 我们的最佳公共祖先应该是4,最终结果应该是 /foo.c = C
git 如何选择公共祖先呢?

你可能会说用 git merge-base ⑥ ⑦ 输出的是 ④ 但是git 就真的是用 ④ 做祖先吗 ?答案是No

从git的解释中,我们就知道 如果有2个都是最佳公共祖先时候,这个时候git 会随便输出一个不确定公共祖先。

git 是这样进行合并的:

当项目中包含多条功能分支时,有时就需要使用 git merge 命令,指定将某个分支的提交合并到当前分支。Git 中有两个合并策略:fast-forward 和 no-fast-forward。

当merge ② 和 ⑥时候 由于②是公共祖先,所以进行Fast-Forward 合并,直接指向⑥ 不用生成一个新的⑧进行merge了。

no-fast-forward(--no-ff)意为非快进式合并,fast-forward的场景很少遇到,基本是:在当前分支分离出子分支后(比如分支dev),后续会有其他分支合并进来的修改,而分离出的dev分支也做了修改。这个时候再使用git merge,就会触发 no-fast-forward 策略了。

看懂下面这个例子你就明白了:

现在 f 提交是我们正在合并的提交

如果现在找 e 和 d 的共同祖先,你会发现并不唯一,b 和 c 都是。那么此时怎么合并呢?

git 会首先将 b 和 c 合并成一个虚拟的提交 x,这个 x 当作 e 和 d 的共同祖先。

而要合并 b 和 c,也需要进行同样的操作,即找到一个共同的祖先 a。

我们这里的 a、b、c 只是个比较简单的例子,实际上提交树往往更加复杂,这就需要不断重复以上操作以便找到一个真实存在的共同祖先,而这个操作是递归的。这便是“递归三路合并”的含义。

这是 git 合并时默认采用的策略。

git官方文档
git merge原理
git merge的合并原理

你啥时候会使用不同的 git 合并策略?

【中文标题】你啥时候会使用不同的 git 合并策略?【英文标题】:When would you use the different git merge strategies?你什么时候会使用不同的 git 合并策略? 【发布时间】:2010-09-26 21:09:54 【问题描述】:

从 git-merge 的手册页中,您可以使用许多合并策略。

解决 - 这只能使用 3 路合并算法解决两个头(即当前分支和您从中提取的另一个分支)。它试图仔细检测交叉合并歧义,通常被认为是安全和快速的。

递归 - 这只能使用 3-way 合并算法解决两个头。当有多个共同祖先可用于三向合并时,它会创建共同祖先的合并树并将其用作三向合并的参考树。据报道,通过对取自 Linux 2.6 内核开发历史的实际合并提交进行的测试,这可以减少合并冲突,而不会导致错误合并。此外,这可以检测和处理涉及重命名的合并。这是拉取或合并一个分支时的默认合并策略。

章鱼 - 这解决了两个以上的情况,但拒绝进行需要手动解决的复杂合并。它主要用于将主题分支头捆绑在一起。这是拉取或合并多个分支时的默认合并策略。

我们的 - 这解决了任意数量的头,但合并的结果始终是当前分支头。它旨在用于取代旧的分支开发历史。

子树 - 这是一种修改后的递归策略。合并树 A 和 B 时,如果 B 对应 A 的子树,则首先调整 B 以匹配 A 的树结构,而不是读取同级树。对共同祖先树也进行了这种调整。

我什么时候应该指定不同于默认值的东西?各自最适合哪些场景?

【问题讨论】:

【参考方案1】:

在 Git 2.30(2021 年第一季度)中,将有一个合并策略:ORT(“Ostensibly Recursive's Twin”)。

git merge -s ort

这来自 Elijah Newren 的 this thread:

现在,我称它为“表面上递归的双胞胎”,或简称为“ort”。 > 起初,人们不应该注意到它与当前的递归策略之间有任何区别,除了我认为我可以让它更快一点(尤其是对于大型 repos)。

但它应该允许我修复一些在当前设计中更难处理的(不可否认的极端情况)错误,我认为不涉及 $GIT_WORK_TREE$GIT_INDEX_FILE 的合并将带来一些乐趣新功能。 无论如何,这就是希望。

Problem:

在理想的世界里,我们应该:

要求unpack_trees() 在没有“-u”的情况下执行“read-tree -m”;

在内核中执行所有合并递归计算并准备 结果索引,同时保持当前索引不变;

比较当前的in-core index和生成的in-core index,注意工作树中需要添加、更新或删除的路径,并保证反映变化时不丢失信息到工作树; 例如。结果想要创建一个文件,其中工作树当前有一个包含不可消耗内容的目录,结果想要删除工作树文件具有本地修改的文件等; 然后终于

执行工作树更新,使其与生成的核心内索引的外观相匹配。

结果:

请参阅commit 14c4586(2020 年 11 月 2 日)、commit fe1a21d(2020 年 10 月 29 日)和 commit 47b1e89、commit 17e5574(2020 年 10 月 27 日)Elijah Newren (newren)。(由 @ 合并987654328@commit a1f9595,2020 年 11 月 18 日)

merge-ort: 空实现的新合并策略的准系统 API

签字人:Elijah Newren

这是新合并策略的开始。

虽然存在一些 API 差异,并且实现在行为上也存在一些差异,但它本质上是作为 merge-recursive.c 的最终替代品。

但是,它被构建为与合并递归并存,因此我们有足够的时间来了解这些差异在现实世界中是如何表现出来的,而人们仍然可以退回到合并递归。 (另外,我打算避免在此过程中修改合并递归,以保持稳定。)

这里值得注意的主要区别是工作树和索引的更新不是与合并算法同时完成的,而是一个单独的后处理步骤。 新的 API 旨在让人们可以进行重复合并(例如,在 rebase 或cherry-pick 期间)并且只在最后更新一次索引和工作树,而不是用每个中间结果更新它。

此外,可以在两个分支之间执行合并,这两个分支都不匹配索引或工作树,而不会破坏索引或工作树。

还有:

参见commit 848a856、commit fd15863、commit 23bef2e、commit c8c35f6、commit c12d1f2、commit 727c75b、commit 489c85f、commit ef52778、commit ef52778、commit f06481f(2020 年 10 月 26 日)@9876。 br>(由 Junio C Hamano -- gitster -- 合并于 commit 66c62ea,2020 年 11 月 18 日)

t6423, t6436:注意改进了脏文件的 ort 处理

签字人:Elijah Newren

“递归”后端依赖unpack_trees() 来检查未暂存的更改是否会被合并覆盖,但unpack_trees() 不理解重命名——一旦它返回,它已经向工作树写入了许多更新和索引。 因此,“递归”必须进行特殊的 4 路合并,它还需要将工作副本视为额外的差异来源,我们必须小心避免覆盖并导致将文件移动到新位置以避免冲突。

相比之下,“ort”后端在内存中进行完整的合并,并且仅在后处理步骤中更新索引和工作副本。 如果途中有脏文件,它可以简单地中止合并。

t6423:期望在 ort 后端改进冲突标记标签

签字人:Elijah Newren

冲突标记带有额外的表单注释 REF-OR-COMMIT:文件名 帮助区分内容的来源,如果 :FILENAME 部分在历史的两边都相同,则省略(因此只有内容冲突的重命名才会带有该部分注释)。

但是,由于合并递归的every-codepath-needs-a-copy-of-all-special-case-code 格式,:FILENAME 注释被意外遗漏了。

t6404, t6423: 期望在 ort 后端改进重命名/删除处理

签字人:Elijah Newren

当一个文件被重命名并且有内容冲突时,merge-recursive 在索引中没有旧文件名的一些阶段和新文件名的一些阶段;相反,它将与旧文件名对应的所有阶段复制到新文件名的相应位置,这样就有三个更高阶的阶段都对应于新文件名。

这样操作让用户更容易访问不同的版本并解决冲突(也无需手动'git rm'(man)旧版本作为 'git add'(man) 新的)。

重命名/删除应该以类似方式处理——重命名文件应该有两个阶段,而不仅仅是一个阶段。 我们现在不想破坏合并递归,因此更新相关测试以根据是否使用“recursive”或“ort”合并策略具有不同的期望。


使用 Git 2.30(2021 年第一季度),为新的合并策略做准备。

参见commit 848a856、commit fd15863、commit 23bef2e、commit c8c35f6、commit c12d1f2、commit 727c75b、commit 489c85f、commit ef52778、commit f06481f(2020 年 10 月 26 日)@9876。 br>(由 Junio C Hamano -- gitster -- 合并于 commit 66c62ea,2020 年 11 月 18 日)

merge tests: 期望在 ort 中改进目录/文件冲突处理

签字人:Elijah Newren

merge-recursive.c 是建立在运行unpack_trees() 然后“做一些小的修饰”以获得结果的想法之上的。 不幸的是,unpack_trees() 以“即用即更新”模式运行,导致merge-recursive.c 效仿并最终立即进行评估和“即用即修复”设计。

目录/文件冲突等某些事情在索引数据结构中无法很好地表示,需要特殊的额外代码来处理。 但是后来发现目录/文件冲突也可能涉及重命名/删除冲突时,不得不将特殊的目录/文件冲突处理代码复制到重命名/删除代码路径中。 ...然后必须复制它以进行修改/删除,以及重命名/重命名(1to2)冲突,...但它仍然遗漏了一些。 此外,当发现还存在文件/子模块冲突和子模块/目录冲突时,我们需要将特殊子模块处理代码复制到整个代码库中的所有特殊情况。

然后发现我们对目录/文件冲突的处理不是最理想的,因为它会创建未跟踪的文件来存储冲突文件的内容,如果有人要运行“git merge --abort”,这些文件将不会被清理(man) 或 'git rebase --abort'(man)

考虑到索引中的目录/文件冲突,尝试添加或删除与这些文件对应的索引条目也是困难或可怕的。 但是更改 merge-recursive.c 以正确处理这些问题是一件非常痛苦的事情,因为代码中有很多站点具有相似但不相同的代码来处理目录/文件/子模块冲突,都需要更新。

我努力通过单个代码路径在 merge-ort 中推送所有目录/文件/子模块冲突处理,并避免创建未跟踪的文件来存储跟踪的内容(它确实在备用路径记录内容,但确保它们具有更高- 索引中的阶数)。


使用 Git 2.31(2021 年第一季度),“正确完成”的合并后端开始出现。 示例:

见commit 6d37ca2(2020 年 11 月 11 日)Junio C Hamano (gitster)。 见commit 89422d2,commit ef2b369,commit 70912f6,commit 6681ce5,commit 9fefce6,commit bb470f4,commit ee4012d,commit a9945bb,commit 8adffaa,commit 6a02dd9,commit 6a02dd9,commit 6a02dd9,commit 6a02dd9,@98765 987654385@、commit 885f006、commit d2bc199、commit 0c0d705、commit c801717、commit e4171b1、commit 231e2dd、commit 5b59c3d(2020 年 12 月 13 日)Elijah Newren (newren)。( 987654394@commit f9d29da,2021 年 1 月 6 日)

merge-ort:添加record_conflicted_index_entries()的实现

签字人:Elijah Newren

checkout()之后,工作树有相应的内容,索引与工作副本匹配。 这意味着所有未修改和干净合并的文件都有正确的索引条目,但需要更新冲突的条目。

我们通过遍历冲突的条目来做到这一点,用CE_REMOVE标记路径的现有索引条目,在索引末尾为路径添加新的高阶暂存(忽略正常的索引排序顺序),然​​后在循环结束时删除 CE_REMOVED-marked 缓存条目并对索引进行排序。


在 Git 2.31(2021 年第一季度)中,重命名检测被添加到“ORT”合并策略中。

请参阅commit 6fcccbd、commit f1665e6、commit 35e47e3、commit 2e91ddd、commit 53e88a0、commit af1e56c(2020 年 12 月 15 日)和commit c2d267d、commit c2d267d、commit 965a7bc、@98765406@、@98765406@ commit 864075e(2020 年 12 月 14 日)Elijah Newren (newren)。(由 Junio C Hamano -- gitster -- 合并,commit 2856089,2021 年 1 月 25 日)

例子:

merge-ort: 添加正常重命名处理的实现

签字人:Elijah Newren

实现对正常重命名的处理。 此代码替换 merge-recurisve.c 中的以下代码:

process_renames() 中与RENAME_NORMAL 相关的代码 RENAME_NORMAL 的情况是process_entry()

此外,merge-recursive.c 中的一些共享代码用于多个不同的重命名案例,我们将不再需要这些案例(或其他重命名案例):

handle_rename_normal() setup_rename_conflict_info()

通过更改设计将四个单独的代码路径合并为一个成为可能:process_renames() 调整了opt->priv->paths 中的conflict_info 条目,以便process_entry() 可以处理所有非重命名冲突类型(目录/file,修改/删除等)正交。

这意味着我们不太可能错过某种冲突类型组合的特殊实现(请参阅66c62ea 带来的提交(“Merge branch 'en/merge-tests'”,2020-11-18 , Git v2.30.0-rc0 -- merge 在batch #6 中列出),尤其是commit ef52778(“合并测试:期望在 ort 中改进目录/文件冲突处理”,2020-10-26,Git v2.30.0- rc0 -- merge 在batch #6 中列出)了解更多详细信息)。

加上让工作树/索引更新在merge_switch_to_result() 函数中正交处理,大大简化了各种特殊重命名情况的代码。

(公平地说,处理正常重命名的代码之前并没有那么复杂,但现在它仍然简单得多。)

而且,仍然使用 Git 2.31(2021 年第一季度),使用 Git 2.31(2021 年第一季度),oRT 合并策略学习了对合并冲突的更多支持。

见commit 4ef88fc、commit 4204cd5、commit 70f19c7、commit c73cda7、commit f591c47、commit 62fdec1、commit 991bbdc、commit 5a1a1e8、commit 5a1a1e8、commit 23366d2、commit 0ccfa4e@@ (01 Jan) 987654429@.(由 Junio C Hamano -- gitster -- 合并于 commit b65b9ff,2021 年 2 月 5 日)

merge-ort:在同一路径添加不同类型文件的处理

签字人:Elijah Newren

添加一些明确考虑以下类型冲突的处理:

文件/子模块 文件/符号链接 submodule/symlink> 将它们作为冲突留在同一路径上对用户来说很难解决,因此将它们中的一个或两个移到一边,以便它们各自获得自己的路径。

请注意,在递归处理的情况下(即call_depth > 0),我们可以只使用两个合并基的合并基作为合并结果,就像我们对修改/删除冲突、二进制文件、冲突的子模块值,等等。

【讨论】:

ORT 会取代“递归”作为默认的合并策略吗? @UndefinedBehavior 正如答案中的第一条提交消息所暗示的那样,是的,它将最终取代递归的。相同的提交消息表明,目前,它与“递归”并存。 @UndefinedBehavior 我已经编辑了答案以添加 git mailing-list 线程,该线程说明了这种新合并策略的起源。 @UndefinedBehavior 是的,在 git 2.34 中 @UndefinedBehavior 确实如此。被释放......下周一! (2021 年 11 月 15 日)【参考方案2】:

我不熟悉resolve,但我用过其他的:

递归

递归是非快进合并的默认值。我们都熟悉那个。

八达通

当我有几棵树需要合并时,我使用了章鱼。您可以在大型项目中看到这一点,其中许多分支机构都进行了独立开发,并且已经准备好整合到一个单一的头部。

章鱼分支在一次提交中合并多个头,只要它可以干净地完成。

为了说明,假设您有一个项目,该项目有一个 master,然后要合并三个分支(称为 a、b 和 c)。

一系列递归合并看起来像这样(注意第一次合并是快进,因为我没有强制递归):

但是,单个章鱼合并看起来像这样:

commit ae632e99ba0ccd0e9e06d09e8647659220d043b9
Merge: f51262e... c9ce629... aa0f25d...

我们的

我们的 == 我想引入另一个头部,但丢弃头部引入的所有变化。

这会保留分支的历史记录,而不会受到分支的任何影响。

(阅读:甚至没有查看这些分支之间的更改。分支只是合并,对文件没有做任何事情。如果您想在另一个分支中合并,并且每次出现“我们的文件”的问题版本或他们的版本”你可以使用git merge -X ours)

子树

当您想将另一个项目合并到当前项目的子目录中时,子树很有用。当您有一个不想包含为子模块的库时很有用。

【讨论】:

那么 Ocotopus 唯一真正的优势就是减少了树中合并提交的数量? 你不需要指定octopus合并策略:如果你合并两个以上的分支(git merge A B ...),它会自动使用。 很抱歉跑题了,但是您制作这些屏幕截图的工具是什么?它看起来像一个非常棒/漂亮的分支历史可视化...... gitg 适用于 linux 环境。 这个-X ours 的提示太棒了,为我节省了一个小时的工作时间。【参考方案3】:

“解决”与“递归”合并策略

递归是当前默认的双头策略,但经过一番搜索,我终于找到了一些关于“解决”合并策略的信息。

取自 O'Reilly 的书 Version Control with Git (Amazon)(释义):

最初,“解决”是 Git 合并的默认策略。

在交叉合并的情况下,有多个可能的合并基础,解决策略的工作原理如下:选择一个可能的合并基础,并希望最好。这实际上并不像听起来那么糟糕。事实证明,用户一直在处理代码的不同部分。在这种情况下,Git 检测到它正在重新合并一些已经存在的更改并跳过重复的更改,从而避免冲突。或者,如果这些细微的更改确实会导致冲突,那么至少开发人员应该可以轻松处理冲突。

我已经使用默认递归策略失败的“resolve”成功合并了树。我遇到了fatal: git write-tree failed to write a tree 错误,感谢this blog post (mirror) 我尝试了“-s resolve”,它奏效了。我仍然不确定为什么......但我认为这是因为我在两棵树中都有重复的更改,并正确解决了“跳过”它们。

【讨论】:

我正在使用 3 路合并 (p4merge),当递归合并失败时,我将冲突写入 .BASE 文件。在这种情况下,回退到解决策略会有所帮助。 This blog post link cited in the answer 现已损坏。【参考方案4】:

实际上,如果您想放弃分支带来的更改,但将分支保留在历史记录中,那么您想要选择的唯一两种策略是 我们的,如果您想选择 subtree正在将独立项目合并到超级项目的子目录中(如“git”存储库中的“git-gui”)。

octopus 合并两个以上的分支时会自动使用合并。 resolve 在这里主要是出于历史原因,以及当您遇到 recursive 合并策略极端情况时。

【讨论】:

对于出现致命 git-write-tree 错误的双头合并,我必须选择“解决”而不是默认的“递归”。 “解决”策略干净利落地合并。这可能与在要合并的分支中移动大量文件有关。 @thaddeusmt:很有趣。如果可能的话,您能否将有关“递归”合并策略失败的错误报告发布到 git 邮件列表?提前致谢。 @JakubNarębski 我不确定如何收集足够的信息来提交有意义的错误报告,我是 Git 的 n00b,抱歉。正如我在此处的回答 (***.com/a/10636464/164439) 中提到的那样,我的 猜测 与我在两个分支中复制更改有关,而“解决”在跳过重复的更改方面做得更好。跨度> @JakubNarębski 现在你也可以选择theirs,根据手册“与ours相反。他们的 i> 不会自动为您选择。您可以稍微更新一下您的 anwser,添加 他们的 选项 @SebTu:没有theirs合并策略(即--strategy=theirs),但有theirs选项默认recursive合并策略(即--strategy=recursive --strategy-option=theirs,或只是-Xtheirs)。

以上是关于git merge原理(递归三路合并算法)的主要内容,如果未能解决你的问题,请参考以下文章

你啥时候会使用不同的 git 合并策略?

变基与合并git rebase git merge

git IDEA的分支合并,merge和rebase的区别

git IDEA的分支合并,merge和rebase的区别

git IDEA的分支合并,merge和rebase的区别

git merge 和 git rebase 小结(转)