// 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/);
});