diff --git a/dist/commands/doctor/checks/transport-notes-refspec.js b/dist/commands/doctor/checks/transport-notes-refspec.js
index fedaf5f..89a44e7 100644
--- a/dist/commands/doctor/checks/transport-notes-refspec.js
+++ b/dist/commands/doctor/checks/transport-notes-refspec.js
@@ -4,7 +4,7 @@
* It owns the reversible fetch-configuration diagnosis and fix because no
* sibling check may alter transport configuration on its behalf.
*/
-import { NOTES_REF, NOTES_REFSPEC, coversNotes, forcesNotes, listRemotes, fetchRefspecs } from '../../../core/notes.js';
+import { NOTES_REF, NOTES_REFSPEC, coversNotes, forcesNotes, listRemotes, fetchRefspecs, notesAbsenceEvidenceKey, } from '../../../core/notes.js';
import { check, evidenceKey, gitOptions } from '../model.js';
const EXACT_NOTES_REFSPEC = `+${NOTES_REF}:${NOTES_REF}`;
const EXACT_NOTES_REFSPEC_PATTERN = `^\\${EXACT_NOTES_REFSPEC}$`;
@@ -17,6 +17,23 @@ const EXACT_NOTES_REFSPEC_PATTERN = `^\\${EXACT_NOTES_REFSPEC}$`;
* in place beside a new one.
*/
const escapeConfigValuePattern = (value) => value.replace(/[\\.*+?[\]^$(){}|]/g, (character) => `\\${character}`);
+const firstLine = (output) => output.trim().split('\n')[0] ?? '';
+/** A stale absence observation must never survive an unsuccessful verification. */
+const clearAbsenceEvidence = (remote, ctx) => ctx.git(['config', '--local', '--unset-all', notesAbsenceEvidenceKey(remote)], gitOptions(ctx.opts)).code === 0;
+/**
+ * Store precisely what made an absent-mirror answer safe: this remote name was
+ * checked while it resolved to this URL, and it advertised no notes ref.
+ */
+const recordAbsenceEvidence = (remote, ctx) => {
+ const url = ctx.git(['config', '--get', `remote.${remote}.url`], gitOptions(ctx.opts));
+ if (url.code !== 0 || url.stdout.trim() === '')
+ return false;
+ const key = notesAbsenceEvidenceKey(remote);
+ const current = ctx.git(['config', '--local', '--get', key], gitOptions(ctx.opts));
+ if (current.code === 0 && current.stdout.trim() === url.stdout.trim())
+ return false;
+ return ctx.git(['config', '--local', '--replace-all', key, url.stdout.trim()], gitOptions(ctx.opts)).code === 0;
+};
export const checkRefspec = (ctx) => {
const { opts, git } = ctx;
const title = 'notes fetch refspec';
@@ -66,6 +83,10 @@ export const checkRefspec = (ctx) => {
.map((remote) => ({ remote, result: git(['fetch', '--dry-run', remote], gitOptions(opts)) }))
.filter(({ result }) => result.code !== 0);
if (failed.length > 0) {
+ // A previous observation says nothing about a remote that cannot be
+ // verified now. `--fix` removes it so the read path returns to fail-closed.
+ if (opts.fix === true)
+ failed.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
return check('notes-refspec', 'transport', title, 'warn', `could not verify (${failed
.map(({ remote, result }) => `${remote}: ${result.stderr.trim().split('\n')[0] ?? 'git fetch failed'}`)
.join('; ')})`, failed.map(({ remote }) => `git fetch ${remote}`).join('\n'), fixed, undefined, {
@@ -78,13 +99,53 @@ export const checkRefspec = (ctx) => {
},
});
}
- // A refspec written by `--fix` has not been fetched through yet, and this
- // check is the last thing the operator reads before believing the mirror is
- // sorted. Without the second sentence `ok` plus `fixed by --fix` reads as
- // "repaired", while every query still answers from a mirror that was never
- // retrieved -- the configuration is right and the records are still missing.
- return check('notes-refspec', 'transport', title, 'ok', fixed
- ? `${NOTES_REF} is now covered for ${remotes.join(', ')} — nothing has been fetched through it yet`
- : `git fetch succeeds for ${remotes.join(', ')} and covers ${NOTES_REF}`, fixed ? `git fetch ${remotes[0] ?? 'origin'}` : null, fixed, undefined, { evidence: remoteEvidence });
+ const local = git(['rev-parse', '--verify', '--quiet', NOTES_REF], gitOptions(opts));
+ if (local.code === 0) {
+ return check('notes-refspec', 'transport', title, 'ok', `git fetch succeeds for ${remotes.join(', ')} and covers ${NOTES_REF}`, null, fixed, undefined, { evidence: { ...remoteEvidence, local_sha: local.stdout.trim() || 'unknown' } });
+ }
+ const advertised = remotes.map((remote) => ({
+ remote,
+ result: git(['ls-remote', remote, NOTES_REF], gitOptions(opts)),
+ }));
+ const unavailable = advertised.filter(({ result }) => result.code !== 0);
+ if (unavailable.length > 0) {
+ if (opts.fix === true)
+ unavailable.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
+ return check('notes-refspec', 'transport', title, 'warn', `could not verify whether ${NOTES_REF} exists upstream (${unavailable
+ .map(({ remote, result }) => `${remote}: ${firstLine(result.stderr) || 'git ls-remote failed'}`)
+ .join('; ')})`, unavailable.map(({ remote }) => `git fetch ${remote}`).join('\n'), fixed, undefined, {
+ evidence: {
+ ...remoteEvidence,
+ ...Object.fromEntries(unavailable.map(({ remote, result }) => [
+ `ls_remote_exit_code_${evidenceKey(remote)}`,
+ String(result.code),
+ ])),
+ },
+ });
+ }
+ const withNotes = advertised.filter(({ result }) => result.stdout.trim() !== '');
+ if (withNotes.length > 0) {
+ if (opts.fix === true)
+ withNotes.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
+ return check('notes-refspec', 'transport', title, 'warn', `${withNotes.map(({ remote }) => remote).join(', ')} advertises ${NOTES_REF}, but it is not fetched here`, withNotes.map(({ remote }) => `git fetch ${remote}`).join('\n'), fixed, undefined, {
+ evidence: {
+ ...remoteEvidence,
+ ...Object.fromEntries(withNotes.map(({ remote, result }) => [
+ `remote_sha_${evidenceKey(remote)}`,
+ result.stdout.trim().split(/\s+/)[0] ?? 'unknown',
+ ])),
+ },
+ });
+ }
+ let recorded = false;
+ if (opts.fix === true) {
+ recorded = remotes.map((remote) => recordAbsenceEvidence(remote, ctx)).some(Boolean);
+ fixed = fixed || recorded;
+ }
+ // The remote probe found no mirror. Only `--fix` stores that fact for query
+ // routes, which must remain read-only and must not perform this probe.
+ return check('notes-refspec', 'transport', title, 'ok', opts.fix === true
+ ? `${remotes.join(', ')} advertises no ${NOTES_REF}; there is nothing to fetch`
+ : `${remotes.join(', ')} advertises no ${NOTES_REF}; run commitlore doctor --fix to record that for queries`, opts.fix === true ? null : 'commitlore doctor --fix', fixed, undefined, { evidence: { ...remoteEvidence, remote_advertises: 'false' } });
};
//# sourceMappingURL=transport-notes-refspec.js.map
\ No newline at end of file
diff --git a/dist/commands/doctor/checks/transport-notes-refspec.js.map b/dist/commands/doctor/checks/transport-notes-refspec.js.map
index 4553dda..789542d 100644
--- a/dist/commands/doctor/checks/transport-notes-refspec.js.map
+++ b/dist/commands/doctor/checks/transport-notes-refspec.js.map
@@ -1 +1 @@
-{"version":3,"file":"transport-notes-refspec.js","sourceRoot":"","sources":["../../../../src/commands/doctor/checks/transport-notes-refspec.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"transport-notes-refspec.js","sourceRoot":"","sources":["../../../../src/commands/doctor/checks/transport-notes-refspec.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/dist/commitlore.mjs b/dist/commitlore.mjs
index 94942ba..3de5da0 100755
--- a/dist/commitlore.mjs
+++ b/dist/commitlore.mjs
@@ -11385,6 +11385,7 @@ var listRecordShas = (opts = {}) => {
return object3;
}).filter((object3) => object3.length > 0);
};
+var notesAbsenceEvidenceKey = (remote) => `commitlore.notesabsence.r${Buffer.from(remote, "utf8").toString("hex")}`;
var listRemotes = (opts) => {
const result = execGit(["remote"], gitOptions(opts));
if (result.code !== 0) return [];
@@ -11395,6 +11396,12 @@ var fetchRefspecs = (remote, opts) => {
if (result.code !== 0) return [];
return result.stdout.split("\n").filter((line2) => line2.length > 0);
};
+var hasNotesAbsenceEvidence = (remote, opts = {}) => {
+ const url = execGit(["config", "--get", `remote.${remote}.url`], gitOptions(opts));
+ if (url.code !== 0 || url.stdout.trim() === "") return false;
+ const observed = execGit(["config", "--local", "--get", notesAbsenceEvidenceKey(remote)], gitOptions(opts));
+ return observed.code === 0 && observed.stdout.trim() === url.stdout.trim();
+};
var coversNotes = (refspec) => {
const [, destination = ""] = refspec.replace(/^\+/, "").split(":");
if (destination === NOTES_REF) return true;
@@ -11407,7 +11414,8 @@ var notesAvailability = (opts = {}) => {
const remotes = listRemotes(opts);
if (remotes.length === 0) return "absent";
const uncovered = remotes.filter((remote) => !fetchRefspecs(remote, opts).some(coversNotes));
- return uncovered.length > 0 ? "unfetched" : "absent";
+ if (uncovered.length > 0) return "unfetched";
+ return remotes.every((remote) => hasNotesAbsenceEvidence(remote, opts)) ? "absent" : "unfetched";
};
// src/core/backfill.ts
@@ -17382,10 +17390,10 @@ import { spawnSync as spawnSync3 } from "node:child_process";
var PROBE_MESSAGE = "commitlore doctor probe\n\nLimit: probe\nBlast: local\n";
var gitOptions2 = (opts) => opts.cwd === void 0 ? {} : { cwd: opts.cwd };
var boundedExcerpt = (output) => {
- const [firstLine5 = ""] = (output ?? "").split(/\r?\n/, 1);
+ const [firstLine6 = ""] = (output ?? "").split(/\r?\n/, 1);
return {
- firstLine: firstLine5.slice(0, 200),
- truncated: firstLine5.length > 200 ? "true" : "false"
+ firstLine: firstLine6.slice(0, 200),
+ truncated: firstLine6.length > 200 ? "true" : "false"
};
};
var streamEvidence = (stream, output) => {
@@ -29212,6 +29220,16 @@ var checkPush = (ctx) => {
var EXACT_NOTES_REFSPEC = `+${NOTES_REF}:${NOTES_REF}`;
var EXACT_NOTES_REFSPEC_PATTERN = `^\\${EXACT_NOTES_REFSPEC}$`;
var escapeConfigValuePattern = (value) => value.replace(/[\\.*+?[\]^$(){}|]/g, (character) => `\\${character}`);
+var firstLine2 = (output) => output.trim().split("\n")[0] ?? "";
+var clearAbsenceEvidence = (remote, ctx) => ctx.git(["config", "--local", "--unset-all", notesAbsenceEvidenceKey(remote)], gitOptions2(ctx.opts)).code === 0;
+var recordAbsenceEvidence = (remote, ctx) => {
+ const url = ctx.git(["config", "--get", `remote.${remote}.url`], gitOptions2(ctx.opts));
+ if (url.code !== 0 || url.stdout.trim() === "") return false;
+ const key = notesAbsenceEvidenceKey(remote);
+ const current = ctx.git(["config", "--local", "--get", key], gitOptions2(ctx.opts));
+ if (current.code === 0 && current.stdout.trim() === url.stdout.trim()) return false;
+ return ctx.git(["config", "--local", "--replace-all", key, url.stdout.trim()], gitOptions2(ctx.opts)).code === 0;
+};
var checkRefspec = (ctx) => {
const { opts, git: git2 } = ctx;
const title = "notes fetch refspec";
@@ -29287,6 +29305,7 @@ var checkRefspec = (ctx) => {
}
const failed = remotes.map((remote) => ({ remote, result: git2(["fetch", "--dry-run", remote], gitOptions2(opts)) })).filter(({ result }) => result.code !== 0);
if (failed.length > 0) {
+ if (opts.fix === true) failed.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
return check(
"notes-refspec",
"transport",
@@ -29309,16 +29328,87 @@ var checkRefspec = (ctx) => {
}
);
}
+ const local = git2(["rev-parse", "--verify", "--quiet", NOTES_REF], gitOptions2(opts));
+ if (local.code === 0) {
+ return check(
+ "notes-refspec",
+ "transport",
+ title,
+ "ok",
+ `git fetch succeeds for ${remotes.join(", ")} and covers ${NOTES_REF}`,
+ null,
+ fixed,
+ void 0,
+ { evidence: { ...remoteEvidence, local_sha: local.stdout.trim() || "unknown" } }
+ );
+ }
+ const advertised = remotes.map((remote) => ({
+ remote,
+ result: git2(["ls-remote", remote, NOTES_REF], gitOptions2(opts))
+ }));
+ const unavailable = advertised.filter(({ result }) => result.code !== 0);
+ if (unavailable.length > 0) {
+ if (opts.fix === true) unavailable.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
+ return check(
+ "notes-refspec",
+ "transport",
+ title,
+ "warn",
+ `could not verify whether ${NOTES_REF} exists upstream (${unavailable.map(({ remote, result }) => `${remote}: ${firstLine2(result.stderr) || "git ls-remote failed"}`).join("; ")})`,
+ unavailable.map(({ remote }) => `git fetch ${remote}`).join("\n"),
+ fixed,
+ void 0,
+ {
+ evidence: {
+ ...remoteEvidence,
+ ...Object.fromEntries(
+ unavailable.map(({ remote, result }) => [
+ `ls_remote_exit_code_${evidenceKey(remote)}`,
+ String(result.code)
+ ])
+ )
+ }
+ }
+ );
+ }
+ const withNotes = advertised.filter(({ result }) => result.stdout.trim() !== "");
+ if (withNotes.length > 0) {
+ if (opts.fix === true) withNotes.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
+ return check(
+ "notes-refspec",
+ "transport",
+ title,
+ "warn",
+ `${withNotes.map(({ remote }) => remote).join(", ")} advertises ${NOTES_REF}, but it is not fetched here`,
+ withNotes.map(({ remote }) => `git fetch ${remote}`).join("\n"),
+ fixed,
+ void 0,
+ {
+ evidence: {
+ ...remoteEvidence,
+ ...Object.fromEntries(withNotes.map(({ remote, result }) => [
+ `remote_sha_${evidenceKey(remote)}`,
+ result.stdout.trim().split(/\s+/)[0] ?? "unknown"
+ ]))
+ }
+ }
+ );
+ }
+ let recorded = false;
+ if (opts.fix === true) {
+ recorded = remotes.map((remote) => recordAbsenceEvidence(remote, ctx)).some(Boolean);
+ fixed = fixed || recorded;
+ }
return check(
"notes-refspec",
"transport",
title,
"ok",
- fixed ? `${NOTES_REF} is now covered for ${remotes.join(", ")} \u2014 nothing has been fetched through it yet` : `git fetch succeeds for ${remotes.join(", ")} and covers ${NOTES_REF}`,
- fixed ? `git fetch ${remotes[0] ?? "origin"}` : null,
+ opts.fix === true ? `${remotes.join(", ")} advertises no ${NOTES_REF}; there is nothing to fetch` : `${remotes.join(", ")} advertises no ${NOTES_REF}; run commitlore doctor --fix to record that for queries`,
+ opts.fix === true ? null : "commitlore doctor --fix",
fixed,
void 0,
- { evidence: remoteEvidence }
+ { evidence: { ...remoteEvidence, remote_advertises: "false" } }
);
};
@@ -30096,7 +30186,7 @@ var register10 = (program3) => {
// src/commands/hooks.ts
var messageOf3 = (error2) => error2 instanceof Error ? error2.message : String(error2);
-var firstLine2 = (text) => (text.trim().split("\n")[0] ?? "").trim();
+var firstLine3 = (text) => (text.trim().split("\n")[0] ?? "").trim();
var failure3 = (message) => ({
code: 2,
stdout: "",
@@ -30113,7 +30203,7 @@ var success2 = (status, lines) => ({
var resolveHooksDir = (cwd) => {
const result = execGit(["rev-parse", "--git-path", "hooks"], { cwd });
if (result.code !== 0) {
- throw new Error(`not a git repository (${firstLine2(result.stderr)})`);
+ throw new Error(`not a git repository (${firstLine3(result.stderr)})`);
}
return resolve14(cwd, result.stdout.trim());
};
@@ -31849,7 +31939,7 @@ var PREFIX4 = "commitlore:";
var USAGE = "usage: commitlore squash-preserve ..
[--target ] [--message-file ] [--json] [--force]";
var SHORT_SHA = 8;
var messageOf5 = (error2) => error2 instanceof Error ? error2.message : String(error2);
-var firstLine3 = (text) => (text.trim().split("\n")[0] ?? "").trim();
+var firstLine4 = (text) => (text.trim().split("\n")[0] ?? "").trim();
var shortSha6 = (sha) => sha.length > SHORT_SHA ? sha.slice(0, SHORT_SHA) : sha;
var usageError = (message) => ({
code: 2,
@@ -31865,7 +31955,7 @@ var countCommits = (range, cwd) => {
cwd === void 0 ? {} : { cwd }
);
if (result.code !== 0) {
- throw new Error(`cannot walk range ${JSON.stringify(range)}: ${firstLine3(result.stderr)}`);
+ throw new Error(`cannot walk range ${JSON.stringify(range)}: ${firstLine4(result.stderr)}`);
}
return Number(result.stdout.trim());
};
@@ -32048,13 +32138,13 @@ ${USAGE2}
checks: []
});
var messageOf6 = (error2) => error2 instanceof Error ? error2.message : String(error2);
-var firstLine4 = (text) => (text.trim().split("\n")[0] ?? "").trim();
+var firstLine5 = (text) => (text.trim().split("\n")[0] ?? "").trim();
var stripCr = (line2) => line2.endsWith("\r") ? line2.slice(0, -1) : line2;
var CONTINUATION = /^[ \t]/;
var LEADING_WHITESPACE = /^[ \t]+/;
var isComment = (line2) => line2.startsWith("#");
var MERGE_TITLE = /^Merge (pull request #\d+ from \S+|branch '[^']+'|remote-tracking branch '[^']+'|tag '[^']+')(?: into \S+)?$/;
-var looksLikeMergeTitle = (message) => MERGE_TITLE.test(firstLine4(message));
+var looksLikeMergeTitle = (message) => MERGE_TITLE.test(firstLine5(message));
var matchTrailersAt = (lines, start, trailers) => {
const found = [];
let cursor = start;
@@ -32208,21 +32298,21 @@ var locateReferenceViolations = (source, trailers, violations) => {
var resolveCommit2 = (ref, cwd) => {
const result = execGit(["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], { cwd });
if (result.code !== 0) {
- throw new Error(`cannot resolve commit ${JSON.stringify(ref)}: ${firstLine4(result.stderr)}`);
+ throw new Error(`cannot resolve commit ${JSON.stringify(ref)}: ${firstLine5(result.stderr)}`);
}
return result.stdout.trim();
};
var readCommitSource = (sha, cwd) => {
const result = execGit(["log", "-1", "--format=%B", sha, "--"], { cwd });
if (result.code !== 0) {
- throw new Error(`cannot read commit ${sha}: ${firstLine4(result.stderr)}`);
+ throw new Error(`cannot read commit ${sha}: ${firstLine5(result.stderr)}`);
}
return { sha, message: result.stdout };
};
var readRange = (range, cwd) => {
const result = execGit(["rev-list", "--reverse", "--end-of-options", range, "--"], { cwd });
if (result.code !== 0) {
- throw new Error(`cannot walk range ${JSON.stringify(range)}: ${firstLine4(result.stderr)}`);
+ throw new Error(`cannot walk range ${JSON.stringify(range)}: ${firstLine5(result.stderr)}`);
}
return result.stdout.split("\n").filter((sha) => sha.length > 0).map((sha) => readCommitSource(sha, cwd));
};
@@ -32287,7 +32377,7 @@ var recordsFor = (source, cwd) => {
var reachableShas = (revision, cwd) => {
const result = execGit(["rev-list", revision], { cwd });
if (result.code !== 0) {
- throw new Error(firstLine4(result.stderr) || `cannot walk revision ${revision}`);
+ throw new Error(firstLine5(result.stderr) || `cannot walk revision ${revision}`);
}
return new Set(result.stdout.trim().split("\n").filter(Boolean));
};
@@ -32383,7 +32473,7 @@ var checkReferences = (input, sources, cwd) => {
check: {
class: "reference",
status: "not-checked",
- reason: `repository scan failed: ${firstLine4(messageOf6(error2))}`
+ reason: `repository scan failed: ${firstLine5(messageOf6(error2))}`
},
violations: []
};
diff --git a/dist/core/notes.d.ts b/dist/core/notes.d.ts
index d36071a..f8c95b1 100644
--- a/dist/core/notes.d.ts
+++ b/dist/core/notes.d.ts
@@ -95,10 +95,11 @@ export declare const listRecordShas: (opts?: NotesOptions) => string[];
* What a consumer route can conclude from an empty answer.
*
* - `present` — the mirror ref exists here; an empty answer means empty
- * - `absent` — no mirror ref, and every remote already fetches one, so
- * nobody has written records: an empty answer means empty
- * - `unfetched` — no mirror ref, and a remote exists that does not fetch it.
- * Records may exist upstream. An empty answer means *unknown*.
+ * - `absent` — no mirror ref, and local doctor evidence says every
+ * configured remote advertised none: an empty answer means empty
+ * - `unfetched` — no mirror ref, and the remote state is not established
+ * locally. Records may exist upstream. An empty answer means
+ * *unknown*.
*
* The third case is the one that matters and the reason this exists. `git fetch`
* does not fetch notes by default, so a plain `git clone` of a repository full
@@ -109,8 +110,21 @@ export declare const listRecordShas: (opts?: NotesOptions) => string[];
* is what an agent runs.
*/
export type NotesAvailability = 'present' | 'absent' | 'unfetched';
+/**
+ * A `doctor --fix` observation is scoped to one remote name and value-bound to
+ * its configured URL. Remote names may contain punctuation that is not valid
+ * in a git-config variable, so encode their UTF-8 bytes instead of interpolating
+ * the name into the key.
+ */
+export declare const notesAbsenceEvidenceKey: (remote: string) => string;
export declare const listRemotes: (opts: NotesOptions) => string[];
export declare const fetchRefspecs: (remote: string, opts: NotesOptions) => string[];
+/**
+ * Whether `doctor --fix` last established, for this exact configured remote,
+ * that it advertised no notes mirror. This is deliberately a local-config
+ * lookup: consumer routes must not put a network round trip before an edit.
+ */
+export declare const hasNotesAbsenceEvidence: (remote: string, opts?: NotesOptions) => boolean;
/**
* Whether a configured refspec lands the mirror where we read it.
*
@@ -129,17 +143,10 @@ export declare const forcesNotes: (refspec: string) => boolean;
/**
* Whether this repository can answer for the notes mirror, and if not, why.
*
- * Reads git config only — no network, no fetch. A repository with no remote at
- * all reports `absent`: there is nowhere for unseen records to be, so an empty
- * answer is a true empty.
- *
- * A configured refspec that has never been fetched through is indistinguishable
- * here from one that was fetched and found nothing, and the difference matters:
- * `doctor --fix` writes the refspec and fetches nothing, so the state it leaves
- * looks exactly like an upstream with no records. Reporting `unfetched` for both
- * was tried and rejected — it fires on every repository whose refspec was added
- * after cloning, and `incomplete` changes `guard`'s exit code. The honest fix
- * lives in `doctor`, which now says a fetch is still owed instead of letting
- * `ok` read as repaired.
+ * Reads local refs and config only — no network, no fetch. Config describes
+ * what this clone intends to fetch; it does not prove what a remote advertised.
+ * `doctor --fix` records a URL-bound absence observation after its remote probe.
+ * Without that observation (including no configured remote), absence is not
+ * evidence that there is nothing upstream, so the answer stays incomplete.
*/
export declare const notesAvailability: (opts?: NotesOptions) => NotesAvailability;
diff --git a/dist/core/notes.js b/dist/core/notes.js
index fa73bfa..7bca922 100644
--- a/dist/core/notes.js
+++ b/dist/core/notes.js
@@ -155,6 +155,13 @@ export const listRecordShas = (opts = {}) => {
})
.filter((object) => object.length > 0);
};
+/**
+ * A `doctor --fix` observation is scoped to one remote name and value-bound to
+ * its configured URL. Remote names may contain punctuation that is not valid
+ * in a git-config variable, so encode their UTF-8 bytes instead of interpolating
+ * the name into the key.
+ */
+export const notesAbsenceEvidenceKey = (remote) => `commitlore.notesabsence.r${Buffer.from(remote, 'utf8').toString('hex')}`;
export const listRemotes = (opts) => {
const result = execGit(['remote'], gitOptions(opts));
if (result.code !== 0)
@@ -168,6 +175,18 @@ export const fetchRefspecs = (remote, opts) => {
return [];
return result.stdout.split('\n').filter((line) => line.length > 0);
};
+/**
+ * Whether `doctor --fix` last established, for this exact configured remote,
+ * that it advertised no notes mirror. This is deliberately a local-config
+ * lookup: consumer routes must not put a network round trip before an edit.
+ */
+export const hasNotesAbsenceEvidence = (remote, opts = {}) => {
+ const url = execGit(['config', '--get', `remote.${remote}.url`], gitOptions(opts));
+ if (url.code !== 0 || url.stdout.trim() === '')
+ return false;
+ const observed = execGit(['config', '--local', '--get', notesAbsenceEvidenceKey(remote)], gitOptions(opts));
+ return observed.code === 0 && observed.stdout.trim() === url.stdout.trim();
+};
/**
* Whether a configured refspec lands the mirror where we read it.
*
@@ -191,27 +210,28 @@ export const forcesNotes = (refspec) => refspec.startsWith('+') && coversNotes(r
/**
* Whether this repository can answer for the notes mirror, and if not, why.
*
- * Reads git config only — no network, no fetch. A repository with no remote at
- * all reports `absent`: there is nowhere for unseen records to be, so an empty
- * answer is a true empty.
- *
- * A configured refspec that has never been fetched through is indistinguishable
- * here from one that was fetched and found nothing, and the difference matters:
- * `doctor --fix` writes the refspec and fetches nothing, so the state it leaves
- * looks exactly like an upstream with no records. Reporting `unfetched` for both
- * was tried and rejected — it fires on every repository whose refspec was added
- * after cloning, and `incomplete` changes `guard`'s exit code. The honest fix
- * lives in `doctor`, which now says a fetch is still owed instead of letting
- * `ok` read as repaired.
+ * Reads local refs and config only — no network, no fetch. Config describes
+ * what this clone intends to fetch; it does not prove what a remote advertised.
+ * `doctor --fix` records a URL-bound absence observation after its remote probe.
+ * Without that observation (including no configured remote), absence is not
+ * evidence that there is nothing upstream, so the answer stays incomplete.
*/
export const notesAvailability = (opts = {}) => {
const ref = execGit(['rev-parse', '--verify', '--quiet', NOTES_REF], gitOptions(opts));
if (ref.code === 0)
return 'present';
+ // No remote is not "unverified"; it is verified. There is nowhere for an
+ // unseen record to be, so an empty answer here is a true empty. Reporting
+ // `unfetched` would warn on every query about an upstream that does not
+ // exist, and nothing could ever clear it — there is no remote to probe, so
+ // `doctor --fix` cannot record evidence about one. That is the incoherence
+ // #512 is about, arriving from the other side.
const remotes = listRemotes(opts);
if (remotes.length === 0)
return 'absent';
const uncovered = remotes.filter((remote) => !fetchRefspecs(remote, opts).some(coversNotes));
- return uncovered.length > 0 ? 'unfetched' : 'absent';
+ if (uncovered.length > 0)
+ return 'unfetched';
+ return remotes.every((remote) => hasNotesAbsenceEvidence(remote, opts)) ? 'absent' : 'unfetched';
};
//# sourceMappingURL=notes.js.map
\ No newline at end of file
diff --git a/dist/core/notes.js.map b/dist/core/notes.js.map
index c305365..b88a739 100644
--- a/dist/core/notes.js.map
+++ b/dist/core/notes.js.map
@@ -1 +1 @@
-{"version":3,"file":"notes.js","sourceRoot":"","sources":["../../src/core/notes.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"notes.js","sourceRoot":"","sources":["../../src/core/notes.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/src/commands/doctor/checks/transport-notes-refspec.ts b/src/commands/doctor/checks/transport-notes-refspec.ts
index 2d3c683..0235426 100644
--- a/src/commands/doctor/checks/transport-notes-refspec.ts
+++ b/src/commands/doctor/checks/transport-notes-refspec.ts
@@ -5,7 +5,15 @@
* sibling check may alter transport configuration on its behalf.
*/
-import { NOTES_REF, NOTES_REFSPEC, coversNotes, forcesNotes, listRemotes, fetchRefspecs } from '../../../core/notes.js';
+import {
+ NOTES_REF,
+ NOTES_REFSPEC,
+ coversNotes,
+ forcesNotes,
+ listRemotes,
+ fetchRefspecs,
+ notesAbsenceEvidenceKey,
+} from '../../../core/notes.js';
import { check, evidenceKey, gitOptions, type DoctorCheck, type DoctorContext } from '../model.js';
const EXACT_NOTES_REFSPEC = `+${NOTES_REF}:${NOTES_REF}`;
@@ -22,6 +30,27 @@ const EXACT_NOTES_REFSPEC_PATTERN = `^\\${EXACT_NOTES_REFSPEC}$`;
const escapeConfigValuePattern = (value: string): string =>
value.replace(/[\\.*+?[\]^$(){}|]/g, (character) => `\\${character}`);
+const firstLine = (output: string): string => output.trim().split('\n')[0] ?? '';
+
+/** A stale absence observation must never survive an unsuccessful verification. */
+const clearAbsenceEvidence = (remote: string, ctx: DoctorContext): boolean =>
+ ctx.git(['config', '--local', '--unset-all', notesAbsenceEvidenceKey(remote)], gitOptions(ctx.opts)).code === 0;
+
+/**
+ * Store precisely what made an absent-mirror answer safe: this remote name was
+ * checked while it resolved to this URL, and it advertised no notes ref.
+ */
+const recordAbsenceEvidence = (remote: string, ctx: DoctorContext): boolean => {
+ const url = ctx.git(['config', '--get', `remote.${remote}.url`], gitOptions(ctx.opts));
+ if (url.code !== 0 || url.stdout.trim() === '') return false;
+
+ const key = notesAbsenceEvidenceKey(remote);
+ const current = ctx.git(['config', '--local', '--get', key], gitOptions(ctx.opts));
+ if (current.code === 0 && current.stdout.trim() === url.stdout.trim()) return false;
+
+ return ctx.git(['config', '--local', '--replace-all', key, url.stdout.trim()], gitOptions(ctx.opts)).code === 0;
+};
+
export const checkRefspec = (ctx: DoctorContext): DoctorCheck => {
const { opts, git } = ctx;
const title = 'notes fetch refspec';
@@ -108,6 +137,9 @@ export const checkRefspec = (ctx: DoctorContext): DoctorCheck => {
.map((remote) => ({ remote, result: git(['fetch', '--dry-run', remote], gitOptions(opts)) }))
.filter(({ result }) => result.code !== 0);
if (failed.length > 0) {
+ // A previous observation says nothing about a remote that cannot be
+ // verified now. `--fix` removes it so the read path returns to fail-closed.
+ if (opts.fix === true) failed.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
return check(
'notes-refspec', 'transport',
title,
@@ -132,21 +164,92 @@ export const checkRefspec = (ctx: DoctorContext): DoctorCheck => {
);
}
- // A refspec written by `--fix` has not been fetched through yet, and this
- // check is the last thing the operator reads before believing the mirror is
- // sorted. Without the second sentence `ok` plus `fixed by --fix` reads as
- // "repaired", while every query still answers from a mirror that was never
- // retrieved -- the configuration is right and the records are still missing.
+ const local = git(['rev-parse', '--verify', '--quiet', NOTES_REF], gitOptions(opts));
+ if (local.code === 0) {
+ return check(
+ 'notes-refspec', 'transport',
+ title,
+ 'ok',
+ `git fetch succeeds for ${remotes.join(', ')} and covers ${NOTES_REF}`,
+ null,
+ fixed,
+ undefined,
+ { evidence: { ...remoteEvidence, local_sha: local.stdout.trim() || 'unknown' } },
+ );
+ }
+
+ const advertised = remotes.map((remote) => ({
+ remote,
+ result: git(['ls-remote', remote, NOTES_REF], gitOptions(opts)),
+ }));
+ const unavailable = advertised.filter(({ result }) => result.code !== 0);
+ if (unavailable.length > 0) {
+ if (opts.fix === true) unavailable.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
+ return check(
+ 'notes-refspec', 'transport',
+ title,
+ 'warn',
+ `could not verify whether ${NOTES_REF} exists upstream (${unavailable
+ .map(({ remote, result }) => `${remote}: ${firstLine(result.stderr) || 'git ls-remote failed'}`)
+ .join('; ')})`,
+ unavailable.map(({ remote }) => `git fetch ${remote}`).join('\n'),
+ fixed,
+ undefined,
+ {
+ evidence: {
+ ...remoteEvidence,
+ ...Object.fromEntries(
+ unavailable.map(({ remote, result }) => [
+ `ls_remote_exit_code_${evidenceKey(remote)}`,
+ String(result.code),
+ ]),
+ ),
+ },
+ },
+ );
+ }
+
+ const withNotes = advertised.filter(({ result }) => result.stdout.trim() !== '');
+ if (withNotes.length > 0) {
+ if (opts.fix === true) withNotes.forEach(({ remote }) => clearAbsenceEvidence(remote, ctx));
+ return check(
+ 'notes-refspec', 'transport',
+ title,
+ 'warn',
+ `${withNotes.map(({ remote }) => remote).join(', ')} advertises ${NOTES_REF}, but it is not fetched here`,
+ withNotes.map(({ remote }) => `git fetch ${remote}`).join('\n'),
+ fixed,
+ undefined,
+ {
+ evidence: {
+ ...remoteEvidence,
+ ...Object.fromEntries(withNotes.map(({ remote, result }) => [
+ `remote_sha_${evidenceKey(remote)}`,
+ result.stdout.trim().split(/\s+/)[0] ?? 'unknown',
+ ])),
+ },
+ },
+ );
+ }
+
+ let recorded = false;
+ if (opts.fix === true) {
+ recorded = remotes.map((remote) => recordAbsenceEvidence(remote, ctx)).some(Boolean);
+ fixed = fixed || recorded;
+ }
+
+ // The remote probe found no mirror. Only `--fix` stores that fact for query
+ // routes, which must remain read-only and must not perform this probe.
return check(
'notes-refspec', 'transport',
title,
'ok',
- fixed
- ? `${NOTES_REF} is now covered for ${remotes.join(', ')} — nothing has been fetched through it yet`
- : `git fetch succeeds for ${remotes.join(', ')} and covers ${NOTES_REF}`,
- fixed ? `git fetch ${remotes[0] ?? 'origin'}` : null,
+ opts.fix === true
+ ? `${remotes.join(', ')} advertises no ${NOTES_REF}; there is nothing to fetch`
+ : `${remotes.join(', ')} advertises no ${NOTES_REF}; run commitlore doctor --fix to record that for queries`,
+ opts.fix === true ? null : 'commitlore doctor --fix',
fixed,
undefined,
- { evidence: remoteEvidence },
+ { evidence: { ...remoteEvidence, remote_advertises: 'false' } },
);
};
diff --git a/src/core/notes.ts b/src/core/notes.ts
index 169a873..fdd2c07 100644
--- a/src/core/notes.ts
+++ b/src/core/notes.ts
@@ -218,10 +218,11 @@ export const listRecordShas = (opts: NotesOptions = {}): string[] => {
* What a consumer route can conclude from an empty answer.
*
* - `present` — the mirror ref exists here; an empty answer means empty
- * - `absent` — no mirror ref, and every remote already fetches one, so
- * nobody has written records: an empty answer means empty
- * - `unfetched` — no mirror ref, and a remote exists that does not fetch it.
- * Records may exist upstream. An empty answer means *unknown*.
+ * - `absent` — no mirror ref, and local doctor evidence says every
+ * configured remote advertised none: an empty answer means empty
+ * - `unfetched` — no mirror ref, and the remote state is not established
+ * locally. Records may exist upstream. An empty answer means
+ * *unknown*.
*
* The third case is the one that matters and the reason this exists. `git fetch`
* does not fetch notes by default, so a plain `git clone` of a repository full
@@ -233,6 +234,15 @@ export const listRecordShas = (opts: NotesOptions = {}): string[] => {
*/
export type NotesAvailability = 'present' | 'absent' | 'unfetched';
+/**
+ * A `doctor --fix` observation is scoped to one remote name and value-bound to
+ * its configured URL. Remote names may contain punctuation that is not valid
+ * in a git-config variable, so encode their UTF-8 bytes instead of interpolating
+ * the name into the key.
+ */
+export const notesAbsenceEvidenceKey = (remote: string): string =>
+ `commitlore.notesabsence.r${Buffer.from(remote, 'utf8').toString('hex')}`;
+
export const listRemotes = (opts: NotesOptions): string[] => {
const result = execGit(['remote'], gitOptions(opts));
if (result.code !== 0) return [];
@@ -246,6 +256,19 @@ export const fetchRefspecs = (remote: string, opts: NotesOptions): string[] => {
return result.stdout.split('\n').filter((line) => line.length > 0);
};
+/**
+ * Whether `doctor --fix` last established, for this exact configured remote,
+ * that it advertised no notes mirror. This is deliberately a local-config
+ * lookup: consumer routes must not put a network round trip before an edit.
+ */
+export const hasNotesAbsenceEvidence = (remote: string, opts: NotesOptions = {}): boolean => {
+ const url = execGit(['config', '--get', `remote.${remote}.url`], gitOptions(opts));
+ if (url.code !== 0 || url.stdout.trim() === '') return false;
+
+ const observed = execGit(['config', '--local', '--get', notesAbsenceEvidenceKey(remote)], gitOptions(opts));
+ return observed.code === 0 && observed.stdout.trim() === url.stdout.trim();
+};
+
/**
* Whether a configured refspec lands the mirror where we read it.
*
@@ -271,26 +294,27 @@ export const forcesNotes = (refspec: string): boolean =>
/**
* Whether this repository can answer for the notes mirror, and if not, why.
*
- * Reads git config only — no network, no fetch. A repository with no remote at
- * all reports `absent`: there is nowhere for unseen records to be, so an empty
- * answer is a true empty.
- *
- * A configured refspec that has never been fetched through is indistinguishable
- * here from one that was fetched and found nothing, and the difference matters:
- * `doctor --fix` writes the refspec and fetches nothing, so the state it leaves
- * looks exactly like an upstream with no records. Reporting `unfetched` for both
- * was tried and rejected — it fires on every repository whose refspec was added
- * after cloning, and `incomplete` changes `guard`'s exit code. The honest fix
- * lives in `doctor`, which now says a fetch is still owed instead of letting
- * `ok` read as repaired.
+ * Reads local refs and config only — no network, no fetch. Config describes
+ * what this clone intends to fetch; it does not prove what a remote advertised.
+ * `doctor --fix` records a URL-bound absence observation after its remote probe.
+ * Without that observation (including no configured remote), absence is not
+ * evidence that there is nothing upstream, so the answer stays incomplete.
*/
export const notesAvailability = (opts: NotesOptions = {}): NotesAvailability => {
const ref = execGit(['rev-parse', '--verify', '--quiet', NOTES_REF], gitOptions(opts));
if (ref.code === 0) return 'present';
+ // No remote is not "unverified"; it is verified. There is nowhere for an
+ // unseen record to be, so an empty answer here is a true empty. Reporting
+ // `unfetched` would warn on every query about an upstream that does not
+ // exist, and nothing could ever clear it — there is no remote to probe, so
+ // `doctor --fix` cannot record evidence about one. That is the incoherence
+ // #512 is about, arriving from the other side.
const remotes = listRemotes(opts);
if (remotes.length === 0) return 'absent';
const uncovered = remotes.filter((remote) => !fetchRefspecs(remote, opts).some(coversNotes));
- return uncovered.length > 0 ? 'unfetched' : 'absent';
+ if (uncovered.length > 0) return 'unfetched';
+
+ return remotes.every((remote) => hasNotesAbsenceEvidence(remote, opts)) ? 'absent' : 'unfetched';
};
diff --git a/test/doctor-invariants.test.ts b/test/doctor-invariants.test.ts
index 2739397..51a6f7a 100644
--- a/test/doctor-invariants.test.ts
+++ b/test/doctor-invariants.test.ts
@@ -266,9 +266,10 @@ describe('#461 doctor invariants', () => {
}
});
- it('writes only remote fetch refspecs under --fix', () => {
- // #63: --fix has already shipped one defect through this surface. A second
- // write surface growing here is what this pins.
+ it('writes only remote fetch refspecs and notes-absence evidence under --fix', () => {
+ // #63: --fix has already shipped one defect through this surface. The
+ // URL-bound absence evidence is the one additional local fact queries may
+ // rely on; a third write surface must still fail this fence.
const repo = populatedRepo('fix');
const configBefore = localConfig(repo);
const before = inventory(repo);
@@ -279,8 +280,8 @@ describe('#461 doctor invariants', () => {
.split('\n')
.filter((line) => line.trim() !== '' && !configBefore.includes(line));
for (const line of added) {
- expect(line, `--fix wrote a config key outside remote..fetch: ${line}`).toMatch(
- /^remote\.[^.]+\.fetch=/,
+ expect(line, `--fix wrote an undocumented config key: ${line}`).toMatch(
+ /^(remote\.[^.]+\.fetch|commitlore\.notesabsence\.r[0-9a-f]+)=/,
);
}
diff --git a/test/guard.test.ts b/test/guard.test.ts
index 007712d..f5f0098 100644
--- a/test/guard.test.ts
+++ b/test/guard.test.ts
@@ -42,6 +42,7 @@ import {
} from '../src/commands/guard.js';
import { execGitOrThrow } from '../src/core/git.js';
import { DEFAULT_THRESHOLD, guard } from '../src/core/guard.js';
+import { NOTES_REFSPEC, notesAbsenceEvidenceKey } from '../src/core/notes.js';
import { runQuery } from '../src/core/query.js';
import { RECORD_ID_RE } from '../src/core/types.js';
import { createTestRepo } from './git-fixtures.js';
@@ -168,6 +169,9 @@ const makeRepo = (seed: readonly RecordFixture[]): string => {
const dir = mkdtempSync(join(tmpdir(), 'commitlore-guard-'));
temporaries.push(dir);
createTestRepo({ path: dir });
+ execGitOrThrow(['remote', 'add', 'origin', '.'], { cwd: dir });
+ execGitOrThrow(['config', '--add', 'remote.origin.fetch', NOTES_REFSPEC], { cwd: dir });
+ execGitOrThrow(['config', '--local', notesAbsenceEvidenceKey('origin'), '.'], { cwd: dir });
for (const record of seed) commitFixture(dir, record);
return dir;
};
diff --git a/test/mcp.test.ts b/test/mcp.test.ts
index 82c1a62..9a5d08f 100644
--- a/test/mcp.test.ts
+++ b/test/mcp.test.ts
@@ -36,6 +36,7 @@ import { fileURLToPath } from 'node:url';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { execGitOrThrow } from '../src/core/git.js';
+import { NOTES_REFSPEC, notesAbsenceEvidenceKey } from '../src/core/notes.js';
import { createTestRepo } from './git-fixtures.js';
const PACKAGE_ROOT = fileURLToPath(new URL('../', import.meta.url));
@@ -136,7 +137,11 @@ const temporaries: string[] = [];
const makeRepo = (): string => {
const dir = mkdtempSync(join(tmpdir(), 'commitlore-mcp-'));
temporaries.push(dir);
- return createTestRepo({ path: dir });
+ createTestRepo({ path: dir });
+ execGitOrThrow(['remote', 'add', 'origin', '.'], { cwd: dir });
+ execGitOrThrow(['config', '--add', 'remote.origin.fetch', NOTES_REFSPEC], { cwd: dir });
+ execGitOrThrow(['config', '--local', notesAbsenceEvidenceKey('origin'), '.'], { cwd: dir });
+ return dir;
};
const commitAt = (
diff --git a/test/notes-availability.test.ts b/test/notes-availability.test.ts
index 21519f4..1c0f763 100644
--- a/test/notes-availability.test.ts
+++ b/test/notes-availability.test.ts
@@ -17,6 +17,7 @@ import { Command } from 'commander';
import { afterAll, describe, expect, it } from 'vitest';
import { register as registerIndex } from '../src/commands/index-cmd.js';
+import { runDoctor } from '../src/commands/doctor.js';
import {
notesAvailability,
coversNotes,
@@ -72,8 +73,10 @@ describe('notesAvailability', () => {
expect(notesAvailability({ cwd: originWithRecords() })).toBe('present');
});
- it('reports absent for a repository with no mirror and no remote', () => {
- // Nowhere for unseen records to be, so an empty answer is a true empty.
+ it('treats a repository with no remote as a true empty', () => {
+ // Fail-closed answers the question "could there be records I cannot see".
+ // With no remote there is nowhere for one to be, and no probe that could
+ // ever settle it, so warning would be permanent and about nothing.
expect(notesAvailability({ cwd: makeRepo() })).toBe('absent');
});
@@ -81,14 +84,36 @@ describe('notesAvailability', () => {
expect(notesAvailability({ cwd: clone(originWithRecords()) })).toBe('unfetched');
});
- it('reports absent once the refspec covers the mirror, even before fetching', () => {
- // The distinction is "could this repository have missed records", not
- // "does it have them": a configured clone that fetched nothing found nothing.
+ it('keeps a configured but unverified mirror incomplete', () => {
const dir = clone(makeRepo());
git(dir, ['config', '--add', 'remote.origin.fetch', NOTES_REFSPEC]);
+ expect(notesAvailability({ cwd: dir })).toBe('unfetched');
+ });
+
+ it('reports absent after doctor verifies that every remote has no notes mirror', () => {
+ const dir = clone(makeRepo());
+
+ const report = runDoctor({ cwd: dir, fix: true });
+ const check = report.checks.find((entry) => entry.id === 'notes-refspec');
+
+ expect(check).toMatchObject({ status: 'ok', detail: expect.stringContaining('there is nothing to fetch') });
expect(notesAvailability({ cwd: dir })).toBe('absent');
});
+ it('keeps warning after doctor fixes the refspec when records do exist upstream', () => {
+ const dir = clone(originWithRecords());
+
+ const report = runDoctor({ cwd: dir, fix: true });
+ const check = report.checks.find((entry) => entry.id === 'notes-refspec');
+
+ expect(check).toMatchObject({ status: 'warn', detail: expect.stringContaining('advertises') });
+ expect(notesAvailability({ cwd: dir })).toBe('unfetched');
+ expect(runQuery({ cwd: dir, noIndex: true }).diagnostics.join(' ')).toContain('may be missing records that exist upstream');
+
+ git(dir, ['fetch', '-q', 'origin']);
+ expect(notesAvailability({ cwd: dir })).toBe('present');
+ });
+
it('reports present after the refspec is added and the notes are fetched', () => {
const dir = clone(originWithRecords());
git(dir, ['config', '--add', 'remote.origin.fetch', NOTES_REFSPEC]);
@@ -116,7 +141,9 @@ describe('coversNotes', () => {
describe('an unfetched mirror does not read as an empty repository', () => {
it('is the same records and the same empty count, and a different state', () => {
const unfetched = runQuery({ cwd: clone(originWithRecords()), noIndex: true });
- const trulyEmpty = runQuery({ cwd: makeRepo(), noIndex: true });
+ const empty = clone(makeRepo());
+ runDoctor({ cwd: empty, fix: true });
+ const trulyEmpty = runQuery({ cwd: empty, noIndex: true });
expect(unfetched.records).toEqual([]);
expect(trulyEmpty.records).toEqual([]);
@@ -133,10 +160,19 @@ describe('an unfetched mirror does not read as an empty repository', () => {
});
it('stays quiet when there is nothing to warn about', () => {
- expect(runQuery({ cwd: makeRepo(), noIndex: true }).diagnostics).toEqual([]);
+ const empty = clone(makeRepo());
+ runDoctor({ cwd: empty, fix: true });
+ expect(runQuery({ cwd: empty, noIndex: true }).diagnostics).toEqual([]);
expect(runQuery({ cwd: originWithRecords(), noIndex: true }).diagnostics).toEqual([]);
});
+ it('does not warn when no remote is configured, because there is no upstream', () => {
+ const result = runQuery({ cwd: makeRepo(), noIndex: true });
+
+ expect(result.notes).toBe('absent');
+ expect(result.diagnostics.join(' ')).not.toContain('may be missing records that exist upstream');
+ });
+
it('finds the records once they are fetched, and stops warning', () => {
const dir = clone(originWithRecords());
git(dir, ['config', '--add', 'remote.origin.fetch', NOTES_REFSPEC]);
@@ -249,9 +285,22 @@ describe('a build over an unfetched mirror says so', () => {
expect(run.code).toBe(0);
});
- it('stays quiet when the mirror is present or there is nothing to fetch', () => {
+ it('stays quiet when the mirror is present or doctor verified there is nothing to fetch', () => {
+ const empty = clone(makeRepo());
+ runDoctor({ cwd: empty, fix: true });
+
expect(runIndexCommand(originWithBothSources(), ['index', '--rebuild']).stderr).toBe('');
- expect(runIndexCommand(makeRepo(), ['index', '--rebuild']).stderr).toBe('');
+ expect(runIndexCommand(empty, ['index', '--rebuild']).stderr).toBe('');
+ });
+
+ it('keeps index diagnostics when a configured remote cannot be verified', () => {
+ const origin = makeRepo();
+ const dir = clone(origin);
+ git(dir, ['config', '--add', 'remote.origin.fetch', NOTES_REFSPEC]);
+ rmSync(origin, { recursive: true, force: true });
+
+ const run = runIndexCommand(dir, ['index', '--rebuild']);
+ expect(run.stderr).toContain('may be missing records that exist upstream');
});
it('stops saying it once the mirror is fetched, and indexes the record', () => {
diff --git a/test/notes.test.ts b/test/notes.test.ts
index 0340096..eac450e 100644
--- a/test/notes.test.ts
+++ b/test/notes.test.ts
@@ -195,7 +195,8 @@ describe('notes mirror', () => {
const report = runDoctor({ cwd: cloneB, fix: true });
const refspec = report.checks.find((entry) => entry.id === 'notes-refspec');
- expect(refspec?.status).toBe('ok');
+ expect(refspec?.status).toBe('warn');
+ expect(refspec?.detail).toContain('advertises');
expect(refspec?.fixed).toBe(true);
git(cloneB, ['fetch', '--quiet', 'origin']);
diff --git a/test/query.test.ts b/test/query.test.ts
index a986c36..abe92f5 100644
--- a/test/query.test.ts
+++ b/test/query.test.ts
@@ -27,7 +27,7 @@ import { buildReport, collectRecords } from '../src/commands/stale.js';
import { execGitOrThrow } from '../src/core/git.js';
import { buildInjection } from '../src/core/inject.js';
import { closeIndex, ensureIndex, indexDbPath, scanTrailers } from '../src/core/index-db.js';
-import { writeRecord } from '../src/core/notes.js';
+import { NOTES_REFSPEC, notesAbsenceEvidenceKey, writeRecord } from '../src/core/notes.js';
import { runQuery, valuesOf, type GradedRecord, type QueryOptions } from '../src/core/query.js';
import { loadFixtures } from './fixtures.js';
import { createTestRepo } from './git-fixtures.js';
@@ -57,7 +57,14 @@ afterAll(() => {
const makeRepo = (): string => {
const dir = mkdtempSync(join(tmpdir(), 'commitlore-query-'));
temporaries.push(dir);
- return createTestRepo({ path: dir });
+ createTestRepo({ path: dir });
+ // Most fixtures here test query semantics rather than transport setup. Give
+ // them the same local absence evidence that `doctor --fix` leaves after
+ // checking an empty remote, so their answers are complete by construction.
+ execGitOrThrow(['remote', 'add', 'origin', '.'], { cwd: dir });
+ execGitOrThrow(['config', '--add', 'remote.origin.fetch', NOTES_REFSPEC], { cwd: dir });
+ execGitOrThrow(['config', '--local', notesAbsenceEvidenceKey('origin'), '.'], { cwd: dir });
+ return dir;
};
const cloneRepo = (origin: string): string => {
@@ -1228,10 +1235,24 @@ describe('the four commands', () => {
expect(runCommand(cloneRepo(dir), [command, AT, PINNED]).code).toBe(3);
});
- it.each(commands)('%s exits 0 in a readable repository with no records', (command) => {
+ it.each(commands)('%s exits 0 in a readable repository known to have no notes', (command) => {
expect(runCommand(makeRepo(), [command, AT, PINNED]).code).toBe(0);
});
+ // A repository with no remote has nowhere for an unseen record to be, so an
+ // empty answer is a true empty. Warning here would point at an upstream that
+ // does not exist, and nothing could clear it: there is no remote to probe, so
+ // `doctor --fix` can never record evidence about one.
+ it.each(commands)('%s exits 0 and says nothing about upstream when there is no remote', (command) => {
+ const noRemote = mkdtempSync(join(tmpdir(), 'commitlore-query-no-remote-'));
+ temporaries.push(noRemote);
+ createTestRepo({ path: noRemote });
+
+ const run = runCommand(noRemote, [command, AT, PINNED]);
+ expect(run.code).toBe(0);
+ expect(run.stderr).not.toContain('may be missing records that exist upstream');
+ });
+
it.each(commands)('%s exits 2 when git cannot answer at all', (command) => {
const run = withPath(brokenGitPath(), () =>
runCommand(dir, [command, AT, PINNED, '--no-index']),
@@ -1511,7 +1532,7 @@ describe('--json', () => {
// Commit history was read, so this is ready rather than empty.
history: 'ready',
counts: { records: 0, limits: 0, ruledOut: 0, warnings: 0, other: 0 },
- // No remote, so an empty answer here is a true empty and says so.
+ // The fixture carries the local doctor evidence for an empty remote.
notes: 'absent',
diagnostics: [],
records: [],
diff --git a/test/shallow-history.test.ts b/test/shallow-history.test.ts
index 55f6da2..27fb311 100644
--- a/test/shallow-history.test.ts
+++ b/test/shallow-history.test.ts
@@ -9,6 +9,7 @@ import { runDoctor } from '../src/commands/doctor.js';
import { hookResult } from '../src/commands/inject.js';
import { guard } from '../src/core/guard.js';
import { buildInjection } from '../src/core/inject.js';
+import { notesAbsenceEvidenceKey } from '../src/core/notes.js';
import { runQuery } from '../src/core/query.js';
import { createTestRepo } from './git-fixtures.js';
@@ -42,6 +43,17 @@ const shallowClone = (): string => {
);
createTestRepo({ path: clone, source: `file://${origin}`, depth: 1 });
git(clone, ['config', '--add', 'remote.origin.fetch', '+refs/notes/commitlore:refs/notes/commitlore']);
+ // A refspec says what this clone would fetch, never what the remote has, so
+ // it alone leaves notes availability unknown and every answer incomplete
+ // (#512). `doctor --fix` records the probe; the fixture records the same
+ // evidence directly, because this suite is about the shallow caveat and an
+ // unrelated incompleteness would mask it.
+ git(clone, [
+ 'config',
+ '--local',
+ notesAbsenceEvidenceKey('origin'),
+ git(clone, ['config', '--get', 'remote.origin.url']).trim(),
+ ]);
return clone;
};
diff --git a/test/validate.test.ts b/test/validate.test.ts
index 29f2bd4..a1915a6 100644
--- a/test/validate.test.ts
+++ b/test/validate.test.ts
@@ -15,7 +15,22 @@ import { join } from 'node:path';
import { afterAll, describe, expect, it } from 'vitest';
import { CHECK_CLASS_NEEDS, runValidate } from '../src/commands/validate.js';
-import { writeRecord } from '../src/core/notes.js';
+import { notesAbsenceEvidenceKey, writeRecord } from '../src/core/notes.js';
+
+/**
+ * A refspec says what a clone would fetch, never what the remote has, so on its
+ * own it leaves notes availability unknown and every reference check reads
+ * `not-checked` for a reason unrelated to what these cases measure (#512).
+ * `doctor --fix` records the remote probe; a fixture records the same evidence.
+ */
+const recordNotesProbe = (cwd: string): void => {
+ const url = execFileSync('git', ['config', '--get', 'remote.origin.url'], {
+ cwd,
+ env: GIT_ENV,
+ encoding: 'utf8',
+ }).trim();
+ execFileSync('git', ['config', '--local', notesAbsenceEvidenceKey('origin'), url], { cwd, env: GIT_ENV });
+};
import { loadFixtures } from './fixtures.js';
import { createTestRepo } from './git-fixtures.js';
@@ -504,6 +519,7 @@ describe('validate — check classes and reference integrity', () => {
['config', '--add', 'remote.origin.fetch', '+refs/notes/commitlore:refs/notes/commitlore'],
{ cwd: shallow, env: GIT_ENV },
);
+ recordNotesProbe(shallow);
const shallowMessage = join(shallow, 'message.txt');
writeFileSync(shallowMessage, message);
@@ -516,6 +532,7 @@ describe('validate — check classes and reference integrity', () => {
['config', '--add', 'remote.origin.fetch', '+refs/notes/commitlore:refs/notes/commitlore'],
{ cwd: full, env: GIT_ENV },
);
+ recordNotesProbe(full);
const fullMessage = join(full, 'message.txt');
writeFileSync(fullMessage, message);