Skip to content

Commit bf649ff

Browse files
fix: mixed quoted headers and rename-to prefix stripping
- Split diff --git headers across all four quoted/unquoted combinations - Stop stripping a//b/ from rename/copy from/to lines (never prefixed by git) - Regression tests: plain<->quoted renames with edits, pure rename into a/ dir
1 parent 7edc36d commit bf649ff

2 files changed

Lines changed: 41 additions & 10 deletions

File tree

‎src/gitmate/diff_extractor.py‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,18 +196,17 @@ def _parse_name_status(output: str) -> dict[str, tuple[FileStatus, str | None]]:
196196

197197

198198
def _split_header_fallback(rest: str) -> tuple[str | None, str | None]:
199-
"""Split `a/X b/Y` on the last ` b/`; prefixes are pinned by our git flags.
199+
"""Split `a/X b/Y` across quoted/unquoted combinations on either side.
200200
201-
Correct for any spaces inside X or Y. Only a filename literally containing
202-
` b/` defeats it; the rename/copy metadata and ---/+++ re-keying backstop
203-
even that pathological case.
201+
Our git flags pin the prefixes, so Y always begins `b/` or `"b/`, and
202+
rfind lets the true boundary win over interior lookalikes. Rename/copy
203+
metadata and ---/+++ re-keying backstop anything stranger still.
204204
"""
205-
if rest.startswith('"'):
206-
# Both paths quoted: "a/X" "b/Y" — split between the quoted strings.
207-
idx = rest.find('" "')
205+
if rest.endswith('"'):
206+
idx = rest.rfind(' "b/')
208207
if idx == -1:
209208
return None, None
210-
return _strip_git_prefix(rest[: idx + 1]), _strip_git_prefix(rest[idx + 1 :].strip())
209+
return _strip_git_prefix(rest[:idx]), _strip_git_prefix(rest[idx + 1 :])
211210
idx = rest.rfind(" b/")
212211
if idx < 2:
213212
return None, None
@@ -289,11 +288,13 @@ def _parse_patch(output: str) -> dict[str, str]:
289288
def flush() -> None:
290289
nonlocal current, minus
291290
if current is not None or minus is not None:
291+
# Rename/copy lines carry bare paths (never a//b/ prefixed), so
292+
# decode quoting only; stripping would eat real a//b/ directories.
292293
for cl in chunk:
293294
if cl.startswith(("rename to ", "copy to ")):
294-
current = _strip_git_prefix(cl.split(" ", 2)[2].rstrip("\n"))
295+
current = _decode_quoted(cl.split(" ", 2)[2].rstrip("\n"))
295296
elif cl.startswith(("rename from ", "copy from ")):
296-
minus = _strip_git_prefix(cl.split(" ", 2)[2].rstrip("\n"))
297+
minus = _decode_quoted(cl.split(" ", 2)[2].rstrip("\n"))
297298
key = current if current is not None and current != "/dev/null" else minus
298299
if key is not None:
299300
if key not in chunks:

‎tests/test_diff_extractor.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,33 @@ def test_non_utf8_git_output_clean_error(tmp_path: Path, monkeypatch: pytest.Mon
228228
monkeypatch.setenv("PATH", f"{bindir}{os.pathsep}{os.environ['PATH']}")
229229
with pytest.raises(GitCommandError):
230230
DiffExtractor(tmp_path).staged()
231+
232+
233+
def test_rename_plain_to_quoted_keeps_patch(git_repo: Path) -> None:
234+
commit_file(git_repo, "plain.txt", "line 1\n", "add plain")
235+
subprocess.run(["git", "mv", "plain.txt", 'weird"name.txt'], cwd=git_repo, check=True)
236+
(git_repo / 'weird"name.txt').write_text("line 1\nline 2\n", encoding="utf-8")
237+
subprocess.run(["git", "add", "-A"], cwd=git_repo, check=True)
238+
(diff,) = DiffExtractor(git_repo).staged()
239+
assert (diff.path, diff.old_path, diff.status) == ('weird"name.txt', "plain.txt", "renamed")
240+
assert "+line 2" in diff.patch_text
241+
242+
243+
def test_rename_quoted_to_plain_keeps_patch(git_repo: Path) -> None:
244+
commit_file(git_repo, 'weird"name.txt', "line 1\n", "add weird")
245+
subprocess.run(["git", "mv", 'weird"name.txt', "plain.txt"], cwd=git_repo, check=True)
246+
(git_repo / "plain.txt").write_text("line 1\nline 2\n", encoding="utf-8")
247+
subprocess.run(["git", "add", "-A"], cwd=git_repo, check=True)
248+
(diff,) = DiffExtractor(git_repo).staged()
249+
assert (diff.path, diff.old_path, diff.status) == ("plain.txt", 'weird"name.txt', "renamed")
250+
assert "+line 2" in diff.patch_text
251+
252+
253+
def test_pure_rename_into_a_dir_keeps_patch(git_repo: Path) -> None:
254+
commit_file(git_repo, "old.py", "line 1\n", "add old")
255+
(git_repo / "a").mkdir()
256+
subprocess.run(["git", "mv", "old.py", "a/foo.py"], cwd=git_repo, check=True)
257+
subprocess.run(["git", "add", "-A"], cwd=git_repo, check=True)
258+
(diff,) = DiffExtractor(git_repo).staged()
259+
assert (diff.path, diff.old_path, diff.status) == ("a/foo.py", "old.py", "renamed")
260+
assert "rename to a/foo.py" in diff.patch_text

0 commit comments

Comments
 (0)