testCrissCrossMerge

tcmts.cli.test.e2e.merge.testCrissCrossMerge · #c6vuezac · term

Criss-cross merges: two branches that merged each other have two best common ancestors, each lacking the other's change. Merged from the newer one alone, the other's change looks like no change, so a side undoing it is lost; merged into a virtual base first, it isn't. And a conflict between them that the two merges resolved differently is a conflict again. (The two merges must differ: the same merge both ways is the same causal, one ancestor.)

import { okIn, tcmIn, tempDir } from "#codebase/tcmts/cli/test/e2e/harness";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";

export const testCrissCrossMerge = () => {
  const dir = tempDir("criss-cross");
  const run = (...args: string[]) => tcmIn(dir, ...args);
  const tcm = (...args: string[]) => okIn(dir, ...args);
  const update = (code: string) => {
    fs.writeFileSync(path.join(dir, "scratch.ts"), `// tcm: namespace lib\n${code}\n`);
    tcm("update");
  };
  const hashesAt = (name: string) =>
    [
      ...tcm("ls", name).matchAll(new RegExp(`^${name.replace(".", "\\.")}\\s+#([a-z2-7]+)`, "gm")),
    ].map((m) => m[1]!);
  tcm("init");
  update(`export const f = () => 0;\nexport const g = () => 0;\nexport const k = () => 0;`);
  tcm("branch", "a");
  tcm("branch", "b");

  tcm("switch", "a");
  update(`export const f = () => 1;\nexport const k = () => 1;`);
  const ka = hashesAt("lib.k")[0]!;
  tcm("branch", "a1"); // a as it is now, for b to merge
  update(`export const h = () => 3;`); // (a moves on before merging b)
  tcm("switch", "b");
  update(`export const g = () => 2;\nexport const k = () => 2;`); // (the newer of the two)
  const kb = hashesAt("lib.k")[0]!;

  // each merges the other's change, keeping its own k
  tcm("switch", "a");
  assert.match(run("merge", "b").out, /conflict lib\.k/);
  tcm("resolve", "lib.k", `#${ka}`);
  tcm("switch", "b");
  assert.match(run("merge", "a1").out, /conflict lib\.k/);
  tcm("resolve", "lib.k", `#${kb}`);

  // then each undoes the other's change
  update(`export const f = () => 0;`);
  tcm("switch", "a");
  update(`export const g = () => 0;`);

  const merged = run("merge", "b");
  assert.doesNotMatch(merged.out, /conflict lib\.[fg]\b/);
  assert.equal(tcm("run", "lib.f").trim(), "0", "b undid a's f");
  assert.equal(tcm("run", "lib.g").trim(), "0", "a undid b's g");
  assert.equal(tcm("run", "lib.h").trim(), "3");
  assert.equal(merged.status, 1, "a conflict makes merge exit non-zero");
  assert.match(merged.out, /conflict lib\.k/, "the two merges resolved k differently");
  assert.deepEqual(hashesAt("lib.k").sort(), [ka, kb].sort());
}

uses