push

tcmts.cli.sync.push · #izw4ho5w · term · runs on the server

uses node:os

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { diffNamespaces, keyPath, stateOf } from "#codebase/tcmts/cli/diff";
import { branchConflicts, conflictAt } from "#codebase/tcmts/cli/merge";
import { bold, dim, green } from "#codebase/tcmts/cli/style";
import { branchFor, openRemote, sendHistory, sendLints, sendReleases, sendResults } from "#codebase/tcmts/cli/sync";
import os from "node:os";

export async function push(
  cb: Codebase,
  spec: string,
  remoteBranch: string | undefined,
  force: boolean,
) {
  const { remote, name } = openRemote(cb, spec);
  if (!name) throw new Error(`push needs a configured remote: \`tcm remote add <name> ${spec}\``);
  // a contributor's branches are theirs, under @<handle>/; what's taken on trust (releases, test results, lint
  // findings) is the maintainer's to send
  const caller = await remote.whoami();
  const branch = branchFor(caller, remoteBranch ?? cb.store.branch().name);
  const trusted = caller.role === "maintainer";
  const names = cb.store.names(undefined, true);
  const mine = cb.store.head();
  const theirs = await remote.branch(branch);
  // a push only moves the remote forward: its head must be in this branch's history (an empty branch, on a
  // fresh server, has nothing a push could lose)
  if (
    theirs &&
    theirs.head !== mine &&
    theirs.names.length &&
    !cb.store.isAncestor(theirs.head, mine) &&
    !force
  ) {
    throw new Error(
      `${name}/${branch} has changes you don't have: \`tcm pull ${name}\` first (or --force to overwrite them)`,
    );
  }
  const sent = await sendHistory(cb, remote, mine);
  // (with the sides of its conflicts, so they're labelled there as here)
  const conflicts = Object.fromEntries(
    Object.entries(branchConflicts(cb, cb.store.branch().name)).filter(
      ([key]) => conflictAt(cb, keyPath(key)) !== undefined,
    ),
  );
  if (theirs?.head !== mine)
    await remote.setBranch(
      branch,
      mine,
      theirs?.head ?? null,
      `push from ${os.hostname()}:${cb.lay.root}`,
      conflicts,
    );
  cb.store.setHead(`${name}/${branch}`, mine, `push ${name}`);
  const released = trusted ? await sendReleases(cb, remote) : [];
  // what its tests came to, so the other side needn't run them again
  const results = trusted ? await sendResults(cb, remote, names) : 0;
  // and what linting it found, so the other side needn't lint it again
  const lints = trusted ? await sendLints(cb, remote, names) : 0;
  if (released.length)
    console.log(`${green("released")} ${released.join(", ")} ${dim(`on ${name}`)}`);
  const d = theirs
    ? diffNamespaces(stateOf(theirs.names), stateOf(names), (h) => cb.store.getDef(h)!.ir)
    : undefined;
  const summary = d
    ? `${d.added.length} added, ${d.edited.length + d.propagated.length} changed, ${d.removed.length} removed, ${d.renamed.length} renamed`
    : `${names.length} names (new branch)`;
  console.log(
    theirs?.head === mine
      ? dim(`${name}/${branch} is up to date`)
      : `${green("pushed")} ${bold(`${name}/${branch}`)}: ${summary}; sent ${sent} definition(s)${results ? `, ${results} test result(s)` : ""}${lints ? `, ${lints} lint result(s)` : ""}`,
  );
  if (theirs?.head === mine && (results || lints))
    console.log(
      dim(
        `sent ${[results ? `${results} test result(s)` : "", lints ? `${lints} lint result(s)` : ""].filter(Boolean).join(", ")}`,
      ),
    );
}

uses

used by