@avi2dg/checks 0.6.0 → 0.9.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/README.md +256 -97
- package/oxlintrc.json +2 -2
- package/package.json +15 -6
- package/scripts/backtest.ts +25 -36
- package/scripts/ci-wiring.ts +138 -62
- package/scripts/comment-gate.ts +23 -41
- package/scripts/comment-matchers.ts +309 -0
- package/scripts/comments.ts +15 -292
- package/scripts/commit-identity.ts +91 -87
- package/scripts/gates.ts +59 -0
- package/scripts/git.ts +41 -0
- package/scripts/lint-coverage.sh +11 -5
- package/scripts/lint.ts +179 -0
- package/scripts/main.ts +28 -0
- package/scripts/mutation-compare.ts +43 -57
- package/scripts/suppressions-ratchet.ts +64 -76
- package/scripts/test-layout.ts +67 -44
package/scripts/backtest.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
import { comments, refused, syntaxOf } from "./comments.ts";
|
|
4
|
+
import { git } from "./git.ts";
|
|
5
|
+
import { runMain, Usage } from "./main.ts";
|
|
3
6
|
|
|
4
7
|
type Landed = {
|
|
5
8
|
readonly sha: string;
|
|
@@ -13,37 +16,20 @@ const USAGE = "usage: backtest.ts [commit-count]";
|
|
|
13
16
|
|
|
14
17
|
const EXCLUDED = ["generated/", "vendor/", "repos/", "node_modules/", "dist/"];
|
|
15
18
|
|
|
16
|
-
function die(message: string): never {
|
|
17
|
-
console.error(message);
|
|
18
|
-
process.exit(2);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function git(...args: readonly string[]): string {
|
|
22
|
-
const result = Bun.spawnSync(["git", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
23
|
-
if (!result.success) {
|
|
24
|
-
die(`backtest: git ${args.join(" ")}: ${result.stderr.toString().trim()}`);
|
|
25
|
-
}
|
|
26
|
-
return result.stdout.toString();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
19
|
function readable(path: string): boolean {
|
|
30
20
|
if (EXCLUDED.some((prefix) => path.startsWith(prefix))) return false;
|
|
31
|
-
|
|
32
|
-
return extension in SYNTAXES;
|
|
21
|
+
return syntaxOf(path) !== undefined;
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
const result = Bun.spawnSync(["git", "show", `${sha}:${path}`], { stdout: "pipe", stderr: "pipe" });
|
|
37
|
-
return result.success ? result.stdout.toString() : undefined;
|
|
38
|
-
}
|
|
24
|
+
const at = (sha: string, path: string) => git(["show", `${sha}:${path}`]).pipe(Effect.option);
|
|
39
25
|
|
|
40
26
|
function withoutLine(refusal: string): string {
|
|
41
27
|
return refusal.replace(/^(\S+?):\d+ /, "$1 ");
|
|
42
28
|
}
|
|
43
29
|
|
|
44
|
-
export
|
|
30
|
+
export const backtest = Effect.fn("backtest")(function* (count: string) {
|
|
45
31
|
const lines: string[] = [];
|
|
46
|
-
const commits = git("log", "--first-parent", "--format=%H%x09%P%x09%s", "-n", count)
|
|
32
|
+
const commits = (yield* git(["log", "--first-parent", "--format=%H%x09%P%x09%s", "-n", count]))
|
|
47
33
|
.trim()
|
|
48
34
|
.split("\n")
|
|
49
35
|
.map((row) => {
|
|
@@ -55,13 +41,13 @@ export async function backtest(count: string): Promise<string> {
|
|
|
55
41
|
const landed: Landed[] = [];
|
|
56
42
|
|
|
57
43
|
for (const { sha, parent, subject } of commits) {
|
|
58
|
-
const changed = git("diff", "--name-only", "--diff-filter=d", parent, sha)
|
|
44
|
+
const changed = (yield* git(["diff", "--name-only", "--diff-filter=d", parent, sha]))
|
|
59
45
|
.trim()
|
|
60
46
|
.split("\n")
|
|
61
47
|
.filter((path) => path !== "" && readable(path));
|
|
62
48
|
if (changed.length === 0) continue;
|
|
63
49
|
|
|
64
|
-
const numstat = git("diff", "--numstat", parent, sha, "--", ...changed).trim();
|
|
50
|
+
const numstat = (yield* git(["diff", "--numstat", parent, sha, "--", ...changed])).trim();
|
|
65
51
|
const added =
|
|
66
52
|
numstat === ""
|
|
67
53
|
? 0
|
|
@@ -71,17 +57,17 @@ export async function backtest(count: string): Promise<string> {
|
|
|
71
57
|
const refusals: string[] = [];
|
|
72
58
|
|
|
73
59
|
for (const path of changed) {
|
|
74
|
-
const after =
|
|
75
|
-
if (after
|
|
76
|
-
const before = (
|
|
60
|
+
const after = yield* at(sha, path);
|
|
61
|
+
if (Option.isNone(after)) continue;
|
|
62
|
+
const before = Option.getOrElse(yield* at(parent, path), () => "");
|
|
77
63
|
|
|
78
|
-
const wasRefused = new Set(refused(path, before).map(withoutLine));
|
|
79
|
-
for (const refusal of refused(path, after)) {
|
|
64
|
+
const wasRefused = new Set((yield* refused(path, before)).map(withoutLine));
|
|
65
|
+
for (const refusal of yield* refused(path, after.value)) {
|
|
80
66
|
if (!wasRefused.has(withoutLine(refusal))) refusals.push(refusal);
|
|
81
67
|
}
|
|
82
68
|
|
|
83
|
-
const wasComment = new Set(comments(path, before).map((one) => one.text));
|
|
84
|
-
for (const one of comments(path, after)) {
|
|
69
|
+
const wasComment = new Set((yield* comments(path, before)).map((one) => one.text));
|
|
70
|
+
for (const one of yield* comments(path, after.value)) {
|
|
85
71
|
if (!wasComment.has(one.text)) commentLines += one.text.split("\n").length;
|
|
86
72
|
}
|
|
87
73
|
}
|
|
@@ -108,10 +94,13 @@ export async function backtest(count: string): Promise<string> {
|
|
|
108
94
|
for (const refusal of one.refusals) lines.push(` ${refusal}`);
|
|
109
95
|
}
|
|
110
96
|
return lines.join("\n");
|
|
111
|
-
}
|
|
97
|
+
});
|
|
112
98
|
|
|
113
|
-
|
|
99
|
+
const main = Effect.gen(function* () {
|
|
114
100
|
const [count = "60", ...extra] = process.argv.slice(2);
|
|
115
|
-
if (extra.length > 0)
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
if (extra.length > 0) return yield* new Usage({ message: USAGE });
|
|
102
|
+
yield* Console.log(yield* backtest(count));
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (import.meta.main) runMain("backtest", main);
|
package/scripts/ci-wiring.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { git } from "./git.ts";
|
|
4
|
+
import { runMain } from "./main.ts";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_BRANCH,
|
|
7
|
+
ENTRY_POINT,
|
|
8
|
+
EVERY_REPOSITORY,
|
|
9
|
+
KIT_GATES,
|
|
10
|
+
LintWiring,
|
|
11
|
+
selectedGates,
|
|
12
|
+
type KitGate,
|
|
13
|
+
} from "./gates.ts";
|
|
4
14
|
|
|
5
15
|
export type Command = readonly string[];
|
|
6
16
|
|
|
@@ -12,6 +22,7 @@ export type Gate = {
|
|
|
12
22
|
export type Declaration = {
|
|
13
23
|
readonly gates: readonly Gate[];
|
|
14
24
|
readonly defaultBranch: string;
|
|
25
|
+
readonly lintGates: readonly KitGate[];
|
|
15
26
|
};
|
|
16
27
|
|
|
17
28
|
export type Workflow = {
|
|
@@ -24,8 +35,15 @@ export type BlockedInvocation = {
|
|
|
24
35
|
readonly blocker: string;
|
|
25
36
|
};
|
|
26
37
|
|
|
38
|
+
export type Omission = {
|
|
39
|
+
readonly gate: string;
|
|
40
|
+
readonly content: string;
|
|
41
|
+
readonly files: readonly [string, ...string[]];
|
|
42
|
+
};
|
|
43
|
+
|
|
27
44
|
export type Gap = {
|
|
28
45
|
readonly gate: string;
|
|
46
|
+
readonly entryPoint?: string;
|
|
29
47
|
readonly blocked: readonly BlockedInvocation[];
|
|
30
48
|
};
|
|
31
49
|
|
|
@@ -35,10 +53,11 @@ type RunStep = {
|
|
|
35
53
|
readonly script: string;
|
|
36
54
|
};
|
|
37
55
|
|
|
38
|
-
export class WiringError extends
|
|
56
|
+
export class WiringError extends Schema.TaggedError<WiringError>()("WiringError", {
|
|
57
|
+
message: Schema.String,
|
|
58
|
+
}) {}
|
|
39
59
|
|
|
40
60
|
const WORKFLOWS = ".github/workflows";
|
|
41
|
-
const DEFAULT_BRANCH = "main";
|
|
42
61
|
// Without these a pull_request workflow never sees the commits a pull request pushes.
|
|
43
62
|
const GATING_TYPES = ["opened", "synchronize"];
|
|
44
63
|
const CONSTANTS = new Map([
|
|
@@ -226,19 +245,32 @@ function runSteps(workflows: readonly Workflow[], branch: string): readonly RunS
|
|
|
226
245
|
return steps;
|
|
227
246
|
}
|
|
228
247
|
|
|
248
|
+
function entryPointCommand(gate: Command, lintGates: readonly KitGate[]): Command | undefined {
|
|
249
|
+
const index = gate.findIndex((word) => lintGates.some((kitGate) => kitGate.bin === word));
|
|
250
|
+
return index === -1 ? undefined : [...gate.slice(0, index), ENTRY_POINT.bin];
|
|
251
|
+
}
|
|
252
|
+
|
|
229
253
|
export function findGaps(declaration: Declaration, workflows: readonly Workflow[]): readonly Gap[] {
|
|
230
254
|
const steps = runSteps(workflows, declaration.defaultBranch);
|
|
231
255
|
return declaration.gates.flatMap((gate) => {
|
|
232
|
-
const
|
|
256
|
+
const entryPoint = entryPointCommand(gate.words, declaration.lintGates);
|
|
257
|
+
const commands = [
|
|
258
|
+
{ command: gate.command, words: gate.words },
|
|
259
|
+
...(entryPoint === undefined ? [] : [{ command: entryPoint.join(" "), words: entryPoint }]),
|
|
260
|
+
];
|
|
233
261
|
const invoking = steps.flatMap(({ location, blocker, script }) => {
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
const plain = plainCommand(script);
|
|
263
|
+
if (commands.some(({ words }) => invokes(plain, words))) return [{ location, blocker }];
|
|
264
|
+
const mentioned = commands.find(({ words }) => mentions(script, words));
|
|
265
|
+
if (mentioned === undefined) return [];
|
|
266
|
+
const alone = `the step runs more than ${mentioned.command}; give it its own step with nothing else in it`;
|
|
267
|
+
return [{ location, blocker: blocker ?? alone }];
|
|
236
268
|
});
|
|
237
269
|
if (invoking.some((step) => step.blocker === undefined)) return [];
|
|
238
270
|
const blocked = invoking.flatMap(({ location, blocker }) =>
|
|
239
271
|
blocker === undefined ? [] : [{ location, blocker }],
|
|
240
272
|
);
|
|
241
|
-
return [{ gate: gate.command, blocked }];
|
|
273
|
+
return [{ gate: gate.command, entryPoint: entryPoint?.join(" "), blocked }];
|
|
242
274
|
});
|
|
243
275
|
}
|
|
244
276
|
|
|
@@ -248,80 +280,124 @@ export function formatReport(declaration: Declaration, gaps: readonly Gap[]): st
|
|
|
248
280
|
const lines = [`ci-wiring: ${gaps.length} of ${declaration.gates.length} gate(s) do not run on ${target}:`];
|
|
249
281
|
for (const gap of gaps) {
|
|
250
282
|
lines.push(` ${gap.gate}`);
|
|
251
|
-
if (gap.blocked.length === 0)
|
|
283
|
+
if (gap.blocked.length === 0) {
|
|
284
|
+
lines.push(gap.entryPoint === undefined ? " no run step invokes it" : ` no run step invokes it or ${gap.entryPoint}`);
|
|
285
|
+
}
|
|
252
286
|
for (const { location, blocker } of gap.blocked) lines.push(` ${location}: ${blocker}`);
|
|
253
287
|
}
|
|
254
288
|
return lines.join("\n");
|
|
255
289
|
}
|
|
256
290
|
|
|
257
|
-
|
|
291
|
+
const decodeWiring = Schema.decodeUnknownEffect(LintWiring);
|
|
292
|
+
|
|
293
|
+
export const parseDeclaration = Effect.fnUntraced(function* (
|
|
294
|
+
manifest: unknown,
|
|
295
|
+
source: string,
|
|
296
|
+
): Effect.fn.Return<Declaration, WiringError> {
|
|
258
297
|
const configured = isRecord(manifest) && isRecord(manifest["ciWiring"]) ? manifest["ciWiring"] : {};
|
|
259
|
-
const listed = configured["gates"];
|
|
298
|
+
const listed: unknown = configured["gates"];
|
|
260
299
|
if (!Array.isArray(listed) || listed.length === 0) {
|
|
261
|
-
|
|
300
|
+
return yield* new WiringError({ message: `${source} sets no ciWiring.gates, a non-empty array of commands` });
|
|
262
301
|
}
|
|
263
|
-
const
|
|
302
|
+
const commands: readonly unknown[] = listed;
|
|
303
|
+
const gates: Gate[] = [];
|
|
304
|
+
for (const command of commands) {
|
|
264
305
|
const words = typeof command === "string" ? plainCommand(command) : undefined;
|
|
265
306
|
if (typeof command !== "string" || words === undefined) {
|
|
266
|
-
|
|
307
|
+
return yield* new WiringError({
|
|
308
|
+
message: `${source} ciWiring gate ${JSON.stringify(command)} is not one plain command`,
|
|
309
|
+
});
|
|
267
310
|
}
|
|
268
|
-
|
|
269
|
-
});
|
|
270
|
-
const defaultBranch = configured["defaultBranch"] ?? DEFAULT_BRANCH;
|
|
271
|
-
if (typeof defaultBranch !== "string" || defaultBranch === "") {
|
|
272
|
-
throw new WiringError(`ci-wiring: ${source} ciWiring.defaultBranch is not a branch name`);
|
|
311
|
+
gates.push({ command, words });
|
|
273
312
|
}
|
|
274
|
-
|
|
275
|
-
}
|
|
313
|
+
const { ciWiring } = yield* decodeWiring(manifest).pipe(
|
|
314
|
+
Effect.mapError((cause) => new WiringError({ message: `${source}: ${cause.message}` })),
|
|
315
|
+
);
|
|
316
|
+
return {
|
|
317
|
+
gates,
|
|
318
|
+
defaultBranch: ciWiring?.defaultBranch ?? DEFAULT_BRANCH,
|
|
319
|
+
lintGates: selectedGates(ciWiring?.lintGates),
|
|
320
|
+
};
|
|
321
|
+
});
|
|
276
322
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return { path, document: Bun.YAML.parse(text) };
|
|
280
|
-
} catch (error) {
|
|
281
|
-
throw new WiringError(`ci-wiring: cannot parse ${path}: ${String(error)}`);
|
|
282
|
-
}
|
|
323
|
+
function omittedFrom(lintGates: readonly KitGate[]): readonly KitGate[] {
|
|
324
|
+
return KIT_GATES.filter((gate) => !lintGates.includes(gate));
|
|
283
325
|
}
|
|
284
326
|
|
|
285
|
-
export function
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
327
|
+
export const findOmissions = Effect.fn("findOmissions")(function* (root: string, lintGates: readonly KitGate[]) {
|
|
328
|
+
const omissions: Omission[] = [];
|
|
329
|
+
for (const gate of omittedFrom(lintGates)) {
|
|
330
|
+
// LintGates refuses a selection that leaves out a gate every repository runs.
|
|
331
|
+
if (gate.appliesTo === EVERY_REPOSITORY) continue;
|
|
332
|
+
const { pathspecs, content } = gate.appliesTo;
|
|
333
|
+
const [first, ...rest] = (yield* git(["ls-files", "-z", "--", ...pathspecs], root)).split("\0").filter(Boolean);
|
|
334
|
+
if (first !== undefined) omissions.push({ gate: gate.bin, content, files: [first, ...rest] });
|
|
292
335
|
}
|
|
293
|
-
return
|
|
294
|
-
}
|
|
336
|
+
return omissions;
|
|
337
|
+
});
|
|
295
338
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (!existsSync(directory)) return [];
|
|
299
|
-
return readdirSync(directory)
|
|
300
|
-
.filter((name) => name.endsWith(".yml") || name.endsWith(".yaml"))
|
|
301
|
-
.sort()
|
|
302
|
-
.map((name) => parseWorkflow(`${WORKFLOWS}/${name}`, readFileSync(join(directory, name), "utf8")));
|
|
339
|
+
function sample([first, ...rest]: Omission["files"]): string {
|
|
340
|
+
return rest.length === 0 ? first : `${first} and ${rest.length} more`;
|
|
303
341
|
}
|
|
304
342
|
|
|
305
|
-
function
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
308
|
-
|
|
343
|
+
export function formatOmissions(lintGates: readonly KitGate[], omissions: readonly Omission[]): string | undefined {
|
|
344
|
+
const omitted = omittedFrom(lintGates);
|
|
345
|
+
if (omitted.length === 0) return undefined;
|
|
346
|
+
if (omissions.length === 0) {
|
|
347
|
+
const bins = omitted.map((gate) => gate.bin).join(", ");
|
|
348
|
+
return `ci-wiring: ciWiring.lintGates leaves out ${bins}, none of which this repository's contents make applicable`;
|
|
309
349
|
}
|
|
310
|
-
return
|
|
350
|
+
return [
|
|
351
|
+
`ci-wiring: ciWiring.lintGates leaves out ${omissions.length} gate(s) this repository's contents make applicable:`,
|
|
352
|
+
...omissions.map(({ gate, content, files }) => ` ${gate}: the repository tracks ${content} (${sample(files)})`),
|
|
353
|
+
].join("\n");
|
|
311
354
|
}
|
|
312
355
|
|
|
313
|
-
|
|
314
|
-
try
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
356
|
+
export const parseWorkflow = (path: string, text: string): Effect.Effect<Workflow, WiringError> =>
|
|
357
|
+
Effect.try({
|
|
358
|
+
try: () => ({ path, document: Bun.YAML.parse(text) }),
|
|
359
|
+
catch: (error) => new WiringError({ message: `cannot parse ${path}: ${String(error)}` }),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const parseJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
|
|
363
|
+
|
|
364
|
+
export const readDeclaration = Effect.fn("readDeclaration")(function* (root: string) {
|
|
365
|
+
const fs = yield* FileSystem.FileSystem;
|
|
366
|
+
const path = (yield* Path.Path).join(root, "package.json");
|
|
367
|
+
const manifest = yield* fs.readFileString(path).pipe(
|
|
368
|
+
Effect.flatMap(parseJson),
|
|
369
|
+
Effect.mapError((cause) => new WiringError({ message: `cannot read ${path} as JSON: ${cause.message}` })),
|
|
370
|
+
);
|
|
371
|
+
return yield* parseDeclaration(manifest, path);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
export const readWorkflows = Effect.fn("readWorkflows")(function* (root: string) {
|
|
375
|
+
const fs = yield* FileSystem.FileSystem;
|
|
376
|
+
const path = yield* Path.Path;
|
|
377
|
+
const directory = path.join(root, WORKFLOWS);
|
|
378
|
+
if (!(yield* fs.exists(directory))) return [];
|
|
379
|
+
const files = (yield* fs.readDirectory(directory))
|
|
380
|
+
.filter((name) => name.endsWith(".yml") || name.endsWith(".yaml"))
|
|
381
|
+
.sort();
|
|
382
|
+
return yield* Effect.forEach(files, (name) =>
|
|
383
|
+
fs
|
|
384
|
+
.readFileString(path.join(directory, name))
|
|
385
|
+
.pipe(Effect.flatMap((text) => parseWorkflow(`${WORKFLOWS}/${name}`, text))),
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const wiring = Effect.gen(function* () {
|
|
390
|
+
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
391
|
+
const declaration = yield* readDeclaration(root);
|
|
392
|
+
const gaps = findGaps(declaration, yield* readWorkflows(root));
|
|
393
|
+
const omissions = yield* findOmissions(root, declaration.lintGates);
|
|
394
|
+
const gapReport = formatReport(declaration, gaps);
|
|
395
|
+
yield* gaps.length > 0 ? Console.error(gapReport) : Console.log(gapReport);
|
|
396
|
+
const omissionReport = formatOmissions(declaration.lintGates, omissions);
|
|
397
|
+
if (omissionReport !== undefined) {
|
|
398
|
+
yield* omissions.length > 0 ? Console.error(omissionReport) : Console.log(omissionReport);
|
|
326
399
|
}
|
|
327
|
-
|
|
400
|
+
return gaps.length === 0 && omissions.length === 0;
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (import.meta.main) runMain("ci-wiring", wiring);
|
package/scripts/comment-gate.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
import { refused, syntaxOf } from "./comments.ts";
|
|
4
|
+
import { git, parentOrEmptyTree } from "./git.ts";
|
|
5
|
+
import { runMain, Usage } from "./main.ts";
|
|
3
6
|
|
|
4
7
|
export type GateResult = {
|
|
5
8
|
readonly files: number;
|
|
@@ -9,21 +12,8 @@ export type GateResult = {
|
|
|
9
12
|
|
|
10
13
|
const USAGE = "usage: comment-gate.ts <ref> | <base-ref> <head-ref>";
|
|
11
14
|
|
|
12
|
-
function die(message: string): never {
|
|
13
|
-
console.error(message);
|
|
14
|
-
process.exit(2);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function git(root: string, ...args: readonly string[]): string {
|
|
18
|
-
const result = Bun.spawnSync(["git", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" });
|
|
19
|
-
if (!result.success) {
|
|
20
|
-
die(`comment-gate: git ${args.join(" ")}: ${result.stderr.toString().trim()}`);
|
|
21
|
-
}
|
|
22
|
-
return result.stdout.toString();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
15
|
function readable(path: string): boolean {
|
|
26
|
-
return
|
|
16
|
+
return syntaxOf(path) !== undefined;
|
|
27
17
|
}
|
|
28
18
|
|
|
29
19
|
function newPathOf(line: string): string | undefined {
|
|
@@ -68,29 +58,23 @@ export function parseAddedLines(diff: string): Map<string, Set<number>> {
|
|
|
68
58
|
return added;
|
|
69
59
|
}
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
cwd: root,
|
|
74
|
-
stdout: "pipe",
|
|
75
|
-
stderr: "pipe",
|
|
76
|
-
});
|
|
77
|
-
return result.success ? result.stdout.toString() : undefined;
|
|
78
|
-
}
|
|
61
|
+
const show = (rev: string, path: string, root: string) =>
|
|
62
|
+
git(["show", `${rev}:${path}`], root).pipe(Effect.option);
|
|
79
63
|
|
|
80
|
-
export
|
|
81
|
-
const diff = git(
|
|
64
|
+
export const runRange = Effect.fn("runRange")(function* (root: string, base: string, head: string) {
|
|
65
|
+
const diff = yield* git(["-c", "core.quotePath=false", "diff", "-U0", "--no-color", "--no-prefix", base, head], root);
|
|
82
66
|
const added = parseAddedLines(diff);
|
|
83
67
|
const violations: string[] = [];
|
|
84
68
|
let addedLines = 0;
|
|
85
69
|
for (const [path, lines] of added) {
|
|
86
70
|
addedLines += lines.size;
|
|
87
71
|
if (!readable(path)) continue;
|
|
88
|
-
const source = show(head, path, root);
|
|
89
|
-
if (source
|
|
90
|
-
violations.push(...refused(path, source, lines));
|
|
72
|
+
const source = yield* show(head, path, root);
|
|
73
|
+
if (Option.isNone(source)) continue;
|
|
74
|
+
violations.push(...(yield* refused(path, source.value, lines)));
|
|
91
75
|
}
|
|
92
76
|
return { files: added.size, addedLines, violations };
|
|
93
|
-
}
|
|
77
|
+
});
|
|
94
78
|
|
|
95
79
|
export function report({ files, addedLines, violations }: GateResult): string {
|
|
96
80
|
if (violations.length === 0) {
|
|
@@ -99,18 +83,16 @@ export function report({ files, addedLines, violations }: GateResult): string {
|
|
|
99
83
|
return [`comment-gate: ${violations.length} violation(s):`, ...violations.map((one) => ` ${one}`)].join("\n");
|
|
100
84
|
}
|
|
101
85
|
|
|
102
|
-
|
|
103
|
-
return git(root, "rev-parse", "--verify", `${rev}^`).trim();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (import.meta.main) {
|
|
86
|
+
const gate = Effect.gen(function* () {
|
|
107
87
|
const [first, second, ...extra] = process.argv.slice(2);
|
|
108
|
-
if (first === undefined || extra.length > 0)
|
|
88
|
+
if (first === undefined || extra.length > 0) return yield* new Usage({ message: USAGE });
|
|
109
89
|
|
|
110
|
-
const root = git(
|
|
111
|
-
const
|
|
112
|
-
|
|
90
|
+
const root = (yield* git(["rev-parse", "--show-toplevel"], process.cwd())).trim();
|
|
91
|
+
const base = second === undefined ? yield* parentOrEmptyTree(first, root) : first;
|
|
92
|
+
const result = yield* runRange(root, base, second ?? first);
|
|
113
93
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
94
|
+
yield* Console.log(report(result));
|
|
95
|
+
return result.violations.length === 0;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (import.meta.main) runMain("comment-gate", gate);
|