@ricsam/r5d-worker 0.0.100 → 0.0.103

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.
Files changed (33) hide show
  1. package/README.md +16 -3
  2. package/dist/cjs/main.cjs +390 -322
  3. package/dist/cjs/package.json +1 -1
  4. package/dist/cjs/project-mirror-fetch-policy.cjs +48 -7
  5. package/dist/cjs/project-mirror-refs-token.cjs +36 -2
  6. package/dist/cjs/project-worktrees.cjs +49 -32
  7. package/dist/cjs/working-tree-mirror.cjs +347 -99
  8. package/dist/cjs/workspace-git-sync.cjs +189 -28
  9. package/dist/cjs/workspace-merge-projection.cjs +5 -19
  10. package/dist/cjs/workspace-mutation-gate.cjs +35 -0
  11. package/dist/cjs/workspace-projection-ledger.cjs +81 -0
  12. package/dist/cjs/workspace-sync-coalescer.cjs +58 -0
  13. package/dist/mjs/main.mjs +395 -324
  14. package/dist/mjs/package.json +1 -1
  15. package/dist/mjs/project-mirror-fetch-policy.mjs +46 -6
  16. package/dist/mjs/project-mirror-refs-token.mjs +30 -1
  17. package/dist/mjs/project-worktrees.mjs +52 -33
  18. package/dist/mjs/working-tree-mirror.mjs +347 -99
  19. package/dist/mjs/workspace-git-sync.mjs +189 -28
  20. package/dist/mjs/workspace-merge-projection.mjs +5 -19
  21. package/dist/mjs/workspace-mutation-gate.mjs +35 -0
  22. package/dist/mjs/workspace-projection-ledger.mjs +57 -0
  23. package/dist/mjs/workspace-sync-coalescer.mjs +34 -0
  24. package/dist/types/main.d.ts +29 -17
  25. package/dist/types/project-mirror-fetch-policy.d.ts +70 -15
  26. package/dist/types/project-mirror-refs-token.d.ts +34 -0
  27. package/dist/types/project-worktrees.d.ts +26 -8
  28. package/dist/types/working-tree-mirror.d.ts +56 -11
  29. package/dist/types/workspace-git-sync.d.ts +41 -4
  30. package/dist/types/workspace-mutation-gate.d.ts +24 -3
  31. package/dist/types/workspace-projection-ledger.d.ts +59 -0
  32. package/dist/types/workspace-sync-coalescer.d.ts +28 -0
  33. package/package.json +1 -1
@@ -128,6 +128,22 @@ async function gitAsync(cwd, args, action) {
128
128
  if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
129
129
  return result.stdout;
130
130
  }
131
+ async function gitOutputAsync(cwd, args, action, input) {
132
+ const subprocess = Bun.spawn(gitCommandArgs(args), {
133
+ cwd,
134
+ stdin: input ?? "ignore",
135
+ stdout: "pipe",
136
+ stderr: "pipe",
137
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
138
+ });
139
+ const [stdout, stderr, exitCode] = await Promise.all([
140
+ new Response(subprocess.stdout).arrayBuffer(),
141
+ new Response(subprocess.stderr).text(),
142
+ subprocess.exited
143
+ ]);
144
+ if (exitCode !== 0) throw new Error(`${action}: ${stderr.trim() || `git exited ${exitCode}`}`);
145
+ return Buffer.from(stdout);
146
+ }
131
147
  async function tryGitAsync(cwd, args) {
132
148
  return (await gitResultAsync(cwd, args)).exitCode === 0;
133
149
  }
@@ -1139,7 +1155,13 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1139
1155
  candidate: outerPath,
1140
1156
  label: `Workspace mount ${mount.id} outer source`
1141
1157
  });
1142
- const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({ sourceRoot: outerPath, targetRoot: sourcePath, sourceMode: "all", deletionMode: "git" });
1158
+ const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({
1159
+ sourceRoot: outerPath,
1160
+ targetRoot: sourcePath,
1161
+ sourceMode: "all",
1162
+ deletionMode: "git",
1163
+ sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
1164
+ });
1143
1165
  scope = (0, import_working_tree_mirror.captureWorkingTreeMirrorPlan)({
1144
1166
  plan,
1145
1167
  targetRoot: sourcePath,
@@ -1367,6 +1389,19 @@ ${result.stdout}`;
1367
1389
  }
1368
1390
  return revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
1369
1391
  }
1392
+ async function resolveWorkspaceRemoteHead(workspacePath, input) {
1393
+ const advertised = input.advertisedRemoteHead;
1394
+ if (advertised && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(advertised)) {
1395
+ const tracked = await revParseAsync(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
1396
+ if (tracked === advertised && await tryGitAsync(workspacePath, ["cat-file", "-e", `${tracked}^{commit}`])) {
1397
+ return { head: tracked, fetched: false };
1398
+ }
1399
+ }
1400
+ return {
1401
+ head: await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername),
1402
+ fetched: true
1403
+ };
1404
+ }
1370
1405
  async function ensureWorkspaceGitClone(input) {
1371
1406
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1372
1407
  const workspaceParent = import_node_path.default.dirname(workspacePath);
@@ -1414,7 +1449,8 @@ async function ensureWorkspaceGitClone(input) {
1414
1449
  if (!await revParseAsync(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && await tryGitAsync(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
1415
1450
  await updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
1416
1451
  }
1417
- const remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1452
+ const resolvedRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
1453
+ const remoteHead = resolvedRemoteHead.head;
1418
1454
  let localHead = await revParseAsync(workspacePath, "HEAD");
1419
1455
  if (!localHead && remoteHead) {
1420
1456
  await runWorkspaceCheckoutTransition(workspacePath, async () => {
@@ -1435,7 +1471,7 @@ async function ensureWorkspaceGitClone(input) {
1435
1471
  await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
1436
1472
  localHead = remoteHead;
1437
1473
  }
1438
- return { localHead, remoteHead };
1474
+ return { localHead, remoteHead, remoteHeadFetched: resolvedRemoteHead.fetched };
1439
1475
  }
1440
1476
  function activeMounts(mounts) {
1441
1477
  const active = [];
@@ -1450,6 +1486,7 @@ function activeMounts(mounts) {
1450
1486
  return { active, tombstones, skipped };
1451
1487
  }
1452
1488
  function mirrorMountsToWorkspace(workspacePath, mounts) {
1489
+ const projected = [];
1453
1490
  for (const mount of mounts) {
1454
1491
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1455
1492
  trustedRoot: mount.durabilityRootPath,
@@ -1461,13 +1498,61 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
1461
1498
  candidate: import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1462
1499
  label: `Workspace mount ${mount.id} outer target`
1463
1500
  });
1464
- (0, import_working_tree_mirror.mirrorWorkingTree)({
1501
+ const mirrored = (0, import_working_tree_mirror.mirrorWorkingTree)({
1465
1502
  sourceRoot: mount.sourcePath,
1466
1503
  targetRoot: import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1467
1504
  sourceMode: mount.sourceMode,
1468
1505
  deletionMode: "all"
1469
1506
  });
1507
+ projected.push({ mount, gitlinks: mirrored.gitlinks });
1470
1508
  }
1509
+ return projected;
1510
+ }
1511
+ async function stageWorkspaceGitlinks(workspacePath, projected) {
1512
+ if (projected.length === 0) return;
1513
+ const mountPaths = projected.map(({ mount }) => normalizedWorkspaceMountPath(mount.workspaceRelativePath));
1514
+ const desired = /* @__PURE__ */ new Map();
1515
+ projected.forEach(({ gitlinks }, index) => {
1516
+ for (const gitlink of gitlinks) desired.set(`${mountPaths[index]}/${gitlink.relativePath}`, gitlink.objectId);
1517
+ });
1518
+ const desiredRoots = new Set(desired.keys());
1519
+ const listed = await gitOutputAsync(
1520
+ workspacePath,
1521
+ ["ls-files", "-z", "--stage", "--", ...mountPaths.map((mountPath) => `:(literal)${mountPath}`)],
1522
+ "inspect staged workspace mount entries"
1523
+ );
1524
+ const feed = [];
1525
+ const current = /* @__PURE__ */ new Map();
1526
+ const drop = (objectId, relativePath) => {
1527
+ feed.push(Buffer.concat([Buffer.from(`0 ${"0".repeat(objectId.length)} `), Buffer.from(relativePath), Buffer.from([0])]));
1528
+ };
1529
+ for (const record of listed.toString().split("\0")) {
1530
+ const match = /^([0-7]{6}) ([0-9a-f]{40,64}) [0-3]\t(.+)$/su.exec(record);
1531
+ if (!match) continue;
1532
+ const mode = match[1];
1533
+ const objectId = match[2];
1534
+ const relativePath = match[3];
1535
+ if (mode === "160000") {
1536
+ current.set(relativePath, objectId);
1537
+ if (!desired.has(relativePath)) drop(objectId, relativePath);
1538
+ continue;
1539
+ }
1540
+ if (desired.has(relativePath) || hasAncestorIn(relativePath, desiredRoots)) drop(objectId, relativePath);
1541
+ }
1542
+ for (const [relativePath, objectId] of desired) {
1543
+ if (current.get(relativePath) === objectId) continue;
1544
+ feed.push(Buffer.concat([Buffer.from(`160000 ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]));
1545
+ }
1546
+ if (feed.length === 0) return;
1547
+ await gitOutputAsync(workspacePath, ["update-index", "-z", "--index-info"], "stage workspace gitlinks", Buffer.concat(feed));
1548
+ }
1549
+ function hasAncestorIn(relativePath, ancestors) {
1550
+ let parent = import_node_path.default.posix.dirname(relativePath);
1551
+ while (parent !== ".") {
1552
+ if (ancestors.has(parent)) return true;
1553
+ parent = import_node_path.default.posix.dirname(parent);
1554
+ }
1555
+ return false;
1471
1556
  }
1472
1557
  function removeWorkspaceMounts(workspacePath, mounts) {
1473
1558
  for (const mount of mounts) {
@@ -1507,6 +1592,24 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
1507
1592
  function workspaceMountOuterPath(workspacePath, mount) {
1508
1593
  return import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1509
1594
  }
1595
+ function outerSubtreeGitlinks(workspacePath, mount) {
1596
+ const gitlinks = /* @__PURE__ */ new Map();
1597
+ const head = revParse(workspacePath, "HEAD");
1598
+ if (!head) return gitlinks;
1599
+ const subtree = `${head}:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`;
1600
+ const listed = Bun.spawnSync(gitCommandArgs(["ls-tree", "-r", "-z", subtree]), {
1601
+ cwd: workspacePath,
1602
+ stdout: "pipe",
1603
+ stderr: "pipe",
1604
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1605
+ });
1606
+ if (listed.exitCode !== 0) return gitlinks;
1607
+ for (const record of Buffer.from(listed.stdout).toString().split("\0")) {
1608
+ const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record);
1609
+ if (match) gitlinks.set(match[2], match[1]);
1610
+ }
1611
+ return gitlinks;
1612
+ }
1510
1613
  function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1511
1614
  const hydratedMountIds = [];
1512
1615
  const skippedMountIds = [];
@@ -1550,6 +1653,9 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1550
1653
  sourceRoot,
1551
1654
  targetRoot: mount.sourcePath,
1552
1655
  sourceMode: "all",
1656
+ // The outer worktree shows a submodule as an empty directory; the tree
1657
+ // carries its pointer, which a Git checkout applies to its index (D4).
1658
+ sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount),
1553
1659
  // A missing checkout has no Git index to define git-only deletions. Its
1554
1660
  // newly created directory is empty, so all-mode is equivalent and lets
1555
1661
  // bootstrap hydration preserve the fetched tree before Git initializes.
@@ -1565,7 +1671,7 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
1565
1671
  return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
1566
1672
  });
1567
1673
  if (candidates.length === 0) return /* @__PURE__ */ new Set();
1568
- const objects = await resolveGitObjectIdsAsync(
1674
+ const objects = await resolveGitObjectsAsync(
1569
1675
  workspacePath,
1570
1676
  candidates.flatMap(({ mount, basisHead }) => [
1571
1677
  workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
@@ -1574,8 +1680,8 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
1574
1680
  );
1575
1681
  const unchanged = /* @__PURE__ */ new Set();
1576
1682
  for (const { mount, basisHead } of candidates) {
1577
- const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
1578
- const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
1683
+ const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath))?.objectId ?? null;
1684
+ const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath))?.objectId ?? null;
1579
1685
  if (before === after) unchanged.add(mount.id);
1580
1686
  }
1581
1687
  return unchanged;
@@ -1841,7 +1947,7 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, p
1841
1947
  if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
1842
1948
  return total;
1843
1949
  }
1844
- async function resolveGitObjectIdsAsync(workspacePath, names) {
1950
+ async function resolveGitObjectsAsync(workspacePath, names) {
1845
1951
  const resolved = /* @__PURE__ */ new Map();
1846
1952
  const unique = [...new Set(names)];
1847
1953
  if (unique.length === 0) return resolved;
@@ -1864,21 +1970,43 @@ async function resolveGitObjectIdsAsync(workspacePath, names) {
1864
1970
  throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1865
1971
  }
1866
1972
  unique.forEach((name, index) => {
1867
- const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
1868
- resolved.set(name, match ? match[1] : null);
1973
+ const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
1974
+ resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
1869
1975
  });
1870
1976
  return resolved;
1871
1977
  }
1872
1978
  function workspaceTreeObjectName(revision, workspaceRelativePath) {
1873
1979
  return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
1874
1980
  }
1981
+ async function pathsReplacedByGitlinks(workspacePath, baseRevision, headRevision) {
1982
+ const base = baseRevision ?? emptyTreeHash(workspacePath);
1983
+ const raw = await gitOutputAsync(
1984
+ workspacePath,
1985
+ ["diff", "--raw", "--no-renames", "-z", base, headRevision],
1986
+ "inspect workspace gitlink changes"
1987
+ );
1988
+ const records = raw.toString().split("\0");
1989
+ const addedGitlinks = [];
1990
+ for (let index = 0; index + 1 < records.length; index += 2) {
1991
+ if (/^:[0-7]{6} 160000 \S+ \S+ A$/u.test(records[index])) addedGitlinks.push(records[index + 1]);
1992
+ }
1993
+ if (addedGitlinks.length === 0) return [];
1994
+ const objects = await resolveGitObjectsAsync(
1995
+ workspacePath,
1996
+ addedGitlinks.map((relativePath) => `${base}:${relativePath}`)
1997
+ );
1998
+ return addedGitlinks.filter((relativePath) => objects.get(`${base}:${relativePath}`)?.type === "tree").sort();
1999
+ }
2000
+ function gitlinkMigrationPathspecs(paths) {
2001
+ return paths.map((relativePath) => `:(exclude,literal)${relativePath}`);
2002
+ }
1875
2003
  async function measureInitialPublicationDiffSize(input) {
1876
2004
  const candidates = input.mounts.filter(
1877
2005
  (mount) => typeof mount.initialPublicationSourceRelativePath === "string"
1878
2006
  );
1879
2007
  if (candidates.length === 0) return null;
1880
2008
  const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
1881
- const objects = await resolveGitObjectIdsAsync(
2009
+ const objects = await resolveGitObjectsAsync(
1882
2010
  input.workspacePath,
1883
2011
  candidates.flatMap(
1884
2012
  (mount) => revisions.flatMap((revision) => [
@@ -1887,7 +2015,7 @@ async function measureInitialPublicationDiffSize(input) {
1887
2015
  ])
1888
2016
  )
1889
2017
  );
1890
- const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
2018
+ const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath))?.objectId ?? null : null;
1891
2019
  const initialPublications = candidates.flatMap((mount) => {
1892
2020
  const published = lookup(input.localHead, mount.workspaceRelativePath);
1893
2021
  if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
@@ -1895,16 +2023,14 @@ async function measureInitialPublicationDiffSize(input) {
1895
2023
  return [{ mount, published, source }];
1896
2024
  });
1897
2025
  if (initialPublications.length === 0) return null;
1898
- let size = await input.measure(
1899
- input.workspacePath,
1900
- input.remoteHead,
1901
- input.localHead,
1902
- input.limit,
1903
- initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
1904
- );
2026
+ let size = await input.measure(input.workspacePath, input.remoteHead, input.localHead, input.limit, [
2027
+ ...initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`),
2028
+ ...input.excludePathspecs ?? []
2029
+ ]);
1905
2030
  for (const { published, source } of initialPublications) {
1906
2031
  if (size > input.limit) break;
1907
- size += await input.measure(input.workspacePath, source, published, input.limit - size);
2032
+ const migrations = source ? await pathsReplacedByGitlinks(input.workspacePath, source, published) : [];
2033
+ size += await input.measure(input.workspacePath, source, published, input.limit - size, gitlinkMigrationPathspecs(migrations));
1908
2034
  }
1909
2035
  return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
1910
2036
  }
@@ -1994,6 +2120,11 @@ async function synchronizeWithFetchedHead(input) {
1994
2120
  return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
1995
2121
  }
1996
2122
  async function synchronizeWorkspaceGit(input) {
2123
+ const remoteHeadFetches = { count: 0 };
2124
+ const result = await runWorkspaceGitSynchronization(input, remoteHeadFetches);
2125
+ return { ...result, remoteHeadFetches: remoteHeadFetches.count };
2126
+ }
2127
+ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
1997
2128
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1998
2129
  const attemptId = input.attemptId ?? crypto.randomUUID();
1999
2130
  const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
@@ -2002,6 +2133,7 @@ async function synchronizeWorkspaceGit(input) {
2002
2133
  const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
2003
2134
  const preserveResolutionInProgress = input.skipMountMirror === true;
2004
2135
  const initial = await ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
2136
+ if (initial.remoteHeadFetched) remoteHeadFetches.count += 1;
2005
2137
  assertWorkspaceRemediationAncestry({
2006
2138
  workspacePath,
2007
2139
  currentHead: initial.localHead,
@@ -2049,11 +2181,22 @@ async function synchronizeWorkspaceGit(input) {
2049
2181
  const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
2050
2182
  refreshCycleSkippedMounts();
2051
2183
  const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
2184
+ const activeMountIds = [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort();
2052
2185
  return {
2053
- activeMountIds: [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort(),
2054
- skippedMountIds: [...skippedIds].sort()
2186
+ activeMountIds,
2187
+ skippedMountIds: [...skippedIds].sort(),
2188
+ // An active mount's token is unchanged since capture (a moved token
2189
+ // lands it in cycleSkippedMountIds above), so this is the token its
2190
+ // projected bytes belong to.
2191
+ projectedMutationTokens: Object.fromEntries(
2192
+ activeMountIds.flatMap((id) => {
2193
+ const token = projectionMutationTokens.get(id);
2194
+ return token === void 0 ? [] : [[id, token]];
2195
+ })
2196
+ )
2055
2197
  };
2056
2198
  };
2199
+ const mountProjectionIsCurrent = (mount) => mount.lastProjectedMutationToken !== void 0 && !mount.deleteWhenSourceMissing && mount.preserveLocalOnHydrationBasisChange !== true && projectionBasisHead !== null && workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount) === projectionBasisHead && projectionMutationTokens.get(mount.id) === mount.lastProjectedMutationToken && mount.mutationToken?.() === mount.lastProjectedMutationToken;
2057
2200
  if (!input.skipMountMirror) {
2058
2201
  const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
2059
2202
  const mergeSupport = mergeProjectionEnabled ? (0, import_workspace_merge_projection.workspaceMergeProjectionSupport)() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
@@ -2168,10 +2311,12 @@ async function synchronizeWorkspaceGit(input) {
2168
2311
  return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
2169
2312
  };
2170
2313
  let newlyStagedPaths = [];
2314
+ const projectedGitlinks = [];
2171
2315
  const stageAndCommitWorkspace = async () => {
2172
2316
  if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
2173
2317
  throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
2174
2318
  }
2319
+ await stageWorkspaceGitlinks(workspacePath, projectedGitlinks);
2175
2320
  await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
2176
2321
  newlyStagedPaths = await stagedPaths(workspacePath);
2177
2322
  if (newlyStagedPaths.length > 0) {
@@ -2190,6 +2335,7 @@ async function synchronizeWorkspaceGit(input) {
2190
2335
  }
2191
2336
  };
2192
2337
  const projectedFastMounts = [];
2338
+ const currentProjectionMounts = [];
2193
2339
  if (!input.skipMountMirror) {
2194
2340
  beginWorkspaceCheckoutTransition(workspacePath);
2195
2341
  try {
@@ -2201,7 +2347,11 @@ async function synchronizeWorkspaceGit(input) {
2201
2347
  cycleSkippedMountIds.add(mount.id);
2202
2348
  continue;
2203
2349
  }
2204
- mirrorMountsToWorkspace(workspacePath, [mount]);
2350
+ if (mountProjectionIsCurrent(mount)) {
2351
+ currentProjectionMounts.push(mount);
2352
+ continue;
2353
+ }
2354
+ projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
2205
2355
  projectedFastMounts.push(mount);
2206
2356
  await yieldToWorkerEventLoop();
2207
2357
  }
@@ -2234,7 +2384,7 @@ async function synchronizeWorkspaceGit(input) {
2234
2384
  const targetProjectionReceipt = receiptWithMountsAtHead(
2235
2385
  projectionBasisReceipt,
2236
2386
  projectedHead,
2237
- projectedFastMounts,
2387
+ [...projectedFastMounts, ...currentProjectionMounts],
2238
2388
  allProjectionReceiptMounts
2239
2389
  );
2240
2390
  const currentProjectionReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
@@ -2247,7 +2397,9 @@ async function synchronizeWorkspaceGit(input) {
2247
2397
  }
2248
2398
  await completeWorkspaceCheckoutTransition(workspacePath);
2249
2399
  }
2250
- let remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
2400
+ const prePublishRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
2401
+ if (prePublishRemoteHead.fetched) remoteHeadFetches.count += 1;
2402
+ let remoteHead = prePublishRemoteHead.head;
2251
2403
  let rebaseCount = 0;
2252
2404
  let updated = false;
2253
2405
  for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
@@ -2300,7 +2452,9 @@ async function synchronizeWorkspaceGit(input) {
2300
2452
  }
2301
2453
  const paths = await changedPaths(workspacePath, remoteHead, localHead);
2302
2454
  const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
2303
- let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2455
+ const gitlinkMigrations = paths.length > 0 ? await pathsReplacedByGitlinks(workspacePath, remoteHead, localHead) : [];
2456
+ const gitlinkMigrationExcludes = gitlinkMigrationPathspecs(gitlinkMigrations);
2457
+ let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes, gitlinkMigrationExcludes) : 0;
2304
2458
  input.assertStillAdmitted?.();
2305
2459
  let initialPublicationMountIds;
2306
2460
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
@@ -2310,7 +2464,8 @@ async function synchronizeWorkspaceGit(input) {
2310
2464
  localHead,
2311
2465
  mounts: input.mounts,
2312
2466
  limit: maxDiffBytes,
2313
- measure: measureDiffSize
2467
+ measure: measureDiffSize,
2468
+ excludePathspecs: gitlinkMigrationExcludes
2314
2469
  });
2315
2470
  input.assertStillAdmitted?.();
2316
2471
  if (initialPublication && initialPublication.size <= maxDiffBytes) {
@@ -2318,6 +2473,11 @@ async function synchronizeWorkspaceGit(input) {
2318
2473
  initialPublicationMountIds = initialPublication.mountIds;
2319
2474
  }
2320
2475
  }
2476
+ let gitlinkMigrationConfirmed = false;
2477
+ if (gitlinkMigrations.length > 0 && size <= maxDiffBytes && !input.allowLargeDiff) {
2478
+ gitlinkMigrationConfirmed = await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) > maxDiffBytes;
2479
+ input.assertStillAdmitted?.();
2480
+ }
2321
2481
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2322
2482
  const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
2323
2483
  return {
@@ -2362,7 +2522,7 @@ async function synchronizeWorkspaceGit(input) {
2362
2522
  "push",
2363
2523
  "--no-recurse-submodules"
2364
2524
  ];
2365
- if (input.allowLargeDiff || initialPublicationMountIds) {
2525
+ if (input.allowLargeDiff || initialPublicationMountIds || gitlinkMigrationConfirmed) {
2366
2526
  pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2367
2527
  }
2368
2528
  pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
@@ -2396,6 +2556,7 @@ ${push.stdout}`)) {
2396
2556
  throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
2397
2557
  }
2398
2558
  remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
2559
+ remoteHeadFetches.count += 1;
2399
2560
  }
2400
2561
  throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
2401
2562
  }
@@ -206,14 +206,6 @@ function indexMode(entry) {
206
206
  function indexInfoRecord(mode, objectId, relativePath) {
207
207
  return Buffer.concat([Buffer.from(`${mode} ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]);
208
208
  }
209
- function basisGitlinks(workspacePath, basis) {
210
- const listed = gitResult(workspacePath, ["ls-tree", "-r", "-z", `${basis.basisHead}:${basis.workspaceRelativePath}`]);
211
- if (listed.exitCode !== 0) return [];
212
- return nulRecords(listed.stdout).flatMap((record) => {
213
- const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record.toString());
214
- return match ? [{ objectId: match[1], relativePath: match[2] }] : [];
215
- });
216
- }
217
209
  function synthesizeMountTree(input) {
218
210
  const sourceRoot = import_node_path.default.resolve(input.sourcePath);
219
211
  const entries = (0, import_working_tree_mirror.inspectWorkingTree)(sourceRoot, input.sourceMode);
@@ -224,19 +216,14 @@ function synthesizeMountTree(input) {
224
216
  const records = [];
225
217
  for (const [relativePath, entry] of [...entries.entries()].sort(([left], [right]) => left.localeCompare(right))) {
226
218
  if (entry.kind === "directory") continue;
219
+ if (entry.kind === "gitlink") {
220
+ records.push(indexInfoRecord("160000", entry.objectId, relativePath));
221
+ continue;
222
+ }
227
223
  const sourcePath = sourceEntryPath(sourceRoot, relativePath);
228
224
  const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
229
225
  records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
230
226
  }
231
- if (input.gitlinkBasis && input.sourceMode === "git") {
232
- for (const gitlink of basisGitlinks(input.workspacePath, input.gitlinkBasis)) {
233
- const populatedPrefix = `${gitlink.relativePath}/`;
234
- const populated = [...entries.keys()].some((relativePath) => relativePath.startsWith(populatedPrefix));
235
- if (entries.get(gitlink.relativePath)?.kind === "directory" && !populated) {
236
- records.push(indexInfoRecord("160000", gitlink.objectId, gitlink.relativePath));
237
- }
238
- }
239
- }
240
227
  if (records.length > 0) {
241
228
  git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
242
229
  environment,
@@ -301,8 +288,7 @@ function synthesizeOursCommit(input) {
301
288
  const mountTree = synthesizeMountTree({
302
289
  workspacePath: input.workspacePath,
303
290
  sourcePath: input.mount.sourcePath,
304
- sourceMode: input.mount.sourceMode,
305
- gitlinkBasis: { basisHead: input.basisHead, workspaceRelativePath: input.workspaceRelativePath }
291
+ sourceMode: input.mount.sourceMode
306
292
  });
307
293
  const rootTree = graftMountTree({
308
294
  workspacePath: input.workspacePath,
@@ -50,8 +50,43 @@ class WorkspaceMutationGate {
50
50
  release();
51
51
  }
52
52
  }
53
+ /** A worktree create/delete lease; see the branch lane in the class comment. */
54
+ acquireBranchMutation() {
55
+ return new Promise((resolve) => {
56
+ this.waiters.push({ kind: "branch", resolve });
57
+ this.drain();
58
+ });
59
+ }
60
+ async runBranchMutation(operation) {
61
+ const release = await this.acquireBranchMutation();
62
+ try {
63
+ return await operation();
64
+ } finally {
65
+ release();
66
+ }
67
+ }
68
+ /**
69
+ * Operations a branch mutation requested now would wait for: the running
70
+ * sync, if any, plus branch mutations queued behind it. Zero means it runs
71
+ * at once. Ordinary waiters are not counted because the branch lane admits
72
+ * ahead of them.
73
+ */
74
+ branchMutationQueuePosition() {
75
+ if (!this.syncActive) return 0;
76
+ return 1 + this.waiters.filter((waiter) => waiter.kind === "branch").length;
77
+ }
53
78
  drain() {
54
79
  if (this.syncActive || this.waiters.length === 0) return;
80
+ for (let index = 0; index < this.waiters.length; ) {
81
+ const waiter = this.waiters[index];
82
+ if (waiter?.kind !== "branch") {
83
+ index += 1;
84
+ continue;
85
+ }
86
+ this.waiters.splice(index, 1);
87
+ this.activeMutations += 1;
88
+ waiter.resolve(this.releaseMutation());
89
+ }
55
90
  const first = this.waiters[0];
56
91
  if (first?.kind === "sync") {
57
92
  if (this.activeMutations > 0) return;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var workspace_projection_ledger_exports = {};
20
+ __export(workspace_projection_ledger_exports, {
21
+ WorkspaceProjectionLedger: () => WorkspaceProjectionLedger
22
+ });
23
+ module.exports = __toCommonJS(workspace_projection_ledger_exports);
24
+ const RETAINED_OUTCOMES = /* @__PURE__ */ new Set(["no_change", "updated", "published", "large_diff_blocked"]);
25
+ class WorkspaceProjectionLedger {
26
+ projectedTokens = /* @__PURE__ */ new Map();
27
+ lastCycle = null;
28
+ static recordKey(mount) {
29
+ return `${mount.hydrationIncarnationKey}\0${mount.id}`;
30
+ }
31
+ get lastCycleResult() {
32
+ return this.lastCycle;
33
+ }
34
+ lastProjectedToken(mount) {
35
+ return this.projectedTokens.get(WorkspaceProjectionLedger.recordKey(mount));
36
+ }
37
+ /**
38
+ * Record a completed cycle. `projectedMutationTokens` names the token each
39
+ * active mount carried when the cycle projected it; a mount absent from it
40
+ * keeps its earlier record. Any outcome that leaves the outer clone without
41
+ * a trustworthy projection clears the ledger.
42
+ */
43
+ recordCycle(input) {
44
+ if (!RETAINED_OUTCOMES.has(input.result.outcome)) {
45
+ this.reset();
46
+ return;
47
+ }
48
+ const mountsById = new Map(input.mounts.map((mount) => [mount.id, mount]));
49
+ for (const [mountId, token] of Object.entries(input.projectedMutationTokens ?? {})) {
50
+ const mount = mountsById.get(mountId);
51
+ if (mount) this.projectedTokens.set(WorkspaceProjectionLedger.recordKey(mount), token);
52
+ }
53
+ this.lastCycle = input.result;
54
+ }
55
+ reset() {
56
+ this.projectedTokens.clear();
57
+ this.lastCycle = null;
58
+ }
59
+ /**
60
+ * The previous cycle's result when every request targets a configured
61
+ * mount and no mount's token moved since its recorded projection. A request
62
+ * without a target (a workspace-rooted command may touch any checkout), a
63
+ * mount without a token (a pending deletion), or pending deletion work all
64
+ * require a real cycle.
65
+ */
66
+ answerFromLastCycle(input) {
67
+ if (!this.lastCycle || input.pendingDeletions || input.targetMountIds.length === 0) return null;
68
+ const mountIds = new Set(input.mounts.map(({ id }) => id));
69
+ if (!input.targetMountIds.every((mountId) => mountId !== null && mountIds.has(mountId))) return null;
70
+ for (const mount of input.mounts) {
71
+ if (!mount.mutationToken) return null;
72
+ const recorded = this.projectedTokens.get(WorkspaceProjectionLedger.recordKey(mount));
73
+ if (recorded === void 0 || recorded !== mount.mutationToken()) return null;
74
+ }
75
+ return this.lastCycle;
76
+ }
77
+ }
78
+ // Annotate the CommonJS export names for ESM import in node:
79
+ 0 && (module.exports = {
80
+ WorkspaceProjectionLedger
81
+ });
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var workspace_sync_coalescer_exports = {};
20
+ __export(workspace_sync_coalescer_exports, {
21
+ WorkspaceSyncCoalescer: () => WorkspaceSyncCoalescer
22
+ });
23
+ module.exports = __toCommonJS(workspace_sync_coalescer_exports);
24
+ class WorkspaceSyncCoalescer {
25
+ constructor(options) {
26
+ this.options = options;
27
+ }
28
+ options;
29
+ pendingBatch = null;
30
+ batchesStarted = 0;
31
+ /** Requests attached to the batch that has not started yet. */
32
+ get queuedRequestCount() {
33
+ return this.pendingBatch?.requests.length ?? 0;
34
+ }
35
+ /** Batches whose body has been entered, whether or not they ran a cycle. */
36
+ get startedBatchCount() {
37
+ return this.batchesStarted;
38
+ }
39
+ request(request) {
40
+ const queued = this.pendingBatch;
41
+ if (queued) {
42
+ queued.requests.push(request);
43
+ return queued.settlement;
44
+ }
45
+ const batch = { requests: [request], settlement: Promise.resolve() };
46
+ this.pendingBatch = batch;
47
+ batch.settlement = this.options.runExclusive(async () => {
48
+ if (this.pendingBatch === batch) this.pendingBatch = null;
49
+ this.batchesStarted += 1;
50
+ return await this.options.runBatch(batch.requests);
51
+ });
52
+ return batch.settlement;
53
+ }
54
+ }
55
+ // Annotate the CommonJS export names for ESM import in node:
56
+ 0 && (module.exports = {
57
+ WorkspaceSyncCoalescer
58
+ });