@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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/working-tree-mirror.cjs +256 -4
- package/dist/cjs/workspace-git-sync.cjs +127 -51
- package/dist/cjs/workspace-merge-projection.cjs +19 -1
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/working-tree-mirror.mjs +250 -3
- package/dist/mjs/workspace-git-sync.mjs +136 -52
- package/dist/mjs/workspace-merge-projection.mjs +19 -1
- package/dist/types/working-tree-mirror.d.ts +80 -1
- package/package.json +1 -1
|
@@ -151,14 +151,55 @@ function addParentDirectories(entries, root) {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
function
|
|
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 {
|
|
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
|
-
|
|
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
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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:
|
|
1045
|
-
candidate:
|
|
1046
|
-
label: `Workspace
|
|
1101
|
+
trustedRoot: workspacePath,
|
|
1102
|
+
candidate: outerPath,
|
|
1103
|
+
label: `Workspace mount ${mount.id} outer source`
|
|
1047
1104
|
});
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|