Skip to main content

git merge 和 rebase 的区别

· 13 min read
Kimi Gao
Fullstack & AI

分支的合并

分支的合并主要有两种方法:mergerebase,例如 release 分支或 hotfix 分支测试完成后,最后要合并回 master 分支,使用这两种方法,合并后分支的历史记录会有很大的差别,本文主要详细论述了这两种方法的差异以及使用场景。

merge

假设有两个分支:bugfix 分支是从 master 分支分叉出来的。下面主要分两种情况来讨论 merge 操作。

master 没有新的提交

如下图所示:

master 分支上执行一下命令会合并 bugfix 分支到 master分支:

git merge bugfix # 等价于 git merge --ff bugfix

如果 master 分支的状态没有被更改过,那么这个合并是非常简单的。 bugfix 分支的历史记录包含 master 分支所有的历史记录,所以通过把 master 分支的指针向前 移动bugfix 的最新分支上,Git 就会合并。这样的合并被称为 fast-forward(快进)合并。

换句话说,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。

执行合并时,如果设定了 non fast-forward 选项,即使在能够 fast-forward 合并的情况下也会生成新的提交并合并。

git merge --no-ff bugfix

如图所示:

master 也有新的提交

master 分支的历史记录有可能在 bugfix 分支分叉出去后有新的更新。这种情况下,要把 master 分支的修改内容和 bugfix 分支的修改内容汇合起来。

如果对于这种两个分支都有新的提交,主要分两种情况讨论:

master 和 bugfix 新的提交没有文件冲突

执行 git merge bugfix 会出现以下信息,可以修改 merge 的 commit message 信息。

Merge branch 'bugfix'

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
~
~

合并两个修改会生成一个提交。这时,master分支的 HEAD 会移动到该提交上。新的合并节点 E 的 commit message 是“Merge branch 'bugfix'”,加不加 --ff 的效果其实是一样的。

master 和 bugfix 新的提交有文件冲突

如果它们都同时修改了 README.md 且导致该文件的有冲突(修改同一行包括该行的前后一行,都会出现冲突。如果连续修改该行的下一行以及下下一行,那么这些连续行也会算是同一个冲突),执行 git merge bugfix 会出现以下错误:

Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

处理冲突的时候

(1)如果用了 bugfix 的代码 或者 修改了部分代码,则处理冲突完成后,不仅需要 add ,还需要 commit

On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)

Changes to be committed:

modified: README.md

那么将会生成一个新的合并节点,其实就是默认的 --ff 模式不能走下去,走的是 --no-ff 模式。 message 就是以上代码 commit 的 message,分支合并示意图如下图所示:

当然合并的时候你可以强制只有 --ff 走得通的情况下才合并,否则终止合并:

git merge --ff-only bugfix

(2)如果全部用master的代码,则处理冲突完成后,只需要执行:

git add --all

add 之后看一下 status 会返回以下信息:

On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)

注意这里不需要进行 commit 即可将 master 的 commit(包括 message 和 hash) 会覆盖掉 bugfix 的 commit。且无论加不加 --no-ff 合并后分支是 线性 的。其实就是还是原来的 master 分支走向。当然现实中这种情况还是比较少的。

PS: 如果中途想无害终止 merge 可以直接 git merge --abort

fast-forward 和 non fast-forward 优缺点

  • fast-forward :删除分支后,会丢掉分支信息,因为合并完之后的视图为线性的,看不出其他分支和交叉合并的信息,但是线性的分支会看起来更加赏心悦目,有洁癖的程序员推荐。
  • non fast-forward :即使 bugfix 分支被删除,master 的分叉合并形状图也会保留下来。那么要查明在这个分支里的操作就很容易了。但当分支很多的时候,分支图会很乱。

git merge --squash

git merge --squash会在当前分支新建一个提交节点,无论 masterbugfix 是否都有新的提交,冲突处理都会增加一个新的提交节点,但最后的分支图都是 线性 的。

--squash--no-ff 非常类似,区别只有一点不会保留对合入分支的引用。

git merge --squash bugfix

小结

  • git merge 默认是 先 fast-forward 模式,如果走不通的情况再 non fast-forward。可以手动强制一直使用 --no-ff 来保留分支图,也可以使用 --ff-only,在不能 fast-forward 的时候自动终止。
  • 在两个分支都有新的提交情况下,合并冲突的时候除非全部使用 当前分支 的代码,否则都会产生新的 merge 节点,分支分叉图会被保留。
  • 想要完全线性的分支图,可以使用 squash ,但会产生一个新的 squash 节点,团队多分支开发的时候可以统一用这个方式,如果使用的是 github 仓库,可以设置 Pull Request 默认 merge 按钮为 Squash and merge ,但 github 的这种方式的前提是要先处理完冲突。

rebase

merge 的例子一样,bugfix 分支是从 master 分支分叉出来的。如果 master 没有新的提交,完全没有必要 rebase 使用 git merge bugfix 即可。下面我们将讨论 master 和 bugfix 分支都有新的提交情况:

如果在 master 分支使用 rebase 方法进行分支合并:

git checkout master
git rebase bugfix # bugfix 的分支 commit hash 不会变,变的是 master

⚠️ 这样操作的话将会导致 master 分支的 commit 被重新改写,最好 master 的 commit 保持不变,去改写 bugfix 分支的 commit ,最后再将新的 bugfix 分支 fast-forward 到 master 分支。

所以我们现在用 git rebase master 来保持 master 的 commit 不变。

现在再想想如果我们在 bugfix 分支上 rebase 没有新提交的 master,其实是不会有任何效果的,所以如果 master 没有新的提交,完全没有必要 rebase 使用 git merge bugfix 即可。

如果 C, D, X, Y 都没有冲突的话,执行以下命令:

git checkout bugfix
git rebase master # 保持 master 的 commit hash 不变
git checkout master
git merge bugfix # 自动使用 fast-forward 模式

最后会出现下图所显示的历史记录:

经过 rebase 相关操作后历史记录成一条线,相当整洁。

注意移动提交X和Y有可能会发生冲突,所以需要修改各自的提交时发生冲突的部分。

git checkout bugfix
git rebase master # 保持 master 的 commit hash 不变

其中冲突的比较过程是:

  1. D 和 X 比较,处理完冲突,git rebase --continue 生成 X'
  2. X' 和 Y 比较,处理完冲突,git rebase --continue 生成 Y'

如果比较过程中没有冲突则会自动跳过,进入下一轮比较,命令行操作示例:

最后一次 git rebase --continue 会生成 Y', rebase 的过程就此结束。

⚠️ 这样不断生成新的节点(该节点只是 commit message 一样,比如 X', Y' )进行比较,可能会导致在冲突的处理过程中会出现类似的冲突要多次重复处理。

最后将 masterHEAD 移动到 bugfixHEAD 这里:

git checkout master
git merge bugfix

总结

merge 和 rebase都是合并历史记录,但是各自的特征不同:

  • merge: 保持修改内容的历史记录,但是历史记录会很复杂。
  • rebase: 历史记录线性整洁,是在原有提交的基础上将差异内容反映进去。提交较多冲突多的时候处理起来非常麻烦。

可以根据开发团队的需要分别使用 merge 和 rebase 。

QA: 什么时候用 merge 或 rebase?

通过上面 rebase 的比较过程示意图你会发现 rebase 的比较次数就是 bugfix 的新的 commit 节点数。所以,节点数越多,可能冲突的概率就会越大,这种情况下推荐使用 merge。

QA: 平时开发如何减少分叉的 commit 数?

一般对于 Pull Reuqest 的粒度要小,一方面方便 review,另一方面可以设置默认的合并操作为 Squash and merge

将一个 PR 中间小的调整都 squash 成一个大的 commit,也方便了分支间在有需要的时候进行 rebase 操作。

参考资料

  1. Git 分支 - 分支的衍合
  2. git rebase和merge的区别,rebase的运动过程和conflict的解决
  3. Git Community Book 中文版: rebase
  4. 猴子都能懂的GIT入门:分支的合并(by 贝格乐)
  5. 分支的整合:git rebase Or git merge(by 柳兮)
  6. 图解4种git合并分支方法(by 颜海镜)
  7. git究竟什么时候才会遇到conflict?(by 浪漫海贼)