@ricsam/r5d-worker 0.0.80 → 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/README.md +7 -1
- package/dist/cjs/main.cjs +331 -98
- 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 +329 -98
- 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
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
4
|
+
import {
|
|
5
|
+
materializeWorkspaceProjectionTree,
|
|
6
|
+
mergeWorkspaceProjectionMount,
|
|
7
|
+
workspaceMergeProjectionSupport
|
|
8
|
+
} from "./workspace-merge-projection.mjs";
|
|
4
9
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
5
10
|
import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
|
|
6
11
|
const WORKSPACE_GIT_BRANCH = "main";
|
|
@@ -95,22 +100,52 @@ function compareWorkspaceHydrationMountBasis(left, right) {
|
|
|
95
100
|
return left.id.localeCompare(right.id);
|
|
96
101
|
}
|
|
97
102
|
function workspaceHydrationReceipt(head, mounts) {
|
|
98
|
-
const bases = mounts.map(workspaceHydrationMountBasis).sort(compareWorkspaceHydrationMountBasis);
|
|
103
|
+
const bases = mounts.map((mount) => ({ ...workspaceHydrationMountBasis(mount), head })).sort(compareWorkspaceHydrationMountBasis);
|
|
99
104
|
const ids = /* @__PURE__ */ new Set();
|
|
100
105
|
for (const basis of bases) {
|
|
101
106
|
if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
|
|
102
107
|
ids.add(basis.id);
|
|
103
108
|
}
|
|
104
|
-
return { version:
|
|
109
|
+
return { version: 3, mounts: bases };
|
|
110
|
+
}
|
|
111
|
+
function receiptWithMountsAtHead(previous, head, mounts, allConfiguredMounts) {
|
|
112
|
+
const advanceIds = new Set(mounts.map(({ id }) => id));
|
|
113
|
+
const previousById = new Map(previous?.mounts.map((entry) => [entry.id, entry]) ?? []);
|
|
114
|
+
const entries = allConfiguredMounts.flatMap((mount) => {
|
|
115
|
+
const basis = workspaceHydrationMountBasis(mount);
|
|
116
|
+
if (advanceIds.has(mount.id)) return [{ ...basis, head }];
|
|
117
|
+
const carried = previousById.get(mount.id);
|
|
118
|
+
if (!carried) return [];
|
|
119
|
+
return [carried];
|
|
120
|
+
});
|
|
121
|
+
entries.sort(compareWorkspaceHydrationMountBasis);
|
|
122
|
+
const ids = /* @__PURE__ */ new Set();
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (ids.has(entry.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${entry.id}`);
|
|
125
|
+
ids.add(entry.id);
|
|
126
|
+
}
|
|
127
|
+
return { version: 3, mounts: entries };
|
|
105
128
|
}
|
|
106
129
|
function workspaceHydrationReceiptsEqual(left, right) {
|
|
107
|
-
|
|
130
|
+
if (JSON.stringify(left) !== JSON.stringify(right)) return false;
|
|
131
|
+
const effectiveLegacyHead = (receipt) => receipt?.mounts.length === 0 ? receipt.legacyHead ?? null : null;
|
|
132
|
+
return effectiveLegacyHead(left) === effectiveLegacyHead(right);
|
|
108
133
|
}
|
|
109
134
|
function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
|
|
110
|
-
if (!receipt
|
|
135
|
+
if (!receipt) return false;
|
|
111
136
|
const expected = workspaceHydrationMountBasis(mount);
|
|
112
137
|
const actual = receipt.mounts.find(({ id }) => id === expected.id);
|
|
113
|
-
|
|
138
|
+
if (!actual || actual.head !== head) return false;
|
|
139
|
+
const { head: _head, ...actualBasis } = actual;
|
|
140
|
+
return JSON.stringify(actualBasis) === JSON.stringify(expected);
|
|
141
|
+
}
|
|
142
|
+
function workspaceHydrationReceiptMountBasisHead(receipt, mount) {
|
|
143
|
+
if (!receipt) return null;
|
|
144
|
+
const expected = workspaceHydrationMountBasis(mount);
|
|
145
|
+
const actual = receipt.mounts.find(({ id }) => id === expected.id);
|
|
146
|
+
if (!actual) return null;
|
|
147
|
+
const { head, ...actualBasis } = actual;
|
|
148
|
+
return JSON.stringify(actualBasis) === JSON.stringify(expected) ? head : null;
|
|
114
149
|
}
|
|
115
150
|
function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
|
|
116
151
|
return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
|
|
@@ -131,7 +166,9 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
131
166
|
if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
|
|
132
167
|
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
133
168
|
}
|
|
134
|
-
|
|
169
|
+
const receipt2 = { version: 3, mounts: [] };
|
|
170
|
+
Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
|
|
171
|
+
return receipt2;
|
|
135
172
|
}
|
|
136
173
|
let parsed;
|
|
137
174
|
try {
|
|
@@ -143,7 +180,8 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
143
180
|
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
144
181
|
}
|
|
145
182
|
const candidate = parsed;
|
|
146
|
-
|
|
183
|
+
const liftedV2Head = candidate.version === 2 && typeof candidate.head === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head) ? candidate.head : null;
|
|
184
|
+
if (candidate.version !== 3 && liftedV2Head === null) {
|
|
147
185
|
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
148
186
|
}
|
|
149
187
|
if (!Array.isArray(candidate.mounts)) {
|
|
@@ -157,23 +195,51 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
157
195
|
const mount = rawMount;
|
|
158
196
|
const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
|
|
159
197
|
const durabilityRelative = typeof mount.sourcePath === "string" && typeof mount.durabilityRootPath === "string" ? path.relative(mount.durabilityRootPath, mount.sourcePath) : "..";
|
|
160
|
-
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" || path.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || path.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.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") {
|
|
198
|
+
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" || path.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || path.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.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))) {
|
|
161
199
|
throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
|
|
162
200
|
}
|
|
163
201
|
seenIds.add(mount.id);
|
|
164
|
-
return
|
|
202
|
+
return {
|
|
203
|
+
id: mount.id,
|
|
204
|
+
incarnationKey: mount.incarnationKey,
|
|
205
|
+
sourcePath: mount.sourcePath,
|
|
206
|
+
durabilityRootPath: mount.durabilityRootPath,
|
|
207
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
208
|
+
sourceMode: mount.sourceMode,
|
|
209
|
+
hydrateDeletionMode: mount.hydrateDeletionMode,
|
|
210
|
+
preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence,
|
|
211
|
+
preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange,
|
|
212
|
+
deleteWhenSourceMissing: mount.deleteWhenSourceMissing,
|
|
213
|
+
head: candidate.version === 3 ? mount.head : liftedV2Head
|
|
214
|
+
};
|
|
165
215
|
});
|
|
216
|
+
const sortedMounts = mounts.sort(compareWorkspaceHydrationMountBasis);
|
|
217
|
+
if (liftedV2Head) {
|
|
218
|
+
const legacyReceipt = {
|
|
219
|
+
version: 2,
|
|
220
|
+
head: liftedV2Head,
|
|
221
|
+
mounts: sortedMounts.map(({ head: _head, ...basis }) => basis)
|
|
222
|
+
};
|
|
223
|
+
if (content !== `${JSON.stringify(legacyReceipt)}
|
|
224
|
+
`) {
|
|
225
|
+
throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
166
228
|
const receipt = {
|
|
167
|
-
version:
|
|
168
|
-
|
|
169
|
-
mounts: mounts.sort(compareWorkspaceHydrationMountBasis)
|
|
229
|
+
version: 3,
|
|
230
|
+
mounts: sortedMounts
|
|
170
231
|
};
|
|
171
|
-
if (
|
|
232
|
+
if (liftedV2Head) Object.defineProperty(receipt, "legacyHead", { value: liftedV2Head, enumerable: false });
|
|
233
|
+
if (candidate.version === 3 && content !== `${JSON.stringify(receipt)}
|
|
172
234
|
`) {
|
|
173
235
|
throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
|
|
174
236
|
}
|
|
175
|
-
|
|
176
|
-
|
|
237
|
+
const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
|
|
238
|
+
if (liftedV2Head) receiptHeads.add(liftedV2Head);
|
|
239
|
+
for (const head of receiptHeads) {
|
|
240
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`])) {
|
|
241
|
+
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
242
|
+
}
|
|
177
243
|
}
|
|
178
244
|
return receipt;
|
|
179
245
|
}
|
|
@@ -187,22 +253,92 @@ function readHydratedWorkspaceReceipt(workspacePath) {
|
|
|
187
253
|
const content = fs.readFileSync(receiptPath, "utf8");
|
|
188
254
|
return parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
|
|
189
255
|
}
|
|
256
|
+
function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
|
|
257
|
+
const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
|
|
258
|
+
const status = lstatIfExists(receiptPath);
|
|
259
|
+
if (!status?.isFile() || status.isSymbolicLink()) return false;
|
|
260
|
+
try {
|
|
261
|
+
return JSON.parse(fs.readFileSync(receiptPath, "utf8")).version === 3;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
190
266
|
function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
|
|
191
267
|
const resolvedWorkspacePath = path.resolve(workspacePath);
|
|
192
268
|
if (!fs.existsSync(path.join(resolvedWorkspacePath, ".git"))) return true;
|
|
193
269
|
if (mounts) validateMounts(resolvedWorkspacePath, mounts);
|
|
194
270
|
const head = revParse(resolvedWorkspacePath, "HEAD");
|
|
195
271
|
if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
|
|
196
|
-
if (head === null) return true;
|
|
197
272
|
const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
|
|
198
|
-
|
|
273
|
+
if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
|
|
274
|
+
if (head === null) return true;
|
|
275
|
+
if (!receipt) return false;
|
|
276
|
+
return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
|
|
277
|
+
(mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean(mount.busy?.() && workspaceHydrationReceiptMountBasisHead(receipt, mount))
|
|
278
|
+
) : Boolean(receipt && receipt.mounts.every((mount) => mount.head === head));
|
|
279
|
+
}
|
|
280
|
+
const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
|
|
281
|
+
const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
|
|
282
|
+
function workspaceBasisRefName(mountId) {
|
|
283
|
+
const sanitized = Buffer.from(mountId, "utf8").toString("base64url");
|
|
284
|
+
if (!sanitized) throw new Error(`Cannot derive a workspace basis ref for mount id: ${mountId}`);
|
|
285
|
+
return `${WORKSPACE_GIT_BASIS_REF_PREFIX}${sanitized}`;
|
|
286
|
+
}
|
|
287
|
+
function listWorkspaceRefs(workspacePath, prefix) {
|
|
288
|
+
return git(workspacePath, ["for-each-ref", "--format=%(refname)", prefix], `list ${prefix} refs`).split("\n").filter(Boolean).sort();
|
|
289
|
+
}
|
|
290
|
+
function desiredWorkspaceBasisRefs(receipt) {
|
|
291
|
+
const desired = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const mount of receipt.mounts) {
|
|
293
|
+
const ref = workspaceBasisRefName(mount.id);
|
|
294
|
+
if (desired.has(ref)) throw new Error(`Workspace hydration mount ids collide at basis ref ${ref}`);
|
|
295
|
+
desired.set(ref, mount.head);
|
|
296
|
+
}
|
|
297
|
+
return desired;
|
|
298
|
+
}
|
|
299
|
+
function workspaceBasisRefsMatch(workspacePath, receipt) {
|
|
300
|
+
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
301
|
+
const actualRefs = listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX);
|
|
302
|
+
if (actualRefs.length !== desired.size) return false;
|
|
303
|
+
return actualRefs.every((ref) => desired.get(ref) === revParse(workspacePath, ref));
|
|
304
|
+
}
|
|
305
|
+
function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
|
|
306
|
+
return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
|
|
307
|
+
}
|
|
308
|
+
function repairWorkspaceBasisRefs(workspacePath, receipt) {
|
|
309
|
+
const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
|
|
310
|
+
if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
|
|
311
|
+
}
|
|
312
|
+
function updateWorkspaceBasisRefs(workspacePath, receipt) {
|
|
313
|
+
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
314
|
+
const transaction = ["start"];
|
|
315
|
+
for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
|
|
316
|
+
for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
|
|
317
|
+
if (!desired.has(ref)) transaction.push(`delete ${ref}`);
|
|
318
|
+
}
|
|
319
|
+
transaction.push("prepare", "commit", "");
|
|
320
|
+
const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
|
|
321
|
+
cwd: workspacePath,
|
|
322
|
+
stdin: Buffer.from(transaction.join("\n")),
|
|
323
|
+
stdout: "pipe",
|
|
324
|
+
stderr: "pipe",
|
|
325
|
+
env: workerGitProcessEnvironment()
|
|
326
|
+
});
|
|
327
|
+
if (updated.exitCode !== 0) {
|
|
328
|
+
throw new Error(`Synchronize workspace hydration basis refs: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
|
|
329
|
+
}
|
|
330
|
+
for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) {
|
|
331
|
+
git(workspacePath, ["update-ref", "-d", ref], "remove staged workspace hydration basis ref");
|
|
332
|
+
}
|
|
199
333
|
}
|
|
200
334
|
function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
335
|
+
for (const mount of receipt.mounts) {
|
|
336
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
|
|
337
|
+
throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
|
|
338
|
+
}
|
|
339
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${mount.head}^{commit}`])) {
|
|
340
|
+
throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${mount.head}`);
|
|
341
|
+
}
|
|
206
342
|
}
|
|
207
343
|
readHydratedWorkspaceReceipt(workspacePath);
|
|
208
344
|
const gitDirectory = path.join(workspacePath, ".git");
|
|
@@ -222,6 +358,12 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
|
222
358
|
throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
|
|
223
359
|
}
|
|
224
360
|
const temporaryPath = path.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
361
|
+
const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
|
|
362
|
+
desiredWorkspaceBasisRefs(receipt);
|
|
363
|
+
for (const mount of receipt.mounts) {
|
|
364
|
+
const stagedRef = `${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)}`;
|
|
365
|
+
git(workspacePath, ["update-ref", stagedRef, mount.head], "stage workspace hydration basis ref");
|
|
366
|
+
}
|
|
225
367
|
let descriptor;
|
|
226
368
|
try {
|
|
227
369
|
descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
|
|
@@ -232,6 +374,7 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
|
232
374
|
descriptor = void 0;
|
|
233
375
|
fs.renameSync(temporaryPath, receiptPath);
|
|
234
376
|
fsyncDirectory(receiptDirectory);
|
|
377
|
+
updateWorkspaceBasisRefs(workspacePath, receipt);
|
|
235
378
|
} catch (error) {
|
|
236
379
|
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
237
380
|
fs.rmSync(temporaryPath, { force: true });
|
|
@@ -359,6 +502,16 @@ function workspaceRebaseInProgress(workspacePath) {
|
|
|
359
502
|
const gitDirectory = path.join(workspacePath, ".git");
|
|
360
503
|
return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
|
|
361
504
|
}
|
|
505
|
+
function workspaceResolutionInProgress(workspacePath) {
|
|
506
|
+
return workspaceRebaseInProgress(workspacePath) || fs.existsSync(path.join(workspacePath, ".git", "MERGE_HEAD"));
|
|
507
|
+
}
|
|
508
|
+
function workspaceHasUnmergedEntries(workspacePath) {
|
|
509
|
+
const result = gitResult(workspacePath, ["ls-files", "-u", "-z"]);
|
|
510
|
+
if (result.exitCode !== 0) {
|
|
511
|
+
throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
512
|
+
}
|
|
513
|
+
return result.stdout.length > 0;
|
|
514
|
+
}
|
|
362
515
|
function clearWorkspaceCheckoutTree(workspacePath) {
|
|
363
516
|
for (const name of fs.readdirSync(workspacePath)) {
|
|
364
517
|
if (name === ".git") continue;
|
|
@@ -366,15 +519,18 @@ function clearWorkspaceCheckoutTree(workspacePath) {
|
|
|
366
519
|
}
|
|
367
520
|
fsyncDirectory(workspacePath);
|
|
368
521
|
}
|
|
369
|
-
function recoverWorkspaceCheckoutDurability(workspacePath) {
|
|
522
|
+
function recoverWorkspaceCheckoutDurability(workspacePath, preserveResolutionInProgress = false) {
|
|
370
523
|
workspacePath = path.resolve(workspacePath);
|
|
371
524
|
const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
|
|
372
525
|
const observedHead = revParse(workspacePath, "HEAD");
|
|
373
526
|
if (record?.state === "durable" && record.head === observedHead) return;
|
|
527
|
+
if (preserveResolutionInProgress && workspaceResolutionInProgress(workspacePath)) {
|
|
528
|
+
throw new Error("Workspace resolution is in progress; finish or abort it before synchronizing");
|
|
529
|
+
}
|
|
374
530
|
let targetHead = record?.state === "transition" ? record.head : observedHead;
|
|
375
531
|
if (record?.state === "transition" && observedHead && observedHead !== record.head) {
|
|
376
532
|
const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
377
|
-
if (hydrationReceipt?.head === observedHead) {
|
|
533
|
+
if (hydrationReceipt?.legacyHead === observedHead || hydrationReceipt?.mounts.some((mount) => mount.head === observedHead)) {
|
|
378
534
|
targetHead = observedHead;
|
|
379
535
|
}
|
|
380
536
|
}
|
|
@@ -456,7 +612,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
456
612
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
457
613
|
}
|
|
458
614
|
const candidate = parsed;
|
|
459
|
-
if (candidate.version !== 2 || !Array.isArray(candidate.mounts)) {
|
|
615
|
+
if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
|
|
460
616
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
461
617
|
}
|
|
462
618
|
const parseReceipt = (rawReceipt) => {
|
|
@@ -488,7 +644,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
488
644
|
return mount;
|
|
489
645
|
});
|
|
490
646
|
return {
|
|
491
|
-
version:
|
|
647
|
+
version: 3,
|
|
492
648
|
targetReceipt,
|
|
493
649
|
receiptBefore,
|
|
494
650
|
mounts
|
|
@@ -581,7 +737,7 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
581
737
|
const snapshotRoot = path.join(stagingPath, "mounts");
|
|
582
738
|
fs.mkdirSync(snapshotRoot, { mode: 448 });
|
|
583
739
|
const manifest = {
|
|
584
|
-
version:
|
|
740
|
+
version: 3,
|
|
585
741
|
targetReceipt,
|
|
586
742
|
receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
|
|
587
743
|
mounts: mounts.map((mount, index) => {
|
|
@@ -760,7 +916,11 @@ function configureWorkspaceRepository(input) {
|
|
|
760
916
|
if (!name || !email) throw new Error("Workspace Git identity must include name and email");
|
|
761
917
|
git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
762
918
|
git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
763
|
-
git(
|
|
919
|
+
git(
|
|
920
|
+
input.workspacePath,
|
|
921
|
+
["config", "--local", "core.fsync", "committed,reference"],
|
|
922
|
+
"configure durable workspace commits and references"
|
|
923
|
+
);
|
|
764
924
|
git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
|
|
765
925
|
}
|
|
766
926
|
function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
@@ -824,7 +984,7 @@ function ensureWorkspaceGitClone(input) {
|
|
|
824
984
|
}
|
|
825
985
|
}
|
|
826
986
|
configureWorkspaceRepository({ ...input, workspacePath });
|
|
827
|
-
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
987
|
+
recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
|
|
828
988
|
const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
829
989
|
const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
|
|
830
990
|
if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
@@ -974,31 +1134,40 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
974
1134
|
}
|
|
975
1135
|
function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
976
1136
|
const workspacePath = path.resolve(input.workspacePath);
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
1137
|
+
const durabilityMounts = input.durabilityMounts ?? input.mounts;
|
|
1138
|
+
const receiptMounts = input.receiptMounts ?? durabilityMounts;
|
|
1139
|
+
const requiredMounts = input.requiredMounts ?? input.mounts;
|
|
1140
|
+
const candidateMounts = [
|
|
1141
|
+
...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
|
|
1142
|
+
];
|
|
1143
|
+
const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1144
|
+
const requestedIds = new Set(input.mounts.map(({ id }) => id));
|
|
1145
|
+
const requiredIds = new Set(requiredMounts.map(({ id }) => id));
|
|
1146
|
+
const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1147
|
+
const advancedDurabilityMounts = durabilityMounts.filter(
|
|
1148
|
+
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1149
|
+
);
|
|
1150
|
+
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
986
1151
|
if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
|
|
987
1152
|
throw new Error("Workspace checkout is not durably materialized at its current HEAD");
|
|
988
1153
|
}
|
|
989
1154
|
const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
|
|
990
|
-
const targetReceipt = targetHead ?
|
|
991
|
-
const manifest = beginHydrationTransaction(workspacePath,
|
|
1155
|
+
const targetReceipt = targetHead ? receiptWithMountsAtHead(readHydratedWorkspaceReceipt(workspacePath), targetHead, advancedDurabilityMounts, receiptMounts) : null;
|
|
1156
|
+
const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
992
1157
|
try {
|
|
993
|
-
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath,
|
|
994
|
-
|
|
1158
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
|
|
1159
|
+
const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
|
|
1160
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
995
1161
|
throw new Error("Workspace hydration did not include every required mount");
|
|
996
1162
|
}
|
|
997
|
-
fsyncHydratedWorkspaceMounts(
|
|
1163
|
+
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
|
|
998
1164
|
markHydrationTransactionDurable(workspacePath);
|
|
999
1165
|
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1000
1166
|
removeHydrationTransaction(workspacePath);
|
|
1001
|
-
return
|
|
1167
|
+
return {
|
|
1168
|
+
hydratedMountIds: hydration.hydratedMountIds,
|
|
1169
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort()
|
|
1170
|
+
};
|
|
1002
1171
|
} catch (error) {
|
|
1003
1172
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1004
1173
|
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
@@ -1031,44 +1200,44 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
|
1031
1200
|
function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
1032
1201
|
workspacePath = path.resolve(workspacePath);
|
|
1033
1202
|
validateMounts(workspacePath, mounts);
|
|
1034
|
-
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
1203
|
+
recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
|
|
1035
1204
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1036
1205
|
const currentHead = revParse(workspacePath, "HEAD");
|
|
1037
|
-
if (!currentHead) return;
|
|
1038
1206
|
const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1207
|
+
repairWorkspaceBasisRefs(workspacePath, currentReceipt);
|
|
1208
|
+
if (!currentHead) return;
|
|
1039
1209
|
const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
|
|
1040
1210
|
const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
|
|
1041
1211
|
const uncoveredBasisMounts = configuredBasisMounts.filter(
|
|
1042
1212
|
(mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
|
|
1043
1213
|
);
|
|
1044
1214
|
if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
|
|
1045
|
-
const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
(mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
|
|
1215
|
+
const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1216
|
+
const recoveryMounts = uncoveredBasisMounts.filter(
|
|
1217
|
+
(mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
|
|
1218
|
+
);
|
|
1219
|
+
const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
|
|
1220
|
+
const hydrationTargets = idleBasislessMounts.filter(
|
|
1221
|
+
(mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
|
|
1053
1222
|
);
|
|
1054
1223
|
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1055
1224
|
workspacePath,
|
|
1056
1225
|
mounts: hydrationTargets,
|
|
1057
|
-
durabilityMounts:
|
|
1226
|
+
durabilityMounts: idleBasislessMounts,
|
|
1058
1227
|
receiptMounts: configuredBasisMounts,
|
|
1059
1228
|
requiredMounts: hydrationTargets,
|
|
1060
1229
|
ignoreBusy: options.ignoreBusy,
|
|
1061
1230
|
recordCurrentHead: true
|
|
1062
1231
|
});
|
|
1063
|
-
|
|
1064
|
-
throw new Error(
|
|
1065
|
-
`Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1232
|
+
void hydration;
|
|
1068
1233
|
}
|
|
1069
1234
|
function resetWorkspaceGit(input) {
|
|
1070
1235
|
const workspacePath = path.resolve(input.workspacePath);
|
|
1071
1236
|
validateMounts(workspacePath, input.mounts);
|
|
1237
|
+
const busyResetMountIds = input.mounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1238
|
+
if (busyResetMountIds.length > 0) {
|
|
1239
|
+
throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
|
|
1240
|
+
}
|
|
1072
1241
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1073
1242
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1074
1243
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
@@ -1255,69 +1424,128 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1255
1424
|
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
1256
1425
|
const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
|
|
1257
1426
|
validateMounts(workspacePath, input.mounts);
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
const cloneExists = fs.existsSync(path.join(workspacePath, ".git"));
|
|
1261
|
-
const localHead = cloneExists ? revParse(workspacePath, "HEAD") : null;
|
|
1262
|
-
const remoteHead2 = cloneExists ? revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`) : null;
|
|
1263
|
-
return {
|
|
1264
|
-
outcome: "no_change",
|
|
1265
|
-
startingHead: localHead,
|
|
1266
|
-
localHead,
|
|
1267
|
-
remoteHead: remoteHead2,
|
|
1268
|
-
publishedHead: remoteHead2,
|
|
1269
|
-
rebaseCount: 0,
|
|
1270
|
-
diffSizeBytes: 0,
|
|
1271
|
-
affectedPaths: [],
|
|
1272
|
-
activeMountIds: [],
|
|
1273
|
-
skippedMountIds: input.mounts.map(({ id }) => id).sort()
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1427
|
+
const preserveResolutionInProgress = input.skipMountMirror === true;
|
|
1428
|
+
const initial = ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
|
|
1277
1429
|
const startingHead = initial.localHead;
|
|
1278
1430
|
const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1279
1431
|
const deferredMountIds = /* @__PURE__ */ new Set();
|
|
1280
|
-
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead
|
|
1432
|
+
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
|
|
1281
1433
|
for (const mount of input.mounts) {
|
|
1282
|
-
if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true &&
|
|
1434
|
+
if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && workspaceHydrationReceiptMountBasisHead(receiptBeforeInitialHydration, mount) === null) {
|
|
1283
1435
|
deferredMountIds.add(mount.id);
|
|
1284
1436
|
}
|
|
1285
1437
|
}
|
|
1286
1438
|
}
|
|
1287
1439
|
recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1288
|
-
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
|
|
1440
|
+
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
|
|
1441
|
+
preserveResolutionInProgress,
|
|
1442
|
+
preserveStaleBases: true
|
|
1289
1443
|
});
|
|
1290
|
-
const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
|
|
1291
1444
|
const selected = activeMounts(input.mounts);
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1445
|
+
const projectionMutationTokens = new Map(
|
|
1446
|
+
input.mounts.flatMap((mount) => mount.mutationToken ? [[mount.id, mount.mutationToken()]] : [])
|
|
1447
|
+
);
|
|
1448
|
+
const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
|
|
1449
|
+
const receiptRequiredLiveMounts = selected.active;
|
|
1294
1450
|
const projectionBasisHead = revParse(workspacePath, "HEAD");
|
|
1295
1451
|
const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
);
|
|
1452
|
+
const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
|
|
1453
|
+
const fastProjectionMounts = [];
|
|
1454
|
+
const mergedProjectionTrees = /* @__PURE__ */ new Map();
|
|
1455
|
+
const mergedProjectionMounts = [];
|
|
1456
|
+
const projectionSkippedIds = /* @__PURE__ */ new Set();
|
|
1457
|
+
let projectionWarning;
|
|
1458
|
+
const refreshCycleSkippedMounts = (mounts = input.mounts) => {
|
|
1459
|
+
for (const mount of mounts) {
|
|
1460
|
+
const projectionToken = projectionMutationTokens.get(mount.id);
|
|
1461
|
+
if (projectionToken !== void 0 && mount.mutationToken?.() !== projectionToken) cycleSkippedMountIds.add(mount.id);
|
|
1462
|
+
if (mount.busy?.()) cycleSkippedMountIds.add(mount.id);
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
|
|
1466
|
+
refreshCycleSkippedMounts();
|
|
1467
|
+
const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
|
|
1468
|
+
return {
|
|
1469
|
+
activeMountIds: [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort(),
|
|
1470
|
+
skippedMountIds: [...skippedIds].sort()
|
|
1471
|
+
};
|
|
1472
|
+
};
|
|
1473
|
+
if (!input.skipMountMirror) {
|
|
1474
|
+
const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
|
|
1475
|
+
const mergeSupport = mergeProjectionEnabled ? workspaceMergeProjectionSupport() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
|
|
1476
|
+
for (const mount of [...selected.active, ...selected.tombstones]) {
|
|
1477
|
+
if (deferredMountIds.has(mount.id)) continue;
|
|
1478
|
+
const basisHead = workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount);
|
|
1479
|
+
if (projectionBasisHead === null || basisHead === projectionBasisHead) {
|
|
1480
|
+
fastProjectionMounts.push(mount);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (basisHead === null) {
|
|
1484
|
+
deferredMountIds.add(mount.id);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (!mergeSupport.supported) {
|
|
1488
|
+
projectionSkippedIds.add(mount.id);
|
|
1489
|
+
projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
const merged = mergeWorkspaceProjectionMount({
|
|
1493
|
+
workspacePath,
|
|
1494
|
+
mount: {
|
|
1495
|
+
id: mount.id,
|
|
1496
|
+
sourcePath: mount.sourcePath,
|
|
1497
|
+
workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
|
|
1498
|
+
sourceMode: mount.sourceMode
|
|
1499
|
+
},
|
|
1500
|
+
basisHead,
|
|
1501
|
+
currentHead: projectionBasisHead,
|
|
1502
|
+
attemptId
|
|
1503
|
+
});
|
|
1504
|
+
if (merged.kind === "conflict") {
|
|
1505
|
+
const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
|
|
1506
|
+
return {
|
|
1507
|
+
outcome: "conflict_blocked",
|
|
1508
|
+
startingHead,
|
|
1509
|
+
localHead: merged.oursCommit,
|
|
1510
|
+
remoteHead: initial.remoteHead,
|
|
1511
|
+
publishedHead: initial.remoteHead,
|
|
1512
|
+
rebaseCount: 0,
|
|
1513
|
+
diffSizeBytes: 0,
|
|
1514
|
+
affectedPaths: merged.conflictPaths,
|
|
1515
|
+
activeMountIds: [],
|
|
1516
|
+
skippedMountIds: input.mounts.map(({ id }) => id).sort(),
|
|
1517
|
+
conflictPaths: merged.conflictPaths,
|
|
1518
|
+
conflictSnapshotRefs: refs,
|
|
1519
|
+
conflictKind: "projection_merge",
|
|
1520
|
+
error: merged.error
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
mergedProjectionTrees.set(mount.id, merged.resultTree);
|
|
1524
|
+
mergedProjectionMounts.push(mount);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
|
|
1528
|
+
`);
|
|
1299
1529
|
const completedMountSelection = (requireCurrentHeadReceipt = false) => {
|
|
1300
1530
|
const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
|
|
1301
1531
|
const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
|
|
1302
|
-
if (currentHeadBeforeHydration && workspaceHydrationReceiptMatchesMounts(
|
|
1303
|
-
readHydratedWorkspaceReceipt(workspacePath),
|
|
1304
|
-
currentHeadBeforeHydration,
|
|
1305
|
-
completionReceiptMounts
|
|
1306
|
-
)) {
|
|
1307
|
-
const lateBusyIds = selected.active.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1308
|
-
const lateBusy = new Set(lateBusyIds);
|
|
1309
|
-
return {
|
|
1310
|
-
activeMountIds: [...selected.active.filter(({ id }) => !lateBusy.has(id)), ...selected.tombstones].map(({ id }) => id).sort(),
|
|
1311
|
-
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...lateBusyIds])].sort()
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
1532
|
const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1533
|
+
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
1315
1534
|
const mountIsAlreadyCovered = (mount) => Boolean(
|
|
1316
1535
|
currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
|
|
1317
1536
|
);
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1537
|
+
if (currentHeadBeforeHydration && completionReceiptMounts.every((mount) => cycleSkippedMountIds.has(mount.id) || mountIsAlreadyCovered(mount))) {
|
|
1538
|
+
return classifyMounts([...selected.active, ...selected.tombstones]);
|
|
1539
|
+
}
|
|
1540
|
+
const uncoveredCompletionMounts = selected.active.filter(
|
|
1541
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1542
|
+
);
|
|
1543
|
+
const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter(
|
|
1544
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1545
|
+
);
|
|
1546
|
+
const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
|
|
1547
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1548
|
+
);
|
|
1321
1549
|
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1322
1550
|
workspacePath,
|
|
1323
1551
|
mounts: uncoveredCompletionMounts,
|
|
@@ -1326,22 +1554,32 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1326
1554
|
requiredMounts: uncoveredRequiredLiveMounts,
|
|
1327
1555
|
recordCurrentHead: true
|
|
1328
1556
|
});
|
|
1557
|
+
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
1558
|
+
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
1329
1559
|
if (requireCurrentHeadReceipt) {
|
|
1330
1560
|
const currentHead = revParse(workspacePath, "HEAD");
|
|
1331
|
-
|
|
1561
|
+
const receipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1562
|
+
const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
|
|
1563
|
+
const idleMounts = completionReceiptMounts.filter(
|
|
1564
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mount.busy?.() && !projectionSkippedIds.has(mount.id)
|
|
1565
|
+
);
|
|
1566
|
+
if (currentHead && !idleMounts.every((mount) => workspaceHydrationReceiptCoversMount(receipt, currentHead, mount))) {
|
|
1332
1567
|
throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
|
|
1333
1568
|
}
|
|
1334
1569
|
}
|
|
1335
|
-
return
|
|
1336
|
-
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
|
|
1337
|
-
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
|
|
1338
|
-
};
|
|
1570
|
+
return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
|
|
1339
1571
|
};
|
|
1340
1572
|
let newlyStagedPaths = [];
|
|
1341
1573
|
const stageAndCommitWorkspace = () => {
|
|
1574
|
+
if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
|
|
1575
|
+
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
1576
|
+
}
|
|
1342
1577
|
git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
1343
1578
|
newlyStagedPaths = stagedPaths(workspacePath);
|
|
1344
1579
|
if (newlyStagedPaths.length > 0) {
|
|
1580
|
+
if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
|
|
1581
|
+
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
1582
|
+
}
|
|
1345
1583
|
git(
|
|
1346
1584
|
workspacePath,
|
|
1347
1585
|
[
|
|
@@ -1356,11 +1594,22 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1356
1594
|
if (!input.skipMountMirror) {
|
|
1357
1595
|
beginWorkspaceCheckoutTransition(workspacePath);
|
|
1358
1596
|
try {
|
|
1359
|
-
|
|
1360
|
-
|
|
1597
|
+
const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
|
|
1598
|
+
const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
|
|
1599
|
+
const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
|
|
1600
|
+
mirrorMountsToWorkspace(workspacePath, fastActiveMounts);
|
|
1601
|
+
removeWorkspaceMounts(workspacePath, fastTombstones);
|
|
1602
|
+
for (const mount of mergedProjectionMounts) {
|
|
1603
|
+
materializeWorkspaceProjectionTree({
|
|
1604
|
+
workspacePath,
|
|
1605
|
+
workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
|
|
1606
|
+
resultTree: mergedProjectionTrees.get(mount.id)
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
const restoreFromHeadIds = /* @__PURE__ */ new Set([...deferredMountIds, ...selected.skipped.map(({ id }) => id)]);
|
|
1361
1610
|
restoreDeferredWorkspaceMountsFromHead(
|
|
1362
1611
|
workspacePath,
|
|
1363
|
-
input.mounts.filter(({ id }) =>
|
|
1612
|
+
input.mounts.filter(({ id }) => restoreFromHeadIds.has(id))
|
|
1364
1613
|
);
|
|
1365
1614
|
stageAndCommitWorkspace();
|
|
1366
1615
|
fsyncWorkspaceCheckoutTree(workspacePath);
|
|
@@ -1374,9 +1623,15 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1374
1623
|
if (!input.skipMountMirror) {
|
|
1375
1624
|
const projectedHead = revParse(workspacePath, "HEAD");
|
|
1376
1625
|
if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
|
|
1377
|
-
const targetProjectionReceipt =
|
|
1378
|
-
|
|
1379
|
-
|
|
1626
|
+
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
1627
|
+
projectionBasisReceipt,
|
|
1628
|
+
projectedHead,
|
|
1629
|
+
fastProjectionMounts,
|
|
1630
|
+
allProjectionReceiptMounts
|
|
1631
|
+
);
|
|
1632
|
+
const currentProjectionReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1633
|
+
if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !workspaceBasisRefsAreCurrent(workspacePath, targetProjectionReceipt)) {
|
|
1634
|
+
fsyncHydratedWorkspaceMounts(fastProjectionMounts);
|
|
1380
1635
|
updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
|
|
1381
1636
|
}
|
|
1382
1637
|
completeWorkspaceCheckoutTransition(workspacePath);
|
|
@@ -1388,6 +1643,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1388
1643
|
const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
|
|
1389
1644
|
if (reconciled.kind === "conflict") {
|
|
1390
1645
|
const localHead2 = revParse(workspacePath, "HEAD");
|
|
1646
|
+
const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
|
|
1391
1647
|
return {
|
|
1392
1648
|
outcome: "conflict_blocked",
|
|
1393
1649
|
startingHead,
|
|
@@ -1397,10 +1653,10 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1397
1653
|
rebaseCount: rebaseCount + 1,
|
|
1398
1654
|
diffSizeBytes: 0,
|
|
1399
1655
|
affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
|
|
1400
|
-
|
|
1401
|
-
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
|
|
1656
|
+
...classifiedMounts,
|
|
1402
1657
|
conflictPaths: reconciled.conflictPaths,
|
|
1403
1658
|
conflictSnapshotRefs: reconciled.refs,
|
|
1659
|
+
conflictKind: "integration_rebase",
|
|
1404
1660
|
error: reconciled.error
|
|
1405
1661
|
};
|
|
1406
1662
|
}
|
|
@@ -1409,6 +1665,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1409
1665
|
if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
|
|
1410
1666
|
const localHead = revParse(workspacePath, "HEAD");
|
|
1411
1667
|
if (!localHead) {
|
|
1668
|
+
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
|
|
1412
1669
|
return {
|
|
1413
1670
|
outcome: "no_change",
|
|
1414
1671
|
startingHead,
|
|
@@ -1418,13 +1675,14 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1418
1675
|
rebaseCount,
|
|
1419
1676
|
diffSizeBytes: 0,
|
|
1420
1677
|
affectedPaths: [],
|
|
1421
|
-
|
|
1422
|
-
|
|
1678
|
+
...classifiedMounts,
|
|
1679
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1423
1680
|
};
|
|
1424
1681
|
}
|
|
1425
1682
|
const paths = changedPaths(workspacePath, remoteHead, localHead);
|
|
1426
1683
|
const size = paths.length > 0 ? await diffSizeBytes(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
|
|
1427
1684
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
1685
|
+
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
|
|
1428
1686
|
return {
|
|
1429
1687
|
outcome: "large_diff_blocked",
|
|
1430
1688
|
startingHead,
|
|
@@ -1434,8 +1692,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1434
1692
|
rebaseCount,
|
|
1435
1693
|
diffSizeBytes: size,
|
|
1436
1694
|
affectedPaths: paths,
|
|
1437
|
-
|
|
1438
|
-
skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
|
|
1695
|
+
...classifiedMounts,
|
|
1439
1696
|
error: `Workspace diff exceeds the ${maxDiffBytes}-byte automatic publication limit`
|
|
1440
1697
|
};
|
|
1441
1698
|
}
|
|
@@ -1454,7 +1711,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1454
1711
|
rebaseCount,
|
|
1455
1712
|
diffSizeBytes: size,
|
|
1456
1713
|
affectedPaths: paths,
|
|
1457
|
-
...completedMounts
|
|
1714
|
+
...completedMounts,
|
|
1715
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1458
1716
|
};
|
|
1459
1717
|
}
|
|
1460
1718
|
const pushArgs = [
|
|
@@ -1481,7 +1739,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1481
1739
|
rebaseCount,
|
|
1482
1740
|
diffSizeBytes: size,
|
|
1483
1741
|
affectedPaths: paths,
|
|
1484
|
-
...completedMounts
|
|
1742
|
+
...completedMounts,
|
|
1743
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1485
1744
|
};
|
|
1486
1745
|
}
|
|
1487
1746
|
if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
|