migrateCodebase

tcmts.cli.migrate.migrateCodebase · #onaalsvo · term · runs on the server

uses node:path

Move a codebase to this tcm's hash format, safely: the database as it was is kept beside it (codebase.v<n>.sqlite), a copy is migrated (migrateFile), and the copy is used only if this tcm then reads every live definition back to its hash (verify); otherwise the codebase is left as it was. Undefined if it's in this format already.

import { Codebase } from "#codebase/tcmts/cli/codebase";
import { formatsOf, migrateFile } from "#codebase/tcmts/cli/migrate";
import type { Migrated } from "#codebase/tcmts/cli/migrate";
import { IR_VERSION } from "#codebase/tcmts/core/hash";
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";

export function migrateCodebase(
  root: string,
): (Migrated & { backup: string; verified: number }) | undefined {
  const db = path.join(root, ".tcm", "codebase.sqlite");
  // everything written so far in the database file itself, not its log
  const wal = new DatabaseSync(db);
  wal.exec("PRAGMA wal_checkpoint(TRUNCATE)");
  wal.close();
  const from = formatsOf(db);
  if (from.every((v) => v === IR_VERSION)) return undefined;
  if (from.some((v) => v > IR_VERSION))
    throw new Error(
      `this codebase is in hash format v${Math.max(...from)}, newer than this tcm's (v${IR_VERSION}): use a newer tcm`,
    );
  let backup = path.join(root, ".tcm", `codebase.v${Math.max(...from)}.sqlite`);
  if (fs.existsSync(backup)) backup = backup.replace(/\.sqlite$/, `.${Date.now()}.sqlite`);
  fs.copyFileSync(db, backup);
  const work = `${db}.migrating`;
  fs.copyFileSync(db, work);
  let report: Migrated;
  try {
    report = migrateFile(work);
  } catch (e) {
    fs.rmSync(work, { force: true });
    throw new Error(`the migration failed, and the codebase is as it was: ${(e as Error).message}`);
  }
  for (const f of [`${db}-wal`, `${db}-shm`]) fs.rmSync(f, { force: true });
  fs.renameSync(work, db);
  try {
    const cb = Codebase.find(root);
    const v = cb.verify(true);
    if (v.mismatches.length || v.problems.length) {
      const said = [
        ...v.mismatches.map(
          (m) => `${m.name} hashes to ${m.rehashed.slice(0, 8)}, not ${m.stored.slice(0, 8)}`,
        ),
        ...v.problems,
      ].slice(0, 5);
      cb.store.close();
      throw new Error(said.join("\n"));
    }
    cb.refreshShim(true);
    cb.store.close();
    return { ...report, backup, verified: v.checked };
  } catch (e) {
    for (const f of [`${db}-wal`, `${db}-shm`]) fs.rmSync(f, { force: true });
    fs.copyFileSync(backup, db);
    throw new Error(
      `the migrated codebase didn't read back as itself, so it's as it was:\n${(e as Error).message}`,
    );
  }
}

uses

used by