canopycms 0.7.1 → 0.8.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/CHANGELOG.md +187 -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-workspace.js +22 -15
- package/dist/cli/cli.js +1060 -172
- package/dist/cli/generate-ai-content.js +732 -101
- 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.js +23 -16
- package/dist/context.js +8 -7
- 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/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 +40 -25
- 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);
|
|
@@ -4538,7 +4705,7 @@ function registryFingerprint(registry2) {
|
|
|
4538
4705
|
init_flatten();
|
|
4539
4706
|
|
|
4540
4707
|
// dist/validation/entry-type-reference-validator.js
|
|
4541
|
-
|
|
4708
|
+
init_validation2();
|
|
4542
4709
|
var editDistance = (a, b) => {
|
|
4543
4710
|
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
4544
4711
|
for (let i = 1; i <= a.length; i++) {
|
|
@@ -4997,24 +5164,13 @@ function validateAndNormalizePath(root, target) {
|
|
|
4997
5164
|
};
|
|
4998
5165
|
}
|
|
4999
5166
|
|
|
5167
|
+
// dist/paths/index.js
|
|
5168
|
+
init_validation();
|
|
5169
|
+
|
|
5000
5170
|
// dist/paths/branch.js
|
|
5001
5171
|
init_operating_mode();
|
|
5002
5172
|
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
|
|
5173
|
+
init_branch_name();
|
|
5018
5174
|
var BranchPathError = class extends Error {
|
|
5019
5175
|
};
|
|
5020
5176
|
var resolveContentBranchesRoot = (mode, override) => {
|
|
@@ -5043,6 +5199,9 @@ function resolveBranchPath(options) {
|
|
|
5043
5199
|
return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
|
|
5044
5200
|
}
|
|
5045
5201
|
|
|
5202
|
+
// dist/paths/index.js
|
|
5203
|
+
init_branch_name();
|
|
5204
|
+
|
|
5046
5205
|
// dist/content-store.js
|
|
5047
5206
|
async function withLocks(keys, fn) {
|
|
5048
5207
|
const sorted = Array.from(new Set(keys)).sort();
|
|
@@ -7640,7 +7799,13 @@ var branchMetadataShape = {
|
|
|
7640
7799
|
historyRewrittenFrom: z7.string().optional(),
|
|
7641
7800
|
syncFailureReason: z7.string().optional(),
|
|
7642
7801
|
editors: z7.array(z7.string()).optional(),
|
|
7643
|
-
uncommittedEditors: z7.array(z7.string()).optional()
|
|
7802
|
+
uncommittedEditors: z7.array(z7.string()).optional(),
|
|
7803
|
+
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
7804
|
+
review: z7.object({
|
|
7805
|
+
state: z7.enum(["approved", "changes-requested"]),
|
|
7806
|
+
by: z7.object({ userId: z7.string(), name: z7.string().optional() }),
|
|
7807
|
+
at: z7.string()
|
|
7808
|
+
}).optional().catch(void 0)
|
|
7644
7809
|
};
|
|
7645
7810
|
var branchMetadataSchema = z7.object(branchMetadataShape).passthrough();
|
|
7646
7811
|
var branchMetadataFileSchema = z7.object({
|
|
@@ -7781,14 +7946,23 @@ var BranchRegistry = class {
|
|
|
7781
7946
|
* genuinely-absent branch costs at most one extra scan per window.
|
|
7782
7947
|
*/
|
|
7783
7948
|
async get(name) {
|
|
7784
|
-
|
|
7785
|
-
|
|
7949
|
+
return this.find((b) => b.branch.name === name);
|
|
7950
|
+
}
|
|
7951
|
+
/**
|
|
7952
|
+
* Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
|
|
7953
|
+
* whatever its name, with {@link get}'s backstop.
|
|
7954
|
+
*/
|
|
7955
|
+
async getByKey(key) {
|
|
7956
|
+
return this.find((b) => path15.basename(b.branchRoot) === key);
|
|
7957
|
+
}
|
|
7958
|
+
async find(matches) {
|
|
7959
|
+
const found = (await this.list()).find(matches);
|
|
7786
7960
|
if (found)
|
|
7787
7961
|
return found;
|
|
7788
7962
|
if (!this.shouldForceRefresh())
|
|
7789
7963
|
return void 0;
|
|
7790
7964
|
const refreshed = await this.regenerate();
|
|
7791
|
-
return refreshed.find(
|
|
7965
|
+
return refreshed.find(matches);
|
|
7792
7966
|
}
|
|
7793
7967
|
shouldForceRefresh() {
|
|
7794
7968
|
const now = Date.now();
|
|
@@ -8257,18 +8431,228 @@ var loadBranchContext = async (options) => {
|
|
|
8257
8431
|
};
|
|
8258
8432
|
};
|
|
8259
8433
|
|
|
8434
|
+
// dist/branch-param.js
|
|
8435
|
+
init_operating_mode();
|
|
8436
|
+
init_branch_name();
|
|
8437
|
+
function cmsBranchNamespace(config) {
|
|
8438
|
+
try {
|
|
8439
|
+
return operatingStrategy(config.mode).getBranchNamespace(config);
|
|
8440
|
+
} catch {
|
|
8441
|
+
return null;
|
|
8442
|
+
}
|
|
8443
|
+
}
|
|
8444
|
+
function namesBaseOrActive(param, config) {
|
|
8445
|
+
let base;
|
|
8446
|
+
try {
|
|
8447
|
+
base = baseBranchOf(config);
|
|
8448
|
+
} catch (err) {
|
|
8449
|
+
if (err instanceof BaseBranchUnresolvedError)
|
|
8450
|
+
return false;
|
|
8451
|
+
throw err;
|
|
8452
|
+
}
|
|
8453
|
+
return param === base || param === (config.defaultActiveBranch ?? base);
|
|
8454
|
+
}
|
|
8455
|
+
function shortNameOf(config, name) {
|
|
8456
|
+
const short = branchShortName(name, cmsBranchNamespace(config));
|
|
8457
|
+
return short !== name && namesBaseOrActive(short, config) ? name : short;
|
|
8458
|
+
}
|
|
8459
|
+
|
|
8260
8460
|
// dist/branch-provisioning.js
|
|
8261
|
-
import { createHash as
|
|
8461
|
+
import { createHash as createHash3, randomBytes } from "node:crypto";
|
|
8262
8462
|
import fs17 from "node:fs/promises";
|
|
8263
8463
|
import path21 from "node:path";
|
|
8264
8464
|
|
|
8265
8465
|
// dist/git-manager.js
|
|
8466
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
8266
8467
|
import fs16 from "node:fs/promises";
|
|
8267
8468
|
import path20 from "node:path";
|
|
8268
8469
|
import { fileURLToPath } from "node:url";
|
|
8269
8470
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
8270
8471
|
init_error();
|
|
8271
8472
|
init_logger();
|
|
8473
|
+
|
|
8474
|
+
// dist/worker/git-process.js
|
|
8475
|
+
init_error();
|
|
8476
|
+
import { spawn } from "node:child_process";
|
|
8477
|
+
import { Readable, Transform, Writable } from "node:stream";
|
|
8478
|
+
import { pipeline } from "node:stream/promises";
|
|
8479
|
+
var GitProcessError = class extends Error {
|
|
8480
|
+
constructor(message, exitCode, stderr) {
|
|
8481
|
+
super(message);
|
|
8482
|
+
this.exitCode = exitCode;
|
|
8483
|
+
this.stderr = stderr;
|
|
8484
|
+
this.name = "GitProcessError";
|
|
8485
|
+
}
|
|
8486
|
+
};
|
|
8487
|
+
var STDERR_CAP_BYTES = 64 * 1024;
|
|
8488
|
+
var KILL_GRACE_MS = 2e3;
|
|
8489
|
+
function spawnGit(args, options) {
|
|
8490
|
+
const child = spawn("git", [...(options.config ?? []).flatMap((c) => ["-c", c]), ...args], {
|
|
8491
|
+
cwd: options.cwd,
|
|
8492
|
+
// An app's own ProcessEnv augmentation (Next.js requires NODE_ENV) says nothing about git's.
|
|
8493
|
+
env: options.env,
|
|
8494
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
8495
|
+
// Its own process group, so a stop reaches what git spawned too (bundle create's
|
|
8496
|
+
// pack-objects), which would otherwise outlive it holding the pipes open.
|
|
8497
|
+
detached: process.platform !== "win32"
|
|
8498
|
+
});
|
|
8499
|
+
const { stdout } = child;
|
|
8500
|
+
if (stdout === null)
|
|
8501
|
+
throw new Error("git was spawned without a stdout pipe");
|
|
8502
|
+
const command = args.find((arg) => !arg.startsWith("-")) ?? "git";
|
|
8503
|
+
let stderr = "";
|
|
8504
|
+
let stderrBytes = 0;
|
|
8505
|
+
let killedFor = null;
|
|
8506
|
+
let closed = false;
|
|
8507
|
+
let exitedCleanly = false;
|
|
8508
|
+
let inactivity;
|
|
8509
|
+
const idle = () => {
|
|
8510
|
+
if (stdout.readableLength > 0 && !stdout.destroyed) {
|
|
8511
|
+
inactivity = setTimeout(idle, options.inactivityTimeoutMs);
|
|
8512
|
+
return;
|
|
8513
|
+
}
|
|
8514
|
+
stop(`git ${command} wrote nothing for ${options.inactivityTimeoutMs}ms`);
|
|
8515
|
+
};
|
|
8516
|
+
const touch = () => {
|
|
8517
|
+
options.onActivity?.();
|
|
8518
|
+
clearTimeout(inactivity);
|
|
8519
|
+
if (exitedCleanly)
|
|
8520
|
+
return;
|
|
8521
|
+
inactivity = setTimeout(idle, options.inactivityTimeoutMs);
|
|
8522
|
+
};
|
|
8523
|
+
const stop = (reason) => {
|
|
8524
|
+
if (closed || killedFor !== null)
|
|
8525
|
+
return;
|
|
8526
|
+
if (child.exitCode === 0 || child.exitCode === null && child.signalCode === null) {
|
|
8527
|
+
killedFor = reason;
|
|
8528
|
+
}
|
|
8529
|
+
terminate(child);
|
|
8530
|
+
stdout.destroy();
|
|
8531
|
+
};
|
|
8532
|
+
child.on("exit", (code) => {
|
|
8533
|
+
if (code !== 0)
|
|
8534
|
+
return;
|
|
8535
|
+
exitedCleanly = true;
|
|
8536
|
+
clearTimeout(inactivity);
|
|
8537
|
+
clearTimeout(deadline);
|
|
8538
|
+
});
|
|
8539
|
+
child.stderr?.on("data", (chunk) => {
|
|
8540
|
+
touch();
|
|
8541
|
+
if (stderrBytes < STDERR_CAP_BYTES) {
|
|
8542
|
+
stderr += chunk.subarray(0, STDERR_CAP_BYTES - stderrBytes).toString("utf8");
|
|
8543
|
+
}
|
|
8544
|
+
stderrBytes += chunk.length;
|
|
8545
|
+
});
|
|
8546
|
+
stdout.on("data", touch);
|
|
8547
|
+
const deadline = options.deadlineMs === void 0 ? void 0 : setTimeout(() => stop(`git ${command} ran longer than ${options.deadlineMs}ms`), options.deadlineMs);
|
|
8548
|
+
const onAbort = () => stop(`git ${command} was aborted`);
|
|
8549
|
+
if (options.signal?.aborted)
|
|
8550
|
+
onAbort();
|
|
8551
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
8552
|
+
const input = feedStdin(child, options.stdin, touch, (err) => stop(`git ${command}'s input failed: ${getErrorMessage(err)}`));
|
|
8553
|
+
input.catch(() => void 0);
|
|
8554
|
+
touch();
|
|
8555
|
+
const exited = new Promise((resolve2, reject) => {
|
|
8556
|
+
child.on("error", (err) => {
|
|
8557
|
+
if (child.pid === void 0) {
|
|
8558
|
+
clearTimeout(inactivity);
|
|
8559
|
+
clearTimeout(deadline);
|
|
8560
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
8561
|
+
reject(new GitProcessError(`git ${command} could not run: ${err.message}`, null, ""));
|
|
8562
|
+
}
|
|
8563
|
+
});
|
|
8564
|
+
child.on("close", (code, signal) => {
|
|
8565
|
+
closed = true;
|
|
8566
|
+
clearTimeout(inactivity);
|
|
8567
|
+
clearTimeout(deadline);
|
|
8568
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
8569
|
+
const written = stderrBytes > STDERR_CAP_BYTES ? `${stderr}
|
|
8570
|
+
[stderr truncated]` : stderr;
|
|
8571
|
+
if (killedFor === null && code === 0) {
|
|
8572
|
+
input.then(resolve2, (err) => reject(new GitProcessError(`git ${command}'s input failed: ${getErrorMessage(err)}`, 0, written)));
|
|
8573
|
+
return;
|
|
8574
|
+
}
|
|
8575
|
+
const why = killedFor ?? (written.trim() || (code === null ? `git ${command} was killed by ${signal ?? "a signal"}` : `git ${command} exited with code ${code}`));
|
|
8576
|
+
reject(new GitProcessError(why, code, written));
|
|
8577
|
+
});
|
|
8578
|
+
});
|
|
8579
|
+
exited.catch(() => void 0);
|
|
8580
|
+
return { stdout, exited, kill: stop };
|
|
8581
|
+
}
|
|
8582
|
+
async function runGit(args, options) {
|
|
8583
|
+
const max = options.maxStdoutBytes ?? 64 * 1024 * 1024;
|
|
8584
|
+
const proc = spawnGit(args, options);
|
|
8585
|
+
const chunks = [];
|
|
8586
|
+
let total = 0;
|
|
8587
|
+
const collect = new Writable({
|
|
8588
|
+
write(chunk, _encoding, callback) {
|
|
8589
|
+
total += chunk.length;
|
|
8590
|
+
if (total > max) {
|
|
8591
|
+
proc.kill(`git printed more than ${max} bytes`);
|
|
8592
|
+
callback();
|
|
8593
|
+
return;
|
|
8594
|
+
}
|
|
8595
|
+
chunks.push(chunk);
|
|
8596
|
+
callback();
|
|
8597
|
+
}
|
|
8598
|
+
});
|
|
8599
|
+
try {
|
|
8600
|
+
await Promise.all([pipeline(proc.stdout, collect).catch(() => void 0), proc.exited]);
|
|
8601
|
+
} catch (err) {
|
|
8602
|
+
if (options.stdoutInFailure && err instanceof GitProcessError && chunks.length > 0) {
|
|
8603
|
+
throw new GitProcessError(`${Buffer.concat(chunks).toString("utf8")}${err.message}`, err.exitCode, err.stderr);
|
|
8604
|
+
}
|
|
8605
|
+
throw err;
|
|
8606
|
+
}
|
|
8607
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
8608
|
+
}
|
|
8609
|
+
function terminate(child) {
|
|
8610
|
+
signalGroup(child, "SIGTERM");
|
|
8611
|
+
const timer = setTimeout(() => signalGroup(child, "SIGKILL"), KILL_GRACE_MS);
|
|
8612
|
+
timer.unref();
|
|
8613
|
+
child.once("close", () => clearTimeout(timer));
|
|
8614
|
+
}
|
|
8615
|
+
function signalGroup(child, signal) {
|
|
8616
|
+
if (child.pid !== void 0 && process.platform !== "win32") {
|
|
8617
|
+
try {
|
|
8618
|
+
process.kill(-child.pid, signal);
|
|
8619
|
+
return;
|
|
8620
|
+
} catch {
|
|
8621
|
+
}
|
|
8622
|
+
}
|
|
8623
|
+
child.kill(signal);
|
|
8624
|
+
}
|
|
8625
|
+
function feedStdin(child, source, touch, onSourceError) {
|
|
8626
|
+
const sink2 = child.stdin;
|
|
8627
|
+
if (sink2 === null)
|
|
8628
|
+
return Promise.resolve();
|
|
8629
|
+
sink2.on("error", () => void 0);
|
|
8630
|
+
if (source === void 0) {
|
|
8631
|
+
sink2.end();
|
|
8632
|
+
return Promise.resolve();
|
|
8633
|
+
}
|
|
8634
|
+
const readable = typeof source === "string" ? Readable.from([Buffer.from(source)]) : source;
|
|
8635
|
+
const counted = new Transform({
|
|
8636
|
+
transform(chunk, _encoding, callback) {
|
|
8637
|
+
touch();
|
|
8638
|
+
callback(null, chunk);
|
|
8639
|
+
}
|
|
8640
|
+
});
|
|
8641
|
+
return new Promise((resolve2, reject) => {
|
|
8642
|
+
readable.once("error", (err) => {
|
|
8643
|
+
onSourceError(err);
|
|
8644
|
+
reject(err);
|
|
8645
|
+
});
|
|
8646
|
+
sink2.once("close", () => {
|
|
8647
|
+
if (!readable.readableEnded)
|
|
8648
|
+
readable.destroy();
|
|
8649
|
+
resolve2();
|
|
8650
|
+
});
|
|
8651
|
+
readable.pipe(counted).pipe(sink2);
|
|
8652
|
+
});
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
// dist/git-manager.js
|
|
8272
8656
|
var log9 = createDebugLogger({ prefix: "GitManager" });
|
|
8273
8657
|
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
8658
|
var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
@@ -8292,6 +8676,13 @@ function gitNetworkChildEnv() {
|
|
|
8292
8676
|
return { ...env, ...FORCE_C_LOCALE };
|
|
8293
8677
|
}
|
|
8294
8678
|
var NO_AUTO_GC_CONFIG = ["gc.auto=0", "maintenance.auto=false"];
|
|
8679
|
+
var HASH_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
8680
|
+
var MAX_HASH_RUNS = 50;
|
|
8681
|
+
var LS_REMOTE_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
8682
|
+
var symlinkBlobSha = (target, likeSha) => {
|
|
8683
|
+
const body = Buffer.from(target, "utf8");
|
|
8684
|
+
return createHash2(likeSha.length === 64 ? "sha256" : "sha1").update(Buffer.concat([Buffer.from(`blob ${body.length}\0`), body])).digest("hex");
|
|
8685
|
+
};
|
|
8295
8686
|
var REMOTE_GIT_CONFIG = [
|
|
8296
8687
|
["gc.auto", "0"],
|
|
8297
8688
|
["receive.autogc", "false"],
|
|
@@ -8392,6 +8783,12 @@ async function ensureGitExcludePattern(repoPath, pattern) {
|
|
|
8392
8783
|
await fs16.writeFile(excludePath, newContent, "utf-8");
|
|
8393
8784
|
log9.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
8394
8785
|
}
|
|
8786
|
+
var BaseBranchMissingError = class extends Error {
|
|
8787
|
+
constructor(remote, baseBranch) {
|
|
8788
|
+
super(`CanopyCMS: remote '${remote}' has no branch '${baseBranch}'`);
|
|
8789
|
+
this.name = "BaseBranchMissingError";
|
|
8790
|
+
}
|
|
8791
|
+
};
|
|
8395
8792
|
var GitConflictError = class extends Error {
|
|
8396
8793
|
constructor(conflictedFiles) {
|
|
8397
8794
|
super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
|
|
@@ -8667,6 +9064,36 @@ var GitManager = class _GitManager {
|
|
|
8667
9064
|
}
|
|
8668
9065
|
return refs.includes(`refs/heads/${branch}`) || refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
|
|
8669
9066
|
}
|
|
9067
|
+
/**
|
|
9068
|
+
* The branches inside `branch` (`<branch>/…`) that a bare mirror holds as local heads or in
|
|
9069
|
+
* GitHub's tracking namespace, which git cannot hold beside `branch` itself. `for-each-ref`
|
|
9070
|
+
* matches a pattern up to a slash, so the pattern `<branch>` lists them. Throws, as
|
|
9071
|
+
* {@link bareRemoteHasBranch} does, when the mirror is unreadable.
|
|
9072
|
+
*/
|
|
9073
|
+
static async bareRemoteBranchesInside(remotePath, branch) {
|
|
9074
|
+
let output;
|
|
9075
|
+
try {
|
|
9076
|
+
output = await simpleGit2().raw([
|
|
9077
|
+
"--git-dir",
|
|
9078
|
+
remotePath,
|
|
9079
|
+
"for-each-ref",
|
|
9080
|
+
"--format=%(refname)",
|
|
9081
|
+
"--end-of-options",
|
|
9082
|
+
`refs/heads/${branch}`,
|
|
9083
|
+
`${GITHUB_TRACKING_REF_PREFIX}${branch}`
|
|
9084
|
+
]);
|
|
9085
|
+
} catch (err) {
|
|
9086
|
+
throw new Error(`Cannot inspect remote mirror at ${remotePath}: ${getErrorMessage(err)}`);
|
|
9087
|
+
}
|
|
9088
|
+
const inside = /* @__PURE__ */ new Set();
|
|
9089
|
+
for (const ref of output.split("\n").map((line) => line.trim())) {
|
|
9090
|
+
for (const prefix of ["refs/heads/", GITHUB_TRACKING_REF_PREFIX]) {
|
|
9091
|
+
if (ref.startsWith(`${prefix}${branch}/`))
|
|
9092
|
+
inside.add(ref.slice(prefix.length));
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
return [...inside].sort();
|
|
9096
|
+
}
|
|
8670
9097
|
/**
|
|
8671
9098
|
* Delete `refs/heads/<branch>` from a bare local mirror, if present. A
|
|
8672
9099
|
* no-op (not an error) when the ref doesn't exist.
|
|
@@ -9365,6 +9792,199 @@ var GitManager = class _GitManager {
|
|
|
9365
9792
|
]);
|
|
9366
9793
|
return output.split("\0").filter((p) => p.length > 0);
|
|
9367
9794
|
}
|
|
9795
|
+
/**
|
|
9796
|
+
* Every file in `rev`'s tree below repo-relative directory `under`, with its size in bytes,
|
|
9797
|
+
* so a caller can refuse a file too large to read before reading it.
|
|
9798
|
+
*/
|
|
9799
|
+
async listFilesWithSizeAt(rev, under) {
|
|
9800
|
+
const dir = `${under.replace(/\/+$/, "")}/`;
|
|
9801
|
+
const output = await this.git.raw([
|
|
9802
|
+
"ls-tree",
|
|
9803
|
+
"-r",
|
|
9804
|
+
"-z",
|
|
9805
|
+
"-l",
|
|
9806
|
+
"--full-tree",
|
|
9807
|
+
"--end-of-options",
|
|
9808
|
+
rev,
|
|
9809
|
+
dir
|
|
9810
|
+
]);
|
|
9811
|
+
const files = [];
|
|
9812
|
+
for (const record of output.split("\0")) {
|
|
9813
|
+
const tab = record.indexOf(" ");
|
|
9814
|
+
if (tab === -1)
|
|
9815
|
+
continue;
|
|
9816
|
+
const [, type, , size] = record.slice(0, tab).split(/ +/);
|
|
9817
|
+
const filePath = record.slice(tab + 1);
|
|
9818
|
+
if (type === "blob" && filePath.startsWith(dir)) {
|
|
9819
|
+
files.push({ path: filePath, size: Number(size) });
|
|
9820
|
+
}
|
|
9821
|
+
}
|
|
9822
|
+
return files;
|
|
9823
|
+
}
|
|
9824
|
+
/**
|
|
9825
|
+
* The commit HEAD forked from the base branch at, or `undefined` when they share no history.
|
|
9826
|
+
* Takes the base tip from the remote by `ls-remote` rather than `<remote>/<base>`: the worker
|
|
9827
|
+
* fetches by path, which moves no remote-tracking ref, so that ref is left behind by every
|
|
9828
|
+
* rebase. A tip this clone lacks is fetched as objects only (`--refmap=`, `--no-tags`,
|
|
9829
|
+
* `--no-write-fetch-head`, git 2.29+): no ref and no FETCH_HEAD moves, so a concurrent fetch
|
|
9830
|
+
* pinning FETCH_HEAD is never repointed. Throws {@link BaseBranchMissingError} when the
|
|
9831
|
+
* remote has no base branch, and any other error when it can't be read.
|
|
9832
|
+
*/
|
|
9833
|
+
async mergeBaseWithBase() {
|
|
9834
|
+
const tip = await this.remoteBranchTip(this.baseBranch);
|
|
9835
|
+
if (!tip)
|
|
9836
|
+
throw new BaseBranchMissingError(this.remote, this.baseBranch);
|
|
9837
|
+
if (!await this.hasCommit(tip)) {
|
|
9838
|
+
await this.git.raw([
|
|
9839
|
+
"fetch",
|
|
9840
|
+
"--refmap=",
|
|
9841
|
+
"--no-tags",
|
|
9842
|
+
"--no-write-fetch-head",
|
|
9843
|
+
"--end-of-options",
|
|
9844
|
+
this.remote,
|
|
9845
|
+
`refs/heads/${this.baseBranch}`
|
|
9846
|
+
]);
|
|
9847
|
+
if (!await this.hasCommit(tip)) {
|
|
9848
|
+
throw new Error(`CanopyCMS: could not fetch ${this.baseBranch}'s tip ${tip}`);
|
|
9849
|
+
}
|
|
9850
|
+
}
|
|
9851
|
+
const forkPoint = (await this.git.raw(["merge-base", "--end-of-options", "HEAD", tip])).trim();
|
|
9852
|
+
return forkPoint || void 0;
|
|
9853
|
+
}
|
|
9854
|
+
/** Whether this clone holds commit `sha`. */
|
|
9855
|
+
async hasCommit(sha) {
|
|
9856
|
+
return (await this.git.raw(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`])).trim() !== "";
|
|
9857
|
+
}
|
|
9858
|
+
/**
|
|
9859
|
+
* Repo-relative paths whose working-tree content differs from commit `rev`, with `T` (a type
|
|
9860
|
+
* change) reported as `M`. A sparse clone leaves a path outside its cone unchecked out, marked
|
|
9861
|
+
* skip-worktree, and git reads that path's index entry in place of the missing file: it is never
|
|
9862
|
+
* reported deleted, and differs from `rev` only as the index does.
|
|
9863
|
+
*
|
|
9864
|
+
* `diff-index`, not `git diff`: the porcelain refreshes the index and writes it back under
|
|
9865
|
+
* `index.lock`, which would fail a concurrent writer's own git step. `diff-index` writes
|
|
9866
|
+
* nothing, so it reports a file whose stat changed but whose content did not; those are
|
|
9867
|
+
* hashed here and dropped when they match `rev`.
|
|
9868
|
+
*/
|
|
9869
|
+
async diffNameStatusAgainst(rev) {
|
|
9870
|
+
const output = await this.git.raw([
|
|
9871
|
+
"diff-index",
|
|
9872
|
+
"--raw",
|
|
9873
|
+
"-z",
|
|
9874
|
+
"--no-renames",
|
|
9875
|
+
"--end-of-options",
|
|
9876
|
+
rev,
|
|
9877
|
+
"--"
|
|
9878
|
+
]);
|
|
9879
|
+
const tokens = output.split("\0");
|
|
9880
|
+
const changes = [];
|
|
9881
|
+
for (let i = 0; i + 1 < tokens.length; i += 2) {
|
|
9882
|
+
const [srcMode, dstMode, srcSha, dstSha, code] = tokens[i].slice(1).split(" ");
|
|
9883
|
+
const path27 = tokens[i + 1];
|
|
9884
|
+
const letter = code?.charAt(0);
|
|
9885
|
+
if (letter === "A" || letter === "D")
|
|
9886
|
+
changes.push({ status: letter, path: path27 });
|
|
9887
|
+
else if (letter === "M" || letter === "T") {
|
|
9888
|
+
const unhashed = /^0+$/.test(dstSha) && srcMode === dstMode ? srcSha : void 0;
|
|
9889
|
+
changes.push({ status: "M", path: path27, unhashed });
|
|
9890
|
+
}
|
|
9891
|
+
}
|
|
9892
|
+
const unchanged = await this.matchingBlobs(changes.flatMap((c) => c.unhashed ? [{ path: c.path, sha: c.unhashed }] : []));
|
|
9893
|
+
return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path: path27 }) => ({ status, path: path27 }));
|
|
9894
|
+
}
|
|
9895
|
+
/**
|
|
9896
|
+
* The paths whose working-tree file hashes to `sha`, as git hashes it: a symlink's blob is its
|
|
9897
|
+
* target, a file's goes through `hash-object --stdin-paths`, so its attributes' filters apply.
|
|
9898
|
+
* Callers pass only paths whose mode is unchanged, since diff-index reports the working tree's
|
|
9899
|
+
* own mode. A path that is gone or can't be hashed matches nothing, so it stays reported as
|
|
9900
|
+
* changed. When git refuses a path (exit 128), having printed
|
|
9901
|
+
* the SHAs before it, the next run resumes past that path, for at most {@link MAX_HASH_RUNS}
|
|
9902
|
+
* runs. A run that ends any other way (killed, timed out, a signal) stops the hashing, and the
|
|
9903
|
+
* paths it didn't reach stay changed.
|
|
9904
|
+
*/
|
|
9905
|
+
async matchingBlobs(files) {
|
|
9906
|
+
const matching = /* @__PURE__ */ new Set();
|
|
9907
|
+
const regular = [];
|
|
9908
|
+
for (const file of files) {
|
|
9909
|
+
if (file.path.includes("\n"))
|
|
9910
|
+
continue;
|
|
9911
|
+
const stat = await fs16.lstat(path20.join(this.repoPath, file.path)).catch(() => void 0);
|
|
9912
|
+
if (!stat)
|
|
9913
|
+
continue;
|
|
9914
|
+
if (stat.isSymbolicLink()) {
|
|
9915
|
+
const target = await fs16.readlink(path20.join(this.repoPath, file.path)).catch(() => void 0);
|
|
9916
|
+
if (target !== void 0 && symlinkBlobSha(target, file.sha) === file.sha) {
|
|
9917
|
+
matching.add(file.path);
|
|
9918
|
+
}
|
|
9919
|
+
} else if (stat.isFile())
|
|
9920
|
+
regular.push(file);
|
|
9921
|
+
}
|
|
9922
|
+
for (let start = 0, runs = 0; start < regular.length && runs < MAX_HASH_RUNS; runs++) {
|
|
9923
|
+
const batch = regular.slice(start);
|
|
9924
|
+
const { shas, outcome } = await this.hashPaths(batch.map((f) => f.path));
|
|
9925
|
+
shas.forEach((sha, i) => {
|
|
9926
|
+
if (batch[i] && sha === batch[i].sha)
|
|
9927
|
+
matching.add(batch[i].path);
|
|
9928
|
+
});
|
|
9929
|
+
if (outcome !== "refused")
|
|
9930
|
+
break;
|
|
9931
|
+
start += shas.length + 1;
|
|
9932
|
+
}
|
|
9933
|
+
return matching;
|
|
9934
|
+
}
|
|
9935
|
+
/**
|
|
9936
|
+
* `hash-object --stdin-paths` over `paths`: one SHA per path, in order, up to where it ended.
|
|
9937
|
+
* `refused` is git's own exit 128 on a path it can't hash; `stopped` is any other ending.
|
|
9938
|
+
*/
|
|
9939
|
+
async hashPaths(paths) {
|
|
9940
|
+
const chunks = [];
|
|
9941
|
+
let proc;
|
|
9942
|
+
try {
|
|
9943
|
+
proc = spawnGit(["hash-object", "--stdin-paths"], {
|
|
9944
|
+
...this.processOptions(),
|
|
9945
|
+
stdin: `${paths.join("\n")}
|
|
9946
|
+
`,
|
|
9947
|
+
inactivityTimeoutMs: HASH_INACTIVITY_TIMEOUT_MS
|
|
9948
|
+
});
|
|
9949
|
+
} catch {
|
|
9950
|
+
return { shas: [], outcome: "stopped" };
|
|
9951
|
+
}
|
|
9952
|
+
const read = (async () => {
|
|
9953
|
+
for await (const chunk of proc.stdout)
|
|
9954
|
+
chunks.push(chunk);
|
|
9955
|
+
})().catch(() => {
|
|
9956
|
+
});
|
|
9957
|
+
const outcome = await proc.exited.then(() => "complete", (err) => err instanceof GitProcessError && err.exitCode === 128 ? "refused" : "stopped");
|
|
9958
|
+
await read;
|
|
9959
|
+
return { shas: Buffer.concat(chunks).toString("utf8").split("\n").filter(Boolean), outcome };
|
|
9960
|
+
}
|
|
9961
|
+
/** What a git run outside simple-git needs to run as `this.git` does. */
|
|
9962
|
+
processOptions() {
|
|
9963
|
+
return {
|
|
9964
|
+
cwd: this.repoPath,
|
|
9965
|
+
env: gitChildEnv({ GIT_CEILING_DIRECTORIES: path20.dirname(this.repoPath) }),
|
|
9966
|
+
config: NO_AUTO_GC_CONFIG
|
|
9967
|
+
};
|
|
9968
|
+
}
|
|
9969
|
+
/** Repo-relative paths of the working tree's untracked, non-ignored files. */
|
|
9970
|
+
async listUntrackedFiles() {
|
|
9971
|
+
const output = await this.git.raw(["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
9972
|
+
return output.split("\0").filter((p) => p.length > 0);
|
|
9973
|
+
}
|
|
9974
|
+
/**
|
|
9975
|
+
* The text of repo-relative `filePath` at commit `rev`, or `undefined` when `rev` has no such
|
|
9976
|
+
* path. Any other failure, such as an unknown `rev`, throws.
|
|
9977
|
+
*/
|
|
9978
|
+
async showFile(rev, filePath) {
|
|
9979
|
+
try {
|
|
9980
|
+
return await this.git.raw(["show", "--end-of-options", `${rev}:${filePath}`]);
|
|
9981
|
+
} catch (err) {
|
|
9982
|
+
if (/fatal: path '.*' (does not exist in|exists on disk, but not in) '/.test(getErrorMessage(err))) {
|
|
9983
|
+
return void 0;
|
|
9984
|
+
}
|
|
9985
|
+
throw err;
|
|
9986
|
+
}
|
|
9987
|
+
}
|
|
9368
9988
|
async ensureAuthor(author) {
|
|
9369
9989
|
const config = await this.git.listConfig();
|
|
9370
9990
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -9535,7 +10155,10 @@ var GitManager = class _GitManager {
|
|
|
9535
10155
|
*/
|
|
9536
10156
|
async remoteBranchTip(branch) {
|
|
9537
10157
|
const ref = `refs/heads/${branch}`;
|
|
9538
|
-
const output = await
|
|
10158
|
+
const output = await runGit(["ls-remote", "--heads", "--end-of-options", this.remote, ref], {
|
|
10159
|
+
...this.processOptions(),
|
|
10160
|
+
inactivityTimeoutMs: LS_REMOTE_INACTIVITY_TIMEOUT_MS
|
|
10161
|
+
});
|
|
9539
10162
|
for (const line of output.split("\n")) {
|
|
9540
10163
|
const [sha, name] = line.trim().split(" ");
|
|
9541
10164
|
if (sha && name === ref)
|
|
@@ -9616,7 +10239,7 @@ var REPAIR_MAX_AGE_MS = 20 * 6e4;
|
|
|
9616
10239
|
var DELETING_MAX_AGE_MS = 10 * 6e4;
|
|
9617
10240
|
var SETTINGS_TRASH_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
|
|
9618
10241
|
var BranchProvisioningBusyError = class extends Error {
|
|
9619
|
-
constructor(dirName, message = `Branch '${
|
|
10242
|
+
constructor(dirName, shortName = dirName, message = `Branch '${shortName}' is still being set up. Try again in a minute.`) {
|
|
9620
10243
|
super(message);
|
|
9621
10244
|
this.dirName = dirName;
|
|
9622
10245
|
this.name = "BranchProvisioningBusyError";
|
|
@@ -9633,7 +10256,7 @@ function formatDirStamp(date) {
|
|
|
9633
10256
|
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
9634
10257
|
}
|
|
9635
10258
|
function shortDirName(dirName) {
|
|
9636
|
-
const digest =
|
|
10259
|
+
const digest = createHash3("sha1").update(dirName).digest("hex").slice(0, 10);
|
|
9637
10260
|
return `${dirName.slice(0, 40)}-${digest}`;
|
|
9638
10261
|
}
|
|
9639
10262
|
function stampedName(prefix, dirName, now) {
|
|
@@ -9825,12 +10448,13 @@ async function stageBranchWorkspace(options) {
|
|
|
9825
10448
|
throw err;
|
|
9826
10449
|
}
|
|
9827
10450
|
}
|
|
9828
|
-
async function lockBranchName(baseRoot, dirName) {
|
|
10451
|
+
async function lockBranchName(baseRoot, dirName, shortName = dirName) {
|
|
9829
10452
|
try {
|
|
9830
10453
|
return await acquireProvisioningLockWithin(baseRoot, branchProvisioningLockName(dirName), PUBLISH_LOCK_WAIT_MS);
|
|
9831
10454
|
} catch (err) {
|
|
9832
|
-
if (isNodeError(err) && err.code === "ELOCKED")
|
|
9833
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10455
|
+
if (isNodeError(err) && err.code === "ELOCKED") {
|
|
10456
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10457
|
+
}
|
|
9834
10458
|
throw err;
|
|
9835
10459
|
}
|
|
9836
10460
|
}
|
|
@@ -9844,9 +10468,9 @@ async function inspectBlocking(finalPath) {
|
|
|
9844
10468
|
}
|
|
9845
10469
|
var RENAME_BLOCKED = /* @__PURE__ */ new Set(["EEXIST", "ENOTEMPTY", "ENOTDIR"]);
|
|
9846
10470
|
async function publishStaging(options) {
|
|
9847
|
-
const { baseRoot, dirName, stagingPath } = options;
|
|
10471
|
+
const { baseRoot, dirName, stagingPath, shortName = dirName } = options;
|
|
9848
10472
|
const finalPath = path21.join(baseRoot, dirName);
|
|
9849
|
-
const release = await lockBranchName(baseRoot, dirName);
|
|
10473
|
+
const release = await lockBranchName(baseRoot, dirName, shortName);
|
|
9850
10474
|
try {
|
|
9851
10475
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
9852
10476
|
try {
|
|
@@ -9868,7 +10492,7 @@ async function publishStaging(options) {
|
|
|
9868
10492
|
if (blocking === "live")
|
|
9869
10493
|
return "exists";
|
|
9870
10494
|
if (blocking === "protected")
|
|
9871
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10495
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
9872
10496
|
const result = await quarantineResidueAt(baseRoot, dirName, {
|
|
9873
10497
|
minQuietMs: CREATE_RESIDUE_MIN_QUIET_MS,
|
|
9874
10498
|
expectedRemoteUrl: options.expectedRemoteUrl
|
|
@@ -9883,9 +10507,9 @@ async function publishStaging(options) {
|
|
|
9883
10507
|
}
|
|
9884
10508
|
if (result.reason === "foreign")
|
|
9885
10509
|
throw new BranchDirOccupiedError(dirName);
|
|
9886
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10510
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
9887
10511
|
}
|
|
9888
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10512
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
9889
10513
|
} finally {
|
|
9890
10514
|
await release().catch((err) => {
|
|
9891
10515
|
canopyLogWarn(`[canopy] Failed to release the provisioning lock for ${dirName}:`, err);
|
|
@@ -10017,12 +10641,16 @@ var BranchWorkspaceManager = class {
|
|
|
10017
10641
|
if (isSettingsBranchName(options.branchName, operatingStrategy(options.mode).getSettingsBranchName(this.config))) {
|
|
10018
10642
|
throw new BranchPathError("Settings branches are not content branches");
|
|
10019
10643
|
}
|
|
10020
|
-
const
|
|
10644
|
+
const named = parseBranchRef(options.branchName);
|
|
10645
|
+
if (!named.ok)
|
|
10646
|
+
throw new BranchPathError(named.error);
|
|
10647
|
+
const { branchRoot, baseRoot } = resolveBranchPath(options);
|
|
10021
10648
|
const paths = {
|
|
10022
10649
|
branchRoot,
|
|
10023
10650
|
baseRoot,
|
|
10024
10651
|
dirName: path23.basename(branchRoot),
|
|
10025
|
-
|
|
10652
|
+
ref: options.branchName,
|
|
10653
|
+
shortName: shortNameOf({ ...this.config, mode: options.mode }, options.branchName)
|
|
10026
10654
|
};
|
|
10027
10655
|
const existing = await readBranchMetadataFile(branchRoot);
|
|
10028
10656
|
if (existing) {
|
|
@@ -10040,7 +10668,7 @@ var BranchWorkspaceManager = class {
|
|
|
10040
10668
|
}
|
|
10041
10669
|
}
|
|
10042
10670
|
async provisionUnshared(options, paths) {
|
|
10043
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
10671
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
10044
10672
|
await fs19.mkdir(baseRoot, { recursive: true });
|
|
10045
10673
|
const baseBranch = await GitManager.resolveWorkspaceBaseBranch({
|
|
10046
10674
|
...this.config,
|
|
@@ -10058,7 +10686,7 @@ var BranchWorkspaceManager = class {
|
|
|
10058
10686
|
});
|
|
10059
10687
|
const metadata = buildInitialBranchMetadata({
|
|
10060
10688
|
branch: {
|
|
10061
|
-
name: paths.
|
|
10689
|
+
name: paths.ref,
|
|
10062
10690
|
title: options.title,
|
|
10063
10691
|
description: options.description,
|
|
10064
10692
|
access: options.access,
|
|
@@ -10070,14 +10698,14 @@ var BranchWorkspaceManager = class {
|
|
|
10070
10698
|
if (state.kind === "live" || state.kind === "corrupt")
|
|
10071
10699
|
return this.existing(paths);
|
|
10072
10700
|
if (state.kind === "protected")
|
|
10073
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10701
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10074
10702
|
if (state.kind === "foreign") {
|
|
10075
10703
|
if (options.mode !== "dev")
|
|
10076
10704
|
throw new BranchDirOccupiedError(dirName);
|
|
10077
10705
|
return this.adoptInPlace(options, paths, baseBranch, metadata);
|
|
10078
10706
|
}
|
|
10079
10707
|
if (state.kind === "residue" && await this.recentlyActive(paths)) {
|
|
10080
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10708
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10081
10709
|
}
|
|
10082
10710
|
const sparseCone = sparseConeFor(this.config.contentRoot);
|
|
10083
10711
|
await recordSparseCone(baseRoot, sparseCone).catch((err) => {
|
|
@@ -10102,11 +10730,11 @@ var BranchWorkspaceManager = class {
|
|
|
10102
10730
|
return outcome;
|
|
10103
10731
|
}
|
|
10104
10732
|
async stageAndPublish(paths, run) {
|
|
10105
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
10733
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
10106
10734
|
const staged = await stageBranchWorkspace({
|
|
10107
10735
|
baseRoot,
|
|
10108
10736
|
dirName,
|
|
10109
|
-
branchName: paths.
|
|
10737
|
+
branchName: paths.ref,
|
|
10110
10738
|
baseBranch: run.baseBranch,
|
|
10111
10739
|
remoteUrl: run.remoteUrl,
|
|
10112
10740
|
clone: {
|
|
@@ -10126,6 +10754,7 @@ var BranchWorkspaceManager = class {
|
|
|
10126
10754
|
published = await run.provisionLog.step("publish", () => publishStaging({
|
|
10127
10755
|
baseRoot,
|
|
10128
10756
|
dirName,
|
|
10757
|
+
shortName,
|
|
10129
10758
|
stagingPath: staged.stagingPath,
|
|
10130
10759
|
writeId: staged.meta.writeId,
|
|
10131
10760
|
expectedRemoteUrl: run.remoteUrl,
|
|
@@ -10145,16 +10774,16 @@ var BranchWorkspaceManager = class {
|
|
|
10145
10774
|
* is, given branch.json under the branch's lock.
|
|
10146
10775
|
*/
|
|
10147
10776
|
async adoptInPlace(options, paths, baseBranch, metadata) {
|
|
10148
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
10777
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
10149
10778
|
const provisionLog = new ProvisionLog(dirName);
|
|
10150
10779
|
let created = false;
|
|
10151
10780
|
try {
|
|
10152
|
-
const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName));
|
|
10781
|
+
const release = await provisionLog.step("lock", () => lockBranchName(baseRoot, dirName, shortName));
|
|
10153
10782
|
try {
|
|
10154
10783
|
if (!await readBranchMetadataFile(branchRoot)) {
|
|
10155
10784
|
await GitManager.initializeWorkspace({
|
|
10156
10785
|
workspacePath: branchRoot,
|
|
10157
|
-
branchName: paths.
|
|
10786
|
+
branchName: paths.ref,
|
|
10158
10787
|
mode: options.mode,
|
|
10159
10788
|
baseBranch,
|
|
10160
10789
|
sourceRoot: this.config.sourceRoot,
|
|
@@ -10189,7 +10818,7 @@ var BranchWorkspaceManager = class {
|
|
|
10189
10818
|
return this.existing(paths);
|
|
10190
10819
|
const meta = await readBranchMetadataFile(branchRoot);
|
|
10191
10820
|
if (!meta)
|
|
10192
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10821
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10193
10822
|
return { kind: "created", context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
10194
10823
|
}
|
|
10195
10824
|
/** Whether residue at the branch path may still belong to a live process. */
|
|
@@ -10199,10 +10828,10 @@ var BranchWorkspaceManager = class {
|
|
|
10199
10828
|
return true;
|
|
10200
10829
|
return await quietAgeMs(branchRoot) < CREATE_RESIDUE_MIN_QUIET_MS;
|
|
10201
10830
|
}
|
|
10202
|
-
async existing({ branchRoot, baseRoot, dirName }) {
|
|
10831
|
+
async existing({ branchRoot, baseRoot, dirName, shortName }) {
|
|
10203
10832
|
const meta = await readBranchMetadataFile(branchRoot);
|
|
10204
10833
|
if (!meta)
|
|
10205
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
10834
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
10206
10835
|
return { kind: "exists", context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
10207
10836
|
}
|
|
10208
10837
|
};
|
|
@@ -10286,21 +10915,23 @@ var normalizePath = (root, target) => {
|
|
|
10286
10915
|
}
|
|
10287
10916
|
return result.normalizedPath;
|
|
10288
10917
|
};
|
|
10918
|
+
var parseEntryParts = (raw, format) => {
|
|
10919
|
+
if (format === "json")
|
|
10920
|
+
return { data: asRecord(JSON.parse(raw)) };
|
|
10921
|
+
if (format === "yaml")
|
|
10922
|
+
return { data: asRecord(yamlParse2(raw)) };
|
|
10923
|
+
const parsed = matter3(raw);
|
|
10924
|
+
return { data: { ...parsed.data ?? {} }, body: parsed.content };
|
|
10925
|
+
};
|
|
10926
|
+
var parseEntryData = (raw, format, bodyFieldName = "body") => {
|
|
10927
|
+
const { data, body } = parseEntryParts(raw, format);
|
|
10928
|
+
if (body)
|
|
10929
|
+
data[bodyFieldName] = body;
|
|
10930
|
+
return data;
|
|
10931
|
+
};
|
|
10289
10932
|
var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
10290
10933
|
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;
|
|
10934
|
+
return parseEntryData(await fs20.readFile(filePath, "utf8"), format, bodyFieldName);
|
|
10304
10935
|
} catch (err) {
|
|
10305
10936
|
if (isNotFoundError(err))
|
|
10306
10937
|
return {};
|