我以前对 rebase 和 merge 的态度比较摇摆。
刚开始写项目的时候,看到提交历史里一堆 Merge branch 'main' into feature/xxx 会觉得很乱,于是就偏向 rebase。后来在团队分支上踩过几次坑,又开始觉得 merge 稳一点。
现在我的习惯比较简单:自己的开发分支可以 rebase,已经给别人用了的分支尽量不要 rebase;合入主干时按项目约定来,不强行追求一种方式。
这件事我现在不再按“哪个更高级”来判断。rebase 和 merge 的区别不是水平高低,而是你愿不愿意改写历史、你需不需要保留分支关系、团队能不能接受这种历史形态。
merge 保留真实合并关系
merge 的特点是保留分支真实的合并关系。
这时 main 上别人也提交了东西,我想把最新 main 合进来:
如果两个分支都有新提交,通常会产生一个 merge commit。这个 merge commit 有时候看起来碍眼,但它也有价值:它明确记录了“这个分支是在这里和 main 合过一次”。
我一般会在这些场景用 merge:
- 公共分支,比如
main、develop、release/*。 - 多个人一起开发的长期 feature 分支。
- 需要保留上下文的功能分支。
- 项目本身要求使用 merge commit 保留 PR 合入记录。
缺点也明显:如果一个分支开发时间很长,又频繁 merge main,提交历史会比较吵。
| |
所以我现在不会无脑 merge。短分支没必要反复 merge main,真有需要再同步。
rebase 适合整理自己的分支
rebase 更像是把当前分支上的提交“挪到”另一个基底后面。
这样历史会更线性:
注意这里的 D'、E' 不是原来的提交了。内容可能一样,但提交哈希会变,因为父提交变了。
我一般会在这些场景用 rebase:
- 只有我自己在用的本地分支。
- PR 提交前整理提交历史。
- 把多个临时提交 squash 成更容易 review 的提交。
- 避免 feature 分支里出现一堆没意义的同步主干 merge commit。
比如整理最近 3 个提交:
| |
可以把一些 pick 改成 squash 或 fixup。这对代码评审会友好很多。
但 rebase 的代价也在这里:它让历史更好看,同时也改变了提交身份。只要这个分支已经被别人拉走,改写历史就不再只是你自己的事。
我的实际取舍
第一,本地分支优先 rebase。
如果冲突不多,rebase 后历史干净,后面提 PR 也舒服。
第二,公共分支不要随便 rebase。
比如一个分支已经推上去了,而且别人也基于它提交了代码,这时候 rebase 会改写历史。你本地看着清爽了,别人那边可能就开始分叉、冲突、重复提交。
这种情况我更倾向于:
第三,合主干看团队约定。
有的项目喜欢 merge commit,因为能看出每个 PR 的边界;有的项目喜欢 squash merge,让主干历史每个提交都对应一个完整变更;也有项目要求 rebase 后 fast-forward。
Git 历史是给人协作和排查问题用的,不是拿来展示洁癖的。
我见过比较舒服的团队约定是:
这不是标准答案,但比“所有情况都 rebase”或“所有情况都 merge”更接近实际协作。
冲突和 force push
merge 冲突时:
如果发现合错了:
| |
rebase 冲突时:
如果不想继续:
| |
rebase 可能会让你连续处理多次冲突,因为它是按提交逐个重放。merge 通常是在一次合并里处理最终结果。
rebase 之后,如果这个分支之前已经 push 到远端,再 push 通常会失败:
| |
如果确认这个分支只有自己在用,我会用:
| |
我基本不用 git push --force。--force-with-lease 会多做一层检查:如果远端分支已经被别人更新过,它不会直接覆盖。
风险提醒写直白一点:
- 不要 rebase
main。 - 不要 rebase 别人正在基于它开发的分支。
- 不要在没看
git log、git status的情况下 force push。 - rebase 前如果心里没底,可以先打个临时备份分支。
| |
我的取舍并不复杂:想保留分支合并上下文,用 merge;想整理自己的提交历史,用 rebase;分支已经共享出去,就谨慎 rebase。
最后再补一句:Git 历史清爽当然好,但它首先要可恢复。每次准备 rebase 已经 push 过的分支前,我都会先确认 git status 是干净的,再看一眼 git log --oneline --graph。这两个动作比任何工作流口号都有用。