fix apply patch when user has no git

This commit is contained in:
K. Hodges 2026-05-17 13:53:10 -07:00
parent ec9181eb64
commit 809ec92e0e
6 changed files with 102 additions and 0 deletions

View File

@ -177,6 +177,7 @@ pipeline:
type: patch_apply type: patch_apply
mode: apply mode: apply
output: patch-apply-output.txt output: patch-apply-output.txt
on_fail: implement
- id: test - id: test
type: command type: command

View File

@ -161,6 +161,7 @@ pipeline:
type: patch_apply type: patch_apply
mode: apply mode: apply
output: patch-apply-output.txt output: patch-apply-output.txt
on_fail: implement
- id: test - id: test
type: command type: command

View File

@ -167,6 +167,7 @@ In `nightshift.yaml`:
type: patch_apply type: patch_apply
mode: dry_run mode: dry_run
output: patch-apply-output.txt output: patch-apply-output.txt
on_fail: implement
``` ```
Run one task: Run one task:
@ -199,6 +200,7 @@ If the dry run looks good, switch to apply mode:
type: patch_apply type: patch_apply
mode: apply mode: apply
output: patch-apply-output.txt output: patch-apply-output.txt
on_fail: implement
``` ```
Run again: Run again:

View File

@ -67,6 +67,7 @@ pipeline:
type: patch_apply type: patch_apply
mode: apply mode: apply
output: patch-apply-output.txt output: patch-apply-output.txt
on_fail: implement
- id: test - id: test
type: command type: command

View File

@ -81,6 +81,8 @@ def validate_patch(
for path_text in files: for path_text in files:
_validate_patch_path(path_text, root, scoped_roots, forbidden_paths) _validate_patch_path(path_text, root, scoped_roots, forbidden_paths)
_validate_hunk_lines(patch)
_validate_file_states(patch, root)
return PatchValidationResult(files=tuple(sorted(files)), changed_lines=changed_lines) return PatchValidationResult(files=tuple(sorted(files)), changed_lines=changed_lines)
@ -174,6 +176,57 @@ def _patch_files(patch: str) -> set[str]:
return {path for path in files if path} return {path for path in files if path}
def _validate_hunk_lines(patch: str) -> None:
in_hunk = False
for line_number, line in enumerate(patch.splitlines(), start=1):
if line.startswith("diff --git "):
in_hunk = False
continue
if line.startswith("@@"):
in_hunk = True
continue
if not in_hunk:
continue
if line.startswith(("+", "-", " ", "\\")):
continue
raise PipelineError(
"Patch validation failed: malformed hunk line "
f"{line_number}; expected ' ', '+', '-', or '\\'."
)
def _validate_file_states(patch: str, root: Path) -> None:
current_path: str | None = None
current_is_new = False
current_is_deleted = False
def flush() -> None:
if not current_path:
return
target = root / current_path
if current_is_new and target.exists():
raise PipelineError(
f"Patch validation failed: patch creates existing file `{current_path}`."
)
if current_is_deleted and not target.exists():
raise PipelineError(
f"Patch validation failed: patch deletes missing file `{current_path}`."
)
for line in patch.splitlines():
if line.startswith("diff --git "):
flush()
parts = line.split()
current_path = _strip_prefix(parts[3]) if len(parts) >= 4 else None
current_is_new = False
current_is_deleted = False
elif line.startswith("new file mode "):
current_is_new = True
elif line.startswith("deleted file mode "):
current_is_deleted = True
flush()
def _changed_line_count(patch: str) -> int: def _changed_line_count(patch: str) -> int:
count = 0 count = 0
for line in patch.splitlines(): for line in patch.splitlines():

View File

@ -52,6 +52,50 @@ class PatchTests(unittest.TestCase):
with self.assertRaisesRegex(PipelineError, "forbidden path"): with self.assertRaisesRegex(PipelineError, "forbidden path"):
validate_patch(patch, root, safety) validate_patch(patch, root, safety)
def test_validate_patch_rejects_malformed_hunk_line(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
patch = """diff --git a/src/app.py b/src/app.py
--- a/src/app.py
+++ b/src/app.py
@@ -1 +1,2 @@
-old
+new
bare line
"""
with self.assertRaisesRegex(PipelineError, "malformed hunk line"):
validate_patch(patch, root, safety)
def test_validate_patch_rejects_new_file_when_target_exists(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
(root / "src" / "app.py").write_text("old\n", encoding="utf-8")
safety = SafetyConfig(
require_clean_worktree=False,
scoped_paths=("src",),
allowed_commands=(),
forbidden_commands=(),
)
patch = """diff --git a/src/app.py b/src/app.py
new file mode 100644
--- /dev/null
+++ b/src/app.py
@@ -0,0 +1 @@
+new
"""
with self.assertRaisesRegex(PipelineError, "creates existing file"):
validate_patch(patch, root, safety)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()