Skip to content

Fix old_path and new_path being wrong for empty added or deleted files - #325

Merged
ishepard merged 1 commit into
ishepard:masterfrom
MaxFreedomPollard:fix/empty-file-paths
Sep 25, 2026
Merged

ishepard merged 1 commit into
ishepard:masterfrom
MaxFreedomPollard:fix/empty-file-paths

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

Fixes #248.

ModifiedFile.old_path returns the file's path instead of None when a commit adds an empty file, and ModifiedFile.new_path returns the path instead of None when a commit deletes one. The same object reports a change_type of ADD while also claiming a previous path, so any code that keys on old_path or new_path to detect creation or deletion silently mis-classifies the file. Empty __init__.py files make this common in Python repositories.

The cause is upstream. Git emits no hunk for an empty file, so there is no --- /dev/null or +++ /dev/null header for GitPython to read the missing side from, and _pick_best_path hands back the fallback taken from the diff --git a/X b/X line instead. The new file mode and deleted file mode lines are still emitted, which is why change_type stays correct while the paths do not. This still reproduces on GitPython 3.1.62, and gitpython-developers/GitPython#749 reports the same wrong a_path on added files and was closed as not planned.

_from_change_to_modification_type at pydriller/domain/commit.py:187 already reads diff.new_file and diff.deleted_file to decide ADD and DELETE, so having old_path and new_path consult the same two flags only makes them agree with the verdict the class already publishes, and with docs/modifiedfile.rst, which already documents both as None in these cases. The change is one guard at the top of each property in pydriller/domain/commit.py. Renames of empty files set neither flag, so renames are untouched. One downstream effect is worth naming: the seven per-file process metrics under pydriller/metrics/process/ (every one except ChangeSet) bucket by new_path, and ModifiedFile.__hash__ interpolates it too, so a deleted empty file now buckets under the None key and hashes as DELETE None .... That is not a regression, since every non-empty deletion already does both.

The bug is visible inside a bundled fixture, so no new test repository is needed. In test-repos/diff, commit 4be0402d adds the empty pydriller/tests/integration/__init__.py next to a non-empty setup.py, and only the empty one gets a bogus old_path. Commit df51c5ef deletes thirteen non-empty files, which all correctly get a new_path of None, plus that same empty file, which does not. The two new tests in tests/test_commit.py cover both cases and fail on master with assert 'pydriller/tests/integration/__init__.py' is None. Five existing tests needed two lines each: they build a ModifiedFile from a bare @patch('git.diff.Diff') mock where every MagicMock attribute is truthy, so new_file and deleted_file now have to be set false. Those mocks were always under-specified: on master they already make change_type report ADD for what the tests treat as an ordinary modification.

mypy --ignore-missing-imports pydriller/ tests/ and flake8 are clean, and the suite passes locally on Python 3.11 and macOS, 313 passed, with tests/test_memory_consumption.py excluded locally because it clones remote repositories.

GitPython learns that an added file has no old path from the
"--- /dev/null" line of the patch header, and that a deleted file has no
new path from "+++ /dev/null". Git emits no hunk, and therefore no such
header, when the file being added or deleted is empty, so GitPython falls
back to the path on the "diff --git a/X b/X" line and both sides of the
diff end up with the same path. The "new file mode" and "deleted file
mode" header lines are still emitted for empty files, which is why
change_type stayed correct while the paths did not.

old_path and new_path now consult diff.new_file and diff.deleted_file,
the same two flags _from_change_to_modification_type already trusts to
report ADD and DELETE, so the paths agree with the change type the object
publishes. Renames of empty files set neither flag and are untouched.
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.23%. Comparing base (a527c83) to head (96f9882).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #325      +/-   ##
==========================================
- Coverage   97.40%   97.23%   -0.17%     
==========================================
  Files          16       16              
  Lines        1154     1158       +4     
==========================================
+ Hits         1124     1126       +2     
- Misses         30       32       +2     
Files with missing lines Coverage Δ
pydriller/domain/commit.py 96.96% <100.00%> (-0.45%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ishepard

Copy link
Copy Markdown
Owner

LGTM! Since GitPython issue is not getting fixed, it's fine to fix it on our side.

@ishepard
ishepard merged commit ba330ef into ishepard:master Sep 25, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracking empty files

2 participants