下午就要合一个 bug 修复,它偏偏和一处你想先吵一架的重构躺在同一个提交里。Agent 给这个提交起的名字叫Update checkout,动了五个文件。其中两个修的是客户今天就能看见的格式 bug,还配了测试;另外三个把写死的税率挪到接口后面,顺手把测试也重写了。这个提交只给你两个按钮:全要,或者全不要。
![]()
写代码的 agent 很容易走到这一步——任务说的是"清理 checkout",它一路上碰过的东西就全进了一个提交。放在以前,想把它拆开只能上交互式 rebase:标记edit、reset、分块重新暂存、提交两次、continue。
Git 2.54(2026 年 4 月发布)加了一条实验性命令专门干这个:git history split。你像用git add -p那样挑 hunk,Git 把它们写进一个新的父提交,所有建在其上的分支被移到重写后的历史上。没有待办清单,也不会卡在 rebase 进行中。
下面用一个一次性仓库复现:造一个 agent 风格的巨型提交,把它拆开,再用reword改消息、用fixup修补一个旧提交。
先把烂摊子故意造出来
split 和 reword 需要 Git 2.54 或更新,fixup 需要 2.55。用git --version确认。文件是几个小 PHP 类,但你不需要装 PHP 或 PHPUnit——Git 只认文本。
先在 main 上放起点。Money::format有个真 bug:1205 分输出成 12.5,因为分位没有补零。初始化仓库、建好 src 和 tests 目录、提交一个 "Initial checkout"。
然后扮演 agent。在任务分支上,一个提交同时修了补零、加了回归测试、抽出TaxRate接口、让 Checkout 依赖它、重写 Checkout 测试。接着第二个更小的提交落在上面,消息什么也没说。跑git show --stat HEAD~1,问题一目了然:五个文件、一条消息、两件不相干的事。
动手重写之前,先打个标签记住起点:git tag before-split。用标签,别用分支。默认情况下 git history 会移动所有从被重写提交派生出来的本地分支,放在顶端的备份分支会被一起挪走;而标签会留在你放它的地方。
用 git history split 拆开
把 split 指向那个巨型提交。它在顶端下面一个,正是以前需要 rebase 的场景:git history split HEAD~1。
Git 按路径顺序遍历这个提交的 hunk,逐个询问,按键和git add -p一样。y把 hunk 移进新提交,n留在原地,d连同该文件剩下的部分一起留下。对这个提交来说,选择是:
- src/Checkout.php:d
- src/Money.php:y
- src/TaxRate.php:d
- tests/CheckoutTest.php:d
- tests/MoneyTest.php:y
然后编辑器会打开两次,两次都从 agent 的原始消息开始。第一次是给拆出来的那个提交——你挑中的 hunk,它成为新的父提交,写Fix zero-padding in Money::format。第二次是给留在原提交里的部分,写Extract TaxRate port from Checkout。作者身份保留原提交的作者。
如果两件事本来就分在不同文件里,用 pathspec 就能省掉按 d 的功夫,只有匹配的文件会被拿出来,其余留在原提交:git history split HEAD~1 -- src/Money.php tests/MoneyTest.php。
注意你没拆出去的东西:Checkout 测试的重写。旧测试调用new Checkout()不带参数,构造函数一旦需要 TaxRate 就会立刻崩。一个同时装着重构和旧测试的提交会让测试套件变红。重写后的测试要跟着逼它改的那处改动走。按"评审者会一起通过"的标准给 hunk 分组。
一个提交进去,两个提交出来:修复成为父提交,重构坐在它上面。接着确认什么都没丢:git log --oneline main..agent/checkout和git diff before-split agent/checkout。日志显示三个提交,原来是两个;diff 应该什么都不打印。split 只是重新分配 hunk,顶端代码不变。
第一次在真实分支上做?加上--dry-run。Git 会写出新对象但不移动任何 ref,并按git update-ref能读的格式打印 ref 更新。只想移动当前分支,就传--update-refs=head。
不用 rebase 的 reword 和 fixup
那个 "more fixes" 提交还在顶上。git history reword会在编辑器里打开任意提交的消息并就地重写,不碰工作区和索引,甚至在裸仓库里也能用。把它改成Document the TaxRate dependency in README。对 HEAD 来说,git commit --amend效果一样;reword 是给三四个提交之前、amend 够不着的地方用的。
然后评审发现一个缺口:修复没有覆盖金额小于 1.00 的测试。Git 2.55 加了git history fixup,把你暂存的改动折进更早的提交,再把后面的重放上去:git history fixup 'agent/checkout^{/Fix zero}'。这个^{/Fix zero}后缀会找到分支上消息匹配的最新提交。修复提交保留自己的消息和作者;想改消息就加--reedit-message。Git 2.56 加了git history drop,删掉一个提交并把它的后代重放到其父提交上。
如果 agent/checkout 已经推过,这些和任何 rebase 一样都是重写:用git push --force-with-lease推。想整个丢掉这次练习,在分支上执行git reset --hard before-split就回去了。
git history 拒绝做的事
手册页对限制写得很直白,而且大多是有意为之:
- 不碰含合并提交的历史,那种情况用git rebase --rebase-merges
- 任何会以合并冲突收尾的操作都会被拒绝。历史重写不该把你留在半成品状态,所以它直接中止,而不是中途停下。
- Git 钩子"在当前时间点"不会运行。如果 commit-msg 钩子检查消息格式,它看不到你在 split 期间输入的消息。
- 源码调用 hunk 选择器时禁用了编辑,所以不提供e。当一处修复和一处重构共享同一个 hunk,试试s把它切成更小的 hunk,或者退回git rebase -i
全选或全不选也是无效的,因为两个提交里会有一个是空的。截至 2.56,整条命令仍标记为实验性,并警告行为可能变化。要重排一段范围或把它移到新基底上,git rebase仍是那个工具;git history 一次只覆盖一处有针对性的编辑。
挑哪些 hunk 该在一起,是拆分里最难的部分,而 agent 的单个提交不会告诉你它按什么顺序干活。如果 agent 在一个回合里做了补零修复、下一个回合做重构,按回合看就能在你敲 pathspec 之前把路径给你。要是在 agent 提交之前就打开评审台,还能逐个文件暂存和提交,那就没什么可拆的了。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.