Git Rebase 的逻辑与动作图解

项目 内容
主题 git rebase 的工作机制与使用边界
一句话本质 rebase = 把一串提交摘下来,换到新的基底上逐个重放,生成内容相同但 hash 不同的新提交
核心推论 rebase 不是"移动提交",而是"复制提交并丢弃原件"——这就是"重写历史"的准确含义
黄金法则 已推送的公共提交,永不 rebase

先建立三个前提概念

理解 rebase 只需要三个事实,全部来自 Git 的对象模型:

  1. 提交(commit)是内容寻址的对象。 每个提交记录快照(tree)、父提交(parent)、作者信息和说明,hash 由这些内容共同决定。parent 一变,hash 必然变。
  2. 分支只是一个可移动的指针。 mainfeature 不"包含"提交,只是指向某个提交对象的标签。历史是从指针出发沿 parent 链回溯出来的。
  3. 分叉 = 两条链有共同祖先。 git rebase main 的第一步就是找 merge base(共同祖先),确定"哪些提交是 feature 独有的"。

rebase 的三个动作

git rebase main(在 feature 分支上执行)实际做三件事:

  1. 找共同祖先 B,列出 feature 独有的提交序列 F、G;
  2. 把 F、G 逐个转成 patch 暂存-i 模式下这一步交给你编辑:pick / squash / reword / drop);
  3. 以 main 的顶端 D 为新 parent,按顺序重放这些 patch,生成新提交 F′、G′,最后把 feature 指针移到 G′。

git rebase 的动作

关键点在第三步:F′ 与 F 的文件改动相同,但 parent 不同,因此 hash 不同——它们是不同的对象。旧的 F、G 变成悬空提交(dangling),等垃圾回收。这就是"rebase 重写历史"的准确含义:它没有改动任何已存在的提交(Git 对象本来就不可变),而是制造了一批新提交并假装它们才是历史

重放是逐个提交进行的,这解释了 rebase 的冲突体验:冲突发生在"F 的 patch 贴不到 D 上"的那一刻,你解决的是每一个提交与当前基底的冲突,而不是两个分支最终状态的一次性三方合并。解决后 --continue 继续重放,--abort 整体回滚到 rebase 前的状态,--skip 丢弃当前这个 patch。

merge 与 rebase:同一个问题,两种历史观

两者都解决"把 feature 集成进 main",分歧在于历史的形状:

merge vs rebase 与黄金法则

维度 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 的危险、黄金法则的边界,全都是它的直接推论。

AI Assistant
Selected Text