From 3d318c09ffecb98023ab0a3c335a157a65eac14e Mon Sep 17 00:00:00 2001 From: NopeCharon <115794254+NopeCharon@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:22:25 +0530 Subject: [PATCH] fix(work): keep spaced paths from diff headers in the attempt diff canvas Git leaves paths that only contain spaces unquoted, so the header for "my notes.txt" is "diff --git a/my notes.txt b/my notes.txt". _paths_from_diff_header split it with shlex and took the third and fourth words, which yields "a/my" and "notes.txt". Text edits hid this because the following "--- a/..." / "+++ b/..." lines overwrite the path. Deleted files (+++ /dev/null), binary files and mode-only changes have no such line, so the canvas named them "notes.txt" and then appended the real path from changed_files as a second, hunk-less "modified" entry. Without a rename or copy, both header names are the same path behind their prefixes. For unquoted headers, split at the space where the two names match after removing their first path component, or where they are identical (diff.noprefix). Renames and copies keep the previous fallback and are still corrected by their "rename from/to" lines. Quoted headers are unchanged. Fixes #280 --- server/unified_diff.py | 17 ++++++++++++++++ tests/test_unified_diff_canvas.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) 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")