* Sync the index with the current file state. * * Change detection is filesystem-based, never git: a (size, mtime) stat * pre-filter skips unchanged files, then a content-hash compare confirms real * changes. This works in non-git projects and catches committed changes from * `git pul
(
onProgress?: (progress: IndexProgress) => void,
/**
* Watcher fast path: the exact project-relative paths the OS reported as
* changed. When provided, reconciliation runs over ONLY these paths —
* per-path logic identical to the full walk (stat pre-filter, hash
* confirm, the #1240 removal/resurrection flow) — skipping the O(repo)
* scan and tracked-load. Callers must pass undefined whenever the change
* set is not exactly known (directory removals, event overflow): the full
* scan-diff remains the ground truth those cases need (#1285).
*/
scopedPaths?: string[]
)
| 2488 | * `git pull`/`checkout`/`merge`/`rebase` that `git status` cannot see. |
| 2489 | */ |
| 2490 | async sync( |
| 2491 | onProgress?: (progress: IndexProgress) => void, |
| 2492 | /** |
| 2493 | * Watcher fast path: the exact project-relative paths the OS reported as |
| 2494 | * changed. When provided, reconciliation runs over ONLY these paths — |
| 2495 | * per-path logic identical to the full walk (stat pre-filter, hash |
| 2496 | * confirm, the #1240 removal/resurrection flow) — skipping the O(repo) |
| 2497 | * scan and tracked-load. Callers must pass undefined whenever the change |
| 2498 | * set is not exactly known (directory removals, event overflow): the full |
| 2499 | * scan-diff remains the ground truth those cases need (#1285). |
| 2500 | */ |
| 2501 | scopedPaths?: string[] |
| 2502 | ): Promise<SyncResult> { |
| 2503 | await initGrammars(); // Initialize WASM runtime (grammars loaded lazily below) |
| 2504 | const startTime = Date.now(); |
| 2505 | let filesChecked = 0; |
| 2506 | let filesAdded = 0; |
| 2507 | let filesModified = 0; |
| 2508 | let filesRemoved = 0; |
| 2509 | let nodesUpdated = 0; |
| 2510 | const changedFilePaths: string[] = []; |
| 2511 | |
| 2512 | onProgress?.({ |
| 2513 | phase: 'scanning', |
| 2514 | current: 0, |
| 2515 | total: 0, |
| 2516 | }); |
| 2517 | |
| 2518 | const filesToIndex: string[] = []; |
| 2519 | // === Filesystem reconcile (git-independent) === |
| 2520 | // The source of truth for "what changed" is the filesystem vs the indexed |
| 2521 | // state — never git. We enumerate the current source files and reconcile |
| 2522 | // each against the DB. A cheap (size, mtime) stat pre-filter skips unchanged |
| 2523 | // files without reading or hashing them, so the expensive read+hash+parse |
| 2524 | // only runs for files that actually changed. This catches edits/adds/deletes |
| 2525 | // whether or not the project uses git, and crucially also catches committed |
| 2526 | // changes from `git pull`/`checkout`/`merge`/`rebase` — which `git status` |
| 2527 | // cannot see, because the working tree is clean afterward. |
| 2528 | const tSyncScan = Date.now(); |
| 2529 | let currentFiles: string[]; |
| 2530 | let trackedFiles: FileRecord[]; |
| 2531 | if (scopedPaths && scopedPaths.length > 0) { |
| 2532 | // Scoped reconcile: stat only the reported paths. filesChecked counts |
| 2533 | // the PATHS examined (not the files found) — it must stay non-zero even |
| 2534 | // when every scoped path was a deletion, because CodeGraph.watch() |
| 2535 | // reads `filesChecked === 0 && durationMs === 0` as the |
| 2536 | // lock-unavailable signature (#449). |
| 2537 | const unique = [...new Set(scopedPaths)]; |
| 2538 | currentFiles = unique.filter((p) => fs.existsSync(path.join(this.rootDir, p))); |
| 2539 | trackedFiles = []; |
| 2540 | for (const p of unique) { |
| 2541 | const rec = this.queries.getFileByPath(p); |
| 2542 | if (rec) trackedFiles.push(rec); |
| 2543 | } |
| 2544 | filesChecked = unique.length; |
| 2545 | if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[phase-timing] sync-scoped: ${Date.now() - tSyncScan}ms (${unique.length} paths, ${trackedFiles.length} tracked)`); |
| 2546 | } else { |
| 2547 | currentFiles = await scanDirectoryAsync(this.rootDir); |
no test coverage detected