@ricsam/r5d-worker 0.0.83 → 0.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +180 -106
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-worktrees.cjs +183 -104
- package/dist/cjs/workspace-automatic-sync-policy.cjs +12 -2
- package/dist/cjs/workspace-command-sync-policy.cjs +21 -0
- package/dist/cjs/workspace-git-sync.cjs +501 -191
- package/dist/mjs/main.mjs +185 -107
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-worktrees.mjs +183 -104
- package/dist/mjs/workspace-automatic-sync-policy.mjs +9 -1
- package/dist/mjs/workspace-command-sync-policy.mjs +20 -0
- package/dist/mjs/workspace-git-sync.mjs +500 -191
- package/dist/types/main.d.ts +19 -2
- package/dist/types/project-worktrees.d.ts +12 -12
- package/dist/types/workspace-automatic-sync-policy.d.ts +24 -4
- package/dist/types/workspace-command-sync-policy.d.ts +7 -0
- package/dist/types/workspace-git-sync.d.ts +17 -9
- package/package.json +1 -1
|
@@ -15,6 +15,9 @@ const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
|
|
|
15
15
|
const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
|
|
16
16
|
const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
|
|
17
17
|
const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
|
|
18
|
+
function workspaceGitMountsForRemediation(mounts) {
|
|
19
|
+
return mounts.map((mount) => ({ ...mount, busy: mount.busyForRecovery ?? mount.busy }));
|
|
20
|
+
}
|
|
18
21
|
class WorkspaceRemediationAncestryError extends Error {
|
|
19
22
|
constructor(message) {
|
|
20
23
|
super(message);
|
|
@@ -60,6 +63,71 @@ function revParse(workspacePath, revision) {
|
|
|
60
63
|
const result = gitResult(workspacePath, ["rev-parse", "--verify", revision]);
|
|
61
64
|
return result.exitCode === 0 ? result.stdout : null;
|
|
62
65
|
}
|
|
66
|
+
async function gitResultAsync(cwd, args) {
|
|
67
|
+
const subprocess = Bun.spawn(gitCommandArgs(args), {
|
|
68
|
+
cwd,
|
|
69
|
+
stdin: "ignore",
|
|
70
|
+
stdout: "pipe",
|
|
71
|
+
stderr: "pipe",
|
|
72
|
+
env: workerGitProcessEnvironment()
|
|
73
|
+
});
|
|
74
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
75
|
+
new Response(subprocess.stdout).text(),
|
|
76
|
+
new Response(subprocess.stderr).text(),
|
|
77
|
+
subprocess.exited
|
|
78
|
+
]);
|
|
79
|
+
return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
80
|
+
}
|
|
81
|
+
async function gitAsync(cwd, args, action) {
|
|
82
|
+
const result = await gitResultAsync(cwd, args);
|
|
83
|
+
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
84
|
+
return result.stdout;
|
|
85
|
+
}
|
|
86
|
+
async function tryGitAsync(cwd, args) {
|
|
87
|
+
return (await gitResultAsync(cwd, args)).exitCode === 0;
|
|
88
|
+
}
|
|
89
|
+
async function revParseAsync(workspacePath, revision) {
|
|
90
|
+
const result = await gitResultAsync(workspacePath, ["rev-parse", "--verify", revision]);
|
|
91
|
+
return result.exitCode === 0 ? result.stdout : null;
|
|
92
|
+
}
|
|
93
|
+
function yieldToWorkerEventLoop() {
|
|
94
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
95
|
+
}
|
|
96
|
+
function workspaceCommitsExist(workspacePath, heads) {
|
|
97
|
+
const uniqueHeads = [...new Set(heads)];
|
|
98
|
+
if (uniqueHeads.length === 0) return true;
|
|
99
|
+
const batch = Bun.spawnSync(gitCommandArgs(["cat-file", "--batch-check"]), {
|
|
100
|
+
cwd: workspacePath,
|
|
101
|
+
stdin: Buffer.from(uniqueHeads.map((head) => `${head}^{commit}
|
|
102
|
+
`).join("")),
|
|
103
|
+
stdout: "pipe",
|
|
104
|
+
stderr: "pipe",
|
|
105
|
+
env: workerGitProcessEnvironment()
|
|
106
|
+
});
|
|
107
|
+
if (batch.exitCode !== 0) return false;
|
|
108
|
+
const lines = batch.stdout.toString().split("\n").filter(Boolean);
|
|
109
|
+
return lines.length === uniqueHeads.length && lines.every((line) => !line.endsWith(" missing"));
|
|
110
|
+
}
|
|
111
|
+
async function workspaceCommitsExistAsync(workspacePath, heads) {
|
|
112
|
+
const uniqueHeads = [...new Set(heads)];
|
|
113
|
+
if (uniqueHeads.length === 0) return true;
|
|
114
|
+
const subprocess = Bun.spawn(gitCommandArgs(["cat-file", "--batch-check"]), {
|
|
115
|
+
cwd: workspacePath,
|
|
116
|
+
stdin: Buffer.from(uniqueHeads.map((head) => `${head}^{commit}
|
|
117
|
+
`).join("")),
|
|
118
|
+
stdout: "pipe",
|
|
119
|
+
stderr: "pipe",
|
|
120
|
+
env: workerGitProcessEnvironment()
|
|
121
|
+
});
|
|
122
|
+
const [stdout, , exitCode] = await Promise.all([
|
|
123
|
+
new Response(subprocess.stdout).text(),
|
|
124
|
+
new Response(subprocess.stderr).text(),
|
|
125
|
+
subprocess.exited
|
|
126
|
+
]);
|
|
127
|
+
if (exitCode !== 0) return false;
|
|
128
|
+
const lines = stdout.split("\n").filter(Boolean);
|
|
129
|
+
return lines.length === uniqueHeads.length && lines.every((line) => !line.endsWith(" missing"));
|
|
130
|
+
}
|
|
63
131
|
function requiredWorkspaceAncestorHeads(value) {
|
|
64
132
|
if (value === void 0) return [];
|
|
65
133
|
if (!Array.isArray(value) || value.length === 0 || value.some((head) => typeof head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head)) || new Set(value).size !== value.length) {
|
|
@@ -83,8 +151,8 @@ function assertWorkspaceRemediationAncestry(input) {
|
|
|
83
151
|
);
|
|
84
152
|
}
|
|
85
153
|
}
|
|
86
|
-
function updateIntegratedWorkspaceHead(workspacePath, head) {
|
|
87
|
-
|
|
154
|
+
async function updateIntegratedWorkspaceHead(workspacePath, head) {
|
|
155
|
+
await gitAsync(workspacePath, ["update-ref", WORKSPACE_GIT_INTEGRATED_REF, head], "record integrated workspace head");
|
|
88
156
|
}
|
|
89
157
|
function fsyncDirectory(directoryPath) {
|
|
90
158
|
const descriptor = fs.openSync(directoryPath, fs.constants.O_RDONLY);
|
|
@@ -188,16 +256,13 @@ function configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, deferMou
|
|
|
188
256
|
return fs.existsSync(mount.sourcePath) || fs.existsSync(outerPath) || mount.sourceMode === "all";
|
|
189
257
|
});
|
|
190
258
|
}
|
|
191
|
-
function
|
|
259
|
+
function parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content) {
|
|
192
260
|
const legacyHead = content.endsWith("\n") ? content.slice(0, -1) : "";
|
|
193
261
|
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(legacyHead) && content === `${legacyHead}
|
|
194
262
|
`) {
|
|
195
|
-
if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
|
|
196
|
-
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
197
|
-
}
|
|
198
263
|
const receipt2 = { version: 3, mounts: [] };
|
|
199
264
|
Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
|
|
200
|
-
return receipt2;
|
|
265
|
+
return { receipt: receipt2, headsToVerify: [legacyHead] };
|
|
201
266
|
}
|
|
202
267
|
let parsed;
|
|
203
268
|
try {
|
|
@@ -265,14 +330,16 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
|
|
|
265
330
|
}
|
|
266
331
|
const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
|
|
267
332
|
if (liftedV2Head) receiptHeads.add(liftedV2Head);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
333
|
+
return { receipt, headsToVerify: [...receiptHeads] };
|
|
334
|
+
}
|
|
335
|
+
function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content) {
|
|
336
|
+
const { receipt, headsToVerify } = parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content);
|
|
337
|
+
if (!workspaceCommitsExist(workspacePath, headsToVerify)) {
|
|
338
|
+
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
|
|
272
339
|
}
|
|
273
340
|
return receipt;
|
|
274
341
|
}
|
|
275
|
-
function
|
|
342
|
+
function readHydratedWorkspaceReceiptUnverified(workspacePath) {
|
|
276
343
|
const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
|
|
277
344
|
const status = lstatIfExists(receiptPath);
|
|
278
345
|
if (!status) return null;
|
|
@@ -280,7 +347,23 @@ function readHydratedWorkspaceReceipt(workspacePath) {
|
|
|
280
347
|
throw new Error(`Workspace hydration receipt is not a regular file: ${receiptPath}`);
|
|
281
348
|
}
|
|
282
349
|
const content = fs.readFileSync(receiptPath, "utf8");
|
|
283
|
-
return
|
|
350
|
+
return { ...parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content), receiptPath };
|
|
351
|
+
}
|
|
352
|
+
function readHydratedWorkspaceReceipt(workspacePath) {
|
|
353
|
+
const unverified = readHydratedWorkspaceReceiptUnverified(workspacePath);
|
|
354
|
+
if (!unverified) return null;
|
|
355
|
+
if (!workspaceCommitsExist(workspacePath, unverified.headsToVerify)) {
|
|
356
|
+
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${unverified.receiptPath}`);
|
|
357
|
+
}
|
|
358
|
+
return unverified.receipt;
|
|
359
|
+
}
|
|
360
|
+
async function readHydratedWorkspaceReceiptAsync(workspacePath) {
|
|
361
|
+
const unverified = readHydratedWorkspaceReceiptUnverified(workspacePath);
|
|
362
|
+
if (!unverified) return null;
|
|
363
|
+
if (!await workspaceCommitsExistAsync(workspacePath, unverified.headsToVerify)) {
|
|
364
|
+
throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${unverified.receiptPath}`);
|
|
365
|
+
}
|
|
366
|
+
return unverified.receipt;
|
|
284
367
|
}
|
|
285
368
|
function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
|
|
286
369
|
const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
|
|
@@ -327,49 +410,142 @@ function desiredWorkspaceBasisRefs(receipt) {
|
|
|
327
410
|
}
|
|
328
411
|
function workspaceBasisRefsMatch(workspacePath, receipt) {
|
|
329
412
|
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
413
|
+
const actual = git(
|
|
414
|
+
workspacePath,
|
|
415
|
+
["for-each-ref", "--format=%(refname) %(objectname)", WORKSPACE_GIT_BASIS_REF_PREFIX],
|
|
416
|
+
`list ${WORKSPACE_GIT_BASIS_REF_PREFIX} refs`
|
|
417
|
+
).split("\n").filter(Boolean);
|
|
418
|
+
if (actual.length !== desired.size) return false;
|
|
419
|
+
return actual.every((line) => {
|
|
420
|
+
const separator = line.lastIndexOf(" ");
|
|
421
|
+
if (separator <= 0) return false;
|
|
422
|
+
return desired.get(line.slice(0, separator)) === line.slice(separator + 1);
|
|
423
|
+
});
|
|
333
424
|
}
|
|
334
425
|
function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
|
|
335
426
|
return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
|
|
336
427
|
}
|
|
428
|
+
async function workspaceBasisRefsAreCurrentAsync(workspacePath, receipt) {
|
|
429
|
+
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
430
|
+
const listing = await gitAsync(
|
|
431
|
+
workspacePath,
|
|
432
|
+
["for-each-ref", "--format=%(refname) %(objectname)", WORKSPACE_GIT_BASIS_REF_PREFIX, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX],
|
|
433
|
+
"list workspace hydration basis refs"
|
|
434
|
+
);
|
|
435
|
+
const stable = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const line of listing.split("\n").filter(Boolean)) {
|
|
437
|
+
const separator = line.lastIndexOf(" ");
|
|
438
|
+
if (separator <= 0) return false;
|
|
439
|
+
const ref = line.slice(0, separator);
|
|
440
|
+
if (ref.startsWith(WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) return false;
|
|
441
|
+
stable.set(ref, line.slice(separator + 1));
|
|
442
|
+
}
|
|
443
|
+
if (stable.size !== desired.size) return false;
|
|
444
|
+
return [...stable].every(([ref, head]) => desired.get(ref) === head);
|
|
445
|
+
}
|
|
337
446
|
function repairWorkspaceBasisRefs(workspacePath, receipt) {
|
|
338
447
|
const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
|
|
339
448
|
if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
|
|
340
449
|
}
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
const transaction = ["start"];
|
|
344
|
-
for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
|
|
345
|
-
for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
|
|
346
|
-
if (!desired.has(ref)) transaction.push(`delete ${ref}`);
|
|
347
|
-
}
|
|
348
|
-
transaction.push("prepare", "commit", "");
|
|
450
|
+
function runWorkspaceRefUpdateBatch(workspacePath, instructions, action) {
|
|
451
|
+
if (instructions.length === 0) return;
|
|
349
452
|
const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
|
|
350
453
|
cwd: workspacePath,
|
|
351
|
-
stdin: Buffer.from(
|
|
454
|
+
stdin: Buffer.from([...instructions, ""].join("\n")),
|
|
352
455
|
stdout: "pipe",
|
|
353
456
|
stderr: "pipe",
|
|
354
457
|
env: workerGitProcessEnvironment()
|
|
355
458
|
});
|
|
356
459
|
if (updated.exitCode !== 0) {
|
|
357
|
-
throw new Error(
|
|
460
|
+
throw new Error(`${action}: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
|
|
358
461
|
}
|
|
359
|
-
|
|
360
|
-
|
|
462
|
+
}
|
|
463
|
+
async function runWorkspaceRefUpdateBatchAsync(workspacePath, instructions, action) {
|
|
464
|
+
if (instructions.length === 0) return;
|
|
465
|
+
const subprocess = Bun.spawn(gitCommandArgs(["update-ref", "--stdin"]), {
|
|
466
|
+
cwd: workspacePath,
|
|
467
|
+
stdin: Buffer.from([...instructions, ""].join("\n")),
|
|
468
|
+
stdout: "pipe",
|
|
469
|
+
stderr: "pipe",
|
|
470
|
+
env: workerGitProcessEnvironment()
|
|
471
|
+
});
|
|
472
|
+
const [, stderr, exitCode] = await Promise.all([
|
|
473
|
+
new Response(subprocess.stdout).text(),
|
|
474
|
+
new Response(subprocess.stderr).text(),
|
|
475
|
+
subprocess.exited
|
|
476
|
+
]);
|
|
477
|
+
if (exitCode !== 0) {
|
|
478
|
+
throw new Error(`${action}: ${stderr.trim() || `git exited ${exitCode}`}`);
|
|
361
479
|
}
|
|
362
480
|
}
|
|
363
|
-
function
|
|
481
|
+
function workspaceBasisRefTransaction(receipt, stableRefs) {
|
|
482
|
+
const desired = desiredWorkspaceBasisRefs(receipt);
|
|
483
|
+
const transaction = ["start"];
|
|
484
|
+
for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
|
|
485
|
+
for (const ref of stableRefs) {
|
|
486
|
+
if (!desired.has(ref)) transaction.push(`delete ${ref}`);
|
|
487
|
+
}
|
|
488
|
+
transaction.push("prepare", "commit");
|
|
489
|
+
return transaction;
|
|
490
|
+
}
|
|
491
|
+
function updateWorkspaceBasisRefs(workspacePath, receipt) {
|
|
492
|
+
runWorkspaceRefUpdateBatch(
|
|
493
|
+
workspacePath,
|
|
494
|
+
workspaceBasisRefTransaction(receipt, listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)),
|
|
495
|
+
"Synchronize workspace hydration basis refs"
|
|
496
|
+
);
|
|
497
|
+
runWorkspaceRefUpdateBatch(
|
|
498
|
+
workspacePath,
|
|
499
|
+
listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).map((ref) => `delete ${ref}`),
|
|
500
|
+
"remove staged workspace hydration basis refs"
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
async function updateWorkspaceBasisRefsAsync(workspacePath, receipt) {
|
|
504
|
+
const stableRefs = (await gitAsync(workspacePath, ["for-each-ref", "--format=%(refname)", WORKSPACE_GIT_BASIS_REF_PREFIX], "list workspace basis refs")).split("\n").filter(Boolean).sort();
|
|
505
|
+
await runWorkspaceRefUpdateBatchAsync(
|
|
506
|
+
workspacePath,
|
|
507
|
+
workspaceBasisRefTransaction(receipt, stableRefs),
|
|
508
|
+
"Synchronize workspace hydration basis refs"
|
|
509
|
+
);
|
|
510
|
+
const stagedRefs = (await gitAsync(
|
|
511
|
+
workspacePath,
|
|
512
|
+
["for-each-ref", "--format=%(refname)", WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX],
|
|
513
|
+
"list staged workspace basis refs"
|
|
514
|
+
)).split("\n").filter(Boolean).sort();
|
|
515
|
+
await runWorkspaceRefUpdateBatchAsync(
|
|
516
|
+
workspacePath,
|
|
517
|
+
stagedRefs.map((ref) => `delete ${ref}`),
|
|
518
|
+
"remove staged workspace hydration basis refs"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
function assertCanonicalHydrationReceiptHeads(receipt) {
|
|
364
522
|
for (const mount of receipt.mounts) {
|
|
365
523
|
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
|
|
366
524
|
throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
|
|
367
525
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function throwMissingHydrationReceiptCommit(workspacePath, receipt) {
|
|
529
|
+
const missing = receipt.mounts.find(({ head }) => !tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`]));
|
|
530
|
+
throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${missing?.head ?? "unknown"}`);
|
|
531
|
+
}
|
|
532
|
+
function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
533
|
+
assertCanonicalHydrationReceiptHeads(receipt);
|
|
534
|
+
if (!workspaceCommitsExist(workspacePath, receipt.mounts.map(({ head }) => head))) {
|
|
535
|
+
throwMissingHydrationReceiptCommit(workspacePath, receipt);
|
|
371
536
|
}
|
|
372
537
|
readHydratedWorkspaceReceipt(workspacePath);
|
|
538
|
+
writeHydratedWorkspaceReceiptCore(workspacePath, receipt);
|
|
539
|
+
}
|
|
540
|
+
async function updateHydratedWorkspaceReceiptAsync(workspacePath, receipt) {
|
|
541
|
+
assertCanonicalHydrationReceiptHeads(receipt);
|
|
542
|
+
if (!await workspaceCommitsExistAsync(workspacePath, receipt.mounts.map(({ head }) => head))) {
|
|
543
|
+
throwMissingHydrationReceiptCommit(workspacePath, receipt);
|
|
544
|
+
}
|
|
545
|
+
await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
546
|
+
await writeHydratedWorkspaceReceiptCoreAsync(workspacePath, receipt);
|
|
547
|
+
}
|
|
548
|
+
function prepareHydratedWorkspaceReceiptWrite(workspacePath) {
|
|
373
549
|
const gitDirectory = path.join(workspacePath, ".git");
|
|
374
550
|
const gitDirectoryStatus = fs.lstatSync(gitDirectory);
|
|
375
551
|
if (!gitDirectoryStatus.isDirectory() || gitDirectoryStatus.isSymbolicLink()) {
|
|
@@ -386,13 +562,16 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
|
386
562
|
if (!receiptDirectoryStatus.isDirectory() || receiptDirectoryStatus.isSymbolicLink()) {
|
|
387
563
|
throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
|
|
388
564
|
}
|
|
389
|
-
|
|
390
|
-
|
|
565
|
+
return { receiptPath, receiptDirectory };
|
|
566
|
+
}
|
|
567
|
+
function stagedWorkspaceBasisRefInstructions(receipt, stagingPrefix) {
|
|
391
568
|
desiredWorkspaceBasisRefs(receipt);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
569
|
+
return receipt.mounts.map(
|
|
570
|
+
(mount) => `update ${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)} ${mount.head}`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
function publishHydratedWorkspaceReceiptFile(workspacePath, receipt, paths) {
|
|
574
|
+
const temporaryPath = path.join(paths.receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
396
575
|
let descriptor;
|
|
397
576
|
try {
|
|
398
577
|
descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
|
|
@@ -401,15 +580,36 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
|
|
|
401
580
|
fs.fsyncSync(descriptor);
|
|
402
581
|
fs.closeSync(descriptor);
|
|
403
582
|
descriptor = void 0;
|
|
404
|
-
fs.renameSync(temporaryPath, receiptPath);
|
|
405
|
-
fsyncDirectory(receiptDirectory);
|
|
406
|
-
updateWorkspaceBasisRefs(workspacePath, receipt);
|
|
583
|
+
fs.renameSync(temporaryPath, paths.receiptPath);
|
|
584
|
+
fsyncDirectory(paths.receiptDirectory);
|
|
407
585
|
} catch (error) {
|
|
408
586
|
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
409
587
|
fs.rmSync(temporaryPath, { force: true });
|
|
410
588
|
throw error;
|
|
411
589
|
}
|
|
412
590
|
}
|
|
591
|
+
function writeHydratedWorkspaceReceiptCore(workspacePath, receipt) {
|
|
592
|
+
const paths = prepareHydratedWorkspaceReceiptWrite(workspacePath);
|
|
593
|
+
const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
|
|
594
|
+
runWorkspaceRefUpdateBatch(
|
|
595
|
+
workspacePath,
|
|
596
|
+
stagedWorkspaceBasisRefInstructions(receipt, stagingPrefix),
|
|
597
|
+
"stage workspace hydration basis refs"
|
|
598
|
+
);
|
|
599
|
+
publishHydratedWorkspaceReceiptFile(workspacePath, receipt, paths);
|
|
600
|
+
updateWorkspaceBasisRefs(workspacePath, receipt);
|
|
601
|
+
}
|
|
602
|
+
async function writeHydratedWorkspaceReceiptCoreAsync(workspacePath, receipt) {
|
|
603
|
+
const paths = prepareHydratedWorkspaceReceiptWrite(workspacePath);
|
|
604
|
+
const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
|
|
605
|
+
await runWorkspaceRefUpdateBatchAsync(
|
|
606
|
+
workspacePath,
|
|
607
|
+
stagedWorkspaceBasisRefInstructions(receipt, stagingPrefix),
|
|
608
|
+
"stage workspace hydration basis refs"
|
|
609
|
+
);
|
|
610
|
+
publishHydratedWorkspaceReceiptFile(workspacePath, receipt, paths);
|
|
611
|
+
await updateWorkspaceBasisRefsAsync(workspacePath, receipt);
|
|
612
|
+
}
|
|
413
613
|
function hydrationTransactionPath(workspacePath) {
|
|
414
614
|
return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_TRANSACTION.split("/"));
|
|
415
615
|
}
|
|
@@ -461,7 +661,7 @@ function fsyncTree(rootPath) {
|
|
|
461
661
|
};
|
|
462
662
|
syncEntry(rootPath);
|
|
463
663
|
}
|
|
464
|
-
function
|
|
664
|
+
function parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, content) {
|
|
465
665
|
let parsed;
|
|
466
666
|
try {
|
|
467
667
|
parsed = JSON.parse(content);
|
|
@@ -484,25 +684,34 @@ function parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, conte
|
|
|
484
684
|
`) {
|
|
485
685
|
throw new Error(`Workspace checkout durability record is noncanonical: ${recordPath}`);
|
|
486
686
|
}
|
|
487
|
-
if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
|
|
488
|
-
throw new Error(`Workspace checkout durability record names a missing commit: ${recordPath}`);
|
|
489
|
-
}
|
|
490
687
|
return record;
|
|
491
688
|
}
|
|
492
|
-
function
|
|
689
|
+
function readWorkspaceCheckoutDurabilityRecordUnverified(workspacePath) {
|
|
493
690
|
const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
|
|
494
691
|
const status = lstatIfExists(recordPath);
|
|
495
692
|
if (!status) return null;
|
|
496
693
|
if (!status.isFile() || status.isSymbolicLink()) {
|
|
497
694
|
throw new Error(`Workspace checkout durability record is not a regular file: ${recordPath}`);
|
|
498
695
|
}
|
|
499
|
-
return
|
|
696
|
+
return { record: parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, fs.readFileSync(recordPath, "utf8")), recordPath };
|
|
500
697
|
}
|
|
501
|
-
function
|
|
502
|
-
|
|
503
|
-
|
|
698
|
+
function readWorkspaceCheckoutDurabilityRecord(workspacePath) {
|
|
699
|
+
const unverified = readWorkspaceCheckoutDurabilityRecordUnverified(workspacePath);
|
|
700
|
+
if (!unverified) return null;
|
|
701
|
+
if (unverified.record.head && !tryGit(workspacePath, ["cat-file", "-e", `${unverified.record.head}^{commit}`])) {
|
|
702
|
+
throw new Error(`Workspace checkout durability record names a missing commit: ${unverified.recordPath}`);
|
|
504
703
|
}
|
|
505
|
-
|
|
704
|
+
return unverified.record;
|
|
705
|
+
}
|
|
706
|
+
async function readWorkspaceCheckoutDurabilityRecordAsync(workspacePath) {
|
|
707
|
+
const unverified = readWorkspaceCheckoutDurabilityRecordUnverified(workspacePath);
|
|
708
|
+
if (!unverified) return null;
|
|
709
|
+
if (unverified.record.head && !await tryGitAsync(workspacePath, ["cat-file", "-e", `${unverified.record.head}^{commit}`])) {
|
|
710
|
+
throw new Error(`Workspace checkout durability record names a missing commit: ${unverified.recordPath}`);
|
|
711
|
+
}
|
|
712
|
+
return unverified.record;
|
|
713
|
+
}
|
|
714
|
+
function writeWorkspaceCheckoutDurabilityRecordFile(workspacePath, record) {
|
|
506
715
|
const stateDirectory = workspacePrivateStateDirectory(workspacePath);
|
|
507
716
|
const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
|
|
508
717
|
const temporaryPath = path.join(stateDirectory, `.workspace-checkout-durability.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
@@ -516,6 +725,20 @@ function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
|
|
|
516
725
|
throw error;
|
|
517
726
|
}
|
|
518
727
|
}
|
|
728
|
+
function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
|
|
729
|
+
if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
|
|
730
|
+
throw new Error(`Cannot record workspace checkout durability for a missing commit: ${record.head}`);
|
|
731
|
+
}
|
|
732
|
+
readWorkspaceCheckoutDurabilityRecord(workspacePath);
|
|
733
|
+
writeWorkspaceCheckoutDurabilityRecordFile(workspacePath, record);
|
|
734
|
+
}
|
|
735
|
+
async function updateWorkspaceCheckoutDurabilityRecordAsync(workspacePath, record) {
|
|
736
|
+
if (record.head && !await tryGitAsync(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
|
|
737
|
+
throw new Error(`Cannot record workspace checkout durability for a missing commit: ${record.head}`);
|
|
738
|
+
}
|
|
739
|
+
await readWorkspaceCheckoutDurabilityRecordAsync(workspacePath);
|
|
740
|
+
writeWorkspaceCheckoutDurabilityRecordFile(workspacePath, record);
|
|
741
|
+
}
|
|
519
742
|
function fsyncWorkspaceCheckoutTree(workspacePath) {
|
|
520
743
|
const status = fs.lstatSync(workspacePath);
|
|
521
744
|
if (!status.isDirectory() || status.isSymbolicLink()) {
|
|
@@ -527,6 +750,40 @@ function fsyncWorkspaceCheckoutTree(workspacePath) {
|
|
|
527
750
|
}
|
|
528
751
|
fsyncDirectory(workspacePath);
|
|
529
752
|
}
|
|
753
|
+
const WORKSPACE_FSYNC_YIELD_INTERVAL = 64;
|
|
754
|
+
async function fsyncWorkspaceCheckoutTreeYielding(workspacePath) {
|
|
755
|
+
const status = fs.lstatSync(workspacePath);
|
|
756
|
+
if (!status.isDirectory() || status.isSymbolicLink()) {
|
|
757
|
+
throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
|
|
758
|
+
}
|
|
759
|
+
let entriesSinceYield = 0;
|
|
760
|
+
const syncEntry = async (entryPath) => {
|
|
761
|
+
const entryStatus = fs.lstatSync(entryPath);
|
|
762
|
+
if (entryStatus.isSymbolicLink()) return;
|
|
763
|
+
if (entryStatus.isDirectory()) {
|
|
764
|
+
for (const name of fs.readdirSync(entryPath)) await syncEntry(path.join(entryPath, name));
|
|
765
|
+
fsyncDirectory(entryPath);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (!entryStatus.isFile()) return;
|
|
769
|
+
const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
770
|
+
try {
|
|
771
|
+
fs.fsyncSync(descriptor);
|
|
772
|
+
} finally {
|
|
773
|
+
fs.closeSync(descriptor);
|
|
774
|
+
}
|
|
775
|
+
entriesSinceYield += 1;
|
|
776
|
+
if (entriesSinceYield >= WORKSPACE_FSYNC_YIELD_INTERVAL) {
|
|
777
|
+
entriesSinceYield = 0;
|
|
778
|
+
await yieldToWorkerEventLoop();
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
for (const name of fs.readdirSync(workspacePath)) {
|
|
782
|
+
if (name === ".git") continue;
|
|
783
|
+
await syncEntry(path.join(workspacePath, name));
|
|
784
|
+
}
|
|
785
|
+
fsyncDirectory(workspacePath);
|
|
786
|
+
}
|
|
530
787
|
function workspaceRebaseInProgress(workspacePath) {
|
|
531
788
|
const gitDirectory = path.join(workspacePath, ".git");
|
|
532
789
|
return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
|
|
@@ -534,8 +791,8 @@ function workspaceRebaseInProgress(workspacePath) {
|
|
|
534
791
|
function workspaceResolutionInProgress(workspacePath) {
|
|
535
792
|
return workspaceRebaseInProgress(workspacePath) || fs.existsSync(path.join(workspacePath, ".git", "MERGE_HEAD"));
|
|
536
793
|
}
|
|
537
|
-
function workspaceHasUnmergedEntries(workspacePath) {
|
|
538
|
-
const result =
|
|
794
|
+
async function workspaceHasUnmergedEntries(workspacePath) {
|
|
795
|
+
const result = await gitResultAsync(workspacePath, ["ls-files", "-u", "-z"]);
|
|
539
796
|
if (result.exitCode !== 0) {
|
|
540
797
|
throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
|
|
541
798
|
}
|
|
@@ -591,17 +848,21 @@ function workspaceCheckoutDurabilityIsCurrent(workspacePath) {
|
|
|
591
848
|
const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
|
|
592
849
|
return Boolean(record?.state === "durable" && record.head === revParse(workspacePath, "HEAD"));
|
|
593
850
|
}
|
|
851
|
+
async function workspaceCheckoutDurabilityIsCurrentAsync(workspacePath) {
|
|
852
|
+
const record = await readWorkspaceCheckoutDurabilityRecordAsync(workspacePath);
|
|
853
|
+
return Boolean(record?.state === "durable" && record.head === await revParseAsync(workspacePath, "HEAD"));
|
|
854
|
+
}
|
|
594
855
|
function beginWorkspaceCheckoutTransition(workspacePath) {
|
|
595
856
|
recoverWorkspaceCheckoutDurability(workspacePath);
|
|
596
857
|
const head = revParse(workspacePath, "HEAD");
|
|
597
858
|
updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "transition", head });
|
|
598
859
|
}
|
|
599
|
-
function completeWorkspaceCheckoutTransition(workspacePath) {
|
|
600
|
-
|
|
601
|
-
|
|
860
|
+
async function completeWorkspaceCheckoutTransition(workspacePath) {
|
|
861
|
+
await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
|
|
862
|
+
await updateWorkspaceCheckoutDurabilityRecordAsync(workspacePath, {
|
|
602
863
|
version: 1,
|
|
603
864
|
state: "durable",
|
|
604
|
-
head:
|
|
865
|
+
head: await revParseAsync(workspacePath, "HEAD")
|
|
605
866
|
});
|
|
606
867
|
}
|
|
607
868
|
function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
|
|
@@ -615,11 +876,11 @@ function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
|
|
|
615
876
|
}
|
|
616
877
|
throw error;
|
|
617
878
|
}
|
|
618
|
-
function runWorkspaceCheckoutTransition(workspacePath, mutate) {
|
|
879
|
+
async function runWorkspaceCheckoutTransition(workspacePath, mutate) {
|
|
619
880
|
beginWorkspaceCheckoutTransition(workspacePath);
|
|
620
881
|
try {
|
|
621
|
-
mutate();
|
|
622
|
-
completeWorkspaceCheckoutTransition(workspacePath);
|
|
882
|
+
await mutate();
|
|
883
|
+
await completeWorkspaceCheckoutTransition(workspacePath);
|
|
623
884
|
} catch (error) {
|
|
624
885
|
throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
|
|
625
886
|
}
|
|
@@ -917,24 +1178,24 @@ function validateMounts(workspacePath, mounts) {
|
|
|
917
1178
|
if (overlap) throw new Error(`Workspace mounts overlap: ${current} and ${overlap}`);
|
|
918
1179
|
}
|
|
919
1180
|
}
|
|
920
|
-
function configureWorkspaceRepository(input) {
|
|
921
|
-
if (
|
|
922
|
-
|
|
1181
|
+
async function configureWorkspaceRepository(input) {
|
|
1182
|
+
if (await tryGitAsync(input.workspacePath, ["remote", "get-url", "origin"])) {
|
|
1183
|
+
await gitAsync(input.workspacePath, ["remote", "set-url", "origin", input.remoteUrl], "configure workspace origin");
|
|
923
1184
|
} else {
|
|
924
|
-
|
|
1185
|
+
await gitAsync(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
|
|
925
1186
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1187
|
+
await tryGitAsync(input.workspacePath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
|
|
1188
|
+
await gitAsync(input.workspacePath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset workspace credential helpers");
|
|
1189
|
+
await gitAsync(input.workspacePath, ["config", "--local", "credential.useHttpPath", "false"], "configure workspace credential path matching");
|
|
929
1190
|
if (input.credentialUsername) {
|
|
930
|
-
|
|
1191
|
+
await gitAsync(
|
|
931
1192
|
input.workspacePath,
|
|
932
1193
|
["config", "--local", "--replace-all", gitCredentialUsernameConfigKey(input.remoteUrl), input.credentialUsername],
|
|
933
1194
|
"configure workspace credential username"
|
|
934
1195
|
);
|
|
935
1196
|
}
|
|
936
1197
|
if (input.credentialHelper) {
|
|
937
|
-
|
|
1198
|
+
await gitAsync(
|
|
938
1199
|
input.workspacePath,
|
|
939
1200
|
["config", "--local", "--add", "credential.helper", input.credentialHelper],
|
|
940
1201
|
"configure workspace credential helper"
|
|
@@ -943,26 +1204,26 @@ function configureWorkspaceRepository(input) {
|
|
|
943
1204
|
const name = input.gitIdentity.name.trim();
|
|
944
1205
|
const email = input.gitIdentity.email.trim();
|
|
945
1206
|
if (!name || !email) throw new Error("Workspace Git identity must include name and email");
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1207
|
+
await gitAsync(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
|
|
1208
|
+
await gitAsync(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
|
|
1209
|
+
await gitAsync(
|
|
949
1210
|
input.workspacePath,
|
|
950
1211
|
["config", "--local", "core.fsync", "committed,reference"],
|
|
951
1212
|
"configure durable workspace commits and references"
|
|
952
1213
|
);
|
|
953
|
-
|
|
1214
|
+
await gitAsync(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
|
|
954
1215
|
}
|
|
955
|
-
function configureExistingWorkspaceGitForRemediation(input) {
|
|
1216
|
+
async function configureExistingWorkspaceGitForRemediation(input) {
|
|
956
1217
|
const workspacePath = path.resolve(input.workspacePath);
|
|
957
1218
|
const workspaceStatus = lstatIfExists(workspacePath);
|
|
958
1219
|
const gitStatus = lstatIfExists(path.join(workspacePath, ".git"));
|
|
959
1220
|
if (!workspaceStatus?.isDirectory() || workspaceStatus.isSymbolicLink() || !gitStatus?.isDirectory() || gitStatus.isSymbolicLink()) {
|
|
960
1221
|
throw new Error("Active workspace remediation requires a regular existing canonical synchronization checkout");
|
|
961
1222
|
}
|
|
962
|
-
configureWorkspaceRepository({ ...input, workspacePath });
|
|
1223
|
+
await configureWorkspaceRepository({ ...input, workspacePath });
|
|
963
1224
|
}
|
|
964
|
-
function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
965
|
-
const result =
|
|
1225
|
+
async function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
|
|
1226
|
+
const result = await gitResultAsync(workspacePath, [
|
|
966
1227
|
...gitTransportSecurityArgs(remoteUrl, credentialHelper, credentialUsername),
|
|
967
1228
|
"fetch",
|
|
968
1229
|
"--no-recurse-submodules",
|
|
@@ -981,7 +1242,7 @@ ${result.stdout}`;
|
|
|
981
1242
|
}
|
|
982
1243
|
return revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
983
1244
|
}
|
|
984
|
-
function ensureWorkspaceGitClone(input) {
|
|
1245
|
+
async function ensureWorkspaceGitClone(input) {
|
|
985
1246
|
const workspacePath = path.resolve(input.workspacePath);
|
|
986
1247
|
const workspaceParent = path.dirname(workspacePath);
|
|
987
1248
|
const parentStatus = fs.lstatSync(workspaceParent);
|
|
@@ -1021,18 +1282,18 @@ function ensureWorkspaceGitClone(input) {
|
|
|
1021
1282
|
git(workspacePath, ["init", `--initial-branch=${WORKSPACE_GIT_BRANCH}`], "initialize workspace clone");
|
|
1022
1283
|
}
|
|
1023
1284
|
}
|
|
1024
|
-
configureWorkspaceRepository({ ...input, workspacePath });
|
|
1285
|
+
await configureWorkspaceRepository({ ...input, workspacePath });
|
|
1025
1286
|
recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
|
|
1026
|
-
const previousRemoteHead =
|
|
1027
|
-
const localHeadBeforeFetch =
|
|
1028
|
-
if (!
|
|
1029
|
-
updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
|
|
1287
|
+
const previousRemoteHead = await revParseAsync(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
1288
|
+
const localHeadBeforeFetch = await revParseAsync(workspacePath, "HEAD");
|
|
1289
|
+
if (!await revParseAsync(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && await tryGitAsync(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
1290
|
+
await updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
|
|
1030
1291
|
}
|
|
1031
|
-
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
1032
|
-
let localHead =
|
|
1292
|
+
const remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
1293
|
+
let localHead = await revParseAsync(workspacePath, "HEAD");
|
|
1033
1294
|
if (!localHead && remoteHead) {
|
|
1034
|
-
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
1035
|
-
|
|
1295
|
+
await runWorkspaceCheckoutTransition(workspacePath, async () => {
|
|
1296
|
+
await gitAsync(
|
|
1036
1297
|
workspacePath,
|
|
1037
1298
|
["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
|
|
1038
1299
|
"check out workspace main"
|
|
@@ -1041,12 +1302,12 @@ function ensureWorkspaceGitClone(input) {
|
|
|
1041
1302
|
localHead = remoteHead;
|
|
1042
1303
|
}
|
|
1043
1304
|
if (localHead && remoteHead && localHead === remoteHead) {
|
|
1044
|
-
updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
1305
|
+
await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
1045
1306
|
} else if (localHead && remoteHead && workspaceIsShallow(workspacePath) && !tryGit(workspacePath, ["merge-base", localHead, remoteHead]) && revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) === localHead && workspaceIsClean(workspacePath)) {
|
|
1046
|
-
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
1047
|
-
|
|
1307
|
+
await runWorkspaceCheckoutTransition(workspacePath, async () => {
|
|
1308
|
+
await gitAsync(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
|
|
1048
1309
|
});
|
|
1049
|
-
updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
1310
|
+
await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
1050
1311
|
localHead = remoteHead;
|
|
1051
1312
|
}
|
|
1052
1313
|
return { localHead, remoteHead };
|
|
@@ -1170,7 +1431,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1170
1431
|
}
|
|
1171
1432
|
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
|
|
1172
1433
|
}
|
|
1173
|
-
function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
1434
|
+
async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
1174
1435
|
const workspacePath = path.resolve(input.workspacePath);
|
|
1175
1436
|
const durabilityMounts = input.durabilityMounts ?? input.mounts;
|
|
1176
1437
|
const receiptMounts = input.receiptMounts ?? durabilityMounts;
|
|
@@ -1178,19 +1439,34 @@ function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1178
1439
|
const candidateMounts = [
|
|
1179
1440
|
...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
|
|
1180
1441
|
];
|
|
1181
|
-
|
|
1182
|
-
const requestedIds = new Set(input.mounts.map(({ id }) => id));
|
|
1183
|
-
const requiredIds = new Set(requiredMounts.map(({ id }) => id));
|
|
1184
|
-
const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1185
|
-
const advancedDurabilityMounts = durabilityMounts.filter(
|
|
1186
|
-
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1187
|
-
);
|
|
1188
|
-
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
1189
|
-
if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
|
|
1442
|
+
if (input.recordCurrentHead && !await workspaceCheckoutDurabilityIsCurrentAsync(workspacePath)) {
|
|
1190
1443
|
throw new Error("Workspace checkout is not durably materialized at its current HEAD");
|
|
1191
1444
|
}
|
|
1192
|
-
const targetHead = input.recordCurrentHead ?
|
|
1193
|
-
const
|
|
1445
|
+
const targetHead = input.recordCurrentHead ? await revParseAsync(workspacePath, "HEAD") : null;
|
|
1446
|
+
const receiptBeforeTransaction = targetHead ? await readHydratedWorkspaceReceiptAsync(workspacePath) : null;
|
|
1447
|
+
const computeSelection = () => {
|
|
1448
|
+
const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1449
|
+
const requestedIds = new Set(input.mounts.map(({ id }) => id));
|
|
1450
|
+
const requiredIds = new Set(requiredMounts.map(({ id }) => id));
|
|
1451
|
+
const hydrationMounts2 = input.mounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1452
|
+
const advancedDurabilityMounts2 = durabilityMounts.filter(
|
|
1453
|
+
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1454
|
+
);
|
|
1455
|
+
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
1456
|
+
return {
|
|
1457
|
+
busyMountIds,
|
|
1458
|
+
hydrationMounts: hydrationMounts2,
|
|
1459
|
+
advancedDurabilityMounts: advancedDurabilityMounts2,
|
|
1460
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
|
|
1461
|
+
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, advancedDurabilityMounts2, receiptMounts) : null
|
|
1462
|
+
};
|
|
1463
|
+
};
|
|
1464
|
+
const noopCandidate = computeSelection();
|
|
1465
|
+
if (noopCandidate.hydrationMounts.length === 0 && noopCandidate.advancedDurabilityMounts.length === 0 && noopCandidate.targetReceipt && lstatIfExists(hydrationTransactionPath(workspacePath)) === null && workspaceHydrationReceiptsEqual(receiptBeforeTransaction, noopCandidate.targetReceipt) && hydratedWorkspaceReceiptFileIsV3(workspacePath) && await workspaceBasisRefsAreCurrentAsync(workspacePath, noopCandidate.targetReceipt)) {
|
|
1466
|
+
return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
|
|
1467
|
+
}
|
|
1468
|
+
const selection = computeSelection();
|
|
1469
|
+
const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
|
|
1194
1470
|
const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
1195
1471
|
try {
|
|
1196
1472
|
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
|
|
@@ -1204,7 +1480,7 @@ function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1204
1480
|
removeHydrationTransaction(workspacePath);
|
|
1205
1481
|
return {
|
|
1206
1482
|
hydratedMountIds: hydration.hydratedMountIds,
|
|
1207
|
-
skippedMountIds:
|
|
1483
|
+
skippedMountIds: selection.skippedMountIds
|
|
1208
1484
|
};
|
|
1209
1485
|
} catch (error) {
|
|
1210
1486
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
@@ -1223,7 +1499,7 @@ function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1223
1499
|
throw error;
|
|
1224
1500
|
}
|
|
1225
1501
|
}
|
|
1226
|
-
function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
1502
|
+
async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
1227
1503
|
const gitDirectory = path.join(path.resolve(workspacePath), ".git");
|
|
1228
1504
|
if (!fs.existsSync(gitDirectory)) {
|
|
1229
1505
|
return hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options);
|
|
@@ -1235,16 +1511,18 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
|
1235
1511
|
ignoreBusy: options.ignoreBusy
|
|
1236
1512
|
});
|
|
1237
1513
|
}
|
|
1238
|
-
function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
1514
|
+
async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
1239
1515
|
workspacePath = path.resolve(workspacePath);
|
|
1240
1516
|
validateMounts(workspacePath, mounts);
|
|
1241
1517
|
recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
|
|
1242
1518
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1243
|
-
const currentHead =
|
|
1244
|
-
const currentReceipt =
|
|
1245
|
-
|
|
1519
|
+
const currentHead = await revParseAsync(workspacePath, "HEAD");
|
|
1520
|
+
const currentReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
1521
|
+
if (!await workspaceBasisRefsAreCurrentAsync(workspacePath, currentReceipt ?? { version: 3, mounts: [] })) {
|
|
1522
|
+
repairWorkspaceBasisRefs(workspacePath, currentReceipt);
|
|
1523
|
+
}
|
|
1246
1524
|
if (!currentHead) return;
|
|
1247
|
-
const outerHeadContainsMount = (mount) =>
|
|
1525
|
+
const outerHeadContainsMount = (mount) => tryGitAsync(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
|
|
1248
1526
|
const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
|
|
1249
1527
|
const uncoveredBasisMounts = configuredBasisMounts.filter(
|
|
1250
1528
|
(mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
|
|
@@ -1255,10 +1533,15 @@ function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
|
1255
1533
|
(mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
|
|
1256
1534
|
);
|
|
1257
1535
|
const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
|
|
1258
|
-
const hydrationTargets =
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1536
|
+
const hydrationTargets = [];
|
|
1537
|
+
for (const mount of idleBasislessMounts) {
|
|
1538
|
+
if (mount.deleteWhenSourceMissing) continue;
|
|
1539
|
+
if (mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) {
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (await outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true) hydrationTargets.push(mount);
|
|
1543
|
+
}
|
|
1544
|
+
const hydration = await hydrateWorkspaceGitMountsTransactionally({
|
|
1262
1545
|
workspacePath,
|
|
1263
1546
|
mounts: hydrationTargets,
|
|
1264
1547
|
durabilityMounts: idleBasislessMounts,
|
|
@@ -1269,27 +1552,31 @@ function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
|
1269
1552
|
});
|
|
1270
1553
|
void hydration;
|
|
1271
1554
|
}
|
|
1272
|
-
function resetWorkspaceGit(input) {
|
|
1555
|
+
async function resetWorkspaceGit(input) {
|
|
1273
1556
|
const workspacePath = path.resolve(input.workspacePath);
|
|
1274
1557
|
validateMounts(workspacePath, input.mounts);
|
|
1275
1558
|
const busyResetMountIds = input.mounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
|
|
1276
1559
|
if (busyResetMountIds.length > 0) {
|
|
1277
1560
|
throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
|
|
1278
1561
|
}
|
|
1279
|
-
const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1562
|
+
const initial = await ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1280
1563
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1281
1564
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
1282
1565
|
const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
|
|
1283
|
-
const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
1566
|
+
const remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
1284
1567
|
if (remoteHead) {
|
|
1285
|
-
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
1286
|
-
|
|
1287
|
-
|
|
1568
|
+
await runWorkspaceCheckoutTransition(workspacePath, async () => {
|
|
1569
|
+
await gitAsync(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
|
|
1570
|
+
await gitAsync(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
|
|
1288
1571
|
});
|
|
1289
1572
|
} else if (revParse(workspacePath, "HEAD")) {
|
|
1290
|
-
runWorkspaceCheckoutTransition(workspacePath, () => {
|
|
1291
|
-
|
|
1292
|
-
|
|
1573
|
+
await runWorkspaceCheckoutTransition(workspacePath, async () => {
|
|
1574
|
+
await gitAsync(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
|
|
1575
|
+
await gitAsync(
|
|
1576
|
+
workspacePath,
|
|
1577
|
+
["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })],
|
|
1578
|
+
"record empty workspace reset"
|
|
1579
|
+
);
|
|
1293
1580
|
});
|
|
1294
1581
|
}
|
|
1295
1582
|
const selected = activeMounts(input.mounts);
|
|
@@ -1299,14 +1586,17 @@ function resetWorkspaceGit(input) {
|
|
|
1299
1586
|
const outerPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
1300
1587
|
return fs.existsSync(outerPath);
|
|
1301
1588
|
});
|
|
1589
|
+
const bootstrapMountIds = new Set(bootstrapMounts.map(({ id }) => id));
|
|
1590
|
+
const durablyAbsentMounts = selected.skipped.filter((mount) => !bootstrapMountIds.has(mount.id) && mount.sourceMode === "all");
|
|
1591
|
+
const uncoveredSkippedMounts = selected.skipped.filter((mount) => !bootstrapMountIds.has(mount.id) && mount.sourceMode !== "all");
|
|
1302
1592
|
const hydrationMounts = [...selected.active, ...bootstrapMounts];
|
|
1303
1593
|
const requiredLiveMounts = [
|
|
1304
1594
|
...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
|
|
1305
1595
|
];
|
|
1306
|
-
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1596
|
+
const hydration = await hydrateWorkspaceGitMountsTransactionally({
|
|
1307
1597
|
workspacePath,
|
|
1308
1598
|
mounts: hydrationMounts,
|
|
1309
|
-
durabilityMounts: [...requiredLiveMounts, ...selected.tombstones],
|
|
1599
|
+
durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
|
|
1310
1600
|
receiptMounts: input.mounts,
|
|
1311
1601
|
requiredMounts: requiredLiveMounts,
|
|
1312
1602
|
recordCurrentHead: true
|
|
@@ -1322,11 +1612,11 @@ function resetWorkspaceGit(input) {
|
|
|
1322
1612
|
remoteHead,
|
|
1323
1613
|
discardedPaths,
|
|
1324
1614
|
activeMountIds: hydration.hydratedMountIds,
|
|
1325
|
-
skippedMountIds: [.../* @__PURE__ */ new Set([...
|
|
1615
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...uncoveredSkippedMounts.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
|
|
1326
1616
|
};
|
|
1327
1617
|
}
|
|
1328
|
-
function stagedPaths(workspacePath) {
|
|
1329
|
-
return
|
|
1618
|
+
async function stagedPaths(workspacePath) {
|
|
1619
|
+
return (await gitAsync(workspacePath, ["diff", "--cached", "--name-only", "--no-renames", "-z"], "inspect staged workspace paths")).split("\0").filter(Boolean).sort();
|
|
1330
1620
|
}
|
|
1331
1621
|
function emptyTreeHash(workspacePath) {
|
|
1332
1622
|
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, "hash-object", "-t", "tree", "--stdin"], {
|
|
@@ -1340,9 +1630,9 @@ function emptyTreeHash(workspacePath) {
|
|
|
1340
1630
|
throw new Error(`Create empty workspace tree: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
1341
1631
|
return result.stdout.toString().trim();
|
|
1342
1632
|
}
|
|
1343
|
-
function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
1633
|
+
async function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
1344
1634
|
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
1345
|
-
return
|
|
1635
|
+
return (await gitAsync(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths")).split("\0").filter(Boolean).sort();
|
|
1346
1636
|
}
|
|
1347
1637
|
async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
|
|
1348
1638
|
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
@@ -1385,15 +1675,15 @@ function snapshotConflict(input) {
|
|
|
1385
1675
|
git(input.workspacePath, ["update-ref", refs.remote, input.remoteHead], "snapshot remote workspace conflict head");
|
|
1386
1676
|
return refs;
|
|
1387
1677
|
}
|
|
1388
|
-
function rebaseWorkspace(input) {
|
|
1678
|
+
async function rebaseWorkspace(input) {
|
|
1389
1679
|
const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
|
|
1390
1680
|
beginWorkspaceCheckoutTransition(input.workspacePath);
|
|
1391
|
-
const rebase =
|
|
1681
|
+
const rebase = await gitResultAsync(
|
|
1392
1682
|
input.workspacePath,
|
|
1393
1683
|
input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
|
|
1394
1684
|
);
|
|
1395
1685
|
if (rebase.exitCode === 0) {
|
|
1396
|
-
completeWorkspaceCheckoutTransition(input.workspacePath);
|
|
1686
|
+
await completeWorkspaceCheckoutTransition(input.workspacePath);
|
|
1397
1687
|
return { ok: true };
|
|
1398
1688
|
}
|
|
1399
1689
|
const conflicts = conflictPaths(input.workspacePath);
|
|
@@ -1402,7 +1692,7 @@ function rebaseWorkspace(input) {
|
|
|
1402
1692
|
if (abort.exitCode !== 0) {
|
|
1403
1693
|
throw new Error(`Abort workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
|
|
1404
1694
|
}
|
|
1405
|
-
completeWorkspaceCheckoutTransition(input.workspacePath);
|
|
1695
|
+
await completeWorkspaceCheckoutTransition(input.workspacePath);
|
|
1406
1696
|
return {
|
|
1407
1697
|
ok: false,
|
|
1408
1698
|
conflictPaths: conflicts,
|
|
@@ -1410,13 +1700,13 @@ function rebaseWorkspace(input) {
|
|
|
1410
1700
|
error: rebase.stderr || rebase.stdout || `Workspace rebase onto ${input.remoteHead} conflicted`
|
|
1411
1701
|
};
|
|
1412
1702
|
}
|
|
1413
|
-
function synchronizeWithFetchedHead(input) {
|
|
1414
|
-
const localHead =
|
|
1703
|
+
async function synchronizeWithFetchedHead(input) {
|
|
1704
|
+
const localHead = await revParseAsync(input.workspacePath, "HEAD");
|
|
1415
1705
|
const remoteHead = input.remoteHead;
|
|
1416
1706
|
if (!remoteHead) return { kind: "ready", updated: false, rebased: false };
|
|
1417
1707
|
if (!localHead) {
|
|
1418
|
-
runWorkspaceCheckoutTransition(input.workspacePath, () => {
|
|
1419
|
-
|
|
1708
|
+
await runWorkspaceCheckoutTransition(input.workspacePath, async () => {
|
|
1709
|
+
await gitAsync(
|
|
1420
1710
|
input.workspacePath,
|
|
1421
1711
|
["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
|
|
1422
1712
|
"check out fetched workspace main"
|
|
@@ -1425,35 +1715,35 @@ function synchronizeWithFetchedHead(input) {
|
|
|
1425
1715
|
return { kind: "ready", updated: true, rebased: false };
|
|
1426
1716
|
}
|
|
1427
1717
|
if (localHead === remoteHead) {
|
|
1428
|
-
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
1718
|
+
await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
1429
1719
|
return { kind: "ready", updated: false, rebased: false };
|
|
1430
1720
|
}
|
|
1431
|
-
if (
|
|
1432
|
-
runWorkspaceCheckoutTransition(input.workspacePath, () => {
|
|
1433
|
-
|
|
1721
|
+
if (await tryGitAsync(input.workspacePath, ["merge-base", "--is-ancestor", localHead, remoteHead])) {
|
|
1722
|
+
await runWorkspaceCheckoutTransition(input.workspacePath, async () => {
|
|
1723
|
+
await gitAsync(input.workspacePath, ["reset", "--hard", remoteHead], "fast-forward workspace main");
|
|
1434
1724
|
});
|
|
1435
|
-
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
1725
|
+
await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
1436
1726
|
return { kind: "ready", updated: true, rebased: false };
|
|
1437
1727
|
}
|
|
1438
|
-
if (
|
|
1439
|
-
updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
1728
|
+
if (await tryGitAsync(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
|
|
1729
|
+
await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
1440
1730
|
return { kind: "ready", updated: false, rebased: false };
|
|
1441
1731
|
}
|
|
1442
|
-
const integratedHead =
|
|
1732
|
+
const integratedHead = await revParseAsync(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
|
|
1443
1733
|
const truncatedHistoryBase = workspaceIsShallow(input.workspacePath) && integratedHead && tryGit(input.workspacePath, ["merge-base", "--is-ancestor", integratedHead, localHead]) ? integratedHead : void 0;
|
|
1444
1734
|
if (workspaceIsShallow(input.workspacePath) && !truncatedHistoryBase) {
|
|
1445
1735
|
throw new Error(
|
|
1446
1736
|
"Workspace history was truncated before this clone's integrated base could be identified; reset the workspace clone before retrying"
|
|
1447
1737
|
);
|
|
1448
1738
|
}
|
|
1449
|
-
const rebased = rebaseWorkspace({
|
|
1739
|
+
const rebased = await rebaseWorkspace({
|
|
1450
1740
|
workspacePath: input.workspacePath,
|
|
1451
1741
|
localHead,
|
|
1452
1742
|
remoteHead,
|
|
1453
1743
|
attemptId: input.attemptId,
|
|
1454
1744
|
truncatedHistoryBase
|
|
1455
1745
|
});
|
|
1456
|
-
if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
1746
|
+
if (rebased.ok) await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
|
|
1457
1747
|
return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
|
|
1458
1748
|
}
|
|
1459
1749
|
async function synchronizeWorkspaceGit(input) {
|
|
@@ -1464,7 +1754,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1464
1754
|
validateMounts(workspacePath, input.mounts);
|
|
1465
1755
|
const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
|
|
1466
1756
|
const preserveResolutionInProgress = input.skipMountMirror === true;
|
|
1467
|
-
const initial = ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
|
|
1757
|
+
const initial = await ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
|
|
1468
1758
|
assertWorkspaceRemediationAncestry({
|
|
1469
1759
|
workspacePath,
|
|
1470
1760
|
currentHead: initial.localHead,
|
|
@@ -1473,7 +1763,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1473
1763
|
});
|
|
1474
1764
|
const verifiedAncestorHeads = requiredAncestorHeads.length > 0 ? requiredAncestorHeads : void 0;
|
|
1475
1765
|
const startingHead = initial.localHead;
|
|
1476
|
-
const receiptBeforeInitialHydration =
|
|
1766
|
+
const receiptBeforeInitialHydration = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
1477
1767
|
const deferredMountIds = /* @__PURE__ */ new Set();
|
|
1478
1768
|
if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
|
|
1479
1769
|
for (const mount of input.mounts) {
|
|
@@ -1482,7 +1772,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1482
1772
|
}
|
|
1483
1773
|
}
|
|
1484
1774
|
}
|
|
1485
|
-
recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1775
|
+
await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1486
1776
|
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
|
|
1487
1777
|
preserveResolutionInProgress,
|
|
1488
1778
|
preserveStaleBases: true
|
|
@@ -1493,8 +1783,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1493
1783
|
);
|
|
1494
1784
|
const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
|
|
1495
1785
|
const receiptRequiredLiveMounts = selected.active;
|
|
1496
|
-
const projectionBasisHead =
|
|
1497
|
-
const projectionBasisReceipt =
|
|
1786
|
+
const projectionBasisHead = await revParseAsync(workspacePath, "HEAD");
|
|
1787
|
+
const projectionBasisReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
1498
1788
|
const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
|
|
1499
1789
|
const fastProjectionMounts = [];
|
|
1500
1790
|
const mergedProjectionTrees = /* @__PURE__ */ new Map();
|
|
@@ -1535,6 +1825,10 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1535
1825
|
projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
|
|
1536
1826
|
continue;
|
|
1537
1827
|
}
|
|
1828
|
+
if (cycleSkippedMountIds.has(mount.id) || mount.busy?.()) {
|
|
1829
|
+
cycleSkippedMountIds.add(mount.id);
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1538
1832
|
const merged = mergeWorkspaceProjectionMount({
|
|
1539
1833
|
workspacePath,
|
|
1540
1834
|
mount: {
|
|
@@ -1569,14 +1863,15 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1569
1863
|
}
|
|
1570
1864
|
mergedProjectionTrees.set(mount.id, merged.resultTree);
|
|
1571
1865
|
mergedProjectionMounts.push(mount);
|
|
1866
|
+
await yieldToWorkerEventLoop();
|
|
1572
1867
|
}
|
|
1573
1868
|
}
|
|
1574
1869
|
if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
|
|
1575
1870
|
`);
|
|
1576
|
-
const completedMountSelection = (requireCurrentHeadReceipt = false) => {
|
|
1577
|
-
const currentHeadBeforeHydration =
|
|
1871
|
+
const completedMountSelection = async (requireCurrentHeadReceipt = false) => {
|
|
1872
|
+
const currentHeadBeforeHydration = await revParseAsync(workspacePath, "HEAD");
|
|
1578
1873
|
const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
|
|
1579
|
-
const currentReceiptBeforeHydration =
|
|
1874
|
+
const currentReceiptBeforeHydration = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
1580
1875
|
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
1581
1876
|
const mountIsAlreadyCovered = (mount) => Boolean(
|
|
1582
1877
|
currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
|
|
@@ -1593,7 +1888,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1593
1888
|
const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
|
|
1594
1889
|
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1595
1890
|
);
|
|
1596
|
-
const hydration = hydrateWorkspaceGitMountsTransactionally({
|
|
1891
|
+
const hydration = await hydrateWorkspaceGitMountsTransactionally({
|
|
1597
1892
|
workspacePath,
|
|
1598
1893
|
mounts: uncoveredCompletionMounts,
|
|
1599
1894
|
durabilityMounts: uncoveredCompletionBasisMounts,
|
|
@@ -1604,8 +1899,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1604
1899
|
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
1605
1900
|
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
1606
1901
|
if (requireCurrentHeadReceipt) {
|
|
1607
|
-
const currentHead =
|
|
1608
|
-
const receipt =
|
|
1902
|
+
const currentHead = await revParseAsync(workspacePath, "HEAD");
|
|
1903
|
+
const receipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
1609
1904
|
const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
|
|
1610
1905
|
const idleMounts = completionReceiptMounts.filter(
|
|
1611
1906
|
(mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mount.busy?.() && !projectionSkippedIds.has(mount.id)
|
|
@@ -1617,17 +1912,17 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1617
1912
|
return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
|
|
1618
1913
|
};
|
|
1619
1914
|
let newlyStagedPaths = [];
|
|
1620
|
-
const stageAndCommitWorkspace = () => {
|
|
1621
|
-
if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
|
|
1915
|
+
const stageAndCommitWorkspace = async () => {
|
|
1916
|
+
if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
|
|
1622
1917
|
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
1623
1918
|
}
|
|
1624
|
-
|
|
1625
|
-
newlyStagedPaths = stagedPaths(workspacePath);
|
|
1919
|
+
await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
|
|
1920
|
+
newlyStagedPaths = await stagedPaths(workspacePath);
|
|
1626
1921
|
if (newlyStagedPaths.length > 0) {
|
|
1627
|
-
if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
|
|
1922
|
+
if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
|
|
1628
1923
|
throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
|
|
1629
1924
|
}
|
|
1630
|
-
|
|
1925
|
+
await gitAsync(
|
|
1631
1926
|
workspacePath,
|
|
1632
1927
|
[
|
|
1633
1928
|
"commit",
|
|
@@ -1638,64 +1933,77 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1638
1933
|
);
|
|
1639
1934
|
}
|
|
1640
1935
|
};
|
|
1936
|
+
const projectedFastMounts = [];
|
|
1641
1937
|
if (!input.skipMountMirror) {
|
|
1642
1938
|
beginWorkspaceCheckoutTransition(workspacePath);
|
|
1643
1939
|
try {
|
|
1644
1940
|
const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
|
|
1645
1941
|
const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
|
|
1646
1942
|
const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
|
|
1647
|
-
|
|
1943
|
+
for (const mount of fastActiveMounts) {
|
|
1944
|
+
if (cycleSkippedMountIds.has(mount.id) || mount.busy?.()) {
|
|
1945
|
+
cycleSkippedMountIds.add(mount.id);
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
mirrorMountsToWorkspace(workspacePath, [mount]);
|
|
1949
|
+
projectedFastMounts.push(mount);
|
|
1950
|
+
await yieldToWorkerEventLoop();
|
|
1951
|
+
}
|
|
1648
1952
|
removeWorkspaceMounts(workspacePath, fastTombstones);
|
|
1953
|
+
projectedFastMounts.push(...fastTombstones);
|
|
1649
1954
|
for (const mount of mergedProjectionMounts) {
|
|
1650
1955
|
materializeWorkspaceProjectionTree({
|
|
1651
1956
|
workspacePath,
|
|
1652
1957
|
workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
|
|
1653
1958
|
resultTree: mergedProjectionTrees.get(mount.id)
|
|
1654
1959
|
});
|
|
1960
|
+
await yieldToWorkerEventLoop();
|
|
1655
1961
|
}
|
|
1656
|
-
const restoreFromHeadIds = /* @__PURE__ */ new Set([...deferredMountIds, ...selected.skipped.map(({ id }) => id)]);
|
|
1657
1962
|
restoreDeferredWorkspaceMountsFromHead(
|
|
1658
1963
|
workspacePath,
|
|
1659
|
-
input.mounts.filter(({ id }) =>
|
|
1964
|
+
input.mounts.filter(({ id }) => deferredMountIds.has(id))
|
|
1660
1965
|
);
|
|
1661
|
-
stageAndCommitWorkspace();
|
|
1662
|
-
|
|
1966
|
+
await stageAndCommitWorkspace();
|
|
1967
|
+
await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
|
|
1663
1968
|
} catch (error) {
|
|
1664
1969
|
throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
|
|
1665
1970
|
}
|
|
1666
1971
|
} else {
|
|
1667
|
-
stageAndCommitWorkspace();
|
|
1668
|
-
if (newlyStagedPaths.length > 0) completeWorkspaceCheckoutTransition(workspacePath);
|
|
1972
|
+
await stageAndCommitWorkspace();
|
|
1973
|
+
if (newlyStagedPaths.length > 0) await completeWorkspaceCheckoutTransition(workspacePath);
|
|
1669
1974
|
}
|
|
1670
1975
|
if (!input.skipMountMirror) {
|
|
1671
|
-
const projectedHead =
|
|
1976
|
+
const projectedHead = await revParseAsync(workspacePath, "HEAD");
|
|
1672
1977
|
if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
|
|
1673
1978
|
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
1674
1979
|
projectionBasisReceipt,
|
|
1675
1980
|
projectedHead,
|
|
1676
|
-
|
|
1981
|
+
projectedFastMounts,
|
|
1677
1982
|
allProjectionReceiptMounts
|
|
1678
1983
|
);
|
|
1679
|
-
const currentProjectionReceipt =
|
|
1680
|
-
if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !
|
|
1681
|
-
|
|
1682
|
-
|
|
1984
|
+
const currentProjectionReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
1985
|
+
if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !await workspaceBasisRefsAreCurrentAsync(workspacePath, targetProjectionReceipt)) {
|
|
1986
|
+
for (const mount of projectedFastMounts) {
|
|
1987
|
+
fsyncHydratedWorkspaceMount(mount);
|
|
1988
|
+
await yieldToWorkerEventLoop();
|
|
1989
|
+
}
|
|
1990
|
+
await updateHydratedWorkspaceReceiptAsync(workspacePath, targetProjectionReceipt);
|
|
1683
1991
|
}
|
|
1684
|
-
completeWorkspaceCheckoutTransition(workspacePath);
|
|
1992
|
+
await completeWorkspaceCheckoutTransition(workspacePath);
|
|
1685
1993
|
}
|
|
1686
|
-
let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
1994
|
+
let remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
1687
1995
|
let rebaseCount = 0;
|
|
1688
1996
|
let updated = false;
|
|
1689
1997
|
for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
|
|
1690
1998
|
assertWorkspaceRemediationAncestry({
|
|
1691
1999
|
workspacePath,
|
|
1692
|
-
currentHead:
|
|
2000
|
+
currentHead: await revParseAsync(workspacePath, "HEAD"),
|
|
1693
2001
|
remoteHead,
|
|
1694
2002
|
requiredAncestorHeads
|
|
1695
2003
|
});
|
|
1696
|
-
const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
|
|
2004
|
+
const reconciled = await synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
|
|
1697
2005
|
if (reconciled.kind === "conflict") {
|
|
1698
|
-
const localHead2 =
|
|
2006
|
+
const localHead2 = await revParseAsync(workspacePath, "HEAD");
|
|
1699
2007
|
const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
|
|
1700
2008
|
return {
|
|
1701
2009
|
outcome: "conflict_blocked",
|
|
@@ -1716,8 +2024,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1716
2024
|
}
|
|
1717
2025
|
updated ||= reconciled.updated;
|
|
1718
2026
|
if (reconciled.rebased) rebaseCount += 1;
|
|
1719
|
-
if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
|
|
1720
|
-
const localHead =
|
|
2027
|
+
if (reconciled.updated || reconciled.rebased) await completedMountSelection(true);
|
|
2028
|
+
const localHead = await revParseAsync(workspacePath, "HEAD");
|
|
1721
2029
|
if (!localHead) {
|
|
1722
2030
|
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
|
|
1723
2031
|
return {
|
|
@@ -1734,7 +2042,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1734
2042
|
...projectionWarning ? { error: projectionWarning } : {}
|
|
1735
2043
|
};
|
|
1736
2044
|
}
|
|
1737
|
-
const paths = changedPaths(workspacePath, remoteHead, localHead);
|
|
2045
|
+
const paths = await changedPaths(workspacePath, remoteHead, localHead);
|
|
1738
2046
|
const size = paths.length > 0 ? await (input.measureDiffSize ?? diffSizeBytes)(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
|
|
1739
2047
|
input.assertStillAdmitted?.();
|
|
1740
2048
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
@@ -1755,7 +2063,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1755
2063
|
}
|
|
1756
2064
|
if (localHead === remoteHead) {
|
|
1757
2065
|
assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
|
|
1758
|
-
const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
|
|
2066
|
+
const completedMounts = await completedMountSelection(Boolean(input.skipMountMirror));
|
|
1759
2067
|
await input.afterWorkspacePublished?.({
|
|
1760
2068
|
publishedHead: localHead,
|
|
1761
2069
|
activeMountIds: completedMounts.activeMountIds
|
|
@@ -1783,11 +2091,11 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1783
2091
|
];
|
|
1784
2092
|
if (input.allowLargeDiff) pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
1785
2093
|
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
1786
|
-
const push =
|
|
2094
|
+
const push = await gitResultAsync(workspacePath, pushArgs);
|
|
1787
2095
|
if (push.exitCode === 0) {
|
|
1788
2096
|
assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
|
|
1789
|
-
updateIntegratedWorkspaceHead(workspacePath, localHead);
|
|
1790
|
-
const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
|
|
2097
|
+
await updateIntegratedWorkspaceHead(workspacePath, localHead);
|
|
2098
|
+
const completedMounts = await completedMountSelection(Boolean(input.skipMountMirror));
|
|
1791
2099
|
await input.afterWorkspacePublished?.({
|
|
1792
2100
|
publishedHead: localHead,
|
|
1793
2101
|
activeMountIds: completedMounts.activeMountIds
|
|
@@ -1811,7 +2119,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1811
2119
|
${push.stdout}`)) {
|
|
1812
2120
|
throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
|
|
1813
2121
|
}
|
|
1814
|
-
remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
2122
|
+
remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
1815
2123
|
}
|
|
1816
2124
|
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
1817
2125
|
}
|
|
@@ -1837,5 +2145,6 @@ export {
|
|
|
1837
2145
|
resetWorkspaceGit,
|
|
1838
2146
|
synchronizeWorkspaceGit,
|
|
1839
2147
|
workspaceGitHydrationIsCurrent,
|
|
2148
|
+
workspaceGitMountsForRemediation,
|
|
1840
2149
|
workspaceGitSyncTestHarness
|
|
1841
2150
|
};
|