settleMerge

tcmts.cli.merge.settleMerge · #lezbaxyr · term · runs on the server

uses tcmts.cli.merge.mergeText, which uses tcmts.cli.merge.silently, which uses process

After a merge's names are set (and its patches recorded), make every definition on the branch use what the branch names. A name a side deleted that the other side's code uses is kept. A definition both sides edited is merged as text where the edits are apart and the result typechecks (#ipxzlfa3); otherwise it stays a conflict, and code derived from either side keeps using that side's definition, until tcm resolve keeps one. Then everything that used what the merge replaced is re-derived against what replaced it, in dependency order: what doesn't typecheck goes to tcm todo. Merges and reverts both settle so. The conflicts left are kept with each definition's side, as labels call them, and the base's (#alvelgcx).

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { keyPath, stateOf } from "#codebase/tcmts/cli/diff";
import type { NameState } from "#codebase/tcmts/cli/diff";
import { mergeText, recordConflicts } from "#codebase/tcmts/cli/merge";
import type { MergeResult, Settled } from "#codebase/tcmts/cli/merge";

export function settleMerge(
  cb: Codebase,
  states: { base: NameState; ours: NameState; theirs: NameState },
  result: MergeResult,
  labels: { ours: string; theirs: string },
): Settled {
  const todoBefore = new Set(cb.store.todos().map((t) => t.hash));
  const kind = (h: string) => cb.store.getDef(h)!.kind;

  // a side's code using a name the other deleted: the name stays (that code would use what nothing names)
  const keptForUse: string[] = [];
  const named = new Map<string, string>();
  for (const s of [states.ours, states.theirs])
    for (const [key, hs] of s) for (const h of hs) if (!result.merged.has(key)) named.set(h, key);
  if (named.size) {
    const restore = new Map<string, string>();
    for (const u of cb.store.unnamedUses())
      if (named.has(u.uses) && !result.patches.has(u.uses)) restore.set(u.uses, named.get(u.uses)!);
    if (restore.size)
      cb.store.transaction(() => {
        for (const [h, key] of restore) {
          // (a merge's names are the merge's, a library's among them)
          cb.store.unguarded(() =>
            cb.store.addName(keyPath(key), h, kind(h), "merge (kept: still used)"),
          );
          keptForUse.push(key);
        }
      });
  }

  // both sides edited it: merged as text, if the edits are apart and it typechecks
  const textMerged: string[] = [];
  const one = (s: Set<string> | undefined) => (s?.size === 1 ? [...s][0] : undefined);
  const byText = (key: string) => {
    const [b, o, t] = [states.base, states.ours, states.theirs].map((s) => one(s.get(key)));
    return !!(b && o && t && mergeText(cb, keyPath(key), b, o, t));
  };
  let tried = result.conflicts.filter((key) => {
    if (!byText(key)) return true;
    textMerged.push(key);
    return false;
  });

  // what used a definition the merge replaced (each side's code, the other's changes) is re-derived against its
  // replacement
  if (result.patches.size) cb.propagate(new Map(result.patches));

  // a text merge that didn't typecheck against the code as it was mid-merge is tried again against it settled, as
  // `tcm check` would check it (and again while that merges more: one can use another)
  for (let more = true; more && tried.length;) {
    more = false;
    const now = stateOf(cb.store.names(undefined, true));
    tried = tried.filter((key) => {
      if ((now.get(key)?.size ?? 0) < 2 || !byText(key)) return true;
      textMerged.push(key);
      more = true;
      return false;
    });
  }

  const after = stateOf(cb.store.names(undefined, true));
  // (a side's definition re-derived since is still that side's)
  const sideOf = (key: string, h: string): "ours" | "theirs" | undefined => {
    const is = (s: NameState) =>
      [...(s.get(key) ?? [])].some((x) => x === h || cb.store.patched(x) === h);
    return is(states.ours) ? "ours" : is(states.theirs) ? "theirs" : undefined;
  };
  const conflicts = result.conflicts
    .filter((key) => (after.get(key)?.size ?? 0) > 1)
    .map((key) => ({
      key,
      sides: [...after.get(key)!].map((hash) => ({ hash, side: sideOf(key, hash) })),
    }));
  const live = cb.liveHashes();
  const failed = cb.store
    .todos()
    .filter((t) => !todoBefore.has(t.hash) && live.has(t.hash))
    .flatMap((t) => cb.store.pathsOf(t.hash));
  recordConflicts(cb, conflicts, states.base, labels);
  return { textMerged, keptForUse, conflicts, failed };
}

uses

used by