From 3e9611b9689597d86814d087d1b9b37be92e0451 Mon Sep 17 00:00:00 2001 From: v1shay Date: Sat, 10 Oct 2026 23:38:46 -0700 Subject: [PATCH 1/2] fix(stats): preserve whitespace in filenames Commit.stats strips leading and trailing whitespace from filenames after splitting the diff output into records. This merges distinct paths such as `name`, ` name`, and `name `, overwrites their per-file statistics, and turns an all-space filename into an empty key. Keep the filename field intact. Add an integration regression covering root commits, later modifications, empty commits, and deletions. Store blobs directly to avoid relying on trailing-space worktree filenames. The regression fails before this change and passes afterward. On Linux with Python 3.12, the full suite passes: 1690 passed, 89 skipped, 1 xfailed, and 64 subtests passed. Pre-commit, mypy, and basedpyright also pass. Prepared by OpenAI Codex (dot); human review is pending. --- git/util.py | 2 +- test/test_commit.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/git/util.py b/git/util.py index 61ddab861..7fd061c7c 100644 --- a/git/util.py +++ b/git/util.py @@ -1107,7 +1107,7 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats": "lines": insertions + deletions, "change_type": change_type, } - hsh["files"][filename.strip()] = files_dict + hsh["files"][filename] = files_dict return Stats(hsh["total"], hsh["files"]) diff --git a/test/test_commit.py b/test/test_commit.py index 08ba4a634..ac155e0df 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -17,6 +17,7 @@ from git import Actor, Commit, Repo from git.exc import UnsafeOptionError +from git.index.typ import BaseIndexEntry from git.objects.util import tzoffset, utc from git.repo.fun import touch @@ -166,6 +167,36 @@ def check_entries(d, has_change_type=False): self.assertEqual(commit.committer_tz_offset, 14400, commit.committer_tz_offset) self.assertEqual(commit.message, "initial project\n") + @with_rw_directory + def test_stats_preserve_filename_spaces(self, rw_dir): + repo = Repo.init(rw_dir) + actor = Actor("Test", "test@example.com") + paths = ("name", " name", "name ", " name ", " ") + # Store blobs directly so trailing spaces work on Windows as well. + for multiplier, change_type in ((1, "A"), (2, "M")): + for count, path in enumerate(paths, 1): + data = b"line\n" * count * 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) + stats = commit.stats + assert stats.total == {"insertions": 15, "deletions": 0, "lines": 15, "files": 5} + assert stats.files == { + path: {"insertions": count, "deletions": 0, "lines": count, "change_type": change_type} + for count, path in enumerate(paths, 1) + } + empty_stats = repo.index.commit("No changes", author=actor, committer=actor).stats + assert empty_stats.files == {} + assert empty_stats.total == {"insertions": 0, "deletions": 0, "lines": 0, "files": 0} + + repo.index.remove(list(paths)) + removed_stats = repo.index.commit("Remove files", author=actor, committer=actor).stats + assert removed_stats.files == { + path: {"insertions": 0, "deletions": 2 * count, "lines": 2 * count, "change_type": "D"} + for count, path in enumerate(paths, 1) + } + assert removed_stats.total == {"insertions": 0, "deletions": 30, "lines": 30, "files": 5} + def test_renames(self): commit = self.rorepo.commit("185d847ec7647fd2642a82d9205fb3d07ea71715") files = commit.stats.files From 963ca1c7d3bccdfad66293d5417f065ce5829b15 Mon Sep 17 00:00:00 2001 From: v1shay Date: Sun, 11 Oct 2026 00:13:14 -0700 Subject: [PATCH 2/2] test(stats): avoid normalizing fixture paths on Windows The stats regression's root and modification assertions pass on Windows, but IndexFile.remove normalizes the all-space test path to `.` and makes `git rm` fail before testing deletion statistics. Clear and write the fixture's in-memory index instead, just as the test already stores its initial blobs directly. The resulting empty-tree commit still exercises deletion statistics for every original filename. The full Linux suite again passes: 1690 passed, 89 skipped, 1 xfailed, and 64 subtests. Focused commit/util/diff tests, pre-commit, mypy, and basedpyright pass. Windows CI will verify the portability correction. Prepared by OpenAI Codex (dot); human review is pending. --- test/test_commit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_commit.py b/test/test_commit.py index ac155e0df..379bc831b 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -189,7 +189,10 @@ def test_stats_preserve_filename_spaces(self, rw_dir): assert empty_stats.files == {} assert empty_stats.total == {"insertions": 0, "deletions": 0, "lines": 0, "files": 0} - repo.index.remove(list(paths)) + # Avoid filesystem path normalization of space-only names on Windows. + index = repo.index + index.entries.clear() + index.write() removed_stats = repo.index.commit("Remove files", author=actor, committer=actor).stats assert removed_stats.files == { path: {"insertions": 0, "deletions": 2 * count, "lines": 2 * count, "change_type": "D"}