diff --git a/dist/commands/index-cmd.js b/dist/commands/index-cmd.js index 18c035b1..dc7b4327 100644 --- a/dist/commands/index-cmd.js +++ b/dist/commands/index-cmd.js @@ -11,12 +11,38 @@ * repository rather than only in tests. */ import { closeIndex, ensureIndex, indexInfo, openIndex, rebuildIndex, scanTrailers, } from '../core/index-db.js'; +import { NOTES_REF, notesAvailability } from '../core/notes.js'; /** Every failure here is a usage error or a missing dependency, never a finding (SPEC §10). */ const fail = (message) => { process.stderr.write(`commitlore: ${message}\n`); process.exitCode = 2; }; const plural = (count, unit) => `${count} ${unit}${count === 1 ? '' : 's'}`; +/** + * Records come from two places, and this command can only ever read one of them + * locally. `rebuild` is what a user runs *because* they suspect the index is + * wrong, so reporting a clean count over a mirror that was never fetched is the + * least useful moment to leave that unsaid — the same defect `r-fetchowed` fixed + * one command over, where `doctor --fix` printed `ok` for configuration it had + * not fetched through. + * + * The state is `notesAvailability`'s, so this is a report and not a new check. + * The exit code deliberately does not follow `context`, which exits 3 on the + * same state: 3 there marks an *answer* drawn from an incomplete store, and this + * command's contract is 0 built, 2 could not run. The index it wrote is the one + * git can support, which is a build that succeeded — moving the code would make + * every unfetched clone's `init` and CI step fail over a cache that is correct. + * + * Like every other diagnostic it goes to stderr, so `--json` stays parseable + * while the caller still hears it. + */ +const reportUnfetchedNotes = (subject) => { + if (notesAvailability() !== 'unfetched') + return; + process.stderr.write(`commitlore: the notes mirror has not been fetched here, so ${subject} covers the commit ` + + `messages alone and may be missing records that exist upstream (git fetch does not fetch ` + + `${NOTES_REF} by default). fix: commitlore doctor --fix, then git fetch, then rerun\n`); +}; const runScan = (options) => { const started = Date.now(); const trailers = scanTrailers(); @@ -102,9 +128,11 @@ export const register = (program) => { fail('--rebuild and --no-index ask for opposite things'); return; } + reportUnfetchedNotes('this scan'); runScan(options); return; } + reportUnfetchedNotes('this index'); runIndex(options); } catch (error) { diff --git a/dist/commands/index-cmd.js.map b/dist/commands/index-cmd.js.map index 1ab7995f..881c9692 100644 --- a/dist/commands/index-cmd.js.map +++ b/dist/commands/index-cmd.js.map @@ -1 +1 @@ -{"version":3,"file":"index-cmd.js","sourceRoot":"","sources":["../../src/commands/index-cmd.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index-cmd.js","sourceRoot":"","sources":["../../src/commands/index-cmd.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/commitlore.mjs b/dist/commitlore.mjs index 289b4cc7..aad23b6e 100755 --- a/dist/commitlore.mjs +++ b/dist/commitlore.mjs @@ -18640,6 +18640,13 @@ var fail = (message) => { process.exitCode = 2; }; var plural = (count2, unit) => `${count2} ${unit}${count2 === 1 ? "" : "s"}`; +var reportUnfetchedNotes = (subject) => { + if (notesAvailability() !== "unfetched") return; + process.stderr.write( + `commitlore: the notes mirror has not been fetched here, so ${subject} covers the commit messages alone and may be missing records that exist upstream (git fetch does not fetch ${NOTES_REF2} by default). fix: commitlore doctor --fix, then git fetch, then rerun +` + ); +}; var runScan = (options) => { const started = Date.now(); const trailers = scanTrailers(); @@ -18710,9 +18717,11 @@ var register12 = (program3) => { fail("--rebuild and --no-index ask for opposite things"); return; } + reportUnfetchedNotes("this scan"); runScan(options); return; } + reportUnfetchedNotes("this index"); runIndex(options); } catch (error2) { fail(error2 instanceof Error ? error2.message : String(error2)); diff --git a/docs/cli.md b/docs/cli.md index e6eaeeac..c6a7b122 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -90,6 +90,8 @@ them; you do not. ## The index is derived `.git/commitlore/index.db` is a cache. The authority is the commit trailers and -`refs/notes/commitlore`, so `commitlore index --rebuild` can always reconstruct -it, and `--no-index` answers the same questions from Git alone — more slowly. -The gap between the two at scale is measured in [evidence.md](evidence.md). +`refs/notes/commitlore`, so `commitlore index --rebuild` reconstructs it from +whatever Git holds here — and says so on stderr when `refs/notes/commitlore` was +never fetched, because then it rebuilds from one source of the two. `--no-index` +answers the same questions from Git alone — more slowly. The gap between the two +at scale is measured in [evidence.md](evidence.md). diff --git a/src/commands/index-cmd.ts b/src/commands/index-cmd.ts index 3ca8be2f..aae4ac99 100644 --- a/src/commands/index-cmd.ts +++ b/src/commands/index-cmd.ts @@ -22,6 +22,7 @@ import { scanTrailers, type IndexStats, } from '../core/index-db.js'; +import { NOTES_REF, notesAvailability } from '../core/notes.js'; interface IndexCommandOptions { rebuild?: boolean; @@ -40,6 +41,33 @@ const fail = (message: string): void => { const plural = (count: number, unit: string): string => `${count} ${unit}${count === 1 ? '' : 's'}`; +/** + * Records come from two places, and this command can only ever read one of them + * locally. `rebuild` is what a user runs *because* they suspect the index is + * wrong, so reporting a clean count over a mirror that was never fetched is the + * least useful moment to leave that unsaid — the same defect `r-fetchowed` fixed + * one command over, where `doctor --fix` printed `ok` for configuration it had + * not fetched through. + * + * The state is `notesAvailability`'s, so this is a report and not a new check. + * The exit code deliberately does not follow `context`, which exits 3 on the + * same state: 3 there marks an *answer* drawn from an incomplete store, and this + * command's contract is 0 built, 2 could not run. The index it wrote is the one + * git can support, which is a build that succeeded — moving the code would make + * every unfetched clone's `init` and CI step fail over a cache that is correct. + * + * Like every other diagnostic it goes to stderr, so `--json` stays parseable + * while the caller still hears it. + */ +const reportUnfetchedNotes = (subject: string): void => { + if (notesAvailability() !== 'unfetched') return; + process.stderr.write( + `commitlore: the notes mirror has not been fetched here, so ${subject} covers the commit ` + + `messages alone and may be missing records that exist upstream (git fetch does not fetch ` + + `${NOTES_REF} by default). fix: commitlore doctor --fix, then git fetch, then rerun\n`, + ); +}; + const runScan = (options: IndexCommandOptions): void => { const started = Date.now(); const trailers = scanTrailers(); @@ -142,9 +170,11 @@ export const register = (program: Command): void => { fail('--rebuild and --no-index ask for opposite things'); return; } + reportUnfetchedNotes('this scan'); runScan(options); return; } + reportUnfetchedNotes('this index'); runIndex(options); } catch (error) { fail(error instanceof Error ? error.message : String(error)); diff --git a/test/notes-availability.test.ts b/test/notes-availability.test.ts index c0503ea3..21519f46 100644 --- a/test/notes-availability.test.ts +++ b/test/notes-availability.test.ts @@ -13,8 +13,10 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { Command } from 'commander'; import { afterAll, describe, expect, it } from 'vitest'; +import { register as registerIndex } from '../src/commands/index-cmd.js'; import { notesAvailability, coversNotes, @@ -146,3 +148,119 @@ describe('an unfetched mirror does not read as an empty repository', () => { expect(result.records.map((record) => record.recordId)).toEqual(['r-note01']); }); }); + +/** An origin holding one record in a commit message and one only in the mirror. */ +const originWithBothSources = (): string => { + const dir = makeRepo(); + writeFileSync(join(dir, 'b.txt'), 'b\n'); + git(dir, ['add', '-A']); + git(dir, [ + 'commit', + '-q', + '-m', + 'Seed a record in the message\n\nLimit: the vendor SSO ships no refresh token\nProvenance: authored\nRecord-Id: r-cmt001\n', + ]); + writeRecord( + git(dir, ['rev-parse', 'HEAD']).trim(), + [ + { key: 'Limit', value: 'the mirror holds this one' }, + { key: 'Provenance', value: 'authored' }, + { key: 'Record-Id', value: 'r-note01' }, + ], + { cwd: dir }, + ); + return dir; +}; + +interface CliRun { + readonly stdout: string; + readonly stderr: string; + readonly code: number; +} + +/** Runs `commitlore index` as registered, so the assertion is on what a user reads. */ +const runIndexCommand = (dir: string, argv: string[]): CliRun => { + const program = new Command(); + program.exitOverride(); + registerIndex(program); + + const out: string[] = []; + const err: string[] = []; + const stdoutWrite = process.stdout.write.bind(process.stdout); + const stderrWrite = process.stderr.write.bind(process.stderr); + const previousCwd = process.cwd(); + const previousExitCode = process.exitCode; + + process.exitCode = 0; + try { + process.chdir(dir); + process.stdout.write = ((chunk: unknown) => { + out.push(String(chunk)); + return true; + }) as typeof process.stdout.write; + process.stderr.write = ((chunk: unknown) => { + err.push(String(chunk)); + return true; + }) as typeof process.stderr.write; + program.parse(argv, { from: 'user' }); + } finally { + process.stdout.write = stdoutWrite; + process.stderr.write = stderrWrite; + process.chdir(previousCwd); + } + + const code = Number(process.exitCode ?? 0); + process.exitCode = previousExitCode; + return { stdout: out.join(''), stderr: err.join(''), code }; +}; + +/** + * #400: `index --rebuild` read the same two sources `context` reads and + * reported a clean rebuild over one it could not open. `rebuild` is the command + * a user runs *because* they suspect the index is wrong, so an unqualified + * success is the worst moment to leave the unfetched mirror unsaid. + */ +describe('a build over an unfetched mirror says so', () => { + it('qualifies --rebuild, and still exits 0', () => { + const run = runIndexCommand(clone(originWithBothSources()), ['index', '--rebuild']); + + expect(run.stdout).toContain('rebuilt: scanned'); + expect(run.stderr).toContain('has not been fetched'); + expect(run.stderr).toContain('commitlore doctor --fix'); + // An unfetched mirror is not a build failure. `context` exits 3 on the same + // state because 3 marks an incomplete *answer*; this command's contract is 0 + // built, 2 could not run, and it did build what git can support. + expect(run.code).toBe(0); + }); + + it('qualifies the incremental build too — it reads the same two sources', () => { + const run = runIndexCommand(clone(originWithBothSources()), ['index']); + + expect(run.stdout).toContain('scanned'); + expect(run.stderr).toContain('has not been fetched'); + expect(run.code).toBe(0); + }); + + it('qualifies --no-index, which answers from the same two sources', () => { + const run = runIndexCommand(clone(originWithBothSources()), ['index', '--no-index']); + + expect(run.stdout).toContain('no-index scan'); + expect(run.stderr).toContain('has not been fetched'); + expect(run.code).toBe(0); + }); + + it('stays quiet when the mirror is present or there is nothing to fetch', () => { + expect(runIndexCommand(originWithBothSources(), ['index', '--rebuild']).stderr).toBe(''); + expect(runIndexCommand(makeRepo(), ['index', '--rebuild']).stderr).toBe(''); + }); + + it('stops saying it once the mirror is fetched, and indexes the record', () => { + const dir = clone(originWithBothSources()); + git(dir, ['config', '--add', 'remote.origin.fetch', NOTES_REFSPEC]); + git(dir, ['fetch', '-q', 'origin']); + + const run = runIndexCommand(dir, ['index', '--rebuild']); + expect(run.stderr).toBe(''); + expect(runQuery({ cwd: dir }).records.map((record) => record.recordId)).toContain('r-note01'); + }); +});