在独立 Git Worktree 中重放 AI 生成的 diff 并运行测试,避免污染主分支,同时确保 patch 适用范围与 CI 构建目标一致。
当一个免费编程模型发给你一个补丁时,乐观的做法是直接应用它、看一下测试变绿就合并。问题是,单次成功运行并不能证明补丁是安全的。它可能会修改超出预期范围的文件,可能因为测试本身已经以某种方式损坏而掩盖了变更,可能只在不同 Python 版本下才会失败,或者能干净地应用到某个 commit 但无法应用到 CI 实际构建的那个 commit。
解决办法不是去骂模型。而是要创建一个小型、可重复的 replay gate,把每个生成的补丁都当作不可信的变更来对待。
我用了一个免费模型端点和一台免费服务器,将 gate 和模型调用放在同一个一次性环境中。声明:本文是 MonkeyCode 产品推广的一部分。MonkeyCode 的免费模型访问和免费服务器选项与此相关,因为 gate 本身是 CPU 和 I/O 密集型的:它应用 diff 并运行测试,所以不需要 GPU。这个工作流与模型无关,任何能返回 unified diff 的端点都可以使用。
不要把模型的 diff 直接合并到你的工作树中,而是把仓库复制到一个独立的 Git worktree 中,然后问四个问题:
补丁能否干净地应用到 CI 实际构建的那个 base commit?
补丁实际修改了哪些文件,这些文件是否在允许的范围内?
应用补丁后测试命令是否通过?
测试耗时多长,输出的末尾说了什么?
如果补丁未通过任何一项检查,gate 返回机器可读的状态,而不是合并 commit。
下面是一个最小化、可运行的实现。把测试命令替换成你仓库实际使用的命令。
#!/usr/bin/env python3
"""gate_patch.py - replay an AI-generated patch in a disposable Git worktree."""
import argparse
import json
import subprocess
import sys
import tempfile
import time
from pathlib import Path
def run(cmd, cwd=None, timeout=120, input_text=None):
return subprocess.run(
cmd,
cwd=cwd,
input=input_text,
capture_output=True,
text=True,
timeout=timeout,
)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--repo", required=True)
ap.add_argument("--patch", required=True)
ap.add_argument("--base-ref", default="HEAD")
ap.add_argument("--test-cmd", default="pytest -q")
ap.add_argument("--timeout", type=int, default=180)
ap.add_argument(
"--allowed-paths",
default="",
help="Comma-separated path prefixes allowed to change. Empty means allow all.",
)
args = ap.parse_args()
patch = Path(args.patch).read_text()
worktree = Path(tempfile.mkdtemp(prefix="gate_"))
result = {
"patch_file": str(Path(args.patch).resolve()),
"worktree": str(worktree),
}
create = run(
["git", "worktree", "add", "--detach", str(worktree), args.base_ref],
cwd=args.repo,
timeout=60,
)
if create.returncode != 0:
result.update({"stage": "worktree_create", "status": "FAIL", "detail": create.stderr.strip()[:500]})
print(json.dumps(result, indent=2))
return 1
check = run(["git", "apply", "--check"], cwd=str(worktree), input_text=patch, timeout=30)
if check.returncode != 0:
result.update({"stage": "apply", "status": "APPLY_FAIL", "detail": check.stderr.strip()[:500]})
print(json.dumps(result, indent=2))
return 2
apply_result = run(["git", "apply"], cwd=str(worktree), input_text=patch, timeout=30)
if apply_result.returncode != 0:
result.update({"stage": "apply", "status": "APPLY_FAIL", "detail": apply_result.stderr.strip()[:500]})
print(json.dumps(result, indent=2))
return 2
names = run(
["git", "diff", "--name-only", "--diff-filter=ACMR"],
cwd=str(worktree),
timeout=30,
)
changed_files = [line for line in names.stdout.splitlines() if line.strip()]
result["changed_files"] = changed_files
allowed_raw = [p.strip() for p in args.allowed_paths.split(",") if p.strip()]
unexpected = []
if allowed_raw:
for f in changed_files:
if not any(f == a or f.startswith(a.rstrip("/") + "/") for a in allowed_raw):
unexpected.append(f)
if unexpected:
result.update({"stage": "changed_files", "status": "UNEXPECTED_FILES", "unexpected_files": unexpected})
print(json.dumps(result, indent=2))
return 5
start = time.time()
try:
test = run(
["bash", "-lc", args.test_cmd],
cwd=str(worktree),
timeout=args.timeout,
)
result["test_exit_code"] = test.returncode
result["test_duration_s"] = round(time.time() - start, 2)
result["test_stdout_tail"] = test.stdout.strip()[-800:]
result["test_stderr_tail"] = test.stderr.strip()[-800:]
except subprocess.TimeoutExpired:
result.update({"stage": "test", "status": "TEST_TIMEOUT", "detail": f"timed out after {args.timeout}s"})
print(json.dumps(result, indent=2))
return 3
if test.returncode == 0:
result["status"] = "CLEAN"
else:
result["status"] = "TEST_FAIL"
print(json.dumps(result, indent=2))
return 0 if test.returncode == 0 else 4
if __name__ == "__main__":
sys.exit(main())
从仓库外部运行它,这样原始 checkout 不会被改动:
python gate_patch.py \
--repo /path/to/repo \
--patch model_change.diff \
--base-ref main \
--test-cmd "pytest -q tests/unit" \
--allowed-paths "src/,tests/"
脚本在失败后会保留 worktree,以便你检查具体状态。用以下命令清理:
git worktree remove /tmp/gate_XXXXXXXX --force
输出是 JSON 格式,所以你可以粘贴到评论线程中或发回给模型作为上下文。重要字段是 status。
这个 gate 能捕捉到我关心的免费编程模型最常见的失败模式:看起来合理但修改了与请求无关的路径的补丁。通过 --allowed-paths,gate 在测试步骤运行之前就会失败,这节省了完整的测试周期。
gate 不训练模型、不运行大型推理任务、也不在内存中持有模型。它调用模型端点、接收文本形式的 diff、应用 diff,然后运行本地命令。这些操作都很轻量。因此免费服务器选项可以托管这个工作流而无需 GPU,而且通常模型的可用性而不是服务器的可用性才是限制因素。
把端点本身当作不稳定的依赖项。把 base commit、补丁文件和 gate 结果一起记录下来。如果端点超时或返回格式错误的 diff,记录会显示问题是模型响应还是包装层。
这是一个 replay gate,不是语义审查器。补丁可以通过测试套件但仍然可能是错的:它可能删除了一个测试、引入安全问题,或者以错误的原因产生了正确的输出。它也只检查你提供的测试命令。如果你的测试套件很稀疏,gate 的检查就很稀疏。
Git worktree 不是沙箱。应用的代码和测试命令在主机上运行。如果你要 replay 来自不可信源的补丁,请在容器或虚拟机中运行 gate,且该容器或虚拟机无法访问凭证。另外,免费模型端点可能随时变化,不要让 gate 成为坏补丁进入生产的唯一防线。
如果你的项目已经有 CI 对每个 proposed patch 隔离运行并要求 reviewer,这个 gate 可能是多余的。如果你需要完整的安全审查或二进制分析,本地 worktree 不够用。如果你的目标是让模型互相比较,这个脚本测试的是补丁而不是模型质量;为此使用单独的 benchmark 工具。
但如果你从免费编程模型生成大量小改动,并且在人工审查之前需要一个廉价的第一层过滤,这个工作流很有用。在下次合并之前,用一个真实的补丁试试。如果它阻止了一个坏 diff 到达 main 分支,它就已经值回它的成本了。