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.
Files changed (152) hide show
  1. package/CHANGELOG.md +197 -47
  2. package/dist/api/admin-branch-health.js +47 -20
  3. package/dist/api/admin.d.ts +13 -1
  4. package/dist/api/admin.js +14 -24
  5. package/dist/api/branch-changes.d.ts +87 -0
  6. package/dist/api/branch-changes.js +269 -0
  7. package/dist/api/branch-entry-changes.d.ts +83 -0
  8. package/dist/api/branch-entry-changes.js +262 -0
  9. package/dist/api/branch-merge.js +2 -2
  10. package/dist/api/branch-review.js +35 -4
  11. package/dist/api/branch-status.js +14 -5
  12. package/dist/api/branch-withdraw.js +7 -0
  13. package/dist/api/branch.d.ts +13 -1
  14. package/dist/api/branch.js +171 -97
  15. package/dist/api/client.d.ts +15 -0
  16. package/dist/api/client.js +13 -1
  17. package/dist/api/content.d.ts +18 -18
  18. package/dist/api/content.js +1 -1
  19. package/dist/api/github-sync.js +7 -3
  20. package/dist/api/groups.d.ts +4 -4
  21. package/dist/api/pr-comment-sync.d.ts +9 -8
  22. package/dist/api/pr-comment-sync.js +23 -13
  23. package/dist/api/routes.js +4 -0
  24. package/dist/api/types.d.ts +3 -1
  25. package/dist/authorization/groups/schema.d.ts +4 -4
  26. package/dist/authorization/protected-branch.d.ts +3 -3
  27. package/dist/authorization/protected-branch.js +3 -3
  28. package/dist/branch-metadata-file.d.ts +84 -0
  29. package/dist/branch-metadata-file.js +9 -0
  30. package/dist/branch-metadata.d.ts +4 -4
  31. package/dist/branch-metadata.js +4 -5
  32. package/dist/branch-param.d.ts +24 -0
  33. package/dist/branch-param.js +87 -0
  34. package/dist/branch-provisioning.d.ts +6 -3
  35. package/dist/branch-provisioning.js +12 -10
  36. package/dist/branch-registry.d.ts +6 -0
  37. package/dist/branch-registry.js +12 -3
  38. package/dist/branch-schema-cache.d.ts +22 -2
  39. package/dist/branch-schema-cache.js +88 -12
  40. package/dist/branch-workspace.js +22 -15
  41. package/dist/cli/cli.js +1122 -181
  42. package/dist/cli/generate-ai-content.js +791 -109
  43. package/dist/cli/init.js +126 -0
  44. package/dist/comment-store.d.ts +3 -0
  45. package/dist/comment-store.js +4 -1
  46. package/dist/config/schemas/config.d.ts +3 -0
  47. package/dist/config/schemas/config.js +2 -0
  48. package/dist/config/types.d.ts +11 -0
  49. package/dist/content-id-index.d.ts +5 -0
  50. package/dist/content-id-index.js +1 -1
  51. package/dist/content-listing.d.ts +53 -0
  52. package/dist/content-listing.js +123 -27
  53. package/dist/content-reader.d.ts +24 -10
  54. package/dist/content-reader.js +66 -22
  55. package/dist/context.js +51 -48
  56. package/dist/editor/BranchManager.d.ts +7 -3
  57. package/dist/editor/BranchManager.js +51 -6
  58. package/dist/editor/Editor.js +166 -34
  59. package/dist/editor/EntryNavigator.d.ts +34 -10
  60. package/dist/editor/EntryNavigator.js +185 -29
  61. package/dist/editor/admin/SystemHealthPanel.js +36 -1
  62. package/dist/editor/admin/useSystemHealth.js +1 -0
  63. package/dist/editor/branch-names.d.ts +43 -0
  64. package/dist/editor/branch-names.js +75 -0
  65. package/dist/editor/components/BranchPill.d.ts +2 -2
  66. package/dist/editor/components/BranchPill.js +2 -2
  67. package/dist/editor/components/EditorHeader.d.ts +3 -1
  68. package/dist/editor/components/EditorHeader.js +2 -2
  69. package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
  70. package/dist/editor/components/EntryChangesDrawer.js +363 -0
  71. package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
  72. package/dist/editor/components/EntryChangesSkeleton.js +7 -0
  73. package/dist/editor/components/EntryHeader.d.ts +9 -1
  74. package/dist/editor/components/EntryHeader.js +25 -3
  75. package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
  76. package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
  77. package/dist/editor/components/ReviewDialogs.js +3 -3
  78. package/dist/editor/context/ApiClientContext.d.ts +9 -0
  79. package/dist/editor/context/ApiClientContext.js +26 -2
  80. package/dist/editor/context/index.d.ts +1 -1
  81. package/dist/editor/context/index.js +1 -1
  82. package/dist/editor/copy.d.ts +1 -0
  83. package/dist/editor/copy.js +1 -0
  84. package/dist/editor/editor-utils.d.ts +2 -0
  85. package/dist/editor/editor-utils.js +15 -0
  86. package/dist/editor/entry-diff.d.ts +143 -0
  87. package/dist/editor/entry-diff.js +593 -0
  88. package/dist/editor/hooks/create-branch-request.d.ts +11 -1
  89. package/dist/editor/hooks/create-branch-request.js +16 -6
  90. package/dist/editor/hooks/useBranchActions.d.ts +19 -2
  91. package/dist/editor/hooks/useBranchActions.js +29 -20
  92. package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
  93. package/dist/editor/hooks/useBranchChangesData.js +134 -0
  94. package/dist/editor/hooks/useBranchManager.d.ts +16 -0
  95. package/dist/editor/hooks/useBranchManager.js +142 -54
  96. package/dist/editor/hooks/useDraftManager.js +6 -2
  97. package/dist/editor/hooks/useEntryManager.d.ts +18 -2
  98. package/dist/editor/hooks/useEntryManager.js +36 -10
  99. package/dist/editor/utils/editor-errors.d.ts +5 -2
  100. package/dist/git-manager.d.ts +71 -0
  101. package/dist/git-manager.js +270 -1
  102. package/dist/http/handler.js +11 -9
  103. package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
  104. package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
  105. package/dist/operating-mode/branch-namespace.d.ts +34 -0
  106. package/dist/operating-mode/branch-namespace.js +121 -0
  107. package/dist/operating-mode/client-unsafe-strategy.js +7 -0
  108. package/dist/operating-mode/types.d.ts +8 -0
  109. package/dist/paths/branch-name.d.ts +11 -19
  110. package/dist/paths/branch-name.js +17 -27
  111. package/dist/paths/index.d.ts +2 -2
  112. package/dist/paths/index.js +2 -2
  113. package/dist/paths/resolve.d.ts +0 -1
  114. package/dist/paths/resolve.js +0 -1
  115. package/dist/paths/validation.d.ts +40 -1
  116. package/dist/paths/validation.js +83 -4
  117. package/dist/pr-comments.d.ts +17 -9
  118. package/dist/pr-comments.js +53 -15
  119. package/dist/server.d.ts +6 -1
  120. package/dist/server.js +6 -1
  121. package/dist/services.js +2 -1
  122. package/dist/submission-attribution.d.ts +22 -0
  123. package/dist/submission-attribution.js +62 -6
  124. package/dist/task-queue/worker-status.d.ts +6 -1
  125. package/dist/task-queue/worker-status.js +23 -2
  126. package/dist/types.d.ts +59 -0
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/dist/worker/branch-policy.d.ts +111 -16
  130. package/dist/worker/branch-policy.js +222 -17
  131. package/dist/worker/cms-worker.d.ts +24 -2
  132. package/dist/worker/cms-worker.js +35 -24
  133. package/dist/worker/git-sync.d.ts +3 -3
  134. package/dist/worker/git-sync.js +42 -1
  135. package/dist/worker/github-gateway-http.js +2 -2
  136. package/dist/worker/github-gateway-wire.d.ts +42 -27
  137. package/dist/worker/github-gateway-wire.js +14 -3
  138. package/dist/worker/github-gateway.d.ts +17 -6
  139. package/dist/worker/github-gateway.js +62 -6
  140. package/dist/worker/github-mirror.d.ts +7 -4
  141. package/dist/worker/github-mirror.js +27 -16
  142. package/dist/worker/history-rewrite.d.ts +23 -10
  143. package/dist/worker/history-rewrite.js +43 -20
  144. package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
  145. package/dist/worker/legacy-branch-fixtures.js +71 -0
  146. package/dist/worker/rebase.d.ts +2 -1
  147. package/dist/worker/rebase.js +93 -36
  148. package/dist/worker/task-runner.d.ts +4 -1
  149. package/dist/worker/task-runner.js +77 -48
  150. package/dist/worker/unpublishable-branches.d.ts +34 -0
  151. package/dist/worker/unpublishable-branches.js +182 -0
  152. 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} with no in-memory
37
- * layer, so it stays coherent across Lambda invocations.
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} with no in-memory
84
- * layer, so it stays coherent across Lambda invocations.
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 = null;
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
- await fs.writeFile(tmpPath, JSON.stringify(newCache, null, 2), 'utf-8');
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
@@ -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
- const { branchRoot, baseRoot, branchName: safeName } = resolveBranchPath(options);
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
- safeName,
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.safeName,
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.safeName,
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.safeName,
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
  }