@ricsam/r5d-worker 0.0.97 → 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/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/types/working-tree-mirror.d.ts +80 -1
- package/package.json +1 -1
package/dist/cjs/package.json
CHANGED
|
@@ -29,8 +29,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var working_tree_mirror_exports = {};
|
|
30
30
|
__export(working_tree_mirror_exports, {
|
|
31
31
|
assertWorkingTreeHasNoPortableGitMetadataAliases: () => assertWorkingTreeHasNoPortableGitMetadataAliases,
|
|
32
|
+
captureWorkingTreeMirrorPlan: () => captureWorkingTreeMirrorPlan,
|
|
33
|
+
fsyncWorkingTreePaths: () => fsyncWorkingTreePaths,
|
|
32
34
|
inspectWorkingTree: () => inspectWorkingTree,
|
|
33
|
-
mirrorWorkingTree: () => mirrorWorkingTree
|
|
35
|
+
mirrorWorkingTree: () => mirrorWorkingTree,
|
|
36
|
+
normalizeWorkingTreeRelativePath: () => normalizeWorkingTreeRelativePath,
|
|
37
|
+
planWorkingTreeMirror: () => planWorkingTreeMirror,
|
|
38
|
+
restoreWorkingTreeMirrorScope: () => restoreWorkingTreeMirrorScope
|
|
34
39
|
});
|
|
35
40
|
module.exports = __toCommonJS(working_tree_mirror_exports);
|
|
36
41
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
@@ -186,14 +191,55 @@ function addParentDirectories(entries, root) {
|
|
|
186
191
|
}
|
|
187
192
|
}
|
|
188
193
|
}
|
|
189
|
-
function
|
|
194
|
+
function gitlinkPaths(root) {
|
|
195
|
+
const result = Bun.spawnSync(
|
|
196
|
+
[
|
|
197
|
+
"git",
|
|
198
|
+
"-c",
|
|
199
|
+
"submodule.recurse=false",
|
|
200
|
+
"-c",
|
|
201
|
+
"fetch.recurseSubmodules=false",
|
|
202
|
+
"-c",
|
|
203
|
+
"push.recurseSubmodules=false",
|
|
204
|
+
"ls-files",
|
|
205
|
+
"-z",
|
|
206
|
+
"--stage",
|
|
207
|
+
"--",
|
|
208
|
+
"."
|
|
209
|
+
],
|
|
210
|
+
{
|
|
211
|
+
cwd: root,
|
|
212
|
+
stdout: "pipe",
|
|
213
|
+
stderr: "pipe",
|
|
214
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
if (result.exitCode !== 0) {
|
|
218
|
+
throw new Error(`Inspect Git index gitlinks: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
219
|
+
}
|
|
220
|
+
const gitlinks = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const record of result.stdout.toString().split("\0")) {
|
|
222
|
+
const match = /^160000 [0-9a-f]+ [0-3]\t(.+)$/su.exec(record);
|
|
223
|
+
if (!match) continue;
|
|
224
|
+
const normalized = normalizeRelativePath(match[1]);
|
|
225
|
+
if (normalized) gitlinks.add(normalized);
|
|
226
|
+
}
|
|
227
|
+
return gitlinks;
|
|
228
|
+
}
|
|
229
|
+
function directoryIsEmpty(root, relativePath) {
|
|
230
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
|
|
231
|
+
return inspected.kind === "entry" && inspected.stat.isDirectory() && import_node_fs.default.readdirSync(inspected.absolutePath).length === 0;
|
|
232
|
+
}
|
|
233
|
+
function inspectWorkingTree(root, mode, options = {}) {
|
|
190
234
|
const entries = /* @__PURE__ */ new Map();
|
|
191
235
|
root = import_node_path.default.resolve(root);
|
|
192
236
|
if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
|
|
193
237
|
if (mode === "all") {
|
|
194
238
|
for (const child of import_node_fs.default.readdirSync(root).sort()) addEntry(entries, root, child, true);
|
|
195
239
|
} else {
|
|
240
|
+
const omittedGitlinks = options.omitUnpopulatedGitlinks ? gitlinkPaths(root) : null;
|
|
196
241
|
for (const relativePath of gitEligibleRoots(root)) {
|
|
242
|
+
if (omittedGitlinks?.has(relativePath) && directoryIsEmpty(root, relativePath)) continue;
|
|
197
243
|
addEntry(entries, root, relativePath, true);
|
|
198
244
|
}
|
|
199
245
|
}
|
|
@@ -367,7 +413,9 @@ function mirrorWorkingTree(input) {
|
|
|
367
413
|
}
|
|
368
414
|
inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
|
|
369
415
|
const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
370
|
-
const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all"
|
|
416
|
+
const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all", {
|
|
417
|
+
omitUnpopulatedGitlinks: input.deletionMode === "git"
|
|
418
|
+
});
|
|
371
419
|
for (const relativePath of [...existing.keys()].sort((left, right) => right.split("/").length - left.split("/").length)) {
|
|
372
420
|
if (!desired.has(relativePath)) removeEntry(targetRoot, relativePath);
|
|
373
421
|
}
|
|
@@ -378,9 +426,213 @@ function mirrorWorkingTree(input) {
|
|
|
378
426
|
}
|
|
379
427
|
return { paths: [...desired.keys()].filter((relativePath) => desired.get(relativePath)?.kind !== "directory").sort() };
|
|
380
428
|
}
|
|
429
|
+
function normalizeWorkingTreeRelativePath(value) {
|
|
430
|
+
return normalizeRelativePath(value);
|
|
431
|
+
}
|
|
432
|
+
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;
|
|
433
|
+
function relativeDepth(relativePath) {
|
|
434
|
+
return relativePath.split("/").length;
|
|
435
|
+
}
|
|
436
|
+
function shallowestFirst(left, right) {
|
|
437
|
+
return relativeDepth(left) - relativeDepth(right) || left.localeCompare(right);
|
|
438
|
+
}
|
|
439
|
+
function deepestFirst(left, right) {
|
|
440
|
+
return relativeDepth(right) - relativeDepth(left) || left.localeCompare(right);
|
|
441
|
+
}
|
|
442
|
+
function hasAncestorIn(relativePath, ancestors) {
|
|
443
|
+
let parent = import_node_path.default.posix.dirname(relativePath);
|
|
444
|
+
while (parent !== ".") {
|
|
445
|
+
if (ancestors.has(parent)) return true;
|
|
446
|
+
parent = import_node_path.default.posix.dirname(parent);
|
|
447
|
+
}
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
function treeEntryFromStat(stat, absolutePath) {
|
|
451
|
+
if (stat.isSymbolicLink()) return { kind: "symlink", mode: stat.mode, target: import_node_fs.default.readlinkSync(absolutePath) };
|
|
452
|
+
if (stat.isFile()) return { kind: "file", mode: stat.mode, size: stat.size };
|
|
453
|
+
if (stat.isDirectory()) return { kind: "directory", mode: stat.mode };
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
function fsyncPath(targetPath, flags) {
|
|
457
|
+
const descriptor = import_node_fs.default.openSync(targetPath, flags);
|
|
458
|
+
try {
|
|
459
|
+
import_node_fs.default.fsyncSync(descriptor);
|
|
460
|
+
} finally {
|
|
461
|
+
import_node_fs.default.closeSync(descriptor);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function planWorkingTreeMirror(input) {
|
|
465
|
+
const sourceRoot = import_node_path.default.resolve(input.sourceRoot);
|
|
466
|
+
const targetRoot = import_node_path.default.resolve(input.targetRoot);
|
|
467
|
+
const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
468
|
+
const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all", {
|
|
469
|
+
omitUnpopulatedGitlinks: input.deletionMode === "git"
|
|
470
|
+
});
|
|
471
|
+
const replaced = /* @__PURE__ */ new Map();
|
|
472
|
+
const absent = [];
|
|
473
|
+
for (const [relativePath, entry] of existing) {
|
|
474
|
+
if (!desired.has(relativePath)) replaced.set(relativePath, entry);
|
|
475
|
+
}
|
|
476
|
+
for (const [relativePath, entry] of desired) {
|
|
477
|
+
const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
478
|
+
if (target.kind !== "entry") {
|
|
479
|
+
absent.push(relativePath);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const { absolutePath, stat } = target;
|
|
483
|
+
if (entry.kind === "directory") {
|
|
484
|
+
if (stat.isDirectory()) continue;
|
|
485
|
+
} else if (entry.kind === "symlink") {
|
|
486
|
+
if (stat.isSymbolicLink() && import_node_fs.default.readlinkSync(absolutePath) === entry.target) continue;
|
|
487
|
+
} else if (stat.isFile() && filesEqual(import_node_path.default.join(sourceRoot, ...relativePath.split("/")), absolutePath, entry)) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const current = treeEntryFromStat(stat, absolutePath);
|
|
491
|
+
if (current) replaced.set(relativePath, current);
|
|
492
|
+
else absent.push(relativePath);
|
|
493
|
+
}
|
|
494
|
+
const replacedDirectories = new Set(
|
|
495
|
+
[...replaced].filter(([, entry]) => entry.kind === "directory").map(([relativePath]) => relativePath)
|
|
496
|
+
);
|
|
497
|
+
for (const relativePath of [...replaced.keys()]) {
|
|
498
|
+
if (hasAncestorIn(relativePath, replacedDirectories)) replaced.delete(relativePath);
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
desired,
|
|
502
|
+
replaced,
|
|
503
|
+
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function captureWorkingTreeMirrorPlan(input) {
|
|
507
|
+
const targetRoot = import_node_path.default.resolve(input.targetRoot);
|
|
508
|
+
const snapshotRoot = import_node_path.default.resolve(input.snapshotRoot);
|
|
509
|
+
const durability = input.durability ?? "per_entry";
|
|
510
|
+
inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
|
|
511
|
+
if (!inspectDirectoryRoot(snapshotRoot, { allowMissing: true, label: "Working-tree snapshot root" })) {
|
|
512
|
+
import_node_fs.default.mkdirSync(snapshotRoot, { recursive: true });
|
|
513
|
+
}
|
|
514
|
+
const trees = [];
|
|
515
|
+
for (const [relativePath, entry] of [...input.plan.replaced.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
516
|
+
if (entry.kind !== "directory") {
|
|
517
|
+
copyEntry(targetRoot, snapshotRoot, relativePath, entry, durability);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const source = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
521
|
+
if (source.kind !== "entry" || !source.stat.isDirectory()) {
|
|
522
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${import_node_path.default.join(targetRoot, ...relativePath.split("/"))}`);
|
|
523
|
+
}
|
|
524
|
+
ensureDirectory(snapshotRoot, relativePath, entry.mode);
|
|
525
|
+
mirrorWorkingTree({
|
|
526
|
+
sourceRoot: source.absolutePath,
|
|
527
|
+
targetRoot: import_node_path.default.join(snapshotRoot, ...relativePath.split("/")),
|
|
528
|
+
sourceMode: "all",
|
|
529
|
+
deletionMode: "all",
|
|
530
|
+
durability
|
|
531
|
+
});
|
|
532
|
+
trees.push(relativePath);
|
|
533
|
+
}
|
|
534
|
+
return { absent: [...input.plan.absent].sort(shallowestFirst), trees: trees.sort(shallowestFirst) };
|
|
535
|
+
}
|
|
536
|
+
function restoreWorkingTreeMirrorScope(input) {
|
|
537
|
+
const snapshotRoot = import_node_path.default.resolve(input.snapshotRoot);
|
|
538
|
+
const targetRoot = import_node_path.default.resolve(input.targetRoot);
|
|
539
|
+
inspectDirectoryRoot(snapshotRoot, { allowMissing: false, label: "Working-tree snapshot root" });
|
|
540
|
+
if (!inspectDirectoryRoot(targetRoot, { allowMissing: true, label: "Working-tree target root" })) {
|
|
541
|
+
import_node_fs.default.mkdirSync(targetRoot, { recursive: true });
|
|
542
|
+
}
|
|
543
|
+
const normalize = (relativePath) => {
|
|
544
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
545
|
+
if (!normalized) throw new Error(`Invalid working-tree scope path: ${relativePath}`);
|
|
546
|
+
return normalized;
|
|
547
|
+
};
|
|
548
|
+
const absent = input.scope.absent.map(normalize);
|
|
549
|
+
const trees = new Set(input.scope.trees.map(normalize));
|
|
550
|
+
const captured = inspectWorkingTree(snapshotRoot, "all");
|
|
551
|
+
for (const tree of trees) {
|
|
552
|
+
if (captured.get(tree)?.kind !== "directory") {
|
|
553
|
+
throw new Error(`Working-tree snapshot does not hold the captured directory: ${tree}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const touched = /* @__PURE__ */ new Set();
|
|
557
|
+
for (const relativePath of [...absent].sort(deepestFirst)) {
|
|
558
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
559
|
+
if (inspected.kind === "entry") import_node_fs.default.rmSync(inspected.absolutePath, { recursive: true, force: true });
|
|
560
|
+
touched.add(relativePath);
|
|
561
|
+
}
|
|
562
|
+
const writtenDirectories = /* @__PURE__ */ new Set();
|
|
563
|
+
for (const relativePath of absent) writtenDirectories.add(import_node_path.default.posix.dirname(relativePath));
|
|
564
|
+
for (const [relativePath, entry] of captured) {
|
|
565
|
+
if (entry.kind !== "directory" && !hasAncestorIn(relativePath, trees)) writtenDirectories.add(import_node_path.default.posix.dirname(relativePath));
|
|
566
|
+
}
|
|
567
|
+
for (const directory of writtenDirectories) {
|
|
568
|
+
let absoluteDirectory = targetRoot;
|
|
569
|
+
if (directory !== ".") {
|
|
570
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, directory);
|
|
571
|
+
if (inspected.kind !== "entry" || !inspected.stat.isDirectory()) continue;
|
|
572
|
+
absoluteDirectory = inspected.absolutePath;
|
|
573
|
+
}
|
|
574
|
+
for (const name of import_node_fs.default.readdirSync(absoluteDirectory)) {
|
|
575
|
+
if (!MIRROR_TEMPORARY_FILE_PATTERN.test(name)) continue;
|
|
576
|
+
const temporaryPath = import_node_path.default.join(absoluteDirectory, name);
|
|
577
|
+
if (!import_node_fs.default.lstatSync(temporaryPath).isFile()) continue;
|
|
578
|
+
import_node_fs.default.rmSync(temporaryPath, { force: true });
|
|
579
|
+
touched.add(directory === "." ? name : import_node_path.default.posix.join(directory, name));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
for (const [relativePath, entry] of [...captured.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
583
|
+
touched.add(relativePath);
|
|
584
|
+
if (hasAncestorIn(relativePath, trees)) continue;
|
|
585
|
+
if (!trees.has(relativePath)) {
|
|
586
|
+
copyEntry(snapshotRoot, targetRoot, relativePath, entry, "per_entry");
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
590
|
+
if (current.kind === "blocked") {
|
|
591
|
+
throw new Error(`Refusing to restore a working-tree directory through a symlink or non-directory: ${current.blockingPath}`);
|
|
592
|
+
}
|
|
593
|
+
if (current.kind === "entry" && !current.stat.isDirectory()) {
|
|
594
|
+
import_node_fs.default.rmSync(current.absolutePath, { recursive: true, force: true });
|
|
595
|
+
}
|
|
596
|
+
ensureDirectory(targetRoot, relativePath, entry.mode);
|
|
597
|
+
mirrorWorkingTree({
|
|
598
|
+
sourceRoot: import_node_path.default.join(snapshotRoot, ...relativePath.split("/")),
|
|
599
|
+
targetRoot: import_node_path.default.join(targetRoot, ...relativePath.split("/")),
|
|
600
|
+
sourceMode: "all",
|
|
601
|
+
deletionMode: "all"
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return { paths: [...touched].sort(shallowestFirst) };
|
|
605
|
+
}
|
|
606
|
+
function fsyncWorkingTreePaths(root, relativePaths) {
|
|
607
|
+
root = import_node_path.default.resolve(root);
|
|
608
|
+
inspectDirectoryRoot(root, { allowMissing: false, label: "Working-tree root" });
|
|
609
|
+
const directories = /* @__PURE__ */ new Set(["."]);
|
|
610
|
+
for (const relativePath of relativePaths) {
|
|
611
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
612
|
+
if (!normalized) throw new Error(`Invalid working-tree path: ${relativePath}`);
|
|
613
|
+
directories.add(import_node_path.default.posix.dirname(normalized));
|
|
614
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, normalized);
|
|
615
|
+
if (inspected.kind !== "entry") continue;
|
|
616
|
+
if (inspected.stat.isDirectory()) directories.add(normalized);
|
|
617
|
+
else if (inspected.stat.isFile()) fsyncPath(inspected.absolutePath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
|
|
618
|
+
}
|
|
619
|
+
for (const directory of directories) {
|
|
620
|
+
if (directory === ".") {
|
|
621
|
+
fsyncPath(root, import_node_fs.default.constants.O_RDONLY);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, directory);
|
|
625
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory()) fsyncPath(inspected.absolutePath, import_node_fs.default.constants.O_RDONLY);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
381
628
|
// Annotate the CommonJS export names for ESM import in node:
|
|
382
629
|
0 && (module.exports = {
|
|
383
630
|
assertWorkingTreeHasNoPortableGitMetadataAliases,
|
|
631
|
+
captureWorkingTreeMirrorPlan,
|
|
632
|
+
fsyncWorkingTreePaths,
|
|
384
633
|
inspectWorkingTree,
|
|
385
|
-
mirrorWorkingTree
|
|
634
|
+
mirrorWorkingTree,
|
|
635
|
+
normalizeWorkingTreeRelativePath,
|
|
636
|
+
planWorkingTreeMirror,
|
|
637
|
+
restoreWorkingTreeMirrorScope
|
|
386
638
|
});
|
|
@@ -954,7 +954,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
954
954
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
955
955
|
}
|
|
956
956
|
const candidate = parsed;
|
|
957
|
-
if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
|
|
957
|
+
if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 || !Array.isArray(candidate.mounts)) {
|
|
958
958
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
959
959
|
}
|
|
960
960
|
const parseReceipt = (rawReceipt) => {
|
|
@@ -967,6 +967,25 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
967
967
|
};
|
|
968
968
|
const targetReceipt = parseReceipt(candidate.targetReceipt);
|
|
969
969
|
const receiptBefore = parseReceipt(candidate.receiptBefore);
|
|
970
|
+
const parseScope = (rawScope, existed) => {
|
|
971
|
+
if (rawScope === void 0) return void 0;
|
|
972
|
+
if (candidate.version !== 4 || !existed || !rawScope || typeof rawScope !== "object") {
|
|
973
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
974
|
+
}
|
|
975
|
+
const parsePaths = (rawPaths) => {
|
|
976
|
+
if (!Array.isArray(rawPaths)) throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
977
|
+
const seen = /* @__PURE__ */ new Set();
|
|
978
|
+
return rawPaths.map((rawPath) => {
|
|
979
|
+
if (typeof rawPath !== "string" || (0, import_working_tree_mirror.normalizeWorkingTreeRelativePath)(rawPath) !== rawPath || seen.has(rawPath)) {
|
|
980
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
981
|
+
}
|
|
982
|
+
seen.add(rawPath);
|
|
983
|
+
return rawPath;
|
|
984
|
+
});
|
|
985
|
+
};
|
|
986
|
+
const { absent, trees } = rawScope;
|
|
987
|
+
return { absent: parsePaths(absent), trees: parsePaths(trees) };
|
|
988
|
+
};
|
|
970
989
|
const seenIds = /* @__PURE__ */ new Set();
|
|
971
990
|
const seenSnapshots = /* @__PURE__ */ new Set();
|
|
972
991
|
const mounts = candidate.mounts.map((rawMount) => {
|
|
@@ -983,10 +1002,20 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
983
1002
|
}
|
|
984
1003
|
seenIds.add(mount.id);
|
|
985
1004
|
seenSnapshots.add(mount.snapshotName);
|
|
986
|
-
|
|
1005
|
+
const scope = parseScope(rawMount.scope, mount.existed);
|
|
1006
|
+
return {
|
|
1007
|
+
id: mount.id,
|
|
1008
|
+
incarnationKey: mount.incarnationKey,
|
|
1009
|
+
sourcePath: mount.sourcePath,
|
|
1010
|
+
durabilityRootPath: mount.durabilityRootPath,
|
|
1011
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
1012
|
+
existed: mount.existed,
|
|
1013
|
+
snapshotName: mount.snapshotName,
|
|
1014
|
+
...scope ? { scope } : {}
|
|
1015
|
+
};
|
|
987
1016
|
});
|
|
988
1017
|
return {
|
|
989
|
-
version: 3,
|
|
1018
|
+
version: candidate.version === 4 ? 4 : 3,
|
|
990
1019
|
targetReceipt,
|
|
991
1020
|
receiptBefore,
|
|
992
1021
|
mounts
|
|
@@ -1008,6 +1037,7 @@ function hydrationTransactionCommitted(transactionPath) {
|
|
|
1008
1037
|
}
|
|
1009
1038
|
function restoreHydrationTransaction(workspacePath, manifest) {
|
|
1010
1039
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1040
|
+
const durabilityScopes = /* @__PURE__ */ new Map();
|
|
1011
1041
|
for (const mount of manifest.mounts) {
|
|
1012
1042
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1013
1043
|
trustedRoot: mount.durabilityRootPath,
|
|
@@ -1023,6 +1053,11 @@ function restoreHydrationTransaction(workspacePath, manifest) {
|
|
|
1023
1053
|
if (!snapshotStatus.isDirectory() || snapshotStatus.isSymbolicLink()) {
|
|
1024
1054
|
throw new Error(`Workspace hydration snapshot is not a regular directory: ${snapshotPath}`);
|
|
1025
1055
|
}
|
|
1056
|
+
if (mount.scope) {
|
|
1057
|
+
const restored = (0, import_working_tree_mirror.restoreWorkingTreeMirrorScope)({ snapshotRoot: snapshotPath, targetRoot: mount.sourcePath, scope: mount.scope });
|
|
1058
|
+
durabilityScopes.set(mount.id, restored.paths);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1026
1061
|
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: snapshotPath, targetRoot: mount.sourcePath, sourceMode: "all", deletionMode: "all" });
|
|
1027
1062
|
}
|
|
1028
1063
|
fsyncHydratedWorkspaceMounts(
|
|
@@ -1031,7 +1066,8 @@ function restoreHydrationTransaction(workspacePath, manifest) {
|
|
|
1031
1066
|
hydrationIncarnationKey: mount.incarnationKey,
|
|
1032
1067
|
sourceMode: "all",
|
|
1033
1068
|
hydrateDeletionMode: "all"
|
|
1034
|
-
}))
|
|
1069
|
+
})),
|
|
1070
|
+
durabilityScopes
|
|
1035
1071
|
);
|
|
1036
1072
|
}
|
|
1037
1073
|
function recoverPendingHydrationTransaction(workspacePath) {
|
|
@@ -1075,57 +1111,80 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1075
1111
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1076
1112
|
const stagingPath = import_node_path.default.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1077
1113
|
import_node_fs.default.mkdirSync(stagingPath, { mode: 448 });
|
|
1114
|
+
const durabilityScopes = /* @__PURE__ */ new Map();
|
|
1078
1115
|
try {
|
|
1079
1116
|
const snapshotRoot = import_node_path.default.join(stagingPath, "mounts");
|
|
1080
1117
|
import_node_fs.default.mkdirSync(snapshotRoot, { mode: 448 });
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1118
|
+
const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
|
|
1119
|
+
const manifestMounts = mounts.map((mount, index) => {
|
|
1120
|
+
const sourcePath = import_node_path.default.resolve(mount.sourcePath);
|
|
1121
|
+
const durabilityRootPath = import_node_path.default.resolve(mount.durabilityRootPath);
|
|
1122
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1123
|
+
trustedRoot: durabilityRootPath,
|
|
1124
|
+
candidate: sourcePath,
|
|
1125
|
+
label: `Workspace hydration snapshot mount ${mount.id}`
|
|
1126
|
+
});
|
|
1127
|
+
const sourceStatus = lstatIfExists(sourcePath);
|
|
1128
|
+
if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
|
|
1129
|
+
throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
|
|
1130
|
+
}
|
|
1131
|
+
const snapshotName = String(index);
|
|
1132
|
+
const snapshotPath = import_node_path.default.join(snapshotRoot, snapshotName);
|
|
1133
|
+
import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
|
|
1134
|
+
let scope;
|
|
1135
|
+
if (sourceStatus && mount.hydrateDeletionMode === "git") {
|
|
1136
|
+
const outerPath = workspaceMountOuterPath(workspacePath, mount);
|
|
1088
1137
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1089
|
-
trustedRoot:
|
|
1090
|
-
candidate:
|
|
1091
|
-
label: `Workspace
|
|
1138
|
+
trustedRoot: workspacePath,
|
|
1139
|
+
candidate: outerPath,
|
|
1140
|
+
label: `Workspace mount ${mount.id} outer source`
|
|
1092
1141
|
});
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1142
|
+
const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({ sourceRoot: outerPath, targetRoot: sourcePath, sourceMode: "all", deletionMode: "git" });
|
|
1143
|
+
scope = (0, import_working_tree_mirror.captureWorkingTreeMirrorPlan)({
|
|
1144
|
+
plan,
|
|
1145
|
+
targetRoot: sourcePath,
|
|
1146
|
+
snapshotRoot: snapshotPath,
|
|
1147
|
+
// Same private-staging deferral as the complete snapshot below.
|
|
1148
|
+
durability: "deferred_private_staging"
|
|
1149
|
+
});
|
|
1150
|
+
durabilityScopes.set(mount.id, [...plan.desired.keys()]);
|
|
1151
|
+
} else if (sourceStatus) {
|
|
1152
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({
|
|
1153
|
+
sourceRoot: sourcePath,
|
|
1154
|
+
targetRoot: snapshotPath,
|
|
1155
|
+
sourceMode: "all",
|
|
1156
|
+
deletionMode: "all",
|
|
1157
|
+
// This tree is still private and the source authority is untouched.
|
|
1158
|
+
// The single recursive barrier below makes the complete snapshot
|
|
1159
|
+
// durable before its directory entry is published or hydration starts.
|
|
1160
|
+
durability: "deferred_private_staging"
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
id: mount.id,
|
|
1165
|
+
incarnationKey: mount.hydrationIncarnationKey,
|
|
1166
|
+
sourcePath,
|
|
1167
|
+
durabilityRootPath,
|
|
1168
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
1169
|
+
existed: Boolean(sourceStatus),
|
|
1170
|
+
snapshotName,
|
|
1171
|
+
...scope ? { scope } : {}
|
|
1172
|
+
};
|
|
1173
|
+
});
|
|
1174
|
+
const manifest = {
|
|
1175
|
+
// Older workers reject version 4, so a scoped snapshot can never be
|
|
1176
|
+
// replayed as a complete copy after a downgrade.
|
|
1177
|
+
version: manifestMounts.some((mount) => mount.scope) ? 4 : 3,
|
|
1178
|
+
targetReceipt,
|
|
1179
|
+
receiptBefore,
|
|
1180
|
+
mounts: manifestMounts
|
|
1122
1181
|
};
|
|
1123
1182
|
writePrivateFileDurably(import_node_path.default.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
|
|
1124
1183
|
`);
|
|
1125
1184
|
fsyncTree(stagingPath);
|
|
1126
1185
|
import_node_fs.default.renameSync(stagingPath, transactionPath);
|
|
1127
1186
|
fsyncDirectory(stateDirectory);
|
|
1128
|
-
return manifest;
|
|
1187
|
+
return { manifest, durabilityScopes };
|
|
1129
1188
|
} catch (error) {
|
|
1130
1189
|
import_node_fs.default.rmSync(stagingPath, { recursive: true, force: true });
|
|
1131
1190
|
fsyncDirectory(stateDirectory);
|
|
@@ -1137,7 +1196,11 @@ function markHydrationTransactionDurable(workspacePath) {
|
|
|
1137
1196
|
writePrivateFileDurably(import_node_path.default.join(transactionPath, "committed"), "committed\n");
|
|
1138
1197
|
fsyncDirectory(transactionPath);
|
|
1139
1198
|
}
|
|
1140
|
-
function
|
|
1199
|
+
function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
|
|
1200
|
+
if (mount.hydrateDeletionMode !== "git" || !sourceStatus.isDirectory() || !lstatIfExists(import_node_path.default.join(sourcePath, ".git"))) return null;
|
|
1201
|
+
return [...(0, import_working_tree_mirror.inspectWorkingTree)(sourcePath, "git").keys()];
|
|
1202
|
+
}
|
|
1203
|
+
function fsyncHydratedWorkspaceMount(mount, scope) {
|
|
1141
1204
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1142
1205
|
trustedRoot: mount.durabilityRootPath,
|
|
1143
1206
|
candidate: mount.sourcePath,
|
|
@@ -1162,7 +1225,17 @@ function fsyncHydratedWorkspaceMount(mount) {
|
|
|
1162
1225
|
import_node_fs.default.closeSync(descriptor);
|
|
1163
1226
|
}
|
|
1164
1227
|
};
|
|
1165
|
-
|
|
1228
|
+
const sourcePath = import_node_path.default.resolve(mount.sourcePath);
|
|
1229
|
+
const sourceStatus = lstatIfExists(sourcePath);
|
|
1230
|
+
if (sourceStatus) {
|
|
1231
|
+
const paths = scope ?? gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus);
|
|
1232
|
+
if (paths) {
|
|
1233
|
+
if (!sourceStatus.isDirectory()) throw new Error(`Workspace mount ${mount.id} source is not a regular directory: ${sourcePath}`);
|
|
1234
|
+
(0, import_working_tree_mirror.fsyncWorkingTreePaths)(sourcePath, paths);
|
|
1235
|
+
} else {
|
|
1236
|
+
syncEntry(sourcePath, true);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1166
1239
|
const durabilityRoot = import_node_path.default.resolve(mount.durabilityRootPath);
|
|
1167
1240
|
let boundaryParent = import_node_path.default.dirname(import_node_path.default.resolve(mount.sourcePath));
|
|
1168
1241
|
while (true) {
|
|
@@ -1181,8 +1254,8 @@ function fsyncHydratedWorkspaceMount(mount) {
|
|
|
1181
1254
|
boundaryParent = nextParent;
|
|
1182
1255
|
}
|
|
1183
1256
|
}
|
|
1184
|
-
function fsyncHydratedWorkspaceMounts(mounts) {
|
|
1185
|
-
for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
|
|
1257
|
+
function fsyncHydratedWorkspaceMounts(mounts, scopes = /* @__PURE__ */ new Map()) {
|
|
1258
|
+
for (const mount of mounts) fsyncHydratedWorkspaceMount(mount, scopes.get(mount.id));
|
|
1186
1259
|
}
|
|
1187
1260
|
function workspaceIsShallow(workspacePath) {
|
|
1188
1261
|
return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
|
|
@@ -1431,6 +1504,9 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
|
|
|
1431
1504
|
}
|
|
1432
1505
|
}
|
|
1433
1506
|
}
|
|
1507
|
+
function workspaceMountOuterPath(workspacePath, mount) {
|
|
1508
|
+
return import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
1509
|
+
}
|
|
1434
1510
|
function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
1435
1511
|
const hydratedMountIds = [];
|
|
1436
1512
|
const skippedMountIds = [];
|
|
@@ -1439,7 +1515,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1439
1515
|
skippedMountIds.push(mount.id);
|
|
1440
1516
|
continue;
|
|
1441
1517
|
}
|
|
1442
|
-
const sourceRoot =
|
|
1518
|
+
const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
|
|
1443
1519
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1444
1520
|
trustedRoot: workspacePath,
|
|
1445
1521
|
candidate: sourceRoot,
|
|
@@ -1545,14 +1621,14 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1545
1621
|
}
|
|
1546
1622
|
const selection = computeSelection();
|
|
1547
1623
|
const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
|
|
1548
|
-
const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
1624
|
+
const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
1549
1625
|
try {
|
|
1550
1626
|
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
|
|
1551
1627
|
const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
|
|
1552
1628
|
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
1553
1629
|
throw new Error("Workspace hydration did not include every required mount");
|
|
1554
1630
|
}
|
|
1555
|
-
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
|
|
1631
|
+
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
|
|
1556
1632
|
markHydrationTransactionDurable(workspacePath);
|
|
1557
1633
|
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1558
1634
|
removeHydrationTransaction(workspacePath);
|
package/dist/mjs/package.json
CHANGED
|
@@ -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);
|
|
@@ -20,7 +20,14 @@ export type TreeEntry = {
|
|
|
20
20
|
* every portable alias must be remediated before the old tree is retired.
|
|
21
21
|
*/
|
|
22
22
|
export declare function assertWorkingTreeHasNoPortableGitMetadataAliases(root: string): void;
|
|
23
|
-
export declare function inspectWorkingTree(root: string, mode: WorkingTreeSourceMode
|
|
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>;
|
|
24
31
|
export declare function mirrorWorkingTree(input: {
|
|
25
32
|
sourceRoot: string;
|
|
26
33
|
targetRoot: string;
|
|
@@ -35,3 +42,75 @@ export declare function mirrorWorkingTree(input: {
|
|
|
35
42
|
}): {
|
|
36
43
|
paths: string[];
|
|
37
44
|
};
|
|
45
|
+
/** Canonical relative form used by mirror plans and scopes, or null when the value is unsafe. */
|
|
46
|
+
export declare function normalizeWorkingTreeRelativePath(value: string): string | null;
|
|
47
|
+
export type WorkingTreeMirrorPlan = {
|
|
48
|
+
/** Every target path the mirror creates, rewrites, or verifies, ancestor directories included. */
|
|
49
|
+
desired: Map<string, TreeEntry>;
|
|
50
|
+
/**
|
|
51
|
+
* Target entries the mirror removes or overwrites, as they are before it
|
|
52
|
+
* runs. A directory entry means the whole subtree goes, ignored files
|
|
53
|
+
* included; entries beneath it are folded into it.
|
|
54
|
+
*/
|
|
55
|
+
replaced: Map<string, TreeEntry>;
|
|
56
|
+
/** Desired paths that have no target entry yet. */
|
|
57
|
+
absent: string[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Compute exactly which target entries `mirrorWorkingTree` would remove,
|
|
61
|
+
* overwrite, or create for the same input, without changing anything. The
|
|
62
|
+
* removal set is the target's deletion-mode file set minus the desired paths.
|
|
63
|
+
* A desired path replaces its target entry when the kinds differ, a symlink
|
|
64
|
+
* points elsewhere, or a file's bytes or mode differ; an equal entry is left
|
|
65
|
+
* 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.
|
|
67
|
+
*/
|
|
68
|
+
export declare function planWorkingTreeMirror(input: {
|
|
69
|
+
sourceRoot: string;
|
|
70
|
+
targetRoot: string;
|
|
71
|
+
sourceMode: WorkingTreeSourceMode;
|
|
72
|
+
deletionMode: WorkingTreeDeletionMode;
|
|
73
|
+
}): WorkingTreeMirrorPlan;
|
|
74
|
+
export type WorkingTreeMirrorScope = {
|
|
75
|
+
/** Desired paths that did not exist before the mirror ran; a restore removes them. */
|
|
76
|
+
absent: string[];
|
|
77
|
+
/** Directories captured whole; a restore makes them identical to the snapshot again. */
|
|
78
|
+
trees: string[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Copy the entries a mirror plan replaces into `snapshotRoot` at their
|
|
82
|
+
* target-relative paths, so that `restoreWorkingTreeMirrorScope` can undo the
|
|
83
|
+
* mirror. Files and symlinks are copied individually and a replaced directory
|
|
84
|
+
* is copied whole; ancestor directories appear only as containers. Returns
|
|
85
|
+
* the scope that must be kept beside the snapshot.
|
|
86
|
+
*/
|
|
87
|
+
export declare function captureWorkingTreeMirrorPlan(input: {
|
|
88
|
+
plan: WorkingTreeMirrorPlan;
|
|
89
|
+
targetRoot: string;
|
|
90
|
+
snapshotRoot: string;
|
|
91
|
+
durability?: WorkingTreeMirrorDurability;
|
|
92
|
+
}): WorkingTreeMirrorScope;
|
|
93
|
+
/**
|
|
94
|
+
* Undo a mirror whose plan was captured with `captureWorkingTreeMirrorPlan`:
|
|
95
|
+
* remove everything the mirror may have created, sweep its interrupted
|
|
96
|
+
* temporary files, and put every captured entry back. Entries the plan did
|
|
97
|
+
* not cover (a Git target's ignored files, for instance) are never read or
|
|
98
|
+
* written. Safe to repeat after a partial run. Returns the target-relative
|
|
99
|
+
* paths it removed or restored, for the caller's durability barrier.
|
|
100
|
+
*/
|
|
101
|
+
export declare function restoreWorkingTreeMirrorScope(input: {
|
|
102
|
+
snapshotRoot: string;
|
|
103
|
+
targetRoot: string;
|
|
104
|
+
scope: WorkingTreeMirrorScope;
|
|
105
|
+
}): {
|
|
106
|
+
paths: string[];
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Make the listed target-relative entries durable together with the directory
|
|
110
|
+
* entries that name them: every listed regular file and real directory is
|
|
111
|
+
* fsynced, and so is each listed path's parent directory and the root.
|
|
112
|
+
* Symlinks and missing paths contribute only their parent, which is what
|
|
113
|
+
* records their creation or removal. Ancestors are never followed through
|
|
114
|
+
* symlinks. Nothing outside the listed paths is read.
|
|
115
|
+
*/
|
|
116
|
+
export declare function fsyncWorkingTreePaths(root: string, relativePaths: Iterable<string>): void;
|