landBranch

tcmts.cli.merge.landBranch · #73bfe6aj · term · runs on the server

uses node:child_process

Land source on this branch (the codebase's own: main), as a session lands: this branch is merged into source first, there (a merge it doesn't finish waits there, this branch untouched); then what needs fixing on source, and the tests the change reaches, run there; and only if nothing's left and they pass does source land here, where a merge that wouldn't be clean (this branch moved meanwhile) isn't made. Each step on source is a tcm of its own (TCM_BRANCH), as an agent's session takes it.

import { TCM_COMMAND } from "#codebase/tcmts/cli/checkers";
import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { mergeApart } from "#codebase/tcmts/cli/merge";
import { bold, dim, red, yellow } from "#codebase/tcmts/cli/style";
import { UsageError } from "#codebase/tcmts/cli/ui";
import { spawnSync } from "node:child_process";

export function landBranch(
  cb: Codebase,
  source: string,
  o: { noFastForward?: boolean; squash?: boolean },
): number {
  const into = cb.store.branch().name;
  if (!cb.store.hasBranch(source)) throw new UsageError(`no branch named '${source}'`);
  const self = ((globalThis as unknown as Record<symbol, unknown>)[TCM_COMMAND] as
    | string[]
    | undefined) ?? [process.execPath, process.argv[1]!];
  // (what each says is said here, as this tcm says things: an agent's server hears it)
  const on = (args: string[], show = true) => {
    const r = spawnSync(self[0]!, [...self.slice(1), ...args], {
      env: { ...process.env, TCM_BRANCH: source },
      encoding: "utf8",
      stdio: ["ignore", "pipe", "pipe"],
      maxBuffer: 64 * 1024 * 1024,
    });
    if (show && (r.stdout || r.stderr)) console.log(`${r.stdout ?? ""}${r.stderr ?? ""}`.trimEnd());
    return r;
  };
  console.log(bold(`landing ${source} on ${into}: ${into} into ${source} first`));
  const up = on(["merge", into]);
  if (up.status !== 0) {
    console.log(
      red(
        `\nnothing landed on ${into}: merging it into ${source} leaves something to settle there (TCM_BRANCH=${source} tcm todo), and once it's settled, merge again`,
      ),
    );
    return 1;
  }
  // what both changed, merged as text: clean, but worth reading before it lands (tests can't tell what each meant)
  const both = [
    ...new Set(
      [
        ...`${up.stdout ?? ""}`.replace(/\x1b\[[0-9;]*m/g, "").matchAll(/merged as text (\S+)/g),
      ].map((m) => m[1]!),
    ),
  ];
  if (both.length)
    console.log(
      `${yellow("both changed")} ${both.map((b) => bold(b)).join(", ")} ${dim(`(merged as text, cleanly: worth reading on ${source} before it lands)`)}`,
    );
  const todo = on(["todo"], false);
  const said = (todo.stdout ?? "").trim();
  if (said && said !== "nothing to do") {
    console.log(
      `\n${said}\n${red(`nothing landed on ${into}: fix these on ${source}, then merge again`)}`,
    );
    return 1;
  }
  console.log(bold(`\ntesting ${source}`));
  const tests = on(["test"]);
  if (tests.status !== 0) {
    console.log(red(`\nnothing landed on ${into}: these fail on ${source}`));
    return 1;
  }
  console.log(bold(`\nlanding ${source} on ${into}`));
  const was = cb.store.head();
  const r = mergeApart(cb, source, { ...o, keep: false });
  if (r.left.conflicts.length || r.left.failed.length) {
    console.log(
      red(
        `${into} moved meanwhile, and merging ${source} into it isn't clean: merge again (${into} comes into ${source} first)`,
      ),
    );
    return 1;
  }
  // squashed, the landing isn't in source's history: merged into it (no names change), so the next merge of this
  // branch into source starts from the landing, not from before it, where each name source changed again since
  // would conflict with its own landed version
  if (
    o.squash &&
    cb.store.head() !== was &&
    !cb.store.isAncestor(cb.store.head(source), cb.store.head())
  ) {
    const after = on(["merge", into], false);
    if (after.status !== 0)
      console.log(
        `${`${after.stdout ?? ""}${after.stderr ?? ""}`.trimEnd()}\n${yellow(`landed, but merging ${into} into ${source} after it leaves something to settle there (TCM_BRANCH=${source} tcm todo)`)}`,
      );
  }
  return 0;
}

uses

used by