* 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)
| 1936 | * `git pull`/`checkout`/`merge`/`rebase` that `git status` cannot see. |
| 1937 | */ |
| 1938 | async sync(onProgress?: (progress: IndexProgress) => void): Promise<SyncResult> { |
| 1939 | await initGrammars(); // Initialize WASM runtime (grammars loaded lazily below) |
| 1940 | const startTime = Date.now(); |
| 1941 | let filesChecked = 0; |
| 1942 | let filesAdded = 0; |
| 1943 | let filesModified = 0; |
| 1944 | let filesRemoved = 0; |
| 1945 | let nodesUpdated = 0; |
| 1946 | const changedFilePaths: string[] = []; |
| 1947 | |
| 1948 | onProgress?.({ |
| 1949 | phase: 'scanning', |
| 1950 | current: 0, |
| 1951 | total: 0, |
| 1952 | }); |
| 1953 | |
| 1954 | const filesToIndex: string[] = []; |
| 1955 | // === Filesystem reconcile (git-independent) === |
| 1956 | // The source of truth for "what changed" is the filesystem vs the indexed |
| 1957 | // state — never git. We enumerate the current source files and reconcile |
| 1958 | // each against the DB. A cheap (size, mtime) stat pre-filter skips unchanged |
| 1959 | // files without reading or hashing them, so the expensive read+hash+parse |
| 1960 | // only runs for files that actually changed. This catches edits/adds/deletes |
| 1961 | // whether or not the project uses git, and crucially also catches committed |
| 1962 | // changes from `git pull`/`checkout`/`merge`/`rebase` — which `git status` |
| 1963 | // cannot see, because the working tree is clean afterward. |
| 1964 | const currentFiles = await scanDirectoryAsync(this.rootDir); |
| 1965 | filesChecked = currentFiles.length; |
| 1966 | const currentSet = new Set(currentFiles); |
| 1967 | |
| 1968 | const trackedFiles = this.queries.getAllFiles(); |
| 1969 | const trackedMap = new Map<string, FileRecord>(); |
| 1970 | for (const f of trackedFiles) { |
| 1971 | trackedMap.set(f.path, f); |
| 1972 | } |
| 1973 | |
| 1974 | // Removals: tracked in the DB but no longer a present source file. Check the |
| 1975 | // filesystem directly — `scanDirectory` (via `git ls-files`) still lists a |
| 1976 | // file deleted from disk but not yet staged, so set membership alone misses it. |
| 1977 | // `reconcileChecks` drives the cooperative yield shared with the adds/mods loop |
| 1978 | // below (see SYNC_RECONCILE_YIELD_INTERVAL / issue #905). |
| 1979 | let reconcileChecks = 0; |
| 1980 | for (const tracked of trackedFiles) { |
| 1981 | if (!currentSet.has(tracked.path) || !fs.existsSync(path.join(this.rootDir, tracked.path))) { |
| 1982 | this.queries.deleteFile(tracked.path); |
| 1983 | filesRemoved++; |
| 1984 | } |
| 1985 | if (++reconcileChecks % SYNC_RECONCILE_YIELD_INTERVAL === 0) { |
| 1986 | await new Promise<void>((resolve) => setImmediate(resolve)); |
| 1987 | } |
| 1988 | } |
| 1989 | |
| 1990 | // Adds / modifications. |
| 1991 | for (const filePath of currentFiles) { |
| 1992 | // Same cooperative yield as the removals loop — this is the other O(files) |
| 1993 | // synchronous-stat loop that wedges the main thread on a large repo (#905). |
| 1994 | // Yield at the top of the body so the `continue` fast-paths below still hit it. |
| 1995 | if (++reconcileChecks % SYNC_RECONCILE_YIELD_INTERVAL === 0) { |
no test coverage detected