diff --git a/server/unified_diff.py b/server/unified_diff.py index fb969974..8727d66c 100644 --- a/server/unified_diff.py +++ b/server/unified_diff.py @@ -202,6 +202,18 @@ def append_file(old_path: str = "", new_path: str = "") -> dict[str, Any] | None def _paths_from_diff_header(line: str) -> tuple[str, str]: + names = line[len("diff --git ") :] + if '"' not in names: + # Git does not quote names that only contain spaces, so a space alone + # cannot separate them. Split where both names denote the same path; + # renames and copies carry their own path lines below the header. + for index, char in enumerate(names): + if char != " ": + continue + old_path, new_path = names[:index], names[index + 1 :] + stripped = _without_prefix(old_path) + if old_path == new_path or (stripped and stripped == _without_prefix(new_path)): + return old_path, new_path try: parts = shlex.split(line) except ValueError: @@ -211,6 +223,11 @@ def _paths_from_diff_header(line: str) -> tuple[str, str]: return "", "" +def _without_prefix(path: str) -> str: + _, separator, rest = path.partition("/") + return rest if separator else "" + + def _marker_path(value: str) -> str: text = str(value or "").split("\t", 1)[0].strip() if len(text) >= 2 and text[0] == text[-1] == '"': diff --git a/tests/test_unified_diff_canvas.py b/tests/test_unified_diff_canvas.py index 2edd3f30..52add5df 100644 --- a/tests/test_unified_diff_canvas.py +++ b/tests/test_unified_diff_canvas.py @@ -136,6 +136,39 @@ def test_parse_unified_diff_keeps_marker_like_content_inside_hunks() -> None: assert second["deletions"] == 1 +def test_parse_unified_diff_keeps_spaced_paths_without_marker_lines() -> None: + parsed = parse_unified_diff( + "\n".join( + [ + "diff --git a/old notes.txt b/old notes.txt", + "deleted file mode 100644", + "index 587be6b..0000000", + "--- a/old notes.txt\t", + "+++ /dev/null", + "@@ -1 +0,0 @@", + "-x", + "diff --git a/cover art.png b/cover art.png", + "index bdc955b..8835708 100644", + "Binary files a/cover art.png and b/cover art.png differ", + "diff --git a/run me.sh b/run me.sh", + "old mode 100644", + "new mode 100755", + ] + ), + changed_files=["old notes.txt", "cover art.png", "run me.sh"], + ) + + assert [(item["path"], item["status"]) for item in parsed["files"]] == [ + ("old notes.txt", "deleted"), + ("cover art.png", "modified"), + ("run me.sh", "modified"), + ] + assert parsed["files"][0]["oldPath"] == "old notes.txt" + assert parsed["files"][0]["deletions"] == 1 + assert parsed["files"][1]["oldPath"] == "cover art.png" + assert parsed["files"][1]["newPath"] == "cover art.png" + + def test_diff_canvas_payload_keeps_report_for_local_mode_switch() -> None: structured = parse_unified_diff( "diff --git a/a.py b/a.py\n--- a/a.py\n+++ b/a.py\n@@ -1 +1 @@\n-old\n+new\n" @@ -188,6 +221,7 @@ def test_parse_unified_diff_caps_files_without_merging_hunks() -> None: test_parse_unified_diff_keeps_untracked_files_without_hunks() test_parse_unified_diff_renders_an_untracked_new_file_patch() test_parse_unified_diff_keeps_marker_like_content_inside_hunks() + test_parse_unified_diff_keeps_spaced_paths_without_marker_lines() test_diff_canvas_payload_keeps_report_for_local_mode_switch() test_parse_unified_diff_caps_files_without_merging_hunks() print("unified diff canvas tests passed")