@avi2dg/checks 0.6.0 → 0.8.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 +164 -91
- package/package.json +13 -6
- package/scripts/backtest.ts +23 -33
- package/scripts/ci-wiring.ts +86 -65
- package/scripts/comment-gate.ts +23 -38
- package/scripts/comments.ts +24 -13
- package/scripts/commit-identity.ts +91 -87
- package/scripts/gates.ts +21 -0
- package/scripts/git.ts +27 -0
- package/scripts/lint.ts +162 -0
- package/scripts/main.ts +28 -0
- package/scripts/mutation-compare.ts +43 -57
- package/scripts/suppressions-ratchet.ts +59 -76
- package/scripts/test-layout.ts +67 -44
package/scripts/lint.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Config, Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
4
|
+
import { DEFAULT_BRANCH, KIT_GATES, type KitGate } from "./gates.ts";
|
|
5
|
+
import { git } from "./git.ts";
|
|
6
|
+
import { runMain, Usage } from "./main.ts";
|
|
7
|
+
|
|
8
|
+
type Range = {
|
|
9
|
+
readonly refs: readonly [tip: string] | readonly [base: string, head: string];
|
|
10
|
+
readonly source: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type Outcome = "passed" | "violated" | "undecided";
|
|
14
|
+
|
|
15
|
+
class RangeUnresolved extends Schema.TaggedError<RangeUnresolved>()("RangeUnresolved", {
|
|
16
|
+
message: Schema.String,
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
class GatesUndecided extends Schema.TaggedError<GatesUndecided>()("GatesUndecided", {
|
|
20
|
+
message: Schema.String,
|
|
21
|
+
}) {}
|
|
22
|
+
|
|
23
|
+
const NAME = "checks-lint";
|
|
24
|
+
const USAGE = "usage: lint.ts [<base-ref> <head-ref>]";
|
|
25
|
+
const PULL_REQUEST_EVENT = "pull_request";
|
|
26
|
+
const SHALLOW_HINT = "a CI checkout needs actions/checkout fetch-depth: 0";
|
|
27
|
+
|
|
28
|
+
const decodePullRequestEvent = Schema.decodeUnknownEffect(
|
|
29
|
+
Schema.fromJsonString(
|
|
30
|
+
Schema.Struct({
|
|
31
|
+
pull_request: Schema.Struct({
|
|
32
|
+
number: Schema.Int,
|
|
33
|
+
base: Schema.Struct({ ref: Schema.String }),
|
|
34
|
+
head: Schema.Struct({ sha: Schema.String }),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const pullRequestEnds = Effect.fn("pullRequestEnds")(function* (eventPath: string) {
|
|
41
|
+
const fs = yield* FileSystem.FileSystem;
|
|
42
|
+
const { pull_request: pullRequest } = yield* fs.readFileString(eventPath).pipe(
|
|
43
|
+
Effect.flatMap(decodePullRequestEvent),
|
|
44
|
+
Effect.mapError(
|
|
45
|
+
(cause) => new RangeUnresolved({ message: `cannot read the pull request from ${eventPath}: ${cause.message}` }),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
base: `origin/${pullRequest.base.ref}`,
|
|
50
|
+
head: pullRequest.head.sha,
|
|
51
|
+
source: `pull request #${pullRequest.number} into ${pullRequest.base.ref}`,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const decodeManifest = Schema.decodeUnknownEffect(
|
|
56
|
+
Schema.fromJsonString(
|
|
57
|
+
Schema.Struct({
|
|
58
|
+
ciWiring: Schema.optionalKey(Schema.Struct({ defaultBranch: Schema.optionalKey(Schema.NonEmptyString) })),
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const declaredDefaultBranch = Effect.gen(function* () {
|
|
64
|
+
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
65
|
+
const manifest = (yield* Path.Path).join(root, "package.json");
|
|
66
|
+
const { ciWiring } = yield* (yield* FileSystem.FileSystem).readFileString(manifest).pipe(Effect.flatMap(decodeManifest));
|
|
67
|
+
return ciWiring?.defaultBranch ?? DEFAULT_BRANCH;
|
|
68
|
+
}).pipe(
|
|
69
|
+
Effect.mapError((cause) => new RangeUnresolved({ message: `cannot read ciWiring.defaultBranch: ${cause.message}` })),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const localEnds = git(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]).pipe(
|
|
73
|
+
Effect.map((ref) => ref.trim()),
|
|
74
|
+
Effect.catchTag("GitFailure", () => declaredDefaultBranch.pipe(Effect.map((branch) => `origin/${branch}`))),
|
|
75
|
+
Effect.map((base) => ({ base, head: "HEAD", source: `HEAD against ${base}` })),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const endsOf = Effect.fn("endsOf")(function* (args: readonly string[]) {
|
|
79
|
+
const [base, head, ...extra] = args;
|
|
80
|
+
if (base !== undefined && head !== undefined && extra.length === 0) {
|
|
81
|
+
return { base, head, source: `${head} against ${base}` };
|
|
82
|
+
}
|
|
83
|
+
if (base !== undefined) return yield* new Usage({ message: USAGE });
|
|
84
|
+
|
|
85
|
+
const event = yield* Config.all({
|
|
86
|
+
name: Config.option(Config.String("GITHUB_EVENT_NAME")),
|
|
87
|
+
path: Config.option(Config.String("GITHUB_EVENT_PATH")),
|
|
88
|
+
}).pipe(Effect.mapError((cause) => new RangeUnresolved({ message: cause.message })));
|
|
89
|
+
if (Option.isSome(event.name) && event.name.value === PULL_REQUEST_EVENT) {
|
|
90
|
+
if (Option.isNone(event.path)) {
|
|
91
|
+
return yield* new RangeUnresolved({ message: `${PULL_REQUEST_EVENT} sets no GITHUB_EVENT_PATH` });
|
|
92
|
+
}
|
|
93
|
+
return yield* pullRequestEnds(event.path.value);
|
|
94
|
+
}
|
|
95
|
+
return yield* localEnds;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const commitOf = (ref: string) =>
|
|
99
|
+
git(["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`]).pipe(
|
|
100
|
+
Effect.map((sha) => sha.trim()),
|
|
101
|
+
Effect.mapError(
|
|
102
|
+
(cause) => new RangeUnresolved({ message: `${ref} is not a commit in this clone; ${SHALLOW_HINT}: ${cause.message}` }),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// From the base branch's tip, every range gate would charge the head with the commits the base
|
|
107
|
+
// branch gained after the head branched off.
|
|
108
|
+
const resolveRange = Effect.fn("resolveRange")(function* (args: readonly string[]) {
|
|
109
|
+
const ends = yield* endsOf(args);
|
|
110
|
+
const head = yield* commitOf(ends.head);
|
|
111
|
+
const base = yield* git(["merge-base", yield* commitOf(ends.base), head]).pipe(
|
|
112
|
+
Effect.map((sha) => sha.trim()),
|
|
113
|
+
Effect.mapError(
|
|
114
|
+
() => new RangeUnresolved({ message: `${ends.base} and ${ends.head} share no commit in this clone; ${SHALLOW_HINT}` }),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
return { refs: base === head ? [head] : [base, head], source: ends.source } satisfies Range;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function describe({ refs }: Range): string {
|
|
121
|
+
return refs.length === 1 ? `tip ${refs[0]}` : `range ${refs[0]}..${refs[1]}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function outcomeOf(exitCode: number): Outcome {
|
|
125
|
+
if (exitCode === 0) return "passed";
|
|
126
|
+
return exitCode === 1 ? "violated" : "undecided";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range) {
|
|
130
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
131
|
+
const script = (yield* Path.Path).join(import.meta.dir, gate.script);
|
|
132
|
+
const program = gate.script.endsWith(".sh") ? "sh" : process.execPath;
|
|
133
|
+
const args = gate.reads === "range" ? [script, ...range.refs] : [script];
|
|
134
|
+
const exitCode = yield* spawner
|
|
135
|
+
.exitCode(ChildProcess.make(program, args, { stdin: "ignore", stdout: "inherit", stderr: "inherit" }))
|
|
136
|
+
.pipe(
|
|
137
|
+
Effect.catch((cause) =>
|
|
138
|
+
Console.error(`${NAME}: cannot run ${gate.bin}: ${cause.message}`).pipe(Effect.as(ChildProcessSpawner.ExitCode(2))),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
return { gate, outcome: outcomeOf(exitCode) };
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const lint = Effect.gen(function* () {
|
|
145
|
+
const range = yield* resolveRange(process.argv.slice(2));
|
|
146
|
+
yield* Console.log(`${NAME}: ${describe(range)} from ${range.source}`);
|
|
147
|
+
|
|
148
|
+
const verdicts = yield* Effect.forEach(KIT_GATES, (gate) => runGate(gate, range));
|
|
149
|
+
const failed = verdicts.filter((verdict) => verdict.outcome !== "passed");
|
|
150
|
+
if (failed.length === 0) {
|
|
151
|
+
yield* Console.log(`${NAME}: ${KIT_GATES.length} gate(s) pass`);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const summary = `${failed.length} of ${KIT_GATES.length} gate(s) failed: ${failed.map((verdict) => verdict.gate.bin).join(", ")}`;
|
|
155
|
+
if (failed.every((verdict) => verdict.outcome === "undecided")) {
|
|
156
|
+
return yield* new GatesUndecided({ message: summary });
|
|
157
|
+
}
|
|
158
|
+
yield* Console.error(`${NAME}: ${summary}`);
|
|
159
|
+
return false;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (import.meta.main) runMain(NAME, lint);
|
package/scripts/main.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
2
|
+
import { Cause, Console, Effect, Exit, Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
const PASSED = 0;
|
|
5
|
+
const VIOLATED = 1;
|
|
6
|
+
const UNDECIDED = 2;
|
|
7
|
+
const INTERRUPTED = 130;
|
|
8
|
+
|
|
9
|
+
export class Usage extends Schema.TaggedError<Usage>()("Usage", {
|
|
10
|
+
message: Schema.String,
|
|
11
|
+
}) {}
|
|
12
|
+
|
|
13
|
+
function statusOf<A, E>(exit: Exit.Exit<A, E>): number {
|
|
14
|
+
if (Exit.isSuccess(exit)) return typeof exit.value === "number" ? exit.value : UNDECIDED;
|
|
15
|
+
return Cause.hasInterruptsOnly(exit.cause) ? INTERRUPTED : UNDECIDED;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function runMain<E extends { readonly message: string }>(
|
|
19
|
+
name: string,
|
|
20
|
+
passes: Effect.Effect<boolean, E, BunServices.BunServices>,
|
|
21
|
+
): void {
|
|
22
|
+
const program = passes.pipe(
|
|
23
|
+
Effect.map((passed) => (passed ? PASSED : VIOLATED)),
|
|
24
|
+
Effect.catch((failure) => Console.error(`${name}: ${failure.message}`).pipe(Effect.as(UNDECIDED))),
|
|
25
|
+
Effect.provide(BunServices.layer),
|
|
26
|
+
);
|
|
27
|
+
BunRuntime.runMain(program, { teardown: (exit, onExit) => onExit(statusOf(exit)) });
|
|
28
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { Console, Effect, FileSystem, Schema } from "effect";
|
|
3
|
+
import { runMain, Usage } from "./main.ts";
|
|
3
4
|
|
|
4
5
|
export type Tally = {
|
|
5
6
|
readonly killed: number;
|
|
@@ -25,42 +26,31 @@ export type Options = {
|
|
|
25
26
|
readonly advisory: boolean;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
export class ReportError extends
|
|
29
|
+
export class ReportError extends Schema.TaggedError<ReportError>()("ReportError", {
|
|
30
|
+
message: Schema.String,
|
|
31
|
+
}) {}
|
|
29
32
|
|
|
30
33
|
const DETECTED = new Set(["Killed", "Timeout"]);
|
|
31
34
|
const UNDETECTED = new Set(["Survived", "NoCoverage"]);
|
|
32
35
|
const USAGE = "usage: mutation-compare.ts [--advisory] <base-report> <head-report>";
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
throw new ReportError(`mutation-compare: ${path} has no mutants list`);
|
|
52
|
-
}
|
|
53
|
-
const statuses: string[] = [];
|
|
54
|
-
for (const mutant of entry["mutants"]) {
|
|
55
|
-
if (!isRecord(mutant) || typeof mutant["status"] !== "string") {
|
|
56
|
-
throw new ReportError(`mutation-compare: ${path} carries a mutant without a status`);
|
|
57
|
-
}
|
|
58
|
-
statuses.push(mutant["status"]);
|
|
59
|
-
}
|
|
60
|
-
files.set(path, statuses);
|
|
61
|
-
}
|
|
62
|
-
return files;
|
|
63
|
-
}
|
|
37
|
+
const Report = Schema.fromJsonString(
|
|
38
|
+
Schema.Struct({
|
|
39
|
+
files: Schema.Record(
|
|
40
|
+
Schema.String,
|
|
41
|
+
Schema.Struct({ mutants: Schema.Array(Schema.Struct({ status: Schema.String })) }),
|
|
42
|
+
),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
const decodeReport = Schema.decodeUnknownEffect(Report);
|
|
46
|
+
|
|
47
|
+
export const parseReport = (source: string, text: string): Effect.Effect<Map<string, readonly string[]>, ReportError> =>
|
|
48
|
+
decodeReport(text).pipe(
|
|
49
|
+
Effect.map(
|
|
50
|
+
({ files }) => new Map(Object.entries(files).map(([path, { mutants }]) => [path, mutants.map(({ status }) => status)])),
|
|
51
|
+
),
|
|
52
|
+
Effect.mapError((cause) => new ReportError({ message: `${source} is not a Stryker mutation report: ${cause.message}` })),
|
|
53
|
+
);
|
|
64
54
|
|
|
65
55
|
function tally(statuses: readonly string[]): Tally {
|
|
66
56
|
let killed = 0;
|
|
@@ -136,38 +126,34 @@ export function formatComparison(comparison: Comparison, advisory: boolean): str
|
|
|
136
126
|
return lines.join("\n");
|
|
137
127
|
}
|
|
138
128
|
|
|
139
|
-
export
|
|
129
|
+
export const parseArgs = Effect.fnUntraced(function* (argv: readonly string[]): Effect.fn.Return<Options, Usage> {
|
|
140
130
|
let advisory = false;
|
|
141
131
|
const paths: string[] = [];
|
|
142
132
|
for (const arg of argv) {
|
|
143
133
|
if (arg === "--advisory") advisory = true;
|
|
144
|
-
else if (arg.startsWith("--"))
|
|
134
|
+
else if (arg.startsWith("--")) return yield* new Usage({ message: `${USAGE}: unknown flag ${arg}` });
|
|
145
135
|
else paths.push(arg);
|
|
146
136
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
137
|
+
const [basePath, headPath, ...extra] = paths;
|
|
138
|
+
if (basePath === undefined || headPath === undefined || extra.length > 0) return yield* new Usage({ message: USAGE });
|
|
139
|
+
return { basePath, headPath, advisory };
|
|
140
|
+
});
|
|
150
141
|
|
|
151
|
-
export function
|
|
152
|
-
return comparison.regression
|
|
142
|
+
export function passes(comparison: Comparison, advisory: boolean): boolean {
|
|
143
|
+
return advisory || !comparison.regression;
|
|
153
144
|
}
|
|
154
145
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
}
|
|
146
|
+
const load = Effect.fn("load")(function* (path: string) {
|
|
147
|
+
const fs = yield* FileSystem.FileSystem;
|
|
148
|
+
const text = yield* fs.readFileString(path).pipe(Effect.mapError(() => new ReportError({ message: `cannot read ${path}` })));
|
|
149
|
+
return yield* parseReport(path, text);
|
|
150
|
+
});
|
|
162
151
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
process.exit(2);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
152
|
+
const compare = Effect.gen(function* () {
|
|
153
|
+
const options = yield* parseArgs(process.argv.slice(2));
|
|
154
|
+
const comparison = compareReports(yield* load(options.basePath), yield* load(options.headPath));
|
|
155
|
+
yield* Console.log(formatComparison(comparison, options.advisory));
|
|
156
|
+
return passes(comparison, options.advisory);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (import.meta.main) runMain("mutation-compare", compare);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, Schema } from "effect";
|
|
3
|
+
import { git } from "./git.ts";
|
|
4
|
+
import { runMain, Usage } from "./main.ts";
|
|
5
|
+
|
|
2
6
|
export const SUPPRESSIONS = "oxlint-suppressions.json";
|
|
3
7
|
|
|
4
8
|
export type Suppressions = ReadonlyMap<string, ReadonlyMap<string, number>>;
|
|
@@ -13,44 +17,37 @@ export type Ratchet = {
|
|
|
13
17
|
readonly rises: readonly Rise[];
|
|
14
18
|
};
|
|
15
19
|
|
|
16
|
-
export class SuppressionsError extends
|
|
20
|
+
export class SuppressionsError extends Schema.TaggedError<SuppressionsError>()("SuppressionsError", {
|
|
21
|
+
message: Schema.String,
|
|
22
|
+
}) {}
|
|
17
23
|
|
|
18
24
|
const USAGE = "usage: suppressions-ratchet.ts <ref> | <base-ref> <head-ref>";
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
if (!isRecord(parsed)) {
|
|
36
|
-
throw new SuppressionsError(`suppressions-ratchet: ${where} is not an object of files`);
|
|
37
|
-
}
|
|
38
|
-
const files = new Map<string, ReadonlyMap<string, number>>();
|
|
39
|
-
for (const [file, rules] of Object.entries(parsed)) {
|
|
40
|
-
if (!isRecord(rules)) {
|
|
41
|
-
throw new SuppressionsError(`suppressions-ratchet: ${where} holds ${file} without an object of rules`);
|
|
42
|
-
}
|
|
26
|
+
const parseJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
|
|
27
|
+
const decodeObject = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
|
|
28
|
+
const decodeEntry = Schema.decodeUnknownEffect(Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) }));
|
|
29
|
+
|
|
30
|
+
export const parseSuppressions = Effect.fn("parseSuppressions")(function* (
|
|
31
|
+
text: string,
|
|
32
|
+
where: string,
|
|
33
|
+
): Effect.fn.Return<Suppressions, SuppressionsError> {
|
|
34
|
+
const refuse = (reason: string) => () => new SuppressionsError({ message: `${where} ${reason}` });
|
|
35
|
+
const parsed = yield* parseJson(text).pipe(Effect.mapError(refuse("is not valid JSON")));
|
|
36
|
+
const files = yield* decodeObject(parsed).pipe(Effect.mapError(refuse("is not an object of files")));
|
|
37
|
+
const suppressions = new Map<string, ReadonlyMap<string, number>>();
|
|
38
|
+
for (const [file, rules] of Object.entries(files)) {
|
|
39
|
+
const entries = yield* decodeObject(rules).pipe(Effect.mapError(refuse(`holds ${file} without an object of rules`)));
|
|
43
40
|
const counts = new Map<string, number>();
|
|
44
|
-
for (const [rule, entry] of Object.entries(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
counts.set(rule,
|
|
41
|
+
for (const [rule, entry] of Object.entries(entries)) {
|
|
42
|
+
const { count } = yield* decodeEntry(entry).pipe(
|
|
43
|
+
Effect.mapError(refuse(`holds ${file} ${rule} without a whole count`)),
|
|
44
|
+
);
|
|
45
|
+
counts.set(rule, count);
|
|
49
46
|
}
|
|
50
|
-
|
|
47
|
+
suppressions.set(file, counts);
|
|
51
48
|
}
|
|
52
|
-
return
|
|
53
|
-
}
|
|
49
|
+
return suppressions;
|
|
50
|
+
});
|
|
54
51
|
|
|
55
52
|
export function compareSuppressions(base: Suppressions, head: Suppressions): Ratchet {
|
|
56
53
|
const rises: Rise[] = [];
|
|
@@ -91,52 +88,38 @@ export function report({ counted, lowered, rises }: Ratchet): string {
|
|
|
91
88
|
].join("\n");
|
|
92
89
|
}
|
|
93
90
|
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return git("
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
if (!result.success) die(`suppressions-ratchet: ${base} and ${head} share no commit`);
|
|
114
|
-
return result.stdout.toString().trim();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function suppressionsAt(commit: string): Suppressions {
|
|
118
|
-
const blob = git("ls-tree", "--object-only", commit, "--", SUPPRESSIONS).trim();
|
|
119
|
-
if (blob === "") return new Map();
|
|
120
|
-
try {
|
|
121
|
-
return parseSuppressions(git("cat-file", "blob", blob), `${SUPPRESSIONS} at ${commit}`);
|
|
122
|
-
} catch (error) {
|
|
123
|
-
return die(error instanceof SuppressionsError ? error.message : `suppressions-ratchet: ${String(error)}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function runRange(base: string, head: string): Ratchet {
|
|
128
|
-
const headCommit = commitOf(head);
|
|
91
|
+
const commitOf = Effect.fn("commitOf")(function* (rev: string) {
|
|
92
|
+
return (yield* git(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const mergeBase = Effect.fn("mergeBase")(function* (base: string, head: string) {
|
|
96
|
+
const commit = yield* git(["merge-base", base, head]).pipe(
|
|
97
|
+
Effect.mapError(() => new SuppressionsError({ message: `${base} and ${head} share no commit` })),
|
|
98
|
+
);
|
|
99
|
+
return commit.trim();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const suppressionsAt = Effect.fn("suppressionsAt")(function* (commit: string) {
|
|
103
|
+
const blob = (yield* git(["ls-tree", "--object-only", commit, "--", SUPPRESSIONS])).trim();
|
|
104
|
+
if (blob === "") return new Map() satisfies Suppressions;
|
|
105
|
+
return yield* parseSuppressions(yield* git(["cat-file", "blob", blob]), `${SUPPRESSIONS} at ${commit}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const runRange = Effect.fn("runRange")(function* (base: string, head: string) {
|
|
109
|
+
const headCommit = yield* commitOf(head);
|
|
129
110
|
// At the base tip, a count the base branch lowered after the head branched off would read as a rise at the head.
|
|
130
|
-
const branchPoint = mergeBase(commitOf(base), headCommit);
|
|
131
|
-
return compareSuppressions(suppressionsAt(branchPoint), suppressionsAt(headCommit));
|
|
132
|
-
}
|
|
111
|
+
const branchPoint = yield* mergeBase(yield* commitOf(base), headCommit);
|
|
112
|
+
return compareSuppressions(yield* suppressionsAt(branchPoint), yield* suppressionsAt(headCommit));
|
|
113
|
+
});
|
|
133
114
|
|
|
134
|
-
|
|
115
|
+
const ratchet = Effect.gen(function* () {
|
|
135
116
|
const [first, second, ...extra] = process.argv.slice(2);
|
|
136
|
-
if (first === undefined || extra.length > 0)
|
|
117
|
+
if (first === undefined || extra.length > 0) return yield* new Usage({ message: USAGE });
|
|
137
118
|
|
|
138
|
-
const
|
|
119
|
+
const result = second !== undefined ? yield* runRange(first, second) : yield* runRange(`${first}^`, first);
|
|
139
120
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
121
|
+
yield* Console.log(report(result));
|
|
122
|
+
return result.rises.length === 0;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (import.meta.main) runMain("suppressions-ratchet", ratchet);
|