在上一节,通过执行 git pull
成功合并后再推送来解决非快进式推送的问题。但在真实的项目环境中,不总是会一帆风顺的,只要有合并就可能出现冲突,这一节重点介绍冲突解决方式。
上节已经说过: git pull = git fetch + git merge
git fetch
:可暂时理解为将远程的版本库的对象(提交、里程碑、分支等)复制到本地。在本地版本库有专门的远程仓库映射引用,如:.git\refs\remotes\origin\master
。git merge
:会被隐式地执行,将其他版本库的提交和本地版本库的提交进行合并。该命令还可以对本版本库的其他分支进行显示的合并操作。默认情况下,合并后的结果会自动提交,如果提供--no-commit
选项,则合并后的结果会放入暂存区,用户可以对合并结果进行检查、更改,然后手动提交。
合并操作并非总会成功,因为合并的不同提交可能同时修改了同一文件相同区域的内容,这会导致冲突。冲突会造成合并操作的中断,冲突的文件会被标识,用户可以对标识为冲突的文件进行冲突解决操作,然后更新暂存区,再提交,最终完成合并操作。
场景一、成功自动合并
大多数情况下,Git
都能非常智能地进行自动合并,下面演示一下成功自动合并的三种情况。
情况一:修改不同的文件
还是用上一节的例子进行演示,为了确保版本库状态的一致性,分别在 user1
和 user2
的本地版本库中执行下面的操作:
git fetch
git reset --hard origin/master
现在两个用户的本地版本都为远程版本库的最新版本,状态一致。都有 team
目录和 user1.txt
和 user2.txt
文件。
- 用户
user1
修改user1.txt
文件,提交并推送到远程。
- 用户
user2
修改user2.txt
文件,提交并推送(推送会失败,遇到非快进式推送的错误)。
- 用户
user2
执行git fetch
操作,获取提交并更新到本地用于跟踪远程版本库master
分支的本地引用origin/master
中。再执行git merge
操作,成功自动合并,最后推送合并后的本地版本库到远程版本库。
- 查看提交关系图。
情况二:修改相同文件的不同区域
当用户 user1
和 user2
在本地提交中修改相同的文件,但修改的是文件的不同位置时,仍可成功自动合并,具体操作如下:
为了确保版本库状态的一致性,分别在
user1
和user2
的本地版本库中执行git pull
操作。-
用户
user1
修改README
文件,在第一行插入内容,更改后内容如下:User1 hacked.
Hello. 用户
user1
本地提交并推送:
-
用户
user2
也在自己的工作区中修改README
文件,在文件的最后插入内容,更改后的文件内容如下:Hello.
User2 hacked. 用户
user2
对修改进行本地提交,并执行git fetch
操作:
- 用户
user2
进行合并操作,完成自动合并并进行推送。这里合并的时候写的是refs/remotes/origin/master
,其简写就是:origin/master
- 追溯一下
README
文件每一行的来源,可以看到user1
和user2
更改的位置:
情况三:同时更改文件名和文件内容
如果用户将文件移动到其他目录(或修改文件名),另外一个用户使用旧的文件进行了修改,Git
还是可以成功自动合并,具体操作如下:
为了确保版本库状态的一致性,分别在
user1
和user2
的本地版本库中执行git pull
操作。用户
user1
将README
移动到doc
目录下,进行本地提交并推送:
- 用户
user2
在本地修改REAMD
文件,在文件的最后插入内容,并本地提交。
- 用户
user2
执行git fetch
操作,并执行git merge
合并操作,最后进行推送。
- 查看日志,并使用
-m
参数查看合并操作所做出的修改。
可以看到,上面的提交是 user1
的,进行了文件的移动。下面的提交是 user2
的,doc/README | 2 +-
表示这个文件从第二行开始,添加了一行并删除了一行 。原来的第三行内容是:User2 hacked.
,而现在的第三行内容是:User2 hacked.User2 hacked again.
。Git` 理解为删除了原来的行,新增了一行。
场景二、自动合并失败,手动冲突解决
如果不同用户修改了同一文件的同一区域,则在合并时会遇到冲突而中断,因为 Git
无法替用户做出决定(是要 user1
的提交内容还是 user2
的提交内容 ,还是两者提交的所有内容),会把决定权交给用户,用户再根据 Git
标识出的冲突位置来进行手动处理。
演示这个场景很简单,两个用户都修改 doc/READE
文件,都在第二行 Hello
. 的后面加上自己的名字,具体操作过程如下:
老规则,为了确保版本库状态的一致性,分别在
user1
和user2
的本地版本库中执行git pull
操作。-
用户
user1
在第二行Hello.
的后面加上自己的名字,内容如下:User1 hacked.
Hello. user1.
User2 hacked.User2 hacked again. -
用户
user1
进行本地提交并推送:git add -u
git commit -m "say hello to user1."
git push -
用户
user2
在第二行Hello.
的后面加上自己的名字,内容如下:User1 hacked.
Hello. user2.
User2 hacked.User2 hacked again. -
用户
user2
对修改进行本地提交:git add -u
git commit -m "say hello to user2." 用户
user2
执行git pull
操作(相当于git fetch
和git merge
):
自动合并失败了,需要手动修复冲突然后再对结果进行提交。
- 通过
git status
命令,可以从状态输出中看到文件doc/README.txt
处于冲突状态,这个文件在两个不同的提交中都做了修改:
输出提示可以使用 git merge --abort
终止合并操作,或者手动修复冲突然后再对结果进行 commit
。
实际上,合并过程是通过 .git
目录下的几个文件进行记录的,当合并成功时,文件会自动被删除,现在合并失败,所以本地还存在这些文件:
.git/MERGE_HEAD
:记录所合并的提交 ID(就是合并成功后的新提交节点)。.git/MERGE_MSG
:记录合并失败的信息。.git/MERGE_MODE
:标识合并状态。
而且暂存区中还会记录冲突文件的多个不同版本,可以使用 git ls-files -s
命令查看:
输出中的每一行有四个字段,第一个是文件的属性,第二个是哈希值,第三个是暂存区编号,当合并冲突发生时,会用到 0 以上的暂存区编号。
编号 1 的暂存区用于保存冲突文件修改之前的副本,也就是冲突双方共同的祖先版本。
编号 2 的暂存区用于保存当前冲突文件修改的副本。
编号 3 的暂存区用于保存合并分支的修改的副本。
可通过 :n:{filename}
语法来访问对应副本的内容,也可以用我们之前学过的 git cat-file -p {commit}
来查看副本的内容:
通过 :n:{filename}
的方式是这样的:git show :1:doc/README
。对暂存区中冲突文件的上述三个副本无须了解太多,这三个副本实际上是提供给冲突解决工具,用于实现三向文件合并的。
工作区的版本则可能同时包含了成功的合并及冲突的合并,其中冲突的合并会用特殊的标记(<<<<<<<=======>>>>>>>)进行标识。当前冲突的文件内容如下:
特殊标记 <<<<<<<
和 =======
之间的内容是当前分支所更改的内容 。 ========
和 >>>>>>>
之间的内容是所合并的分支修改的内容。
冲突解决的本质:就是通过编辑操作,将冲突标识符所标识的冲突内容替换为合适的内容,并去掉冲突标识符。编辑完毕后执行 add add 命令将文件添加到暂存区(编号0),然后再提交,就完成了冲突解决。
现在,工作区处于合并冲突状态,是无法再执行提交操作的。此时有两个选择:放弃合并操作或者解决冲突。放弃合并很简单,可以使用 git merge --abort
终止合并或执行 git reset
将暂存区重置即可。下面重点介绍如何进行冲突解决,也有两个方法:一个是对少量冲突非常适合的手工编辑操作,另一个是使用图形化冲突解决工具。
冲突解决方式一:手工编辑完成冲突解决
很简单,直接将 README
文件的标识符去掉,并修改为想要的提交内容,再进行提交。
修改后的文件内容如下:
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
目录下与合并相关的文件 .git/MERGE_HEAD
、.git/MERGE_MSG
、.git/MERGE_MODE
文件都自动删除了,而且暂存区中的三个副本也都清除了(实际在对编辑完的冲突文件执行 git add
后就已经被清除了)。
冲突解决方式二:图形工具完成冲突解决
- 由于已经手动完成了冲突解决,只能先回滚提交,再执行合并重新进入冲突状态。
再次合并操作,进入冲突状态:
git merge origin/master
开始使用图形工具,根据不同的冲突解决软件来做具体操作,最终结果跟手动处理的一致。
最后进行
git push
操作。