[控场AI]
· 9 分钟阅读· 4,622 字

用 git history split 拆分 AI 编程代理的巨型提交

用 git history split 拆分 AI 编程代理的巨型提交

Git 2.54 的 git history split 让拆分 AI 代理的巨型提交像 git add -p 一样简单。

AI 编程代理常把多个不相关的改动塞进同一个提交,过去只能靠交互式 rebase 逐一分解。Git 2.54 引入实验性命令 `git history split`,用与 `git add -p` 相同的按键挑选 hunk,把选中内容写成新的父提交,并自动迁移从旧提交派生的本地分支。配套的 `reword`(2.54)、`fixup`(2.55)、`drop`(2.56)分别覆盖消息改写、折叠进较早提交、删除提交并重放后代等场景。备份应打 tag 而非 branch,因为分支会被一并迁移。该命令不处理合并提交、拒绝以冲突收尾的操作、不运行 githooks,截至 2.56 仍是实验性功能。

AI 编程代理一次性交出五文件、两件事的 Update checkout 提交,是很多人最近都遇到的场景。你想今天下午合入的格式化 bug 修复,和你想先争论一番的重构,被塞进了同一个 commit 里。Git 2.54 引入的实验性命令 git history split 正是为这种情况准备的:它让你像 git add -p 一样挑选 hunk,把选中的内容写成一个新的父提交,并自动把建立在旧提交之上的所有本地分支迁移到改写后的历史上。没有待办清单,没有中途停下的 rebase。

rss source: Split a Coding Agent's Giant Commit With git history split (Git 2.54+)

问题:一个提交,两种意图

代理很容易落入这个陷阱。任务说"清理 checkout",它沿途碰到的一切就都进了同一个提交。git show --stat HEAD~1 会告诉你问题所在:五个文件、一条消息、两个互不相关的变化。

其中两个文件修复用户今天就能看到的格式化 bug,并加上回归测试;另外三个把硬编码的税率挪到接口后面,并重写相关测试。这个提交只给你两个按钮:全要,或者全都不要。

在 git history split 之前,拆开它意味着交互式 rebase:标记 edit、reset、分块重新暂存、提交两次、continue。Git 2.54 把这个流程压缩成一条命令。

Hunk(代码块) 是 git diff 输出的基本单位:Git 会把每个文件中连续的改动聚成一段,附上若干行上下文,形成一个可以直接接受或拒绝的补丁块。git add -p 和 git history split 都以 hunk 为粒度提问,而不是以文件或整行为粒度。

需要注意,hunk 的边界由 Git 的 diff 启发式算法决定,并不总与语义边界一致。一次格式化修复和一次逻辑重构如果在同一片代码区域相邻出现,就可能被合并进同一个 hunk——这正是文中提到需要按 s 把它拆成更小 hunk 的原因。

用 git history split 拆分提交

把 split 指向那个巨型提交。当它位于 tip 下方一层时,正是过去需要 rebase 才能处理的场景:

git history split HEAD~1

Git 按路径顺序遍历提交的 hunk,用与 git add -p 相同的按键逐一询问。回答 y 把 hunk 移入新提交,n 保持原位,d 则连同该文件的其余 hunk 一起留在原处。

选取完成后,编辑器会打开两次。两次都以代理的原始消息为起点:第一次对应拆分出去的提交(即新的父提交),第二次对应原提交保留的部分。作者信息保持原提交的作者不变。

当关注的改动位于不同文件时,路径规格可以在不按 d 的情况下直达目标:

git history split HEAD~1 -- src/Money.php tests/MoneyTest.php

只有匹配的文件会被提供,其余留在原提交里。

这里有个容易被忽略的判断点:不要拆出 Checkout 测试的重写。旧测试调用无参构造函数,一旦构造函数需要 TaxRate 就会立刻失败。把重构和旧测试放进同一个提交,会让测试套件变成红色。重写后的测试必须跟随迫使它改变的那次改动。按"审阅者会一起批准什么"来对 hunk 分组。

拆分后验证没有内容丢失:

git log --oneline main..agent/checkout
git diff before-split agent/checkout

日志显示三个提交取代了原来的两个,而 diff 应当什么都不输出。根据官方文档,原提交"保持完整,只是其父提交变为新拆分出的提交",所以 tip 处的代码不变。

首次在真实分支上操作时,加上 --dry-run。Git 会写入新对象但不移动任何 ref,并打印出 git update-ref 可以读取的引用更新。若只想移动当前分支,传 --update-refs=head。

用 tag 而非 branch 做备份

改写前先打一个标签标记起点:

git tag before-split

这里的选择有讲究。默认情况下 git history 会移动所有从被改写提交派生出的本地分支,因此放在 tip 的备份分支会连同一并被移走。文档只谈到移动分支,所以标签会留在你放置的位置。

--dry-run 与 --update-refs 是两个降低风险的关键参数。--dry-run 会完成全部对象写入和引用计算,但不动任何 ref,只打印出将要执行的 git update-ref 命令,你可以先审阅再真正执行。

--update-refs=head 则把自动迁移的范围收窄到当前所在的单个分支上。默认行为是移动所有从被改写提交派生出的本地分支,这在有多个并行工作分支时可能超出预期,因此当你只想改写自己手头这条线时,显式指定该参数更安全。

不用 rebase 的 reword 与 fixup

顶部的 more fixes 提交依然在。git history reword 会在编辑器中打开任意提交的消息并就地改写。它不触碰工作树和索引,甚至在裸仓库里也能工作。把 more fixes 换成 Document the TaxRate dependency in README 只需一条命令:

git history reword HEAD

对于 HEAD 本身,git commit --amend 效果相同。reword 的价值在于三四个提交之前、amend 够不到的位置。

随后审阅发现一个缺口:修复缺少针对小于 1.00 金额的测试。Git 2.55 新增的 git history fixup 会把已暂存的改动折叠进较早的提交,并把其后的提交重放到上面:

git add tests/MoneyTest.php
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 即可。

它明确拒绝做什么

官方 man page 直接列出了限制,其中大部分是刻意的设计:

  • 不碰含合并提交的历史。这类情况请用 git rebase --rebase-merges。
  • 任何会以合并冲突结束的操作都会被拒绝。历史改写不该把你留在半完成状态,所以它直接中止而不是中途停下。
  • Githooks 不会在"当前时间点"运行。如果 commit-msg 钩子检查消息格式,它看不到你在拆分过程中输入的消息。
  • hunk 选择器以禁用编辑的方式调用,因此不提供 e。当一次修复和一次重构共享同一个 hunk 时,可以试试 s 把它拆成更小的 hunk,或者退回 git rebase -i。

全选或全不选同样是无效的,因为那样两个提交中会有一个为空。截至 2.56,整个命令仍标记为实验性,官方警告行为可能变化。

需要重排一段提交范围或整体搬到新基座时,git rebase 依然是正确的工具。git history 覆盖的是"一次一个目标明确的编辑"。

Githooks 是 Git 在特定事件(如 pre-commit、commit-msg、post-commit)时自动调用的脚本,常用于校验提交消息格式、跑 lint 或触发 CI。通常它们在每次 git commit 时按顺序触发。

git history split 在内部以编程方式生成提交对象,跳过了这些钩子,因此依赖 commit-msg 校验的工具不会看到拆分过程中输入的两次消息。如果团队的合规检查依赖钩子,拆分完成后需要手工核验提交消息,或另行运行校验脚本。

拆分之前,先看清代理的每一步

选择哪些 hunk 应该放在一起,是拆分中最难的部分;而代理留下的单个提交,并不会告诉你它工作的顺序。

The Hermes Review Desk in By turn view: two turns (T1: 3 files, T2: 1 file), turn 1's diff of src/db.ts, and a commit message drafted from the turns.

Hermes IDE 让每个agent(Claude Code、Codex 等)在自己的终端里运行,并保存每一个 agent 轮次,因此你可以对任意单轮做 diff 或恢复。Review Desk(macOS 上 ⌘G,Windows 和 Linux 上 Ctrl+Shift+G)按轮次或按文件展示这些改动,并附带风险标记。

如果代理在第一轮做了补零修复、在第二轮做了重构,按轮次视图会在你敲下命令之前就展示出 git history split 所需的路径规格。如果在代理提交前就打开 Review Desk,你可以从那里按文件暂存并提交,根本不需要拆分。对某一轮留下的行内评论会回到写下它的代理,这样"把测试重写和重构放在一起"的建议就能抵达正确的终端。

需要说明的是,Hermes IDE 是本文作者所在的项目,阅读相关段落时请据此权衡。它免费、源码可见(BSL 1.1),支持 macOS、Windows 和 Linux,与 Nous Research、Anthropic、OpenAI 或其他代理厂商均无关联。

#git history split#Git 2.54#AI编程代理#提交拆分#git rebase#git add -p#代码审查#git history reword
分享:

相关推荐