diff --git a/CHANGELOG.md b/CHANGELOG.md index 20cdf37c2..cc129fb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `codegraph upgrade` now upgrades a global npm install when run from your home folder, which holds npm's global folder on Windows and with tools like nvm: before, it ran `npm install` without `-g` there, which left the global copy on the old version and put a second copy in your home folder. A copy installed in a project is now upgraded in that project wherever you run the command, with a warning if npm left it on the old version, and a copy installed with pnpm, yarn or bun gets the command that upgrades it instead of npm. Thanks @timlincool for the report and @maxmilian for the first fix. (#2477) - The Windows installer, `irm …/install.ps1 | iex`, no longer stops with "(403) Forbidden" once GitHub's hourly limit for anonymous requests is used up, as it often is on shared networks and in CI: it now finds the latest release the way the macOS and Linux installer and `codegraph upgrade` do, and asks GitHub's API only when that fails. A version set without its `v`, like `CODEGRAPH_VERSION=1.6.2`, now installs too, where before its download failed. Thanks @hiyochi. - `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336) +- `codegraph sync` no longer says "Already up to date" on an index built by an older CodeGraph: it syncs your changed files as before, then warns that the files it didn't touch still need a full `codegraph index`. Thanks @bompus. - Running `codegraph status` while `codegraph index` rebuilds a project no longer makes the rebuild fail with "database is locked": status now only reads the index, and it says when a rebuild is running in another process, in its output and as `rebuildInProgress` in `status --json`. Thanks @sealad886 for the report, and @danusha2345 and @KissFeng for the fixes. (#2913) - Running `codegraph query`, `explore`, `node`, `callers`, `callees`, `impact`, `context`, `files` or `affected` while `codegraph index` rebuilds a project no longer makes the rebuild fail with "database is locked", and neither does the context hook Claude Code runs on its own when you ask about your code: like `codegraph status`, they now only read the index and say when a rebuild is running, and the hook stays quiet until the rebuild is done. They still answer from an index an older CodeGraph version built. Thanks @sealad886 for the report. (#2913) - Opening a project whose index needs a schema update no longer fails when several CodeGraph processes open it at the same moment, like agent sessions starting right after an upgrade, or the MCP server and a git hook: one of them updates the index while the others wait for it and then carry on. Before, the ones that lost the race applied the same update again, and failed with "UNIQUE constraint failed" or "database is locked". Thanks @inth3shadows for the report and @johnhldiver-beep for the first fix. (#2980) diff --git a/__tests__/cli-sync-stale-index.test.ts b/__tests__/cli-sync-stale-index.test.ts new file mode 100644 index 000000000..9fffdbab1 --- /dev/null +++ b/__tests__/cli-sync-stale-index.test.ts @@ -0,0 +1,107 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { EXTRACTION_VERSION } from '../src/extraction/extraction-version'; + +const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); +const { DatabaseSync } = require('node:sqlite'); +const WARNING = 'Run "codegraph index" for a full rebuild'; +let root: string; + +function run(...args: string[]) { + return spawnSync(process.execPath, [BIN, ...args], { + cwd: root, + encoding: 'utf8', + timeout: 30_000, + env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NODE_NO_WARNINGS: '1', NO_COLOR: '1' }, + }); +} + +function setStamp(stamp: number | null) { + const db = new DatabaseSync(path.join(root, '.codegraph', 'codegraph.db')); + try { + if (stamp === null) { + db.prepare('DELETE FROM project_metadata WHERE key = ?').run('indexed_with_extraction_version'); + } else { + db.prepare('UPDATE project_metadata SET value = ? WHERE key = ?') + .run(String(stamp), 'indexed_with_extraction_version'); + } + } finally { + db.close(); + } +} + +function indexState() { + const cg = CodeGraph.openSync(root); + try { + return { stale: cg.isIndexStale(), files: cg.getStats().fileCount }; + } finally { cg.close(); } +} + +beforeEach(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-stale-')); + fs.writeFileSync(path.join(root, 'original.ts'), 'export function original() { return 1; }\n'); + const cg = CodeGraph.initSync(root); + try { await cg.indexAll(); } finally { cg.close(); } +}); + +afterEach(() => fs.rmSync(root, { recursive: true, force: true })); + +describe('explicit CLI sync with stale extraction', () => { + for (const stamp of [EXTRACTION_VERSION - 1, null]) { + it(`warns for stamp ${stamp} and still syncs`, () => { + setStamp(stamp); + + const unchanged = run('sync'); + expect(unchanged.status).toBe(0); + expect(unchanged.stdout).toContain(WARNING); + expect(unchanged.stdout).toContain('No files changed'); + expect(unchanged.stdout).not.toContain('Already up to date'); + expect(unchanged.stdout).not.toContain('codegraph sync'); + + // The git hooks depend on this half: the changed file goes in. + fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n'); + const changed = run('sync'); + expect(changed.status).toBe(0); + expect(changed.stdout).toContain('Synced 1 changed files'); + expect(changed.stdout).toContain(WARNING); + // A sync can't stand in for the rebuild, so the warning keeps coming. + expect(indexState()).toEqual({ stale: true, files: 2 }); + }, 60_000); + + it(`stays silent and still syncs for stamp ${stamp} under --quiet`, () => { + setStamp(stamp); + fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n'); + const result = run('sync', '--quiet'); + expect(result.status).toBe(0); + expect(result.stdout + result.stderr).toBe(''); + expect(indexState()).toEqual({ stale: true, files: 2 }); + }, 60_000); + } + + it('does not warn about a current index', () => { + fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n'); + const changed = run('sync'); + expect(changed.status).toBe(0); + expect(changed.stdout).not.toContain(WARNING); + expect(indexState()).toEqual({ stale: false, files: 2 }); + + const unchanged = run('sync'); + expect(unchanged.status).toBe(0); + expect(unchanged.stdout).toContain('Already up to date'); + expect(unchanged.stdout).not.toContain(WARNING); + }, 60_000); + + it('stops warning after the full rebuild it names', () => { + setStamp(EXTRACTION_VERSION - 1); + expect(run('sync').stdout).toContain(WARNING); + expect(run('index').status).toBe(0); + const after = run('sync'); + expect(after.status).toBe(0); + expect(after.stdout).toContain('Already up to date'); + expect(after.stdout).not.toContain(WARNING); + }, 60_000); +}); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 7c53fa4b3..9f37d748b 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -1091,9 +1091,14 @@ program }).finally(() => progress.stop()); const totalChanges = result.filesAdded + result.filesModified + result.filesRemoved; + // Sync re-reads changed files only, so a file left alone keeps what an + // older engine extracted from it, and the index isn't up to date however + // little changed. It warns and carries on: the git hooks discard this + // output, so a refusal would quietly stop them after every upgrade. + const staleIndex = cg.isIndexStale(); if (totalChanges === 0 && !result.pendingRefsProcessed) { - clack.log.info('Already up to date'); + clack.log.info(staleIndex ? 'No files changed' : 'Already up to date'); } else if (totalChanges > 0) { clack.log.success(`Synced ${formatNumber(totalChanges)} changed files`); const details: string[] = []; @@ -1109,6 +1114,10 @@ program clack.log.info(`Resolved ${formatNumber(result.pendingRefsResolved ?? 0)} pending references${unresolved}`); } + if (staleIndex) { + clack.log.warn('This index was built by an older CodeGraph engine, and sync only re-reads changed files. Run "codegraph index" for a full rebuild.'); + } + clack.outro('Done'); } finally { cg.destroy();