mergeContribution

tcmts.cli.contributions.mergeContribution · #kvds3uls · term · runs on the server

uses tcmts.cli.contributions.typeProblems, which uses node:util

Merge a contribution into its target, by history: a fast-forward, or a three-way merge with no conflicts. With conflicts it's refused, saying how to bring the target in first.

import { needsFixing } from "#codebase/tcmts/cli/codebase";
import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { ContributionError, find, mergeable, onBranch, quietly, typeProblems } from "#codebase/tcmts/cli/contributions";
import { mergeBranch, silently } from "#codebase/tcmts/cli/merge";
import { shortHash } from "#codebase/tcmts/core/hash";
import { cleanMessage } from "#codebase/tcmts/store";

export function mergeContribution(
  cb: Codebase,
  id: number,
  by: string,
): { mergedAs: string; todo: number } {
  const c = find(cb, id);
  if (c.status !== "open") throw new ContributionError(`#${id} is ${c.status}`);
  const m = mergeable(cb, c);
  if (m.kind === "conflicts") {
    throw new ContributionError(
      `#${id} conflicts with ${c.target} in ${m.names.join(", ")}: merge ${c.target} into ${c.source} (\`tcm pull <remote> ${c.target}\` on that branch), resolve, push, and merge again`,
    );
  }
  // what it brings must typecheck here: receiving it checked only that it's what it claims to be
  if (m.kind !== "up-to-date") {
    const problems = typeProblems(cb, c);
    if (problems.length)
      throw new ContributionError(
        `#${id} doesn't typecheck, so it isn't merged (fix it on ${c.source}, push, and merge again):\n${problems.join("\n")}`,
      );
  }
  // what merges as text must typecheck merged too: tried first on a branch of its own, so a merge that would
  // leave a conflict on the target isn't made
  if (m.kind === "clean" && m.asText?.length) {
    const trial = `trial/merge-${id}-${Date.now()}`;
    onBranch(cb, c.target, () => cb.store.createBranch(trial));
    let left: string[] = [];
    try {
      left = onBranch(cb, trial, () => {
        cb.store.endBatch();
        return silently(() => mergeBranch(cb, c.source, `merge #${id} (tried)`)).conflicts;
      });
    } finally {
      cb.store.deleteBranch(trial);
      for (const [k] of cb.store.settings(`conflict ${trial} `)) cb.store.deleteSetting(k);
    }
    if (left.length)
      throw new ContributionError(
        `#${id} conflicts with ${c.target} in ${left.join(", ")} (both edited apart, but merged ${left.length > 1 ? "they don't" : "it doesn't"} typecheck): merge ${c.target} into ${c.source} (\`tcm pull <remote> ${c.target}\` on that branch), resolve, push, and merge again`,
      );
  }
  const from = cb.store.head(c.target);
  const todo = onBranch(cb, c.target, () => {
    cb.store.endBatch();
    if (m.kind !== "up-to-date") {
      // (what it's for, as the contribution says, where it says more than its title)
      cb.store.message = c.description.trim()
        ? cleanMessage(`${c.title}\n\n${c.description}`)
        : undefined;
      try {
        quietly(() => mergeBranch(cb, c.source, `merge #${id}: ${c.title}`));
      } finally {
        cb.store.message = undefined;
      }
    }
    return needsFixing(cb).length;
  });
  const as = cb.store.head(c.target);
  cb.store.setContributionStatus(id, "merged", { from, as });
  cb.store.addComment(id, { author: by, body: `merged into ${c.target} as ${shortHash(as)}` });
  return { mergedAs: as, todo };
}

uses

used by