From 6ef056f842fe7a302ae7bf7f332eec60802929ea Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 7 Dec 2025 21:28:13 +0530 Subject: [PATCH 1/3] 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/3] 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] From 4aeaf56f8211fb6c307a465d857025d4994adc2b Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 9 Dec 2025 10:34:32 +0530 Subject: [PATCH 3/3] Address review feedback: improve test coverage for all mnemonicPrefix values Changes per reviewer (Byron) and Copilot suggestions: - Add h/ (HEAD) to the comment listing supported prefixes - Update test docstring to include h/ prefix - Expand test to cover all prefix combinations using subTest: - c/ (commit) vs w/ (worktree) - c/ (commit) vs i/ (index) - i/ (index) vs w/ (worktree) - o/ (object) vs w/ (worktree) - h/ (HEAD) vs i/ (index) - h/ (HEAD) vs w/ (worktree) This ensures the regex pattern [abciwoh] and decode_path() work correctly with all supported mnemonicPrefix values. --- git/diff.py | 2 +- test/test_diff.py | 52 ++++++++++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/git/diff.py b/git/diff.py index f82057ffb..eeac6767b 100644 --- a/git/diff.py +++ b/git/diff.py @@ -371,7 +371,7 @@ 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) + # which can use prefixes like c/ (commit), w/ (worktree), i/ (index), o/ (object), and h/ (HEAD) re_header = re.compile( rb""" ^diff[ ]--git diff --git a/test/test_diff.py b/test/test_diff.py index 79ca3ef67..1c4d521da 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -290,28 +290,42 @@ def test_diff_mnemonic_prefix(self): - w/ for worktree - i/ for index - o/ for object + - h/ for HEAD 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") + # Test all mnemonicPrefix combinations + # Each tuple is (a_prefix, b_prefix) representing different comparison types + prefix_pairs = [ + (b"c/", b"w/"), # commit vs worktree + (b"c/", b"i/"), # commit vs index + (b"i/", b"w/"), # index vs worktree + (b"o/", b"w/"), # object vs worktree + (b"h/", b"i/"), # HEAD vs index + (b"h/", b"w/"), # HEAD vs worktree + ] + + for a_prefix, b_prefix in prefix_pairs: + with self.subTest(a_prefix=a_prefix, b_prefix=b_prefix): + diff_mnemonic = ( + b"diff --git " + a_prefix + b".vscode/launch.json " + b_prefix + b".vscode/launch.json\n" + b"index 1234567890abcdef1234567890abcdef12345678.." + b"abcdef1234567890abcdef1234567890abcdef12 100644\n" + b"--- " + a_prefix + b".vscode/launch.json\n" + b"+++ " + b_prefix + b".vscode/launch.json\n" + b"@@ -1,3 +1,3 @@\n" + b"-old content\n" + b"+new content\n" + ) + 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 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