push
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
- tcmts.cli.codebase.Codebase
- tcmts.cli.diff.diffNamespaces
- tcmts.cli.diff.keyPath
- tcmts.cli.diff.stateOf
- tcmts.cli.merge.branchConflicts
- tcmts.cli.merge.conflictAt
- tcmts.cli.style.bold
- tcmts.cli.style.dim
- tcmts.cli.style.green
- tcmts.cli.sync.branchFor
- tcmts.cli.sync.openRemote
- tcmts.cli.sync.sendHistory
- tcmts.cli.sync.sendLints
- tcmts.cli.sync.sendReleases
- tcmts.cli.sync.sendResults