[控场AI]
· 14 分钟阅读· 7,197 字

Agent 分支落地与干净回滚:CI 冲突时的 Git 实战指南

Agent 分支落地与干净回滚:CI 冲突时的 Git 实战指南

面向 AI 编码代理的分支落地与干净回滚流程:优先 squash、记录基础分支、精确合并,并在共享分支上用 revert 回滚。

文章针对 AI 编码代理完成任务并合并后 CI 变红的常见场景,整理了一套从 git 官方文档、GitHub CLI 手册和 Linus Torvalds 关于 revert 错误合并的 how-to 中提炼的落地与回滚流程。核心思路是:落地方式应优先考虑回滚便利性而非历史美观。首先记录任务启动时的基础分支并用 gh-merge-base 配置持久化,落地前用 merge-base --is-ancestor 检查基础分支是否移动,避免在过时基础上得到「假绿」测试。其次,代理分支推荐用 squash 而非 merge commit,因为 squash 提交被 revert 后再次 squash 修复分支即可带回全部工作,而 merge commit 被 revert 后必须先 revert 那次 revert 再合并。第三,用 gh pr merge --match-head-commit 确保合并的正是 CI 测试过的提交。最后,一旦变更已推送到共享分支,应通过 revert + CI 流程回滚,而不是本地强制重置。

当 AI 编码代理(如 Claude Code)完成任务并将代码合并到发布分支后,CI 突然变红,这是许多开发者都遇到过的场景。更棘手的是回滚:如果你直接 revert 了一个合并提交,git 会认为那些提交已经被合并过,后续代理修复分支再合并时,只有新的修复会进来,其余工作仍处于被 revert 的状态。

本文基于 git 官方文档、GitHub CLI 手册以及 Linus Torvalds 关于 revert 错误合并的 how-to,整理出一套适用于任何 AI 代理的落地与回滚流程。

rss source: Land an Agent's Branch, Then Undo It Cleanly When CI Disagrees

第一步:记录任务启动时的基础分支

代理可能从 main、发布分支或队友的特性分支启动。一周后没人记得基础分支是哪个,热修复就可能错误地落在 main 而不是 release/2.4。

在创建代理工作树时记录基础分支。GitHub CLI 已经为此读取每个分支的配置键:当你没有传递 --base 时,gh pr create 会使用分支的 gh-merge-base 配置。设置一次,你和 gh 都能读回:

git worktree add -b agent/fix-login ../fix-login release/2.4
git config branch.agent/fix-login.gh-merge-base release/2.4

落地前检查基础分支是否在代理分叉后移动过。git merge-base --is-ancestor 在第一个提交是第二个提交的祖先时退出码为 0:

base=$(git config branch.agent/fix-login.gh-merge-base)
git fetch origin
if git merge-base --is-ancestor "origin/$base" agent/fix-login
then
  echo "$base has not moved"
else
  git log --oneline agent/fix-login.."origin/$base"
fi

如果日志打印出提交,说明基础分支在代理工作期间发生了移动。需要在代理的工作树中执行 rebase(git -C ../fix-login rebase "origin/$base")并重新运行测试。在过时基础上的绿色测试,其可信度远低于看起来的那样。

关于 git merge-base --is-ancestor 与「过时基础」

git merge-base --is-ancestor A B 用于判断提交 A 是否为提交 B 的祖先:是则退出码为 0,否则为 1(非 0 不代表错误,需与真正的失败区分)。在脚本里它常被用作「快照点是否仍然可达」的布尔判断。

所谓「基础分支移动了」指的是:代理从某个提交分叉出工作分支后,基础分支又前进了新的提交。此时代理的测试是在旧代码之上跑的,即使全绿也无法保证与最新基础分支的代码兼容——尤其是当别人改了同一文件或同一接口时。因此落地前必须检查祖先关系,一旦发现移动就 rebase 到最新 origin/$base,再重新跑测试。这一检查是防止「假绿」的关键闸门,比事后回滚便宜得多。

第二步:根据回滚方式选择 squash 还是 merge commit

大多数关于 squash 与 merge 的争论都围绕历史记录展开。对于代理工作,应该根据你需要的回滚方式来选择。

Squash 将整个分支作为一个普通提交放到基础分支上。git-merge 手册指出 --squash 会产生合并后的树,但不会「创建提交、移动 HEAD 或记录 $GIT_DIR/MERGE_HEAD」,因此需要你自己提交:

git switch release/2.4
git pull --ff-only
before=$(git rev-parse HEAD)
git merge --squash agent/fix-login
git commit -m "Fix flaky login check"

Merge commit 保留每个代理提交并记录连接点。--no-ff 会创建一个合并提交,即使合并本可以通过快进解决,因此落地始终是一个可以精确指向的提交:

before=$(git rev-parse HEAD)
git merge --no-ff agent/fix-login

保存 before 只需一行代码,却给你一个精确的回滚目标。合并还会将旧 tip 留在 ORIG_HEAD 中,但其他命令会覆盖它。

SquashMerge commit (--no-ff)
落地形式一个普通提交代理提交加一个合并提交
回滚(未推送)git reset --keep "$before"git reset --keep "$before"
回滚(已推送)git revert <sha>git revert -m 1 <sha>
revert 后重新落地再次 squash 修复后的分支先 revert 那次 revert

最后一行正是开头提到的陷阱。Squash 从不将代理的提交记录为已合并,因此对修复后分支再次 squash 会带回所有工作。而被 revert 的合并提交则不同,第四步会详细说明。

A branch line joining a main line, with a curved arrow looping back from the join point to show the undo path

Squash 的一个副作用:git branch -d 只删除「完全合并」的分支,而 squash 的分支永远不是。在基础分支上的 CI 变绿之前保留它,因为它是你再次落地时的来源。然后用 -D 删除。

--squash、--no-ff 与 ORIG_HEAD 的语义

git merge --squash 只把对方分支的改动整合进工作区和暂存区,不生成任何提交,也不更新 MERGE_HEAD 或 HEAD,所以必须手动 git commit。其结果是基础分支上出现一个「普通提交」,历史里不再保留「这一批改动来自哪个分支」的连接信息。

git merge --no-ff 则强制生成一个双亲的合并提交,即使本可以快进(fast-forward)。它的价值在于:合并点是一个明确、可被引用的提交,后续 git revert -m 1 可以精确地「撤销整条分支的整合」。

无论哪种方式,合并前执行 git merge 都会把当前 HEAD 写入 ORIG_HEAD,因此 ORIG_HEAD 可作为临时回滚目标;但它会被 rebase、reset 等命令覆盖,所以文章建议手动把 before=$(git rev-parse HEAD) 存进变量或文件。

第三步:从终端打开 pull request

如果基础分支是共享的,应该打开 pull request 而不是本地合并。CI 在 PR 上运行,在变更到达其他人之前完成验证。在代理的工作树中:

cd ../fix-login
git push -u origin agent/fix-login
gh pr create --base release/2.4 --head agent/fix-login \
  --fill --draft
sha=$(gh pr view --json headRefOid -q .headRefOid)
gh pr checks --watch --fail-fast

gh pr create 将 --base 作为「你希望代码合并到的分支」。第一步中设置了 gh-merge-base 后,默认值已经正确。没有它的话,gh 会回退到仓库的默认分支,所以传递 --base 是廉价的保险。--fill 使用提交信息作为标题和正文,--draft 在 CI 表态之前不让审查者介入。gh pr view --json headRefOid 在检查开始前从 GitHub 读取 PR 的头部提交。gh pr checks --watch 等待检查,--fail-fast 在首次失败时停止。

变绿后,标记为就绪并精确合并 CI 测试过的那个提交:

gh pr ready
gh pr merge --squash --match-head-commit "$sha"

--match-head-commit 在 PR 头部不再是那个 SHA 时拒绝合并。由于你在检查运行前从 GitHub 读取了 sha,它就是检查所针对的提交,而不是你本地检出当前持有的任何内容。代理推送很快,这可以阻止未经测试的提交在你的检查和合并之间溜进来。如果基础分支使用合并队列,gh pr merge 会启用自动合并或将 PR 添加到队列。

--match-head-commit 与 headRefOid

在 GitHub 上,PR 的「头部提交」是 headRefOid,即分支当前指向的 SHA。CI 检查是针对某个具体 SHA 跑的,而不是针对「分支」这个概念。代理推送非常快,可能在检查完成与合并之间又推入新提交,导致合并的其实是未测试的代码。

gh pr merge --match-head-commit <sha> 正是为此设计的防篡改机制:如果合并时 PR 头部不再等于给定 SHA,命令会拒绝合并。配合先读取 gh pr view --json headRefOid,就能保证「检查所针对的提交」与「最终合并的提交」是同一个。这与 GitHub 的合并队列(merge queue)配合时尤为有用,因为队列会在合并前做最后一次检查。

第四步:基础分支 CI 变红,根据落地方式回滚

选择与实际情况匹配的行。

本地合并、未推送。 重置到你保存的提交。--keep 在文件有本地更改时会中止而不是覆盖:

git reset --keep "$before"

Squash 已推送到基础分支。 Revert 那一个提交并推送。这是基础分支上的一个普通新提交:

git revert <squash-sha>
git push

PR 已在 GitHub 上合并。 较新的 gh 版本有 gh pr revert,它从终端打开一个 revert pull request。GitHub 文档对 Web UI 描述相同:revert「创建一个新的 pull request,回滚原始合并提交」。你需要写权限,revert 像任何其他 PR 一样经过 CI:

gh pr revert 123 --title "Revert login fix: CI red"

Merge commit 已推送到基础分支。 使用 -m 1 让 git 知道基础分支是主线:

git revert -m 1 <merge-sha>
git push

然后是开头的陷阱。在 Linus Torvalds 的 revert-a-faulty-merge how-to 中,他解释了 revert 之后「合并仍然存在,仍会被视为将两个分支连接在一起」。当代理在其分支上修复后,先 revert 你的 revert,然后再次合并:

git revert <revert-sha>
git merge --no-ff agent/fix-login

该 how-to 添加了一个例外。如果分支是从头重建而不是在顶部修复,跳过 revert 那次 revert。这是更倾向于为代理分支使用 squash 的另一个原因:squash revert 不需要后续步骤。

第五步:在 Hermes IDE 中的相同流程

Hermes IDE 并排运行 Claude Code 和其他 CLI 代理,每个任务在自己的 hermes/<name> 分支和自己的工作树中。它的 Land 步骤覆盖了第一步到第四步的基础分支、落地和本地回滚部分。根据 README 和发布说明:

  • 一步完成提交、打开 pull request 或本地合并。
  • 它落地到任务启动时的分支。这覆盖了第一步,无需配置键。
  • 你自己的检查可以决定任务何时完成。
  • 已落地的任务可以撤销。已落地任务的撤销数据在 14 天后清理(2.2.9),所以撤销适用于落地后的几天内。

Three doorways side by side, marked only by icons: a single commit dot, a pull request arrow and a merge fork, with a small curved undo arrow above them

来自 Hermes 的推送使用你自己的 git 凭据助手或 GITHUB_TOKEN 登录。

工作纯 git 和 ghHermes
记住基础分支gh-merge-base 配置任务启动时的分支
落地前测试你的 CI,gh pr checks你自己的检查决定任务何时完成
落地merge --squash 或 gh pr createLand:提交、PR 或本地合并
撤销本地落地reset --keep "$before"落地后撤销

一个限制:无论你用什么工具落地,一旦变更在共享分支上且其他人已经拉取,建议用第四步的方式回滚,即通过 CI 的 revert。Hermes 免费,基于 BSL 1.1 源代码可用,运行在 macOS、Windows 和 Linux 上,与 Nous Research、Anthropic、OpenAI 或任何代理供应商无关。

关于 Hermes IDE 与 BSL 1.1

Hermes IDE 是一款面向 AI 编码代理的并行工作环境:它为每个任务创建独立的 hermes/<name> 分支和 git 工作树(worktree),从而让多个 CLI 代理(如 Claude Code)互不干扰地并行运行。这直接对应文章第一步强调的「记录基础分支」——因为工作树本身就绑定了分叉点,所以无需额外配置键。

其授权方式是 BSL 1.1(Business Source License 1.1):源码可获取,但商业使用受限于许可人设定的附加条款;通常在一段变更日期后会转成开源许可证。它免费,但并非传统意义上的 OSI 开源。它的「Land」与「Undo」覆盖了本地落地与撤销,而一旦变更进入共享分支,仍应回到第四步用 CI 流程回滚。

核心要点

对于代理工作,选择落地方式时应该优先考虑回滚的便利性而非历史记录的美观。Squash 是代理分支的首选,因为 revert 一个 squash 提交不需要后续的 revert-the-revert 步骤。无论使用什么工具,一旦代码在共享分支上,回滚应该通过 CI 流程完成,而不是本地强制重置。

#AI编码代理#git回滚#CI冲突#Claude Code#gh CLI#merge commit revert#squash merge
分享:

相关推荐