这里,没有难懂的知识;
只有最浅显的文字,
最直观的图示,
最鲜活的例子;
晴耕 · 白话,让知识更简单!
(点击文末阅读原文,查看本文在晴耕小筑网站上的完整内容)
多人开发中的合并冲突是我们使用Git时常常会遇到的情况,小小合并门道大,讲述合并的那些事儿,晴耕 · 白话之“Git合并那些事”系列持续连载中……

准备实验环境
本文推荐大家使用Hello Git提供的两个Docker镜像作为实验环境(GitHub项目网址:https://git.io/fpN0M):一个代表远程Git服务(lab-git-remote),一个代表本地Git客户端(lab-git-local)。这两个镜像都可以从Docker Hub上找到:
docker pull morningspace/lab-git-remote
docker pull morningspace/lab-git-local
有关这两个Docker镜像的具体使用方法,请见Hello Git项目的README。本文后续讨论的所有动手环节,都将围绕这两个Docker镜像展开。
为了方便后面做实验,我们先把这两个Docker镜像启动起来。首先是远程Git服务:
docker run -it --name team-git-remote --hostname team-git-remote --net=lab -p 8080:80 morningspace/lab-git-remote
然后是本地Git客户端:
docker run -it --name git-local-william --hostname git-local-william --net=lab -e user_name=William -e user_email=william@example.com morningspace/lab-git-local
注意,这里的用户名和邮件地址是通过参数-e传入容器的,容器会根据传入的值,自动生成相应的Public Key。这个Public Key在容器启动时会打印到控制台。为了让Git客户端成功访问远程Git服务,我们需要从Git客户端通过SSH以root账号登录到远程Git服务(密码为passw0rd),然后把Public Key加入/home/git/.ssh/authorized_keys文件:
$ ssh root@team-git-remote
$ echo <your_public_key> >> /home/git/.ssh/authorized_keys
最后,我们来创建一个本地Git库,本文我们要讨论的所有关于合并的故事都将在这个Git库里发生:
$ git init merge-stories
Initialized empty Git repository in /root/merge-stories/.git/
$ cd merge-stories
Fast-Forward Merge
首先,我们来看一下什么是Fast-Forward Merge(快进式合并)。Fast-Forward Merge是Git的各种合并方式中最容易理解的,也是较为常见的一种情况。
下面我们先往本地Git库里提交一些变更。在命令行下,按照顺序依次提交变更c1:
$ vi README
$ cat README
c1
$ git add .
$ git commit -m c1
[master (root-commit) cfbff28] c1
1 file changed, 1 insertion(+)
create mode 100644 README
和c2:
$ vi README
$ cat README
c1 - c2
$ git commit -am c2
[master 9229adb] c2
1 file changed, 1 insertion(+), 1 deletion(-)
然后,再从master分支切换到dev分支:
$ git checkout -b dev
Switched to a new branch 'dev'
继续提交变更c3:
$ vi README
$ cat README
c1 - c2
+ - c3
$ git commit -am c3
[dev c594fa9] c3
1 file changed, 1 insertion(+)
和c4:
$ vi README
$ cat README
c1 - c2
+ - c3 - c4
$ git commit -am c4
[dev 0f029c3] c4
1 file changed, 1 insertion(+), 1 deletion(-)
执行上述命令后,用git log可以看到我们的提交历史:
$ git log --oneline --graph --all
* 0f029c3 (dev) c4
* c594fa9 c3
* 9229adb (HEAD -> master) c2
* cfbff28 c1
我们在两个分支上的提交历史如图所示:

现在,让我们切换回master分支:
$ git checkout master
Switched to branch 'master'
并把dev分支上的变更合并到master:
$ git merge dev
Updating 9229adb..0f029c3
Fast-forward
README | 1 +
1 file changed, 1 insertion(+)
可以看到输出结果中的“Fast-forward”,这表明Git在做合并时采用的是Fast-Forward Merge。此时,再观察一下Git的提交历史:
$ git log --oneline --graph --all
* 0f029c3 (HEAD -> master, dev) c4
* c594fa9 c3
* 9229adb c2
* cfbff28 c1
以及相应的图示:

我们会发现,由于master分支从c2开始与dev分叉以后就再也没有新的提交了,所以Git只是简单地把master的head指针向前移动到c4,合并就完成了。这就是所谓的Fast-Forward Merge。因为不涉及内容变更的比较,所以这种合并方式效率很高。Fast-Forward Merge要求参与合并的两个分支上的提交必须是“一脉相承”的父子或祖孙关系。不过它有个缺点,作为被合并的dev分支,它的提交历史在合并以后会和master分支的提交历史重合。
如果我们想在合并后保留来自被合并分支的提交历史,并显式标注出合并发生的位置,那就需要在执行合并时加上参数--no-ff。当然,这样也表示我们在合并时将不使用Fast-Forward Merge。为了演示这一点,让我们用git reset回退到c2:
$ git reset --hard 9229adb
HEAD is now at 9229adb c2
然后,再进行一次从dev到master的合并,并指定参数--no-ff:
$ git merge --no-ff -m c5 dev
Merge made by the 'recursive' strategy.
README | 1 +
1 file changed, 1 insertion(+)
这一次,由于没有采用Fast-Forward Merge,Git会为我们生成一个新的提交。在执行git merge时,我们还利用参数-m为这个新的提交记录指定了“名称”:c5。这里是合并之后的提交历史:
git log --oneline --graph --all
* b735987 (HEAD -> master) c5
|\
| * 0f029c3 (dev) c4
| * c594fa9 c3
|/
* 9229adb c2
* cfbff28 c1
以及相应的图示:

实际上,当使用--no-ff参数进行合并时,我们的合并方式就变成了Three-Way Merge了。下面我们就来看一看,什么是Three-Way Merge。另外,还有一种叫做Squash Merge的合并方法也经常被用到。关于Three-Way Merge和Squash Merge的部分,欢迎点击文末阅读原文,查看本文在晴耕小筑网站上的完整内容。
(未完待续)




