@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
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);
|
|
@@ -206,6 +206,14 @@ function indexMode(entry) {
|
|
|
206
206
|
function indexInfoRecord(mode, objectId, relativePath) {
|
|
207
207
|
return Buffer.concat([Buffer.from(`${mode} ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]);
|
|
208
208
|
}
|
|
209
|
+
function basisGitlinks(workspacePath, basis) {
|
|
210
|
+
const listed = gitResult(workspacePath, ["ls-tree", "-r", "-z", `${basis.basisHead}:${basis.workspaceRelativePath}`]);
|
|
211
|
+
if (listed.exitCode !== 0) return [];
|
|
212
|
+
return nulRecords(listed.stdout).flatMap((record) => {
|
|
213
|
+
const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record.toString());
|
|
214
|
+
return match ? [{ objectId: match[1], relativePath: match[2] }] : [];
|
|
215
|
+
});
|
|
216
|
+
}
|
|
209
217
|
function synthesizeMountTree(input) {
|
|
210
218
|
const sourceRoot = import_node_path.default.resolve(input.sourcePath);
|
|
211
219
|
const entries = (0, import_working_tree_mirror.inspectWorkingTree)(sourceRoot, input.sourceMode);
|
|
@@ -220,6 +228,15 @@ function synthesizeMountTree(input) {
|
|
|
220
228
|
const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
|
|
221
229
|
records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
|
|
222
230
|
}
|
|
231
|
+
if (input.gitlinkBasis && input.sourceMode === "git") {
|
|
232
|
+
for (const gitlink of basisGitlinks(input.workspacePath, input.gitlinkBasis)) {
|
|
233
|
+
const populatedPrefix = `${gitlink.relativePath}/`;
|
|
234
|
+
const populated = [...entries.keys()].some((relativePath) => relativePath.startsWith(populatedPrefix));
|
|
235
|
+
if (entries.get(gitlink.relativePath)?.kind === "directory" && !populated) {
|
|
236
|
+
records.push(indexInfoRecord("160000", gitlink.objectId, gitlink.relativePath));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
223
240
|
if (records.length > 0) {
|
|
224
241
|
git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
|
|
225
242
|
environment,
|
|
@@ -284,7 +301,8 @@ function synthesizeOursCommit(input) {
|
|
|
284
301
|
const mountTree = synthesizeMountTree({
|
|
285
302
|
workspacePath: input.workspacePath,
|
|
286
303
|
sourcePath: input.mount.sourcePath,
|
|
287
|
-
sourceMode: input.mount.sourceMode
|
|
304
|
+
sourceMode: input.mount.sourceMode,
|
|
305
|
+
gitlinkBasis: { basisHead: input.basisHead, workspaceRelativePath: input.workspaceRelativePath }
|
|
288
306
|
});
|
|
289
307
|
const rootTree = graftMountTree({
|
|
290
308
|
workspacePath: input.workspacePath,
|
package/dist/mjs/package.json
CHANGED