写这篇文章的契机,源于一次内部技术分享会上的讨论。merge和rebase这两个指令都用于合并分支代码,rebase能将分支记录变成一条直线,而merge则保留了分支的树状图。我个人更习惯使用merge,因为在大部分开发场景中,我很少需要用到rebase。
我们先来看一个开发中非常常见的场景:两个人基于 master 分支的同一个提交节点,各自拉出一个新的 feature 分支。其中,开发者 A 有多个提交,并率先完成任务,将代码合入了 master;开发者 B 也有多个提交,但在他完成任务准备合入 master 时,发现有冲突。我在这里模拟的每个提交都是针对同一个文件进行的,因此每个提交都可能产生冲突。
在 A 合并到 master 之前,分支图如下:

可以看到,feature/A 分支和 feature/B 分支都是基于 master 分支最新的提交(docs: add README)拉出的,两条分支并行开发。为了模拟时间线,我用顺序数字表示提交信息,两条分支各自新增了 3 个提交。
现在,我们通过 merge 把 A 合并到 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 就产生了。
merge 是否会创建新的合并节点,取决于 Git 是否需要执行一次“非快进式”合并。
- 快进式合并(fast-forward)
当 master 分支的指针还停留在你分支起点的那个提交时。比如 feature/A 合入到 master 的时候,master 和 feature/A 的起点相同,这就属于快进式合并,不会产生新的合并节点。
- 非快进式合并
当 master 分支相较于你的分支有了新的提交时。比如 feature/B 的起点是最初的 docs: add README,但在 feature/B 准备合入 master 时,master 的最新提交已经是 feature/A 的 docs: 5 了。在这种情况下,即使没有代码冲突,非快进式合并也会产生一个新的合并提交节点。
好了,我们现在撤消 feature/B 到 master 的 merge 操作,改用 rebase 来看看有什么不同。
撤消之后,分支图恢复到如下状态,master 仍然停留在 docs: 5:

现在使用 rebase:
git checkout feature/B
git rebase master
可以看到,编辑器提示需要解决冲突,而这还只是第一个提交产生的冲突:

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

可以看到,最终的 Git 提交历史变成了一条直线,看起来确实整洁不少。并且,你的分支所有提交的“时间线”都被提前了,不会产生合并提交节点。但是,你分支上原本的提交哈希值(如 docs: 3, docs: 4, docs: 6 对应的哈希值)都发生了变化。
结论
从 Git 提交记录看
- merge:保留了原始的历史记录,是非线性的,容易产生合并提交节点。
- rebase:将自己的提交记录“移植”到目标分支的最新提交之后,并更改你之前提交的
commit hash,历史是线性的,不会产生多余的合并提交节点。
从解决冲突的方式看
- merge:在最终合并的那一刻,一次性解决所有冲突,直接对比两个分支代码的最终形态。
- rebase:会按顺序逐个重放你的所有提交,但凡有冲突的提交,都需要你依次去解决。
rebase 有其适用的场景,但如果不清楚其后果,无脑使用 merge 是更稳妥的选择。一般来说,只有在你独自使用的、且尚未推送到远程仓库的分支上,可以使用 rebase 来整理提交历史。其余所有情况,尤其是在团队协作的共享分支上,都建议使用 merge。
重要警告:绝对不能在公共分支(如团队共用的 develop 或 master 分支)上使用 rebase,因为这会改变公共分支上已有的提交哈希值,导致其他协作者拉取代码时出现混乱和冲突。
至于 git rebase -i,它是一个强大的工具,可以用来整理提交顺序、合并多个小提交或改写提交信息,但这通常用于本地分支的个人提交整理。
希望这篇基于具体场景的对比,能帮助你在日常的 Git 工作流中,做出更合适的选择。如果你有更多关于版本控制或 DevOps 实践的想法,欢迎到技术社区交流探讨。
