三路合并 —— Git 学习笔记 17
三路合并
和其他版本控制系统不同,Git 提供的合并冲突解决方案并不会聪明过头,不会尝试自动将所有问题都解决。Git 的设计哲学是智能判断一个合并是否可以非常容易地自动完成,如果自动化方案不可行,就不要自作聪明地去尝试解决它,最好让用户自己去解决合并冲突。
文件层面
通过比较共同的祖先(base)、当前分支(ours),待合并分支(theirs),Git 采用三路合并算法生成合并结果。该算法至少在文件目录层面非常简单。其规则如下:
祖先(base) | HEAD(ours) | 分支(theirs) | 结果 | 说明 |
---|---|---|---|---|
A | A | A | A | |
A | A | B | B | 如果一方修改了某个文件,选择修改版的文件 |
A | B | A | B | 如果一方修改了某个文件,选择修改版的文件 |
A | B | B | B | 如果双方拥有相同的变更,选择修改过的版本 |
A | B | C | merge | 如果一方包含与另一方不一样的变更,那么在内容层面存在合并冲突 |
内容层面
如果是最后一行的情况,那么 Git 就会在内容上尝试合并。我们举个例子,假设提交图如下:
B-C-D master(*)
\
E-F dev
目前所在分支是 master,当尝试把 dev 合并入 master 分支时,base 是提交 B,ours 是提交 D,theirs 是提交 F.
B、D、F 三个提交都有 foo.c 这个文件,它们的内容分别如下:
请问 Git 会怎么合并 ours 和 theirs 呢?
当然需要参考他们的共同祖先 base, 下图标出了ours 和 theirs 分别与祖先不同的地方(用下划线及蓝色背景表示)。
祖先(base) | HEAD(ours) | 分支(theirs) | 结果 | 说明 |
---|---|---|---|---|
A | A | A | A | |
A | A | B | B | 如果一方修改了某一行,那么这一行选择修改版的 |
A | B | A | B | 同上 |
A | B | B | B | 如果某一行双方拥有相同的变更,则选择修改过的行 |
A | B | C | conflict | 如果某一行双方都修改了,且修改得不一样,则报告冲突,需要用户解决 |
根据上表的规则,合并过程类似这样:
可以看到,第四行,双方都修改了,且各自修改的内容不一样,所以 Git 不知道怎么解决,所以就把问题抛给用户了。
递归三路合并(Recursive three-way merge)
三路合并算法的基础是找到被合并文件的共同祖先文件。在遇到十字交叉合并(criss-cross merge)时,不存在独一无二的最小共同祖先。如下图所示,merge D 和 F 时,发现 C 和 E 都是它们的公共祖先,这可怎么办,C 和 E 哪一个作为 Base 呢?
B--C--D master
\ \/
\ /\
E--F dev
Git 的策略是:先合并 C 和 E 得到一个虚拟的公共祖先 G,把这个 G 作为 Base. 那如果合并 C 和 E 的时候发现他们的公共祖先也不止一个怎么办?所以就要递归进行了。
如下图:合并 M 和 N,发现他们的共同祖先有2个——L 和 K,于是合并 L 和 K,又发现他们的祖先不止一个——I 和 J,于是合并 I 和 J,就这样递归下去。
B--C---D--I--L--M master
\ \/ \/
\ /\ /\
E---F--J--K--N dev
参考资料
【1】《Git 高手之路》,人民邮电出版社
【2】https://*.com/questions/4129049/why-is-a-3-way-merge-advantageous-over-a-2-way-merge