@ricsam/r5d-worker 0.0.132 → 0.0.133
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/atomic-rename.cjs +303 -0
- package/dist/cjs/git-blob-hash.cjs +41 -0
- package/dist/cjs/main.cjs +187 -38
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/three-way-merge.cjs +346 -0
- package/dist/cjs/working-tree-mirror.cjs +1049 -64
- package/dist/cjs/workspace-command-sync-policy.cjs +8 -4
- package/dist/cjs/workspace-command-targets.cjs +63 -0
- package/dist/cjs/workspace-filesystem-job-types.cjs +11 -1
- package/dist/cjs/workspace-filesystem-jobs.cjs +2 -0
- package/dist/cjs/workspace-git-sync.cjs +846 -61
- package/dist/cjs/workspace-hydration-ledger.cjs +66 -0
- package/dist/cjs/workspace-hydration-merge.cjs +433 -0
- package/dist/cjs/workspace-hydration-recovery-state.cjs +53 -0
- package/dist/cjs/workspace-merge-projection.cjs +81 -10
- package/dist/cjs/workspace-project-config-policy.cjs +19 -12
- package/dist/mjs/atomic-rename.mjs +261 -0
- package/dist/mjs/git-blob-hash.mjs +16 -0
- package/dist/mjs/main.mjs +196 -39
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/three-way-merge.mjs +318 -0
- package/dist/mjs/working-tree-mirror.mjs +1035 -64
- package/dist/mjs/workspace-command-sync-policy.mjs +8 -4
- package/dist/mjs/workspace-command-targets.mjs +37 -0
- package/dist/mjs/workspace-filesystem-job-types.mjs +11 -1
- package/dist/mjs/workspace-filesystem-jobs.mjs +4 -0
- package/dist/mjs/workspace-git-sync.mjs +854 -62
- package/dist/mjs/workspace-hydration-ledger.mjs +42 -0
- package/dist/mjs/workspace-hydration-merge.mjs +399 -0
- package/dist/mjs/workspace-hydration-recovery-state.mjs +29 -0
- package/dist/mjs/workspace-merge-projection.mjs +85 -11
- package/dist/mjs/workspace-project-config-policy.mjs +16 -10
- package/dist/types/atomic-rename.d.ts +78 -0
- package/dist/types/git-blob-hash.d.ts +10 -0
- package/dist/types/main.d.ts +21 -2
- package/dist/types/three-way-merge.d.ts +77 -0
- package/dist/types/working-tree-mirror.d.ts +270 -7
- package/dist/types/workspace-command-sync-policy.d.ts +12 -6
- package/dist/types/workspace-command-targets.d.ts +37 -0
- package/dist/types/workspace-filesystem-job-types.d.ts +46 -4
- package/dist/types/workspace-git-sync.d.ts +125 -3
- package/dist/types/workspace-hydration-ledger.d.ts +43 -0
- package/dist/types/workspace-hydration-merge.d.ts +95 -0
- package/dist/types/workspace-hydration-recovery-state.d.ts +10 -0
- package/dist/types/workspace-merge-projection.d.ts +19 -1
- package/dist/types/workspace-project-config-policy.d.ts +17 -3
- package/package.json +2 -2
|
@@ -6,15 +6,30 @@ import {
|
|
|
6
6
|
workspaceMergeProjectionSupport
|
|
7
7
|
} from "./workspace-merge-projection.mjs";
|
|
8
8
|
import { workspaceFilesystemExecutor } from "./workspace-filesystem-executor.mjs";
|
|
9
|
+
import { gitBlobHash, gitObjectHashAlgorithmFor } from "./git-blob-hash.mjs";
|
|
9
10
|
import {
|
|
11
|
+
applyWorkingTreeMirror,
|
|
10
12
|
captureWorkingTreeMirrorPlan,
|
|
11
13
|
fsyncWorkingTreePaths,
|
|
12
14
|
inspectWorkingTree,
|
|
15
|
+
inspectWorkingTreePath,
|
|
13
16
|
mirrorWorkingTree,
|
|
14
17
|
normalizeWorkingTreeRelativePath,
|
|
18
|
+
planPreparedWorkingTreeMirror,
|
|
15
19
|
planWorkingTreeMirror,
|
|
16
|
-
|
|
20
|
+
prepareWorkingTreeMirror,
|
|
21
|
+
restoreWorkingTreeMirrorScope,
|
|
22
|
+
rollbackWorkingTreeMirrorJournal,
|
|
23
|
+
workingTreeDisplacedRetentionDirectory,
|
|
24
|
+
WorkingTreeTargetChangedError
|
|
17
25
|
} from "./working-tree-mirror.mjs";
|
|
26
|
+
import {
|
|
27
|
+
decideWorkspaceHydrationMerge,
|
|
28
|
+
readHydrationBinaryPaths,
|
|
29
|
+
describeHydrationSkipPaths,
|
|
30
|
+
parseWorkspaceSubtreeChanges
|
|
31
|
+
} from "./workspace-hydration-merge.mjs";
|
|
32
|
+
import { WorkspaceHydrationPreBlobLedger } from "./workspace-hydration-ledger.mjs";
|
|
18
33
|
import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
|
|
19
34
|
const WORKSPACE_GIT_BRANCH = "main";
|
|
20
35
|
const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
|
|
@@ -22,7 +37,36 @@ const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v
|
|
|
22
37
|
const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
|
|
23
38
|
const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
|
|
24
39
|
const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
|
|
40
|
+
const WORKSPACE_GIT_HYDRATION_RECOVERIES = "r5d/workspace-hydration-recoveries";
|
|
41
|
+
const WORKSPACE_GIT_HYDRATION_PRESERVED_BASES = "r5d/workspace-hydration-preserved-bases";
|
|
25
42
|
const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
|
|
43
|
+
const WORKSPACE_MERGE_HYDRATION_SWITCH = "R5D_MERGE_HYDRATION";
|
|
44
|
+
function workspaceMergeHydrationEnabled() {
|
|
45
|
+
return process.env[WORKSPACE_MERGE_HYDRATION_SWITCH] !== "0";
|
|
46
|
+
}
|
|
47
|
+
const hydrationPreBlobLedgers = /* @__PURE__ */ new Map();
|
|
48
|
+
function hydrationPreBlobLedger(workspacePath) {
|
|
49
|
+
const key = path.resolve(workspacePath);
|
|
50
|
+
let ledger = hydrationPreBlobLedgers.get(key);
|
|
51
|
+
if (!ledger) {
|
|
52
|
+
ledger = new WorkspaceHydrationPreBlobLedger();
|
|
53
|
+
hydrationPreBlobLedgers.set(key, ledger);
|
|
54
|
+
}
|
|
55
|
+
return ledger;
|
|
56
|
+
}
|
|
57
|
+
class WorkspaceHydrationRecoveryRequiredError extends Error {
|
|
58
|
+
constructor(hydrationRecovery, options) {
|
|
59
|
+
super(
|
|
60
|
+
`Interrupted guarded workspace hydration ${hydrationRecovery.transactionId} requires explicit preservation recovery; visible files and the prior receipt are preserved, and snapshots remain at ${hydrationRecovery.transactionPath} (mounts: ${hydrationRecovery.mounts.map(({ id }) => id).join(", ")})${hydrationRecovery.mounts.some(({ retainedPaths }) => retainedPaths?.length) ? `; displaced entries retained at ${hydrationRecovery.mounts.flatMap(({ retainedPaths }) => retainedPaths ?? []).join(", ")}` : ""}`,
|
|
61
|
+
options
|
|
62
|
+
);
|
|
63
|
+
this.hydrationRecovery = hydrationRecovery;
|
|
64
|
+
this.name = "WorkspaceHydrationRecoveryRequiredError";
|
|
65
|
+
}
|
|
66
|
+
hydrationRecovery;
|
|
67
|
+
code = "hydration_recovery_required";
|
|
68
|
+
}
|
|
69
|
+
const HYDRATION_TRANSACTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
26
70
|
function workspaceGitMountData(mount) {
|
|
27
71
|
return {
|
|
28
72
|
id: mount.id,
|
|
@@ -878,9 +922,8 @@ async function runWorkspaceCheckoutTransition(workspacePath, mutate) {
|
|
|
878
922
|
await throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
|
|
879
923
|
}
|
|
880
924
|
}
|
|
881
|
-
function parseHydrationTransactionManifest(transactionPath) {
|
|
925
|
+
function parseHydrationTransactionManifest(transactionPath, workspacePath = path.resolve(transactionPath, "..", "..", ".."), options = {}) {
|
|
882
926
|
const manifestPath = path.join(transactionPath, "manifest.json");
|
|
883
|
-
const workspacePath = path.resolve(transactionPath, "..", "..", "..");
|
|
884
927
|
const status = fs.lstatSync(manifestPath);
|
|
885
928
|
if (!status.isFile() || status.isSymbolicLink()) {
|
|
886
929
|
throw new Error(`Workspace hydration transaction manifest is not a regular file: ${manifestPath}`);
|
|
@@ -895,22 +938,26 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
895
938
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
896
939
|
}
|
|
897
940
|
const candidate = parsed;
|
|
898
|
-
if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 || !Array.isArray(candidate.mounts)) {
|
|
941
|
+
if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 && candidate.version !== 5 || !Array.isArray(candidate.mounts) || candidate.preservedMounts !== void 0 && (candidate.version !== 5 || !Array.isArray(candidate.preservedMounts))) {
|
|
899
942
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
900
943
|
}
|
|
944
|
+
if (candidate.version === 5 && (typeof candidate.transactionId !== "string" || !HYDRATION_TRANSACTION_ID.test(candidate.transactionId))) {
|
|
945
|
+
throw new Error(`Workspace hydration transaction identity is invalid: ${manifestPath}`);
|
|
946
|
+
}
|
|
901
947
|
const parseReceipt = (rawReceipt) => {
|
|
902
948
|
if (rawReceipt === null) return null;
|
|
903
949
|
if (!rawReceipt || typeof rawReceipt !== "object") {
|
|
904
950
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
905
951
|
}
|
|
906
|
-
|
|
907
|
-
|
|
952
|
+
const content = `${JSON.stringify(rawReceipt)}
|
|
953
|
+
`;
|
|
954
|
+
return options.verifyReceiptCommits === false ? parseWorkspaceHydrationReceiptContentUnverified(manifestPath, content).receipt : parseWorkspaceHydrationReceiptContent(workspacePath, manifestPath, content);
|
|
908
955
|
};
|
|
909
956
|
const targetReceipt = parseReceipt(candidate.targetReceipt);
|
|
910
957
|
const receiptBefore = parseReceipt(candidate.receiptBefore);
|
|
911
958
|
const parseScope = (rawScope, existed) => {
|
|
912
959
|
if (rawScope === void 0) return void 0;
|
|
913
|
-
if (candidate.version !== 4 || !existed || !rawScope || typeof rawScope !== "object") {
|
|
960
|
+
if (candidate.version !== 4 && candidate.version !== 5 || !existed || !rawScope || typeof rawScope !== "object") {
|
|
914
961
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
915
962
|
}
|
|
916
963
|
const parsePaths = (rawPaths) => {
|
|
@@ -929,7 +976,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
929
976
|
};
|
|
930
977
|
const seenIds = /* @__PURE__ */ new Set();
|
|
931
978
|
const seenSnapshots = /* @__PURE__ */ new Set();
|
|
932
|
-
const
|
|
979
|
+
const parsedMounts = [...candidate.mounts, ...candidate.preservedMounts ?? []].map((rawMount) => {
|
|
933
980
|
if (!rawMount || typeof rawMount !== "object") {
|
|
934
981
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
935
982
|
}
|
|
@@ -944,6 +991,9 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
944
991
|
seenIds.add(mount.id);
|
|
945
992
|
seenSnapshots.add(mount.snapshotName);
|
|
946
993
|
const scope = parseScope(rawMount.scope, mount.existed);
|
|
994
|
+
if (candidate.version === 5 && mount.recovery !== "snapshot" && mount.recovery !== "guarded" || candidate.version !== 5 && mount.recovery !== void 0 || mount.recovery === "guarded" && (!mount.existed || !scope)) {
|
|
995
|
+
throw new Error(`Workspace hydration recovery policy is invalid: ${manifestPath}`);
|
|
996
|
+
}
|
|
947
997
|
return {
|
|
948
998
|
id: mount.id,
|
|
949
999
|
incarnationKey: mount.incarnationKey,
|
|
@@ -952,16 +1002,22 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
952
1002
|
workspaceRelativePath: mount.workspaceRelativePath,
|
|
953
1003
|
existed: mount.existed,
|
|
954
1004
|
snapshotName: mount.snapshotName,
|
|
955
|
-
...scope ? { scope } : {}
|
|
1005
|
+
...scope ? { scope } : {},
|
|
1006
|
+
...candidate.version === 5 ? { recovery: mount.recovery } : {}
|
|
956
1007
|
};
|
|
957
1008
|
});
|
|
958
1009
|
return {
|
|
959
|
-
version: candidate.version === 4 ? 4 : 3,
|
|
1010
|
+
version: candidate.version === 5 ? 5 : candidate.version === 4 ? 4 : 3,
|
|
1011
|
+
...candidate.version === 5 ? { transactionId: candidate.transactionId } : {},
|
|
960
1012
|
targetReceipt,
|
|
961
1013
|
receiptBefore,
|
|
962
|
-
mounts
|
|
1014
|
+
mounts: parsedMounts.slice(0, candidate.mounts.length),
|
|
1015
|
+
...candidate.preservedMounts !== void 0 ? { preservedMounts: parsedMounts.slice(candidate.mounts.length) } : {}
|
|
963
1016
|
};
|
|
964
1017
|
}
|
|
1018
|
+
function hydrationEvidenceMounts(manifest) {
|
|
1019
|
+
return [...manifest.mounts, ...manifest.preservedMounts ?? []];
|
|
1020
|
+
}
|
|
965
1021
|
function removeHydrationTransaction(workspacePath) {
|
|
966
1022
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
967
1023
|
fs.rmSync(transactionPath, { recursive: true, force: true });
|
|
@@ -976,7 +1032,303 @@ function hydrationTransactionCommitted(transactionPath) {
|
|
|
976
1032
|
}
|
|
977
1033
|
return true;
|
|
978
1034
|
}
|
|
1035
|
+
function hydrationRetainedEvidence(mount) {
|
|
1036
|
+
if (!lstatIfExists(mount.sourcePath)) return null;
|
|
1037
|
+
assertManagedDirectoryPath({
|
|
1038
|
+
trustedRoot: mount.durabilityRootPath,
|
|
1039
|
+
candidate: mount.sourcePath,
|
|
1040
|
+
label: `Hydration evidence mount ${mount.id}`
|
|
1041
|
+
});
|
|
1042
|
+
const dotGit = path.join(mount.sourcePath, ".git");
|
|
1043
|
+
if (lstatIfExists(dotGit)?.isSymbolicLink()) throw new Error(`Hydration evidence Git directory is a symlink: ${dotGit}`);
|
|
1044
|
+
const directory = workingTreeDisplacedRetentionDirectory(mount.sourcePath);
|
|
1045
|
+
if (!directory) return null;
|
|
1046
|
+
assertManagedDirectoryPath({
|
|
1047
|
+
trustedRoot: mount.durabilityRootPath,
|
|
1048
|
+
candidate: directory,
|
|
1049
|
+
label: `Hydration retained evidence ${mount.id}`
|
|
1050
|
+
});
|
|
1051
|
+
if (!lstatIfExists(directory)) return null;
|
|
1052
|
+
return {
|
|
1053
|
+
directory,
|
|
1054
|
+
paths: fs.readdirSync(directory).sort().map((name) => path.join(directory, name))
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function fsyncHydrationRetainedEvidence(mount) {
|
|
1058
|
+
const evidence = hydrationRetainedEvidence(mount);
|
|
1059
|
+
if (!evidence || evidence.paths.length === 0) return;
|
|
1060
|
+
fsyncTree(evidence.directory);
|
|
1061
|
+
for (let directory = path.dirname(evidence.directory); ; directory = path.dirname(directory)) {
|
|
1062
|
+
fsyncDirectory(directory);
|
|
1063
|
+
if (directory === path.resolve(mount.durabilityRootPath)) break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function readHydrationArchivedEvidence(transactionPath, manifest) {
|
|
1067
|
+
const recordPath = path.join(transactionPath, "retained-evidence.json");
|
|
1068
|
+
const status = lstatIfExists(recordPath);
|
|
1069
|
+
if (!status) return [];
|
|
1070
|
+
if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Invalid hydration retained-evidence record: ${recordPath}`);
|
|
1071
|
+
const record = JSON.parse(fs.readFileSync(recordPath, "utf8"));
|
|
1072
|
+
if (record.version !== 1 || !Array.isArray(record.entries)) throw new Error(`Invalid hydration retained-evidence record: ${recordPath}`);
|
|
1073
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1074
|
+
return record.entries.map((raw) => {
|
|
1075
|
+
if (!raw || typeof raw !== "object") throw new Error(`Invalid hydration retained-evidence entry: ${recordPath}`);
|
|
1076
|
+
const entry = raw;
|
|
1077
|
+
const mount = hydrationEvidenceMounts(manifest).find(({ id }) => id === entry.mountId);
|
|
1078
|
+
const parts = typeof entry.archiveRelativePath === "string" ? entry.archiveRelativePath.split("/") : [];
|
|
1079
|
+
const sourceRelative = typeof entry.sourcePath === "string" && mount ? path.relative(mount.durabilityRootPath, entry.sourcePath) : "";
|
|
1080
|
+
if (!mount || typeof entry.sourcePath !== "string" || path.resolve(entry.sourcePath) !== entry.sourcePath || !sourceRelative || sourceRelative === ".." || sourceRelative.startsWith(`..${path.sep}`) || path.isAbsolute(sourceRelative) || parts.length !== 4 || parts[0] !== "displaced" || !HYDRATION_TRANSACTION_ID.test(parts[1]) || parts[2] !== mount.snapshotName || !parts[3] || parts[3] === "." || parts[3] === ".." || parts[3].includes("\\") || seen.has(entry.archiveRelativePath)) {
|
|
1081
|
+
throw new Error(`Invalid hydration retained-evidence entry: ${recordPath}`);
|
|
1082
|
+
}
|
|
1083
|
+
const archivedPath = path.join(transactionPath, ...parts);
|
|
1084
|
+
assertManagedDirectoryPath({
|
|
1085
|
+
trustedRoot: transactionPath,
|
|
1086
|
+
candidate: path.dirname(archivedPath),
|
|
1087
|
+
label: "Archived hydration evidence"
|
|
1088
|
+
});
|
|
1089
|
+
if (!lstatIfExists(archivedPath)) throw new Error(`Archived hydration evidence is missing: ${archivedPath}`);
|
|
1090
|
+
seen.add(entry.archiveRelativePath);
|
|
1091
|
+
return { mountId: entry.mountId, sourcePath: entry.sourcePath, archiveRelativePath: entry.archiveRelativePath };
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function captureHydrationRetainedEvidence(transactionPath, manifest) {
|
|
1095
|
+
const entries = readHydrationArchivedEvidence(transactionPath, manifest);
|
|
1096
|
+
const captureId = crypto.randomUUID();
|
|
1097
|
+
let captured = false;
|
|
1098
|
+
for (const mount of hydrationEvidenceMounts(manifest)) {
|
|
1099
|
+
const evidence = hydrationRetainedEvidence(mount);
|
|
1100
|
+
if (!evidence || evidence.paths.length === 0) continue;
|
|
1101
|
+
fsyncHydrationRetainedEvidence(mount);
|
|
1102
|
+
const relativeDirectory = `displaced/${captureId}/${mount.snapshotName}`;
|
|
1103
|
+
const destinationDirectory = path.join(transactionPath, relativeDirectory);
|
|
1104
|
+
assertManagedDirectoryPath({ trustedRoot: transactionPath, candidate: destinationDirectory, label: "Hydration evidence capture" });
|
|
1105
|
+
fs.mkdirSync(destinationDirectory, { recursive: true, mode: 448 });
|
|
1106
|
+
for (const sourcePath of evidence.paths) {
|
|
1107
|
+
const archiveRelativePath = `${relativeDirectory}/${path.basename(sourcePath)}`;
|
|
1108
|
+
fs.cpSync(sourcePath, path.join(transactionPath, archiveRelativePath), {
|
|
1109
|
+
recursive: true,
|
|
1110
|
+
dereference: false,
|
|
1111
|
+
verbatimSymlinks: true,
|
|
1112
|
+
errorOnExist: true,
|
|
1113
|
+
force: false,
|
|
1114
|
+
preserveTimestamps: true
|
|
1115
|
+
});
|
|
1116
|
+
entries.push({ mountId: mount.id, sourcePath, archiveRelativePath });
|
|
1117
|
+
captured = true;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (captured) {
|
|
1121
|
+
fsyncTree(path.join(transactionPath, "displaced"));
|
|
1122
|
+
const temporaryPath = path.join(transactionPath, `.retained-evidence.${crypto.randomUUID()}.tmp`);
|
|
1123
|
+
writePrivateFileDurably(temporaryPath, `${JSON.stringify({ version: 1, entries })}
|
|
1124
|
+
`);
|
|
1125
|
+
fs.renameSync(temporaryPath, path.join(transactionPath, "retained-evidence.json"));
|
|
1126
|
+
fsyncDirectory(transactionPath);
|
|
1127
|
+
}
|
|
1128
|
+
return entries;
|
|
1129
|
+
}
|
|
1130
|
+
function hydrationRecoveryRequiredError(workspacePath, manifest, cause) {
|
|
1131
|
+
if (manifest.version !== 5 || !manifest.transactionId) throw new Error("Guarded hydration lacks a durable transaction identity");
|
|
1132
|
+
return new WorkspaceHydrationRecoveryRequiredError(
|
|
1133
|
+
{
|
|
1134
|
+
transactionId: manifest.transactionId,
|
|
1135
|
+
transactionPath: hydrationTransactionPath(workspacePath),
|
|
1136
|
+
mounts: hydrationEvidenceMounts(manifest).map(({ id, incarnationKey, sourcePath, durabilityRootPath, workspaceRelativePath }) => {
|
|
1137
|
+
const retainedPaths = hydrationRetainedEvidence({ id, sourcePath, durabilityRootPath })?.paths;
|
|
1138
|
+
return {
|
|
1139
|
+
id,
|
|
1140
|
+
incarnationKey,
|
|
1141
|
+
sourcePath,
|
|
1142
|
+
durabilityRootPath,
|
|
1143
|
+
workspaceRelativePath,
|
|
1144
|
+
...retainedPaths?.length ? { retainedPaths } : {}
|
|
1145
|
+
};
|
|
1146
|
+
}),
|
|
1147
|
+
receiptBefore: manifest.receiptBefore
|
|
1148
|
+
},
|
|
1149
|
+
cause === void 0 ? void 0 : { cause }
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
function runHydrationRecoveryInspectJob(input) {
|
|
1153
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
1154
|
+
const workspaceStatus = lstatIfExists(workspacePath);
|
|
1155
|
+
if (!workspaceStatus) return null;
|
|
1156
|
+
if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink())
|
|
1157
|
+
throw new Error(`Invalid workspace hydration root: ${workspacePath}`);
|
|
1158
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1159
|
+
assertManagedDirectoryPath({
|
|
1160
|
+
trustedRoot: workspacePath,
|
|
1161
|
+
candidate: transactionPath,
|
|
1162
|
+
label: "Workspace hydration transaction inspection"
|
|
1163
|
+
});
|
|
1164
|
+
const status = lstatIfExists(transactionPath);
|
|
1165
|
+
if (!status) return null;
|
|
1166
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new Error(`Invalid workspace hydration transaction: ${transactionPath}`);
|
|
1167
|
+
const manifest = parseHydrationTransactionManifest(transactionPath);
|
|
1168
|
+
if (hydrationTransactionCommitted(transactionPath) || !manifest.mounts.some(({ recovery }) => recovery === "guarded")) return null;
|
|
1169
|
+
return hydrationRecoveryRequiredError(workspacePath, manifest).hydrationRecovery;
|
|
1170
|
+
}
|
|
1171
|
+
function readPreservedHydrationBases(workspacePath) {
|
|
1172
|
+
const recordPath = path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
|
|
1173
|
+
const status = lstatIfExists(recordPath);
|
|
1174
|
+
if (!status) return null;
|
|
1175
|
+
if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Preserved hydration bases are not a regular file: ${recordPath}`);
|
|
1176
|
+
return parseWorkspaceHydrationReceiptContentUnverified(recordPath, fs.readFileSync(recordPath, "utf8")).receipt;
|
|
1177
|
+
}
|
|
1178
|
+
async function readPreservedHydrationBasesAsync(workspacePath) {
|
|
1179
|
+
const recordPath = path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
|
|
1180
|
+
let status;
|
|
1181
|
+
try {
|
|
1182
|
+
status = await fs.promises.lstat(recordPath);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (error.code === "ENOENT") return null;
|
|
1185
|
+
throw error;
|
|
1186
|
+
}
|
|
1187
|
+
if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Preserved hydration bases are not a regular file: ${recordPath}`);
|
|
1188
|
+
return parseWorkspaceHydrationReceiptContentUnverified(recordPath, await fs.promises.readFile(recordPath, "utf8")).receipt;
|
|
1189
|
+
}
|
|
1190
|
+
function preserveHydrationBases(workspacePath, manifest) {
|
|
1191
|
+
const current = readHydratedWorkspaceReceipt(workspacePath);
|
|
1192
|
+
if (!workspaceHydrationReceiptsEqual(current, manifest.receiptBefore)) {
|
|
1193
|
+
throw new Error(`Hydration receipt changed before preservation of transaction ${manifest.transactionId}`);
|
|
1194
|
+
}
|
|
1195
|
+
const previous = readPreservedHydrationBases(workspacePath);
|
|
1196
|
+
const affectedIds = new Set(hydrationEvidenceMounts(manifest).map(({ id }) => id));
|
|
1197
|
+
const mounts = (current?.mounts ?? []).filter(
|
|
1198
|
+
(entry) => affectedIds.has(entry.id) || previous?.mounts.some(
|
|
1199
|
+
(preserved) => workspaceHydrationReceiptsEqual({ version: 3, mounts: [entry] }, { version: 3, mounts: [preserved] })
|
|
1200
|
+
)
|
|
1201
|
+
);
|
|
1202
|
+
const recordPath = path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
|
|
1203
|
+
const temporaryPath = `${recordPath}.${crypto.randomUUID()}.tmp`;
|
|
1204
|
+
writePrivateFileDurably(temporaryPath, `${JSON.stringify({ version: 3, mounts })}
|
|
1205
|
+
`);
|
|
1206
|
+
fs.renameSync(temporaryPath, recordPath);
|
|
1207
|
+
fsyncDirectory(path.dirname(recordPath));
|
|
1208
|
+
}
|
|
1209
|
+
function runHydrationPreserveInterruptedJob(input) {
|
|
1210
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
1211
|
+
if (!HYDRATION_TRANSACTION_ID.test(input.expectedTransactionId)) throw new Error("Invalid hydration recovery transaction identity");
|
|
1212
|
+
const stateDirectory = workspacePrivateStateDirectory(workspacePath);
|
|
1213
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1214
|
+
const archivesPath = path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_RECOVERIES.split("/"));
|
|
1215
|
+
const archivePath = path.join(archivesPath, input.expectedTransactionId);
|
|
1216
|
+
assertManagedDirectoryPath({ trustedRoot: stateDirectory, candidate: archivesPath, label: "Workspace hydration archives" });
|
|
1217
|
+
const pending = lstatIfExists(transactionPath);
|
|
1218
|
+
if (!pending) {
|
|
1219
|
+
assertManagedDirectoryPath({ trustedRoot: stateDirectory, candidate: archivePath, label: "Workspace hydration archive" });
|
|
1220
|
+
if (!lstatIfExists(archivePath))
|
|
1221
|
+
throw new Error(`No pending or preserved hydration transaction matches ${input.expectedTransactionId}`);
|
|
1222
|
+
const archived = parseHydrationTransactionManifest(archivePath, workspacePath, { verifyReceiptCommits: false });
|
|
1223
|
+
const markerPath2 = path.join(archivePath, "preserved.json");
|
|
1224
|
+
const markerStatus = fs.lstatSync(markerPath2);
|
|
1225
|
+
if (!markerStatus.isFile() || markerStatus.isSymbolicLink()) throw new Error(`Invalid hydration preservation record: ${markerPath2}`);
|
|
1226
|
+
const marker = JSON.parse(fs.readFileSync(markerPath2, "utf8"));
|
|
1227
|
+
if (archived.version !== 5 || archived.transactionId !== input.expectedTransactionId || marker.version !== 1 || marker.transactionId !== archived.transactionId) {
|
|
1228
|
+
throw new Error(`Hydration archive identity does not match ${input.expectedTransactionId}`);
|
|
1229
|
+
}
|
|
1230
|
+
const retainedEntries2 = readHydrationArchivedEvidence(archivePath, archived).map(({ mountId, sourcePath, archiveRelativePath }) => ({
|
|
1231
|
+
mountId,
|
|
1232
|
+
sourcePath,
|
|
1233
|
+
archivePath: path.join(archivePath, archiveRelativePath)
|
|
1234
|
+
}));
|
|
1235
|
+
fsyncDirectory(archivePath);
|
|
1236
|
+
fsyncDirectory(archivesPath);
|
|
1237
|
+
fsyncDirectory(stateDirectory);
|
|
1238
|
+
return {
|
|
1239
|
+
transactionId: archived.transactionId,
|
|
1240
|
+
archivePath,
|
|
1241
|
+
receiptBefore: archived.receiptBefore,
|
|
1242
|
+
preservedMountIds: hydrationEvidenceMounts(archived).map(({ id }) => id).sort(),
|
|
1243
|
+
alreadyPreserved: true,
|
|
1244
|
+
...retainedEntries2.length > 0 ? { retainedEntries: retainedEntries2 } : {}
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (!pending.isDirectory() || pending.isSymbolicLink()) throw new Error(`Invalid workspace hydration transaction: ${transactionPath}`);
|
|
1248
|
+
const manifest = parseHydrationTransactionManifest(transactionPath);
|
|
1249
|
+
if (manifest.version !== 5 || manifest.transactionId !== input.expectedTransactionId) {
|
|
1250
|
+
throw new Error(
|
|
1251
|
+
`Stale hydration recovery transaction identity: expected ${input.expectedTransactionId}, pending ${manifest.transactionId ?? "legacy transaction"}`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
if (hydrationTransactionCommitted(transactionPath))
|
|
1255
|
+
throw new Error(`Hydration transaction ${manifest.transactionId} is committed; ordinary recovery must finish its receipt`);
|
|
1256
|
+
if (!manifest.mounts.some(({ recovery }) => recovery === "guarded"))
|
|
1257
|
+
throw new Error(`Hydration transaction ${manifest.transactionId} has no interrupted guarded mounts`);
|
|
1258
|
+
if (lstatIfExists(archivePath))
|
|
1259
|
+
throw new Error(`Hydration archive already exists while transaction ${manifest.transactionId} is pending`);
|
|
1260
|
+
if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), manifest.receiptBefore)) {
|
|
1261
|
+
throw new Error(`Hydration receipt changed before preservation of transaction ${manifest.transactionId}`);
|
|
1262
|
+
}
|
|
1263
|
+
for (const mount of hydrationEvidenceMounts(manifest)) {
|
|
1264
|
+
assertManagedDirectoryPath({
|
|
1265
|
+
trustedRoot: mount.durabilityRootPath,
|
|
1266
|
+
candidate: mount.sourcePath,
|
|
1267
|
+
label: `Hydration preservation mount ${mount.id}`
|
|
1268
|
+
});
|
|
1269
|
+
const source = lstatIfExists(mount.sourcePath);
|
|
1270
|
+
if (mount.existed && !source) throw new Error(`Hydration preservation mount ${mount.id} is missing: ${mount.sourcePath}`);
|
|
1271
|
+
const snapshotPath = path.join(transactionPath, "mounts", mount.snapshotName);
|
|
1272
|
+
assertManagedDirectoryPath({
|
|
1273
|
+
trustedRoot: transactionPath,
|
|
1274
|
+
candidate: snapshotPath,
|
|
1275
|
+
label: `Hydration preservation snapshot ${mount.id}`
|
|
1276
|
+
});
|
|
1277
|
+
if (!lstatIfExists(snapshotPath)) throw new Error(`Hydration preservation snapshot ${mount.id} is missing`);
|
|
1278
|
+
const basis = manifest.receiptBefore?.mounts.find((entry) => entry.id === mount.id);
|
|
1279
|
+
if (!basis || basis.incarnationKey !== mount.incarnationKey || basis.sourcePath !== mount.sourcePath || basis.durabilityRootPath !== mount.durabilityRootPath || basis.workspaceRelativePath !== mount.workspaceRelativePath) {
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
`Hydration preservation mount ${mount.id} has no matching prior receipt basis; manual salvage is required. The transaction and all snapshots remain at ${transactionPath}`
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
for (const mount of hydrationEvidenceMounts(manifest)) {
|
|
1286
|
+
fsyncHydratedWorkspaceMount({ ...mount, hydrationIncarnationKey: mount.incarnationKey, sourceMode: "all", hydrateDeletionMode: "all" });
|
|
1287
|
+
}
|
|
1288
|
+
const retainedEntries = captureHydrationRetainedEvidence(transactionPath, manifest).map(
|
|
1289
|
+
({ mountId, sourcePath, archiveRelativePath }) => ({
|
|
1290
|
+
mountId,
|
|
1291
|
+
sourcePath,
|
|
1292
|
+
archivePath: path.join(archivePath, archiveRelativePath)
|
|
1293
|
+
})
|
|
1294
|
+
);
|
|
1295
|
+
fsyncTree(transactionPath);
|
|
1296
|
+
preserveHydrationBases(workspacePath, manifest);
|
|
1297
|
+
repairWorkspaceBasisRefs(workspacePath, manifest.receiptBefore);
|
|
1298
|
+
const markerPath = path.join(transactionPath, "preserved.json");
|
|
1299
|
+
const markerContent = `${JSON.stringify({ version: 1, transactionId: manifest.transactionId })}
|
|
1300
|
+
`;
|
|
1301
|
+
const existingMarker = lstatIfExists(markerPath);
|
|
1302
|
+
if (existingMarker) {
|
|
1303
|
+
if (!existingMarker.isFile() || existingMarker.isSymbolicLink() || fs.readFileSync(markerPath, "utf8") !== markerContent) {
|
|
1304
|
+
throw new Error(`Invalid hydration preservation record: ${markerPath}`);
|
|
1305
|
+
}
|
|
1306
|
+
} else {
|
|
1307
|
+
const markerTemporaryPath = path.join(transactionPath, `.preserved.${crypto.randomUUID()}.tmp`);
|
|
1308
|
+
writePrivateFileDurably(markerTemporaryPath, markerContent);
|
|
1309
|
+
fs.renameSync(markerTemporaryPath, markerPath);
|
|
1310
|
+
}
|
|
1311
|
+
fsyncDirectory(transactionPath);
|
|
1312
|
+
if (!lstatIfExists(archivesPath)) {
|
|
1313
|
+
fs.mkdirSync(archivesPath, { mode: 448 });
|
|
1314
|
+
fsyncDirectory(stateDirectory);
|
|
1315
|
+
}
|
|
1316
|
+
fs.renameSync(transactionPath, archivePath);
|
|
1317
|
+
fsyncDirectory(archivesPath);
|
|
1318
|
+
fsyncDirectory(stateDirectory);
|
|
1319
|
+
return {
|
|
1320
|
+
transactionId: manifest.transactionId,
|
|
1321
|
+
archivePath,
|
|
1322
|
+
receiptBefore: manifest.receiptBefore,
|
|
1323
|
+
preservedMountIds: hydrationEvidenceMounts(manifest).map(({ id }) => id).sort(),
|
|
1324
|
+
alreadyPreserved: false,
|
|
1325
|
+
...retainedEntries.length > 0 ? { retainedEntries } : {}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
979
1328
|
function restoreHydrationTransaction(workspacePath, manifest) {
|
|
1329
|
+
if (manifest.mounts.some(({ recovery }) => recovery === "guarded")) {
|
|
1330
|
+
throw hydrationRecoveryRequiredError(workspacePath, manifest);
|
|
1331
|
+
}
|
|
980
1332
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
981
1333
|
const durabilityScopes = /* @__PURE__ */ new Map();
|
|
982
1334
|
for (const mount of manifest.mounts) {
|
|
@@ -1039,25 +1391,132 @@ function recoverPendingHydrationTransaction(workspacePath) {
|
|
|
1039
1391
|
return;
|
|
1040
1392
|
}
|
|
1041
1393
|
const receipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1042
|
-
if (manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
|
|
1394
|
+
if (manifest.version !== 5 && manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
|
|
1043
1395
|
removeHydrationTransaction(workspacePath);
|
|
1044
1396
|
return;
|
|
1045
1397
|
}
|
|
1046
1398
|
restoreHydrationTransaction(workspacePath, manifest);
|
|
1047
1399
|
removeHydrationTransaction(workspacePath);
|
|
1048
1400
|
}
|
|
1049
|
-
function
|
|
1401
|
+
function gitOutputSync(cwd, args, action, input) {
|
|
1402
|
+
const result = Bun.spawnSync(gitCommandArgs(args), {
|
|
1403
|
+
cwd,
|
|
1404
|
+
...input ? { stdin: input } : {},
|
|
1405
|
+
stdout: "pipe",
|
|
1406
|
+
stderr: "pipe",
|
|
1407
|
+
env: workerGitProcessEnvironment()
|
|
1408
|
+
});
|
|
1409
|
+
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
1410
|
+
return Buffer.from(result.stdout);
|
|
1411
|
+
}
|
|
1412
|
+
function workspaceSubtreeChanges(workspacePath, projectedCommit, head, workspaceRelativePath) {
|
|
1413
|
+
const normalized = normalizedWorkspaceMountPath(workspaceRelativePath);
|
|
1414
|
+
const names = [`${projectedCommit}:${normalized}`, `${head}:${normalized}`];
|
|
1415
|
+
const resolved = resolveGitObjectsSync(workspacePath, names);
|
|
1416
|
+
const emptyTree = emptyTreeHash(workspacePath);
|
|
1417
|
+
const subtree = (name) => {
|
|
1418
|
+
const object = resolved.get(name);
|
|
1419
|
+
return object?.type === "tree" ? object.objectId : emptyTree;
|
|
1420
|
+
};
|
|
1421
|
+
const base = subtree(names[0]);
|
|
1422
|
+
const theirs = subtree(names[1]);
|
|
1423
|
+
if (base === theirs) return parseWorkspaceSubtreeChanges(Buffer.alloc(0));
|
|
1424
|
+
return parseWorkspaceSubtreeChanges(
|
|
1425
|
+
gitOutputSync(
|
|
1426
|
+
workspacePath,
|
|
1427
|
+
["diff-tree", "-r", "-z", "--no-renames", "--raw", base, theirs],
|
|
1428
|
+
"compare projected and hydration subtrees"
|
|
1429
|
+
)
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
function resolveGitObjectsSync(workspacePath, names) {
|
|
1433
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1434
|
+
const unique = [...new Set(names)];
|
|
1435
|
+
if (unique.length === 0) return resolved;
|
|
1436
|
+
const output = gitOutputSync(
|
|
1437
|
+
workspacePath,
|
|
1438
|
+
["cat-file", "--batch-check"],
|
|
1439
|
+
"resolve workspace objects",
|
|
1440
|
+
Buffer.from(`${unique.join("\n")}
|
|
1441
|
+
`)
|
|
1442
|
+
);
|
|
1443
|
+
const lines = output.toString().split("\n").filter((line) => line.length > 0);
|
|
1444
|
+
if (lines.length !== unique.length) {
|
|
1445
|
+
throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
|
|
1446
|
+
}
|
|
1447
|
+
unique.forEach((name, index) => {
|
|
1448
|
+
const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
|
|
1449
|
+
resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
|
|
1450
|
+
});
|
|
1451
|
+
return resolved;
|
|
1452
|
+
}
|
|
1453
|
+
function readWorkspaceBlobs(workspacePath, objectIds) {
|
|
1454
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
1455
|
+
const unique = [...new Set(objectIds)];
|
|
1456
|
+
if (unique.length === 0) return blobs;
|
|
1457
|
+
const output = gitOutputSync(
|
|
1458
|
+
workspacePath,
|
|
1459
|
+
["cat-file", "--batch"],
|
|
1460
|
+
"read projected workspace blobs",
|
|
1461
|
+
Buffer.from(`${unique.join("\n")}
|
|
1462
|
+
`)
|
|
1463
|
+
);
|
|
1464
|
+
let offset = 0;
|
|
1465
|
+
while (offset < output.length) {
|
|
1466
|
+
const newline = output.indexOf(10, offset);
|
|
1467
|
+
if (newline < 0) break;
|
|
1468
|
+
const header = output.subarray(offset, newline).toString();
|
|
1469
|
+
offset = newline + 1;
|
|
1470
|
+
const match = /^([0-9a-f]{40,64}) (\S+) (\d+)$/u.exec(header);
|
|
1471
|
+
if (!match) {
|
|
1472
|
+
if (/ missing$/u.test(header)) continue;
|
|
1473
|
+
throw new Error(`Unexpected cat-file record while reading projected blobs: ${header}`);
|
|
1474
|
+
}
|
|
1475
|
+
const size = Number(match[3]);
|
|
1476
|
+
if (match[2] === "blob") blobs.set(match[1], Buffer.from(output.subarray(offset, offset + size)));
|
|
1477
|
+
offset += size + 1;
|
|
1478
|
+
}
|
|
1479
|
+
return blobs;
|
|
1480
|
+
}
|
|
1481
|
+
function revertReceiptMounts(target, before, mountIds) {
|
|
1482
|
+
if (!target || mountIds.size === 0) return target;
|
|
1483
|
+
const beforeById = new Map(before?.mounts.map((entry) => [entry.id, entry]) ?? []);
|
|
1484
|
+
const mounts = target.mounts.flatMap((entry) => {
|
|
1485
|
+
if (!mountIds.has(entry.id)) return [entry];
|
|
1486
|
+
const previous = beforeById.get(entry.id);
|
|
1487
|
+
return previous ? [previous] : [];
|
|
1488
|
+
}).sort(compareWorkspaceHydrationMountBasis);
|
|
1489
|
+
return { version: 3, mounts };
|
|
1490
|
+
}
|
|
1491
|
+
function rewriteHydrationTransactionManifest(workspacePath, manifest) {
|
|
1492
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1493
|
+
const temporaryPath = path.join(transactionPath, `.manifest.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1494
|
+
try {
|
|
1495
|
+
writePrivateFileDurably(temporaryPath, `${JSON.stringify(manifest)}
|
|
1496
|
+
`);
|
|
1497
|
+
fs.renameSync(temporaryPath, path.join(transactionPath, "manifest.json"));
|
|
1498
|
+
fsyncDirectory(transactionPath);
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
1501
|
+
throw error;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
function beginHydrationTransaction(workspacePath, mounts, targetReceipt, mergeBases) {
|
|
1050
1505
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1051
1506
|
const stateDirectory = workspacePrivateStateDirectory(workspacePath);
|
|
1052
1507
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1053
1508
|
const stagingPath = path.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1054
1509
|
fs.mkdirSync(stagingPath, { mode: 448 });
|
|
1055
1510
|
const durabilityScopes = /* @__PURE__ */ new Map();
|
|
1511
|
+
const mergePlans = /* @__PURE__ */ new Map();
|
|
1512
|
+
const skippedMounts = [];
|
|
1056
1513
|
try {
|
|
1057
1514
|
const snapshotRoot = path.join(stagingPath, "mounts");
|
|
1058
1515
|
fs.mkdirSync(snapshotRoot, { mode: 448 });
|
|
1059
1516
|
const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
|
|
1060
|
-
const
|
|
1517
|
+
const head = mergeBases && Object.keys(mergeBases).length > 0 ? revParse(workspacePath, "HEAD") : null;
|
|
1518
|
+
const manifestMounts = [];
|
|
1519
|
+
for (const mount of mounts) {
|
|
1061
1520
|
const sourcePath = path.resolve(mount.sourcePath);
|
|
1062
1521
|
const durabilityRootPath = path.resolve(mount.durabilityRootPath);
|
|
1063
1522
|
assertManagedDirectoryPath({
|
|
@@ -1069,45 +1528,89 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1069
1528
|
if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
|
|
1070
1529
|
throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
|
|
1071
1530
|
}
|
|
1072
|
-
const snapshotName = String(
|
|
1531
|
+
const snapshotName = String(manifestMounts.length);
|
|
1073
1532
|
const snapshotPath = path.join(snapshotRoot, snapshotName);
|
|
1074
|
-
fs.mkdirSync(snapshotPath, { mode: 448 });
|
|
1075
1533
|
let scope;
|
|
1076
|
-
|
|
1534
|
+
const basis = mergeBases?.[mount.id];
|
|
1535
|
+
if (sourceStatus && mount.hydrateDeletionMode === "git" && basis && head) {
|
|
1077
1536
|
const outerPath = workspaceMountOuterPath(workspacePath, mount);
|
|
1078
1537
|
assertManagedDirectoryPath({
|
|
1079
1538
|
trustedRoot: workspacePath,
|
|
1080
1539
|
candidate: outerPath,
|
|
1081
1540
|
label: `Workspace mount ${mount.id} outer source`
|
|
1082
1541
|
});
|
|
1083
|
-
const
|
|
1542
|
+
const prepared = prepareWorkingTreeMirror({
|
|
1084
1543
|
sourceRoot: outerPath,
|
|
1085
1544
|
targetRoot: sourcePath,
|
|
1086
1545
|
sourceMode: "all",
|
|
1087
1546
|
deletionMode: "git",
|
|
1088
1547
|
sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
|
|
1089
1548
|
});
|
|
1549
|
+
const decision = decideWorkspaceHydrationMerge({
|
|
1550
|
+
prepared,
|
|
1551
|
+
changes: workspaceSubtreeChanges(workspacePath, basis.projectedCommit, head, mount.workspaceRelativePath),
|
|
1552
|
+
projectedFiles: basis.projectedFiles,
|
|
1553
|
+
...basis.readAtMs === void 0 ? {} : { readAtMs: basis.readAtMs },
|
|
1554
|
+
readBaseBlobs: (objectIds) => readWorkspaceBlobs(workspacePath, objectIds),
|
|
1555
|
+
readBinaryPaths: (relativePaths) => readHydrationBinaryPaths({ checkoutRoot: prepared.targetRoot, outerRoot: prepared.sourceRoot, relativePaths })
|
|
1556
|
+
});
|
|
1557
|
+
if (decision.kind !== "apply") {
|
|
1558
|
+
skippedMounts.push({
|
|
1559
|
+
mountId: mount.id,
|
|
1560
|
+
reason: decision.kind === "conflict" ? "hydration_merge_conflict" : "hydration_target_changed",
|
|
1561
|
+
paths: decision.paths
|
|
1562
|
+
});
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
fs.mkdirSync(snapshotPath, { mode: 448 });
|
|
1566
|
+
const plan = planPreparedWorkingTreeMirror(prepared, { selection: decision.selection, overrides: decision.overrides });
|
|
1090
1567
|
scope = captureWorkingTreeMirrorPlan({
|
|
1091
1568
|
plan,
|
|
1092
1569
|
targetRoot: sourcePath,
|
|
1093
1570
|
snapshotRoot: snapshotPath,
|
|
1094
|
-
// Same private-staging deferral as the complete snapshot below.
|
|
1095
1571
|
durability: "deferred_private_staging"
|
|
1096
1572
|
});
|
|
1097
|
-
durabilityScopes.set(mount.id, [...plan.desired.keys()]);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1573
|
+
durabilityScopes.set(mount.id, [.../* @__PURE__ */ new Set([...plan.desired.keys(), ...decision.selection.remove])]);
|
|
1574
|
+
mergePlans.set(mount.id, { prepared, decision, plan, snapshotPath: path.join(transactionPath, "mounts", snapshotName), basis });
|
|
1575
|
+
} else if (sourceStatus && mount.hydrateDeletionMode === "git") {
|
|
1576
|
+
fs.mkdirSync(snapshotPath, { mode: 448 });
|
|
1577
|
+
const outerPath = workspaceMountOuterPath(workspacePath, mount);
|
|
1578
|
+
assertManagedDirectoryPath({
|
|
1579
|
+
trustedRoot: workspacePath,
|
|
1580
|
+
candidate: outerPath,
|
|
1581
|
+
label: `Workspace mount ${mount.id} outer source`
|
|
1582
|
+
});
|
|
1583
|
+
const plan = planWorkingTreeMirror({
|
|
1584
|
+
sourceRoot: outerPath,
|
|
1585
|
+
targetRoot: sourcePath,
|
|
1102
1586
|
sourceMode: "all",
|
|
1103
|
-
deletionMode: "
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1587
|
+
deletionMode: "git",
|
|
1588
|
+
sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
|
|
1589
|
+
});
|
|
1590
|
+
scope = captureWorkingTreeMirrorPlan({
|
|
1591
|
+
plan,
|
|
1592
|
+
targetRoot: sourcePath,
|
|
1593
|
+
snapshotRoot: snapshotPath,
|
|
1594
|
+
// Same private-staging deferral as the complete snapshot below.
|
|
1107
1595
|
durability: "deferred_private_staging"
|
|
1108
1596
|
});
|
|
1597
|
+
durabilityScopes.set(mount.id, [...plan.desired.keys()]);
|
|
1598
|
+
} else {
|
|
1599
|
+
fs.mkdirSync(snapshotPath, { mode: 448 });
|
|
1600
|
+
if (sourceStatus) {
|
|
1601
|
+
mirrorWorkingTree({
|
|
1602
|
+
sourceRoot: sourcePath,
|
|
1603
|
+
targetRoot: snapshotPath,
|
|
1604
|
+
sourceMode: "all",
|
|
1605
|
+
deletionMode: "all",
|
|
1606
|
+
// This tree is still private and the source authority is untouched.
|
|
1607
|
+
// The single recursive barrier below makes the complete snapshot
|
|
1608
|
+
// durable before its directory entry is published or hydration starts.
|
|
1609
|
+
durability: "deferred_private_staging"
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1109
1612
|
}
|
|
1110
|
-
|
|
1613
|
+
manifestMounts.push({
|
|
1111
1614
|
id: mount.id,
|
|
1112
1615
|
incarnationKey: mount.hydrationIncarnationKey,
|
|
1113
1616
|
sourcePath,
|
|
@@ -1115,23 +1618,27 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1115
1618
|
workspaceRelativePath: mount.workspaceRelativePath,
|
|
1116
1619
|
existed: Boolean(sourceStatus),
|
|
1117
1620
|
snapshotName,
|
|
1118
|
-
...scope ? { scope } : {}
|
|
1119
|
-
|
|
1120
|
-
|
|
1621
|
+
...scope ? { scope } : {},
|
|
1622
|
+
recovery: mergePlans.has(mount.id) ? "guarded" : "snapshot"
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
const effectiveTargetReceipt = revertReceiptMounts(targetReceipt, receiptBefore, new Set(skippedMounts.map(({ mountId }) => mountId)));
|
|
1626
|
+
const guarded = mergePlans.size > 0;
|
|
1121
1627
|
const manifest = {
|
|
1122
|
-
// Older workers reject
|
|
1123
|
-
//
|
|
1124
|
-
version: manifestMounts.some((mount) => mount.scope) ? 4 : 3,
|
|
1125
|
-
|
|
1628
|
+
// Older workers reject guarded transactions rather than replaying them
|
|
1629
|
+
// through an unguarded snapshot restore after a downgrade.
|
|
1630
|
+
version: guarded ? 5 : manifestMounts.some((mount) => mount.scope) ? 4 : 3,
|
|
1631
|
+
...guarded ? { transactionId: crypto.randomUUID() } : {},
|
|
1632
|
+
targetReceipt: effectiveTargetReceipt,
|
|
1126
1633
|
receiptBefore,
|
|
1127
|
-
mounts: manifestMounts
|
|
1634
|
+
mounts: guarded ? manifestMounts : manifestMounts.map(({ recovery: _recovery, ...mount }) => mount)
|
|
1128
1635
|
};
|
|
1129
1636
|
writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
|
|
1130
1637
|
`);
|
|
1131
1638
|
fsyncTree(stagingPath);
|
|
1132
1639
|
fs.renameSync(stagingPath, transactionPath);
|
|
1133
1640
|
fsyncDirectory(stateDirectory);
|
|
1134
|
-
return { manifest, durabilityScopes };
|
|
1641
|
+
return { manifest, durabilityScopes, mergePlans, skippedMounts, targetReceipt: effectiveTargetReceipt };
|
|
1135
1642
|
} catch (error) {
|
|
1136
1643
|
fs.rmSync(stagingPath, { recursive: true, force: true });
|
|
1137
1644
|
fsyncDirectory(stateDirectory);
|
|
@@ -1140,7 +1647,9 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1140
1647
|
}
|
|
1141
1648
|
function markHydrationTransactionDurable(workspacePath) {
|
|
1142
1649
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1143
|
-
|
|
1650
|
+
const temporaryPath = path.join(transactionPath, `.committed.${crypto.randomUUID()}.tmp`);
|
|
1651
|
+
writePrivateFileDurably(temporaryPath, "committed\n");
|
|
1652
|
+
fs.renameSync(temporaryPath, path.join(transactionPath, "committed"));
|
|
1144
1653
|
fsyncDirectory(transactionPath);
|
|
1145
1654
|
}
|
|
1146
1655
|
function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
|
|
@@ -1292,6 +1801,22 @@ async function configureWorkspaceRepository(input) {
|
|
|
1292
1801
|
"configure durable workspace commits and references"
|
|
1293
1802
|
);
|
|
1294
1803
|
await gitAsync(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
|
|
1804
|
+
const attributesPath = path.join(input.workspacePath, ".git", "info", "attributes");
|
|
1805
|
+
const attributes = [
|
|
1806
|
+
"# r5d workspace clone: checkout bytes are carried verbatim; no conversion or filter may change what projection reads or hydration writes.",
|
|
1807
|
+
"* -text -eol -filter -ident -working-tree-encoding",
|
|
1808
|
+
""
|
|
1809
|
+
].join("\n");
|
|
1810
|
+
let currentAttributes = null;
|
|
1811
|
+
try {
|
|
1812
|
+
currentAttributes = await fs.promises.readFile(attributesPath, "utf8");
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
if (error.code !== "ENOENT") throw error;
|
|
1815
|
+
}
|
|
1816
|
+
if (currentAttributes !== attributes) {
|
|
1817
|
+
await fs.promises.mkdir(path.dirname(attributesPath), { recursive: true });
|
|
1818
|
+
await fs.promises.writeFile(attributesPath, attributes, "utf8");
|
|
1819
|
+
}
|
|
1295
1820
|
}
|
|
1296
1821
|
async function configureExistingWorkspaceGitForRemediation(input) {
|
|
1297
1822
|
const workspacePath = path.resolve(input.workspacePath);
|
|
@@ -1423,7 +1948,7 @@ function activeMounts(mounts) {
|
|
|
1423
1948
|
}
|
|
1424
1949
|
return { active, tombstones, skipped };
|
|
1425
1950
|
}
|
|
1426
|
-
function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
1951
|
+
function mirrorMountsToWorkspace(workspacePath, mounts, projectedFiles) {
|
|
1427
1952
|
const projected = [];
|
|
1428
1953
|
for (const mount of mounts) {
|
|
1429
1954
|
assertManagedDirectoryPath({
|
|
@@ -1440,7 +1965,8 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
|
1440
1965
|
sourceRoot: mount.sourcePath,
|
|
1441
1966
|
targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
1442
1967
|
sourceMode: mount.sourceMode,
|
|
1443
|
-
deletionMode: "all"
|
|
1968
|
+
deletionMode: "all",
|
|
1969
|
+
...projectedFiles ? { projected: projectedFiles } : {}
|
|
1444
1970
|
});
|
|
1445
1971
|
projected.push({ mount, gitlinks: mirrored.gitlinks });
|
|
1446
1972
|
}
|
|
@@ -1551,11 +2077,60 @@ function outerSubtreeGitlinks(workspacePath, mount) {
|
|
|
1551
2077
|
function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
1552
2078
|
const hydratedMountIds = [];
|
|
1553
2079
|
const skippedMountIds = [];
|
|
2080
|
+
const mergeSkips = [];
|
|
2081
|
+
const rollbackScopes = /* @__PURE__ */ new Map();
|
|
2082
|
+
const preBlobs = {};
|
|
1554
2083
|
for (const mount of mounts) {
|
|
1555
2084
|
if (!options.ignoreBusy && mount.busy?.()) {
|
|
1556
2085
|
skippedMountIds.push(mount.id);
|
|
1557
2086
|
continue;
|
|
1558
2087
|
}
|
|
2088
|
+
const mergePlan = options.mergePlans?.get(mount.id);
|
|
2089
|
+
if (mergePlan) {
|
|
2090
|
+
const journal = { entries: [] };
|
|
2091
|
+
const runtimeJournal = { journal, snapshotPath: mergePlan.snapshotPath };
|
|
2092
|
+
options.journals?.set(mount.id, runtimeJournal);
|
|
2093
|
+
try {
|
|
2094
|
+
applyWorkingTreeMirror(mergePlan.prepared, {
|
|
2095
|
+
selection: mergePlan.decision.selection,
|
|
2096
|
+
guards: {
|
|
2097
|
+
expectations: mergePlan.decision.expectations,
|
|
2098
|
+
overrides: mergePlan.decision.overrides,
|
|
2099
|
+
preBlobs: mergePlan.decision.preBlobs,
|
|
2100
|
+
journal
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
if (!(error instanceof WorkingTreeTargetChangedError)) throw error;
|
|
2105
|
+
const rolledBack = rollbackWorkingTreeMirrorJournal({
|
|
2106
|
+
journal,
|
|
2107
|
+
targetRoot: mount.sourcePath,
|
|
2108
|
+
snapshotRoot: mergePlan.snapshotPath
|
|
2109
|
+
});
|
|
2110
|
+
runtimeJournal.rolledBack = rolledBack;
|
|
2111
|
+
const retainedPaths = [
|
|
2112
|
+
...new Set([...error.retained, ...rolledBack.retained ?? []].map(({ path: retainedPath }) => retainedPath))
|
|
2113
|
+
].sort();
|
|
2114
|
+
if (retainedPaths.length > 0) {
|
|
2115
|
+
process.stderr.write(
|
|
2116
|
+
`[r5d-worker] hydration preserved displaced writer entries for mount ${mount.id}: ${retainedPaths.join(", ")}
|
|
2117
|
+
`
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
options.onMergeRollback?.(mount, rolledBack);
|
|
2121
|
+
mergeSkips.push({
|
|
2122
|
+
mountId: mount.id,
|
|
2123
|
+
reason: "hydration_target_changed",
|
|
2124
|
+
paths: [.../* @__PURE__ */ new Set([error.relativePath, ...rolledBack.abandoned])].sort(),
|
|
2125
|
+
...retainedPaths.length > 0 ? { retainedPaths } : {}
|
|
2126
|
+
});
|
|
2127
|
+
rollbackScopes.set(mount.id, rolledBack.paths);
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
hydratedMountIds.push(mount.id);
|
|
2131
|
+
preBlobs[mount.id] = Object.fromEntries(mergePlan.decision.preBlobs);
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
1559
2134
|
const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
|
|
1560
2135
|
assertManagedDirectoryPath({
|
|
1561
2136
|
trustedRoot: workspacePath,
|
|
@@ -1601,7 +2176,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1601
2176
|
});
|
|
1602
2177
|
hydratedMountIds.push(mount.id);
|
|
1603
2178
|
}
|
|
1604
|
-
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
|
|
2179
|
+
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort(), mergeSkips, rollbackScopes, preBlobs };
|
|
1605
2180
|
}
|
|
1606
2181
|
async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
|
|
1607
2182
|
const candidates = mounts.flatMap((mount) => {
|
|
@@ -1661,7 +2236,7 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1661
2236
|
};
|
|
1662
2237
|
const noopCandidate = computeSelection();
|
|
1663
2238
|
if (noopCandidate.hydrationMounts.length === 0 && noopCandidate.advancedDurabilityMounts.length === 0 && noopCandidate.targetReceipt && lstatIfExists(hydrationTransactionPath(workspacePath)) === null && workspaceHydrationReceiptsEqual(receiptBeforeTransaction, noopCandidate.targetReceipt) && await hydratedWorkspaceReceiptFileIsV3(workspacePath) && await workspaceBasisRefsAreCurrentAsync(workspacePath, noopCandidate.targetReceipt)) {
|
|
1664
|
-
return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
|
|
2239
|
+
return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds, mergeSkips: [] };
|
|
1665
2240
|
}
|
|
1666
2241
|
const hooks = input.hydrationHooks;
|
|
1667
2242
|
const context = {
|
|
@@ -1676,13 +2251,30 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1676
2251
|
const heldMountIds = [...new Set([...hydrationMounts, ...advancedDurabilityMounts].map(({ id }) => id))];
|
|
1677
2252
|
const releaseHold = hooks?.holdMounts(heldMountIds, `hydration transaction ${context.transactionId}`);
|
|
1678
2253
|
try {
|
|
2254
|
+
const mergeBases = {};
|
|
2255
|
+
for (const mount of hydrationMounts) {
|
|
2256
|
+
const basis = input.mergeBases?.get(mount.id);
|
|
2257
|
+
if (basis) mergeBases[mount.id] = basis;
|
|
2258
|
+
}
|
|
1679
2259
|
const hydration = await workspaceFilesystemExecutor().run("hydration_transaction", {
|
|
1680
2260
|
workspacePath,
|
|
1681
2261
|
hydrationMounts: hydrationMounts.map(workspaceGitMountData),
|
|
1682
2262
|
advancedDurabilityMounts: advancedDurabilityMounts.map(workspaceGitMountData),
|
|
1683
|
-
targetReceipt
|
|
2263
|
+
targetReceipt,
|
|
2264
|
+
...Object.keys(mergeBases).length > 0 ? { mergeBases } : {}
|
|
1684
2265
|
});
|
|
1685
|
-
|
|
2266
|
+
const ledger = hydrationPreBlobLedger(workspacePath);
|
|
2267
|
+
for (const mount of hydrationMounts) {
|
|
2268
|
+
if (!hydration.hydratedMountIds.includes(mount.id)) continue;
|
|
2269
|
+
const preBlobs = mergeBases[mount.id] ? hydration.hydratedPreBlobs[mount.id] : void 0;
|
|
2270
|
+
if (preBlobs) ledger.record(mount, preBlobs);
|
|
2271
|
+
else ledger.clearMount(mount);
|
|
2272
|
+
}
|
|
2273
|
+
return {
|
|
2274
|
+
hydratedMountIds: hydration.hydratedMountIds,
|
|
2275
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...selection.skippedMountIds, ...hydration.skippedMounts.map(({ mountId }) => mountId)])].sort(),
|
|
2276
|
+
mergeSkips: hydration.skippedMounts
|
|
2277
|
+
};
|
|
1686
2278
|
} finally {
|
|
1687
2279
|
releaseHold?.();
|
|
1688
2280
|
}
|
|
@@ -1692,28 +2284,90 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1692
2284
|
}
|
|
1693
2285
|
function runHydrationTransactionJob(input) {
|
|
1694
2286
|
const workspacePath = path.resolve(input.workspacePath);
|
|
1695
|
-
const { hydrationMounts, advancedDurabilityMounts
|
|
1696
|
-
const
|
|
2287
|
+
const { hydrationMounts, advancedDurabilityMounts } = input;
|
|
2288
|
+
const begun = beginHydrationTransaction(workspacePath, hydrationMounts, input.targetReceipt, input.mergeBases);
|
|
2289
|
+
const { durabilityScopes, mergePlans, skippedMounts } = begun;
|
|
2290
|
+
let manifest = begun.manifest;
|
|
2291
|
+
let targetReceipt = begun.targetReceipt;
|
|
2292
|
+
const skippedIds = new Set(skippedMounts.map(({ mountId }) => mountId));
|
|
2293
|
+
const journals = /* @__PURE__ */ new Map();
|
|
2294
|
+
const excludeRolledBackMount = (mount, rollback) => {
|
|
2295
|
+
const settledMount = manifest.mounts.find(({ id }) => id === mount.id);
|
|
2296
|
+
if (!settledMount) return;
|
|
2297
|
+
fsyncHydratedWorkspaceMount(mount, rollback.paths);
|
|
2298
|
+
fsyncHydrationRetainedEvidence(mount);
|
|
2299
|
+
if (rollback.retained?.length) {
|
|
2300
|
+
process.stderr.write(
|
|
2301
|
+
`[r5d-worker] hydration rollback retained writer entries for mount ${mount.id}: ${rollback.retained.map(({ path: retainedPath }) => retainedPath).join(", ")}
|
|
2302
|
+
`
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
const nextReceipt = revertReceiptMounts(targetReceipt, manifest.receiptBefore, /* @__PURE__ */ new Set([mount.id]));
|
|
2306
|
+
const nextManifest = {
|
|
2307
|
+
...manifest,
|
|
2308
|
+
targetReceipt: nextReceipt,
|
|
2309
|
+
mounts: manifest.mounts.filter(({ id }) => id !== mount.id),
|
|
2310
|
+
preservedMounts: [...manifest.preservedMounts ?? [], settledMount]
|
|
2311
|
+
};
|
|
2312
|
+
rewriteHydrationTransactionManifest(workspacePath, nextManifest);
|
|
2313
|
+
manifest = nextManifest;
|
|
2314
|
+
targetReceipt = nextReceipt;
|
|
2315
|
+
};
|
|
1697
2316
|
try {
|
|
1698
|
-
const
|
|
1699
|
-
const
|
|
2317
|
+
const activeMounts2 = hydrationMounts.filter(({ id }) => !skippedIds.has(id));
|
|
2318
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, activeMounts2, {
|
|
2319
|
+
ignoreBusy: true,
|
|
2320
|
+
mergePlans,
|
|
2321
|
+
journals,
|
|
2322
|
+
onMergeRollback: excludeRolledBackMount
|
|
2323
|
+
});
|
|
2324
|
+
if (hydration.mergeSkips.length > 0) {
|
|
2325
|
+
for (const skip of hydration.mergeSkips) {
|
|
2326
|
+
skippedMounts.push(skip);
|
|
2327
|
+
skippedIds.add(skip.mountId);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
const expectedHydratedIds = activeMounts2.map(({ id }) => id).filter((id) => !skippedIds.has(id)).sort();
|
|
1700
2331
|
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
1701
2332
|
throw new Error("Workspace hydration did not include every required mount");
|
|
1702
2333
|
}
|
|
1703
|
-
fsyncHydratedWorkspaceMounts(
|
|
2334
|
+
fsyncHydratedWorkspaceMounts(
|
|
2335
|
+
advancedDurabilityMounts.filter(({ id }) => !skippedIds.has(id)),
|
|
2336
|
+
durabilityScopes
|
|
2337
|
+
);
|
|
1704
2338
|
markHydrationTransactionDurable(workspacePath);
|
|
1705
2339
|
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1706
2340
|
removeHydrationTransaction(workspacePath);
|
|
1707
|
-
return { hydratedMountIds: hydration.hydratedMountIds };
|
|
2341
|
+
return { hydratedMountIds: hydration.hydratedMountIds, skippedMounts, hydratedPreBlobs: hydration.preBlobs };
|
|
1708
2342
|
} catch (error) {
|
|
1709
2343
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1710
2344
|
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1711
2345
|
throw error;
|
|
1712
2346
|
}
|
|
1713
2347
|
try {
|
|
2348
|
+
for (const [mountId, mergePlan] of mergePlans) {
|
|
2349
|
+
const mount = hydrationMounts.find(({ id }) => id === mountId);
|
|
2350
|
+
if (!mount || !manifest.mounts.some(({ id }) => id === mountId)) continue;
|
|
2351
|
+
const runtimeJournal = journals.get(mountId);
|
|
2352
|
+
const rolledBack = runtimeJournal ? runtimeJournal.rolledBack ?? rollbackWorkingTreeMirrorJournal({
|
|
2353
|
+
journal: runtimeJournal.journal,
|
|
2354
|
+
targetRoot: mount.sourcePath,
|
|
2355
|
+
snapshotRoot: mergePlan.snapshotPath
|
|
2356
|
+
}) : { paths: [], abandoned: [] };
|
|
2357
|
+
if (runtimeJournal) runtimeJournal.rolledBack = rolledBack;
|
|
2358
|
+
excludeRolledBackMount(mount, rolledBack);
|
|
2359
|
+
}
|
|
1714
2360
|
restoreHydrationTransaction(workspacePath, manifest);
|
|
1715
2361
|
removeHydrationTransaction(workspacePath);
|
|
1716
2362
|
} catch (restoreError) {
|
|
2363
|
+
const pendingManifest = parseHydrationTransactionManifest(transactionPath);
|
|
2364
|
+
if (pendingManifest.mounts.some(({ recovery }) => recovery === "guarded")) {
|
|
2365
|
+
throw hydrationRecoveryRequiredError(
|
|
2366
|
+
workspacePath,
|
|
2367
|
+
pendingManifest,
|
|
2368
|
+
new AggregateError([error, restoreError], "Guarded hydration rollback did not settle")
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
1717
2371
|
throw new AggregateError(
|
|
1718
2372
|
[error, restoreError],
|
|
1719
2373
|
`Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -1775,6 +2429,7 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
|
|
|
1775
2429
|
});
|
|
1776
2430
|
const currentHead = await revParseAsync(workspacePath, "HEAD");
|
|
1777
2431
|
const currentReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
2432
|
+
const preservedBases = await readPreservedHydrationBasesAsync(workspacePath);
|
|
1778
2433
|
if (!currentHead) return;
|
|
1779
2434
|
const outerHeadContainsMount = (mount) => tryGitAsync(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
|
|
1780
2435
|
const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
|
|
@@ -1784,7 +2439,7 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
|
|
|
1784
2439
|
if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
|
|
1785
2440
|
const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1786
2441
|
const recoveryMounts = uncoveredBasisMounts.filter(
|
|
1787
|
-
(mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
|
|
2442
|
+
(mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id) && workspaceHydrationReceiptMountBasisHead(preservedBases, mount) !== workspaceHydrationReceiptMountBasisHead(currentReceipt, mount)
|
|
1788
2443
|
);
|
|
1789
2444
|
const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
|
|
1790
2445
|
const hydrationTargets = [];
|
|
@@ -1814,6 +2469,7 @@ async function resetWorkspaceGit(input) {
|
|
|
1814
2469
|
if (busyResetMountIds.length > 0) {
|
|
1815
2470
|
throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
|
|
1816
2471
|
}
|
|
2472
|
+
hydrationPreBlobLedger(workspacePath).clear();
|
|
1817
2473
|
const initial = await ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1818
2474
|
await workspaceFilesystemExecutor().run("hydration_recovery", { workspacePath, preserveResolutionInProgress: false });
|
|
1819
2475
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
@@ -2157,6 +2813,24 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2157
2813
|
const mergedProjectionMounts = [];
|
|
2158
2814
|
const projectionSkippedIds = /* @__PURE__ */ new Set();
|
|
2159
2815
|
let projectionWarning;
|
|
2816
|
+
const projectedMountFiles = /* @__PURE__ */ new Map();
|
|
2817
|
+
const mergedProjectionBases = /* @__PURE__ */ new Map();
|
|
2818
|
+
const projectedMountBases = /* @__PURE__ */ new Map();
|
|
2819
|
+
const mountSkipReasons = {};
|
|
2820
|
+
const staleRewriteLedger = hydrationPreBlobLedger(workspacePath);
|
|
2821
|
+
const staleRewriteBlobsFor = (mount) => {
|
|
2822
|
+
const preBlobs = staleRewriteLedger.preBlobs(mount);
|
|
2823
|
+
return preBlobs.size > 0 ? Object.fromEntries(preBlobs) : void 0;
|
|
2824
|
+
};
|
|
2825
|
+
const recordStaleRewrite = (mount, paths) => {
|
|
2826
|
+
projectionSkippedIds.add(mount.id);
|
|
2827
|
+
mountSkipReasons[mount.id] = { reason: "stale_rewrite_detected", paths: [...paths] };
|
|
2828
|
+
const holders = mount.describeHolders?.();
|
|
2829
|
+
process.stderr.write(
|
|
2830
|
+
`[r5d-worker] stale rewrite detected in mount ${mount.id}: ${describeHydrationSkipPaths(mount.sourcePath, paths)} hold exactly the bytes the last hydration replaced; the mount stays pinned at its recorded basis until they change${holders ? `; holders: ${holders}` : ""}
|
|
2831
|
+
`
|
|
2832
|
+
);
|
|
2833
|
+
};
|
|
2160
2834
|
const refreshCycleSkippedMounts = (mounts = input.mounts) => {
|
|
2161
2835
|
for (const mount of mounts) {
|
|
2162
2836
|
const projectionToken = projectionMutationTokens.get(mount.id);
|
|
@@ -2179,7 +2853,8 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2179
2853
|
const token = projectionMutationTokens.get(id);
|
|
2180
2854
|
return token === void 0 ? [] : [[id, token]];
|
|
2181
2855
|
})
|
|
2182
|
-
)
|
|
2856
|
+
),
|
|
2857
|
+
...Object.keys(mountSkipReasons).length > 0 ? { mountSkipReasons: { ...mountSkipReasons } } : {}
|
|
2183
2858
|
};
|
|
2184
2859
|
};
|
|
2185
2860
|
const mountProjectionIsCurrent = (mount) => mount.lastProjectedMutationToken !== void 0 && !mount.deleteWhenSourceMissing && mount.preserveLocalOnHydrationBasisChange !== true && projectionBasisHead !== null && workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount) === projectionBasisHead && projectionMutationTokens.get(mount.id) === mount.lastProjectedMutationToken && mount.mutationToken?.() === mount.lastProjectedMutationToken;
|
|
@@ -2209,6 +2884,7 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2209
2884
|
const releaseMergeHold = input.hydrationHooks?.holdMounts([mount.id], `merge projection of mount ${mount.id}`);
|
|
2210
2885
|
let merged;
|
|
2211
2886
|
try {
|
|
2887
|
+
const staleRewriteBlobs = staleRewriteBlobsFor(mount);
|
|
2212
2888
|
merged = await workspaceFilesystemExecutor().run("projection_merge", {
|
|
2213
2889
|
workspacePath,
|
|
2214
2890
|
mount: {
|
|
@@ -2219,11 +2895,22 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2219
2895
|
},
|
|
2220
2896
|
basisHead,
|
|
2221
2897
|
currentHead: projectionBasisHead,
|
|
2222
|
-
attemptId
|
|
2898
|
+
attemptId,
|
|
2899
|
+
...staleRewriteBlobs ? { staleRewriteBlobs } : {}
|
|
2223
2900
|
});
|
|
2224
2901
|
} finally {
|
|
2225
2902
|
releaseMergeHold?.();
|
|
2226
2903
|
}
|
|
2904
|
+
if (merged.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, merged.staleRewriteCleared);
|
|
2905
|
+
if (merged.kind === "stale_rewrite") {
|
|
2906
|
+
recordStaleRewrite(mount, merged.paths);
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
mergedProjectionBases.set(mount.id, {
|
|
2910
|
+
projectedCommit: merged.oursCommit,
|
|
2911
|
+
projectedFiles: merged.projectedFiles,
|
|
2912
|
+
readAtMs: merged.readAtMs
|
|
2913
|
+
});
|
|
2227
2914
|
if (merged.kind === "conflict") {
|
|
2228
2915
|
const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
|
|
2229
2916
|
return {
|
|
@@ -2276,17 +2963,44 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2276
2963
|
currentHeadBeforeHydration,
|
|
2277
2964
|
uncoveredCompletionMounts
|
|
2278
2965
|
) : /* @__PURE__ */ new Set();
|
|
2966
|
+
const hydrationMounts = uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id));
|
|
2967
|
+
const mergeBases = /* @__PURE__ */ new Map();
|
|
2968
|
+
if (workspaceMergeHydrationEnabled()) {
|
|
2969
|
+
for (const mount of hydrationMounts) {
|
|
2970
|
+
const basis = projectedMountBases.get(mount.id);
|
|
2971
|
+
if (basis) mergeBases.set(mount.id, basis);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2279
2974
|
const hydration = await hydrateWorkspaceGitMountsTransactionally({
|
|
2280
2975
|
workspacePath,
|
|
2281
|
-
mounts:
|
|
2976
|
+
mounts: hydrationMounts,
|
|
2282
2977
|
durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2283
2978
|
carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
|
|
2284
2979
|
receiptMounts: completionReceiptMounts,
|
|
2285
2980
|
requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2286
2981
|
recordCurrentHead: true,
|
|
2287
|
-
hydrationHooks: input.hydrationHooks
|
|
2982
|
+
hydrationHooks: input.hydrationHooks,
|
|
2983
|
+
...mergeBases.size > 0 ? { mergeBases } : {}
|
|
2288
2984
|
});
|
|
2289
2985
|
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
2986
|
+
if (currentHeadBeforeHydration) {
|
|
2987
|
+
for (const id of hydration.hydratedMountIds) {
|
|
2988
|
+
if (mergeBases.has(id)) projectedMountBases.set(id, { projectedCommit: currentHeadBeforeHydration, projectedFiles: null });
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
for (const skip of hydration.mergeSkips) {
|
|
2992
|
+
const mount = hydrationMounts.find(({ id }) => id === skip.mountId);
|
|
2993
|
+
const basis = mount ? workspaceHydrationReceiptMountBasisHead(currentReceiptBeforeHydration, mount) : null;
|
|
2994
|
+
mountSkipReasons[skip.mountId] = {
|
|
2995
|
+
reason: skip.reason,
|
|
2996
|
+
paths: skip.paths,
|
|
2997
|
+
...skip.retainedPaths?.length ? { retainedPaths: skip.retainedPaths } : {}
|
|
2998
|
+
};
|
|
2999
|
+
process.stderr.write(
|
|
3000
|
+
`[r5d-worker] merge hydration pinned mount ${skip.mountId} at ${basis ?? "its recorded basis"} (${skip.reason}): ${skip.reason === "hydration_merge_conflict" ? "writes that landed after the projection read conflict with the inbound change at" : "the checkout changed while it was being hydrated at"} ${describeHydrationSkipPaths(mount?.sourcePath ?? skip.mountId, skip.paths)}; the next projection merges it from that basis${skip.retainedPaths?.length ? `; displaced writer entries preserved at ${skip.retainedPaths.join(", ")}` : ""}
|
|
3001
|
+
`
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
2290
3004
|
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
2291
3005
|
if (requireCurrentHeadReceipt) {
|
|
2292
3006
|
const currentHead = await revParseAsync(workspacePath, "HEAD");
|
|
@@ -2327,10 +3041,11 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2327
3041
|
};
|
|
2328
3042
|
const projectedFastMounts = [];
|
|
2329
3043
|
const currentProjectionMounts = [];
|
|
3044
|
+
const selectedActiveIdSet = new Set(selected.active.map(({ id }) => id));
|
|
2330
3045
|
if (!input.skipMountMirror) {
|
|
2331
3046
|
await beginWorkspaceCheckoutTransition(workspacePath);
|
|
2332
3047
|
try {
|
|
2333
|
-
const selectedActiveIds =
|
|
3048
|
+
const selectedActiveIds = selectedActiveIdSet;
|
|
2334
3049
|
const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
|
|
2335
3050
|
const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
|
|
2336
3051
|
for (const mount of fastActiveMounts) {
|
|
@@ -2343,15 +3058,27 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2343
3058
|
continue;
|
|
2344
3059
|
}
|
|
2345
3060
|
const releaseMirrorHold = input.hydrationHooks?.holdMounts([mount.id], `projection of mount ${mount.id}`);
|
|
3061
|
+
let staleRewritePaths;
|
|
2346
3062
|
try {
|
|
3063
|
+
const staleRewriteBlobs = staleRewriteBlobsFor(mount);
|
|
2347
3064
|
const mirrored = await workspaceFilesystemExecutor().run("projection_mirror", {
|
|
2348
3065
|
workspacePath,
|
|
2349
|
-
mount: workspaceGitMountData(mount)
|
|
3066
|
+
mount: workspaceGitMountData(mount),
|
|
3067
|
+
...staleRewriteBlobs ? { staleRewriteBlobs } : {}
|
|
2350
3068
|
});
|
|
2351
|
-
|
|
3069
|
+
staleRewritePaths = mirrored.staleRewritePaths;
|
|
3070
|
+
if (mirrored.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, mirrored.staleRewriteCleared);
|
|
3071
|
+
if (staleRewritePaths.length === 0) {
|
|
3072
|
+
projectedGitlinks.push({ mount, gitlinks: mirrored.gitlinks });
|
|
3073
|
+
projectedMountFiles.set(mount.id, { files: mirrored.projectedFiles, readAtMs: mirrored.readAtMs });
|
|
3074
|
+
}
|
|
2352
3075
|
} finally {
|
|
2353
3076
|
releaseMirrorHold?.();
|
|
2354
3077
|
}
|
|
3078
|
+
if (staleRewritePaths.length > 0) {
|
|
3079
|
+
recordStaleRewrite(mount, staleRewritePaths);
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
2355
3082
|
projectedFastMounts.push(mount);
|
|
2356
3083
|
}
|
|
2357
3084
|
await workspaceFilesystemExecutor().run("projection_outer_apply", {
|
|
@@ -2378,6 +3105,18 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2378
3105
|
if (!input.skipMountMirror) {
|
|
2379
3106
|
const projectedHead = await revParseAsync(workspacePath, "HEAD");
|
|
2380
3107
|
if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
|
|
3108
|
+
for (const mount of projectedFastMounts) {
|
|
3109
|
+
if (!selectedActiveIdSet.has(mount.id)) continue;
|
|
3110
|
+
const read = projectedMountFiles.get(mount.id);
|
|
3111
|
+
projectedMountBases.set(mount.id, {
|
|
3112
|
+
projectedCommit: projectedHead,
|
|
3113
|
+
projectedFiles: read?.files ?? null,
|
|
3114
|
+
...read ? { readAtMs: read.readAtMs } : {}
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
for (const mount of currentProjectionMounts)
|
|
3118
|
+
projectedMountBases.set(mount.id, { projectedCommit: projectedHead, projectedFiles: null });
|
|
3119
|
+
for (const [mountId, basis] of mergedProjectionBases) projectedMountBases.set(mountId, basis);
|
|
2381
3120
|
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
2382
3121
|
projectionBasisReceipt,
|
|
2383
3122
|
projectedHead,
|
|
@@ -2557,9 +3296,52 @@ ${push.stdout}`)) {
|
|
|
2557
3296
|
}
|
|
2558
3297
|
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
2559
3298
|
}
|
|
3299
|
+
function detectStaleRewrites(workspacePath, mount, staleRewriteBlobs) {
|
|
3300
|
+
const outerRoot = workspaceMountOuterPath(workspacePath, mount);
|
|
3301
|
+
const stale = [];
|
|
3302
|
+
const cleared = [];
|
|
3303
|
+
for (const [relativePath, blob] of Object.entries(staleRewriteBlobs)) {
|
|
3304
|
+
const algorithm = gitObjectHashAlgorithmFor(blob);
|
|
3305
|
+
if (!algorithm) continue;
|
|
3306
|
+
const source = inspectWorkingTreePath(mount.sourcePath, relativePath);
|
|
3307
|
+
if (source.kind !== "entry" || !source.stat.isFile()) {
|
|
3308
|
+
cleared.push(relativePath);
|
|
3309
|
+
continue;
|
|
3310
|
+
}
|
|
3311
|
+
const content = fs.readFileSync(source.absolutePath);
|
|
3312
|
+
const outer = inspectWorkingTreePath(outerRoot, relativePath);
|
|
3313
|
+
const outerEqual = outer.kind === "entry" && outer.stat.isFile() && fs.readFileSync(outer.absolutePath).equals(content);
|
|
3314
|
+
if (gitBlobHash(content, algorithm) !== blob) {
|
|
3315
|
+
if (!outerEqual) cleared.push(relativePath);
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
if (outerEqual) {
|
|
3319
|
+
cleared.push(relativePath);
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
stale.push(relativePath);
|
|
3323
|
+
}
|
|
3324
|
+
return { stale: stale.sort(), cleared: cleared.sort() };
|
|
3325
|
+
}
|
|
2560
3326
|
function runProjectionMirrorJob(input) {
|
|
2561
|
-
const
|
|
2562
|
-
|
|
3327
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
3328
|
+
let staleRewriteCleared = [];
|
|
3329
|
+
if (input.staleRewriteBlobs) {
|
|
3330
|
+
const detected = detectStaleRewrites(workspacePath, input.mount, input.staleRewriteBlobs);
|
|
3331
|
+
staleRewriteCleared = detected.cleared;
|
|
3332
|
+
if (detected.stale.length > 0) {
|
|
3333
|
+
return { gitlinks: [], projectedFiles: {}, readAtMs: Date.now(), staleRewritePaths: detected.stale, staleRewriteCleared };
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
const projectedFiles = /* @__PURE__ */ new Map();
|
|
3337
|
+
const projected = mirrorMountsToWorkspace(workspacePath, [input.mount], projectedFiles);
|
|
3338
|
+
return {
|
|
3339
|
+
gitlinks: projected[0]?.gitlinks ?? [],
|
|
3340
|
+
projectedFiles: Object.fromEntries(projectedFiles),
|
|
3341
|
+
readAtMs: Date.now(),
|
|
3342
|
+
staleRewritePaths: [],
|
|
3343
|
+
staleRewriteCleared
|
|
3344
|
+
};
|
|
2563
3345
|
}
|
|
2564
3346
|
function runProjectionOuterApplyJob(input) {
|
|
2565
3347
|
const workspacePath = path.resolve(input.workspacePath);
|
|
@@ -2577,11 +3359,16 @@ function runProjectionReceiptJob(input) {
|
|
|
2577
3359
|
return null;
|
|
2578
3360
|
}
|
|
2579
3361
|
const workspaceGitSyncTestHarness = {
|
|
3362
|
+
beginHydrationTransaction,
|
|
2580
3363
|
commandArgs: gitCommandArgs,
|
|
2581
3364
|
workspaceCloneCommandArgs,
|
|
2582
3365
|
configureWorkspaceRepository,
|
|
2583
3366
|
mirrorMountsToWorkspace,
|
|
2584
|
-
hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts
|
|
3367
|
+
hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts,
|
|
3368
|
+
hydrationPreBlobLedger,
|
|
3369
|
+
resetHydrationPreBlobLedgers() {
|
|
3370
|
+
hydrationPreBlobLedgers.clear();
|
|
3371
|
+
}
|
|
2585
3372
|
};
|
|
2586
3373
|
export {
|
|
2587
3374
|
MAX_WORKSPACE_GIT_DIFF_BYTES,
|
|
@@ -2589,7 +3376,10 @@ export {
|
|
|
2589
3376
|
WORKSPACE_GIT_CHECKOUT_DURABILITY,
|
|
2590
3377
|
WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
|
|
2591
3378
|
WORKSPACE_GIT_HYDRATED_RECEIPT,
|
|
3379
|
+
WORKSPACE_GIT_HYDRATION_RECOVERIES,
|
|
2592
3380
|
WORKSPACE_GIT_HYDRATION_TRANSACTION,
|
|
3381
|
+
WORKSPACE_MERGE_HYDRATION_SWITCH,
|
|
3382
|
+
WorkspaceHydrationRecoveryRequiredError,
|
|
2593
3383
|
WorkspaceRemediationAncestryError,
|
|
2594
3384
|
configureExistingWorkspaceGitForRemediation,
|
|
2595
3385
|
ensureWorkspaceGitClone,
|
|
@@ -2599,7 +3389,9 @@ export {
|
|
|
2599
3389
|
runCheckoutDurabilityRecoveryJob,
|
|
2600
3390
|
runCheckoutTransitionBeginJob,
|
|
2601
3391
|
runCheckoutTransitionCompleteJob,
|
|
3392
|
+
runHydrationPreserveInterruptedJob,
|
|
2602
3393
|
runHydrationRawJob,
|
|
3394
|
+
runHydrationRecoveryInspectJob,
|
|
2603
3395
|
runHydrationRecoveryJob,
|
|
2604
3396
|
runHydrationTransactionJob,
|
|
2605
3397
|
runOuterCheckoutFsyncJob,
|