Featured image of post Git rebase 和 merge:我在项目里怎么取舍

Git rebase 和 merge:我在项目里怎么取舍

记录 Git rebase 和 merge 在个人分支、共享分支和主干合入时的取舍,以及 force-with-lease、冲突处理和团队约定。

我以前对 rebasemerge 的态度比较摇摆。

刚开始写项目的时候,看到提交历史里一堆 Merge branch 'main' into feature/xxx 会觉得很乱,于是就偏向 rebase。后来在团队分支上踩过几次坑,又开始觉得 merge 稳一点。

现在我的习惯比较简单:自己的开发分支可以 rebase,已经给别人用了的分支尽量不要 rebase;合入主干时按项目约定来,不强行追求一种方式。

这件事我现在不再按“哪个更高级”来判断。rebase 和 merge 的区别不是水平高低,而是你愿不愿意改写历史、你需不需要保留分支关系、团队能不能接受这种历史形态。

merge 保留真实合并关系

merge 的特点是保留分支真实的合并关系。

1
2
3
4
5
6
git checkout main
git pull origin main

git checkout -b feature/login
git commit -m "add login api"
git commit -m "add login middleware"

这时 main 上别人也提交了东西,我想把最新 main 合进来:

1
2
3
git checkout feature/login
git fetch origin
git merge origin/main

如果两个分支都有新提交,通常会产生一个 merge commit。这个 merge commit 有时候看起来碍眼,但它也有价值:它明确记录了“这个分支是在这里和 main 合过一次”。

我一般会在这些场景用 merge

  • 公共分支,比如 maindeveloprelease/*
  • 多个人一起开发的长期 feature 分支。
  • 需要保留上下文的功能分支。
  • 项目本身要求使用 merge commit 保留 PR 合入记录。

缺点也明显:如果一个分支开发时间很长,又频繁 merge main,提交历史会比较吵。

1
git log --oneline --graph --decorate --all

所以我现在不会无脑 merge。短分支没必要反复 merge main,真有需要再同步。

rebase 适合整理自己的分支

rebase 更像是把当前分支上的提交“挪到”另一个基底后面。

1
2
3
git checkout feature/login
git fetch origin
git rebase origin/main

这样历史会更线性:

1
2
3
main:            A---B---C
feature before:      \---D---E
feature after:           \---D'---E'

注意这里的 D'E' 不是原来的提交了。内容可能一样,但提交哈希会变,因为父提交变了。

我一般会在这些场景用 rebase

  • 只有我自己在用的本地分支。
  • PR 提交前整理提交历史。
  • 把多个临时提交 squash 成更容易 review 的提交。
  • 避免 feature 分支里出现一堆没意义的同步主干 merge commit。

比如整理最近 3 个提交:

1
git rebase -i HEAD~3

可以把一些 pick 改成 squashfixup。这对代码评审会友好很多。

但 rebase 的代价也在这里:它让历史更好看,同时也改变了提交身份。只要这个分支已经被别人拉走,改写历史就不再只是你自己的事。

我的实际取舍

第一,本地分支优先 rebase

1
2
3
git checkout feature/foo
git fetch origin
git rebase origin/main

如果冲突不多,rebase 后历史干净,后面提 PR 也舒服。

第二,公共分支不要随便 rebase

比如一个分支已经推上去了,而且别人也基于它提交了代码,这时候 rebase 会改写历史。你本地看着清爽了,别人那边可能就开始分叉、冲突、重复提交。

这种情况我更倾向于:

1
2
3
git checkout shared/feature
git fetch origin
git merge origin/main

第三,合主干看团队约定

有的项目喜欢 merge commit,因为能看出每个 PR 的边界;有的项目喜欢 squash merge,让主干历史每个提交都对应一个完整变更;也有项目要求 rebase 后 fast-forward。

Git 历史是给人协作和排查问题用的,不是拿来展示洁癖的。

我见过比较舒服的团队约定是:

1
2
3
4
个人 feature 分支:允许 rebase,提 PR 前整理
多人共享分支:使用 merge,同步主干时不改写历史
主干合入:小 PR squash,大 PR 保留 merge commit
release 分支:尽量保留真实历史,方便追溯

这不是标准答案,但比“所有情况都 rebase”或“所有情况都 merge”更接近实际协作。

冲突和 force push

merge 冲突时:

1
2
3
4
5
git merge origin/main

git status
git add .
git commit

如果发现合错了:

1
git merge --abort

rebase 冲突时:

1
2
3
4
5
git rebase origin/main

git status
git add .
git rebase --continue

如果不想继续:

1
git rebase --abort

rebase 可能会让你连续处理多次冲突,因为它是按提交逐个重放。merge 通常是在一次合并里处理最终结果。

rebase 之后,如果这个分支之前已经 push 到远端,再 push 通常会失败:

1
git push origin feature/login

如果确认这个分支只有自己在用,我会用:

1
git push --force-with-lease origin feature/login

我基本不用 git push --force--force-with-lease 会多做一层检查:如果远端分支已经被别人更新过,它不会直接覆盖。

风险提醒写直白一点:

  • 不要 rebase main
  • 不要 rebase 别人正在基于它开发的分支。
  • 不要在没看 git loggit status 的情况下 force push。
  • rebase 前如果心里没底,可以先打个临时备份分支。
1
git branch backup/feature-login-before-rebase

我的取舍并不复杂:想保留分支合并上下文,用 merge;想整理自己的提交历史,用 rebase;分支已经共享出去,就谨慎 rebase。

最后再补一句:Git 历史清爽当然好,但它首先要可恢复。每次准备 rebase 已经 push 过的分支前,我都会先确认 git status 是干净的,再看一眼 git log --oneline --graph。这两个动作比任何工作流口号都有用。

参考资料

本文采用 CC BY-NC-SA 4.0 协议,转载请注明出处。
记录工程实践和排障过程。
使用 Hugo 构建
主题 StackJimmy 设计