94 lines
3.7 KiB
JavaScript
94 lines
3.7 KiB
JavaScript
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/);
|
|
});
|