文章分析了撤销合并后再次合并的 Git 陷阱:原提交仍被视为已合并,后续可能只引入新增修复。内容围绕 Agent 分支的落地与回退展开,使用通用 Git 和 GitHub CLI 流程。
书籍:《Ship It — 务实的创业公司技术栈》
我的其他作品:《Thinking in Go》两册系列——《Go 编程完整指南》与《Go 中的六边形架构》
我的项目:Hermes IDE | GitHub——为使用 Claude Code 和其他 AI 编程工具交付代码的开发者打造的 IDE
关于我:xgabriel.com | GitHub
“撤销一个 merge commit,就意味着你声明自己永远不再需要这次合并带来的目录树变更。”这句话出自 git-revert 手册,也是合入 Agent 工作成果时,一个往往要到后面才会踩中的坑。
你让 Claude Code 为 release/2.4 做一个热修复。它在自己的 worktree 中完成工作,告诉你搞定了。你合并并推送了代码,结果发布分支上的 CI 挂了。于是,你撤销了这次合并,让 Agent 在它的分支上修好问题,然后再次合并。结果,只有 Agent 新增的修复被合了进来,其余工作仍然处于被撤销的状态,因为 git 依然认为那些 commit 已经合并过了。
到了这一步,Agent 的工作已经结束。剩下的事得由你负责:把代码合到哪里,以及出问题时怎么撤回。下面的操作只用到 git 和 GitHub CLI,因此适用于任何 Agent。最后一节会介绍如何在我开发的 Hermes IDE 中完成同样的流程。
Agent 的任务可能从各种分支开始:main、发布分支,或者同事的功能分支。一周之后,谁也记不清当初从哪个分支切出来的,结果热修复被合进了 main,而不是 release/2.4。
创建 Agent 的 worktree 时,就把基准分支记录下来。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
合入之前,检查基准分支自 Agent 创建分支以来是否有新提交。如果第一个 commit 是第二个 commit 的祖先,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
如果日志输出了 commit,就说明 Agent 工作期间,基准分支已经向前推进。在 Agent 的 worktree 中,把它的分支 rebase 到最新基准上(git -C ../fix-login rebase "origin/$base"),然后重新运行测试。基于过期基准跑出的绿色测试,说明不了你以为的那么多。
大多数关于 squash 和 merge 的争论,都围绕提交历史展开。对于 Agent 的工作,应该根据你需要怎样撤销来选择。
squash 会把整个分支的变更压成一个普通 commit,放到基准分支上。git-merge 手册说明,--squash 会生成合并后的目录树,但不会“创建 commit、移动 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 会保留 Agent 的每个 commit,并记录两个分支的汇合关系。--no-ff 会创建一个 merge commit,“即使这次合并原本可以通过 fast-forward 完成”。这样,每次合入都有一个明确的 commit 可以定位:
before=$(git rev-parse HEAD)
git merge --no-ff agent/fix-login
保存 before 只需要一行,却能给你一个精确的撤销目标。合并也会把原来的分支顶端保存在 ORIG_HEAD 中(见 git-reset),但其他命令可能覆盖它。
最后一种情况,就是开头提到的坑。squash 从来不会把 Agent 的那些 commit 记录为已合并,因此,对修复后的分支重新做一次 squash,就能把它的全部工作重新带回来。被撤销的 merge commit 则不同,第 4 步会详细说明。
撤销路径从分支汇合处开始:分支当初如何合入,决定了你应该如何撤回。

squash 还有一个副作用:git branch -d 只会删除“已完全合并”的分支,而经过 squash 的分支永远不满足这个条件。先保留它,等基准分支上的 CI 通过,因为如果需要重新合入,它就是你的代码来源。之后再用 -D 删除。
如果基准分支是共享分支,就创建 pull request,而不是在本地合并。这样,变更影响其他人之前,CI 会先在 PR 上运行。在 Agent 的 worktree 中执行:
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 指定“你希望将代码合入的分支”。如果在第 1 步设置了 gh-merge-base,默认值就已经正确。如果没设置,gh 会退回使用仓库的默认分支,所以显式传入 --base 是一份成本很低的保险。--fill 使用 commit 信息填充标题和正文,--draft 则让 PR 先保持草稿状态,等 CI 给出结果后再请人评审。gh pr view --json headRefOid 会在检查开始前,从 GitHub 读取 PR 的 head commit。gh pr checks --watch 会等待检查完成,--fail-fast 则会在第一次失败时停止等待。
检查全部通过后,将 PR 标记为可评审,并确保合并的就是 CI 测试过的那个 commit:
gh pr ready
gh pr merge --squash --match-head-commit "$sha"
如果 PR 的 head 已经不再是指定的 SHA,--match-head-commit 就会拒绝合并。你在检查运行前从 GitHub 读取了 sha,因此它对应的是检查所针对的 commit,而不是你本地工作区此刻碰巧检出的 commit。Agent 推送代码很快,这个参数能防止一个没人测试过的 commit,在你检查完成到执行合并之间混进来。如果基准分支使用 merge queue,同一份手册也说明了,此时不需要指定合并策略。gh pr merge 会启用 auto-merge,或将 PR 加入队列。
根据实际情况,选择对应的处理方式。
已在本地合并,尚未推送。 reset 到之前保存的 commit。如果某个文件存在本地修改,--keep 会中止操作,避免覆盖它:
git reset --keep "$before"
squash 后已推送到基准分支。 revert 那一个 commit,然后推送。这会在基准分支上创建一个普通的新 commit:
git revert <squash-sha>
git push
PR 已在 GitHub 上合并。 较新的 gh 版本提供了 gh pr revert,可以从终端创建一个撤销 PR。GitHub 文档对网页端的说明也是一样:撤销操作会“创建一个新的 pull request,用于撤销原来的 merge commit”。你需要有写入权限,而这个撤销 PR 也会像其他 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
接下来就是开头那个坑。在 revert-a-faulty-merge 操作指南中,Linus Torvalds 解释说,撤销之后,“那次合并依然存在,也依然会被视为已经把两个分支连接在一起”。如果 Agent 在原分支上继续提交修复,你需要先撤销之前的撤销,再次合并:
git revert <revert-sha>
git merge --no-ff agent/fix-login
这份指南还补充了一个例外:如果分支是从头重建的,而不是在原有提交上继续修复,就跳过“撤销之前的撤销”这一步。这也是 Agent 分支更适合使用 squash 的另一个原因:撤销 squash 后,不需要这个额外步骤。
Hermes IDE 是我开发的,阅读这一节时,请把这一点考虑在内。Hermes 可以并排运行 Claude Code 和其他 CLI Agent,每个任务都在独立的 worktree 中,使用自己的 hermes/<name> 分支(见 README)。它的 Land 步骤涵盖了前面第 1 至第 4 步中的基准分支选择、合入和本地撤销。
根据 README 和发布说明:
Land 可以一步完成提交、创建 pull request 或本地合并。
它会将代码合入任务开始时所在的分支。不需要配置项,也能完成第 1 步的要求。
你可以用自己的检查来决定任务何时算完成。
已经合入的任务可以撤销。已合入任务的撤销数据会在 14 天后清理(2.2.9),因此,这项撤销功能适用于合入后的几天内。
提交、pull request 或本地合并,目标都是任务开始时所在的分支。

Hermes 推送代码时,使用你自己的 git credential helper 或 GITHUB_TOKEN 进行身份验证(见 README)。
有一个边界:无论你使用什么工具合入,只要变更已经进入共享分支,而且其他人已经拉取,我都会按第 4 步的方式撤销,也就是创建一个经过 CI 检查的 revert。Hermes 免费提供,源码以 BSL 1.1 许可开放,可运行于 macOS、Windows 和 Linux,与 Nous Research、Anthropic、OpenAI 或任何 Agent 厂商均无关联。2.3.3 已于 2026 年 10 月 13 日发布。
把第 4 步存到一个 CI 挂掉时能找到的地方,下次合入 Agent 分支时,使用 squash,并保存 before。如果你希望把基准分支、检查和撤销操作放在同一个地方,可以下载 Hermes IDE;如果它帮你避免了一次糟糕的合并,也欢迎在 GitHub 上给它点个 star。基准分支的 CI 变红,既是 git 问题,也是 CI/CD 问题。《Ship It》介绍了小团队从这里到生产环境的其余环节:托管、部署、CI/CD、身份验证、支付和监控,覆盖 localhost 与生产环境之间的整条路径。

如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。