交互式 rebase 的 todo 列表里的每个命令到底做了什么?改顺序、squash、fixup、edit、exec、autosquash 各自的结果是什么,又有哪些提交会生成新 hash?这篇用脚本编辑 todo(而不是真的打开编辑器)逐个实测。
$ GIT_SEQUENCE_EDITOR='cat' git rebase -i HEAD~4 2>&1 | sed -E 's/^(pick|p) [0-9a-f]{7} /\1 <sha> /' | head -22
pick <sha> # A 加功能
pick <sha> # B 修 bug
pick <sha> # C 写文档
pick <sha> # D 调样式
# Rebase f9b607e..8276f49 onto f9b607e (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]git rebase -i 先生成一份 todo:每个提交一行“命令 hash # 说明”,注释里是命令速查。你编辑这份清单,git 按它从上往下重放。
$ git log --format=%s --reverse | tail -n +2 | tr '\n' ' '; echo ' ← rebase 之前' A 加功能 B 修 bug C 写文档 D 调样式 ← rebase 之前 $ git log --format=%s --reverse | tail -n +2 | tr '\n' ' '; echo ' ← todo 里把 A 挪到 B 后面、C 改成 drop' B 修 bug A 加功能 D 调样式 ← todo 里把 A 挪到 B 后面、C 改成 drop
todo 里行的顺序就是新历史的顺序:把 A 挪到 B 后面、把 C 改成 drop,结果是 B、A、D。被丢掉的提交的改动不再出现;后面的提交如果依赖它,重放时会冲突。
[squash] 提交数 4(原来 5);合并后那个提交的完整说明:'A 加功能 / / B 修 bug';它的 tree 里有: a f1 f2 [fixup] 提交数 4(原来 5);合并后那个提交的完整说明:'A 加功能';它的 tree 里有: a f1 f2
都把提交的改动合并进它上面的那个提交。squash 把两份说明拼起来交给你编辑,fixup 直接丢掉被合并提交的说明。
$ git status | head -4 | sed -E "s/[0-9a-f]{7}/<sha>/g"; git log -1 --format=%s
interactive rebase in progress; onto <sha>
Last commands done (2 commands done):
pick <sha> # A 加功能
edit <sha> # B 修 bug
B 修 bug
$ git log --format=%s --reverse | tail -n +2 | tr '\n' ' '; echo; git show --stat --format= HEAD~2 | tail -2
A 加功能 B 修 bug C 写文档 D 调样式(新说明)
f2 | 1 +
2 files changed, 2 insertions(+)edit 在这个提交停下来,你可以改文件、git commit --amend 或者拆成多个提交,再 git rebase --continue;reword 只让你改说明。
$ git rebase -i --exec 'test ! -f f3 || (echo 测试失败:f3 存在 && false)' HEAD~4 2>&1 | grep -E 'Executing|Rebasing|测试失败|stopped|error' | head -6
Rebasing (2/8)
Executing: test ! -f f3 || (echo 测试失败:f3 存在 && false)
Rebasing (3/8)
Rebasing (4/8)
Executing: test ! -f f3 || (echo 测试失败:f3 存在 && false)
Rebasing (5/8)
Rebasing (6/8)
Executing: test ! -f f3 || (echo 测试失败:f3 存在 && false)
测试失败:f3 存在
warning: execution failed: test ! -f f3 || (echo 测试失败:f3 存在 && false)
$ git status | head -3 | sed -E "s/[0-9a-f]{7}/<sha>/g"; git rebase --abort
interactive rebase in progress; onto <sha>
Last commands done (6 commands done):
pick <sha> # C 写文档exec 是在重放序列里运行一条 shell 命令,--exec 会在每个提交后插入一个。失败就停下,适合逐个提交跑测试。
$ git log --format=%s -1
fixup! A 加功能
$ GIT_SEQUENCE_EDITOR=cat git rebase -i --autosquash HEAD~5 2>&1 | grep -E '^(pick|fixup)' | sed -E 's/^(pick|fixup) [0-9a-f]{7} /\1 /'
pick # A 加功能
fixup # fixup! A 加功能
pick # B 修 bug
pick # C 写文档
pick # D 调样式git commit --fixup=<提交> 生成 “fixup! 说明” 的提交;--autosquash 把它自动挪到目标后面并标成 fixup。
A 加功能 30880d5 → 30880d5 没变 B 修 bug 669626b → 669626b 没变 C 写文档 76c516a → 0dd55b2 新 hash D 调样式 8276f49 → dc37dfa 新 hash
rebase 会把没被改动的开头一段直接复用,所以第一个被修改的提交之前的提交 hash 不变;它自己和之后的都因为内容或 parent 变化而得到新 hash。