@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.
@@ -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;
@@ -1804,7 +1896,7 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, p
1804
1896
  if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
1805
1897
  return total;
1806
1898
  }
1807
- async function resolveGitObjectIdsAsync(workspacePath, names) {
1899
+ async function resolveGitObjectsAsync(workspacePath, names) {
1808
1900
  const resolved = /* @__PURE__ */ new Map();
1809
1901
  const unique = [...new Set(names)];
1810
1902
  if (unique.length === 0) return resolved;
@@ -1827,21 +1919,43 @@ async function resolveGitObjectIdsAsync(workspacePath, names) {
1827
1919
  throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1828
1920
  }
1829
1921
  unique.forEach((name, index) => {
1830
- const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
1831
- 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);
1832
1924
  });
1833
1925
  return resolved;
1834
1926
  }
1835
1927
  function workspaceTreeObjectName(revision, workspaceRelativePath) {
1836
1928
  return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
1837
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
+ }
1838
1952
  async function measureInitialPublicationDiffSize(input) {
1839
1953
  const candidates = input.mounts.filter(
1840
1954
  (mount) => typeof mount.initialPublicationSourceRelativePath === "string"
1841
1955
  );
1842
1956
  if (candidates.length === 0) return null;
1843
1957
  const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
1844
- const objects = await resolveGitObjectIdsAsync(
1958
+ const objects = await resolveGitObjectsAsync(
1845
1959
  input.workspacePath,
1846
1960
  candidates.flatMap(
1847
1961
  (mount) => revisions.flatMap((revision) => [
@@ -1850,7 +1964,7 @@ async function measureInitialPublicationDiffSize(input) {
1850
1964
  ])
1851
1965
  )
1852
1966
  );
1853
- 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;
1854
1968
  const initialPublications = candidates.flatMap((mount) => {
1855
1969
  const published = lookup(input.localHead, mount.workspaceRelativePath);
1856
1970
  if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
@@ -1858,16 +1972,14 @@ async function measureInitialPublicationDiffSize(input) {
1858
1972
  return [{ mount, published, source }];
1859
1973
  });
1860
1974
  if (initialPublications.length === 0) return null;
1861
- let size = await input.measure(
1862
- input.workspacePath,
1863
- input.remoteHead,
1864
- input.localHead,
1865
- input.limit,
1866
- initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
1867
- );
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
+ ]);
1868
1979
  for (const { published, source } of initialPublications) {
1869
1980
  if (size > input.limit) break;
1870
- 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));
1871
1983
  }
1872
1984
  return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
1873
1985
  }
@@ -2131,10 +2243,12 @@ async function synchronizeWorkspaceGit(input) {
2131
2243
  return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
2132
2244
  };
2133
2245
  let newlyStagedPaths = [];
2246
+ const projectedGitlinks = [];
2134
2247
  const stageAndCommitWorkspace = async () => {
2135
2248
  if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
2136
2249
  throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
2137
2250
  }
2251
+ await stageWorkspaceGitlinks(workspacePath, projectedGitlinks);
2138
2252
  await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
2139
2253
  newlyStagedPaths = await stagedPaths(workspacePath);
2140
2254
  if (newlyStagedPaths.length > 0) {
@@ -2164,7 +2278,7 @@ async function synchronizeWorkspaceGit(input) {
2164
2278
  cycleSkippedMountIds.add(mount.id);
2165
2279
  continue;
2166
2280
  }
2167
- mirrorMountsToWorkspace(workspacePath, [mount]);
2281
+ projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
2168
2282
  projectedFastMounts.push(mount);
2169
2283
  await yieldToWorkerEventLoop();
2170
2284
  }
@@ -2263,7 +2377,9 @@ async function synchronizeWorkspaceGit(input) {
2263
2377
  }
2264
2378
  const paths = await changedPaths(workspacePath, remoteHead, localHead);
2265
2379
  const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
2266
- 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;
2267
2383
  input.assertStillAdmitted?.();
2268
2384
  let initialPublicationMountIds;
2269
2385
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
@@ -2273,7 +2389,8 @@ async function synchronizeWorkspaceGit(input) {
2273
2389
  localHead,
2274
2390
  mounts: input.mounts,
2275
2391
  limit: maxDiffBytes,
2276
- measure: measureDiffSize
2392
+ measure: measureDiffSize,
2393
+ excludePathspecs: gitlinkMigrationExcludes
2277
2394
  });
2278
2395
  input.assertStillAdmitted?.();
2279
2396
  if (initialPublication && initialPublication.size <= maxDiffBytes) {
@@ -2281,6 +2398,11 @@ async function synchronizeWorkspaceGit(input) {
2281
2398
  initialPublicationMountIds = initialPublication.mountIds;
2282
2399
  }
2283
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
+ }
2284
2406
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2285
2407
  const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
2286
2408
  return {
@@ -2325,7 +2447,7 @@ async function synchronizeWorkspaceGit(input) {
2325
2447
  "push",
2326
2448
  "--no-recurse-submodules"
2327
2449
  ];
2328
- if (input.allowLargeDiff || initialPublicationMountIds) {
2450
+ if (input.allowLargeDiff || initialPublicationMountIds || gitlinkMigrationConfirmed) {
2329
2451
  pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2330
2452
  }
2331
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,
@@ -12,6 +12,44 @@ export type TreeEntry = {
12
12
  kind: "symlink";
13
13
  mode: number;
14
14
  target: string;
15
+ }
16
+ /**
17
+ * A submodule (D4): the enclosing repository records a commit at this path
18
+ * and never its contents. A leaf for every operation here: nothing beneath
19
+ * it is inspected, copied, captured, removed, or restored.
20
+ */
21
+ | {
22
+ kind: "gitlink";
23
+ objectId: string;
24
+ };
25
+ export type WorkingTreeGitlink = {
26
+ relativePath: string;
27
+ objectId: string;
28
+ };
29
+ /** One index record of a Git checkout, in the form `git update-index --index-info` accepts back. */
30
+ export type WorkingTreeIndexRecord = {
31
+ mode: string;
32
+ objectId: string;
33
+ stage: number;
34
+ relativePath: string;
35
+ };
36
+ /**
37
+ * Index entries a Git-target mirror changes: the records it removes or
38
+ * overwrites (a restore puts them back) and the gitlinks it writes (a restore
39
+ * drops them first).
40
+ */
41
+ export type WorkingTreeIndexChanges = {
42
+ removed: WorkingTreeIndexRecord[];
43
+ written: WorkingTreeGitlink[];
44
+ };
45
+ export type WorkingTreeInspectionOptions = {
46
+ /**
47
+ * Gitlinks the enclosing repository records under an all-mode root, keyed
48
+ * by relative path. Git checks a submodule out as an empty directory, so
49
+ * the filesystem alone cannot show one; the entry replaces whatever is on
50
+ * disk at that path. Ignored in Git mode, where the index is read directly.
51
+ */
52
+ gitlinks?: ReadonlyMap<string, string>;
15
53
  };
16
54
  /**
17
55
  * Destructive native-tree moves cannot silently apply the Git-target filter:
@@ -20,19 +58,14 @@ export type TreeEntry = {
20
58
  * every portable alias must be remediated before the old tree is retired.
21
59
  */
22
60
  export declare function assertWorkingTreeHasNoPortableGitMetadataAliases(root: string): void;
23
- export declare function inspectWorkingTree(root: string, mode: WorkingTreeSourceMode, options?: {
24
- /**
25
- * Leave an uninitialized submodule (index gitlink whose directory is
26
- * empty) out of a Git-mode inspection. Its empty directory is what keeps
27
- * `git status` clean, so a deletion set must never remove it.
28
- */
29
- omitUnpopulatedGitlinks?: boolean;
30
- }): Map<string, TreeEntry>;
61
+ export declare function inspectWorkingTree(root: string, mode: WorkingTreeSourceMode, options?: WorkingTreeInspectionOptions): Map<string, TreeEntry>;
31
62
  export declare function mirrorWorkingTree(input: {
32
63
  sourceRoot: string;
33
64
  targetRoot: string;
34
65
  sourceMode: WorkingTreeSourceMode;
35
66
  deletionMode: WorkingTreeDeletionMode;
67
+ /** Gitlinks the enclosing repository records for an all-mode source (see `inspectWorkingTree`). */
68
+ sourceGitlinks?: ReadonlyMap<string, string>;
36
69
  /**
37
70
  * Defer destination fsync only while building an unpublished private tree.
38
71
  * The caller must durably fsync that complete tree before publishing it or
@@ -41,20 +74,28 @@ export declare function mirrorWorkingTree(input: {
41
74
  durability?: WorkingTreeMirrorDurability;
42
75
  }): {
43
76
  paths: string[];
77
+ gitlinks: WorkingTreeGitlink[];
44
78
  };
45
79
  /** Canonical relative form used by mirror plans and scopes, or null when the value is unsafe. */
46
80
  export declare function normalizeWorkingTreeRelativePath(value: string): string | null;
47
81
  export type WorkingTreeMirrorPlan = {
48
- /** Every target path the mirror creates, rewrites, or verifies, ancestor directories included. */
82
+ /**
83
+ * Every target path the mirror creates, rewrites, or verifies, ancestor
84
+ * directories included. A gitlink is verified as a directory; paths a
85
+ * submodule directory on the target shields are left out.
86
+ */
49
87
  desired: Map<string, TreeEntry>;
50
88
  /**
51
89
  * Target entries the mirror removes or overwrites, as they are before it
52
90
  * runs. A directory entry means the whole subtree goes, ignored files
53
- * included; entries beneath it are folded into it.
91
+ * included; entries beneath it are folded into it. Never a gitlink: a
92
+ * submodule directory stays, and only its index entry can change.
54
93
  */
55
94
  replaced: Map<string, TreeEntry>;
56
95
  /** Desired paths that have no target entry yet. */
57
96
  absent: string[];
97
+ /** Index entries the mirror changes on a Git target; empty for any other target. */
98
+ index: WorkingTreeIndexChanges;
58
99
  };
59
100
  /**
60
101
  * Compute exactly which target entries `mirrorWorkingTree` would remove,
@@ -63,19 +104,23 @@ export type WorkingTreeMirrorPlan = {
63
104
  * A desired path replaces its target entry when the kinds differ, a symlink
64
105
  * points elsewhere, or a file's bytes or mode differ; an equal entry is left
65
106
  * alone, so the plan covers only what the mirror actually touches. A Git
66
- * target's ignored trees appear only where a desired path lands on them.
107
+ * target's ignored trees appear only where a desired path lands on them, and
108
+ * its submodule directories only as index changes.
67
109
  */
68
110
  export declare function planWorkingTreeMirror(input: {
69
111
  sourceRoot: string;
70
112
  targetRoot: string;
71
113
  sourceMode: WorkingTreeSourceMode;
72
114
  deletionMode: WorkingTreeDeletionMode;
115
+ sourceGitlinks?: ReadonlyMap<string, string>;
73
116
  }): WorkingTreeMirrorPlan;
74
117
  export type WorkingTreeMirrorScope = {
75
118
  /** Desired paths that did not exist before the mirror ran; a restore removes them. */
76
119
  absent: string[];
77
120
  /** Directories captured whole; a restore makes them identical to the snapshot again. */
78
121
  trees: string[];
122
+ /** Index entries the mirror changed on a Git target; a restore drops the written gitlinks and puts the removed records back. */
123
+ index?: WorkingTreeIndexChanges;
79
124
  };
80
125
  /**
81
126
  * Copy the entries a mirror plan replaces into `snapshotRoot` at their
@@ -1,4 +1,4 @@
1
- import { type WorkingTreeSourceMode } from "./working-tree-mirror";
1
+ import { type WorkingTreeGitlink, type WorkingTreeSourceMode } from "./working-tree-mirror";
2
2
  export declare const WORKSPACE_GIT_BRANCH = "main";
3
3
  export declare const MAX_WORKSPACE_GIT_DIFF_BYTES: number;
4
4
  export declare const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v1";
@@ -154,7 +154,16 @@ export declare function ensureWorkspaceGitClone(input: {
154
154
  localHead: string | null;
155
155
  remoteHead: string | null;
156
156
  }>;
157
- declare function mirrorMountsToWorkspace(workspacePath: string, mounts: readonly WorkspaceGitMount[]): void;
157
+ type ProjectedMountGitlinks = {
158
+ mount: WorkspaceGitMount;
159
+ gitlinks: WorkingTreeGitlink[];
160
+ };
161
+ /**
162
+ * Mirror each mount's visible tree into the outer worktree. A submodule
163
+ * arrives as an empty directory plus a gitlink the caller must stage with
164
+ * `stageWorkspaceGitlinks` before `git add -A`, which can never create one.
165
+ */
166
+ declare function mirrorMountsToWorkspace(workspacePath: string, mounts: readonly WorkspaceGitMount[]): ProjectedMountGitlinks[];
158
167
  export declare function hydrateWorkspaceGitMounts(workspacePath: string, mounts: readonly WorkspaceGitMount[], options?: {
159
168
  ignoreBusy?: boolean;
160
169
  hydrationHooks?: WorkspaceHydrationTransactionHooks;
@@ -188,6 +197,12 @@ export declare function resetWorkspaceGit(input: {
188
197
  activeMountIds: string[];
189
198
  skippedMountIds: string[];
190
199
  }>;
200
+ /**
201
+ * Bytes of the raw `git diff --binary` between two revisions, capped at
202
+ * `limit + 1`. `pathspecs` narrows what is measured; callers pass
203
+ * `:(exclude,literal)<path>` for paths whose text is not publication content,
204
+ * such as a subtree a gitlink replaces (see `pathsReplacedByGitlinks`).
205
+ */
191
206
  declare function diffSizeBytes(workspacePath: string, baseRevision: string | null, headRevision: string, limit: number, pathspecs?: readonly string[]): Promise<number>;
192
207
  export declare function synchronizeWorkspaceGit(input: {
193
208
  attemptId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.100",
3
+ "version": "0.0.101",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",