@ricsam/r5d-worker 0.0.96 → 0.0.99

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.
@@ -151,14 +151,55 @@ function addParentDirectories(entries, root) {
151
151
  }
152
152
  }
153
153
  }
154
- function inspectWorkingTree(root, mode) {
154
+ function gitlinkPaths(root) {
155
+ const result = Bun.spawnSync(
156
+ [
157
+ "git",
158
+ "-c",
159
+ "submodule.recurse=false",
160
+ "-c",
161
+ "fetch.recurseSubmodules=false",
162
+ "-c",
163
+ "push.recurseSubmodules=false",
164
+ "ls-files",
165
+ "-z",
166
+ "--stage",
167
+ "--",
168
+ "."
169
+ ],
170
+ {
171
+ cwd: root,
172
+ stdout: "pipe",
173
+ stderr: "pipe",
174
+ env: workerGitProcessEnvironment()
175
+ }
176
+ );
177
+ if (result.exitCode !== 0) {
178
+ throw new Error(`Inspect Git index gitlinks: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
179
+ }
180
+ const gitlinks = /* @__PURE__ */ new Set();
181
+ for (const record of result.stdout.toString().split("\0")) {
182
+ const match = /^160000 [0-9a-f]+ [0-3]\t(.+)$/su.exec(record);
183
+ if (!match) continue;
184
+ const normalized = normalizeRelativePath(match[1]);
185
+ if (normalized) gitlinks.add(normalized);
186
+ }
187
+ return gitlinks;
188
+ }
189
+ function directoryIsEmpty(root, relativePath) {
190
+ const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
191
+ return inspected.kind === "entry" && inspected.stat.isDirectory() && fs.readdirSync(inspected.absolutePath).length === 0;
192
+ }
193
+ function inspectWorkingTree(root, mode, options = {}) {
155
194
  const entries = /* @__PURE__ */ new Map();
156
195
  root = path.resolve(root);
157
196
  if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
158
197
  if (mode === "all") {
159
198
  for (const child of fs.readdirSync(root).sort()) addEntry(entries, root, child, true);
160
199
  } else {
200
+ const omittedGitlinks = options.omitUnpopulatedGitlinks ? gitlinkPaths(root) : null;
161
201
  for (const relativePath of gitEligibleRoots(root)) {
202
+ if (omittedGitlinks?.has(relativePath) && directoryIsEmpty(root, relativePath)) continue;
162
203
  addEntry(entries, root, relativePath, true);
163
204
  }
164
205
  }
@@ -332,7 +373,9 @@ function mirrorWorkingTree(input) {
332
373
  }
333
374
  inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
334
375
  const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
335
- const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all");
376
+ const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all", {
377
+ omitUnpopulatedGitlinks: input.deletionMode === "git"
378
+ });
336
379
  for (const relativePath of [...existing.keys()].sort((left, right) => right.split("/").length - left.split("/").length)) {
337
380
  if (!desired.has(relativePath)) removeEntry(targetRoot, relativePath);
338
381
  }
@@ -343,8 +386,212 @@ function mirrorWorkingTree(input) {
343
386
  }
344
387
  return { paths: [...desired.keys()].filter((relativePath) => desired.get(relativePath)?.kind !== "directory").sort() };
345
388
  }
389
+ function normalizeWorkingTreeRelativePath(value) {
390
+ return normalizeRelativePath(value);
391
+ }
392
+ const MIRROR_TEMPORARY_FILE_PATTERN = /^\.r5d-working-tree-mirror-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/u;
393
+ function relativeDepth(relativePath) {
394
+ return relativePath.split("/").length;
395
+ }
396
+ function shallowestFirst(left, right) {
397
+ return relativeDepth(left) - relativeDepth(right) || left.localeCompare(right);
398
+ }
399
+ function deepestFirst(left, right) {
400
+ return relativeDepth(right) - relativeDepth(left) || left.localeCompare(right);
401
+ }
402
+ function hasAncestorIn(relativePath, ancestors) {
403
+ let parent = path.posix.dirname(relativePath);
404
+ while (parent !== ".") {
405
+ if (ancestors.has(parent)) return true;
406
+ parent = path.posix.dirname(parent);
407
+ }
408
+ return false;
409
+ }
410
+ function treeEntryFromStat(stat, absolutePath) {
411
+ if (stat.isSymbolicLink()) return { kind: "symlink", mode: stat.mode, target: fs.readlinkSync(absolutePath) };
412
+ if (stat.isFile()) return { kind: "file", mode: stat.mode, size: stat.size };
413
+ if (stat.isDirectory()) return { kind: "directory", mode: stat.mode };
414
+ return null;
415
+ }
416
+ function fsyncPath(targetPath, flags) {
417
+ const descriptor = fs.openSync(targetPath, flags);
418
+ try {
419
+ fs.fsyncSync(descriptor);
420
+ } finally {
421
+ fs.closeSync(descriptor);
422
+ }
423
+ }
424
+ function planWorkingTreeMirror(input) {
425
+ const sourceRoot = path.resolve(input.sourceRoot);
426
+ const targetRoot = path.resolve(input.targetRoot);
427
+ const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
428
+ const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all", {
429
+ omitUnpopulatedGitlinks: input.deletionMode === "git"
430
+ });
431
+ const replaced = /* @__PURE__ */ new Map();
432
+ const absent = [];
433
+ for (const [relativePath, entry] of existing) {
434
+ if (!desired.has(relativePath)) replaced.set(relativePath, entry);
435
+ }
436
+ for (const [relativePath, entry] of desired) {
437
+ const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
438
+ if (target.kind !== "entry") {
439
+ absent.push(relativePath);
440
+ continue;
441
+ }
442
+ const { absolutePath, stat } = target;
443
+ if (entry.kind === "directory") {
444
+ if (stat.isDirectory()) continue;
445
+ } else if (entry.kind === "symlink") {
446
+ if (stat.isSymbolicLink() && fs.readlinkSync(absolutePath) === entry.target) continue;
447
+ } else if (stat.isFile() && filesEqual(path.join(sourceRoot, ...relativePath.split("/")), absolutePath, entry)) {
448
+ continue;
449
+ }
450
+ const current = treeEntryFromStat(stat, absolutePath);
451
+ if (current) replaced.set(relativePath, current);
452
+ else absent.push(relativePath);
453
+ }
454
+ const replacedDirectories = new Set(
455
+ [...replaced].filter(([, entry]) => entry.kind === "directory").map(([relativePath]) => relativePath)
456
+ );
457
+ for (const relativePath of [...replaced.keys()]) {
458
+ if (hasAncestorIn(relativePath, replacedDirectories)) replaced.delete(relativePath);
459
+ }
460
+ return {
461
+ desired,
462
+ replaced,
463
+ absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst)
464
+ };
465
+ }
466
+ function captureWorkingTreeMirrorPlan(input) {
467
+ const targetRoot = path.resolve(input.targetRoot);
468
+ const snapshotRoot = path.resolve(input.snapshotRoot);
469
+ const durability = input.durability ?? "per_entry";
470
+ inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
471
+ if (!inspectDirectoryRoot(snapshotRoot, { allowMissing: true, label: "Working-tree snapshot root" })) {
472
+ fs.mkdirSync(snapshotRoot, { recursive: true });
473
+ }
474
+ const trees = [];
475
+ for (const [relativePath, entry] of [...input.plan.replaced.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
476
+ if (entry.kind !== "directory") {
477
+ copyEntry(targetRoot, snapshotRoot, relativePath, entry, durability);
478
+ continue;
479
+ }
480
+ const source = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
481
+ if (source.kind !== "entry" || !source.stat.isDirectory()) {
482
+ throw new Error(`Working-tree source changed while it was being mirrored: ${path.join(targetRoot, ...relativePath.split("/"))}`);
483
+ }
484
+ ensureDirectory(snapshotRoot, relativePath, entry.mode);
485
+ mirrorWorkingTree({
486
+ sourceRoot: source.absolutePath,
487
+ targetRoot: path.join(snapshotRoot, ...relativePath.split("/")),
488
+ sourceMode: "all",
489
+ deletionMode: "all",
490
+ durability
491
+ });
492
+ trees.push(relativePath);
493
+ }
494
+ return { absent: [...input.plan.absent].sort(shallowestFirst), trees: trees.sort(shallowestFirst) };
495
+ }
496
+ function restoreWorkingTreeMirrorScope(input) {
497
+ const snapshotRoot = path.resolve(input.snapshotRoot);
498
+ const targetRoot = path.resolve(input.targetRoot);
499
+ inspectDirectoryRoot(snapshotRoot, { allowMissing: false, label: "Working-tree snapshot root" });
500
+ if (!inspectDirectoryRoot(targetRoot, { allowMissing: true, label: "Working-tree target root" })) {
501
+ fs.mkdirSync(targetRoot, { recursive: true });
502
+ }
503
+ const normalize = (relativePath) => {
504
+ const normalized = normalizeRelativePath(relativePath);
505
+ if (!normalized) throw new Error(`Invalid working-tree scope path: ${relativePath}`);
506
+ return normalized;
507
+ };
508
+ const absent = input.scope.absent.map(normalize);
509
+ const trees = new Set(input.scope.trees.map(normalize));
510
+ const captured = inspectWorkingTree(snapshotRoot, "all");
511
+ for (const tree of trees) {
512
+ if (captured.get(tree)?.kind !== "directory") {
513
+ throw new Error(`Working-tree snapshot does not hold the captured directory: ${tree}`);
514
+ }
515
+ }
516
+ const touched = /* @__PURE__ */ new Set();
517
+ for (const relativePath of [...absent].sort(deepestFirst)) {
518
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
519
+ if (inspected.kind === "entry") fs.rmSync(inspected.absolutePath, { recursive: true, force: true });
520
+ touched.add(relativePath);
521
+ }
522
+ const writtenDirectories = /* @__PURE__ */ new Set();
523
+ for (const relativePath of absent) writtenDirectories.add(path.posix.dirname(relativePath));
524
+ for (const [relativePath, entry] of captured) {
525
+ if (entry.kind !== "directory" && !hasAncestorIn(relativePath, trees)) writtenDirectories.add(path.posix.dirname(relativePath));
526
+ }
527
+ for (const directory of writtenDirectories) {
528
+ let absoluteDirectory = targetRoot;
529
+ if (directory !== ".") {
530
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, directory);
531
+ if (inspected.kind !== "entry" || !inspected.stat.isDirectory()) continue;
532
+ absoluteDirectory = inspected.absolutePath;
533
+ }
534
+ for (const name of fs.readdirSync(absoluteDirectory)) {
535
+ if (!MIRROR_TEMPORARY_FILE_PATTERN.test(name)) continue;
536
+ const temporaryPath = path.join(absoluteDirectory, name);
537
+ if (!fs.lstatSync(temporaryPath).isFile()) continue;
538
+ fs.rmSync(temporaryPath, { force: true });
539
+ touched.add(directory === "." ? name : path.posix.join(directory, name));
540
+ }
541
+ }
542
+ for (const [relativePath, entry] of [...captured.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
543
+ touched.add(relativePath);
544
+ if (hasAncestorIn(relativePath, trees)) continue;
545
+ if (!trees.has(relativePath)) {
546
+ copyEntry(snapshotRoot, targetRoot, relativePath, entry, "per_entry");
547
+ continue;
548
+ }
549
+ const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
550
+ if (current.kind === "blocked") {
551
+ throw new Error(`Refusing to restore a working-tree directory through a symlink or non-directory: ${current.blockingPath}`);
552
+ }
553
+ if (current.kind === "entry" && !current.stat.isDirectory()) {
554
+ fs.rmSync(current.absolutePath, { recursive: true, force: true });
555
+ }
556
+ ensureDirectory(targetRoot, relativePath, entry.mode);
557
+ mirrorWorkingTree({
558
+ sourceRoot: path.join(snapshotRoot, ...relativePath.split("/")),
559
+ targetRoot: path.join(targetRoot, ...relativePath.split("/")),
560
+ sourceMode: "all",
561
+ deletionMode: "all"
562
+ });
563
+ }
564
+ return { paths: [...touched].sort(shallowestFirst) };
565
+ }
566
+ function fsyncWorkingTreePaths(root, relativePaths) {
567
+ root = path.resolve(root);
568
+ inspectDirectoryRoot(root, { allowMissing: false, label: "Working-tree root" });
569
+ const directories = /* @__PURE__ */ new Set(["."]);
570
+ for (const relativePath of relativePaths) {
571
+ const normalized = normalizeRelativePath(relativePath);
572
+ if (!normalized) throw new Error(`Invalid working-tree path: ${relativePath}`);
573
+ directories.add(path.posix.dirname(normalized));
574
+ const inspected = inspectRelativePathWithoutFollowingAncestors(root, normalized);
575
+ if (inspected.kind !== "entry") continue;
576
+ if (inspected.stat.isDirectory()) directories.add(normalized);
577
+ else if (inspected.stat.isFile()) fsyncPath(inspected.absolutePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
578
+ }
579
+ for (const directory of directories) {
580
+ if (directory === ".") {
581
+ fsyncPath(root, fs.constants.O_RDONLY);
582
+ continue;
583
+ }
584
+ const inspected = inspectRelativePathWithoutFollowingAncestors(root, directory);
585
+ if (inspected.kind === "entry" && inspected.stat.isDirectory()) fsyncPath(inspected.absolutePath, fs.constants.O_RDONLY);
586
+ }
587
+ }
346
588
  export {
347
589
  assertWorkingTreeHasNoPortableGitMetadataAliases,
590
+ captureWorkingTreeMirrorPlan,
591
+ fsyncWorkingTreePaths,
348
592
  inspectWorkingTree,
349
- mirrorWorkingTree
593
+ mirrorWorkingTree,
594
+ normalizeWorkingTreeRelativePath,
595
+ planWorkingTreeMirror,
596
+ restoreWorkingTreeMirrorScope
350
597
  };
@@ -6,7 +6,15 @@ import {
6
6
  mergeWorkspaceProjectionMount,
7
7
  workspaceMergeProjectionSupport
8
8
  } from "./workspace-merge-projection.mjs";
9
- import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
9
+ import {
10
+ captureWorkingTreeMirrorPlan,
11
+ fsyncWorkingTreePaths,
12
+ inspectWorkingTree,
13
+ mirrorWorkingTree,
14
+ normalizeWorkingTreeRelativePath,
15
+ planWorkingTreeMirror,
16
+ restoreWorkingTreeMirrorScope
17
+ } from "./working-tree-mirror.mjs";
10
18
  import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
11
19
  const WORKSPACE_GIT_BRANCH = "main";
12
20
  const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
@@ -909,7 +917,7 @@ function parseHydrationTransactionManifest(transactionPath) {
909
917
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
910
918
  }
911
919
  const candidate = parsed;
912
- if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
920
+ if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 || !Array.isArray(candidate.mounts)) {
913
921
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
914
922
  }
915
923
  const parseReceipt = (rawReceipt) => {
@@ -922,6 +930,25 @@ function parseHydrationTransactionManifest(transactionPath) {
922
930
  };
923
931
  const targetReceipt = parseReceipt(candidate.targetReceipt);
924
932
  const receiptBefore = parseReceipt(candidate.receiptBefore);
933
+ const parseScope = (rawScope, existed) => {
934
+ if (rawScope === void 0) return void 0;
935
+ if (candidate.version !== 4 || !existed || !rawScope || typeof rawScope !== "object") {
936
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
937
+ }
938
+ const parsePaths = (rawPaths) => {
939
+ if (!Array.isArray(rawPaths)) throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
940
+ const seen = /* @__PURE__ */ new Set();
941
+ return rawPaths.map((rawPath) => {
942
+ if (typeof rawPath !== "string" || normalizeWorkingTreeRelativePath(rawPath) !== rawPath || seen.has(rawPath)) {
943
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
944
+ }
945
+ seen.add(rawPath);
946
+ return rawPath;
947
+ });
948
+ };
949
+ const { absent, trees } = rawScope;
950
+ return { absent: parsePaths(absent), trees: parsePaths(trees) };
951
+ };
925
952
  const seenIds = /* @__PURE__ */ new Set();
926
953
  const seenSnapshots = /* @__PURE__ */ new Set();
927
954
  const mounts = candidate.mounts.map((rawMount) => {
@@ -938,10 +965,20 @@ function parseHydrationTransactionManifest(transactionPath) {
938
965
  }
939
966
  seenIds.add(mount.id);
940
967
  seenSnapshots.add(mount.snapshotName);
941
- return mount;
968
+ const scope = parseScope(rawMount.scope, mount.existed);
969
+ return {
970
+ id: mount.id,
971
+ incarnationKey: mount.incarnationKey,
972
+ sourcePath: mount.sourcePath,
973
+ durabilityRootPath: mount.durabilityRootPath,
974
+ workspaceRelativePath: mount.workspaceRelativePath,
975
+ existed: mount.existed,
976
+ snapshotName: mount.snapshotName,
977
+ ...scope ? { scope } : {}
978
+ };
942
979
  });
943
980
  return {
944
- version: 3,
981
+ version: candidate.version === 4 ? 4 : 3,
945
982
  targetReceipt,
946
983
  receiptBefore,
947
984
  mounts
@@ -963,6 +1000,7 @@ function hydrationTransactionCommitted(transactionPath) {
963
1000
  }
964
1001
  function restoreHydrationTransaction(workspacePath, manifest) {
965
1002
  const transactionPath = hydrationTransactionPath(workspacePath);
1003
+ const durabilityScopes = /* @__PURE__ */ new Map();
966
1004
  for (const mount of manifest.mounts) {
967
1005
  assertManagedDirectoryPath({
968
1006
  trustedRoot: mount.durabilityRootPath,
@@ -978,6 +1016,11 @@ function restoreHydrationTransaction(workspacePath, manifest) {
978
1016
  if (!snapshotStatus.isDirectory() || snapshotStatus.isSymbolicLink()) {
979
1017
  throw new Error(`Workspace hydration snapshot is not a regular directory: ${snapshotPath}`);
980
1018
  }
1019
+ if (mount.scope) {
1020
+ const restored = restoreWorkingTreeMirrorScope({ snapshotRoot: snapshotPath, targetRoot: mount.sourcePath, scope: mount.scope });
1021
+ durabilityScopes.set(mount.id, restored.paths);
1022
+ continue;
1023
+ }
981
1024
  mirrorWorkingTree({ sourceRoot: snapshotPath, targetRoot: mount.sourcePath, sourceMode: "all", deletionMode: "all" });
982
1025
  }
983
1026
  fsyncHydratedWorkspaceMounts(
@@ -986,7 +1029,8 @@ function restoreHydrationTransaction(workspacePath, manifest) {
986
1029
  hydrationIncarnationKey: mount.incarnationKey,
987
1030
  sourceMode: "all",
988
1031
  hydrateDeletionMode: "all"
989
- }))
1032
+ })),
1033
+ durabilityScopes
990
1034
  );
991
1035
  }
992
1036
  function recoverPendingHydrationTransaction(workspacePath) {
@@ -1030,57 +1074,80 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1030
1074
  const transactionPath = hydrationTransactionPath(workspacePath);
1031
1075
  const stagingPath = path.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
1032
1076
  fs.mkdirSync(stagingPath, { mode: 448 });
1077
+ const durabilityScopes = /* @__PURE__ */ new Map();
1033
1078
  try {
1034
1079
  const snapshotRoot = path.join(stagingPath, "mounts");
1035
1080
  fs.mkdirSync(snapshotRoot, { mode: 448 });
1036
- const manifest = {
1037
- version: 3,
1038
- targetReceipt,
1039
- receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
1040
- mounts: mounts.map((mount, index) => {
1041
- const sourcePath = path.resolve(mount.sourcePath);
1042
- const durabilityRootPath = path.resolve(mount.durabilityRootPath);
1081
+ const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
1082
+ const manifestMounts = mounts.map((mount, index) => {
1083
+ const sourcePath = path.resolve(mount.sourcePath);
1084
+ const durabilityRootPath = path.resolve(mount.durabilityRootPath);
1085
+ assertManagedDirectoryPath({
1086
+ trustedRoot: durabilityRootPath,
1087
+ candidate: sourcePath,
1088
+ label: `Workspace hydration snapshot mount ${mount.id}`
1089
+ });
1090
+ const sourceStatus = lstatIfExists(sourcePath);
1091
+ if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
1092
+ throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
1093
+ }
1094
+ const snapshotName = String(index);
1095
+ const snapshotPath = path.join(snapshotRoot, snapshotName);
1096
+ fs.mkdirSync(snapshotPath, { mode: 448 });
1097
+ let scope;
1098
+ if (sourceStatus && mount.hydrateDeletionMode === "git") {
1099
+ const outerPath = workspaceMountOuterPath(workspacePath, mount);
1043
1100
  assertManagedDirectoryPath({
1044
- trustedRoot: durabilityRootPath,
1045
- candidate: sourcePath,
1046
- label: `Workspace hydration snapshot mount ${mount.id}`
1101
+ trustedRoot: workspacePath,
1102
+ candidate: outerPath,
1103
+ label: `Workspace mount ${mount.id} outer source`
1047
1104
  });
1048
- const sourceStatus = lstatIfExists(sourcePath);
1049
- if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
1050
- throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
1051
- }
1052
- const snapshotName = String(index);
1053
- const snapshotPath = path.join(snapshotRoot, snapshotName);
1054
- fs.mkdirSync(snapshotPath, { mode: 448 });
1055
- if (sourceStatus) {
1056
- mirrorWorkingTree({
1057
- sourceRoot: sourcePath,
1058
- targetRoot: snapshotPath,
1059
- sourceMode: "all",
1060
- deletionMode: "all",
1061
- // This tree is still private and the source authority is untouched.
1062
- // The single recursive barrier below makes the complete snapshot
1063
- // durable before its directory entry is published or hydration starts.
1064
- durability: "deferred_private_staging"
1065
- });
1066
- }
1067
- return {
1068
- id: mount.id,
1069
- incarnationKey: mount.hydrationIncarnationKey,
1070
- sourcePath,
1071
- durabilityRootPath,
1072
- workspaceRelativePath: mount.workspaceRelativePath,
1073
- existed: Boolean(sourceStatus),
1074
- snapshotName
1075
- };
1076
- })
1105
+ const plan = planWorkingTreeMirror({ sourceRoot: outerPath, targetRoot: sourcePath, sourceMode: "all", deletionMode: "git" });
1106
+ scope = captureWorkingTreeMirrorPlan({
1107
+ plan,
1108
+ targetRoot: sourcePath,
1109
+ snapshotRoot: snapshotPath,
1110
+ // Same private-staging deferral as the complete snapshot below.
1111
+ durability: "deferred_private_staging"
1112
+ });
1113
+ durabilityScopes.set(mount.id, [...plan.desired.keys()]);
1114
+ } else if (sourceStatus) {
1115
+ mirrorWorkingTree({
1116
+ sourceRoot: sourcePath,
1117
+ targetRoot: snapshotPath,
1118
+ sourceMode: "all",
1119
+ deletionMode: "all",
1120
+ // This tree is still private and the source authority is untouched.
1121
+ // The single recursive barrier below makes the complete snapshot
1122
+ // durable before its directory entry is published or hydration starts.
1123
+ durability: "deferred_private_staging"
1124
+ });
1125
+ }
1126
+ return {
1127
+ id: mount.id,
1128
+ incarnationKey: mount.hydrationIncarnationKey,
1129
+ sourcePath,
1130
+ durabilityRootPath,
1131
+ workspaceRelativePath: mount.workspaceRelativePath,
1132
+ existed: Boolean(sourceStatus),
1133
+ snapshotName,
1134
+ ...scope ? { scope } : {}
1135
+ };
1136
+ });
1137
+ const manifest = {
1138
+ // Older workers reject version 4, so a scoped snapshot can never be
1139
+ // replayed as a complete copy after a downgrade.
1140
+ version: manifestMounts.some((mount) => mount.scope) ? 4 : 3,
1141
+ targetReceipt,
1142
+ receiptBefore,
1143
+ mounts: manifestMounts
1077
1144
  };
1078
1145
  writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
1079
1146
  `);
1080
1147
  fsyncTree(stagingPath);
1081
1148
  fs.renameSync(stagingPath, transactionPath);
1082
1149
  fsyncDirectory(stateDirectory);
1083
- return manifest;
1150
+ return { manifest, durabilityScopes };
1084
1151
  } catch (error) {
1085
1152
  fs.rmSync(stagingPath, { recursive: true, force: true });
1086
1153
  fsyncDirectory(stateDirectory);
@@ -1092,7 +1159,11 @@ function markHydrationTransactionDurable(workspacePath) {
1092
1159
  writePrivateFileDurably(path.join(transactionPath, "committed"), "committed\n");
1093
1160
  fsyncDirectory(transactionPath);
1094
1161
  }
1095
- function fsyncHydratedWorkspaceMount(mount) {
1162
+ function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
1163
+ if (mount.hydrateDeletionMode !== "git" || !sourceStatus.isDirectory() || !lstatIfExists(path.join(sourcePath, ".git"))) return null;
1164
+ return [...inspectWorkingTree(sourcePath, "git").keys()];
1165
+ }
1166
+ function fsyncHydratedWorkspaceMount(mount, scope) {
1096
1167
  assertManagedDirectoryPath({
1097
1168
  trustedRoot: mount.durabilityRootPath,
1098
1169
  candidate: mount.sourcePath,
@@ -1117,7 +1188,17 @@ function fsyncHydratedWorkspaceMount(mount) {
1117
1188
  fs.closeSync(descriptor);
1118
1189
  }
1119
1190
  };
1120
- if (fs.existsSync(mount.sourcePath)) syncEntry(mount.sourcePath, true);
1191
+ const sourcePath = path.resolve(mount.sourcePath);
1192
+ const sourceStatus = lstatIfExists(sourcePath);
1193
+ if (sourceStatus) {
1194
+ const paths = scope ?? gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus);
1195
+ if (paths) {
1196
+ if (!sourceStatus.isDirectory()) throw new Error(`Workspace mount ${mount.id} source is not a regular directory: ${sourcePath}`);
1197
+ fsyncWorkingTreePaths(sourcePath, paths);
1198
+ } else {
1199
+ syncEntry(sourcePath, true);
1200
+ }
1201
+ }
1121
1202
  const durabilityRoot = path.resolve(mount.durabilityRootPath);
1122
1203
  let boundaryParent = path.dirname(path.resolve(mount.sourcePath));
1123
1204
  while (true) {
@@ -1136,8 +1217,8 @@ function fsyncHydratedWorkspaceMount(mount) {
1136
1217
  boundaryParent = nextParent;
1137
1218
  }
1138
1219
  }
1139
- function fsyncHydratedWorkspaceMounts(mounts) {
1140
- for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
1220
+ function fsyncHydratedWorkspaceMounts(mounts, scopes = /* @__PURE__ */ new Map()) {
1221
+ for (const mount of mounts) fsyncHydratedWorkspaceMount(mount, scopes.get(mount.id));
1141
1222
  }
1142
1223
  function workspaceIsShallow(workspacePath) {
1143
1224
  return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
@@ -1386,6 +1467,9 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
1386
1467
  }
1387
1468
  }
1388
1469
  }
1470
+ function workspaceMountOuterPath(workspacePath, mount) {
1471
+ return path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1472
+ }
1389
1473
  function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1390
1474
  const hydratedMountIds = [];
1391
1475
  const skippedMountIds = [];
@@ -1394,7 +1478,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1394
1478
  skippedMountIds.push(mount.id);
1395
1479
  continue;
1396
1480
  }
1397
- const sourceRoot = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1481
+ const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
1398
1482
  assertManagedDirectoryPath({
1399
1483
  trustedRoot: workspacePath,
1400
1484
  candidate: sourceRoot,
@@ -1500,14 +1584,14 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1500
1584
  }
1501
1585
  const selection = computeSelection();
1502
1586
  const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
1503
- const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1587
+ const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1504
1588
  try {
1505
1589
  const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1506
1590
  const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1507
1591
  if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1508
1592
  throw new Error("Workspace hydration did not include every required mount");
1509
1593
  }
1510
- fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
1594
+ fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
1511
1595
  markHydrationTransactionDurable(workspacePath);
1512
1596
  if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1513
1597
  removeHydrationTransaction(workspacePath);
@@ -170,6 +170,14 @@ 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
+ }
173
181
  function synthesizeMountTree(input) {
174
182
  const sourceRoot = path.resolve(input.sourcePath);
175
183
  const entries = inspectWorkingTree(sourceRoot, input.sourceMode);
@@ -184,6 +192,15 @@ function synthesizeMountTree(input) {
184
192
  const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
185
193
  records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
186
194
  }
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
+ }
187
204
  if (records.length > 0) {
188
205
  git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
189
206
  environment,
@@ -248,7 +265,8 @@ function synthesizeOursCommit(input) {
248
265
  const mountTree = synthesizeMountTree({
249
266
  workspacePath: input.workspacePath,
250
267
  sourcePath: input.mount.sourcePath,
251
- sourceMode: input.mount.sourceMode
268
+ sourceMode: input.mount.sourceMode,
269
+ gitlinkBasis: { basisHead: input.basisHead, workspaceRelativePath: input.workspaceRelativePath }
252
270
  });
253
271
  const rootTree = graftMountTree({
254
272
  workspacePath: input.workspacePath,