写这篇文章的契机,源于一次团队内部的技术分享会,我们深入讨论了 merge 和 rebase 这两个分支合并命令。它们都用于整合代码,但方式与结果迥异。rebase 能让提交历史变成一条整洁的直线,而 merge 则保留了分支的树状结构。坦白说,我个人更习惯使用 merge,因为在我绝大多数开发场景里,rebase 的优势并不明显。
我们先来看一个开发中极其常见的场景:两位开发者基于 master 分支的同一个提交节点,各自拉出新的功能分支(feature/A 和 feature/B)进行开发。开发者 A 完成了多个提交,并率先将代码合并回了 master。开发者 B 也有多个提交,在他准备将代码合并进 master 时,却发现存在冲突。为了模拟最典型的冲突情况,我让每个提交都修改了同一个文件,这样每个 commit 都可能产生冲突。
在 A 将分支合并到 master 之前,分支图如下所示:

可以看到,feature/A 和 feature/B 分支都是从 master 的最新提交拉出来的,两条分支并行开发。为了清晰地展示时间线,我用顺序数字来模拟提交信息,每条分支各自新增了 3 个提交。
现在,我们把 A 的分支通过 merge 方式合并到 master:
git checkout master
git merge feature/A

可以看到,A merge 到 master 后,master 分支的指针前进到了与 feature/A 分支一致的位置。
好了,开篇提到的经典场景已经出现。此时如果你是开发者 B,你会怎么做?如果是我,通常会选择 merge。那我们首先看看使用 merge 的表现:
git checkout master
git merge feature/B
由于我的每个提交都修改了同一个文件,最终合并时必然需要解决冲突。

可以看到,这里的冲突是需要你一次性全部解决的。解决完冲突并提交后,我们再看分支图:

可以清晰地看到,merge 会创建一个新的合并提交节点,将两个分支的历史平行地连接在一起,从而完整保留了分支的演进历史。
这里顺便解释一个常见的疑问:为什么有时 merge 没有产生新的合并节点(MR节点),有时却又产生了?其实在大多数协作场景下都会产生。比如这里的 feature/A 合入 master 就没产生新节点,而 feature/B 后合入 master 就产生了。
合并节点是否产生,取决于 Git 是否需要创建一个新的合并提交来连接两个分叉的历史。
你可能感觉永远都会有合并节点,因为在远程协作中,主分支 master 几乎总是在不断前进的。
-
快进(fast-forward)合并
当 master 分支还停留在你功能分支起点的那个提交时,比如 feature/A 合入时,master 和 feature/A 指向同一个历史节点。这就是一次 fast-forward 提交,Git 只需将 master 指针直接移到 feature/A 的最新提交,因此不会产生额外的合并节点。
-
非快进(non-fast-forward)合并
当 master 分支相较于你的功能分支有了新的提交时,比如 feature/B 的起点是最初的 docs: add README,在它准备合入 master 时,master 的最新提交已经是 feature/A 的 docs: 5。
此时进行合并,Git 无法直接快进,必须创建一个新的提交来融合两个分支的更改,因此就会产生合并节点。需要注意的是,即使没有代码冲突,只要是非 fast-forward 合并,就一定会产生合并节点。冲突只是在合并节点提交前需要你手动处理的内容差异。
现在,我们撤销刚才 feature/B merge 到 master 的操作,改用 rebase 来看看区别。
撤销之后的分支图如下,master 仍然停留在 docs: 5:

现在在 feature/B 分支上使用 rebase:
git checkout feature/B
git rebase master
可以看到 VS Code 编辑器提示需要解决冲突,而这仅仅是第一个提交遇到的冲突。

如果每个提交都有冲突,那么每个提交都需要单独解决一次冲突。 一旦解决完当前提交的冲突并执行 git add,再运行 git rebase --continue,Git 就会继续重放下一个提交。如果同一段代码在多个提交中被修改过,你就会感觉像是在重复解决相似的冲突。
解决完所有冲突并完成 rebase 后,我们再来看看 Git 历史树:

可以看到,最终的 Git 提交历史变成了一条直线,看起来确实整洁不少。你的分支(feature/B)上的所有提交都仿佛被“嫁接”到了 master 分支的最新提交之后,并且不会产生额外的合并节点。但是,你分支上原本的 commit hash(如 docs:3, docs:4, docs:6)会全部发生变化。
结论
从 Git 提交记录来看:
merge:保留完整的历史记录,是非线性的树状结构,容易产生合并提交节点。
rebase:将自己的提交记录“提前”并重新应用,这会改变你之前所有提交的 commit hash,形成线性的历史,不会产生多余的合并节点。
从解决冲突的体验来看:
merge:在最终合并的那一刻,一次性解决所有冲突,直接对比两个分支代码的最终状态。
rebase:会按照你提交的顺序,逐个重放每一个提交。任何一个提交遇到冲突,你都需要停下来单独解决,这对于有多个冲突提交的分支来说,过程可能比较繁琐。
rebase 有其特定的适用场景。如果不清楚其机制,无脑用 merge 是更安全的选择。通常,只在你自己一个人使用的、尚未推送到远程仓库的分支上,可以使用 rebase 来整理杂乱的 Git 历史(例如合并多个琐碎的提交,修改提交信息)。除此之外,绝大多数团队协作场景,使用 merge 更为稳妥。
小提示:git rebase -i 命令可以交互式地整理提交,例如调整提交顺序、合并多个小提交、改写提交信息等,是本地整理提交历史的利器。
一个至关重要的原则是:rebase 绝对不能在公共分支(如 master、develop)上使用! 因为这会重写公共分支的历史(改变 commit hash),其他协作者基于旧历史拉取代码后,会引发大量的混乱和冲突。在云栈社区的运维/DevOps/SRE板块,常有开发者分享因误用 rebase 导致团队协作问题的案例。
总而言之,在多人协作的开源项目或企业项目中,优先使用 merge 来合并分支是更符合协作精神、更不易出错的方式。而 rebase 则是一把锋利的“手术刀”,适合在本地分支上对提交历史进行精细化的“美容”,但在推送到远程并与他人协作前,需要格外谨慎。