mergeContribution
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
- tcmts.cli.codebase.Codebase
- tcmts.cli.codebase.needsFixing
- tcmts.cli.contributions.ContributionError
- tcmts.cli.contributions.find
- tcmts.cli.contributions.mergeable
- tcmts.cli.contributions.onBranch
- tcmts.cli.contributions.quietly
- tcmts.cli.contributions.typeProblems
- tcmts.cli.merge.mergeBranch
- tcmts.cli.merge.silently
- tcmts.core.hash.shortHash
- tcmts.store.cleanMessage