bigywater.tech
Git · 50

交互式 rebase 的 todo 列表

交互式 rebase 的 todo 列表里的每个命令到底做了什么?改顺序、squash、fixup、edit、exec、autosquash 各自的结果是什么,又有哪些提交会生成新 hash?这篇用脚本编辑 todo(而不是真的打开编辑器)逐个实测。

8 个任务
--exec 把 4 个提交扩成 4 个 pick 加 4 个 exec,共 8 个任务
fixup vs squash
都合并进上一个提交;squash 拼接两份说明,fixup 只保留上一个提交的说明
前缀不变
只有第一个被修改的提交及之后的提交才有新 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 与 fixup

实测 · 本机执行输出
  [squash] 提交数 4(原来 5);合并后那个提交的完整说明:'A 加功能 /  / B 修 bug';它的 tree 里有: a f1 f2
  [fixup] 提交数 4(原来 5);合并后那个提交的完整说明:'A 加功能';它的 tree 里有: a f1 f2

都把提交的改动合并进它上面的那个提交。squash 把两份说明拼起来交给你编辑,fixup 直接丢掉被合并提交的说明。

edit 与 reword

实测 · 本机执行输出
  $ 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 只让你改说明。

exec

实测 · 本机执行输出
  $ 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 会在每个提交后插入一个。失败就停下,适合逐个提交跑测试。

autosquash

实测 · 本机执行输出
  $ 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。

hash

实测 · 本机执行输出
  A 加功能      30880d5 → 30880d5  没变
  B 修 bug    669626b → 669626b  没变
  C 写文档      76c516a → 0dd55b2  新 hash
  D 调样式      8276f49 → dc37dfa  新 hash

rebase 会把没被改动的开头一段直接复用,所以第一个被修改的提交之前的提交 hash 不变;它自己和之后的都因为内容或 parent 变化而得到新 hash。

分步演示

交互式 rebase 的 todo
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。todo 的编辑不是手工的:GIT_SEQUENCE_EDITOR 指向我写的小 Python 脚本(按提交说明找行,改命令或调顺序),提交说明的编辑用 GIT_EDITOR 指向另一个脚本写入固定文字;rebase 的作者与时间固定,所以 hash 可复现(输出中 7 位 hash 为实验中的值)
    • 没有验证:label / reset / merge 命令与 --rebase-merges;break 命令;fixup -C / -c 的用法;rebase.autoSquash 配置的默认值;squash 时编辑器里的完整注释文本;autostash;--root
    京ICP备2026067502号-1京公网安备11010502063487号