Skip to content

Commit 3900431

Browse files
committed
fix(repo): unquote the filename line in Repo.blame_incremental
`Repo.blame_incremental` took `BlameEntry.orig_path` from the `filename` line of `git blame --incremental` verbatim. Git writes that line with `write_name_quoted`, so a path containing a double quote, a backslash, a control character or, with the default `core.quotePath=true`, any non-ASCII byte is C-quoted: a line attributed to `café.txt` came back as `orig_path == '"caf\303\251.txt"'` and `"quoted".txt` as `'"\\"quoted\\".txt"'`, names that exist nowhere in the repository. The quoted form reaches callers whenever the blamed file was renamed from such a name, which the author of the history chooses. Decode a quoted `filename` value with the existing `_unquote_path` helper from `git.diff`, which already reverses Git's quoting for diff headers; unquoted values are unchanged. The non-incremental porcelain parser in `Repo.blame` reads the same line but does not expose it, so it needs no change. Adds a `blame-quoted-filename` case to `tests/fixtures/repo.sh` and `test_blame_incremental_unquotes_filenames` in `test/test_repo.py`, which fails before the change and passes after on both the CLI and the Gix backend. Ruff, mypy and ShellCheck pass.
1 parent 4debc51 commit 3900431

3 files changed

Lines changed: 25 additions & 0 deletions

File tree

‎git/repo/base.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,8 @@ def blame_incremental(
12931293
If you combine all line number ranges outputted by this command, you should get
12941294
a continuous range spanning all line numbers in the file.
12951295
"""
1296+
from git.diff import _unquote_path
1297+
12961298
if not allow_unsafe_options:
12971299
Git.check_unsafe_options(
12981300
options=Git._option_candidates([rev], kwargs),
@@ -1375,6 +1377,10 @@ def blame_incremental(
13751377
orig_filename = value
13761378
break
13771379

1380+
# Git C-quotes a filename containing control, quote, backslash or,
1381+
# with core.quotePath, non-ASCII bytes, the way it does in diff output.
1382+
if orig_filename.startswith(b'"') and orig_filename.endswith(b'"'):
1383+
orig_filename = _unquote_path(orig_filename[1:-1])
13781384
yield BlameEntry(
13791385
commits[hexsha],
13801386
range(lineno, lineno + num_lines),

‎test/test_repo.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,17 @@ def test_blame_content_with_carriage_returns(self):
792792
self.assertEqual(blames[0][0].author.name, "GitPython Tests")
793793
self.assertEqual(blames[0][1], ["first", forged])
794794

795+
def test_blame_incremental_unquotes_filenames(self):
796+
# Git C-quotes the filename line of its incremental output for a path with
797+
# non-ASCII, control, quote or backslash bytes. The entry must name the path.
798+
for name in ("caf\u00e9.txt", '"quoted".txt', "tab\tname.txt", "back\\slash.txt"):
799+
with self.subTest(name=name), fixture_repo("repo.sh", "blame-quoted-filename", name) as repo:
800+
quoted = repo.git.blame("HEAD", "-p", "--incremental", "--", "plain.txt", stdout_as_string=False)
801+
assert b'\nfilename "' in quoted
802+
entries = list(repo.blame_incremental("HEAD", "plain.txt"))
803+
self.assertEqual([entry.orig_path for entry in entries], [name])
804+
self.assertEqual([entry.linenos for entry in entries], [range(1, 3)])
805+
795806
def test_untracked_files(self):
796807
for command in ("index", "git"):
797808
with fixture_repo("repo.sh", "untracked", write=True) as repo:

‎tests/fixtures/repo.sh‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ blame-carriage-returns)
123123
git -C repo add file.txt
124124
git -C repo commit -qm init --author='GitPython Tests <gitpython@example.com>'
125125
;;
126+
blame-quoted-filename)
127+
git init -q repo
128+
printf 'first\nsecond\n' > "repo/$2"
129+
git -C repo add -- "$2"
130+
git -C repo commit -qm init --author='GitPython Tests <gitpython@example.com>'
131+
git -C repo mv -- "$2" plain.txt
132+
git -C repo commit -qm rename --author='GitPython Tests <gitpython@example.com>'
133+
;;
126134
relocated-git-dir)
127135
git init -q repo
128136
mv repo/.git repo/.real

0 commit comments

Comments
 (0)