diff --git a/CHANGELOG.md b/CHANGELOG.md index 1815c4150..fd51f00cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,6 +201,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). #### Symbols, tests and the viewer +- `codegraph index ` now rebuilds exactly the project you name. A path without an index of its own used to be silently resolved to the nearest initialized parent — a monorepo container, an ancestor with a stale index — and rebuilt under a normal "Done"; it is now an error that names that parent and how to index the path on its own. (#1524) + - **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges. - **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does. diff --git a/__tests__/cli-index-explicit-path.test.ts b/__tests__/cli-index-explicit-path.test.ts new file mode 100644 index 000000000..c880716ac --- /dev/null +++ b/__tests__/cli-index-explicit-path.test.ts @@ -0,0 +1,64 @@ +/** + * `codegraph index ` rebuilds , never an ancestor (#1524). + * + * The command used to resolve an uninitialized upward to the nearest + * initialized parent and rebuild THAT under a normal "Done" — so + * `codegraph index child` from a monorepo re-indexed the whole container and + * never said so. An explicit path that is not initialized is now an error that + * names the ancestor it would have picked. + */ +import { describe, it, expect, beforeAll, afterAll } 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'; + +const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); + +function run(cwd: string, args: string[]) { + const r = spawnSync(process.execPath, [BIN, ...args], { + cwd, + encoding: 'utf-8', + env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NO_COLOR: '1' }, + }); + return { status: r.status, out: (r.stdout ?? '') + (r.stderr ?? '') }; +} + +describe('codegraph index (#1524)', () => { + let root: string; + let parent: string; + let child: string; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-index-path-')); + parent = path.join(root, 'parent'); + child = path.join(parent, 'child'); + fs.mkdirSync(child, { recursive: true }); + fs.writeFileSync(path.join(parent, 'p.py'), 'def parent_only():\n return 1\n'); + fs.writeFileSync(path.join(child, 'c.py'), 'def child_only():\n return 2\n'); + const cg = CodeGraph.initSync(parent); + await cg.indexAll(); + cg.close(); + }); + + afterAll(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + it('refuses an explicit path that has no index of its own, naming the ancestor it would have rebuilt', () => { + const before = fs.statSync(path.join(parent, '.codegraph', 'codegraph.db')).mtimeMs; + const r = run(root, ['index', child, '--quiet']); + expect(r.status).toBe(1); + expect(r.out).toContain(`not initialized in ${child}`); + expect(r.out).toContain(parent); + // The parent's index was not touched. + expect(fs.statSync(path.join(parent, '.codegraph', 'codegraph.db')).mtimeMs).toBe(before); + expect(fs.existsSync(path.join(child, '.codegraph'))).toBe(false); + }); + + it('rebuilds the explicit path when it is initialized, and a bare `index` still resolves upward from a subdirectory', () => { + expect(run(root, ['index', parent, '--quiet']).status).toBe(0); + expect(run(child, ['index', '--quiet']).status).toBe(0); + }); +}); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index a4dccb976..d23267c07 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -791,7 +791,13 @@ program .option('-q, --quiet', 'Suppress progress output') .option('-v, --verbose', 'Show detailed worker lifecycle and memory info') .action(async (pathArg: string | undefined, options: { force?: boolean; quiet?: boolean; verbose?: boolean }) => { - const projectPath = resolveProjectPath(pathArg); + // An EXPLICIT path names the project to rebuild — it is never a hint to go + // looking for one. resolveProjectPath walks up to the nearest initialized + // ancestor, which is right for `codegraph query` run from a subdirectory, + // but for a full re-index it silently rebuilt the parent's graph under a + // normal "Done" when had no index of its own (#1524). Only a bare + // `codegraph index` (cwd) may resolve upward. + const projectPath = pathArg ? path.resolve(pathArg) : resolveProjectPath(); try { // Don't (re)index your home directory / a filesystem root (#845). --force @@ -804,7 +810,12 @@ program if (!isInitialized(projectPath)) { error(`CodeGraph not initialized in ${projectPath}`); - info('Run "codegraph init" first'); + const ancestor = pathArg ? resolveProjectPath(pathArg) : projectPath; + if (ancestor !== projectPath) { + info(`The nearest initialized project is ${ancestor} — pass that path to rebuild it, or run "codegraph init" in ${projectPath} to index it on its own.`); + } else { + info('Run "codegraph init" first'); + } process.exit(1); }