什么是合并冲突(merge conflict)
想象你和小王都是同一个开源项目的贡献者,这一天你拉取了远程仓库的最新版本,但是git却报出了一个错误:
自动合并 xxx
冲突(内容):合并冲突于xxx
自动合并失败,修正冲突然后提交修正的结果。
你仔细一看,原来是你上一次commit时的“utils.java”的实现被小王重构并且优化了一下,并且小王也将这个“utils.java”给push到了远程仓库。这样,远程仓库的“utils.java”的内容,和你自己本地分支最新commit的“utils.java”的内容出现了不一致。git的merge能自行解决一部分差异,但另一部分相同位置出现的不同实现就无法被git的自动merge解决了,因为这个时候git就不知道,你是想要保留自己的,还是要保留小王的版本,于是就这样git报出了合并冲突的错误。
所谓“**合并冲突(merge conflict)**”,便是git在合并两个分支的时候,出现了对同一处位置的不同内容。大部分时候git都能自动完成merge工作,但一旦出现上述的同一位置的不同内容的情况,git就会“犯难”,它并不知道该保留哪一处分支的内容,此时就会出现“分支冲突”的错误,并把冲突的解决交给用户。
一个简单的实例
现在我们来看一个简单的合并冲突的实例。
比如我现在想要布置一个“实现斐波那契数列第n项求解”的任务,我在一开始就初始化了一个git仓库,并且在默认的分支master上给出了代码的基本框架并且提交了一个commit:
新建工作目录,初始化git仓库
1
2
3mkdir merge-conflict-demo # 在当前目录新建一个名为“merge-conflict-demo”的目录
cd merge-conflict-demo # 进入这个目录
git init. # 在这个进入的目录下初始化一个git仓库给出“求解斐波那契数列第n项”的基本代码框架
1
touch fibonacci.cpp # 新建一个cpp源文件用来求解斐波那契数列的第n项
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 求解斐波那契数列第n项的代码框架
long long fib(int n) {
// To be implemented.
}
int main() {
std::cout << "请输入一个正整数:";
int n;
std::cin >> n;
std::cout << "Fibonacci(" << n << ") = " << fib(n) << std::endl;
return 0;
}提交代码
1
2git add fibonacci.cpp
git commit -m "Provide skeleton of Fibonacci series evaluation by cpp"
这样子git的仓库就有包含代码框架的commit了。现在比方说有两个学生小王和小李,他们接到的任务就是函数long long fib(int n)
的具体实现。
小王说,这还不简单,我根据定义写个递归就行,于是他:
基于初始commit创建了自己的分支,并且进行了fib的递归实现
1
git checkout -b wang-implementation
1
2
3
4
5
6
7long long fib(int n) {
// 小王的递归实现
if (n == 0 || n == 1)
return 1;
else
return fib(n - 1) + fib(n - 2);
}简单测试并通过了样例以后,小王提交了代码
1
2git add fibonacci.cpp
git commit -m "Implement Fibonacci series evaluation recursively"
小李一开始也是这么想的,但小李发现这么写会产生很多的重复计算,比如计算“fib(n)”的时候,按递归定义计算了“f(n-2)”;但计算“fib(n-1)”的时候按递归定义又要重新把“fib(n-2)”算一遍,会导致更多不必要的开销。深思熟虑以后,小李决定:
基于初始commit创建了自己的分支,并完成了fib的迭代实现
1
git checkout -b li-implementation
1
2
3
4
5
6
7long long fib(int n) {
// 小李的迭代实现
int a = 1, b = 1;
for (int i = 2; i <= n; i++)
b = b + a, a = b - a;
return b;
}通过了测试并提交了自己的代码
1
2git add fibonacci.cpp
git commit -m "Implement Fibonacci series evaluation iteratively"
于是我现在首先merge了小王的代码,然后git成功把我的“fibonacci.cpp”更新成了小王的递归版(记得我自己默认的分支最开始就是master):
当我看完小王的实现后,觉得没毛病,所以我现在想看小李的实现。于是我把小李的实现merge了进来,git就报出了合并冲突的bug:
造成合并冲突的原因在第一部分已经有讲过了,要合并分支的commit和当前分支所记录的commit里存在对同一位置的不同内容,这个时候git会报出合并冲突并把选择权交给用户。反映到这里,就是小王和小李在“fibonacci.cpp”文件的long long fib(int n)
这个函数的实现里选择了不同的方式,让git在合并时感到了“为难”。
合并冲突问题的解决
还是第二部分的例子——在合并冲突的文件“fibonacci.cpp”中,我们可以看到git为它生成了一些额外的信息:
1 | long long fib(int n) { |
我们对这些信息进行一点解释说明
<<<<<<< HEAD
表示当前分支记录的内容,在这里就是因为一开始commit的就是小王的递归版本,所以“<<<<<<< HEAD”下面保存的就是小王的实现。
=======
分隔符,用来区分当前分支的内容与merge进来的分支的内容。
>>>>>>> [merge进来的分支名]
从“=======”到“>>>>>>> [merge进来的分支名]”之间的内容,表示merge进来的分支所记录的内容,在这里就是小李所实现的基于迭代的fib函数。
要解决分支冲突,我们需要对保留当前还是接受merge的内容进行取舍。因为显然小李的实现更加高效,所以我们选择保留小李的实现,并且删去小王的实现以及git生成的助记符,然后进行add与commit。
1 | long long fib(int n) { |
如图所示,经过上述的步骤后,我们已经成功解决了合并冲突,并且HEAD指针指向master分支,最近的一次commit显示我们把小李的分支merge进了master分支。
综上所述,进行分支冲突修复的办法就是:
- 找到发生冲突的文件以及冲突产生的具体位置
- 选择需要保留的版本,并删去不需要的内容和额外生成的信息
- 重新add那些发生冲突的文件,然后commit
后记
看到这里,我想有些细心的小伙伴可能会问,还记得最开始有一步——先merge小王的递归版本进master么,喏,你看:
一开始的fib函数
1 | long long fib(int n) { |
merge进来的小王的fib函数
1 | long long fib(int n) { |
那不是也符合我们之前说的“同一位置的不同内容”,那为什么这个没有造成git的合并冲突呢?那这里我们说,这是因为git的合并冲突是牢牢基于“分支”这个结构的,也就是说,不同的分支要存在并行的关系,才有可能出现“分支冲突”。
我们说小王和小李的代码,逻辑上是呈“并行”结构的——他们都是基于最开始的框架代码所在的master分支checkout出来的,所以合并起来会产生冲突。但也正是这个
基于最开始的框架代码所在的master分支checkout出来的
这个条件,使得小王的递归代码在一开始和master进行merge的时候,它们二者呈现的是一个先后的“顺序”关系而不是一种并行的关系,所以git认为——这是你用新版本去覆盖老版本啊,完全没有问题。所以这样一种先后的串行关系并不会造成“合并冲突”,只有“并行”的分支关系才有可能造成合并冲突。