Vectrum
Sign In
74 lines · 3.4 KB · TypeScript
// 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/);
});