git merge 应该是开发者最常用的 git 指令之一,
默认情况下你直接使用 git merge 命令,没有附加任何选项命令的话,那么应该是交给 git 来判断使用哪种 merge 模式,实际上 git 默认执行的指令是 git merge -ff 指令(默认值)

1. Fast-forward

1.1. ff

1.1.1. 触发场景

当dev是master最新commit(HEAD)的孩子,默认触发此模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cd ~
rm -rf mergedemo
mkdir mergedemo;cd mergedemo
git init .
echo "hello A" > file.txt
git add .
git commit -m "A"
echo "hello B" >> file.txt
git add .
git commit -m "B"
echo "hello M1" >> file.txt
git add .
git commit -m "M1"
echo "hello M2" >> file.txt
git add .
git commit -m "M2"
git checkout -b dev
echo "hello D1" >> file.txt
git add .
git commit -m "D1"
echo "hello D2" >> file.txt
git add .
git commit -m "D2"
echo "hello D3" >> file.txt
git add .
git commit -m "D3"
git log --oneline
1
2
git checkout master
gitk

1
2
git checkout dev
gitk

1
2
3
4
5
6
➜  mergedemo git:(master) git merge dev      

更新 8b3d646..cca25ff
Fast-forward
 file.txt | 3 +++
 1 file changed, 3 insertions(+)

git merge dev 会默认执行 fast-forward模式

Fast-forward 是指 Master 合并 Feature 时候发现 Master 当前节点一直和 Feature 的根节点相同,没有发生改变,那么 Master 快速移动头指针到 Feature 的位置,所以 Fast-forward 并不会发生真正的合并,只是通过移动指针造成合并的假象,这也体现 git 设计的巧妙之处。合并后的分支指针上图所示

通常功能分支(feature556) 合并 master 后会被删除,通过下图可以看到,通过 Fast-forward 模式产生的合并可以产生干净并且线性的历史记录

1.1.2. 优缺点

  • 优点:直接修改 HEAD 指针指向,不会创造一个新的 commit 节点,所以合并速度非常快,且产生干净并且线性的历史记录
  • 缺点:删除分支或指针向前走会丢失分支信息 (log中体现不出原来的分支操作)

1.2. no-ff

1.2.1. 触发场景

dev非master最新commit(HEAD)的孩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cd ~
rm -rf mergedemo
mkdir mergedemo;cd mergedemo
git init .
echo "hello A" > file.txt
git add .
git commit -m "A"
echo "hello B" >> file.txt
git add .
git commit -m "B"
git checkout -b dev
echo "hello D1" >> file.txt
git add .
git commit -m "D1"
echo "hello D2" >> file.txt
git add .
git commit -m "D2"
echo "hello D3" >> file.txt
git add .
git commit -m "D3"
git checkout master
echo "hello M1" >> file.txt
git add .
git commit -m "M1"
echo "hello M2" >> file.txt
git add .
git commit -m "M2"
git log

master分支的git log

1
2
3
4
1d190f3 (HEAD -> master) M2
70a4baa M1
87652d3 B
2610c63 A

dev分支的git log

1
2
3
4
5
cb082ff (HEAD -> dev) D3
63ee19a D2
f43408e D1
87652d3 B
2610c63 A
1
git merge dev

master 已经没办法通过移动头指针来完成 Fast-forward,所以在 master 合并 dev 的时候就不得不做出真正的合并,真正的合并会让 git 多做很多工作,具体合并的动作如下:

  • 找出 master 和 dev 的公共祖先,节点 B,已经各自的最新提交M2, D3 三个节点的版本 (如果有冲突需要处理,我们这里的示例代码就需要手动合并,然后需要git add,git commit,因此又多出来一个commit历史,这里我备注的是“master merge dev”)
  • 创建新的节点 D4,并且将三个版本的差异合并到 D4,并且创建 commit
  • 将 master 和 HEAD 指针移动到 D4

此时生产了一个merge commit (D4’),这个merge commit不包含任何代码改动,而包含在dev分支上的几个commit列表(D1, D2和D3)。查看git的提交历史(git log)可以看到所有的这些提交历史记录。

1.2.2. 优缺点

  • 优点:保留commit历史
  • 缺点:产生一个新的merge commit

1.3. ff-only

只会按照 Fast-forward 模式进行合并,如果不符合条件(并非当前分支的直接后代),则会拒绝合并请求并且退出

1
2
3
➜  mergedemo git:(master) git merge dev --ff-only

fatal: 无法快进,终止。

1.4. 手动设置合并模式

先简单介绍一下 git merge 的三个合并参数模式:

  • –ff 自动合并模式:当合并的分支为当前分支的后代的,那么会自动执行 --ff (Fast-forward) 模式,如果不匹配则执行 --no-ff(non-Fast-forward) 合并模式
  • –no-ff 非 Fast-forward 模式:在任何情况下都会创建新的 commit 进行多方合并(即使被合并的分支为自己的直接后代)
  • –ff-only Fast-forward 模式:只会按照 Fast-forward 模式进行合并,如果不符合条件(并非当前分支的直接后代),则会拒绝合并请求并且退出
1
2
git config --global --add merge.ff false
git config branch.[branch name].mergeoptions "--no-ff"

1.5. 适用场景

三种 merge 模式没有好坏和优劣之分,只有根据你团队的需求和实际情况选择合适的合并模式才是最优解,那么应该怎么选择呢? 我给出以下推荐:

  • 如果你是小型团队,并且追求干净线性 git 历史记录,那么我推荐使用 git merge --ff-only 方式保持主线模式开发是一种不错的选择
  • 如果你团队不大不小,并且也不追求线性的 git 历史记录,要体现相对真实的 merge 记录,那么默认的 git --ff 比较合适
  • 如果你是大型团队,并且要严格监控每个功能分支的合并情况,那么使用 --no-ff 禁用 Fast-forward 是一个不错的选择

2. squash merge

--squash选项的含义是:本地文件内容与不使用该选项的合并结果相同,但是不提交、不移动HEAD,因此需要一条额外的commit命令。其效果相当于将another分支上的多个commit合并成一个,放在当前分支上,原来的commit历史则没有拿过来。

https://www.processon.com/view/6325a4f01efad46b0ab39c8d?fromnew=1

2.1. 适用场景

2.1.1. 简化提交历史

在dev分支上执行的是开发工作,有一些很小的提交,或者是纠正前面的错误的提交,对于这类提交对整个工程来说不需要单独显示出来一次提交,不然导致项目的提交历史过于复杂;所以基于这种原因,我们可以把dev上的所有提交都合并成一个提交;然后提交到主干。

判断是否使用--squash选项最根本的标准是,待合并分支上的历史是否有意义
在某些情况下,我们应该优先选择使用--squash选项,如下:

联想发散一下,同一个分支上压缩commit,请见Git进阶--1、Git-后悔药-回退撤销-reset

2.1.2. github squash选项

2.2. 注意事项

2.2.1. 分支必须是直接孩子

1
2
3
4
5eb656a (HEAD -> master) M2
3c31c9c M1
782742b B
685ce99 A


需要commit一下

2.2.2. 合并提交归属问题

在这个例子中,我们把D1,D2和D3的改动合并成了一个D。

注意,squash merge并不会替你产生提交,它只是把所有的改动合并,然后放在本地文件,需要你再次手动执行git commit操作;此时又要注意了,因为你要你手动commit,也就是说这个commit是你产生的,不是有原来dev分支上的开发人员产生的,提交者身份发生了变化。也可以这么理解,就是你把dev分支上的所有代码改动一次性porting到master分支上而已。

由于squash merge会变更提交者作者信息,这是一个很大的问题,后期问题追溯不好处理(当然也可以由分支dev的所有者来执行squash merge操作,以解决部分问题),rebase merge可以保留提交的作者信息,同时可以合并commit历史,完美的解决了上面的问题。

Git进阶--11、git-rebase用法解析与merge区别

3. 参考

https://static.kancloud.cn/apachecn/git-doc-zh/1945501
https://www.jianshu.com/p/ff1877c5864e
https://www.cnblogs.com/xiao2shiqi/p/14715119.html