fix(status): see committed-but-unindexed changes without losing the fast path (#1829) - #1848
Open
inth3shadows wants to merge 2 commits into
Open
fix(status): see committed-but-unindexed changes without losing the fast path (#1829)#1848inth3shadows wants to merge 2 commits into
inth3shadows wants to merge 2 commits into
Conversation
…ast path (colbymchenry#1829) `getChangedFiles` sourced its candidates from `git status`, which only ever describes the working tree — so committing a change is exactly what removed it from the answer. The hash comparison below was correct and simply never reached: an edit read as pending until you committed it, then read as nothing while the index still lacked it. The index now stamps the commit it was built at, and the git fast path adds `git diff --name-status --no-renames <stamp> HEAD` to its candidates. That covers added, modified and deleted in one cheap call — and it is the only cheap way to catch a committed MODIFICATION, since a file the index already tracks is invisible to both `git status` and a DB-side inventory unless its content is re-hashed. - The stamp is written by a full index and by every whole-tree sync (a `--paths` sync absorbs only part of the diff, so it must not stamp), captured before change detection runs so a commit landing mid-sync is never claimed as absorbed. - Committed lines run through the same classifier working-tree lines do, so an excluded dir stays excluded (colbymchenry#766, colbymchenry#999) and a committed `A` the index never saw still lands in `added`. - A path reaching the list from both sources — committed, then edited again — is counted once. - A stamp git cannot resolve (rebase, gc, shallow clone) falls back to the full scan, as does an index built before stamping existed; the next sync stamps it and the fast path returns. The exported `getGitChangedFiles` keeps its contract — a working-tree diff for callers that hold no index — with the trust decision moved to `canTrustGitFastPath`, which only the DB-aware caller asks.
bompus
added a commit
to bompus/codegraph
that referenced
this pull request
Sep 11, 2026
…es (colbymchenry#1829) Replace the full source-inventory comparison from colbymchenry#1843 with upstream PR colbymchenry#1848: the index stamps the commit it was built at and change detection adds the committed diff since that commit to the git status candidates. Falls back to the full scan when the stamp cannot be resolved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An alternative to #1843 for the same bug — same symptom, same repro, but it keeps the git fast path. @bompus got there first and his diagnosis matches mine exactly; this is offered as a choice of shape, not as a competing claim on the bug. Take whichever you prefer, or take his and treat this as a follow-up.
The difference is what replaces the incomplete candidate set. #1843 drops the fast path and compares the full source inventory on every call. This stamps the commit the index was built at and asks git for the committed diff since that commit, so the candidate set becomes complete without the tree walk.
Why it matters
#1843's own table, on a 14,486-file VS Code checkout:
getChangedFilesis on the hot path —codegraph status, every sync, and the watcher's catch-up all land here — so that cost is paid per call, forever, to catch a case that is rare per call.Measured here, same checkout, all three arms on one machine
VS Code at
7b7e49c8— the commit #1843 measured — indexed once (14,137 files, 475,316 nodes) and read by all three builds. TiminggetChangedFiles()in-process, excluding CLI startup, as #1843's table does: 2 warm-ups then 10 samples, two passes per state with the arm order reversed on the second,nice -n 10. WSL2/ext4, Node 22.23.2.main@ 3ed73bcMedian of 10, both passes within ~1% of each other. Ranges across all samples:
main117–184 ms, #1843 1451–2517 ms, this PR 129–150 ms. Both fixes reportmodified: 1in the committed state;mainreports nothing.The absolute numbers are higher than #1843's on every arm — different machine, and this box was busier — but all three arms are measured under the same conditions, so the ratio is the point: the full-inventory walk is ~11× the current cost, the committed diff is ~1.1×.
What it does
The index stamps the commit it was built at (
indexed_at_commit), and the git fast path addsgit diff --name-status --no-renames <stamp> HEADto the candidatesgit statusalready gives it. One extra git call; the rest of the function is untouched.This is also the only cheap way to catch a committed modification. A file the index already tracks is invisible to
git statusonce committed, and a DB-side inventory can't see it either without re-hashing the file — which is the walk we are trying to avoid. The committed diff names it directly.Mechanics worth reviewing:
--pathssync absorbs only part of the diff, so it must not stamp. It is captured before change detection runs, so a commit landing mid-sync is never claimed as absorbed.Athe index never saw still lands inadded.--no-renamesis deliberate: the index keys files by path, so a rename is a removal plus an add.getGitChangedFileskeeps its contract — a working-tree diff for callers holding no index. The trust decision lives incanTrustGitFastPath, which only the DB-aware caller asks. (git-changed-untracked-dir.test.tscatches this if it regresses; it caught it for me.)Where #1843 is strictly better
One case, and it is real: a file hidden from git with
git update-index --assume-unchangedand then edited. #1843's inventory walk sees it; this does not, because neithergit statusnorgit diffreports it. Verified on all three builds:If that case matters more than the per-call cost, #1843 is the right fix and this one isn't.
Tests
Nine cases in
__tests__/sync.test.ts: committed add, committed modify, committed delete, committed rename, zero-after-sync, the double-source count, the exclusion filter, an unresolvable stamp, and a pre-stamp index upgrading. Six of them fail against pre-fix source.Validation
Based on upstream
3ed73bc. Linux/WSL2, Node 22.23.2 for the source build (the source tree's Node gate rejects 26; the published bundle carries its own runtime).npx tsc --noEmitclean.npm test: 13 failures, all of which reproduce on pristineupstream/main@3ed73bcin a throwaway worktree against the same index —mcp-callers-truncation,nextjs,object-literal-methods,react-native-bridge,ui-server-api,ui-steps-api,ui-steps-api-servers,ui-steps-cross-tier. No new failures.