Git Rebase 还是 Merge?为你的团队选择正确的策略
1. 概述
git merge 和 git rebase 都用来在两个分支之间集成变更,但它们创建的提交历史截然不同。选错策略会导致日志混乱、难以审查,甚至更糟——破坏同事的历史。本文通过具体示例帮助你理解每条命令的机制,知道何时应该 merge、何时应该 rebase,以及在团队中使用 rebase 时如何避免最常见的错误。
2. 示例背景
假设你正在从 main 创建 feature/payment 分支进行开发。在你编码期间,同事已经将几个提交合并到 main。
git log --oneline --graph --all
* c3f9a1d (main) 添加 PaymentValidator
* 7b2e4aa (main) 修复 OrderService bug
| * 9d1a0ef (feature/payment) 添加 PaymentService.processPayment()
| * 4f3c2bb (feature/payment) 初始化 PaymentService
|/
* a1b2c3d 基础共同提交
两个分支从 a1b2c3d 开始分叉。现在你想将 feature/payment 更新到与 main 同步,然后再创建 pull request。有两种方法:merge 或 rebase。
3. Git Merge:保留历史,创建合并提交
3.1. 用法
git checkout feature/payment
git merge main
此命令创建一个新的合并提交,它有两个父提交:feature/payment 的最后一个提交和 main 的最后一个提交。
* e5f6a7b (HEAD -> feature/payment) Merge branch 'main' into feature/payment
|\
| * c3f9a1d (main) 添加 PaymentValidator
| * 7b2e4aa (main) 修复 OrderService bug
* | 9d1a0ef 添加 PaymentService.processPayment()
* | 4f3c2bb 初始化 PaymentService
|/
* a1b2c3d 基础共同提交
3.2. 特点
- 两个分支的原始历史保持 100% 不变,没有任何提交被修改。
- 多了一个“垃圾”合并提交,记录两个分支汇合的时刻。
- 对已推送的分支绝对安全——不会反向影响任何人。
注意:
git merge不会重写任何已存在提交的 SHA。这就是为什么它总是对共享分支安全的原因。
4. Git Rebase:重写历史,创建线性分支
4.1. 用法
git checkout feature/payment
git rebase main
Rebase 从分叉点 (a1b2c3d) 开始,逐个获取 feature/payment 上的提交,然后将它们“重放”到 main 的最顶端。结果是完全新的提交,SHA 也不同。
* 2a9e1cc (HEAD -> feature/payment) 添加 PaymentService.processPayment()
* d8b4f0a 初始化 PaymentService
* c3f9a1d (main) 添加 PaymentValidator
* 7b2e4aa (main) 修复 OrderService bug
* a1b2c3d 基础共同提交
现在历史是一条直线(linear history),没有任何合并提交。
4.2. 特点
9d1a0ef和4f3c2bb不再存在——被2a9e1cc和d8b4f0a取代,它们有新的 SHA。- 日志干净,易于阅读,易于
git bisect。 - 如果分支已经被其他人拉取就很危险,因为他们保留的旧提交已“消失”。
5. 冲突处理:操作中最大的差异
使用 merge,冲突(如果有)只需在合并提交处解决一次:
git merge main
# CONFLICT (content): Merge conflict in PaymentService.java
# ... 编辑文件,解决冲突 ...
git add PaymentService.java
git commit
使用 rebase,如果有多个提交,冲突可能在重放过程中的每个提交都发生:
git rebase main
# CONFLICT in commit 4f3c2bb
# 编辑文件,解决冲突:
git add PaymentService.java
git rebase --continue
# 如果下一个提交也有冲突则重复
如果想停止并返回到 rebase 之前的状态:
git rebase --abort
注意: Rebase 一个有 10 个提交的分支有时意味着你可能需要解决 10 次冲突——这就是为什么在 rebase 前进行
squash(见第 7 节)非常有用。
6. 黄金法则:不要 rebase 已共享的分支
这是使用 rebase 时最重要的原则。
# main 分支已被 5 个其他人拉取
git checkout main
git rebase feature/old-stuff # ❌ 极其危险
git push --force origin main
因为 rebase 为每个提交创建新的 SHA,在 push --force 后,任何拉取过旧 main 的人将有与远程分叉的历史。他们下次拉取时会出错或产生大量重复提交。
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
如果必须在 rebase 后进行 force push,使用 --force-with-lease 而不是 --force 来避免覆盖他人的提交:
git push --force-with-lease origin feature/payment
--force-with-lease 会检查自你上次 fetch 以来是否有人向远程推送了新提交,如果有则命令失败而不是盲目覆盖。
黄金法则: 只 rebase 个人分支,未共享的或只有你在处理的分支。永远不要 rebase main、develop 或任何共享分支。
7. 交互式 rebase:在合并前清理历史
Rebase 的一个非常实用的应用是在创建 pull request 之前清理个人分支上的“垃圾”提交。
git checkout feature/payment
git rebase -i HEAD~3
编辑器打开:
pick 4f3c2bb 初始化 PaymentService
pick 9d1a0ef 添加 PaymentService.processPayment()
pick 1c8f3aa fix typo
将后两行改为 squash 以合并到第一个提交中:
pick 4f3c2bb 初始化 PaymentService
squash 9d1a0ef 添加 PaymentService.processPayment()
squash 1c8f3aa fix typo
保存后,Git 会要求你编写新的提交消息。结果:3 个零碎提交变成 1 个干净、有意义的提交——非常适合在打开 PR 前做,这样审查者不用读“fix typo”、“wip”、“oops forgot import”之类的提交。
8. 实用策略:结合两者
大多数高效团队不会绝对选择其中一个,而是根据情况灵活组合:
| 情况 | 应该使用 |
|---|---|
用最新的 main 更新个人分支 |
rebase |
| 打开 PR 前清理零碎提交 | rebase -i (squash) |
合并已审查的 feature 分支到 main |
merge(通常通过 PR,可使用 squash 合并) |
| 分支被多人处理/拉取 | merge,绝对不用 rebase |
| 需要保留集成时刻的审计记录 | merge |
实际中的常见工作流程:
# 1. 打开 PR 前:rebase 到 main 以更新,避免后期冲突
git checkout feature/payment
git rebase main
# 2. 如需要则清理提交
git rebase -i main
# 3. 推送个人分支(安全,因为只有你使用)
git push --force-with-lease origin feature/payment
# 4. 通过 PR 合并到 main(使用合并提交或 squash 合并,不要 rebase main)
这种方式使 feature/payment 的日志干净、线性、易于在 PR 中审查,同时 main 保持安全,因为它只接收来自已批准 PR 的合并提交(或 squash 提交)。
9. Squash 合并:中间方案
许多平台(GitHub、GitLab)都有“Squash and merge”按钮——将 PR 的所有提交合并为一个单独的提交,然后合并到 main。
git checkout main
git merge --squash feature/payment
git commit -m "添加支付处理功能 (#42)"
结果:main 只有一个新提交,没有合并提交,如果你还没删除分支,feature/payment 的历史(在远程上)保持不变。这是使用 PR workflow 的团队最常见的选择,因为它既能保持 main 的日志干净,又不会改动原始历史。
10. 常见错误
-
Rebase 后正常 push(不用
--force):Git 会报错,因为历史已分叉。不要用普通的git pull来“修复”——它会创建不期望的合并提交,破坏你刚才 rebase 的线性历史。 -
忘记正在 rebase 的分支是共享分支:始终检查
git log origin/<branch>看是否有其他人依赖了旧提交。 -
Rebase 后解决冲突但忘了
--continue:rebase 状态仍然“悬空”,git status会清楚地提醒你正在 rebase 中间。 -
在个人分支上连续使用
merge导致日志中充满小的合并提交,难以阅读——应该用 rebase 更新,而不是多次将main反向合并到你的分支。 -
过度 squash:将不相关的多个变更合并到一个提交中,使
git bisect和部分git revert变得无用。
11. 结论
merge 安全,保留真实历史,适合共享分支。rebase 创建干净、线性的历史,但重写 SHA,所以只能用在未共享的个人分支上。实际上,你不需要绝对选择一方:rebase 来更新和清理个人分支,通过 PR 使用 merge(或 squash 合并)来集成到 main。记住黄金法则——永远不要 rebase 他人依赖的分支——你就能避免团队中大多数 Git 相关的麻烦。