@ricsam/r5d-worker 0.0.55 → 0.0.57
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/README.md +2 -2
- package/dist/cjs/main.cjs +21 -11
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-command-sync-policy.cjs +40 -0
- package/dist/cjs/workspace-sync.cjs +741 -64
- package/dist/mjs/main.mjs +21 -11
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-command-sync-policy.mjs +14 -0
- package/dist/mjs/workspace-sync.mjs +741 -64
- package/dist/types/workspace-command-sync-policy.d.ts +19 -0
- package/dist/types/workspace-mutation-gate.d.ts +3 -1
- package/dist/types/workspace-sync.d.ts +12 -1
- package/package.json +1 -1
|
@@ -64,6 +64,8 @@ const DESTRUCTIVE_CHECKOUT_MIN_TRACKED_FILES = 20;
|
|
|
64
64
|
const DESTRUCTIVE_CHECKOUT_REMOVAL_RATIO = 0.8;
|
|
65
65
|
const ACTIVE_GIT_LOCK_PATHS = ["index.lock", "HEAD.lock", "packed-refs.lock", "shallow.lock"];
|
|
66
66
|
const mirrorComparisonCache = /* @__PURE__ */ new Map();
|
|
67
|
+
const gitObjectFormatCache = /* @__PURE__ */ new Map();
|
|
68
|
+
const hydrationEntryCache = /* @__PURE__ */ new Map();
|
|
67
69
|
function gitArgs(input, args) {
|
|
68
70
|
const configArgs = ["-c", "submodule.recurse=false", "-c", "fetch.recurseSubmodules=false", "-c", "push.recurseSubmodules=false"];
|
|
69
71
|
return input.authHeader ? ["git", ...configArgs, "-c", `http.extraHeader=${input.authHeader}`, ...args] : ["git", ...configArgs, ...args];
|
|
@@ -268,6 +270,127 @@ function lstatOrNull(filePath) {
|
|
|
268
270
|
throw error;
|
|
269
271
|
}
|
|
270
272
|
}
|
|
273
|
+
const UNSTABLE_VISIBLE_ENTRY = /* @__PURE__ */ Symbol("unstable-visible-entry");
|
|
274
|
+
function hydrationEntriesEqual(left, right) {
|
|
275
|
+
if (!left || !right || left.kind !== right.kind || left.objectId !== right.objectId) return left === right;
|
|
276
|
+
return left.kind === "gitlink" || right.kind === "blob" && left.mode === right.mode;
|
|
277
|
+
}
|
|
278
|
+
function gitObjectFormat(input) {
|
|
279
|
+
const cached = gitObjectFormatCache.get(input.shadowRoot);
|
|
280
|
+
if (cached) return cached;
|
|
281
|
+
const format = runGit(input, input.shadowRoot, ["rev-parse", "--show-object-format"], "inspect workspace object format");
|
|
282
|
+
if (format !== "sha1" && format !== "sha256") throw new Error(`Unsupported Git object format ${JSON.stringify(format)}`);
|
|
283
|
+
gitObjectFormatCache.set(input.shadowRoot, format);
|
|
284
|
+
return format;
|
|
285
|
+
}
|
|
286
|
+
function hashGitBlobBuffer(input, content) {
|
|
287
|
+
return (0, import_node_crypto.createHash)(gitObjectFormat(input)).update(`blob ${content.byteLength}\0`).update(content).digest("hex");
|
|
288
|
+
}
|
|
289
|
+
function hashGitBlobFile(input, filePath, size) {
|
|
290
|
+
const hash = (0, import_node_crypto.createHash)(gitObjectFormat(input));
|
|
291
|
+
hash.update(`blob ${size}\0`);
|
|
292
|
+
const file = import_node_fs.default.openSync(filePath, "r");
|
|
293
|
+
const buffer = Buffer.allocUnsafe(Math.min(MIRROR_COMPARE_BUFFER_BYTES, Math.max(1, size)));
|
|
294
|
+
try {
|
|
295
|
+
let offset = 0;
|
|
296
|
+
while (offset < size) {
|
|
297
|
+
const bytesRead = import_node_fs.default.readSync(file, buffer, 0, Math.min(buffer.length, size - offset), offset);
|
|
298
|
+
if (bytesRead === 0) return null;
|
|
299
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
300
|
+
offset += bytesRead;
|
|
301
|
+
}
|
|
302
|
+
return hash.digest("hex");
|
|
303
|
+
} finally {
|
|
304
|
+
import_node_fs.default.closeSync(file);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function filesystemHydrationEntry(input, filePath) {
|
|
308
|
+
const before = lstatOrNull(filePath);
|
|
309
|
+
if (!before) return void 0;
|
|
310
|
+
if (!before.isFile() && !before.isSymbolicLink()) return UNSTABLE_VISIBLE_ENTRY;
|
|
311
|
+
const objectFormat = gitObjectFormat(input);
|
|
312
|
+
const signature = entrySignature(before);
|
|
313
|
+
const cached = hydrationEntryCache.get(filePath);
|
|
314
|
+
if (cached?.signature === signature && cached.objectFormat === objectFormat) return cached.entry;
|
|
315
|
+
let objectId = null;
|
|
316
|
+
if (before.isSymbolicLink()) {
|
|
317
|
+
try {
|
|
318
|
+
objectId = hashGitBlobBuffer(input, Buffer.from(import_node_fs.default.readlinkSync(filePath)));
|
|
319
|
+
} catch {
|
|
320
|
+
return UNSTABLE_VISIBLE_ENTRY;
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
try {
|
|
324
|
+
objectId = hashGitBlobFile(input, filePath, before.size);
|
|
325
|
+
} catch {
|
|
326
|
+
return UNSTABLE_VISIBLE_ENTRY;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const after = lstatOrNull(filePath);
|
|
330
|
+
if (!objectId || !after || entrySignature(before) !== entrySignature(after)) return UNSTABLE_VISIBLE_ENTRY;
|
|
331
|
+
const entry = {
|
|
332
|
+
kind: "blob",
|
|
333
|
+
mode: before.isSymbolicLink() ? "120000" : (before.mode & 73) !== 0 ? "100755" : "100644",
|
|
334
|
+
objectId
|
|
335
|
+
};
|
|
336
|
+
if (hydrationEntryCache.size >= MAX_MIRROR_COMPARISON_CACHE_ENTRIES) hydrationEntryCache.clear();
|
|
337
|
+
hydrationEntryCache.set(filePath, { signature, objectFormat, entry });
|
|
338
|
+
return entry;
|
|
339
|
+
}
|
|
340
|
+
function projectVisibleHydrationEntry(input, visibleRoot, filePath, gitlinks) {
|
|
341
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot)) return UNSTABLE_VISIBLE_ENTRY;
|
|
342
|
+
const gitlink = gitlinks.get(filePath);
|
|
343
|
+
if (gitlink) {
|
|
344
|
+
const target = lstatOrNull(import_node_path.default.join(visibleRoot, ...filePath.split("/")));
|
|
345
|
+
if (target && !target.isDirectory()) return UNSTABLE_VISIBLE_ENTRY;
|
|
346
|
+
return { kind: "gitlink", objectId: gitlink.objectId };
|
|
347
|
+
}
|
|
348
|
+
return filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/")));
|
|
349
|
+
}
|
|
350
|
+
function revisionHydrationPreimage(input, revision, relativeRoot) {
|
|
351
|
+
const result = runGitResult(input, input.shadowRoot, ["--literal-pathspecs", "ls-tree", "-r", "-z", revision, "--", relativeRoot]);
|
|
352
|
+
if (result.exitCode !== 0) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Inspect workspace hydration baseline ${revision}:${relativeRoot}: ${result.stderr || `git exited ${result.exitCode}`}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (!result.stdout) return /* @__PURE__ */ new Map();
|
|
358
|
+
const prefix = `${relativeRoot}/`;
|
|
359
|
+
const preimage = /* @__PURE__ */ new Map();
|
|
360
|
+
for (const serialized of result.stdout.split("\0").filter(Boolean)) {
|
|
361
|
+
const separator = serialized.indexOf(" ");
|
|
362
|
+
if (separator === -1) continue;
|
|
363
|
+
const [mode, type, objectId] = serialized.slice(0, separator).split(" ");
|
|
364
|
+
const workspacePath = serialized.slice(separator + 1);
|
|
365
|
+
if (!objectId || !workspacePath.startsWith(prefix)) continue;
|
|
366
|
+
const filePath = workspacePath.slice(prefix.length);
|
|
367
|
+
if (mode === "160000" && type === "commit") {
|
|
368
|
+
preimage.set(filePath, { kind: "gitlink", objectId });
|
|
369
|
+
} else if ((mode === "100644" || mode === "100755" || mode === "120000") && type === "blob") {
|
|
370
|
+
preimage.set(filePath, { kind: "blob", mode, objectId });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return preimage;
|
|
374
|
+
}
|
|
375
|
+
function indexHydrationPreimage(input, relativeRoot) {
|
|
376
|
+
const prefix = `${relativeRoot}/`;
|
|
377
|
+
const entries = parseGitIndexEntries(
|
|
378
|
+
runGit(input, input.shadowRoot, ["--literal-pathspecs", "ls-files", "--stage", "-z", "--", relativeRoot], "inspect hydrated workspace")
|
|
379
|
+
);
|
|
380
|
+
const unmergedPaths = entries.filter((entry) => entry.stage !== "0").map((entry) => entry.filePath);
|
|
381
|
+
if (unmergedPaths.length > 0) throw new Error(`Canonical workspace has unmerged Git index stages: ${JSON.stringify(unmergedPaths)}`);
|
|
382
|
+
const preimage = /* @__PURE__ */ new Map();
|
|
383
|
+
for (const entry of entries) {
|
|
384
|
+
if (!entry.filePath.startsWith(prefix)) continue;
|
|
385
|
+
const filePath = entry.filePath.slice(prefix.length);
|
|
386
|
+
if (entry.mode === "160000") {
|
|
387
|
+
preimage.set(filePath, { kind: "gitlink", objectId: entry.objectId });
|
|
388
|
+
} else if (entry.mode === "100644" || entry.mode === "100755" || entry.mode === "120000") {
|
|
389
|
+
preimage.set(filePath, { kind: "blob", mode: entry.mode, objectId: entry.objectId });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return preimage;
|
|
393
|
+
}
|
|
271
394
|
function worktreeDiffersFromIndex(checkoutPath, filePath) {
|
|
272
395
|
const result = Bun.spawnSync(
|
|
273
396
|
["git", "--literal-pathspecs", "-c", "core.fileMode=true", "diff", "--quiet", "--no-ext-diff", "--", filePath],
|
|
@@ -422,11 +545,17 @@ function removeEmptyDirectories(root, opaqueRoots = []) {
|
|
|
422
545
|
};
|
|
423
546
|
visit(root, "");
|
|
424
547
|
}
|
|
425
|
-
function copyEntry(sourceRoot, targetRoot, relativePath) {
|
|
548
|
+
function copyEntry(sourceRoot, targetRoot, relativePath, trustedRoots = {}) {
|
|
426
549
|
const sourcePath = import_node_path.default.resolve(sourceRoot, ...relativePath.split("/"));
|
|
427
550
|
const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
|
|
428
551
|
assertInside(sourceRoot, sourcePath, "Workspace source path");
|
|
429
552
|
assertInside(targetRoot, targetPath, "Workspace target path");
|
|
553
|
+
if (!hasSafeDirectoryAncestors(sourceRoot, relativePath, trustedRoots.source ?? sourceRoot)) {
|
|
554
|
+
throw new Error(`Workspace source path has a symlink or non-directory ancestor: ${relativePath}`);
|
|
555
|
+
}
|
|
556
|
+
if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
|
|
557
|
+
throw new Error(`Workspace target path has a symlink or non-directory ancestor: ${relativePath}`);
|
|
558
|
+
}
|
|
430
559
|
const stat = import_node_fs.default.lstatSync(sourcePath);
|
|
431
560
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetPath), { recursive: true });
|
|
432
561
|
import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
|
|
@@ -439,6 +568,22 @@ function copyEntry(sourceRoot, targetRoot, relativePath) {
|
|
|
439
568
|
import_node_fs.default.chmodSync(targetPath, stat.mode);
|
|
440
569
|
}
|
|
441
570
|
}
|
|
571
|
+
function hasSafeDirectoryAncestors(root, relativePath, trustedRoot = root) {
|
|
572
|
+
const resolvedTrustedRoot = import_node_path.default.resolve(trustedRoot);
|
|
573
|
+
const trustedRootStat = lstatOrNull(resolvedTrustedRoot);
|
|
574
|
+
if (trustedRootStat && (!trustedRootStat.isDirectory() || trustedRootStat.isSymbolicLink())) return false;
|
|
575
|
+
const resolvedRoot = import_node_path.default.resolve(root);
|
|
576
|
+
const targetParent = import_node_path.default.dirname(import_node_path.default.resolve(resolvedRoot, ...relativePath.split("/")));
|
|
577
|
+
const relativeParent = import_node_path.default.relative(resolvedTrustedRoot, targetParent);
|
|
578
|
+
if (relativeParent === ".." || relativeParent.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relativeParent)) return false;
|
|
579
|
+
let current = resolvedTrustedRoot;
|
|
580
|
+
for (const segment of relativeParent.split(import_node_path.default.sep).filter(Boolean)) {
|
|
581
|
+
current = import_node_path.default.join(current, segment);
|
|
582
|
+
const stat = lstatOrNull(current);
|
|
583
|
+
if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) return false;
|
|
584
|
+
}
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
442
587
|
function entrySignature(stat) {
|
|
443
588
|
return [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
|
|
444
589
|
}
|
|
@@ -490,7 +635,10 @@ function entriesEqual(sourcePath, targetPath) {
|
|
|
490
635
|
}
|
|
491
636
|
return equal;
|
|
492
637
|
}
|
|
493
|
-
function mirrorFileSet(sourceRoot, targetRoot, sourceFiles, targetFiles, opaqueTargetRoots = [], opaqueSourceRoots = []) {
|
|
638
|
+
function mirrorFileSet(sourceRoot, targetRoot, sourceFiles, targetFiles, opaqueTargetRoots = [], opaqueSourceRoots = [], trustedRoots = {}) {
|
|
639
|
+
if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-mirror-root", trustedRoots.target ?? targetRoot)) {
|
|
640
|
+
throw new Error(`Workspace target root has a symlink or non-directory ancestor: ${targetRoot}`);
|
|
641
|
+
}
|
|
494
642
|
import_node_fs.default.mkdirSync(targetRoot, { recursive: true });
|
|
495
643
|
const sourceSet = new Set(sourceFiles);
|
|
496
644
|
for (const relativePath of targetFiles) {
|
|
@@ -498,18 +646,27 @@ function mirrorFileSet(sourceRoot, targetRoot, sourceFiles, targetFiles, opaqueT
|
|
|
498
646
|
if (sourceSet.has(relativePath)) continue;
|
|
499
647
|
const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
|
|
500
648
|
assertInside(targetRoot, targetPath, "Workspace deletion path");
|
|
649
|
+
if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
|
|
650
|
+
throw new Error(`Workspace deletion path has a symlink or non-directory ancestor: ${relativePath}`);
|
|
651
|
+
}
|
|
501
652
|
import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
|
|
502
653
|
}
|
|
503
654
|
for (const relativePath of sourceFiles) {
|
|
504
655
|
if (pathIsWithinOpaqueRoot(relativePath, opaqueSourceRoots)) continue;
|
|
505
656
|
if (pathIsWithinOpaqueRoot(relativePath, opaqueTargetRoots)) continue;
|
|
657
|
+
if (!hasSafeDirectoryAncestors(sourceRoot, relativePath, trustedRoots.source ?? sourceRoot)) {
|
|
658
|
+
throw new Error(`Workspace source path has a symlink or non-directory ancestor: ${relativePath}`);
|
|
659
|
+
}
|
|
660
|
+
if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
|
|
661
|
+
throw new Error(`Workspace target path has a symlink or non-directory ancestor: ${relativePath}`);
|
|
662
|
+
}
|
|
506
663
|
const sourcePath = import_node_path.default.resolve(sourceRoot, ...relativePath.split("/"));
|
|
507
664
|
const stat = lstatOrNull(sourcePath);
|
|
508
665
|
if (!stat) continue;
|
|
509
666
|
if (!stat.isFile() && !stat.isSymbolicLink()) continue;
|
|
510
667
|
const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
|
|
511
668
|
if (entriesEqual(sourcePath, targetPath)) continue;
|
|
512
|
-
copyEntry(sourceRoot, targetRoot, relativePath);
|
|
669
|
+
copyEntry(sourceRoot, targetRoot, relativePath, trustedRoots);
|
|
513
670
|
}
|
|
514
671
|
removeEmptyDirectories(targetRoot, opaqueTargetRoots);
|
|
515
672
|
}
|
|
@@ -521,18 +678,37 @@ function indexPathsUnderCheckoutPath(checkoutPath, filePath) {
|
|
|
521
678
|
).split("\0").filter(Boolean);
|
|
522
679
|
}
|
|
523
680
|
function replaceCheckoutIndexPathWithGitlink(checkoutPath, gitlink) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
681
|
+
const zeroObjectId = "0".repeat(gitlink.objectId.length);
|
|
682
|
+
const records = [
|
|
683
|
+
...indexPathsUnderCheckoutPath(checkoutPath, gitlink.filePath).map((indexPath) => `0 ${zeroObjectId} ${indexPath}`),
|
|
684
|
+
`160000 ${gitlink.objectId} ${gitlink.filePath}`
|
|
685
|
+
];
|
|
686
|
+
const result = Bun.spawnSync(
|
|
687
|
+
[
|
|
688
|
+
"git",
|
|
689
|
+
"-c",
|
|
690
|
+
"submodule.recurse=false",
|
|
691
|
+
"-c",
|
|
692
|
+
"fetch.recurseSubmodules=false",
|
|
693
|
+
"-c",
|
|
694
|
+
"push.recurseSubmodules=false",
|
|
695
|
+
"update-index",
|
|
696
|
+
"-z",
|
|
697
|
+
"--index-info"
|
|
698
|
+
],
|
|
699
|
+
{
|
|
700
|
+
cwd: checkoutPath,
|
|
701
|
+
stdin: Buffer.from(`${records.join("\0")}\0`),
|
|
702
|
+
stdout: "pipe",
|
|
703
|
+
stderr: "pipe",
|
|
704
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
705
|
+
}
|
|
706
|
+
);
|
|
707
|
+
if (result.exitCode !== 0) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`record gitlink ${JSON.stringify(gitlink.filePath)}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`
|
|
529
710
|
);
|
|
530
711
|
}
|
|
531
|
-
runCheckoutGitRaw(
|
|
532
|
-
checkoutPath,
|
|
533
|
-
["--literal-pathspecs", "update-index", "--add", "--cacheinfo", "160000", gitlink.objectId, gitlink.filePath],
|
|
534
|
-
`record gitlink ${JSON.stringify(gitlink.filePath)}`
|
|
535
|
-
);
|
|
536
712
|
}
|
|
537
713
|
function removeCheckoutIndexPath(checkoutPath, filePath) {
|
|
538
714
|
runCheckoutGitRaw(
|
|
@@ -593,12 +769,15 @@ function mirrorVisibleGitlinksToShadow(input, relativeRoot, visibleGitlinks) {
|
|
|
593
769
|
}
|
|
594
770
|
for (const gitlink of visibleGitlinks) replaceShadowIndexPathWithGitlink(input, relativeRoot, gitlink);
|
|
595
771
|
}
|
|
596
|
-
function mirrorShadowGitlinksToVisible(desired, visibleRoot) {
|
|
772
|
+
function mirrorShadowGitlinksToVisible(desired, visibleRoot, trustedRoot) {
|
|
597
773
|
const desiredByPath = new Map(desired.map((entry) => [entry.filePath, entry]));
|
|
598
774
|
for (const current of checkoutGitlinks(visibleRoot)) {
|
|
599
775
|
if (!desiredByPath.has(current.filePath)) removeCheckoutIndexPath(visibleRoot, current.filePath);
|
|
600
776
|
}
|
|
601
777
|
for (const gitlink of desired) {
|
|
778
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, gitlink.filePath, trustedRoot)) {
|
|
779
|
+
throw new Error(`Visible gitlink path has a symlink or non-directory ancestor: ${gitlink.filePath}`);
|
|
780
|
+
}
|
|
602
781
|
const visiblePath = import_node_path.default.join(visibleRoot, ...gitlink.filePath.split("/"));
|
|
603
782
|
const existing = lstatOrNull(visiblePath);
|
|
604
783
|
if (existing && !existing.isDirectory()) import_node_fs.default.rmSync(visiblePath, { recursive: true, force: true });
|
|
@@ -606,7 +785,7 @@ function mirrorShadowGitlinksToVisible(desired, visibleRoot) {
|
|
|
606
785
|
replaceCheckoutIndexPathWithGitlink(visibleRoot, gitlink);
|
|
607
786
|
}
|
|
608
787
|
}
|
|
609
|
-
function assertSafeVisibleGitlinkTransitions(visibleRoot, currentGitlinks, desiredGitlinks, shadowFiles) {
|
|
788
|
+
function assertSafeVisibleGitlinkTransitions(visibleRoot, trustedRoot, currentGitlinks, desiredGitlinks, shadowFiles) {
|
|
610
789
|
const desiredGitlinkPaths = new Set(desiredGitlinks.map((entry) => entry.filePath));
|
|
611
790
|
for (const current of currentGitlinks) {
|
|
612
791
|
if (desiredGitlinkPaths.has(current.filePath)) continue;
|
|
@@ -614,6 +793,9 @@ function assertSafeVisibleGitlinkTransitions(visibleRoot, currentGitlinks, desir
|
|
|
614
793
|
(filePath) => filePath === current.filePath || filePath.startsWith(`${current.filePath}/`)
|
|
615
794
|
);
|
|
616
795
|
if (!becomesOrdinaryPath) continue;
|
|
796
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, current.filePath, trustedRoot)) {
|
|
797
|
+
throw new Error(`Visible gitlink path has a symlink or non-directory ancestor: ${current.filePath}`);
|
|
798
|
+
}
|
|
617
799
|
const existing = lstatOrNull(import_node_path.default.join(visibleRoot, ...current.filePath.split("/")));
|
|
618
800
|
if (!existing) continue;
|
|
619
801
|
if (existing.isDirectory() && import_node_fs.default.readdirSync(import_node_path.default.join(visibleRoot, ...current.filePath.split("/"))).length === 0) {
|
|
@@ -642,15 +824,46 @@ function restoreShadowProjectSnapshot(input, relativeRoot) {
|
|
|
642
824
|
runGitResult(input, input.shadowRoot, ["--literal-pathspecs", "rm", "-r", "-f", "--cached", "--ignore-unmatch", "--", relativeRoot]);
|
|
643
825
|
import_node_fs.default.rmSync(shadowRoot, { recursive: true, force: true });
|
|
644
826
|
}
|
|
827
|
+
function copiedHydrationPreimage(input, shadowRoot, ordinaryFiles, gitlinks = []) {
|
|
828
|
+
const preimage = new Map(
|
|
829
|
+
gitlinks.map((entry) => [entry.filePath, { kind: "gitlink", objectId: entry.objectId }])
|
|
830
|
+
);
|
|
831
|
+
for (const filePath of ordinaryFiles) {
|
|
832
|
+
const entry = filesystemHydrationEntry(input, import_node_path.default.join(shadowRoot, ...filePath.split("/")));
|
|
833
|
+
if (!entry || entry === UNSTABLE_VISIBLE_ENTRY) return null;
|
|
834
|
+
preimage.set(filePath, entry);
|
|
835
|
+
}
|
|
836
|
+
return preimage;
|
|
837
|
+
}
|
|
838
|
+
function visibleProjectMatchesCopiedPreimage(input, visibleRoot, ordinaryFiles, gitlinks, preimage, opaqueRoots) {
|
|
839
|
+
const currentFiles = listGitEligibleFiles(visibleRoot).filter((filePath) => !pathIsWithinOpaqueRoot(filePath, opaqueRoots));
|
|
840
|
+
if (currentFiles.length !== ordinaryFiles.length || currentFiles.some((filePath, index) => filePath !== ordinaryFiles[index]))
|
|
841
|
+
return false;
|
|
842
|
+
const currentGitlinks = checkoutGitlinks(visibleRoot);
|
|
843
|
+
if (currentGitlinks.length !== gitlinks.length || currentGitlinks.some((entry, index) => entry.filePath !== gitlinks[index]?.filePath || entry.objectId !== gitlinks[index]?.objectId)) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
const currentGitlinksByPath = new Map(currentGitlinks.map((entry) => [entry.filePath, entry]));
|
|
847
|
+
return ordinaryFiles.every((filePath) => {
|
|
848
|
+
const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinksByPath);
|
|
849
|
+
return current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath));
|
|
850
|
+
});
|
|
851
|
+
}
|
|
645
852
|
function mirrorVisibleProjectToShadow(input, manifest, branchName) {
|
|
646
853
|
const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
|
|
647
|
-
|
|
854
|
+
const unavailableProjection = () => ({
|
|
855
|
+
entries: [],
|
|
856
|
+
opaqueRoots: [],
|
|
857
|
+
visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, null]])
|
|
858
|
+
});
|
|
859
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-project-scan", input.projectsRoot)) return unavailableProjection();
|
|
860
|
+
if (!import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) return unavailableProjection();
|
|
648
861
|
const isCanonicalCheckout = manifest.canonicalCheckouts?.some((checkout) => checkout.branchName === branchName);
|
|
649
862
|
if (isCanonicalCheckout) {
|
|
650
863
|
assertCanonicalCheckoutIndexMatchesWorktree(visibleRoot);
|
|
651
864
|
}
|
|
652
865
|
const beforeSnapshot = checkoutGitSnapshot(visibleRoot);
|
|
653
|
-
if (!beforeSnapshot) return
|
|
866
|
+
if (!beforeSnapshot) return unavailableProjection();
|
|
654
867
|
const visibleGitlinks = checkoutGitlinks(visibleRoot);
|
|
655
868
|
const visibleIndexEntries = checkoutIndexEntries(visibleRoot).filter((entry) => entry.stage === "0");
|
|
656
869
|
const indexedGitlinkPaths = new Set(visibleGitlinks.map((entry) => entry.filePath));
|
|
@@ -665,21 +878,142 @@ function mirrorVisibleProjectToShadow(input, manifest, branchName) {
|
|
|
665
878
|
const visibleFiles = listGitEligibleFiles(visibleRoot).filter((filePath) => !pathIsWithinOpaqueRoot(filePath, visibleGitlinkRoots));
|
|
666
879
|
const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
|
|
667
880
|
mirrorVisibleGitlinksToShadow(input, relativeRoot, visibleGitlinks);
|
|
668
|
-
mirrorFileSet(visibleRoot, shadowRoot, visibleFiles, listFilesRecursively(shadowRoot), visibleGitlinkRoots, visibleGitlinkRoots
|
|
669
|
-
|
|
881
|
+
mirrorFileSet(visibleRoot, shadowRoot, visibleFiles, listFilesRecursively(shadowRoot), visibleGitlinkRoots, visibleGitlinkRoots, {
|
|
882
|
+
source: input.projectsRoot,
|
|
883
|
+
target: input.shadowRoot
|
|
884
|
+
});
|
|
885
|
+
const hydrationPreimage = copiedHydrationPreimage(input, shadowRoot, visibleFiles, visibleGitlinks);
|
|
886
|
+
if (!hydrationPreimage || checkoutGitSnapshot(visibleRoot) !== beforeSnapshot || !visibleProjectMatchesCopiedPreimage(input, visibleRoot, visibleFiles, visibleGitlinks, hydrationPreimage, visibleGitlinkRoots)) {
|
|
670
887
|
restoreShadowProjectSnapshot(input, relativeRoot);
|
|
671
|
-
return { entries: [], opaqueRoots: [] };
|
|
888
|
+
return { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, null]]) };
|
|
672
889
|
}
|
|
673
890
|
return {
|
|
674
891
|
entries: visibleGitlinks.map((entry) => ({
|
|
675
892
|
filePath: import_node_path.default.posix.join(relativeRoot, entry.filePath),
|
|
676
893
|
objectId: entry.objectId
|
|
677
894
|
})),
|
|
678
|
-
opaqueRoots: visibleGitlinkRoots.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath))
|
|
895
|
+
opaqueRoots: visibleGitlinkRoots.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)),
|
|
896
|
+
visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, hydrationPreimage]])
|
|
679
897
|
};
|
|
680
898
|
}
|
|
681
|
-
function
|
|
899
|
+
function pathDepth(filePath) {
|
|
900
|
+
return filePath.split("/").length;
|
|
901
|
+
}
|
|
902
|
+
function pathsOverlap(left, right) {
|
|
903
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
904
|
+
}
|
|
905
|
+
function removeVisibleBlob(visibleRoot, filePath, trustedRoot) {
|
|
906
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return false;
|
|
907
|
+
const absolutePath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
|
|
908
|
+
const stat = lstatOrNull(absolutePath);
|
|
909
|
+
if (!stat || !stat.isFile() && !stat.isSymbolicLink()) return false;
|
|
910
|
+
import_node_fs.default.rmSync(absolutePath, { force: true });
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
function removeEmptyVisibleAncestors(visibleRoot, filePath, trustedRoot, opaqueRoots = []) {
|
|
914
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return;
|
|
915
|
+
const resolvedRoot = import_node_path.default.resolve(visibleRoot);
|
|
916
|
+
let current = import_node_path.default.dirname(import_node_path.default.join(resolvedRoot, ...filePath.split("/")));
|
|
917
|
+
while (current !== resolvedRoot) {
|
|
918
|
+
const relative = import_node_path.default.relative(resolvedRoot, current).split(import_node_path.default.sep).join("/");
|
|
919
|
+
if (!relative || pathIsWithinOpaqueRoot(relative, opaqueRoots)) return;
|
|
920
|
+
const stat = lstatOrNull(current);
|
|
921
|
+
if (!stat) {
|
|
922
|
+
current = import_node_path.default.dirname(current);
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || import_node_fs.default.readdirSync(current).length > 0) return;
|
|
926
|
+
try {
|
|
927
|
+
import_node_fs.default.rmdirSync(current);
|
|
928
|
+
} catch (error) {
|
|
929
|
+
if (error.code === "ENOTEMPTY" || error.code === "ENOENT") return;
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
current = import_node_path.default.dirname(current);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function copyShadowBlobToVisible(shadowRoot, visibleRoot, filePath, trustedRoot) {
|
|
936
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return false;
|
|
937
|
+
const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
|
|
938
|
+
const existing = lstatOrNull(targetPath);
|
|
939
|
+
if (existing?.isDirectory()) {
|
|
940
|
+
if (import_node_fs.default.readdirSync(targetPath).length > 0) return false;
|
|
941
|
+
import_node_fs.default.rmdirSync(targetPath);
|
|
942
|
+
} else if (existing) {
|
|
943
|
+
import_node_fs.default.rmSync(targetPath, { force: true });
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
copyEntry(shadowRoot, visibleRoot, filePath, { target: trustedRoot });
|
|
947
|
+
return true;
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (error.code === "EEXIST" || error.code === "ENOTDIR") return false;
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot, preimage, opaqueVisibleGitlinks) {
|
|
954
|
+
const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
|
|
955
|
+
const desired = indexHydrationPreimage(input, relativeRoot);
|
|
956
|
+
let expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
|
|
957
|
+
if (!expectedCheckoutSnapshot) return;
|
|
958
|
+
const checkoutIndexIsStable = () => checkoutGitSnapshot(visibleRoot) === expectedCheckoutSnapshot;
|
|
959
|
+
const refreshCheckoutSnapshot = () => {
|
|
960
|
+
expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
|
|
961
|
+
return expectedCheckoutSnapshot !== null;
|
|
962
|
+
};
|
|
963
|
+
let currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
|
|
964
|
+
const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
|
|
965
|
+
(filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
|
|
966
|
+
);
|
|
967
|
+
for (const filePath of changedPaths.filter((candidate) => !desired.has(candidate)).sort((left, right) => pathDepth(right) - pathDepth(left) || left.localeCompare(right))) {
|
|
968
|
+
const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
|
|
969
|
+
if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath))) continue;
|
|
970
|
+
const expected = preimage.get(filePath);
|
|
971
|
+
if (expected?.kind === "gitlink") {
|
|
972
|
+
if (!checkoutIndexIsStable()) return;
|
|
973
|
+
removeCheckoutIndexPath(visibleRoot, filePath);
|
|
974
|
+
if (!refreshCheckoutSnapshot()) return;
|
|
975
|
+
currentGitlinks.delete(filePath);
|
|
976
|
+
} else if (removeVisibleBlob(visibleRoot, filePath, input.projectsRoot)) {
|
|
977
|
+
removeEmptyVisibleAncestors(visibleRoot, filePath, input.projectsRoot, opaqueVisibleGitlinks);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
for (const filePath of changedPaths.filter((candidate) => desired.has(candidate)).sort((left, right) => pathDepth(left) - pathDepth(right) || left.localeCompare(right))) {
|
|
981
|
+
const wanted = desired.get(filePath);
|
|
982
|
+
if (!wanted) continue;
|
|
983
|
+
const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
|
|
984
|
+
if (current === UNSTABLE_VISIBLE_ENTRY || hydrationEntriesEqual(current, wanted)) continue;
|
|
985
|
+
if (!hydrationEntriesEqual(current, preimage.get(filePath))) continue;
|
|
986
|
+
if (wanted.kind === "gitlink") {
|
|
987
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot) || !checkoutIndexIsStable()) return;
|
|
988
|
+
const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
|
|
989
|
+
const existing = lstatOrNull(targetPath);
|
|
990
|
+
if (existing && !existing.isDirectory()) import_node_fs.default.rmSync(targetPath, { force: true });
|
|
991
|
+
import_node_fs.default.mkdirSync(targetPath, { recursive: true });
|
|
992
|
+
replaceCheckoutIndexPathWithGitlink(visibleRoot, { filePath, objectId: wanted.objectId });
|
|
993
|
+
if (!refreshCheckoutSnapshot()) return;
|
|
994
|
+
currentGitlinks.set(filePath, { filePath, objectId: wanted.objectId });
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
const expected = preimage.get(filePath);
|
|
998
|
+
if (expected?.kind === "gitlink") {
|
|
999
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot) || !checkoutIndexIsStable()) return;
|
|
1000
|
+
const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
|
|
1001
|
+
const existing = lstatOrNull(targetPath);
|
|
1002
|
+
if (existing?.isDirectory() && import_node_fs.default.readdirSync(targetPath).length > 0) continue;
|
|
1003
|
+
removeCheckoutIndexPath(visibleRoot, filePath);
|
|
1004
|
+
if (!refreshCheckoutSnapshot()) return;
|
|
1005
|
+
currentGitlinks.delete(filePath);
|
|
1006
|
+
}
|
|
1007
|
+
copyShadowBlobToVisible(shadowRoot, visibleRoot, filePath, input.projectsRoot);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function mirrorShadowProjectToVisible(input, manifest, branchName, additionalOpaqueRoots = [], hydrationPreimage) {
|
|
682
1011
|
const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
|
|
1012
|
+
if (hydrationPreimage === null) return;
|
|
1013
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-project-hydration", input.projectsRoot)) {
|
|
1014
|
+
if (hydrationPreimage) return;
|
|
1015
|
+
throw new Error(`Visible project root has a symlink or non-directory ancestor: ${visibleRoot}`);
|
|
1016
|
+
}
|
|
683
1017
|
if (!import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) return;
|
|
684
1018
|
checkoutGitlinks(visibleRoot);
|
|
685
1019
|
const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
|
|
@@ -693,6 +1027,7 @@ function mirrorShadowProjectToVisible(input, manifest, branchName, additionalOpa
|
|
|
693
1027
|
].sort();
|
|
694
1028
|
assertSafeVisibleGitlinkTransitions(
|
|
695
1029
|
visibleRoot,
|
|
1030
|
+
input.projectsRoot,
|
|
696
1031
|
protectedGitlinkRoots.map((filePath) => ({ filePath, objectId: "" })),
|
|
697
1032
|
desiredGitlinks,
|
|
698
1033
|
shadowFiles
|
|
@@ -700,77 +1035,166 @@ function mirrorShadowProjectToVisible(input, manifest, branchName, additionalOpa
|
|
|
700
1035
|
const opaqueVisibleGitlinks = protectedGitlinkRoots.filter(
|
|
701
1036
|
(filePath) => desiredGitlinkPaths.has(filePath) || !shadowFiles.some((shadowFile) => shadowFile === filePath || shadowFile.startsWith(`${filePath}/`))
|
|
702
1037
|
);
|
|
703
|
-
|
|
704
|
-
|
|
1038
|
+
if (hydrationPreimage) {
|
|
1039
|
+
hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot, hydrationPreimage, opaqueVisibleGitlinks);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
mirrorFileSet(shadowRoot, visibleRoot, shadowFiles, listGitEligibleFiles(visibleRoot), opaqueVisibleGitlinks, [], {
|
|
1043
|
+
source: input.shadowRoot,
|
|
1044
|
+
target: input.projectsRoot
|
|
1045
|
+
});
|
|
1046
|
+
mirrorShadowGitlinksToVisible(desiredGitlinks, visibleRoot, input.projectsRoot);
|
|
1047
|
+
}
|
|
1048
|
+
function visibleRegularRootMatchesCopiedPreimage(input, visibleRoot, files, preimage, filter) {
|
|
1049
|
+
const currentFiles = listFilesRecursively(visibleRoot, filter);
|
|
1050
|
+
if (currentFiles.length !== files.length || currentFiles.some((filePath, index) => filePath !== files[index])) return false;
|
|
1051
|
+
return files.every((filePath) => {
|
|
1052
|
+
const current = hasSafeDirectoryAncestors(visibleRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
|
|
1053
|
+
return current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath));
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
function mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, filter) {
|
|
1057
|
+
if (!hasSafeDirectoryAncestors(sourceRoot, ".r5d-plan-scan", input.plansRoot)) return null;
|
|
1058
|
+
if (!import_node_fs.default.existsSync(sourceRoot)) return /* @__PURE__ */ new Map();
|
|
1059
|
+
const sourceFiles = listFilesRecursively(sourceRoot, filter);
|
|
1060
|
+
mirrorFileSet(sourceRoot, targetRoot, sourceFiles, listFilesRecursively(targetRoot, filter), [], [], {
|
|
1061
|
+
source: input.plansRoot,
|
|
1062
|
+
target: input.shadowRoot
|
|
1063
|
+
});
|
|
1064
|
+
const preimage = copiedHydrationPreimage(input, targetRoot, sourceFiles);
|
|
1065
|
+
if (!preimage || !visibleRegularRootMatchesCopiedPreimage(input, sourceRoot, sourceFiles, preimage, filter)) {
|
|
1066
|
+
restoreShadowProjectSnapshot(input, relativeRoot);
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
return preimage;
|
|
1070
|
+
}
|
|
1071
|
+
function hydrateRegularRootOptimistically(input, relativeRoot, targetRoot, preimage, filter) {
|
|
1072
|
+
const sourceRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
|
|
1073
|
+
if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) return;
|
|
1074
|
+
const desired = indexHydrationPreimage(input, relativeRoot);
|
|
1075
|
+
for (const [filePath, entry] of desired) {
|
|
1076
|
+
if (entry.kind === "gitlink" || filter && !filter(filePath)) desired.delete(filePath);
|
|
1077
|
+
}
|
|
1078
|
+
const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
|
|
1079
|
+
(filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
|
|
1080
|
+
);
|
|
1081
|
+
for (const filePath of changedPaths.filter((candidate) => !desired.has(candidate)).sort((left, right) => pathDepth(right) - pathDepth(left) || left.localeCompare(right))) {
|
|
1082
|
+
const current = hasSafeDirectoryAncestors(targetRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(targetRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
|
|
1083
|
+
if (current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath))) {
|
|
1084
|
+
if (removeVisibleBlob(targetRoot, filePath, input.plansRoot)) {
|
|
1085
|
+
removeEmptyVisibleAncestors(targetRoot, filePath, input.plansRoot);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
for (const filePath of changedPaths.filter((candidate) => desired.has(candidate)).sort((left, right) => pathDepth(left) - pathDepth(right) || left.localeCompare(right))) {
|
|
1090
|
+
const current = hasSafeDirectoryAncestors(targetRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(targetRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
|
|
1091
|
+
const wanted = desired.get(filePath);
|
|
1092
|
+
if (!wanted || current === UNSTABLE_VISIBLE_ENTRY || hydrationEntriesEqual(current, wanted)) continue;
|
|
1093
|
+
if (!hydrationEntriesEqual(current, preimage.get(filePath))) continue;
|
|
1094
|
+
copyShadowBlobToVisible(sourceRoot, targetRoot, filePath, input.plansRoot);
|
|
1095
|
+
}
|
|
705
1096
|
}
|
|
706
1097
|
function mirrorLocalPlansToShadow(input, manifest, branchName) {
|
|
707
1098
|
const sourceRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
|
|
708
|
-
|
|
709
|
-
const targetRoot = import_node_path.default.join(input.shadowRoot, ...
|
|
1099
|
+
const relativeRoot = workspacePlansRelativePath(manifest.projectId, branchName);
|
|
1100
|
+
const targetRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
|
|
710
1101
|
const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
|
|
711
|
-
|
|
1102
|
+
return mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, planFilter);
|
|
712
1103
|
}
|
|
713
|
-
function mirrorShadowPlansToLocal(input, manifest, branchName) {
|
|
1104
|
+
function mirrorShadowPlansToLocal(input, manifest, branchName, hydrationPreimage) {
|
|
1105
|
+
if (hydrationPreimage === null) return;
|
|
714
1106
|
const relativeRoot = workspacePlansRelativePath(manifest.projectId, branchName);
|
|
715
1107
|
const sourceRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
|
|
716
1108
|
const targetRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
|
|
717
1109
|
const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
|
|
1110
|
+
if (hydrationPreimage) {
|
|
1111
|
+
hydrateRegularRootOptimistically(input, relativeRoot, targetRoot, hydrationPreimage, planFilter);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) {
|
|
1115
|
+
throw new Error(`Visible plan root has a symlink or non-directory ancestor: ${targetRoot}`);
|
|
1116
|
+
}
|
|
718
1117
|
mirrorFileSet(
|
|
719
1118
|
sourceRoot,
|
|
720
1119
|
targetRoot,
|
|
721
1120
|
listShadowTrackedFiles(input, relativeRoot).filter(planFilter),
|
|
722
|
-
listFilesRecursively(targetRoot, planFilter)
|
|
1121
|
+
listFilesRecursively(targetRoot, planFilter),
|
|
1122
|
+
[],
|
|
1123
|
+
[],
|
|
1124
|
+
{ source: input.shadowRoot, target: input.plansRoot }
|
|
723
1125
|
);
|
|
724
1126
|
}
|
|
725
1127
|
function mirrorLocalWorkspacePlansToShadow(input) {
|
|
726
|
-
if (input.trigger.canonicalCheckoutOnly) return;
|
|
1128
|
+
if (input.trigger.canonicalCheckoutOnly) return /* @__PURE__ */ new Map();
|
|
727
1129
|
const sourceRoot = localWorkspacePlansPath(input.plansRoot);
|
|
728
|
-
|
|
729
|
-
const targetRoot = import_node_path.default.join(input.shadowRoot,
|
|
1130
|
+
const relativeRoot = WORKSPACE_NATIVE_PLANS_RELATIVE_PATH;
|
|
1131
|
+
const targetRoot = import_node_path.default.join(input.shadowRoot, relativeRoot);
|
|
730
1132
|
const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
|
|
731
|
-
|
|
1133
|
+
return mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, planFilter);
|
|
732
1134
|
}
|
|
733
|
-
function mirrorShadowWorkspacePlansToLocal(input) {
|
|
1135
|
+
function mirrorShadowWorkspacePlansToLocal(input, hydrationPreimage) {
|
|
734
1136
|
if (input.trigger.canonicalCheckoutOnly) return;
|
|
1137
|
+
if (hydrationPreimage === null) return;
|
|
735
1138
|
const sourceRoot = import_node_path.default.join(input.shadowRoot, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH);
|
|
736
1139
|
const targetRoot = localWorkspacePlansPath(input.plansRoot);
|
|
737
1140
|
const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
|
|
1141
|
+
if (hydrationPreimage) {
|
|
1142
|
+
hydrateRegularRootOptimistically(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH, targetRoot, hydrationPreimage, planFilter);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) {
|
|
1146
|
+
throw new Error(`Visible workspace-plan root has a symlink or non-directory ancestor: ${targetRoot}`);
|
|
1147
|
+
}
|
|
738
1148
|
mirrorFileSet(
|
|
739
1149
|
sourceRoot,
|
|
740
1150
|
targetRoot,
|
|
741
1151
|
listShadowTrackedFiles(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH).filter(planFilter),
|
|
742
|
-
listFilesRecursively(targetRoot, planFilter)
|
|
1152
|
+
listFilesRecursively(targetRoot, planFilter),
|
|
1153
|
+
[],
|
|
1154
|
+
[],
|
|
1155
|
+
{ source: input.shadowRoot, target: input.plansRoot }
|
|
743
1156
|
);
|
|
744
1157
|
}
|
|
745
|
-
function reconcileWorkspaceNativePlans(input) {
|
|
1158
|
+
function reconcileWorkspaceNativePlans(input, projection) {
|
|
746
1159
|
if (input.trigger.canonicalCheckoutOnly) return;
|
|
1160
|
+
const localRoot = localWorkspacePlansPath(input.plansRoot);
|
|
1161
|
+
projection.visiblePreimages ??= /* @__PURE__ */ new Map();
|
|
1162
|
+
projection.visiblePreimages.set(localRoot, /* @__PURE__ */ new Map());
|
|
747
1163
|
if (import_node_fs.default.existsSync(localWorkspacePlansPath(input.plansRoot))) {
|
|
748
|
-
mirrorLocalWorkspacePlansToShadow(input);
|
|
1164
|
+
projection.visiblePreimages.set(localRoot, mirrorLocalWorkspacePlansToShadow(input));
|
|
749
1165
|
return;
|
|
750
1166
|
}
|
|
751
1167
|
if (listShadowTrackedFiles(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH).length > 0 || restoreShadowRootFromRemote(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH)) {
|
|
752
|
-
mirrorShadowWorkspacePlansToLocal(input);
|
|
1168
|
+
mirrorShadowWorkspacePlansToLocal(input, /* @__PURE__ */ new Map());
|
|
1169
|
+
projection.visiblePreimages.set(localRoot, indexHydrationPreimage(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH));
|
|
753
1170
|
}
|
|
754
1171
|
}
|
|
755
1172
|
function mergeGitlinkProjection(target, source) {
|
|
756
1173
|
target.entries.push(...source.entries);
|
|
757
1174
|
target.opaqueRoots.push(...source.opaqueRoots);
|
|
1175
|
+
if (source.visiblePreimages) {
|
|
1176
|
+
target.visiblePreimages ??= /* @__PURE__ */ new Map();
|
|
1177
|
+
for (const [visibleRoot, preimage] of source.visiblePreimages) target.visiblePreimages.set(visibleRoot, preimage);
|
|
1178
|
+
}
|
|
758
1179
|
}
|
|
759
1180
|
function mirrorVisibleWorkspaceToShadow(input, excludedCheckouts = /* @__PURE__ */ new Set()) {
|
|
760
|
-
const projection = { entries: [], opaqueRoots: [] };
|
|
761
|
-
reconcileWorkspaceNativePlans(input);
|
|
1181
|
+
const projection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
|
|
1182
|
+
reconcileWorkspaceNativePlans(input, projection);
|
|
762
1183
|
for (const manifest of [...input.projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
|
|
763
1184
|
for (const branchName of [...new Set(manifest.branches)].sort()) {
|
|
764
1185
|
if (excludedCheckouts.has(`${manifest.projectId}\0${branchName}`)) continue;
|
|
765
1186
|
mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, branchName));
|
|
766
|
-
if (!input.trigger.canonicalCheckoutOnly)
|
|
1187
|
+
if (!input.trigger.canonicalCheckoutOnly) {
|
|
1188
|
+
const localPlansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
|
|
1189
|
+
projection.visiblePreimages?.set(localPlansRoot, mirrorLocalPlansToShadow(input, manifest, branchName));
|
|
1190
|
+
}
|
|
767
1191
|
}
|
|
768
1192
|
}
|
|
769
1193
|
projection.opaqueRoots = [...new Set(projection.opaqueRoots)].sort();
|
|
770
1194
|
return projection;
|
|
771
1195
|
}
|
|
772
1196
|
function reconcileNewVisibleCheckouts(input) {
|
|
773
|
-
const projection = { entries: [], opaqueRoots: [] };
|
|
1197
|
+
const projection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
|
|
774
1198
|
for (const target of input.newVisibleCheckouts ?? []) {
|
|
775
1199
|
const manifest = input.projects.find((project) => project.projectId === target.projectId);
|
|
776
1200
|
if (!manifest || !manifest.branches.includes(target.branchName)) continue;
|
|
@@ -779,6 +1203,10 @@ function reconcileNewVisibleCheckouts(input) {
|
|
|
779
1203
|
mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, target.branchName));
|
|
780
1204
|
} else if (listShadowTrackedFiles(input, projectRoot).length > 0 || restoreShadowRootFromRemote(input, projectRoot)) {
|
|
781
1205
|
mirrorShadowProjectToVisible(input, manifest, target.branchName);
|
|
1206
|
+
projection.visiblePreimages?.set(
|
|
1207
|
+
visibleProjectBranchPath(input.projectsRoot, manifest, target.branchName),
|
|
1208
|
+
indexHydrationPreimage(input, projectRoot)
|
|
1209
|
+
);
|
|
782
1210
|
} else {
|
|
783
1211
|
mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, target.branchName));
|
|
784
1212
|
}
|
|
@@ -786,23 +1214,194 @@ function reconcileNewVisibleCheckouts(input) {
|
|
|
786
1214
|
const plansRoot = workspacePlansRelativePath(target.projectId, target.branchName);
|
|
787
1215
|
if (listShadowTrackedFiles(input, plansRoot).length > 0 || restoreShadowRootFromRemote(input, plansRoot)) {
|
|
788
1216
|
mirrorShadowPlansToLocal(input, manifest, target.branchName);
|
|
1217
|
+
projection.visiblePreimages?.set(
|
|
1218
|
+
localPlansBranchPath(input.plansRoot, manifest.projectId, target.branchName),
|
|
1219
|
+
indexHydrationPreimage(input, plansRoot)
|
|
1220
|
+
);
|
|
789
1221
|
} else {
|
|
790
|
-
|
|
1222
|
+
projection.visiblePreimages?.set(
|
|
1223
|
+
localPlansBranchPath(input.plansRoot, manifest.projectId, target.branchName),
|
|
1224
|
+
mirrorLocalPlansToShadow(input, manifest, target.branchName)
|
|
1225
|
+
);
|
|
791
1226
|
}
|
|
792
1227
|
}
|
|
793
1228
|
projection.opaqueRoots = [...new Set(projection.opaqueRoots)].sort();
|
|
794
1229
|
return projection;
|
|
795
1230
|
}
|
|
796
|
-
function mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots = []) {
|
|
797
|
-
mirrorShadowWorkspacePlansToLocal(input);
|
|
1231
|
+
function mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots = [], hydrationPreimages) {
|
|
1232
|
+
mirrorShadowWorkspacePlansToLocal(input, hydrationPreimages?.get(localWorkspacePlansPath(input.plansRoot)));
|
|
798
1233
|
for (const manifest of [...input.projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
|
|
799
1234
|
for (const branchName of [...new Set(manifest.branches)].sort()) {
|
|
800
1235
|
const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
|
|
1236
|
+
const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
|
|
801
1237
|
const opaqueProjectRoots = opaqueWorkspaceRoots.filter((root) => root.startsWith(`${relativeRoot}/`)).map((root) => root.slice(relativeRoot.length + 1));
|
|
802
|
-
mirrorShadowProjectToVisible(input, manifest, branchName, opaqueProjectRoots);
|
|
803
|
-
if (!input.trigger.canonicalCheckoutOnly)
|
|
1238
|
+
mirrorShadowProjectToVisible(input, manifest, branchName, opaqueProjectRoots, hydrationPreimages?.get(visibleRoot));
|
|
1239
|
+
if (!input.trigger.canonicalCheckoutOnly) {
|
|
1240
|
+
const localPlansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
|
|
1241
|
+
mirrorShadowPlansToLocal(input, manifest, branchName, hydrationPreimages?.get(localPlansRoot));
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
function hydrationPreimagesFromShadowRevision(input, revision) {
|
|
1247
|
+
const preimages = /* @__PURE__ */ new Map();
|
|
1248
|
+
const fromRevision = (relativeRoot) => revision ? revisionHydrationPreimage(input, revision, relativeRoot) : /* @__PURE__ */ new Map();
|
|
1249
|
+
if (!input.trigger.canonicalCheckoutOnly) {
|
|
1250
|
+
preimages.set(localWorkspacePlansPath(input.plansRoot), fromRevision(WORKSPACE_NATIVE_PLANS_RELATIVE_PATH));
|
|
1251
|
+
}
|
|
1252
|
+
for (const manifest of input.projects) {
|
|
1253
|
+
for (const branchName of manifest.branches) {
|
|
1254
|
+
preimages.set(
|
|
1255
|
+
visibleProjectBranchPath(input.projectsRoot, manifest, branchName),
|
|
1256
|
+
fromRevision(workspaceProjectBranchRelativePath(manifest.projectId, branchName))
|
|
1257
|
+
);
|
|
1258
|
+
if (!input.trigger.canonicalCheckoutOnly) {
|
|
1259
|
+
preimages.set(
|
|
1260
|
+
localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
|
|
1261
|
+
fromRevision(workspacePlansRelativePath(manifest.projectId, branchName))
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return preimages;
|
|
1267
|
+
}
|
|
1268
|
+
function visibleHydrationConflictPaths(input, preimages, desiredRevision) {
|
|
1269
|
+
const conflicts = [];
|
|
1270
|
+
const inspectRegularRoot = (visibleRoot, relativeRoot, filter) => {
|
|
1271
|
+
const preimage = preimages.get(visibleRoot);
|
|
1272
|
+
if (!preimage) return;
|
|
1273
|
+
const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
|
|
1274
|
+
for (const [filePath, entry] of desired) {
|
|
1275
|
+
if (entry.kind === "gitlink" || filter && !filter(filePath)) desired.delete(filePath);
|
|
1276
|
+
}
|
|
1277
|
+
const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
|
|
1278
|
+
(filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
|
|
1279
|
+
);
|
|
1280
|
+
if (changedPaths.length === 0) return;
|
|
1281
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-hydration-scan", input.plansRoot)) {
|
|
1282
|
+
conflicts.push(...changedPaths.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)));
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const affectsChangedTree = (filePath) => changedPaths.some((changedPath) => pathsOverlap(filePath, changedPath));
|
|
1286
|
+
const currentEntries = /* @__PURE__ */ new Map();
|
|
1287
|
+
const unstablePaths = /* @__PURE__ */ new Set();
|
|
1288
|
+
for (const filePath of listFilesRecursively(visibleRoot).filter(affectsChangedTree)) {
|
|
1289
|
+
const current = hasSafeDirectoryAncestors(visibleRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
|
|
1290
|
+
if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
|
|
1291
|
+
else currentEntries.set(filePath, current);
|
|
804
1292
|
}
|
|
1293
|
+
for (const filePath of /* @__PURE__ */ new Set([...changedPaths, ...currentEntries.keys(), ...unstablePaths])) {
|
|
1294
|
+
if (!affectsChangedTree(filePath)) continue;
|
|
1295
|
+
const current = unstablePaths.has(filePath) ? UNSTABLE_VISIBLE_ENTRY : currentEntries.get(filePath);
|
|
1296
|
+
if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath)) && !hydrationEntriesEqual(current, desired.get(filePath))) {
|
|
1297
|
+
conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
if (!input.trigger.canonicalCheckoutOnly) {
|
|
1302
|
+
inspectRegularRoot(
|
|
1303
|
+
localWorkspacePlansPath(input.plansRoot),
|
|
1304
|
+
WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
|
|
1305
|
+
(filePath) => filePath.endsWith(".plan.md")
|
|
1306
|
+
);
|
|
805
1307
|
}
|
|
1308
|
+
for (const manifest of input.projects) {
|
|
1309
|
+
for (const branchName of manifest.branches) {
|
|
1310
|
+
const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
|
|
1311
|
+
const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
|
|
1312
|
+
const preimage = preimages.get(visibleRoot);
|
|
1313
|
+
if (preimage && import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) {
|
|
1314
|
+
const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
|
|
1315
|
+
const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
|
|
1316
|
+
(filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
|
|
1317
|
+
);
|
|
1318
|
+
if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-hydration-scan", input.projectsRoot)) {
|
|
1319
|
+
conflicts.push(...changedPaths.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)));
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
const affectsChangedTree = (filePath) => changedPaths.some((changedPath) => pathsOverlap(filePath, changedPath));
|
|
1323
|
+
const currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
|
|
1324
|
+
const residualOpaqueGitlinks = [...preimage].filter(
|
|
1325
|
+
([filePath, entry]) => entry.kind === "gitlink" && !desired.has(filePath) && ![...desired.keys()].some((desiredPath) => desiredPath.startsWith(`${filePath}/`))
|
|
1326
|
+
).map(([filePath]) => filePath);
|
|
1327
|
+
const opaqueRoots = [.../* @__PURE__ */ new Set([...currentGitlinks.keys(), ...residualOpaqueGitlinks])];
|
|
1328
|
+
const currentEntries = /* @__PURE__ */ new Map();
|
|
1329
|
+
const unstablePaths = /* @__PURE__ */ new Set();
|
|
1330
|
+
for (const filePath of [...currentGitlinks.keys()].filter(affectsChangedTree)) {
|
|
1331
|
+
const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
|
|
1332
|
+
if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
|
|
1333
|
+
else currentEntries.set(filePath, current);
|
|
1334
|
+
}
|
|
1335
|
+
for (const filePath of listGitEligibleFiles(visibleRoot).filter(
|
|
1336
|
+
(candidate) => affectsChangedTree(candidate) && !pathIsWithinOpaqueRoot(candidate, opaqueRoots)
|
|
1337
|
+
)) {
|
|
1338
|
+
const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
|
|
1339
|
+
if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
|
|
1340
|
+
else currentEntries.set(filePath, current);
|
|
1341
|
+
}
|
|
1342
|
+
for (const filePath of /* @__PURE__ */ new Set([...changedPaths, ...currentEntries.keys(), ...unstablePaths])) {
|
|
1343
|
+
if (!affectsChangedTree(filePath)) continue;
|
|
1344
|
+
const current = unstablePaths.has(filePath) ? UNSTABLE_VISIBLE_ENTRY : currentEntries.get(filePath);
|
|
1345
|
+
if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath)) && !hydrationEntriesEqual(current, desired.get(filePath))) {
|
|
1346
|
+
conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (!input.trigger.canonicalCheckoutOnly) {
|
|
1351
|
+
inspectRegularRoot(
|
|
1352
|
+
localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
|
|
1353
|
+
workspacePlansRelativePath(manifest.projectId, branchName),
|
|
1354
|
+
(filePath) => filePath.endsWith(".plan.md")
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return [...new Set(conflicts)].sort();
|
|
1360
|
+
}
|
|
1361
|
+
function unavailableHydrationConflictPaths(input, preimages, baselineRevision, desiredRevision) {
|
|
1362
|
+
const conflicts = [];
|
|
1363
|
+
const inspectRoot = (visibleRoot, relativeRoot, filter) => {
|
|
1364
|
+
if (preimages.get(visibleRoot) !== null) return;
|
|
1365
|
+
const baseline = revisionHydrationPreimage(input, baselineRevision, relativeRoot);
|
|
1366
|
+
const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
|
|
1367
|
+
for (const filePath of /* @__PURE__ */ new Set([...baseline.keys(), ...desired.keys()])) {
|
|
1368
|
+
if (filter && !filter(filePath)) continue;
|
|
1369
|
+
if (!hydrationEntriesEqual(baseline.get(filePath), desired.get(filePath))) {
|
|
1370
|
+
conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
if (!input.trigger.canonicalCheckoutOnly) {
|
|
1375
|
+
inspectRoot(
|
|
1376
|
+
localWorkspacePlansPath(input.plansRoot),
|
|
1377
|
+
WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
|
|
1378
|
+
(filePath) => filePath.endsWith(".plan.md")
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
for (const manifest of input.projects) {
|
|
1382
|
+
for (const branchName of manifest.branches) {
|
|
1383
|
+
inspectRoot(
|
|
1384
|
+
visibleProjectBranchPath(input.projectsRoot, manifest, branchName),
|
|
1385
|
+
workspaceProjectBranchRelativePath(manifest.projectId, branchName)
|
|
1386
|
+
);
|
|
1387
|
+
if (!input.trigger.canonicalCheckoutOnly) {
|
|
1388
|
+
inspectRoot(
|
|
1389
|
+
localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
|
|
1390
|
+
workspacePlansRelativePath(manifest.projectId, branchName),
|
|
1391
|
+
(filePath) => filePath.endsWith(".plan.md")
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return [...new Set(conflicts)].sort();
|
|
1397
|
+
}
|
|
1398
|
+
function hydrationConflictPaths(input, preimages, baselineRevision, desiredRevision) {
|
|
1399
|
+
return [
|
|
1400
|
+
.../* @__PURE__ */ new Set([
|
|
1401
|
+
...visibleHydrationConflictPaths(input, preimages, desiredRevision),
|
|
1402
|
+
...unavailableHydrationConflictPaths(input, preimages, baselineRevision, desiredRevision)
|
|
1403
|
+
])
|
|
1404
|
+
].sort();
|
|
806
1405
|
}
|
|
807
1406
|
function forceStageCanonicalCheckoutFiles(input, preservedGitlinks = [], opaqueRoots = []) {
|
|
808
1407
|
for (const manifest of input.projects) {
|
|
@@ -883,7 +1482,9 @@ function assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, revision) {
|
|
|
883
1482
|
}
|
|
884
1483
|
function ensureShadowWorkspace(input) {
|
|
885
1484
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(input.shadowRoot), { recursive: true });
|
|
1485
|
+
let created = false;
|
|
886
1486
|
if (!import_node_fs.default.existsSync(import_node_path.default.join(input.shadowRoot, ".git"))) {
|
|
1487
|
+
created = true;
|
|
887
1488
|
import_node_fs.default.rmSync(input.shadowRoot, { recursive: true, force: true });
|
|
888
1489
|
const clone = runGitResult(input, import_node_path.default.dirname(input.shadowRoot), ["clone", "--origin", "origin", input.remoteUrl, input.shadowRoot]);
|
|
889
1490
|
if (clone.exitCode !== 0) {
|
|
@@ -905,6 +1506,7 @@ function ensureShadowWorkspace(input) {
|
|
|
905
1506
|
} else if (!hasHead) {
|
|
906
1507
|
runGit(input, input.shadowRoot, ["checkout", "--orphan", WORKSPACE_BRANCH], "create canonical workspace branch");
|
|
907
1508
|
}
|
|
1509
|
+
return created;
|
|
908
1510
|
}
|
|
909
1511
|
function revParse(input, revision) {
|
|
910
1512
|
const result = runGitResult(input, input.shadowRoot, ["rev-parse", "--verify", revision]);
|
|
@@ -981,14 +1583,15 @@ function resetUncommittedShadowSnapshot(input) {
|
|
|
981
1583
|
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
|
|
982
1584
|
}
|
|
983
1585
|
function fastForwardCleanShadowForNewCheckouts(input) {
|
|
984
|
-
if (!input.newVisibleCheckouts?.length) return;
|
|
1586
|
+
if (!input.newVisibleCheckouts?.length) return null;
|
|
985
1587
|
runGit(input, input.shadowRoot, ["add", "-A"], "stage existing workspace changes before checkout hydration");
|
|
986
|
-
if (gitStatus(input)) return;
|
|
1588
|
+
if (gitStatus(input)) return null;
|
|
987
1589
|
const localHead = revParse(input, "HEAD");
|
|
988
1590
|
const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
|
|
989
|
-
if (!localHead || !remoteHead || localHead === remoteHead) return;
|
|
990
|
-
if (!tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", localHead, remoteHead])) return;
|
|
1591
|
+
if (!localHead || !remoteHead || localHead === remoteHead) return null;
|
|
1592
|
+
if (!tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", localHead, remoteHead])) return null;
|
|
991
1593
|
runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "fast-forward before checkout hydration");
|
|
1594
|
+
return localHead;
|
|
992
1595
|
}
|
|
993
1596
|
function restoreShadowRootFromRemote(input, relativeRoot) {
|
|
994
1597
|
const remoteRevision = `origin/${WORKSPACE_BRANCH}`;
|
|
@@ -1185,12 +1788,12 @@ function convergeRedundantInboundCandidate(input, result) {
|
|
|
1185
1788
|
);
|
|
1186
1789
|
if (stagedTree !== remoteTree) return null;
|
|
1187
1790
|
const localHead = revParse(input, "HEAD");
|
|
1791
|
+
if (localHead === remoteHead) return null;
|
|
1188
1792
|
runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "converge redundant inbound workspace candidate");
|
|
1189
1793
|
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean redundant inbound workspace candidate");
|
|
1190
|
-
mirrorShadowWorkspaceToVisible(input);
|
|
1191
1794
|
return {
|
|
1192
1795
|
...result,
|
|
1193
|
-
outcome:
|
|
1796
|
+
outcome: "updated",
|
|
1194
1797
|
publishedHead: remoteHead,
|
|
1195
1798
|
diffSizeBytes: 0,
|
|
1196
1799
|
gitStatus: gitStatus(input),
|
|
@@ -1202,10 +1805,13 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1202
1805
|
let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
|
|
1203
1806
|
try {
|
|
1204
1807
|
input = { ...input, projects: workspaceProjectsForSync(input.projects, input.trigger) };
|
|
1205
|
-
let mirroredGitlinkProjection = { entries: [], opaqueRoots: [] };
|
|
1206
|
-
|
|
1808
|
+
let mirroredGitlinkProjection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
|
|
1809
|
+
let fastForwardedFromHead = null;
|
|
1810
|
+
const shadowWasCreated = ensureShadowWorkspace(input);
|
|
1811
|
+
let hydrationPreimages = input.skipVisibleMirror ? hydrationPreimagesFromShadowRevision(input, shadowWasCreated ? null : revParse(input, "HEAD")) : /* @__PURE__ */ new Map();
|
|
1207
1812
|
const remoteHeadAtStart = revParse(input, `origin/${WORKSPACE_BRANCH}`);
|
|
1208
1813
|
const result = baseResult(input, remoteHeadAtStart);
|
|
1814
|
+
const inboundConflictsAtStart = input.skipVisibleMirror && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
|
|
1209
1815
|
if (input.resetToCanonical) {
|
|
1210
1816
|
if (input.trigger.canonicalCheckoutOnly) {
|
|
1211
1817
|
throw new Error("A canonical-checkout-only synchronization cannot reset the authoritative resolver checkout");
|
|
@@ -1224,9 +1830,10 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1224
1830
|
if (!input.skipVisibleMirror) {
|
|
1225
1831
|
const newCheckoutKeys = new Set((input.newVisibleCheckouts ?? []).map((target) => `${target.projectId}\0${target.branchName}`));
|
|
1226
1832
|
mirroredGitlinkProjection = mirrorVisibleWorkspaceToShadow(input, newCheckoutKeys);
|
|
1227
|
-
fastForwardCleanShadowForNewCheckouts(input);
|
|
1833
|
+
fastForwardedFromHead = fastForwardCleanShadowForNewCheckouts(input);
|
|
1228
1834
|
mergeGitlinkProjection(mirroredGitlinkProjection, reconcileNewVisibleCheckouts(input));
|
|
1229
1835
|
mirroredGitlinkProjection.opaqueRoots = [...new Set(mirroredGitlinkProjection.opaqueRoots)].sort();
|
|
1836
|
+
hydrationPreimages = mirroredGitlinkProjection.visiblePreimages ?? hydrationPreimages;
|
|
1230
1837
|
}
|
|
1231
1838
|
const stagePathspecs = mirroredGitlinkProjection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
|
|
1232
1839
|
runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage workspace changes");
|
|
@@ -1267,9 +1874,22 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1267
1874
|
runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
|
|
1268
1875
|
}
|
|
1269
1876
|
let candidateHead = revParse(input, "HEAD");
|
|
1877
|
+
const candidateHeadBeforeRebase = candidateHead;
|
|
1270
1878
|
if (candidateHead) assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
|
|
1271
1879
|
const hasRemoteMain = Boolean(revParse(input, `origin/${WORKSPACE_BRANCH}`));
|
|
1272
1880
|
const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
|
|
1881
|
+
if (hasUnpushedCommit && inboundConflictsAtStart.length > 0) {
|
|
1882
|
+
const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflictsAtStart])].sort();
|
|
1883
|
+
return {
|
|
1884
|
+
...observed,
|
|
1885
|
+
outcome: "failed",
|
|
1886
|
+
candidateHead: candidateHead ?? void 0,
|
|
1887
|
+
affectedProjects: affectedProjects(conflictPaths),
|
|
1888
|
+
affectedPaths: reportedPaths(conflictPaths),
|
|
1889
|
+
error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation.",
|
|
1890
|
+
gitStatus: gitStatus(input)
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1273
1893
|
if (!hasUnpushedCommit) {
|
|
1274
1894
|
const localHead = revParse(input, "HEAD");
|
|
1275
1895
|
const currentRemoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
|
|
@@ -1277,7 +1897,23 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1277
1897
|
runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "update workspace from canonical state");
|
|
1278
1898
|
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean updated workspace");
|
|
1279
1899
|
assertPublishedCanonicalCheckoutTree(input, currentRemoteHead, sampledCanonicalCheckoutTreeHash);
|
|
1280
|
-
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
|
|
1900
|
+
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
|
|
1901
|
+
const inboundConflicts = input.trigger.canonicalCheckoutOnly ? [] : localHead ? hydrationConflictPaths(input, hydrationPreimages, localHead, currentRemoteHead) : visibleHydrationConflictPaths(input, hydrationPreimages, currentRemoteHead);
|
|
1902
|
+
if (inboundConflicts.length > 0 && localHead) {
|
|
1903
|
+
runGit(input, input.shadowRoot, ["reset", "--hard", localHead], "preserve old workspace base after inbound conflict");
|
|
1904
|
+
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved old workspace base");
|
|
1905
|
+
const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflicts])].sort();
|
|
1906
|
+
return {
|
|
1907
|
+
...observed,
|
|
1908
|
+
outcome: "failed",
|
|
1909
|
+
candidateHead: candidateHead ?? void 0,
|
|
1910
|
+
publishedHead: currentRemoteHead,
|
|
1911
|
+
affectedProjects: affectedProjects(conflictPaths),
|
|
1912
|
+
affectedPaths: reportedPaths(conflictPaths),
|
|
1913
|
+
error: "Inbound workspace update overlaps local visible changes; disjoint updates were hydrated and the old base was preserved for reconciliation.",
|
|
1914
|
+
gitStatus: gitStatus(input)
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1281
1917
|
return {
|
|
1282
1918
|
...observed,
|
|
1283
1919
|
outcome: "updated",
|
|
@@ -1289,9 +1925,27 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1289
1925
|
const authoritativeHead = currentRemoteHead ?? localHead;
|
|
1290
1926
|
if (authoritativeHead) {
|
|
1291
1927
|
assertPublishedCanonicalCheckoutTree(input, authoritativeHead, sampledCanonicalCheckoutTreeHash);
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1928
|
+
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
|
|
1929
|
+
const hydrationConflicts2 = input.trigger.canonicalCheckoutOnly ? [] : fastForwardedFromHead ? hydrationConflictPaths(input, hydrationPreimages, fastForwardedFromHead, authoritativeHead) : visibleHydrationConflictPaths(input, hydrationPreimages, authoritativeHead);
|
|
1930
|
+
if (hydrationConflicts2.length > 0 && fastForwardedFromHead) {
|
|
1931
|
+
runGit(
|
|
1932
|
+
input,
|
|
1933
|
+
input.shadowRoot,
|
|
1934
|
+
["reset", "--hard", fastForwardedFromHead],
|
|
1935
|
+
"preserve old workspace base after new-checkout fast-forward conflict"
|
|
1936
|
+
);
|
|
1937
|
+
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved new-checkout workspace base");
|
|
1938
|
+
return {
|
|
1939
|
+
...observed,
|
|
1940
|
+
outcome: "failed",
|
|
1941
|
+
candidateHead: candidateHead ?? void 0,
|
|
1942
|
+
publishedHead: currentRemoteHead ?? void 0,
|
|
1943
|
+
affectedProjects: affectedProjects(hydrationConflicts2),
|
|
1944
|
+
affectedPaths: reportedPaths(hydrationConflicts2),
|
|
1945
|
+
error: "Workspace fast-forward for a new checkout overlaps local visible changes; compatible updates were hydrated and the old base was preserved for reconciliation.",
|
|
1946
|
+
gitStatus: gitStatus(input)
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1295
1949
|
}
|
|
1296
1950
|
return {
|
|
1297
1951
|
...observed,
|
|
@@ -1308,7 +1962,7 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1308
1962
|
rebaseCount += 1;
|
|
1309
1963
|
if (rebase.exitCode !== 0) {
|
|
1310
1964
|
const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
|
|
1311
|
-
const conflictPaths =
|
|
1965
|
+
const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort();
|
|
1312
1966
|
runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
|
|
1313
1967
|
return {
|
|
1314
1968
|
...observed,
|
|
@@ -1316,7 +1970,8 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1316
1970
|
expectedHead,
|
|
1317
1971
|
candidateHead: candidateHead ?? void 0,
|
|
1318
1972
|
rebaseCount,
|
|
1319
|
-
|
|
1973
|
+
affectedProjects: affectedProjects(conflictPaths),
|
|
1974
|
+
affectedPaths: reportedPaths(conflictPaths),
|
|
1320
1975
|
error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`,
|
|
1321
1976
|
gitStatus: gitStatus(input)
|
|
1322
1977
|
};
|
|
@@ -1334,6 +1989,30 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1334
1989
|
gitStatus: gitStatus(input)
|
|
1335
1990
|
};
|
|
1336
1991
|
}
|
|
1992
|
+
assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
|
|
1993
|
+
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
|
|
1994
|
+
const hydrationConflicts = input.trigger.canonicalCheckoutOnly ? [] : candidateHeadBeforeRebase ? hydrationConflictPaths(input, hydrationPreimages, candidateHeadBeforeRebase, "HEAD") : visibleHydrationConflictPaths(input, hydrationPreimages, "HEAD");
|
|
1995
|
+
if (hydrationConflicts.length > 0 && candidateHeadBeforeRebase) {
|
|
1996
|
+
runGit(
|
|
1997
|
+
input,
|
|
1998
|
+
input.shadowRoot,
|
|
1999
|
+
["reset", "--hard", candidateHeadBeforeRebase],
|
|
2000
|
+
"preserve old-base workspace candidate after hydration conflict"
|
|
2001
|
+
);
|
|
2002
|
+
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace candidate");
|
|
2003
|
+
const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...hydrationConflicts])].sort();
|
|
2004
|
+
return {
|
|
2005
|
+
...observed,
|
|
2006
|
+
outcome: "failed",
|
|
2007
|
+
expectedHead,
|
|
2008
|
+
candidateHead,
|
|
2009
|
+
rebaseCount,
|
|
2010
|
+
affectedProjects: affectedProjects(conflictPaths),
|
|
2011
|
+
affectedPaths: reportedPaths(conflictPaths),
|
|
2012
|
+
error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved.",
|
|
2013
|
+
gitStatus: gitStatus(input)
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
1337
2016
|
const upload = uploadWorkspaceCandidate(input, candidateHead);
|
|
1338
2017
|
if (upload.exitCode !== 0) {
|
|
1339
2018
|
return {
|
|
@@ -1346,8 +2025,6 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1346
2025
|
gitStatus: gitStatus(input)
|
|
1347
2026
|
};
|
|
1348
2027
|
}
|
|
1349
|
-
assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
|
|
1350
|
-
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
|
|
1351
2028
|
return {
|
|
1352
2029
|
...observed,
|
|
1353
2030
|
outcome: "candidate_ready",
|