diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e3656c..d0723ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,18 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Thanks to [@essopsp](https://github.com/essopsp) for the repro. ### Fixed +- **Sync / status**: git-untracked files are no longer reported as pending + "Added" forever. After `codegraph sync` indexed a newly-created untracked + source file, `codegraph status` kept listing it under Pending Changes and + every subsequent `sync` re-indexed it from scratch — even though its symbols + were already queryable. Change detection trusted `git status` and counted + every untracked (`??`) entry as new without checking the index, but indexing + a file doesn't make git track it, so the file stayed `??` and got re-added on + each run. CodeGraph now hash-compares untracked files against the index the + same way it does tracked files: a file counts as "added" only if it's missing + from the index, "modified" if its contents changed, and is skipped otherwise. + Closes [#206](https://github.com/colbymchenry/codegraph/issues/206). Thanks to + [@15290391025](https://github.com/15290391025) for the report. - **Indexing**: `codegraph init -i` now finds source inside nested, independent git repositories — separate clones living inside the workspace that are **not** git submodules (common in CMake "super-repo" layouts). When the top-level diff --git a/__tests__/sync.test.ts b/__tests__/sync.test.ts index 8365f63..374e778 100644 --- a/__tests__/sync.test.ts +++ b/__tests__/sync.test.ts @@ -225,6 +225,50 @@ describe('Sync Module', () => { expect(nodes.length).toBeGreaterThan(0); }); + it('should stop reporting untracked files once they are indexed (issue #206)', async () => { + // Untracked files stay `??` in git status even after codegraph indexes + // them. Change detection must compare them against the DB by hash, not + // report every untracked file as "added" on every sync/status. + fs.writeFileSync( + path.join(testDir, 'src', 'new.ts'), + `export function newFunc() { return 42; }` + ); + + // First sync indexes the untracked file. + const first = await cg.sync(); + expect(first.filesAdded).toBe(1); + + // The file is still untracked in git, but now lives in the DB. + expect(cg.searchNodes('newFunc').length).toBeGreaterThan(0); + + // status must not keep flagging it as a pending addition... + const changes = cg.getChangedFiles(); + expect(changes.added).not.toContain('src/new.ts'); + expect(changes.modified).not.toContain('src/new.ts'); + + // ...and a second sync must be a no-op for it. + const second = await cg.sync(); + expect(second.filesAdded).toBe(0); + expect(second.filesModified).toBe(0); + }); + + it('should re-index an untracked file when its contents change', async () => { + const filePath = path.join(testDir, 'src', 'new.ts'); + fs.writeFileSync(filePath, `export function newFunc() { return 42; }`); + await cg.sync(); + + // Modify the still-untracked file. + fs.writeFileSync(filePath, `export function renamedFunc() { return 7; }`); + + const changes = cg.getChangedFiles(); + expect(changes.modified).toContain('src/new.ts'); + + const result = await cg.sync(); + expect(result.filesModified).toBe(1); + expect(cg.searchNodes('renamedFunc').length).toBeGreaterThan(0); + expect(cg.searchNodes('newFunc').length).toBe(0); + }); + it('should detect deleted files via git', async () => { fs.unlinkSync(path.join(testDir, 'src', 'index.ts')); diff --git a/src/extraction/index.ts b/src/extraction/index.ts index b5269cb..18086bd 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -1261,8 +1261,12 @@ export class ExtractionOrchestrator { } } - // Handle modified files — read + hash only these files - for (const filePath of gitChanges.modified) { + // Handle modified + added files — read + hash only these. Untracked + // (`??`) files stay untracked in git even after we index them, so they + // can't be trusted as "new": re-hash and compare against the DB exactly + // like modified files. Otherwise every sync re-indexes them and status + // reports them as pending forever. (See issue #206.) + for (const filePath of [...gitChanges.modified, ...gitChanges.added]) { const fullPath = path.join(this.rootDir, filePath); let content: string; try { @@ -1285,13 +1289,6 @@ export class ExtractionOrchestrator { filesModified++; } } - - // Handle added (untracked) files - for (const filePath of gitChanges.added) { - filesToIndex.push(filePath); - changedFilePaths.push(filePath); - filesAdded++; - } } else { // === Fallback: full scan (non-git project or git failure) === const currentFiles = new Set(scanDirectory(this.rootDir, this.config)); @@ -1395,8 +1392,11 @@ export class ExtractionOrchestrator { } } - // Modified files — read + hash only these, compare with DB - for (const filePath of gitChanges.modified) { + // Modified + added files — read + hash, compare with DB. Untracked (`??`) + // files stay untracked in git even after indexing, so they must be + // hash-compared like modified files instead of always counting as added — + // otherwise status reports them as pending forever. (See issue #206.) + for (const filePath of [...gitChanges.modified, ...gitChanges.added]) { const fullPath = path.join(this.rootDir, filePath); let content: string; try { @@ -1416,11 +1416,6 @@ export class ExtractionOrchestrator { } } - // Added (untracked) files - for (const filePath of gitChanges.added) { - added.push(filePath); - } - return { added, modified, removed }; }