fix(incremental): purge old paths on rename — parse git diff --name-status - #729
Conversation
…tatus (tirth8205#684) git diff --name-only reports a rename a.py -> b.py as only the new path, so incremental_update() never saw the old path and its nodes/edges survived forever, diverging from a full rebuild. Switch change discovery to --name-status -z and emit BOTH sides of R*/C* records; the old path then flows through the existing purge loop (is_file() check) and is removed. Applied to the --cached fallback too. _decode_nul_paths had no remaining callers and is removed. Tests: unit coverage for the record parser (M/A/D, R100, C75, dedupe, empty), a mocked rename in get_changed_files, and an end-to-end git-mv repro asserting old-path purge + new-path presence. Full suite: 1966 passed. ruff clean.
|
I reviewed this alongside #686, which fixes the same issue with a very similar approach — noting it here so the maintainer can compare them easily, since the two would conflict if both landed. My read is that each PR has one thing the other would benefit from: This PR's parser implementation looks the more robust of the two. Using a slice ( #686's tests are the stronger of the two. It adds Combining the two — this parser with #686's tests — would probably give the best result, if that's helpful. I checked the assumption both PRs depend on and it holds: One scope note that applies equally to both: this fixes the git-diff path, while Thanks for working on this. |
code-review-graph reviewOverall risk: 0.40 (MEDIUM) — 14 changed function(s)/class(es), 0 affected flow(s), 2 test gap(s) Risk-scored changes
Test gaps
Token savings: this graph-backed report used ~24,581 fewer tokens (~90%) than reading every changed file in full (estimated, chars/4 approximation). Powered by code-review-graph — local-first analysis; no code leaves the CI runner. |
Fixes #684.
Problem
git diff --name-onlyreports a renamea.py → b.pyas only the new path, soincremental_update()never seesa.py. The purge loop only removes paths present in the changed/dependent set, so the old path's File/Function/Class nodes and every edge keyed to it survive — an incremental update permanently diverges from a full rebuild (stale nodes, dangling importer edges, inflated counts).Fix
git diff --name-status -z(both in the primary call and the--cachedfallback)._decode_name_status_paths()parses the NUL-delimited records and emits both sides ofR*/C*entries (order-preserving, deduped). The old path then flows through the existing purge loop'sis_file()check and is removed — no changes needed to the purge logic itself._decode_nul_pathshad no remaining callers and is removed.Tests
M/A/D,R100(two paths),C75, dedupe, empty input.get_changed_fileswith a mocked rename returns both paths.git mv a.py b.py+ commit,get_changed_files(base="HEAD~1")→{a.py, b.py}, thenincremental_update— asserts the old path is fully purged and the new one is present (full-rebuild parity).get_changed_filesmocks to the--name-statusoutput format.Full suite: 1966 passed, 5 skipped.
ruff checkclean.AI-assisted; every change reviewed and understood.