rebase 把提交“搬”到新的基础上,原来的提交去哪了?中途停下来时 git 把进度记在哪里?为什么有些提交会被悄悄跳过?这篇用一个小例子把 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 m3rebase 做的事:找出 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 还存在吗: Falserebase 中途停下时,.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,不改写任何提交。