今天在收拾一串 fixup 提交的时候踩了个坑,顺手记一下。场景是用 git rebase -i --autosquash 合并补丁提交,Git 打印了 Successfully rebased and updated,我以为完事了,回头一看 log,那条 fixup! 提交还完整地躺在历史里。这篇笔记讲清楚两件事:为什么这条成功提示不能当作验证依据,以及在真正改写历史之前,怎么先看到 fixup 实际挂到了哪条提交上。
先把坑复现一遍
复现条件是 rebase 写的范围没有盖住 fixup 指向的那条目标提交,比如目标提交落在更早的位置,而你写的范围从较新的提交开始:
git rebase -i --autosquash <base>
# Successfully rebased and updated 'my-branch'
提示语和真正成功时一模一样,没有警告,没有「本次未处理某些提交」的说明,退出码也是 0。→ 我第一次遇到时压根没想过要怀疑这条提示,是隔了几天翻 log 才发现不对,中间还以为是自己命令敲错了。这个坑我也是绕了两次才躲开。
更阴的一种:合错了对象
后来翻到 dev.to 上一篇讲这类问题的文章,评论区有人补了一个更隐蔽的场景,我自己照着搭了一遍确实能复现:当范围内有两条提交共享相同的主题前缀时,--autosquash 会按部分主题匹配,把 fixup 静默合并到较旧的那条上——哪怕你创建 fixup 时用的是目标提交的完整 SHA。也就是说历史确实被改写了,只是改错了对象,事后翻 log 看到的不是遗留的 fixup!,而是一条内容挂错了位置的干净历史,这种比明着遗留更难发现。原理上 --autosquash 是靠提交主题里的前缀来配对的,这一点我在文档里翻了一圈,也没找到足够醒目的说明。
我原本的检查方式是下面这条,输出为空就当作没问题:
git log --grep='fixup!' origin/main..HEAD
但在前缀冲突的场景里它是「假阴性」:fixup 确实不在了,只是它被合进了错误的提交。这条检查我用了一阵子,看完别人的实验记录才死心,这篇笔记把检查方式一并换掉。
看计划,别看提示
拆开看看验证这一步怎么补。交互式 rebase 会把整理好的提交清单交给「序列编辑器」,让这个编辑器只打印清单、然后故意失败退出,Git 就会在写任何对象之前中止:
GIT_SEQUENCE_EDITOR='cat "$1"; false' git rebase -i --autosquash <base>
打印出来的就是 fixup 实际挂载的位置,哪条提交被排在哪一步,直接对着清单核对。💡 这里有个我差点栽进去的坑:一开始没搞懂为什么非要带那半句 false,后来才想明白——我最初写的是 GIT_SEQUENCE_EDITOR=cat,cat 读完清单自然退出 0,而退出 0 在 Git 眼里等于「计划已批准」,它会照着这份计划真的改写历史。幸好当时是在一个专门用来做实验的分支上,这条记下来了,下次应该用得上。
试下来比较稳的流程是这么几步,多花的工夫都花在核对上:
git rev-parse HEAD记下动手前的哈希;- 用上面那句带
false的预览命令打印计划,核对 fixup 实际挂到哪条提交; - 确认没问题,去掉环境变量真正执行 rebase;
- 再
git rev-parse HEAD一次,对比前后两个哈希。
退出码也靠不住
写到一半我又回头折腾了一轮退出码,结论是它比我想的还不稳。同一句预览命令,在不同 shell、不同管道写法下会分别返回 0、1 或者 141,受 PIPESTATUS、pipefail、stderr 重定向的影响,换台机器跑一遍结果都可能不一样。0 这个值尤其麻烦,因为它和「批准计划」在语义上正好撞车。所以最后我不再依赖任何退出码,只用上面第 1 步和第 4 步的前后哈希对比:两个哈希相同,说明历史一点没动,无论刚才那行提示说了什么;不同,再回头核对这次的改动是不是你想要的那种不同。
顺手把 rerere.autoupdate 关了
顺着同一个思路,我把自己的 rerere.autoupdate 也关了。这个开关开着的时候,被重放的冲突解法会自动暂存,git status 里文件显示成 M,看起来一切顺利;关掉之后文件保持 UU,你必须自己过一眼这个解法确实适用于当前这次冲突,再手动暂存。rerere 的本意是记住你的解法,但「记住」和「替你拍板」是两回事,autoupdate 就属于替你拍板的那一类——和我这次踩的坑是同一个味道,工具把「我已经处理好了」演给你看,而中间没有留出人能介入的窗口。这一段我也是翻了不少文档才把两种状态的行为对齐清楚,不装一开始就想明白了。
划重点
划重点:第一,Successfully rebased and updated 只说明 rebase 这个过程结束了,不说明 autosquash 按你的意图合并了,范围没盖住目标提交时它照样打印成功;第二,grep fixup! 不是可靠的检查,前缀冲突场景下是假阴性,要看就看 rebase 计划本身;第三,预览命令里那半句 false 不能省,省了就变成批准计划;第四,退出码在管道环境下不可复现,前后对比 HEAD 哈希才是稳定的验证。这篇就记到这里,你可以挑一个手头带 fixup 的分支,先用预览命令打一遍计划,看看 fixup 挂的位置和你心里想的是不是同一个,如果踩到别的坑,欢迎回来留言,我一起补一条笔记。
