
相同目标,不同历史
git merge 和 git rebase 都将一个分支的更改集成到另一个分支。区别在于它们如何处理提交历史——这种区别对可读性、调试和协作有实际影响。

Git Merge:保留历史
git merge 创建一个新的提交来连接两个分支的历史。它是非破坏性的:没有现有提交被更改。
合并前:
main: A --- B --- C
feature: \ D --- E
将 main 合并到 feature 后:
main: A --- B --- C
feature: \ D --- E --- M (合并提交)
\___________/
M 是合并提交,有两个父提交:E(feature 的顶端)和 C(main 的顶端)。所有历史都按原样保留。
git checkout feature
git merge main
# 或:git merge main --no-ff # 即使可以快进也强制创建合并提交
快进合并: 如果 main 没有分叉(自 feature 分支以来没有新提交),git 可以直接向前移动指针而不创建合并提交:
之前:
main: A --- B
feature: \ C --- D
执行 git checkout main && git merge feature(快进)后:
main: A --- B --- C --- D
feature: \________/ (现在与 main 相同)
使用 --no-ff 总是创建合并提交,保留功能分支存在的事实。
Git Rebase:线性历史
git rebase 将一个分支的提交重放到另一个分支之上。它重写历史——提交获得新的 SHA。
之前:
main: A --- B --- C
feature: \ D --- E
在 feature 分支上执行 git rebase main 后:
main: A --- B --- C
feature: \ D' --- E' (D 和 E 被重新应用到 C 之上)
D' 和 E' 具有与 D 和 E 相同的更改,但父提交不同(因此 SHA 也不同)。分支历史现在是线性的——就好像你一直是从 C 分支出来的。
git checkout feature
git rebase main
处理冲突
当相同行被不同方式更改时,两种方法都需要解决冲突。
合并期间:
git merge main
# CONFLICT:解决文件
git add resolved-files
git merge --continue
# 或:git merge --abort (取消合并)

变基期间:
git rebase main
# 在提交 D 上冲突:解决文件
git add resolved-files
git rebase --continue
# 在提交 E 上冲突:再次解决
git add resolved-files
git rebase --continue
# 或:git rebase --abort (取消,回到变基前状态)
变基冲突可能出现多次——每个被重放的提交一次。使用合并,你只需在合并提交中解决所有冲突一次。
交互式变基:清理历史
git rebase -i(交互式)允许你在分享之前重写、压缩、重新排序和编辑提交:
git rebase -i HEAD~4 # 交互式变基最后 4 个提交
这会打开一个编辑器:
pick a1b2c3 Add user authentication
pick d4e5f6 Fix typo in auth module
pick g7h8i9 Add token refresh logic
pick j0k1l2 WIP: fix edge case
# 命令:
# p, pick <commit> = 使用提交
# r, reword <commit> = 编辑提交信息
# e, edit <commit> = 停止以修改
# s, squash <commit> = 合并到前一个提交
# f, fixup <commit> = 合并,丢弃日志信息
# d, drop <commit> = 删除提交
你可以更改为:
pick a1b2c3 Add user authentication
fixup d4e5f6 Fix typo in auth module ← 压缩到前一个
pick g7h8i9 Add token refresh logic
drop j0k1l2 WIP: fix edge case ← 删除此提交
结果:干净的两个提交历史,拼写修复被吸收到原始提交中,WIP 提交消失。
变基的黄金法则
永远不要变基已经推送到共享分支的提交。
当你变基时,你在重写历史——创建具有新 SHA 的新提交。如果队友基于你的原始提交工作,他们的 git 历史会与你的不同。合并会变得一团糟。
# ❌ 如果其他人已经拉取了 main,不要这样做
git checkout main
git rebase feature # 重写 main 历史
# ✅ 变基在这里是安全的
git checkout feature
git rebase main # 重写 feature(尚未共享或强制推送到你自己的分支)
何时可以强制推送? 当你在自己的功能分支上工作,且没有其他人跟踪时:
git rebase -i HEAD~3 # 清理你的功能分支
git push --force-with-lease origin feature # 比 --force 更安全:如果远程已更新则失败
--force-with-lease 比 --force 更安全:如果自上次拉取后远程已更新,它会失败,防止意外覆盖。
比较表
| 方面 | Merge | Rebase |
|---|---|---|
| 历史形状 | 非线性(分支) | 线性 |
| 提交 SHA | 不变 | 重写 |
| 合并提交 | 是(通常) | 否 |
| 冲突解决 | 一次,在合并提交中 | 每个提交一次 |
| 在共享分支上安全使用 | ✅ 是 | ❌ 否 |
| 适合功能分支 | ✅ 是(no-ff 合并) | ✅ 是(推送前) |
| 使用 git bisect 调试 | 可行 | 更好(线性) |
| CHANGELOG 可读性 | 将功能显示为一个单元 | 更难看到功能 |
团队工作流
功能分支工作流(合并)
# 开始功能
git checkout -b feature/user-auth main
# 工作,提交,工作,提交
git commit -m "Add login endpoint"
git commit -m "Add JWT validation"
# 完成后合并回
git checkout main
git merge --no-ff feature/user-auth -m "Merge feature/user-auth"
git branch -d feature/user-auth
这保留了功能作为一个单元在历史中。适合查看哪些提交属于哪个功能。
合并前变基(干净历史)
# 开始功能
git checkout -b feature/user-auth main
# 工作,工作,工作(混乱的提交可以)
git commit -m "WIP auth"
git commit -m "fix"
git commit -m "more fixes"
# 合并前,用变基清理
git rebase -i main # 压缩 WIP 提交,修复信息
git rebase main # 更新到最新的 main
# 现在合并——一个干净的提交或几个逻辑提交
git checkout main
git merge feature/user-auth
主干开发(压缩合并)
许多团队使用 --squash 合并功能分支,将所有功能提交合并为一个:
git checkout main
git merge --squash feature/user-auth
git commit -m "Add user authentication (#142)"
main 总是每个功能有一个提交。历史干净,但失去了细粒度的功能提交。
何时使用每种方法
使用 merge 时:
- 将长期存在的功能分支合并回 main
- 在其他人跟踪的共享分支上工作
- 你想保留事件发生的确切顺序
- 代码审查在 PR 级别进行(合并整个功能)
使用 rebase 时:
- 在 PR 之前用最新的 main 更新功能分支
- 在代码审查前清理混乱的 WIP 提交
- 你想要一个线性、可读的 git 日志
- 独自在尚未共享的功能分支上工作
→ 使用 Git Memo 工具 参考常见 git 命令。