save
uses node:path
Save a scratch file (tcm add, tcm update). Its history entry is named by what it changes (update lib.f, lib.g (+3); a conflict it settles: resolve lib.k (merged)), or how.command; how.mates are paths it holds only as
the rest of a cycle, saved as they are. A
definition how.respell has names for (by path) is stored with its references written so (see respelled); every
definition is stored as it reads alone (spelledAlone), whatever names the file had to give what it uses. A
function or class declared find_1 where the codebase has it as find isn't saved: its name is part of its hash.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { LINT_CONFIG } from "#codebase/tcmts/cli/lint"; import { apply, changedCode, cycleWarnings, entryName, fileRows, formattedRows, keptAs, printScratchLint, respelled, spelledAlone, textChanged, withTheirCycles, } from "#codebase/tcmts/cli/main"; import type { Row } from "#codebase/tcmts/cli/main"; import { conflictMarkers, resolvedEntry } from "#codebase/tcmts/cli/merge"; import { red, yellow } from "#codebase/tcmts/cli/style"; import fs from "node:fs"; import path_1 from "node:path"; export function save( cb: Codebase, file: string, mode: "add" | "update", into?: string, how: { command?: string; mates?: ReadonlySet<string>; respell?: ReadonlyMap<string, ReadonlyMap<string, string>>; } = {}, ): boolean { // (saved over a conflicted name, it resolves it: the entry says so, and how) const command = (rows: Row[]) => { if (how.command) return how.command; const named = rows.filter( (r) => !r.mate && r.status !== "identical" && (mode === "update" || (r.status !== "update" && r.status !== "conflict")), ); const resolved = named.filter((r) => r.status === "conflict"); const rest = named.filter((r) => r.status !== "conflict").map((r) => r.path); if (!resolved.length) return entryName(mode, rest, `${mode} ${path_1.basename(file)}`); return `${resolvedEntry( cb, resolved.map((r) => ({ path: r.path, kind: r.def.kind, hash: r.def.hash })), )}${rest.length ? `; ${entryName(mode, rest)}` : ""}`; }; // a conflict merged with markers left in it isn't what either side, or anyone, means yet const marker = conflictMarkers(fs.readFileSync(file, "utf8")); if (marker) { console.log( red( `${path_1.basename(file)}:${marker} still has conflict markers: keep what each side should, take the markers out, then save it`, ), ); return false; } // a file kept as written (Markdown, CSS, an image) is a definition of its own const format = keptAs(cb, file, into); if (format !== undefined) { const rows = fileRows(cb, file, into, format); return !!rows && apply(cb, rows, mode, command(rows), false); } const whole = withTheirCycles(cb, file, into); if (!whole) return false; const { a, text } = whole; const rows = whole.rows.map((r) => (how.mates?.has(r.path) ? { ...r, mate: true } : r)); // lint what's being saved: warnings are shown, errors (rules set to "error" in lint.config) stop the save const saving = changedCode(cb, rows); if (saving.length && printScratchLint(cb, file, saving)) { console.log( red( `\n${path_1.basename(file)} has lint errors; nothing was saved. Fix them, or set the rule to "warn" in ${LINT_CONFIG}.`, ), ); return false; } // a function's or class's name is part of its hash (code can read it, as `.name`): one written with another's, // as a file holding both has to, would be another function for (const r of rows) { if (r.mate || !r.def.nominal || (r.status !== "update" && r.status !== "conflict")) continue; const was = cb.store .hashesAt(r.path, r.def.kind) .map((h) => cb.store.getDef(h)?.nameHint) .find( (n) => n !== undefined && /^_\d+$/.test(r.def.nameHint.slice(n.length)) && r.def.nameHint.startsWith(n), ); if (was === undefined) continue; console.log( red( `${r.path} is declared ${r.def.nameHint} in ${path_1.basename(file)}, and ${was} in the codebase: a function's or class's name is part of its hash, so saved so it would be another definition. Nothing was saved: declare it ${was} (in a file of its own, if another here is declared ${was} too).`, ), ); return false; } cb.lockPackages(a.packages); const analyzed = text ?? fs.readFileSync(file, "utf8"); // a cycle it makes or grows, said before it's saved (it's saved all the same) for (const w of cycleWarnings(cb, rows)) console.log(yellow(`warning: ${w}`)); return apply( cb, // the same code written differently (a comment changed): the codebase takes the new text, as formatting does formattedRows(cb, file, into, a, rows, text) .map((r) => { const names = how.respell?.get(r.path); return names ? { ...r, def: respelled(r.def, names) } : r; }) .map((r, _, all) => spelledAlone(cb, file, analyzed, r, all)) .map((r) => !r.mate && r.status === "identical" && textChanged(cb, r.def) ? { ...r, status: "text" as const, note: "comments, layout or import names changed; the code is the same", } : r, ), mode, command(rows), false, ); }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.lint.LINT_CONFIG
- tcmts.cli.main.Row
- tcmts.cli.main.apply
- tcmts.cli.main.changedCode
- tcmts.cli.main.cycleWarnings
- tcmts.cli.main.entryName
- tcmts.cli.main.fileRows
- tcmts.cli.main.formattedRows
- tcmts.cli.main.keptAs
- tcmts.cli.main.printScratchLint
- tcmts.cli.main.respelled
- tcmts.cli.main.spelledAlone
- tcmts.cli.main.textChanged
- tcmts.cli.main.withTheirCycles
- tcmts.cli.merge.conflictMarkers
- tcmts.cli.merge.resolvedEntry
- tcmts.cli.style.red
- tcmts.cli.style.yellow