mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
214 lines
8.5 KiB
JavaScript
214 lines
8.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/* Lists the numbers that open pull requests claim in the repository's
|
|
* numbered registries, and reports the ones two changes claim differently:
|
|
*
|
|
* diagnostic codes a code an added line names that origin/main has no
|
|
* row for in packages/compiler/src/diagnostics/codes.ts
|
|
* dyn handle tags an added pinned row X(NAME, value) in
|
|
* packages/runtime/src/scr_dyn.h
|
|
*
|
|
* The current branch (its changes since it left origin/main) counts as one
|
|
* more claimant. Run it before picking a number, or before pushing a change
|
|
* that takes one; it needs `gh` access to the repository.
|
|
*
|
|
* node scripts/registry-claims.mjs every claim and every collision
|
|
* node scripts/registry-claims.mjs --check only collisions with this branch;
|
|
* exits 1 when there is one
|
|
*
|
|
* A pull request whose diff cannot be read is named in the output, and the
|
|
* run exits 2 (unless it found a collision), since its claims are missing. */
|
|
import { execFile, execFileSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import { promisify } from "node:util";
|
|
|
|
const run = promisify(execFile);
|
|
const check = process.argv.includes("--check");
|
|
const git = (...args) =>
|
|
execFileSync("git", args, {
|
|
encoding: "utf8",
|
|
maxBuffer: 1 << 30,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
const CODES = "packages/compiler/src/diagnostics/codes.ts";
|
|
const DYN = "packages/runtime/src/scr_dyn.h";
|
|
|
|
git("fetch", "-q", "origin", "main");
|
|
// The codes origin/main has: its active and retired rows (a reserved code is
|
|
// still a claim), or, before codes.ts existed there, every code its sources name.
|
|
const baseCodes = new Map();
|
|
const codesSource = (() => {
|
|
try {
|
|
return git("show", `origin/main:${CODES}`);
|
|
} catch {
|
|
return null;
|
|
}
|
|
})();
|
|
if (codesSource !== null) {
|
|
const reserved = codesSource.indexOf("export const RESERVED_DIAGNOSTIC_CODES");
|
|
for (const m of codesSource.matchAll(/^ {2}(SC\d{4}): "([^"]*)"/gm)) {
|
|
if (reserved < 0 || m.index < reserved) baseCodes.set(m[1], m[2]);
|
|
}
|
|
} else {
|
|
const named = git("grep", "-ohE", "SC[0-9]{4}", "origin/main", "--", "packages/*/src/**");
|
|
for (const code of named.split("\n").filter(Boolean)) baseCodes.set(code, "");
|
|
}
|
|
const baseTags = new Map(
|
|
[...git("show", `origin/main:${DYN}`).matchAll(/^\s*X\(([A-Z0-9_]+), (\d+)\)/gm)].map(
|
|
([, name, value]) => [Number(value), name],
|
|
),
|
|
);
|
|
|
|
/** The claims of one unified diff: [registry, number, label] triples. */
|
|
function claimsOf(diff) {
|
|
const claims = [];
|
|
let file = "";
|
|
let table = ""; // the codes.ts table the current line is in
|
|
for (const line of diff.split("\n")) {
|
|
if (line.startsWith("+++ ")) {
|
|
file = line.slice(4).replace(/^b\//, "");
|
|
continue;
|
|
}
|
|
const section = /^(?:@@.*@@ |[ +])export const (\w+)/.exec(line);
|
|
if (section) table = section[1];
|
|
if (!line.startsWith("+")) continue;
|
|
// A reserved row holds a code for other work; it is not this change's claim.
|
|
if (file === CODES && table === "RESERVED_DIAGNOSTIC_CODES") continue;
|
|
if (file === DYN) {
|
|
const tag = /^\+\s*X\(([A-Z0-9_]+), (\d+)\)/.exec(line);
|
|
if (tag) claims.push(["dyn handle tag", Number(tag[2]), tag[1]]);
|
|
}
|
|
if (!file.startsWith("packages/")) continue;
|
|
const row = file === CODES ? /^\+ {2}(SC\d{4}): "([^"]*)"/.exec(line) : null;
|
|
if (row) {
|
|
if (baseCodes.get(row[1]) !== row[2]) claims.push(["diagnostic code", row[1], row[2]]);
|
|
continue;
|
|
}
|
|
for (const [code] of line.matchAll(/\bSC\d{4}\b/g)) {
|
|
if (!baseCodes.has(code))
|
|
claims.push(["diagnostic code", code, `${file}: ${line.slice(1).trim()}`]);
|
|
}
|
|
}
|
|
return claims;
|
|
}
|
|
|
|
const prs = JSON.parse(
|
|
(
|
|
await run("gh", [
|
|
"pr",
|
|
"list",
|
|
"--state",
|
|
"open",
|
|
"--limit",
|
|
"300",
|
|
"--json",
|
|
"number,title,headRefName,baseRefName",
|
|
])
|
|
).stdout,
|
|
);
|
|
const head = git("rev-parse", "--abbrev-ref", "HEAD").trim();
|
|
const here = `this branch (${head})`;
|
|
|
|
// A stack of pull requests (each based on the previous one's branch) is one
|
|
// claimant: a later PR's diff only shows what it adds to its base.
|
|
const byHead = new Map(prs.map((pr) => [pr.headRefName, pr]));
|
|
const stackOf = (pr) => {
|
|
while (byHead.has(pr.baseRefName) && byHead.get(pr.baseRefName) !== pr)
|
|
pr = byHead.get(pr.baseRefName);
|
|
return pr.headRefName;
|
|
};
|
|
const stacks = new Map();
|
|
const claimant = (key) => stacks.get(key) ?? stacks.set(key, { names: [], claims: [] }).get(key);
|
|
|
|
const queue = [...prs];
|
|
// Pull requests whose diff could not be read: their claims are missing.
|
|
const unread = [];
|
|
await Promise.all(
|
|
Array.from({ length: 8 }, async () => {
|
|
for (let pr = queue.shift(); pr; pr = queue.shift()) {
|
|
const diff = await run("gh", ["pr", "diff", String(pr.number)], { maxBuffer: 1 << 30 }).then(
|
|
(r) => r.stdout,
|
|
(error) => {
|
|
const reason = String(error.stderr || error.message)
|
|
.trim()
|
|
.split("\n")[0];
|
|
unread.push(`#${pr.number} (${reason})`);
|
|
return null;
|
|
},
|
|
);
|
|
const stack = claimant(stackOf(pr));
|
|
stack.names.push({ number: pr.number, title: pr.title });
|
|
if (diff !== null) stack.claims.push(...claimsOf(diff));
|
|
}
|
|
}),
|
|
);
|
|
// This branch is its own PR's claimant when it has one open.
|
|
const mine = claimant(byHead.has(head) ? stackOf(byHead.get(head)) : head);
|
|
mine.names.push({ number: Infinity, title: here });
|
|
// Committed and uncommitted changes since the branch left origin/main, and
|
|
// untracked files as wholly added.
|
|
const untracked = git("ls-files", "--others", "--exclude-standard", "-z")
|
|
.split("\0")
|
|
.filter(Boolean);
|
|
mine.claims.push(
|
|
...claimsOf(git("diff", git("merge-base", "origin/main", "HEAD").trim())),
|
|
...untracked
|
|
.map((file) => claimsOf(`+++ b/${file}\n` + readFileSync(file, "utf8").replace(/^/gm, "+")))
|
|
.flat(),
|
|
);
|
|
// A stack reads "#1 #2 #3: <the first PR's title>".
|
|
const sources = [...stacks.values()].map(({ names, claims }) => {
|
|
names.sort((a, b) => a.number - b.number);
|
|
const prs = names.filter((n) => n.number !== Infinity).map((n) => `#${n.number}`);
|
|
const title = names[0].title;
|
|
const name = prs.length === 0 ? title : `${prs.join(" ")}: ${title}`;
|
|
return {
|
|
name: names.some((n) => n.title === here) && prs.length > 0 ? `${name} (${here})` : name,
|
|
claims,
|
|
};
|
|
});
|
|
|
|
// registry → number → label → claimants. Each claimant labels a number once:
|
|
// with the codes.ts row that claims it when it adds one, otherwise with the
|
|
// first line that names it.
|
|
const table = new Map();
|
|
for (const { name, claims } of sources) {
|
|
const labelOf = new Map();
|
|
for (const [registry, number, label] of claims) {
|
|
const key = `${registry}\0${number}`;
|
|
const row = registry === "dyn handle tag" || !label.includes(": ");
|
|
if (!labelOf.has(key) || (row && !labelOf.get(key).row))
|
|
labelOf.set(key, { registry, number, label, row });
|
|
}
|
|
for (const { registry, number, label } of labelOf.values()) {
|
|
const numbers = table.get(registry) ?? table.set(registry, new Map()).get(registry);
|
|
const labels = numbers.get(number) ?? numbers.set(number, new Map()).get(number);
|
|
(labels.get(label) ?? labels.set(label, []).get(label)).push(name);
|
|
}
|
|
}
|
|
|
|
let collisions = 0;
|
|
for (const [registry, numbers] of [...table].sort()) {
|
|
for (const [number, labels] of [...numbers].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
const onMain = registry === "dyn handle tag" ? baseTags.get(number) : undefined;
|
|
const clash = labels.size > 1 || (onMain !== undefined && !labels.has(onMain));
|
|
const ours = [...labels.values()].some((who) => who.some((name) => name.includes(here)));
|
|
if (check && !(clash && ours)) continue;
|
|
if (clash) collisions++;
|
|
console.log(`${clash ? "COLLISION" : "claimed"} ${registry} ${number}`);
|
|
if (onMain !== undefined && !labels.has(onMain)) console.log(` origin/main: ${onMain}`);
|
|
for (const [label, who] of labels) console.log(` ${label}\n ${who.join("\n ")}`);
|
|
}
|
|
}
|
|
if (unread.length > 0) {
|
|
console.log(
|
|
`could not read the diff of ${unread.length} pull request${unread.length === 1 ? "" : "s"}; ${unread.length === 1 ? "its" : "their"} claims are missing:`,
|
|
);
|
|
for (const pr of unread.sort()) console.log(` ${pr}`);
|
|
}
|
|
if (collisions === 0)
|
|
console.log(
|
|
`${check ? "no collisions with this branch" : "no collisions"}${unread.length > 0 ? " among the diffs read" : ""}`,
|
|
);
|
|
process.exitCode = check && collisions > 0 ? 1 : unread.length > 0 ? 2 : 0;
|