testThreeWayMerge

tcmts.cli.test.e2e.merge.testThreeWayMerge · #7x5tlkvk · term

Three-way merge: one-sided changes combine, real conflicts stay conflicts, code follows the other side.

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 testThreeWayMerge = () => {
  const dir = tempDir("merge");
  const run = (...args: string[]) => tcmIn(dir, ...args);
  const tcm = (...args: string[]) => okIn(dir, ...args);
  const scratch = (text: string) => fs.writeFileSync(path.join(dir, "scratch.ts"), text);
  const hashesAt = (name: string) =>
    [
      ...tcm("ls", name).matchAll(new RegExp(`^${name.replace(".", "\\.")}\\s+#([a-z2-7]+)`, "gm")),
    ].map((m) => m[1]);
  tcm("init");
  tcm("add");
  scratch(`// tcm: namespace lib
import { square } from "#codebase";
export const sumSquares = (xs: number[]) => xs.map(square).reduce((a, b) => a + b, 0);
export const report = (xs: number[]) => \`sum: \${sumSquares(xs)}\`;
`);
  tcm("add");
  tcm("branch", "feature");
  // (merged into a branch of main's: main itself only lands what's settled)
  tcm("branch", "m");

  tcm("switch", "feature");
  scratch(`export const square = (x: number): number => x * x * 10;\n`);
  tcm("update"); // also re-hashes sumSquares and report on feature, without editing them
  scratch(
    `// tcm: namespace lib\nimport { sumSquares } from "#codebase/lib";\nexport const fromFeature = 1;\nexport const report = (xs: number[]) => \`feature: \${sumSquares(xs)}\`;\n`,
  );
  tcm("update");

  tcm("switch", "m");
  scratch(`// tcm: namespace lib
import { square } from "#codebase";
export const sumSquares = (xs: number[]) => xs.map(square).reduce((a, b) => a + b, 100);
export const report = (xs: number[]) => \`main: \${sumSquares(xs)}\`;
export const fromMain = 2;
`);
  tcm("update");

  const merged = run("merge", "feature");
  assert.equal(merged.status, 1, "a conflict makes merge exit non-zero");
  assert.match(merged.out, /conflict lib\.report/);
  assert.doesNotMatch(
    merged.out,
    /conflict lib\.sumSquares/,
    "feature only re-hashed sumSquares; main's edit wins",
  );
  assert.match(merged.out, /\+ lib\.fromFeature/);
  assert.equal(
    hashesAt("lib.report").length,
    2,
    "propagation must not silently resolve the conflict",
  );
  assert.equal(hashesAt("lib.sumSquares").length, 1);

  // main's sumSquares (+100) now runs against feature's square (*10): (1 + 4) * 10 + 100
  scratch(
    `// tcm: namespace lib\nimport { sumSquares } from "#codebase/lib";\nexport const probe = () => sumSquares([1, 2]);\n`,
  );
  tcm("add");
  assert.equal(tcm("run", "lib.probe").trim(), "150");

  tcm("resolve", "lib.report", `#${hashesAt("lib.report")[0]}`);
  assert.match(tcm("todo"), /nothing to do/);

  // the merge base moved: merging again brings nothing, and later feature changes come in alone
  assert.match(tcm("merge", "feature"), /no changes/);
  tcm("switch", "feature");
  scratch(`// tcm: namespace lib\nexport const later = 3;\n`);
  tcm("add");
  tcm("switch", "m");
  const again = tcm("merge", "feature");
  assert.match(again, /added\s+\+ lib\.later/);
  assert.doesNotMatch(again, /conflict/);
}

uses