Files
zopu-code/scripts/subtree.ts

322 lines
9.1 KiB
TypeScript

import { parseArgs } from "node:util";
import manifest from "./subtrees.json";
interface Subtree {
readonly name: string;
readonly prefix: string;
readonly upstream: string;
readonly ref: string;
readonly squash: true;
readonly role: string;
readonly refreshPolicy: "allowed" | "pinned";
}
interface CommandResult {
readonly exitCode: number;
readonly stdout: string;
readonly stderr: string;
}
const cwd = process.cwd();
const subtrees = manifest.subtrees as readonly Subtree[];
const run = async (
command: readonly string[],
options: { readonly inherit?: boolean } = {}
): Promise<CommandResult> => {
const process = Bun.spawn([...command], {
cwd,
stdin: options.inherit ? "inherit" : "ignore",
stdout: options.inherit ? "inherit" : "pipe",
stderr: options.inherit ? "inherit" : "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
process.exited,
options.inherit ? Promise.resolve("") : new Response(process.stdout).text(),
options.inherit ? Promise.resolve("") : new Response(process.stderr).text(),
]);
return { exitCode, stdout, stderr };
};
const shellQuote = (value: string) => `'${value.replaceAll("'", `'\\''`)}'`;
const formatCommand = (command: readonly string[]) =>
command.map(shellQuote).join(" ");
const requireRepositoryRoot = async () => {
const result = await run(["git", "rev-parse", "--show-toplevel"]);
if (result.exitCode !== 0) {
throw new Error("The current directory is not inside a Git checkout.");
}
if (result.stdout.trim() !== cwd) {
throw new Error(
`Run this script from the repository root: ${result.stdout.trim()}`
);
}
};
const requireCleanWorktree = async () => {
const result = await run(["git", "status", "--porcelain"]);
if (result.exitCode !== 0) {
throw new Error(result.stderr.trim());
}
if (result.stdout.trim() !== "") {
throw new Error(
"Subtree mutations require a clean worktree. Commit or stash changes first."
);
}
};
const getSubtree = (name: string | undefined) => {
if (name === undefined) {
throw new Error(
`A subtree name is required. Available: ${subtrees.map((entry) => entry.name).join(", ")}`
);
}
const subtree = subtrees.find((entry) => entry.name === name);
if (subtree === undefined) {
throw new Error(
`Unknown subtree ${shellQuote(name)}. Available: ${subtrees.map((entry) => entry.name).join(", ")}`
);
}
if (
!subtree.prefix.startsWith("repos/") ||
!subtree.upstream.startsWith("https://")
) {
throw new Error(
`Unsafe subtree manifest entry ${shellQuote(subtree.name)}.`
);
}
return subtree;
};
const readSubtreeMetadata = async (subtree: Subtree) => {
const merge = await run([
"git",
"log",
"--all",
"-n",
"1",
"--format=%H%x09%ci%x09%s",
"--",
subtree.prefix,
]);
if (merge.exitCode !== 0) {
throw new Error(merge.stderr.trim());
}
const mergeLine = merge.stdout.trim();
if (mergeLine === "") {
return { mergeLine: "No subtree merge found", split: undefined };
}
const mergeSha = mergeLine.split("\t")[0];
if (mergeSha === undefined) {
return { mergeLine, split: undefined };
}
const squash = await run([
"git",
"show",
"--no-patch",
"--format=%b",
`${mergeSha}^2`,
]);
const split = squash.stdout
.split("\n")
.find((line) => line.startsWith("git-subtree-split: "))
?.slice("git-subtree-split: ".length);
return { mergeLine, split };
};
const readStatus = async (subtree: Subtree) => {
const [dirty, metadata] = await Promise.all([
run(["git", "status", "--porcelain", "--", subtree.prefix]),
readSubtreeMetadata(subtree),
]);
if (dirty.exitCode !== 0) {
throw new Error(dirty.stderr.trim());
}
return {
dirty: dirty.stdout.trim() !== "",
latest: metadata.mergeLine,
};
};
const updateCommand = (subtree: Subtree) => [
"git",
"subtree",
"pull",
`--prefix=${subtree.prefix}`,
subtree.upstream,
subtree.ref,
"--squash",
];
const addCommand = (subtree: Subtree) => [
"git",
"subtree",
"add",
`--prefix=${subtree.prefix}`,
subtree.upstream,
subtree.ref,
"--squash",
];
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
"dry-run": { type: "boolean", default: false },
"force-pinned": { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
yes: { type: "boolean", short: "y", default: false },
},
allowPositionals: true,
strict: true,
});
const [operation = "list", name, ...extra] = positionals;
if (values.help || extra.length > 0) {
console.log(`Usage: bun run subtree [list|status|preview|add|update] [name] [options]
Commands:
list List configured vendored subtrees
status [name] Show local subtree metadata and dirtiness
preview <name> Fetch the configured upstream and show incoming commits
add <name> Add an absent manifest entry with git subtree add --squash
update <name> Pull a configured subtree with git subtree pull --squash
Options:
--dry-run Print mutating/fetch commands without executing them
--yes, -y Required for add and update
--force-pinned Allow updating a pinned historical subtree together with --yes`);
process.exit(extra.length > 0 ? 1 : 0);
}
try {
await requireRepositoryRoot();
switch (operation) {
case "list": {
for (const subtree of subtrees) {
console.log(
`${subtree.name}\t${subtree.prefix}\t${subtree.upstream}#${subtree.ref}\t${subtree.refreshPolicy}`
);
}
break;
}
case "status": {
const selected = name === undefined ? subtrees : [getSubtree(name)];
for (const subtree of selected) {
const status = await readStatus(subtree);
console.log(`${subtree.name}:`);
console.log(` prefix: ${subtree.prefix}`);
console.log(` upstream: ${subtree.upstream}#${subtree.ref}`);
console.log(` policy: ${subtree.refreshPolicy}`);
console.log(` prefix dirty: ${status.dirty ? "yes" : "no"}`);
console.log(` latest metadata: ${status.latest}`);
}
break;
}
case "preview": {
const subtree = getSubtree(name);
const fetch = [
"git",
"fetch",
"--no-tags",
subtree.upstream,
subtree.ref,
];
if (values["dry-run"]) {
console.log(formatCommand(fetch));
console.log(
formatCommand([
"git",
"log",
"--oneline",
"--decorate",
"<last-subtree-split>..FETCH_HEAD",
])
);
break;
}
const fetched = await run(fetch, { inherit: true });
if (fetched.exitCode !== 0) {
process.exit(fetched.exitCode);
}
const metadata = await readSubtreeMetadata(subtree);
if (metadata.split === undefined) {
throw new Error(
`No git-subtree-split metadata found for ${subtree.prefix}.`
);
}
const log = await run([
"git",
"log",
"--oneline",
"--decorate",
`${metadata.split}..FETCH_HEAD`,
]);
if (log.exitCode !== 0) {
throw new Error(log.stderr.trim());
}
console.log(log.stdout.trim() || `${subtree.name} is up to date.`);
break;
}
case "add":
case "update": {
const subtree = getSubtree(name);
const prefixType = await run([
"git",
"cat-file",
"-t",
`HEAD:${subtree.prefix}`,
]);
const prefixExists =
prefixType.exitCode === 0 && prefixType.stdout.trim() === "tree";
if (
operation === "update" &&
subtree.refreshPolicy === "pinned" &&
!values["force-pinned"]
) {
throw new Error(
`${subtree.name} is pinned historical reference material. Pass --force-pinned --yes only when historical synchronization is explicitly required.`
);
}
if (!values["dry-run"]) {
await requireCleanWorktree();
}
if (operation === "add" && prefixExists) {
throw new Error(
`Cannot add ${subtree.name}: ${subtree.prefix} already exists.`
);
}
if (operation === "update" && !prefixExists) {
throw new Error(
`Cannot update ${subtree.name}: ${subtree.prefix} does not exist.`
);
}
const command =
operation === "add" ? addCommand(subtree) : updateCommand(subtree);
console.log(formatCommand(command));
if (values["dry-run"]) {
break;
}
if (!values.yes) {
throw new Error(
`Refusing to ${operation} without --yes. Re-run after reviewing the command above.`
);
}
const result = await run(command, { inherit: true });
if (result.exitCode !== 0) {
process.exit(result.exitCode);
}
break;
}
default:
throw new Error(
`Unknown command ${shellQuote(operation)}. Use list, status, preview, add, or update.`
);
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}