附录 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):
*** 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 apply | sed / python | |
|---|---|---|---|
| 作者 | 模型这一枪 | 云端 diff 或人 | 模型藏在 shell 里 |
| 定位 | @@ + 内容匹配 | 行号 hunk | 无结构 |
| 审批 | 按文件路径 | E3 只打进工作区 | 整条命令 |
| 实现 | apply-patch/ | git | unified_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。