Git-Conflict
最近在工作中频繁使用到了Git
。虽然之前基本把《Pro
Git》读完了,但在实际使用中还是遇到不少问题。
究其原因,这可能因为国内的Git
教程普遍基于命令行,并且只教最基础的使用。而大部分的开发人员往往使用集成在IDE
中的Git
,并且对于Git
背后的原理和设计思路并不了解。
上一篇讲到当未commit
的修改和将要进入的分支有conflict
时,该怎么保存这些修改。这里将进入的分支可以是checkout
到别的分支,或者是将要update
、rebase
、merge
的分支。其中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 |
|
commit
后,新建分支branch1
和branch2
。
同时增删
从直觉上讲,如果两个分支同时对一行代码进行了增加和删除,那么此时必然会发生冲突,因为两个分支上的改动是互相矛盾的,必须决定这些改动的去留。实验也证实了这一点。在实验中,一个分支将first删除,另一个分支在行末添加了.,并触发了conflict
。结果也不会智能地合成The
line.。如果在同一行地不同位置进行了增删都会触发conflict
,那在同一光标就更无疑问地会触发了。
1 |
|
在提到智能地合成The line.,其实默认了一种改动去留的规则,也就是将两个分支的改动都保留。而实际情况,根据对A,B分支改动的去留,共有四种情况。解决冲突的过程本质就是在决定,对于这些发生在同一位置的改动,是都留,都舍弃,还是只保留其中一侧的改动。
同时删除
同时删除不同内容会触发conflict
,而删除相同内容不会触发conflict
,这也很符合直观理解。因为删除相同内容后,两个文件在这个位置地内容是完全相同的,不需要考虑改动的去留。
同时增加
同时增加是有些争议的地方。添加相同内容不会触发冲突,这点原理和删除相同内容一样。但同时添加不同内容Git
会如何处理就有些无法确定,Git
是否会将两部分添加都保留。这种情况非常常见,例如两个人同时在代码文件最末尾添加了新的函数。
1 |
|
两个分支分别在文件末添加了不同的内容,实验结果触发了conflict
。对于多人开发的场景,解决冲突的方式更多是将两个人的修改都保留。
一侧改动,另一侧没改动
上面实验给人的感觉是,如果两个分支的文件内容不同,则发生冲突。但有一种情况两个文件不同时也会发生conflict
。也就是当对一侧(分支)文件没有改动,另一侧没改动时。对此猜测,可能其中一侧的文件快照是另一侧的父/祖节点时(Git底层使用树的结构存储信息),则以更新的子节点为准。
为了验证这个猜测,对没改动的一侧进行了改动并提交,随后再进行一次改动,恢复初始状态并提交。此时原来没改动的一侧因为两次commit
,文件的状态和另一侧不再是父/祖关系。此时合并两个分支,也没触发conflict
。所以否定了两个文件不同时,父/祖关系可以避免conflict
。
并且上面的测试,提交修改一次,再提交返回修改前状态,这也和矛盾最原始的定义有矛盾。即发生了同一文件同一位置的修改,但没conflict
。
基于上述实验,本人猜测Git
并不是对比了两个分支的文件,而是将两个文件和他们的共同祖先状态进行了对比。通过diff
算法,计算了每个分支所做的修改。Git
本身带有diff
工具,但对其内部原理没有深究。通过对比两个分支的修改,如果修改发生在同一位置,那么则触发conflict
。并且IDE
提供的解决冲突的界面,三路合并,其中两路是两个分支的最新状态,另外一路是两个分支的共同祖先。这变相地也证明了对比的可能是两者从相同状态后,发生的差异。由于此部分内容没有查到详细资料,可能存在一定误解。
修改位置初始化
新建一个ConflictTest.txt
文本文件,并初始化为如下。
1 |
|
commit
后,新建分支branch1
和branch2
。
修改位置
修改位置的实验相对简单。对其中一侧进行第一行修改,对另一侧进行第二/三行修改时,发生了不同结果(同一行已在之前测试)。第二行触发了conflict
而第三行没有。如果简单点说,这样可能就把问题解决了,相同位置指的是相邻行。
但往复杂了说,怎么样定义相邻行。原本相邻的行,通过插入换行符,使得其不在相邻,再进行修改,这会引起冲突吗?
1 |
|
答案是会。这可能因为差异是基于原祖先文件计算。一侧变动修改了第1行,另一侧变动修改了第2行,并且1行和2和之间插入了内容(空行)。这样看改动仍然发生在相邻行。
另外还有一种情况。
1 |
|
此时在原来第1行之前、之后插入了内容。从某种程度上也算是相邻行,但没有触发conflict
。所以说相邻行的解释也不够完备。
总结
Git
在合并不同分支时,如果两个分支的同一文件状态不一致,则会计算其与共同祖先的差异。如果差异发生在相对于祖先文件的同一行或相邻行,则会引起conflict
。
在解决冲突时,需要针对每个位置,决定是否舍弃和保留每个分支的改动。