// The working repositories (src/working.mjs, #1587), against real git // repositories in a temp directory. The bundles are made the way a // session's runner makes them (packages/harness/src/git-tools.mjs). Every // case also checks that the root's files and refs are byte for byte what // they were, that no temp ref is left, and that only the task's own ref moved. import { test } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { gitEnv } from "../src/checkout.mjs"; import { dropLand, finishLand, initializeWorking, prepareLand, push, review, validBranch, workingRepo, workRef, WorkingError } from "../src/working.mjs"; import { rootHits } from "../src/landing.mjs"; const TASK = "vikunja:1/2"; const REF = "refs/heads/work/vikunja-1-2"; function tmp(t) { const dir = mkdtempSync(join(tmpdir(), "mosaic-working-")); t.after(() => rmSync(dir, { recursive: true, force: true })); return dir; } function git(cwd, ...args) { const r = spawnSync("git", ["-c", "maintenance.auto=false", "-c", "gc.auto=0", "-c", "user.name=t", "-c", "user.email=t@example.invalid", ...args], { cwd, encoding: "utf8", env: gitEnv() }); assert.equal(r.status, 0, `git ${args.join(" ")}: ${r.stderr}`); return r.stdout.trim(); } function commit(dir, text) { writeFileSync(join(dir, "README"), `${text}\n`); git(dir, "add", "README"); git(dir, "commit", "-q", "-m", text); return git(dir, "rev-parse", "HEAD"); } function fingerprint(dir) { const h = createHash("sha256"); const walk = (d) => { for (const name of readdirSync(d).sort()) { const p = join(d, name); const st = lstatSync(p); h.update(`${p}\0${st.mode}\0`); if (st.isDirectory()) walk(p); else if (st.isSymbolicLink()) h.update(readlinkSync(p)); else if (st.isFile()) h.update(readFileSync(p)); } }; walk(dir); return h.digest("hex"); } // A root on main with one commit, a coder's clone of it, and the data root. function world(t) { const dir = tmp(t); const root = join(dir, "root"); mkdirSync(root); git(root, "init", "-q", "-b", "main"); const first = commit(root, "one"); const clone = join(dir, "clone"); git(dir, "clone", "-q", root, clone); const dataRoot = join(dir, "data"); mkdirSync(dataRoot, { mode: 0o700 }); const repo = workingRepo(dataRoot, "acme", "stack"); let n = 0; // A bundle of `branch` the way the runner makes one. const bundle = (branch = "feat", from = clone) => { const file = join(dir, `b${n++}.bundle`); git(from, "bundle", "create", "-q", file, `refs/heads/${branch}`, "--not", "--remotes=origin", "--remotes=working"); return file; }; const refs = () => (existsSync(repo) && lstatSync(repo).isDirectory() ? git(dir, `--git-dir=${repo}`, "for-each-ref", "--format=%(refname) %(objectname)") : ""); const go = (file, extra = {}) => push({ dataRoot, business: "acme", project: "stack", root, taskRef: TASK, branch: "feat", bundle: file, ...extra }); return { dir, root, first, clone, dataRoot, repo, bundle, refs, go }; } // A refusal with `code`, and the root, the working repository's refs and its // temp refs as they were. async function refuses(w, fn, code) { const root = fingerprint(w.root); const refs = w.refs(); await assert.rejects(async () => fn(), (e) => e instanceof WorkingError && e.code === code); assert.equal(fingerprint(w.root), root, "the root is unchanged"); if (refs) assert.equal(w.refs(), refs, "no ref of the working repository moved"); assert.doesNotMatch(w.refs(), /refs\/mosaic\//, "no temp ref is left"); } // The .lock files under a repository's refs. function locks(repo) { const found = []; const walk = (d) => { for (const name of readdirSync(d)) { const p = join(d, name); if (lstatSync(p).isDirectory()) walk(p); else if (name.endsWith(".lock")) found.push(p.slice(repo.length + 1)); } }; walk(repo); return found; } // A `git` first in PATH that runs `body` (bash) with REAL as the real git, // and the env that puts it there. const REAL_GIT = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim(); function wrapGit(w, body) { const bin = join(w.dir, "bin"); mkdirSync(bin); writeFileSync(join(bin, "git"), `#!/bin/bash REAL='${REAL_GIT}' ${body} `); chmodSync(join(bin, "git"), 0o755); return { ...process.env, PATH: `${bin}:${process.env.PATH}` }; } async function lands(w, fn) { const root = fingerprint(w.root); const r = await fn(); assert.equal(fingerprint(w.root), root, "the root is unchanged"); assert.doesNotMatch(w.refs(), /refs\/mosaic\//, "no temp ref is left"); return r; } // #802 N1: the helper makes its directories one at a time and never follows a link or a file among them. for (const [what, plant] of [ ["the working directory is a link", (w, outside) => symlinkSync(outside, join(w.dataRoot, "working"))], ["the business directory is a link", (w, outside) => { mkdirSync(join(w.dataRoot, "working")); symlinkSync(outside, join(w.dataRoot, "working", "acme")); }], ["the working directory is a file", (w) => writeFileSync(join(w.dataRoot, "working"), "not a directory\n")], ]) { test(`push and initializeWorking refuse when ${what}, and nothing lands behind it`, async (t) => { const w = world(t); const outside = join(w.dir, "outside"); mkdirSync(outside); plant(w, outside); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "two"); const file = w.bundle(); await assert.rejects(w.go(file), (e) => e instanceof WorkingError && e.code === "working-repo-invalid"); await assert.rejects(initializeWorking({ dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root }), (e) => e instanceof WorkingError && e.code === "working-repo-invalid"); assert.deepEqual(readdirSync(outside), [], "nothing was made behind the link"); assert.equal(existsSync(join(w.dataRoot, "working", "acme", "stack.git")), false); }); } // #802 I1: every write initializeWorking makes goes through `around`, so a caller that checks there never has a // write begin after its check went stale. A refusal from `around` stops that write and every later one. test("initializeWorking runs each of its five writes through around, and a refusal stops it there", async (t) => { const run = (w, around) => initializeWorking({ dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, around }); let calls = 0; const w = world(t); const made = await run(w, (effect) => { calls++; return effect(); }); assert.equal(calls, 5, "the directories, the init, its rename, the root fetch and the main ref"); assert.equal(made.commit, w.first); const at = [ { repo: false, refs: "" }, { repo: false, refs: "" }, { repo: false, refs: "" }, { repo: true, refs: "" }, { repo: true, refs: `refs/heads/root ${w.first}` }, ]; for (let stop = 0; stop < 5; stop++) { const v = world(t), refusal = new Error(`stale at ${stop}`); let n = 0; await assert.rejects(run(v, (effect) => { if (n++ === stop) throw refusal; return effect(); }), (e) => e === refusal); assert.equal(existsSync(v.repo), at[stop].repo, `repository after a refusal at write ${stop}`); assert.equal(v.refs(), at[stop].refs.replace(w.first, v.first), `refs after a refusal at write ${stop}`); assert.deepEqual(existsSync(join(v.dataRoot, "working", "acme")) ? readdirSync(join(v.dataRoot, "working", "acme")).filter((x) => x.startsWith(".init-")) : [], [], "no init temp is left"); } }); // #802 C1: no git child may follow indirection in an existing bare repository. for (const kind of ['objects_link', 'nested_link', 'alternates']) { test(`c1_working_refuses_${kind}_before_any_git_child`, async t => { const w = world(t); await initializeWorking({ dataRoot: w.dataRoot, business: 'acme', project: 'stack', root: w.root }); const outside = join(w.dir, 'outside'); mkdirSync(outside); if (kind === 'objects_link') { rmSync(join(w.repo, 'objects'), { recursive: true }); symlinkSync(outside, join(w.repo, 'objects')); } else if (kind === 'nested_link') symlinkSync(outside, join(w.repo, 'objects/info/linked')); else writeFileSync(join(w.repo, 'objects/info/alternates'), outside + '\n'); const before = fingerprint(w.repo), touched = join(w.dir, 'git-started'); const env = wrapGit(w, `touch '${touched}'\nexec "$REAL" "$@"`); await assert.rejects(() => initializeWorking({ dataRoot: w.dataRoot, business: 'acme', project: 'stack', root: w.root, env }), { code: 'working-repo-invalid' }); assert.equal(existsSync(touched), false); assert.equal(fingerprint(w.repo), before); assert.deepEqual(readdirSync(outside), []); }); } test("workRef and validBranch take only what a ref name can hold", () => { assert.equal(workRef("vikunja:12/345"), "work/vikunja-12-345"); for (const bad of ["vikunja:0/1", "vikunja:1", "gitea:1/2", "vikunja:1/2\n", null]) assert.throws(() => workRef(bad), WorkingError); for (const ok of ["feat", "fix/thing-2", "a.b_c"]) assert.equal(validBranch(ok), true, ok); for (const bad of ["", "-x", "a..b", "a//b", "a/.b", "a@{1}", "x.lock", "a/", "a.", "a b", "a:b", "a~1", "a".repeat(201)]) assert.equal(validBranch(bad), false, bad); }); test("a push lands as the task's work ref, a fast-forward lands after it, and main is the root's", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); const c1 = commit(w.clone, "two"); const seen = []; const r1 = await lands(w, () => w.go(w.bundle(), { record: async (r) => seen.push(r) })); assert.deepEqual(r1, { ref: "work/vikunja-1-2", commit: c1, previous: null }); assert.deepEqual(seen, [r1]); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}\n${REF} ${c1}`); // The runner's clone remembers the push, so the next bundle carries only c2. git(w.clone, "update-ref", "refs/remotes/working/work/vikunja-1-2", c1); const c2 = commit(w.clone, "three"); const file = w.bundle(); assert.match(git(w.dir, "bundle", "list-heads", file), new RegExp(c2)); const r2 = await lands(w, () => w.go(file)); assert.deepEqual(r2, { ref: "work/vikunja-1-2", commit: c2, previous: c1 }); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}\n${REF} ${c2}`); }); test("a rewritten branch refuses non-fast-forward and the work ref stays", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "two"); await w.go(w.bundle()); git(w.clone, "reset", "-q", "--hard", w.first); commit(w.clone, "other two"); await refuses(w, () => w.go(w.bundle()), "non-fast-forward"); }); test("a branch with no history in common with main refuses unrelated-history", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "--orphan", "feat"); git(w.clone, "rm", "-q", "-rf", "."); commit(w.clone, "alone"); await refuses(w, () => w.go(w.bundle()), "unrelated-history"); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}`); }); test("a bundle that needs commits the working repository lacks refuses missing-prerequisite", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); const local = commit(w.clone, "never pushed"); commit(w.clone, "on top"); const file = join(w.dir, "partial.bundle"); git(w.clone, "bundle", "create", "-q", file, "refs/heads/feat", "--not", local); await refuses(w, () => w.go(file), "missing-prerequisite"); }); test("the root is fetched again before each push and main follows it, so a clone of a newer root pushes", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "two"); await w.go(w.bundle()); // The root moves on; a second coder clones it and branches from there. const newer = commit(w.root, "root two"); const other = join(w.dir, "other"); git(w.dir, "clone", "-q", w.root, other); git(other, "checkout", "-q", "-b", "feat"); const c = commit(other, "from newer"); const r = await lands(w, () => push({ dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, taskRef: "vikunja:1/3", branch: "feat", bundle: w.bundle("feat", other) })); assert.equal(r.commit, c); assert.match(w.refs(), new RegExp(`refs/heads/main ${newer}`)); }); test("a root moved back after the clone leaves the push's base unknown: missing-prerequisite", async (t) => { const w = world(t); commit(w.root, "root two"); git(w.clone, "pull", "-q", "--ff-only"); git(w.root, "reset", "-q", "--hard", w.first); git(w.root, "reflog", "expire", "--expire=now", "--all"); git(w.root, "gc", "-q", "--prune=now"); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "three"); await refuses(w, () => w.go(w.bundle()), "missing-prerequisite"); }); test("a commit with a broken object refuses invalid-bundle, and only because fsck is on", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); const tree = git(w.clone, "rev-parse", "HEAD^{tree}"); const raw = `tree ${tree}\nparent ${w.first}\nauthor nobody\ncommitter t 1700000000 +0000\n\nbad\n`; const r = spawnSync("git", ["hash-object", "-t", "commit", "-w", "--literally", "--stdin"], { cwd: w.clone, input: raw, encoding: "utf8", env: gitEnv() }); assert.equal(r.status, 0, r.stderr); git(w.clone, "update-ref", "refs/heads/feat", r.stdout.trim()); const file = w.bundle(); await refuses(w, () => w.go(file), "invalid-bundle"); assert.doesNotMatch(w.refs(), /work\//); // The same bundle with no fsck lands: the refusal above is fsck's. const landed = await w.go(file, { fsck: [] }); assert.equal(landed.commit, r.stdout.trim()); }); test("only a v2 bundle with the named branch is read", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "two"); const v2 = w.bundle(); const v3 = join(w.dir, "v3.bundle"); writeFileSync(v3, readFileSync(v2, "latin1").replace("# v2 git bundle\n", "# v3 git bundle\n@object-format=sha1\n"), "latin1"); await refuses(w, () => w.go(v3), "invalid-bundle"); const junk = join(w.dir, "junk.bundle"); writeFileSync(junk, "# v2 git bundle\nnot a bundle\n"); await refuses(w, () => w.go(junk), "invalid-bundle"); await refuses(w, () => push({ dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, taskRef: TASK, branch: "other", bundle: v2 }), "branch-not-in-bundle"); }); test("a branch named main, or named for another task, moves only this task's work ref", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "work/vikunja-9-9"); const c = commit(w.clone, "two"); git(w.clone, "branch", "-f", "main", c); for (const branch of ["main", "work/vikunja-9-9"]) { const r = await lands(w, () => push({ dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, taskRef: TASK, branch, bundle: w.bundle(branch) })); assert.equal(r.ref, "work/vikunja-1-2"); } assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}\n${REF} ${c}`); }); test("a record that throws moves the work ref back, or removes a new one", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "two"); const boom = async () => { throw new Error("not recorded"); }; await assert.rejects(w.go(w.bundle(), { record: boom }), /not recorded/); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}`); const c1 = (await w.go(w.bundle())).commit; commit(w.clone, "three"); await assert.rejects(w.go(w.bundle(), { record: boom }), /not recorded/); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}\n${REF} ${c1}`); }); test("a work ref that can't go back refuses rollback-failed, naming the ref and the commit", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); const c1 = commit(w.clone, "two"); // record() fails after taking the ref's lock, so moving the ref back fails. const lock = join(w.repo, `${REF}.lock`); const stuck = async () => { writeFileSync(lock, ""); throw new Error("not recorded"); }; const message = (c) => (e) => e instanceof WorkingError && e.code === "rollback-failed" && e.message.includes(`${REF} still names ${c}`) && /not recorded/.test(e.message); // A new ref that can't be removed. await assert.rejects(w.go(w.bundle(), { record: stuck }), message(c1)); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}\n${REF} ${c1}`); rmSync(lock); // A ref that can't go back to the one before. git(w.clone, "update-ref", "refs/remotes/working/work/vikunja-1-2", c1); const c2 = commit(w.clone, "three"); await assert.rejects(w.go(w.bundle(), { record: stuck }), message(c2)); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}\n${REF} ${c2}`); rmSync(lock); assert.deepEqual(locks(w.repo), []); }); test("a root that can't be read refuses root-unreadable", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "two"); await refuses(w, () => w.go(w.bundle(), { root: join(w.dir, "no-root") }), "root-unreadable"); }); test("a git past its time gets SIGTERM, leaves no ref lock, and the next push lands", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); const c1 = commit(w.clone, "two"); await w.go(w.bundle()); git(w.clone, "update-ref", "refs/remotes/working/work/vikunja-1-2", c1); // The move of the work ref holds the ref's lock in a reference-transaction // hook until the lock is gone, so the git is still holding it when its time // is up. Only that one fetch gets the hook. const hooks = join(w.dir, "hooks"); mkdirSync(hooks); const lock = join(w.repo, `${REF}.lock`); writeFileSync(join(hooks, "reference-transaction"), `#!/bin/bash\n[ "$1" = prepared ] || exit 0\nfor i in $(seq 200); do [ -e '${lock}' ] || exit 0; sleep 0.05; done\n`); chmodSync(join(hooks, "reference-transaction"), 0o755); const env = wrapGit( w, `self=0 for a in "$@"; do case "$a" in refs/mosaic/incoming/*:refs/heads/work/*) self=1;; esac; done args=() for a in "$@"; do [ $self = 1 ] && [ "$a" = core.hooksPath=/dev/null ] && a='core.hooksPath=${hooks}' args+=("$a") done exec "$REAL" "\${args[@]}"`, ); commit(w.clone, "three"); const limits = { timeoutMs: 2000, graceMs: 5000 }; await assert.rejects(w.go(w.bundle(), { env, limits }), (e) => e instanceof WorkingError && e.code === "push-failed" && /killed by SIGTERM/.test(e.message)); assert.deepEqual(locks(w.repo), []); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}\n${REF} ${c1}`); const c3 = commit(w.clone, "four"); const r = await lands(w, () => w.go(w.bundle())); assert.deepEqual(r, { ref: "work/vikunja-1-2", commit: c3, previous: c1 }); }); test("a git that ignores SIGTERM gets SIGKILL once the grace period is over", { timeout: 20000 }, async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "two"); // The fetch of the root never ends and ignores SIGTERM. const env = wrapGit( w, `for a in "$@"; do [ "$a" = +HEAD:refs/heads/root ] && { trap '' TERM; while :; do sleep 0.05; done; }; done exec "$REAL" "$@"`, ); await assert.rejects(w.go(w.bundle(), { env, limits: { timeoutMs: 500, graceMs: 500 } }), (e) => e instanceof WorkingError && e.code === "root-unreadable" && /killed by SIGKILL/.test(e.message)); }); test("a working repository path that isn't a directory refuses", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); commit(w.clone, "two"); mkdirSync(join(w.dataRoot, "working", "acme"), { recursive: true }); writeFileSync(w.repo, ""); await refuses(w, () => w.go(w.bundle()), "working-repo-invalid"); }); test("review bundles a pushed commit as refs/mosaic/review, leaving out what the reviewer has", async (t) => { const w = world(t); git(w.clone, "checkout", "-q", "-b", "feat"); const c1 = commit(w.clone, "two"); await w.go(w.bundle()); const out = join(w.dir, "review.bundle"); const args = { dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, taskRef: TASK, commit: c1, out }; assert.deepEqual(await lands(w, () => review({ ...args, have: [w.first, "f".repeat(40)] })), { ref: "work/vikunja-1-2", commit: c1 }); assert.equal(git(w.dir, "bundle", "list-heads", out), `${c1} refs/mosaic/review`); // Only the one commit: the reviewer has main. assert.match(git(w.dir, `--git-dir=${w.repo}`, "bundle", "verify", out), /requires this ref/); rmSync(out); await refuses(w, () => review({ ...args, commit: "e".repeat(40) }), "unknown-commit"); await refuses(w, () => review({ ...args, have: ["x"] }), "invalid-request"); await refuses(w, () => review({ ...args, have: Array(33).fill(w.first) }), "invalid-request"); await refuses(w, () => review({ ...args, have: [c1] }), "nothing-to-fetch"); assert.equal(existsSync(out), false); assert.doesNotMatch(w.refs(), /refs\/mosaic/); }); test("review of a project with no working repository refuses unknown-commit", async (t) => { const w = world(t); await refuses(w, () => review({ dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, taskRef: TASK, commit: w.first, out: join(w.dir, "x.bundle") }), "unknown-commit"); assert.equal(existsSync(w.repo), false); }); test("the runner's git tools hold the host's limits and branch rule", async () => { const tools = await import("../../harness/src/git-tools.mjs"); const working = await import("../src/working.mjs"); assert.equal(tools.BUNDLE_MAX, working.BUNDLE_MAX); assert.equal(tools.HAVE_MAX, working.HAVE_MAX); for (const b of ["feat", "a/b", "-x", "a..b", "a.lock", "a/", "x".repeat(200), "x".repeat(201)]) assert.equal(tools.validBranch(b), working.validBranch(b), b); }); // Row 137 (#1631): the seed keeps a landed main, and a landing squashes the // reviewed diff onto main. const main = (w) => git(w.dir, `--git-dir=${w.repo}`, "rev-parse", "refs/heads/main"); const message = ({ old, base, patch_id }) => `Land it\n\nBase: main ${old}\nReviewed-base: ${base}\nPatch-id: ${patch_id}\n`; // The repository's worktrees other than itself. const trees = (w) => git(w.dir, `--git-dir=${w.repo}`, "worktree", "list", "--porcelain") .split("\n") .filter((l) => l.startsWith("worktree ") && l !== `worktree ${w.repo}`); // A commit on `branch` of the clone, from `from`, that writes `file`. function change(w, branch, from, file, text) { git(w.clone, "checkout", "-q", "-B", branch, from); writeFileSync(join(w.clone, file), `${text}\n`); git(w.clone, "add", file); git(w.clone, "commit", "-q", "-m", text); return git(w.clone, "rev-parse", "HEAD"); } const pushAs = (w, taskRef, branch) => push({ dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, taskRef, branch, bundle: w.bundle(branch) }); function landing(w, extra = {}) { const dir = mkdtempSync(join(w.dir, "land-")); const args = { dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, taskRef: TASK, dir, name: "land-0a1b2c", rootHits, message, ...extra }; return { dir, prepare: () => prepareLand(args), finish: (land, record) => finishLand({ ...args, land, record }), drop: (worktree) => dropLand({ ...args, worktree }) }; } // The task's change on feat, pushed and landed. async function landed(w) { const c = change(w, "feat", w.first, "README", "two"); await w.go(w.bundle()); const l = landing(w, { commit: c }); const land = await l.prepare(); await l.finish(land); await l.drop(land.worktree); return { c, land }; } test("a reseed moves root but leaves a landed main, and main forwards once root contains it", async (t) => { const w = world(t); const { land } = await landed(w); assert.equal(main(w), land.commit); // The root moves on without the landing. main isn't behind root, so the seed leaves it. writeFileSync(join(w.root, "NOTES"), "owner\n"); git(w.root, "add", "NOTES"); git(w.root, "commit", "-q", "-m", "owner"); const newer = git(w.root, "rev-parse", "HEAD"); const c2 = commit(w.clone, "three"); await w.go(w.bundle()); assert.equal(w.refs(), `refs/heads/main ${land.commit}\nrefs/heads/root ${newer}\n${REF} ${c2}`); // The owner merges main into the root; the next seed forwards main to it. git(w.root, "fetch", "-q", w.repo, "refs/heads/main"); git(w.root, "merge", "-q", "--no-edit", "FETCH_HEAD"); const merged = git(w.root, "rev-parse", "HEAD"); commit(w.clone, "four"); await w.go(w.bundle()); assert.equal(main(w), merged); }); test("after a landing, a push built on the root still lands and main stays where the landing put it", async (t) => { const w = world(t); const { land } = await landed(w); const c = change(w, "other", w.first, "LATER", "on the root"); const r = await pushAs(w, "vikunja:1/3", "other"); assert.equal(r.commit, c); assert.equal(main(w), land.commit); }); test("once the root shares no history with a landed main, a push is checked against main, not the root", async (t) => { const w = world(t); const { land } = await landed(w); // The owner starts the root over on an orphan branch, so root and main share nothing. git(w.root, "checkout", "-q", "--orphan", "fresh"); git(w.root, "rm", "-q", "-rf", "."); const orphan = commit(w.root, "fresh start"); // Built on main's history: it lands, though the root shares none of it. const c = change(w, "other", w.first, "LATER", "on main"); const r = await pushAs(w, "vikunja:1/3", "other"); assert.equal(r.commit, c); assert.equal(main(w), land.commit); assert.equal(git(w.dir, `--git-dir=${w.repo}`, "rev-parse", "refs/heads/root"), orphan); // Built on the new root: it shares history with the root only, and refuses. git(w.clone, "fetch", "-q", "origin"); git(w.clone, "checkout", "-q", "-B", "fresh", "origin/fresh"); commit(w.clone, "on the new root"); await refuses(w, () => pushAs(w, "vikunja:1/4", "fresh"), "unrelated-history"); }); test("a landing squashes the reviewed diff onto main as the bus host and moves main only at finish", async (t) => { const w = world(t); change(w, "feat", w.first, "README", "two"); const c2 = commit(w.clone, "three"); await w.go(w.bundle()); const l = landing(w, { commit: c2 }); const land = await lands(w, () => l.prepare()); assert.equal(land.old, w.first); assert.equal(land.base, w.first); assert.equal(land.reviewed, c2); assert.match(land.patch_id, /^[0-9a-f]{40}$/); assert.equal(land.worktree, join(l.dir, "land-0a1b2c")); assert.equal(main(w), w.first, "prepare moves nothing"); const at = `--git-dir=${w.repo}`; assert.equal(git(w.dir, at, "rev-parse", `${land.commit}^@`), w.first, "one parent, the old main"); assert.equal(git(w.dir, at, "rev-parse", `${land.commit}^{tree}`), git(w.dir, at, "rev-parse", `${c2}^{tree}`)); assert.equal(git(w.dir, at, "log", "-1", "--format=%an <%ae>|%cn <%ce>", land.commit), "Mosaic bus host |Mosaic bus host "); assert.equal(git(w.dir, at, "log", "-1", "--format=%B", land.commit), message({ old: w.first, base: w.first, patch_id: land.patch_id }).trim()); assert.equal(readFileSync(join(land.worktree, "README"), "utf8"), "three\n"); assert.equal(git(land.worktree, "rev-parse", "HEAD"), land.commit); assert.equal(git(land.worktree, "status", "--porcelain"), ""); const seen = []; const done = await lands(w, () => l.finish(land, async (m) => seen.push(m))); assert.deepEqual(done, { previous: w.first, commit: land.commit }); assert.deepEqual(seen, [done]); assert.equal(main(w), land.commit); await l.drop(land.worktree); assert.equal(existsSync(land.worktree), false); assert.deepEqual(trees(w), []); }); test("a landing refuses ref-moved, base-conflict and root-file before it makes a commit", async (t) => { const w = world(t); const { c } = await landed(w); // The verdict approved another commit than the work ref's tip, or a task never pushed. await refuses(w, () => landing(w, { commit: w.first }).prepare(), "ref-moved"); await refuses(w, () => landing(w, { commit: c, taskRef: "vikunja:1/9" }).prepare(), "ref-moved"); // The same change again no longer applies to main. await refuses(w, () => landing(w, { commit: c }).prepare(), "base-conflict"); // A change that conflicts with what landed. const clash = change(w, "clash", w.first, "README", "clash"); await pushAs(w, "vikunja:1/3", "clash"); await refuses(w, () => landing(w, { commit: clash, taskRef: "vikunja:1/3" }).prepare(), "base-conflict"); // An empty diff. git(w.clone, "checkout", "-q", "-B", "empty", w.first); git(w.clone, "commit", "-q", "--allow-empty", "-m", "nothing"); const empty = git(w.clone, "rev-parse", "HEAD"); await pushAs(w, "vikunja:1/4", "empty"); await refuses(w, () => landing(w, { commit: empty, taskRef: "vikunja:1/4" }).prepare(), "base-conflict"); // A new file at the root. const rooty = change(w, "rooty", w.first, "NEW", "root file"); await pushAs(w, "vikunja:1/5", "rooty"); await assert.rejects(landing(w, { commit: rooty, taskRef: "vikunja:1/5" }).prepare(), (e) => e instanceof WorkingError && e.code === "root-file" && /NEW/.test(e.message)); assert.deepEqual(trees(w), [], "no worktree is left"); }); test("a landing whose applied diff isn't the reviewed one refuses patch-drift and leaves no worktree", async (t) => { const w = world(t); const c = change(w, "feat", w.first, "README", "two"); await w.go(w.bundle()); // A git that adds a line to what `apply --index` staged, as land-work's test does. const env = wrapGit( w, `"$REAL" "$@"; rc=$? dir=; apply=0; prev= for a in "$@"; do [ "$prev" = -C ] && dir=$a; [ "$a" = apply ] && apply=1; prev=$a; done if [ $apply = 1 ] && [ $rc -eq 0 ]; then printf 'tampered\\n' >> "$dir/README" && "$REAL" -C "$dir" add README; fi exit $rc`, ); await assert.rejects(landing(w, { commit: c, env }).prepare(), (e) => e instanceof WorkingError && e.code === "patch-drift" && /^applied [0-9a-f]{40}, reviewed [0-9a-f]{40}$/.test(e.message)); assert.equal(main(w), w.first); assert.deepEqual(trees(w), [], "no worktree is left"); // The same landing with the real git prepares. const l = landing(w, { commit: c }); const land = await l.prepare(); assert.equal(land.old, w.first); await l.drop(land.worktree); assert.deepEqual(trees(w), []); }); test("finish refuses base-moved when main moved, and a record that throws moves main back", async (t) => { const w = world(t); const c = change(w, "feat", w.first, "README", "two"); await w.go(w.bundle()); const l = landing(w, { commit: c }); const land = await l.prepare(); await assert.rejects(l.finish(land, async () => { throw new Error("not recorded"); }), /not recorded/); assert.equal(main(w), w.first); const at = `--git-dir=${w.repo}`; git(w.dir, at, "update-ref", "refs/heads/main", c, w.first); await assert.rejects(l.finish(land), (e) => e instanceof WorkingError && e.code === "base-moved"); assert.equal(main(w), c); // A lock on main when record() fails, so main can't go back. git(w.dir, at, "update-ref", "refs/heads/main", w.first, c); const lock = join(w.repo, "refs/heads/main.lock"); await assert.rejects(l.finish(land, async () => { writeFileSync(lock, ""); throw new Error("not recorded"); }), (e) => e instanceof WorkingError && e.code === "rollback-failed" && e.message.includes(`main still names ${land.commit}`) && e.message.includes(`back to ${w.first}`)); rmSync(lock); assert.equal(main(w), land.commit); await l.drop(land.worktree); assert.deepEqual(locks(w.repo), []); assert.deepEqual(trees(w), []); }); // Row 191: a halt at receipt time must not prevent the existing compare-and-swap rollback. test('halted push receipt rolls back only its unrecorded ref and cleans temporary refs', async t => { const { initializeControl, transition } = await import('../../processing-control/src/state.mjs'); const { withProcessing } = await import('../../processing-control/src/effects.mjs'); const w = world(t); initializeControl(w.dataRoot); git(w.clone, 'checkout', '-q', '-b', 'feat'); commit(w.clone, 'two'); await assert.rejects(withProcessing(w.dataRoot, () => w.go(w.bundle(), { record: async () => { transition(w.dataRoot, { business: 'acme', human: 'jason', halted: true }); throw Error('not recorded after halt'); } })), /not recorded after halt/); assert.equal(w.refs(), `refs/heads/main ${w.first}\nrefs/heads/root ${w.first}`); }); test('AU prepareRevert makes a new commit, finishLand preserves history and rechecks the reverse window',async t=>{ const w=world(t);await initializeWorking({dataRoot:w.dataRoot,business:'acme',project:'stack',root:w.root}); git(w.clone,'checkout','-q','-b','feat');const changed=commit(w.clone,'two');await w.go(w.bundle()); const dir=join(w.dir,'land');mkdirSync(dir); const landed=await prepareLand({dataRoot:w.dataRoot,business:'acme',project:'stack',root:w.root,taskRef:TASK,commit:changed,dir,name:'original',rootHits:()=>[],message:()=> 'Change'}); await finishLand({dataRoot:w.dataRoot,business:'acme',project:'stack',land:landed});await dropLand({dataRoot:w.dataRoot,business:'acme',project:'stack',worktree:landed.worktree}); const {prepareRevert}=await import('../src/working.mjs'); const reverse=await prepareRevert({dataRoot:w.dataRoot,business:'acme',project:'stack',commit:landed.commit,dir,name:'reverse',decision:'decision-1'}); assert.equal(git(w.dir,`--git-dir=${w.repo}`,'show',`${reverse.commit}:README`),'one'); assert.equal(git(w.dir,`--git-dir=${w.repo}`,'rev-parse',`${reverse.commit}^`),landed.commit); await assert.rejects(finishLand({dataRoot:w.dataRoot,business:'acme',project:'stack',land:reverse,record:()=>{throw Object.assign(new Error('closed'),{code:'reverse-window-closed'});}}),{code:'reverse-window-closed'}); assert.equal(git(w.dir,`--git-dir=${w.repo}`,'rev-parse','main'),landed.commit); await finishLand({dataRoot:w.dataRoot,business:'acme',project:'stack',land:reverse}); assert.match(git(w.dir,`--git-dir=${w.repo}`,'log','-1','--format=%B','main'),/decision-1/); await dropLand({dataRoot:w.dataRoot,business:'acme',project:'stack',worktree:reverse.worktree}); });