Skip to content

附录 E|apply-patch 文法:文件变更必须是可解析的 hunk,不是 sed ​

先别把补丁理解成 unified diff 贴进 JSON ​

B2 说写文件不走万能 shell。没有这一附录,会把 apply_patch 当成「模型输出 git diff 字符串」,或把 --codex-run-as-apply-patch 理解成用户该敲的 CLI。

判断先说清楚:这是一份独立的、给模型用的 freeform 文法,crate 是 apply-patch/,和 git 的 diff -u 不是同一种格式。 Spec 用 Lark(apply_patch.lark),解析器比文法略宽;应用侧按 hunk 算出路径和内容,再交给已经过审批的 Runtime。UI 要看的 unified diff 是 从 hunk 派生的,不是模型直接写的。

工具描述写明:FREEFORM,不要包 JSON(apply_patch_spec.rs:20)。JSON 转义会把 *** 和 + 行弄碎;文法让模型按行说话。


1. 一份补丁长什么样 ​

官方 Lark(crate 头注释 parser.rs:5-22,资源文件 core/assets/tools/apply_patch.lark):

text
*** Begin Patch
*** Add File: path
+...
*** Delete File: path
*** Update File: path
*** Move to: newpath          # 可选
@@ context                    # 可选,用来定位
-old
+new
 *** End of File              # 可选
*** End Patch

三种 hunk(parser.rs:66-81):

Hunk含义
AddFile新文件,内容全是 + 行
DeleteFile删文件,没有正文
UpdateFile改已有文件;可选 Move to;chunks 必须按文件中出现的顺序

@@ 是定位上下文(通常是函数/类名),不是 git 的 @@ -12,5 +12,7 @@。改动行以 + / - / 空格开头。远程执行可在 Begin 后插 *** Environment ID: …(spec 里可选;core 的 create_apply_patch_freeform_tool(include_environment_id) 会改 start 规则)。

解析器比 spec 更宽:允许标记前后空白;PARSE_IN_STRICT_MODE = false,因为 gpt-4.1 需要宽松模式,不想把 strict 参数穿遍所有调用点(parser.rs:47-53)。流式解析在 streaming_parser.rs,TUI 可以边收边画,不必等 End Patch。

没有「Begin/End + 三种 hunk」会怎样?模型输出标准 git diff,行号对不上正在改的工作区;或包进 JSON,引号把 + 行吃掉。freeform 文法是给 生成 用的,不是给 git apply 用的。


2. 应用:按内容匹配,再派生 UI diff ​

解析 不检查能不能落到磁盘(parser.rs:1-2)。落地在 file_update.rs:读原文 → seek_sequence 找 chunk → 拼新内容。

seek_sequence(seek_sequence.rs:1-7)从 start 往后找 old_lines:先精确,再忽略行尾空白,再忽略两端空白。eof 为真时先从文件末尾试(配合 *** End of File)。空 pattern 是 no-op。chunk 必须严格出现在上一个之后——乱序直接失败,避免把同一段改两次。

路径:Hunk::resolve_path 相对 cwd;Update 若有 Move,对外 affected path 是目的地(parser.rs:93-107)。审批和沙箱看到的是这些路径,不是命令字符串。

换行:默认 NormalizeToLf;可 PreserveLineEndings(lib.rs:62-70),standalone 进程用环境变量传递。Options 里 follow_symlinks 独立开关;standalone 默认跟随,agent 路径由 Runtime 决定——补丁不该顺着 symlink 写出工作区。

给 UI 的 unified diff:unified_diff_from_chunks,用 similar 从 old/new 生成。审批 overlay 和 /diff 看的是这个派生结果。模型不会被要求先写出合法的 diff -u。

独立可执行:CODEX_CORE_APPLY_PATCH_ARG1 = "--codex-run-as-apply-patch"(lib.rs:48-55)。Codex 二进制 arg0 分发到同一套解析器,让 shell 里的 apply_patch heredoc 和工具 call 同一份实现。这就是 B2 拦截的终点。


3. 从 shell 进来也必须变成这份文法 ​

APPLY_PATCH_COMMANDS = ["apply_patch", "applypatch"](invocation.rs:28)。maybe_parse_apply_patch 用 tree-sitter bash 抽 heredoc,再 parse_patch。Unix / PowerShell / Cmd 各有一条。解析失败不是「当普通 shell 跑掉」,是 PatchParseError。NotApplyPatch 才走 unified_exec。

没有拦截:模型 exec_command 里 python -c 改文件,TurnDiffTracker 空白,沙箱只能批整条未知命令。文法存在的理由是 副作用在解析期就变成路径列表。

远程 exec-server 用同一 crate、同一 ExecutorFileSystem。本机 TUI 和远端环境看到的 hunk 语义一致,不会一边 git apply 一边自己 sed。


4. 和 git apply、sed、code-mode 的边界 ​

apply-patch 文法git apply / codex applysed / python
作者模型这一枪云端 diff 或人模型藏在 shell 里
定位@@ + 内容匹配行号 hunk无结构
审批按文件路径E3 只打进工作区整条命令
实现apply-patch/gitunified_exec

code-mode(附录 D)里 tools.apply_patch(...) 传的仍是这份 freeform 文本,不是在 V8 里写文件。


5. 结语:带走一句话 ​

apply-patch 是给模型的 Lark 文法:Begin/End 包住 Add/Delete/Update hunk,按内容匹配落地,再派生 UI diff——独立 crate 让工具 call、shell 拦截、远程文件系统走同一套解析,而不是每人发明一种 sed。