@ricsam/r5d-worker 0.0.78 → 0.0.80
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 +6 -0
- package/dist/cjs/main.cjs +1550 -335
- package/dist/cjs/managed-paths.cjs +101 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-workspace-state.cjs +184 -5
- package/dist/cjs/project-worktrees.cjs +676 -58
- package/dist/cjs/registry-auth.cjs +310 -0
- package/dist/cjs/repository-transition-policy.cjs +49 -0
- package/dist/cjs/supervisor.cjs +62 -11
- package/dist/cjs/working-tree-mirror.cjs +196 -36
- package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
- package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
- package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
- package/dist/cjs/workspace-git-sync.cjs +1037 -54
- package/dist/cjs/workspace-mount-boundary.cjs +71 -0
- package/dist/cjs/workspace-path-move.cjs +195 -0
- package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
- package/dist/cjs/workspace-project-config-policy.cjs +46 -0
- package/dist/cjs/workspace-publication-evidence.cjs +46 -0
- package/dist/mjs/main.mjs +1584 -341
- package/dist/mjs/managed-paths.mjs +100 -1
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-workspace-state.mjs +181 -5
- package/dist/mjs/project-worktrees.mjs +667 -57
- package/dist/mjs/registry-auth.mjs +269 -0
- package/dist/mjs/repository-transition-policy.mjs +22 -0
- package/dist/mjs/supervisor.mjs +61 -11
- package/dist/mjs/working-tree-mirror.mjs +195 -36
- package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
- package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
- package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
- package/dist/mjs/workspace-git-sync.mjs +1032 -54
- package/dist/mjs/workspace-mount-boundary.mjs +37 -0
- package/dist/mjs/workspace-path-move.mjs +160 -0
- package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
- package/dist/mjs/workspace-project-config-policy.mjs +21 -0
- package/dist/mjs/workspace-publication-evidence.mjs +21 -0
- package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
- package/dist/types/main.d.ts +175 -1
- package/dist/types/managed-paths.d.ts +11 -0
- package/dist/types/project-workspace-state.d.ts +45 -1
- package/dist/types/project-worktrees.d.ts +119 -2
- package/dist/types/registry-auth.d.ts +47 -0
- package/dist/types/repository-transition-policy.d.ts +26 -0
- package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
- package/dist/types/supervisor-signal-fixture.d.ts +1 -0
- package/dist/types/supervisor.d.ts +6 -4
- package/dist/types/working-tree-mirror.d.ts +14 -0
- package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
- package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
- package/dist/types/workspace-command-sync-policy.d.ts +9 -0
- package/dist/types/workspace-git-sync.d.ts +33 -3
- package/dist/types/workspace-mount-boundary.d.ts +10 -0
- package/dist/types/workspace-path-move.d.ts +21 -0
- package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
- package/dist/types/workspace-project-config-policy.d.ts +34 -0
- package/dist/types/workspace-publication-evidence.d.ts +17 -0
- package/package.json +1 -1
|
@@ -2,10 +2,14 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
4
4
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
5
|
+
import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
|
|
5
6
|
const WORKSPACE_GIT_BRANCH = "main";
|
|
6
7
|
const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
|
|
7
8
|
const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v1";
|
|
8
9
|
const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
|
|
10
|
+
const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
|
|
11
|
+
const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
|
|
12
|
+
const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
|
|
9
13
|
const NON_RECURSIVE_GIT_CONFIG = [
|
|
10
14
|
"-c",
|
|
11
15
|
"submodule.recurse=false",
|
|
@@ -48,6 +52,640 @@ function revParse(workspacePath, revision) {
|
|
|
48
52
|
function updateIntegratedWorkspaceHead(workspacePath, head) {
|
|
49
53
|
git(workspacePath, ["update-ref", WORKSPACE_GIT_INTEGRATED_REF, head], "record integrated workspace head");
|
|
50
54
|
}
|
|
55
|
+
function fsyncDirectory(directoryPath) {
|
|
56
|
+
const descriptor = fs.openSync(directoryPath, fs.constants.O_RDONLY);
|
|
57
|
+
try {
|
|
58
|
+
fs.fsyncSync(descriptor);
|
|
59
|
+
} finally {
|
|
60
|
+
fs.closeSync(descriptor);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function lstatIfExists(targetPath) {
|
|
64
|
+
try {
|
|
65
|
+
return fs.lstatSync(targetPath);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code === "ENOENT") return null;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function hydratedWorkspaceReceiptPath(workspacePath) {
|
|
72
|
+
return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATED_RECEIPT.split("/"));
|
|
73
|
+
}
|
|
74
|
+
function workspaceCheckoutDurabilityPath(workspacePath) {
|
|
75
|
+
return path.join(workspacePath, ".git", ...WORKSPACE_GIT_CHECKOUT_DURABILITY.split("/"));
|
|
76
|
+
}
|
|
77
|
+
function normalizedWorkspaceMountPath(workspaceRelativePath) {
|
|
78
|
+
return workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
79
|
+
}
|
|
80
|
+
function workspaceHydrationMountBasis(mount) {
|
|
81
|
+
return {
|
|
82
|
+
id: mount.id,
|
|
83
|
+
incarnationKey: mount.hydrationIncarnationKey,
|
|
84
|
+
sourcePath: path.resolve(mount.sourcePath),
|
|
85
|
+
durabilityRootPath: path.resolve(mount.durabilityRootPath),
|
|
86
|
+
workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
|
|
87
|
+
sourceMode: mount.sourceMode,
|
|
88
|
+
hydrateDeletionMode: mount.hydrateDeletionMode,
|
|
89
|
+
preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence === true,
|
|
90
|
+
preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange === true,
|
|
91
|
+
deleteWhenSourceMissing: mount.deleteWhenSourceMissing === true
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function compareWorkspaceHydrationMountBasis(left, right) {
|
|
95
|
+
return left.id.localeCompare(right.id);
|
|
96
|
+
}
|
|
97
|
+
function workspaceHydrationReceipt(head, mounts) {
|
|
98
|
+
const bases = mounts.map(workspaceHydrationMountBasis).sort(compareWorkspaceHydrationMountBasis);
|
|
99
|
+
const ids = /* @__PURE__ */ new Set();
|
|
100
|
+
for (const basis of bases) {
|
|
101
|
+
if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
|
|
102
|
+
ids.add(basis.id);
|
|
103
|
+
}
|
|
104
|
+
return { version: 2, head, mounts: bases };
|
|
105
|
+
}
|
|
106
|
+
function workspaceHydrationReceiptsEqual(left, right) {
|
|
107
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
108
|
+
}
|
|
109
|
+
function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
|
|
110
|
+
if (!receipt || receipt.head !== head) return false;
|
|
111
|
+
const expected = workspaceHydrationMountBasis(mount);
|
|
112
|
+
const actual = receipt.mounts.find(({ id }) => id === expected.id);
|
|
113
|
+
return Boolean(actual && JSON.stringify(actual) === JSON.stringify(expected));
|
|
114
|
+
}
|
|
115
|
+
function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
|
|
116
|
+
return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
|
|
117
|
+
}
|
|
118
|
+
function configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, deferMountIds) {
|
|
119
|
+
const resolvedWorkspacePath = path.resolve(workspacePath);
|
|
120
|
+
return mounts.filter((mount) => {
|
|
121
|
+
if (deferMountIds?.has(mount.id)) return false;
|
|
122
|
+
if (mount.deleteWhenSourceMissing) return !fs.existsSync(mount.sourcePath);
|
|
123
|
+
const outerPath = path.join(resolvedWorkspacePath, ...normalizedWorkspaceMountPath(mount.workspaceRelativePath).split("/"));
|
|
124
|
+
return fs.existsSync(mount.sourcePath) || fs.existsSync(outerPath) || mount.sourceMode === "all";
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content) {
|
|
128
|
+
const legacyHead = content.endsWith("\n") ? content.slice(0, -1) : "";
|
|
129
|
+
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(legacyHead) && content === `${legacyHead}
|
|
130
|
+
`) {
|
|
131
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
|
|
132
|
+
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
133
|
+
}
|
|
134
|
+
return { version: 2, head: legacyHead, mounts: [] };
|
|
135
|
+
}
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = JSON.parse(content);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
141
|
+
}
|
|
142
|
+
if (!parsed || typeof parsed !== "object") {
|
|
143
|
+
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
144
|
+
}
|
|
145
|
+
const candidate = parsed;
|
|
146
|
+
if (candidate.version !== 2 || typeof candidate.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head)) {
|
|
147
|
+
throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
|
|
148
|
+
}
|
|
149
|
+
if (!Array.isArray(candidate.mounts)) {
|
|
150
|
+
throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
|
|
151
|
+
}
|
|
152
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
153
|
+
const mounts = candidate.mounts.map((rawMount) => {
|
|
154
|
+
if (!rawMount || typeof rawMount !== "object") {
|
|
155
|
+
throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
|
|
156
|
+
}
|
|
157
|
+
const mount = rawMount;
|
|
158
|
+
const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
|
|
159
|
+
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") {
|
|
161
|
+
throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
|
|
162
|
+
}
|
|
163
|
+
seenIds.add(mount.id);
|
|
164
|
+
return mount;
|
|
165
|
+
});
|
|
166
|
+
const receipt = {
|
|
167
|
+
version: 2,
|
|
168
|
+
head: candidate.head,
|
|
169
|
+
mounts: mounts.sort(compareWorkspaceHydrationMountBasis)
|
|
170
|
+
};
|
|
171
|
+
if (content !== `${JSON.stringify(receipt)}
|
|
172
|
+
`) {
|
|
173
|
+
throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
|
|
174
|
+
}
|
|
175
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
|
|
176
|
+
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
177
|
+
}
|
|
178
|
+
return receipt;
|
|
179
|
+
}
|
|
180
|
+
function readHydratedWorkspaceReceipt(workspacePath) {
|
|
181
|
+
const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
|
|
182
|
+
const status = lstatIfExists(receiptPath);
|
|
183
|
+
if (!status) return null;
|
|
184
|
+
if (!status.isFile() || status.isSymbolicLink()) {
|
|
185
|
+
throw new Error(`Workspace hydration receipt is not a regular file: ${receiptPath}`);
|
|
186
|
+
}
|
|
187
|
+
const content = fs.readFileSync(receiptPath, "utf8");
|
|
188
|
+
return parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
|
|
189
|
+
}
|
|
190
|
+
function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
|
|
191
|
+
const resolvedWorkspacePath = path.resolve(workspacePath);
|
|
192
|
+
if (!fs.existsSync(path.join(resolvedWorkspacePath, ".git"))) return true;
|
|
193
|
+
if (mounts) validateMounts(resolvedWorkspacePath, mounts);
|
|
194
|
+
const head = revParse(resolvedWorkspacePath, "HEAD");
|
|
195
|
+
if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
|
|
196
|
+
if (head === null) return true;
|
|
197
|
+
const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
|
|
198
|
+
return mounts ? workspaceHydrationReceiptMatchesMounts(receipt, head, configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts)) : receipt?.head === head;
|
|
199
|
+
}
|
|
200
|
+
function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
201
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(receipt.head)) {
|
|
202
|
+
throw new Error(`Cannot record invalid fully hydrated workspace object id: ${receipt.head}`);
|
|
203
|
+
}
|
|
204
|
+
if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
|
|
205
|
+
throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${receipt.head}`);
|
|
206
|
+
}
|
|
207
|
+
readHydratedWorkspaceReceipt(workspacePath);
|
|
208
|
+
const gitDirectory = path.join(workspacePath, ".git");
|
|
209
|
+
const gitDirectoryStatus = fs.lstatSync(gitDirectory);
|
|
210
|
+
if (!gitDirectoryStatus.isDirectory() || gitDirectoryStatus.isSymbolicLink()) {
|
|
211
|
+
throw new Error(`Workspace Git directory is not a regular directory: ${gitDirectory}`);
|
|
212
|
+
}
|
|
213
|
+
const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
|
|
214
|
+
const receiptDirectory = path.dirname(receiptPath);
|
|
215
|
+
const existingReceiptDirectory = lstatIfExists(receiptDirectory);
|
|
216
|
+
if (!existingReceiptDirectory) {
|
|
217
|
+
fs.mkdirSync(receiptDirectory, { recursive: false, mode: 448 });
|
|
218
|
+
fsyncDirectory(gitDirectory);
|
|
219
|
+
}
|
|
220
|
+
const receiptDirectoryStatus = fs.lstatSync(receiptDirectory);
|
|
221
|
+
if (!receiptDirectoryStatus.isDirectory() || receiptDirectoryStatus.isSymbolicLink()) {
|
|
222
|
+
throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
|
|
223
|
+
}
|
|
224
|
+
const temporaryPath = path.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
225
|
+
let descriptor;
|
|
226
|
+
try {
|
|
227
|
+
descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
|
|
228
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(receipt)}
|
|
229
|
+
`, "utf8");
|
|
230
|
+
fs.fsyncSync(descriptor);
|
|
231
|
+
fs.closeSync(descriptor);
|
|
232
|
+
descriptor = void 0;
|
|
233
|
+
fs.renameSync(temporaryPath, receiptPath);
|
|
234
|
+
fsyncDirectory(receiptDirectory);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
237
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function hydrationTransactionPath(workspacePath) {
|
|
242
|
+
return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_TRANSACTION.split("/"));
|
|
243
|
+
}
|
|
244
|
+
function workspacePrivateStateDirectory(workspacePath) {
|
|
245
|
+
const gitDirectory = path.join(workspacePath, ".git");
|
|
246
|
+
const gitDirectoryStatus = fs.lstatSync(gitDirectory);
|
|
247
|
+
if (!gitDirectoryStatus.isDirectory() || gitDirectoryStatus.isSymbolicLink()) {
|
|
248
|
+
throw new Error(`Workspace Git directory is not a regular directory: ${gitDirectory}`);
|
|
249
|
+
}
|
|
250
|
+
const stateDirectory = path.dirname(hydrationTransactionPath(workspacePath));
|
|
251
|
+
const existing = lstatIfExists(stateDirectory);
|
|
252
|
+
if (!existing) {
|
|
253
|
+
fs.mkdirSync(stateDirectory, { recursive: false, mode: 448 });
|
|
254
|
+
fsyncDirectory(gitDirectory);
|
|
255
|
+
} else if (!existing.isDirectory() || existing.isSymbolicLink()) {
|
|
256
|
+
throw new Error(`Workspace Git private state is not a regular directory: ${stateDirectory}`);
|
|
257
|
+
}
|
|
258
|
+
return stateDirectory;
|
|
259
|
+
}
|
|
260
|
+
function writePrivateFileDurably(filePath, content) {
|
|
261
|
+
const descriptor = fs.openSync(
|
|
262
|
+
filePath,
|
|
263
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
|
|
264
|
+
384
|
|
265
|
+
);
|
|
266
|
+
try {
|
|
267
|
+
fs.writeFileSync(descriptor, content, "utf8");
|
|
268
|
+
fs.fsyncSync(descriptor);
|
|
269
|
+
} finally {
|
|
270
|
+
fs.closeSync(descriptor);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function fsyncTree(rootPath) {
|
|
274
|
+
const syncEntry = (entryPath) => {
|
|
275
|
+
const status = fs.lstatSync(entryPath);
|
|
276
|
+
if (status.isSymbolicLink()) return;
|
|
277
|
+
if (status.isDirectory()) {
|
|
278
|
+
for (const name of fs.readdirSync(entryPath)) syncEntry(path.join(entryPath, name));
|
|
279
|
+
fsyncDirectory(entryPath);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (!status.isFile()) return;
|
|
283
|
+
const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
284
|
+
try {
|
|
285
|
+
fs.fsyncSync(descriptor);
|
|
286
|
+
} finally {
|
|
287
|
+
fs.closeSync(descriptor);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
syncEntry(rootPath);
|
|
291
|
+
}
|
|
292
|
+
function parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, content) {
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(content);
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
|
|
298
|
+
}
|
|
299
|
+
if (!parsed || typeof parsed !== "object") {
|
|
300
|
+
throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
|
|
301
|
+
}
|
|
302
|
+
const candidate = parsed;
|
|
303
|
+
if (candidate.version !== 1 || candidate.state !== "durable" && candidate.state !== "transition" || candidate.head !== null && (typeof candidate.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head))) {
|
|
304
|
+
throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
|
|
305
|
+
}
|
|
306
|
+
const record = {
|
|
307
|
+
version: 1,
|
|
308
|
+
state: candidate.state,
|
|
309
|
+
head: candidate.head
|
|
310
|
+
};
|
|
311
|
+
if (content !== `${JSON.stringify(record)}
|
|
312
|
+
`) {
|
|
313
|
+
throw new Error(`Workspace checkout durability record is noncanonical: ${recordPath}`);
|
|
314
|
+
}
|
|
315
|
+
if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
|
|
316
|
+
throw new Error(`Workspace checkout durability record names a missing commit: ${recordPath}`);
|
|
317
|
+
}
|
|
318
|
+
return record;
|
|
319
|
+
}
|
|
320
|
+
function readWorkspaceCheckoutDurabilityRecord(workspacePath) {
|
|
321
|
+
const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
|
|
322
|
+
const status = lstatIfExists(recordPath);
|
|
323
|
+
if (!status) return null;
|
|
324
|
+
if (!status.isFile() || status.isSymbolicLink()) {
|
|
325
|
+
throw new Error(`Workspace checkout durability record is not a regular file: ${recordPath}`);
|
|
326
|
+
}
|
|
327
|
+
return parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, fs.readFileSync(recordPath, "utf8"));
|
|
328
|
+
}
|
|
329
|
+
function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
|
|
330
|
+
if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
|
|
331
|
+
throw new Error(`Cannot record workspace checkout durability for a missing commit: ${record.head}`);
|
|
332
|
+
}
|
|
333
|
+
readWorkspaceCheckoutDurabilityRecord(workspacePath);
|
|
334
|
+
const stateDirectory = workspacePrivateStateDirectory(workspacePath);
|
|
335
|
+
const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
|
|
336
|
+
const temporaryPath = path.join(stateDirectory, `.workspace-checkout-durability.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
337
|
+
try {
|
|
338
|
+
writePrivateFileDurably(temporaryPath, `${JSON.stringify(record)}
|
|
339
|
+
`);
|
|
340
|
+
fs.renameSync(temporaryPath, recordPath);
|
|
341
|
+
fsyncDirectory(stateDirectory);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function fsyncWorkspaceCheckoutTree(workspacePath) {
|
|
348
|
+
const status = fs.lstatSync(workspacePath);
|
|
349
|
+
if (!status.isDirectory() || status.isSymbolicLink()) {
|
|
350
|
+
throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
|
|
351
|
+
}
|
|
352
|
+
for (const name of fs.readdirSync(workspacePath)) {
|
|
353
|
+
if (name === ".git") continue;
|
|
354
|
+
fsyncTree(path.join(workspacePath, name));
|
|
355
|
+
}
|
|
356
|
+
fsyncDirectory(workspacePath);
|
|
357
|
+
}
|
|
358
|
+
function workspaceRebaseInProgress(workspacePath) {
|
|
359
|
+
const gitDirectory = path.join(workspacePath, ".git");
|
|
360
|
+
return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
|
|
361
|
+
}
|
|
362
|
+
function clearWorkspaceCheckoutTree(workspacePath) {
|
|
363
|
+
for (const name of fs.readdirSync(workspacePath)) {
|
|
364
|
+
if (name === ".git") continue;
|
|
365
|
+
fs.rmSync(path.join(workspacePath, name), { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
fsyncDirectory(workspacePath);
|
|
368
|
+
}
|
|
369
|
+
function recoverWorkspaceCheckoutDurability(workspacePath) {
|
|
370
|
+
workspacePath = path.resolve(workspacePath);
|
|
371
|
+
const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
|
|
372
|
+
const observedHead = revParse(workspacePath, "HEAD");
|
|
373
|
+
if (record?.state === "durable" && record.head === observedHead) return;
|
|
374
|
+
let targetHead = record?.state === "transition" ? record.head : observedHead;
|
|
375
|
+
if (record?.state === "transition" && observedHead && observedHead !== record.head) {
|
|
376
|
+
const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
377
|
+
if (hydrationReceipt?.head === observedHead) {
|
|
378
|
+
targetHead = observedHead;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (workspaceRebaseInProgress(workspacePath)) {
|
|
382
|
+
const abort = gitResult(workspacePath, ["rebase", "--abort"]);
|
|
383
|
+
if (abort.exitCode !== 0) {
|
|
384
|
+
throw new Error(`Abort interrupted workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
|
|
385
|
+
}
|
|
386
|
+
if (record?.state !== "transition") targetHead = revParse(workspacePath, "HEAD");
|
|
387
|
+
}
|
|
388
|
+
clearWorkspaceCheckoutTree(workspacePath);
|
|
389
|
+
if (targetHead) {
|
|
390
|
+
git(workspacePath, ["reset", "--hard", targetHead], "reconstruct durable workspace checkout");
|
|
391
|
+
if (revParse(workspacePath, "HEAD") !== targetHead) {
|
|
392
|
+
throw new Error(`Workspace checkout reconstruction did not restore ${targetHead}`);
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
git(workspacePath, ["symbolic-ref", "HEAD", `refs/heads/${WORKSPACE_GIT_BRANCH}`], "restore unborn workspace HEAD");
|
|
396
|
+
git(workspacePath, ["update-ref", "-d", `refs/heads/${WORKSPACE_GIT_BRANCH}`], "remove interrupted workspace branch ref");
|
|
397
|
+
git(workspacePath, ["read-tree", "--empty"], "reconstruct unborn workspace checkout");
|
|
398
|
+
if (revParse(workspacePath, "HEAD") !== null) {
|
|
399
|
+
throw new Error("Workspace checkout reconstruction did not restore its unborn HEAD");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
fsyncWorkspaceCheckoutTree(workspacePath);
|
|
403
|
+
updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "durable", head: targetHead });
|
|
404
|
+
}
|
|
405
|
+
function workspaceCheckoutDurabilityIsCurrent(workspacePath) {
|
|
406
|
+
const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
|
|
407
|
+
return Boolean(record?.state === "durable" && record.head === revParse(workspacePath, "HEAD"));
|
|
408
|
+
}
|
|
409
|
+
function beginWorkspaceCheckoutTransition(workspacePath) {
|
|
410
|
+
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
411
|
+
const head = revParse(workspacePath, "HEAD");
|
|
412
|
+
updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "transition", head });
|
|
413
|
+
}
|
|
414
|
+
function completeWorkspaceCheckoutTransition(workspacePath) {
|
|
415
|
+
fsyncWorkspaceCheckoutTree(workspacePath);
|
|
416
|
+
updateWorkspaceCheckoutDurabilityRecord(workspacePath, {
|
|
417
|
+
version: 1,
|
|
418
|
+
state: "durable",
|
|
419
|
+
head: revParse(workspacePath, "HEAD")
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
|
|
423
|
+
try {
|
|
424
|
+
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
425
|
+
} catch (recoveryError) {
|
|
426
|
+
throw new AggregateError(
|
|
427
|
+
[error, recoveryError],
|
|
428
|
+
`Workspace checkout mutation failed and its durable head could not be restored: ${error instanceof Error ? error.message : String(error)}`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
function runWorkspaceCheckoutTransition(workspacePath, mutate) {
|
|
434
|
+
beginWorkspaceCheckoutTransition(workspacePath);
|
|
435
|
+
try {
|
|
436
|
+
mutate();
|
|
437
|
+
completeWorkspaceCheckoutTransition(workspacePath);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function parseHydrationTransactionManifest(transactionPath) {
|
|
443
|
+
const manifestPath = path.join(transactionPath, "manifest.json");
|
|
444
|
+
const workspacePath = path.resolve(transactionPath, "..", "..", "..");
|
|
445
|
+
const status = fs.lstatSync(manifestPath);
|
|
446
|
+
if (!status.isFile() || status.isSymbolicLink()) {
|
|
447
|
+
throw new Error(`Workspace hydration transaction manifest is not a regular file: ${manifestPath}`);
|
|
448
|
+
}
|
|
449
|
+
let parsed;
|
|
450
|
+
try {
|
|
451
|
+
parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`, { cause: error });
|
|
454
|
+
}
|
|
455
|
+
if (!parsed || typeof parsed !== "object") {
|
|
456
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
457
|
+
}
|
|
458
|
+
const candidate = parsed;
|
|
459
|
+
if (candidate.version !== 2 || !Array.isArray(candidate.mounts)) {
|
|
460
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
461
|
+
}
|
|
462
|
+
const parseReceipt = (rawReceipt) => {
|
|
463
|
+
if (rawReceipt === null) return null;
|
|
464
|
+
if (!rawReceipt || typeof rawReceipt !== "object") {
|
|
465
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
466
|
+
}
|
|
467
|
+
return parseWorkspaceHydrationReceiptContent(workspacePath, manifestPath, `${JSON.stringify(rawReceipt)}
|
|
468
|
+
`);
|
|
469
|
+
};
|
|
470
|
+
const targetReceipt = parseReceipt(candidate.targetReceipt);
|
|
471
|
+
const receiptBefore = parseReceipt(candidate.receiptBefore);
|
|
472
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
473
|
+
const seenSnapshots = /* @__PURE__ */ new Set();
|
|
474
|
+
const mounts = candidate.mounts.map((rawMount) => {
|
|
475
|
+
if (!rawMount || typeof rawMount !== "object") {
|
|
476
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
477
|
+
}
|
|
478
|
+
const mount = rawMount;
|
|
479
|
+
if (typeof mount.id !== "string" || !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 || typeof mount.workspaceRelativePath !== "string" || typeof mount.existed !== "boolean" || typeof mount.snapshotName !== "string" || !/^[0-9]+$/.test(mount.snapshotName) || seenIds.has(mount.id) || seenSnapshots.has(mount.snapshotName)) {
|
|
480
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
481
|
+
}
|
|
482
|
+
const relative = path.relative(mount.durabilityRootPath, mount.sourcePath);
|
|
483
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
484
|
+
throw new Error(`Workspace hydration transaction source escapes its durability root: ${mount.sourcePath}`);
|
|
485
|
+
}
|
|
486
|
+
seenIds.add(mount.id);
|
|
487
|
+
seenSnapshots.add(mount.snapshotName);
|
|
488
|
+
return mount;
|
|
489
|
+
});
|
|
490
|
+
return {
|
|
491
|
+
version: 2,
|
|
492
|
+
targetReceipt,
|
|
493
|
+
receiptBefore,
|
|
494
|
+
mounts
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function removeHydrationTransaction(workspacePath) {
|
|
498
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
499
|
+
fs.rmSync(transactionPath, { recursive: true, force: true });
|
|
500
|
+
fsyncDirectory(path.dirname(transactionPath));
|
|
501
|
+
}
|
|
502
|
+
function hydrationTransactionCommitted(transactionPath) {
|
|
503
|
+
const markerPath = path.join(transactionPath, "committed");
|
|
504
|
+
const markerStatus = lstatIfExists(markerPath);
|
|
505
|
+
if (!markerStatus) return false;
|
|
506
|
+
if (!markerStatus.isFile() || markerStatus.isSymbolicLink() || fs.readFileSync(markerPath, "utf8") !== "committed\n") {
|
|
507
|
+
throw new Error(`Workspace hydration transaction commit marker is invalid: ${markerPath}`);
|
|
508
|
+
}
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
function restoreHydrationTransaction(workspacePath, manifest) {
|
|
512
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
513
|
+
for (const mount of manifest.mounts) {
|
|
514
|
+
assertManagedDirectoryPath({
|
|
515
|
+
trustedRoot: mount.durabilityRootPath,
|
|
516
|
+
candidate: mount.sourcePath,
|
|
517
|
+
label: `Workspace hydration recovery mount ${mount.id}`
|
|
518
|
+
});
|
|
519
|
+
if (!mount.existed) {
|
|
520
|
+
fs.rmSync(mount.sourcePath, { recursive: true, force: true });
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const snapshotPath = path.join(transactionPath, "mounts", mount.snapshotName);
|
|
524
|
+
const snapshotStatus = fs.lstatSync(snapshotPath);
|
|
525
|
+
if (!snapshotStatus.isDirectory() || snapshotStatus.isSymbolicLink()) {
|
|
526
|
+
throw new Error(`Workspace hydration snapshot is not a regular directory: ${snapshotPath}`);
|
|
527
|
+
}
|
|
528
|
+
mirrorWorkingTree({ sourceRoot: snapshotPath, targetRoot: mount.sourcePath, sourceMode: "all", deletionMode: "all" });
|
|
529
|
+
}
|
|
530
|
+
fsyncHydratedWorkspaceMounts(
|
|
531
|
+
manifest.mounts.map((mount) => ({
|
|
532
|
+
...mount,
|
|
533
|
+
hydrationIncarnationKey: mount.incarnationKey,
|
|
534
|
+
sourceMode: "all",
|
|
535
|
+
hydrateDeletionMode: "all"
|
|
536
|
+
}))
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
function recoverPendingHydrationTransaction(workspacePath) {
|
|
540
|
+
const stateDirectory = path.dirname(hydrationTransactionPath(workspacePath));
|
|
541
|
+
const stateStatus = lstatIfExists(stateDirectory);
|
|
542
|
+
if (!stateStatus) return;
|
|
543
|
+
if (!stateStatus.isDirectory() || stateStatus.isSymbolicLink()) {
|
|
544
|
+
throw new Error(`Workspace Git private state is not a regular directory: ${stateDirectory}`);
|
|
545
|
+
}
|
|
546
|
+
for (const name of fs.readdirSync(stateDirectory)) {
|
|
547
|
+
if (!name.startsWith(".workspace-hydration-transaction.") || !name.endsWith(".tmp")) continue;
|
|
548
|
+
fs.rmSync(path.join(stateDirectory, name), { recursive: true, force: true });
|
|
549
|
+
fsyncDirectory(stateDirectory);
|
|
550
|
+
}
|
|
551
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
552
|
+
const transactionStatus = lstatIfExists(transactionPath);
|
|
553
|
+
if (!transactionStatus) return;
|
|
554
|
+
if (!transactionStatus.isDirectory() || transactionStatus.isSymbolicLink()) {
|
|
555
|
+
throw new Error(`Workspace hydration transaction is not a regular directory: ${transactionPath}`);
|
|
556
|
+
}
|
|
557
|
+
const manifest = parseHydrationTransactionManifest(transactionPath);
|
|
558
|
+
if (hydrationTransactionCommitted(transactionPath)) {
|
|
559
|
+
const receipt2 = readHydratedWorkspaceReceipt(workspacePath);
|
|
560
|
+
if (manifest.targetReceipt && !workspaceHydrationReceiptsEqual(receipt2, manifest.targetReceipt)) {
|
|
561
|
+
updateHydratedWorkspaceReceipt(workspacePath, manifest.targetReceipt);
|
|
562
|
+
}
|
|
563
|
+
removeHydrationTransaction(workspacePath);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const receipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
567
|
+
if (manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
|
|
568
|
+
removeHydrationTransaction(workspacePath);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
restoreHydrationTransaction(workspacePath, manifest);
|
|
572
|
+
removeHydrationTransaction(workspacePath);
|
|
573
|
+
}
|
|
574
|
+
function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
575
|
+
recoverPendingHydrationTransaction(workspacePath);
|
|
576
|
+
const stateDirectory = workspacePrivateStateDirectory(workspacePath);
|
|
577
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
578
|
+
const stagingPath = path.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
579
|
+
fs.mkdirSync(stagingPath, { mode: 448 });
|
|
580
|
+
try {
|
|
581
|
+
const snapshotRoot = path.join(stagingPath, "mounts");
|
|
582
|
+
fs.mkdirSync(snapshotRoot, { mode: 448 });
|
|
583
|
+
const manifest = {
|
|
584
|
+
version: 2,
|
|
585
|
+
targetReceipt,
|
|
586
|
+
receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
|
|
587
|
+
mounts: mounts.map((mount, index) => {
|
|
588
|
+
const sourcePath = path.resolve(mount.sourcePath);
|
|
589
|
+
const durabilityRootPath = path.resolve(mount.durabilityRootPath);
|
|
590
|
+
assertManagedDirectoryPath({
|
|
591
|
+
trustedRoot: durabilityRootPath,
|
|
592
|
+
candidate: sourcePath,
|
|
593
|
+
label: `Workspace hydration snapshot mount ${mount.id}`
|
|
594
|
+
});
|
|
595
|
+
const sourceStatus = lstatIfExists(sourcePath);
|
|
596
|
+
if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
|
|
597
|
+
throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
|
|
598
|
+
}
|
|
599
|
+
const snapshotName = String(index);
|
|
600
|
+
const snapshotPath = path.join(snapshotRoot, snapshotName);
|
|
601
|
+
fs.mkdirSync(snapshotPath, { mode: 448 });
|
|
602
|
+
if (sourceStatus) {
|
|
603
|
+
mirrorWorkingTree({
|
|
604
|
+
sourceRoot: sourcePath,
|
|
605
|
+
targetRoot: snapshotPath,
|
|
606
|
+
sourceMode: "all",
|
|
607
|
+
deletionMode: "all",
|
|
608
|
+
// This tree is still private and the source authority is untouched.
|
|
609
|
+
// The single recursive barrier below makes the complete snapshot
|
|
610
|
+
// durable before its directory entry is published or hydration starts.
|
|
611
|
+
durability: "deferred_private_staging"
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
id: mount.id,
|
|
616
|
+
incarnationKey: mount.hydrationIncarnationKey,
|
|
617
|
+
sourcePath,
|
|
618
|
+
durabilityRootPath,
|
|
619
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
620
|
+
existed: Boolean(sourceStatus),
|
|
621
|
+
snapshotName
|
|
622
|
+
};
|
|
623
|
+
})
|
|
624
|
+
};
|
|
625
|
+
writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
|
|
626
|
+
`);
|
|
627
|
+
fsyncTree(stagingPath);
|
|
628
|
+
fs.renameSync(stagingPath, transactionPath);
|
|
629
|
+
fsyncDirectory(stateDirectory);
|
|
630
|
+
return manifest;
|
|
631
|
+
} catch (error) {
|
|
632
|
+
fs.rmSync(stagingPath, { recursive: true, force: true });
|
|
633
|
+
fsyncDirectory(stateDirectory);
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function markHydrationTransactionDurable(workspacePath) {
|
|
638
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
639
|
+
writePrivateFileDurably(path.join(transactionPath, "committed"), "committed\n");
|
|
640
|
+
fsyncDirectory(transactionPath);
|
|
641
|
+
}
|
|
642
|
+
function fsyncHydratedWorkspaceMount(mount) {
|
|
643
|
+
assertManagedDirectoryPath({
|
|
644
|
+
trustedRoot: mount.durabilityRootPath,
|
|
645
|
+
candidate: mount.sourcePath,
|
|
646
|
+
label: `Workspace mount ${mount.id} source`
|
|
647
|
+
});
|
|
648
|
+
const syncEntry = (entryPath, root) => {
|
|
649
|
+
const status = fs.lstatSync(entryPath);
|
|
650
|
+
if (status.isSymbolicLink()) return;
|
|
651
|
+
if (status.isDirectory()) {
|
|
652
|
+
for (const name of fs.readdirSync(entryPath)) {
|
|
653
|
+
if (root && name === ".git") continue;
|
|
654
|
+
syncEntry(path.join(entryPath, name), false);
|
|
655
|
+
}
|
|
656
|
+
fsyncDirectory(entryPath);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (!status.isFile()) return;
|
|
660
|
+
const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY);
|
|
661
|
+
try {
|
|
662
|
+
fs.fsyncSync(descriptor);
|
|
663
|
+
} finally {
|
|
664
|
+
fs.closeSync(descriptor);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
if (fs.existsSync(mount.sourcePath)) syncEntry(mount.sourcePath, true);
|
|
668
|
+
const durabilityRoot = path.resolve(mount.durabilityRootPath);
|
|
669
|
+
let boundaryParent = path.dirname(path.resolve(mount.sourcePath));
|
|
670
|
+
while (true) {
|
|
671
|
+
if (fs.existsSync(boundaryParent)) {
|
|
672
|
+
const boundaryStatus = fs.lstatSync(boundaryParent);
|
|
673
|
+
if (!boundaryStatus.isDirectory() || boundaryStatus.isSymbolicLink()) {
|
|
674
|
+
throw new Error(`Workspace mount durability boundary is not a regular directory: ${boundaryParent}`);
|
|
675
|
+
}
|
|
676
|
+
fsyncDirectory(boundaryParent);
|
|
677
|
+
}
|
|
678
|
+
if (boundaryParent === durabilityRoot) break;
|
|
679
|
+
const nextParent = path.dirname(boundaryParent);
|
|
680
|
+
if (nextParent === boundaryParent) {
|
|
681
|
+
throw new Error(`Workspace mount ${mount.sourcePath} does not descend from durability root ${durabilityRoot}`);
|
|
682
|
+
}
|
|
683
|
+
boundaryParent = nextParent;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function fsyncHydratedWorkspaceMounts(mounts) {
|
|
687
|
+
for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
|
|
688
|
+
}
|
|
51
689
|
function workspaceIsShallow(workspacePath) {
|
|
52
690
|
return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
|
|
53
691
|
}
|
|
@@ -60,7 +698,22 @@ function validateMounts(workspacePath, mounts) {
|
|
|
60
698
|
const paths = [];
|
|
61
699
|
for (const mount of mounts) {
|
|
62
700
|
if (!mount.id || ids.has(mount.id)) throw new Error(`Workspace mount id must be unique: ${mount.id}`);
|
|
701
|
+
if (!mount.hydrationIncarnationKey || mount.hydrationIncarnationKey.includes("\0")) {
|
|
702
|
+
throw new Error(`Workspace mount hydration incarnation is invalid: ${mount.id}`);
|
|
703
|
+
}
|
|
63
704
|
ids.add(mount.id);
|
|
705
|
+
const sourcePath = path.resolve(mount.sourcePath);
|
|
706
|
+
const durabilityRoot = path.resolve(mount.durabilityRootPath);
|
|
707
|
+
const durabilityRelative = path.relative(durabilityRoot, sourcePath);
|
|
708
|
+
const durabilityRootStatus = fs.lstatSync(durabilityRoot);
|
|
709
|
+
if (!durabilityRootStatus.isDirectory() || durabilityRootStatus.isSymbolicLink() || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.isAbsolute(durabilityRelative)) {
|
|
710
|
+
throw new Error(`Workspace mount source escapes its durability root: ${mount.sourcePath}`);
|
|
711
|
+
}
|
|
712
|
+
assertManagedDirectoryPath({
|
|
713
|
+
trustedRoot: durabilityRoot,
|
|
714
|
+
candidate: sourcePath,
|
|
715
|
+
label: `Workspace mount ${mount.id} source`
|
|
716
|
+
});
|
|
64
717
|
const normalized = mount.workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
65
718
|
if (!normalized || normalized === "." || normalized.split("/").some((segment) => segment === ".." || segment === ".git")) {
|
|
66
719
|
throw new Error(`Invalid workspace mount path: ${mount.workspaceRelativePath}`);
|
|
@@ -107,6 +760,8 @@ function configureWorkspaceRepository(input) {
|
|
|
107
760
|
if (!name || !email) throw new Error("Workspace Git identity must include name and email");
|
|
108
761
|
git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
109
762
|
git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
763
|
+
git(input.workspacePath, ["config", "--local", "core.fsync", "committed"], "configure durable workspace commits");
|
|
764
|
+
git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
|
|
110
765
|
}
|
|
111
766
|
function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
112
767
|
const result = gitResult(workspacePath, [
|
|
@@ -130,7 +785,24 @@ ${result.stdout}`;
|
|
|
130
785
|
}
|
|
131
786
|
function ensureWorkspaceGitClone(input) {
|
|
132
787
|
const workspacePath = path.resolve(input.workspacePath);
|
|
788
|
+
const workspaceParent = path.dirname(workspacePath);
|
|
789
|
+
const parentStatus = fs.lstatSync(workspaceParent);
|
|
790
|
+
if (!parentStatus.isDirectory() || parentStatus.isSymbolicLink()) {
|
|
791
|
+
throw new Error(`Workspace clone parent is not a regular directory: ${workspaceParent}`);
|
|
792
|
+
}
|
|
793
|
+
const workspaceStatus = lstatIfExists(workspacePath);
|
|
794
|
+
if (workspaceStatus) {
|
|
795
|
+
if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink()) {
|
|
796
|
+
throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
133
799
|
const gitPath = path.join(workspacePath, ".git");
|
|
800
|
+
const gitStatus = lstatIfExists(gitPath);
|
|
801
|
+
if (gitStatus) {
|
|
802
|
+
if (!gitStatus.isDirectory() || gitStatus.isSymbolicLink()) {
|
|
803
|
+
throw new Error(`Workspace clone Git metadata is not a regular directory: ${gitPath}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
134
806
|
if (!fs.existsSync(gitPath)) {
|
|
135
807
|
fs.rmSync(workspacePath, { recursive: true, force: true });
|
|
136
808
|
fs.mkdirSync(path.dirname(workspacePath), { recursive: true });
|
|
@@ -152,6 +824,7 @@ function ensureWorkspaceGitClone(input) {
|
|
|
152
824
|
}
|
|
153
825
|
}
|
|
154
826
|
configureWorkspaceRepository({ ...input, workspacePath });
|
|
827
|
+
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
155
828
|
const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
156
829
|
const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
|
|
157
830
|
if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
@@ -160,17 +833,21 @@ function ensureWorkspaceGitClone(input) {
|
|
|
160
833
|
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
161
834
|
let localHead = revParse(workspacePath, "HEAD");
|
|
162
835
|
if (!localHead && remoteHead) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
836
|
+
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
837
|
+
git(
|
|
838
|
+
workspacePath,
|
|
839
|
+
["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
|
|
840
|
+
"check out workspace main"
|
|
841
|
+
);
|
|
842
|
+
});
|
|
168
843
|
localHead = remoteHead;
|
|
169
844
|
}
|
|
170
845
|
if (localHead && remoteHead && localHead === remoteHead) {
|
|
171
846
|
updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
172
847
|
} else if (localHead && remoteHead && workspaceIsShallow(workspacePath) && !tryGit(workspacePath, ["merge-base", localHead, remoteHead]) && revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) === localHead && workspaceIsClean(workspacePath)) {
|
|
173
|
-
|
|
848
|
+
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
849
|
+
git(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
|
|
850
|
+
});
|
|
174
851
|
updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
175
852
|
localHead = remoteHead;
|
|
176
853
|
}
|
|
@@ -190,6 +867,16 @@ function activeMounts(mounts) {
|
|
|
190
867
|
}
|
|
191
868
|
function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
192
869
|
for (const mount of mounts) {
|
|
870
|
+
assertManagedDirectoryPath({
|
|
871
|
+
trustedRoot: mount.durabilityRootPath,
|
|
872
|
+
candidate: mount.sourcePath,
|
|
873
|
+
label: `Workspace mount ${mount.id} source`
|
|
874
|
+
});
|
|
875
|
+
assertManagedDirectoryPath({
|
|
876
|
+
trustedRoot: workspacePath,
|
|
877
|
+
candidate: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
878
|
+
label: `Workspace mount ${mount.id} outer target`
|
|
879
|
+
});
|
|
193
880
|
mirrorWorkingTree({
|
|
194
881
|
sourceRoot: mount.sourcePath,
|
|
195
882
|
targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
@@ -201,6 +888,11 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
|
201
888
|
function removeWorkspaceMounts(workspacePath, mounts) {
|
|
202
889
|
for (const mount of mounts) {
|
|
203
890
|
const targetPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
891
|
+
assertManagedDirectoryPath({
|
|
892
|
+
trustedRoot: workspacePath,
|
|
893
|
+
candidate: targetPath,
|
|
894
|
+
label: `Workspace mount ${mount.id} outer target`
|
|
895
|
+
});
|
|
204
896
|
const relative = path.relative(path.resolve(workspacePath), path.resolve(targetPath));
|
|
205
897
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
206
898
|
throw new Error(`Workspace mount escapes the workspace clone: ${mount.workspaceRelativePath}`);
|
|
@@ -208,9 +900,45 @@ function removeWorkspaceMounts(workspacePath, mounts) {
|
|
|
208
900
|
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
209
901
|
}
|
|
210
902
|
}
|
|
211
|
-
function
|
|
903
|
+
function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
|
|
904
|
+
const head = revParse(workspacePath, "HEAD");
|
|
212
905
|
for (const mount of mounts) {
|
|
906
|
+
const normalized = normalizedWorkspaceMountPath(mount.workspaceRelativePath);
|
|
907
|
+
const targetPath = path.join(workspacePath, ...normalized.split("/"));
|
|
908
|
+
assertManagedDirectoryPath({
|
|
909
|
+
trustedRoot: workspacePath,
|
|
910
|
+
candidate: targetPath,
|
|
911
|
+
label: `Deferred workspace mount ${mount.id} outer target`
|
|
912
|
+
});
|
|
913
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
914
|
+
if (head && tryGit(workspacePath, ["cat-file", "-e", `${head}:${normalized}`])) {
|
|
915
|
+
git(
|
|
916
|
+
workspacePath,
|
|
917
|
+
["checkout", "--no-recurse-submodules", head, "--", `:(literal)${normalized}`],
|
|
918
|
+
`restore deferred workspace mount ${mount.id} from its integration basis`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
924
|
+
const hydratedMountIds = [];
|
|
925
|
+
const skippedMountIds = [];
|
|
926
|
+
for (const mount of mounts) {
|
|
927
|
+
if (!options.ignoreBusy && mount.busy?.()) {
|
|
928
|
+
skippedMountIds.push(mount.id);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
213
931
|
const sourceRoot = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
932
|
+
assertManagedDirectoryPath({
|
|
933
|
+
trustedRoot: workspacePath,
|
|
934
|
+
candidate: sourceRoot,
|
|
935
|
+
label: `Workspace mount ${mount.id} outer source`
|
|
936
|
+
});
|
|
937
|
+
assertManagedDirectoryPath({
|
|
938
|
+
trustedRoot: mount.durabilityRootPath,
|
|
939
|
+
candidate: mount.sourcePath,
|
|
940
|
+
label: `Workspace mount ${mount.id} hydration target`
|
|
941
|
+
});
|
|
214
942
|
if (!fs.existsSync(sourceRoot)) {
|
|
215
943
|
if (mount.hydrateDeletionMode === "all") {
|
|
216
944
|
fs.rmSync(mount.sourcePath, { recursive: true, force: true });
|
|
@@ -227,37 +955,167 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts) {
|
|
|
227
955
|
fs.rmSync(emptyRoot, { recursive: true, force: true });
|
|
228
956
|
}
|
|
229
957
|
}
|
|
958
|
+
hydratedMountIds.push(mount.id);
|
|
230
959
|
continue;
|
|
231
960
|
}
|
|
961
|
+
const targetExists = fs.existsSync(mount.sourcePath);
|
|
232
962
|
mirrorWorkingTree({
|
|
233
963
|
sourceRoot,
|
|
234
964
|
targetRoot: mount.sourcePath,
|
|
235
965
|
sourceMode: "all",
|
|
236
|
-
|
|
966
|
+
// A missing checkout has no Git index to define git-only deletions. Its
|
|
967
|
+
// newly created directory is empty, so all-mode is equivalent and lets
|
|
968
|
+
// bootstrap hydration preserve the fetched tree before Git initializes.
|
|
969
|
+
deletionMode: targetExists ? mount.hydrateDeletionMode : "all"
|
|
237
970
|
});
|
|
971
|
+
hydratedMountIds.push(mount.id);
|
|
972
|
+
}
|
|
973
|
+
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
|
|
974
|
+
}
|
|
975
|
+
function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
976
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
977
|
+
const mountIds = input.mounts.map(({ id }) => id).sort();
|
|
978
|
+
const requiredIds = (input.requiredMounts ?? input.mounts).map(({ id }) => id).sort();
|
|
979
|
+
if (mountIds.length !== requiredIds.length || mountIds.some((id, index) => id !== requiredIds[index])) {
|
|
980
|
+
return { hydratedMountIds: [], skippedMountIds: requiredIds };
|
|
981
|
+
}
|
|
982
|
+
const busyMountIds = input.ignoreBusy ? [] : (input.durabilityMounts ?? input.mounts).filter((mount) => mount.busy?.()).map(({ id }) => id).sort();
|
|
983
|
+
if (busyMountIds.length > 0) {
|
|
984
|
+
return { hydratedMountIds: [], skippedMountIds: busyMountIds };
|
|
985
|
+
}
|
|
986
|
+
if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
|
|
987
|
+
throw new Error("Workspace checkout is not durably materialized at its current HEAD");
|
|
988
|
+
}
|
|
989
|
+
const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
|
|
990
|
+
const targetReceipt = targetHead ? workspaceHydrationReceipt(targetHead, input.receiptMounts ?? input.durabilityMounts ?? input.mounts) : null;
|
|
991
|
+
const manifest = beginHydrationTransaction(workspacePath, input.mounts, targetReceipt);
|
|
992
|
+
try {
|
|
993
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, input.mounts, { ignoreBusy: true });
|
|
994
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredIds[index])) {
|
|
995
|
+
throw new Error("Workspace hydration did not include every required mount");
|
|
996
|
+
}
|
|
997
|
+
fsyncHydratedWorkspaceMounts(input.durabilityMounts ?? input.mounts);
|
|
998
|
+
markHydrationTransactionDurable(workspacePath);
|
|
999
|
+
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1000
|
+
removeHydrationTransaction(workspacePath);
|
|
1001
|
+
return hydration;
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1004
|
+
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1005
|
+
throw error;
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
restoreHydrationTransaction(workspacePath, manifest);
|
|
1009
|
+
removeHydrationTransaction(workspacePath);
|
|
1010
|
+
} catch (restoreError) {
|
|
1011
|
+
throw new AggregateError(
|
|
1012
|
+
[error, restoreError],
|
|
1013
|
+
`Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
throw error;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
1020
|
+
const gitDirectory = path.join(path.resolve(workspacePath), ".git");
|
|
1021
|
+
if (!fs.existsSync(gitDirectory)) {
|
|
1022
|
+
return hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options);
|
|
1023
|
+
}
|
|
1024
|
+
recoverWorkspaceCheckoutDurability(path.resolve(workspacePath));
|
|
1025
|
+
return hydrateWorkspaceGitMountsTransactionally({
|
|
1026
|
+
workspacePath,
|
|
1027
|
+
mounts,
|
|
1028
|
+
ignoreBusy: options.ignoreBusy
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
1032
|
+
workspacePath = path.resolve(workspacePath);
|
|
1033
|
+
validateMounts(workspacePath, mounts);
|
|
1034
|
+
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
1035
|
+
recoverPendingHydrationTransaction(workspacePath);
|
|
1036
|
+
const currentHead = revParse(workspacePath, "HEAD");
|
|
1037
|
+
if (!currentHead) return;
|
|
1038
|
+
const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1039
|
+
const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
|
|
1040
|
+
const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
|
|
1041
|
+
const uncoveredBasisMounts = configuredBasisMounts.filter(
|
|
1042
|
+
(mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
|
|
1043
|
+
);
|
|
1044
|
+
if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
|
|
1045
|
+
const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1046
|
+
if (busyMountIds.length > 0) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${busyMountIds.join(", ")}`
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
const hydrationTargets = uncoveredBasisMounts.filter(
|
|
1052
|
+
(mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
|
|
1053
|
+
);
|
|
1054
|
+
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1055
|
+
workspacePath,
|
|
1056
|
+
mounts: hydrationTargets,
|
|
1057
|
+
durabilityMounts: uncoveredBasisMounts,
|
|
1058
|
+
receiptMounts: configuredBasisMounts,
|
|
1059
|
+
requiredMounts: hydrationTargets,
|
|
1060
|
+
ignoreBusy: options.ignoreBusy,
|
|
1061
|
+
recordCurrentHead: true
|
|
1062
|
+
});
|
|
1063
|
+
if (hydration.skippedMountIds.length > 0) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
`Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
|
|
1066
|
+
);
|
|
238
1067
|
}
|
|
239
1068
|
}
|
|
240
1069
|
function resetWorkspaceGit(input) {
|
|
241
1070
|
const workspacePath = path.resolve(input.workspacePath);
|
|
242
1071
|
validateMounts(workspacePath, input.mounts);
|
|
243
1072
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1073
|
+
recoverPendingHydrationTransaction(workspacePath);
|
|
244
1074
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
245
1075
|
const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
|
|
246
1076
|
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
247
1077
|
if (remoteHead) {
|
|
248
|
-
|
|
249
|
-
|
|
1078
|
+
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
1079
|
+
git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
|
|
1080
|
+
git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
|
|
1081
|
+
});
|
|
250
1082
|
} else if (revParse(workspacePath, "HEAD")) {
|
|
251
|
-
|
|
252
|
-
|
|
1083
|
+
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
1084
|
+
git(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
|
|
1085
|
+
git(workspacePath, ["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })], "record empty workspace reset");
|
|
1086
|
+
});
|
|
253
1087
|
}
|
|
254
1088
|
const selected = activeMounts(input.mounts);
|
|
255
|
-
|
|
1089
|
+
const requiredLiveMountsBeforeHydration = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
|
|
1090
|
+
const bootstrapMounts = selected.skipped.filter((mount) => {
|
|
1091
|
+
if (mount.deleteWhenSourceMissing || fs.existsSync(mount.sourcePath) || mount.busy?.()) return false;
|
|
1092
|
+
const outerPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
1093
|
+
return fs.existsSync(outerPath);
|
|
1094
|
+
});
|
|
1095
|
+
const hydrationMounts = [...selected.active, ...bootstrapMounts];
|
|
1096
|
+
const requiredLiveMounts = [
|
|
1097
|
+
...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
|
|
1098
|
+
];
|
|
1099
|
+
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1100
|
+
workspacePath,
|
|
1101
|
+
mounts: hydrationMounts,
|
|
1102
|
+
durabilityMounts: [...requiredLiveMounts, ...selected.tombstones],
|
|
1103
|
+
receiptMounts: input.mounts,
|
|
1104
|
+
requiredMounts: requiredLiveMounts,
|
|
1105
|
+
recordCurrentHead: true
|
|
1106
|
+
});
|
|
1107
|
+
const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
|
|
1108
|
+
const resetHead = revParse(workspacePath, "HEAD");
|
|
1109
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredLiveIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredLiveIds[index]) || resetHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), resetHead, input.mounts)) {
|
|
1110
|
+
throw new Error("Workspace reset could not durably hydrate every live mount; remediation remains active");
|
|
1111
|
+
}
|
|
256
1112
|
return {
|
|
257
1113
|
startingHead: initial.localHead,
|
|
258
1114
|
localHead: revParse(workspacePath, "HEAD"),
|
|
259
1115
|
remoteHead,
|
|
260
|
-
discardedPaths
|
|
1116
|
+
discardedPaths,
|
|
1117
|
+
activeMountIds: hydration.hydratedMountIds,
|
|
1118
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
|
|
261
1119
|
};
|
|
262
1120
|
}
|
|
263
1121
|
function stagedPaths(workspacePath) {
|
|
@@ -322,17 +1180,22 @@ function snapshotConflict(input) {
|
|
|
322
1180
|
}
|
|
323
1181
|
function rebaseWorkspace(input) {
|
|
324
1182
|
const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
|
|
1183
|
+
beginWorkspaceCheckoutTransition(input.workspacePath);
|
|
325
1184
|
const rebase = gitResult(
|
|
326
1185
|
input.workspacePath,
|
|
327
1186
|
input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
|
|
328
1187
|
);
|
|
329
|
-
if (rebase.exitCode === 0)
|
|
1188
|
+
if (rebase.exitCode === 0) {
|
|
1189
|
+
completeWorkspaceCheckoutTransition(input.workspacePath);
|
|
1190
|
+
return { ok: true };
|
|
1191
|
+
}
|
|
330
1192
|
const conflicts = conflictPaths(input.workspacePath);
|
|
331
1193
|
const refs = snapshotConflict(input);
|
|
332
1194
|
const abort = gitResult(input.workspacePath, ["rebase", "--abort"]);
|
|
333
1195
|
if (abort.exitCode !== 0) {
|
|
334
1196
|
throw new Error(`Abort workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
|
|
335
1197
|
}
|
|
1198
|
+
completeWorkspaceCheckoutTransition(input.workspacePath);
|
|
336
1199
|
return {
|
|
337
1200
|
ok: false,
|
|
338
1201
|
conflictPaths: conflicts,
|
|
@@ -342,26 +1205,31 @@ function rebaseWorkspace(input) {
|
|
|
342
1205
|
}
|
|
343
1206
|
function synchronizeWithFetchedHead(input) {
|
|
344
1207
|
const localHead = revParse(input.workspacePath, "HEAD");
|
|
345
|
-
|
|
1208
|
+
const remoteHead = input.remoteHead;
|
|
1209
|
+
if (!remoteHead) return { kind: "ready", updated: false, rebased: false };
|
|
346
1210
|
if (!localHead) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
1211
|
+
runWorkspaceCheckoutTransition(input.workspacePath, () => {
|
|
1212
|
+
git(
|
|
1213
|
+
input.workspacePath,
|
|
1214
|
+
["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
|
|
1215
|
+
"check out fetched workspace main"
|
|
1216
|
+
);
|
|
1217
|
+
});
|
|
352
1218
|
return { kind: "ready", updated: true, rebased: false };
|
|
353
1219
|
}
|
|
354
|
-
if (localHead ===
|
|
355
|
-
updateIntegratedWorkspaceHead(input.workspacePath,
|
|
1220
|
+
if (localHead === remoteHead) {
|
|
1221
|
+
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
356
1222
|
return { kind: "ready", updated: false, rebased: false };
|
|
357
1223
|
}
|
|
358
|
-
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead,
|
|
359
|
-
|
|
360
|
-
|
|
1224
|
+
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead, remoteHead])) {
|
|
1225
|
+
runWorkspaceCheckoutTransition(input.workspacePath, () => {
|
|
1226
|
+
git(input.workspacePath, ["reset", "--hard", remoteHead], "fast-forward workspace main");
|
|
1227
|
+
});
|
|
1228
|
+
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
361
1229
|
return { kind: "ready", updated: true, rebased: false };
|
|
362
1230
|
}
|
|
363
|
-
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor",
|
|
364
|
-
updateIntegratedWorkspaceHead(input.workspacePath,
|
|
1231
|
+
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
|
|
1232
|
+
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
365
1233
|
return { kind: "ready", updated: false, rebased: false };
|
|
366
1234
|
}
|
|
367
1235
|
const integratedHead = revParse(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
|
|
@@ -374,11 +1242,11 @@ function synchronizeWithFetchedHead(input) {
|
|
|
374
1242
|
const rebased = rebaseWorkspace({
|
|
375
1243
|
workspacePath: input.workspacePath,
|
|
376
1244
|
localHead,
|
|
377
|
-
remoteHead
|
|
1245
|
+
remoteHead,
|
|
378
1246
|
attemptId: input.attemptId,
|
|
379
1247
|
truncatedHistoryBase
|
|
380
1248
|
});
|
|
381
|
-
if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath,
|
|
1249
|
+
if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
382
1250
|
return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
|
|
383
1251
|
}
|
|
384
1252
|
async function synchronizeWorkspaceGit(input) {
|
|
@@ -387,25 +1255,131 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
387
1255
|
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
388
1256
|
const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
|
|
389
1257
|
validateMounts(workspacePath, input.mounts);
|
|
1258
|
+
const busyLiveMountIds = input.skipMountMirror ? [] : input.mounts.filter((mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()).map(({ id }) => id).sort();
|
|
1259
|
+
if (busyLiveMountIds.length > 0) {
|
|
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
|
+
}
|
|
390
1276
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
391
1277
|
const startingHead = initial.localHead;
|
|
1278
|
+
const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1279
|
+
const deferredMountIds = /* @__PURE__ */ new Set();
|
|
1280
|
+
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead && receiptBeforeInitialHydration?.head === initial.localHead) {
|
|
1281
|
+
for (const mount of input.mounts) {
|
|
1282
|
+
if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && !workspaceHydrationReceiptCoversMount(receiptBeforeInitialHydration, initial.localHead, mount)) {
|
|
1283
|
+
deferredMountIds.add(mount.id);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1288
|
+
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
|
|
1289
|
+
});
|
|
1290
|
+
const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
|
|
392
1291
|
const selected = activeMounts(input.mounts);
|
|
1292
|
+
const projectedActiveMounts = selected.active.filter(({ id }) => !deferredMountIds.has(id));
|
|
1293
|
+
const projectedTombstones = selected.tombstones.filter(({ id }) => !deferredMountIds.has(id));
|
|
1294
|
+
const projectionBasisHead = revParse(workspacePath, "HEAD");
|
|
1295
|
+
const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1296
|
+
const projectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts).filter(
|
|
1297
|
+
(mount) => !deferredMountIds.has(mount.id) && Boolean(projectionBasisHead && workspaceHydrationReceiptCoversMount(projectionBasisReceipt, projectionBasisHead, mount))
|
|
1298
|
+
);
|
|
1299
|
+
const completedMountSelection = (requireCurrentHeadReceipt = false) => {
|
|
1300
|
+
const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
|
|
1301
|
+
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
|
+
const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1315
|
+
const mountIsAlreadyCovered = (mount) => Boolean(
|
|
1316
|
+
currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
|
|
1317
|
+
);
|
|
1318
|
+
const uncoveredCompletionMounts = selected.active.filter((mount) => !mountIsAlreadyCovered(mount));
|
|
1319
|
+
const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter((mount) => !mountIsAlreadyCovered(mount));
|
|
1320
|
+
const uncoveredCompletionBasisMounts = completionReceiptMounts.filter((mount) => !mountIsAlreadyCovered(mount));
|
|
1321
|
+
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1322
|
+
workspacePath,
|
|
1323
|
+
mounts: uncoveredCompletionMounts,
|
|
1324
|
+
durabilityMounts: uncoveredCompletionBasisMounts,
|
|
1325
|
+
receiptMounts: completionReceiptMounts,
|
|
1326
|
+
requiredMounts: uncoveredRequiredLiveMounts,
|
|
1327
|
+
recordCurrentHead: true
|
|
1328
|
+
});
|
|
1329
|
+
if (requireCurrentHeadReceipt) {
|
|
1330
|
+
const currentHead = revParse(workspacePath, "HEAD");
|
|
1331
|
+
if (currentHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), currentHead, completionReceiptMounts)) {
|
|
1332
|
+
throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
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
|
+
};
|
|
1339
|
+
};
|
|
1340
|
+
let newlyStagedPaths = [];
|
|
1341
|
+
const stageAndCommitWorkspace = () => {
|
|
1342
|
+
git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
1343
|
+
newlyStagedPaths = stagedPaths(workspacePath);
|
|
1344
|
+
if (newlyStagedPaths.length > 0) {
|
|
1345
|
+
git(
|
|
1346
|
+
workspacePath,
|
|
1347
|
+
[
|
|
1348
|
+
"commit",
|
|
1349
|
+
"-m",
|
|
1350
|
+
JSON.stringify({ type: "workspace_sync", worker: input.workerLabel, attemptId, detail: input.commitDetail ?? null })
|
|
1351
|
+
],
|
|
1352
|
+
"commit workspace working trees"
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
393
1356
|
if (!input.skipMountMirror) {
|
|
394
|
-
|
|
395
|
-
|
|
1357
|
+
beginWorkspaceCheckoutTransition(workspacePath);
|
|
1358
|
+
try {
|
|
1359
|
+
mirrorMountsToWorkspace(workspacePath, projectedActiveMounts);
|
|
1360
|
+
removeWorkspaceMounts(workspacePath, projectedTombstones);
|
|
1361
|
+
restoreDeferredWorkspaceMountsFromHead(
|
|
1362
|
+
workspacePath,
|
|
1363
|
+
input.mounts.filter(({ id }) => deferredMountIds.has(id))
|
|
1364
|
+
);
|
|
1365
|
+
stageAndCommitWorkspace();
|
|
1366
|
+
fsyncWorkspaceCheckoutTree(workspacePath);
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
|
|
1369
|
+
}
|
|
1370
|
+
} else {
|
|
1371
|
+
stageAndCommitWorkspace();
|
|
1372
|
+
if (newlyStagedPaths.length > 0) completeWorkspaceCheckoutTransition(workspacePath);
|
|
396
1373
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
[
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
],
|
|
407
|
-
"commit workspace working trees"
|
|
408
|
-
);
|
|
1374
|
+
if (!input.skipMountMirror) {
|
|
1375
|
+
const projectedHead = revParse(workspacePath, "HEAD");
|
|
1376
|
+
if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
|
|
1377
|
+
const targetProjectionReceipt = workspaceHydrationReceipt(projectedHead, projectionReceiptMounts);
|
|
1378
|
+
if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), targetProjectionReceipt)) {
|
|
1379
|
+
fsyncHydratedWorkspaceMounts([...projectedActiveMounts, ...projectedTombstones]);
|
|
1380
|
+
updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
|
|
1381
|
+
}
|
|
1382
|
+
completeWorkspaceCheckoutTransition(workspacePath);
|
|
409
1383
|
}
|
|
410
1384
|
let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
411
1385
|
let rebaseCount = 0;
|
|
@@ -423,8 +1397,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
423
1397
|
rebaseCount: rebaseCount + 1,
|
|
424
1398
|
diffSizeBytes: 0,
|
|
425
1399
|
affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
|
|
426
|
-
activeMountIds: [...
|
|
427
|
-
skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
|
|
1400
|
+
activeMountIds: [...projectedActiveMounts, ...projectedTombstones].map(({ id }) => id).sort(),
|
|
1401
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
|
|
428
1402
|
conflictPaths: reconciled.conflictPaths,
|
|
429
1403
|
conflictSnapshotRefs: reconciled.refs,
|
|
430
1404
|
error: reconciled.error
|
|
@@ -432,6 +1406,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
432
1406
|
}
|
|
433
1407
|
updated ||= reconciled.updated;
|
|
434
1408
|
if (reconciled.rebased) rebaseCount += 1;
|
|
1409
|
+
if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
|
|
435
1410
|
const localHead = revParse(workspacePath, "HEAD");
|
|
436
1411
|
if (!localHead) {
|
|
437
1412
|
return {
|
|
@@ -465,11 +1440,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
465
1440
|
};
|
|
466
1441
|
}
|
|
467
1442
|
if (localHead === remoteHead) {
|
|
1443
|
+
const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
|
|
468
1444
|
await input.afterWorkspacePublished?.({
|
|
469
1445
|
publishedHead: localHead,
|
|
470
|
-
activeMountIds:
|
|
1446
|
+
activeMountIds: completedMounts.activeMountIds
|
|
471
1447
|
});
|
|
472
|
-
if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
|
|
473
1448
|
return {
|
|
474
1449
|
outcome: updated ? "updated" : "no_change",
|
|
475
1450
|
startingHead,
|
|
@@ -479,8 +1454,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
479
1454
|
rebaseCount,
|
|
480
1455
|
diffSizeBytes: size,
|
|
481
1456
|
affectedPaths: paths,
|
|
482
|
-
|
|
483
|
-
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
1457
|
+
...completedMounts
|
|
484
1458
|
};
|
|
485
1459
|
}
|
|
486
1460
|
const pushArgs = [
|
|
@@ -493,11 +1467,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
493
1467
|
const push = gitResult(workspacePath, pushArgs);
|
|
494
1468
|
if (push.exitCode === 0) {
|
|
495
1469
|
updateIntegratedWorkspaceHead(workspacePath, localHead);
|
|
1470
|
+
const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
|
|
496
1471
|
await input.afterWorkspacePublished?.({
|
|
497
1472
|
publishedHead: localHead,
|
|
498
|
-
activeMountIds:
|
|
1473
|
+
activeMountIds: completedMounts.activeMountIds
|
|
499
1474
|
});
|
|
500
|
-
if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
|
|
501
1475
|
return {
|
|
502
1476
|
outcome: "pushed",
|
|
503
1477
|
startingHead,
|
|
@@ -507,8 +1481,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
507
1481
|
rebaseCount,
|
|
508
1482
|
diffSizeBytes: size,
|
|
509
1483
|
affectedPaths: paths,
|
|
510
|
-
|
|
511
|
-
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
1484
|
+
...completedMounts
|
|
512
1485
|
};
|
|
513
1486
|
}
|
|
514
1487
|
if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
|
|
@@ -529,10 +1502,15 @@ const workspaceGitSyncTestHarness = {
|
|
|
529
1502
|
export {
|
|
530
1503
|
MAX_WORKSPACE_GIT_DIFF_BYTES,
|
|
531
1504
|
WORKSPACE_GIT_BRANCH,
|
|
1505
|
+
WORKSPACE_GIT_CHECKOUT_DURABILITY,
|
|
532
1506
|
WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
|
|
1507
|
+
WORKSPACE_GIT_HYDRATED_RECEIPT,
|
|
1508
|
+
WORKSPACE_GIT_HYDRATION_TRANSACTION,
|
|
533
1509
|
ensureWorkspaceGitClone,
|
|
534
1510
|
hydrateWorkspaceGitMounts,
|
|
1511
|
+
recoverWorkspaceGitHydration,
|
|
535
1512
|
resetWorkspaceGit,
|
|
536
1513
|
synchronizeWorkspaceGit,
|
|
1514
|
+
workspaceGitHydrationIsCurrent,
|
|
537
1515
|
workspaceGitSyncTestHarness
|
|
538
1516
|
};
|