Apply a single patch to a file. Returns True if successful.
(self, code_dir: Path, patch: dict)
| 328 | }""" |
| 329 | |
| 330 | def _apply_patch(self, code_dir: Path, patch: dict) -> bool: |
| 331 | """Apply a single patch to a file. Returns True if successful.""" |
| 332 | filename = patch["file"] |
| 333 | old_text = patch["old"] |
| 334 | new_text = patch["new"] |
| 335 | |
| 336 | filepath = code_dir / filename |
| 337 | try: |
| 338 | filepath.resolve().relative_to(code_dir.resolve()) |
| 339 | except ValueError: |
| 340 | logger.warning(f"Patch target outside code_dir: {filepath}, skipping") |
| 341 | return False |
| 342 | if not filepath.exists(): |
| 343 | if not old_text or old_text.strip() == "": |
| 344 | filepath.parent.mkdir(parents=True, exist_ok=True) |
| 345 | filepath.write_text(new_text) |
| 346 | logger.info(f"Created new file: {filepath}") |
| 347 | return True |
| 348 | logger.warning(f"Patch target not found: {filepath}, skipping") |
| 349 | return False |
| 350 | |
| 351 | content = filepath.read_text(errors="replace") |
| 352 | |
| 353 | # Strategy 1: Exact match |
| 354 | if old_text in content: |
| 355 | filepath.write_text(content.replace(old_text, new_text, 1)) |
| 356 | return True |
| 357 | |
| 358 | # Strategy 2: Strip trailing whitespace |
| 359 | def strip_trailing(text: str) -> str: |
| 360 | return "\n".join(line.rstrip() for line in text.split("\n")) |
| 361 | |
| 362 | content_stripped = strip_trailing(content) |
| 363 | old_stripped = strip_trailing(old_text) |
| 364 | if old_stripped in content_stripped: |
| 365 | filepath.write_text(content_stripped.replace(old_stripped, strip_trailing(new_text), 1)) |
| 366 | return True |
| 367 | |
| 368 | # Strategy 3: Line-by-line fuzzy match |
| 369 | content_lines = content.split("\n") |
| 370 | old_lines = old_text.strip().split("\n") |
| 371 | if len(old_lines) >= 2: |
| 372 | first_line = old_lines[0].strip() |
| 373 | last_line = old_lines[-1].strip() |
| 374 | for i in range(len(content_lines)): |
| 375 | if first_line and first_line in content_lines[i].strip(): |
| 376 | for j in range(i + len(old_lines) - 1, min(i + len(old_lines) + 5, len(content_lines))): |
| 377 | if last_line and last_line in content_lines[j].strip(): |
| 378 | new_lines = new_text.rstrip().split("\n") |
| 379 | content_lines[i:j+1] = new_lines |
| 380 | filepath.write_text("\n".join(content_lines)) |
| 381 | return True |
| 382 | |
| 383 | # Strategy 4: Single line matching |
| 384 | if "\n" not in old_text.strip(): |
| 385 | old_line = old_text.strip() |
| 386 | for i, line in enumerate(content_lines): |
| 387 | if old_line == line.strip(): |
no test coverage detected