diff --git a/git/repo/base.py b/git/repo/base.py index a4161d620..6bf885280 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -8,6 +8,7 @@ __all__ = ["Repo"] import gc +from io import BytesIO import logging import os import os.path as osp @@ -1358,8 +1359,10 @@ class InfoTD(TypedDict, total=False): info: InfoTD = {} - keepends = True - for line_bytes in data.splitlines(keepends): + # Git ends each porcelain line with a line feed only. bytes.splitlines() also + # splits at a lone carriage return, which a line of file content can contain, + # and would then parse the rest of that content line as blame headers. + for line_bytes in BytesIO(data): line_str = "" try: line_str = line_bytes.rstrip().decode(defenc) diff --git a/test/test_repo.py b/test/test_repo.py index 08cd4a1bf..8106a88ca 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -839,9 +839,44 @@ def test_blame_complex_revision(self, git): def test_blame_accepts_rev_opts(self, git): expected_args = ["blame", "HEAD", "-M", "-C", "-C", "--", "README.md"] boilerplate_kwargs = {"p": True, "stdout_as_string": False} + git.return_value = b"" self.rorepo.blame("HEAD", "README.md", rev_opts=["-M", "-C", "-C"]) git.assert_called_once_with(*expected_args, **boilerplate_kwargs) + @with_rw_directory + def test_blame_content_with_carriage_returns(self, rw_dir): + repo = Repo.init(rw_dir) + with repo.config_writer() as writer: + writer.set_value("user", "name", "GitPython Tests") + writer.set_value("user", "email", "gitpython@example.com") + + # A lone CR inside a line of content, followed by text shaped like blame + # porcelain headers for a forged commit and author. + forged = "\r".join( + [ + "ok", + "0" * 40 + " 2 2 1", + "author Forged Author", + "author-mail ", + "author-time 1000000000", + "committer Forged Author", + "committer-mail ", + "committer-time 1000000000", + "\tforged()", + ] + ) + tracked_file = Path(rw_dir) / "file.txt" + tracked_file.write_bytes(b"first\n" + forged.encode("ascii") + b"\n") + repo.index.add([str(tracked_file)]) + commit = repo.index.commit("init") + + blames = repo.blame("HEAD", "file.txt") + + self.assertEqual(len(blames), 1) + self.assertEqual(blames[0][0], commit) + self.assertEqual(blames[0][0].author.name, "GitPython Tests") + self.assertEqual(blames[0][1], ["first", forged]) + @with_rw_repo("HEAD", bare=False) def test_untracked_files(self, rwrepo): for run, repo_add in enumerate((rwrepo.index.add, rwrepo.git.add)):