diff --git a/git/objects/commit.py b/git/objects/commit.py index 3239079c1..b7f3e1709 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -405,6 +405,8 @@ def stats(self) -> Stats: def process_lines(lines: List[str]) -> str: text = "" for file_info, line in zip(lines, lines[len(lines) // 2 :]): + if not line: + continue change_type = file_info.split("\t")[0][-1] (insertions, deletions, filename) = line.split("\t") text += "%s\t%s\t%s\t%s\n" % (change_type, insertions, deletions, filename) @@ -413,12 +415,12 @@ def process_lines(lines: List[str]) -> str: if not self.parents: lines = self.repo.git.diff_tree( self.hexsha, "--", numstat=True, no_renames=True, root=True, raw=True - ).splitlines()[1:] + ).split("\n")[1:] text = process_lines(lines) else: lines = self.repo.git.diff( self.parents[0].hexsha, self.hexsha, "--", numstat=True, no_renames=True, raw=True - ).splitlines() + ).split("\n") text = process_lines(lines) return Stats._list_from_string(self.repo, text) diff --git a/git/util.py b/git/util.py index 137318eb8..5fabd9b42 100644 --- a/git/util.py +++ b/git/util.py @@ -1111,7 +1111,9 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats": "total": {"insertions": 0, "deletions": 0, "lines": 0, "files": 0}, "files": {}, } - for line in text.splitlines(): + for line in text.split("\n"): + if not line: + continue (change_type, raw_insertions, raw_deletions, filename) = line.split("\t") insertions = raw_insertions != "-" and int(raw_insertions) or 0 deletions = raw_deletions != "-" and int(raw_deletions) or 0 diff --git a/test/test_commit.py b/test/test_commit.py index a22daa87a..1e70f46a8 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -200,6 +200,24 @@ def test_stats_preserve_filename_spaces(self, rw_dir): } assert removed_stats.total == {"insertions": 0, "deletions": 30, "lines": 30, "files": 5} + @with_rw_directory + def test_stats_preserve_unicode_line_separators(self, rw_dir): + with Repo.init(rw_dir) as repo: + repo.git.config("core.quotePath", "false") + actor = Actor("Test", "test@example.com") + paths = [f"before{separator}after.txt" for separator in ("\x85", "\u2028", "\u2029")] + for multiplier, change_type in ((1, "A"), (2, "M")): + for path in paths: + data = b"line\n" * multiplier + blob = repo.odb.store(IStream("blob", len(data), BytesIO(data))) + repo.index.add([BaseIndexEntry((0o100644, blob.binsha, 0, path))]) + commit = repo.index.commit("Update files", author=actor, committer=actor) + assert commit.stats.total == {"insertions": 3, "deletions": 0, "lines": 3, "files": 3} + assert commit.stats.files == { + path: {"insertions": 1, "deletions": 0, "lines": 1, "change_type": change_type} for path in paths + } + assert repo.index.commit("No changes", author=actor, committer=actor).stats.files == {} + def test_renames(self): commit = self.rorepo.commit("185d847ec7647fd2642a82d9205fb3d07ea71715") files = commit.stats.files