// Forks at the git level: copying a repository, and moving commits between a fork and its original. // Run with `bun test`. import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { after, before, test } from "node:test"; const tmp = mkdtempSync(path.join(os.tmpdir(), "vectrum-fork-test-")); process.env.DATA_ROOT = path.join(tmp, "data"); const sh = (cwd: string, ...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: "pipe" }).trim(); const commit = (cwd: string, message: string) => sh(cwd, "-c", "user.name=a", "-c", "user.email=a@x", "commit", "-q", "--allow-empty", "-m", message); let git: typeof import("./git"); let upstream: string; let work: string; before(async () => { git = await import("./git"); upstream = path.join(tmp, "upstream.git"); await git.initBareRepo(upstream); work = path.join(tmp, "work"); sh(tmp, "init", "-q", "-b", "main", work); commit(work, "first"); sh(work, "tag", "v1"); sh(work, "checkout", "-q", "-b", "feature"); commit(work, "feature work"); sh(work, "push", "-q", upstream, "main", "feature", "v1"); }); after(() => rmSync(tmp, { recursive: true, force: true })); test("a fork copies the default branch (or all of them) and tags, and doesn't point back", async () => { const one = path.join(tmp, "one.git"); await git.cloneBare(upstream, one, "main"); assert.deepEqual((await git.listBranches(one)).map((b) => b.name), ["main"]); assert.deepEqual(await git.listTags(one), ["v1"]); assert.equal(await git.defaultBranch(one), "main"); assert.equal(sh(one, "remote"), ""); assert.equal(sh(one, "config", "http.receivepack"), "true"); const all = path.join(tmp, "all.git"); await git.cloneBare(upstream, all, null); assert.deepEqual((await git.listBranches(all)).map((b) => b.name).sort(), ["feature", "main"]); }); test("a fork's branch is fetched into the original, unreferenced or into a ref", async () => { const fork = path.join(tmp, "fork.git"); await git.cloneBare(upstream, fork, "main"); const clone = path.join(tmp, "fork-work"); sh(tmp, "clone", "-q", fork, clone); commit(clone, "fix in the fork"); sh(clone, "push", "-q", "origin", "main:fix"); const sha = (await git.revParse(fork, "refs/heads/fix"))!; assert.equal(await git.commitExists(upstream, sha), false); assert.equal(await git.fetchBranch(upstream, fork, "fix"), sha); assert.equal(await git.commitExists(upstream, sha), true); assert.equal(await git.isBranch(upstream, "fix"), false, "no branch appears in the original"); assert.equal(await git.fetchBranch(upstream, fork, "fix", "refs/pull/1/head"), sha); assert.equal(await git.revParse(upstream, "refs/pull/1/head"), sha); // Force-pushed in the fork: the ref follows. sh(clone, "-c", "user.name=a", "-c", "user.email=a@x", "commit", "-q", "--amend", "--allow-empty", "-m", "fix, again"); sh(clone, "push", "-q", "-f", "origin", "HEAD:fix"); const amended = (await git.revParse(fork, "refs/heads/fix"))!; assert.equal(await git.fetchBranch(upstream, fork, "fix", "refs/pull/1/head"), amended); assert.equal(await git.revParse(upstream, "refs/pull/1/head"), amended); await assert.rejects(git.fetchBranch(upstream, fork, "missing"), /no branch/); await assert.rejects(git.fetchBranch(upstream, fork, "--upload-pack=touch x"), /no branch|Invalid/); });