@ricsam/r5d-worker 0.0.81 → 0.0.82
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/dist/cjs/main.cjs +324 -94
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-command-sync-policy.cjs +3 -3
- package/dist/cjs/workspace-git-sync.cjs +377 -122
- package/dist/cjs/workspace-merge-projection.cjs +392 -0
- package/dist/mjs/main.mjs +322 -94
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-command-sync-policy.mjs +3 -3
- package/dist/mjs/workspace-git-sync.mjs +381 -122
- package/dist/mjs/workspace-merge-projection.mjs +355 -0
- package/dist/types/main.d.ts +63 -0
- package/dist/types/working-tree-mirror.d.ts +1 -2
- package/dist/types/workspace-command-sync-policy.d.ts +4 -3
- package/dist/types/workspace-git-sync.d.ts +6 -0
- package/dist/types/workspace-merge-projection.d.ts +42 -0
- package/package.json +1 -1
|
@@ -46,6 +46,7 @@ module.exports = __toCommonJS(workspace_git_sync_exports);
|
|
|
46
46
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
47
47
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
48
48
|
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
49
|
+
var import_workspace_merge_projection = require("./workspace-merge-projection.cjs");
|
|
49
50
|
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
50
51
|
var import_workspace_mount_boundary = require("./workspace-mount-boundary.cjs");
|
|
51
52
|
const WORKSPACE_GIT_BRANCH = "main";
|
|
@@ -140,22 +141,52 @@ function compareWorkspaceHydrationMountBasis(left, right) {
|
|
|
140
141
|
return left.id.localeCompare(right.id);
|
|
141
142
|
}
|
|
142
143
|
function workspaceHydrationReceipt(head, mounts) {
|
|
143
|
-
const bases = mounts.map(workspaceHydrationMountBasis).sort(compareWorkspaceHydrationMountBasis);
|
|
144
|
+
const bases = mounts.map((mount) => ({ ...workspaceHydrationMountBasis(mount), head })).sort(compareWorkspaceHydrationMountBasis);
|
|
144
145
|
const ids = /* @__PURE__ */ new Set();
|
|
145
146
|
for (const basis of bases) {
|
|
146
147
|
if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
|
|
147
148
|
ids.add(basis.id);
|
|
148
149
|
}
|
|
149
|
-
return { version:
|
|
150
|
+
return { version: 3, mounts: bases };
|
|
151
|
+
}
|
|
152
|
+
function receiptWithMountsAtHead(previous, head, mounts, allConfiguredMounts) {
|
|
153
|
+
const advanceIds = new Set(mounts.map(({ id }) => id));
|
|
154
|
+
const previousById = new Map(previous?.mounts.map((entry) => [entry.id, entry]) ?? []);
|
|
155
|
+
const entries = allConfiguredMounts.flatMap((mount) => {
|
|
156
|
+
const basis = workspaceHydrationMountBasis(mount);
|
|
157
|
+
if (advanceIds.has(mount.id)) return [{ ...basis, head }];
|
|
158
|
+
const carried = previousById.get(mount.id);
|
|
159
|
+
if (!carried) return [];
|
|
160
|
+
return [carried];
|
|
161
|
+
});
|
|
162
|
+
entries.sort(compareWorkspaceHydrationMountBasis);
|
|
163
|
+
const ids = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
if (ids.has(entry.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${entry.id}`);
|
|
166
|
+
ids.add(entry.id);
|
|
167
|
+
}
|
|
168
|
+
return { version: 3, mounts: entries };
|
|
150
169
|
}
|
|
151
170
|
function workspaceHydrationReceiptsEqual(left, right) {
|
|
152
|
-
|
|
171
|
+
if (JSON.stringify(left) !== JSON.stringify(right)) return false;
|
|
172
|
+
const effectiveLegacyHead = (receipt) => receipt?.mounts.length === 0 ? receipt.legacyHead ?? null : null;
|
|
173
|
+
return effectiveLegacyHead(left) === effectiveLegacyHead(right);
|
|
153
174
|
}
|
|
154
175
|
function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
|
|
155
|
-
if (!receipt
|
|
176
|
+
if (!receipt) return false;
|
|
156
177
|
const expected = workspaceHydrationMountBasis(mount);
|
|
157
178
|
const actual = receipt.mounts.find(({ id }) => id === expected.id);
|
|
158
|
-
|
|
179
|
+
if (!actual || actual.head !== head) return false;
|
|
180
|
+
const { head: _head, ...actualBasis } = actual;
|
|
181
|
+
return JSON.stringify(actualBasis) === JSON.stringify(expected);
|
|
182
|
+
}
|
|
183
|
+
function workspaceHydrationReceiptMountBasisHead(receipt, mount) {
|
|
184
|
+
if (!receipt) return null;
|
|
185
|
+
const expected = workspaceHydrationMountBasis(mount);
|
|
186
|
+
const actual = receipt.mounts.find(({ id }) => id === expected.id);
|
|
187
|
+
if (!actual) return null;
|
|
188
|
+
const { head, ...actualBasis } = actual;
|
|
189
|
+
return JSON.stringify(actualBasis) === JSON.stringify(expected) ? head : null;
|
|
159
190
|
}
|
|
160
191
|
function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
|
|
161
192
|
return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
|
|
@@ -176,7 +207,9 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
176
207
|
if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
|
|
177
208
|
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
178
209
|
}
|
|
179
|
-
|
|
210
|
+
const receipt2 = { version: 3, mounts: [] };
|
|
211
|
+
Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
|
|
212
|
+
return receipt2;
|
|
180
213
|
}
|
|
181
214
|
let parsed;
|
|
182
215
|
try {
|
|
@@ -188,7 +221,8 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
188
221
|
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
189
222
|
}
|
|
190
223
|
const candidate = parsed;
|
|
191
|
-
|
|
224
|
+
const liftedV2Head = candidate.version === 2 && typeof candidate.head === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head) ? candidate.head : null;
|
|
225
|
+
if (candidate.version !== 3 && liftedV2Head === null) {
|
|
192
226
|
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
193
227
|
}
|
|
194
228
|
if (!Array.isArray(candidate.mounts)) {
|
|
@@ -202,23 +236,51 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
202
236
|
const mount = rawMount;
|
|
203
237
|
const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
|
|
204
238
|
const durabilityRelative = typeof mount.sourcePath === "string" && typeof mount.durabilityRootPath === "string" ? import_node_path.default.relative(mount.durabilityRootPath, mount.sourcePath) : "..";
|
|
205
|
-
if (typeof mount.id !== "string" || !mount.id || seenIds.has(mount.id) || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || import_node_path.default.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || import_node_path.default.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(durabilityRelative) || typeof mount.workspaceRelativePath !== "string" || mount.workspaceRelativePath !== normalizedRelativePath || !normalizedRelativePath || normalizedRelativePath === "." || normalizedRelativePath.split("/").some((segment) => segment === ".." || segment === ".git") || mount.sourceMode !== "all" && mount.sourceMode !== "git" || mount.hydrateDeletionMode !== "all" && mount.hydrateDeletionMode !== "git" || typeof mount.preserveLocalOnInitialOuterAbsence !== "boolean" || typeof mount.preserveLocalOnHydrationBasisChange !== "boolean" || typeof mount.deleteWhenSourceMissing !== "boolean") {
|
|
239
|
+
if (typeof mount.id !== "string" || !mount.id || seenIds.has(mount.id) || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || import_node_path.default.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || import_node_path.default.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(durabilityRelative) || typeof mount.workspaceRelativePath !== "string" || mount.workspaceRelativePath !== normalizedRelativePath || !normalizedRelativePath || normalizedRelativePath === "." || normalizedRelativePath.split("/").some((segment) => segment === ".." || segment === ".git") || mount.sourceMode !== "all" && mount.sourceMode !== "git" || mount.hydrateDeletionMode !== "all" && mount.hydrateDeletionMode !== "git" || typeof mount.preserveLocalOnInitialOuterAbsence !== "boolean" || typeof mount.preserveLocalOnHydrationBasisChange !== "boolean" || typeof mount.deleteWhenSourceMissing !== "boolean" || candidate.version === 3 && (typeof mount.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head))) {
|
|
206
240
|
throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
|
|
207
241
|
}
|
|
208
242
|
seenIds.add(mount.id);
|
|
209
|
-
return
|
|
243
|
+
return {
|
|
244
|
+
id: mount.id,
|
|
245
|
+
incarnationKey: mount.incarnationKey,
|
|
246
|
+
sourcePath: mount.sourcePath,
|
|
247
|
+
durabilityRootPath: mount.durabilityRootPath,
|
|
248
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
249
|
+
sourceMode: mount.sourceMode,
|
|
250
|
+
hydrateDeletionMode: mount.hydrateDeletionMode,
|
|
251
|
+
preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence,
|
|
252
|
+
preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange,
|
|
253
|
+
deleteWhenSourceMissing: mount.deleteWhenSourceMissing,
|
|
254
|
+
head: candidate.version === 3 ? mount.head : liftedV2Head
|
|
255
|
+
};
|
|
210
256
|
});
|
|
257
|
+
const sortedMounts = mounts.sort(compareWorkspaceHydrationMountBasis);
|
|
258
|
+
if (liftedV2Head) {
|
|
259
|
+
const legacyReceipt = {
|
|
260
|
+
version: 2,
|
|
261
|
+
head: liftedV2Head,
|
|
262
|
+
mounts: sortedMounts.map(({ head: _head, ...basis }) => basis)
|
|
263
|
+
};
|
|
264
|
+
if (content !== `${JSON.stringify(legacyReceipt)}
|
|
265
|
+
`) {
|
|
266
|
+
throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
211
269
|
const receipt = {
|
|
212
|
-
version:
|
|
213
|
-
|
|
214
|
-
mounts: mounts.sort(compareWorkspaceHydrationMountBasis)
|
|
270
|
+
version: 3,
|
|
271
|
+
mounts: sortedMounts
|
|
215
272
|
};
|
|
216
|
-
if (
|
|
273
|
+
if (liftedV2Head) Object.defineProperty(receipt, "legacyHead", { value: liftedV2Head, enumerable: false });
|
|
274
|
+
if (candidate.version === 3 && content !== `${JSON.stringify(receipt)}
|
|
217
275
|
`) {
|
|
218
276
|
throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
|
|
219
277
|
}
|
|
220
|
-
|
|
221
|
-
|
|
278
|
+
const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
|
|
279
|
+
if (liftedV2Head) receiptHeads.add(liftedV2Head);
|
|
280
|
+
for (const head of receiptHeads) {
|
|
281
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`])) {
|
|
282
|
+
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
283
|
+
}
|
|
222
284
|
}
|
|
223
285
|
return receipt;
|
|
224
286
|
}
|
|
@@ -232,22 +294,92 @@ function readHydratedWorkspaceReceipt(workspacePath) {
|
|
|
232
294
|
const content = import_node_fs.default.readFileSync(receiptPath, "utf8");
|
|
233
295
|
return parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
|
|
234
296
|
}
|
|
297
|
+
function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
|
|
298
|
+
const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
|
|
299
|
+
const status = lstatIfExists(receiptPath);
|
|
300
|
+
if (!status?.isFile() || status.isSymbolicLink()) return false;
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(import_node_fs.default.readFileSync(receiptPath, "utf8")).version === 3;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
235
307
|
function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
|
|
236
308
|
const resolvedWorkspacePath = import_node_path.default.resolve(workspacePath);
|
|
237
309
|
if (!import_node_fs.default.existsSync(import_node_path.default.join(resolvedWorkspacePath, ".git"))) return true;
|
|
238
310
|
if (mounts) validateMounts(resolvedWorkspacePath, mounts);
|
|
239
311
|
const head = revParse(resolvedWorkspacePath, "HEAD");
|
|
240
312
|
if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
|
|
241
|
-
if (head === null) return true;
|
|
242
313
|
const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
|
|
243
|
-
|
|
314
|
+
if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
|
|
315
|
+
if (head === null) return true;
|
|
316
|
+
if (!receipt) return false;
|
|
317
|
+
return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
|
|
318
|
+
(mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean(mount.busy?.() && workspaceHydrationReceiptMountBasisHead(receipt, mount))
|
|
319
|
+
) : Boolean(receipt && receipt.mounts.every((mount) => mount.head === head));
|
|
320
|
+
}
|
|
321
|
+
const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
|
|
322
|
+
const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
|
|
323
|
+
function workspaceBasisRefName(mountId) {
|
|
324
|
+
const sanitized = Buffer.from(mountId, "utf8").toString("base64url");
|
|
325
|
+
if (!sanitized) throw new Error(`Cannot derive a workspace basis ref for mount id: ${mountId}`);
|
|
326
|
+
return `${WORKSPACE_GIT_BASIS_REF_PREFIX}${sanitized}`;
|
|
327
|
+
}
|
|
328
|
+
function listWorkspaceRefs(workspacePath, prefix) {
|
|
329
|
+
return git(workspacePath, ["for-each-ref", "--format=%(refname)", prefix], `list ${prefix} refs`).split("\n").filter(Boolean).sort();
|
|
330
|
+
}
|
|
331
|
+
function desiredWorkspaceBasisRefs(receipt) {
|
|
332
|
+
const desired = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const mount of receipt.mounts) {
|
|
334
|
+
const ref = workspaceBasisRefName(mount.id);
|
|
335
|
+
if (desired.has(ref)) throw new Error(`Workspace hydration mount ids collide at basis ref ${ref}`);
|
|
336
|
+
desired.set(ref, mount.head);
|
|
337
|
+
}
|
|
338
|
+
return desired;
|
|
339
|
+
}
|
|
340
|
+
function workspaceBasisRefsMatch(workspacePath, receipt) {
|
|
341
|
+
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
342
|
+
const actualRefs = listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX);
|
|
343
|
+
if (actualRefs.length !== desired.size) return false;
|
|
344
|
+
return actualRefs.every((ref) => desired.get(ref) === revParse(workspacePath, ref));
|
|
345
|
+
}
|
|
346
|
+
function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
|
|
347
|
+
return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
|
|
348
|
+
}
|
|
349
|
+
function repairWorkspaceBasisRefs(workspacePath, receipt) {
|
|
350
|
+
const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
|
|
351
|
+
if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
|
|
352
|
+
}
|
|
353
|
+
function updateWorkspaceBasisRefs(workspacePath, receipt) {
|
|
354
|
+
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
355
|
+
const transaction = ["start"];
|
|
356
|
+
for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
|
|
357
|
+
for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
|
|
358
|
+
if (!desired.has(ref)) transaction.push(`delete ${ref}`);
|
|
359
|
+
}
|
|
360
|
+
transaction.push("prepare", "commit", "");
|
|
361
|
+
const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
|
|
362
|
+
cwd: workspacePath,
|
|
363
|
+
stdin: Buffer.from(transaction.join("\n")),
|
|
364
|
+
stdout: "pipe",
|
|
365
|
+
stderr: "pipe",
|
|
366
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
367
|
+
});
|
|
368
|
+
if (updated.exitCode !== 0) {
|
|
369
|
+
throw new Error(`Synchronize workspace hydration basis refs: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
|
|
370
|
+
}
|
|
371
|
+
for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) {
|
|
372
|
+
git(workspacePath, ["update-ref", "-d", ref], "remove staged workspace hydration basis ref");
|
|
373
|
+
}
|
|
244
374
|
}
|
|
245
375
|
function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
376
|
+
for (const mount of receipt.mounts) {
|
|
377
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
|
|
378
|
+
throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
|
|
379
|
+
}
|
|
380
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${mount.head}^{commit}`])) {
|
|
381
|
+
throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${mount.head}`);
|
|
382
|
+
}
|
|
251
383
|
}
|
|
252
384
|
readHydratedWorkspaceReceipt(workspacePath);
|
|
253
385
|
const gitDirectory = import_node_path.default.join(workspacePath, ".git");
|
|
@@ -267,6 +399,12 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
|
267
399
|
throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
|
|
268
400
|
}
|
|
269
401
|
const temporaryPath = import_node_path.default.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
402
|
+
const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
|
|
403
|
+
desiredWorkspaceBasisRefs(receipt);
|
|
404
|
+
for (const mount of receipt.mounts) {
|
|
405
|
+
const stagedRef = `${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)}`;
|
|
406
|
+
git(workspacePath, ["update-ref", stagedRef, mount.head], "stage workspace hydration basis ref");
|
|
407
|
+
}
|
|
270
408
|
let descriptor;
|
|
271
409
|
try {
|
|
272
410
|
descriptor = import_node_fs.default.openSync(temporaryPath, import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL, 384);
|
|
@@ -277,6 +415,7 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
|
277
415
|
descriptor = void 0;
|
|
278
416
|
import_node_fs.default.renameSync(temporaryPath, receiptPath);
|
|
279
417
|
fsyncDirectory(receiptDirectory);
|
|
418
|
+
updateWorkspaceBasisRefs(workspacePath, receipt);
|
|
280
419
|
} catch (error) {
|
|
281
420
|
if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
|
|
282
421
|
import_node_fs.default.rmSync(temporaryPath, { force: true });
|
|
@@ -404,6 +543,16 @@ function workspaceRebaseInProgress(workspacePath) {
|
|
|
404
543
|
const gitDirectory = import_node_path.default.join(workspacePath, ".git");
|
|
405
544
|
return import_node_fs.default.existsSync(import_node_path.default.join(gitDirectory, "rebase-merge")) || import_node_fs.default.existsSync(import_node_path.default.join(gitDirectory, "rebase-apply"));
|
|
406
545
|
}
|
|
546
|
+
function workspaceResolutionInProgress(workspacePath) {
|
|
547
|
+
return workspaceRebaseInProgress(workspacePath) || import_node_fs.default.existsSync(import_node_path.default.join(workspacePath, ".git", "MERGE_HEAD"));
|
|
548
|
+
}
|
|
549
|
+
function workspaceHasUnmergedEntries(workspacePath) {
|
|
550
|
+
const result = gitResult(workspacePath, ["ls-files", "-u", "-z"]);
|
|
551
|
+
if (result.exitCode !== 0) {
|
|
552
|
+
throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
553
|
+
}
|
|
554
|
+
return result.stdout.length > 0;
|
|
555
|
+
}
|
|
407
556
|
function clearWorkspaceCheckoutTree(workspacePath) {
|
|
408
557
|
for (const name of import_node_fs.default.readdirSync(workspacePath)) {
|
|
409
558
|
if (name === ".git") continue;
|
|
@@ -411,15 +560,18 @@ function clearWorkspaceCheckoutTree(workspacePath) {
|
|
|
411
560
|
}
|
|
412
561
|
fsyncDirectory(workspacePath);
|
|
413
562
|
}
|
|
414
|
-
function recoverWorkspaceCheckoutDurability(workspacePath) {
|
|
563
|
+
function recoverWorkspaceCheckoutDurability(workspacePath, preserveResolutionInProgress = false) {
|
|
415
564
|
workspacePath = import_node_path.default.resolve(workspacePath);
|
|
416
565
|
const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
|
|
417
566
|
const observedHead = revParse(workspacePath, "HEAD");
|
|
418
567
|
if (record?.state === "durable" && record.head === observedHead) return;
|
|
568
|
+
if (preserveResolutionInProgress && workspaceResolutionInProgress(workspacePath)) {
|
|
569
|
+
throw new Error("Workspace resolution is in progress; finish or abort it before synchronizing");
|
|
570
|
+
}
|
|
419
571
|
let targetHead = record?.state === "transition" ? record.head : observedHead;
|
|
420
572
|
if (record?.state === "transition" && observedHead && observedHead !== record.head) {
|
|
421
573
|
const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
422
|
-
if (hydrationReceipt?.head === observedHead) {
|
|
574
|
+
if (hydrationReceipt?.legacyHead === observedHead || hydrationReceipt?.mounts.some((mount) => mount.head === observedHead)) {
|
|
423
575
|
targetHead = observedHead;
|
|
424
576
|
}
|
|
425
577
|
}
|
|
@@ -501,7 +653,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
501
653
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
502
654
|
}
|
|
503
655
|
const candidate = parsed;
|
|
504
|
-
if (candidate.version !== 2 || !Array.isArray(candidate.mounts)) {
|
|
656
|
+
if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
|
|
505
657
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
506
658
|
}
|
|
507
659
|
const parseReceipt = (rawReceipt) => {
|
|
@@ -533,7 +685,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
533
685
|
return mount;
|
|
534
686
|
});
|
|
535
687
|
return {
|
|
536
|
-
version:
|
|
688
|
+
version: 3,
|
|
537
689
|
targetReceipt,
|
|
538
690
|
receiptBefore,
|
|
539
691
|
mounts
|
|
@@ -626,7 +778,7 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
626
778
|
const snapshotRoot = import_node_path.default.join(stagingPath, "mounts");
|
|
627
779
|
import_node_fs.default.mkdirSync(snapshotRoot, { mode: 448 });
|
|
628
780
|
const manifest = {
|
|
629
|
-
version:
|
|
781
|
+
version: 3,
|
|
630
782
|
targetReceipt,
|
|
631
783
|
receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
|
|
632
784
|
mounts: mounts.map((mount, index) => {
|
|
@@ -805,7 +957,11 @@ function configureWorkspaceRepository(input) {
|
|
|
805
957
|
if (!name || !email) throw new Error("Workspace Git identity must include name and email");
|
|
806
958
|
git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
807
959
|
git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
808
|
-
git(
|
|
960
|
+
git(
|
|
961
|
+
input.workspacePath,
|
|
962
|
+
["config", "--local", "core.fsync", "committed,reference"],
|
|
963
|
+
"configure durable workspace commits and references"
|
|
964
|
+
);
|
|
809
965
|
git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
|
|
810
966
|
}
|
|
811
967
|
function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
@@ -869,7 +1025,7 @@ function ensureWorkspaceGitClone(input) {
|
|
|
869
1025
|
}
|
|
870
1026
|
}
|
|
871
1027
|
configureWorkspaceRepository({ ...input, workspacePath });
|
|
872
|
-
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
1028
|
+
recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
|
|
873
1029
|
const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
874
1030
|
const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
|
|
875
1031
|
if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
@@ -1019,31 +1175,40 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1019
1175
|
}
|
|
1020
1176
|
function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
1021
1177
|
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
1022
|
-
const
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1178
|
+
const durabilityMounts = input.durabilityMounts ?? input.mounts;
|
|
1179
|
+
const receiptMounts = input.receiptMounts ?? durabilityMounts;
|
|
1180
|
+
const requiredMounts = input.requiredMounts ?? input.mounts;
|
|
1181
|
+
const candidateMounts = [
|
|
1182
|
+
...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
|
|
1183
|
+
];
|
|
1184
|
+
const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1185
|
+
const requestedIds = new Set(input.mounts.map(({ id }) => id));
|
|
1186
|
+
const requiredIds = new Set(requiredMounts.map(({ id }) => id));
|
|
1187
|
+
const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1188
|
+
const advancedDurabilityMounts = durabilityMounts.filter(
|
|
1189
|
+
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1190
|
+
);
|
|
1191
|
+
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
1031
1192
|
if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
|
|
1032
1193
|
throw new Error("Workspace checkout is not durably materialized at its current HEAD");
|
|
1033
1194
|
}
|
|
1034
1195
|
const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
|
|
1035
|
-
const targetReceipt = targetHead ?
|
|
1036
|
-
const manifest = beginHydrationTransaction(workspacePath,
|
|
1196
|
+
const targetReceipt = targetHead ? receiptWithMountsAtHead(readHydratedWorkspaceReceipt(workspacePath), targetHead, advancedDurabilityMounts, receiptMounts) : null;
|
|
1197
|
+
const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
1037
1198
|
try {
|
|
1038
|
-
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath,
|
|
1039
|
-
|
|
1199
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
|
|
1200
|
+
const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
|
|
1201
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
1040
1202
|
throw new Error("Workspace hydration did not include every required mount");
|
|
1041
1203
|
}
|
|
1042
|
-
fsyncHydratedWorkspaceMounts(
|
|
1204
|
+
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
|
|
1043
1205
|
markHydrationTransactionDurable(workspacePath);
|
|
1044
1206
|
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1045
1207
|
removeHydrationTransaction(workspacePath);
|
|
1046
|
-
return
|
|
1208
|
+
return {
|
|
1209
|
+
hydratedMountIds: hydration.hydratedMountIds,
|
|
1210
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort()
|
|
1211
|
+
};
|
|
1047
1212
|
} catch (error) {
|
|
1048
1213
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1049
1214
|
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
@@ -1076,44 +1241,44 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
|
1076
1241
|
function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
1077
1242
|
workspacePath = import_node_path.default.resolve(workspacePath);
|
|
1078
1243
|
validateMounts(workspacePath, mounts);
|
|
1079
|
-
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
1244
|
+
recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
|
|
1080
1245
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1081
1246
|
const currentHead = revParse(workspacePath, "HEAD");
|
|
1082
|
-
if (!currentHead) return;
|
|
1083
1247
|
const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1248
|
+
repairWorkspaceBasisRefs(workspacePath, currentReceipt);
|
|
1249
|
+
if (!currentHead) return;
|
|
1084
1250
|
const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
|
|
1085
1251
|
const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
|
|
1086
1252
|
const uncoveredBasisMounts = configuredBasisMounts.filter(
|
|
1087
1253
|
(mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
|
|
1088
1254
|
);
|
|
1089
1255
|
if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
|
|
1090
|
-
const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
(mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
|
|
1256
|
+
const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1257
|
+
const recoveryMounts = uncoveredBasisMounts.filter(
|
|
1258
|
+
(mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
|
|
1259
|
+
);
|
|
1260
|
+
const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
|
|
1261
|
+
const hydrationTargets = idleBasislessMounts.filter(
|
|
1262
|
+
(mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
|
|
1098
1263
|
);
|
|
1099
1264
|
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1100
1265
|
workspacePath,
|
|
1101
1266
|
mounts: hydrationTargets,
|
|
1102
|
-
durabilityMounts:
|
|
1267
|
+
durabilityMounts: idleBasislessMounts,
|
|
1103
1268
|
receiptMounts: configuredBasisMounts,
|
|
1104
1269
|
requiredMounts: hydrationTargets,
|
|
1105
1270
|
ignoreBusy: options.ignoreBusy,
|
|
1106
1271
|
recordCurrentHead: true
|
|
1107
1272
|
});
|
|
1108
|
-
|
|
1109
|
-
throw new Error(
|
|
1110
|
-
`Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1273
|
+
void hydration;
|
|
1113
1274
|
}
|
|
1114
1275
|
function resetWorkspaceGit(input) {
|
|
1115
1276
|
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
1116
1277
|
validateMounts(workspacePath, input.mounts);
|
|
1278
|
+
const busyResetMountIds = input.mounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1279
|
+
if (busyResetMountIds.length > 0) {
|
|
1280
|
+
throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
|
|
1281
|
+
}
|
|
1117
1282
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1118
1283
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1119
1284
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
@@ -1300,69 +1465,128 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1300
1465
|
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
1301
1466
|
const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
|
|
1302
1467
|
validateMounts(workspacePath, input.mounts);
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
const cloneExists = import_node_fs.default.existsSync(import_node_path.default.join(workspacePath, ".git"));
|
|
1306
|
-
const localHead = cloneExists ? revParse(workspacePath, "HEAD") : null;
|
|
1307
|
-
const remoteHead2 = cloneExists ? revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`) : null;
|
|
1308
|
-
return {
|
|
1309
|
-
outcome: "no_change",
|
|
1310
|
-
startingHead: localHead,
|
|
1311
|
-
localHead,
|
|
1312
|
-
remoteHead: remoteHead2,
|
|
1313
|
-
publishedHead: remoteHead2,
|
|
1314
|
-
rebaseCount: 0,
|
|
1315
|
-
diffSizeBytes: 0,
|
|
1316
|
-
affectedPaths: [],
|
|
1317
|
-
activeMountIds: [],
|
|
1318
|
-
skippedMountIds: input.mounts.map(({ id }) => id).sort()
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
1321
|
-
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1468
|
+
const preserveResolutionInProgress = input.skipMountMirror === true;
|
|
1469
|
+
const initial = ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
|
|
1322
1470
|
const startingHead = initial.localHead;
|
|
1323
1471
|
const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1324
1472
|
const deferredMountIds = /* @__PURE__ */ new Set();
|
|
1325
|
-
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead
|
|
1473
|
+
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
|
|
1326
1474
|
for (const mount of input.mounts) {
|
|
1327
|
-
if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true &&
|
|
1475
|
+
if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && workspaceHydrationReceiptMountBasisHead(receiptBeforeInitialHydration, mount) === null) {
|
|
1328
1476
|
deferredMountIds.add(mount.id);
|
|
1329
1477
|
}
|
|
1330
1478
|
}
|
|
1331
1479
|
}
|
|
1332
1480
|
recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1333
|
-
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
|
|
1481
|
+
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
|
|
1482
|
+
preserveResolutionInProgress,
|
|
1483
|
+
preserveStaleBases: true
|
|
1334
1484
|
});
|
|
1335
|
-
const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => import_node_fs.default.existsSync(sourcePath));
|
|
1336
1485
|
const selected = activeMounts(input.mounts);
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1486
|
+
const projectionMutationTokens = new Map(
|
|
1487
|
+
input.mounts.flatMap((mount) => mount.mutationToken ? [[mount.id, mount.mutationToken()]] : [])
|
|
1488
|
+
);
|
|
1489
|
+
const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
|
|
1490
|
+
const receiptRequiredLiveMounts = selected.active;
|
|
1339
1491
|
const projectionBasisHead = revParse(workspacePath, "HEAD");
|
|
1340
1492
|
const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
);
|
|
1493
|
+
const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
|
|
1494
|
+
const fastProjectionMounts = [];
|
|
1495
|
+
const mergedProjectionTrees = /* @__PURE__ */ new Map();
|
|
1496
|
+
const mergedProjectionMounts = [];
|
|
1497
|
+
const projectionSkippedIds = /* @__PURE__ */ new Set();
|
|
1498
|
+
let projectionWarning;
|
|
1499
|
+
const refreshCycleSkippedMounts = (mounts = input.mounts) => {
|
|
1500
|
+
for (const mount of mounts) {
|
|
1501
|
+
const projectionToken = projectionMutationTokens.get(mount.id);
|
|
1502
|
+
if (projectionToken !== void 0 && mount.mutationToken?.() !== projectionToken) cycleSkippedMountIds.add(mount.id);
|
|
1503
|
+
if (mount.busy?.()) cycleSkippedMountIds.add(mount.id);
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
|
|
1507
|
+
refreshCycleSkippedMounts();
|
|
1508
|
+
const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
|
|
1509
|
+
return {
|
|
1510
|
+
activeMountIds: [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort(),
|
|
1511
|
+
skippedMountIds: [...skippedIds].sort()
|
|
1512
|
+
};
|
|
1513
|
+
};
|
|
1514
|
+
if (!input.skipMountMirror) {
|
|
1515
|
+
const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
|
|
1516
|
+
const mergeSupport = mergeProjectionEnabled ? (0, import_workspace_merge_projection.workspaceMergeProjectionSupport)() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
|
|
1517
|
+
for (const mount of [...selected.active, ...selected.tombstones]) {
|
|
1518
|
+
if (deferredMountIds.has(mount.id)) continue;
|
|
1519
|
+
const basisHead = workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount);
|
|
1520
|
+
if (projectionBasisHead === null || basisHead === projectionBasisHead) {
|
|
1521
|
+
fastProjectionMounts.push(mount);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
if (basisHead === null) {
|
|
1525
|
+
deferredMountIds.add(mount.id);
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
if (!mergeSupport.supported) {
|
|
1529
|
+
projectionSkippedIds.add(mount.id);
|
|
1530
|
+
projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
const merged = (0, import_workspace_merge_projection.mergeWorkspaceProjectionMount)({
|
|
1534
|
+
workspacePath,
|
|
1535
|
+
mount: {
|
|
1536
|
+
id: mount.id,
|
|
1537
|
+
sourcePath: mount.sourcePath,
|
|
1538
|
+
workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
|
|
1539
|
+
sourceMode: mount.sourceMode
|
|
1540
|
+
},
|
|
1541
|
+
basisHead,
|
|
1542
|
+
currentHead: projectionBasisHead,
|
|
1543
|
+
attemptId
|
|
1544
|
+
});
|
|
1545
|
+
if (merged.kind === "conflict") {
|
|
1546
|
+
const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
|
|
1547
|
+
return {
|
|
1548
|
+
outcome: "conflict_blocked",
|
|
1549
|
+
startingHead,
|
|
1550
|
+
localHead: merged.oursCommit,
|
|
1551
|
+
remoteHead: initial.remoteHead,
|
|
1552
|
+
publishedHead: initial.remoteHead,
|
|
1553
|
+
rebaseCount: 0,
|
|
1554
|
+
diffSizeBytes: 0,
|
|
1555
|
+
affectedPaths: merged.conflictPaths,
|
|
1556
|
+
activeMountIds: [],
|
|
1557
|
+
skippedMountIds: input.mounts.map(({ id }) => id).sort(),
|
|
1558
|
+
conflictPaths: merged.conflictPaths,
|
|
1559
|
+
conflictSnapshotRefs: refs,
|
|
1560
|
+
conflictKind: "projection_merge",
|
|
1561
|
+
error: merged.error
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
mergedProjectionTrees.set(mount.id, merged.resultTree);
|
|
1565
|
+
mergedProjectionMounts.push(mount);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
|
|
1569
|
+
`);
|
|
1344
1570
|
const completedMountSelection = (requireCurrentHeadReceipt = false) => {
|
|
1345
1571
|
const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
|
|
1346
1572
|
const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
|
|
1347
|
-
if (currentHeadBeforeHydration && workspaceHydrationReceiptMatchesMounts(
|
|
1348
|
-
readHydratedWorkspaceReceipt(workspacePath),
|
|
1349
|
-
currentHeadBeforeHydration,
|
|
1350
|
-
completionReceiptMounts
|
|
1351
|
-
)) {
|
|
1352
|
-
const lateBusyIds = selected.active.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1353
|
-
const lateBusy = new Set(lateBusyIds);
|
|
1354
|
-
return {
|
|
1355
|
-
activeMountIds: [...selected.active.filter(({ id }) => !lateBusy.has(id)), ...selected.tombstones].map(({ id }) => id).sort(),
|
|
1356
|
-
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...lateBusyIds])].sort()
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
1573
|
const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1574
|
+
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
1360
1575
|
const mountIsAlreadyCovered = (mount) => Boolean(
|
|
1361
1576
|
currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
|
|
1362
1577
|
);
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1578
|
+
if (currentHeadBeforeHydration && completionReceiptMounts.every((mount) => cycleSkippedMountIds.has(mount.id) || mountIsAlreadyCovered(mount))) {
|
|
1579
|
+
return classifyMounts([...selected.active, ...selected.tombstones]);
|
|
1580
|
+
}
|
|
1581
|
+
const uncoveredCompletionMounts = selected.active.filter(
|
|
1582
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1583
|
+
);
|
|
1584
|
+
const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter(
|
|
1585
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1586
|
+
);
|
|
1587
|
+
const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
|
|
1588
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1589
|
+
);
|
|
1366
1590
|
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1367
1591
|
workspacePath,
|
|
1368
1592
|
mounts: uncoveredCompletionMounts,
|
|
@@ -1371,22 +1595,32 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1371
1595
|
requiredMounts: uncoveredRequiredLiveMounts,
|
|
1372
1596
|
recordCurrentHead: true
|
|
1373
1597
|
});
|
|
1598
|
+
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
1599
|
+
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
1374
1600
|
if (requireCurrentHeadReceipt) {
|
|
1375
1601
|
const currentHead = revParse(workspacePath, "HEAD");
|
|
1376
|
-
|
|
1602
|
+
const receipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1603
|
+
const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
|
|
1604
|
+
const idleMounts = completionReceiptMounts.filter(
|
|
1605
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mount.busy?.() && !projectionSkippedIds.has(mount.id)
|
|
1606
|
+
);
|
|
1607
|
+
if (currentHead && !idleMounts.every((mount) => workspaceHydrationReceiptCoversMount(receipt, currentHead, mount))) {
|
|
1377
1608
|
throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
|
|
1378
1609
|
}
|
|
1379
1610
|
}
|
|
1380
|
-
return
|
|
1381
|
-
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
|
|
1382
|
-
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
|
|
1383
|
-
};
|
|
1611
|
+
return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
|
|
1384
1612
|
};
|
|
1385
1613
|
let newlyStagedPaths = [];
|
|
1386
1614
|
const stageAndCommitWorkspace = () => {
|
|
1615
|
+
if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
|
|
1616
|
+
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
1617
|
+
}
|
|
1387
1618
|
git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
1388
1619
|
newlyStagedPaths = stagedPaths(workspacePath);
|
|
1389
1620
|
if (newlyStagedPaths.length > 0) {
|
|
1621
|
+
if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
|
|
1622
|
+
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
1623
|
+
}
|
|
1390
1624
|
git(
|
|
1391
1625
|
workspacePath,
|
|
1392
1626
|
[
|
|
@@ -1401,11 +1635,22 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1401
1635
|
if (!input.skipMountMirror) {
|
|
1402
1636
|
beginWorkspaceCheckoutTransition(workspacePath);
|
|
1403
1637
|
try {
|
|
1404
|
-
|
|
1405
|
-
|
|
1638
|
+
const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
|
|
1639
|
+
const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
|
|
1640
|
+
const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
|
|
1641
|
+
mirrorMountsToWorkspace(workspacePath, fastActiveMounts);
|
|
1642
|
+
removeWorkspaceMounts(workspacePath, fastTombstones);
|
|
1643
|
+
for (const mount of mergedProjectionMounts) {
|
|
1644
|
+
(0, import_workspace_merge_projection.materializeWorkspaceProjectionTree)({
|
|
1645
|
+
workspacePath,
|
|
1646
|
+
workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
|
|
1647
|
+
resultTree: mergedProjectionTrees.get(mount.id)
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
const restoreFromHeadIds = /* @__PURE__ */ new Set([...deferredMountIds, ...selected.skipped.map(({ id }) => id)]);
|
|
1406
1651
|
restoreDeferredWorkspaceMountsFromHead(
|
|
1407
1652
|
workspacePath,
|
|
1408
|
-
input.mounts.filter(({ id }) =>
|
|
1653
|
+
input.mounts.filter(({ id }) => restoreFromHeadIds.has(id))
|
|
1409
1654
|
);
|
|
1410
1655
|
stageAndCommitWorkspace();
|
|
1411
1656
|
fsyncWorkspaceCheckoutTree(workspacePath);
|
|
@@ -1419,9 +1664,15 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1419
1664
|
if (!input.skipMountMirror) {
|
|
1420
1665
|
const projectedHead = revParse(workspacePath, "HEAD");
|
|
1421
1666
|
if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
|
|
1422
|
-
const targetProjectionReceipt =
|
|
1423
|
-
|
|
1424
|
-
|
|
1667
|
+
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
1668
|
+
projectionBasisReceipt,
|
|
1669
|
+
projectedHead,
|
|
1670
|
+
fastProjectionMounts,
|
|
1671
|
+
allProjectionReceiptMounts
|
|
1672
|
+
);
|
|
1673
|
+
const currentProjectionReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1674
|
+
if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !workspaceBasisRefsAreCurrent(workspacePath, targetProjectionReceipt)) {
|
|
1675
|
+
fsyncHydratedWorkspaceMounts(fastProjectionMounts);
|
|
1425
1676
|
updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
|
|
1426
1677
|
}
|
|
1427
1678
|
completeWorkspaceCheckoutTransition(workspacePath);
|
|
@@ -1433,6 +1684,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1433
1684
|
const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
|
|
1434
1685
|
if (reconciled.kind === "conflict") {
|
|
1435
1686
|
const localHead2 = revParse(workspacePath, "HEAD");
|
|
1687
|
+
const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
|
|
1436
1688
|
return {
|
|
1437
1689
|
outcome: "conflict_blocked",
|
|
1438
1690
|
startingHead,
|
|
@@ -1442,10 +1694,10 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1442
1694
|
rebaseCount: rebaseCount + 1,
|
|
1443
1695
|
diffSizeBytes: 0,
|
|
1444
1696
|
affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
|
|
1445
|
-
|
|
1446
|
-
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
|
|
1697
|
+
...classifiedMounts,
|
|
1447
1698
|
conflictPaths: reconciled.conflictPaths,
|
|
1448
1699
|
conflictSnapshotRefs: reconciled.refs,
|
|
1700
|
+
conflictKind: "integration_rebase",
|
|
1449
1701
|
error: reconciled.error
|
|
1450
1702
|
};
|
|
1451
1703
|
}
|
|
@@ -1454,6 +1706,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1454
1706
|
if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
|
|
1455
1707
|
const localHead = revParse(workspacePath, "HEAD");
|
|
1456
1708
|
if (!localHead) {
|
|
1709
|
+
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
|
|
1457
1710
|
return {
|
|
1458
1711
|
outcome: "no_change",
|
|
1459
1712
|
startingHead,
|
|
@@ -1463,13 +1716,14 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1463
1716
|
rebaseCount,
|
|
1464
1717
|
diffSizeBytes: 0,
|
|
1465
1718
|
affectedPaths: [],
|
|
1466
|
-
|
|
1467
|
-
|
|
1719
|
+
...classifiedMounts,
|
|
1720
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1468
1721
|
};
|
|
1469
1722
|
}
|
|
1470
1723
|
const paths = changedPaths(workspacePath, remoteHead, localHead);
|
|
1471
1724
|
const size = paths.length > 0 ? await diffSizeBytes(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
|
|
1472
1725
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
1726
|
+
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
|
|
1473
1727
|
return {
|
|
1474
1728
|
outcome: "large_diff_blocked",
|
|
1475
1729
|
startingHead,
|
|
@@ -1479,8 +1733,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1479
1733
|
rebaseCount,
|
|
1480
1734
|
diffSizeBytes: size,
|
|
1481
1735
|
affectedPaths: paths,
|
|
1482
|
-
|
|
1483
|
-
skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
|
|
1736
|
+
...classifiedMounts,
|
|
1484
1737
|
error: `Workspace diff exceeds the ${maxDiffBytes}-byte automatic publication limit`
|
|
1485
1738
|
};
|
|
1486
1739
|
}
|
|
@@ -1499,7 +1752,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1499
1752
|
rebaseCount,
|
|
1500
1753
|
diffSizeBytes: size,
|
|
1501
1754
|
affectedPaths: paths,
|
|
1502
|
-
...completedMounts
|
|
1755
|
+
...completedMounts,
|
|
1756
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1503
1757
|
};
|
|
1504
1758
|
}
|
|
1505
1759
|
const pushArgs = [
|
|
@@ -1526,7 +1780,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1526
1780
|
rebaseCount,
|
|
1527
1781
|
diffSizeBytes: size,
|
|
1528
1782
|
affectedPaths: paths,
|
|
1529
|
-
...completedMounts
|
|
1783
|
+
...completedMounts,
|
|
1784
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1530
1785
|
};
|
|
1531
1786
|
}
|
|
1532
1787
|
if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
|