Skip to content

fix(status): see committed-but-unindexed changes without losing the fast path (#1829) - #1848

Open
inth3shadows wants to merge 2 commits into
colbymchenry:mainfrom
inth3shadows:fix/1829-committed-unindexed
Open

fix(status): see committed-but-unindexed changes without losing the fast path (#1829)#1848
inth3shadows wants to merge 2 commits into
colbymchenry:mainfrom
inth3shadows:fix/1829-committed-unindexed

Conversation

@inth3shadows

Copy link
Copy Markdown

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:

State Before #1843
Clean 50.2 ms 614.2 ms
One uncommitted edit 59.1 ms 676.7 ms
Committed, not indexed 51.1 ms (wrong) 604.4 ms (right)

getChangedFiles is 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. Timing getChangedFiles() 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.

State main @ 3ed73bc #1843 this PR
Clean 119 ms 1470 ms 132 ms
One uncommitted edit 131 ms 1488 ms 136 ms
Committed, not indexed 122 ms (reports 0 — the bug) 1547 ms 146 ms

Median of 10, both passes within ~1% of each other. Ranges across all samples: main 117–184 ms, #1843 1451–2517 ms, this PR 129–150 ms. Both fixes report modified: 1 in the committed state; main reports 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 adds git diff --name-status --no-renames <stamp> HEAD to the candidates git status already 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 status once 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:

Where #1843 is strictly better

One case, and it is real: a file hidden from git with git update-index --assume-unchanged and then edited. #1843's inventory walk sees it; this does not, because neither git status nor git diff reports it. Verified on all three builds:

base     pending {added: 0, modified: 0}
#1843    pending {added: 0, modified: 1}   <- correct
this PR  pending {added: 0, modified: 0}

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).

…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.
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.

1 participant