Skip to content

Commit 8457e34

Browse files
committed
make the git diff step more robust
1 parent 294ea6e commit 8457e34

2 files changed

Lines changed: 66 additions & 8 deletions

File tree

reviewbot/clone_cache.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,21 +398,43 @@ def apply_patch(self, checkout: Checkout, diff_text: str) -> None:
398398
``subprocess.CalledProcessError`` if the patch does not apply
399399
cleanly. ``--index`` keeps the index in sync so newly added files
400400
show up in :meth:`collect_changes`. The patch is written to a temp
401-
file (not passed on the command line) so its size is unbounded."""
401+
file (not passed on the command line) so its size is unbounded.
402+
403+
Strict ``git apply`` requires the hunk's context *and* its ``@@``
404+
line numbers to match exactly. LLM-authored diffs routinely have
405+
slightly-off line numbers or whitespace, so we retry with
406+
``--recount`` (recompute line numbers from the hunk body) and
407+
``--whitespace=fix``. The retry still requires the context lines to
408+
match, so it tolerates bad geometry without applying to the wrong
409+
place — ``git apply --index`` is all-or-nothing, so a failed attempt
410+
leaves the tree untouched for the next one."""
402411
with tempfile.NamedTemporaryFile(
403412
"w", suffix=".patch", delete=False, encoding="utf-8"
404413
) as fh:
405414
patch_path = fh.name
406415
# git apply requires the diff to end with a newline.
407416
fh.write(diff_text if diff_text.endswith("\n") else diff_text + "\n")
417+
attempts = (
418+
("strict", ["apply", "--index", "--whitespace=nowarn", patch_path]),
419+
(
420+
"recount",
421+
["apply", "--index", "--whitespace=fix", "--recount", patch_path],
422+
),
423+
)
408424
try:
409-
self._git(
410-
checkout.path,
411-
"apply",
412-
"--index",
413-
"--whitespace=nowarn",
414-
patch_path,
415-
timeout=120,
425+
last_stderr = ""
426+
for label, args in attempts:
427+
proc = self._git(checkout.path, *args, timeout=120, check=False)
428+
if proc.returncode == 0:
429+
if label != "strict":
430+
log.info(
431+
"patch applied via '%s' fallback (strict git apply failed)",
432+
label,
433+
)
434+
return
435+
last_stderr = proc.stderr.decode("utf-8", errors="replace")
436+
raise subprocess.CalledProcessError(
437+
1, ["git", "apply"], stderr=last_stderr.encode()
416438
)
417439
finally:
418440
os.unlink(patch_path)

tests/test_clone_cache_tasks.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,42 @@ def test_apply_patch_failure_raises(self):
116116
with self.assertRaises(subprocess.CalledProcessError):
117117
self.cache.apply_patch(co, bad)
118118

119+
def test_apply_patch_recount_fallback_fixes_bad_line_numbers(self):
120+
# The @@ header line counts are wrong (claims 4 lines); strict
121+
# `git apply` rejects this as corrupt, mirroring the off-by-some
122+
# line numbers in LLM-authored diffs. The --recount fallback
123+
# recomputes them from the hunk body, so the edit still applies.
124+
co = self._acquire()
125+
bad_geometry = (
126+
"diff --git a/hello.txt b/hello.txt\n"
127+
"--- a/hello.txt\n"
128+
"+++ b/hello.txt\n"
129+
"@@ -1,4 +1,4 @@\n"
130+
"-hi from main\n"
131+
"+hi patched\n"
132+
)
133+
# Strict apply alone fails on the bad geometry…
134+
strict = self.cache._git(
135+
co.path,
136+
"apply",
137+
"--index",
138+
"--whitespace=nowarn",
139+
self._patch_file(bad_geometry),
140+
check=False,
141+
)
142+
self.assertNotEqual(strict.returncode, 0)
143+
# …but apply_patch's recount fallback lands it.
144+
self.cache.apply_patch(co, bad_geometry)
145+
changes = {c.path: c for c in self.cache.collect_changes(co)}
146+
self.assertEqual(changes["hello.txt"].content, b"hi patched\n")
147+
148+
def _patch_file(self, text):
149+
fh = tempfile.NamedTemporaryFile("w", suffix=".patch", delete=False)
150+
self.addCleanup(os.unlink, fh.name)
151+
fh.write(text if text.endswith("\n") else text + "\n")
152+
fh.close()
153+
return fh.name
154+
119155

120156
if __name__ == "__main__":
121157
unittest.main()

0 commit comments

Comments
 (0)