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 相关的麻烦。

gitrebasemergeversion-controlworkflow