Skip to content

Commit 9658d0f

Browse files
authored
Merge pull request #2300 from Keerthana-64/blame-porcelain-line-feeds
fix(repo): split `git blame` porcelain output at line feeds only
2 parents e5b960a + 6747637 commit 9658d0f

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

‎git/repo/base.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
__all__ = ["Repo"]
99

1010
import gc
11+
from io import BytesIO
1112
import logging
1213
import os
1314
import os.path as osp
@@ -1358,8 +1359,10 @@ class InfoTD(TypedDict, total=False):
13581359

13591360
info: InfoTD = {}
13601361

1361-
keepends = True
1362-
for line_bytes in data.splitlines(keepends):
1362+
# Git ends each porcelain line with a line feed only. bytes.splitlines() also
1363+
# splits at a lone carriage return, which a line of file content can contain,
1364+
# and would then parse the rest of that content line as blame headers.
1365+
for line_bytes in BytesIO(data):
13631366
line_str = ""
13641367
try:
13651368
line_str = line_bytes.rstrip().decode(defenc)

‎test/test_repo.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,9 +839,44 @@ def test_blame_complex_revision(self, git):
839839
def test_blame_accepts_rev_opts(self, git):
840840
expected_args = ["blame", "HEAD", "-M", "-C", "-C", "--", "README.md"]
841841
boilerplate_kwargs = {"p": True, "stdout_as_string": False}
842+
git.return_value = b""
842843
self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"])
843844
git.assert_called_once_with(*expected_args, **boilerplate_kwargs)
844845

846+
@with_rw_directory
847+
def test_blame_content_with_carriage_returns(self, rw_dir):
848+
repo = Repo.init(rw_dir)
849+
with repo.config_writer() as writer:
850+
writer.set_value("user", "name", "GitPython Tests")
851+
writer.set_value("user", "email", "gitpython@example.com")
852+
853+
# A lone CR inside a line of content, followed by text shaped like blame
854+
# porcelain headers for a forged commit and author.
855+
forged = "\r".join(
856+
[
857+
"ok",
858+
"0" * 40 + " 2 2 1",
859+
"author Forged Author",
860+
"author-mail <forged@example.com>",
861+
"author-time 1000000000",
862+
"committer Forged Author",
863+
"committer-mail <forged@example.com>",
864+
"committer-time 1000000000",
865+
"\tforged()",
866+
]
867+
)
868+
tracked_file = Path(rw_dir) / "file.txt"
869+
tracked_file.write_bytes(b"first\n" + forged.encode("ascii") + b"\n")
870+
repo.index.add([str(tracked_file)])
871+
commit = repo.index.commit("init")
872+
873+
blames = repo.blame("HEAD", "file.txt")
874+
875+
self.assertEqual(len(blames), 1)
876+
self.assertEqual(blames[0][0], commit)
877+
self.assertEqual(blames[0][0].author.name, "GitPython Tests")
878+
self.assertEqual(blames[0][1], ["first", forged])
879+
845880
@with_rw_repo("HEAD", bare=False)
846881
def test_untracked_files(self, rwrepo):
847882
for run, repo_add in enumerate((rwrepo.index.add, rwrepo.git.add)):

0 commit comments

Comments
 (0)