@ricsam/r5d-worker 0.0.78 → 0.0.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +641 -56
- 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 +191 -34
- 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 +1028 -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 +632 -55
- 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 +190 -34
- 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 +1023 -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 +7 -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,631 @@ 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({ sourceRoot: sourcePath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
id: mount.id,
|
|
607
|
+
incarnationKey: mount.hydrationIncarnationKey,
|
|
608
|
+
sourcePath,
|
|
609
|
+
durabilityRootPath,
|
|
610
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
611
|
+
existed: Boolean(sourceStatus),
|
|
612
|
+
snapshotName
|
|
613
|
+
};
|
|
614
|
+
})
|
|
615
|
+
};
|
|
616
|
+
writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
|
|
617
|
+
`);
|
|
618
|
+
fsyncTree(stagingPath);
|
|
619
|
+
fs.renameSync(stagingPath, transactionPath);
|
|
620
|
+
fsyncDirectory(stateDirectory);
|
|
621
|
+
return manifest;
|
|
622
|
+
} catch (error) {
|
|
623
|
+
fs.rmSync(stagingPath, { recursive: true, force: true });
|
|
624
|
+
fsyncDirectory(stateDirectory);
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function markHydrationTransactionDurable(workspacePath) {
|
|
629
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
630
|
+
writePrivateFileDurably(path.join(transactionPath, "committed"), "committed\n");
|
|
631
|
+
fsyncDirectory(transactionPath);
|
|
632
|
+
}
|
|
633
|
+
function fsyncHydratedWorkspaceMount(mount) {
|
|
634
|
+
assertManagedDirectoryPath({
|
|
635
|
+
trustedRoot: mount.durabilityRootPath,
|
|
636
|
+
candidate: mount.sourcePath,
|
|
637
|
+
label: `Workspace mount ${mount.id} source`
|
|
638
|
+
});
|
|
639
|
+
const syncEntry = (entryPath, root) => {
|
|
640
|
+
const status = fs.lstatSync(entryPath);
|
|
641
|
+
if (status.isSymbolicLink()) return;
|
|
642
|
+
if (status.isDirectory()) {
|
|
643
|
+
for (const name of fs.readdirSync(entryPath)) {
|
|
644
|
+
if (root && name === ".git") continue;
|
|
645
|
+
syncEntry(path.join(entryPath, name), false);
|
|
646
|
+
}
|
|
647
|
+
fsyncDirectory(entryPath);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (!status.isFile()) return;
|
|
651
|
+
const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY);
|
|
652
|
+
try {
|
|
653
|
+
fs.fsyncSync(descriptor);
|
|
654
|
+
} finally {
|
|
655
|
+
fs.closeSync(descriptor);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
if (fs.existsSync(mount.sourcePath)) syncEntry(mount.sourcePath, true);
|
|
659
|
+
const durabilityRoot = path.resolve(mount.durabilityRootPath);
|
|
660
|
+
let boundaryParent = path.dirname(path.resolve(mount.sourcePath));
|
|
661
|
+
while (true) {
|
|
662
|
+
if (fs.existsSync(boundaryParent)) {
|
|
663
|
+
const boundaryStatus = fs.lstatSync(boundaryParent);
|
|
664
|
+
if (!boundaryStatus.isDirectory() || boundaryStatus.isSymbolicLink()) {
|
|
665
|
+
throw new Error(`Workspace mount durability boundary is not a regular directory: ${boundaryParent}`);
|
|
666
|
+
}
|
|
667
|
+
fsyncDirectory(boundaryParent);
|
|
668
|
+
}
|
|
669
|
+
if (boundaryParent === durabilityRoot) break;
|
|
670
|
+
const nextParent = path.dirname(boundaryParent);
|
|
671
|
+
if (nextParent === boundaryParent) {
|
|
672
|
+
throw new Error(`Workspace mount ${mount.sourcePath} does not descend from durability root ${durabilityRoot}`);
|
|
673
|
+
}
|
|
674
|
+
boundaryParent = nextParent;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function fsyncHydratedWorkspaceMounts(mounts) {
|
|
678
|
+
for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
|
|
679
|
+
}
|
|
51
680
|
function workspaceIsShallow(workspacePath) {
|
|
52
681
|
return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
|
|
53
682
|
}
|
|
@@ -60,7 +689,22 @@ function validateMounts(workspacePath, mounts) {
|
|
|
60
689
|
const paths = [];
|
|
61
690
|
for (const mount of mounts) {
|
|
62
691
|
if (!mount.id || ids.has(mount.id)) throw new Error(`Workspace mount id must be unique: ${mount.id}`);
|
|
692
|
+
if (!mount.hydrationIncarnationKey || mount.hydrationIncarnationKey.includes("\0")) {
|
|
693
|
+
throw new Error(`Workspace mount hydration incarnation is invalid: ${mount.id}`);
|
|
694
|
+
}
|
|
63
695
|
ids.add(mount.id);
|
|
696
|
+
const sourcePath = path.resolve(mount.sourcePath);
|
|
697
|
+
const durabilityRoot = path.resolve(mount.durabilityRootPath);
|
|
698
|
+
const durabilityRelative = path.relative(durabilityRoot, sourcePath);
|
|
699
|
+
const durabilityRootStatus = fs.lstatSync(durabilityRoot);
|
|
700
|
+
if (!durabilityRootStatus.isDirectory() || durabilityRootStatus.isSymbolicLink() || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.isAbsolute(durabilityRelative)) {
|
|
701
|
+
throw new Error(`Workspace mount source escapes its durability root: ${mount.sourcePath}`);
|
|
702
|
+
}
|
|
703
|
+
assertManagedDirectoryPath({
|
|
704
|
+
trustedRoot: durabilityRoot,
|
|
705
|
+
candidate: sourcePath,
|
|
706
|
+
label: `Workspace mount ${mount.id} source`
|
|
707
|
+
});
|
|
64
708
|
const normalized = mount.workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
65
709
|
if (!normalized || normalized === "." || normalized.split("/").some((segment) => segment === ".." || segment === ".git")) {
|
|
66
710
|
throw new Error(`Invalid workspace mount path: ${mount.workspaceRelativePath}`);
|
|
@@ -107,6 +751,8 @@ function configureWorkspaceRepository(input) {
|
|
|
107
751
|
if (!name || !email) throw new Error("Workspace Git identity must include name and email");
|
|
108
752
|
git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
109
753
|
git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
754
|
+
git(input.workspacePath, ["config", "--local", "core.fsync", "committed"], "configure durable workspace commits");
|
|
755
|
+
git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
|
|
110
756
|
}
|
|
111
757
|
function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
112
758
|
const result = gitResult(workspacePath, [
|
|
@@ -130,7 +776,24 @@ ${result.stdout}`;
|
|
|
130
776
|
}
|
|
131
777
|
function ensureWorkspaceGitClone(input) {
|
|
132
778
|
const workspacePath = path.resolve(input.workspacePath);
|
|
779
|
+
const workspaceParent = path.dirname(workspacePath);
|
|
780
|
+
const parentStatus = fs.lstatSync(workspaceParent);
|
|
781
|
+
if (!parentStatus.isDirectory() || parentStatus.isSymbolicLink()) {
|
|
782
|
+
throw new Error(`Workspace clone parent is not a regular directory: ${workspaceParent}`);
|
|
783
|
+
}
|
|
784
|
+
const workspaceStatus = lstatIfExists(workspacePath);
|
|
785
|
+
if (workspaceStatus) {
|
|
786
|
+
if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink()) {
|
|
787
|
+
throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
133
790
|
const gitPath = path.join(workspacePath, ".git");
|
|
791
|
+
const gitStatus = lstatIfExists(gitPath);
|
|
792
|
+
if (gitStatus) {
|
|
793
|
+
if (!gitStatus.isDirectory() || gitStatus.isSymbolicLink()) {
|
|
794
|
+
throw new Error(`Workspace clone Git metadata is not a regular directory: ${gitPath}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
134
797
|
if (!fs.existsSync(gitPath)) {
|
|
135
798
|
fs.rmSync(workspacePath, { recursive: true, force: true });
|
|
136
799
|
fs.mkdirSync(path.dirname(workspacePath), { recursive: true });
|
|
@@ -152,6 +815,7 @@ function ensureWorkspaceGitClone(input) {
|
|
|
152
815
|
}
|
|
153
816
|
}
|
|
154
817
|
configureWorkspaceRepository({ ...input, workspacePath });
|
|
818
|
+
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
155
819
|
const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
156
820
|
const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
|
|
157
821
|
if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
@@ -160,17 +824,21 @@ function ensureWorkspaceGitClone(input) {
|
|
|
160
824
|
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
161
825
|
let localHead = revParse(workspacePath, "HEAD");
|
|
162
826
|
if (!localHead && remoteHead) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
827
|
+
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
828
|
+
git(
|
|
829
|
+
workspacePath,
|
|
830
|
+
["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
|
|
831
|
+
"check out workspace main"
|
|
832
|
+
);
|
|
833
|
+
});
|
|
168
834
|
localHead = remoteHead;
|
|
169
835
|
}
|
|
170
836
|
if (localHead && remoteHead && localHead === remoteHead) {
|
|
171
837
|
updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
172
838
|
} else if (localHead && remoteHead && workspaceIsShallow(workspacePath) && !tryGit(workspacePath, ["merge-base", localHead, remoteHead]) && revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) === localHead && workspaceIsClean(workspacePath)) {
|
|
173
|
-
|
|
839
|
+
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
840
|
+
git(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
|
|
841
|
+
});
|
|
174
842
|
updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
175
843
|
localHead = remoteHead;
|
|
176
844
|
}
|
|
@@ -190,6 +858,16 @@ function activeMounts(mounts) {
|
|
|
190
858
|
}
|
|
191
859
|
function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
192
860
|
for (const mount of mounts) {
|
|
861
|
+
assertManagedDirectoryPath({
|
|
862
|
+
trustedRoot: mount.durabilityRootPath,
|
|
863
|
+
candidate: mount.sourcePath,
|
|
864
|
+
label: `Workspace mount ${mount.id} source`
|
|
865
|
+
});
|
|
866
|
+
assertManagedDirectoryPath({
|
|
867
|
+
trustedRoot: workspacePath,
|
|
868
|
+
candidate: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
869
|
+
label: `Workspace mount ${mount.id} outer target`
|
|
870
|
+
});
|
|
193
871
|
mirrorWorkingTree({
|
|
194
872
|
sourceRoot: mount.sourcePath,
|
|
195
873
|
targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
@@ -201,6 +879,11 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
|
201
879
|
function removeWorkspaceMounts(workspacePath, mounts) {
|
|
202
880
|
for (const mount of mounts) {
|
|
203
881
|
const targetPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
882
|
+
assertManagedDirectoryPath({
|
|
883
|
+
trustedRoot: workspacePath,
|
|
884
|
+
candidate: targetPath,
|
|
885
|
+
label: `Workspace mount ${mount.id} outer target`
|
|
886
|
+
});
|
|
204
887
|
const relative = path.relative(path.resolve(workspacePath), path.resolve(targetPath));
|
|
205
888
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
206
889
|
throw new Error(`Workspace mount escapes the workspace clone: ${mount.workspaceRelativePath}`);
|
|
@@ -208,9 +891,45 @@ function removeWorkspaceMounts(workspacePath, mounts) {
|
|
|
208
891
|
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
209
892
|
}
|
|
210
893
|
}
|
|
211
|
-
function
|
|
894
|
+
function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
|
|
895
|
+
const head = revParse(workspacePath, "HEAD");
|
|
896
|
+
for (const mount of mounts) {
|
|
897
|
+
const normalized = normalizedWorkspaceMountPath(mount.workspaceRelativePath);
|
|
898
|
+
const targetPath = path.join(workspacePath, ...normalized.split("/"));
|
|
899
|
+
assertManagedDirectoryPath({
|
|
900
|
+
trustedRoot: workspacePath,
|
|
901
|
+
candidate: targetPath,
|
|
902
|
+
label: `Deferred workspace mount ${mount.id} outer target`
|
|
903
|
+
});
|
|
904
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
905
|
+
if (head && tryGit(workspacePath, ["cat-file", "-e", `${head}:${normalized}`])) {
|
|
906
|
+
git(
|
|
907
|
+
workspacePath,
|
|
908
|
+
["checkout", "--no-recurse-submodules", head, "--", `:(literal)${normalized}`],
|
|
909
|
+
`restore deferred workspace mount ${mount.id} from its integration basis`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
915
|
+
const hydratedMountIds = [];
|
|
916
|
+
const skippedMountIds = [];
|
|
212
917
|
for (const mount of mounts) {
|
|
918
|
+
if (!options.ignoreBusy && mount.busy?.()) {
|
|
919
|
+
skippedMountIds.push(mount.id);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
213
922
|
const sourceRoot = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
923
|
+
assertManagedDirectoryPath({
|
|
924
|
+
trustedRoot: workspacePath,
|
|
925
|
+
candidate: sourceRoot,
|
|
926
|
+
label: `Workspace mount ${mount.id} outer source`
|
|
927
|
+
});
|
|
928
|
+
assertManagedDirectoryPath({
|
|
929
|
+
trustedRoot: mount.durabilityRootPath,
|
|
930
|
+
candidate: mount.sourcePath,
|
|
931
|
+
label: `Workspace mount ${mount.id} hydration target`
|
|
932
|
+
});
|
|
214
933
|
if (!fs.existsSync(sourceRoot)) {
|
|
215
934
|
if (mount.hydrateDeletionMode === "all") {
|
|
216
935
|
fs.rmSync(mount.sourcePath, { recursive: true, force: true });
|
|
@@ -227,37 +946,167 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts) {
|
|
|
227
946
|
fs.rmSync(emptyRoot, { recursive: true, force: true });
|
|
228
947
|
}
|
|
229
948
|
}
|
|
949
|
+
hydratedMountIds.push(mount.id);
|
|
230
950
|
continue;
|
|
231
951
|
}
|
|
952
|
+
const targetExists = fs.existsSync(mount.sourcePath);
|
|
232
953
|
mirrorWorkingTree({
|
|
233
954
|
sourceRoot,
|
|
234
955
|
targetRoot: mount.sourcePath,
|
|
235
956
|
sourceMode: "all",
|
|
236
|
-
|
|
957
|
+
// A missing checkout has no Git index to define git-only deletions. Its
|
|
958
|
+
// newly created directory is empty, so all-mode is equivalent and lets
|
|
959
|
+
// bootstrap hydration preserve the fetched tree before Git initializes.
|
|
960
|
+
deletionMode: targetExists ? mount.hydrateDeletionMode : "all"
|
|
237
961
|
});
|
|
962
|
+
hydratedMountIds.push(mount.id);
|
|
963
|
+
}
|
|
964
|
+
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
|
|
965
|
+
}
|
|
966
|
+
function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
967
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
968
|
+
const mountIds = input.mounts.map(({ id }) => id).sort();
|
|
969
|
+
const requiredIds = (input.requiredMounts ?? input.mounts).map(({ id }) => id).sort();
|
|
970
|
+
if (mountIds.length !== requiredIds.length || mountIds.some((id, index) => id !== requiredIds[index])) {
|
|
971
|
+
return { hydratedMountIds: [], skippedMountIds: requiredIds };
|
|
972
|
+
}
|
|
973
|
+
const busyMountIds = input.ignoreBusy ? [] : (input.durabilityMounts ?? input.mounts).filter((mount) => mount.busy?.()).map(({ id }) => id).sort();
|
|
974
|
+
if (busyMountIds.length > 0) {
|
|
975
|
+
return { hydratedMountIds: [], skippedMountIds: busyMountIds };
|
|
976
|
+
}
|
|
977
|
+
if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
|
|
978
|
+
throw new Error("Workspace checkout is not durably materialized at its current HEAD");
|
|
979
|
+
}
|
|
980
|
+
const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
|
|
981
|
+
const targetReceipt = targetHead ? workspaceHydrationReceipt(targetHead, input.receiptMounts ?? input.durabilityMounts ?? input.mounts) : null;
|
|
982
|
+
const manifest = beginHydrationTransaction(workspacePath, input.mounts, targetReceipt);
|
|
983
|
+
try {
|
|
984
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, input.mounts, { ignoreBusy: true });
|
|
985
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredIds[index])) {
|
|
986
|
+
throw new Error("Workspace hydration did not include every required mount");
|
|
987
|
+
}
|
|
988
|
+
fsyncHydratedWorkspaceMounts(input.durabilityMounts ?? input.mounts);
|
|
989
|
+
markHydrationTransactionDurable(workspacePath);
|
|
990
|
+
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
991
|
+
removeHydrationTransaction(workspacePath);
|
|
992
|
+
return hydration;
|
|
993
|
+
} catch (error) {
|
|
994
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
995
|
+
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
try {
|
|
999
|
+
restoreHydrationTransaction(workspacePath, manifest);
|
|
1000
|
+
removeHydrationTransaction(workspacePath);
|
|
1001
|
+
} catch (restoreError) {
|
|
1002
|
+
throw new AggregateError(
|
|
1003
|
+
[error, restoreError],
|
|
1004
|
+
`Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
1011
|
+
const gitDirectory = path.join(path.resolve(workspacePath), ".git");
|
|
1012
|
+
if (!fs.existsSync(gitDirectory)) {
|
|
1013
|
+
return hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options);
|
|
1014
|
+
}
|
|
1015
|
+
recoverWorkspaceCheckoutDurability(path.resolve(workspacePath));
|
|
1016
|
+
return hydrateWorkspaceGitMountsTransactionally({
|
|
1017
|
+
workspacePath,
|
|
1018
|
+
mounts,
|
|
1019
|
+
ignoreBusy: options.ignoreBusy
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
1023
|
+
workspacePath = path.resolve(workspacePath);
|
|
1024
|
+
validateMounts(workspacePath, mounts);
|
|
1025
|
+
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
1026
|
+
recoverPendingHydrationTransaction(workspacePath);
|
|
1027
|
+
const currentHead = revParse(workspacePath, "HEAD");
|
|
1028
|
+
if (!currentHead) return;
|
|
1029
|
+
const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1030
|
+
const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
|
|
1031
|
+
const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
|
|
1032
|
+
const uncoveredBasisMounts = configuredBasisMounts.filter(
|
|
1033
|
+
(mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
|
|
1034
|
+
);
|
|
1035
|
+
if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
|
|
1036
|
+
const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1037
|
+
if (busyMountIds.length > 0) {
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
`Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${busyMountIds.join(", ")}`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
const hydrationTargets = uncoveredBasisMounts.filter(
|
|
1043
|
+
(mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
|
|
1044
|
+
);
|
|
1045
|
+
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1046
|
+
workspacePath,
|
|
1047
|
+
mounts: hydrationTargets,
|
|
1048
|
+
durabilityMounts: uncoveredBasisMounts,
|
|
1049
|
+
receiptMounts: configuredBasisMounts,
|
|
1050
|
+
requiredMounts: hydrationTargets,
|
|
1051
|
+
ignoreBusy: options.ignoreBusy,
|
|
1052
|
+
recordCurrentHead: true
|
|
1053
|
+
});
|
|
1054
|
+
if (hydration.skippedMountIds.length > 0) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
`Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
|
|
1057
|
+
);
|
|
238
1058
|
}
|
|
239
1059
|
}
|
|
240
1060
|
function resetWorkspaceGit(input) {
|
|
241
1061
|
const workspacePath = path.resolve(input.workspacePath);
|
|
242
1062
|
validateMounts(workspacePath, input.mounts);
|
|
243
1063
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1064
|
+
recoverPendingHydrationTransaction(workspacePath);
|
|
244
1065
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
245
1066
|
const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
|
|
246
1067
|
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
247
1068
|
if (remoteHead) {
|
|
248
|
-
|
|
249
|
-
|
|
1069
|
+
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
1070
|
+
git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
|
|
1071
|
+
git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
|
|
1072
|
+
});
|
|
250
1073
|
} else if (revParse(workspacePath, "HEAD")) {
|
|
251
|
-
|
|
252
|
-
|
|
1074
|
+
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
1075
|
+
git(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
|
|
1076
|
+
git(workspacePath, ["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })], "record empty workspace reset");
|
|
1077
|
+
});
|
|
253
1078
|
}
|
|
254
1079
|
const selected = activeMounts(input.mounts);
|
|
255
|
-
|
|
1080
|
+
const requiredLiveMountsBeforeHydration = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
|
|
1081
|
+
const bootstrapMounts = selected.skipped.filter((mount) => {
|
|
1082
|
+
if (mount.deleteWhenSourceMissing || fs.existsSync(mount.sourcePath) || mount.busy?.()) return false;
|
|
1083
|
+
const outerPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
1084
|
+
return fs.existsSync(outerPath);
|
|
1085
|
+
});
|
|
1086
|
+
const hydrationMounts = [...selected.active, ...bootstrapMounts];
|
|
1087
|
+
const requiredLiveMounts = [
|
|
1088
|
+
...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
|
|
1089
|
+
];
|
|
1090
|
+
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1091
|
+
workspacePath,
|
|
1092
|
+
mounts: hydrationMounts,
|
|
1093
|
+
durabilityMounts: [...requiredLiveMounts, ...selected.tombstones],
|
|
1094
|
+
receiptMounts: input.mounts,
|
|
1095
|
+
requiredMounts: requiredLiveMounts,
|
|
1096
|
+
recordCurrentHead: true
|
|
1097
|
+
});
|
|
1098
|
+
const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
|
|
1099
|
+
const resetHead = revParse(workspacePath, "HEAD");
|
|
1100
|
+
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)) {
|
|
1101
|
+
throw new Error("Workspace reset could not durably hydrate every live mount; remediation remains active");
|
|
1102
|
+
}
|
|
256
1103
|
return {
|
|
257
1104
|
startingHead: initial.localHead,
|
|
258
1105
|
localHead: revParse(workspacePath, "HEAD"),
|
|
259
1106
|
remoteHead,
|
|
260
|
-
discardedPaths
|
|
1107
|
+
discardedPaths,
|
|
1108
|
+
activeMountIds: hydration.hydratedMountIds,
|
|
1109
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
|
|
261
1110
|
};
|
|
262
1111
|
}
|
|
263
1112
|
function stagedPaths(workspacePath) {
|
|
@@ -322,17 +1171,22 @@ function snapshotConflict(input) {
|
|
|
322
1171
|
}
|
|
323
1172
|
function rebaseWorkspace(input) {
|
|
324
1173
|
const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
|
|
1174
|
+
beginWorkspaceCheckoutTransition(input.workspacePath);
|
|
325
1175
|
const rebase = gitResult(
|
|
326
1176
|
input.workspacePath,
|
|
327
1177
|
input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
|
|
328
1178
|
);
|
|
329
|
-
if (rebase.exitCode === 0)
|
|
1179
|
+
if (rebase.exitCode === 0) {
|
|
1180
|
+
completeWorkspaceCheckoutTransition(input.workspacePath);
|
|
1181
|
+
return { ok: true };
|
|
1182
|
+
}
|
|
330
1183
|
const conflicts = conflictPaths(input.workspacePath);
|
|
331
1184
|
const refs = snapshotConflict(input);
|
|
332
1185
|
const abort = gitResult(input.workspacePath, ["rebase", "--abort"]);
|
|
333
1186
|
if (abort.exitCode !== 0) {
|
|
334
1187
|
throw new Error(`Abort workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
|
|
335
1188
|
}
|
|
1189
|
+
completeWorkspaceCheckoutTransition(input.workspacePath);
|
|
336
1190
|
return {
|
|
337
1191
|
ok: false,
|
|
338
1192
|
conflictPaths: conflicts,
|
|
@@ -342,26 +1196,31 @@ function rebaseWorkspace(input) {
|
|
|
342
1196
|
}
|
|
343
1197
|
function synchronizeWithFetchedHead(input) {
|
|
344
1198
|
const localHead = revParse(input.workspacePath, "HEAD");
|
|
345
|
-
|
|
1199
|
+
const remoteHead = input.remoteHead;
|
|
1200
|
+
if (!remoteHead) return { kind: "ready", updated: false, rebased: false };
|
|
346
1201
|
if (!localHead) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
1202
|
+
runWorkspaceCheckoutTransition(input.workspacePath, () => {
|
|
1203
|
+
git(
|
|
1204
|
+
input.workspacePath,
|
|
1205
|
+
["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
|
|
1206
|
+
"check out fetched workspace main"
|
|
1207
|
+
);
|
|
1208
|
+
});
|
|
352
1209
|
return { kind: "ready", updated: true, rebased: false };
|
|
353
1210
|
}
|
|
354
|
-
if (localHead ===
|
|
355
|
-
updateIntegratedWorkspaceHead(input.workspacePath,
|
|
1211
|
+
if (localHead === remoteHead) {
|
|
1212
|
+
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
356
1213
|
return { kind: "ready", updated: false, rebased: false };
|
|
357
1214
|
}
|
|
358
|
-
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead,
|
|
359
|
-
|
|
360
|
-
|
|
1215
|
+
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead, remoteHead])) {
|
|
1216
|
+
runWorkspaceCheckoutTransition(input.workspacePath, () => {
|
|
1217
|
+
git(input.workspacePath, ["reset", "--hard", remoteHead], "fast-forward workspace main");
|
|
1218
|
+
});
|
|
1219
|
+
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
361
1220
|
return { kind: "ready", updated: true, rebased: false };
|
|
362
1221
|
}
|
|
363
|
-
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor",
|
|
364
|
-
updateIntegratedWorkspaceHead(input.workspacePath,
|
|
1222
|
+
if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
|
|
1223
|
+
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
365
1224
|
return { kind: "ready", updated: false, rebased: false };
|
|
366
1225
|
}
|
|
367
1226
|
const integratedHead = revParse(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
|
|
@@ -374,11 +1233,11 @@ function synchronizeWithFetchedHead(input) {
|
|
|
374
1233
|
const rebased = rebaseWorkspace({
|
|
375
1234
|
workspacePath: input.workspacePath,
|
|
376
1235
|
localHead,
|
|
377
|
-
remoteHead
|
|
1236
|
+
remoteHead,
|
|
378
1237
|
attemptId: input.attemptId,
|
|
379
1238
|
truncatedHistoryBase
|
|
380
1239
|
});
|
|
381
|
-
if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath,
|
|
1240
|
+
if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
382
1241
|
return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
|
|
383
1242
|
}
|
|
384
1243
|
async function synchronizeWorkspaceGit(input) {
|
|
@@ -387,25 +1246,131 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
387
1246
|
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
388
1247
|
const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
|
|
389
1248
|
validateMounts(workspacePath, input.mounts);
|
|
1249
|
+
const busyLiveMountIds = input.skipMountMirror ? [] : input.mounts.filter((mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()).map(({ id }) => id).sort();
|
|
1250
|
+
if (busyLiveMountIds.length > 0) {
|
|
1251
|
+
const cloneExists = fs.existsSync(path.join(workspacePath, ".git"));
|
|
1252
|
+
const localHead = cloneExists ? revParse(workspacePath, "HEAD") : null;
|
|
1253
|
+
const remoteHead2 = cloneExists ? revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`) : null;
|
|
1254
|
+
return {
|
|
1255
|
+
outcome: "no_change",
|
|
1256
|
+
startingHead: localHead,
|
|
1257
|
+
localHead,
|
|
1258
|
+
remoteHead: remoteHead2,
|
|
1259
|
+
publishedHead: remoteHead2,
|
|
1260
|
+
rebaseCount: 0,
|
|
1261
|
+
diffSizeBytes: 0,
|
|
1262
|
+
affectedPaths: [],
|
|
1263
|
+
activeMountIds: [],
|
|
1264
|
+
skippedMountIds: input.mounts.map(({ id }) => id).sort()
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
390
1267
|
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
391
1268
|
const startingHead = initial.localHead;
|
|
1269
|
+
const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1270
|
+
const deferredMountIds = /* @__PURE__ */ new Set();
|
|
1271
|
+
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead && receiptBeforeInitialHydration?.head === initial.localHead) {
|
|
1272
|
+
for (const mount of input.mounts) {
|
|
1273
|
+
if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && !workspaceHydrationReceiptCoversMount(receiptBeforeInitialHydration, initial.localHead, mount)) {
|
|
1274
|
+
deferredMountIds.add(mount.id);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1279
|
+
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
|
|
1280
|
+
});
|
|
1281
|
+
const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
|
|
392
1282
|
const selected = activeMounts(input.mounts);
|
|
1283
|
+
const projectedActiveMounts = selected.active.filter(({ id }) => !deferredMountIds.has(id));
|
|
1284
|
+
const projectedTombstones = selected.tombstones.filter(({ id }) => !deferredMountIds.has(id));
|
|
1285
|
+
const projectionBasisHead = revParse(workspacePath, "HEAD");
|
|
1286
|
+
const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1287
|
+
const projectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts).filter(
|
|
1288
|
+
(mount) => !deferredMountIds.has(mount.id) && Boolean(projectionBasisHead && workspaceHydrationReceiptCoversMount(projectionBasisReceipt, projectionBasisHead, mount))
|
|
1289
|
+
);
|
|
1290
|
+
const completedMountSelection = (requireCurrentHeadReceipt = false) => {
|
|
1291
|
+
const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
|
|
1292
|
+
const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
|
|
1293
|
+
if (currentHeadBeforeHydration && workspaceHydrationReceiptMatchesMounts(
|
|
1294
|
+
readHydratedWorkspaceReceipt(workspacePath),
|
|
1295
|
+
currentHeadBeforeHydration,
|
|
1296
|
+
completionReceiptMounts
|
|
1297
|
+
)) {
|
|
1298
|
+
const lateBusyIds = selected.active.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1299
|
+
const lateBusy = new Set(lateBusyIds);
|
|
1300
|
+
return {
|
|
1301
|
+
activeMountIds: [...selected.active.filter(({ id }) => !lateBusy.has(id)), ...selected.tombstones].map(({ id }) => id).sort(),
|
|
1302
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...lateBusyIds])].sort()
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
|
|
1306
|
+
const mountIsAlreadyCovered = (mount) => Boolean(
|
|
1307
|
+
currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
|
|
1308
|
+
);
|
|
1309
|
+
const uncoveredCompletionMounts = selected.active.filter((mount) => !mountIsAlreadyCovered(mount));
|
|
1310
|
+
const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter((mount) => !mountIsAlreadyCovered(mount));
|
|
1311
|
+
const uncoveredCompletionBasisMounts = completionReceiptMounts.filter((mount) => !mountIsAlreadyCovered(mount));
|
|
1312
|
+
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1313
|
+
workspacePath,
|
|
1314
|
+
mounts: uncoveredCompletionMounts,
|
|
1315
|
+
durabilityMounts: uncoveredCompletionBasisMounts,
|
|
1316
|
+
receiptMounts: completionReceiptMounts,
|
|
1317
|
+
requiredMounts: uncoveredRequiredLiveMounts,
|
|
1318
|
+
recordCurrentHead: true
|
|
1319
|
+
});
|
|
1320
|
+
if (requireCurrentHeadReceipt) {
|
|
1321
|
+
const currentHead = revParse(workspacePath, "HEAD");
|
|
1322
|
+
if (currentHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), currentHead, completionReceiptMounts)) {
|
|
1323
|
+
throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
|
|
1328
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
|
|
1329
|
+
};
|
|
1330
|
+
};
|
|
1331
|
+
let newlyStagedPaths = [];
|
|
1332
|
+
const stageAndCommitWorkspace = () => {
|
|
1333
|
+
git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
1334
|
+
newlyStagedPaths = stagedPaths(workspacePath);
|
|
1335
|
+
if (newlyStagedPaths.length > 0) {
|
|
1336
|
+
git(
|
|
1337
|
+
workspacePath,
|
|
1338
|
+
[
|
|
1339
|
+
"commit",
|
|
1340
|
+
"-m",
|
|
1341
|
+
JSON.stringify({ type: "workspace_sync", worker: input.workerLabel, attemptId, detail: input.commitDetail ?? null })
|
|
1342
|
+
],
|
|
1343
|
+
"commit workspace working trees"
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
393
1347
|
if (!input.skipMountMirror) {
|
|
394
|
-
|
|
395
|
-
|
|
1348
|
+
beginWorkspaceCheckoutTransition(workspacePath);
|
|
1349
|
+
try {
|
|
1350
|
+
mirrorMountsToWorkspace(workspacePath, projectedActiveMounts);
|
|
1351
|
+
removeWorkspaceMounts(workspacePath, projectedTombstones);
|
|
1352
|
+
restoreDeferredWorkspaceMountsFromHead(
|
|
1353
|
+
workspacePath,
|
|
1354
|
+
input.mounts.filter(({ id }) => deferredMountIds.has(id))
|
|
1355
|
+
);
|
|
1356
|
+
stageAndCommitWorkspace();
|
|
1357
|
+
fsyncWorkspaceCheckoutTree(workspacePath);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
|
|
1360
|
+
}
|
|
1361
|
+
} else {
|
|
1362
|
+
stageAndCommitWorkspace();
|
|
1363
|
+
if (newlyStagedPaths.length > 0) completeWorkspaceCheckoutTransition(workspacePath);
|
|
396
1364
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
[
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
],
|
|
407
|
-
"commit workspace working trees"
|
|
408
|
-
);
|
|
1365
|
+
if (!input.skipMountMirror) {
|
|
1366
|
+
const projectedHead = revParse(workspacePath, "HEAD");
|
|
1367
|
+
if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
|
|
1368
|
+
const targetProjectionReceipt = workspaceHydrationReceipt(projectedHead, projectionReceiptMounts);
|
|
1369
|
+
if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), targetProjectionReceipt)) {
|
|
1370
|
+
fsyncHydratedWorkspaceMounts([...projectedActiveMounts, ...projectedTombstones]);
|
|
1371
|
+
updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
|
|
1372
|
+
}
|
|
1373
|
+
completeWorkspaceCheckoutTransition(workspacePath);
|
|
409
1374
|
}
|
|
410
1375
|
let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
411
1376
|
let rebaseCount = 0;
|
|
@@ -423,8 +1388,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
423
1388
|
rebaseCount: rebaseCount + 1,
|
|
424
1389
|
diffSizeBytes: 0,
|
|
425
1390
|
affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
|
|
426
|
-
activeMountIds: [...
|
|
427
|
-
skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
|
|
1391
|
+
activeMountIds: [...projectedActiveMounts, ...projectedTombstones].map(({ id }) => id).sort(),
|
|
1392
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
|
|
428
1393
|
conflictPaths: reconciled.conflictPaths,
|
|
429
1394
|
conflictSnapshotRefs: reconciled.refs,
|
|
430
1395
|
error: reconciled.error
|
|
@@ -432,6 +1397,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
432
1397
|
}
|
|
433
1398
|
updated ||= reconciled.updated;
|
|
434
1399
|
if (reconciled.rebased) rebaseCount += 1;
|
|
1400
|
+
if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
|
|
435
1401
|
const localHead = revParse(workspacePath, "HEAD");
|
|
436
1402
|
if (!localHead) {
|
|
437
1403
|
return {
|
|
@@ -465,11 +1431,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
465
1431
|
};
|
|
466
1432
|
}
|
|
467
1433
|
if (localHead === remoteHead) {
|
|
1434
|
+
const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
|
|
468
1435
|
await input.afterWorkspacePublished?.({
|
|
469
1436
|
publishedHead: localHead,
|
|
470
|
-
activeMountIds:
|
|
1437
|
+
activeMountIds: completedMounts.activeMountIds
|
|
471
1438
|
});
|
|
472
|
-
if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
|
|
473
1439
|
return {
|
|
474
1440
|
outcome: updated ? "updated" : "no_change",
|
|
475
1441
|
startingHead,
|
|
@@ -479,8 +1445,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
479
1445
|
rebaseCount,
|
|
480
1446
|
diffSizeBytes: size,
|
|
481
1447
|
affectedPaths: paths,
|
|
482
|
-
|
|
483
|
-
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
1448
|
+
...completedMounts
|
|
484
1449
|
};
|
|
485
1450
|
}
|
|
486
1451
|
const pushArgs = [
|
|
@@ -493,11 +1458,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
493
1458
|
const push = gitResult(workspacePath, pushArgs);
|
|
494
1459
|
if (push.exitCode === 0) {
|
|
495
1460
|
updateIntegratedWorkspaceHead(workspacePath, localHead);
|
|
1461
|
+
const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
|
|
496
1462
|
await input.afterWorkspacePublished?.({
|
|
497
1463
|
publishedHead: localHead,
|
|
498
|
-
activeMountIds:
|
|
1464
|
+
activeMountIds: completedMounts.activeMountIds
|
|
499
1465
|
});
|
|
500
|
-
if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
|
|
501
1466
|
return {
|
|
502
1467
|
outcome: "pushed",
|
|
503
1468
|
startingHead,
|
|
@@ -507,8 +1472,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
507
1472
|
rebaseCount,
|
|
508
1473
|
diffSizeBytes: size,
|
|
509
1474
|
affectedPaths: paths,
|
|
510
|
-
|
|
511
|
-
skippedMountIds: selected.skipped.map(({ id }) => id).sort()
|
|
1475
|
+
...completedMounts
|
|
512
1476
|
};
|
|
513
1477
|
}
|
|
514
1478
|
if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
|
|
@@ -529,10 +1493,15 @@ const workspaceGitSyncTestHarness = {
|
|
|
529
1493
|
export {
|
|
530
1494
|
MAX_WORKSPACE_GIT_DIFF_BYTES,
|
|
531
1495
|
WORKSPACE_GIT_BRANCH,
|
|
1496
|
+
WORKSPACE_GIT_CHECKOUT_DURABILITY,
|
|
532
1497
|
WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
|
|
1498
|
+
WORKSPACE_GIT_HYDRATED_RECEIPT,
|
|
1499
|
+
WORKSPACE_GIT_HYDRATION_TRANSACTION,
|
|
533
1500
|
ensureWorkspaceGitClone,
|
|
534
1501
|
hydrateWorkspaceGitMounts,
|
|
1502
|
+
recoverWorkspaceGitHydration,
|
|
535
1503
|
resetWorkspaceGit,
|
|
536
1504
|
synchronizeWorkspaceGit,
|
|
1505
|
+
workspaceGitHydrationIsCurrent,
|
|
537
1506
|
workspaceGitSyncTestHarness
|
|
538
1507
|
};
|