← 返回
AI技术

使用 git range-diff 实际比较 Claude Code 与 Codex 解决同一工单的代码分支

✍️ zhirenhun 📅 2026/10/20 👁 141 阅读 ⏱ 24 分钟
使用 git range-diff 实际比较 Claude Code 与 Codex 解决同一工单的代码分支

工单说:为订单添加软删除。Claude Code 在分支 agent-a 上处理了此事,Codex 在分支 agent-b 上处理了此事,两者都报告了工作完成。于是你按显而易见的方式比较这两个分支:

git diff agent-a agent-b

Git 按你的要求执行。它会比较两个分支的最新提交(git diff docs),于是 Claude 添加而 Codex 未添加的每一行会显示为删除,而 Codex 添加而 Claude 未添加的每一行会显示为新增。两个智能体以相同方式编写的行会消失。你实际上是在读取“撤销 Claude,然后应用 Codex”这一个补丁,两种方法的更改逐行交织在一起。

Git 自 version 2.19 起就有了处理此问题的命令:git range-diff。它会把两个分支上的提交配对,然后比较补丁,而不是比较文件。

设置竞赛:一个工单,两个工作树

完整工单如下:DELETE /orders/:id 隐藏订单而不是删除该行,并且 GET /orders 会跳过被隐藏的订单。这需要一次模式更改,而这就是两个智能体往往会做出不同调用的地方。

为每个智能体分配独立的分支和工作目录,使用 git worktree:

cd ~/code/app
git worktree add -b agent-a ../app-agent-a main
git worktree add -b agent-b ../app-agent-b main

-b 会在 main 分支上创建分支并在新文件夹中检出。默认情况下,Git 拒绝检出另一个工作树已经拥有的分支,这样两个智能体就不会最终共享同一个分支。

工作树是一个全新的检出,因此像 .env 这样的被 git 忽略的文件不在其中(Claude Code 工作树文档)。将你的应用所需的文件复制过去,并在两个文件夹中安装依赖,否则智能体测试运行会因与工单无关的原因而失败。

现在启动智能体。在第一个终端中,Claude Code 使用 claude "query" 将工单作为第一个提示词:

TICKET='Add soft delete to orders: DELETE /orders/:id
sets a flag instead of removing the row, GET /orders
skips deleted orders. Write a migration and tests.
Commit your work when the tests pass.'
cd ~/code/app-agent-a && claude "$TICKET"

在第二个终端中,执行 cd ~/code/app-agent-b,运行 codex 并粘贴相同的文本。文字相同,起始提交也相同,因此分支之间的任何差异都来源于这些 Agent。

Claude Code 自带的 --worktree 标志用于 Claude 会话,位于 .claude/worktrees/<name>。使用两个供应商时,普通的 git worktree 会将两个文件夹保持在同一命名方案下。

工单的最后一行很重要:range-diff 比较的是提交,而不是工作树。如果 Agent 在有未提交的更改时停止,运行 git -C ../app-agent-b status,查看内容,然后自行提交。

第一遍:每个 Agent 触及了什么?

在阅读任何代码之前,先了解每个回答的形状:

git log --oneline main..agent-a
git log --oneline main..agent-b
git diff --stat main...agent-a
git diff --stat main...agent-b

在 git log 中,两个点列出了 main 没有的 Agent 分支提交。在 git diff 中,三个点把分支与它从 main 分离的点比较,这样你只能看到 Agent 的更改(git diff 文档)。

检查以下三点。每个分支是否都有迁移文件?每个分支是否都有测试?哪些文件只出现在一个列表而不在另一个列表中?如果一个 Agent 只改动了少量文件,而另一个 Agent 改动了十几个文件(其中可能包括锁文件或 CI 配置),那么你已经对第二个分支提出了问题。

这张图展示了一个树形结构,树干是黑色的,树枝是橘色的,树叶是黑色的圆圈

range-diff 通过比较补丁来将两个分支上的提交配对。

git range-diff: 配对提交,比较补丁

两个分支都从 main 开始,因此两种形式都有效:

git range-diff main..agent-a main..agent-b
# same thing, shorter:
git range-diff main agent-a agent-b

三参数形式的文档说明相当于传入 <base>..<rev1> 和 <base>..<rev2>(range-diff 文档)。这是这些文档中的示例输出,展示了你可以得到的所有类型的行:

-:  ------- > 1:  0ddba11 Prepare for the inevitable!
1:  c0debee = 2:  cab005e Add a helpful message at the start
2:  f00dbal ! 3:  decafe1 Describe a bug
  @@ -1,3 +1,3 @@
   Author: A U Thor <author@example.com>

  -TODO: Describe a bug
  +Describe a bug
  @@ -324,5 +324,6
    This is expected.

  -+What is unexpected is that it will also crash.
  ++Unexpectedly, it also crashes. This is a bug, and the jury is
  ++still out there how to fix it best. See ticket #314 for details.

    Contact
3:  bedead < -:  ------- TO-UNDO

(文档将内部块缩进四个空格。我把它改为两个空格,以便在这里显示行。)

每个标头行将第一个范围(左)的提交与第二个范围(右)的提交配对。

  • = 表示两个补丁相同。
  • ! 表示提交对应但有差异,其下缩进块是这两个补丁的差异。
  • > 左侧带破折号,是仅第二个范围拥有的提交。
  • < 右侧带破折号,是仅第一个范围拥有的提交。

在 ! 块中,将第一列视为 range-diff 标记,第二列视为原始补丁。-+What is unexpected... 是第一个补丁添加、第二个补丁未添加的行。++Unexpectedly, it also crashes... 是仅第二个补丁添加的行。在你的比赛中,-+ 行属于 Claude,++ 行属于 Codex。

该块还以作者和信息行开头,因为 range-diff 在决定哪些提交对应时会比较“作者信息、提交消息和提交差异”。两个智能体很少写出相同的提交消息,因此可以跳过每对的这部分。

当两个答案无法配对时

两个独立的解决方案可能返回为一个 < 行和一个 > 行,中间没有 !。range-diff 认为这些补丁差异太大,无法视为同一次提交的两个版本。这已经是一个答案:智能体选择了不同的设计。

无论如何想看到补丁的差异,可以提升创建因子。它的默认值是 60,文档建议在 range-diff 错误地将大量更改视为完全重写时尝试更大的值:

git range-diff --creation-factor=100 main agent-a agent-b

如果它们仍然无法配对,请进一步提高。

提交次数导致另一种不匹配。如果 Claude 提交了三次而 Codex 只提交了一次,最多只能形成一对。请改为比较压缩后的副本,这样既不会动到任何一个分支:

base=$(git merge-base agent-a agent-b)
a=$(git commit-tree 'agent-a^{tree}' -p "$base" -m squashed)
b=$(git commit-tree 'agent-b^{tree}' -p "$base" -m squashed)
git range-diff "$base" "$a" "$b"

git commit-tree 会根据树创建一个新的提交对象并打印其 ID。每个都保存着共享基础之上一个分支的最终文件,使用相同提交信息,提交信息噪声也随之消失。没有任何分支指向它们,且智能体的分支保持原样。

range-diff 也接受 pathspec,用于将两个范围限制到这些路径。对于此票据,从以下 schema 开始:

git range-diff --creation-factor=100 "$base" "$a" "$b" \
  -- db/migrations

两个独立的迁移是共享很少的小补丁,因此它们仍可能以 < 行和 > 行的形式出现。如果出现这种情况,请用 git diff main...agent-a -- db/migrations 并排查看,同样也适用于 agent-b。

先阅读迁移再查看代码

假设 Claude 的分支存储一个时间戳,而 Codex 的分支存储一个标志:

-- agent-a
ALTER TABLE orders ADD COLUMN deleted_at timestamptz;

-- agent-b
ALTER TABLE orders
  ADD COLUMN is_deleted boolean NOT NULL DEFAULT false;

两种方式都不会重写表。PostgreSQL 18 文档指出,非易失默认值存储在表的元数据中,而没有默认值的列为 NULL,且「在这两种情况下都不需要重写表」。因此选择取决于语义:deleted_at 记录订单被隐藏的时间,而标志位则不记录。

在每个分支的迁移中,查找:

  • 易失默认值,例如 clock_timestamp()。根据同一页文档,这会导致整个表及其索引被重写。
  • 在 orders 表上创建普通的 CREATE INDEX。标准索引构建会「在完成之前锁定表的写入(但不锁定读取)」(CREATE INDEX 文档)。
  • 在你的工具把迁移放在事务里时使用 CREATE INDEX CONCURRENTLY。它不能在事务块中运行,因此迁移在运行时会失败。
  • 查询更改,在新列上进行过滤,但没有匹配过滤条件的索引。

在两个分支上运行测试,然后合并其一

从主检出目录,在各自文件夹中运行每个分支测试。用你自己的测试命令替换 npm test:

for br in agent-a agent-b; do
  echo "== $br"
  git diff --stat main..."$br" | tail -n 1
  if (cd "../app-$br" && npm test); then
    echo "$br: tests pass"
  else
    echo "$br: tests fail"
  fi
done

测试不通过的分支直接淘汰。在两个通过测试的分支之间,先选在繁忙的 orders 表上运行迁移。再选惊喜文件更少的较小 diff。如果 range-diff 将这些提交配对,你就在选择同一个修复的两种风格。如果没有配对,你就在选择一种设计。

落地胜者并清理:

git merge --no-ff agent-a
git worktree remove ../app-agent-a
git worktree remove ../app-agent-b
git branch -d agent-a
git branch -D agent-b

git worktree remove 会拒绝删除包含已修改或未跟踪文件的文件夹,除非你传入 --force(文档)。保持该标志关闭,这样残留的工作会阻止你。-D 用于未合并的失败分支。

Hermes IDE 中的同样的竞态

我构建 Hermes IDE,因此在阅读此部分时请结合这一点考虑。按下 ⌘N(Windows 和 Linux 上为 Ctrl+Shift+N),输入工单,选择 Claude Code,然后启动。再用 Codex 重复一次。每个任务都在各自的工作区中以 hermes/<name> 分支启动,工单作为智能体的首个提示词。新工作区会在几秒钟内获取依赖并分配独立端口,这覆盖了上面设置步骤的一半(安装部分)。


工作台的 Worktrees 标签页:分别为 orders-api、web-app 和 orders-types 各有一个 Hermes 工作区,每个工作区都有其 hermes/ 分支、任务、年龄和磁盘大小,以及剩余磁盘空间。

这些分支是普通的 Git 分支,因此本文中的所有操作都适用于它们。运行 git branch --list 'hermes/*' 可以得到两个分支名,而 git range-diff main hermes/<one> hermes/<other> 可以比较它们。Hermes 不会为你自动执行 range-diff。

它所补充的正是评审的另一半。每个智能体的轮次都会被保存,因此你可以撤销地对单个轮次进行差异比较或恢复。按下 ⌘G(Ctrl+Shift+G)打开工作台,它会按轮次或按文件展示每个智能体的更改,并带有风险标记,同时会将行注释发回给编写该行的智能体。range-diff 会告诉你两个答案之间的差异。轮次视图会告诉你每个智能体是如何得到其答案的。


工作台(⌘G)在按轮次视图中显示的 Claude Code 任务(位于 orders-api):共有两轮,第一轮的 diff 展示了在 src/db.ts 中添加 refunded 状态的更改、已更改文件列表,以及根据这些轮次草拟的提交信息。

当你选定后,Land 按钮会提交、打开拉取请求或将更改合并到任务起始的分支,并且可以撤销。你的检查可以决定任务何时算完成。Hermes 是免费的,采用 BSL 1.1 协议开源,可在 macOS、Windows 和 Linux 上运行。它与 Anthropic、OpenAI 或 Nous Research 无关。点此下载,源码位于 GitHub。


如果这对你有帮助

了解 range-diff 后,这次竞赛的 Git 部分就变得机械化。难点在于 schema:是时间戳还是标志,是当前索引还是后续索引,或者是可以在繁忙表上运行的迁移。Database Playbook 是我编写的简短指南,帮助你为正在构建的系统选出合适存储和形状。它是 8 份简短的 Pocket Guides for Developers 之一,内容涵盖 agents、RAG、提示词、可观测性、事件、数据库和系统设计:完整系列在此。

这张技术文章配图的内容是关于数据库的,图中有一个数据库的图标

——

广告 广告
🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

git ai tutorial productivity