源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
||||
|
||||
export function parseArgs(args) {
|
||||
const options = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!["--base", "--head", "--reverted"].includes(arg)) {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
const value = args[index + 1];
|
||||
if (!value || value.startsWith("--")) throw new Error(`${arg} requires a value.`);
|
||||
options[arg.slice(2)] = value;
|
||||
index += 1;
|
||||
}
|
||||
if (!options.base || !options.head) {
|
||||
throw new Error("Usage: node scripts/ci/verify-clean-revert.mjs --base <sha> --head <sha> [--reverted <sha>]");
|
||||
}
|
||||
if (options.reverted && !SHA_PATTERN.test(options.reverted)) {
|
||||
throw new Error("--reverted must be a full 40-character commit SHA.");
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function command(run, args, cwd, allowFailure = false) {
|
||||
const result = run("git", args, { cwd, encoding: "utf8" });
|
||||
if (!allowFailure && result.status !== 0) {
|
||||
throw new Error(result.stderr.trim() || `git ${args[0]} failed with exit code ${result.status ?? "unknown"}.`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseRevertedSha(message) {
|
||||
const matches = [...message.matchAll(/This reverts commit ([0-9a-f]{40})/gi)];
|
||||
const shas = new Set(matches.map((match) => match[1].toLowerCase()));
|
||||
if (shas.size === 0) throw new Error("head commit message has no 'This reverts commit <40-hex>' line");
|
||||
if (shas.size > 1) throw new Error("head commit message names multiple distinct reverted commits; only single-commit reverts are supported");
|
||||
return [...shas][0];
|
||||
}
|
||||
|
||||
export function verifyCleanRevert(options, run = spawnSync) {
|
||||
const cwd = process.cwd();
|
||||
let reverted = options.reverted?.toLowerCase();
|
||||
if (!reverted) {
|
||||
const message = command(run, ["log", "-1", "--format=%B", options.head], cwd).stdout;
|
||||
reverted = parseRevertedSha(message);
|
||||
}
|
||||
|
||||
const parents = command(run, ["rev-list", "--parents", "-n", "1", reverted], cwd).stdout.trim().split(/\s+/);
|
||||
if (parents.length > 2) throw new Error("reverted commit is a merge commit; only non-merge commits are supported");
|
||||
|
||||
const ancestor = command(run, ["merge-base", "--is-ancestor", reverted, options.base], cwd, true);
|
||||
if (ancestor.status !== 0) throw new Error("reverted commit is not an ancestor of the PR base");
|
||||
|
||||
const tempRoot = mkdtempSync(resolve(tmpdir(), "verify-clean-revert-"));
|
||||
const worktree = resolve(tempRoot, "worktree");
|
||||
let added = false;
|
||||
try {
|
||||
command(run, ["worktree", "add", "--detach", worktree, options.base], cwd);
|
||||
added = true;
|
||||
const revert = command(run, ["revert", "--no-commit", "--no-edit", reverted], worktree, true);
|
||||
if (revert.status !== 0) throw new Error("revert does not apply cleanly to PR base");
|
||||
|
||||
const expectedTree = command(run, ["write-tree"], worktree).stdout.trim();
|
||||
const headTree = command(run, ["rev-parse", `${options.head}^{tree}`], cwd).stdout.trim();
|
||||
if (expectedTree !== headTree) throw new Error("PR tree is not the exact inverse of the reverted commit");
|
||||
return reverted;
|
||||
} finally {
|
||||
if (added) command(run, ["worktree", "remove", "--force", worktree], cwd, true);
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
let reverted = "";
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
reverted = options.reverted?.toLowerCase() ?? "";
|
||||
reverted = verifyCleanRevert(options);
|
||||
console.log(`PASS clean-revert of ${reverted}`);
|
||||
console.log("verdict=pass");
|
||||
console.log(`reverted=${reverted}`);
|
||||
} catch (error) {
|
||||
console.log(`FAIL ${error instanceof Error ? error.message : String(error)}`);
|
||||
console.log("verdict=fail");
|
||||
console.log(`reverted=${reverted}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();
|
||||
@@ -0,0 +1,93 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { afterEach, test } from "node:test";
|
||||
|
||||
const script = resolve(import.meta.dirname, "verify-clean-revert.mjs");
|
||||
const tempDirs = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of tempDirs.splice(0)) rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function git(repo, ...args) {
|
||||
const result = spawnSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", ...args], {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const repo = mkdtempSync(resolve(tmpdir(), "clean-revert-test-"));
|
||||
tempDirs.push(repo);
|
||||
git(repo, "init", "-q");
|
||||
writeFileSync(resolve(repo, "file.txt"), "A\n");
|
||||
git(repo, "add", "file.txt");
|
||||
git(repo, "commit", "-q", "-m", "A");
|
||||
const a = git(repo, "rev-parse", "HEAD");
|
||||
writeFileSync(resolve(repo, "file.txt"), "B\n");
|
||||
git(repo, "commit", "-q", "-am", "B");
|
||||
const b = git(repo, "rev-parse", "HEAD");
|
||||
git(repo, "revert", "--no-edit", b);
|
||||
const c = git(repo, "rev-parse", "HEAD");
|
||||
return { repo, a, b, c };
|
||||
}
|
||||
|
||||
function verify(repo, ...args) {
|
||||
return spawnSync(process.execPath, [script, ...args], { cwd: repo, encoding: "utf8" });
|
||||
}
|
||||
|
||||
test("passes an exact revert parsed from the head message", () => {
|
||||
const { repo, b, c } = fixture();
|
||||
const result = verify(repo, "--base", b, "--head", c);
|
||||
assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
assert.match(result.stdout, new RegExp(`PASS clean-revert of ${b}`));
|
||||
assert.match(result.stdout, /verdict=pass/);
|
||||
});
|
||||
|
||||
test("fails when the revert tree contains an extra edit", () => {
|
||||
const { repo, b, c } = fixture();
|
||||
writeFileSync(resolve(repo, "extra.txt"), "tampered\n");
|
||||
git(repo, "add", "extra.txt");
|
||||
git(repo, "commit", "-q", "-m", "extra edit");
|
||||
const tampered = git(repo, "rev-parse", "HEAD");
|
||||
const result = verify(repo, "--base", b, "--head", tampered, "--reverted", b);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stdout, /FAIL PR tree is not the exact inverse/);
|
||||
assert.notEqual(c, tampered);
|
||||
});
|
||||
|
||||
test("fails when source drift makes the revert conflict", () => {
|
||||
const { repo, b } = fixture();
|
||||
git(repo, "switch", "-q", "--detach", b);
|
||||
writeFileSync(resolve(repo, "file.txt"), "unrelated edit to the same line\n");
|
||||
git(repo, "commit", "-q", "-am", "source drift");
|
||||
const driftedBase = git(repo, "rev-parse", "HEAD");
|
||||
const result = verify(repo, "--base", driftedBase, "--head", driftedBase, "--reverted", b);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stdout, /FAIL revert does not apply cleanly to PR base/);
|
||||
});
|
||||
|
||||
test("fails when the reverted commit is not an ancestor of base", () => {
|
||||
const { repo, a, b, c } = fixture();
|
||||
git(repo, "switch", "-q", "--detach", a);
|
||||
writeFileSync(resolve(repo, "other.txt"), "other\n");
|
||||
git(repo, "add", "other.txt");
|
||||
git(repo, "commit", "-q", "-m", "divergent");
|
||||
const divergent = git(repo, "rev-parse", "HEAD");
|
||||
const result = verify(repo, "--base", b, "--head", c, "--reverted", divergent);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stdout, /FAIL reverted commit is not an ancestor of the PR base/);
|
||||
});
|
||||
|
||||
test("fails when the head message does not identify a reverted commit", () => {
|
||||
const { repo, a, b } = fixture();
|
||||
const result = verify(repo, "--base", a, "--head", b);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stdout, /FAIL head commit message has no 'This reverts commit <40-hex>' line/);
|
||||
assert.match(result.stdout, /verdict=fail/);
|
||||
});
|
||||
Reference in New Issue
Block a user