mergeApart

tcmts.cli.merge.mergeApart · #h6jihzkd · term · runs on the server

uses tcmts.cli.merge.mergeBranch, which uses tcmts.cli.style.dim, which uses node:util

Merge theirs into this branch, and only if nothing's left to settle: a merge that leaves conflicts, or definitions that stop typechecking, doesn't move the branch. With keep, it waits apart (#yisipbwb): scratch files hold what to settle (each conflict's sides merged, as tcm edit merges them, and what stopped typechecking), and saving them makes the merge once nothing's left (inPendingMerge); tcm merge --abort drops it. Without, it's simply not made (a landing on main, which no one settles there).

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { mergeBranch, mergeLeft, pendingMerge, writeMergeFiles } from "#codebase/tcmts/cli/merge";
import type { MergeResult, PendingMerge } from "#codebase/tcmts/cli/merge";
import { bold, dim, red, yellow } from "#codebase/tcmts/cli/style";
import { UsageError } from "#codebase/tcmts/cli/ui";

export function mergeApart(
  cb: Codebase,
  theirs: string,
  o: {
    command?: string;
    noFastForward?: boolean;
    squash?: boolean;
    keep: boolean;
    scratch?: (f?: string, fallback?: string) => string;
  },
): MergeResult & { left: ReturnType<typeof mergeLeft> } {
  const branch = cb.store.branch().name;
  const waiting = pendingMerge(cb, branch);
  if (waiting)
    throw new UsageError(
      `${branch} is merging ${waiting.theirs} still: finish that (\`tcm todo\` says what's left) or drop it (\`tcm merge --abort\`) first`,
    );
  cb.store.endBatch();
  const from = cb.store.head();
  const work = cb.store.workOf(branch);
  const keys = new Set(cb.store.settings(`conflict ${branch} `).map(([k]) => k));
  const r = mergeBranch(cb, theirs, o.command, o.noFastForward, o.squash);
  cb.store.endBatch();
  const none = { conflicts: [], failed: [] };
  if (cb.store.head() === from) return { ...r, left: none };
  const left = mergeLeft(
    cb,
    work.todo.map((t) => t.hash),
  );
  if (!left.conflicts.length && !left.failed.length) return { ...r, left };
  // what to settle, written while the merge's names are there to read
  if (o.keep && o.scratch) writeMergeFiles(cb, left, o.scratch);
  const pending: PendingMerge = {
    theirs,
    from,
    head: cb.store.head(),
    work: cb.store.workOf(branch),
    before: work.todo.map((t) => t.hash),
    conflictKeys: cb.store
      .settings(`conflict ${branch} `)
      .map(([k]) => k)
      .filter((k) => !keys.has(k)),
  };
  cb.store.setHead(branch, from, `merge ${theirs} (kept apart)`, true);
  cb.store.setWorkOf(branch, work);
  if (o.keep) cb.store.setSetting(`merging ${branch}`, JSON.stringify(pending));
  else for (const k of pending.conflictKeys) cb.store.deleteSetting(k);
  cb.refreshShim(true);
  console.log(
    `\n${red(o.keep ? "not merged yet" : "not merged")} ${bold(theirs)} into ${bold(branch)}: ${[
      left.conflicts.length && `${left.conflicts.length} conflicted`,
      left.failed.length &&
        `${left.failed.length} no longer typecheck${left.failed.length === 1 ? "s" : ""}`,
    ]
      .filter(Boolean)
      .join(", ")} merged. ${branch} is as it was.`,
  );
  for (const c of left.conflicts.slice(0, 20)) console.log(`  ${yellow("conflicted")} ${bold(c)}`);
  for (const f of left.failed.slice(0, 20))
    console.log(`  ${yellow("needs fixing")} ${bold(f.name)}: ${f.message.split("\n")[0]}`);
  if (left.conflicts.length + left.failed.length > 40)
    console.log(dim("  … and more (`tcm todo` lists them)"));
  if (o.keep)
    console.log(
      dim(
        `The merge waits apart: commands on ${branch} work on it as merged (\`tcm todo\` says what's left), and once a\ncommand leaves nothing to settle, ${branch} moves to it. The scratch files above hold what to settle: change them,\nthen \`tcm update <file>\`. \`tcm merge --abort\` drops the merge.`,
      ),
    );
  return { ...r, left, conflicts: left.conflicts };
}

uses

used by