@diff-cov/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DiffCov contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
1
+ # @diff-cov/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Line coverage for the lines a git diff adds or modifies, built for the commit
4
+ and push workflow.
5
+
6
+ ```bash
7
+ pnpm add -D @diff-cov/cli
8
+ pnpm exec diff-cov --base new --threshold 90
9
+ ```
10
+
11
+ The package installs a `diff-cov` binary.
12
+
13
+ ## Pre-push
14
+
15
+ Generate coverage with your test runner, then:
16
+
17
+ ```bash
18
+ # .husky/pre-push
19
+ pnpm exec diff-cov --push --threshold 90
20
+ ```
21
+
22
+ `--push` reads the refs git passes on stdin, so it measures exactly what the
23
+ push adds to the remote.
24
+
25
+ ## Direct invocation
26
+
27
+ ```bash
28
+ npx @diff-cov/cli --base auto --threshold 90
29
+ npx @diff-cov/cli --base origin/main --threshold 90
30
+ ```
31
+
32
+ ## Exit codes
33
+
34
+ | Code | Meaning |
35
+ | ---- | -------------------------------------------------------------------- |
36
+ | `0` | Passed, or no measurable changed lines |
37
+ | `1` | Below the threshold |
38
+ | `2` | Configuration, usage, or git error |
39
+ | `3` | Coverage report missing, unreadable, or older than the changed files |
40
+
41
+ ## Base modes
42
+
43
+ - `--base <ref>`: diff from `merge-base(ref, HEAD)`.
44
+ - `--base auto`: find the closest trunk (`main`, `master`, `develop`, `release/*`).
45
+ - `--base new`: use the pre-push remote sha, or the closest remote-tracking merge-base.
46
+
47
+ Reports can be lcov or Cobertura. See the main repository README for the full
48
+ configuration and the metric definition.
49
+
50
+ MIT licensed.
package/dist/index.cjs ADDED
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/run.ts
30
+ var import_node_fs = require("fs");
31
+ var import_promises2 = require("fs/promises");
32
+ var import_node_path2 = require("path");
33
+ var import_picocolors = __toESM(require("picocolors"), 1);
34
+ var import_core2 = require("@diff-cov/core");
35
+
36
+ // src/coverage.ts
37
+ var import_promises = require("fs/promises");
38
+ var import_node_path = require("path");
39
+ var import_tinyglobby = require("tinyglobby");
40
+ var import_core = require("@diff-cov/core");
41
+ var DEFAULT_COVERAGE_PATTERNS = [
42
+ "coverage/lcov.info",
43
+ "coverage/cobertura-coverage.xml",
44
+ "coverage/cobertura.xml"
45
+ ];
46
+ async function loadCoverage(root, patterns = DEFAULT_COVERAGE_PATTERNS, projectRoot = root) {
47
+ const find = (cwd) => (0, import_tinyglobby.glob)(patterns, {
48
+ cwd,
49
+ absolute: true,
50
+ onlyFiles: true,
51
+ dot: true
52
+ });
53
+ let files = await find(projectRoot);
54
+ if (files.length === 0 && projectRoot !== root) {
55
+ files = await find(root);
56
+ }
57
+ if (files.length === 0) {
58
+ throw new import_core.DiffCovError(
59
+ `No coverage report found (looked for ${patterns.join(", ")}). Run your tests with coverage first, or pass --coverage <path>.`,
60
+ "NO_COVERAGE"
61
+ );
62
+ }
63
+ const sources = [];
64
+ for (const file of files) {
65
+ const text = await (0, import_promises.readFile)(file, "utf8");
66
+ const context = { root, projectRoot, reportDir: (0, import_node_path.dirname)(file) };
67
+ if (file.endsWith(".xml") || text.trimStart().startsWith("<")) {
68
+ sources.push((0, import_core.parseCobertura)(text, context));
69
+ } else {
70
+ sources.push((0, import_core.parseLcov)(text, context));
71
+ }
72
+ }
73
+ return {
74
+ source: (0, import_core.mergeCoverage)(sources),
75
+ files: files.map((file) => (0, import_node_path.relative)(root, file))
76
+ };
77
+ }
78
+
79
+ // src/push.ts
80
+ function parsePushRefs(input) {
81
+ const refs = [];
82
+ for (const line of input.split(/\r?\n/)) {
83
+ const trimmed = line.trim();
84
+ if (!trimmed) continue;
85
+ const [localRef, localSha, remoteRef, remoteSha] = trimmed.split(/\s+/);
86
+ if (!localRef || !localSha || !remoteRef || !remoteSha) continue;
87
+ refs.push({ localRef, localSha, remoteRef, remoteSha });
88
+ }
89
+ return refs;
90
+ }
91
+ async function readStdin() {
92
+ if (process.stdin.isTTY) return "";
93
+ let input = "";
94
+ for await (const chunk of process.stdin) {
95
+ input += chunk;
96
+ }
97
+ return input;
98
+ }
99
+
100
+ // src/run.ts
101
+ var ZERO_SHA = /^0+$/;
102
+ function mergeChanges(target, changes) {
103
+ for (const change of changes) {
104
+ const existing = target.get(change.path);
105
+ if (!existing) {
106
+ target.set(change.path, { ...change, added: new Set(change.added) });
107
+ continue;
108
+ }
109
+ for (const line of change.added) existing.added.add(line);
110
+ if (change.status === "added") existing.status = "added";
111
+ }
112
+ }
113
+ async function collectPush(root, refs) {
114
+ const headSha = await (0, import_core2.revParse)(root, "HEAD");
115
+ if (!headSha) throw new import_core2.DiffCovError("HEAD cannot be resolved.", "BAD_HEAD");
116
+ const branchRefs = refs.filter(
117
+ (ref) => !ZERO_SHA.test(ref.localSha) && ref.remoteRef.startsWith("refs/heads/")
118
+ );
119
+ if (branchRefs.length === 0) {
120
+ if (refs.length > 0) return { changes: [], bases: [], warnings: [] };
121
+ const base = await (0, import_core2.resolveBase)(root, "new");
122
+ const changes2 = await (0, import_core2.collectChanges)(root, base, { scope: "committed" });
123
+ return { changes: changes2, bases: [base], warnings: [] };
124
+ }
125
+ const changes = /* @__PURE__ */ new Map();
126
+ const bases = [];
127
+ const warnings = [];
128
+ for (const ref of branchRefs) {
129
+ if (ref.localSha !== headSha) {
130
+ throw new import_core2.DiffCovError(
131
+ `Pushing ${ref.localRef} (${ref.localSha.slice(0, 8)}) while HEAD is ${headSha.slice(0, 8)}. DiffCov reads the worktree coverage report, so it can only check HEAD.`,
132
+ "NOT_HEAD"
133
+ );
134
+ }
135
+ let base;
136
+ if (!ZERO_SHA.test(ref.remoteSha) && await (0, import_core2.refExists)(root, ref.remoteSha)) {
137
+ base = {
138
+ mode: "new",
139
+ ref: ref.remoteRef.replace(/^refs\/heads\//, ""),
140
+ mergeBase: ref.remoteSha,
141
+ head: ref.localSha,
142
+ distance: await (0, import_core2.countCommits)(root, ref.remoteSha, ref.localSha),
143
+ warnings: []
144
+ };
145
+ } else {
146
+ base = await (0, import_core2.resolveBase)(root, "new", ref.localSha);
147
+ if (!ZERO_SHA.test(ref.remoteSha)) {
148
+ warnings.push(
149
+ `Remote commit ${ref.remoteSha.slice(0, 8)} is not available locally; used ${base.ref} as the base.`
150
+ );
151
+ }
152
+ }
153
+ if (!bases.some((item) => item.mergeBase === base.mergeBase && item.head === base.head)) {
154
+ bases.push(base);
155
+ }
156
+ mergeChanges(changes, await (0, import_core2.collectChanges)(root, base, { scope: "committed" }));
157
+ }
158
+ return { changes: [...changes.values()], bases, warnings };
159
+ }
160
+ async function checkFreshness(root, coverageFiles, sourcePaths) {
161
+ const reportStats = await Promise.all(
162
+ coverageFiles.map((file) => (0, import_promises2.stat)((0, import_node_path2.resolve)(root, file)).catch(() => null))
163
+ );
164
+ const reportTimes = reportStats.filter((item) => item !== null).map((item) => item.mtimeMs);
165
+ if (reportTimes.length === 0) return null;
166
+ const reportTime = Math.min(...reportTimes);
167
+ const sourceTimes = await Promise.all(
168
+ sourcePaths.map(
169
+ (path) => (0, import_promises2.stat)((0, import_node_path2.resolve)(root, path)).then((item) => item.mtimeMs).catch(() => 0)
170
+ )
171
+ );
172
+ const newestSource = Math.max(0, ...sourceTimes);
173
+ if (newestSource > reportTime + 1e3) {
174
+ const newest = sourcePaths[sourceTimes.indexOf(newestSource)];
175
+ return `Coverage report is older than ${newest ?? "the changed files"}. Re-run tests with coverage, or pass --allow-stale.`;
176
+ }
177
+ return null;
178
+ }
179
+ function colorize(text, enabled) {
180
+ if (!enabled || !import_picocolors.default.isColorSupported) return text;
181
+ return text.replace(/\bPASS\b/g, import_picocolors.default.green("PASS")).replace(/\bFAIL\b/g, import_picocolors.default.red("FAIL")).replace(/^warning:/gm, import_picocolors.default.yellow("warning:"));
182
+ }
183
+ function exitCodeFor(error) {
184
+ if (error instanceof import_core2.MissingCoverageError) return 3;
185
+ if (error instanceof import_core2.DiffCovError) {
186
+ if (["NO_COVERAGE", "MISSING_COVERAGE", "STALE_COVERAGE"].includes(error.code)) return 3;
187
+ return 2;
188
+ }
189
+ return 2;
190
+ }
191
+ async function run(options) {
192
+ const requestedCwd = (0, import_node_path2.resolve)(options.cwd ?? process.cwd());
193
+ const cwd = (() => {
194
+ try {
195
+ return (0, import_node_fs.realpathSync)(requestedCwd);
196
+ } catch {
197
+ return requestedCwd;
198
+ }
199
+ })();
200
+ const root = await (0, import_core2.getRepoRoot)(cwd);
201
+ const localConfig = await (0, import_core2.loadConfig)(cwd, options.config);
202
+ const rootConfig = !options.config && cwd !== root ? await (0, import_core2.loadConfig)(root) : {};
203
+ const config = { ...rootConfig, ...localConfig };
204
+ const scope = options.scope ?? config.scope ?? "committed";
205
+ const base = options.base ?? config.base ?? (options.push ? "new" : "auto");
206
+ const coveragePatterns = options.coverage?.length ? options.coverage : config.coverage?.length ? config.coverage : DEFAULT_COVERAGE_PATTERNS;
207
+ const include = options.include?.length ? options.include : config.include;
208
+ const exclude = options.exclude?.length ? options.exclude : config.exclude;
209
+ const missing = options.missing ?? config.missing ?? "fail";
210
+ const threshold = options.threshold ?? config.threshold ?? 90;
211
+ const perFile = options.perFile ?? config.perFile;
212
+ const relativeCwd = (0, import_node_path2.relative)(root, cwd);
213
+ const pathPrefix = relativeCwd && !relativeCwd.startsWith("..") ? (0, import_core2.toPosix)(relativeCwd) : void 0;
214
+ const loaded = await loadCoverage(root, coveragePatterns, cwd);
215
+ if (options.verbose) {
216
+ process.stderr.write(
217
+ `diff-cov: cwd=${cwd}
218
+ root=${root}
219
+ base=${base}
220
+ coverage=${loaded.files.join(", ")}
221
+ `
222
+ );
223
+ }
224
+ let changes;
225
+ let bases;
226
+ let scopeUsed = scope;
227
+ if (options.push) {
228
+ const input = options.stdin ?? await readStdin();
229
+ const refs = parsePushRefs(input);
230
+ const push = await collectPush(root, refs);
231
+ changes = push.changes;
232
+ bases = push.bases;
233
+ scopeUsed = "committed";
234
+ } else {
235
+ const resolved = await (0, import_core2.resolveBase)(root, base);
236
+ changes = await (0, import_core2.collectChanges)(root, resolved, {
237
+ scope,
238
+ includeUntracked: config.includeUntracked
239
+ });
240
+ bases = [resolved];
241
+ }
242
+ const report = (0, import_core2.buildReport)({
243
+ changes,
244
+ bases,
245
+ scope: scopeUsed,
246
+ coverage: loaded.source,
247
+ coverageFiles: loaded.files,
248
+ include,
249
+ exclude,
250
+ missing,
251
+ pathPrefix
252
+ });
253
+ const measuredPaths = report.files.filter((file) => file.status === "measured").map((file) => file.path);
254
+ const stale = await checkFreshness(root, loaded.files, measuredPaths);
255
+ if (stale) {
256
+ const allowStale = options.allowStale ?? config.allowStale ?? false;
257
+ if (!allowStale) {
258
+ throw new import_core2.DiffCovError(stale, "STALE_COVERAGE");
259
+ }
260
+ report.warnings.push(stale);
261
+ }
262
+ const verdict = (0, import_core2.evaluateThreshold)(report, {
263
+ total: threshold,
264
+ ...perFile !== void 0 ? { perFile } : {}
265
+ });
266
+ if (options.report) {
267
+ const path = (0, import_node_path2.resolve)(cwd, options.report);
268
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
269
+ await (0, import_promises2.writeFile)(path, JSON.stringify({ ...report, verdict }, null, 2) + "\n", "utf8");
270
+ }
271
+ if (options.json) {
272
+ process.stdout.write(JSON.stringify({ ...report, verdict }, null, 2) + "\n");
273
+ } else {
274
+ process.stdout.write(colorize((0, import_core2.formatReport)(report, verdict), options.color) + "\n");
275
+ }
276
+ return verdict.passed ? 0 : 1;
277
+ }
278
+
279
+ // src/cli.ts
280
+ function collect(value, previous) {
281
+ return [...previous, value];
282
+ }
283
+ function parseNumber(value) {
284
+ const parsed = Number(value);
285
+ if (!Number.isFinite(parsed)) throw new Error(`Expected a number, got ${value}`);
286
+ return parsed;
287
+ }
288
+ function createProgram() {
289
+ const program = new import_commander.Command();
290
+ program.name("diff-cov").description("Line coverage of the lines a git diff adds or modifies.").version("0.1.0").option("--cwd <dir>", "working directory", process.cwd()).option("--config <path>", "config file").option("--base <ref>", 'base ref, "auto" or "new"').addOption(new import_commander.Option("--scope <scope>", "diff scope").choices(["committed", "worktree"])).option("--push", "pre-push mode: read refs from stdin").option("--coverage <path>", "coverage report path or glob (repeatable)", collect, []).option("--threshold <percent>", "total diff coverage threshold", parseNumber).option("--per-file <percent>", "per-file diff coverage threshold", parseNumber).addOption(
291
+ new import_commander.Option(
292
+ "--missing <policy>",
293
+ "what to do when a changed file is absent from coverage"
294
+ ).choices(["fail", "warn", "ignore"])
295
+ ).option("--allow-stale", "accept a coverage report older than the changed files").option("--include <glob>", "source glob to include (repeatable)", collect, []).option("--exclude <glob>", "glob to exclude (repeatable)", collect, []).option("--json", "print JSON instead of text").option("--report <path>", "write a JSON report").option("--no-color", "disable color").option("--verbose", "print extra diagnostics");
296
+ return program;
297
+ }
298
+ async function main(argv = process.argv) {
299
+ const program = createProgram();
300
+ await program.parseAsync(argv);
301
+ const options = program.opts();
302
+ try {
303
+ process.exitCode = await run(options);
304
+ } catch (error) {
305
+ const code = exitCodeFor(error);
306
+ const message = error instanceof Error ? error.message : String(error);
307
+ process.stderr.write(`diff-cov: ${message}
308
+ `);
309
+ if (options.verbose && error instanceof Error && error.stack) {
310
+ process.stderr.write(`${error.stack}
311
+ `);
312
+ }
313
+ process.exitCode = code;
314
+ }
315
+ }
316
+
317
+ // src/index.ts
318
+ void main();
319
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/run.ts","../src/coverage.ts","../src/push.ts","../src/index.ts"],"sourcesContent":["import { Command, Option } from 'commander'\nimport { exitCodeFor, run, type CliOptions } from './run.js'\n\ndeclare const __DIFF_COV_VERSION__: string\n\nfunction collect(value: string, previous: string[]): string[] {\n return [...previous, value]\n}\n\nfunction parseNumber(value: string): number {\n const parsed = Number(value)\n if (!Number.isFinite(parsed)) throw new Error(`Expected a number, got ${value}`)\n return parsed\n}\n\nexport function createProgram(): Command {\n const program = new Command()\n program\n .name('diff-cov')\n .description('Line coverage of the lines a git diff adds or modifies.')\n .version(__DIFF_COV_VERSION__)\n .option('--cwd <dir>', 'working directory', process.cwd())\n .option('--config <path>', 'config file')\n .option('--base <ref>', 'base ref, \"auto\" or \"new\"')\n .addOption(new Option('--scope <scope>', 'diff scope').choices(['committed', 'worktree']))\n .option('--push', 'pre-push mode: read refs from stdin')\n .option('--coverage <path>', 'coverage report path or glob (repeatable)', collect, [])\n .option('--threshold <percent>', 'total diff coverage threshold', parseNumber)\n .option('--per-file <percent>', 'per-file diff coverage threshold', parseNumber)\n .addOption(\n new Option(\n '--missing <policy>',\n 'what to do when a changed file is absent from coverage',\n ).choices(['fail', 'warn', 'ignore']),\n )\n .option('--allow-stale', 'accept a coverage report older than the changed files')\n .option('--include <glob>', 'source glob to include (repeatable)', collect, [])\n .option('--exclude <glob>', 'glob to exclude (repeatable)', collect, [])\n .option('--json', 'print JSON instead of text')\n .option('--report <path>', 'write a JSON report')\n .option('--no-color', 'disable color')\n .option('--verbose', 'print extra diagnostics')\n return program\n}\n\nexport async function main(argv: string[] = process.argv): Promise<void> {\n const program = createProgram()\n await program.parseAsync(argv)\n const options = program.opts<CliOptions>()\n try {\n process.exitCode = await run(options)\n } catch (error) {\n const code = exitCodeFor(error)\n const message = error instanceof Error ? error.message : String(error)\n process.stderr.write(`diff-cov: ${message}\\n`)\n if (options.verbose && error instanceof Error && error.stack) {\n process.stderr.write(`${error.stack}\\n`)\n }\n process.exitCode = code\n }\n}\n","import { realpathSync } from 'node:fs'\nimport { mkdir, stat, writeFile } from 'node:fs/promises'\nimport { dirname, relative, resolve } from 'node:path'\nimport pc from 'picocolors'\nimport {\n buildReport,\n collectChanges,\n countCommits,\n DiffCovError,\n evaluateThreshold,\n formatReport,\n getRepoRoot,\n loadConfig,\n MissingCoverageError,\n refExists,\n resolveBase,\n revParse,\n toPosix,\n type BaseSpec,\n type ChangedFile,\n type DiffCovConfig,\n type MissingPolicy,\n type Report,\n type ResolvedBase,\n type Scope,\n type Verdict,\n} from '@diff-cov/core'\nimport { DEFAULT_COVERAGE_PATTERNS, loadCoverage } from './coverage.js'\nimport { parsePushRefs, readStdin, type PushRef } from './push.js'\n\nconst ZERO_SHA = /^0+$/\n\nexport interface CliOptions {\n cwd?: string\n config?: string\n base?: BaseSpec\n scope?: Scope\n push?: boolean\n coverage?: string[]\n threshold?: number\n perFile?: number\n missing?: MissingPolicy\n allowStale?: boolean\n include?: string[]\n exclude?: string[]\n json?: boolean\n report?: string\n color: boolean\n verbose?: boolean\n /** Pre-push stdin, for tests and embedders. */\n stdin?: string\n}\n\nfunction mergeChanges(target: Map<string, ChangedFile>, changes: ChangedFile[]): void {\n for (const change of changes) {\n const existing = target.get(change.path)\n if (!existing) {\n target.set(change.path, { ...change, added: new Set(change.added) })\n continue\n }\n for (const line of change.added) existing.added.add(line)\n if (change.status === 'added') existing.status = 'added'\n }\n}\n\nexport async function collectPush(\n root: string,\n refs: PushRef[],\n): Promise<{ changes: ChangedFile[]; bases: ResolvedBase[]; warnings: string[] }> {\n const headSha = await revParse(root, 'HEAD')\n if (!headSha) throw new DiffCovError('HEAD cannot be resolved.', 'BAD_HEAD')\n\n const branchRefs = refs.filter(\n (ref) => !ZERO_SHA.test(ref.localSha) && ref.remoteRef.startsWith('refs/heads/'),\n )\n if (branchRefs.length === 0) {\n // No stdin (manual --push) means \"everything not on the remote yet\".\n // If stdin only contained tag pushes there is nothing to measure.\n if (refs.length > 0) return { changes: [], bases: [], warnings: [] }\n const base = await resolveBase(root, 'new')\n const changes = await collectChanges(root, base, { scope: 'committed' })\n return { changes, bases: [base], warnings: [] }\n }\n\n const changes = new Map<string, ChangedFile>()\n const bases: ResolvedBase[] = []\n const warnings: string[] = []\n\n for (const ref of branchRefs) {\n if (ref.localSha !== headSha) {\n throw new DiffCovError(\n `Pushing ${ref.localRef} (${ref.localSha.slice(0, 8)}) while HEAD is ${headSha.slice(0, 8)}. ` +\n 'DiffCov reads the worktree coverage report, so it can only check HEAD.',\n 'NOT_HEAD',\n )\n }\n\n let base: ResolvedBase\n if (!ZERO_SHA.test(ref.remoteSha) && (await refExists(root, ref.remoteSha))) {\n base = {\n mode: 'new',\n ref: ref.remoteRef.replace(/^refs\\/heads\\//, ''),\n mergeBase: ref.remoteSha,\n head: ref.localSha,\n distance: await countCommits(root, ref.remoteSha, ref.localSha),\n warnings: [],\n }\n } else {\n base = await resolveBase(root, 'new', ref.localSha)\n if (!ZERO_SHA.test(ref.remoteSha)) {\n warnings.push(\n `Remote commit ${ref.remoteSha.slice(0, 8)} is not available locally; used ${base.ref} as the base.`,\n )\n }\n }\n\n if (!bases.some((item) => item.mergeBase === base.mergeBase && item.head === base.head)) {\n bases.push(base)\n }\n mergeChanges(changes, await collectChanges(root, base, { scope: 'committed' }))\n }\n\n return { changes: [...changes.values()], bases, warnings }\n}\n\nexport async function checkFreshness(\n root: string,\n coverageFiles: string[],\n sourcePaths: string[],\n): Promise<string | null> {\n const reportStats = await Promise.all(\n coverageFiles.map((file) => stat(resolve(root, file)).catch(() => null)),\n )\n const reportTimes = reportStats\n .filter((item): item is NonNullable<typeof item> => item !== null)\n .map((item) => item.mtimeMs)\n if (reportTimes.length === 0) return null\n\n const reportTime = Math.min(...reportTimes)\n const sourceTimes = await Promise.all(\n sourcePaths.map((path) =>\n stat(resolve(root, path))\n .then((item) => item.mtimeMs)\n .catch(() => 0),\n ),\n )\n const newestSource = Math.max(0, ...sourceTimes)\n if (newestSource > reportTime + 1000) {\n const newest = sourcePaths[sourceTimes.indexOf(newestSource)]\n return (\n `Coverage report is older than ${newest ?? 'the changed files'}. ` +\n 'Re-run tests with coverage, or pass --allow-stale.'\n )\n }\n return null\n}\n\nfunction colorize(text: string, enabled: boolean): string {\n if (!enabled || !pc.isColorSupported) return text\n return text\n .replace(/\\bPASS\\b/g, pc.green('PASS'))\n .replace(/\\bFAIL\\b/g, pc.red('FAIL'))\n .replace(/^warning:/gm, pc.yellow('warning:'))\n}\n\nexport function exitCodeFor(error: unknown): number {\n if (error instanceof MissingCoverageError) return 3\n if (error instanceof DiffCovError) {\n if (['NO_COVERAGE', 'MISSING_COVERAGE', 'STALE_COVERAGE'].includes(error.code)) return 3\n return 2\n }\n return 2\n}\n\nexport async function run(options: CliOptions): Promise<number> {\n const requestedCwd = resolve(options.cwd ?? process.cwd())\n const cwd = (() => {\n try {\n return realpathSync(requestedCwd)\n } catch {\n return requestedCwd\n }\n })()\n const root = await getRepoRoot(cwd)\n const localConfig = await loadConfig(cwd, options.config)\n const rootConfig: DiffCovConfig = !options.config && cwd !== root ? await loadConfig(root) : {}\n const config: DiffCovConfig = { ...rootConfig, ...localConfig }\n\n const scope: Scope = options.scope ?? config.scope ?? 'committed'\n const base: BaseSpec = options.base ?? config.base ?? (options.push ? 'new' : 'auto')\n const coveragePatterns = options.coverage?.length\n ? options.coverage\n : config.coverage?.length\n ? config.coverage\n : DEFAULT_COVERAGE_PATTERNS\n const include = options.include?.length ? options.include : config.include\n const exclude = options.exclude?.length ? options.exclude : config.exclude\n const missing: MissingPolicy = options.missing ?? config.missing ?? 'fail'\n const threshold = options.threshold ?? config.threshold ?? 90\n const perFile = options.perFile ?? config.perFile\n const relativeCwd = relative(root, cwd)\n const pathPrefix = relativeCwd && !relativeCwd.startsWith('..') ? toPosix(relativeCwd) : undefined\n\n const loaded = await loadCoverage(root, coveragePatterns, cwd)\n if (options.verbose) {\n process.stderr.write(\n `diff-cov: cwd=${cwd}\\nroot=${root}\\nbase=${base}\\ncoverage=${loaded.files.join(', ')}\\n`,\n )\n }\n\n let changes: ChangedFile[]\n let bases: ResolvedBase[]\n let scopeUsed: Scope = scope\n\n if (options.push) {\n const input = options.stdin ?? (await readStdin())\n const refs = parsePushRefs(input)\n const push = await collectPush(root, refs)\n changes = push.changes\n bases = push.bases\n scopeUsed = 'committed'\n } else {\n const resolved = await resolveBase(root, base)\n changes = await collectChanges(root, resolved, {\n scope,\n includeUntracked: config.includeUntracked,\n })\n bases = [resolved]\n }\n\n const report: Report = buildReport({\n changes,\n bases,\n scope: scopeUsed,\n coverage: loaded.source,\n coverageFiles: loaded.files,\n include,\n exclude,\n missing,\n pathPrefix,\n })\n\n const measuredPaths = report.files\n .filter((file) => file.status === 'measured')\n .map((file) => file.path)\n const stale = await checkFreshness(root, loaded.files, measuredPaths)\n if (stale) {\n const allowStale = options.allowStale ?? config.allowStale ?? false\n if (!allowStale) {\n throw new DiffCovError(stale, 'STALE_COVERAGE')\n }\n report.warnings.push(stale)\n }\n\n const verdict: Verdict = evaluateThreshold(report, {\n total: threshold,\n ...(perFile !== undefined ? { perFile } : {}),\n })\n\n if (options.report) {\n const path = resolve(cwd, options.report)\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, JSON.stringify({ ...report, verdict }, null, 2) + '\\n', 'utf8')\n }\n\n if (options.json) {\n process.stdout.write(JSON.stringify({ ...report, verdict }, null, 2) + '\\n')\n } else {\n process.stdout.write(colorize(formatReport(report, verdict), options.color) + '\\n')\n }\n\n return verdict.passed ? 0 : 1\n}\n","import { readFile } from 'node:fs/promises'\nimport { dirname, relative } from 'node:path'\nimport { glob } from 'tinyglobby'\nimport {\n DiffCovError,\n mergeCoverage,\n parseCobertura,\n parseLcov,\n type CoverageSource,\n} from '@diff-cov/core'\n\nexport const DEFAULT_COVERAGE_PATTERNS = [\n 'coverage/lcov.info',\n 'coverage/cobertura-coverage.xml',\n 'coverage/cobertura.xml',\n]\n\nexport interface LoadedCoverage {\n source: CoverageSource\n files: string[]\n}\n\nexport async function loadCoverage(\n root: string,\n patterns: string[] = DEFAULT_COVERAGE_PATTERNS,\n projectRoot: string = root,\n): Promise<LoadedCoverage> {\n const find = (cwd: string) =>\n glob(patterns, {\n cwd,\n absolute: true,\n onlyFiles: true,\n dot: true,\n })\n\n let files = await find(projectRoot)\n if (files.length === 0 && projectRoot !== root) {\n files = await find(root)\n }\n\n if (files.length === 0) {\n throw new DiffCovError(\n `No coverage report found (looked for ${patterns.join(', ')}). ` +\n 'Run your tests with coverage first, or pass --coverage <path>.',\n 'NO_COVERAGE',\n )\n }\n\n const sources: CoverageSource[] = []\n for (const file of files) {\n const text = await readFile(file, 'utf8')\n const context = { root, projectRoot, reportDir: dirname(file) }\n if (file.endsWith('.xml') || text.trimStart().startsWith('<')) {\n sources.push(parseCobertura(text, context))\n } else {\n sources.push(parseLcov(text, context))\n }\n }\n\n return {\n source: mergeCoverage(sources),\n files: files.map((file) => relative(root, file)),\n }\n}\n","export interface PushRef {\n localRef: string\n localSha: string\n remoteRef: string\n remoteSha: string\n}\n\nexport function parsePushRefs(input: string): PushRef[] {\n const refs: PushRef[] = []\n for (const line of input.split(/\\r?\\n/)) {\n const trimmed = line.trim()\n if (!trimmed) continue\n const [localRef, localSha, remoteRef, remoteSha] = trimmed.split(/\\s+/)\n if (!localRef || !localSha || !remoteRef || !remoteSha) continue\n refs.push({ localRef, localSha, remoteRef, remoteSha })\n }\n return refs\n}\n\nexport async function readStdin(): Promise<string> {\n if (process.stdin.isTTY) return ''\n let input = ''\n for await (const chunk of process.stdin) {\n input += chunk\n }\n return input\n}\n","#!/usr/bin/env node\nimport { main } from './cli.js'\n\nvoid main()\n"],"mappings":"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","names":["import_promises","import_node_path","import_core","changes","pc"]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command, Option } from "commander";
5
+
6
+ // src/run.ts
7
+ import { realpathSync } from "fs";
8
+ import { mkdir, stat, writeFile } from "fs/promises";
9
+ import { dirname as dirname2, relative as relative2, resolve } from "path";
10
+ import pc from "picocolors";
11
+ import {
12
+ buildReport,
13
+ collectChanges,
14
+ countCommits,
15
+ DiffCovError as DiffCovError2,
16
+ evaluateThreshold,
17
+ formatReport,
18
+ getRepoRoot,
19
+ loadConfig,
20
+ MissingCoverageError,
21
+ refExists,
22
+ resolveBase,
23
+ revParse,
24
+ toPosix
25
+ } from "@diff-cov/core";
26
+
27
+ // src/coverage.ts
28
+ import { readFile } from "fs/promises";
29
+ import { dirname, relative } from "path";
30
+ import { glob } from "tinyglobby";
31
+ import {
32
+ DiffCovError,
33
+ mergeCoverage,
34
+ parseCobertura,
35
+ parseLcov
36
+ } from "@diff-cov/core";
37
+ var DEFAULT_COVERAGE_PATTERNS = [
38
+ "coverage/lcov.info",
39
+ "coverage/cobertura-coverage.xml",
40
+ "coverage/cobertura.xml"
41
+ ];
42
+ async function loadCoverage(root, patterns = DEFAULT_COVERAGE_PATTERNS, projectRoot = root) {
43
+ const find = (cwd) => glob(patterns, {
44
+ cwd,
45
+ absolute: true,
46
+ onlyFiles: true,
47
+ dot: true
48
+ });
49
+ let files = await find(projectRoot);
50
+ if (files.length === 0 && projectRoot !== root) {
51
+ files = await find(root);
52
+ }
53
+ if (files.length === 0) {
54
+ throw new DiffCovError(
55
+ `No coverage report found (looked for ${patterns.join(", ")}). Run your tests with coverage first, or pass --coverage <path>.`,
56
+ "NO_COVERAGE"
57
+ );
58
+ }
59
+ const sources = [];
60
+ for (const file of files) {
61
+ const text = await readFile(file, "utf8");
62
+ const context = { root, projectRoot, reportDir: dirname(file) };
63
+ if (file.endsWith(".xml") || text.trimStart().startsWith("<")) {
64
+ sources.push(parseCobertura(text, context));
65
+ } else {
66
+ sources.push(parseLcov(text, context));
67
+ }
68
+ }
69
+ return {
70
+ source: mergeCoverage(sources),
71
+ files: files.map((file) => relative(root, file))
72
+ };
73
+ }
74
+
75
+ // src/push.ts
76
+ function parsePushRefs(input) {
77
+ const refs = [];
78
+ for (const line of input.split(/\r?\n/)) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed) continue;
81
+ const [localRef, localSha, remoteRef, remoteSha] = trimmed.split(/\s+/);
82
+ if (!localRef || !localSha || !remoteRef || !remoteSha) continue;
83
+ refs.push({ localRef, localSha, remoteRef, remoteSha });
84
+ }
85
+ return refs;
86
+ }
87
+ async function readStdin() {
88
+ if (process.stdin.isTTY) return "";
89
+ let input = "";
90
+ for await (const chunk of process.stdin) {
91
+ input += chunk;
92
+ }
93
+ return input;
94
+ }
95
+
96
+ // src/run.ts
97
+ var ZERO_SHA = /^0+$/;
98
+ function mergeChanges(target, changes) {
99
+ for (const change of changes) {
100
+ const existing = target.get(change.path);
101
+ if (!existing) {
102
+ target.set(change.path, { ...change, added: new Set(change.added) });
103
+ continue;
104
+ }
105
+ for (const line of change.added) existing.added.add(line);
106
+ if (change.status === "added") existing.status = "added";
107
+ }
108
+ }
109
+ async function collectPush(root, refs) {
110
+ const headSha = await revParse(root, "HEAD");
111
+ if (!headSha) throw new DiffCovError2("HEAD cannot be resolved.", "BAD_HEAD");
112
+ const branchRefs = refs.filter(
113
+ (ref) => !ZERO_SHA.test(ref.localSha) && ref.remoteRef.startsWith("refs/heads/")
114
+ );
115
+ if (branchRefs.length === 0) {
116
+ if (refs.length > 0) return { changes: [], bases: [], warnings: [] };
117
+ const base = await resolveBase(root, "new");
118
+ const changes2 = await collectChanges(root, base, { scope: "committed" });
119
+ return { changes: changes2, bases: [base], warnings: [] };
120
+ }
121
+ const changes = /* @__PURE__ */ new Map();
122
+ const bases = [];
123
+ const warnings = [];
124
+ for (const ref of branchRefs) {
125
+ if (ref.localSha !== headSha) {
126
+ throw new DiffCovError2(
127
+ `Pushing ${ref.localRef} (${ref.localSha.slice(0, 8)}) while HEAD is ${headSha.slice(0, 8)}. DiffCov reads the worktree coverage report, so it can only check HEAD.`,
128
+ "NOT_HEAD"
129
+ );
130
+ }
131
+ let base;
132
+ if (!ZERO_SHA.test(ref.remoteSha) && await refExists(root, ref.remoteSha)) {
133
+ base = {
134
+ mode: "new",
135
+ ref: ref.remoteRef.replace(/^refs\/heads\//, ""),
136
+ mergeBase: ref.remoteSha,
137
+ head: ref.localSha,
138
+ distance: await countCommits(root, ref.remoteSha, ref.localSha),
139
+ warnings: []
140
+ };
141
+ } else {
142
+ base = await resolveBase(root, "new", ref.localSha);
143
+ if (!ZERO_SHA.test(ref.remoteSha)) {
144
+ warnings.push(
145
+ `Remote commit ${ref.remoteSha.slice(0, 8)} is not available locally; used ${base.ref} as the base.`
146
+ );
147
+ }
148
+ }
149
+ if (!bases.some((item) => item.mergeBase === base.mergeBase && item.head === base.head)) {
150
+ bases.push(base);
151
+ }
152
+ mergeChanges(changes, await collectChanges(root, base, { scope: "committed" }));
153
+ }
154
+ return { changes: [...changes.values()], bases, warnings };
155
+ }
156
+ async function checkFreshness(root, coverageFiles, sourcePaths) {
157
+ const reportStats = await Promise.all(
158
+ coverageFiles.map((file) => stat(resolve(root, file)).catch(() => null))
159
+ );
160
+ const reportTimes = reportStats.filter((item) => item !== null).map((item) => item.mtimeMs);
161
+ if (reportTimes.length === 0) return null;
162
+ const reportTime = Math.min(...reportTimes);
163
+ const sourceTimes = await Promise.all(
164
+ sourcePaths.map(
165
+ (path) => stat(resolve(root, path)).then((item) => item.mtimeMs).catch(() => 0)
166
+ )
167
+ );
168
+ const newestSource = Math.max(0, ...sourceTimes);
169
+ if (newestSource > reportTime + 1e3) {
170
+ const newest = sourcePaths[sourceTimes.indexOf(newestSource)];
171
+ return `Coverage report is older than ${newest ?? "the changed files"}. Re-run tests with coverage, or pass --allow-stale.`;
172
+ }
173
+ return null;
174
+ }
175
+ function colorize(text, enabled) {
176
+ if (!enabled || !pc.isColorSupported) return text;
177
+ return text.replace(/\bPASS\b/g, pc.green("PASS")).replace(/\bFAIL\b/g, pc.red("FAIL")).replace(/^warning:/gm, pc.yellow("warning:"));
178
+ }
179
+ function exitCodeFor(error) {
180
+ if (error instanceof MissingCoverageError) return 3;
181
+ if (error instanceof DiffCovError2) {
182
+ if (["NO_COVERAGE", "MISSING_COVERAGE", "STALE_COVERAGE"].includes(error.code)) return 3;
183
+ return 2;
184
+ }
185
+ return 2;
186
+ }
187
+ async function run(options) {
188
+ const requestedCwd = resolve(options.cwd ?? process.cwd());
189
+ const cwd = (() => {
190
+ try {
191
+ return realpathSync(requestedCwd);
192
+ } catch {
193
+ return requestedCwd;
194
+ }
195
+ })();
196
+ const root = await getRepoRoot(cwd);
197
+ const localConfig = await loadConfig(cwd, options.config);
198
+ const rootConfig = !options.config && cwd !== root ? await loadConfig(root) : {};
199
+ const config = { ...rootConfig, ...localConfig };
200
+ const scope = options.scope ?? config.scope ?? "committed";
201
+ const base = options.base ?? config.base ?? (options.push ? "new" : "auto");
202
+ const coveragePatterns = options.coverage?.length ? options.coverage : config.coverage?.length ? config.coverage : DEFAULT_COVERAGE_PATTERNS;
203
+ const include = options.include?.length ? options.include : config.include;
204
+ const exclude = options.exclude?.length ? options.exclude : config.exclude;
205
+ const missing = options.missing ?? config.missing ?? "fail";
206
+ const threshold = options.threshold ?? config.threshold ?? 90;
207
+ const perFile = options.perFile ?? config.perFile;
208
+ const relativeCwd = relative2(root, cwd);
209
+ const pathPrefix = relativeCwd && !relativeCwd.startsWith("..") ? toPosix(relativeCwd) : void 0;
210
+ const loaded = await loadCoverage(root, coveragePatterns, cwd);
211
+ if (options.verbose) {
212
+ process.stderr.write(
213
+ `diff-cov: cwd=${cwd}
214
+ root=${root}
215
+ base=${base}
216
+ coverage=${loaded.files.join(", ")}
217
+ `
218
+ );
219
+ }
220
+ let changes;
221
+ let bases;
222
+ let scopeUsed = scope;
223
+ if (options.push) {
224
+ const input = options.stdin ?? await readStdin();
225
+ const refs = parsePushRefs(input);
226
+ const push = await collectPush(root, refs);
227
+ changes = push.changes;
228
+ bases = push.bases;
229
+ scopeUsed = "committed";
230
+ } else {
231
+ const resolved = await resolveBase(root, base);
232
+ changes = await collectChanges(root, resolved, {
233
+ scope,
234
+ includeUntracked: config.includeUntracked
235
+ });
236
+ bases = [resolved];
237
+ }
238
+ const report = buildReport({
239
+ changes,
240
+ bases,
241
+ scope: scopeUsed,
242
+ coverage: loaded.source,
243
+ coverageFiles: loaded.files,
244
+ include,
245
+ exclude,
246
+ missing,
247
+ pathPrefix
248
+ });
249
+ const measuredPaths = report.files.filter((file) => file.status === "measured").map((file) => file.path);
250
+ const stale = await checkFreshness(root, loaded.files, measuredPaths);
251
+ if (stale) {
252
+ const allowStale = options.allowStale ?? config.allowStale ?? false;
253
+ if (!allowStale) {
254
+ throw new DiffCovError2(stale, "STALE_COVERAGE");
255
+ }
256
+ report.warnings.push(stale);
257
+ }
258
+ const verdict = evaluateThreshold(report, {
259
+ total: threshold,
260
+ ...perFile !== void 0 ? { perFile } : {}
261
+ });
262
+ if (options.report) {
263
+ const path = resolve(cwd, options.report);
264
+ await mkdir(dirname2(path), { recursive: true });
265
+ await writeFile(path, JSON.stringify({ ...report, verdict }, null, 2) + "\n", "utf8");
266
+ }
267
+ if (options.json) {
268
+ process.stdout.write(JSON.stringify({ ...report, verdict }, null, 2) + "\n");
269
+ } else {
270
+ process.stdout.write(colorize(formatReport(report, verdict), options.color) + "\n");
271
+ }
272
+ return verdict.passed ? 0 : 1;
273
+ }
274
+
275
+ // src/cli.ts
276
+ function collect(value, previous) {
277
+ return [...previous, value];
278
+ }
279
+ function parseNumber(value) {
280
+ const parsed = Number(value);
281
+ if (!Number.isFinite(parsed)) throw new Error(`Expected a number, got ${value}`);
282
+ return parsed;
283
+ }
284
+ function createProgram() {
285
+ const program = new Command();
286
+ program.name("diff-cov").description("Line coverage of the lines a git diff adds or modifies.").version("0.1.0").option("--cwd <dir>", "working directory", process.cwd()).option("--config <path>", "config file").option("--base <ref>", 'base ref, "auto" or "new"').addOption(new Option("--scope <scope>", "diff scope").choices(["committed", "worktree"])).option("--push", "pre-push mode: read refs from stdin").option("--coverage <path>", "coverage report path or glob (repeatable)", collect, []).option("--threshold <percent>", "total diff coverage threshold", parseNumber).option("--per-file <percent>", "per-file diff coverage threshold", parseNumber).addOption(
287
+ new Option(
288
+ "--missing <policy>",
289
+ "what to do when a changed file is absent from coverage"
290
+ ).choices(["fail", "warn", "ignore"])
291
+ ).option("--allow-stale", "accept a coverage report older than the changed files").option("--include <glob>", "source glob to include (repeatable)", collect, []).option("--exclude <glob>", "glob to exclude (repeatable)", collect, []).option("--json", "print JSON instead of text").option("--report <path>", "write a JSON report").option("--no-color", "disable color").option("--verbose", "print extra diagnostics");
292
+ return program;
293
+ }
294
+ async function main(argv = process.argv) {
295
+ const program = createProgram();
296
+ await program.parseAsync(argv);
297
+ const options = program.opts();
298
+ try {
299
+ process.exitCode = await run(options);
300
+ } catch (error) {
301
+ const code = exitCodeFor(error);
302
+ const message = error instanceof Error ? error.message : String(error);
303
+ process.stderr.write(`diff-cov: ${message}
304
+ `);
305
+ if (options.verbose && error instanceof Error && error.stack) {
306
+ process.stderr.write(`${error.stack}
307
+ `);
308
+ }
309
+ process.exitCode = code;
310
+ }
311
+ }
312
+
313
+ // src/index.ts
314
+ void main();
315
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/run.ts","../src/coverage.ts","../src/push.ts","../src/index.ts"],"sourcesContent":["import { Command, Option } from 'commander'\nimport { exitCodeFor, run, type CliOptions } from './run.js'\n\ndeclare const __DIFF_COV_VERSION__: string\n\nfunction collect(value: string, previous: string[]): string[] {\n return [...previous, value]\n}\n\nfunction parseNumber(value: string): number {\n const parsed = Number(value)\n if (!Number.isFinite(parsed)) throw new Error(`Expected a number, got ${value}`)\n return parsed\n}\n\nexport function createProgram(): Command {\n const program = new Command()\n program\n .name('diff-cov')\n .description('Line coverage of the lines a git diff adds or modifies.')\n .version(__DIFF_COV_VERSION__)\n .option('--cwd <dir>', 'working directory', process.cwd())\n .option('--config <path>', 'config file')\n .option('--base <ref>', 'base ref, \"auto\" or \"new\"')\n .addOption(new Option('--scope <scope>', 'diff scope').choices(['committed', 'worktree']))\n .option('--push', 'pre-push mode: read refs from stdin')\n .option('--coverage <path>', 'coverage report path or glob (repeatable)', collect, [])\n .option('--threshold <percent>', 'total diff coverage threshold', parseNumber)\n .option('--per-file <percent>', 'per-file diff coverage threshold', parseNumber)\n .addOption(\n new Option(\n '--missing <policy>',\n 'what to do when a changed file is absent from coverage',\n ).choices(['fail', 'warn', 'ignore']),\n )\n .option('--allow-stale', 'accept a coverage report older than the changed files')\n .option('--include <glob>', 'source glob to include (repeatable)', collect, [])\n .option('--exclude <glob>', 'glob to exclude (repeatable)', collect, [])\n .option('--json', 'print JSON instead of text')\n .option('--report <path>', 'write a JSON report')\n .option('--no-color', 'disable color')\n .option('--verbose', 'print extra diagnostics')\n return program\n}\n\nexport async function main(argv: string[] = process.argv): Promise<void> {\n const program = createProgram()\n await program.parseAsync(argv)\n const options = program.opts<CliOptions>()\n try {\n process.exitCode = await run(options)\n } catch (error) {\n const code = exitCodeFor(error)\n const message = error instanceof Error ? error.message : String(error)\n process.stderr.write(`diff-cov: ${message}\\n`)\n if (options.verbose && error instanceof Error && error.stack) {\n process.stderr.write(`${error.stack}\\n`)\n }\n process.exitCode = code\n }\n}\n","import { realpathSync } from 'node:fs'\nimport { mkdir, stat, writeFile } from 'node:fs/promises'\nimport { dirname, relative, resolve } from 'node:path'\nimport pc from 'picocolors'\nimport {\n buildReport,\n collectChanges,\n countCommits,\n DiffCovError,\n evaluateThreshold,\n formatReport,\n getRepoRoot,\n loadConfig,\n MissingCoverageError,\n refExists,\n resolveBase,\n revParse,\n toPosix,\n type BaseSpec,\n type ChangedFile,\n type DiffCovConfig,\n type MissingPolicy,\n type Report,\n type ResolvedBase,\n type Scope,\n type Verdict,\n} from '@diff-cov/core'\nimport { DEFAULT_COVERAGE_PATTERNS, loadCoverage } from './coverage.js'\nimport { parsePushRefs, readStdin, type PushRef } from './push.js'\n\nconst ZERO_SHA = /^0+$/\n\nexport interface CliOptions {\n cwd?: string\n config?: string\n base?: BaseSpec\n scope?: Scope\n push?: boolean\n coverage?: string[]\n threshold?: number\n perFile?: number\n missing?: MissingPolicy\n allowStale?: boolean\n include?: string[]\n exclude?: string[]\n json?: boolean\n report?: string\n color: boolean\n verbose?: boolean\n /** Pre-push stdin, for tests and embedders. */\n stdin?: string\n}\n\nfunction mergeChanges(target: Map<string, ChangedFile>, changes: ChangedFile[]): void {\n for (const change of changes) {\n const existing = target.get(change.path)\n if (!existing) {\n target.set(change.path, { ...change, added: new Set(change.added) })\n continue\n }\n for (const line of change.added) existing.added.add(line)\n if (change.status === 'added') existing.status = 'added'\n }\n}\n\nexport async function collectPush(\n root: string,\n refs: PushRef[],\n): Promise<{ changes: ChangedFile[]; bases: ResolvedBase[]; warnings: string[] }> {\n const headSha = await revParse(root, 'HEAD')\n if (!headSha) throw new DiffCovError('HEAD cannot be resolved.', 'BAD_HEAD')\n\n const branchRefs = refs.filter(\n (ref) => !ZERO_SHA.test(ref.localSha) && ref.remoteRef.startsWith('refs/heads/'),\n )\n if (branchRefs.length === 0) {\n // No stdin (manual --push) means \"everything not on the remote yet\".\n // If stdin only contained tag pushes there is nothing to measure.\n if (refs.length > 0) return { changes: [], bases: [], warnings: [] }\n const base = await resolveBase(root, 'new')\n const changes = await collectChanges(root, base, { scope: 'committed' })\n return { changes, bases: [base], warnings: [] }\n }\n\n const changes = new Map<string, ChangedFile>()\n const bases: ResolvedBase[] = []\n const warnings: string[] = []\n\n for (const ref of branchRefs) {\n if (ref.localSha !== headSha) {\n throw new DiffCovError(\n `Pushing ${ref.localRef} (${ref.localSha.slice(0, 8)}) while HEAD is ${headSha.slice(0, 8)}. ` +\n 'DiffCov reads the worktree coverage report, so it can only check HEAD.',\n 'NOT_HEAD',\n )\n }\n\n let base: ResolvedBase\n if (!ZERO_SHA.test(ref.remoteSha) && (await refExists(root, ref.remoteSha))) {\n base = {\n mode: 'new',\n ref: ref.remoteRef.replace(/^refs\\/heads\\//, ''),\n mergeBase: ref.remoteSha,\n head: ref.localSha,\n distance: await countCommits(root, ref.remoteSha, ref.localSha),\n warnings: [],\n }\n } else {\n base = await resolveBase(root, 'new', ref.localSha)\n if (!ZERO_SHA.test(ref.remoteSha)) {\n warnings.push(\n `Remote commit ${ref.remoteSha.slice(0, 8)} is not available locally; used ${base.ref} as the base.`,\n )\n }\n }\n\n if (!bases.some((item) => item.mergeBase === base.mergeBase && item.head === base.head)) {\n bases.push(base)\n }\n mergeChanges(changes, await collectChanges(root, base, { scope: 'committed' }))\n }\n\n return { changes: [...changes.values()], bases, warnings }\n}\n\nexport async function checkFreshness(\n root: string,\n coverageFiles: string[],\n sourcePaths: string[],\n): Promise<string | null> {\n const reportStats = await Promise.all(\n coverageFiles.map((file) => stat(resolve(root, file)).catch(() => null)),\n )\n const reportTimes = reportStats\n .filter((item): item is NonNullable<typeof item> => item !== null)\n .map((item) => item.mtimeMs)\n if (reportTimes.length === 0) return null\n\n const reportTime = Math.min(...reportTimes)\n const sourceTimes = await Promise.all(\n sourcePaths.map((path) =>\n stat(resolve(root, path))\n .then((item) => item.mtimeMs)\n .catch(() => 0),\n ),\n )\n const newestSource = Math.max(0, ...sourceTimes)\n if (newestSource > reportTime + 1000) {\n const newest = sourcePaths[sourceTimes.indexOf(newestSource)]\n return (\n `Coverage report is older than ${newest ?? 'the changed files'}. ` +\n 'Re-run tests with coverage, or pass --allow-stale.'\n )\n }\n return null\n}\n\nfunction colorize(text: string, enabled: boolean): string {\n if (!enabled || !pc.isColorSupported) return text\n return text\n .replace(/\\bPASS\\b/g, pc.green('PASS'))\n .replace(/\\bFAIL\\b/g, pc.red('FAIL'))\n .replace(/^warning:/gm, pc.yellow('warning:'))\n}\n\nexport function exitCodeFor(error: unknown): number {\n if (error instanceof MissingCoverageError) return 3\n if (error instanceof DiffCovError) {\n if (['NO_COVERAGE', 'MISSING_COVERAGE', 'STALE_COVERAGE'].includes(error.code)) return 3\n return 2\n }\n return 2\n}\n\nexport async function run(options: CliOptions): Promise<number> {\n const requestedCwd = resolve(options.cwd ?? process.cwd())\n const cwd = (() => {\n try {\n return realpathSync(requestedCwd)\n } catch {\n return requestedCwd\n }\n })()\n const root = await getRepoRoot(cwd)\n const localConfig = await loadConfig(cwd, options.config)\n const rootConfig: DiffCovConfig = !options.config && cwd !== root ? await loadConfig(root) : {}\n const config: DiffCovConfig = { ...rootConfig, ...localConfig }\n\n const scope: Scope = options.scope ?? config.scope ?? 'committed'\n const base: BaseSpec = options.base ?? config.base ?? (options.push ? 'new' : 'auto')\n const coveragePatterns = options.coverage?.length\n ? options.coverage\n : config.coverage?.length\n ? config.coverage\n : DEFAULT_COVERAGE_PATTERNS\n const include = options.include?.length ? options.include : config.include\n const exclude = options.exclude?.length ? options.exclude : config.exclude\n const missing: MissingPolicy = options.missing ?? config.missing ?? 'fail'\n const threshold = options.threshold ?? config.threshold ?? 90\n const perFile = options.perFile ?? config.perFile\n const relativeCwd = relative(root, cwd)\n const pathPrefix = relativeCwd && !relativeCwd.startsWith('..') ? toPosix(relativeCwd) : undefined\n\n const loaded = await loadCoverage(root, coveragePatterns, cwd)\n if (options.verbose) {\n process.stderr.write(\n `diff-cov: cwd=${cwd}\\nroot=${root}\\nbase=${base}\\ncoverage=${loaded.files.join(', ')}\\n`,\n )\n }\n\n let changes: ChangedFile[]\n let bases: ResolvedBase[]\n let scopeUsed: Scope = scope\n\n if (options.push) {\n const input = options.stdin ?? (await readStdin())\n const refs = parsePushRefs(input)\n const push = await collectPush(root, refs)\n changes = push.changes\n bases = push.bases\n scopeUsed = 'committed'\n } else {\n const resolved = await resolveBase(root, base)\n changes = await collectChanges(root, resolved, {\n scope,\n includeUntracked: config.includeUntracked,\n })\n bases = [resolved]\n }\n\n const report: Report = buildReport({\n changes,\n bases,\n scope: scopeUsed,\n coverage: loaded.source,\n coverageFiles: loaded.files,\n include,\n exclude,\n missing,\n pathPrefix,\n })\n\n const measuredPaths = report.files\n .filter((file) => file.status === 'measured')\n .map((file) => file.path)\n const stale = await checkFreshness(root, loaded.files, measuredPaths)\n if (stale) {\n const allowStale = options.allowStale ?? config.allowStale ?? false\n if (!allowStale) {\n throw new DiffCovError(stale, 'STALE_COVERAGE')\n }\n report.warnings.push(stale)\n }\n\n const verdict: Verdict = evaluateThreshold(report, {\n total: threshold,\n ...(perFile !== undefined ? { perFile } : {}),\n })\n\n if (options.report) {\n const path = resolve(cwd, options.report)\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, JSON.stringify({ ...report, verdict }, null, 2) + '\\n', 'utf8')\n }\n\n if (options.json) {\n process.stdout.write(JSON.stringify({ ...report, verdict }, null, 2) + '\\n')\n } else {\n process.stdout.write(colorize(formatReport(report, verdict), options.color) + '\\n')\n }\n\n return verdict.passed ? 0 : 1\n}\n","import { readFile } from 'node:fs/promises'\nimport { dirname, relative } from 'node:path'\nimport { glob } from 'tinyglobby'\nimport {\n DiffCovError,\n mergeCoverage,\n parseCobertura,\n parseLcov,\n type CoverageSource,\n} from '@diff-cov/core'\n\nexport const DEFAULT_COVERAGE_PATTERNS = [\n 'coverage/lcov.info',\n 'coverage/cobertura-coverage.xml',\n 'coverage/cobertura.xml',\n]\n\nexport interface LoadedCoverage {\n source: CoverageSource\n files: string[]\n}\n\nexport async function loadCoverage(\n root: string,\n patterns: string[] = DEFAULT_COVERAGE_PATTERNS,\n projectRoot: string = root,\n): Promise<LoadedCoverage> {\n const find = (cwd: string) =>\n glob(patterns, {\n cwd,\n absolute: true,\n onlyFiles: true,\n dot: true,\n })\n\n let files = await find(projectRoot)\n if (files.length === 0 && projectRoot !== root) {\n files = await find(root)\n }\n\n if (files.length === 0) {\n throw new DiffCovError(\n `No coverage report found (looked for ${patterns.join(', ')}). ` +\n 'Run your tests with coverage first, or pass --coverage <path>.',\n 'NO_COVERAGE',\n )\n }\n\n const sources: CoverageSource[] = []\n for (const file of files) {\n const text = await readFile(file, 'utf8')\n const context = { root, projectRoot, reportDir: dirname(file) }\n if (file.endsWith('.xml') || text.trimStart().startsWith('<')) {\n sources.push(parseCobertura(text, context))\n } else {\n sources.push(parseLcov(text, context))\n }\n }\n\n return {\n source: mergeCoverage(sources),\n files: files.map((file) => relative(root, file)),\n }\n}\n","export interface PushRef {\n localRef: string\n localSha: string\n remoteRef: string\n remoteSha: string\n}\n\nexport function parsePushRefs(input: string): PushRef[] {\n const refs: PushRef[] = []\n for (const line of input.split(/\\r?\\n/)) {\n const trimmed = line.trim()\n if (!trimmed) continue\n const [localRef, localSha, remoteRef, remoteSha] = trimmed.split(/\\s+/)\n if (!localRef || !localSha || !remoteRef || !remoteSha) continue\n refs.push({ localRef, localSha, remoteRef, remoteSha })\n }\n return refs\n}\n\nexport async function readStdin(): Promise<string> {\n if (process.stdin.isTTY) return ''\n let input = ''\n for await (const chunk of process.stdin) {\n input += chunk\n }\n return input\n}\n","#!/usr/bin/env node\nimport { main } from './cli.js'\n\nvoid main()\n"],"mappings":"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","names":["dirname","relative","DiffCovError","DiffCovError","changes","relative","dirname"]}
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@diff-cov/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Pre-push diff coverage gate for git and Vitest.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "coverage",
9
+ "diff",
10
+ "lcov",
11
+ "cobertura",
12
+ "vitest",
13
+ "husky",
14
+ "pre-push"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "bin": {
20
+ "diff-cov": "./dist/index.js"
21
+ },
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md"
35
+ ],
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "dependencies": {
40
+ "commander": "^15.1.0",
41
+ "picocolors": "^1.1.1",
42
+ "tinyglobby": "^0.2.17",
43
+ "@diff-cov/core": "0.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "tsup": "^9.0.0",
47
+ "typescript": "^5.9.3",
48
+ "vitest": "4.1.9"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "test": "vitest run",
53
+ "typecheck": "tsc --noEmit"
54
+ }
6
55
  }