git中的分支十分轻量,因此我们在使用git的时候会频繁的用到分支,不可避免的需要进行分支合并。
在git中合并分支有两种选择:merge 和 rebase。但是,无论哪一种,都有可能产生冲突。
一、冲突产生的原因
因此我们先来看一下冲突是如何产生的:
如上图所示,将feature分支合并到master,我们需要一个比较的标准,如果只是3和6比较,那么3与6相比,添加了一个文件,也可以说成是6与3比删除了一个文件,这无法确切表示当前的冲突状态。
因此我们选取他们的两个分支的分歧点(merge base)作为参考点,进行比较。 比较时,相对于 merge base(提交1)进行比较:
首先把1、3、6中所有的文件做一个列表,然后依次遍历这个列表中的文件。现在我们拿列表中的一个文件进行举例,把在提交1、3、6中的该文件分别称为版本1、版本3、版本6。
版本1、版本3、版本6的 SHA-1 值完全相同,这种情况表明没有冲突
版本3或6至少一个与版本1状态相同(指的是SHA-1值相同或都不存在),这种情况可以自动合并。
版本3或版本6都与版本1的状态不同,情况复杂一些,自动合并策略很难生效,需要手动解决。
二、冲突的状态定义
冲突状态定义:
1 and 3: DELETED_BY_THEM;
1 and 6: DELETED_BY_US;
3 and 6: BOTH_ADDED;
1 and 3 and 6: BOTH_MODIFIED
我们拿第一种情况举例,文件有两种状态 1 和 3,1 表示该文件存在于 commit 1(也就是MERGE_BASE),3 表示该文件在 commit 3 (master 分支)中被修改了,没有 6,也就是该文件在 commit 6(feature 分支)被删除了,总结来说这种状态就是 DELETED_BY_THEM。
可以再看一下第四种情况,文件有三种状态 1、3、6,1 表示 commit 1(MERGE_BASE)中存在,3 表示 commit 3(master 分支)进行了修改,6 表示(feature 分支)也进行了修改,总结来说就是 BOTH_MODIFIED(双方修改)。
遇到不可自动合并冲突时,git会将这些状态写入到暂存区。与我们讨论不同的是,git使用1,2,3标记文件,1表示文件的base版本,2表示当前的分支的版本,3表示要合并分支的版本。
三、冲突是如何表示的
当产生合并冲突时,该部分会以<<<<<<<, ========= 和 >>>>>>>表示。在======= 之前的部分是当前分支的情况,在=======之后的部分是对方分支的情况。
四、如何解决冲突
在看到冲突以后,你可以选择以下方式来处理冲突:
决定不合并。这时,唯一要做的就是重置index到HEAD节点。使用命令 git merge --abort 来放弃合并。
解决冲突。Git会标记冲突的地方,解决完冲突的地方后使用 git add 加入到暂存区中,然后使用 git commit 产生合并节点。
你可以用以下方式来解决冲突:
使用合并工具。git mergetool 将会调用一个可视化的合并工具来处理冲突合并。
查看差异。git diff 将会显示三路差异(三路合并中所采用的三路比较算法)。
查看每个分支的差异。git log --merge -p
将会显示HEAD版本和MERGE_HEAD版本的差异。
查看合并前的版本。git show :1:文件名 显示共同祖先的版本,git show :2:文件名 显示当前分支的HEAD版本,git show :3:文件名 显示对方分支的MERGE_HEAD版本。
五、分支合并Merge和Rebase
在解决完冲突后,我们可以将合并过来的内容提交为一个新的提交。如下图所示为 merge:
可以看到 merge 是一种不修改分支历史提交记录的方式,这也是我们常用的方式。
而 rebase 会把从 merge base 以来的所有提交,以补丁的形式一个一个重新打到目标分支上。这使得目标分支合并该分支的时候会直接 fast forward,即不会产生任何冲突。
从上图中,我们就可以看到 rebase 的一个缺点,那就是修改了分支的历史提交。如果已经将分支推送到了远程仓库,会导致无法将修改后的分支推送上去,必须使用 -f 参数(force)强行推送。
所以使用 rebase 最好不要在公共分支上进行操作。