关于 revdiff
revdiff 是用于终端覆盖层的差异审查技能,可在 Git、hg 和 jj 仓库中展示变更并添加行内注释。它还支持审查单文件、全部文件或通过标准输入传入的补丁,并可读取持久化审查记录。
主要功能
- 01自动检测 Git、hg 和 jj 仓库
- 02逐行浏览差异并添加注释
- 03审查单文件或全部文件
- 04通过标准输入审查补丁
- 05读取并恢复审查历史
适用场景
- 01审查当前分支或未提交的改动
- 02为特定文件的变更添加行级意见
- 03审阅不在本地工作树中的 PR 补丁
- 04恢复中断会话中的审查注释
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ umputun/ revdiff --skill revdiff安装后在 Claude Code 中输入/revdiff确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/config.mdinstall.mdusage.mdscripts/agentdeck-window.shdetect-ref.shlaunch-revdiff.shread-latest-history.sh详细介绍
原文在翻译后有更新,译文可能不是最新。
revdiff - TUI 差异审查
在终端覆盖层中使用 revdiff TUI 审查差异并添加行内注释。支持 git、hg 和 jj 仓库(自动检测)。
脚本路径解析
根据可用技能目录中的此技能绝对路径,解析 <plugin-root>。它是包含此插件 .codex-plugin/plugin.json 的目录。然后设置:
SCRIPT_DIR="<plugin-root>/skills/revdiff/scripts"
运行命令前,将 <plugin-root> 替换为解析出的绝对路径。在此技能中,使用 $SCRIPT_DIR 代替脚本路径。
注意:启动器覆盖链(用户配置 ${CLAUDE_PLUGIN_DATA} → 捆绑版本)仅适用于 Claude。Codex 用户可以在本地市场检出版本中自定义启动器,然后重新安装插件。
触发条件
- “revdiff”、“review diff”、“review changes”、“annotate diff”
- “revdiff HEAD~1”、“revdiff main”
- “hg review with revdiff”、“review jj change”
- “revdiff all files”、“review all files”、“browse all files”
- “revdiff all files exclude vendor”
- “revdiff README.md”、“revdiff docs/plan.md”、“revdiff /tmp/notes.txt”——单文件审查(
--only模式) - “review this file”、“annotate this file”、“review file with revdiff”
- “open this review in revdiff”、“show review in revdiff”、“review in revdiff”——打开会话中的审查(预加载模式)
回答问题
如果用户询问的是 revdiff 相关问题(配置、主题、键绑定、安装、用法),而不是请求开启审查会话,请查阅 references/ 中的参考文件并直接回答。不要为信息类问题启动 TUI。
references/install.md——安装方式和插件设置references/config.md——配置文件、选项、颜色、Chroma 主题references/usage.md——示例、键绑定、输出格式
使用现有审查历史记录
如果用户提出“找到我的审查记录”“使用我最新的 revdiff 注释”“调出我刚才在另一个终端里做的审查”或“我之前注释了什么”等请求,说明用户是在此插件流程之外运行了 revdiff,并希望 Codex 处理已保存的注释。通过辅助脚本从持久化历史记录目录读取最新文件,然后按第 3.5 步分类处理注释,就像它们来自新启动器调用一样:
$SCRIPT_DIR/read-latest-history.sh
该脚本从 $REVDIFF_HISTORY_DIR(默认为 ~/.config/revdiff/history)解析历史记录目录,根据 VCS 根目录名称(jj/git/hg)查找仓库子目录,并打印找到的最新 .md 文件。每个历史记录文件都包含一个标头(路径、引用,以及可用时的 git 提交哈希)、采用 ## file:line (type) 格式的注释,以及所注释文件的原始 git 差异。commit: 行和差异块仅从 git 获取;在 hg/jj 仓库中,差异块为空,也不会记录提交哈希。有关目录布局、stdin/only 处理和覆盖选项,请参阅 references/usage.md 中的“Review History”部分。
当连接中断导致审查被提前终止时,历史记录文件也可用于恢复:收到终止信号(例如 SSH/tmux 客户端断开时发出的 SIGHUP,或 SIGTERM)后,revdiff 会将当前注释保存到历史记录中,而不是保存到 -o 输出中,因此 read-latest-history.sh 仍可恢复这些注释。
打开会话中的审查
当用户要求在 revdiff 中打开会话中的审查时(对话中已经包含本次会话早先生成的审查评论),请按 references/usage.md(“Output Format”部分)中说明的格式,将这些评论写入临时文件(例如 /tmp/revdiff-review-XXXXXX.md),然后运行常规启动器流程(第 1 步检测引用,第 2 步调用),并追加 --annotations=<temp-path>。从第 3 步开始,按常规方式处理整理好的注释。
审查工作树之外的差异
有些审查目标并非当前仓库的状态:GitHub PR 差异、磁盘上的补丁文件,或 git format-patch -1 --stdout 的输出。将统一差异通过管道传给 revdiff --stdin,输入会被解析为真实的多文件差异(每个文件对应一个树条目,支持分块导航和逐文件批注),而不是仅显示上下文的缓冲区。revdiff 会自动检测统一差异的特征;如果补丁格式错误,输入会静默回退到原始文本模式。
遇到以下情况时,请使用此方式,而非常规启动器流程:
- 用户要求“审查 PR #N”、“审查此补丁”、“审查
gh pr diff输出”,或提供补丁 URL/路径 - 差异描述的提交未在本地检出(例如,远程 PR 中其他人的分支)
- 用户粘贴了统一差异,并要求审查该差异,而非工作树
示例调用(使用与常规流程相同的启动器):
gh pr diff 123 | $SCRIPT_DIR/launch-revdiff.sh --stdin
git format-patch -1 --stdout | $SCRIPT_DIR/launch-revdiff.sh --stdin
cat /tmp/feature.patch | $SCRIPT_DIR/launch-revdiff.sh --stdin
--stdin 不能与 refs、--staged、--only、--all-files、--include、--exclude 和 --annotations 同时使用,因此不要与步骤 1 的 ref 检测结合使用——启动器返回后直接进入步骤 3。批注会以差异中的真实文件路径为键返回(而不是 --stdin-name)。
工作原理
- 在终端叠加层中启动 revdiff(agterm 全窗格叠加层、tmux 弹出窗口、Zellij 浮动窗格、herdr 标签页、kitty 叠加层、wezterm/Kaku 分屏窗格、cmux 分屏、ghostty 分屏并缩放、iTerm2 分屏窗格或 Emacs vterm 窗口)
- 用户浏览差异,并在特定行添加批注
- 退出时,从标准输出捕获批注
- Codex 读取批注并逐条处理
- 循环:重新启动 revdiff 以验证修复,用户可继续添加批注
- 用户退出且未添加批注时,流程结束
工作流
步骤 1:确定审查模式
全部文件模式:如果 $ARGUMENTS 与“all files”、“all-files”或“browse all files”匹配(可选带有“exclude ”部分),则使用全部文件模式:
- 向启动器传递
--all-files - 如果用户提到排除模式(例如,“exclude vendor”、“exclude vendor and mocks”),则分别传递为
--exclude=<prefix> - 完全跳过引用检测,直接进入步骤 2
- 示例:“all files exclude vendor” →
--all-files --exclude=vendor
文件审查模式:如果 $ARGUMENTS 是一个指向磁盘上文件的单一词元(例如,docs/plans/feature.md、/tmp/notes.txt、README.md、main.go、file.blah),则将其视为文件审查:
- 使用
test -f "$ARGUMENTS"判断——如果文件存在,则使用文件审查模式 - 如果词元以
/或./开头,或包含/且带有文件扩展名(例如,src/app.go),即使当前目录中尚无法访问该文件,也将其视为文件审查 - 完全跳过引用检测
- 直接进入步骤 2,并使用
--only=<filepath>(不带引用参数) - 在 VCS 仓库内外均可使用——revdiff 会将磁盘上的文件作为仅供参考的上下文读取
- 含义不明确的词元(例如,
main——既可能是分支名称,也可能是不带扩展名的潜在文件名)→ 优先使用引用模式;仅当test -f和git rev-parse --verify均无法确定时,才询问用户
引用模式:如果 $ARGUMENTS 包含明确的引用(例如,HEAD~1、main,或用于双引用差异的 main feature),则按原样使用。
自动检测:如果未提供引用,则运行智能检测脚本:
$SCRIPT_DIR/detect-ref.sh
脚本会输出结构化字段:
branch、main_branch、is_main、has_uncommitted、has_staged_onlysuggested_ref— 要传递给 revdiff 的引用(为空 = 未提交的更改)use_staged— 如果为true,则向启动器传递--staged(检测到仅暂存的更改)needs_ask— 如果为true,则在继续前询问用户
当 use_staged: true 时,向启动器传递 --staged。这表示所有更改都在索引中(已暂存),没有未暂存的更改——如果不传递 --staged,revdiff 会显示空差异。
当 needs_ask: true 时(位于有未提交更改的功能分支上),以编号列表向用户提供选项,并等待其回复:
- 仅未提交的更改 — 不传递引用(仅审查工作区中的更改)
- 分支与 {main_branch} 的差异 — 将 main_branch 作为引用传递(包含未提交更改的完整分支差异)
当 needs_ask: false 时,直接使用 suggested_ref:
- 在主分支上且有未提交更改 → 不传递引用(未提交的更改)
- 在主分支上且仅有暂存更改 → 不传递引用 +
--staged(已暂存的更改) - 在主分支上且工作区干净 →
HEAD~1(上一次提交) - 在功能分支上且工作区干净 → 主分支名称(完整分支差异)
步骤 2:启动审查
为用户启动 revdiff 时(例如,重构或分析后),传入 --description="...",以便信息弹窗(i 键)说明做了哪些更改以及需要关注什么 — 支持 Markdown。对于较长的文字,将 Markdown 写入临时文件,并传入 --description-file=/tmp/revdiff-desc-XXXXXX.md。这两个标志互斥;都不是必需的。没有有用的上下文可补充时,跳过。
如果近期更改可能创建了新的未跟踪文件(新软件包、新测试文件、新文档、新脚本,且尚未执行 git add),请传入 --untracked,以便这些文件显示在树中。在工作树模式下(不传 ref,也不传 --staged)使用此选项;对于 ref 到 ref 的审查则跳过,因为未跟踪文件不属于历史差异。
只有当用户明确要求这种光标偏好时,才传入 --start-at-change;切勿自动推断。
只有当用户要求树中仅显示未标记为已审查的文件时,才传入 --filter-unreviewed;切勿自动推断。审查期间可按 F 键切换同一筛选条件。
只有当用户明确要求光标移动继续到相邻文件时,才传入 --cross-file-motion;切勿自动推断。
运行启动脚本:
$SCRIPT_DIR/launch-revdiff.sh [base] [against] [--staged] [--untracked] [--filter-unreviewed] [--cross-file-motion] [--only=file1] [--all-files] [--exclude=prefix] [--description=text|--description-file=path]
重要 — 长时间运行的命令:启动器会一直阻塞,直到用户在 TUI 覆盖层中完成审查;这可能超过 bash 工具的默认超时时间。将 bash 超时参数设置为你的运行环境允许的最大值(例如 1800000 或更高)。此操作不要使用 run_in_background — 后台任务处理不适用于交互式 TUI 启动器。如果审查时间超过超时上限,第 3 步中的备用方案会处理。
可抵御断连的 tmux 窗口模式:在 tmux 下运行时,在启动器前加上 REVDIFF_TMUX_WINDOW=1,以便在持久的、由服务器拥有的 tmux 窗口中打开 revdiff,而不是由客户端拥有的 display-popup。即使 SSH 或 tmux 客户端断开,审查也会继续;重新连接后仍可继续。这是启动器环境变量,不是 revdiff 标志。
面板范围覆盖层(herdr):在 herdr 下运行时,REVDIFF_HERDR_PANE=1 会在 Agent 自己的面板中打开一个缩放后的分屏来运行 revdiff,而不是新建全屏标签页,让 Agent 面板只需按一次键即可切回。用户在环境中设置此变量;如果 herdr CLI 版本较旧,则会回退到标签页覆盖层。这是启动器环境变量,不是 revdiff 标志。
面板范围覆盖层(agterm):在 agterm 分屏中运行时,REVDIFF_AGTERM_PANE=1 会在 Agent 自己的面板中打开 revdiff,而不是覆盖整个会话,从而让相邻面板保持活动且可见。用户在环境中设置此变量;在非分屏环境下会忽略它。这是启动器环境变量,不是 revdiff 标志。
脚本会:
- 检测可用的终端(agterm → tmux → Zellij → herdr → kitty → wezterm/Kaku → cmux → ghostty → iTerm2 → Emacs vterm)
- 在覆盖层中启动 revdiff
- 将批注输出捕获到临时文件
- 将捕获的批注打印到 stdout
随附的启动器会设置 REVDIFF_EXIT_CODE_ON_ANNOTATIONS;退出码 10 表示已捕获批注,不代表启动器失败。其他非零状态均视为失败。发生这些失败时,启动器会转发 revdiff 自身的 stderr — 请原样报告该文本,不要猜测是哪个参数有问题。
Agterm 会话与审批升级
当 Codex 在 agterm 会话中运行时,启动器会在 agterm 的原生全窗格覆盖层中打开 revdiff——这是它的首选后端,优先于 tmux 检查。该分支需要启动器环境中包含 AGTERM_SESSION_ID 和 AGTERM_SOCKET。
需要审批或升级的 Codex 命令会在一个全新的环境中运行,该环境会移除所有 AGTERM_* 变量(以及 TERM_PROGRAM 和 GHOSTTY_*),只保留 PATH。从那里启动时,由于缺少这些变量,启动器会跳过 agterm 分支,并报错 no overlay terminal available。
升级前,请在正常(未升级的)环境中读取这些值,并将其作为字面值写入已获批的命令:
AGTERM_SESSION_ID='<captured-id>' \
AGTERM_SOCKET='<captured-socket>' \
$SCRIPT_DIR/launch-revdiff.sh --only=<file>
- 不要在已获批的命令中引用
$AGTERM_SESSION_ID/$AGTERM_SOCKET——这些变量在那里已经被取消设置,因此展开结果为空。请先在正常环境中读取它们,然后粘贴字面字符串。 - 不要通过指定
active会话(agterm 的默认会话)来绕过缺少会话 ID 的问题:这可能会在无关的 agterm 窗口或会话中打开 revdiff。捕获的会话 ID 能确保覆盖层显示在正在审查的会话中。
第 3 步:处理批注
收集启动器输出:通常情况下,启动器会同步返回,并在 stdout 上输出批注——请按下文所述进行处理。如果 bash 工具报告退出代码 10,请读取 stdout 并将其作为批注处理;不要将其视为失败。如果 bash 工具报告超时,则只有启动器进程已终止,revdiff 本身仍在覆盖层中运行,也不会丢失任何批注:用户退出时,revdiff 会立即将批注写入磁盘,按 O 可随时刷新。不要重试启动器。请采用以下备用流程:
- 先安抚用户并提供两种选择,明确说明不会丢失任何内容——保持简短,不要解释保存机制(写入磁盘、按
O刷新、退出时保存);用户不需要了解这些。可以这样说:“等待你完成 revdiff 审阅的进程已超时并退出——这没有影响,你写下的任何批注都安全保留。完成后,随时告诉我加载你的批注并继续,或者告诉我你已完成并希望停止。”不要假定用户想要加载;未添加批注就退出或选择停止,都是有效结果。 - 等待用户回复。覆盖层获得焦点时,用户无法回复;因此收到回复就意味着他们已返回会话(他们已退出,或按
O刷新后切回)。 - 用户回复后,你必须采取行动;不要停留在第 1 步。如果他们选择停止,请确认并结束。否则读取已保存的批注,优先读取最新的输出文件(如果设置了
$TMPDIR,启动器会写入该目录;否则写入/tmp):output_file="$(ls -t "${TMPDIR:-/tmp}"/revdiff-output-* 2>/dev/null | head -1)" if [ -n "$output_file" ] && [ -f "$output_file" ]; then cat "$output_file" fi - 如果输出文件有内容,就按下文所述将其作为批注处理。如果文件为空或不存在,则回退到持久化的审阅历史记录;即使启动器的清理操作已删除临时文件,该记录仍会保留:运行
$SCRIPT_DIR/read-latest-history.sh,并处理其中## Annotations部分的批注(参见“使用现有审阅历史记录”)。只有当两者都为空时,才说明用户退出时没有添加批注。
两种读取方式都会返回完整内容:revdiff 会在退出时以原子方式写入输出文件,且历史记录条目会在进程退出前写入完毕。这可以确保读取内容不会不完整,但不能保证文件属于本次审阅:如果同一个 $TMPDIR 下同时进行两次审阅,最新匹配项可能属于另一次审阅。
审阅者也可以有意保持 revdiff 打开,并按 O 将当前批注刷新到同一个输出文件,而不退出。刷新采用相同的原子写入方式,因此上述备用读取仍会返回完整文件。如果用户在覆盖层仍打开时说了类似“我已刷新笔记,可以继续了”的话,请严格按照超时备用流程读取最新的输出文件并处理批注;不要重新启动 revdiff。完成代码更改后,审阅者会按 R 重新加载,并在同一会话中继续。无需更改启动器标志——启动器已经传入输出文件,按 O 会重复使用该文件。
如果脚本生成了输出,就表示用户添加了批注。输出格式为:
## file.go:43 (+)
use errors.Is() instead of direct comparison
## store.go:18 (-)
don't remove this validation
每个批注块包含:
## filename:line (type)—— 文件和行号,(+)= 新增,(-)= 删除,(file-level)= 文件级备注- 下方的评论文本 —— 用户希望修改的内容
步骤 3.5:分类批注
将批注分为两类:
解释请求——批注符合以下任一规则(不区分大小写):
- 文本中任意位置包含两个或更多连续问号(
??、???等)——表示“请解释”的语言无关简写 - 或以以下任一内容开头:
explain、remind、describe、what is、what are、how does、how do、clarify
这些是用户希望获得解答的问题,而不是代码修改。
代码更改指令——其他所有批注。这些是修改代码的指示。
如果发现解释请求:
-
回答每个解释请求——阅读引用的代码,并生成清晰的 Markdown 解释
-
如果同一批次中还有代码更改指令,将其标记为待处理(完成解释循环后再进入步骤 4)
-
进入解释循环:
a. 将解释写入临时 Markdown 文件(例如
/tmp/revdiff-explain-XXXXXX.md) b. 通过启动脚本使用--only=/tmp/revdiff-explain-XXXXXX.md启动 revdiff——这会打开带有目录侧边栏、可滚动的 Markdown 视图 c. 如果用户退出且未添加批注 → 解释已获接受,清理临时文件,然后继续:- 如果有待处理的代码更改指令 → 前往步骤 4
- 否则 → 前往步骤 6(使用原始 diff ref 重新启动 revdiff) d. 如果用户对解释添加批注 → 这些是后续问题或澄清请求。阅读批注,完善或扩展解释 Markdown,写入更新后的临时文件,然后返回步骤 (b)
解释循环会持续进行,直到用户退出且未添加批注。这样用户就能围绕解释自然地来回交流,并针对特定部分请求更多细节或更正。
如果没有解释请求——所有批注都是代码更改指令,直接进入步骤 4。
步骤 4:规划更改
将计划写成 Markdown 列表,分析代码更改批注:
- 列出每条批注及其文件和行号
- 描述每项计划更改
- 询问用户:“是否继续进行这些更改?”
等待用户确认后再修改代码。
步骤 5:处理批注
用户批准计划后,修复实际源代码。每条批注都是一项指令。
步骤 6:循环
完成修复后(或在步骤 3.5 中选择“Continue review”后),使用相同的 ref 再次运行启动脚本。用户可以:
- 添加更多批注 → 返回步骤 3
- 退出且未添加批注 → 审核完成(无输出)
步骤 7:完成
脚本不再产生输出时,审核即告完成。告知用户。
示例会话
User: "revdiff HEAD~1"
→ launch revdiff in tmux popup with HEAD~1 diff
→ user annotates: "handler.go:43 - use errors.Is()"
→ user quits
→ annotations captured
→ plan changes: "add errors.Is() check at handler.go:43"
→ user approves
→ fix applied
→ re-launch revdiff HEAD~1
→ user sees fix, quits without annotations
→ "review complete"
User: "revdiff HEAD~3"
→ launch revdiff in tmux popup with HEAD~3 diff
→ user annotates: "server.go:72 - explain what this mutex protects"
→ user quits
→ annotation classified as explanation request (starts with "explain")
→ Codex reads server.go:72, generates markdown explanation
→ writes to /tmp/revdiff-explain-XXXXXX.md
→ launch revdiff --only=/tmp/revdiff-explain-XXXXXX.md (explanation view with TOC)
→ user reads explanation, annotates: "what about the race condition on line 80?"
→ Codex refines explanation, rewrites temp file
→ re-launch revdiff --only=/tmp/revdiff-explain-XXXXXX.md
→ user reads updated explanation, quits without annotations
→ explanation accepted, clean up temp file
→ re-launch revdiff HEAD~3 (back to diff review)
→ user quits without annotations
→ "review complete"
User: "revdiff all files exclude vendor"
→ launch revdiff with --all-files --exclude=vendor
→ user browses all tracked files, annotates as needed
→ same annotation loop as above
User: "revdiff docs/plans/feature.md"
→ test -f docs/plans/feature.md succeeds → file review mode
→ launch revdiff with --only=docs/plans/feature.md (context-only view, no ref)
→ user annotates prose: "section 'Open questions':3 - drop this, resolved"
→ user quits
→ same annotation loop as above (applies to the file content)