import { test } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; const HOOKS = fileURLToPath(new URL("../.githooks/", import.meta.url)); const SHA = "1".repeat(40); const ZERO = "0".repeat(40); function prePush(...lines) { return spawnSync("sh", [join(HOOKS, "pre-push"), "origin", "git@example:repo.git"], { input: lines.map((l) => l + "\n").join(""), encoding: "utf8", }); } function repoOn(branch) { const dir = mkdtempSync(join(tmpdir(), "todo-hooks-")); spawnSync("git", ["init", "-q", "-b", branch, dir]); return dir; } for (const [label, line] of [ ["an update", `refs/heads/fix/x ${SHA} refs/heads/main ${SHA}`], ["a new ref", `refs/heads/fix/x ${SHA} refs/heads/main ${ZERO}`], ["a deletion", `(delete) ${ZERO} refs/heads/main ${SHA}`], ]) { test(`pre-push rejects ${label} of main`, () => { const res = prePush(line); assert.equal(res.status, 1); assert.match(res.stderr, /pushing to main is not allowed/); }); } test("pre-push rejects --all style pushes that include main", () => { const res = prePush(`refs/heads/fix/x ${SHA} refs/heads/fix/x ${SHA}`, `refs/heads/main ${SHA} refs/heads/main ${SHA}`); assert.equal(res.status, 1); }); test("pre-push allows other branches, including ones named like main", () => { const res = prePush(`refs/heads/fix/x ${SHA} refs/heads/fix/x ${ZERO}`, `refs/heads/maintenance ${SHA} refs/heads/maintenance ${SHA}`); assert.equal(res.status, 0); }); for (const hook of ["pre-commit", "pre-merge-commit"]) { test(`${hook} rejects main and allows other branches`, () => { const onMain = spawnSync("sh", [join(HOOKS, hook)], { cwd: repoOn("main"), encoding: "utf8" }); assert.equal(onMain.status, 1); assert.match(onMain.stderr, /committing on main is not allowed/); assert.equal(spawnSync("sh", [join(HOOKS, hook)], { cwd: repoOn("fix/x") }).status, 0); }); }