@biffo/cli 0.48.0 → 0.49.1

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.1
package/dist/index.js CHANGED
@@ -1304,20 +1304,50 @@ 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
+ var MODULE_DOCSTRING_RE = /^\s*("""|''')[\s\S]*?\1/;
1318
+ function migrationBodyHash(content) {
1319
+ const normalised = content.replace(MODULE_DOCSTRING_RE, "").split("\n").map((line) => line.trimEnd()).filter(
1320
+ (line) => !REVISION_RE.test(line) && !DOWN_REVISION_RE.test(line) && !line.trimStart().startsWith("#") && line !== ""
1321
+ ).join("\n");
1322
+ return createHash("sha256").update(normalised).digest("hex");
1323
+ }
1324
+ function migrationSlug(file) {
1325
+ return file.replace(/\.py$/, "").replace(/^[0-9a-f]+_/i, "");
1326
+ }
1307
1327
  function planMigrationCarry(options) {
1308
1328
  const templateVersions = join5(options.templateDir, MIGRATIONS_VERSIONS_DIR);
1309
1329
  const instanceVersions = join5(options.instanceDir, MIGRATIONS_VERSIONS_DIR);
1310
1330
  const template = readMigrations(templateVersions);
1311
1331
  const instance = readMigrations(instanceVersions);
1312
1332
  const instanceHead = validateChain(instance, `${MIGRATIONS_VERSIONS_DIR} (instance)`);
1313
- const instanceFiles = new Set(instance.map((m) => m.file));
1314
1333
  const usedRevisions = new Set(instance.map((m) => m.revision));
1334
+ const identity = indexInstanceMigrations(instance);
1335
+ const templateBodyCounts = /* @__PURE__ */ new Map();
1336
+ for (const m of template) {
1337
+ const body = migrationBodyHash(m.content);
1338
+ templateBodyCounts.set(body, (templateBodyCounts.get(body) ?? 0) + 1);
1339
+ }
1315
1340
  const entries = [];
1316
1341
  const skipped = [];
1342
+ const recognised = [];
1317
1343
  let head = instanceHead;
1318
1344
  for (const m of chainOrder(template)) {
1319
- if (instanceFiles.has(m.file)) {
1345
+ const already = alreadyCarried(m, identity, templateBodyCounts);
1346
+ if (already) {
1320
1347
  skipped.push(m.file);
1348
+ if (already.how !== "filename") {
1349
+ recognised.push({ file: m.file, instanceFile: already.instance.file, how: already.how });
1350
+ }
1321
1351
  continue;
1322
1352
  }
1323
1353
  let revision = m.revision;
@@ -1337,7 +1367,9 @@ function planMigrationCarry(options) {
1337
1367
  file: m.file,
1338
1368
  revision,
1339
1369
  downRevision: head,
1340
- content: rechainMigration(m.content, revision, head)
1370
+ // Stamped before re-chaining so the instance's copy records which template
1371
+ // migration it is, whatever it is later renamed or renumbered to.
1372
+ content: rechainMigration(stampCarriedFrom(m.content, m.file), revision, head)
1341
1373
  };
1342
1374
  if (reissuedFrom !== void 0) entry.reissuedFrom = reissuedFrom;
1343
1375
  entries.push(entry);
@@ -1356,7 +1388,56 @@ function planMigrationCarry(options) {
1356
1388
  ],
1357
1389
  `${MIGRATIONS_VERSIONS_DIR} (after carry)`
1358
1390
  );
1359
- return { entries, instanceHead, skipped };
1391
+ return { entries, instanceHead, skipped, recognised };
1392
+ }
1393
+ function indexInstanceMigrations(instance) {
1394
+ const index = {
1395
+ byCarriedFrom: /* @__PURE__ */ new Map(),
1396
+ byFile: /* @__PURE__ */ new Map(),
1397
+ byBody: /* @__PURE__ */ new Map(),
1398
+ bySlug: /* @__PURE__ */ new Map()
1399
+ };
1400
+ for (const m of instance) {
1401
+ index.byFile.set(m.file, m);
1402
+ const from = parseCarriedFrom(m.content);
1403
+ if (from !== null) index.byCarriedFrom.set(from, m);
1404
+ const body = migrationBodyHash(m.content);
1405
+ index.byBody.set(body, [...index.byBody.get(body) ?? [], m]);
1406
+ const slug = migrationSlug(m.file);
1407
+ index.bySlug.set(slug, [...index.bySlug.get(slug) ?? [], m]);
1408
+ }
1409
+ return index;
1410
+ }
1411
+ function alreadyCarried(m, index, templateBodyCounts) {
1412
+ const byProvenance = index.byCarriedFrom.get(m.file);
1413
+ if (byProvenance) return { instance: byProvenance, how: "provenance" };
1414
+ const byFile = index.byFile.get(m.file);
1415
+ if (byFile) return { instance: byFile, how: "filename" };
1416
+ const body = migrationBodyHash(m.content);
1417
+ const bodyMatches = index.byBody.get(body) ?? [];
1418
+ if (bodyMatches.length === 1 && templateBodyCounts.get(body) === 1) {
1419
+ return { instance: bodyMatches[0], how: "body" };
1420
+ }
1421
+ const slugMatches = (index.bySlug.get(migrationSlug(m.file)) ?? []).filter(
1422
+ // A file already claimed as a copy of some OTHER template migration is not
1423
+ // an ambiguous match for this one.
1424
+ (candidate) => parseCarriedFrom(candidate.content) === null
1425
+ );
1426
+ if (slugMatches.length > 0) {
1427
+ const names = slugMatches.map((c) => c.file).join(", ");
1428
+ throw new Error(
1429
+ `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.
1430
+
1431
+ If it IS this migration (carried before provenance was recorded, then renamed or edited), add this line above its 'revision' assignment and re-run:
1432
+
1433
+ ${CARRIED_FROM_MARKER} ${m.file}
1434
+
1435
+ If it is unrelated, rename it so the descriptions differ.
1436
+
1437
+ Carrying it blindly would re-issue an already-applied migration and run its DDL against a database that already has those objects (#366).`
1438
+ );
1439
+ }
1440
+ return null;
1360
1441
  }
1361
1442
  function applyMigrationCarry(instanceDir, plan) {
1362
1443
  const written = [];
@@ -1798,6 +1879,11 @@ var STATUS_COLOR = {
1798
1879
  "keep-ours": chalk4.dim
1799
1880
  };
1800
1881
  function printMigrationCarry(migrations) {
1882
+ for (const r of migrations.recognised) {
1883
+ console.log(
1884
+ ` ${chalk4.dim("already carried".padEnd(15))} ${r.file} ` + chalk4.dim(`\u2192 this instance calls it ${r.instanceFile} (matched by ${r.how})`)
1885
+ );
1886
+ }
1801
1887
  for (const e of migrations.entries) {
1802
1888
  const suffix = e.reissuedFrom ? chalk4.dim(
1803
1889
  ` (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.1",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",