@biffo/cli 0.47.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 +1 -1
- package/dist/index.js +94 -4
- package/package.json +1 -1
package/core.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
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
|
-
|
|
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
|
-
|
|
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"})`
|