@ricsam/r5d-worker 0.0.81 → 0.0.83
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 +743 -121
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-command-sync-policy.cjs +3 -3
- package/dist/cjs/workspace-git-sync.cjs +446 -123
- package/dist/cjs/workspace-merge-projection.cjs +392 -0
- package/dist/mjs/main.mjs +744 -122
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-command-sync-policy.mjs +3 -3
- package/dist/mjs/workspace-git-sync.mjs +448 -123
- package/dist/mjs/workspace-merge-projection.mjs +355 -0
- package/dist/types/main.d.ts +395 -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 +34 -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";
|
|
@@ -10,6 +15,12 @@ const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
|
|
|
10
15
|
const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
|
|
11
16
|
const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
|
|
12
17
|
const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
|
|
18
|
+
class WorkspaceRemediationAncestryError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "WorkspaceRemediationAncestryError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
const NON_RECURSIVE_GIT_CONFIG = [
|
|
14
25
|
"-c",
|
|
15
26
|
"submodule.recurse=false",
|
|
@@ -49,6 +60,29 @@ function revParse(workspacePath, revision) {
|
|
|
49
60
|
const result = gitResult(workspacePath, ["rev-parse", "--verify", revision]);
|
|
50
61
|
return result.exitCode === 0 ? result.stdout : null;
|
|
51
62
|
}
|
|
63
|
+
function requiredWorkspaceAncestorHeads(value) {
|
|
64
|
+
if (value === void 0) return [];
|
|
65
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((head) => typeof head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head)) || new Set(value).size !== value.length) {
|
|
66
|
+
throw new WorkspaceRemediationAncestryError("Workspace remediation ancestor requirements are invalid");
|
|
67
|
+
}
|
|
68
|
+
return [...value];
|
|
69
|
+
}
|
|
70
|
+
function assertWorkspaceRemediationAncestry(input) {
|
|
71
|
+
if (input.requiredAncestorHeads.length === 0) return;
|
|
72
|
+
if (!input.currentHead) throw new WorkspaceRemediationAncestryError("Workspace remediation has no resolved HEAD to verify");
|
|
73
|
+
for (const requiredHead of input.requiredAncestorHeads) {
|
|
74
|
+
if (!tryGit(input.workspacePath, ["cat-file", "-e", `${requiredHead}^{commit}`]) || !tryGit(input.workspacePath, ["merge-base", "--is-ancestor", requiredHead, input.currentHead])) {
|
|
75
|
+
throw new WorkspaceRemediationAncestryError(
|
|
76
|
+
`Workspace remediation HEAD does not descend from required conflict commit ${requiredHead}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!input.remoteHead || !tryGit(input.workspacePath, ["cat-file", "-e", `${input.remoteHead}^{commit}`]) || !tryGit(input.workspacePath, ["merge-base", "--is-ancestor", input.remoteHead, input.currentHead])) {
|
|
81
|
+
throw new WorkspaceRemediationAncestryError(
|
|
82
|
+
"Fetched workspace head is not an ancestor of the resolved remediation HEAD; merge it explicitly before synchronizing"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
52
86
|
function updateIntegratedWorkspaceHead(workspacePath, head) {
|
|
53
87
|
git(workspacePath, ["update-ref", WORKSPACE_GIT_INTEGRATED_REF, head], "record integrated workspace head");
|
|
54
88
|
}
|
|
@@ -95,22 +129,52 @@ function compareWorkspaceHydrationMountBasis(left, right) {
|
|
|
95
129
|
return left.id.localeCompare(right.id);
|
|
96
130
|
}
|
|
97
131
|
function workspaceHydrationReceipt(head, mounts) {
|
|
98
|
-
const bases = mounts.map(workspaceHydrationMountBasis).sort(compareWorkspaceHydrationMountBasis);
|
|
132
|
+
const bases = mounts.map((mount) => ({ ...workspaceHydrationMountBasis(mount), head })).sort(compareWorkspaceHydrationMountBasis);
|
|
99
133
|
const ids = /* @__PURE__ */ new Set();
|
|
100
134
|
for (const basis of bases) {
|
|
101
135
|
if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
|
|
102
136
|
ids.add(basis.id);
|
|
103
137
|
}
|
|
104
|
-
return { version:
|
|
138
|
+
return { version: 3, mounts: bases };
|
|
139
|
+
}
|
|
140
|
+
function receiptWithMountsAtHead(previous, head, mounts, allConfiguredMounts) {
|
|
141
|
+
const advanceIds = new Set(mounts.map(({ id }) => id));
|
|
142
|
+
const previousById = new Map(previous?.mounts.map((entry) => [entry.id, entry]) ?? []);
|
|
143
|
+
const entries = allConfiguredMounts.flatMap((mount) => {
|
|
144
|
+
const basis = workspaceHydrationMountBasis(mount);
|
|
145
|
+
if (advanceIds.has(mount.id)) return [{ ...basis, head }];
|
|
146
|
+
const carried = previousById.get(mount.id);
|
|
147
|
+
if (!carried) return [];
|
|
148
|
+
return [carried];
|
|
149
|
+
});
|
|
150
|
+
entries.sort(compareWorkspaceHydrationMountBasis);
|
|
151
|
+
const ids = /* @__PURE__ */ new Set();
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (ids.has(entry.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${entry.id}`);
|
|
154
|
+
ids.add(entry.id);
|
|
155
|
+
}
|
|
156
|
+
return { version: 3, mounts: entries };
|
|
105
157
|
}
|
|
106
158
|
function workspaceHydrationReceiptsEqual(left, right) {
|
|
107
|
-
|
|
159
|
+
if (JSON.stringify(left) !== JSON.stringify(right)) return false;
|
|
160
|
+
const effectiveLegacyHead = (receipt) => receipt?.mounts.length === 0 ? receipt.legacyHead ?? null : null;
|
|
161
|
+
return effectiveLegacyHead(left) === effectiveLegacyHead(right);
|
|
108
162
|
}
|
|
109
163
|
function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
|
|
110
|
-
if (!receipt
|
|
164
|
+
if (!receipt) return false;
|
|
165
|
+
const expected = workspaceHydrationMountBasis(mount);
|
|
166
|
+
const actual = receipt.mounts.find(({ id }) => id === expected.id);
|
|
167
|
+
if (!actual || actual.head !== head) return false;
|
|
168
|
+
const { head: _head, ...actualBasis } = actual;
|
|
169
|
+
return JSON.stringify(actualBasis) === JSON.stringify(expected);
|
|
170
|
+
}
|
|
171
|
+
function workspaceHydrationReceiptMountBasisHead(receipt, mount) {
|
|
172
|
+
if (!receipt) return null;
|
|
111
173
|
const expected = workspaceHydrationMountBasis(mount);
|
|
112
174
|
const actual = receipt.mounts.find(({ id }) => id === expected.id);
|
|
113
|
-
|
|
175
|
+
if (!actual) return null;
|
|
176
|
+
const { head, ...actualBasis } = actual;
|
|
177
|
+
return JSON.stringify(actualBasis) === JSON.stringify(expected) ? head : null;
|
|
114
178
|
}
|
|
115
179
|
function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
|
|
116
180
|
return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
|
|
@@ -131,7 +195,9 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
131
195
|
if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
|
|
132
196
|
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
133
197
|
}
|
|
134
|
-
|
|
198
|
+
const receipt2 = { version: 3, mounts: [] };
|
|
199
|
+
Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
|
|
200
|
+
return receipt2;
|
|
135
201
|
}
|
|
136
202
|
let parsed;
|
|
137
203
|
try {
|
|
@@ -143,7 +209,8 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
143
209
|
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
144
210
|
}
|
|
145
211
|
const candidate = parsed;
|
|
146
|
-
|
|
212
|
+
const liftedV2Head = candidate.version === 2 && typeof candidate.head === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head) ? candidate.head : null;
|
|
213
|
+
if (candidate.version !== 3 && liftedV2Head === null) {
|
|
147
214
|
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
148
215
|
}
|
|
149
216
|
if (!Array.isArray(candidate.mounts)) {
|
|
@@ -157,23 +224,51 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
157
224
|
const mount = rawMount;
|
|
158
225
|
const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
|
|
159
226
|
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") {
|
|
227
|
+
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
228
|
throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
|
|
162
229
|
}
|
|
163
230
|
seenIds.add(mount.id);
|
|
164
|
-
return
|
|
231
|
+
return {
|
|
232
|
+
id: mount.id,
|
|
233
|
+
incarnationKey: mount.incarnationKey,
|
|
234
|
+
sourcePath: mount.sourcePath,
|
|
235
|
+
durabilityRootPath: mount.durabilityRootPath,
|
|
236
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
237
|
+
sourceMode: mount.sourceMode,
|
|
238
|
+
hydrateDeletionMode: mount.hydrateDeletionMode,
|
|
239
|
+
preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence,
|
|
240
|
+
preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange,
|
|
241
|
+
deleteWhenSourceMissing: mount.deleteWhenSourceMissing,
|
|
242
|
+
head: candidate.version === 3 ? mount.head : liftedV2Head
|
|
243
|
+
};
|
|
165
244
|
});
|
|
245
|
+
const sortedMounts = mounts.sort(compareWorkspaceHydrationMountBasis);
|
|
246
|
+
if (liftedV2Head) {
|
|
247
|
+
const legacyReceipt = {
|
|
248
|
+
version: 2,
|
|
249
|
+
head: liftedV2Head,
|
|
250
|
+
mounts: sortedMounts.map(({ head: _head, ...basis }) => basis)
|
|
251
|
+
};
|
|
252
|
+
if (content !== `${JSON.stringify(legacyReceipt)}
|
|
253
|
+
`) {
|
|
254
|
+
throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
166
257
|
const receipt = {
|
|
167
|
-
version:
|
|
168
|
-
|
|
169
|
-
mounts: mounts.sort(compareWorkspaceHydrationMountBasis)
|
|
258
|
+
version: 3,
|
|
259
|
+
mounts: sortedMounts
|
|
170
260
|
};
|
|
171
|
-
if (
|
|
261
|
+
if (liftedV2Head) Object.defineProperty(receipt, "legacyHead", { value: liftedV2Head, enumerable: false });
|
|
262
|
+
if (candidate.version === 3 && content !== `${JSON.stringify(receipt)}
|
|
172
263
|
`) {
|
|
173
264
|
throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
|
|
174
265
|
}
|
|
175
|
-
|
|
176
|
-
|
|
266
|
+
const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
|
|
267
|
+
if (liftedV2Head) receiptHeads.add(liftedV2Head);
|
|
268
|
+
for (const head of receiptHeads) {
|
|
269
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`])) {
|
|
270
|
+
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
271
|
+
}
|
|
177
272
|
}
|
|
178
273
|
return receipt;
|
|
179
274
|
}
|
|
@@ -187,22 +282,92 @@ function readHydratedWorkspaceReceipt(workspacePath) {
|
|
|
187
282
|
const content = fs.readFileSync(receiptPath, "utf8");
|
|
188
283
|
return parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
|
|
189
284
|
}
|
|
285
|
+
function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
|
|
286
|
+
const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
|
|
287
|
+
const status = lstatIfExists(receiptPath);
|
|
288
|
+
if (!status?.isFile() || status.isSymbolicLink()) return false;
|
|
289
|
+
try {
|
|
290
|
+
return JSON.parse(fs.readFileSync(receiptPath, "utf8")).version === 3;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
190
295
|
function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
|
|
191
296
|
const resolvedWorkspacePath = path.resolve(workspacePath);
|
|
192
297
|
if (!fs.existsSync(path.join(resolvedWorkspacePath, ".git"))) return true;
|
|
193
298
|
if (mounts) validateMounts(resolvedWorkspacePath, mounts);
|
|
194
299
|
const head = revParse(resolvedWorkspacePath, "HEAD");
|
|
195
300
|
if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
|
|
196
|
-
if (head === null) return true;
|
|
197
301
|
const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
|
|
198
|
-
|
|
302
|
+
if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
|
|
303
|
+
if (head === null) return true;
|
|
304
|
+
if (!receipt) return false;
|
|
305
|
+
return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
|
|
306
|
+
(mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean(mount.busy?.() && workspaceHydrationReceiptMountBasisHead(receipt, mount))
|
|
307
|
+
) : Boolean(receipt && receipt.mounts.every((mount) => mount.head === head));
|
|
308
|
+
}
|
|
309
|
+
const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
|
|
310
|
+
const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
|
|
311
|
+
function workspaceBasisRefName(mountId) {
|
|
312
|
+
const sanitized = Buffer.from(mountId, "utf8").toString("base64url");
|
|
313
|
+
if (!sanitized) throw new Error(`Cannot derive a workspace basis ref for mount id: ${mountId}`);
|
|
314
|
+
return `${WORKSPACE_GIT_BASIS_REF_PREFIX}${sanitized}`;
|
|
315
|
+
}
|
|
316
|
+
function listWorkspaceRefs(workspacePath, prefix) {
|
|
317
|
+
return git(workspacePath, ["for-each-ref", "--format=%(refname)", prefix], `list ${prefix} refs`).split("\n").filter(Boolean).sort();
|
|
318
|
+
}
|
|
319
|
+
function desiredWorkspaceBasisRefs(receipt) {
|
|
320
|
+
const desired = /* @__PURE__ */ new Map();
|
|
321
|
+
for (const mount of receipt.mounts) {
|
|
322
|
+
const ref = workspaceBasisRefName(mount.id);
|
|
323
|
+
if (desired.has(ref)) throw new Error(`Workspace hydration mount ids collide at basis ref ${ref}`);
|
|
324
|
+
desired.set(ref, mount.head);
|
|
325
|
+
}
|
|
326
|
+
return desired;
|
|
327
|
+
}
|
|
328
|
+
function workspaceBasisRefsMatch(workspacePath, receipt) {
|
|
329
|
+
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
330
|
+
const actualRefs = listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX);
|
|
331
|
+
if (actualRefs.length !== desired.size) return false;
|
|
332
|
+
return actualRefs.every((ref) => desired.get(ref) === revParse(workspacePath, ref));
|
|
333
|
+
}
|
|
334
|
+
function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
|
|
335
|
+
return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
|
|
336
|
+
}
|
|
337
|
+
function repairWorkspaceBasisRefs(workspacePath, receipt) {
|
|
338
|
+
const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
|
|
339
|
+
if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
|
|
340
|
+
}
|
|
341
|
+
function updateWorkspaceBasisRefs(workspacePath, receipt) {
|
|
342
|
+
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
343
|
+
const transaction = ["start"];
|
|
344
|
+
for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
|
|
345
|
+
for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
|
|
346
|
+
if (!desired.has(ref)) transaction.push(`delete ${ref}`);
|
|
347
|
+
}
|
|
348
|
+
transaction.push("prepare", "commit", "");
|
|
349
|
+
const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
|
|
350
|
+
cwd: workspacePath,
|
|
351
|
+
stdin: Buffer.from(transaction.join("\n")),
|
|
352
|
+
stdout: "pipe",
|
|
353
|
+
stderr: "pipe",
|
|
354
|
+
env: workerGitProcessEnvironment()
|
|
355
|
+
});
|
|
356
|
+
if (updated.exitCode !== 0) {
|
|
357
|
+
throw new Error(`Synchronize workspace hydration basis refs: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
|
|
358
|
+
}
|
|
359
|
+
for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) {
|
|
360
|
+
git(workspacePath, ["update-ref", "-d", ref], "remove staged workspace hydration basis ref");
|
|
361
|
+
}
|
|
199
362
|
}
|
|
200
363
|
function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
364
|
+
for (const mount of receipt.mounts) {
|
|
365
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
|
|
366
|
+
throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
|
|
367
|
+
}
|
|
368
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${mount.head}^{commit}`])) {
|
|
369
|
+
throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${mount.head}`);
|
|
370
|
+
}
|
|
206
371
|
}
|
|
207
372
|
readHydratedWorkspaceReceipt(workspacePath);
|
|
208
373
|
const gitDirectory = path.join(workspacePath, ".git");
|
|
@@ -222,6 +387,12 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
|
222
387
|
throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
|
|
223
388
|
}
|
|
224
389
|
const temporaryPath = path.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
390
|
+
const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
|
|
391
|
+
desiredWorkspaceBasisRefs(receipt);
|
|
392
|
+
for (const mount of receipt.mounts) {
|
|
393
|
+
const stagedRef = `${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)}`;
|
|
394
|
+
git(workspacePath, ["update-ref", stagedRef, mount.head], "stage workspace hydration basis ref");
|
|
395
|
+
}
|
|
225
396
|
let descriptor;
|
|
226
397
|
try {
|
|
227
398
|
descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
|
|
@@ -232,6 +403,7 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
|
232
403
|
descriptor = void 0;
|
|
233
404
|
fs.renameSync(temporaryPath, receiptPath);
|
|
234
405
|
fsyncDirectory(receiptDirectory);
|
|
406
|
+
updateWorkspaceBasisRefs(workspacePath, receipt);
|
|
235
407
|
} catch (error) {
|
|
236
408
|
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
237
409
|
fs.rmSync(temporaryPath, { force: true });
|
|
@@ -359,6 +531,16 @@ function workspaceRebaseInProgress(workspacePath) {
|
|
|
359
531
|
const gitDirectory = path.join(workspacePath, ".git");
|
|
360
532
|
return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
|
|
361
533
|
}
|
|
534
|
+
function workspaceResolutionInProgress(workspacePath) {
|
|
535
|
+
return workspaceRebaseInProgress(workspacePath) || fs.existsSync(path.join(workspacePath, ".git", "MERGE_HEAD"));
|
|
536
|
+
}
|
|
537
|
+
function workspaceHasUnmergedEntries(workspacePath) {
|
|
538
|
+
const result = gitResult(workspacePath, ["ls-files", "-u", "-z"]);
|
|
539
|
+
if (result.exitCode !== 0) {
|
|
540
|
+
throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
541
|
+
}
|
|
542
|
+
return result.stdout.length > 0;
|
|
543
|
+
}
|
|
362
544
|
function clearWorkspaceCheckoutTree(workspacePath) {
|
|
363
545
|
for (const name of fs.readdirSync(workspacePath)) {
|
|
364
546
|
if (name === ".git") continue;
|
|
@@ -366,15 +548,18 @@ function clearWorkspaceCheckoutTree(workspacePath) {
|
|
|
366
548
|
}
|
|
367
549
|
fsyncDirectory(workspacePath);
|
|
368
550
|
}
|
|
369
|
-
function recoverWorkspaceCheckoutDurability(workspacePath) {
|
|
551
|
+
function recoverWorkspaceCheckoutDurability(workspacePath, preserveResolutionInProgress = false) {
|
|
370
552
|
workspacePath = path.resolve(workspacePath);
|
|
371
553
|
const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
|
|
372
554
|
const observedHead = revParse(workspacePath, "HEAD");
|
|
373
555
|
if (record?.state === "durable" && record.head === observedHead) return;
|
|
556
|
+
if (preserveResolutionInProgress && workspaceResolutionInProgress(workspacePath)) {
|
|
557
|
+
throw new Error("Workspace resolution is in progress; finish or abort it before synchronizing");
|
|
558
|
+
}
|
|
374
559
|
let targetHead = record?.state === "transition" ? record.head : observedHead;
|
|
375
560
|
if (record?.state === "transition" && observedHead && observedHead !== record.head) {
|
|
376
561
|
const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
377
|
-
if (hydrationReceipt?.head === observedHead) {
|
|
562
|
+
if (hydrationReceipt?.legacyHead === observedHead || hydrationReceipt?.mounts.some((mount) => mount.head === observedHead)) {
|
|
378
563
|
targetHead = observedHead;
|
|
379
564
|
}
|
|
380
565
|
}
|
|
@@ -456,7 +641,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
456
641
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
457
642
|
}
|
|
458
643
|
const candidate = parsed;
|
|
459
|
-
if (candidate.version !== 2 || !Array.isArray(candidate.mounts)) {
|
|
644
|
+
if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
|
|
460
645
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
461
646
|
}
|
|
462
647
|
const parseReceipt = (rawReceipt) => {
|
|
@@ -488,7 +673,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
488
673
|
return mount;
|
|
489
674
|
});
|
|
490
675
|
return {
|
|
491
|
-
version:
|
|
676
|
+
version: 3,
|
|
492
677
|
targetReceipt,
|
|
493
678
|
receiptBefore,
|
|
494
679
|
mounts
|
|
@@ -581,7 +766,7 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
581
766
|
const snapshotRoot = path.join(stagingPath, "mounts");
|
|
582
767
|
fs.mkdirSync(snapshotRoot, { mode: 448 });
|
|
583
768
|
const manifest = {
|
|
584
|
-
version:
|
|
769
|
+
version: 3,
|
|
585
770
|
targetReceipt,
|
|
586
771
|
receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
|
|
587
772
|
mounts: mounts.map((mount, index) => {
|
|
@@ -760,9 +945,22 @@ function configureWorkspaceRepository(input) {
|
|
|
760
945
|
if (!name || !email) throw new Error("Workspace Git identity must include name and email");
|
|
761
946
|
git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
762
947
|
git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
763
|
-
git(
|
|
948
|
+
git(
|
|
949
|
+
input.workspacePath,
|
|
950
|
+
["config", "--local", "core.fsync", "committed,reference"],
|
|
951
|
+
"configure durable workspace commits and references"
|
|
952
|
+
);
|
|
764
953
|
git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
|
|
765
954
|
}
|
|
955
|
+
function configureExistingWorkspaceGitForRemediation(input) {
|
|
956
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
957
|
+
const workspaceStatus = lstatIfExists(workspacePath);
|
|
958
|
+
const gitStatus = lstatIfExists(path.join(workspacePath, ".git"));
|
|
959
|
+
if (!workspaceStatus?.isDirectory() || workspaceStatus.isSymbolicLink() || !gitStatus?.isDirectory() || gitStatus.isSymbolicLink()) {
|
|
960
|
+
throw new Error("Active workspace remediation requires a regular existing canonical synchronization checkout");
|
|
961
|
+
}
|
|
962
|
+
configureWorkspaceRepository({ ...input, workspacePath });
|
|
963
|
+
}
|
|
766
964
|
function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
767
965
|
const result = gitResult(workspacePath, [
|
|
768
966
|
...gitTransportSecurityArgs(remoteUrl, credentialHelper, credentialUsername),
|
|
@@ -824,7 +1022,7 @@ function ensureWorkspaceGitClone(input) {
|
|
|
824
1022
|
}
|
|
825
1023
|
}
|
|
826
1024
|
configureWorkspaceRepository({ ...input, workspacePath });
|
|
827
|
-
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
1025
|
+
recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
|
|
828
1026
|
const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
829
1027
|
const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
|
|
830
1028
|
if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
@@ -974,31 +1172,40 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
974
1172
|
}
|
|
975
1173
|
function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
976
1174
|
const workspacePath = path.resolve(input.workspacePath);
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
1175
|
+
const durabilityMounts = input.durabilityMounts ?? input.mounts;
|
|
1176
|
+
const receiptMounts = input.receiptMounts ?? durabilityMounts;
|
|
1177
|
+
const requiredMounts = input.requiredMounts ?? input.mounts;
|
|
1178
|
+
const candidateMounts = [
|
|
1179
|
+
...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
|
|
1180
|
+
];
|
|
1181
|
+
const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1182
|
+
const requestedIds = new Set(input.mounts.map(({ id }) => id));
|
|
1183
|
+
const requiredIds = new Set(requiredMounts.map(({ id }) => id));
|
|
1184
|
+
const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1185
|
+
const advancedDurabilityMounts = durabilityMounts.filter(
|
|
1186
|
+
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1187
|
+
);
|
|
1188
|
+
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
986
1189
|
if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
|
|
987
1190
|
throw new Error("Workspace checkout is not durably materialized at its current HEAD");
|
|
988
1191
|
}
|
|
989
1192
|
const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
|
|
990
|
-
const targetReceipt = targetHead ?
|
|
991
|
-
const manifest = beginHydrationTransaction(workspacePath,
|
|
1193
|
+
const targetReceipt = targetHead ? receiptWithMountsAtHead(readHydratedWorkspaceReceipt(workspacePath), targetHead, advancedDurabilityMounts, receiptMounts) : null;
|
|
1194
|
+
const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
992
1195
|
try {
|
|
993
|
-
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath,
|
|
994
|
-
|
|
1196
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
|
|
1197
|
+
const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
|
|
1198
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
995
1199
|
throw new Error("Workspace hydration did not include every required mount");
|
|
996
1200
|
}
|
|
997
|
-
fsyncHydratedWorkspaceMounts(
|
|
1201
|
+
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
|
|
998
1202
|
markHydrationTransactionDurable(workspacePath);
|
|
999
1203
|
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1000
1204
|
removeHydrationTransaction(workspacePath);
|
|
1001
|
-
return
|
|
1205
|
+
return {
|
|
1206
|
+
hydratedMountIds: hydration.hydratedMountIds,
|
|
1207
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort()
|
|
1208
|
+
};
|
|
1002
1209
|
} catch (error) {
|
|
1003
1210
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1004
1211
|
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
@@ -1031,44 +1238,44 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
|
1031
1238
|
function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
1032
1239
|
workspacePath = path.resolve(workspacePath);
|
|
1033
1240
|
validateMounts(workspacePath, mounts);
|
|
1034
|
-
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
1241
|
+
recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
|
|
1035
1242
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1036
1243
|
const currentHead = revParse(workspacePath, "HEAD");
|
|
1037
|
-
if (!currentHead) return;
|
|
1038
1244
|
const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1245
|
+
repairWorkspaceBasisRefs(workspacePath, currentReceipt);
|
|
1246
|
+
if (!currentHead) return;
|
|
1039
1247
|
const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
|
|
1040
1248
|
const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
|
|
1041
1249
|
const uncoveredBasisMounts = configuredBasisMounts.filter(
|
|
1042
1250
|
(mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
|
|
1043
1251
|
);
|
|
1044
1252
|
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)
|
|
1253
|
+
const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1254
|
+
const recoveryMounts = uncoveredBasisMounts.filter(
|
|
1255
|
+
(mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
|
|
1256
|
+
);
|
|
1257
|
+
const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
|
|
1258
|
+
const hydrationTargets = idleBasislessMounts.filter(
|
|
1259
|
+
(mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
|
|
1053
1260
|
);
|
|
1054
1261
|
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1055
1262
|
workspacePath,
|
|
1056
1263
|
mounts: hydrationTargets,
|
|
1057
|
-
durabilityMounts:
|
|
1264
|
+
durabilityMounts: idleBasislessMounts,
|
|
1058
1265
|
receiptMounts: configuredBasisMounts,
|
|
1059
1266
|
requiredMounts: hydrationTargets,
|
|
1060
1267
|
ignoreBusy: options.ignoreBusy,
|
|
1061
1268
|
recordCurrentHead: true
|
|
1062
1269
|
});
|
|
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
|
-
}
|
|
1270
|
+
void hydration;
|
|
1068
1271
|
}
|
|
1069
1272
|
function resetWorkspaceGit(input) {
|
|
1070
1273
|
const workspacePath = path.resolve(input.workspacePath);
|
|
1071
1274
|
validateMounts(workspacePath, input.mounts);
|
|
1275
|
+
const busyResetMountIds = input.mounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1276
|
+
if (busyResetMountIds.length > 0) {
|
|
1277
|
+
throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
|
|
1278
|
+
}
|
|
1072
1279
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1073
1280
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1074
1281
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
@@ -1255,69 +1462,137 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1255
1462
|
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
1256
1463
|
const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
|
|
1257
1464
|
validateMounts(workspacePath, input.mounts);
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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 });
|
|
1465
|
+
const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
|
|
1466
|
+
const preserveResolutionInProgress = input.skipMountMirror === true;
|
|
1467
|
+
const initial = ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
|
|
1468
|
+
assertWorkspaceRemediationAncestry({
|
|
1469
|
+
workspacePath,
|
|
1470
|
+
currentHead: initial.localHead,
|
|
1471
|
+
remoteHead: initial.remoteHead,
|
|
1472
|
+
requiredAncestorHeads
|
|
1473
|
+
});
|
|
1474
|
+
const verifiedAncestorHeads = requiredAncestorHeads.length > 0 ? requiredAncestorHeads : void 0;
|
|
1277
1475
|
const startingHead = initial.localHead;
|
|
1278
1476
|
const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1279
1477
|
const deferredMountIds = /* @__PURE__ */ new Set();
|
|
1280
|
-
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead
|
|
1478
|
+
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
|
|
1281
1479
|
for (const mount of input.mounts) {
|
|
1282
|
-
if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true &&
|
|
1480
|
+
if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && workspaceHydrationReceiptMountBasisHead(receiptBeforeInitialHydration, mount) === null) {
|
|
1283
1481
|
deferredMountIds.add(mount.id);
|
|
1284
1482
|
}
|
|
1285
1483
|
}
|
|
1286
1484
|
}
|
|
1287
1485
|
recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1288
|
-
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
|
|
1486
|
+
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
|
|
1487
|
+
preserveResolutionInProgress,
|
|
1488
|
+
preserveStaleBases: true
|
|
1289
1489
|
});
|
|
1290
|
-
const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
|
|
1291
1490
|
const selected = activeMounts(input.mounts);
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1491
|
+
const projectionMutationTokens = new Map(
|
|
1492
|
+
input.mounts.flatMap((mount) => mount.mutationToken ? [[mount.id, mount.mutationToken()]] : [])
|
|
1493
|
+
);
|
|
1494
|
+
const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
|
|
1495
|
+
const receiptRequiredLiveMounts = selected.active;
|
|
1294
1496
|
const projectionBasisHead = revParse(workspacePath, "HEAD");
|
|
1295
1497
|
const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
);
|
|
1498
|
+
const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
|
|
1499
|
+
const fastProjectionMounts = [];
|
|
1500
|
+
const mergedProjectionTrees = /* @__PURE__ */ new Map();
|
|
1501
|
+
const mergedProjectionMounts = [];
|
|
1502
|
+
const projectionSkippedIds = /* @__PURE__ */ new Set();
|
|
1503
|
+
let projectionWarning;
|
|
1504
|
+
const refreshCycleSkippedMounts = (mounts = input.mounts) => {
|
|
1505
|
+
for (const mount of mounts) {
|
|
1506
|
+
const projectionToken = projectionMutationTokens.get(mount.id);
|
|
1507
|
+
if (projectionToken !== void 0 && mount.mutationToken?.() !== projectionToken) cycleSkippedMountIds.add(mount.id);
|
|
1508
|
+
if (mount.busy?.()) cycleSkippedMountIds.add(mount.id);
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
|
|
1512
|
+
refreshCycleSkippedMounts();
|
|
1513
|
+
const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
|
|
1514
|
+
return {
|
|
1515
|
+
activeMountIds: [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort(),
|
|
1516
|
+
skippedMountIds: [...skippedIds].sort()
|
|
1517
|
+
};
|
|
1518
|
+
};
|
|
1519
|
+
if (!input.skipMountMirror) {
|
|
1520
|
+
const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
|
|
1521
|
+
const mergeSupport = mergeProjectionEnabled ? workspaceMergeProjectionSupport() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
|
|
1522
|
+
for (const mount of [...selected.active, ...selected.tombstones]) {
|
|
1523
|
+
if (deferredMountIds.has(mount.id)) continue;
|
|
1524
|
+
const basisHead = workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount);
|
|
1525
|
+
if (projectionBasisHead === null || basisHead === projectionBasisHead) {
|
|
1526
|
+
fastProjectionMounts.push(mount);
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
if (basisHead === null) {
|
|
1530
|
+
deferredMountIds.add(mount.id);
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
if (!mergeSupport.supported) {
|
|
1534
|
+
projectionSkippedIds.add(mount.id);
|
|
1535
|
+
projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const merged = mergeWorkspaceProjectionMount({
|
|
1539
|
+
workspacePath,
|
|
1540
|
+
mount: {
|
|
1541
|
+
id: mount.id,
|
|
1542
|
+
sourcePath: mount.sourcePath,
|
|
1543
|
+
workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
|
|
1544
|
+
sourceMode: mount.sourceMode
|
|
1545
|
+
},
|
|
1546
|
+
basisHead,
|
|
1547
|
+
currentHead: projectionBasisHead,
|
|
1548
|
+
attemptId
|
|
1549
|
+
});
|
|
1550
|
+
if (merged.kind === "conflict") {
|
|
1551
|
+
const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
|
|
1552
|
+
return {
|
|
1553
|
+
outcome: "conflict_blocked",
|
|
1554
|
+
startingHead,
|
|
1555
|
+
localHead: merged.oursCommit,
|
|
1556
|
+
remoteHead: initial.remoteHead,
|
|
1557
|
+
publishedHead: initial.remoteHead,
|
|
1558
|
+
rebaseCount: 0,
|
|
1559
|
+
diffSizeBytes: 0,
|
|
1560
|
+
affectedPaths: merged.conflictPaths,
|
|
1561
|
+
activeMountIds: [],
|
|
1562
|
+
skippedMountIds: input.mounts.map(({ id }) => id).sort(),
|
|
1563
|
+
conflictPaths: merged.conflictPaths,
|
|
1564
|
+
conflictSnapshotRefs: refs,
|
|
1565
|
+
conflictKind: "projection_merge",
|
|
1566
|
+
...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
|
|
1567
|
+
error: merged.error
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
mergedProjectionTrees.set(mount.id, merged.resultTree);
|
|
1571
|
+
mergedProjectionMounts.push(mount);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
|
|
1575
|
+
`);
|
|
1299
1576
|
const completedMountSelection = (requireCurrentHeadReceipt = false) => {
|
|
1300
1577
|
const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
|
|
1301
1578
|
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
1579
|
const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1580
|
+
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
1315
1581
|
const mountIsAlreadyCovered = (mount) => Boolean(
|
|
1316
1582
|
currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
|
|
1317
1583
|
);
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1584
|
+
if (currentHeadBeforeHydration && completionReceiptMounts.every((mount) => cycleSkippedMountIds.has(mount.id) || mountIsAlreadyCovered(mount))) {
|
|
1585
|
+
return classifyMounts([...selected.active, ...selected.tombstones]);
|
|
1586
|
+
}
|
|
1587
|
+
const uncoveredCompletionMounts = selected.active.filter(
|
|
1588
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1589
|
+
);
|
|
1590
|
+
const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter(
|
|
1591
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1592
|
+
);
|
|
1593
|
+
const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
|
|
1594
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1595
|
+
);
|
|
1321
1596
|
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1322
1597
|
workspacePath,
|
|
1323
1598
|
mounts: uncoveredCompletionMounts,
|
|
@@ -1326,22 +1601,32 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1326
1601
|
requiredMounts: uncoveredRequiredLiveMounts,
|
|
1327
1602
|
recordCurrentHead: true
|
|
1328
1603
|
});
|
|
1604
|
+
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
1605
|
+
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
1329
1606
|
if (requireCurrentHeadReceipt) {
|
|
1330
1607
|
const currentHead = revParse(workspacePath, "HEAD");
|
|
1331
|
-
|
|
1608
|
+
const receipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1609
|
+
const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
|
|
1610
|
+
const idleMounts = completionReceiptMounts.filter(
|
|
1611
|
+
(mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mount.busy?.() && !projectionSkippedIds.has(mount.id)
|
|
1612
|
+
);
|
|
1613
|
+
if (currentHead && !idleMounts.every((mount) => workspaceHydrationReceiptCoversMount(receipt, currentHead, mount))) {
|
|
1332
1614
|
throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
|
|
1333
1615
|
}
|
|
1334
1616
|
}
|
|
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
|
-
};
|
|
1617
|
+
return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
|
|
1339
1618
|
};
|
|
1340
1619
|
let newlyStagedPaths = [];
|
|
1341
1620
|
const stageAndCommitWorkspace = () => {
|
|
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
|
+
}
|
|
1342
1624
|
git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
1343
1625
|
newlyStagedPaths = stagedPaths(workspacePath);
|
|
1344
1626
|
if (newlyStagedPaths.length > 0) {
|
|
1627
|
+
if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
|
|
1628
|
+
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
1629
|
+
}
|
|
1345
1630
|
git(
|
|
1346
1631
|
workspacePath,
|
|
1347
1632
|
[
|
|
@@ -1356,11 +1641,22 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1356
1641
|
if (!input.skipMountMirror) {
|
|
1357
1642
|
beginWorkspaceCheckoutTransition(workspacePath);
|
|
1358
1643
|
try {
|
|
1359
|
-
|
|
1360
|
-
|
|
1644
|
+
const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
|
|
1645
|
+
const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
|
|
1646
|
+
const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
|
|
1647
|
+
mirrorMountsToWorkspace(workspacePath, fastActiveMounts);
|
|
1648
|
+
removeWorkspaceMounts(workspacePath, fastTombstones);
|
|
1649
|
+
for (const mount of mergedProjectionMounts) {
|
|
1650
|
+
materializeWorkspaceProjectionTree({
|
|
1651
|
+
workspacePath,
|
|
1652
|
+
workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
|
|
1653
|
+
resultTree: mergedProjectionTrees.get(mount.id)
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
const restoreFromHeadIds = /* @__PURE__ */ new Set([...deferredMountIds, ...selected.skipped.map(({ id }) => id)]);
|
|
1361
1657
|
restoreDeferredWorkspaceMountsFromHead(
|
|
1362
1658
|
workspacePath,
|
|
1363
|
-
input.mounts.filter(({ id }) =>
|
|
1659
|
+
input.mounts.filter(({ id }) => restoreFromHeadIds.has(id))
|
|
1364
1660
|
);
|
|
1365
1661
|
stageAndCommitWorkspace();
|
|
1366
1662
|
fsyncWorkspaceCheckoutTree(workspacePath);
|
|
@@ -1374,9 +1670,15 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1374
1670
|
if (!input.skipMountMirror) {
|
|
1375
1671
|
const projectedHead = revParse(workspacePath, "HEAD");
|
|
1376
1672
|
if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
|
|
1377
|
-
const targetProjectionReceipt =
|
|
1378
|
-
|
|
1379
|
-
|
|
1673
|
+
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
1674
|
+
projectionBasisReceipt,
|
|
1675
|
+
projectedHead,
|
|
1676
|
+
fastProjectionMounts,
|
|
1677
|
+
allProjectionReceiptMounts
|
|
1678
|
+
);
|
|
1679
|
+
const currentProjectionReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1680
|
+
if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !workspaceBasisRefsAreCurrent(workspacePath, targetProjectionReceipt)) {
|
|
1681
|
+
fsyncHydratedWorkspaceMounts(fastProjectionMounts);
|
|
1380
1682
|
updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
|
|
1381
1683
|
}
|
|
1382
1684
|
completeWorkspaceCheckoutTransition(workspacePath);
|
|
@@ -1385,9 +1687,16 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1385
1687
|
let rebaseCount = 0;
|
|
1386
1688
|
let updated = false;
|
|
1387
1689
|
for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
|
|
1690
|
+
assertWorkspaceRemediationAncestry({
|
|
1691
|
+
workspacePath,
|
|
1692
|
+
currentHead: revParse(workspacePath, "HEAD"),
|
|
1693
|
+
remoteHead,
|
|
1694
|
+
requiredAncestorHeads
|
|
1695
|
+
});
|
|
1388
1696
|
const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
|
|
1389
1697
|
if (reconciled.kind === "conflict") {
|
|
1390
1698
|
const localHead2 = revParse(workspacePath, "HEAD");
|
|
1699
|
+
const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
|
|
1391
1700
|
return {
|
|
1392
1701
|
outcome: "conflict_blocked",
|
|
1393
1702
|
startingHead,
|
|
@@ -1397,10 +1706,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1397
1706
|
rebaseCount: rebaseCount + 1,
|
|
1398
1707
|
diffSizeBytes: 0,
|
|
1399
1708
|
affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
|
|
1400
|
-
|
|
1401
|
-
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
|
|
1709
|
+
...classifiedMounts,
|
|
1402
1710
|
conflictPaths: reconciled.conflictPaths,
|
|
1403
1711
|
conflictSnapshotRefs: reconciled.refs,
|
|
1712
|
+
conflictKind: "integration_rebase",
|
|
1713
|
+
...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
|
|
1404
1714
|
error: reconciled.error
|
|
1405
1715
|
};
|
|
1406
1716
|
}
|
|
@@ -1409,6 +1719,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1409
1719
|
if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
|
|
1410
1720
|
const localHead = revParse(workspacePath, "HEAD");
|
|
1411
1721
|
if (!localHead) {
|
|
1722
|
+
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
|
|
1412
1723
|
return {
|
|
1413
1724
|
outcome: "no_change",
|
|
1414
1725
|
startingHead,
|
|
@@ -1418,13 +1729,16 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1418
1729
|
rebaseCount,
|
|
1419
1730
|
diffSizeBytes: 0,
|
|
1420
1731
|
affectedPaths: [],
|
|
1421
|
-
|
|
1422
|
-
|
|
1732
|
+
...classifiedMounts,
|
|
1733
|
+
...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
|
|
1734
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1423
1735
|
};
|
|
1424
1736
|
}
|
|
1425
1737
|
const paths = changedPaths(workspacePath, remoteHead, localHead);
|
|
1426
|
-
const size = paths.length > 0 ? await diffSizeBytes(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
|
|
1738
|
+
const size = paths.length > 0 ? await (input.measureDiffSize ?? diffSizeBytes)(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
|
|
1739
|
+
input.assertStillAdmitted?.();
|
|
1427
1740
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
1741
|
+
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
|
|
1428
1742
|
return {
|
|
1429
1743
|
outcome: "large_diff_blocked",
|
|
1430
1744
|
startingHead,
|
|
@@ -1434,17 +1748,19 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1434
1748
|
rebaseCount,
|
|
1435
1749
|
diffSizeBytes: size,
|
|
1436
1750
|
affectedPaths: paths,
|
|
1437
|
-
|
|
1438
|
-
|
|
1751
|
+
...classifiedMounts,
|
|
1752
|
+
...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
|
|
1439
1753
|
error: `Workspace diff exceeds the ${maxDiffBytes}-byte automatic publication limit`
|
|
1440
1754
|
};
|
|
1441
1755
|
}
|
|
1442
1756
|
if (localHead === remoteHead) {
|
|
1757
|
+
assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
|
|
1443
1758
|
const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
|
|
1444
1759
|
await input.afterWorkspacePublished?.({
|
|
1445
1760
|
publishedHead: localHead,
|
|
1446
1761
|
activeMountIds: completedMounts.activeMountIds
|
|
1447
1762
|
});
|
|
1763
|
+
input.assertStillAdmitted?.();
|
|
1448
1764
|
return {
|
|
1449
1765
|
outcome: updated ? "updated" : "no_change",
|
|
1450
1766
|
startingHead,
|
|
@@ -1454,9 +1770,12 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1454
1770
|
rebaseCount,
|
|
1455
1771
|
diffSizeBytes: size,
|
|
1456
1772
|
affectedPaths: paths,
|
|
1457
|
-
...completedMounts
|
|
1773
|
+
...completedMounts,
|
|
1774
|
+
...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
|
|
1775
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1458
1776
|
};
|
|
1459
1777
|
}
|
|
1778
|
+
assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
|
|
1460
1779
|
const pushArgs = [
|
|
1461
1780
|
...gitTransportSecurityArgs(input.remoteUrl, input.credentialHelper, input.credentialUsername),
|
|
1462
1781
|
"push",
|
|
@@ -1466,12 +1785,14 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1466
1785
|
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
1467
1786
|
const push = gitResult(workspacePath, pushArgs);
|
|
1468
1787
|
if (push.exitCode === 0) {
|
|
1788
|
+
assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
|
|
1469
1789
|
updateIntegratedWorkspaceHead(workspacePath, localHead);
|
|
1470
1790
|
const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
|
|
1471
1791
|
await input.afterWorkspacePublished?.({
|
|
1472
1792
|
publishedHead: localHead,
|
|
1473
1793
|
activeMountIds: completedMounts.activeMountIds
|
|
1474
1794
|
});
|
|
1795
|
+
input.assertStillAdmitted?.();
|
|
1475
1796
|
return {
|
|
1476
1797
|
outcome: "pushed",
|
|
1477
1798
|
startingHead,
|
|
@@ -1481,7 +1802,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1481
1802
|
rebaseCount,
|
|
1482
1803
|
diffSizeBytes: size,
|
|
1483
1804
|
affectedPaths: paths,
|
|
1484
|
-
...completedMounts
|
|
1805
|
+
...completedMounts,
|
|
1806
|
+
...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
|
|
1807
|
+
...projectionWarning ? { error: projectionWarning } : {}
|
|
1485
1808
|
};
|
|
1486
1809
|
}
|
|
1487
1810
|
if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
|
|
@@ -1506,6 +1829,8 @@ export {
|
|
|
1506
1829
|
WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
|
|
1507
1830
|
WORKSPACE_GIT_HYDRATED_RECEIPT,
|
|
1508
1831
|
WORKSPACE_GIT_HYDRATION_TRANSACTION,
|
|
1832
|
+
WorkspaceRemediationAncestryError,
|
|
1833
|
+
configureExistingWorkspaceGitForRemediation,
|
|
1509
1834
|
ensureWorkspaceGitClone,
|
|
1510
1835
|
hydrateWorkspaceGitMounts,
|
|
1511
1836
|
recoverWorkspaceGitHydration,
|