mergeStates

tcmts.cli.merge.mergeStates · #nusrtumt · term · runs anywhere

Three-way merge of namespaces. A name changed on one side takes that side's value; changed on both sides to the same thing, it agrees; changed differently, it becomes conflicted (several hashes on one name).

import type { NameState } from "#codebase/tcmts/cli/diff";
import { same } from "#codebase/tcmts/cli/merge";
import type { MergeResult } from "#codebase/tcmts/cli/merge";

export function mergeStates(
  base: NameState,
  ours: NameState,
  theirs: NameState,
  propagated: { ours: Set<string>; theirs: Set<string> } = { ours: new Set(), theirs: new Set() },
): MergeResult {
  const merged: NameState = new Map();
  const conflicts: string[] = [];
  const keptOverDelete: string[] = [];
  const patches = new Map<string, string>();
  // what any side had is replaced by what's merged, so the code of either side (and the base's, a stale definition
  // still using it) follows
  const patch = (to: Set<string> | undefined, ...from: (Set<string> | undefined)[]) => {
    if (to?.size !== 1) return;
    const [h] = to;
    for (const f of from) if (f?.size === 1 && !f.has(h!)) patches.set([...f][0]!, h!);
  };
  for (const path of [...new Set([...base.keys(), ...ours.keys(), ...theirs.keys()])].sort()) {
    const b = base.get(path);
    const o = ours.get(path);
    const t = theirs.get(path);
    let r: Set<string> | undefined;
    if (same(o, t)) {
      r = o;
      patch(r, b);
    } else if (same(o, b)) {
      r = t;
      patch(r, b);
    } else if (same(t, b)) {
      r = o;
      patch(r, b);
    } else if (o?.size && t?.size && (propagated.ours.has(path) || propagated.theirs.has(path))) {
      // A side whose "change" was only re-hashing after a dependency moved didn't edit this definition.
      // Keep the edit (or ours, if neither edited it); propagation then re-derives it against the other side's
      // updates, and the patch points the other side's dependents at it.
      const takeTheirs = propagated.ours.has(path) && !propagated.theirs.has(path);
      r = takeTheirs ? t : o;
      patch(r, o, t, b);
    } else if (!o?.size || !t?.size) {
      // deleted on one side, changed on the other: deleting loses information, changing doesn't
      r = o?.size ? o : t;
      keptOverDelete.push(path);
    } else {
      r = new Set([...o, ...t]);
      conflicts.push(path);
    }
    if (r?.size) merged.set(path, new Set(r));
  }
  return { merged, conflicts, keptOverDelete, patches };
}

uses

used by