Git 的合并策略是多种纬度的:
- 有我们平时常用的:分支合并
- 以及分支合并的基础:文件合并
- 还有我们查看 Git log tree 时候:Git 节点的合并规则
本小节将全面的手把手教你如何深入理解 Git 的合并原理
-
文件合并策略:三项合并(Three-Way Merge)
在我们讲解 Git 的分支合并策略之前,需要先了解一下 Git 合并两个文件里不同内容的基本原则 —— 三项合并(Three-Way Merge)
当我们合并两个内容不同的文件时,如图 1.1.1,我们文件的内容(Yours)和对方文件的内容(Theirs)不一致,Git 在这个时候需要判断我们合并的最红结果到底是哪个。
暂时无法在飞书文档外展示此内容
然而,如果只把两个文件进行比对,可以得到下面三种情况:
-
Case 1:我们没有对 File 文件进行改动,对方修改了 File 文件内容
-
Case2:对方没有对 File 文件进行改动,我们修改了 File 文件内容
-
Case3:我们都修改了 File 文件内容
显然,当我们只有两个文件的时候,Git 无法判断是以上哪种情况,此时 Git 没办法帮我们做自动合并,所以这个时候我们需要进行——三项合并,所谓三项合并,就是找到需要合并的两个文件的一个基础,以这个基础来判断,到底属于哪种情况。
如图 1.1.2,如果我们的基准文件【Base】的内容是"php is the best language",说明我们没有修改文件,而【Theirs】将文件内容修改为了"java is the language",即上述的 Case 1。 同理如果基准文件的内容为"php is the best language",即为 Case 2
暂时无法在飞书文档外展示此内容
如果【Base】的内容和【Yours】和【Theirs】都不一样的话,说明我们和对方都对该文件同一行进行了修改,这种情况即冲突,需要我们手动去解决,即 Case3
暂时无法在飞书文档外展示此内容
-
分支合并策略
🤔 阅读之前的思考:当我们使用 Git merge 来合并分支的时候,发生了什么 ?
当我们在开发分支Feature开发过程中,使用Git merge将master分支的更新合并到Feature分支后,在Feature分支上会新增一个Commit节点,用于记录这一次的合并动作(图 1.2.1)
暂时无法在飞书文档外展示此内容
🤔 那这个新增的 Commit 节点是根据什么原则生成的呢?在有冲突的情况下又是怎么样的呢?
根据上面的问题,其实 Git 在执行 Git merge 的时候有不同的合并策略,我们平时最常遇到的,也是 Git 默认的策略是—— Recursive 策略
下面我们就来具体看看合并策略具体有哪些,它们的合并规则是怎么样的吧~
一点 Tips:Git 的合并策略指的是 Git merge 过程中可通过-s , --strategy=进行设置所使用的 strategy 的模式
这里的合并策略包括 :⭐️ recursive,resolve, octopus ,ours,subtree 五种策略
⭐️ Recursive
当两个分支有分叉的情况下进行 Git merge就会默认使用—— Recursive 模式,Recursive 模式 是最重要和最常用的策略。
该算法的基本思路是:递归查找两个分支的最短共同祖先节点,然后以此节点作为基准节点进行递归三项合并。下面我们来看看 Recursive 模式下的合并会有哪些情况
Case One 单个最短共同祖先节点
节点内的文字代表当前该 Commit 的文件的内容
最简单也是最常见的情况是:如图 1.2.3,需要合并的两个节点 A,B,它们的共同最短祖先节点是 A,根据三项合并的原理,可以得出结论:节点 B 是新增的修改内容,所以两者合并的结果是 B
暂时无法在飞书文档外展示此内容
Case Two 多个最短共同祖先节点
节点内的文字代表当前该 Commit 的文件同一位置的内容
为方便描述,给节点加上序号以作区分,但是实际内容并不改变
梦想很美好,然而现实却很骨感,并非所有的情况都像 Case one 那样简单
如图 1.2.4,合并节点 C(1) 和节点 B(3),可以看到它们的最短公共祖先有两个:节点 B(1) 和 节点 A(2)
- 如果以节点 B(1)作为基准,节点 C(1) 和节点 B(3) 的合并结果为:B
- 如果以节点 A(2)作为基准,节点 C(1) 和节点 B(3) 的合并结果根据三项合并原理,这里是有冲突的,需要手动进行解决
选择不同的祖先节点作为基准,得到的合并结果不一样,这明显是不合理的,所以我们需要继续递归往前找,直到我们找到唯一的最短公共祖先节点
暂时无法在飞书文档外展示此内容
如图 1.2.5,接下来,我们以节点 B(1) 和 节点 A(2) 为要合并的两个节点,去寻找它们的最短公共节点,由图 1.2.5 可知为 A(1),所以根据三项合并得到节点 B(1) 和 节点 A(2) 合并结果为:B
暂时无法在飞书文档外展示此内容
最后,我们把节点 B(1) 和 节点 A(2)的合并结果:节点 B 作为一个临时节点,以临时节点 B 作为节点 C(1) 和节点 B(3) 最短公共祖先,进行三项合并得到合并结果为 C,至此,我们就完成了我们的Git merge
暂时无法在飞书文档外展示此内容
对于更加复杂的情况,比如在递归寻找最短公共祖先的时候,发现此时的祖先节点仍有多个,那么 Git 会重复上述的操作继续往上找,直到我们找到唯一的祖先节点,然后形成虚拟节点,递归往下合并,最后得到合并结果。
Recursive 模式会尽量帮我们减少冲突,但遇到真的冲突的时候,Git 还是会提示需要我们手动地去解决的~
有趣小知识:在 Git v2.33.0 版本之前,recursive 策略都是 Git 合并的默认策略,之后的默认合并策略改为了 ort 策略(as reflected in its acronym — "Ostensibly Recursive’s Twin")
-
ort 策略修复了递归策略处理次优的极端情况,并且在大型存储库中明显更快—特别是涉及许多重命名的情况
-
ort 策略所有的参数和 recursive 策略一模一样,官方指出,它的出现就是为了替代 recursive 策略😫
Resolve
This can only resolve two heads (i.e. the current branch and another branch you pulled from) using a 3-way merge algorithm. It tries to carefully detect criss-cross merge ambiguities. It does not handle renames.
上面这段描述来自 Git 官网对 Resolve 策略的解释 👉 Git-scm.com/docs/Git-me…
简单的中文解释是:同 Recursive 策略一样,Resolve 策略只能用于合并两个分支(即当前分支和另一个分支),并且使用三项合并算法。它会试图仔细检测十字交叉合并(criss-cross merge)下的分歧,且它不处理重命名。
上面的给人感觉好像什么都说了,又什么都没明白,小小的脑袋充满了大大的疑惑:它到底是如何解决冲突的?它和 Recursive 策略又有什么区别?
下面我们就来看看 Resolve 策略是怎么工作的吧~
Criss-cross
在上文 Recursive 小节中有说道,Recursive 在遇到多个最短公共节点时会把它们合并为一个虚拟节点,再递归往前找到最短公共节点
但是,我们并不是每次都能幸运地继续往前找到新的公共节点的,例如图 1.2.7,可以看到 A(3)和 B(3)进行合并,它们有两个最短公共节点 A(2) 和 B(2),然而我们发现节点 A(2) 和节点 B(2),它们没有唯一共同祖先了
所以这里,如何去处理这种情况,就是 Recursive 策略和 Resolve 策略的不同之处所在!
暂时无法在飞书文档外展示此内容
Recursive 策略选择将两个节点作为合并基础。为了实现这一点,它合并节点 A(2) 和节点 B(2) 为虚拟节点,我们暂且称为 X,X 将作为节点 A(3) 和节点 B(3) 的合并基础。
Resolve 策略,它将随机地从节点 A(2) 和节点 B(2) 中选择一个,作为合并基础。具体选择哪个节点取决于所使用的算法,该算法没有指定,并且可能会随着 Git 版本的不同而变化。
暂时无法在飞书文档外展示此内容
其他策略
Octopus
这种合并方式用于两个以上的分支,但是在遇到冲突需要手动合并时会拒绝合并。这种合并方式更适合于将多个分支捆绑在一起的情况,也是多分支合并的默认合并策略。
ours
如果不冲突,那么与默认的合并方式相同。但如果发生冲突,将自动应用自己这一方的修改。
Subtree
此策略使用的是修改后的递归三路合并算法。与 recursive 不同的是,此策略会将合并的两个分支的其中一个视为另一个的子树,就像 Git subtree 中使用的子树一样。
-
Head 合并策略
Fast-Forward
在两个分支不存在分叉的情况下,
Git merge会默认使用最简单的合并策略 —— Fast-Forward 模式即分支会直接吧当前 Head 指针指向所 merge 的分支的最新 Commit 节点
暂时无法在飞书文档外展示此内容
Rebase
Git rebase也是一种经常被用来做合并的方法,其与Git merge的最大区别是,他会更改变更历史对应的 commit 节点。暂时无法在飞书文档外展示此内容
如图 1.2.3,
Feature分支在从master分支切出去之后新增了两个 commit 节点commit-feature-1和commit-feature-2,master 分支也新增了两个 commit 节点。此时如果在
Feature分支上进行Git rebase master后,Git 会以 master 分支为基础,新增commit-feature-1*和commit-feature-2*代替Feature分支中的commit-feature-1和commit-feature-2两个节点有趣小知识:
Feature分支新增两个全新的 commit 节点的 commitID 和之前的 commit 节点的 commitID 是不一样的,原因是因为:新的 commit 指向的 parent 变了,所以对应的 SHA1 值也会改变,所以没办法复用原Feature分支中的 commit具体原理可见 👉 www.lzane.com/tech/Git-in…