git merge和gith rebase

git merge的三种选项参数

git merge有三种选项参数:

  1. –ff
  2. –no-ff
  3. –ff-only

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

对于专业的开发者来说,你可能无须每次合并都指定合并模式(如果需要的话还是要指定的),但是你可能需要知道git在背后为你默认做了什么事情,这样才能保证你的代码万无一失。

Fast-forward(–ff)

我们从一个正常开发流程来看看:

开发者接到需求任务,从master分支中创建功能分支,git指令如下:

1
git checkout -b dev

开发者在dev分支上完成的功能开发工作,然后产生多次 commit,

1
git commit -m 'commit msg'

这时候当前分支的git历史记录,输入git log --online --all或者git log --oneline --graph --decorate --all可以看到全部分支的历史线。直接看下图可能会更好理解一些

1
2
3
4
5
6
flowchart LR
A((m1)) --> B((m2))
B -.- M(分支master)
B --> D1((D1))
D1 --> D2((D2))
D2 -.- D(分支dev)

功能完成后自然要上线,我们把代码合并,完成上线动作,代码如下

1
2
git checkout master
git merge dev

这种情况,你会发现git自动执行了Fast-forward操作,那么什么是Fast-forward

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

1
2
3
4
5
6
flowchart LR
A((m1)) --> B((m2))
B --> D1((D1))
D1 --> D2((D2))
D2 -.- D(分支dev)
D2 -.- M(分支master)

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

1
2
3
4
5
flowchart LR
A((m1)) --> B((m2))
B --> D1((D1))
D1 --> D2((D2))
D2 -.- M(分支master)

non-Fast-forward(–no-ff)

什么情况会产生non-Fast-forward? 通常,当合并的分支跟master不存在共同祖先节点的时候,这时候在merge的时候 git 默认无法使用Fast-forward模式,
我们先看看下图的模型:

1
2
3
4
5
6
7
flowchart LR
A((m1)) --> B((m2))
B --> m3((m3))
m3 -.- M(分支master)
B --> D1((D1))
D1 --> D2((D2))
D2 -.- D(分支dev)

可以看到 master 分支已经比dev的祖先节点快了1个版本,master已经没办法通过移动头指针来完成Fast-forward,所以在 master合并dev的时候就不得不做出真正的合并,真正的合并会让 git 多做很多工作,具体合并的动作如下:

  • 找出master和dev的公共祖先,节点m2,m3,d2三个节点的版本(如果有冲突需要处理)
  • 创建新的节点md,并且将三个版本的差异合并到md,并且创建commit
  • 将master和HEAD指针移动到md

补充:大家在git log看到很多类似:Merge branch ‘dev’ into master 的 commit 就是non-Fast-forward 产生的。执行完以上动作,最终分支流程图如下:

1
2
3
4
5
6
7
8
9
flowchart LR
A((m1)) --> B((m2))
B --> m3((m3))
B --> D1((D1))
D1 --> D2((D2))
m3-->md((md))
D2-->md((md))
md -.- M(分支master)
md -.- D(分支dev)

如何手动设置合并模式 ?

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

  • -ff 自动合并模式:当合并的分支为当前分支的后代的,那么会自动执行 –ff (Fast-forward) 模式,如果不匹配则执行 - –no-ff(non-Fast-forward) 合并模式
  • –no-ff 非 Fast-forward 模式:在任何情况下都会创建新的 commit 进行多方合并(及时被合并的分支为自己的直接后代)
  • –ff-only Fast-forward 模式:只会按照 Fast-forward 模式进行合并,如果不符合条件(并非当前分支的直接后代),则会拒绝合并请求并且推出

以下是关于 –ff, –no-ff, –ff-only 三种模式的官方说明(使用 git merge –helo 即可查看):

Specifies how a merge is handled when the merged-in history is already a descendant of the >current history. –ff is the default unless merging an annotated (and possibly signed) tag that >is not stored in its natural place in the refs/tags/ hierarchy, in which case –no-ff is assumed.

With –ff, when possible resolve the merge as a fast-forward (only update the branch pointer to >match the merged branch; do not create a merge commit). When not possible (when the merged-in >history is not a descendant of the current history), create a merge commit.

With –no-ff, create a merge commit in all cases, even when the merge could instead be resolved >as a fast-forward.

With –ff-only, resolve the merge as a fast-forward when possible. When not possible, refuse to >merge and exit with a non-zero status.

总结

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

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

git rebase

git rebase 你其实可以把它理解成是“重新设置基线”,将你的当前分支重新设置开始点。这个时候才能知道你当前分支于你需要比较的分支之间的差异。

原理很简单:rebase需要基于一个分支来设置你当前的分支的基线,这基线就是当前分支的开始时间轴向后移动到最新的跟踪分支的最后面,这样你的当前分支就是最新的跟踪分支。这里的操作是基于文件事务处理的,所以你不用怕中间失败会影响文件的一致性。在中间的过程中你可以随时取消rebase 事务

rebase会把你当前分支的 commit 放到公共分支的最后面,所以叫变基。就好像你从公共分支又重新拉出来这个分支一样。
举例:如果你从 master 拉了个feature分支出来,然后你提交了几个 commit,这个时候刚好有人把他开发的东西合并到 master 了,这个时候 master 就比你拉分支的时候多了几个 commit,如果这个时候你 rebase master 的话,就会把你当前的几个 commit,放到那个人 commit 的后面。

从main分支上开发新分支dev,然后其他人在main上新提交,想要获取最新的代码:

1
2
3
4
git checkout main
git pull
git checkout dev
git rebase main

如此,就将dev的commit变基到main最新的节点,获取了main中最新的代码。

注意,不要再公共分支上使用git rebase方式合并,合并到公共分支使用git merge。