From 6ef056f842fe7a302ae7bf7f332eec60802929ea Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 7 Dec 2025 21:28:13 +0530 Subject: [PATCH 1/2] Fix diff parsing to support mnemonicPrefix configuration Fixes #2013 When diff.mnemonicPrefix=true is set in git config, git uses different prefixes for diff paths instead of the standard a/ and b/: - c/ for commit - w/ for worktree - i/ for index - o/ for object - h/ for HEAD Previously, the diff regex and decode_path() function only accepted a/ and b/ prefixes, causing create_patch=True diffs to fail parsing. Changes: - Update re_header regex to accept [abciwoh]/ prefixes - Update decode_path() assertion to accept all valid mnemonic prefixes - Add test case for mnemonicPrefix-style diffs --- git/diff.py | 9 +++++++-- test/test_diff.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/git/diff.py b/git/diff.py index 23cb5675e..ddbe7c594 100644 --- a/git/diff.py +++ b/git/diff.py @@ -113,7 +113,10 @@ def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]: path = _octal_byte_re.sub(_octal_repl, path) if has_ab_prefix: - assert path.startswith(b"a/") or path.startswith(b"b/") + # Support standard (a/b) and mnemonicPrefix (c/w/i/o/h) prefixes + # See git-config diff.mnemonicPrefix documentation + valid_prefixes = (b"a/", b"b/", b"c/", b"w/", b"i/", b"o/", b"h/") + assert any(path.startswith(p) for p in valid_prefixes), f"Unexpected path prefix: {path[:10]}" path = path[2:] return path @@ -367,10 +370,12 @@ class Diff: """ # Precompiled regex. + # Note: The path prefixes support both default (a/b) and mnemonicPrefix mode + # which can use prefixes like c/ (commit), w/ (worktree), i/ (index), o/ (object) re_header = re.compile( rb""" ^diff[ ]--git - [ ](?P"?[ab]/.+?"?)[ ](?P"?[ab]/.+?"?)\n + [ ](?P"?[abciwoh]/.+?"?)[ ](?P"?[abciwoh]/.+?"?)\n (?:^old[ ]mode[ ](?P\d+)\n ^new[ ]mode[ ](?P\d+)(?:\n|$))? (?:^similarity[ ]index[ ]\d+%\n diff --git a/test/test_diff.py b/test/test_diff.py index 612fbd9e0..61783e737 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -281,6 +281,38 @@ def test_diff_unsafe_paths(self): self.assertEqual(res[13].a_path, 'a/"with-quotes"') self.assertEqual(res[13].b_path, 'b/"with even more quotes"') + def test_diff_mnemonic_prefix(self): + """Test that diff parsing works with mnemonicPrefix enabled. + + When diff.mnemonicPrefix=true is set in git config, git uses different + prefixes for diff paths: + - c/ for commit + - w/ for worktree + - i/ for index + - o/ for object + + This addresses issue #2013 where the regex only matched [ab]/ prefixes. + """ + # Create a diff with mnemonicPrefix-style c/ and w/ prefixes + # Using valid 40-char hex SHAs + diff_mnemonic = b"""diff --git c/.vscode/launch.json w/.vscode/launch.json +index 1234567890abcdef1234567890abcdef12345678..abcdef1234567890abcdef1234567890abcdef12 100644 +--- c/.vscode/launch.json ++++ w/.vscode/launch.json +@@ -1,3 +1,3 @@ +-old content ++new content +""" + diff_proc = StringProcessAdapter(diff_mnemonic) + diffs = Diff._index_from_patch_format(self.rorepo, diff_proc) + + # Should parse successfully (previously would fail or return empty) + self.assertEqual(len(diffs), 1) + diff = diffs[0] + # The path should be extracted correctly (without the c/ or w/ prefix) + self.assertEqual(diff.a_path, ".vscode/launch.json") + self.assertEqual(diff.b_path, ".vscode/launch.json") + def test_diff_patch_format(self): # Test all of the 'old' format diffs for completeness - it should at least be # able to deal with it. From 6e77e26096ec3a2e8f204e2b1b10a068181537ce Mon Sep 17 00:00:00 2001 From: Paul Desai Date: Mon, 8 Dec 2025 14:00:42 +0530 Subject: [PATCH 2/2] Fix lint and mypy errors - Remove whitespace from blank lines in docstrings (ruff W293) - Use !r format specifier for bytes in f-string (mypy str-bytes-safe) --- git/diff.py | 2 +- test/test_diff.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/git/diff.py b/git/diff.py index ddbe7c594..f82057ffb 100644 --- a/git/diff.py +++ b/git/diff.py @@ -116,7 +116,7 @@ def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]: # Support standard (a/b) and mnemonicPrefix (c/w/i/o/h) prefixes # See git-config diff.mnemonicPrefix documentation valid_prefixes = (b"a/", b"b/", b"c/", b"w/", b"i/", b"o/", b"h/") - assert any(path.startswith(p) for p in valid_prefixes), f"Unexpected path prefix: {path[:10]}" + assert any(path.startswith(p) for p in valid_prefixes), f"Unexpected path prefix: {path[:10]!r}" path = path[2:] return path diff --git a/test/test_diff.py b/test/test_diff.py index 61783e737..79ca3ef67 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -283,14 +283,14 @@ def test_diff_unsafe_paths(self): def test_diff_mnemonic_prefix(self): """Test that diff parsing works with mnemonicPrefix enabled. - + When diff.mnemonicPrefix=true is set in git config, git uses different prefixes for diff paths: - c/ for commit - - w/ for worktree + - w/ for worktree - i/ for index - o/ for object - + This addresses issue #2013 where the regex only matched [ab]/ prefixes. """ # Create a diff with mnemonicPrefix-style c/ and w/ prefixes @@ -305,7 +305,7 @@ def test_diff_mnemonic_prefix(self): """ diff_proc = StringProcessAdapter(diff_mnemonic) diffs = Diff._index_from_patch_format(self.rorepo, diff_proc) - + # Should parse successfully (previously would fail or return empty) self.assertEqual(len(diffs), 1) diff = diffs[0]