变基详解:rebase vs merge¶
本文你会学到:
- rebase 的核心原理("改写提交的父节点")
- merge vs rebase 的视觉差异
- 交互式 rebase:整理杂乱的提交历史
- rebase 的黄金法则:什么情况下绝对不能 rebase
🤔 为什么会有 rebase?¶
当你在功能分支工作了几天,main 分支已经向前走了好几步。你想把 main 的最新代码合进来,有两种方式:
graph LR
subgraph "初始状态"
A["E1"] --> A2["E2(main)"]
A --> A3["F1"] --> A4["F2(feature,HEAD)"]
end
classDef commit fill:transparent,stroke:#768390,color:#adbac7,stroke-width:1px
class A,A2,A3,A4 commit
方式一(merge):产生一个额外的合并提交 M:
graph TD
subgraph "merge(保留分叉历史)"
E1["E1"] --> E2["E2(main)"]
E1 --> F1["F1"] --> F2["F2(feature)"]
E2 --> M["M(合并提交)"]
F2 --> M
end
subgraph "rebase(线性历史)"
E3["E1"] --> E4["E2(main)"] --> F3["F1'"] --> F4["F2'(feature)"]
end
classDef commit fill:transparent,stroke:#768390,color:#adbac7,stroke-width:1px
classDef merge fill:transparent,stroke:#388e3c,color:#adbac7,stroke-width:2px
classDef newc fill:transparent,stroke:#539bf5,color:#adbac7,stroke-width:2px
class E1,E2,F1,F2,E3,E4 commit
class M merge
class F3,F4 newc
方式二(rebase):把 F1、F2 "移植"到 E2 之后,历史变成一条直线:
🔧 rebase 的工作原理¶
git rebase main 的实际步骤:
- 找到 feature 和 main 的**共同祖先**(E1)
- 把 E1 之后 feature 上的提交(F1、F2)暂存起来(成为"补丁")
- 把 feature 的起点移到 main 的最新提交(E2)
- 依次把补丁重新应用上去,生成 F1'、F2'(内容相同,但 SHA 不同)
graph LR
A["E1"] --> B["E2(main)"] --> C["F1'"] --> D["F2'(feature,HEAD)"]
classDef commit fill:transparent,stroke:#768390,color:#adbac7,stroke-width:1px
classDef new fill:transparent,stroke:#388e3c,color:#adbac7,stroke-width:2px
class A,B commit
class C,D new
| 基本 rebase 用法 | |
|---|---|
⚖️ merge vs rebase 对比¶
| merge | rebase | |
|---|---|---|
| 历史形状 | 有分叉 + 合并提交,保留真实历史 | 线性,干净整洁 |
| 是否改写 SHA | ❌ 不改 | ✅ 改(生成新的提交) |
| 冲突处理 | 一次性解决所有 | 逐提交解决(可能更清晰) |
| 适合场景 | 保留功能分支历史、公共分支合并 | 功能分支同步主线、清理本地提交 |
| 安全性 | 总是安全 | 已推送的分支使用危险 |
什么时候用哪个:
- 想保留完整分支历史 → merge
- 想保持线性历史(PR 前整理) → rebase
- 公共分支(main/develop)上 → 永远用 merge,不用 rebase
✏️ 交互式 rebase:整理提交历史¶
git rebase -i(interactive)是最强大的历史整理工具。它允许你在一系列提交上执行:合并、拆分、修改、重排、删除。
典型场景:合并零碎提交¶
开发过程中常常会有这样的提交历史:
用 rebase -i 整理成一个干净的提交:
rebase -i 的所有操作指令¶
| 指令 | 缩写 | 含义 |
|---|---|---|
pick |
p |
保留提交(不变) |
reword |
r |
保留提交,但修改提交信息 |
edit |
e |
暂停,允许你修改这个提交 |
squash |
s |
合并到前一个提交,并合并提交信息 |
fixup |
f |
合并到前一个提交,**丢弃**本次提交信息 |
drop |
d |
直接删除这个提交 |
exec |
x |
执行一条 shell 命令 |
| 常用组合示例 | |
|---|---|
⚠️ 黄金法则:rebase 的禁区¶
绝对不要 rebase 已经推送到公共仓库的提交
rebase 会改写提交的 SHA。如果你 rebase 了已经 push 到 main 或团队共享分支的提交,其他人的本地仓库里还有旧的 SHA,下次他们 pull 时会造成**历史分叉混乱**,Git 记录会变成两份相同改动的"平行历史"。
安全边界:
- ✅ 可以 rebase:只在本地、还没推送**的提交
- ✅ 可以 rebase:**自己独享的功能分支(没人基于它工作)
- ❌ 禁止 rebase:main、develop 等公共分支
- ❌ 禁止 rebase:已经有 PR 且其他人看过的提交(会让 review 历史失效)
如果已经推送了,强制 push 的代价:
🎯 rebase --onto:精确控制变基目标¶
当你需要把一段提交从一个地方"搬"到完全不同的地方时,--onto 参数非常有用。
场景:你在 feature-b(基于 feature-a)上开发,但 feature-a 被砍掉了,需要把 feature-b 的提交直接接到 main 上。
| --onto 用法 | |
|---|---|
| 另一个常见场景:只保留部分提交 | |
|---|---|
⚡ rebase 实用技巧¶
自动 stash(--autostash)¶
rebase 前要求工作区是干净的,--autostash 可以自动帮你 stash/unstash:
冲突时的三个选择¶
rebase 遇到冲突时,你有三个选择:
PR 前的标准整理流程¶
| 提 PR 前的完整整理流程 | |
|---|---|
🎯 最佳实践总结¶
下一篇「远程协作」将讲解如何与团队成员通过远程仓库协同工作。