@ricsam/r5d-worker 0.0.97 → 0.0.100
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/main.cjs +37 -5
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/working-tree-mirror.cjs +256 -4
- package/dist/cjs/workspace-git-sync.cjs +179 -87
- package/dist/mjs/main.mjs +37 -5
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/working-tree-mirror.mjs +250 -3
- package/dist/mjs/workspace-git-sync.mjs +188 -88
- package/dist/types/main.d.ts +181 -0
- package/dist/types/working-tree-mirror.d.ts +80 -1
- package/dist/types/workspace-git-sync.d.ts +25 -0
- package/package.json +1 -1
|
@@ -151,14 +151,55 @@ function addParentDirectories(entries, root) {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
function
|
|
154
|
+
function gitlinkPaths(root) {
|
|
155
|
+
const result = Bun.spawnSync(
|
|
156
|
+
[
|
|
157
|
+
"git",
|
|
158
|
+
"-c",
|
|
159
|
+
"submodule.recurse=false",
|
|
160
|
+
"-c",
|
|
161
|
+
"fetch.recurseSubmodules=false",
|
|
162
|
+
"-c",
|
|
163
|
+
"push.recurseSubmodules=false",
|
|
164
|
+
"ls-files",
|
|
165
|
+
"-z",
|
|
166
|
+
"--stage",
|
|
167
|
+
"--",
|
|
168
|
+
"."
|
|
169
|
+
],
|
|
170
|
+
{
|
|
171
|
+
cwd: root,
|
|
172
|
+
stdout: "pipe",
|
|
173
|
+
stderr: "pipe",
|
|
174
|
+
env: workerGitProcessEnvironment()
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
if (result.exitCode !== 0) {
|
|
178
|
+
throw new Error(`Inspect Git index gitlinks: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
179
|
+
}
|
|
180
|
+
const gitlinks = /* @__PURE__ */ new Set();
|
|
181
|
+
for (const record of result.stdout.toString().split("\0")) {
|
|
182
|
+
const match = /^160000 [0-9a-f]+ [0-3]\t(.+)$/su.exec(record);
|
|
183
|
+
if (!match) continue;
|
|
184
|
+
const normalized = normalizeRelativePath(match[1]);
|
|
185
|
+
if (normalized) gitlinks.add(normalized);
|
|
186
|
+
}
|
|
187
|
+
return gitlinks;
|
|
188
|
+
}
|
|
189
|
+
function directoryIsEmpty(root, relativePath) {
|
|
190
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
|
|
191
|
+
return inspected.kind === "entry" && inspected.stat.isDirectory() && fs.readdirSync(inspected.absolutePath).length === 0;
|
|
192
|
+
}
|
|
193
|
+
function inspectWorkingTree(root, mode, options = {}) {
|
|
155
194
|
const entries = /* @__PURE__ */ new Map();
|
|
156
195
|
root = path.resolve(root);
|
|
157
196
|
if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
|
|
158
197
|
if (mode === "all") {
|
|
159
198
|
for (const child of fs.readdirSync(root).sort()) addEntry(entries, root, child, true);
|
|
160
199
|
} else {
|
|
200
|
+
const omittedGitlinks = options.omitUnpopulatedGitlinks ? gitlinkPaths(root) : null;
|
|
161
201
|
for (const relativePath of gitEligibleRoots(root)) {
|
|
202
|
+
if (omittedGitlinks?.has(relativePath) && directoryIsEmpty(root, relativePath)) continue;
|
|
162
203
|
addEntry(entries, root, relativePath, true);
|
|
163
204
|
}
|
|
164
205
|
}
|
|
@@ -332,7 +373,9 @@ function mirrorWorkingTree(input) {
|
|
|
332
373
|
}
|
|
333
374
|
inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
|
|
334
375
|
const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
335
|
-
const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all"
|
|
376
|
+
const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all", {
|
|
377
|
+
omitUnpopulatedGitlinks: input.deletionMode === "git"
|
|
378
|
+
});
|
|
336
379
|
for (const relativePath of [...existing.keys()].sort((left, right) => right.split("/").length - left.split("/").length)) {
|
|
337
380
|
if (!desired.has(relativePath)) removeEntry(targetRoot, relativePath);
|
|
338
381
|
}
|
|
@@ -343,8 +386,212 @@ function mirrorWorkingTree(input) {
|
|
|
343
386
|
}
|
|
344
387
|
return { paths: [...desired.keys()].filter((relativePath) => desired.get(relativePath)?.kind !== "directory").sort() };
|
|
345
388
|
}
|
|
389
|
+
function normalizeWorkingTreeRelativePath(value) {
|
|
390
|
+
return normalizeRelativePath(value);
|
|
391
|
+
}
|
|
392
|
+
const MIRROR_TEMPORARY_FILE_PATTERN = /^\.r5d-working-tree-mirror-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/u;
|
|
393
|
+
function relativeDepth(relativePath) {
|
|
394
|
+
return relativePath.split("/").length;
|
|
395
|
+
}
|
|
396
|
+
function shallowestFirst(left, right) {
|
|
397
|
+
return relativeDepth(left) - relativeDepth(right) || left.localeCompare(right);
|
|
398
|
+
}
|
|
399
|
+
function deepestFirst(left, right) {
|
|
400
|
+
return relativeDepth(right) - relativeDepth(left) || left.localeCompare(right);
|
|
401
|
+
}
|
|
402
|
+
function hasAncestorIn(relativePath, ancestors) {
|
|
403
|
+
let parent = path.posix.dirname(relativePath);
|
|
404
|
+
while (parent !== ".") {
|
|
405
|
+
if (ancestors.has(parent)) return true;
|
|
406
|
+
parent = path.posix.dirname(parent);
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
function treeEntryFromStat(stat, absolutePath) {
|
|
411
|
+
if (stat.isSymbolicLink()) return { kind: "symlink", mode: stat.mode, target: fs.readlinkSync(absolutePath) };
|
|
412
|
+
if (stat.isFile()) return { kind: "file", mode: stat.mode, size: stat.size };
|
|
413
|
+
if (stat.isDirectory()) return { kind: "directory", mode: stat.mode };
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function fsyncPath(targetPath, flags) {
|
|
417
|
+
const descriptor = fs.openSync(targetPath, flags);
|
|
418
|
+
try {
|
|
419
|
+
fs.fsyncSync(descriptor);
|
|
420
|
+
} finally {
|
|
421
|
+
fs.closeSync(descriptor);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function planWorkingTreeMirror(input) {
|
|
425
|
+
const sourceRoot = path.resolve(input.sourceRoot);
|
|
426
|
+
const targetRoot = path.resolve(input.targetRoot);
|
|
427
|
+
const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
428
|
+
const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all", {
|
|
429
|
+
omitUnpopulatedGitlinks: input.deletionMode === "git"
|
|
430
|
+
});
|
|
431
|
+
const replaced = /* @__PURE__ */ new Map();
|
|
432
|
+
const absent = [];
|
|
433
|
+
for (const [relativePath, entry] of existing) {
|
|
434
|
+
if (!desired.has(relativePath)) replaced.set(relativePath, entry);
|
|
435
|
+
}
|
|
436
|
+
for (const [relativePath, entry] of desired) {
|
|
437
|
+
const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
438
|
+
if (target.kind !== "entry") {
|
|
439
|
+
absent.push(relativePath);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const { absolutePath, stat } = target;
|
|
443
|
+
if (entry.kind === "directory") {
|
|
444
|
+
if (stat.isDirectory()) continue;
|
|
445
|
+
} else if (entry.kind === "symlink") {
|
|
446
|
+
if (stat.isSymbolicLink() && fs.readlinkSync(absolutePath) === entry.target) continue;
|
|
447
|
+
} else if (stat.isFile() && filesEqual(path.join(sourceRoot, ...relativePath.split("/")), absolutePath, entry)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const current = treeEntryFromStat(stat, absolutePath);
|
|
451
|
+
if (current) replaced.set(relativePath, current);
|
|
452
|
+
else absent.push(relativePath);
|
|
453
|
+
}
|
|
454
|
+
const replacedDirectories = new Set(
|
|
455
|
+
[...replaced].filter(([, entry]) => entry.kind === "directory").map(([relativePath]) => relativePath)
|
|
456
|
+
);
|
|
457
|
+
for (const relativePath of [...replaced.keys()]) {
|
|
458
|
+
if (hasAncestorIn(relativePath, replacedDirectories)) replaced.delete(relativePath);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
desired,
|
|
462
|
+
replaced,
|
|
463
|
+
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst)
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function captureWorkingTreeMirrorPlan(input) {
|
|
467
|
+
const targetRoot = path.resolve(input.targetRoot);
|
|
468
|
+
const snapshotRoot = path.resolve(input.snapshotRoot);
|
|
469
|
+
const durability = input.durability ?? "per_entry";
|
|
470
|
+
inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
|
|
471
|
+
if (!inspectDirectoryRoot(snapshotRoot, { allowMissing: true, label: "Working-tree snapshot root" })) {
|
|
472
|
+
fs.mkdirSync(snapshotRoot, { recursive: true });
|
|
473
|
+
}
|
|
474
|
+
const trees = [];
|
|
475
|
+
for (const [relativePath, entry] of [...input.plan.replaced.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
476
|
+
if (entry.kind !== "directory") {
|
|
477
|
+
copyEntry(targetRoot, snapshotRoot, relativePath, entry, durability);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const source = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
481
|
+
if (source.kind !== "entry" || !source.stat.isDirectory()) {
|
|
482
|
+
throw new Error(`Working-tree source changed while it was being mirrored: ${path.join(targetRoot, ...relativePath.split("/"))}`);
|
|
483
|
+
}
|
|
484
|
+
ensureDirectory(snapshotRoot, relativePath, entry.mode);
|
|
485
|
+
mirrorWorkingTree({
|
|
486
|
+
sourceRoot: source.absolutePath,
|
|
487
|
+
targetRoot: path.join(snapshotRoot, ...relativePath.split("/")),
|
|
488
|
+
sourceMode: "all",
|
|
489
|
+
deletionMode: "all",
|
|
490
|
+
durability
|
|
491
|
+
});
|
|
492
|
+
trees.push(relativePath);
|
|
493
|
+
}
|
|
494
|
+
return { absent: [...input.plan.absent].sort(shallowestFirst), trees: trees.sort(shallowestFirst) };
|
|
495
|
+
}
|
|
496
|
+
function restoreWorkingTreeMirrorScope(input) {
|
|
497
|
+
const snapshotRoot = path.resolve(input.snapshotRoot);
|
|
498
|
+
const targetRoot = path.resolve(input.targetRoot);
|
|
499
|
+
inspectDirectoryRoot(snapshotRoot, { allowMissing: false, label: "Working-tree snapshot root" });
|
|
500
|
+
if (!inspectDirectoryRoot(targetRoot, { allowMissing: true, label: "Working-tree target root" })) {
|
|
501
|
+
fs.mkdirSync(targetRoot, { recursive: true });
|
|
502
|
+
}
|
|
503
|
+
const normalize = (relativePath) => {
|
|
504
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
505
|
+
if (!normalized) throw new Error(`Invalid working-tree scope path: ${relativePath}`);
|
|
506
|
+
return normalized;
|
|
507
|
+
};
|
|
508
|
+
const absent = input.scope.absent.map(normalize);
|
|
509
|
+
const trees = new Set(input.scope.trees.map(normalize));
|
|
510
|
+
const captured = inspectWorkingTree(snapshotRoot, "all");
|
|
511
|
+
for (const tree of trees) {
|
|
512
|
+
if (captured.get(tree)?.kind !== "directory") {
|
|
513
|
+
throw new Error(`Working-tree snapshot does not hold the captured directory: ${tree}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const touched = /* @__PURE__ */ new Set();
|
|
517
|
+
for (const relativePath of [...absent].sort(deepestFirst)) {
|
|
518
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
519
|
+
if (inspected.kind === "entry") fs.rmSync(inspected.absolutePath, { recursive: true, force: true });
|
|
520
|
+
touched.add(relativePath);
|
|
521
|
+
}
|
|
522
|
+
const writtenDirectories = /* @__PURE__ */ new Set();
|
|
523
|
+
for (const relativePath of absent) writtenDirectories.add(path.posix.dirname(relativePath));
|
|
524
|
+
for (const [relativePath, entry] of captured) {
|
|
525
|
+
if (entry.kind !== "directory" && !hasAncestorIn(relativePath, trees)) writtenDirectories.add(path.posix.dirname(relativePath));
|
|
526
|
+
}
|
|
527
|
+
for (const directory of writtenDirectories) {
|
|
528
|
+
let absoluteDirectory = targetRoot;
|
|
529
|
+
if (directory !== ".") {
|
|
530
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, directory);
|
|
531
|
+
if (inspected.kind !== "entry" || !inspected.stat.isDirectory()) continue;
|
|
532
|
+
absoluteDirectory = inspected.absolutePath;
|
|
533
|
+
}
|
|
534
|
+
for (const name of fs.readdirSync(absoluteDirectory)) {
|
|
535
|
+
if (!MIRROR_TEMPORARY_FILE_PATTERN.test(name)) continue;
|
|
536
|
+
const temporaryPath = path.join(absoluteDirectory, name);
|
|
537
|
+
if (!fs.lstatSync(temporaryPath).isFile()) continue;
|
|
538
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
539
|
+
touched.add(directory === "." ? name : path.posix.join(directory, name));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
for (const [relativePath, entry] of [...captured.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
543
|
+
touched.add(relativePath);
|
|
544
|
+
if (hasAncestorIn(relativePath, trees)) continue;
|
|
545
|
+
if (!trees.has(relativePath)) {
|
|
546
|
+
copyEntry(snapshotRoot, targetRoot, relativePath, entry, "per_entry");
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
550
|
+
if (current.kind === "blocked") {
|
|
551
|
+
throw new Error(`Refusing to restore a working-tree directory through a symlink or non-directory: ${current.blockingPath}`);
|
|
552
|
+
}
|
|
553
|
+
if (current.kind === "entry" && !current.stat.isDirectory()) {
|
|
554
|
+
fs.rmSync(current.absolutePath, { recursive: true, force: true });
|
|
555
|
+
}
|
|
556
|
+
ensureDirectory(targetRoot, relativePath, entry.mode);
|
|
557
|
+
mirrorWorkingTree({
|
|
558
|
+
sourceRoot: path.join(snapshotRoot, ...relativePath.split("/")),
|
|
559
|
+
targetRoot: path.join(targetRoot, ...relativePath.split("/")),
|
|
560
|
+
sourceMode: "all",
|
|
561
|
+
deletionMode: "all"
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return { paths: [...touched].sort(shallowestFirst) };
|
|
565
|
+
}
|
|
566
|
+
function fsyncWorkingTreePaths(root, relativePaths) {
|
|
567
|
+
root = path.resolve(root);
|
|
568
|
+
inspectDirectoryRoot(root, { allowMissing: false, label: "Working-tree root" });
|
|
569
|
+
const directories = /* @__PURE__ */ new Set(["."]);
|
|
570
|
+
for (const relativePath of relativePaths) {
|
|
571
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
572
|
+
if (!normalized) throw new Error(`Invalid working-tree path: ${relativePath}`);
|
|
573
|
+
directories.add(path.posix.dirname(normalized));
|
|
574
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, normalized);
|
|
575
|
+
if (inspected.kind !== "entry") continue;
|
|
576
|
+
if (inspected.stat.isDirectory()) directories.add(normalized);
|
|
577
|
+
else if (inspected.stat.isFile()) fsyncPath(inspected.absolutePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
578
|
+
}
|
|
579
|
+
for (const directory of directories) {
|
|
580
|
+
if (directory === ".") {
|
|
581
|
+
fsyncPath(root, fs.constants.O_RDONLY);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, directory);
|
|
585
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory()) fsyncPath(inspected.absolutePath, fs.constants.O_RDONLY);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
346
588
|
export {
|
|
347
589
|
assertWorkingTreeHasNoPortableGitMetadataAliases,
|
|
590
|
+
captureWorkingTreeMirrorPlan,
|
|
591
|
+
fsyncWorkingTreePaths,
|
|
348
592
|
inspectWorkingTree,
|
|
349
|
-
mirrorWorkingTree
|
|
593
|
+
mirrorWorkingTree,
|
|
594
|
+
normalizeWorkingTreeRelativePath,
|
|
595
|
+
planWorkingTreeMirror,
|
|
596
|
+
restoreWorkingTreeMirrorScope
|
|
350
597
|
};
|