@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.
@@ -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
- git(workspacePath, ["update-ref", WORKSPACE_GIT_INTEGRATED_REF, head], "record integrated workspace head");
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 parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content) {
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
- for (const head of receiptHeads) {
269
- if (!tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`])) {
270
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
271
- }
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 readHydratedWorkspaceReceipt(workspacePath) {
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 parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
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 actualRefs = listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX);
331
- if (actualRefs.length !== desired.size) return false;
332
- return actualRefs.every((ref) => desired.get(ref) === revParse(workspacePath, ref));
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 updateWorkspaceBasisRefs(workspacePath, receipt) {
342
- const desired = desiredWorkspaceBasisRefs(receipt);
343
- const transaction = ["start"];
344
- for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
345
- for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
346
- if (!desired.has(ref)) transaction.push(`delete ${ref}`);
347
- }
348
- transaction.push("prepare", "commit", "");
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(transaction.join("\n")),
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(`Synchronize workspace hydration basis refs: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
460
+ throw new Error(`${action}: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
358
461
  }
359
- for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) {
360
- git(workspacePath, ["update-ref", "-d", ref], "remove staged workspace hydration basis ref");
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 updateHydratedWorkspaceReceipt(workspacePath, receipt) {
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
- if (!tryGit(workspacePath, ["cat-file", "-e", `${mount.head}^{commit}`])) {
369
- throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${mount.head}`);
370
- }
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
- const temporaryPath = path.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
390
- const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
565
+ return { receiptPath, receiptDirectory };
566
+ }
567
+ function stagedWorkspaceBasisRefInstructions(receipt, stagingPrefix) {
391
568
  desiredWorkspaceBasisRefs(receipt);
392
- for (const mount of receipt.mounts) {
393
- const stagedRef = `${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)}`;
394
- git(workspacePath, ["update-ref", stagedRef, mount.head], "stage workspace hydration basis ref");
395
- }
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 parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, content) {
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 readWorkspaceCheckoutDurabilityRecord(workspacePath) {
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 parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, fs.readFileSync(recordPath, "utf8"));
696
+ return { record: parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, fs.readFileSync(recordPath, "utf8")), recordPath };
500
697
  }
501
- function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
502
- if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
503
- throw new Error(`Cannot record workspace checkout durability for a missing commit: ${record.head}`);
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
- readWorkspaceCheckoutDurabilityRecord(workspacePath);
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 = gitResult(workspacePath, ["ls-files", "-u", "-z"]);
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
- fsyncWorkspaceCheckoutTree(workspacePath);
601
- updateWorkspaceCheckoutDurabilityRecord(workspacePath, {
860
+ async function completeWorkspaceCheckoutTransition(workspacePath) {
861
+ await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
862
+ await updateWorkspaceCheckoutDurabilityRecordAsync(workspacePath, {
602
863
  version: 1,
603
864
  state: "durable",
604
- head: revParse(workspacePath, "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 (tryGit(input.workspacePath, ["remote", "get-url", "origin"])) {
922
- git(input.workspacePath, ["remote", "set-url", "origin", input.remoteUrl], "configure workspace origin");
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
- git(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
1185
+ await gitAsync(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
925
1186
  }
926
- tryGit(input.workspacePath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
927
- git(input.workspacePath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset workspace credential helpers");
928
- git(input.workspacePath, ["config", "--local", "credential.useHttpPath", "false"], "configure workspace credential path matching");
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
- git(
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
- git(
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
- git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
947
- git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
948
- git(
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
- git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
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 = gitResult(workspacePath, [
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 = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
1027
- const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
1028
- if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
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 = revParse(workspacePath, "HEAD");
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
- git(
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
- git(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
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
- const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1182
- const requestedIds = new Set(input.mounts.map(({ id }) => id));
1183
- const requiredIds = new Set(requiredMounts.map(({ id }) => id));
1184
- const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1185
- const advancedDurabilityMounts = durabilityMounts.filter(
1186
- ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1187
- );
1188
- const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
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 ? revParse(workspacePath, "HEAD") : null;
1193
- const targetReceipt = targetHead ? receiptWithMountsAtHead(readHydratedWorkspaceReceipt(workspacePath), targetHead, advancedDurabilityMounts, receiptMounts) : null;
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: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort()
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 = revParse(workspacePath, "HEAD");
1244
- const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
1245
- repairWorkspaceBasisRefs(workspacePath, currentReceipt);
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) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
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 = idleBasislessMounts.filter(
1259
- (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1260
- );
1261
- const hydration = hydrateWorkspaceGitMountsTransactionally({
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
- git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
1287
- git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
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
- git(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
1292
- git(workspacePath, ["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })], "record empty workspace reset");
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([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1615
+ skippedMountIds: [.../* @__PURE__ */ new Set([...uncoveredSkippedMounts.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1326
1616
  };
1327
1617
  }
1328
- function stagedPaths(workspacePath) {
1329
- return git(workspacePath, ["diff", "--cached", "--name-only", "--no-renames", "-z"], "inspect staged workspace paths").split("\0").filter(Boolean).sort();
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 git(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths").split("\0").filter(Boolean).sort();
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 = gitResult(
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 = revParse(input.workspacePath, "HEAD");
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
- git(
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 (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead, remoteHead])) {
1432
- runWorkspaceCheckoutTransition(input.workspacePath, () => {
1433
- git(input.workspacePath, ["reset", "--hard", remoteHead], "fast-forward workspace main");
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 (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
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 = revParse(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
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 = readHydratedWorkspaceReceipt(workspacePath);
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 = revParse(workspacePath, "HEAD");
1497
- const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
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 = revParse(workspacePath, "HEAD");
1871
+ const completedMountSelection = async (requireCurrentHeadReceipt = false) => {
1872
+ const currentHeadBeforeHydration = await revParseAsync(workspacePath, "HEAD");
1578
1873
  const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1579
- const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
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 = revParse(workspacePath, "HEAD");
1608
- const receipt = readHydratedWorkspaceReceipt(workspacePath);
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
- git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
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
- git(
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
- mirrorMountsToWorkspace(workspacePath, fastActiveMounts);
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 }) => restoreFromHeadIds.has(id))
1964
+ input.mounts.filter(({ id }) => deferredMountIds.has(id))
1660
1965
  );
1661
- stageAndCommitWorkspace();
1662
- fsyncWorkspaceCheckoutTree(workspacePath);
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 = revParse(workspacePath, "HEAD");
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
- fastProjectionMounts,
1981
+ projectedFastMounts,
1677
1982
  allProjectionReceiptMounts
1678
1983
  );
1679
- const currentProjectionReceipt = readHydratedWorkspaceReceipt(workspacePath);
1680
- if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !workspaceBasisRefsAreCurrent(workspacePath, targetProjectionReceipt)) {
1681
- fsyncHydratedWorkspaceMounts(fastProjectionMounts);
1682
- updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
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: revParse(workspacePath, "HEAD"),
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 = revParse(workspacePath, "HEAD");
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 = revParse(workspacePath, "HEAD");
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 = gitResult(workspacePath, pushArgs);
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
  };