diff --git a/git/repo/base.py b/git/repo/base.py index 0de87d01c..af182740b 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -1293,6 +1293,8 @@ def blame_incremental( If you combine all line number ranges outputted by this command, you should get a continuous range spanning all line numbers in the file. """ + from git.diff import _unquote_path + if not allow_unsafe_options: Git.check_unsafe_options( options=Git._option_candidates([rev], kwargs), @@ -1375,6 +1377,9 @@ def blame_incremental( orig_filename = value break + if orig_filename.startswith(b'"') and orig_filename.endswith(b'"'): + orig_filename = _unquote_path(orig_filename[1:-1]) + yield BlameEntry( commits[hexsha], range(lineno, lineno + num_lines), diff --git a/test/test_repo.py b/test/test_repo.py index 7ecf2a920..38ac3e630 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -36,6 +36,7 @@ Submodule, Tree, ) +from git.compat import safe_decode from git.exc import BadObject, UnsafeOptionError, UnsafeProtocolError from git.repo.fun import find_worktree_git_dir from git.util import bin_to_hex, cwd, rmfile, rmtree @@ -754,6 +755,35 @@ def test_blame_incremental(self, git): ), ) + def test_blame_incremental_preserves_historical_paths(self): + self.maxDiff = None + paths = ( + b"plain.txt", + b" spaced name.txt ", + "café.txt".encode(), + b"byte-\xff.txt", + b'"literal-\\303\\251.txt"', + b'quote"name.txt', + b"back\\slash.txt", + b"control\a\b\t\n\v\f\r.txt", + ) + for quote_path in ("true", "false"): + for i, path in enumerate(paths): + with self.subTest(quote_path=quote_path, path=path), fixture_repo( + "blame-paths.sh", quote_path, subdir=str(i) + ) as repo: + entries = list(repo.blame_incremental("HEAD", "current.txt")) + original = repo.commit("HEAD~2") + self.assertEqual( + sorted(entries, key=lambda e: e.linenos.start), + [ + (original, range(1, 2), safe_decode(path), range(1, 2)), + (repo.head.commit, range(2, 3), "current.txt", range(2, 3)), + (original, range(3, 4), safe_decode(path), range(3, 4)), + ], + ) + self.assertEqual(list(repo.blame("HEAD", "current.txt", incremental=True)), entries) + @mock.patch.object(Git, "_call_process") def test_blame_complex_revision(self, git): git.return_value = fixture("blame_complex_revision") diff --git a/tests/fixtures/blame-paths.sh b/tests/fixtures/blame-paths.sh new file mode 100644 index 000000000..3ce20214c --- /dev/null +++ b/tests/fixtures/blame-paths.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -eu +# C-quoted tree input preserves control bytes through Windows fixture creation. +paths=('plain.txt' ' spaced name.txt ' '"caf\303\251.txt"' '"byte-\377.txt"' '"\"literal-\\303\\251.txt\""' '"quote\"name.txt"' '"back\\slash.txt"' '"control\a\b\t\n\v\f\r.txt"') +for i in "${!paths[@]}"; do + git init -q --initial-branch=main "$i" + ( + cd "$i" + git config core.quotePath "$1" + blob=$(printf 'first\nmiddle\nlast\n' | git hash-object -w --stdin) + tree=$(printf '100644 blob %s\t%s\n' "$blob" "${paths[$i]}" | git mktree) + original=$(git commit-tree "$tree" -m original) + tree=$(printf '100644 blob %s\tcurrent.txt\0' "$blob" | git mktree -z) + renamed=$(git commit-tree "$tree" -p "$original" -m rename) + blob=$(printf 'first\nchanged\nlast\n' | git hash-object -w --stdin) + tree=$(printf '100644 blob %s\tcurrent.txt\0' "$blob" | git mktree -z) + edited=$(git commit-tree "$tree" -p "$renamed" -m edit) + git update-ref refs/heads/main "$edited" + ) +done