mergeStates
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 }; }