Git Rebase 的逻辑与动作图解
| 项目 | 内容 |
|---|---|
| 主题 | git rebase 的工作机制与使用边界 |
| 一句话本质 | rebase = 把一串提交摘下来,换到新的基底上逐个重放,生成内容相同但 hash 不同的新提交 |
| 核心推论 | rebase 不是"移动提交",而是"复制提交并丢弃原件"——这就是"重写历史"的准确含义 |
| 黄金法则 | 已推送的公共提交,永不 rebase |
先建立三个前提概念
理解 rebase 只需要三个事实,全部来自 Git 的对象模型:
- 提交(commit)是内容寻址的对象。 每个提交记录快照(tree)、父提交(parent)、作者信息和说明,hash 由这些内容共同决定。parent 一变,hash 必然变。
- 分支只是一个可移动的指针。
main、feature不"包含"提交,只是指向某个提交对象的标签。历史是从指针出发沿 parent 链回溯出来的。 - 分叉 = 两条链有共同祖先。
git rebase main的第一步就是找 merge base(共同祖先),确定"哪些提交是 feature 独有的"。
rebase 的三个动作
git rebase main(在 feature 分支上执行)实际做三件事:
- 找共同祖先 B,列出 feature 独有的提交序列 F、G;
- 把 F、G 逐个转成 patch 暂存(
-i模式下这一步交给你编辑:pick / squash / reword / drop); - 以 main 的顶端 D 为新 parent,按顺序重放这些 patch,生成新提交 F′、G′,最后把 feature 指针移到 G′。
关键点在第三步:F′ 与 F 的文件改动相同,但 parent 不同,因此 hash 不同——它们是不同的对象。旧的 F、G 变成悬空提交(dangling),等垃圾回收。这就是"rebase 重写历史"的准确含义:它没有改动任何已存在的提交(Git 对象本来就不可变),而是制造了一批新提交并假装它们才是历史。
重放是逐个提交进行的,这解释了 rebase 的冲突体验:冲突发生在"F 的 patch 贴不到 D 上"的那一刻,你解决的是每一个提交与当前基底的冲突,而不是两个分支最终状态的一次性三方合并。解决后 --continue 继续重放,--abort 整体回滚到 rebase 前的状态,--skip 丢弃当前这个 patch。
merge 与 rebase:同一个问题,两种历史观
两者都解决"把 feature 集成进 main",分歧在于历史的形状:
| 维度 | git merge | git rebase |
|---|---|---|
| 历史形状 | 保留分叉事实,多一个合并提交 M | 线性,看起来像从未分叉 |
| 原有提交 | 全部不动 | feature 独有提交被复制为新对象 |
| 冲突解决 | 一次性三方合并 | 逐提交重放,可能解决多次 |
| 适用场景 | 公共分支集成、需要保留协作事实 | 推送前整理本地提交、让 feature 跟上 main |
一个常见的搭配:feature 开发过程中用 git rebase main 保持与主线同步(自己的小提交被重放,代价只有自已承担),合回主线时由平台走 merge / squash merge。
黄金法则:永不 rebase 公共提交
推论链很短:rebase 生成新 hash → 远程的旧提交和本地的新提交是"内容相同的不同对象" → --force 推送后,队友本地的历史与远程分裂 → 下次合并出现重复提交和幽灵冲突。
所以界限划在推送上,而不是 rebase 本身:
- 本地从未推送的提交:随便 rebase,这正是它的主场;
- 推送前用
git rebase -i整理提交(squash 掉 wip、reword 说明、drop 实验),把"工作过程"改写成"可读的变更叙述"; - 已推送、别人可能基于它开发的分支:用 merge,或先全队协调再
--force-with-lease。
常用命令速查
git rebase main # 把当前分支独有提交重放到 main 顶端
git rebase -i HEAD~4 # 交互式整理最近 4 个提交
git rebase --onto main B feature # 把 feature 上 B 之后的提交搬到 main(摘一段换底)
git rebase --continue / --skip / --abort
git push --force-with-lease # 重写已推送分支时至少加上租约保护
--onto 是 rebase 的完全体形态:git rebase --onto <新基底> <起点> <分支>,意思是"把 <起点>..<分支> 这段提交重放到 <新基底> 上"。理解了"摘一段、换底、重放"这个模型,所有 rebase 变体都只是参数的排列。
结论
rebase 的全部逻辑可以压成一句话:Git 提交不可变,所以"移动"只能是"复制"——rebase 把你的提交在新基底上重新提交了一遍,hash 全新,原件作废。 记住这一句,线性历史的好处、逐提交冲突的体验、--force 的危险、黄金法则的边界,全都是它的直接推论。