@biffo/cli 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core.version CHANGED
@@ -1 +1 @@
1
- 0.48.0
1
+ 0.49.0
package/dist/index.js CHANGED
@@ -1304,20 +1304,54 @@ function chainOrder(migrations) {
1304
1304
  function reissuedRevisionId(file) {
1305
1305
  return `core_${createHash("sha256").update(file).digest("hex").slice(0, 8)}`;
1306
1306
  }
1307
+ var CARRIED_FROM_MARKER = "# biffo:carried-from:";
1308
+ var CARRIED_FROM_RE = /^# biffo:carried-from:[ \t]*(\S+)[ \t]*$/m;
1309
+ function parseCarriedFrom(content) {
1310
+ return CARRIED_FROM_RE.exec(content)?.[1] ?? null;
1311
+ }
1312
+ function stampCarriedFrom(content, templateFile) {
1313
+ if (CARRIED_FROM_RE.test(content)) return content;
1314
+ return content.replace(REVISION_RE, (m) => `${CARRIED_FROM_MARKER} ${templateFile}
1315
+ ${m}`);
1316
+ }
1317
+ function migrationBodyHash(content) {
1318
+ const normalised = content.split("\n").map((line) => line.trimEnd()).filter(
1319
+ (line) => !REVISION_RE.test(line) && !DOWN_REVISION_RE.test(line) && !CARRIED_FROM_RE.test(line) && // No trailing space in the patterns: lines are trimEnd()ed first, and a
1320
+ // base migration's `Revises: ` is empty, so requiring the space would
1321
+ // leave that one line in and give every base migration a hash of its
1322
+ // own — a difference in chaining metadata masquerading as a difference
1323
+ // in DDL.
1324
+ !/^Revision ID:/.test(line) && !/^Revises:/.test(line) && !/^Create Date:/.test(line) && line !== ""
1325
+ ).join("\n");
1326
+ return createHash("sha256").update(normalised).digest("hex");
1327
+ }
1328
+ function migrationSlug(file) {
1329
+ return file.replace(/\.py$/, "").replace(/^[0-9a-f]+_/i, "");
1330
+ }
1307
1331
  function planMigrationCarry(options) {
1308
1332
  const templateVersions = join5(options.templateDir, MIGRATIONS_VERSIONS_DIR);
1309
1333
  const instanceVersions = join5(options.instanceDir, MIGRATIONS_VERSIONS_DIR);
1310
1334
  const template = readMigrations(templateVersions);
1311
1335
  const instance = readMigrations(instanceVersions);
1312
1336
  const instanceHead = validateChain(instance, `${MIGRATIONS_VERSIONS_DIR} (instance)`);
1313
- const instanceFiles = new Set(instance.map((m) => m.file));
1314
1337
  const usedRevisions = new Set(instance.map((m) => m.revision));
1338
+ const identity = indexInstanceMigrations(instance);
1339
+ const templateBodyCounts = /* @__PURE__ */ new Map();
1340
+ for (const m of template) {
1341
+ const body = migrationBodyHash(m.content);
1342
+ templateBodyCounts.set(body, (templateBodyCounts.get(body) ?? 0) + 1);
1343
+ }
1315
1344
  const entries = [];
1316
1345
  const skipped = [];
1346
+ const recognised = [];
1317
1347
  let head = instanceHead;
1318
1348
  for (const m of chainOrder(template)) {
1319
- if (instanceFiles.has(m.file)) {
1349
+ const already = alreadyCarried(m, identity, templateBodyCounts);
1350
+ if (already) {
1320
1351
  skipped.push(m.file);
1352
+ if (already.how !== "filename") {
1353
+ recognised.push({ file: m.file, instanceFile: already.instance.file, how: already.how });
1354
+ }
1321
1355
  continue;
1322
1356
  }
1323
1357
  let revision = m.revision;
@@ -1337,7 +1371,9 @@ function planMigrationCarry(options) {
1337
1371
  file: m.file,
1338
1372
  revision,
1339
1373
  downRevision: head,
1340
- content: rechainMigration(m.content, revision, head)
1374
+ // Stamped before re-chaining so the instance's copy records which template
1375
+ // migration it is, whatever it is later renamed or renumbered to.
1376
+ content: rechainMigration(stampCarriedFrom(m.content, m.file), revision, head)
1341
1377
  };
1342
1378
  if (reissuedFrom !== void 0) entry.reissuedFrom = reissuedFrom;
1343
1379
  entries.push(entry);
@@ -1356,7 +1392,56 @@ function planMigrationCarry(options) {
1356
1392
  ],
1357
1393
  `${MIGRATIONS_VERSIONS_DIR} (after carry)`
1358
1394
  );
1359
- return { entries, instanceHead, skipped };
1395
+ return { entries, instanceHead, skipped, recognised };
1396
+ }
1397
+ function indexInstanceMigrations(instance) {
1398
+ const index = {
1399
+ byCarriedFrom: /* @__PURE__ */ new Map(),
1400
+ byFile: /* @__PURE__ */ new Map(),
1401
+ byBody: /* @__PURE__ */ new Map(),
1402
+ bySlug: /* @__PURE__ */ new Map()
1403
+ };
1404
+ for (const m of instance) {
1405
+ index.byFile.set(m.file, m);
1406
+ const from = parseCarriedFrom(m.content);
1407
+ if (from !== null) index.byCarriedFrom.set(from, m);
1408
+ const body = migrationBodyHash(m.content);
1409
+ index.byBody.set(body, [...index.byBody.get(body) ?? [], m]);
1410
+ const slug = migrationSlug(m.file);
1411
+ index.bySlug.set(slug, [...index.bySlug.get(slug) ?? [], m]);
1412
+ }
1413
+ return index;
1414
+ }
1415
+ function alreadyCarried(m, index, templateBodyCounts) {
1416
+ const byProvenance = index.byCarriedFrom.get(m.file);
1417
+ if (byProvenance) return { instance: byProvenance, how: "provenance" };
1418
+ const byFile = index.byFile.get(m.file);
1419
+ if (byFile) return { instance: byFile, how: "filename" };
1420
+ const body = migrationBodyHash(m.content);
1421
+ const bodyMatches = index.byBody.get(body) ?? [];
1422
+ if (bodyMatches.length === 1 && templateBodyCounts.get(body) === 1) {
1423
+ return { instance: bodyMatches[0], how: "body" };
1424
+ }
1425
+ const slugMatches = (index.bySlug.get(migrationSlug(m.file)) ?? []).filter(
1426
+ // A file already claimed as a copy of some OTHER template migration is not
1427
+ // an ambiguous match for this one.
1428
+ (candidate) => parseCarriedFrom(candidate.content) === null
1429
+ );
1430
+ if (slugMatches.length > 0) {
1431
+ const names = slugMatches.map((c) => c.file).join(", ");
1432
+ throw new Error(
1433
+ `Cannot carry ${m.file}: the instance has ${names}, which describes the same migration but whose contents differ, and which carries no provenance marker. Refusing to guess.
1434
+
1435
+ If it IS this migration (carried before provenance was recorded, then renamed or edited), add this line above its 'revision' assignment and re-run:
1436
+
1437
+ ${CARRIED_FROM_MARKER} ${m.file}
1438
+
1439
+ If it is unrelated, rename it so the descriptions differ.
1440
+
1441
+ Carrying it blindly would re-issue an already-applied migration and run its DDL against a database that already has those objects (#366).`
1442
+ );
1443
+ }
1444
+ return null;
1360
1445
  }
1361
1446
  function applyMigrationCarry(instanceDir, plan) {
1362
1447
  const written = [];
@@ -1798,6 +1883,11 @@ var STATUS_COLOR = {
1798
1883
  "keep-ours": chalk4.dim
1799
1884
  };
1800
1885
  function printMigrationCarry(migrations) {
1886
+ for (const r of migrations.recognised) {
1887
+ console.log(
1888
+ ` ${chalk4.dim("already carried".padEnd(15))} ${r.file} ` + chalk4.dim(`\u2192 this instance calls it ${r.instanceFile} (matched by ${r.how})`)
1889
+ );
1890
+ }
1801
1891
  for (const e of migrations.entries) {
1802
1892
  const suffix = e.reissuedFrom ? chalk4.dim(
1803
1893
  ` (revision ${e.revision}, re-issued from ${e.reissuedFrom}; revises ${e.downRevision ?? "base"})`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",