@ricsam/r5d-worker 0.0.86 → 0.0.88

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.
@@ -63,6 +63,71 @@ function revParse(workspacePath, revision) {
63
63
  const result = gitResult(workspacePath, ["rev-parse", "--verify", revision]);
64
64
  return result.exitCode === 0 ? result.stdout : null;
65
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
+ }
66
131
  function requiredWorkspaceAncestorHeads(value) {
67
132
  if (value === void 0) return [];
68
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) {
@@ -86,8 +151,8 @@ function assertWorkspaceRemediationAncestry(input) {
86
151
  );
87
152
  }
88
153
  }
89
- function updateIntegratedWorkspaceHead(workspacePath, head) {
90
- 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");
91
156
  }
92
157
  function fsyncDirectory(directoryPath) {
93
158
  const descriptor = fs.openSync(directoryPath, fs.constants.O_RDONLY);
@@ -191,16 +256,13 @@ function configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, deferMou
191
256
  return fs.existsSync(mount.sourcePath) || fs.existsSync(outerPath) || mount.sourceMode === "all";
192
257
  });
193
258
  }
194
- function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content) {
259
+ function parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content) {
195
260
  const legacyHead = content.endsWith("\n") ? content.slice(0, -1) : "";
196
261
  if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(legacyHead) && content === `${legacyHead}
197
262
  `) {
198
- if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
199
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
200
- }
201
263
  const receipt2 = { version: 3, mounts: [] };
202
264
  Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
203
- return receipt2;
265
+ return { receipt: receipt2, headsToVerify: [legacyHead] };
204
266
  }
205
267
  let parsed;
206
268
  try {
@@ -268,14 +330,16 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
268
330
  }
269
331
  const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
270
332
  if (liftedV2Head) receiptHeads.add(liftedV2Head);
271
- for (const head of receiptHeads) {
272
- if (!tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`])) {
273
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
274
- }
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}`);
275
339
  }
276
340
  return receipt;
277
341
  }
278
- function readHydratedWorkspaceReceipt(workspacePath) {
342
+ function readHydratedWorkspaceReceiptUnverified(workspacePath) {
279
343
  const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
280
344
  const status = lstatIfExists(receiptPath);
281
345
  if (!status) return null;
@@ -283,7 +347,23 @@ function readHydratedWorkspaceReceipt(workspacePath) {
283
347
  throw new Error(`Workspace hydration receipt is not a regular file: ${receiptPath}`);
284
348
  }
285
349
  const content = fs.readFileSync(receiptPath, "utf8");
286
- 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;
287
367
  }
288
368
  function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
289
369
  const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
@@ -330,49 +410,142 @@ function desiredWorkspaceBasisRefs(receipt) {
330
410
  }
331
411
  function workspaceBasisRefsMatch(workspacePath, receipt) {
332
412
  const desired = desiredWorkspaceBasisRefs(receipt);
333
- const actualRefs = listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX);
334
- if (actualRefs.length !== desired.size) return false;
335
- 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
+ });
336
424
  }
337
425
  function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
338
426
  return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
339
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
+ }
340
446
  function repairWorkspaceBasisRefs(workspacePath, receipt) {
341
447
  const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
342
448
  if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
343
449
  }
344
- function updateWorkspaceBasisRefs(workspacePath, receipt) {
345
- const desired = desiredWorkspaceBasisRefs(receipt);
346
- const transaction = ["start"];
347
- for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
348
- for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
349
- if (!desired.has(ref)) transaction.push(`delete ${ref}`);
350
- }
351
- transaction.push("prepare", "commit", "");
450
+ function runWorkspaceRefUpdateBatch(workspacePath, instructions, action) {
451
+ if (instructions.length === 0) return;
352
452
  const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
353
453
  cwd: workspacePath,
354
- stdin: Buffer.from(transaction.join("\n")),
454
+ stdin: Buffer.from([...instructions, ""].join("\n")),
355
455
  stdout: "pipe",
356
456
  stderr: "pipe",
357
457
  env: workerGitProcessEnvironment()
358
458
  });
359
459
  if (updated.exitCode !== 0) {
360
- 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}`}`);
361
461
  }
362
- for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) {
363
- 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}`}`);
364
479
  }
365
480
  }
366
- 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) {
367
522
  for (const mount of receipt.mounts) {
368
523
  if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
369
524
  throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
370
525
  }
371
- if (!tryGit(workspacePath, ["cat-file", "-e", `${mount.head}^{commit}`])) {
372
- throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${mount.head}`);
373
- }
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);
374
536
  }
375
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) {
376
549
  const gitDirectory = path.join(workspacePath, ".git");
377
550
  const gitDirectoryStatus = fs.lstatSync(gitDirectory);
378
551
  if (!gitDirectoryStatus.isDirectory() || gitDirectoryStatus.isSymbolicLink()) {
@@ -389,13 +562,16 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
389
562
  if (!receiptDirectoryStatus.isDirectory() || receiptDirectoryStatus.isSymbolicLink()) {
390
563
  throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
391
564
  }
392
- const temporaryPath = path.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
393
- const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
565
+ return { receiptPath, receiptDirectory };
566
+ }
567
+ function stagedWorkspaceBasisRefInstructions(receipt, stagingPrefix) {
394
568
  desiredWorkspaceBasisRefs(receipt);
395
- for (const mount of receipt.mounts) {
396
- const stagedRef = `${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)}`;
397
- git(workspacePath, ["update-ref", stagedRef, mount.head], "stage workspace hydration basis ref");
398
- }
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`);
399
575
  let descriptor;
400
576
  try {
401
577
  descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
@@ -404,15 +580,36 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
404
580
  fs.fsyncSync(descriptor);
405
581
  fs.closeSync(descriptor);
406
582
  descriptor = void 0;
407
- fs.renameSync(temporaryPath, receiptPath);
408
- fsyncDirectory(receiptDirectory);
409
- updateWorkspaceBasisRefs(workspacePath, receipt);
583
+ fs.renameSync(temporaryPath, paths.receiptPath);
584
+ fsyncDirectory(paths.receiptDirectory);
410
585
  } catch (error) {
411
586
  if (descriptor !== void 0) fs.closeSync(descriptor);
412
587
  fs.rmSync(temporaryPath, { force: true });
413
588
  throw error;
414
589
  }
415
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
+ }
416
613
  function hydrationTransactionPath(workspacePath) {
417
614
  return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_TRANSACTION.split("/"));
418
615
  }
@@ -464,7 +661,7 @@ function fsyncTree(rootPath) {
464
661
  };
465
662
  syncEntry(rootPath);
466
663
  }
467
- function parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, content) {
664
+ function parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, content) {
468
665
  let parsed;
469
666
  try {
470
667
  parsed = JSON.parse(content);
@@ -487,25 +684,34 @@ function parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, conte
487
684
  `) {
488
685
  throw new Error(`Workspace checkout durability record is noncanonical: ${recordPath}`);
489
686
  }
490
- if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
491
- throw new Error(`Workspace checkout durability record names a missing commit: ${recordPath}`);
492
- }
493
687
  return record;
494
688
  }
495
- function readWorkspaceCheckoutDurabilityRecord(workspacePath) {
689
+ function readWorkspaceCheckoutDurabilityRecordUnverified(workspacePath) {
496
690
  const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
497
691
  const status = lstatIfExists(recordPath);
498
692
  if (!status) return null;
499
693
  if (!status.isFile() || status.isSymbolicLink()) {
500
694
  throw new Error(`Workspace checkout durability record is not a regular file: ${recordPath}`);
501
695
  }
502
- return parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, fs.readFileSync(recordPath, "utf8"));
696
+ return { record: parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, fs.readFileSync(recordPath, "utf8")), recordPath };
503
697
  }
504
- function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
505
- if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
506
- 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}`);
507
703
  }
508
- 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) {
509
715
  const stateDirectory = workspacePrivateStateDirectory(workspacePath);
510
716
  const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
511
717
  const temporaryPath = path.join(stateDirectory, `.workspace-checkout-durability.${process.pid}.${crypto.randomUUID()}.tmp`);
@@ -519,6 +725,20 @@ function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
519
725
  throw error;
520
726
  }
521
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
+ }
522
742
  function fsyncWorkspaceCheckoutTree(workspacePath) {
523
743
  const status = fs.lstatSync(workspacePath);
524
744
  if (!status.isDirectory() || status.isSymbolicLink()) {
@@ -530,6 +750,40 @@ function fsyncWorkspaceCheckoutTree(workspacePath) {
530
750
  }
531
751
  fsyncDirectory(workspacePath);
532
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
+ }
533
787
  function workspaceRebaseInProgress(workspacePath) {
534
788
  const gitDirectory = path.join(workspacePath, ".git");
535
789
  return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
@@ -537,8 +791,8 @@ function workspaceRebaseInProgress(workspacePath) {
537
791
  function workspaceResolutionInProgress(workspacePath) {
538
792
  return workspaceRebaseInProgress(workspacePath) || fs.existsSync(path.join(workspacePath, ".git", "MERGE_HEAD"));
539
793
  }
540
- function workspaceHasUnmergedEntries(workspacePath) {
541
- const result = gitResult(workspacePath, ["ls-files", "-u", "-z"]);
794
+ async function workspaceHasUnmergedEntries(workspacePath) {
795
+ const result = await gitResultAsync(workspacePath, ["ls-files", "-u", "-z"]);
542
796
  if (result.exitCode !== 0) {
543
797
  throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
544
798
  }
@@ -594,17 +848,21 @@ function workspaceCheckoutDurabilityIsCurrent(workspacePath) {
594
848
  const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
595
849
  return Boolean(record?.state === "durable" && record.head === revParse(workspacePath, "HEAD"));
596
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
+ }
597
855
  function beginWorkspaceCheckoutTransition(workspacePath) {
598
856
  recoverWorkspaceCheckoutDurability(workspacePath);
599
857
  const head = revParse(workspacePath, "HEAD");
600
858
  updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "transition", head });
601
859
  }
602
- function completeWorkspaceCheckoutTransition(workspacePath) {
603
- fsyncWorkspaceCheckoutTree(workspacePath);
604
- updateWorkspaceCheckoutDurabilityRecord(workspacePath, {
860
+ async function completeWorkspaceCheckoutTransition(workspacePath) {
861
+ await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
862
+ await updateWorkspaceCheckoutDurabilityRecordAsync(workspacePath, {
605
863
  version: 1,
606
864
  state: "durable",
607
- head: revParse(workspacePath, "HEAD")
865
+ head: await revParseAsync(workspacePath, "HEAD")
608
866
  });
609
867
  }
610
868
  function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
@@ -618,11 +876,11 @@ function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
618
876
  }
619
877
  throw error;
620
878
  }
621
- function runWorkspaceCheckoutTransition(workspacePath, mutate) {
879
+ async function runWorkspaceCheckoutTransition(workspacePath, mutate) {
622
880
  beginWorkspaceCheckoutTransition(workspacePath);
623
881
  try {
624
- mutate();
625
- completeWorkspaceCheckoutTransition(workspacePath);
882
+ await mutate();
883
+ await completeWorkspaceCheckoutTransition(workspacePath);
626
884
  } catch (error) {
627
885
  throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
628
886
  }
@@ -920,24 +1178,24 @@ function validateMounts(workspacePath, mounts) {
920
1178
  if (overlap) throw new Error(`Workspace mounts overlap: ${current} and ${overlap}`);
921
1179
  }
922
1180
  }
923
- function configureWorkspaceRepository(input) {
924
- if (tryGit(input.workspacePath, ["remote", "get-url", "origin"])) {
925
- 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");
926
1184
  } else {
927
- git(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
1185
+ await gitAsync(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
928
1186
  }
929
- tryGit(input.workspacePath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
930
- git(input.workspacePath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset workspace credential helpers");
931
- 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");
932
1190
  if (input.credentialUsername) {
933
- git(
1191
+ await gitAsync(
934
1192
  input.workspacePath,
935
1193
  ["config", "--local", "--replace-all", gitCredentialUsernameConfigKey(input.remoteUrl), input.credentialUsername],
936
1194
  "configure workspace credential username"
937
1195
  );
938
1196
  }
939
1197
  if (input.credentialHelper) {
940
- git(
1198
+ await gitAsync(
941
1199
  input.workspacePath,
942
1200
  ["config", "--local", "--add", "credential.helper", input.credentialHelper],
943
1201
  "configure workspace credential helper"
@@ -946,26 +1204,26 @@ function configureWorkspaceRepository(input) {
946
1204
  const name = input.gitIdentity.name.trim();
947
1205
  const email = input.gitIdentity.email.trim();
948
1206
  if (!name || !email) throw new Error("Workspace Git identity must include name and email");
949
- git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
950
- git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
951
- 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(
952
1210
  input.workspacePath,
953
1211
  ["config", "--local", "core.fsync", "committed,reference"],
954
1212
  "configure durable workspace commits and references"
955
1213
  );
956
- 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");
957
1215
  }
958
- function configureExistingWorkspaceGitForRemediation(input) {
1216
+ async function configureExistingWorkspaceGitForRemediation(input) {
959
1217
  const workspacePath = path.resolve(input.workspacePath);
960
1218
  const workspaceStatus = lstatIfExists(workspacePath);
961
1219
  const gitStatus = lstatIfExists(path.join(workspacePath, ".git"));
962
1220
  if (!workspaceStatus?.isDirectory() || workspaceStatus.isSymbolicLink() || !gitStatus?.isDirectory() || gitStatus.isSymbolicLink()) {
963
1221
  throw new Error("Active workspace remediation requires a regular existing canonical synchronization checkout");
964
1222
  }
965
- configureWorkspaceRepository({ ...input, workspacePath });
1223
+ await configureWorkspaceRepository({ ...input, workspacePath });
966
1224
  }
967
- function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
968
- const result = gitResult(workspacePath, [
1225
+ async function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
1226
+ const result = await gitResultAsync(workspacePath, [
969
1227
  ...gitTransportSecurityArgs(remoteUrl, credentialHelper, credentialUsername),
970
1228
  "fetch",
971
1229
  "--no-recurse-submodules",
@@ -984,7 +1242,7 @@ ${result.stdout}`;
984
1242
  }
985
1243
  return revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
986
1244
  }
987
- function ensureWorkspaceGitClone(input) {
1245
+ async function ensureWorkspaceGitClone(input) {
988
1246
  const workspacePath = path.resolve(input.workspacePath);
989
1247
  const workspaceParent = path.dirname(workspacePath);
990
1248
  const parentStatus = fs.lstatSync(workspaceParent);
@@ -1024,18 +1282,18 @@ function ensureWorkspaceGitClone(input) {
1024
1282
  git(workspacePath, ["init", `--initial-branch=${WORKSPACE_GIT_BRANCH}`], "initialize workspace clone");
1025
1283
  }
1026
1284
  }
1027
- configureWorkspaceRepository({ ...input, workspacePath });
1285
+ await configureWorkspaceRepository({ ...input, workspacePath });
1028
1286
  recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
1029
- const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
1030
- const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
1031
- if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
1032
- 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);
1033
1291
  }
1034
- const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1035
- let localHead = revParse(workspacePath, "HEAD");
1292
+ const remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1293
+ let localHead = await revParseAsync(workspacePath, "HEAD");
1036
1294
  if (!localHead && remoteHead) {
1037
- runWorkspaceCheckoutTransition(workspacePath, () => {
1038
- git(
1295
+ await runWorkspaceCheckoutTransition(workspacePath, async () => {
1296
+ await gitAsync(
1039
1297
  workspacePath,
1040
1298
  ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
1041
1299
  "check out workspace main"
@@ -1044,12 +1302,12 @@ function ensureWorkspaceGitClone(input) {
1044
1302
  localHead = remoteHead;
1045
1303
  }
1046
1304
  if (localHead && remoteHead && localHead === remoteHead) {
1047
- updateIntegratedWorkspaceHead(workspacePath, remoteHead);
1305
+ await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
1048
1306
  } else if (localHead && remoteHead && workspaceIsShallow(workspacePath) && !tryGit(workspacePath, ["merge-base", localHead, remoteHead]) && revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) === localHead && workspaceIsClean(workspacePath)) {
1049
- runWorkspaceCheckoutTransition(workspacePath, () => {
1050
- 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");
1051
1309
  });
1052
- updateIntegratedWorkspaceHead(workspacePath, remoteHead);
1310
+ await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
1053
1311
  localHead = remoteHead;
1054
1312
  }
1055
1313
  return { localHead, remoteHead };
@@ -1173,7 +1431,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1173
1431
  }
1174
1432
  return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
1175
1433
  }
1176
- function hydrateWorkspaceGitMountsTransactionally(input) {
1434
+ async function hydrateWorkspaceGitMountsTransactionally(input) {
1177
1435
  const workspacePath = path.resolve(input.workspacePath);
1178
1436
  const durabilityMounts = input.durabilityMounts ?? input.mounts;
1179
1437
  const receiptMounts = input.receiptMounts ?? durabilityMounts;
@@ -1181,19 +1439,34 @@ function hydrateWorkspaceGitMountsTransactionally(input) {
1181
1439
  const candidateMounts = [
1182
1440
  ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
1183
1441
  ];
1184
- const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1185
- const requestedIds = new Set(input.mounts.map(({ id }) => id));
1186
- const requiredIds = new Set(requiredMounts.map(({ id }) => id));
1187
- const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1188
- const advancedDurabilityMounts = durabilityMounts.filter(
1189
- ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1190
- );
1191
- const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1192
- if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
1442
+ if (input.recordCurrentHead && !await workspaceCheckoutDurabilityIsCurrentAsync(workspacePath)) {
1193
1443
  throw new Error("Workspace checkout is not durably materialized at its current HEAD");
1194
1444
  }
1195
- const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
1196
- 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;
1197
1470
  const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1198
1471
  try {
1199
1472
  const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
@@ -1207,7 +1480,7 @@ function hydrateWorkspaceGitMountsTransactionally(input) {
1207
1480
  removeHydrationTransaction(workspacePath);
1208
1481
  return {
1209
1482
  hydratedMountIds: hydration.hydratedMountIds,
1210
- skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort()
1483
+ skippedMountIds: selection.skippedMountIds
1211
1484
  };
1212
1485
  } catch (error) {
1213
1486
  const transactionPath = hydrationTransactionPath(workspacePath);
@@ -1226,7 +1499,7 @@ function hydrateWorkspaceGitMountsTransactionally(input) {
1226
1499
  throw error;
1227
1500
  }
1228
1501
  }
1229
- function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1502
+ async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1230
1503
  const gitDirectory = path.join(path.resolve(workspacePath), ".git");
1231
1504
  if (!fs.existsSync(gitDirectory)) {
1232
1505
  return hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options);
@@ -1238,16 +1511,18 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1238
1511
  ignoreBusy: options.ignoreBusy
1239
1512
  });
1240
1513
  }
1241
- function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1514
+ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1242
1515
  workspacePath = path.resolve(workspacePath);
1243
1516
  validateMounts(workspacePath, mounts);
1244
1517
  recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
1245
1518
  recoverPendingHydrationTransaction(workspacePath);
1246
- const currentHead = revParse(workspacePath, "HEAD");
1247
- const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
1248
- 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
+ }
1249
1524
  if (!currentHead) return;
1250
- 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)}`]);
1251
1526
  const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
1252
1527
  const uncoveredBasisMounts = configuredBasisMounts.filter(
1253
1528
  (mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
@@ -1258,10 +1533,15 @@ function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1258
1533
  (mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
1259
1534
  );
1260
1535
  const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
1261
- const hydrationTargets = idleBasislessMounts.filter(
1262
- (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1263
- );
1264
- 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({
1265
1545
  workspacePath,
1266
1546
  mounts: hydrationTargets,
1267
1547
  durabilityMounts: idleBasislessMounts,
@@ -1272,27 +1552,31 @@ function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1272
1552
  });
1273
1553
  void hydration;
1274
1554
  }
1275
- function resetWorkspaceGit(input) {
1555
+ async function resetWorkspaceGit(input) {
1276
1556
  const workspacePath = path.resolve(input.workspacePath);
1277
1557
  validateMounts(workspacePath, input.mounts);
1278
1558
  const busyResetMountIds = input.mounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1279
1559
  if (busyResetMountIds.length > 0) {
1280
1560
  throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
1281
1561
  }
1282
- const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1562
+ const initial = await ensureWorkspaceGitClone({ ...input, workspacePath });
1283
1563
  recoverPendingHydrationTransaction(workspacePath);
1284
1564
  const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
1285
1565
  const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
1286
- const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1566
+ const remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1287
1567
  if (remoteHead) {
1288
- runWorkspaceCheckoutTransition(workspacePath, () => {
1289
- git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
1290
- 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");
1291
1571
  });
1292
1572
  } else if (revParse(workspacePath, "HEAD")) {
1293
- runWorkspaceCheckoutTransition(workspacePath, () => {
1294
- git(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
1295
- 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
+ );
1296
1580
  });
1297
1581
  }
1298
1582
  const selected = activeMounts(input.mounts);
@@ -1309,7 +1593,7 @@ function resetWorkspaceGit(input) {
1309
1593
  const requiredLiveMounts = [
1310
1594
  ...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
1311
1595
  ];
1312
- const hydration = hydrateWorkspaceGitMountsTransactionally({
1596
+ const hydration = await hydrateWorkspaceGitMountsTransactionally({
1313
1597
  workspacePath,
1314
1598
  mounts: hydrationMounts,
1315
1599
  durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
@@ -1331,8 +1615,8 @@ function resetWorkspaceGit(input) {
1331
1615
  skippedMountIds: [.../* @__PURE__ */ new Set([...uncoveredSkippedMounts.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1332
1616
  };
1333
1617
  }
1334
- function stagedPaths(workspacePath) {
1335
- 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();
1336
1620
  }
1337
1621
  function emptyTreeHash(workspacePath) {
1338
1622
  const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, "hash-object", "-t", "tree", "--stdin"], {
@@ -1346,9 +1630,9 @@ function emptyTreeHash(workspacePath) {
1346
1630
  throw new Error(`Create empty workspace tree: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
1347
1631
  return result.stdout.toString().trim();
1348
1632
  }
1349
- function changedPaths(workspacePath, baseRevision, headRevision) {
1633
+ async function changedPaths(workspacePath, baseRevision, headRevision) {
1350
1634
  const base = baseRevision ?? emptyTreeHash(workspacePath);
1351
- 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();
1352
1636
  }
1353
1637
  async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
1354
1638
  const base = baseRevision ?? emptyTreeHash(workspacePath);
@@ -1391,15 +1675,15 @@ function snapshotConflict(input) {
1391
1675
  git(input.workspacePath, ["update-ref", refs.remote, input.remoteHead], "snapshot remote workspace conflict head");
1392
1676
  return refs;
1393
1677
  }
1394
- function rebaseWorkspace(input) {
1678
+ async function rebaseWorkspace(input) {
1395
1679
  const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
1396
1680
  beginWorkspaceCheckoutTransition(input.workspacePath);
1397
- const rebase = gitResult(
1681
+ const rebase = await gitResultAsync(
1398
1682
  input.workspacePath,
1399
1683
  input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
1400
1684
  );
1401
1685
  if (rebase.exitCode === 0) {
1402
- completeWorkspaceCheckoutTransition(input.workspacePath);
1686
+ await completeWorkspaceCheckoutTransition(input.workspacePath);
1403
1687
  return { ok: true };
1404
1688
  }
1405
1689
  const conflicts = conflictPaths(input.workspacePath);
@@ -1408,7 +1692,7 @@ function rebaseWorkspace(input) {
1408
1692
  if (abort.exitCode !== 0) {
1409
1693
  throw new Error(`Abort workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
1410
1694
  }
1411
- completeWorkspaceCheckoutTransition(input.workspacePath);
1695
+ await completeWorkspaceCheckoutTransition(input.workspacePath);
1412
1696
  return {
1413
1697
  ok: false,
1414
1698
  conflictPaths: conflicts,
@@ -1416,13 +1700,13 @@ function rebaseWorkspace(input) {
1416
1700
  error: rebase.stderr || rebase.stdout || `Workspace rebase onto ${input.remoteHead} conflicted`
1417
1701
  };
1418
1702
  }
1419
- function synchronizeWithFetchedHead(input) {
1420
- const localHead = revParse(input.workspacePath, "HEAD");
1703
+ async function synchronizeWithFetchedHead(input) {
1704
+ const localHead = await revParseAsync(input.workspacePath, "HEAD");
1421
1705
  const remoteHead = input.remoteHead;
1422
1706
  if (!remoteHead) return { kind: "ready", updated: false, rebased: false };
1423
1707
  if (!localHead) {
1424
- runWorkspaceCheckoutTransition(input.workspacePath, () => {
1425
- git(
1708
+ await runWorkspaceCheckoutTransition(input.workspacePath, async () => {
1709
+ await gitAsync(
1426
1710
  input.workspacePath,
1427
1711
  ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
1428
1712
  "check out fetched workspace main"
@@ -1431,35 +1715,35 @@ function synchronizeWithFetchedHead(input) {
1431
1715
  return { kind: "ready", updated: true, rebased: false };
1432
1716
  }
1433
1717
  if (localHead === remoteHead) {
1434
- updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
1718
+ await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
1435
1719
  return { kind: "ready", updated: false, rebased: false };
1436
1720
  }
1437
- if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead, remoteHead])) {
1438
- runWorkspaceCheckoutTransition(input.workspacePath, () => {
1439
- 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");
1440
1724
  });
1441
- updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
1725
+ await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
1442
1726
  return { kind: "ready", updated: true, rebased: false };
1443
1727
  }
1444
- if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
1445
- updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
1728
+ if (await tryGitAsync(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
1729
+ await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
1446
1730
  return { kind: "ready", updated: false, rebased: false };
1447
1731
  }
1448
- const integratedHead = revParse(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
1732
+ const integratedHead = await revParseAsync(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
1449
1733
  const truncatedHistoryBase = workspaceIsShallow(input.workspacePath) && integratedHead && tryGit(input.workspacePath, ["merge-base", "--is-ancestor", integratedHead, localHead]) ? integratedHead : void 0;
1450
1734
  if (workspaceIsShallow(input.workspacePath) && !truncatedHistoryBase) {
1451
1735
  throw new Error(
1452
1736
  "Workspace history was truncated before this clone's integrated base could be identified; reset the workspace clone before retrying"
1453
1737
  );
1454
1738
  }
1455
- const rebased = rebaseWorkspace({
1739
+ const rebased = await rebaseWorkspace({
1456
1740
  workspacePath: input.workspacePath,
1457
1741
  localHead,
1458
1742
  remoteHead,
1459
1743
  attemptId: input.attemptId,
1460
1744
  truncatedHistoryBase
1461
1745
  });
1462
- if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
1746
+ if (rebased.ok) await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
1463
1747
  return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
1464
1748
  }
1465
1749
  async function synchronizeWorkspaceGit(input) {
@@ -1470,7 +1754,7 @@ async function synchronizeWorkspaceGit(input) {
1470
1754
  validateMounts(workspacePath, input.mounts);
1471
1755
  const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
1472
1756
  const preserveResolutionInProgress = input.skipMountMirror === true;
1473
- const initial = ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
1757
+ const initial = await ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
1474
1758
  assertWorkspaceRemediationAncestry({
1475
1759
  workspacePath,
1476
1760
  currentHead: initial.localHead,
@@ -1479,7 +1763,7 @@ async function synchronizeWorkspaceGit(input) {
1479
1763
  });
1480
1764
  const verifiedAncestorHeads = requiredAncestorHeads.length > 0 ? requiredAncestorHeads : void 0;
1481
1765
  const startingHead = initial.localHead;
1482
- const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
1766
+ const receiptBeforeInitialHydration = await readHydratedWorkspaceReceiptAsync(workspacePath);
1483
1767
  const deferredMountIds = /* @__PURE__ */ new Set();
1484
1768
  if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
1485
1769
  for (const mount of input.mounts) {
@@ -1488,7 +1772,7 @@ async function synchronizeWorkspaceGit(input) {
1488
1772
  }
1489
1773
  }
1490
1774
  }
1491
- recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1775
+ await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1492
1776
  ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
1493
1777
  preserveResolutionInProgress,
1494
1778
  preserveStaleBases: true
@@ -1499,8 +1783,8 @@ async function synchronizeWorkspaceGit(input) {
1499
1783
  );
1500
1784
  const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
1501
1785
  const receiptRequiredLiveMounts = selected.active;
1502
- const projectionBasisHead = revParse(workspacePath, "HEAD");
1503
- const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
1786
+ const projectionBasisHead = await revParseAsync(workspacePath, "HEAD");
1787
+ const projectionBasisReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
1504
1788
  const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1505
1789
  const fastProjectionMounts = [];
1506
1790
  const mergedProjectionTrees = /* @__PURE__ */ new Map();
@@ -1541,6 +1825,10 @@ async function synchronizeWorkspaceGit(input) {
1541
1825
  projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
1542
1826
  continue;
1543
1827
  }
1828
+ if (cycleSkippedMountIds.has(mount.id) || mount.busy?.()) {
1829
+ cycleSkippedMountIds.add(mount.id);
1830
+ continue;
1831
+ }
1544
1832
  const merged = mergeWorkspaceProjectionMount({
1545
1833
  workspacePath,
1546
1834
  mount: {
@@ -1575,14 +1863,15 @@ async function synchronizeWorkspaceGit(input) {
1575
1863
  }
1576
1864
  mergedProjectionTrees.set(mount.id, merged.resultTree);
1577
1865
  mergedProjectionMounts.push(mount);
1866
+ await yieldToWorkerEventLoop();
1578
1867
  }
1579
1868
  }
1580
1869
  if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
1581
1870
  `);
1582
- const completedMountSelection = (requireCurrentHeadReceipt = false) => {
1583
- const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
1871
+ const completedMountSelection = async (requireCurrentHeadReceipt = false) => {
1872
+ const currentHeadBeforeHydration = await revParseAsync(workspacePath, "HEAD");
1584
1873
  const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1585
- const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
1874
+ const currentReceiptBeforeHydration = await readHydratedWorkspaceReceiptAsync(workspacePath);
1586
1875
  refreshCycleSkippedMounts(completionReceiptMounts);
1587
1876
  const mountIsAlreadyCovered = (mount) => Boolean(
1588
1877
  currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
@@ -1599,7 +1888,7 @@ async function synchronizeWorkspaceGit(input) {
1599
1888
  const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
1600
1889
  (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1601
1890
  );
1602
- const hydration = hydrateWorkspaceGitMountsTransactionally({
1891
+ const hydration = await hydrateWorkspaceGitMountsTransactionally({
1603
1892
  workspacePath,
1604
1893
  mounts: uncoveredCompletionMounts,
1605
1894
  durabilityMounts: uncoveredCompletionBasisMounts,
@@ -1610,8 +1899,8 @@ async function synchronizeWorkspaceGit(input) {
1610
1899
  for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
1611
1900
  refreshCycleSkippedMounts(completionReceiptMounts);
1612
1901
  if (requireCurrentHeadReceipt) {
1613
- const currentHead = revParse(workspacePath, "HEAD");
1614
- const receipt = readHydratedWorkspaceReceipt(workspacePath);
1902
+ const currentHead = await revParseAsync(workspacePath, "HEAD");
1903
+ const receipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
1615
1904
  const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
1616
1905
  const idleMounts = completionReceiptMounts.filter(
1617
1906
  (mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mount.busy?.() && !projectionSkippedIds.has(mount.id)
@@ -1623,17 +1912,17 @@ async function synchronizeWorkspaceGit(input) {
1623
1912
  return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
1624
1913
  };
1625
1914
  let newlyStagedPaths = [];
1626
- const stageAndCommitWorkspace = () => {
1627
- if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
1915
+ const stageAndCommitWorkspace = async () => {
1916
+ if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
1628
1917
  throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
1629
1918
  }
1630
- git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
1631
- newlyStagedPaths = stagedPaths(workspacePath);
1919
+ await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
1920
+ newlyStagedPaths = await stagedPaths(workspacePath);
1632
1921
  if (newlyStagedPaths.length > 0) {
1633
- if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
1922
+ if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
1634
1923
  throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
1635
1924
  }
1636
- git(
1925
+ await gitAsync(
1637
1926
  workspacePath,
1638
1927
  [
1639
1928
  "commit",
@@ -1644,64 +1933,77 @@ async function synchronizeWorkspaceGit(input) {
1644
1933
  );
1645
1934
  }
1646
1935
  };
1936
+ const projectedFastMounts = [];
1647
1937
  if (!input.skipMountMirror) {
1648
1938
  beginWorkspaceCheckoutTransition(workspacePath);
1649
1939
  try {
1650
1940
  const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
1651
1941
  const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
1652
1942
  const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
1653
- 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
+ }
1654
1952
  removeWorkspaceMounts(workspacePath, fastTombstones);
1953
+ projectedFastMounts.push(...fastTombstones);
1655
1954
  for (const mount of mergedProjectionMounts) {
1656
1955
  materializeWorkspaceProjectionTree({
1657
1956
  workspacePath,
1658
1957
  workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
1659
1958
  resultTree: mergedProjectionTrees.get(mount.id)
1660
1959
  });
1960
+ await yieldToWorkerEventLoop();
1661
1961
  }
1662
- const restoreFromHeadIds = /* @__PURE__ */ new Set([...deferredMountIds, ...selected.skipped.map(({ id }) => id)]);
1663
1962
  restoreDeferredWorkspaceMountsFromHead(
1664
1963
  workspacePath,
1665
- input.mounts.filter(({ id }) => restoreFromHeadIds.has(id))
1964
+ input.mounts.filter(({ id }) => deferredMountIds.has(id))
1666
1965
  );
1667
- stageAndCommitWorkspace();
1668
- fsyncWorkspaceCheckoutTree(workspacePath);
1966
+ await stageAndCommitWorkspace();
1967
+ await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
1669
1968
  } catch (error) {
1670
1969
  throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
1671
1970
  }
1672
1971
  } else {
1673
- stageAndCommitWorkspace();
1674
- if (newlyStagedPaths.length > 0) completeWorkspaceCheckoutTransition(workspacePath);
1972
+ await stageAndCommitWorkspace();
1973
+ if (newlyStagedPaths.length > 0) await completeWorkspaceCheckoutTransition(workspacePath);
1675
1974
  }
1676
1975
  if (!input.skipMountMirror) {
1677
- const projectedHead = revParse(workspacePath, "HEAD");
1976
+ const projectedHead = await revParseAsync(workspacePath, "HEAD");
1678
1977
  if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
1679
1978
  const targetProjectionReceipt = receiptWithMountsAtHead(
1680
1979
  projectionBasisReceipt,
1681
1980
  projectedHead,
1682
- fastProjectionMounts,
1981
+ projectedFastMounts,
1683
1982
  allProjectionReceiptMounts
1684
1983
  );
1685
- const currentProjectionReceipt = readHydratedWorkspaceReceipt(workspacePath);
1686
- if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !workspaceBasisRefsAreCurrent(workspacePath, targetProjectionReceipt)) {
1687
- fsyncHydratedWorkspaceMounts(fastProjectionMounts);
1688
- 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);
1689
1991
  }
1690
- completeWorkspaceCheckoutTransition(workspacePath);
1992
+ await completeWorkspaceCheckoutTransition(workspacePath);
1691
1993
  }
1692
- let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1994
+ let remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1693
1995
  let rebaseCount = 0;
1694
1996
  let updated = false;
1695
1997
  for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
1696
1998
  assertWorkspaceRemediationAncestry({
1697
1999
  workspacePath,
1698
- currentHead: revParse(workspacePath, "HEAD"),
2000
+ currentHead: await revParseAsync(workspacePath, "HEAD"),
1699
2001
  remoteHead,
1700
2002
  requiredAncestorHeads
1701
2003
  });
1702
- const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
2004
+ const reconciled = await synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
1703
2005
  if (reconciled.kind === "conflict") {
1704
- const localHead2 = revParse(workspacePath, "HEAD");
2006
+ const localHead2 = await revParseAsync(workspacePath, "HEAD");
1705
2007
  const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
1706
2008
  return {
1707
2009
  outcome: "conflict_blocked",
@@ -1722,8 +2024,8 @@ async function synchronizeWorkspaceGit(input) {
1722
2024
  }
1723
2025
  updated ||= reconciled.updated;
1724
2026
  if (reconciled.rebased) rebaseCount += 1;
1725
- if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
1726
- const localHead = revParse(workspacePath, "HEAD");
2027
+ if (reconciled.updated || reconciled.rebased) await completedMountSelection(true);
2028
+ const localHead = await revParseAsync(workspacePath, "HEAD");
1727
2029
  if (!localHead) {
1728
2030
  const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
1729
2031
  return {
@@ -1740,7 +2042,7 @@ async function synchronizeWorkspaceGit(input) {
1740
2042
  ...projectionWarning ? { error: projectionWarning } : {}
1741
2043
  };
1742
2044
  }
1743
- const paths = changedPaths(workspacePath, remoteHead, localHead);
2045
+ const paths = await changedPaths(workspacePath, remoteHead, localHead);
1744
2046
  const size = paths.length > 0 ? await (input.measureDiffSize ?? diffSizeBytes)(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
1745
2047
  input.assertStillAdmitted?.();
1746
2048
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
@@ -1761,7 +2063,7 @@ async function synchronizeWorkspaceGit(input) {
1761
2063
  }
1762
2064
  if (localHead === remoteHead) {
1763
2065
  assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
1764
- const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
2066
+ const completedMounts = await completedMountSelection(Boolean(input.skipMountMirror));
1765
2067
  await input.afterWorkspacePublished?.({
1766
2068
  publishedHead: localHead,
1767
2069
  activeMountIds: completedMounts.activeMountIds
@@ -1789,11 +2091,11 @@ async function synchronizeWorkspaceGit(input) {
1789
2091
  ];
1790
2092
  if (input.allowLargeDiff) pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
1791
2093
  pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
1792
- const push = gitResult(workspacePath, pushArgs);
2094
+ const push = await gitResultAsync(workspacePath, pushArgs);
1793
2095
  if (push.exitCode === 0) {
1794
2096
  assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
1795
- updateIntegratedWorkspaceHead(workspacePath, localHead);
1796
- const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
2097
+ await updateIntegratedWorkspaceHead(workspacePath, localHead);
2098
+ const completedMounts = await completedMountSelection(Boolean(input.skipMountMirror));
1797
2099
  await input.afterWorkspacePublished?.({
1798
2100
  publishedHead: localHead,
1799
2101
  activeMountIds: completedMounts.activeMountIds
@@ -1817,7 +2119,7 @@ async function synchronizeWorkspaceGit(input) {
1817
2119
  ${push.stdout}`)) {
1818
2120
  throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
1819
2121
  }
1820
- remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
2122
+ remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1821
2123
  }
1822
2124
  throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
1823
2125
  }