Fix incremental update leaving stale nodes after a file rename - #686
Fix incremental update leaving stale nodes after a file rename#686thejesh23 wants to merge 1 commit into
Conversation
get_changed_files() used 'git diff --name-only', which reports a rename a.py -> b.py as only the new path b.py. incremental_update() therefore never saw a.py, and its purge loop (which only removes paths present in the changed/dependent set and missing on disk) left a.py's File/Function/ Class nodes and all edges keyed to it in the graph. The result diverged from a full rebuild, which drops the old path. Switch to 'git diff --name-status -z' and parse it so renames/copies emit BOTH the old and new path. The old path is then seen by the existing purge loop and removed, making an incremental update equal a full rebuild. Adds a unit test for the name-status parser and a real-git integration test asserting the renamed-away source is purged and node/edge/file counts match a fresh full_build.
|
I reviewed this alongside #729, which fixes the same issue with a very similar approach — flagging that here in case it's helpful for the maintainer to compare them side by side, since they'd conflict if both merged. Having read both, I think each has one thing the other would benefit from: This PR's tests are the stronger of the two. #729's parser implementation may be slightly more robust. It slices ( A combination — #729's parser with this PR's tests — would probably be the strongest result, if that's useful. I verified the underlying assumption both PRs rest on, and it holds: One scope note that applies to both: this fixes the git-diff path, but Thanks for putting this together. |
|
This bug is now fixed and shipped through #729 at |
Linked issue
Closes #684
What & why
get_changed_files()discovered changes withgit diff --name-only, which reports a renamea.py → b.pyas only the new pathb.py.incremental_update()therefore never sawa.py, and its purge loop only removes paths that are in the changed/dependent set and missing on disk — the old path is in neither. Unlikefull_build()(which purgesexisting_files - current_abs), the incremental path had no "purge files no longer on disk" step, so the renamed-away file's File/Function/Class nodes and every edge keyed to it survived. The incremental graph diverged from a full rebuild.This switches change discovery to
git diff --name-status -zand adds_parse_name_status_z(), which emits both the old and new path for rename/copy (R/C) records. The old path then flows through the existing purge loop and is removed, so an incremental update again equals a full rebuild. The--cachedfallback branch gets the same treatment. The now-unused_decode_nul_paths()helper is removed.How it was tested
New tests (both fail on
main, pass here):test_incremental.py::TestGitOperations::test_get_changed_files_reports_both_rename_paths— unit test of the name-status parser.test_integration_git.py::test_incremental_update_purges_renamed_source— real-git test asserting the renamed-away source is purged and node/edge/file counts match a freshfull_build.Checklist
uv run pytest tests/ --tb=short -q(one pre-existing, unrelated Windows/py3.14 failure)uv run ruff check code_review_graph/uv run mypy code_review_graph/incremental.py --ignore-missing-imports --no-strict-optional