16.4 合并三:冲突解决
如果两个用户修改了同一文件的同一区域,则在合并的时候会遇到冲突而导致合并过程中断。这是因为Git并不能越俎代庖地替用户做出决定,而是把决定权交给用户。在这种情况下,Gits标识出合并冲突,等待用户对冲突做出抉择。
下面的实践非常简单,两个用户都修改doc/README.txt文件,在第二行"Hello."的后面加上自己的名字,具体操作过程如下。
(1)为确保两个用户的本地版本库和共享版本库状态一致,先分别对两个用户的本地版本库执行拉回操作。
$git pull
(2)用户user1在自己的工作区中修改doc/README.txt文件(仅改动了第二行)。修改后内容如下:
User1 hacked.
Hello,user1.
User2 hacked.
User2 hacked again.
(3)用户user1对修改进行本地提交并推送到共享版本库。
$git add-u
$git commit-m "Say hello to user1."
$git push
(4)用户user2在自己的工作区中修改doc/README.txt文件(仅改动了第二行)。修改后内容如下:
User1 hacked.
Hello,user2.
User2 hacked.
User2 hacked again.
(5)用户user2对修改进行本地提交。
$git add-u
$git commit-m "Say hello to user2."
(6)用户user2执行拉回操作,遇到冲突。
git pull操作相当于git fetch和git merge两个操作。
$git pull
remote:Counting objects:7,done.
remote:Compressing objects:100%(3/3),done.
remote:Total 4(delta 0),reused 0(delta 0)
Unpacking objects:100%(4/4),done.
From file:///path/to/repos/shared
f73db10..a123390 master->origin/master
Auto-merging doc/README.txt
CONFLICT(content):Merge conflict in doc/README.txt
Automatic merge failed;fix conflicts and then commit the result.
执行git pull时所做的合并操作由于遇到冲突导致中断。来看看处于合并冲突状态时工作区和暂存区的状态。
执行git status命令,可以从状态输出中看到文件doc/README.txt处于未合并(冲突)的状态,这个文件在两个不同的提交中都做了修改。
$git status
On branch master
Your branch and 'refs/remotes/origin/master' have diverged,
and have 1 and 1 different commit(s)each,respectively.
#
Unmerged paths:
(use "git add/rm<file>…" as appropriate to mark resolution)
#
both modified:doc/README.txt
#
no changes added to commit(use "git add" and/or "git commit-a")
那么Git是如何记录合并过程及冲突的呢?实际上合并过程是通过.git目录下的几个文件进行记录的:
文件.git/MERGE_HEAD记录所合并的提交ID。
文件.git/MERGE_MSG记录合并失败的信息。
文件.git/MERGE_MODE标识合并状态。
版本库暂存区中则会记录冲突文件的多个不同版本。可以使用git ls-files命令查看:
$git ls-files-s
100644 ea501534d70a13b47b3b4b85c39ab487fa6471c2 1 doc/README.txt
100644 5611db505157d312e4f6fb1db2e2c5bac2a55432 2 doc/README.txt
100644 036dbc5c11b0a0cefc8247cf0e9a3e678f8de060 3 doc/README.txt
100644 430bd4314705257a53241bc1d2cb2cc30f06f5ea 0 team/user1.txt
100644 a72ca0b4f2b9661d12d2a0c1456649fc074a38e3 0 team/user2.txt
在上面的输出中,每一行分为四个字段,前两个分别是文件的属性和SHA1哈希值。第三个字段是暂存区编号。当合并冲突发生后,会用到0以上的暂存区编号。
编号为1的暂存区用于保存冲突文件修改之前的副本,即冲突双方共同的祖先版本。可以用:1:<filename>访问。
$git show:1:doc/README.txt
User1 hacked.
Hello.
User2 hacked.
User2 hacked again.
编号为2的暂存区用于保存当前冲突文件在当前分支中修改的副本。可以用:2:<filename>访问。
$git show:2:doc/README.txt
User1 hacked.
Hello,user2.
User2 hacked.
User2 hacked again.
编号为3的暂存区用于保存当前冲突文件在合并版本(分支)中修改的副本。可以用:3:<filename>访问。
$git show:3:doc/README.txt
User1 hacked.
Hello,user1.
User2 hacked.
User2 hacked again.
对暂存区中冲突文件的上述三个副本无须了解太多,这三个副本实际上是提供冲突解决工具,用于实现三向文件合并的。
工作区的版本则可能同时包含了成功的合并及冲突的合并,其中冲突的合并会用特殊的标记(<<<<<<<=======>>>>>>>)进行标识。查看当前工作区中冲突的文件:
$cat doc/README.txt
User1 hacked.
<<<<<<<HEAD
Hello,user2.
=======
Hello,user1.
>>>>>>>a123390b8936882bd53033a582ab540850b6b5fb
User2 hacked.
User2 hacked again.
特殊标识<<<<<<<(七个小于号)和=======(七个等号)之间的内容是当前分支所更改的内容。特殊标识=======(七个等号)和>>>>>>>(七个大于号)之间的内容是所合并的版本更改的内容。
冲突解决的实质就是通过编辑操作,将冲突标识符所标识的冲突内容替换为合适的内容,并去掉冲突标识符。编辑完毕后执行git add命令将文件添加到暂存区(标号0),然后再提交就完成了冲突解决。
当工作区处于合并冲突状态时,无法再执行提交操作。此时有两个选择:放弃合并操作,或者对合并冲突进行冲突解决操作。放弃合并操作非常简单,只须执行git reset将暂存区重置即可。下面重点介绍如何进行冲突解决的操作。有两个方法进行冲突解决,一个是对少量冲突非常适合的手工编辑操作,另外一个是使用图形化冲突解决工具。
16.4.1 手工编辑完成冲突解决
先来看看不使用工具,直接手动编辑完成冲突解决。打开文件doc/README.txt,将冲突标识符所标识的文字替换为Hello,user1 and user2.。修改后的文件内容如下:
User1 hacked.
Hello,user1 and user2.
User2 hacked.
User2 hacked again.
然后添加到暂存区,并提交:
$git add-u
$git commit-m "Merge completed:say hello to all users."
查看最近三次提交的日志,会看到最新的提交就是一个合并提交:
$git log—oneline—graph-3
*bd3ad1a Merge completed:say hello to all users.
|\
|*a123390 Say hello to user1.
*|60b10f3 Say hello to user2.
|/
提交完成后,会看到.git目录下与合并相关的文件.git/MERGE_HEAD、.git/MERGE_MSG、.git/MERGE_MODE文件都自动删除了。
如果查看暂存区,会发现冲突文件在暂存区中的三个副本也都清除了(实际在对编辑完成的冲突文件执行git add后就已经清除了)。
$git ls-files-s
100644 463dd451d94832f196096bbc0c9cf9f2d0f82527 0 doc/README.txt
100644 430bd4314705257a53241bc1d2cb2cc30f06f5ea 0 team/user1.txt
100644 a72ca0b4f2b9661d12d2a0c1456649fc074a38e3 0 team/user2.txt