Skip to content

Commit 3e9611b

Browse files
committed
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.
1 parent 804bc80 commit 3e9611b

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

‎git/util.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1107,7 +1107,7 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats":
11071107
"lines": insertions + deletions,
11081108
"change_type": change_type,
11091109
}
1110-
hsh["files"][filename.strip()] = files_dict
1110+
hsh["files"][filename] = files_dict
11111111
return Stats(hsh["total"], hsh["files"])
11121112

11131113

‎test/test_commit.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from git import Actor, Commit, Repo
1919
from git.exc import UnsafeOptionError
20+
from git.index.typ import BaseIndexEntry
2021
from git.objects.util import tzoffset, utc
2122
from git.repo.fun import touch
2223

@@ -166,6 +167,36 @@ def check_entries(d, has_change_type=False):
166167
self.assertEqual(commit.committer_tz_offset, 14400, commit.committer_tz_offset)
167168
self.assertEqual(commit.message, "initial project\n")
168169

170+
@with_rw_directory
171+
def test_stats_preserve_filename_spaces(self, rw_dir):
172+
repo = Repo.init(rw_dir)
173+
actor = Actor("Test", "test@example.com")
174+
paths = ("name", " name", "name ", " name ", " ")
175+
# Store blobs directly so trailing spaces work on Windows as well.
176+
for multiplier, change_type in ((1, "A"), (2, "M")):
177+
for count, path in enumerate(paths, 1):
178+
data = b"line\n" * count * multiplier
179+
blob = repo.odb.store(IStream("blob", len(data), BytesIO(data)))
180+
repo.index.add([BaseIndexEntry((0o100644, blob.binsha, 0, path))])
181+
commit = repo.index.commit("Update files", author=actor, committer=actor)
182+
stats = commit.stats
183+
assert stats.total == {"insertions": 15, "deletions": 0, "lines": 15, "files": 5}
184+
assert stats.files == {
185+
path: {"insertions": count, "deletions": 0, "lines": count, "change_type": change_type}
186+
for count, path in enumerate(paths, 1)
187+
}
188+
empty_stats = repo.index.commit("No changes", author=actor, committer=actor).stats
189+
assert empty_stats.files == {}
190+
assert empty_stats.total == {"insertions": 0, "deletions": 0, "lines": 0, "files": 0}
191+
192+
repo.index.remove(list(paths))
193+
removed_stats = repo.index.commit("Remove files", author=actor, committer=actor).stats
194+
assert removed_stats.files == {
195+
path: {"insertions": 0, "deletions": 2 * count, "lines": 2 * count, "change_type": "D"}
196+
for count, path in enumerate(paths, 1)
197+
}
198+
assert removed_stats.total == {"insertions": 0, "deletions": 30, "lines": 30, "files": 5}
199+
169200
def test_renames(self):
170201
commit = self.rorepo.commit("185d847ec7647fd2642a82d9205fb3d07ea71715")
171202
files = commit.stats.files

0 commit comments

Comments
 (0)