@ricsam/r5d-worker 0.0.78 → 0.0.79

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 (58) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/main.cjs +1550 -335
  3. package/dist/cjs/managed-paths.cjs +101 -1
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/project-workspace-state.cjs +184 -5
  6. package/dist/cjs/project-worktrees.cjs +641 -56
  7. package/dist/cjs/registry-auth.cjs +310 -0
  8. package/dist/cjs/repository-transition-policy.cjs +49 -0
  9. package/dist/cjs/supervisor.cjs +62 -11
  10. package/dist/cjs/working-tree-mirror.cjs +191 -34
  11. package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
  12. package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
  14. package/dist/cjs/workspace-git-sync.cjs +1028 -54
  15. package/dist/cjs/workspace-mount-boundary.cjs +71 -0
  16. package/dist/cjs/workspace-path-move.cjs +195 -0
  17. package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
  18. package/dist/cjs/workspace-project-config-policy.cjs +46 -0
  19. package/dist/cjs/workspace-publication-evidence.cjs +46 -0
  20. package/dist/mjs/main.mjs +1584 -341
  21. package/dist/mjs/managed-paths.mjs +100 -1
  22. package/dist/mjs/package.json +1 -1
  23. package/dist/mjs/project-workspace-state.mjs +181 -5
  24. package/dist/mjs/project-worktrees.mjs +632 -55
  25. package/dist/mjs/registry-auth.mjs +269 -0
  26. package/dist/mjs/repository-transition-policy.mjs +22 -0
  27. package/dist/mjs/supervisor.mjs +61 -11
  28. package/dist/mjs/working-tree-mirror.mjs +190 -34
  29. package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
  30. package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
  31. package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
  32. package/dist/mjs/workspace-git-sync.mjs +1023 -54
  33. package/dist/mjs/workspace-mount-boundary.mjs +37 -0
  34. package/dist/mjs/workspace-path-move.mjs +160 -0
  35. package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
  36. package/dist/mjs/workspace-project-config-policy.mjs +21 -0
  37. package/dist/mjs/workspace-publication-evidence.mjs +21 -0
  38. package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
  39. package/dist/types/main.d.ts +175 -1
  40. package/dist/types/managed-paths.d.ts +11 -0
  41. package/dist/types/project-workspace-state.d.ts +45 -1
  42. package/dist/types/project-worktrees.d.ts +119 -2
  43. package/dist/types/registry-auth.d.ts +47 -0
  44. package/dist/types/repository-transition-policy.d.ts +26 -0
  45. package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
  46. package/dist/types/supervisor-signal-fixture.d.ts +1 -0
  47. package/dist/types/supervisor.d.ts +6 -4
  48. package/dist/types/working-tree-mirror.d.ts +7 -0
  49. package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
  50. package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
  51. package/dist/types/workspace-command-sync-policy.d.ts +9 -0
  52. package/dist/types/workspace-git-sync.d.ts +33 -3
  53. package/dist/types/workspace-mount-boundary.d.ts +10 -0
  54. package/dist/types/workspace-path-move.d.ts +21 -0
  55. package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
  56. package/dist/types/workspace-project-config-policy.d.ts +34 -0
  57. package/dist/types/workspace-publication-evidence.d.ts +17 -0
  58. package/package.json +1 -1
@@ -1,8 +1,37 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { workerGitProcessEnvironment } from "./git-process-environment.mjs";
5
+ const HFS_IGNORED_CODE_POINTS = /[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/gu;
6
+ const MIRROR_TEMPORARY_FILE_PREFIX = ".r5d-working-tree-mirror-";
7
+ function isPortableGitMetadataSegment(segment) {
8
+ const hfsFolded = segment.replace(HFS_IGNORED_CODE_POINTS, "").toLowerCase();
9
+ if (hfsFolded === ".git") return true;
10
+ const ntfsFolded = segment.toLowerCase();
11
+ return /^(?:\.git|git~1)[ .]*(?::|$)/u.test(ntfsFolded);
12
+ }
4
13
  function isGitMetadataPath(relativePath) {
5
- return relativePath.split("/").includes(".git");
14
+ return relativePath.split(/[\\/]/u).some(isPortableGitMetadataSegment);
15
+ }
16
+ function assertWorkingTreeHasNoPortableGitMetadataAliases(root) {
17
+ root = path.resolve(root);
18
+ const rootStatus = fs.lstatSync(root);
19
+ if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) {
20
+ throw new Error(`Working-tree root is not a regular directory: ${root}`);
21
+ }
22
+ const inspectDirectory = (directoryPath, relativeDirectory) => {
23
+ for (const name of fs.readdirSync(directoryPath).sort()) {
24
+ const relativePath = relativeDirectory ? path.posix.join(relativeDirectory, name) : name;
25
+ if (name === ".git") continue;
26
+ if (isGitMetadataPath(relativePath)) {
27
+ throw new Error(`Working tree contains a portable Git metadata alias that cannot be moved safely: ${relativePath}`);
28
+ }
29
+ const entryPath = path.join(directoryPath, name);
30
+ const status = fs.lstatSync(entryPath);
31
+ if (status.isDirectory() && !status.isSymbolicLink()) inspectDirectory(entryPath, relativePath);
32
+ }
33
+ };
34
+ inspectDirectory(root, "");
6
35
  }
7
36
  function normalizeRelativePath(value) {
8
37
  const normalized = value.split(path.sep).join(path.posix.sep).replace(/^\.\/+/, "").replace(/^\/+|\/+$/g, "");
@@ -15,6 +44,41 @@ function assertInside(root, candidate) {
15
44
  throw new Error(`Working-tree path escapes its root: ${candidate}`);
16
45
  }
17
46
  }
47
+ function inspectRelativePathWithoutFollowingAncestors(root, relativePath) {
48
+ const normalized = normalizeRelativePath(relativePath);
49
+ if (!normalized) return { kind: "blocked", blockingPath: relativePath };
50
+ let current = path.resolve(root);
51
+ const segments = normalized.split("/");
52
+ for (let index = 0; index < segments.length; index += 1) {
53
+ current = path.join(current, segments[index]);
54
+ assertInside(root, current);
55
+ let stat;
56
+ try {
57
+ stat = fs.lstatSync(current);
58
+ } catch (error) {
59
+ if (error.code === "ENOENT") return { kind: "missing" };
60
+ throw error;
61
+ }
62
+ if (index < segments.length - 1 && (!stat.isDirectory() || stat.isSymbolicLink())) {
63
+ return { kind: "blocked", blockingPath: current };
64
+ }
65
+ if (index === segments.length - 1) return { kind: "entry", absolutePath: current, stat };
66
+ }
67
+ return { kind: "missing" };
68
+ }
69
+ function inspectDirectoryRoot(root, options) {
70
+ let stat;
71
+ try {
72
+ stat = fs.lstatSync(root);
73
+ } catch (error) {
74
+ if (options.allowMissing && error.code === "ENOENT") return false;
75
+ throw error;
76
+ }
77
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
78
+ throw new Error(`${options.label} is not a regular directory: ${root}`);
79
+ }
80
+ return true;
81
+ }
18
82
  function gitEligibleRoots(root) {
19
83
  const result = Bun.spawnSync(
20
84
  [
@@ -51,16 +115,13 @@ function gitEligibleRoots(root) {
51
115
  function addEntry(entries, root, relativePath, recurseDirectories) {
52
116
  const normalized = normalizeRelativePath(relativePath);
53
117
  if (!normalized || entries.has(normalized)) return;
54
- const absolutePath = path.join(root, ...normalized.split("/"));
55
- assertInside(root, absolutePath);
56
- let stat;
57
- try {
58
- stat = fs.lstatSync(absolutePath);
59
- } catch (error) {
60
- if (error.code === "ENOENT") return;
61
- throw error;
62
- }
118
+ const inspected = inspectRelativePathWithoutFollowingAncestors(root, normalized);
119
+ if (inspected.kind !== "entry") return;
120
+ const { absolutePath, stat } = inspected;
63
121
  if (stat.isSymbolicLink()) {
122
+ for (const existing of [...entries.keys()]) {
123
+ if (existing.startsWith(`${normalized}/`)) entries.delete(existing);
124
+ }
64
125
  entries.set(normalized, { kind: "symlink", mode: stat.mode, target: fs.readlinkSync(absolutePath) });
65
126
  return;
66
127
  }
@@ -80,9 +141,11 @@ function addParentDirectories(entries, root) {
80
141
  let parent = path.posix.dirname(relativePath);
81
142
  while (parent !== ".") {
82
143
  if (!entries.has(parent)) {
83
- const absolutePath = path.join(root, ...parent.split("/"));
84
- const stat = fs.lstatSync(absolutePath);
85
- entries.set(parent, { kind: "directory", mode: stat.mode });
144
+ const inspected = inspectRelativePathWithoutFollowingAncestors(root, parent);
145
+ if (inspected.kind !== "entry" || !inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) {
146
+ throw new Error(`Working-tree entry has an unsafe parent directory: ${parent}`);
147
+ }
148
+ entries.set(parent, { kind: "directory", mode: inspected.stat.mode });
86
149
  }
87
150
  parent = path.posix.dirname(parent);
88
151
  }
@@ -90,7 +153,8 @@ function addParentDirectories(entries, root) {
90
153
  }
91
154
  function inspectWorkingTree(root, mode) {
92
155
  const entries = /* @__PURE__ */ new Map();
93
- if (!fs.existsSync(root)) return entries;
156
+ root = path.resolve(root);
157
+ if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
94
158
  if (mode === "all") {
95
159
  for (const child of fs.readdirSync(root).sort()) addEntry(entries, root, child, true);
96
160
  } else {
@@ -102,32 +166,58 @@ function inspectWorkingTree(root, mode) {
102
166
  return entries;
103
167
  }
104
168
  function removeEntry(targetRoot, relativePath) {
105
- const targetPath = path.join(targetRoot, ...relativePath.split("/"));
106
- assertInside(targetRoot, targetPath);
107
- fs.rmSync(targetPath, { recursive: true, force: true });
169
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
170
+ if (inspected.kind === "missing") return;
171
+ if (inspected.kind === "blocked") {
172
+ throw new Error(`Refusing to remove a working-tree path through a symlink or non-directory: ${inspected.blockingPath}`);
173
+ }
174
+ fs.rmSync(inspected.absolutePath, { recursive: true, force: true });
108
175
  }
109
176
  function ensureDirectory(targetRoot, relativePath, mode) {
110
- const targetPath = path.join(targetRoot, ...relativePath.split("/"));
111
- assertInside(targetRoot, targetPath);
112
- if (fs.existsSync(targetPath) && !fs.lstatSync(targetPath).isDirectory()) removeEntry(targetRoot, relativePath);
113
- fs.mkdirSync(targetPath, { recursive: true, mode: mode & 511 });
177
+ const normalized = normalizeRelativePath(relativePath);
178
+ if (!normalized) throw new Error(`Invalid working-tree directory path: ${relativePath}`);
179
+ let current = path.resolve(targetRoot);
180
+ const segments = normalized.split("/");
181
+ for (let index = 0; index < segments.length; index += 1) {
182
+ current = path.join(current, segments[index]);
183
+ assertInside(targetRoot, current);
184
+ let stat = null;
185
+ try {
186
+ stat = fs.lstatSync(current);
187
+ } catch (error) {
188
+ if (error.code !== "ENOENT") throw error;
189
+ }
190
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) {
191
+ fs.rmSync(current, { recursive: true, force: true });
192
+ stat = null;
193
+ }
194
+ if (!stat) fs.mkdirSync(current, { mode: index === segments.length - 1 ? mode & 511 : 493 });
195
+ }
114
196
  }
115
- function filesEqual(leftPath, rightPath, size) {
197
+ function filesEqual(leftPath, rightPath, entry) {
116
198
  let rightStat;
117
199
  try {
118
200
  rightStat = fs.lstatSync(rightPath);
119
201
  } catch {
120
202
  return false;
121
203
  }
122
- if (!rightStat.isFile() || rightStat.size !== size) return false;
123
- const left = fs.openSync(leftPath, "r");
124
- const right = fs.openSync(rightPath, "r");
204
+ if (!rightStat.isFile() || rightStat.size !== entry.size || (rightStat.mode & 511) !== (entry.mode & 511)) return false;
205
+ let left;
206
+ let right;
207
+ try {
208
+ left = fs.openSync(leftPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
209
+ right = fs.openSync(rightPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
210
+ } catch {
211
+ if (left !== void 0) fs.closeSync(left);
212
+ if (right !== void 0) fs.closeSync(right);
213
+ return false;
214
+ }
125
215
  const leftBuffer = Buffer.allocUnsafe(64 * 1024);
126
216
  const rightBuffer = Buffer.allocUnsafe(64 * 1024);
127
217
  try {
128
218
  let offset = 0;
129
- while (offset < size) {
130
- const length = Math.min(leftBuffer.length, size - offset);
219
+ while (offset < entry.size) {
220
+ const length = Math.min(leftBuffer.length, entry.size - offset);
131
221
  const leftRead = fs.readSync(left, leftBuffer, 0, length, offset);
132
222
  const rightRead = fs.readSync(right, rightBuffer, 0, length, offset);
133
223
  if (leftRead !== rightRead || !leftBuffer.subarray(0, leftRead).equals(rightBuffer.subarray(0, rightRead))) return false;
@@ -139,18 +229,73 @@ function filesEqual(leftPath, rightPath, size) {
139
229
  fs.closeSync(right);
140
230
  }
141
231
  }
232
+ function copyRegularFile(sourcePath, targetPath, entry) {
233
+ const source = fs.openSync(sourcePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
234
+ const temporaryPath = path.join(path.dirname(targetPath), `${MIRROR_TEMPORARY_FILE_PREFIX}${randomUUID()}.tmp`);
235
+ let target;
236
+ try {
237
+ const sourceStat = fs.fstatSync(source);
238
+ if (!sourceStat.isFile() || sourceStat.size !== entry.size || (sourceStat.mode & 511) !== (entry.mode & 511)) {
239
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
240
+ }
241
+ target = fs.openSync(
242
+ temporaryPath,
243
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
244
+ entry.mode & 511
245
+ );
246
+ const buffer = Buffer.allocUnsafe(64 * 1024);
247
+ let offset = 0;
248
+ while (offset < sourceStat.size) {
249
+ const bytesRead = fs.readSync(source, buffer, 0, Math.min(buffer.length, sourceStat.size - offset), offset);
250
+ if (bytesRead <= 0) throw new Error(`Working-tree source ended while it was being mirrored: ${sourcePath}`);
251
+ let written = 0;
252
+ while (written < bytesRead) {
253
+ written += fs.writeSync(target, buffer, written, bytesRead - written, offset + written);
254
+ }
255
+ offset += bytesRead;
256
+ }
257
+ fs.fchmodSync(target, entry.mode & 511);
258
+ fs.fsyncSync(target);
259
+ const finalSourceStat = fs.fstatSync(source);
260
+ if (!finalSourceStat.isFile() || finalSourceStat.size !== sourceStat.size || (finalSourceStat.mode & 511) !== (sourceStat.mode & 511) || finalSourceStat.mtimeMs !== sourceStat.mtimeMs || finalSourceStat.ctimeMs !== sourceStat.ctimeMs) {
261
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
262
+ }
263
+ fs.closeSync(target);
264
+ target = void 0;
265
+ fs.renameSync(temporaryPath, targetPath);
266
+ const parent = fs.openSync(path.dirname(targetPath), fs.constants.O_RDONLY);
267
+ try {
268
+ fs.fsyncSync(parent);
269
+ } finally {
270
+ fs.closeSync(parent);
271
+ }
272
+ } finally {
273
+ if (target !== void 0) fs.closeSync(target);
274
+ fs.closeSync(source);
275
+ fs.rmSync(temporaryPath, { force: true });
276
+ }
277
+ }
142
278
  function copyEntry(sourceRoot, targetRoot, relativePath, entry) {
143
- const sourcePath = path.join(sourceRoot, ...relativePath.split("/"));
144
279
  const targetPath = path.join(targetRoot, ...relativePath.split("/"));
145
- assertInside(sourceRoot, sourcePath);
146
280
  assertInside(targetRoot, targetPath);
147
281
  const parent = path.posix.dirname(relativePath);
148
282
  if (parent !== ".") ensureDirectory(targetRoot, parent, 493);
283
+ const source = inspectRelativePathWithoutFollowingAncestors(sourceRoot, relativePath);
284
+ if (source.kind !== "entry") {
285
+ throw new Error(`Working-tree source became unsafe while it was being mirrored: ${relativePath}`);
286
+ }
287
+ const sourcePath = source.absolutePath;
149
288
  if (entry.kind === "directory") {
289
+ if (!source.stat.isDirectory() || source.stat.isSymbolicLink()) {
290
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
291
+ }
150
292
  ensureDirectory(targetRoot, relativePath, entry.mode);
151
293
  return;
152
294
  }
153
295
  if (entry.kind === "symlink") {
296
+ if (!source.stat.isSymbolicLink() || fs.readlinkSync(sourcePath) !== entry.target) {
297
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
298
+ }
154
299
  let unchanged = false;
155
300
  try {
156
301
  unchanged = fs.lstatSync(targetPath).isSymbolicLink() && fs.readlinkSync(targetPath) === entry.target;
@@ -163,16 +308,26 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry) {
163
308
  }
164
309
  return;
165
310
  }
166
- if (!filesEqual(sourcePath, targetPath, entry.size)) {
167
- if (fs.existsSync(targetPath) && !fs.lstatSync(targetPath).isFile()) removeEntry(targetRoot, relativePath);
168
- fs.copyFileSync(sourcePath, targetPath);
311
+ if (!source.stat.isFile() || source.stat.isSymbolicLink() || source.stat.size !== entry.size) {
312
+ throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
313
+ }
314
+ if (!filesEqual(sourcePath, targetPath, entry)) {
315
+ const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
316
+ if (target.kind === "blocked") {
317
+ throw new Error(`Refusing to copy through a symlink or non-directory: ${target.blockingPath}`);
318
+ }
319
+ const targetStat = target.kind === "entry" ? target.stat : null;
320
+ if (targetStat && !targetStat.isFile()) removeEntry(targetRoot, relativePath);
321
+ copyRegularFile(sourcePath, targetPath, entry);
169
322
  }
170
- fs.chmodSync(targetPath, entry.mode & 511);
171
323
  }
172
324
  function mirrorWorkingTree(input) {
173
325
  const sourceRoot = path.resolve(input.sourceRoot);
174
326
  const targetRoot = path.resolve(input.targetRoot);
175
- fs.mkdirSync(targetRoot, { recursive: true });
327
+ if (!inspectDirectoryRoot(targetRoot, { allowMissing: true, label: "Working-tree target root" })) {
328
+ fs.mkdirSync(targetRoot, { recursive: true });
329
+ }
330
+ inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
176
331
  const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
177
332
  const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all");
178
333
  for (const relativePath of [...existing.keys()].sort((left, right) => right.split("/").length - left.split("/").length)) {
@@ -186,6 +341,7 @@ function mirrorWorkingTree(input) {
186
341
  return { paths: [...desired.keys()].filter((relativePath) => desired.get(relativePath)?.kind !== "directory").sort() };
187
342
  }
188
343
  export {
344
+ assertWorkingTreeHasNoPortableGitMetadataAliases,
189
345
  inspectWorkingTree,
190
346
  mirrorWorkingTree
191
347
  };
@@ -0,0 +1,40 @@
1
+ const PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS = 12e4;
2
+ function pendingCreatedBranchKey(projectId, branchName) {
3
+ return `${projectId}\0${branchName}`;
4
+ }
5
+ function hasActiveVisibleProjectsWorkspaceTarget(activeTargets) {
6
+ for (const target of activeTargets) {
7
+ if (target.type === "workspace" && target.rootProfile === "visible_projects") return true;
8
+ }
9
+ return false;
10
+ }
11
+ function projectBranchHasActiveWorkspaceTarget(projectId, branchName, activeTargets) {
12
+ for (const target of activeTargets) {
13
+ if (target.type === "project" && target.projectId === projectId && target.branchName === branchName) return true;
14
+ if (target.type === "workspace" && target.rootProfile === "visible_projects") return true;
15
+ }
16
+ return false;
17
+ }
18
+ function shouldDeferAutomaticWorkspaceSyncForPendingBranch(pendingCreatedBranches, activeTargets) {
19
+ if (pendingCreatedBranches.length === 0) return false;
20
+ const targets = [...activeTargets];
21
+ return pendingCreatedBranches.some(({ projectId, branchName }) => projectBranchHasActiveWorkspaceTarget(projectId, branchName, targets));
22
+ }
23
+ function pendingCreatedBranchAutomaticSyncDeferral(pendingCreatedBranches, activeTargets, publicationNotBeforeByBranch, nowMs = Date.now()) {
24
+ if (shouldDeferAutomaticWorkspaceSyncForPendingBranch(pendingCreatedBranches, activeTargets)) {
25
+ return { kind: "active_target" };
26
+ }
27
+ const notBeforeMs = pendingCreatedBranches.reduce(
28
+ (latest, { projectId, branchName }) => Math.max(latest, publicationNotBeforeByBranch.get(pendingCreatedBranchKey(projectId, branchName)) ?? 0),
29
+ 0
30
+ );
31
+ return notBeforeMs > nowMs ? { kind: "creation_grace", retryAfterMs: notBeforeMs - nowMs } : null;
32
+ }
33
+ export {
34
+ PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS,
35
+ hasActiveVisibleProjectsWorkspaceTarget,
36
+ pendingCreatedBranchAutomaticSyncDeferral,
37
+ pendingCreatedBranchKey,
38
+ projectBranchHasActiveWorkspaceTarget,
39
+ shouldDeferAutomaticWorkspaceSyncForPendingBranch
40
+ };
@@ -0,0 +1,13 @@
1
+ function assertProjectBranchDeletionIncarnation(input) {
2
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(input.requiredBranchId)) {
3
+ throw new Error("Project branch deletion requires a valid durable branch incarnation");
4
+ }
5
+ for (const branch of [input.configuredBranch, input.pendingLocalBranch]) {
6
+ if (branch && branch.branchId !== input.requiredBranchId) {
7
+ throw new Error("Project branch deletion belongs to another durable branch incarnation");
8
+ }
9
+ }
10
+ }
11
+ export {
12
+ assertProjectBranchDeletionIncarnation
13
+ };
@@ -1,3 +1,19 @@
1
+ function usesVisibleWorkspace(target) {
2
+ return target.type === "project" || target.rootProfile === "visible_projects";
3
+ }
4
+ async function reserveWorkspaceCommandAfterCurrentSync(target, coordinator, reserve) {
5
+ if (!usesVisibleWorkspace(target)) {
6
+ reserve();
7
+ return;
8
+ }
9
+ while (true) {
10
+ const currentSync = coordinator.afterCurrent();
11
+ await currentSync;
12
+ if (currentSync !== coordinator.afterCurrent()) continue;
13
+ reserve();
14
+ return;
15
+ }
16
+ }
1
17
  function shouldSerializeWorkspaceCommand(target) {
2
18
  return target.type === "workspace" && target.rootProfile === "canonical_sync";
3
19
  }
@@ -9,6 +25,7 @@ function acquireWorkspaceCommandMutation(target, coordinator) {
9
25
  }
10
26
  export {
11
27
  acquireWorkspaceCommandMutation,
28
+ reserveWorkspaceCommandAfterCurrentSync,
12
29
  runWorkspaceCommand,
13
30
  shouldSerializeWorkspaceCommand
14
31
  };