在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

变基

变基把当前分支独有的提交按从旧到新的顺序,重放到另一个基点上。替代提交保留各次修改和消息,但父链接改变,得到新对象 ID。目标分支不移动。它适合整理或更新尚未共享的功能分支;他人已基于这些 ID 开发时,需要先协调历史重写。

可以把它看成将旧版本书上的两处修订依次抄到新版:不是移动旧页,而是在新版本的基础上创建替代记录。后一处修订可能依赖前一处,顺序不能倒置。

变基如何工作

假设 feature 与 main 有共同分叉点。Git 从 feature 末端向后遍历,收集 main 不能到达的提交。向后先遇到新提交,因此重放前要反转,保证从旧到新。

重放从目标末端开始。对每个旧提交,先比较其父快照与自身快照,得到修改,再应用到临时重放快照,写新树和新提交。新提交保留原作者信息和消息,父改变,提交者信息更新。父头部参与序列化哈希,所以即使补丁字节相同,ID 也会改变。

期间 HEAD 暂时分离,指向目标或最近一次重放提交。feature 名称仍指向原末端。全部修改成功后,才一次移动 feature 并重新附着 HEAD。这使中止有明确锚点:原分支一直保留旧末端。

旧提交不被编辑,也不立即删除。feature 移走后,它们可能不再由分支可达,但仍留在对象库,直到保留规则和垃圾回收处理。

真实 Git 可能识别上游已有的等价补丁,并将对应的干净 cherry-pick 从重放队列中省去。--reapply-cherry-picks 要求重新尝试。本课不扫描补丁等价性,始终排入共同基点之后的当前分支提交。

变基逐步演示

默认使用三个单行文件:app.py、README.md、config.json。

  1. commit base 创建 app.py=print("v1")、README=hello、config={"port":3000},对象存在后 main 才前进。
  2. branch feature 在相同基点建引用,HEAD 留在 main。
  3. switch feature 将 HEAD 附着到 feature。
  4. commit feature-app 把 app.py 从 print("v1") 改为 print("feature")。
  5. commit feature-v2 再改为 print("feature v2")。它要求前一提交产生的 print("feature") 已存在,若先重放这条就无法匹配。
  6. switch main 返回 main。
  7. commit main-config 只把 config 改为 {"port":8080}。main 有配置修改,feature 有两次其他修改,历史已分叉。
  8. switch feature 再次选择 feature。
  9. rebase main 找到共同基点,队列依次为 feature-app、feature-v2,临时分离 HEAD 从 main-config 开始。

第一次重放保留 main 的端口 8080,应用 app.py 的 print("feature"),创建新 feature-app。第二次终于能找到所需旧值,将其改为 print("feature v2"),创建新 feature-v2。倒序会让后一个修改找不到自己的输入状态。

两次成功后,feature 一次移到替代末端;main 留在 main-config;HEAD 重新附着 feature。两个旧提交仍存储,但已无分支可达。画面旧链淡出,带撇号的新链接在新基点上,映射表列出每个旧提交和替代提交。

变基的成本

设重放提交数为 k,计算和应用修改时检查的文件内容总量为 D。历史遍历与实际祖先范围成正比,当前普通单父模型的重放约为 O(D),最多写 k 个新提交及所需树、blob。

实际成本往往来自冲突。干净补丁可以自动应用,与目标编辑重叠的补丁则暂停,要求决定新的文件内容。长分支的一次大合并冲突,可能在重放中变成分散于多个原提交的小冲突,处理难度取决于提交组织。

私有功能分支在评审前更新基点、整理线性提交可用变基。团队已依赖原 ID 时,宜合并或先协调,因为重写会让其他仓库面对内容相近但 ID 不同的两套历史。

本课只模拟干净本地仓库、唯一基点、普通非交互变基、单父提交;补丁按固定单行文件的三方合并应用,是 Git 合并机制的简化版。省略合并提交、交互编辑、--onto、fork-point 启发式、子模块、大量重命名以及冲突后的继续操作。这些扩展不改变替代提交是新对象的事实。

边界情况

  • 目标已是当前祖先:本模型无需移植,不创建提交,不移动引用。
  • 当前是目标祖先:可直接把当前分支快进到目标,不需替代提交。
  • 首个补丁冲突:临时 HEAD 留在新基点,REBASE_HEAD 指向失败的旧提交,当前分支仍在原末端。
  • 成功一部分后冲突:临时 HEAD 指向最后替代提交,分支还未移动;git rebase --abort 可恢复原来的附着状态。
  • 中止后临时提交失去可达性,不保证对象字节立刻被删除。
  • 缺失或未诞生引用:目标或当前分支没有可解析提交,无法提供所需两个末端。
  • 补丁已在上游:重放后快照不变,则不写提交,直接丢弃该补丁,与 Git 默认的 --empty=drop 一致。
  • 上游已有等价干净 cherry-pick:真实 Git 可能跳过,--reapply-cherry-picks 改变该选择;本模型不模拟该扫描。

常见错误

  • 说变基“移动提交”。不可变旧对象被读取,新对象被创建。
  • 认为补丁和消息相同就 ID 相同。父字段改变,提交字节和哈希也改变。
  • 搞反方向。在 feature 上执行 git rebase main 重写 feature,不是把 main 放到 feature 上。
  • 把目标分支当输出。成功后移动当前分支,目标引用不动。
  • 认为冲突时分支已经指向半完成新链。部分进度由分离 HEAD 保存,分支仍指旧末端。
  • 重写团队依赖的提交后,假装 ID 未变直接推送。其他人必须协调两套不同对象的历史。