Git-Conflict

最近在工作中频繁使用到了Git。虽然之前基本把《Pro Git》读完了,但在实际使用中还是遇到不少问题。

究其原因,这可能因为国内的Git教程普遍基于命令行,并且只教最基础的使用。而大部分的开发人员往往使用集成在IDE中的Git,并且对于Git背后的原理和设计思路并不了解。

上一篇讲到当未commit的修改和将要进入的分支有conflict时,该怎么保存这些修改。这里将进入的分支可以是checkout到别的分支,或者是将要updaterebasemerge的分支。其中checkout引起的矛盾只有IDE才会发生,原生的命令行Git在那种情况下根本不允许checkout。但未详细说明什么是conflict,以及什么情况下会发生。

Git官方文档中,对于冲突只有一句简单的解释。

If you changed the same part of the same file differently in the two branches you’re merging, Git won’t be able to merge them cleanly.

冲突发生在修改同一个文件同一个位置。同一个文件好理解,但修改是指什么,同一个位置又指什么?由于这部分内容实在未找到详细的文档,只能通过实验测试,来验证这两个到底指的什么意思。

实验

实验设置很简单,初始化一个空的Git库,并在其中建立一个txt文本文件,通过进行不同的位置,不同类型的修改,观察是否触发conflict,来尝试找出其中规律。

修改的类型,主要是增加和删除两种,修改可以看作是删除和修改同时进行。

同一个位置,可以是同一光标,同一行,相邻行,或者间隔更大的行。

修改类型初始化设置

基于此,新建一个ConflictTest.txt文本文件,并初始化为如下。

1
The first line

commit后,新建分支branch1branch2

同时增删

从直觉上讲,如果两个分支同时对一行代码进行了增加和删除,那么此时必然会发生冲突,因为两个分支上的改动是互相矛盾的,必须决定这些改动的去留。实验也证实了这一点。在实验中,一个分支将first删除,另一个分支在行末添加了.,并触发了conflict。结果也不会智能地合成The line.。如果在同一行地不同位置进行了增删都会触发conflict,那在同一光标就更无疑问地会触发了。

1
2
3
The line
===============
The first line.

在提到智能地合成The line.,其实默认了一种改动去留的规则,也就是将两个分支的改动都保留。而实际情况,根据对A,B分支改动的去留,共有四种情况。解决冲突的过程本质就是在决定,对于这些发生在同一位置的改动,是都留,都舍弃,还是只保留其中一侧的改动。

同时删除

同时删除不同内容会触发conflict,而删除相同内容不会触发conflict,这也很符合直观理解。因为删除相同内容后,两个文件在这个位置地内容是完全相同的,不需要考虑改动的去留。

同时增加

同时增加是有些争议的地方。添加相同内容不会触发冲突,这点原理和删除相同内容一样。但同时添加不同内容Git会如何处理就有些无法确定,Git是否会将两部分添加都保留。这种情况非常常见,例如两个人同时在代码文件最末尾添加了新的函数。

1
2
3
4
5
The first line
The second line 1
===============
The first line
The second line 2

两个分支分别在文件末添加了不同的内容,实验结果触发了conflict。对于多人开发的场景,解决冲突的方式更多是将两个人的修改都保留。

一侧改动,另一侧没改动

上面实验给人的感觉是,如果两个分支的文件内容不同,则发生冲突。但有一种情况两个文件不同时也会发生conflict。也就是当对一侧(分支)文件没有改动,另一侧没改动时。对此猜测,可能其中一侧的文件快照是另一侧的父/祖节点时(Git底层使用树的结构存储信息),则以更新的子节点为准。

为了验证这个猜测,对没改动的一侧进行了改动并提交,随后再进行一次改动,恢复初始状态并提交。此时原来没改动的一侧因为两次commit,文件的状态和另一侧不再是父/祖关系。此时合并两个分支,也没触发conflict。所以否定了两个文件不同时,父/祖关系可以避免conflict

并且上面的测试,提交修改一次,再提交返回修改前状态,这也和矛盾最原始的定义有矛盾。即发生了同一文件同一位置的修改,但没conflict

基于上述实验,本人猜测Git并不是对比了两个分支的文件,而是将两个文件和他们的共同祖先状态进行了对比。通过diff算法,计算了每个分支所做的修改。Git本身带有diff工具,但对其内部原理没有深究。通过对比两个分支的修改,如果修改发生在同一位置,那么则触发conflict。并且IDE提供的解决冲突的界面,三路合并,其中两路是两个分支的最新状态,另外一路是两个分支的共同祖先。这变相地也证明了对比的可能是两者从相同状态后,发生的差异。由于此部分内容没有查到详细资料,可能存在一定误解。

修改位置初始化

新建一个ConflictTest.txt文本文件,并初始化为如下。

1
2
3
The first line
The second line
The third line

commit后,新建分支branch1branch2

修改位置

修改位置的实验相对简单。对其中一侧进行第一行修改,对另一侧进行第二/三行修改时,发生了不同结果(同一行已在之前测试)。第二行触发了conflict而第三行没有。如果简单点说,这样可能就把问题解决了,相同位置指的是相邻行。

但往复杂了说,怎么样定义相邻行。原本相邻的行,通过插入换行符,使得其不在相邻,再进行修改,这会引起冲突吗?

1
2
3
4
5
6
7
8
9
The first line 1
The second line
The third line
===============
The first line


The second line 2
The third line

答案是会。这可能因为差异是基于原祖先文件计算。一侧变动修改了第1行,另一侧变动修改了第2行,并且1行和2和之间插入了内容(空行)。这样看改动仍然发生在相邻行。

另外还有一种情况。

1
2
3
4
5
6
7
8
9
branch1
The first line
The second line
The third line
===============
The first line
branch2
The second line
The third line

此时在原来第1行之前、之后插入了内容。从某种程度上也算是相邻行,但没有触发conflict。所以说相邻行的解释也不够完备。

总结

Git在合并不同分支时,如果两个分支的同一文件状态不一致,则会计算其与共同祖先的差异。如果差异发生在相对于祖先文件的同一行或相邻行,则会引起conflict

在解决冲突时,需要针对每个位置,决定是否舍弃和保留每个分支的改动。


Git-Conflict
http://dracoyus.github.io/2023/04/27/Git-Conflict/
作者
DracoYu
发布于
2023年4月27日
许可协议