@trofimyukdev/repo-truth 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 +21 -0
- package/README.md +273 -2
- package/dist/action.d.ts +52 -0
- package/dist/action.js +182 -0
- package/dist/action.js.map +1 -0
- package/dist/checks/action-pins.d.ts +52 -0
- package/dist/checks/action-pins.js +196 -0
- package/dist/checks/action-pins.js.map +1 -0
- package/dist/checks/autosquash.d.ts +23 -0
- package/dist/checks/autosquash.js +85 -0
- package/dist/checks/autosquash.js.map +1 -0
- package/dist/checks/bidi-controls.d.ts +58 -0
- package/dist/checks/bidi-controls.js +172 -0
- package/dist/checks/bidi-controls.js.map +1 -0
- package/dist/checks/binary-files.d.ts +53 -0
- package/dist/checks/binary-files.js +157 -0
- package/dist/checks/binary-files.js.map +1 -0
- package/dist/checks/config-changes.d.ts +57 -0
- package/dist/checks/config-changes.js +175 -0
- package/dist/checks/config-changes.js.map +1 -0
- package/dist/checks/credentials.d.ts +35 -0
- package/dist/checks/credentials.js +138 -0
- package/dist/checks/credentials.js.map +1 -0
- package/dist/checks/install-scripts.d.ts +39 -0
- package/dist/checks/install-scripts.js +173 -0
- package/dist/checks/install-scripts.js.map +1 -0
- package/dist/checks/lint-rules.d.ts +49 -0
- package/dist/checks/lint-rules.js +184 -0
- package/dist/checks/lint-rules.js.map +1 -0
- package/dist/checks/path-policy.d.ts +40 -0
- package/dist/checks/path-policy.js +195 -0
- package/dist/checks/path-policy.js.map +1 -0
- package/dist/checks/raised-timeouts.d.ts +66 -0
- package/dist/checks/raised-timeouts.js +248 -0
- package/dist/checks/raised-timeouts.js.map +1 -0
- package/dist/checks/script-changes.d.ts +51 -0
- package/dist/checks/script-changes.js +199 -0
- package/dist/checks/script-changes.js.map +1 -0
- package/dist/checks/secret-files.d.ts +58 -0
- package/dist/checks/secret-files.js +165 -0
- package/dist/checks/secret-files.js.map +1 -0
- package/dist/checks/strictness.d.ts +59 -0
- package/dist/checks/strictness.js +260 -0
- package/dist/checks/strictness.js.map +1 -0
- package/dist/checks/submodules.d.ts +51 -0
- package/dist/checks/submodules.js +172 -0
- package/dist/checks/submodules.js.map +1 -0
- package/dist/checks/suppressions.d.ts +53 -0
- package/dist/checks/suppressions.js +203 -0
- package/dist/checks/suppressions.js.map +1 -0
- package/dist/checks/workflow-permissions.d.ts +56 -0
- package/dist/checks/workflow-permissions.js +237 -0
- package/dist/checks/workflow-permissions.js.map +1 -0
- package/dist/checks/workflow-weakening.d.ts +38 -0
- package/dist/checks/workflow-weakening.js +172 -0
- package/dist/checks/workflow-weakening.js.map +1 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +300 -0
- package/dist/cli.js.map +1 -0
- package/dist/commit-range.d.ts +28 -0
- package/dist/commit-range.js +152 -0
- package/dist/commit-range.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +235 -0
- package/dist/config.js.map +1 -0
- package/dist/conflict-markers.d.ts +40 -0
- package/dist/conflict-markers.js +234 -0
- package/dist/conflict-markers.js.map +1 -0
- package/dist/dependency-sources.d.ts +32 -0
- package/dist/dependency-sources.js +260 -0
- package/dist/dependency-sources.js.map +1 -0
- package/dist/figure-claims.d.ts +35 -0
- package/dist/figure-claims.js +139 -0
- package/dist/figure-claims.js.map +1 -0
- package/dist/figure-text.d.ts +22 -0
- package/dist/figure-text.js +119 -0
- package/dist/figure-text.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/large-blobs.d.ts +35 -0
- package/dist/large-blobs.js +163 -0
- package/dist/large-blobs.js.map +1 -0
- package/dist/lockfile-drift.d.ts +36 -0
- package/dist/lockfile-drift.js +206 -0
- package/dist/lockfile-drift.js.map +1 -0
- package/dist/lowered-thresholds.d.ts +70 -0
- package/dist/lowered-thresholds.js +281 -0
- package/dist/lowered-thresholds.js.map +1 -0
- package/dist/measurement-rule.d.ts +39 -0
- package/dist/measurement-rule.js +213 -0
- package/dist/measurement-rule.js.map +1 -0
- package/dist/portable-paths.d.ts +37 -0
- package/dist/portable-paths.js +215 -0
- package/dist/portable-paths.js.map +1 -0
- package/dist/registry.d.ts +43 -0
- package/dist/registry.js +144 -0
- package/dist/registry.js.map +1 -0
- package/dist/replay-plan.d.ts +36 -0
- package/dist/replay-plan.js +196 -0
- package/dist/replay-plan.js.map +1 -0
- package/dist/replayed-figures.d.ts +36 -0
- package/dist/replayed-figures.js +159 -0
- package/dist/replayed-figures.js.map +1 -0
- package/dist/snapshot-refreshes.d.ts +36 -0
- package/dist/snapshot-refreshes.js +202 -0
- package/dist/snapshot-refreshes.js.map +1 -0
- package/dist/stray-files.d.ts +52 -0
- package/dist/stray-files.js +234 -0
- package/dist/stray-files.js.map +1 -0
- package/dist/symlinks.d.ts +28 -0
- package/dist/symlinks.js +193 -0
- package/dist/symlinks.js.map +1 -0
- package/dist/trailers.d.ts +36 -0
- package/dist/trailers.js +110 -0
- package/dist/trailers.js.map +1 -0
- package/dist/weakened-tests.d.ts +94 -0
- package/dist/weakened-tests.js +458 -0
- package/dist/weakened-tests.js.map +1 -0
- package/package.json +31 -4
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The measurement rule.
|
|
3
|
+
*
|
|
4
|
+
* "A number without a command and a date is not a fact." This check reads the
|
|
5
|
+
* bodies of the commits in `base..candidate` and the lines the range adds to
|
|
6
|
+
* changed `.md` files, finds the author's own sentences that assert a
|
|
7
|
+
* measured quantity, and reports the ones whose BLOCK holds no command or no
|
|
8
|
+
* date.
|
|
9
|
+
*
|
|
10
|
+
* A BLOCK is the whole body of one commit (the message without its subject
|
|
11
|
+
* line), or one paragraph of a document - the lines between two blank lines,
|
|
12
|
+
* a fenced block never split. A COMMAND is an inline code span or a fenced
|
|
13
|
+
* block in the block; a DATE is an ISO date in it.
|
|
14
|
+
*
|
|
15
|
+
* A CLAIM is a numeral that carries a unit of measure (`12 ms`, `40%`,
|
|
16
|
+
* `3 files`, `2x`). Digits alone are not a claim: a version, a SHA, an ISO
|
|
17
|
+
* date, a section or figure number, a line reference, or a path containing
|
|
18
|
+
* digits has no unit and is never matched. Lines inside a fenced block, lines
|
|
19
|
+
* beginning with `>`, and text inside an inline code span are not the
|
|
20
|
+
* author's own prose and are never read as claims.
|
|
21
|
+
*
|
|
22
|
+
* Git is always run with an argument vector, and every failure is a `fail`
|
|
23
|
+
* record - never a `pass`, never a throw.
|
|
24
|
+
*/
|
|
25
|
+
import { execFile } from "node:child_process";
|
|
26
|
+
const NAME = "RT-02";
|
|
27
|
+
function runGit(cwd, args) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
30
|
+
if (error) {
|
|
31
|
+
const detail = stderr.trim();
|
|
32
|
+
reject(new Error(detail.length > 0 ? detail : error.message));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
resolve(stdout);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Units that turn a numeral into a measured quantity. */
|
|
40
|
+
const UNITS = [
|
|
41
|
+
"ms", "msec", "msecs", "millisecond", "milliseconds", "s", "sec", "secs", "second", "seconds",
|
|
42
|
+
"min", "mins", "minute", "minutes", "h", "hr", "hrs", "hour", "hours", "days",
|
|
43
|
+
"b", "kb", "mb", "gb", "tb", "kib", "mib", "gib", "byte", "bytes",
|
|
44
|
+
"fps", "rps", "loc", "percent",
|
|
45
|
+
"test", "tests", "case", "cases", "file", "files", "line", "lines", "commit", "commits",
|
|
46
|
+
"failure", "failures", "failing", "passing", "error", "errors", "warning", "warnings",
|
|
47
|
+
"check", "checks", "request", "requests", "call", "calls", "time", "times", "run", "runs",
|
|
48
|
+
"sample", "samples", "iteration", "iterations", "module", "modules", "function", "functions",
|
|
49
|
+
"violation", "violations", "hit", "hits", "match", "matches", "regression", "regressions",
|
|
50
|
+
];
|
|
51
|
+
/** A numeral not glued to a version, path, reference or identifier, then a unit. */
|
|
52
|
+
const CLAIM = new RegExp("(?<![\\w./#:@$-])(\\d[\\d,]*(?:\\.\\d+)?)(?:\\s?%|x(?!\\w)|×|\\s+(?:" +
|
|
53
|
+
UNITS.join("|") +
|
|
54
|
+
")(?![\\w/-]|\\.\\w))", "i");
|
|
55
|
+
const ISO_DATE = /(?<![\d-])\d{4}-\d{2}-\d{2}(?![\d-])/;
|
|
56
|
+
const CODE_SPAN = /(`+)[^`\n]+?\1/;
|
|
57
|
+
const CODE_SPAN_ALL = /(`+)[^`\n]+?\1/g;
|
|
58
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
59
|
+
function annotate(source) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
let marker;
|
|
62
|
+
source.split("\n").forEach((raw, index) => {
|
|
63
|
+
const text = raw.replace(/\r$/, "");
|
|
64
|
+
const fence = FENCE.exec(text);
|
|
65
|
+
let fenced = marker !== undefined;
|
|
66
|
+
if (marker === undefined) {
|
|
67
|
+
if (fence) {
|
|
68
|
+
marker = fence[1];
|
|
69
|
+
fenced = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (fence?.[1] !== undefined && fence[1][0] === marker[0] && fence[1].length >= marker.length && text.trim() === fence[1]) {
|
|
73
|
+
marker = undefined;
|
|
74
|
+
}
|
|
75
|
+
lines.push({ number: index + 1, text, fenced });
|
|
76
|
+
});
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
/** Split into blocks at blank lines outside fences. */
|
|
80
|
+
function paragraphs(lines) {
|
|
81
|
+
const blocks = [];
|
|
82
|
+
let current = [];
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (!line.fenced && line.text.trim() === "") {
|
|
85
|
+
if (current.length > 0)
|
|
86
|
+
blocks.push(current);
|
|
87
|
+
current = [];
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
current.push(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (current.length > 0)
|
|
94
|
+
blocks.push(current);
|
|
95
|
+
return blocks;
|
|
96
|
+
}
|
|
97
|
+
function claimIn(line) {
|
|
98
|
+
if (line.fenced || /^\s*>/.test(line.text))
|
|
99
|
+
return undefined;
|
|
100
|
+
const prose = line.text
|
|
101
|
+
.replace(CODE_SPAN_ALL, " ")
|
|
102
|
+
.replace(/\]\([^)]*\)/g, "]")
|
|
103
|
+
.replace(/https?:\/\/\S+/g, " ");
|
|
104
|
+
return CLAIM.exec(prose)?.[0].trim();
|
|
105
|
+
}
|
|
106
|
+
function missingHalf(block) {
|
|
107
|
+
const text = block.map((line) => line.text).join("\n");
|
|
108
|
+
const hasCommand = block.some((line) => line.fenced) || CODE_SPAN.test(text);
|
|
109
|
+
const hasDate = ISO_DATE.test(text);
|
|
110
|
+
if (hasCommand && hasDate)
|
|
111
|
+
return undefined;
|
|
112
|
+
if (!hasCommand && !hasDate)
|
|
113
|
+
return "command and date";
|
|
114
|
+
return hasCommand ? "date" : "command";
|
|
115
|
+
}
|
|
116
|
+
/** Violations in one block; `wanted` limits which lines count as the range's own. */
|
|
117
|
+
function violationsIn(block, wanted, where) {
|
|
118
|
+
const claims = block
|
|
119
|
+
.filter((line) => (wanted ? wanted(line) : true))
|
|
120
|
+
.flatMap((line) => {
|
|
121
|
+
const figure = claimIn(line);
|
|
122
|
+
return figure === undefined ? [] : [{ line, figure }];
|
|
123
|
+
});
|
|
124
|
+
if (claims.length === 0)
|
|
125
|
+
return [];
|
|
126
|
+
const missing = missingHalf(block);
|
|
127
|
+
if (missing === undefined)
|
|
128
|
+
return [];
|
|
129
|
+
return claims.map(({ line, figure }) => `${where(line)}: figure "${figure}" is stated without its ${missing}`);
|
|
130
|
+
}
|
|
131
|
+
async function commitEvidence(cwd, base, candidate) {
|
|
132
|
+
const listing = await runGit(cwd, ["rev-list", "--reverse", `${base}..${candidate}`, "--"]);
|
|
133
|
+
const evidence = [];
|
|
134
|
+
for (const sha of listing.split("\n").filter((l) => l.length > 0)) {
|
|
135
|
+
const message = (await runGit(cwd, ["show", "-s", "--format=%B", sha])).replace(/\n+$/, "");
|
|
136
|
+
// The body is everything after the subject line; numbering is body-relative.
|
|
137
|
+
const lines = annotate(message.split("\n").slice(1).join("\n"));
|
|
138
|
+
evidence.push(...violationsIn(lines, undefined, (line) => `${sha.slice(0, 7)} body line ${line.number}`));
|
|
139
|
+
}
|
|
140
|
+
return evidence;
|
|
141
|
+
}
|
|
142
|
+
function addedLines(diff) {
|
|
143
|
+
const added = new Set();
|
|
144
|
+
for (const match of diff.matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)) {
|
|
145
|
+
const start = Number(match[1]);
|
|
146
|
+
const count = match[2] === undefined ? 1 : Number(match[2]);
|
|
147
|
+
for (let i = 0; i < count; i++)
|
|
148
|
+
added.add(start + i);
|
|
149
|
+
}
|
|
150
|
+
return added;
|
|
151
|
+
}
|
|
152
|
+
async function documentEvidence(cwd, base, candidate) {
|
|
153
|
+
const span = `${base}...${candidate}`;
|
|
154
|
+
const names = await runGit(cwd, ["diff", "--name-only", "-z", "--diff-filter=d", span, "--"]);
|
|
155
|
+
const evidence = [];
|
|
156
|
+
for (const path of names.split("\0").filter((n) => n.endsWith(".md"))) {
|
|
157
|
+
const diff = await runGit(cwd, ["diff", "--unified=0", "--no-color", "--no-ext-diff", span, "--", path]);
|
|
158
|
+
const added = addedLines(diff);
|
|
159
|
+
if (added.size === 0)
|
|
160
|
+
continue;
|
|
161
|
+
const content = await runGit(cwd, ["show", `${candidate}:${path}`]);
|
|
162
|
+
for (const block of paragraphs(annotate(content))) {
|
|
163
|
+
if (!block.some((line) => added.has(line.number)))
|
|
164
|
+
continue;
|
|
165
|
+
evidence.push(...violationsIn(block, (line) => added.has(line.number), (line) => `${path}:${line.number}`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return evidence;
|
|
169
|
+
}
|
|
170
|
+
async function resolvesToCommit(cwd, revision) {
|
|
171
|
+
if (revision.length === 0 || revision.startsWith("-"))
|
|
172
|
+
return false;
|
|
173
|
+
try {
|
|
174
|
+
await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`]);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Check the commit bodies in `base..candidate` and the lines the range adds
|
|
183
|
+
* to `.md` files. Fails closed on any git failure.
|
|
184
|
+
*/
|
|
185
|
+
export async function checkMeasurementRule(options) {
|
|
186
|
+
const { cwd, base, candidate } = options;
|
|
187
|
+
try {
|
|
188
|
+
for (const revision of [base, candidate]) {
|
|
189
|
+
if (!(await resolvesToCommit(cwd, revision))) {
|
|
190
|
+
return { name: NAME, status: "fail", evidence: [`revision ${revision} names no commit`] };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const evidence = [
|
|
194
|
+
...(await commitEvidence(cwd, base, candidate)),
|
|
195
|
+
...(await documentEvidence(cwd, base, candidate)),
|
|
196
|
+
];
|
|
197
|
+
if (evidence.length > 0)
|
|
198
|
+
return { name: NAME, status: "fail", evidence };
|
|
199
|
+
return {
|
|
200
|
+
name: NAME,
|
|
201
|
+
status: "pass",
|
|
202
|
+
evidence: [`no unmeasured figure in the commit bodies or added documentation lines of ${base}..${candidate}`],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return {
|
|
207
|
+
name: NAME,
|
|
208
|
+
status: "fail",
|
|
209
|
+
evidence: [`git failed while reading ${base}..${candidate}: ${error.message}`],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=measurement-rule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measurement-rule.js","sourceRoot":"","sources":["../src/measurement-rule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAI9C,MAAM,IAAI,GAAG,OAAO,CAAC;AAYrB,SAAS,MAAM,CAAC,GAAW,EAAE,IAAuB;IAClD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,KAAK,EACL,IAAgB,EAChB,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EACxD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;gBAC7B,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC9D,OAAO;YACT,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,0DAA0D;AAC1D,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS;IAC7F,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAC7E,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO;IACjE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS;IAC9B,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS;IACvF,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU;IACrF,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM;IACzF,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW;IAC5F,WAAW,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa;CAC1F,CAAC;AAEF,oFAAoF;AACpF,MAAM,KAAK,GAAG,IAAI,MAAM,CACtB,sEAAsE;IACpE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;IACf,sBAAsB,EACxB,GAAG,CACJ,CAAC;AAEF,MAAM,QAAQ,GAAG,sCAAsC,CAAC;AACxD,MAAM,SAAS,GAAG,gBAAgB,CAAC;AACnC,MAAM,aAAa,GAAG,iBAAiB,CAAC;AACxC,MAAM,KAAK,GAAG,sBAAsB,CAAC;AAUrC,SAAS,QAAQ,CAAC,MAAc;IAC9B,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,IAAI,MAA0B,CAAC;IAC/B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;QACxC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,MAAM,GAAG,MAAM,KAAK,SAAS,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBAClB,MAAM,GAAG,IAAI,CAAC;YAChB,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACjI,MAAM,GAAG,SAAS,CAAC;QACrB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACf,CAAC;AAED,uDAAuD;AACvD,SAAS,UAAU,CAAC,KAAsB;IACxC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,OAAO,GAAW,EAAE,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC5C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC7C,OAAO,GAAG,EAAE,CAAC;QACf,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,OAAO,CAAC,IAAU;IACzB,IAAI,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI;SACpB,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACvC,CAAC;AAED,SAAS,WAAW,CAAC,KAAsB;IACzC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,UAAU,IAAI,OAAO;QAAE,OAAO,SAAS,CAAC;IAC5C,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO;QAAE,OAAO,kBAAkB,CAAC;IACvD,OAAO,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACzC,CAAC;AAED,qFAAqF;AACrF,SAAS,YAAY,CACnB,KAAsB,EACtB,MAA6C,EAC7C,KAA6B;IAE7B,MAAM,MAAM,GAAG,KAAK;SACjB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SAChD,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAChB,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7B,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IACL,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,MAAM,2BAA2B,OAAO,EAAE,CAAC,CAAC;AACjH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,GAAW,EAAE,IAAY,EAAE,SAAiB;IACxE,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,WAAW,EAAE,GAAG,IAAI,KAAK,SAAS,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;IAC5F,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QAClE,MAAM,OAAO,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC5F,6EAA6E;QAC7E,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,cAAc,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC5G,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,2CAA2C,CAAC,EAAE,CAAC;QAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,GAAW,EAAE,IAAY,EAAE,SAAiB;IAC1E,MAAM,IAAI,GAAG,GAAG,IAAI,MAAM,SAAS,EAAE,CAAC;IACtC,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAC9F,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACzG,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;YAAE,SAAS;QAC/B,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;QACpE,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YAClD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAAE,SAAS;YAC5D,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC9G,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,GAAW,EAAE,QAAgB;IAC3D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACpE,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,QAAQ,WAAW,CAAC,CAAC,CAAC;QAChF,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAA+B;IACxE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IACzC,IAAI,CAAC;QACH,KAAK,MAAM,QAAQ,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,CAAC,MAAM,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;gBAC7C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,YAAY,QAAQ,kBAAkB,CAAC,EAAE,CAAC;YAC5F,CAAC;QACH,CAAC;QACD,MAAM,QAAQ,GAAG;YACf,GAAG,CAAC,MAAM,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;YAC/C,GAAG,CAAC,MAAM,gBAAgB,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;SAClD,CAAC;QACF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QACzE,OAAO;YACL,IAAI,EAAE,IAAI;YACV,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,CAAC,6EAA6E,IAAI,KAAK,SAAS,EAAE,CAAC;SAC9G,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,IAAI,EAAE,IAAI;YACV,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,CAAC,4BAA4B,IAAI,KAAK,SAAS,KAAM,KAAe,CAAC,OAAO,EAAE,CAAC;SAC1F,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable paths.
|
|
3
|
+
*
|
|
4
|
+
* Linux accepts almost any byte in a file name, so a repository developed
|
|
5
|
+
* there can take in `Readme.md` beside `README.md`, a file called `aux.ts` or
|
|
6
|
+
* a name that ends in a dot, and every check on Linux stays green. A clone on
|
|
7
|
+
* macOS or Windows then silently keeps one of two files, or cannot check the
|
|
8
|
+
* tree out at all. This check reads the names a range introduces and reports
|
|
9
|
+
* the ones another operating system cannot hold.
|
|
10
|
+
*
|
|
11
|
+
* A NAME is the path of a tree entry or of a directory above one. The range
|
|
12
|
+
* INTRODUCES a name when the candidate's tree holds it and the tree of the
|
|
13
|
+
* merge base does not. Names the base already held are never reported by the
|
|
14
|
+
* last three rules, and a case collision is reported only when at least one
|
|
15
|
+
* of its spellings is introduced: a check that fails on names nobody in the
|
|
16
|
+
* range wrote is a check that gets switched off.
|
|
17
|
+
*
|
|
18
|
+
* Both trees are read through git with an argument vector, never from the
|
|
19
|
+
* working tree.
|
|
20
|
+
*/
|
|
21
|
+
import type { CheckRecord } from "./index.js";
|
|
22
|
+
/** Where to look, and which two revisions bound the range. */
|
|
23
|
+
export interface PortablePathsOptions {
|
|
24
|
+
/** The git repository to run in. */
|
|
25
|
+
readonly cwd: string;
|
|
26
|
+
/** The revision the range starts after. */
|
|
27
|
+
readonly base: string;
|
|
28
|
+
/** The revision the range ends at. */
|
|
29
|
+
readonly candidate: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check the names `base..candidate` introduces for the ones another operating
|
|
33
|
+
* system cannot hold. Fails closed: a revision that names no commit, or any
|
|
34
|
+
* git invocation that fails, is a `fail` record naming the cause - never a
|
|
35
|
+
* `pass`, and never a thrown error.
|
|
36
|
+
*/
|
|
37
|
+
export declare function checkPortablePaths(options: PortablePathsOptions): Promise<CheckRecord>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable paths.
|
|
3
|
+
*
|
|
4
|
+
* Linux accepts almost any byte in a file name, so a repository developed
|
|
5
|
+
* there can take in `Readme.md` beside `README.md`, a file called `aux.ts` or
|
|
6
|
+
* a name that ends in a dot, and every check on Linux stays green. A clone on
|
|
7
|
+
* macOS or Windows then silently keeps one of two files, or cannot check the
|
|
8
|
+
* tree out at all. This check reads the names a range introduces and reports
|
|
9
|
+
* the ones another operating system cannot hold.
|
|
10
|
+
*
|
|
11
|
+
* A NAME is the path of a tree entry or of a directory above one. The range
|
|
12
|
+
* INTRODUCES a name when the candidate's tree holds it and the tree of the
|
|
13
|
+
* merge base does not. Names the base already held are never reported by the
|
|
14
|
+
* last three rules, and a case collision is reported only when at least one
|
|
15
|
+
* of its spellings is introduced: a check that fails on names nobody in the
|
|
16
|
+
* range wrote is a check that gets switched off.
|
|
17
|
+
*
|
|
18
|
+
* Both trees are read through git with an argument vector, never from the
|
|
19
|
+
* working tree.
|
|
20
|
+
*/
|
|
21
|
+
import { execFile } from "node:child_process";
|
|
22
|
+
const NAME = "RT-10";
|
|
23
|
+
function runGit(cwd, args) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" }, (error, stdout, stderr) => {
|
|
26
|
+
if (error) {
|
|
27
|
+
const detail = stderr.toString("utf8").trim();
|
|
28
|
+
reject(new Error(detail.length > 0 ? detail : error.message));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
resolve(stdout);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Escape every character outside printable ASCII so a name can be printed to
|
|
37
|
+
* a terminal without replaying whatever control sequence it smuggled in.
|
|
38
|
+
*/
|
|
39
|
+
function escapeText(text) {
|
|
40
|
+
let escaped = "";
|
|
41
|
+
for (const char of text) {
|
|
42
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
43
|
+
if (codePoint >= 0x20 && codePoint <= 0x7e) {
|
|
44
|
+
escaped += char;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
escaped += `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return escaped;
|
|
51
|
+
}
|
|
52
|
+
function quote(text) {
|
|
53
|
+
return `"${escapeText(text)}"`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decode a path as UTF-8; a path that is not valid UTF-8 is decoded byte for
|
|
57
|
+
* byte instead, so that two different byte strings never become one name.
|
|
58
|
+
*/
|
|
59
|
+
function decodePath(bytes) {
|
|
60
|
+
try {
|
|
61
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return bytes.toString("latin1");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Every revision is resolved to a commit before it is used as anything else. */
|
|
68
|
+
async function resolveCommit(cwd, revision) {
|
|
69
|
+
// A revision that begins with a dash would be read as an option.
|
|
70
|
+
if (revision.length === 0 || revision.startsWith("-")) {
|
|
71
|
+
throw new Error(`revision ${quote(revision)} names no commit`);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const out = await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`]);
|
|
75
|
+
const hash = out.toString("utf8").trim();
|
|
76
|
+
if (hash.length === 0) {
|
|
77
|
+
throw new Error("empty answer");
|
|
78
|
+
}
|
|
79
|
+
return hash;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw new Error(`revision ${quote(revision)} names no commit: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function mergeBase(cwd, base, candidate) {
|
|
86
|
+
const label = `${base.slice(0, 12)} and ${candidate.slice(0, 12)}`;
|
|
87
|
+
let out;
|
|
88
|
+
try {
|
|
89
|
+
out = await runGit(cwd, ["merge-base", base, candidate]);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new Error(`no merge base of ${label}: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
const hash = out.toString("utf8").trim();
|
|
95
|
+
if (hash.length === 0) {
|
|
96
|
+
throw new Error(`no merge base of ${label}`);
|
|
97
|
+
}
|
|
98
|
+
return hash;
|
|
99
|
+
}
|
|
100
|
+
/** Every name of a commit's tree: each path and each directory above one. */
|
|
101
|
+
async function readNames(cwd, commit) {
|
|
102
|
+
let out;
|
|
103
|
+
try {
|
|
104
|
+
out = await runGit(cwd, ["ls-tree", "-r", "-z", "--name-only", commit, "--"]);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new Error(`could not read the tree of ${commit.slice(0, 12)}: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
const names = new Set();
|
|
110
|
+
let start = 0;
|
|
111
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
112
|
+
if (out[i] !== 0) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const path = decodePath(out.subarray(start, i));
|
|
116
|
+
start = i + 1;
|
|
117
|
+
if (path.length === 0) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
names.add(path);
|
|
121
|
+
for (let slash = path.indexOf("/"); slash !== -1; slash = path.indexOf("/", slash + 1)) {
|
|
122
|
+
names.add(path.slice(0, slash));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return names;
|
|
126
|
+
}
|
|
127
|
+
const RESERVED = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
128
|
+
// eslint-disable-next-line no-control-regex
|
|
129
|
+
const FORBIDDEN_CHARACTER = /[<>:"\\|?*\u0000-\u001f\u007f-\u009f]/;
|
|
130
|
+
function lastComponent(name) {
|
|
131
|
+
return name.slice(name.lastIndexOf("/") + 1);
|
|
132
|
+
}
|
|
133
|
+
/** The rules that judge one introduced name by itself. */
|
|
134
|
+
function judge(name) {
|
|
135
|
+
const component = lastComponent(name);
|
|
136
|
+
const findings = [];
|
|
137
|
+
if (RESERVED.test(component.split(".")[0] ?? "")) {
|
|
138
|
+
findings.push(`${quote(name)} is a reserved device name on Windows [reserved]`);
|
|
139
|
+
}
|
|
140
|
+
if (component.endsWith(".") || component.endsWith(" ")) {
|
|
141
|
+
findings.push(`${quote(name)} ends in a dot or a space, which Windows drops [trailing]`);
|
|
142
|
+
}
|
|
143
|
+
if (FORBIDDEN_CHARACTER.test(component)) {
|
|
144
|
+
findings.push(`${quote(name)} holds a character Windows forbids in a name [character]`);
|
|
145
|
+
}
|
|
146
|
+
return findings;
|
|
147
|
+
}
|
|
148
|
+
/** Names that differ only in letter case, at least one of them introduced. */
|
|
149
|
+
function caseFindings(candidateNames, introduced) {
|
|
150
|
+
const groups = new Map();
|
|
151
|
+
for (const name of candidateNames) {
|
|
152
|
+
const key = name.toLowerCase();
|
|
153
|
+
const group = groups.get(key);
|
|
154
|
+
if (group === undefined) {
|
|
155
|
+
groups.set(key, [name]);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
group.push(name);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const findings = [];
|
|
162
|
+
for (const group of groups.values()) {
|
|
163
|
+
if (group.length < 2 || !group.some((name) => introduced.has(name))) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
group.sort();
|
|
167
|
+
findings.push(`${group.map(quote).join(" and ")} differ only in letter case [case]`);
|
|
168
|
+
}
|
|
169
|
+
return findings;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check the names `base..candidate` introduces for the ones another operating
|
|
173
|
+
* system cannot hold. Fails closed: a revision that names no commit, or any
|
|
174
|
+
* git invocation that fails, is a `fail` record naming the cause - never a
|
|
175
|
+
* `pass`, and never a thrown error.
|
|
176
|
+
*/
|
|
177
|
+
export async function checkPortablePaths(options) {
|
|
178
|
+
try {
|
|
179
|
+
const { cwd, base, candidate } = options;
|
|
180
|
+
const baseCommit = await resolveCommit(cwd, base);
|
|
181
|
+
const candidateCommit = await resolveCommit(cwd, candidate);
|
|
182
|
+
const ancestor = await mergeBase(cwd, baseCommit, candidateCommit);
|
|
183
|
+
const [heldByBase, candidateNames] = await Promise.all([
|
|
184
|
+
readNames(cwd, ancestor),
|
|
185
|
+
readNames(cwd, candidateCommit),
|
|
186
|
+
]);
|
|
187
|
+
const introduced = new Set();
|
|
188
|
+
for (const name of candidateNames) {
|
|
189
|
+
if (!heldByBase.has(name)) {
|
|
190
|
+
introduced.add(name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const evidence = caseFindings(candidateNames, introduced);
|
|
194
|
+
for (const name of [...introduced].sort()) {
|
|
195
|
+
evidence.push(...judge(name));
|
|
196
|
+
}
|
|
197
|
+
if (evidence.length > 0) {
|
|
198
|
+
return { name: NAME, status: "fail", evidence };
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
name: NAME,
|
|
202
|
+
status: "pass",
|
|
203
|
+
evidence: [
|
|
204
|
+
introduced.size === 0
|
|
205
|
+
? `no name introduced in ${quote(base)}..${quote(candidate)}`
|
|
206
|
+
: `${introduced.size} name(s) introduced in ${quote(base)}..${quote(candidate)}, all portable`,
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
return { name: NAME, status: "fail", evidence: [escapeText(message)] };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=portable-paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-paths.js","sourceRoot":"","sources":["../src/portable-paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry of checks `repo-truth check` runs.
|
|
3
|
+
*
|
|
4
|
+
* This array is data the command reads when it runs; the command holds no list
|
|
5
|
+
* of its own. Adding a check means adding an entry here and nothing else.
|
|
6
|
+
*/
|
|
7
|
+
import type { CheckRecord } from "./index.js";
|
|
8
|
+
import type { OptionKind, OptionKinds } from "./config.js";
|
|
9
|
+
/** The repository a check runs in. */
|
|
10
|
+
export interface Repository {
|
|
11
|
+
/** The working directory git runs in. */
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
/** The path --config named, as it was given; absent when the settings were read from repo-truth.json at the base. */
|
|
14
|
+
readonly config?: string;
|
|
15
|
+
}
|
|
16
|
+
/** The range a check answers for, as resolved commit ids. */
|
|
17
|
+
export interface ResolvedRange {
|
|
18
|
+
/** The commit the range starts after (exclusive). */
|
|
19
|
+
readonly base: string;
|
|
20
|
+
/** The commit the range ends at (inclusive). */
|
|
21
|
+
readonly candidate: string;
|
|
22
|
+
}
|
|
23
|
+
/** The options a check takes, from the name of each to its kind. */
|
|
24
|
+
export type EntryOptions = {
|
|
25
|
+
readonly [option: string]: OptionKind;
|
|
26
|
+
};
|
|
27
|
+
export interface RegistryEntry {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
/** The options the check takes; absent on an entry written by hand that takes none. */
|
|
30
|
+
readonly options?: EntryOptions;
|
|
31
|
+
readonly run: (repo: Repository, range: ResolvedRange, options?: {
|
|
32
|
+
readonly [option: string]: unknown;
|
|
33
|
+
}) => CheckRecord | Promise<CheckRecord>;
|
|
34
|
+
}
|
|
35
|
+
/** The options of every entry given, by entry name, in the form parseConfig reads. */
|
|
36
|
+
export declare function optionKindsOf(entries: readonly RegistryEntry[]): OptionKinds;
|
|
37
|
+
/**
|
|
38
|
+
* The entries for the check modules directly in one directory, in the order of
|
|
39
|
+
* their file names. A module that cannot be registered becomes an entry whose
|
|
40
|
+
* record fails and names the file and the cause. A missing directory yields none.
|
|
41
|
+
*/
|
|
42
|
+
export declare function loadCheckDirectory(directory: URL, extension: string): Promise<RegistryEntry[]>;
|
|
43
|
+
export declare const REGISTRY: RegistryEntry[];
|