@ricsam/r5d-worker 0.0.100 → 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.
@@ -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,
@@ -1450,6 +1472,7 @@ function activeMounts(mounts) {
1450
1472
  return { active, tombstones, skipped };
1451
1473
  }
1452
1474
  function mirrorMountsToWorkspace(workspacePath, mounts) {
1475
+ const projected = [];
1453
1476
  for (const mount of mounts) {
1454
1477
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1455
1478
  trustedRoot: mount.durabilityRootPath,
@@ -1461,13 +1484,61 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
1461
1484
  candidate: import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1462
1485
  label: `Workspace mount ${mount.id} outer target`
1463
1486
  });
1464
- (0, import_working_tree_mirror.mirrorWorkingTree)({
1487
+ const mirrored = (0, import_working_tree_mirror.mirrorWorkingTree)({
1465
1488
  sourceRoot: mount.sourcePath,
1466
1489
  targetRoot: import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1467
1490
  sourceMode: mount.sourceMode,
1468
1491
  deletionMode: "all"
1469
1492
  });
1493
+ projected.push({ mount, gitlinks: mirrored.gitlinks });
1494
+ }
1495
+ return projected;
1496
+ }
1497
+ async function stageWorkspaceGitlinks(workspacePath, projected) {
1498
+ if (projected.length === 0) return;
1499
+ const mountPaths = projected.map(({ mount }) => normalizedWorkspaceMountPath(mount.workspaceRelativePath));
1500
+ const desired = /* @__PURE__ */ new Map();
1501
+ projected.forEach(({ gitlinks }, index) => {
1502
+ for (const gitlink of gitlinks) desired.set(`${mountPaths[index]}/${gitlink.relativePath}`, gitlink.objectId);
1503
+ });
1504
+ const desiredRoots = new Set(desired.keys());
1505
+ const listed = await gitOutputAsync(
1506
+ workspacePath,
1507
+ ["ls-files", "-z", "--stage", "--", ...mountPaths.map((mountPath) => `:(literal)${mountPath}`)],
1508
+ "inspect staged workspace mount entries"
1509
+ );
1510
+ const feed = [];
1511
+ const current = /* @__PURE__ */ new Map();
1512
+ const drop = (objectId, relativePath) => {
1513
+ feed.push(Buffer.concat([Buffer.from(`0 ${"0".repeat(objectId.length)} `), Buffer.from(relativePath), Buffer.from([0])]));
1514
+ };
1515
+ for (const record of listed.toString().split("\0")) {
1516
+ const match = /^([0-7]{6}) ([0-9a-f]{40,64}) [0-3]\t(.+)$/su.exec(record);
1517
+ if (!match) continue;
1518
+ const mode = match[1];
1519
+ const objectId = match[2];
1520
+ const relativePath = match[3];
1521
+ if (mode === "160000") {
1522
+ current.set(relativePath, objectId);
1523
+ if (!desired.has(relativePath)) drop(objectId, relativePath);
1524
+ continue;
1525
+ }
1526
+ if (desired.has(relativePath) || hasAncestorIn(relativePath, desiredRoots)) drop(objectId, relativePath);
1470
1527
  }
1528
+ for (const [relativePath, objectId] of desired) {
1529
+ if (current.get(relativePath) === objectId) continue;
1530
+ feed.push(Buffer.concat([Buffer.from(`160000 ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]));
1531
+ }
1532
+ if (feed.length === 0) return;
1533
+ await gitOutputAsync(workspacePath, ["update-index", "-z", "--index-info"], "stage workspace gitlinks", Buffer.concat(feed));
1534
+ }
1535
+ function hasAncestorIn(relativePath, ancestors) {
1536
+ let parent = import_node_path.default.posix.dirname(relativePath);
1537
+ while (parent !== ".") {
1538
+ if (ancestors.has(parent)) return true;
1539
+ parent = import_node_path.default.posix.dirname(parent);
1540
+ }
1541
+ return false;
1471
1542
  }
1472
1543
  function removeWorkspaceMounts(workspacePath, mounts) {
1473
1544
  for (const mount of mounts) {
@@ -1507,6 +1578,24 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
1507
1578
  function workspaceMountOuterPath(workspacePath, mount) {
1508
1579
  return import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1509
1580
  }
1581
+ function outerSubtreeGitlinks(workspacePath, mount) {
1582
+ const gitlinks = /* @__PURE__ */ new Map();
1583
+ const head = revParse(workspacePath, "HEAD");
1584
+ if (!head) return gitlinks;
1585
+ const subtree = `${head}:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`;
1586
+ const listed = Bun.spawnSync(gitCommandArgs(["ls-tree", "-r", "-z", subtree]), {
1587
+ cwd: workspacePath,
1588
+ stdout: "pipe",
1589
+ stderr: "pipe",
1590
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1591
+ });
1592
+ if (listed.exitCode !== 0) return gitlinks;
1593
+ for (const record of Buffer.from(listed.stdout).toString().split("\0")) {
1594
+ const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record);
1595
+ if (match) gitlinks.set(match[2], match[1]);
1596
+ }
1597
+ return gitlinks;
1598
+ }
1510
1599
  function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1511
1600
  const hydratedMountIds = [];
1512
1601
  const skippedMountIds = [];
@@ -1550,6 +1639,9 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1550
1639
  sourceRoot,
1551
1640
  targetRoot: mount.sourcePath,
1552
1641
  sourceMode: "all",
1642
+ // The outer worktree shows a submodule as an empty directory; the tree
1643
+ // carries its pointer, which a Git checkout applies to its index (D4).
1644
+ sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount),
1553
1645
  // A missing checkout has no Git index to define git-only deletions. Its
1554
1646
  // newly created directory is empty, so all-mode is equivalent and lets
1555
1647
  // bootstrap hydration preserve the fetched tree before Git initializes.
@@ -1565,7 +1657,7 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
1565
1657
  return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
1566
1658
  });
1567
1659
  if (candidates.length === 0) return /* @__PURE__ */ new Set();
1568
- const objects = await resolveGitObjectIdsAsync(
1660
+ const objects = await resolveGitObjectsAsync(
1569
1661
  workspacePath,
1570
1662
  candidates.flatMap(({ mount, basisHead }) => [
1571
1663
  workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
@@ -1574,8 +1666,8 @@ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, hea
1574
1666
  );
1575
1667
  const unchanged = /* @__PURE__ */ new Set();
1576
1668
  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;
1669
+ const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath))?.objectId ?? null;
1670
+ const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath))?.objectId ?? null;
1579
1671
  if (before === after) unchanged.add(mount.id);
1580
1672
  }
1581
1673
  return unchanged;
@@ -1841,7 +1933,7 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, p
1841
1933
  if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
1842
1934
  return total;
1843
1935
  }
1844
- async function resolveGitObjectIdsAsync(workspacePath, names) {
1936
+ async function resolveGitObjectsAsync(workspacePath, names) {
1845
1937
  const resolved = /* @__PURE__ */ new Map();
1846
1938
  const unique = [...new Set(names)];
1847
1939
  if (unique.length === 0) return resolved;
@@ -1864,21 +1956,43 @@ async function resolveGitObjectIdsAsync(workspacePath, names) {
1864
1956
  throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1865
1957
  }
1866
1958
  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);
1959
+ const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
1960
+ resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
1869
1961
  });
1870
1962
  return resolved;
1871
1963
  }
1872
1964
  function workspaceTreeObjectName(revision, workspaceRelativePath) {
1873
1965
  return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
1874
1966
  }
1967
+ async function pathsReplacedByGitlinks(workspacePath, baseRevision, headRevision) {
1968
+ const base = baseRevision ?? emptyTreeHash(workspacePath);
1969
+ const raw = await gitOutputAsync(
1970
+ workspacePath,
1971
+ ["diff", "--raw", "--no-renames", "-z", base, headRevision],
1972
+ "inspect workspace gitlink changes"
1973
+ );
1974
+ const records = raw.toString().split("\0");
1975
+ const addedGitlinks = [];
1976
+ for (let index = 0; index + 1 < records.length; index += 2) {
1977
+ if (/^:[0-7]{6} 160000 \S+ \S+ A$/u.test(records[index])) addedGitlinks.push(records[index + 1]);
1978
+ }
1979
+ if (addedGitlinks.length === 0) return [];
1980
+ const objects = await resolveGitObjectsAsync(
1981
+ workspacePath,
1982
+ addedGitlinks.map((relativePath) => `${base}:${relativePath}`)
1983
+ );
1984
+ return addedGitlinks.filter((relativePath) => objects.get(`${base}:${relativePath}`)?.type === "tree").sort();
1985
+ }
1986
+ function gitlinkMigrationPathspecs(paths) {
1987
+ return paths.map((relativePath) => `:(exclude,literal)${relativePath}`);
1988
+ }
1875
1989
  async function measureInitialPublicationDiffSize(input) {
1876
1990
  const candidates = input.mounts.filter(
1877
1991
  (mount) => typeof mount.initialPublicationSourceRelativePath === "string"
1878
1992
  );
1879
1993
  if (candidates.length === 0) return null;
1880
1994
  const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
1881
- const objects = await resolveGitObjectIdsAsync(
1995
+ const objects = await resolveGitObjectsAsync(
1882
1996
  input.workspacePath,
1883
1997
  candidates.flatMap(
1884
1998
  (mount) => revisions.flatMap((revision) => [
@@ -1887,7 +2001,7 @@ async function measureInitialPublicationDiffSize(input) {
1887
2001
  ])
1888
2002
  )
1889
2003
  );
1890
- const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
2004
+ const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath))?.objectId ?? null : null;
1891
2005
  const initialPublications = candidates.flatMap((mount) => {
1892
2006
  const published = lookup(input.localHead, mount.workspaceRelativePath);
1893
2007
  if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
@@ -1895,16 +2009,14 @@ async function measureInitialPublicationDiffSize(input) {
1895
2009
  return [{ mount, published, source }];
1896
2010
  });
1897
2011
  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
- );
2012
+ let size = await input.measure(input.workspacePath, input.remoteHead, input.localHead, input.limit, [
2013
+ ...initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`),
2014
+ ...input.excludePathspecs ?? []
2015
+ ]);
1905
2016
  for (const { published, source } of initialPublications) {
1906
2017
  if (size > input.limit) break;
1907
- size += await input.measure(input.workspacePath, source, published, input.limit - size);
2018
+ const migrations = source ? await pathsReplacedByGitlinks(input.workspacePath, source, published) : [];
2019
+ size += await input.measure(input.workspacePath, source, published, input.limit - size, gitlinkMigrationPathspecs(migrations));
1908
2020
  }
1909
2021
  return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
1910
2022
  }
@@ -2168,10 +2280,12 @@ async function synchronizeWorkspaceGit(input) {
2168
2280
  return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
2169
2281
  };
2170
2282
  let newlyStagedPaths = [];
2283
+ const projectedGitlinks = [];
2171
2284
  const stageAndCommitWorkspace = async () => {
2172
2285
  if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
2173
2286
  throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
2174
2287
  }
2288
+ await stageWorkspaceGitlinks(workspacePath, projectedGitlinks);
2175
2289
  await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
2176
2290
  newlyStagedPaths = await stagedPaths(workspacePath);
2177
2291
  if (newlyStagedPaths.length > 0) {
@@ -2201,7 +2315,7 @@ async function synchronizeWorkspaceGit(input) {
2201
2315
  cycleSkippedMountIds.add(mount.id);
2202
2316
  continue;
2203
2317
  }
2204
- mirrorMountsToWorkspace(workspacePath, [mount]);
2318
+ projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
2205
2319
  projectedFastMounts.push(mount);
2206
2320
  await yieldToWorkerEventLoop();
2207
2321
  }
@@ -2300,7 +2414,9 @@ async function synchronizeWorkspaceGit(input) {
2300
2414
  }
2301
2415
  const paths = await changedPaths(workspacePath, remoteHead, localHead);
2302
2416
  const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
2303
- let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2417
+ const gitlinkMigrations = paths.length > 0 ? await pathsReplacedByGitlinks(workspacePath, remoteHead, localHead) : [];
2418
+ const gitlinkMigrationExcludes = gitlinkMigrationPathspecs(gitlinkMigrations);
2419
+ let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes, gitlinkMigrationExcludes) : 0;
2304
2420
  input.assertStillAdmitted?.();
2305
2421
  let initialPublicationMountIds;
2306
2422
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
@@ -2310,7 +2426,8 @@ async function synchronizeWorkspaceGit(input) {
2310
2426
  localHead,
2311
2427
  mounts: input.mounts,
2312
2428
  limit: maxDiffBytes,
2313
- measure: measureDiffSize
2429
+ measure: measureDiffSize,
2430
+ excludePathspecs: gitlinkMigrationExcludes
2314
2431
  });
2315
2432
  input.assertStillAdmitted?.();
2316
2433
  if (initialPublication && initialPublication.size <= maxDiffBytes) {
@@ -2318,6 +2435,11 @@ async function synchronizeWorkspaceGit(input) {
2318
2435
  initialPublicationMountIds = initialPublication.mountIds;
2319
2436
  }
2320
2437
  }
2438
+ let gitlinkMigrationConfirmed = false;
2439
+ if (gitlinkMigrations.length > 0 && size <= maxDiffBytes && !input.allowLargeDiff) {
2440
+ gitlinkMigrationConfirmed = await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) > maxDiffBytes;
2441
+ input.assertStillAdmitted?.();
2442
+ }
2321
2443
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2322
2444
  const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
2323
2445
  return {
@@ -2362,7 +2484,7 @@ async function synchronizeWorkspaceGit(input) {
2362
2484
  "push",
2363
2485
  "--no-recurse-submodules"
2364
2486
  ];
2365
- if (input.allowLargeDiff || initialPublicationMountIds) {
2487
+ if (input.allowLargeDiff || initialPublicationMountIds || gitlinkMigrationConfirmed) {
2366
2488
  pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2367
2489
  }
2368
2490
  pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
@@ -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,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.100",
3
+ "version": "0.0.101",
4
4
  "type": "module"
5
5
  }