canopycms 0.7.1 → 0.8.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/CHANGELOG.md +197 -47
- package/dist/api/admin-branch-health.js +47 -20
- package/dist/api/admin.d.ts +13 -1
- package/dist/api/admin.js +14 -24
- package/dist/api/branch-changes.d.ts +87 -0
- package/dist/api/branch-changes.js +269 -0
- package/dist/api/branch-entry-changes.d.ts +83 -0
- package/dist/api/branch-entry-changes.js +262 -0
- package/dist/api/branch-merge.js +2 -2
- package/dist/api/branch-review.js +35 -4
- package/dist/api/branch-status.js +14 -5
- package/dist/api/branch-withdraw.js +7 -0
- package/dist/api/branch.d.ts +13 -1
- package/dist/api/branch.js +171 -97
- package/dist/api/client.d.ts +15 -0
- package/dist/api/client.js +13 -1
- package/dist/api/content.d.ts +18 -18
- package/dist/api/content.js +1 -1
- package/dist/api/github-sync.js +7 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/pr-comment-sync.d.ts +9 -8
- package/dist/api/pr-comment-sync.js +23 -13
- package/dist/api/routes.js +4 -0
- package/dist/api/types.d.ts +3 -1
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/protected-branch.d.ts +3 -3
- package/dist/authorization/protected-branch.js +3 -3
- package/dist/branch-metadata-file.d.ts +84 -0
- package/dist/branch-metadata-file.js +9 -0
- package/dist/branch-metadata.d.ts +4 -4
- package/dist/branch-metadata.js +4 -5
- package/dist/branch-param.d.ts +24 -0
- package/dist/branch-param.js +87 -0
- package/dist/branch-provisioning.d.ts +6 -3
- package/dist/branch-provisioning.js +12 -10
- package/dist/branch-registry.d.ts +6 -0
- package/dist/branch-registry.js +12 -3
- package/dist/branch-schema-cache.d.ts +22 -2
- package/dist/branch-schema-cache.js +88 -12
- package/dist/branch-workspace.js +22 -15
- package/dist/cli/cli.js +1122 -181
- package/dist/cli/generate-ai-content.js +791 -109
- package/dist/cli/init.js +126 -0
- package/dist/comment-store.d.ts +3 -0
- package/dist/comment-store.js +4 -1
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/types.d.ts +11 -0
- package/dist/content-id-index.d.ts +5 -0
- package/dist/content-id-index.js +1 -1
- package/dist/content-listing.d.ts +53 -0
- package/dist/content-listing.js +123 -27
- package/dist/content-reader.d.ts +24 -10
- package/dist/content-reader.js +66 -22
- package/dist/context.js +51 -48
- package/dist/editor/BranchManager.d.ts +7 -3
- package/dist/editor/BranchManager.js +51 -6
- package/dist/editor/Editor.js +166 -34
- package/dist/editor/EntryNavigator.d.ts +34 -10
- package/dist/editor/EntryNavigator.js +185 -29
- package/dist/editor/admin/SystemHealthPanel.js +36 -1
- package/dist/editor/admin/useSystemHealth.js +1 -0
- package/dist/editor/branch-names.d.ts +43 -0
- package/dist/editor/branch-names.js +75 -0
- package/dist/editor/components/BranchPill.d.ts +2 -2
- package/dist/editor/components/BranchPill.js +2 -2
- package/dist/editor/components/EditorHeader.d.ts +3 -1
- package/dist/editor/components/EditorHeader.js +2 -2
- package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
- package/dist/editor/components/EntryChangesDrawer.js +363 -0
- package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
- package/dist/editor/components/EntryChangesSkeleton.js +7 -0
- package/dist/editor/components/EntryHeader.d.ts +9 -1
- package/dist/editor/components/EntryHeader.js +25 -3
- package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
- package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
- package/dist/editor/components/ReviewDialogs.js +3 -3
- package/dist/editor/context/ApiClientContext.d.ts +9 -0
- package/dist/editor/context/ApiClientContext.js +26 -2
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/copy.d.ts +1 -0
- package/dist/editor/copy.js +1 -0
- package/dist/editor/editor-utils.d.ts +2 -0
- package/dist/editor/editor-utils.js +15 -0
- package/dist/editor/entry-diff.d.ts +143 -0
- package/dist/editor/entry-diff.js +593 -0
- package/dist/editor/hooks/create-branch-request.d.ts +11 -1
- package/dist/editor/hooks/create-branch-request.js +16 -6
- package/dist/editor/hooks/useBranchActions.d.ts +19 -2
- package/dist/editor/hooks/useBranchActions.js +29 -20
- package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
- package/dist/editor/hooks/useBranchChangesData.js +134 -0
- package/dist/editor/hooks/useBranchManager.d.ts +16 -0
- package/dist/editor/hooks/useBranchManager.js +142 -54
- package/dist/editor/hooks/useDraftManager.js +6 -2
- package/dist/editor/hooks/useEntryManager.d.ts +18 -2
- package/dist/editor/hooks/useEntryManager.js +36 -10
- package/dist/editor/utils/editor-errors.d.ts +5 -2
- package/dist/git-manager.d.ts +71 -0
- package/dist/git-manager.js +270 -1
- package/dist/http/handler.js +11 -9
- package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
- package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
- package/dist/operating-mode/branch-namespace.d.ts +34 -0
- package/dist/operating-mode/branch-namespace.js +121 -0
- package/dist/operating-mode/client-unsafe-strategy.js +7 -0
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/paths/branch-name.d.ts +11 -19
- package/dist/paths/branch-name.js +17 -27
- package/dist/paths/index.d.ts +2 -2
- package/dist/paths/index.js +2 -2
- package/dist/paths/resolve.d.ts +0 -1
- package/dist/paths/resolve.js +0 -1
- package/dist/paths/validation.d.ts +40 -1
- package/dist/paths/validation.js +83 -4
- package/dist/pr-comments.d.ts +17 -9
- package/dist/pr-comments.js +53 -15
- package/dist/server.d.ts +6 -1
- package/dist/server.js +6 -1
- package/dist/services.js +2 -1
- package/dist/submission-attribution.d.ts +22 -0
- package/dist/submission-attribution.js +62 -6
- package/dist/task-queue/worker-status.d.ts +6 -1
- package/dist/task-queue/worker-status.js +23 -2
- package/dist/types.d.ts +59 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +111 -16
- package/dist/worker/branch-policy.js +222 -17
- package/dist/worker/cms-worker.d.ts +24 -2
- package/dist/worker/cms-worker.js +35 -24
- package/dist/worker/git-sync.d.ts +3 -3
- package/dist/worker/git-sync.js +42 -1
- package/dist/worker/github-gateway-http.js +2 -2
- package/dist/worker/github-gateway-wire.d.ts +42 -27
- package/dist/worker/github-gateway-wire.js +14 -3
- package/dist/worker/github-gateway.d.ts +17 -6
- package/dist/worker/github-gateway.js +62 -6
- package/dist/worker/github-mirror.d.ts +7 -4
- package/dist/worker/github-mirror.js +27 -16
- package/dist/worker/history-rewrite.d.ts +23 -10
- package/dist/worker/history-rewrite.js +43 -20
- package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
- package/dist/worker/legacy-branch-fixtures.js +71 -0
- package/dist/worker/rebase.d.ts +2 -1
- package/dist/worker/rebase.js +93 -36
- package/dist/worker/task-runner.d.ts +4 -1
- package/dist/worker/task-runner.js +77 -48
- package/dist/worker/unpublishable-branches.d.ts +34 -0
- package/dist/worker/unpublishable-branches.js +182 -0
- package/package.json +2 -1
|
@@ -346,6 +346,114 @@ var init_normalize = __esm({
|
|
|
346
346
|
}
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
+
// dist/paths/branch-name.js
|
|
350
|
+
function sanitizeBranchName(branchName) {
|
|
351
|
+
const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
352
|
+
const squashed = replaced.replace(/-+/g, "-");
|
|
353
|
+
const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
|
|
354
|
+
return trimmedDots || "branch";
|
|
355
|
+
}
|
|
356
|
+
function branchShortName(name, namespace) {
|
|
357
|
+
if (namespace === null || !name.startsWith(namespace) || name.length === namespace.length) {
|
|
358
|
+
return name;
|
|
359
|
+
}
|
|
360
|
+
const rest = name.slice(namespace.length);
|
|
361
|
+
return rest.includes("/") ? name : rest;
|
|
362
|
+
}
|
|
363
|
+
function isSettingsBranchName(branchName, settingsBranchName) {
|
|
364
|
+
const sanitized = sanitizeBranchName(branchName);
|
|
365
|
+
return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName(settingsBranchName);
|
|
366
|
+
}
|
|
367
|
+
var RESERVED_SETTINGS_BRANCH_PREFIX;
|
|
368
|
+
var init_branch_name = __esm({
|
|
369
|
+
"dist/paths/branch-name.js"() {
|
|
370
|
+
"use strict";
|
|
371
|
+
RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// dist/paths/validation.js
|
|
376
|
+
function isValidContentId(id) {
|
|
377
|
+
return CONTENT_ID_PATTERN.test(id);
|
|
378
|
+
}
|
|
379
|
+
function parseBranchRef(name) {
|
|
380
|
+
if (!name || typeof name !== "string") {
|
|
381
|
+
return { ok: false, error: "Branch name is required" };
|
|
382
|
+
}
|
|
383
|
+
if (name.length > MAX_BRANCH_NAME_LENGTH) {
|
|
384
|
+
return { ok: false, error: `Branch name too long (max ${MAX_BRANCH_NAME_LENGTH} characters)` };
|
|
385
|
+
}
|
|
386
|
+
if (name.includes("..")) {
|
|
387
|
+
return { ok: false, error: 'Branch name cannot contain ".."' };
|
|
388
|
+
}
|
|
389
|
+
if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
|
|
390
|
+
return { ok: false, error: "Invalid branch name format (invalid slashes)" };
|
|
391
|
+
}
|
|
392
|
+
if (name.includes(" ")) {
|
|
393
|
+
return { ok: false, error: "Branch name cannot contain spaces" };
|
|
394
|
+
}
|
|
395
|
+
if (name.startsWith("-")) {
|
|
396
|
+
return { ok: false, error: 'Branch name cannot start with "-"' };
|
|
397
|
+
}
|
|
398
|
+
if (name.startsWith(".") || name.endsWith(".")) {
|
|
399
|
+
return { ok: false, error: "Branch name cannot start or end with a dot" };
|
|
400
|
+
}
|
|
401
|
+
if (name.includes("@{")) {
|
|
402
|
+
return { ok: false, error: 'Branch name cannot contain "@{"' };
|
|
403
|
+
}
|
|
404
|
+
if (name === "HEAD" || name === "@") {
|
|
405
|
+
return { ok: false, error: 'Branch name cannot be the reserved ref "HEAD" or "@"' };
|
|
406
|
+
}
|
|
407
|
+
if (/[~^:?*[\\\x00-\x1f\x7f]/.test(name)) {
|
|
408
|
+
return { ok: false, error: "Branch name contains invalid characters" };
|
|
409
|
+
}
|
|
410
|
+
if (name.endsWith(".lock")) {
|
|
411
|
+
return { ok: false, error: 'Branch name cannot end with ".lock"' };
|
|
412
|
+
}
|
|
413
|
+
return { ok: true, name };
|
|
414
|
+
}
|
|
415
|
+
function parseSlug(slug) {
|
|
416
|
+
if (!slug) {
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
error: "Slug is required"
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (slug.length > 64) {
|
|
423
|
+
return { ok: false, error: "Slug too long (max 64 characters)" };
|
|
424
|
+
}
|
|
425
|
+
if (slug.includes("/") || slug.includes("\\")) {
|
|
426
|
+
return {
|
|
427
|
+
ok: false,
|
|
428
|
+
error: "Slug cannot contain path separators"
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (slug === "." || slug === "..") {
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
error: "Slug cannot be a traversal sequence"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const normalized = slug.toLowerCase();
|
|
438
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return { ok: true, slug: normalized };
|
|
445
|
+
}
|
|
446
|
+
var BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, MAX_BRANCH_NAME_LENGTH;
|
|
447
|
+
var init_validation = __esm({
|
|
448
|
+
"dist/paths/validation.js"() {
|
|
449
|
+
"use strict";
|
|
450
|
+
BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
451
|
+
CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
|
|
452
|
+
PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
|
|
453
|
+
MAX_BRANCH_NAME_LENGTH = 250;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
349
457
|
// dist/paths/types.js
|
|
350
458
|
var ROOT_COLLECTION_ID;
|
|
351
459
|
var init_types = __esm({
|
|
@@ -946,6 +1054,8 @@ var init_config = __esm({
|
|
|
946
1054
|
// (operating-mode/deployment-name.ts) implements — config would always "win" over
|
|
947
1055
|
// modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
|
|
948
1056
|
deploymentName: deploymentNameSchema.optional(),
|
|
1057
|
+
// Checked where it is resolved (operating-mode/branch-namespace.ts), with the deployment name.
|
|
1058
|
+
branchNamespace: z6.string().optional(),
|
|
949
1059
|
contentRoot: contentRootSchema.default("content"),
|
|
950
1060
|
sourceRoot: sourceRootSchema.optional(),
|
|
951
1061
|
basePath: basePathSchema,
|
|
@@ -1000,7 +1110,7 @@ var init_mode_env = __esm({
|
|
|
1000
1110
|
|
|
1001
1111
|
// dist/config/validation.js
|
|
1002
1112
|
var forEachReferenceField;
|
|
1003
|
-
var
|
|
1113
|
+
var init_validation2 = __esm({
|
|
1004
1114
|
"dist/config/validation.js"() {
|
|
1005
1115
|
"use strict";
|
|
1006
1116
|
forEachReferenceField = (fields, visit3) => {
|
|
@@ -1134,6 +1244,80 @@ var init_deployment_name = __esm({
|
|
|
1134
1244
|
}
|
|
1135
1245
|
});
|
|
1136
1246
|
|
|
1247
|
+
// dist/operating-mode/branch-namespace.js
|
|
1248
|
+
function warnOnce(message) {
|
|
1249
|
+
if (warned4)
|
|
1250
|
+
return;
|
|
1251
|
+
warned4 = true;
|
|
1252
|
+
canopyLogWarn(message);
|
|
1253
|
+
}
|
|
1254
|
+
function branchNamespaceProblem(namespace) {
|
|
1255
|
+
if (!namespace.endsWith("/"))
|
|
1256
|
+
return 'it must end with "/"';
|
|
1257
|
+
const sample = `${namespace}x`;
|
|
1258
|
+
if (!/^[\x21-\x7e]+$/.test(namespace))
|
|
1259
|
+
return "it is not printable ASCII without spaces";
|
|
1260
|
+
if (/["%{}]/.test(namespace))
|
|
1261
|
+
return 'it contains ", %, { or }';
|
|
1262
|
+
if (/^(?:refs|heads|tags|remotes)\//i.test(namespace)) {
|
|
1263
|
+
return "it starts with 'refs/', 'heads/', 'tags/' or 'remotes/'";
|
|
1264
|
+
}
|
|
1265
|
+
const parsed = parseBranchRef(sample);
|
|
1266
|
+
if (!parsed.ok)
|
|
1267
|
+
return `a branch in it would be refused (${parsed.error})`;
|
|
1268
|
+
if (sample.split("/").some((part) => part.startsWith(".") || part.endsWith(".lock"))) {
|
|
1269
|
+
return "a part of it starts with '.' or ends with '.lock'";
|
|
1270
|
+
}
|
|
1271
|
+
const directoryPrefix = sanitizeBranchName(namespace);
|
|
1272
|
+
if (RESERVED_SETTINGS_BRANCH_PREFIX.startsWith(directoryPrefix) || sanitizeBranchName(sample).startsWith(RESERVED_SETTINGS_BRANCH_PREFIX)) {
|
|
1273
|
+
return `some branches in it would be ${RESERVED_SETTINGS_BRANCH_PREFIX}* settings branches`;
|
|
1274
|
+
}
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
function resolveBranchNamespaceTemplate(template, deploymentName, source) {
|
|
1278
|
+
const unknown = template.replaceAll(DEPLOYMENT_PLACEHOLDER, "").match(/\{[^}]*\}?/);
|
|
1279
|
+
if (unknown) {
|
|
1280
|
+
throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(template)} (from ${source}): ${JSON.stringify(unknown[0])} is not a placeholder; the only one is ${DEPLOYMENT_PLACEHOLDER}`);
|
|
1281
|
+
}
|
|
1282
|
+
const namespace = template.replaceAll(DEPLOYMENT_PLACEHOLDER, deploymentName);
|
|
1283
|
+
const problem = branchNamespaceProblem(namespace);
|
|
1284
|
+
if (problem !== null) {
|
|
1285
|
+
throw new Error(`CanopyCMS: invalid branch namespace ${JSON.stringify(namespace)} (from ${source}): ${problem}`);
|
|
1286
|
+
}
|
|
1287
|
+
return namespace;
|
|
1288
|
+
}
|
|
1289
|
+
function resolveBranchNamespace(config, modeDefault, env = process.env) {
|
|
1290
|
+
const fromEnv = env[BRANCH_NAMESPACE_ENV]?.trim();
|
|
1291
|
+
const [template, source] = fromEnv ? [fromEnv, BRANCH_NAMESPACE_ENV] : config.branchNamespace !== void 0 ? [config.branchNamespace, "config.branchNamespace"] : [DEFAULT_BRANCH_NAMESPACE_TEMPLATE, "the default"];
|
|
1292
|
+
const deploymentName = resolveDeploymentName({ deploymentName: config.deploymentName }, modeDefault);
|
|
1293
|
+
const namespace = resolveBranchNamespaceTemplate(template, deploymentName, source);
|
|
1294
|
+
if (fromEnv && config.branchNamespace !== void 0) {
|
|
1295
|
+
let fromConfig = null;
|
|
1296
|
+
try {
|
|
1297
|
+
fromConfig = resolveBranchNamespaceTemplate(config.branchNamespace, deploymentName, "config.branchNamespace");
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
if (fromConfig !== namespace) {
|
|
1301
|
+
warnOnce(`CanopyCMS: ${BRANCH_NAMESPACE_ENV} (${JSON.stringify(fromEnv)}) and config.branchNamespace (${JSON.stringify(config.branchNamespace)}) do not resolve to the same valid namespace; using the environment variable, which infrastructure stamps per stack.`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return namespace;
|
|
1305
|
+
}
|
|
1306
|
+
var warned4, DEPLOYMENT_PLACEHOLDER, DEFAULT_BRANCH_NAMESPACE_TEMPLATE, BRANCH_NAMESPACE_ENV;
|
|
1307
|
+
var init_branch_namespace = __esm({
|
|
1308
|
+
"dist/operating-mode/branch-namespace.js"() {
|
|
1309
|
+
"use strict";
|
|
1310
|
+
init_branch_name();
|
|
1311
|
+
init_validation();
|
|
1312
|
+
init_logger();
|
|
1313
|
+
init_deployment_name();
|
|
1314
|
+
warned4 = false;
|
|
1315
|
+
DEPLOYMENT_PLACEHOLDER = "{deployment}";
|
|
1316
|
+
DEFAULT_BRANCH_NAMESPACE_TEMPLATE = `canopycms/${DEPLOYMENT_PLACEHOLDER}/`;
|
|
1317
|
+
BRANCH_NAMESPACE_ENV = "CANOPYCMS_BRANCH_NAMESPACE";
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1137
1321
|
// dist/config/index.js
|
|
1138
1322
|
var init_config2 = __esm({
|
|
1139
1323
|
"dist/config/index.js"() {
|
|
@@ -1173,6 +1357,7 @@ var init_client_unsafe_strategy = __esm({
|
|
|
1173
1357
|
"dist/operating-mode/client-unsafe-strategy.js"() {
|
|
1174
1358
|
"use strict";
|
|
1175
1359
|
init_client_safe_strategy();
|
|
1360
|
+
init_branch_namespace();
|
|
1176
1361
|
init_deployment_name();
|
|
1177
1362
|
init_config2();
|
|
1178
1363
|
ProdStrategy = class extends ProdClientSafeStrategy {
|
|
@@ -1213,6 +1398,9 @@ var init_client_unsafe_strategy = __esm({
|
|
|
1213
1398
|
return config.settingsBranch;
|
|
1214
1399
|
return `canopycms-settings-${resolveDeploymentName(config, "prod")}`;
|
|
1215
1400
|
}
|
|
1401
|
+
getBranchNamespace(config) {
|
|
1402
|
+
return resolveBranchNamespace(config, "prod");
|
|
1403
|
+
}
|
|
1216
1404
|
getSettingsRoot(sourceRoot) {
|
|
1217
1405
|
return path9.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
1218
1406
|
}
|
|
@@ -1262,6 +1450,9 @@ var init_client_unsafe_strategy = __esm({
|
|
|
1262
1450
|
return config.settingsBranch;
|
|
1263
1451
|
return `canopycms-settings-${resolveDeploymentName(config, "local")}`;
|
|
1264
1452
|
}
|
|
1453
|
+
getBranchNamespace(config) {
|
|
1454
|
+
return resolveBranchNamespace(config, "local");
|
|
1455
|
+
}
|
|
1265
1456
|
getSettingsRoot(sourceRoot) {
|
|
1266
1457
|
return path9.join(this.getWorkspaceRoot(sourceRoot), "settings");
|
|
1267
1458
|
}
|
|
@@ -1326,11 +1517,27 @@ __export(worker_status_exports, {
|
|
|
1326
1517
|
WORKER_STATUS_FILE: () => WORKER_STATUS_FILE,
|
|
1327
1518
|
readCarriedOverStatus: () => readCarriedOverStatus,
|
|
1328
1519
|
readWorkerStartupFailure: () => readWorkerStartupFailure,
|
|
1520
|
+
readWorkerStatus: () => readWorkerStatus,
|
|
1329
1521
|
readWorkerStatusStartedAt: () => readWorkerStatusStartedAt,
|
|
1330
1522
|
writeWorkerStatus: () => writeWorkerStatus
|
|
1331
1523
|
});
|
|
1332
1524
|
import fs15 from "node:fs/promises";
|
|
1333
1525
|
import path19 from "node:path";
|
|
1526
|
+
async function readWorkerStatus(taskDir) {
|
|
1527
|
+
let content;
|
|
1528
|
+
try {
|
|
1529
|
+
content = await fs15.readFile(path19.join(taskDir, WORKER_STATUS_FILE), "utf-8");
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
if (isNotFoundError(err))
|
|
1532
|
+
return { workerStatus: null };
|
|
1533
|
+
return { workerStatus: null, statusReadError: getErrorMessage(err) };
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
return { workerStatus: JSON.parse(content) };
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
return { workerStatus: null, statusReadError: getErrorMessage(err) };
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1334
1541
|
async function readCarriedOverStatus(taskDir) {
|
|
1335
1542
|
let previous;
|
|
1336
1543
|
try {
|
|
@@ -3667,48 +3874,8 @@ import fs3 from "node:fs/promises";
|
|
|
3667
3874
|
import path4 from "node:path";
|
|
3668
3875
|
|
|
3669
3876
|
// dist/id.js
|
|
3877
|
+
init_validation();
|
|
3670
3878
|
import { generate } from "short-uuid";
|
|
3671
|
-
|
|
3672
|
-
// dist/paths/validation.js
|
|
3673
|
-
var BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
3674
|
-
var CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
|
|
3675
|
-
var PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
|
|
3676
|
-
function isValidContentId(id) {
|
|
3677
|
-
return CONTENT_ID_PATTERN.test(id);
|
|
3678
|
-
}
|
|
3679
|
-
function parseSlug(slug) {
|
|
3680
|
-
if (!slug) {
|
|
3681
|
-
return {
|
|
3682
|
-
ok: false,
|
|
3683
|
-
error: "Slug is required"
|
|
3684
|
-
};
|
|
3685
|
-
}
|
|
3686
|
-
if (slug.length > 64) {
|
|
3687
|
-
return { ok: false, error: "Slug too long (max 64 characters)" };
|
|
3688
|
-
}
|
|
3689
|
-
if (slug.includes("/") || slug.includes("\\")) {
|
|
3690
|
-
return {
|
|
3691
|
-
ok: false,
|
|
3692
|
-
error: "Slug cannot contain path separators"
|
|
3693
|
-
};
|
|
3694
|
-
}
|
|
3695
|
-
if (slug === "." || slug === "..") {
|
|
3696
|
-
return {
|
|
3697
|
-
ok: false,
|
|
3698
|
-
error: "Slug cannot be a traversal sequence"
|
|
3699
|
-
};
|
|
3700
|
-
}
|
|
3701
|
-
const normalized = slug.toLowerCase();
|
|
3702
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
|
|
3703
|
-
return {
|
|
3704
|
-
ok: false,
|
|
3705
|
-
error: "Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens"
|
|
3706
|
-
};
|
|
3707
|
-
}
|
|
3708
|
-
return { ok: true, slug: normalized };
|
|
3709
|
-
}
|
|
3710
|
-
|
|
3711
|
-
// dist/id.js
|
|
3712
3879
|
function generateId() {
|
|
3713
3880
|
const full = generate();
|
|
3714
3881
|
return full.substring(0, 12);
|
|
@@ -4276,6 +4443,7 @@ function isGenerationCurrent(snapshotToken, read) {
|
|
|
4276
4443
|
// dist/branch-schema-cache.js
|
|
4277
4444
|
import fs7 from "node:fs/promises";
|
|
4278
4445
|
import path8 from "node:path";
|
|
4446
|
+
import { LRUCache } from "lru-cache";
|
|
4279
4447
|
|
|
4280
4448
|
// dist/schema/meta-loader.js
|
|
4281
4449
|
import { promises as fs6 } from "fs";
|
|
@@ -4538,7 +4706,7 @@ function registryFingerprint(registry2) {
|
|
|
4538
4706
|
init_flatten();
|
|
4539
4707
|
|
|
4540
4708
|
// dist/validation/entry-type-reference-validator.js
|
|
4541
|
-
|
|
4709
|
+
init_validation2();
|
|
4542
4710
|
var editDistance = (a, b) => {
|
|
4543
4711
|
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
4544
4712
|
for (let i = 1; i <= a.length; i++) {
|
|
@@ -4735,6 +4903,7 @@ async function resolveBaseBranch(options) {
|
|
|
4735
4903
|
init_logger();
|
|
4736
4904
|
var SCHEMA_CACHE_VERSION = 4;
|
|
4737
4905
|
var MTIME_CHECK_DEBOUNCE_MS = 1e3;
|
|
4906
|
+
var SNAPSHOT_MEMO_MAX_BYTES = 64 * 1024 * 1024;
|
|
4738
4907
|
var SCHEMA_CACHE_FILE = "schema-cache.json";
|
|
4739
4908
|
var SCHEMA_GENERATION_RESOURCE = "schema";
|
|
4740
4909
|
var reportedIssues = /* @__PURE__ */ new Set();
|
|
@@ -4768,6 +4937,9 @@ async function isStaleByMtime(dir, cachedAt) {
|
|
|
4768
4937
|
}
|
|
4769
4938
|
return false;
|
|
4770
4939
|
}
|
|
4940
|
+
function fileIdentity(stat) {
|
|
4941
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}`;
|
|
4942
|
+
}
|
|
4771
4943
|
async function schemaCacheDir(branchRoot) {
|
|
4772
4944
|
const gitDir = path8.join(branchRoot, ".git");
|
|
4773
4945
|
const isClone = await fs7.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
|
|
@@ -4776,6 +4948,11 @@ async function schemaCacheDir(branchRoot) {
|
|
|
4776
4948
|
var BranchSchemaCache = class {
|
|
4777
4949
|
constructor(mode = "prod") {
|
|
4778
4950
|
this.lastMtimeCheck = /* @__PURE__ */ new Map();
|
|
4951
|
+
this.snapshots = new LRUCache({
|
|
4952
|
+
maxSize: SNAPSHOT_MEMO_MAX_BYTES,
|
|
4953
|
+
sizeCalculation: (snapshot) => Math.max(1, snapshot.bytes)
|
|
4954
|
+
});
|
|
4955
|
+
this.buildResolves = /* @__PURE__ */ new Map();
|
|
4779
4956
|
this.devMode = mode === "dev";
|
|
4780
4957
|
}
|
|
4781
4958
|
/**
|
|
@@ -4805,15 +4982,12 @@ var BranchSchemaCache = class {
|
|
|
4805
4982
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
4806
4983
|
const contentRoot = path8.join(branchRoot, contentRootName);
|
|
4807
4984
|
const fromCheckout = this.readsCheckout(branchRoot);
|
|
4985
|
+
if (fromCheckout && isBuildMode()) {
|
|
4986
|
+
return this.resolveForBuild(branchRoot, entrySchemaRegistry, contentRootName);
|
|
4987
|
+
}
|
|
4808
4988
|
if (!fromCheckout) {
|
|
4809
4989
|
const cachePath = path8.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
|
|
4810
|
-
let cacheData =
|
|
4811
|
-
try {
|
|
4812
|
-
const cacheContent = await fs7.readFile(cachePath, "utf-8");
|
|
4813
|
-
cacheData = JSON.parse(cacheContent);
|
|
4814
|
-
} catch {
|
|
4815
|
-
cacheData = null;
|
|
4816
|
-
}
|
|
4990
|
+
let cacheData = await this.readSnapshot(cachePath);
|
|
4817
4991
|
if (cacheData && cacheData.version === SCHEMA_CACHE_VERSION && cacheData.registryFingerprint === registryFingerprint(entrySchemaRegistry)) {
|
|
4818
4992
|
const read = await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
|
|
4819
4993
|
if (isGenerationCurrent(cacheData.generation, read)) {
|
|
@@ -4843,6 +5017,42 @@ var BranchSchemaCache = class {
|
|
|
4843
5017
|
fromCheckout
|
|
4844
5018
|
});
|
|
4845
5019
|
}
|
|
5020
|
+
resolveForBuild(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
5021
|
+
const key = `${path8.resolve(branchRoot, contentRootName)}\0${registryFingerprint(entrySchemaRegistry)}`;
|
|
5022
|
+
let pending = this.buildResolves.get(key);
|
|
5023
|
+
if (!pending) {
|
|
5024
|
+
pending = this.resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, {
|
|
5025
|
+
fromCheckout: true
|
|
5026
|
+
});
|
|
5027
|
+
this.buildResolves.set(key, pending);
|
|
5028
|
+
pending.catch(() => this.buildResolves.delete(key));
|
|
5029
|
+
}
|
|
5030
|
+
return pending;
|
|
5031
|
+
}
|
|
5032
|
+
/** The parsed cache file, or null when it is absent or unreadable. */
|
|
5033
|
+
async readSnapshot(cachePath) {
|
|
5034
|
+
let handle;
|
|
5035
|
+
try {
|
|
5036
|
+
handle = await fs7.open(cachePath, "r");
|
|
5037
|
+
} catch {
|
|
5038
|
+
return null;
|
|
5039
|
+
}
|
|
5040
|
+
try {
|
|
5041
|
+
const stat = await handle.stat({ bigint: true });
|
|
5042
|
+
const identity = fileIdentity(stat);
|
|
5043
|
+
const memo = this.snapshots.get(cachePath);
|
|
5044
|
+
if (memo?.identity === identity)
|
|
5045
|
+
return memo.entry;
|
|
5046
|
+
const entry = JSON.parse(await fs7.readFile(handle, "utf-8"));
|
|
5047
|
+
this.snapshots.set(cachePath, { identity, entry, bytes: Number(stat.size) });
|
|
5048
|
+
return entry;
|
|
5049
|
+
} catch {
|
|
5050
|
+
return null;
|
|
5051
|
+
} finally {
|
|
5052
|
+
await handle.close().catch(() => {
|
|
5053
|
+
});
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
4846
5056
|
/**
|
|
4847
5057
|
* Resolve the schema fresh from disk and persist it (subject to the
|
|
4848
5058
|
* skip-persist rule below), UNCONDITIONALLY -- never through the cache-read
|
|
@@ -4884,14 +5094,22 @@ var BranchSchemaCache = class {
|
|
|
4884
5094
|
};
|
|
4885
5095
|
await fs7.mkdir(cacheDir, { recursive: true });
|
|
4886
5096
|
const tmpPath = path8.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
|
|
4887
|
-
|
|
5097
|
+
const body = JSON.stringify(newCache);
|
|
5098
|
+
let identity;
|
|
4888
5099
|
try {
|
|
5100
|
+
await fs7.writeFile(tmpPath, body, "utf-8");
|
|
5101
|
+
identity = fileIdentity(await fs7.stat(tmpPath, { bigint: true }));
|
|
4889
5102
|
await fs7.rename(tmpPath, cachePath);
|
|
4890
5103
|
} catch (err) {
|
|
4891
5104
|
await fs7.unlink(tmpPath).catch(() => {
|
|
4892
5105
|
});
|
|
4893
5106
|
throw err;
|
|
4894
5107
|
}
|
|
5108
|
+
this.snapshots.set(cachePath, {
|
|
5109
|
+
identity,
|
|
5110
|
+
entry: JSON.parse(body),
|
|
5111
|
+
bytes: Buffer.byteLength(body)
|
|
5112
|
+
});
|
|
4895
5113
|
}
|
|
4896
5114
|
}
|
|
4897
5115
|
reportSchemaIssues(issues);
|
|
@@ -4997,24 +5215,13 @@ function validateAndNormalizePath(root, target) {
|
|
|
4997
5215
|
};
|
|
4998
5216
|
}
|
|
4999
5217
|
|
|
5218
|
+
// dist/paths/index.js
|
|
5219
|
+
init_validation();
|
|
5220
|
+
|
|
5000
5221
|
// dist/paths/branch.js
|
|
5001
5222
|
init_operating_mode();
|
|
5002
5223
|
import path10 from "node:path";
|
|
5003
|
-
|
|
5004
|
-
// dist/paths/branch-name.js
|
|
5005
|
-
function sanitizeBranchName(branchName) {
|
|
5006
|
-
const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
5007
|
-
const squashed = replaced.replace(/-+/g, "-");
|
|
5008
|
-
const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
|
|
5009
|
-
return trimmedDots || "branch";
|
|
5010
|
-
}
|
|
5011
|
-
var RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
|
|
5012
|
-
function isSettingsBranchName(branchName, settingsBranchName) {
|
|
5013
|
-
const sanitized = sanitizeBranchName(branchName);
|
|
5014
|
-
return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName(settingsBranchName);
|
|
5015
|
-
}
|
|
5016
|
-
|
|
5017
|
-
// dist/paths/branch.js
|
|
5224
|
+
init_branch_name();
|
|
5018
5225
|
var BranchPathError = class extends Error {
|
|
5019
5226
|
};
|
|
5020
5227
|
var resolveContentBranchesRoot = (mode, override) => {
|
|
@@ -5043,6 +5250,9 @@ function resolveBranchPath(options) {
|
|
|
5043
5250
|
return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
|
|
5044
5251
|
}
|
|
5045
5252
|
|
|
5253
|
+
// dist/paths/index.js
|
|
5254
|
+
init_branch_name();
|
|
5255
|
+
|
|
5046
5256
|
// dist/content-store.js
|
|
5047
5257
|
async function withLocks(keys, fn) {
|
|
5048
5258
|
const sorted = Array.from(new Set(keys)).sort();
|
|
@@ -7640,7 +7850,13 @@ var branchMetadataShape = {
|
|
|
7640
7850
|
historyRewrittenFrom: z7.string().optional(),
|
|
7641
7851
|
syncFailureReason: z7.string().optional(),
|
|
7642
7852
|
editors: z7.array(z7.string()).optional(),
|
|
7643
|
-
uncommittedEditors: z7.array(z7.string()).optional()
|
|
7853
|
+
uncommittedEditors: z7.array(z7.string()).optional(),
|
|
7854
|
+
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
7855
|
+
review: z7.object({
|
|
7856
|
+
state: z7.enum(["approved", "changes-requested"]),
|
|
7857
|
+
by: z7.object({ userId: z7.string(), name: z7.string().optional() }),
|
|
7858
|
+
at: z7.string()
|
|
7859
|
+
}).optional().catch(void 0)
|
|
7644
7860
|
};
|
|
7645
7861
|
var branchMetadataSchema = z7.object(branchMetadataShape).passthrough();
|
|
7646
7862
|
var branchMetadataFileSchema = z7.object({
|
|
@@ -7781,14 +7997,23 @@ var BranchRegistry = class {
|
|
|
7781
7997
|
* genuinely-absent branch costs at most one extra scan per window.
|
|
7782
7998
|
*/
|
|
7783
7999
|
async get(name) {
|
|
7784
|
-
|
|
7785
|
-
|
|
8000
|
+
return this.find((b) => b.branch.name === name);
|
|
8001
|
+
}
|
|
8002
|
+
/**
|
|
8003
|
+
* Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
|
|
8004
|
+
* whatever its name, with {@link get}'s backstop.
|
|
8005
|
+
*/
|
|
8006
|
+
async getByKey(key) {
|
|
8007
|
+
return this.find((b) => path15.basename(b.branchRoot) === key);
|
|
8008
|
+
}
|
|
8009
|
+
async find(matches) {
|
|
8010
|
+
const found = (await this.list()).find(matches);
|
|
7786
8011
|
if (found)
|
|
7787
8012
|
return found;
|
|
7788
8013
|
if (!this.shouldForceRefresh())
|
|
7789
8014
|
return void 0;
|
|
7790
8015
|
const refreshed = await this.regenerate();
|
|
7791
|
-
return refreshed.find(
|
|
8016
|
+
return refreshed.find(matches);
|
|
7792
8017
|
}
|
|
7793
8018
|
shouldForceRefresh() {
|
|
7794
8019
|
const now = Date.now();
|
|
@@ -8257,18 +8482,228 @@ var loadBranchContext = async (options) => {
|
|
|
8257
8482
|
};
|
|
8258
8483
|
};
|
|
8259
8484
|
|
|
8485
|
+
// dist/branch-param.js
|
|
8486
|
+
init_operating_mode();
|
|
8487
|
+
init_branch_name();
|
|
8488
|
+
function cmsBranchNamespace(config) {
|
|
8489
|
+
try {
|
|
8490
|
+
return operatingStrategy(config.mode).getBranchNamespace(config);
|
|
8491
|
+
} catch {
|
|
8492
|
+
return null;
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
function namesBaseOrActive(param, config) {
|
|
8496
|
+
let base;
|
|
8497
|
+
try {
|
|
8498
|
+
base = baseBranchOf(config);
|
|
8499
|
+
} catch (err) {
|
|
8500
|
+
if (err instanceof BaseBranchUnresolvedError)
|
|
8501
|
+
return false;
|
|
8502
|
+
throw err;
|
|
8503
|
+
}
|
|
8504
|
+
return param === base || param === (config.defaultActiveBranch ?? base);
|
|
8505
|
+
}
|
|
8506
|
+
function shortNameOf(config, name) {
|
|
8507
|
+
const short = branchShortName(name, cmsBranchNamespace(config));
|
|
8508
|
+
return short !== name && namesBaseOrActive(short, config) ? name : short;
|
|
8509
|
+
}
|
|
8510
|
+
|
|
8260
8511
|
// dist/branch-provisioning.js
|
|
8261
|
-
import { createHash as
|
|
8512
|
+
import { createHash as createHash3, randomBytes } from "node:crypto";
|
|
8262
8513
|
import fs17 from "node:fs/promises";
|
|
8263
8514
|
import path21 from "node:path";
|
|
8264
8515
|
|
|
8265
8516
|
// dist/git-manager.js
|
|
8517
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
8266
8518
|
import fs16 from "node:fs/promises";
|
|
8267
8519
|
import path20 from "node:path";
|
|
8268
8520
|
import { fileURLToPath } from "node:url";
|
|
8269
8521
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
8270
8522
|
init_error();
|
|
8271
8523
|
init_logger();
|
|
8524
|
+
|
|
8525
|
+
// dist/worker/git-process.js
|
|
8526
|
+
init_error();
|
|
8527
|
+
import { spawn } from "node:child_process";
|
|
8528
|
+
import { Readable, Transform, Writable } from "node:stream";
|
|
8529
|
+
import { pipeline } from "node:stream/promises";
|
|
8530
|
+
var GitProcessError = class extends Error {
|
|
8531
|
+
constructor(message, exitCode, stderr) {
|
|
8532
|
+
super(message);
|
|
8533
|
+
this.exitCode = exitCode;
|
|
8534
|
+
this.stderr = stderr;
|
|
8535
|
+
this.name = "GitProcessError";
|
|
8536
|
+
}
|
|
8537
|
+
};
|
|
8538
|
+
var STDERR_CAP_BYTES = 64 * 1024;
|
|
8539
|
+
var KILL_GRACE_MS = 2e3;
|
|
8540
|
+
function spawnGit(args, options) {
|
|
8541
|
+
const child = spawn("git", [...(options.config ?? []).flatMap((c) => ["-c", c]), ...args], {
|
|
8542
|
+
cwd: options.cwd,
|
|
8543
|
+
// An app's own ProcessEnv augmentation (Next.js requires NODE_ENV) says nothing about git's.
|
|
8544
|
+
env: options.env,
|
|
8545
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
8546
|
+
// Its own process group, so a stop reaches what git spawned too (bundle create's
|
|
8547
|
+
// pack-objects), which would otherwise outlive it holding the pipes open.
|
|
8548
|
+
detached: process.platform !== "win32"
|
|
8549
|
+
});
|
|
8550
|
+
const { stdout } = child;
|
|
8551
|
+
if (stdout === null)
|
|
8552
|
+
throw new Error("git was spawned without a stdout pipe");
|
|
8553
|
+
const command = args.find((arg) => !arg.startsWith("-")) ?? "git";
|
|
8554
|
+
let stderr = "";
|
|
8555
|
+
let stderrBytes = 0;
|
|
8556
|
+
let killedFor = null;
|
|
8557
|
+
let closed = false;
|
|
8558
|
+
let exitedCleanly = false;
|
|
8559
|
+
let inactivity;
|
|
8560
|
+
const idle = () => {
|
|
8561
|
+
if (stdout.readableLength > 0 && !stdout.destroyed) {
|
|
8562
|
+
inactivity = setTimeout(idle, options.inactivityTimeoutMs);
|
|
8563
|
+
return;
|
|
8564
|
+
}
|
|
8565
|
+
stop(`git ${command} wrote nothing for ${options.inactivityTimeoutMs}ms`);
|
|
8566
|
+
};
|
|
8567
|
+
const touch = () => {
|
|
8568
|
+
options.onActivity?.();
|
|
8569
|
+
clearTimeout(inactivity);
|
|
8570
|
+
if (exitedCleanly)
|
|
8571
|
+
return;
|
|
8572
|
+
inactivity = setTimeout(idle, options.inactivityTimeoutMs);
|
|
8573
|
+
};
|
|
8574
|
+
const stop = (reason) => {
|
|
8575
|
+
if (closed || killedFor !== null)
|
|
8576
|
+
return;
|
|
8577
|
+
if (child.exitCode === 0 || child.exitCode === null && child.signalCode === null) {
|
|
8578
|
+
killedFor = reason;
|
|
8579
|
+
}
|
|
8580
|
+
terminate(child);
|
|
8581
|
+
stdout.destroy();
|
|
8582
|
+
};
|
|
8583
|
+
child.on("exit", (code) => {
|
|
8584
|
+
if (code !== 0)
|
|
8585
|
+
return;
|
|
8586
|
+
exitedCleanly = true;
|
|
8587
|
+
clearTimeout(inactivity);
|
|
8588
|
+
clearTimeout(deadline);
|
|
8589
|
+
});
|
|
8590
|
+
child.stderr?.on("data", (chunk) => {
|
|
8591
|
+
touch();
|
|
8592
|
+
if (stderrBytes < STDERR_CAP_BYTES) {
|
|
8593
|
+
stderr += chunk.subarray(0, STDERR_CAP_BYTES - stderrBytes).toString("utf8");
|
|
8594
|
+
}
|
|
8595
|
+
stderrBytes += chunk.length;
|
|
8596
|
+
});
|
|
8597
|
+
stdout.on("data", touch);
|
|
8598
|
+
const deadline = options.deadlineMs === void 0 ? void 0 : setTimeout(() => stop(`git ${command} ran longer than ${options.deadlineMs}ms`), options.deadlineMs);
|
|
8599
|
+
const onAbort = () => stop(`git ${command} was aborted`);
|
|
8600
|
+
if (options.signal?.aborted)
|
|
8601
|
+
onAbort();
|
|
8602
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
8603
|
+
const input = feedStdin(child, options.stdin, touch, (err) => stop(`git ${command}'s input failed: ${getErrorMessage(err)}`));
|
|
8604
|
+
input.catch(() => void 0);
|
|
8605
|
+
touch();
|
|
8606
|
+
const exited = new Promise((resolve2, reject) => {
|
|
8607
|
+
child.on("error", (err) => {
|
|
8608
|
+
if (child.pid === void 0) {
|
|
8609
|
+
clearTimeout(inactivity);
|
|
8610
|
+
clearTimeout(deadline);
|
|
8611
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
8612
|
+
reject(new GitProcessError(`git ${command} could not run: ${err.message}`, null, ""));
|
|
8613
|
+
}
|
|
8614
|
+
});
|
|
8615
|
+
child.on("close", (code, signal) => {
|
|
8616
|
+
closed = true;
|
|
8617
|
+
clearTimeout(inactivity);
|
|
8618
|
+
clearTimeout(deadline);
|
|
8619
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
8620
|
+
const written = stderrBytes > STDERR_CAP_BYTES ? `${stderr}
|
|
8621
|
+
[stderr truncated]` : stderr;
|
|
8622
|
+
if (killedFor === null && code === 0) {
|
|
8623
|
+
input.then(resolve2, (err) => reject(new GitProcessError(`git ${command}'s input failed: ${getErrorMessage(err)}`, 0, written)));
|
|
8624
|
+
return;
|
|
8625
|
+
}
|
|
8626
|
+
const why = killedFor ?? (written.trim() || (code === null ? `git ${command} was killed by ${signal ?? "a signal"}` : `git ${command} exited with code ${code}`));
|
|
8627
|
+
reject(new GitProcessError(why, code, written));
|
|
8628
|
+
});
|
|
8629
|
+
});
|
|
8630
|
+
exited.catch(() => void 0);
|
|
8631
|
+
return { stdout, exited, kill: stop };
|
|
8632
|
+
}
|
|
8633
|
+
async function runGit(args, options) {
|
|
8634
|
+
const max = options.maxStdoutBytes ?? 64 * 1024 * 1024;
|
|
8635
|
+
const proc = spawnGit(args, options);
|
|
8636
|
+
const chunks = [];
|
|
8637
|
+
let total = 0;
|
|
8638
|
+
const collect = new Writable({
|
|
8639
|
+
write(chunk, _encoding, callback) {
|
|
8640
|
+
total += chunk.length;
|
|
8641
|
+
if (total > max) {
|
|
8642
|
+
proc.kill(`git printed more than ${max} bytes`);
|
|
8643
|
+
callback();
|
|
8644
|
+
return;
|
|
8645
|
+
}
|
|
8646
|
+
chunks.push(chunk);
|
|
8647
|
+
callback();
|
|
8648
|
+
}
|
|
8649
|
+
});
|
|
8650
|
+
try {
|
|
8651
|
+
await Promise.all([pipeline(proc.stdout, collect).catch(() => void 0), proc.exited]);
|
|
8652
|
+
} catch (err) {
|
|
8653
|
+
if (options.stdoutInFailure && err instanceof GitProcessError && chunks.length > 0) {
|
|
8654
|
+
throw new GitProcessError(`${Buffer.concat(chunks).toString("utf8")}${err.message}`, err.exitCode, err.stderr);
|
|
8655
|
+
}
|
|
8656
|
+
throw err;
|
|
8657
|
+
}
|
|
8658
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
8659
|
+
}
|
|
8660
|
+
function terminate(child) {
|
|
8661
|
+
signalGroup(child, "SIGTERM");
|
|
8662
|
+
const timer = setTimeout(() => signalGroup(child, "SIGKILL"), KILL_GRACE_MS);
|
|
8663
|
+
timer.unref();
|
|
8664
|
+
child.once("close", () => clearTimeout(timer));
|
|
8665
|
+
}
|
|
8666
|
+
function signalGroup(child, signal) {
|
|
8667
|
+
if (child.pid !== void 0 && process.platform !== "win32") {
|
|
8668
|
+
try {
|
|
8669
|
+
process.kill(-child.pid, signal);
|
|
8670
|
+
return;
|
|
8671
|
+
} catch {
|
|
8672
|
+
}
|
|
8673
|
+
}
|
|
8674
|
+
child.kill(signal);
|
|
8675
|
+
}
|
|
8676
|
+
function feedStdin(child, source, touch, onSourceError) {
|
|
8677
|
+
const sink2 = child.stdin;
|
|
8678
|
+
if (sink2 === null)
|
|
8679
|
+
return Promise.resolve();
|
|
8680
|
+
sink2.on("error", () => void 0);
|
|
8681
|
+
if (source === void 0) {
|
|
8682
|
+
sink2.end();
|
|
8683
|
+
return Promise.resolve();
|
|
8684
|
+
}
|
|
8685
|
+
const readable = typeof source === "string" ? Readable.from([Buffer.from(source)]) : source;
|
|
8686
|
+
const counted = new Transform({
|
|
8687
|
+
transform(chunk, _encoding, callback) {
|
|
8688
|
+
touch();
|
|
8689
|
+
callback(null, chunk);
|
|
8690
|
+
}
|
|
8691
|
+
});
|
|
8692
|
+
return new Promise((resolve2, reject) => {
|
|
8693
|
+
readable.once("error", (err) => {
|
|
8694
|
+
onSourceError(err);
|
|
8695
|
+
reject(err);
|
|
8696
|
+
});
|
|
8697
|
+
sink2.once("close", () => {
|
|
8698
|
+
if (!readable.readableEnded)
|
|
8699
|
+
readable.destroy();
|
|
8700
|
+
resolve2();
|
|
8701
|
+
});
|
|
8702
|
+
readable.pipe(counted).pipe(sink2);
|
|
8703
|
+
});
|
|
8704
|
+
}
|
|
8705
|
+
|
|
8706
|
+
// dist/git-manager.js
|
|
8272
8707
|
var log9 = createDebugLogger({ prefix: "GitManager" });
|
|
8273
8708
|
var GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
|
|
8274
8709
|
var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
@@ -8292,6 +8727,13 @@ function gitNetworkChildEnv() {
|
|
|
8292
8727
|
return { ...env, ...FORCE_C_LOCALE };
|
|
8293
8728
|
}
|
|
8294
8729
|
var NO_AUTO_GC_CONFIG = ["gc.auto=0", "maintenance.auto=false"];
|
|
8730
|
+
var HASH_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
8731
|
+
var MAX_HASH_RUNS = 50;
|
|
8732
|
+
var LS_REMOTE_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
8733
|
+
var symlinkBlobSha = (target, likeSha) => {
|
|
8734
|
+
const body = Buffer.from(target, "utf8");
|
|
8735
|
+
return createHash2(likeSha.length === 64 ? "sha256" : "sha1").update(Buffer.concat([Buffer.from(`blob ${body.length}\0`), body])).digest("hex");
|
|
8736
|
+
};
|
|
8295
8737
|
var REMOTE_GIT_CONFIG = [
|
|
8296
8738
|
["gc.auto", "0"],
|
|
8297
8739
|
["receive.autogc", "false"],
|
|
@@ -8392,6 +8834,12 @@ async function ensureGitExcludePattern(repoPath, pattern) {
|
|
|
8392
8834
|
await fs16.writeFile(excludePath, newContent, "utf-8");
|
|
8393
8835
|
log9.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
8394
8836
|
}
|
|
8837
|
+
var BaseBranchMissingError = class extends Error {
|
|
8838
|
+
constructor(remote, baseBranch) {
|
|
8839
|
+
super(`CanopyCMS: remote '${remote}' has no branch '${baseBranch}'`);
|
|
8840
|
+
this.name = "BaseBranchMissingError";
|
|
8841
|
+
}
|
|
8842
|
+
};
|
|
8395
8843
|
var GitConflictError = class extends Error {
|
|
8396
8844
|
constructor(conflictedFiles) {
|
|
8397
8845
|
super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
|
|
@@ -8667,6 +9115,36 @@ var GitManager = class _GitManager {
|
|
|
8667
9115
|
}
|
|
8668
9116
|
return refs.includes(`refs/heads/${branch}`) || refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
|
|
8669
9117
|
}
|
|
9118
|
+
/**
|
|
9119
|
+
* The branches inside `branch` (`<branch>/…`) that a bare mirror holds as local heads or in
|
|
9120
|
+
* GitHub's tracking namespace, which git cannot hold beside `branch` itself. `for-each-ref`
|
|
9121
|
+
* matches a pattern up to a slash, so the pattern `<branch>` lists them. Throws, as
|
|
9122
|
+
* {@link bareRemoteHasBranch} does, when the mirror is unreadable.
|
|
9123
|
+
*/
|
|
9124
|
+
static async bareRemoteBranchesInside(remotePath, branch) {
|
|
9125
|
+
let output;
|
|
9126
|
+
try {
|
|
9127
|
+
output = await simpleGit2().raw([
|
|
9128
|
+
"--git-dir",
|
|
9129
|
+
remotePath,
|
|
9130
|
+
"for-each-ref",
|
|
9131
|
+
"--format=%(refname)",
|
|
9132
|
+
"--end-of-options",
|
|
9133
|
+
`refs/heads/${branch}`,
|
|
9134
|
+
`${GITHUB_TRACKING_REF_PREFIX}${branch}`
|
|
9135
|
+
]);
|
|
9136
|
+
} catch (err) {
|
|
9137
|
+
throw new Error(`Cannot inspect remote mirror at ${remotePath}: ${getErrorMessage(err)}`);
|
|
9138
|
+
}
|
|
9139
|
+
const inside = /* @__PURE__ */ new Set();
|
|
9140
|
+
for (const ref of output.split("\n").map((line) => line.trim())) {
|
|
9141
|
+
for (const prefix of ["refs/heads/", GITHUB_TRACKING_REF_PREFIX]) {
|
|
9142
|
+
if (ref.startsWith(`${prefix}${branch}/`))
|
|
9143
|
+
inside.add(ref.slice(prefix.length));
|
|
9144
|
+
}
|
|
9145
|
+
}
|
|
9146
|
+
return [...inside].sort();
|
|
9147
|
+
}
|
|
8670
9148
|
/**
|
|
8671
9149
|
* Delete `refs/heads/<branch>` from a bare local mirror, if present. A
|
|
8672
9150
|
* no-op (not an error) when the ref doesn't exist.
|
|
@@ -9365,6 +9843,199 @@ var GitManager = class _GitManager {
|
|
|
9365
9843
|
]);
|
|
9366
9844
|
return output.split("\0").filter((p) => p.length > 0);
|
|
9367
9845
|
}
|
|
9846
|
+
/**
|
|
9847
|
+
* Every file in `rev`'s tree below repo-relative directory `under`, with its size in bytes,
|
|
9848
|
+
* so a caller can refuse a file too large to read before reading it.
|
|
9849
|
+
*/
|
|
9850
|
+
async listFilesWithSizeAt(rev, under) {
|
|
9851
|
+
const dir = `${under.replace(/\/+$/, "")}/`;
|
|
9852
|
+
const output = await this.git.raw([
|
|
9853
|
+
"ls-tree",
|
|
9854
|
+
"-r",
|
|
9855
|
+
"-z",
|
|
9856
|
+
"-l",
|
|
9857
|
+
"--full-tree",
|
|
9858
|
+
"--end-of-options",
|
|
9859
|
+
rev,
|
|
9860
|
+
dir
|
|
9861
|
+
]);
|
|
9862
|
+
const files = [];
|
|
9863
|
+
for (const record of output.split("\0")) {
|
|
9864
|
+
const tab = record.indexOf(" ");
|
|
9865
|
+
if (tab === -1)
|
|
9866
|
+
continue;
|
|
9867
|
+
const [, type, , size] = record.slice(0, tab).split(/ +/);
|
|
9868
|
+
const filePath = record.slice(tab + 1);
|
|
9869
|
+
if (type === "blob" && filePath.startsWith(dir)) {
|
|
9870
|
+
files.push({ path: filePath, size: Number(size) });
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
return files;
|
|
9874
|
+
}
|
|
9875
|
+
/**
|
|
9876
|
+
* The commit HEAD forked from the base branch at, or `undefined` when they share no history.
|
|
9877
|
+
* Takes the base tip from the remote by `ls-remote` rather than `<remote>/<base>`: the worker
|
|
9878
|
+
* fetches by path, which moves no remote-tracking ref, so that ref is left behind by every
|
|
9879
|
+
* rebase. A tip this clone lacks is fetched as objects only (`--refmap=`, `--no-tags`,
|
|
9880
|
+
* `--no-write-fetch-head`, git 2.29+): no ref and no FETCH_HEAD moves, so a concurrent fetch
|
|
9881
|
+
* pinning FETCH_HEAD is never repointed. Throws {@link BaseBranchMissingError} when the
|
|
9882
|
+
* remote has no base branch, and any other error when it can't be read.
|
|
9883
|
+
*/
|
|
9884
|
+
async mergeBaseWithBase() {
|
|
9885
|
+
const tip = await this.remoteBranchTip(this.baseBranch);
|
|
9886
|
+
if (!tip)
|
|
9887
|
+
throw new BaseBranchMissingError(this.remote, this.baseBranch);
|
|
9888
|
+
if (!await this.hasCommit(tip)) {
|
|
9889
|
+
await this.git.raw([
|
|
9890
|
+
"fetch",
|
|
9891
|
+
"--refmap=",
|
|
9892
|
+
"--no-tags",
|
|
9893
|
+
"--no-write-fetch-head",
|
|
9894
|
+
"--end-of-options",
|
|
9895
|
+
this.remote,
|
|
9896
|
+
`refs/heads/${this.baseBranch}`
|
|
9897
|
+
]);
|
|
9898
|
+
if (!await this.hasCommit(tip)) {
|
|
9899
|
+
throw new Error(`CanopyCMS: could not fetch ${this.baseBranch}'s tip ${tip}`);
|
|
9900
|
+
}
|
|
9901
|
+
}
|
|
9902
|
+
const forkPoint = (await this.git.raw(["merge-base", "--end-of-options", "HEAD", tip])).trim();
|
|
9903
|
+
return forkPoint || void 0;
|
|
9904
|
+
}
|
|
9905
|
+
/** Whether this clone holds commit `sha`. */
|
|
9906
|
+
async hasCommit(sha) {
|
|
9907
|
+
return (await this.git.raw(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`])).trim() !== "";
|
|
9908
|
+
}
|
|
9909
|
+
/**
|
|
9910
|
+
* Repo-relative paths whose working-tree content differs from commit `rev`, with `T` (a type
|
|
9911
|
+
* change) reported as `M`. A sparse clone leaves a path outside its cone unchecked out, marked
|
|
9912
|
+
* skip-worktree, and git reads that path's index entry in place of the missing file: it is never
|
|
9913
|
+
* reported deleted, and differs from `rev` only as the index does.
|
|
9914
|
+
*
|
|
9915
|
+
* `diff-index`, not `git diff`: the porcelain refreshes the index and writes it back under
|
|
9916
|
+
* `index.lock`, which would fail a concurrent writer's own git step. `diff-index` writes
|
|
9917
|
+
* nothing, so it reports a file whose stat changed but whose content did not; those are
|
|
9918
|
+
* hashed here and dropped when they match `rev`.
|
|
9919
|
+
*/
|
|
9920
|
+
async diffNameStatusAgainst(rev) {
|
|
9921
|
+
const output = await this.git.raw([
|
|
9922
|
+
"diff-index",
|
|
9923
|
+
"--raw",
|
|
9924
|
+
"-z",
|
|
9925
|
+
"--no-renames",
|
|
9926
|
+
"--end-of-options",
|
|
9927
|
+
rev,
|
|
9928
|
+
"--"
|
|
9929
|
+
]);
|
|
9930
|
+
const tokens = output.split("\0");
|
|
9931
|
+
const changes = [];
|
|
9932
|
+
for (let i = 0; i + 1 < tokens.length; i += 2) {
|
|
9933
|
+
const [srcMode, dstMode, srcSha, dstSha, code] = tokens[i].slice(1).split(" ");
|
|
9934
|
+
const path27 = tokens[i + 1];
|
|
9935
|
+
const letter = code?.charAt(0);
|
|
9936
|
+
if (letter === "A" || letter === "D")
|
|
9937
|
+
changes.push({ status: letter, path: path27 });
|
|
9938
|
+
else if (letter === "M" || letter === "T") {
|
|
9939
|
+
const unhashed = /^0+$/.test(dstSha) && srcMode === dstMode ? srcSha : void 0;
|
|
9940
|
+
changes.push({ status: "M", path: path27, unhashed });
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9943
|
+
const unchanged = await this.matchingBlobs(changes.flatMap((c) => c.unhashed ? [{ path: c.path, sha: c.unhashed }] : []));
|
|
9944
|
+
return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path: path27 }) => ({ status, path: path27 }));
|
|
9945
|
+
}
|
|
9946
|
+
/**
|
|
9947
|
+
* The paths whose working-tree file hashes to `sha`, as git hashes it: a symlink's blob is its
|
|
9948
|
+
* target, a file's goes through `hash-object --stdin-paths`, so its attributes' filters apply.
|
|
9949
|
+
* Callers pass only paths whose mode is unchanged, since diff-index reports the working tree's
|
|
9950
|
+
* own mode. A path that is gone or can't be hashed matches nothing, so it stays reported as
|
|
9951
|
+
* changed. When git refuses a path (exit 128), having printed
|
|
9952
|
+
* the SHAs before it, the next run resumes past that path, for at most {@link MAX_HASH_RUNS}
|
|
9953
|
+
* runs. A run that ends any other way (killed, timed out, a signal) stops the hashing, and the
|
|
9954
|
+
* paths it didn't reach stay changed.
|
|
9955
|
+
*/
|
|
9956
|
+
async matchingBlobs(files) {
|
|
9957
|
+
const matching = /* @__PURE__ */ new Set();
|
|
9958
|
+
const regular = [];
|
|
9959
|
+
for (const file of files) {
|
|
9960
|
+
if (file.path.includes("\n"))
|
|
9961
|
+
continue;
|
|
9962
|
+
const stat = await fs16.lstat(path20.join(this.repoPath, file.path)).catch(() => void 0);
|
|
9963
|
+
if (!stat)
|
|
9964
|
+
continue;
|
|
9965
|
+
if (stat.isSymbolicLink()) {
|
|
9966
|
+
const target = await fs16.readlink(path20.join(this.repoPath, file.path)).catch(() => void 0);
|
|
9967
|
+
if (target !== void 0 && symlinkBlobSha(target, file.sha) === file.sha) {
|
|
9968
|
+
matching.add(file.path);
|
|
9969
|
+
}
|
|
9970
|
+
} else if (stat.isFile())
|
|
9971
|
+
regular.push(file);
|
|
9972
|
+
}
|
|
9973
|
+
for (let start = 0, runs = 0; start < regular.length && runs < MAX_HASH_RUNS; runs++) {
|
|
9974
|
+
const batch = regular.slice(start);
|
|
9975
|
+
const { shas, outcome } = await this.hashPaths(batch.map((f) => f.path));
|
|
9976
|
+
shas.forEach((sha, i) => {
|
|
9977
|
+
if (batch[i] && sha === batch[i].sha)
|
|
9978
|
+
matching.add(batch[i].path);
|
|
9979
|
+
});
|
|
9980
|
+
if (outcome !== "refused")
|
|
9981
|
+
break;
|
|
9982
|
+
start += shas.length + 1;
|
|
9983
|
+
}
|
|
9984
|
+
return matching;
|
|
9985
|
+
}
|
|
9986
|
+
/**
|
|
9987
|
+
* `hash-object --stdin-paths` over `paths`: one SHA per path, in order, up to where it ended.
|
|
9988
|
+
* `refused` is git's own exit 128 on a path it can't hash; `stopped` is any other ending.
|
|
9989
|
+
*/
|
|
9990
|
+
async hashPaths(paths) {
|
|
9991
|
+
const chunks = [];
|
|
9992
|
+
let proc;
|
|
9993
|
+
try {
|
|
9994
|
+
proc = spawnGit(["hash-object", "--stdin-paths"], {
|
|
9995
|
+
...this.processOptions(),
|
|
9996
|
+
stdin: `${paths.join("\n")}
|
|
9997
|
+
`,
|
|
9998
|
+
inactivityTimeoutMs: HASH_INACTIVITY_TIMEOUT_MS
|
|
9999
|
+
});
|
|
10000
|
+
} catch {
|
|
10001
|
+
return { shas: [], outcome: "stopped" };
|
|
10002
|
+
}
|
|
10003
|
+
const read = (async () => {
|
|
10004
|
+
for await (const chunk of proc.stdout)
|
|
10005
|
+
chunks.push(chunk);
|
|
10006
|
+
})().catch(() => {
|
|
10007
|
+
});
|
|
10008
|
+
const outcome = await proc.exited.then(() => "complete", (err) => err instanceof GitProcessError && err.exitCode === 128 ? "refused" : "stopped");
|
|
10009
|
+
await read;
|
|
10010
|
+
return { shas: Buffer.concat(chunks).toString("utf8").split("\n").filter(Boolean), outcome };
|
|
10011
|
+
}
|
|
10012
|
+
/** What a git run outside simple-git needs to run as `this.git` does. */
|
|
10013
|
+
processOptions() {
|
|
10014
|
+
return {
|
|
10015
|
+
cwd: this.repoPath,
|
|
10016
|
+
env: gitChildEnv({ GIT_CEILING_DIRECTORIES: path20.dirname(this.repoPath) }),
|
|
10017
|
+
config: NO_AUTO_GC_CONFIG
|
|
10018
|
+
};
|
|
10019
|
+
}
|
|
10020
|
+
/** Repo-relative paths of the working tree's untracked, non-ignored files. */
|
|
10021
|
+
async listUntrackedFiles() {
|
|
10022
|
+
const output = await this.git.raw(["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
10023
|
+
return output.split("\0").filter((p) => p.length > 0);
|
|
10024
|
+
}
|
|
10025
|
+
/**
|
|
10026
|
+
* The text of repo-relative `filePath` at commit `rev`, or `undefined` when `rev` has no such
|
|
10027
|
+
* path. Any other failure, such as an unknown `rev`, throws.
|
|
10028
|
+
*/
|
|
10029
|
+
async showFile(rev, filePath) {
|
|
10030
|
+
try {
|
|
10031
|
+
return await this.git.raw(["show", "--end-of-options", `${rev}:${filePath}`]);
|
|
10032
|
+
} catch (err) {
|
|
10033
|
+
if (/fatal: path '.*' (does not exist in|exists on disk, but not in) '/.test(getErrorMessage(err))) {
|
|
10034
|
+
return void 0;
|
|
10035
|
+
}
|
|
10036
|
+
throw err;
|
|
10037
|
+
}
|
|
10038
|
+
}
|
|
9368
10039
|
async ensureAuthor(author) {
|
|
9369
10040
|
const config = await this.git.listConfig();
|
|
9370
10041
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -9535,7 +10206,10 @@ var GitManager = class _GitManager {
|
|
|
9535
10206
|
*/
|
|
9536
10207
|
async remoteBranchTip(branch) {
|
|
9537
10208
|
const ref = `refs/heads/${branch}`;
|
|
9538
|
-
const output = await
|
|
10209
|
+
const output = await runGit(["ls-remote", "--heads", "--end-of-options", this.remote, ref], {
|
|
10210
|
+
...this.processOptions(),
|
|
10211
|
+
inactivityTimeoutMs: LS_REMOTE_INACTIVITY_TIMEOUT_MS
|
|
10212
|
+
});
|
|
9539
10213
|
for (const line of output.split("\n")) {
|
|
9540
10214
|
const [sha, name] = line.trim().split(" ");
|
|
9541
10215
|
if (sha && name === ref)
|
|
@@ -9616,7 +10290,7 @@ var REPAIR_MAX_AGE_MS = 20 * 6e4;
|
|
|
9616
10290
|
var DELETING_MAX_AGE_MS = 10 * 6e4;
|
|
9617
10291
|
var SETTINGS_TRASH_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
|
|
9618
10292
|
var BranchProvisioningBusyError = class extends Error {
|
|
9619
|
-
constructor(dirName, message = `Branch '${
|
|
10293
|
+
constructor(dirName, shortName = dirName, message = `Branch '${shortName}' is still being set up. Try again in a minute.`) {
|
|
9620
10294
|
super(message);
|
|
9621
10295
|
this.dirName = dirName;
|
|
9622
10296
|
this.name = "BranchProvisioningBusyError";
|
|
@@ -9633,7 +10307,7 @@ function formatDirStamp(date) {
|
|
|
9633
10307
|
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
9634
10308
|
}
|
|
9635
10309
|
function shortDirName(dirName) {
|
|
9636
|
-
const digest =
|
|
10310
|
+
const digest = createHash3("sha1").update(dirName).digest("hex").slice(0, 10);
|
|
9637
10311
|
return `${dirName.slice(0, 40)}-${digest}`;
|
|
9638
10312
|
}
|
|
9639
10313
|
function stampedName(prefix, dirName, now) {
|
|
@@ -9825,12 +10499,13 @@ async function stageBranchWorkspace(options) {
|
|
|
9825
10499
|
throw err;
|
|
9826
10500
|
}
|
|
9827
10501
|
}
|
|
9828
|
-
async function lockBranchName(baseRoot, dirName) {
|
|
10502
|
+
async function lockBranchName(baseRoot, dirName, shortName = dirName) {
|
|
9829
10503
|
try {
|
|
9830
10504
|
return await acquireProvisioningLockWithin(baseRoot, branchProvisioningLockName(dirName), PUBLISH_LOCK_WAIT_MS);
|
|
9831
10505
|
} catch (err) {
|
|
9832
|
-
if (isNodeError(err) && err.code === "ELOCKED")
|
|
9833
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10506
|
+
if (isNodeError(err) && err.code === "ELOCKED") {
|
|
10507
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10508
|
+
}
|
|
9834
10509
|
throw err;
|
|
9835
10510
|
}
|
|
9836
10511
|
}
|
|
@@ -9844,9 +10519,9 @@ async function inspectBlocking(finalPath) {
|
|
|
9844
10519
|
}
|
|
9845
10520
|
var RENAME_BLOCKED = /* @__PURE__ */ new Set(["EEXIST", "ENOTEMPTY", "ENOTDIR"]);
|
|
9846
10521
|
async function publishStaging(options) {
|
|
9847
|
-
const { baseRoot, dirName, stagingPath } = options;
|
|
10522
|
+
const { baseRoot, dirName, stagingPath, shortName = dirName } = options;
|
|
9848
10523
|
const finalPath = path21.join(baseRoot, dirName);
|
|
9849
|
-
const release = await lockBranchName(baseRoot, dirName);
|
|
10524
|
+
const release = await lockBranchName(baseRoot, dirName, shortName);
|
|
9850
10525
|
try {
|
|
9851
10526
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
9852
10527
|
try {
|
|
@@ -9868,7 +10543,7 @@ async function publishStaging(options) {
|
|
|
9868
10543
|
if (blocking === "live")
|
|
9869
10544
|
return "exists";
|
|
9870
10545
|
if (blocking === "protected")
|
|
9871
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10546
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
9872
10547
|
const result = await quarantineResidueAt(baseRoot, dirName, {
|
|
9873
10548
|
minQuietMs: CREATE_RESIDUE_MIN_QUIET_MS,
|
|
9874
10549
|
expectedRemoteUrl: options.expectedRemoteUrl
|
|
@@ -9883,9 +10558,9 @@ async function publishStaging(options) {
|
|
|
9883
10558
|
}
|
|
9884
10559
|
if (result.reason === "foreign")
|
|
9885
10560
|
throw new BranchDirOccupiedError(dirName);
|
|
9886
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10561
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
9887
10562
|
}
|
|
9888
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10563
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
9889
10564
|
} finally {
|
|
9890
10565
|
await release().catch((err) => {
|
|
9891
10566
|
canopyLogWarn(`[canopy] Failed to release the provisioning lock for ${dirName}:`, err);
|
|
@@ -10017,12 +10692,16 @@ var BranchWorkspaceManager = class {
|
|
|
10017
10692
|
if (isSettingsBranchName(options.branchName, operatingStrategy(options.mode).getSettingsBranchName(this.config))) {
|
|
10018
10693
|
throw new BranchPathError("Settings branches are not content branches");
|
|
10019
10694
|
}
|
|
10020
|
-
const
|
|
10695
|
+
const named = parseBranchRef(options.branchName);
|
|
10696
|
+
if (!named.ok)
|
|
10697
|
+
throw new BranchPathError(named.error);
|
|
10698
|
+
const { branchRoot, baseRoot } = resolveBranchPath(options);
|
|
10021
10699
|
const paths = {
|
|
10022
10700
|
branchRoot,
|
|
10023
10701
|
baseRoot,
|
|
10024
10702
|
dirName: path23.basename(branchRoot),
|
|
10025
|
-
|
|
10703
|
+
ref: options.branchName,
|
|
10704
|
+
shortName: shortNameOf({ ...this.config, mode: options.mode }, options.branchName)
|
|
10026
10705
|
};
|
|
10027
10706
|
const existing = await readBranchMetadataFile(branchRoot);
|
|
10028
10707
|
if (existing) {
|
|
@@ -10040,7 +10719,7 @@ var BranchWorkspaceManager = class {
|
|
|
10040
10719
|
}
|
|
10041
10720
|
}
|
|
10042
10721
|
async provisionUnshared(options, paths) {
|
|
10043
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
10722
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
10044
10723
|
await fs19.mkdir(baseRoot, { recursive: true });
|
|
10045
10724
|
const baseBranch = await GitManager.resolveWorkspaceBaseBranch({
|
|
10046
10725
|
...this.config,
|
|
@@ -10058,7 +10737,7 @@ var BranchWorkspaceManager = class {
|
|
|
10058
10737
|
});
|
|
10059
10738
|
const metadata = buildInitialBranchMetadata({
|
|
10060
10739
|
branch: {
|
|
10061
|
-
name: paths.
|
|
10740
|
+
name: paths.ref,
|
|
10062
10741
|
title: options.title,
|
|
10063
10742
|
description: options.description,
|
|
10064
10743
|
access: options.access,
|
|
@@ -10070,14 +10749,14 @@ var BranchWorkspaceManager = class {
|
|
|
10070
10749
|
if (state.kind === "live" || state.kind === "corrupt")
|
|
10071
10750
|
return this.existing(paths);
|
|
10072
10751
|
if (state.kind === "protected")
|
|
10073
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10752
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10074
10753
|
if (state.kind === "foreign") {
|
|
10075
10754
|
if (options.mode !== "dev")
|
|
10076
10755
|
throw new BranchDirOccupiedError(dirName);
|
|
10077
10756
|
return this.adoptInPlace(options, paths, baseBranch, metadata);
|
|
10078
10757
|
}
|
|
10079
10758
|
if (state.kind === "residue" && await this.recentlyActive(paths)) {
|
|
10080
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10759
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10081
10760
|
}
|
|
10082
10761
|
const sparseCone = sparseConeFor(this.config.contentRoot);
|
|
10083
10762
|
await recordSparseCone(baseRoot, sparseCone).catch((err) => {
|
|
@@ -10102,11 +10781,11 @@ var BranchWorkspaceManager = class {
|
|
|
10102
10781
|
return outcome;
|
|
10103
10782
|
}
|
|
10104
10783
|
async stageAndPublish(paths, run) {
|
|
10105
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
10784
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
10106
10785
|
const staged = await stageBranchWorkspace({
|
|
10107
10786
|
baseRoot,
|
|
10108
10787
|
dirName,
|
|
10109
|
-
branchName: paths.
|
|
10788
|
+
branchName: paths.ref,
|
|
10110
10789
|
baseBranch: run.baseBranch,
|
|
10111
10790
|
remoteUrl: run.remoteUrl,
|
|
10112
10791
|
clone: {
|
|
@@ -10126,6 +10805,7 @@ var BranchWorkspaceManager = class {
|
|
|
10126
10805
|
published = await run.provisionLog.step("publish", () => publishStaging({
|
|
10127
10806
|
baseRoot,
|
|
10128
10807
|
dirName,
|
|
10808
|
+
shortName,
|
|
10129
10809
|
stagingPath: staged.stagingPath,
|
|
10130
10810
|
writeId: staged.meta.writeId,
|
|
10131
10811
|
expectedRemoteUrl: run.remoteUrl,
|
|
@@ -10145,16 +10825,16 @@ var BranchWorkspaceManager = class {
|
|
|
10145
10825
|
* is, given branch.json under the branch's lock.
|
|
10146
10826
|
*/
|
|
10147
10827
|
async adoptInPlace(options, paths, baseBranch, metadata) {
|
|
10148
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
10828
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
10149
10829
|
const provisionLog = new ProvisionLog(dirName);
|
|
10150
10830
|
let created = false;
|
|
10151
10831
|
try {
|
|
10152
|
-
const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName));
|
|
10832
|
+
const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName, shortName));
|
|
10153
10833
|
try {
|
|
10154
10834
|
if (!await readBranchMetadataFile(branchRoot)) {
|
|
10155
10835
|
await GitManager.initializeWorkspace({
|
|
10156
10836
|
workspacePath: branchRoot,
|
|
10157
|
-
branchName: paths.
|
|
10837
|
+
branchName: paths.ref,
|
|
10158
10838
|
mode: options.mode,
|
|
10159
10839
|
baseBranch,
|
|
10160
10840
|
sourceRoot: this.config.sourceRoot,
|
|
@@ -10189,7 +10869,7 @@ var BranchWorkspaceManager = class {
|
|
|
10189
10869
|
return this.existing(paths);
|
|
10190
10870
|
const meta = await readBranchMetadataFile(branchRoot);
|
|
10191
10871
|
if (!meta)
|
|
10192
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10872
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10193
10873
|
return { kind: "created", context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
10194
10874
|
}
|
|
10195
10875
|
/** Whether residue at the branch path may still belong to a live process. */
|
|
@@ -10199,10 +10879,10 @@ var BranchWorkspaceManager = class {
|
|
|
10199
10879
|
return true;
|
|
10200
10880
|
return await quietAgeMs(branchRoot) < CREATE_RESIDUE_MIN_QUIET_MS;
|
|
10201
10881
|
}
|
|
10202
|
-
async existing({ branchRoot, baseRoot, dirName }) {
|
|
10882
|
+
async existing({ branchRoot, baseRoot, dirName, shortName }) {
|
|
10203
10883
|
const meta = await readBranchMetadataFile(branchRoot);
|
|
10204
10884
|
if (!meta)
|
|
10205
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10885
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10206
10886
|
return { kind: "exists", context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
10207
10887
|
}
|
|
10208
10888
|
};
|
|
@@ -10286,21 +10966,23 @@ var normalizePath = (root, target) => {
|
|
|
10286
10966
|
}
|
|
10287
10967
|
return result.normalizedPath;
|
|
10288
10968
|
};
|
|
10969
|
+
var parseEntryParts = (raw, format) => {
|
|
10970
|
+
if (format === "json")
|
|
10971
|
+
return { data: asRecord(JSON.parse(raw)) };
|
|
10972
|
+
if (format === "yaml")
|
|
10973
|
+
return { data: asRecord(yamlParse2(raw)) };
|
|
10974
|
+
const parsed = matter3(raw);
|
|
10975
|
+
return { data: { ...parsed.data ?? {} }, body: parsed.content };
|
|
10976
|
+
};
|
|
10977
|
+
var parseEntryData = (raw, format, bodyFieldName = "body") => {
|
|
10978
|
+
const { data, body } = parseEntryParts(raw, format);
|
|
10979
|
+
if (body)
|
|
10980
|
+
data[bodyFieldName] = body;
|
|
10981
|
+
return data;
|
|
10982
|
+
};
|
|
10289
10983
|
var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
10290
10984
|
try {
|
|
10291
|
-
|
|
10292
|
-
if (format === "json") {
|
|
10293
|
-
return asRecord(JSON.parse(raw));
|
|
10294
|
-
}
|
|
10295
|
-
if (format === "yaml") {
|
|
10296
|
-
return asRecord(yamlParse2(raw));
|
|
10297
|
-
}
|
|
10298
|
-
const parsed = matter3(raw);
|
|
10299
|
-
const data = { ...parsed.data ?? {} };
|
|
10300
|
-
if (parsed.content) {
|
|
10301
|
-
data[bodyFieldName] = parsed.content;
|
|
10302
|
-
}
|
|
10303
|
-
return data;
|
|
10985
|
+
return parseEntryData(await fs20.readFile(filePath, "utf8"), format, bodyFieldName);
|
|
10304
10986
|
} catch (err) {
|
|
10305
10987
|
if (isNotFoundError(err))
|
|
10306
10988
|
return {};
|