`Repo.blame` runs `git blame -p` and iterated over its stdout with
`bytes.splitlines()`, which breaks at a lone carriage return as well as
at a line feed. git ends every porcelain line with a line feed only, and
the content lines in that output are the raw lines of the blamed file,
so a CR inside a line of file content ended the line early in GitPython
and the rest of it was parsed as porcelain headers.
A file line such as
ok\r<40 hex digits> 2 2 1\rauthor Forged Author\rauthor-mail <forged@example.com>\r...\r\tforged()
made `Repo.blame` return an extra entry that attributes `forged()` to a
commit id and author of the file author's choosing, while `git blame`
attributes the whole line to the commit that really added it. Short of a
forgery, any line with a lone CR was truncated at the CR and the rest of
it dropped. Blamed file content is controlled by whoever wrote it, so a
tool that uses `Repo.blame` to decide who wrote a line (code ownership,
review routing, audit) could be shown attribution chosen by that author.
Iterate over a `BytesIO` of the output instead, which splits at line
feeds only and keeps the terminators, so CRLF lines and the binary-line
handling are unchanged. `blame_incremental` does not print file content
and is unaffected.
Adds `test_blame_content_with_carriage_returns` to `test/test_repo.py`;
it fails before this change and passes after. `test_blame_accepts_rev_opts`
mocks `Git._call_process`, so it now returns `b""` instead of a
`MagicMock` to the parser. `ruff check`, `ruff format --check` and `mypy`
pass. In a full clone prepared with `init-tests-after-clone.sh`, the
pytest suite passes except `test_diff_with_staged_file`, `test_empty_repo`
and `test_rebasing`, which fail the same way without this change.
repo.blame split git blame -p output with bytes.splitlines(), which also breaks at a lone carriage return, but porcelain content lines are the raw lines of the blamed file and git ends each porcelain line with a line feed only. a CR inside a file line therefore ended it early and the rest was parsed as porcelain headers: a line like ok\r<40 hex> 2 2 1\rauthor Forged Author\rauthor-mail <...>\r...\r\tforged() made repo.blame return an extra entry attributing forged() to a commit id and author chosen by whoever wrote the file, while git blame attributes the whole line to the real commit, and any line with a lone CR was truncated. iterate over a BytesIO of the output instead, which splits at line feeds only and keeps terminators, so CRLF and binary lines are unchanged; blame_incremental prints no content and is unaffected. this is the same separator mismatch #2290 fixed for tag headers, in a different parser. the new test in test/test_repo.py fails before and passes after, and test_blame_accepts_rev_opts now gives the parser b"" instead of a MagicMock.