@ricsam/r5d-worker 0.0.99 → 0.0.101

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.
@@ -91,6 +91,22 @@ async function gitAsync(cwd, args, action) {
91
91
  if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
92
92
  return result.stdout;
93
93
  }
94
+ async function gitOutputAsync(cwd, args, action, input) {
95
+ const subprocess = Bun.spawn(gitCommandArgs(args), {
96
+ cwd,
97
+ stdin: input ?? "ignore",
98
+ stdout: "pipe",
99
+ stderr: "pipe",
100
+ env: workerGitProcessEnvironment()
101
+ });
102
+ const [stdout, stderr, exitCode] = await Promise.all([
103
+ new Response(subprocess.stdout).arrayBuffer(),
104
+ new Response(subprocess.stderr).text(),
105
+ subprocess.exited
106
+ ]);
107
+ if (exitCode !== 0) throw new Error(`${action}: ${stderr.trim() || `git exited ${exitCode}`}`);
108
+ return Buffer.from(stdout);
109
+ }
94
110
  async function tryGitAsync(cwd, args) {
95
111
  return (await gitResultAsync(cwd, args)).exitCode === 0;
96
112
  }
@@ -1102,7 +1118,13 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1102
1118
  candidate: outerPath,
1103
1119
  label: `Workspace mount ${mount.id} outer source`
1104
1120
  });
1105
- const plan = planWorkingTreeMirror({ sourceRoot: outerPath, targetRoot: sourcePath, sourceMode: "all", deletionMode: "git" });
1121
+ const plan = planWorkingTreeMirror({
1122
+ sourceRoot: outerPath,
1123
+ targetRoot: sourcePath,
1124
+ sourceMode: "all",
1125
+ deletionMode: "git",
1126
+ sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
1127
+ });
1106
1128
  scope = captureWorkingTreeMirrorPlan({
1107
1129
  plan,
1108
1130
  targetRoot: sourcePath,
@@ -1413,6 +1435,7 @@ function activeMounts(mounts) {
1413
1435
  return { active, tombstones, skipped };
1414
1436
  }
1415
1437
  function mirrorMountsToWorkspace(workspacePath, mounts) {
1438
+ const projected = [];
1416
1439
  for (const mount of mounts) {
1417
1440
  assertManagedDirectoryPath({
1418
1441
  trustedRoot: mount.durabilityRootPath,
@@ -1424,13 +1447,61 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
1424
1447
  candidate: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1425
1448
  label: `Workspace mount ${mount.id} outer target`
1426
1449
  });
1427
- mirrorWorkingTree({
1450
+ const mirrored = mirrorWorkingTree({
1428
1451
  sourceRoot: mount.sourcePath,
1429
1452
  targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1430
1453
  sourceMode: mount.sourceMode,
1431
1454
  deletionMode: "all"
1432
1455
  });
1456
+ projected.push({ mount, gitlinks: mirrored.gitlinks });
1457
+ }
1458
+ return projected;
1459
+ }
1460
+ async function stageWorkspaceGitlinks(workspacePath, projected) {
1461
+ if (projected.length === 0) return;
1462
+ const mountPaths = projected.map(({ mount }) => normalizedWorkspaceMountPath(mount.workspaceRelativePath));
1463
+ const desired = /* @__PURE__ */ new Map();
1464
+ projected.forEach(({ gitlinks }, index) => {
1465
+ for (const gitlink of gitlinks) desired.set(`${mountPaths[index]}/${gitlink.relativePath}`, gitlink.objectId);
1466
+ });
1467
+ const desiredRoots = new Set(desired.keys());
1468
+ const listed = await gitOutputAsync(
1469
+ workspacePath,
1470
+ ["ls-files", "-z", "--stage", "--", ...mountPaths.map((mountPath) => `:(literal)${mountPath}`)],
1471
+ "inspect staged workspace mount entries"
1472
+ );
1473
+ const feed = [];
1474
+ const current = /* @__PURE__ */ new Map();
1475
+ const drop = (objectId, relativePath) => {
1476
+ feed.push(Buffer.concat([Buffer.from(`0 ${"0".repeat(objectId.length)} `), Buffer.from(relativePath), Buffer.from([0])]));
1477
+ };
1478
+ for (const record of listed.toString().split("\0")) {
1479
+ const match = /^([0-7]{6}) ([0-9a-f]{40,64}) [0-3]\t(.+)$/su.exec(record);
1480
+ if (!match) continue;
1481
+ const mode = match[1];
1482
+ const objectId = match[2];
1483
+ const relativePath = match[3];
1484
+ if (mode === "160000") {
1485
+ current.set(relativePath, objectId);
1486
+ if (!desired.has(relativePath)) drop(objectId, relativePath);
1487
+ continue;
1488
+ }
1489
+ if (desired.has(relativePath) || hasAncestorIn(relativePath, desiredRoots)) drop(objectId, relativePath);
1433
1490
  }
1491
+ for (const [relativePath, objectId] of desired) {
1492
+ if (current.get(relativePath) === objectId) continue;
1493
+ feed.push(Buffer.concat([Buffer.from(`160000 ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]));
1494
+ }
1495
+ if (feed.length === 0) return;
1496
+ await gitOutputAsync(workspacePath, ["update-index", "-z", "--index-info"], "stage workspace gitlinks", Buffer.concat(feed));
1497
+ }
1498
+ function hasAncestorIn(relativePath, ancestors) {
1499
+ let parent = path.posix.dirname(relativePath);
1500
+ while (parent !== ".") {
1501
+ if (ancestors.has(parent)) return true;
1502
+ parent = path.posix.dirname(parent);
1503
+ }
1504
+ return false;
1434
1505
  }
1435
1506
  function removeWorkspaceMounts(workspacePath, mounts) {
1436
1507
  for (const mount of mounts) {
@@ -1470,6 +1541,24 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
1470
1541
  function workspaceMountOuterPath(workspacePath, mount) {
1471
1542
  return path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1472
1543
  }
1544
+ function outerSubtreeGitlinks(workspacePath, mount) {
1545
+ const gitlinks = /* @__PURE__ */ new Map();
1546
+ const head = revParse(workspacePath, "HEAD");
1547
+ if (!head) return gitlinks;
1548
+ const subtree = `${head}:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`;
1549
+ const listed = Bun.spawnSync(gitCommandArgs(["ls-tree", "-r", "-z", subtree]), {
1550
+ cwd: workspacePath,
1551
+ stdout: "pipe",
1552
+ stderr: "pipe",
1553
+ env: workerGitProcessEnvironment()
1554
+ });
1555
+ if (listed.exitCode !== 0) return gitlinks;
1556
+ for (const record of Buffer.from(listed.stdout).toString().split("\0")) {
1557
+ const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record);
1558
+ if (match) gitlinks.set(match[2], match[1]);
1559
+ }
1560
+ return gitlinks;
1561
+ }
1473
1562
  function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1474
1563
  const hydratedMountIds = [];
1475
1564
  const skippedMountIds = [];
@@ -1513,6 +1602,9 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1513
1602
  sourceRoot,
1514
1603
  targetRoot: mount.sourcePath,
1515
1604
  sourceMode: "all",
1605
+ // The outer worktree shows a submodule as an empty directory; the tree
1606
+ // carries its pointer, which a Git checkout applies to its index (D4).
1607
+ sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount),
1516
1608
  // A missing checkout has no Git index to define git-only deletions. Its
1517
1609
  // newly created directory is empty, so all-mode is equivalent and lets
1518
1610
  // bootstrap hydration preserve the fetched tree before Git initializes.
@@ -1528,7 +1620,7 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
1528
1620
  return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
1529
1621
  });
1530
1622
  if (candidates.length === 0) return /* @__PURE__ */ new Set();
1531
- const objects = await resolveGitObjectIdsAsync(
1623
+ const objects = await resolveGitObjectsAsync(
1532
1624
  workspacePath,
1533
1625
  candidates.flatMap(({ mount, basisHead }) => [
1534
1626
  workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
@@ -1537,8 +1629,8 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
1537
1629
  );
1538
1630
  const unchanged = /* @__PURE__ */ new Set();
1539
1631
  for (const { mount, basisHead } of candidates) {
1540
- const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
1541
- const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
1632
+ const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath))?.objectId ?? null;
1633
+ const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath))?.objectId ?? null;
1542
1634
  if (before === after) unchanged.add(mount.id);
1543
1635
  }
1544
1636
  return unchanged;
@@ -1564,56 +1656,67 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1564
1656
  const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1565
1657
  const requestedIds = new Set(input.mounts.map(({ id }) => id));
1566
1658
  const requiredIds = new Set(requiredMounts.map(({ id }) => id));
1567
- const hydrationMounts2 = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1568
- const advancedDurabilityMounts2 = durabilityMounts.filter(
1659
+ const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1660
+ const advancedDurabilityMounts = durabilityMounts.filter(
1569
1661
  ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1570
1662
  );
1571
1663
  const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1572
1664
  const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
1573
1665
  return {
1574
1666
  busyMountIds,
1575
- hydrationMounts: hydrationMounts2,
1576
- advancedDurabilityMounts: advancedDurabilityMounts2,
1667
+ hydrationMounts,
1668
+ advancedDurabilityMounts,
1577
1669
  skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
1578
- targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts2, ...carriedMounts], receiptMounts) : null
1670
+ targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts, ...carriedMounts], receiptMounts) : null
1579
1671
  };
1580
1672
  };
1581
1673
  const noopCandidate = computeSelection();
1582
1674
  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)) {
1583
1675
  return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
1584
1676
  }
1585
- const selection = computeSelection();
1586
- const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
1587
- const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1677
+ const hooks = input.hydrationHooks;
1678
+ const context = {
1679
+ transactionId: crypto.randomUUID(),
1680
+ workspacePath,
1681
+ mountIds: [...new Set([...noopCandidate.hydrationMounts, ...noopCandidate.advancedDurabilityMounts].map(({ id }) => id))].sort()
1682
+ };
1683
+ if (hooks) await hooks.beforeTransaction(context);
1588
1684
  try {
1589
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1590
- const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1591
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1592
- throw new Error("Workspace hydration did not include every required mount");
1593
- }
1594
- fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
1595
- markHydrationTransactionDurable(workspacePath);
1596
- if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1597
- removeHydrationTransaction(workspacePath);
1598
- return {
1599
- hydratedMountIds: hydration.hydratedMountIds,
1600
- skippedMountIds: selection.skippedMountIds
1601
- };
1602
- } catch (error) {
1603
- const transactionPath = hydrationTransactionPath(workspacePath);
1604
- if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1605
- throw error;
1606
- }
1685
+ const selection = computeSelection();
1686
+ const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
1687
+ const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1607
1688
  try {
1608
- restoreHydrationTransaction(workspacePath, manifest);
1689
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1690
+ const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1691
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1692
+ throw new Error("Workspace hydration did not include every required mount");
1693
+ }
1694
+ fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
1695
+ markHydrationTransactionDurable(workspacePath);
1696
+ if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1609
1697
  removeHydrationTransaction(workspacePath);
1610
- } catch (restoreError) {
1611
- throw new AggregateError(
1612
- [error, restoreError],
1613
- `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1614
- );
1698
+ return {
1699
+ hydratedMountIds: hydration.hydratedMountIds,
1700
+ skippedMountIds: selection.skippedMountIds
1701
+ };
1702
+ } catch (error) {
1703
+ const transactionPath = hydrationTransactionPath(workspacePath);
1704
+ if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1705
+ throw error;
1706
+ }
1707
+ try {
1708
+ restoreHydrationTransaction(workspacePath, manifest);
1709
+ removeHydrationTransaction(workspacePath);
1710
+ } catch (restoreError) {
1711
+ throw new AggregateError(
1712
+ [error, restoreError],
1713
+ `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1714
+ );
1715
+ }
1716
+ throw error;
1615
1717
  }
1616
- throw error;
1718
+ } finally {
1719
+ hooks?.afterTransaction(context);
1617
1720
  }
1618
1721
  }
1619
1722
  async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
@@ -1625,7 +1728,8 @@ async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1625
1728
  return hydrateWorkspaceGitMountsTransactionally({
1626
1729
  workspacePath,
1627
1730
  mounts,
1628
- ignoreBusy: options.ignoreBusy
1731
+ ignoreBusy: options.ignoreBusy,
1732
+ hydrationHooks: options.hydrationHooks
1629
1733
  });
1630
1734
  }
1631
1735
  async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
@@ -1665,7 +1769,8 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
1665
1769
  receiptMounts: configuredBasisMounts,
1666
1770
  requiredMounts: hydrationTargets,
1667
1771
  ignoreBusy: options.ignoreBusy,
1668
- recordCurrentHead: true
1772
+ recordCurrentHead: true,
1773
+ hydrationHooks: options.hydrationHooks
1669
1774
  });
1670
1775
  void hydration;
1671
1776
  }
@@ -1716,7 +1821,8 @@ async function resetWorkspaceGit(input) {
1716
1821
  durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
1717
1822
  receiptMounts: input.mounts,
1718
1823
  requiredMounts: requiredLiveMounts,
1719
- recordCurrentHead: true
1824
+ recordCurrentHead: true,
1825
+ hydrationHooks: input.hydrationHooks
1720
1826
  });
1721
1827
  const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
1722
1828
  const resetHead = revParse(workspacePath, "HEAD");
@@ -1790,7 +1896,7 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, p
1790
1896
  if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
1791
1897
  return total;
1792
1898
  }
1793
- async function resolveGitObjectIdsAsync(workspacePath, names) {
1899
+ async function resolveGitObjectsAsync(workspacePath, names) {
1794
1900
  const resolved = /* @__PURE__ */ new Map();
1795
1901
  const unique = [...new Set(names)];
1796
1902
  if (unique.length === 0) return resolved;
@@ -1813,21 +1919,43 @@ async function resolveGitObjectIdsAsync(workspacePath, names) {
1813
1919
  throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1814
1920
  }
1815
1921
  unique.forEach((name, index) => {
1816
- const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
1817
- resolved.set(name, match ? match[1] : null);
1922
+ const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
1923
+ resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
1818
1924
  });
1819
1925
  return resolved;
1820
1926
  }
1821
1927
  function workspaceTreeObjectName(revision, workspaceRelativePath) {
1822
1928
  return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
1823
1929
  }
1930
+ async function pathsReplacedByGitlinks(workspacePath, baseRevision, headRevision) {
1931
+ const base = baseRevision ?? emptyTreeHash(workspacePath);
1932
+ const raw = await gitOutputAsync(
1933
+ workspacePath,
1934
+ ["diff", "--raw", "--no-renames", "-z", base, headRevision],
1935
+ "inspect workspace gitlink changes"
1936
+ );
1937
+ const records = raw.toString().split("\0");
1938
+ const addedGitlinks = [];
1939
+ for (let index = 0; index + 1 < records.length; index += 2) {
1940
+ if (/^:[0-7]{6} 160000 \S+ \S+ A$/u.test(records[index])) addedGitlinks.push(records[index + 1]);
1941
+ }
1942
+ if (addedGitlinks.length === 0) return [];
1943
+ const objects = await resolveGitObjectsAsync(
1944
+ workspacePath,
1945
+ addedGitlinks.map((relativePath) => `${base}:${relativePath}`)
1946
+ );
1947
+ return addedGitlinks.filter((relativePath) => objects.get(`${base}:${relativePath}`)?.type === "tree").sort();
1948
+ }
1949
+ function gitlinkMigrationPathspecs(paths) {
1950
+ return paths.map((relativePath) => `:(exclude,literal)${relativePath}`);
1951
+ }
1824
1952
  async function measureInitialPublicationDiffSize(input) {
1825
1953
  const candidates = input.mounts.filter(
1826
1954
  (mount) => typeof mount.initialPublicationSourceRelativePath === "string"
1827
1955
  );
1828
1956
  if (candidates.length === 0) return null;
1829
1957
  const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
1830
- const objects = await resolveGitObjectIdsAsync(
1958
+ const objects = await resolveGitObjectsAsync(
1831
1959
  input.workspacePath,
1832
1960
  candidates.flatMap(
1833
1961
  (mount) => revisions.flatMap((revision) => [
@@ -1836,7 +1964,7 @@ async function measureInitialPublicationDiffSize(input) {
1836
1964
  ])
1837
1965
  )
1838
1966
  );
1839
- const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
1967
+ const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath))?.objectId ?? null : null;
1840
1968
  const initialPublications = candidates.flatMap((mount) => {
1841
1969
  const published = lookup(input.localHead, mount.workspaceRelativePath);
1842
1970
  if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
@@ -1844,16 +1972,14 @@ async function measureInitialPublicationDiffSize(input) {
1844
1972
  return [{ mount, published, source }];
1845
1973
  });
1846
1974
  if (initialPublications.length === 0) return null;
1847
- let size = await input.measure(
1848
- input.workspacePath,
1849
- input.remoteHead,
1850
- input.localHead,
1851
- input.limit,
1852
- initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
1853
- );
1975
+ let size = await input.measure(input.workspacePath, input.remoteHead, input.localHead, input.limit, [
1976
+ ...initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`),
1977
+ ...input.excludePathspecs ?? []
1978
+ ]);
1854
1979
  for (const { published, source } of initialPublications) {
1855
1980
  if (size > input.limit) break;
1856
- size += await input.measure(input.workspacePath, source, published, input.limit - size);
1981
+ const migrations = source ? await pathsReplacedByGitlinks(input.workspacePath, source, published) : [];
1982
+ size += await input.measure(input.workspacePath, source, published, input.limit - size, gitlinkMigrationPathspecs(migrations));
1857
1983
  }
1858
1984
  return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
1859
1985
  }
@@ -1971,7 +2097,8 @@ async function synchronizeWorkspaceGit(input) {
1971
2097
  await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1972
2098
  ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
1973
2099
  preserveResolutionInProgress,
1974
- preserveStaleBases: true
2100
+ preserveStaleBases: true,
2101
+ hydrationHooks: input.hydrationHooks
1975
2102
  });
1976
2103
  const selected = activeMounts(input.mounts);
1977
2104
  const projectionMutationTokens = new Map(
@@ -2097,7 +2224,8 @@ async function synchronizeWorkspaceGit(input) {
2097
2224
  carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
2098
2225
  receiptMounts: completionReceiptMounts,
2099
2226
  requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2100
- recordCurrentHead: true
2227
+ recordCurrentHead: true,
2228
+ hydrationHooks: input.hydrationHooks
2101
2229
  });
2102
2230
  for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
2103
2231
  refreshCycleSkippedMounts(completionReceiptMounts);
@@ -2115,10 +2243,12 @@ async function synchronizeWorkspaceGit(input) {
2115
2243
  return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
2116
2244
  };
2117
2245
  let newlyStagedPaths = [];
2246
+ const projectedGitlinks = [];
2118
2247
  const stageAndCommitWorkspace = async () => {
2119
2248
  if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
2120
2249
  throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
2121
2250
  }
2251
+ await stageWorkspaceGitlinks(workspacePath, projectedGitlinks);
2122
2252
  await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
2123
2253
  newlyStagedPaths = await stagedPaths(workspacePath);
2124
2254
  if (newlyStagedPaths.length > 0) {
@@ -2148,7 +2278,7 @@ async function synchronizeWorkspaceGit(input) {
2148
2278
  cycleSkippedMountIds.add(mount.id);
2149
2279
  continue;
2150
2280
  }
2151
- mirrorMountsToWorkspace(workspacePath, [mount]);
2281
+ projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
2152
2282
  projectedFastMounts.push(mount);
2153
2283
  await yieldToWorkerEventLoop();
2154
2284
  }
@@ -2247,7 +2377,9 @@ async function synchronizeWorkspaceGit(input) {
2247
2377
  }
2248
2378
  const paths = await changedPaths(workspacePath, remoteHead, localHead);
2249
2379
  const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
2250
- let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2380
+ const gitlinkMigrations = paths.length > 0 ? await pathsReplacedByGitlinks(workspacePath, remoteHead, localHead) : [];
2381
+ const gitlinkMigrationExcludes = gitlinkMigrationPathspecs(gitlinkMigrations);
2382
+ let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes, gitlinkMigrationExcludes) : 0;
2251
2383
  input.assertStillAdmitted?.();
2252
2384
  let initialPublicationMountIds;
2253
2385
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
@@ -2257,7 +2389,8 @@ async function synchronizeWorkspaceGit(input) {
2257
2389
  localHead,
2258
2390
  mounts: input.mounts,
2259
2391
  limit: maxDiffBytes,
2260
- measure: measureDiffSize
2392
+ measure: measureDiffSize,
2393
+ excludePathspecs: gitlinkMigrationExcludes
2261
2394
  });
2262
2395
  input.assertStillAdmitted?.();
2263
2396
  if (initialPublication && initialPublication.size <= maxDiffBytes) {
@@ -2265,6 +2398,11 @@ async function synchronizeWorkspaceGit(input) {
2265
2398
  initialPublicationMountIds = initialPublication.mountIds;
2266
2399
  }
2267
2400
  }
2401
+ let gitlinkMigrationConfirmed = false;
2402
+ if (gitlinkMigrations.length > 0 && size <= maxDiffBytes && !input.allowLargeDiff) {
2403
+ gitlinkMigrationConfirmed = await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) > maxDiffBytes;
2404
+ input.assertStillAdmitted?.();
2405
+ }
2268
2406
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2269
2407
  const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
2270
2408
  return {
@@ -2309,7 +2447,7 @@ async function synchronizeWorkspaceGit(input) {
2309
2447
  "push",
2310
2448
  "--no-recurse-submodules"
2311
2449
  ];
2312
- if (input.allowLargeDiff || initialPublicationMountIds) {
2450
+ if (input.allowLargeDiff || initialPublicationMountIds || gitlinkMigrationConfirmed) {
2313
2451
  pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2314
2452
  }
2315
2453
  pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
@@ -170,14 +170,6 @@ function indexMode(entry) {
170
170
  function indexInfoRecord(mode, objectId, relativePath) {
171
171
  return Buffer.concat([Buffer.from(`${mode} ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]);
172
172
  }
173
- function basisGitlinks(workspacePath, basis) {
174
- const listed = gitResult(workspacePath, ["ls-tree", "-r", "-z", `${basis.basisHead}:${basis.workspaceRelativePath}`]);
175
- if (listed.exitCode !== 0) return [];
176
- return nulRecords(listed.stdout).flatMap((record) => {
177
- const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record.toString());
178
- return match ? [{ objectId: match[1], relativePath: match[2] }] : [];
179
- });
180
- }
181
173
  function synthesizeMountTree(input) {
182
174
  const sourceRoot = path.resolve(input.sourcePath);
183
175
  const entries = inspectWorkingTree(sourceRoot, input.sourceMode);
@@ -188,19 +180,14 @@ function synthesizeMountTree(input) {
188
180
  const records = [];
189
181
  for (const [relativePath, entry] of [...entries.entries()].sort(([left], [right]) => left.localeCompare(right))) {
190
182
  if (entry.kind === "directory") continue;
183
+ if (entry.kind === "gitlink") {
184
+ records.push(indexInfoRecord("160000", entry.objectId, relativePath));
185
+ continue;
186
+ }
191
187
  const sourcePath = sourceEntryPath(sourceRoot, relativePath);
192
188
  const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
193
189
  records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
194
190
  }
195
- if (input.gitlinkBasis && input.sourceMode === "git") {
196
- for (const gitlink of basisGitlinks(input.workspacePath, input.gitlinkBasis)) {
197
- const populatedPrefix = `${gitlink.relativePath}/`;
198
- const populated = [...entries.keys()].some((relativePath) => relativePath.startsWith(populatedPrefix));
199
- if (entries.get(gitlink.relativePath)?.kind === "directory" && !populated) {
200
- records.push(indexInfoRecord("160000", gitlink.objectId, gitlink.relativePath));
201
- }
202
- }
203
- }
204
191
  if (records.length > 0) {
205
192
  git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
206
193
  environment,
@@ -265,8 +252,7 @@ function synthesizeOursCommit(input) {
265
252
  const mountTree = synthesizeMountTree({
266
253
  workspacePath: input.workspacePath,
267
254
  sourcePath: input.mount.sourcePath,
268
- sourceMode: input.mount.sourceMode,
269
- gitlinkBasis: { basisHead: input.basisHead, workspaceRelativePath: input.workspaceRelativePath }
255
+ sourceMode: input.mount.sourceMode
270
256
  });
271
257
  const rootTree = graftMountTree({
272
258
  workspacePath: input.workspacePath,