Git fixup 与 autosquash:把后来的修正合回它真正属于的提交
一个分支先加配置,再写文档,随后发现第一条提交的默认值写错了。直接追加修复可以保留工作进度,但整理评审历史时,可能希望配置提交一开始就是正确版本,文档仍单独成一条。fixup 与 autosquash 可以把“修正属于哪里”明确写进提交整理流程。
普通 git commit --fixup 先产生一条新的修复提交,并不会立刻修改目标历史。之后的交互式 rebase 加上 --autosquash,才会把这条修复移到目标后面并折入目标。创建标记与实际改写,是两个独立步骤。
先证明创建 fixup 后仍有三条提交
将脚本保存为 demo.sh,用 Bash 执行。它在 Git 2.52.0、Bash 5.2.37 验证,只处理自动清理的临时仓库,不推送远程。例子使用不同标题和明确目标哈希,避免同名提交让读者难以判断修正指向哪一条。
AI概念示意图:用抽象图形说明本文主题,不代表真实界面或运行结果。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
lab=$(mktemp -d)
trap 'rm -rf -- "$lab"' EXIT
git -c init.templateDir= init -q -b main "$lab/repo"
cd "$lab/repo"
git config user.name Demo
git config user.email demo@example.invalid
git config commit.gpgSign false
printf 'project\n' > readme.txt
git add .
git commit -qm base
base=$(git rev-parse HEAD)
printf 'retries=1\n' > settings.txt
git add .
git commit -qm 'Add retry setting'
target=$(git rev-parse HEAD)
printf 'usage\n' > guide.txt
git add .
git commit -qm 'Add guide'
printf 'retries=3\n' > settings.txt
git add settings.txt
git commit -q --fixup="$target"
[[ $(git log -1 --format=%s) == 'fixup! Add retry setting' ]]
[[ $(git rev-list --count "$base..HEAD") == 3 ]]
[[ $(git show "$target:settings.txt") == retries=1 ]]
before_tree=$(git rev-parse 'HEAD^{tree}')
printf 'before=3\n'
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash "$base"
[[ $(git rev-list --count "$base..HEAD") == 2 ]]
[[ $(git log -1 --format=%s HEAD~1) == 'Add retry setting' ]]
[[ $(git show HEAD~1:settings.txt) == retries=3 ]]
[[ $(git log -1 --format=%s) == 'Add guide' ]]
[[ $(git rev-parse 'HEAD^{tree}') == "$before_tree" ]]
git log --reverse --format=%s "$base..HEAD"
printf 'after=2; tree unchanged; earlier setting=3\n'一次整理改变了什么
before=3 说明 fixup 只是新增第三条提交。该提交标题带有 fixup! 标记;前面的功能提交仍保留旧值 retries=1。执行自动折叠后输出 after=2,配置修正已经进入第一条功能提交,第二条仍是 Add guide,原来单独的修复提交不再属于这条整理后的分支历史。
代码不仅检查提交数量,还确认新的第一条提交已经包含 retries=3,同时保留原来的标题 Add retry setting。普通 fixup 保留目标的提交说明;若要替换说明,Git 另有 amend 或 reword 形式,应按实际目的选择,不要期待普通 fixup 自动保留修复提交的额外备注。
为什么脚本没有打开编辑器
GIT_SEQUENCE_EDITOR=true 只在这一条命令上生效,让演示直接接受 autosquash 生成的执行清单。真实项目第一次整理时,通常应打开清单检查目标和范围,确认该折到哪里,再继续。它不是让所有 rebase 都跳过审查的全局设置。
传入的 base 是功能开发之前的提交,所以目标和修复都包含在本次重放范围里。若起点选在目标之后,自动整理就缺少应当接收修复的那一条。检查范围比反复调整提交标题更重要,尤其是在分支上混有其他人的工作时。
历史更清楚,也要验证内容没有丢
整理前保存 HEAD 的树对象,整理后断言树标识相同,证明本例最终文件内容与目录结构没有变化;同时检查更早那条配置提交里的内容,证明修正确实被移到历史中合适的位置。这两类断言回答不同问题,不能只验证文件最终看起来对了。
重放会生成新的提交对象,后续提交的父关系也会改变。因此它适合尚未共享或团队已约定可以整理的分支。对于别人已经基于其继续开发的提交,先协调改写和后续同步方式;本教程的临时实验不包含强制推送步骤。
实际修正还可能依赖后来的代码:把它移回较早位置时,会产生冲突,甚至虽然能应用却不能独立运行。遇到冲突,应重新判断提交分组和依赖,而不是一律选择某一侧。整理之后仍需运行测试;重要项目还可以逐条检查提交是否可构建。
验收完成的标志应同时包括:每条修正归属明确,提交清单符合预期,最终状态正确,必要的测试通过。自动化帮你移动和折叠补丁,提交要表达的逻辑边界仍由作者负责。


