bigywater.tech
Git · 49

rebase 的实现:原 commit 去哪了

rebase 把提交“搬”到新的基础上,原来的提交去哪了?中途停下来时 git 把进度记在哪里?为什么有些提交会被悄悄跳过?这篇用一个小例子把 rebase 拆开,逐项对账。

3 → 3
rebase 之后 3 个提交都有了新的 hash(parent 变了),旧的 3 个仍可通过 ORIG_HEAD 和 reflog 找到
range-diff
把新旧两组提交一一配对,= 表示改动相同、只是基础变了
patch-id
rebase 靠它识别上游已经有的等价补丁并跳过

重放

实测 · 本机执行输出
  $ git log --format='%s  author=%an  committer=%cn' main..feature
  F3  author=Alice  committer=Alice
  F2  author=Alice  committer=Alice
  F1  author=Alice  committer=Alice
  $ git log --graph --format='%s  author=%an  committer=%cn' main..feature; git log --oneline -1 main | sed -E 's/^[0-9a-f]+ /main 的末端: /'
  * F3  author=Alice  committer=Bob
  * F2  author=Alice  committer=Bob
  * F1  author=Alice  committer=Bob
  main 的末端: M3
  旧提交与新提交对应(同样的改动,不同的 parent,所以 hash 不同):
    旧F1 → 新F1   tree 相同? False  parent: M1 → M3
    旧F2 → 新F2   tree 相同? False  parent: 旧F1 → 新F1
    旧F3 → 新F3   tree 相同? False  parent: 旧F2 → 新F2
  新提交的 tree 都和旧的不同,因为每个新提交都建立在 M3 之上,tree 里多了 m2、m3(以第 1 个为例):
   旧F1 的文件: a f1 | 新F1 的文件: a f1 m2 m3

rebase 做的事:找出 upstream..分支 的提交列表,把每个提交的改动在新基础上重新应用(本质上是一次次 cherry-pick),分支指针最后移到新的末端。新提交的作者不变,提交者与时间是现在的,parent 链和 tree 都不同,所以 hash 全部是新的。

旧提交

实测 · 本机执行输出
  ORIG_HEAD 指向旧F3: True;有多少个分支包含旧F3: 0
  $ git reflog -8 --format='%gd: %gs'
  HEAD@{0}: rebase (finish): returning to refs/heads/feature
  HEAD@{1}: rebase (pick): F3
  HEAD@{2}: rebase (pick): F2
  HEAD@{3}: rebase (pick): F1
  HEAD@{4}: rebase (start): checkout main
  HEAD@{5}: checkout: moving from main to feature
  HEAD@{6}: commit: M3
  HEAD@{7}: commit: M2
  $ git range-diff main ORIG_HEAD feature
  1:  c58861a = 1:  15e5082 F1
  2:  e30e803 = 2:  19aa103 F2
  3:  7f32687 = 3:  04039c8 F3

旧提交不会立刻消失:ORIG_HEAD 指向旧的末端,reflog 里有 rebase (start)、每个 rebase (pick) 和 rebase (finish)。没有分支包含它们,所以只能通过这些间接引用找到;git range-diff 能把新旧提交一一对应起来。

状态目录

实测 · 本机执行输出
  $ git rebase main 2>&1 | head -3; ls .git/rebase-merge | tr "\n" " "; echo
  Rebasing (1/3)
  Rebasing (2/3)
  Auto-merging f2
  CONFLICT (add/add): Merge conflict in f2
  error: could not apply e30e803... F2
  author-script done drop_redundant_commits end git-rebase-todo git-rebase-todo.backup head-name interactive message msgnum no-reschedule-failed-exec onto orig-head patch r
  $ cd .git/rebase-merge; for f in head-name onto orig-head msgnum end; do printf "%-10s" $f; cat $f | sed -E "s/[0-9a-f]{40}/<sha40>/"; done; echo "--- git-rebase-todo:"; cat git-rebase-todo | sed -E "s/^(pick|p) [0-9a-f]+/\1 <sha>/"; echo "--- done:"; cat done | sed -E "s/^(pick|p) [0-9a-f]+/\1 <sha>/"
  head-name refs/heads/feature
  onto      <sha40>
  orig-head <sha40>
  msgnum    2
  end       3
  --- git-rebase-todo:
  pick <sha> # F3
  --- done:
  pick <sha> # F1
  pick <sha> # F2
  $ git status | sed -n 1,4p
  interactive rebase in progress; onto a0f5b01
  Last commands done (2 commands done):
     pick c58861a # F1
     pick e30e803 # F2
  $ GIT_EDITOR=true git rebase --continue 2>&1 | tail -1
  Rebasing (3/3)
  Successfully rebased and updated refs/heads/feature.
  完成后 .git/rebase-merge 还存在吗: False

rebase 中途停下时,.git/rebase-merge/ 记着:正在改写的分支(head-name)、新基础(onto)、旧末端(orig-head)、进度(msgnum、end)、已完成的 done 与待办的 git-rebase-todo。--continue 或 --abort 之后目录被删除。

跳过重复

实测 · 本机执行输出
  $ git rebase main 2>&1 | sed -E "s/[0-9a-f]{7,}/<sha>/g"; git log --format=%s main..feature
  warning: skipped previously applied commit <sha>
  hint: use --reapply-cherry-picks to include skipped commits
  hint: Disable this message with "git config set advice.skippedCherryPicks false"
  Rebasing (1/1)
  Successfully rebased and updated refs/heads/feature.
  F1
  $ git cherry -v main feature | sed -E "s/[0-9a-f]{40}/<sha40>/"
  + <sha40> F1

如果上游已经有等价的补丁(patch-id 相同),rebase 会跳过那个提交并警告;--reapply-cherry-picks 可以保留它。

无事可做

实测 · 本机执行输出
  $ git rebase main 2>&1 | head -2
  Current branch feature is up to date.

当前分支已经包含 upstream 的全部提交时,rebase 直接报告 up to date,不改写任何提交。

分步演示

rebase 的实现
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。提交时间固定,Bob 的 rebase 时间也固定,所以 hash 可复现;range-diff 一节显示的是实验中的 hash
    • 没有验证:rebase --interactive 的 todo 命令(见下一篇);--rebase-merges、--onto 的用法(--onto 在 merge-base 一篇出现过);rebase 对合并提交的处理;autosquash、autostash;rebase.backend 的 apply 与 merge 两种实现(本机默认 merge);空提交的处理
    京ICP备2026067502号-1京公网安备11010502063487号