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
|
@@ -33,8 +33,15 @@ export interface ResolvedBranchSchema {
|
|
|
33
33
|
issues: SchemaIssue[];
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* Per-branch schema cache: one file in {@link schemaCacheDir}
|
|
37
|
-
*
|
|
36
|
+
* Per-branch schema cache: one file in {@link schemaCacheDir}, the only state
|
|
37
|
+
* shared across processes. Each process keeps the entries it parsed or wrote,
|
|
38
|
+
* keyed by the file's identity, so it parses an unchanged file once while the
|
|
39
|
+
* entry stays in its size-bounded memo. Every call still opens the file and
|
|
40
|
+
* runs every freshness check below on the entry, and the file is only ever
|
|
41
|
+
* replaced by rename (see {@link fileIdentity}), so a process is as fresh as if
|
|
42
|
+
* it re-read the file. The identity is fstat'ed on an open handle because NFS
|
|
43
|
+
* revalidates attributes on open (close-to-open), while a bare stat() may
|
|
44
|
+
* answer from its attribute cache.
|
|
38
45
|
*
|
|
39
46
|
* Freshness follows the generation-marker protocol owned by
|
|
40
47
|
* resource-generation.ts, and this is one of that protocol's durable-snapshot
|
|
@@ -58,6 +65,16 @@ export interface ResolvedBranchSchema {
|
|
|
58
65
|
export declare class BranchSchemaCache {
|
|
59
66
|
/** Tracks when we last checked mtimes per contentRoot, to debounce rapid requests */
|
|
60
67
|
private lastMtimeCheck;
|
|
68
|
+
/** Parsed cache files, by absolute path; see the class doc. */
|
|
69
|
+
private readonly snapshots;
|
|
70
|
+
/**
|
|
71
|
+
* In-flight resolves of the checkout during a build, by content root and registry. A build
|
|
72
|
+
* reads a checkout that does not change under it, so those two are its whole input and the
|
|
73
|
+
* result needs no revalidation. Only {@link isBuildMode}: `CANOPY_BUILD_MODE` is a build and
|
|
74
|
+
* script switch, and a long-lived process setting it would see a schema edit only on
|
|
75
|
+
* restart. A rejected resolve is dropped, so a strict-mode failure fails every read.
|
|
76
|
+
*/
|
|
77
|
+
private readonly buildResolves;
|
|
61
78
|
private readonly devMode;
|
|
62
79
|
constructor(mode?: OperatingMode);
|
|
63
80
|
/**
|
|
@@ -79,6 +96,9 @@ export declare class BranchSchemaCache {
|
|
|
79
96
|
*/
|
|
80
97
|
protected resolveFresh(contentRoot: string, entrySchemaRegistry: EntrySchemaRegistry, unknownSchema: UnknownSchemaPolicy): Promise<SchemaResolutionResult>;
|
|
81
98
|
private loadFromCacheOrResolve;
|
|
99
|
+
private resolveForBuild;
|
|
100
|
+
/** The parsed cache file, or null when it is absent or unreadable. */
|
|
101
|
+
private readSnapshot;
|
|
82
102
|
/**
|
|
83
103
|
* Resolve the schema fresh from disk and persist it (subject to the
|
|
84
104
|
* skip-persist rule below), UNCONDITIONALLY -- never through the cache-read
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { LRUCache } from 'lru-cache';
|
|
3
4
|
import { resolveSchema, isValidSchema } from './schema/resolver.js';
|
|
4
5
|
import { registryFingerprint } from './schema/registry-fingerprint.js';
|
|
5
6
|
import { flattenSchema } from './config/flatten.js';
|
|
@@ -13,6 +14,8 @@ import { canopyLogWarn } from './utils/logger.js';
|
|
|
13
14
|
const SCHEMA_CACHE_VERSION = 4;
|
|
14
15
|
/** Minimum interval between mtime staleness checks (ms) */
|
|
15
16
|
const MTIME_CHECK_DEBOUNCE_MS = 1000;
|
|
17
|
+
/** Cap on the parsed snapshots one process keeps, counted in bytes of their files. */
|
|
18
|
+
const SNAPSHOT_MEMO_MAX_BYTES = 64 * 1024 * 1024;
|
|
16
19
|
/** File name of the schema cache inside {@link schemaCacheDir}. */
|
|
17
20
|
export const SCHEMA_CACHE_FILE = 'schema-cache.json';
|
|
18
21
|
/** resource-generation.ts resource key for the schema cache's marker. */
|
|
@@ -65,6 +68,13 @@ async function isStaleByMtime(dir, cachedAt) {
|
|
|
65
68
|
}
|
|
66
69
|
return false;
|
|
67
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Names one version of a file. Every write of the schema cache renames a new temp file into
|
|
73
|
+
* place, so a rewrite always changes the inode, whatever token or content it carries.
|
|
74
|
+
*/
|
|
75
|
+
function fileIdentity(stat) {
|
|
76
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}`;
|
|
77
|
+
}
|
|
68
78
|
/**
|
|
69
79
|
* Directory holding a branch's schema cache: `{branchRoot}/.git/canopycms` when
|
|
70
80
|
* the branch root is a full clone (every canopycms branch workspace is), else
|
|
@@ -80,8 +90,15 @@ async function schemaCacheDir(branchRoot) {
|
|
|
80
90
|
return isClone ? path.join(gitDir, 'canopycms') : path.join(branchRoot, CANOPY_META_DIR);
|
|
81
91
|
}
|
|
82
92
|
/**
|
|
83
|
-
* Per-branch schema cache: one file in {@link schemaCacheDir}
|
|
84
|
-
*
|
|
93
|
+
* Per-branch schema cache: one file in {@link schemaCacheDir}, the only state
|
|
94
|
+
* shared across processes. Each process keeps the entries it parsed or wrote,
|
|
95
|
+
* keyed by the file's identity, so it parses an unchanged file once while the
|
|
96
|
+
* entry stays in its size-bounded memo. Every call still opens the file and
|
|
97
|
+
* runs every freshness check below on the entry, and the file is only ever
|
|
98
|
+
* replaced by rename (see {@link fileIdentity}), so a process is as fresh as if
|
|
99
|
+
* it re-read the file. The identity is fstat'ed on an open handle because NFS
|
|
100
|
+
* revalidates attributes on open (close-to-open), while a bare stat() may
|
|
101
|
+
* answer from its attribute cache.
|
|
85
102
|
*
|
|
86
103
|
* Freshness follows the generation-marker protocol owned by
|
|
87
104
|
* resource-generation.ts, and this is one of that protocol's durable-snapshot
|
|
@@ -106,6 +123,19 @@ export class BranchSchemaCache {
|
|
|
106
123
|
constructor(mode = 'prod') {
|
|
107
124
|
/** Tracks when we last checked mtimes per contentRoot, to debounce rapid requests */
|
|
108
125
|
this.lastMtimeCheck = new Map();
|
|
126
|
+
/** Parsed cache files, by absolute path; see the class doc. */
|
|
127
|
+
this.snapshots = new LRUCache({
|
|
128
|
+
maxSize: SNAPSHOT_MEMO_MAX_BYTES,
|
|
129
|
+
sizeCalculation: (snapshot) => Math.max(1, snapshot.bytes),
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* In-flight resolves of the checkout during a build, by content root and registry. A build
|
|
133
|
+
* reads a checkout that does not change under it, so those two are its whole input and the
|
|
134
|
+
* result needs no revalidation. Only {@link isBuildMode}: `CANOPY_BUILD_MODE` is a build and
|
|
135
|
+
* script switch, and a long-lived process setting it would see a schema edit only on
|
|
136
|
+
* restart. A rejected resolve is dropped, so a strict-mode failure fails every read.
|
|
137
|
+
*/
|
|
138
|
+
this.buildResolves = new Map();
|
|
109
139
|
this.devMode = mode === 'dev';
|
|
110
140
|
}
|
|
111
141
|
/**
|
|
@@ -135,17 +165,12 @@ export class BranchSchemaCache {
|
|
|
135
165
|
async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
136
166
|
const contentRoot = path.join(branchRoot, contentRootName);
|
|
137
167
|
const fromCheckout = this.readsCheckout(branchRoot);
|
|
168
|
+
if (fromCheckout && isBuildMode()) {
|
|
169
|
+
return this.resolveForBuild(branchRoot, entrySchemaRegistry, contentRootName);
|
|
170
|
+
}
|
|
138
171
|
if (!fromCheckout) {
|
|
139
172
|
const cachePath = path.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
|
|
140
|
-
let cacheData =
|
|
141
|
-
try {
|
|
142
|
-
const cacheContent = await fs.readFile(cachePath, 'utf-8');
|
|
143
|
-
cacheData = JSON.parse(cacheContent);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Cache doesn't exist or can't be read
|
|
147
|
-
cacheData = null;
|
|
148
|
-
}
|
|
173
|
+
let cacheData = await this.readSnapshot(cachePath);
|
|
149
174
|
// Strict version check, not truthiness: a snapshot from an older version
|
|
150
175
|
// left on EFS by a rolling deploy has no `generation` field, and an
|
|
151
176
|
// `undefined` token breaks the freshness comparison below.
|
|
@@ -187,6 +212,44 @@ export class BranchSchemaCache {
|
|
|
187
212
|
fromCheckout,
|
|
188
213
|
});
|
|
189
214
|
}
|
|
215
|
+
resolveForBuild(branchRoot, entrySchemaRegistry, contentRootName) {
|
|
216
|
+
const key = `${path.resolve(branchRoot, contentRootName)}\0${registryFingerprint(entrySchemaRegistry)}`;
|
|
217
|
+
let pending = this.buildResolves.get(key);
|
|
218
|
+
if (!pending) {
|
|
219
|
+
pending = this.resolveFreshAndPersist(branchRoot, entrySchemaRegistry, contentRootName, {
|
|
220
|
+
fromCheckout: true,
|
|
221
|
+
});
|
|
222
|
+
this.buildResolves.set(key, pending);
|
|
223
|
+
pending.catch(() => this.buildResolves.delete(key));
|
|
224
|
+
}
|
|
225
|
+
return pending;
|
|
226
|
+
}
|
|
227
|
+
/** The parsed cache file, or null when it is absent or unreadable. */
|
|
228
|
+
async readSnapshot(cachePath) {
|
|
229
|
+
let handle;
|
|
230
|
+
try {
|
|
231
|
+
handle = await fs.open(cachePath, 'r');
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const stat = await handle.stat({ bigint: true });
|
|
238
|
+
const identity = fileIdentity(stat);
|
|
239
|
+
const memo = this.snapshots.get(cachePath);
|
|
240
|
+
if (memo?.identity === identity)
|
|
241
|
+
return memo.entry;
|
|
242
|
+
const entry = JSON.parse(await fs.readFile(handle, 'utf-8'));
|
|
243
|
+
this.snapshots.set(cachePath, { identity, entry, bytes: Number(stat.size) });
|
|
244
|
+
return entry;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await handle.close().catch(() => { });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
190
253
|
/**
|
|
191
254
|
* Resolve the schema fresh from disk and persist it (subject to the
|
|
192
255
|
* skip-persist rule below), UNCONDITIONALLY -- never through the cache-read
|
|
@@ -241,16 +304,29 @@ export class BranchSchemaCache {
|
|
|
241
304
|
};
|
|
242
305
|
// Temp file then rename, with the temp file unlinked on a failed
|
|
243
306
|
// rename so a transient error leaves no stray `.tmp` beside the cache.
|
|
307
|
+
// The memo is seeded from the temp file's own identity, which the
|
|
308
|
+
// rename keeps: stat'ing cachePath afterwards could see a concurrent
|
|
309
|
+
// writer's file and pair it with this entry. It holds the parsed body,
|
|
310
|
+
// as every reader does, not the resolver's objects, which alias the
|
|
311
|
+
// registry and keep values JSON changes or drops.
|
|
244
312
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
245
313
|
const tmpPath = path.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
|
|
246
|
-
|
|
314
|
+
const body = JSON.stringify(newCache);
|
|
315
|
+
let identity;
|
|
247
316
|
try {
|
|
317
|
+
await fs.writeFile(tmpPath, body, 'utf-8');
|
|
318
|
+
identity = fileIdentity(await fs.stat(tmpPath, { bigint: true }));
|
|
248
319
|
await fs.rename(tmpPath, cachePath);
|
|
249
320
|
}
|
|
250
321
|
catch (err) {
|
|
251
322
|
await fs.unlink(tmpPath).catch(() => { });
|
|
252
323
|
throw err;
|
|
253
324
|
}
|
|
325
|
+
this.snapshots.set(cachePath, {
|
|
326
|
+
identity,
|
|
327
|
+
entry: JSON.parse(body),
|
|
328
|
+
bytes: Buffer.byteLength(body),
|
|
329
|
+
});
|
|
254
330
|
}
|
|
255
331
|
// else: the marker read failed for a reason other than "never bumped",
|
|
256
332
|
// so no token can be attributed to this resolve, and a snapshot stamped
|
package/dist/branch-workspace.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { BranchPathError, isSettingsBranchName, resolveBranchPath } from './paths/index.js';
|
|
3
|
+
import { BranchPathError, isSettingsBranchName, parseBranchRef, resolveBranchPath } from './paths/index.js';
|
|
4
4
|
import { buildInitialBranchMetadata, loadBranchContext } from './branch-metadata.js';
|
|
5
5
|
import { BRANCH_META_DIR, BRANCH_META_FILE, readBranchMetadataFile } from './branch-metadata-file.js';
|
|
6
|
+
import { shortNameOf } from './branch-param.js';
|
|
6
7
|
import { BranchRegistry } from './branch-registry.js';
|
|
7
8
|
import { BranchDirOccupiedError, BranchProvisioningBusyError, CREATE_RESIDUE_MIN_QUIET_MS, classifyFinalDir, removeLeftoverDir, lockBranchName, publishStaging, quietAgeMs, stageBranchWorkspace, } from './branch-provisioning.js';
|
|
8
9
|
import { recordSparseCone, sparseConeFor } from './branch-sparse.js';
|
|
@@ -49,12 +50,17 @@ export class BranchWorkspaceManager {
|
|
|
49
50
|
if (isSettingsBranchName(options.branchName, operatingStrategy(options.mode).getSettingsBranchName(this.config))) {
|
|
50
51
|
throw new BranchPathError('Settings branches are not content branches');
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
// The name reaches git and branch.json as given; only the directory is sanitized.
|
|
54
|
+
const named = parseBranchRef(options.branchName);
|
|
55
|
+
if (!named.ok)
|
|
56
|
+
throw new BranchPathError(named.error);
|
|
57
|
+
const { branchRoot, baseRoot } = resolveBranchPath(options);
|
|
53
58
|
const paths = {
|
|
54
59
|
branchRoot,
|
|
55
60
|
baseRoot,
|
|
56
61
|
dirName: path.basename(branchRoot),
|
|
57
|
-
|
|
62
|
+
ref: options.branchName,
|
|
63
|
+
shortName: shortNameOf({ ...this.config, mode: options.mode }, options.branchName),
|
|
58
64
|
};
|
|
59
65
|
const existing = await readBranchMetadataFile(branchRoot);
|
|
60
66
|
if (existing) {
|
|
@@ -73,7 +79,7 @@ export class BranchWorkspaceManager {
|
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
async provisionUnshared(options, paths) {
|
|
76
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
82
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
77
83
|
await fs.mkdir(baseRoot, { recursive: true });
|
|
78
84
|
// Resolve the fork point once so the clone and the recorded metadata agree, and the remote
|
|
79
85
|
// before any clone is paid for.
|
|
@@ -93,7 +99,7 @@ export class BranchWorkspaceManager {
|
|
|
93
99
|
});
|
|
94
100
|
const metadata = buildInitialBranchMetadata({
|
|
95
101
|
branch: {
|
|
96
|
-
name: paths.
|
|
102
|
+
name: paths.ref,
|
|
97
103
|
title: options.title,
|
|
98
104
|
description: options.description,
|
|
99
105
|
access: options.access,
|
|
@@ -107,7 +113,7 @@ export class BranchWorkspaceManager {
|
|
|
107
113
|
if (state.kind === 'live' || state.kind === 'corrupt')
|
|
108
114
|
return this.existing(paths);
|
|
109
115
|
if (state.kind === 'protected')
|
|
110
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
116
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
111
117
|
if (state.kind === 'foreign') {
|
|
112
118
|
// dev's CLI sync builds a branch workspace by `git init` and real commits, with no
|
|
113
119
|
// remote and no branch.json; it is adopted where it is.
|
|
@@ -116,7 +122,7 @@ export class BranchWorkspaceManager {
|
|
|
116
122
|
return this.adoptInPlace(options, paths, baseBranch, metadata);
|
|
117
123
|
}
|
|
118
124
|
if (state.kind === 'residue' && (await this.recentlyActive(paths))) {
|
|
119
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
125
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
120
126
|
}
|
|
121
127
|
const sparseCone = sparseConeFor(this.config.contentRoot);
|
|
122
128
|
await recordSparseCone(baseRoot, sparseCone).catch((err) => {
|
|
@@ -142,11 +148,11 @@ export class BranchWorkspaceManager {
|
|
|
142
148
|
return outcome;
|
|
143
149
|
}
|
|
144
150
|
async stageAndPublish(paths, run) {
|
|
145
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
151
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
146
152
|
const staged = await stageBranchWorkspace({
|
|
147
153
|
baseRoot,
|
|
148
154
|
dirName,
|
|
149
|
-
branchName: paths.
|
|
155
|
+
branchName: paths.ref,
|
|
150
156
|
baseBranch: run.baseBranch,
|
|
151
157
|
remoteUrl: run.remoteUrl,
|
|
152
158
|
clone: {
|
|
@@ -166,6 +172,7 @@ export class BranchWorkspaceManager {
|
|
|
166
172
|
published = await run.provisionLog.step('publish', () => publishStaging({
|
|
167
173
|
baseRoot,
|
|
168
174
|
dirName,
|
|
175
|
+
shortName,
|
|
169
176
|
stagingPath: staged.stagingPath,
|
|
170
177
|
writeId: staged.meta.writeId,
|
|
171
178
|
expectedRemoteUrl: run.remoteUrl,
|
|
@@ -186,16 +193,16 @@ export class BranchWorkspaceManager {
|
|
|
186
193
|
* is, given branch.json under the branch's lock.
|
|
187
194
|
*/
|
|
188
195
|
async adoptInPlace(options, paths, baseBranch, metadata) {
|
|
189
|
-
const { branchRoot, baseRoot, dirName } = paths;
|
|
196
|
+
const { branchRoot, baseRoot, dirName, shortName } = paths;
|
|
190
197
|
const provisionLog = new ProvisionLog(dirName);
|
|
191
198
|
let created = false;
|
|
192
199
|
try {
|
|
193
|
-
const release = await provisionLog.step('lock', () => lockBranchName(baseRoot, dirName));
|
|
200
|
+
const release = await provisionLog.step('lock', () => lockBranchName(baseRoot, dirName, shortName));
|
|
194
201
|
try {
|
|
195
202
|
if (!(await readBranchMetadataFile(branchRoot))) {
|
|
196
203
|
await GitManager.initializeWorkspace({
|
|
197
204
|
workspacePath: branchRoot,
|
|
198
|
-
branchName: paths.
|
|
205
|
+
branchName: paths.ref,
|
|
199
206
|
mode: options.mode,
|
|
200
207
|
baseBranch,
|
|
201
208
|
sourceRoot: this.config.sourceRoot,
|
|
@@ -231,7 +238,7 @@ export class BranchWorkspaceManager {
|
|
|
231
238
|
return this.existing(paths);
|
|
232
239
|
const meta = await readBranchMetadataFile(branchRoot);
|
|
233
240
|
if (!meta)
|
|
234
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
241
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
235
242
|
return { kind: 'created', context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
236
243
|
}
|
|
237
244
|
/** Whether residue at the branch path may still belong to a live process. */
|
|
@@ -241,10 +248,10 @@ export class BranchWorkspaceManager {
|
|
|
241
248
|
return true;
|
|
242
249
|
return (await quietAgeMs(branchRoot)) < CREATE_RESIDUE_MIN_QUIET_MS;
|
|
243
250
|
}
|
|
244
|
-
async existing({ branchRoot, baseRoot, dirName, }) {
|
|
251
|
+
async existing({ branchRoot, baseRoot, dirName, shortName, }) {
|
|
245
252
|
const meta = await readBranchMetadataFile(branchRoot);
|
|
246
253
|
if (!meta)
|
|
247
|
-
throw new BranchProvisioningBusyError(dirName);
|
|
254
|
+
throw new BranchProvisioningBusyError(dirName, shortName);
|
|
248
255
|
return { kind: 'exists', context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
249
256
|
}
|
|
250
257
|
}
|