如何处理冲突(conflict)

前面说了分支的一些事情,还简单地合并了一个分支。但是平时多人协作的工作过程中,几乎没有不碰到冲突(conflict)的情况,下面的示例就是剖析一下冲突成因及背后的故事:

还是老规矩,新建一个空的Git仓库作试验:

$rm -rf test_merge_proj
$mkdir test_merge_proj
$cd test_merge_proj
$git init
Initialized empty Git repository in /home/test/test_merge_proj/.git/

在主分支里建一个“readme.txt”的文件,并且提交本地仓库的主分支里(master):

$echo "hello, world" > readme.txt
$git add readme.txt
$git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached ..." to unstage)
#
# new file: readme.txt
#
git commit -m "project init"
[master (root-commit) d58353e] project init 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 readme.txt

当看当前版本所包含的blob:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob 4b5fa63702dd96796042e92787f464e28f09f17d readme.txt

虽然前面把“readme.txt”这个文件提交了,但是暂存区里还是会暂存一下,直到下次“git add”时把它冲掉:

$git ls-files --stage
100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 readme.txt

然后再创建测试分支(test branch),并且切换到测试分支下工作:

$git branch test
$git checkout test
Switched to branch 'test'

再在测试分支里改写“readme.txt”的内容,并且提交到本地仓库中:

$echo "hello, mundo" > readme.txt
$git add readme.txt
$git commit -m "test branch modified"
[test 7459649] test branch modified 1 files changed, 1 insertions(+), 1 deletions(-)

现在看一下当前分支里的“readme.txt”的“SHA1哈希串值”确实不同了:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob 034a81de5dfb592a22039db1a9f3f50f66f474dd readme.txt

暂存区里的东东也不一样了:

$git ls-files --stage
100644 034a81de5dfb592a22039db1a9f3f50f66f474dd 0 readme.txt

现在我们切换到主分支(master)下工作,再在“readme.txt”上作一些修改,并把它提交到本地的仓库里面:

$git checkout master
Switched to branch 'master'
$git add readme.txt
echo "hola,world" > readme.txt
$git add readme.txt
$git commit -m "master branch modified"
[master 269ef45] master branch modified 1 files changed, 1 insertions(+), 1 deletions(-)

现在再来看一下当前分支里的“readme.txt”的“SHA1哈希串值”:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob aac629fb789684a5d9c662e6548fdc595608c002 readme.txt

暂存区里的内容也改变了:

$git ls-files --stage
100644 aac629fb789684a5d9c662e6548fdc595608c002 0 readme.txt

主分支(master) 和测试分支(test)里的内容已经各自改变了(diverged),我们现在用“git merge”命令来把两个分支合一下看看:

$git merge test
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

合并命令的执行结果不是“Fast-foward”,而是“CONFLICT”。是的,两个分支的内容有差异,致使它们不能自动合并(Auto-merging)。

还是先看一下工作目录的状态:

$git status
# On branch master
# Unmerged paths:
# (use "git add/rm ..." as appropriate to mark resolution)
#
# both modified: readme.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

现在Git提示当前有一个文件“readme.txt”没有被合并,原因是“both modified”。

再看一下暂存区里的内容:

$git ls-files --stage
100644 4b5fa63702dd96796042e92787f464e28f09f17d 1 readme.txt
100644 aac629fb789684a5d9c662e6548fdc595608c002 2 readme.txt
100644 034a81de5dfb592a22039db1a9f3f50f66f474dd 3 readme.txt

看一下里面的每个blob对象的内容:

$git cat-file -p 4b5fa6
hello, world
$git cat-file -p aac629
hola,world
$git cat-file -p 034a81
hello, mundo

我们不难发现,“aac629”是当前主分支的内容,“034a81”是测试分支里的内容,而“4b5fa6”是它们共同父对象(Parent)里的内容。因为在合并过程中出现了错误,所以Git把它们三个放到了暂存区了。

现在我们再来看一下工作目录里的“readme.txt”文件的内容:

$cat readme.txt
<<<<<<< HEAD
hola,world
=======
hello, mundo
>>>>>>> test

“<<<<<<< HEAD“下面就是当前版本里的内容;而“=======”之下,“>>>>>>> test”之上则表示测试分支里与之对应的有冲突的容。修复冲突时我们要做的,一般就是把“ <<<<<<< HEAD”,“=======”和“ >>>>>>> test”这些东东先去掉,然后把代码改成我们想要的内容。

假设我们用编辑器把“readme.txt“改成了下面的内容:

$cat readme.txt
hola, mundo

然再把改好的“readme.txt”用“git add”添加到暂存区中,最后再用“git commit”提交到本地仓库中,这个冲突(conflict)就算解决了:

$git add readme.txt
$git commit -m "fix conflict"
[master ebe2f18] fix conflict

这里看起来比较怪异的地方是Git解决了冲突的办法:怎么用“git add”添加到暂存区去,“git add”不是用来未暂存文件的吧,怎么又来解决冲突了。不过我想如果你仔细读过上一篇文章的话就不难理解,因为Git是一个“snapshot”存储系统,所有新增加的内容都是直接存储的,而不是和老版本作一个比较后存储新旧版本间的差异。

Git里面合并两个版本之间的同一文件,如果两者间内容相同则不作处理,两者间内容不同但是可以合并则产生一个新的blob对象,两者间内容不同但是合并时产生了冲突,那么我们解决了冲突后要把文件“git add”到暂存区中再“git commit”提交到本地仓库即可,这就和前面一样产生一个新的blob对象。

假设我们对合并的结果不满意,可以用下面的命令来撤消前面的合并:

$git reset --hard HEAD^
HEAD is now at 050d890 master branch modified

从git reset(2)命令的输出结果可以看到,主分支已经回到了合并前的状态了。

我们再用下面的命令看一下“readme.txt”文件,确认一下文件改回来没有:

$cat readme.txt
hola,world

小结

由于Git采用了“SHA1哈希串值内容寻值”、“快照存储(snapshot)”等方法, Git中创建分支代价是很小的速度很快;也这是因为如此,它处理合并冲突的方法与众不同。

在这里我想起了“C语言就是汇编(计算机硬件)的一个马甲”这句话,其实Git也就是底层文件系统的一个马甲,只不过它带了版本控制功能,而且更加高效。Git里有些命令可能不是很好理解(如解决合并冲突用git add),但是对于系统层而言,它是最高效的,就像是C语言的数组下标从0开始一样。

原文连接:http://www.infoq.com/cn/news/2011/03/git-adventures-branch-merge



相关内容