@ricsam/r5d-worker 0.0.131 → 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
|
@@ -33,7 +33,10 @@ __export(workspace_git_sync_exports, {
|
|
|
33
33
|
WORKSPACE_GIT_CHECKOUT_DURABILITY: () => WORKSPACE_GIT_CHECKOUT_DURABILITY,
|
|
34
34
|
WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION: () => WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
|
|
35
35
|
WORKSPACE_GIT_HYDRATED_RECEIPT: () => WORKSPACE_GIT_HYDRATED_RECEIPT,
|
|
36
|
+
WORKSPACE_GIT_HYDRATION_RECOVERIES: () => WORKSPACE_GIT_HYDRATION_RECOVERIES,
|
|
36
37
|
WORKSPACE_GIT_HYDRATION_TRANSACTION: () => WORKSPACE_GIT_HYDRATION_TRANSACTION,
|
|
38
|
+
WORKSPACE_MERGE_HYDRATION_SWITCH: () => WORKSPACE_MERGE_HYDRATION_SWITCH,
|
|
39
|
+
WorkspaceHydrationRecoveryRequiredError: () => WorkspaceHydrationRecoveryRequiredError,
|
|
37
40
|
WorkspaceRemediationAncestryError: () => WorkspaceRemediationAncestryError,
|
|
38
41
|
configureExistingWorkspaceGitForRemediation: () => configureExistingWorkspaceGitForRemediation,
|
|
39
42
|
ensureWorkspaceGitClone: () => ensureWorkspaceGitClone,
|
|
@@ -43,7 +46,9 @@ __export(workspace_git_sync_exports, {
|
|
|
43
46
|
runCheckoutDurabilityRecoveryJob: () => runCheckoutDurabilityRecoveryJob,
|
|
44
47
|
runCheckoutTransitionBeginJob: () => runCheckoutTransitionBeginJob,
|
|
45
48
|
runCheckoutTransitionCompleteJob: () => runCheckoutTransitionCompleteJob,
|
|
49
|
+
runHydrationPreserveInterruptedJob: () => runHydrationPreserveInterruptedJob,
|
|
46
50
|
runHydrationRawJob: () => runHydrationRawJob,
|
|
51
|
+
runHydrationRecoveryInspectJob: () => runHydrationRecoveryInspectJob,
|
|
47
52
|
runHydrationRecoveryJob: () => runHydrationRecoveryJob,
|
|
48
53
|
runHydrationTransactionJob: () => runHydrationTransactionJob,
|
|
49
54
|
runOuterCheckoutFsyncJob: () => runOuterCheckoutFsyncJob,
|
|
@@ -63,7 +68,10 @@ var import_node_path = __toESM(require("node:path"), 1);
|
|
|
63
68
|
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
64
69
|
var import_workspace_merge_projection = require("./workspace-merge-projection.cjs");
|
|
65
70
|
var import_workspace_filesystem_executor = require("./workspace-filesystem-executor.cjs");
|
|
71
|
+
var import_git_blob_hash = require("./git-blob-hash.cjs");
|
|
66
72
|
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
73
|
+
var import_workspace_hydration_merge = require("./workspace-hydration-merge.cjs");
|
|
74
|
+
var import_workspace_hydration_ledger = require("./workspace-hydration-ledger.cjs");
|
|
67
75
|
var import_workspace_mount_boundary = require("./workspace-mount-boundary.cjs");
|
|
68
76
|
const WORKSPACE_GIT_BRANCH = "main";
|
|
69
77
|
const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
|
|
@@ -71,7 +79,36 @@ const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v
|
|
|
71
79
|
const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
|
|
72
80
|
const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
|
|
73
81
|
const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
|
|
82
|
+
const WORKSPACE_GIT_HYDRATION_RECOVERIES = "r5d/workspace-hydration-recoveries";
|
|
83
|
+
const WORKSPACE_GIT_HYDRATION_PRESERVED_BASES = "r5d/workspace-hydration-preserved-bases";
|
|
74
84
|
const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
|
|
85
|
+
const WORKSPACE_MERGE_HYDRATION_SWITCH = "R5D_MERGE_HYDRATION";
|
|
86
|
+
function workspaceMergeHydrationEnabled() {
|
|
87
|
+
return process.env[WORKSPACE_MERGE_HYDRATION_SWITCH] !== "0";
|
|
88
|
+
}
|
|
89
|
+
const hydrationPreBlobLedgers = /* @__PURE__ */ new Map();
|
|
90
|
+
function hydrationPreBlobLedger(workspacePath) {
|
|
91
|
+
const key = import_node_path.default.resolve(workspacePath);
|
|
92
|
+
let ledger = hydrationPreBlobLedgers.get(key);
|
|
93
|
+
if (!ledger) {
|
|
94
|
+
ledger = new import_workspace_hydration_ledger.WorkspaceHydrationPreBlobLedger();
|
|
95
|
+
hydrationPreBlobLedgers.set(key, ledger);
|
|
96
|
+
}
|
|
97
|
+
return ledger;
|
|
98
|
+
}
|
|
99
|
+
class WorkspaceHydrationRecoveryRequiredError extends Error {
|
|
100
|
+
constructor(hydrationRecovery, options) {
|
|
101
|
+
super(
|
|
102
|
+
`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(", ")}` : ""}`,
|
|
103
|
+
options
|
|
104
|
+
);
|
|
105
|
+
this.hydrationRecovery = hydrationRecovery;
|
|
106
|
+
this.name = "WorkspaceHydrationRecoveryRequiredError";
|
|
107
|
+
}
|
|
108
|
+
hydrationRecovery;
|
|
109
|
+
code = "hydration_recovery_required";
|
|
110
|
+
}
|
|
111
|
+
const HYDRATION_TRANSACTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
75
112
|
function workspaceGitMountData(mount) {
|
|
76
113
|
return {
|
|
77
114
|
id: mount.id,
|
|
@@ -927,9 +964,8 @@ async function runWorkspaceCheckoutTransition(workspacePath, mutate) {
|
|
|
927
964
|
await throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
|
|
928
965
|
}
|
|
929
966
|
}
|
|
930
|
-
function parseHydrationTransactionManifest(transactionPath) {
|
|
967
|
+
function parseHydrationTransactionManifest(transactionPath, workspacePath = import_node_path.default.resolve(transactionPath, "..", "..", ".."), options = {}) {
|
|
931
968
|
const manifestPath = import_node_path.default.join(transactionPath, "manifest.json");
|
|
932
|
-
const workspacePath = import_node_path.default.resolve(transactionPath, "..", "..", "..");
|
|
933
969
|
const status = import_node_fs.default.lstatSync(manifestPath);
|
|
934
970
|
if (!status.isFile() || status.isSymbolicLink()) {
|
|
935
971
|
throw new Error(`Workspace hydration transaction manifest is not a regular file: ${manifestPath}`);
|
|
@@ -944,22 +980,26 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
944
980
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
945
981
|
}
|
|
946
982
|
const candidate = parsed;
|
|
947
|
-
if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 || !Array.isArray(candidate.mounts)) {
|
|
983
|
+
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))) {
|
|
948
984
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
949
985
|
}
|
|
986
|
+
if (candidate.version === 5 && (typeof candidate.transactionId !== "string" || !HYDRATION_TRANSACTION_ID.test(candidate.transactionId))) {
|
|
987
|
+
throw new Error(`Workspace hydration transaction identity is invalid: ${manifestPath}`);
|
|
988
|
+
}
|
|
950
989
|
const parseReceipt = (rawReceipt) => {
|
|
951
990
|
if (rawReceipt === null) return null;
|
|
952
991
|
if (!rawReceipt || typeof rawReceipt !== "object") {
|
|
953
992
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
954
993
|
}
|
|
955
|
-
|
|
956
|
-
|
|
994
|
+
const content = `${JSON.stringify(rawReceipt)}
|
|
995
|
+
`;
|
|
996
|
+
return options.verifyReceiptCommits === false ? parseWorkspaceHydrationReceiptContentUnverified(manifestPath, content).receipt : parseWorkspaceHydrationReceiptContent(workspacePath, manifestPath, content);
|
|
957
997
|
};
|
|
958
998
|
const targetReceipt = parseReceipt(candidate.targetReceipt);
|
|
959
999
|
const receiptBefore = parseReceipt(candidate.receiptBefore);
|
|
960
1000
|
const parseScope = (rawScope, existed) => {
|
|
961
1001
|
if (rawScope === void 0) return void 0;
|
|
962
|
-
if (candidate.version !== 4 || !existed || !rawScope || typeof rawScope !== "object") {
|
|
1002
|
+
if (candidate.version !== 4 && candidate.version !== 5 || !existed || !rawScope || typeof rawScope !== "object") {
|
|
963
1003
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
964
1004
|
}
|
|
965
1005
|
const parsePaths = (rawPaths) => {
|
|
@@ -978,7 +1018,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
978
1018
|
};
|
|
979
1019
|
const seenIds = /* @__PURE__ */ new Set();
|
|
980
1020
|
const seenSnapshots = /* @__PURE__ */ new Set();
|
|
981
|
-
const
|
|
1021
|
+
const parsedMounts = [...candidate.mounts, ...candidate.preservedMounts ?? []].map((rawMount) => {
|
|
982
1022
|
if (!rawMount || typeof rawMount !== "object") {
|
|
983
1023
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
984
1024
|
}
|
|
@@ -993,6 +1033,9 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
993
1033
|
seenIds.add(mount.id);
|
|
994
1034
|
seenSnapshots.add(mount.snapshotName);
|
|
995
1035
|
const scope = parseScope(rawMount.scope, mount.existed);
|
|
1036
|
+
if (candidate.version === 5 && mount.recovery !== "snapshot" && mount.recovery !== "guarded" || candidate.version !== 5 && mount.recovery !== void 0 || mount.recovery === "guarded" && (!mount.existed || !scope)) {
|
|
1037
|
+
throw new Error(`Workspace hydration recovery policy is invalid: ${manifestPath}`);
|
|
1038
|
+
}
|
|
996
1039
|
return {
|
|
997
1040
|
id: mount.id,
|
|
998
1041
|
incarnationKey: mount.incarnationKey,
|
|
@@ -1001,16 +1044,22 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
1001
1044
|
workspaceRelativePath: mount.workspaceRelativePath,
|
|
1002
1045
|
existed: mount.existed,
|
|
1003
1046
|
snapshotName: mount.snapshotName,
|
|
1004
|
-
...scope ? { scope } : {}
|
|
1047
|
+
...scope ? { scope } : {},
|
|
1048
|
+
...candidate.version === 5 ? { recovery: mount.recovery } : {}
|
|
1005
1049
|
};
|
|
1006
1050
|
});
|
|
1007
1051
|
return {
|
|
1008
|
-
version: candidate.version === 4 ? 4 : 3,
|
|
1052
|
+
version: candidate.version === 5 ? 5 : candidate.version === 4 ? 4 : 3,
|
|
1053
|
+
...candidate.version === 5 ? { transactionId: candidate.transactionId } : {},
|
|
1009
1054
|
targetReceipt,
|
|
1010
1055
|
receiptBefore,
|
|
1011
|
-
mounts
|
|
1056
|
+
mounts: parsedMounts.slice(0, candidate.mounts.length),
|
|
1057
|
+
...candidate.preservedMounts !== void 0 ? { preservedMounts: parsedMounts.slice(candidate.mounts.length) } : {}
|
|
1012
1058
|
};
|
|
1013
1059
|
}
|
|
1060
|
+
function hydrationEvidenceMounts(manifest) {
|
|
1061
|
+
return [...manifest.mounts, ...manifest.preservedMounts ?? []];
|
|
1062
|
+
}
|
|
1014
1063
|
function removeHydrationTransaction(workspacePath) {
|
|
1015
1064
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1016
1065
|
import_node_fs.default.rmSync(transactionPath, { recursive: true, force: true });
|
|
@@ -1025,7 +1074,303 @@ function hydrationTransactionCommitted(transactionPath) {
|
|
|
1025
1074
|
}
|
|
1026
1075
|
return true;
|
|
1027
1076
|
}
|
|
1077
|
+
function hydrationRetainedEvidence(mount) {
|
|
1078
|
+
if (!lstatIfExists(mount.sourcePath)) return null;
|
|
1079
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1080
|
+
trustedRoot: mount.durabilityRootPath,
|
|
1081
|
+
candidate: mount.sourcePath,
|
|
1082
|
+
label: `Hydration evidence mount ${mount.id}`
|
|
1083
|
+
});
|
|
1084
|
+
const dotGit = import_node_path.default.join(mount.sourcePath, ".git");
|
|
1085
|
+
if (lstatIfExists(dotGit)?.isSymbolicLink()) throw new Error(`Hydration evidence Git directory is a symlink: ${dotGit}`);
|
|
1086
|
+
const directory = (0, import_working_tree_mirror.workingTreeDisplacedRetentionDirectory)(mount.sourcePath);
|
|
1087
|
+
if (!directory) return null;
|
|
1088
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1089
|
+
trustedRoot: mount.durabilityRootPath,
|
|
1090
|
+
candidate: directory,
|
|
1091
|
+
label: `Hydration retained evidence ${mount.id}`
|
|
1092
|
+
});
|
|
1093
|
+
if (!lstatIfExists(directory)) return null;
|
|
1094
|
+
return {
|
|
1095
|
+
directory,
|
|
1096
|
+
paths: import_node_fs.default.readdirSync(directory).sort().map((name) => import_node_path.default.join(directory, name))
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
function fsyncHydrationRetainedEvidence(mount) {
|
|
1100
|
+
const evidence = hydrationRetainedEvidence(mount);
|
|
1101
|
+
if (!evidence || evidence.paths.length === 0) return;
|
|
1102
|
+
fsyncTree(evidence.directory);
|
|
1103
|
+
for (let directory = import_node_path.default.dirname(evidence.directory); ; directory = import_node_path.default.dirname(directory)) {
|
|
1104
|
+
fsyncDirectory(directory);
|
|
1105
|
+
if (directory === import_node_path.default.resolve(mount.durabilityRootPath)) break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function readHydrationArchivedEvidence(transactionPath, manifest) {
|
|
1109
|
+
const recordPath = import_node_path.default.join(transactionPath, "retained-evidence.json");
|
|
1110
|
+
const status = lstatIfExists(recordPath);
|
|
1111
|
+
if (!status) return [];
|
|
1112
|
+
if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Invalid hydration retained-evidence record: ${recordPath}`);
|
|
1113
|
+
const record = JSON.parse(import_node_fs.default.readFileSync(recordPath, "utf8"));
|
|
1114
|
+
if (record.version !== 1 || !Array.isArray(record.entries)) throw new Error(`Invalid hydration retained-evidence record: ${recordPath}`);
|
|
1115
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1116
|
+
return record.entries.map((raw) => {
|
|
1117
|
+
if (!raw || typeof raw !== "object") throw new Error(`Invalid hydration retained-evidence entry: ${recordPath}`);
|
|
1118
|
+
const entry = raw;
|
|
1119
|
+
const mount = hydrationEvidenceMounts(manifest).find(({ id }) => id === entry.mountId);
|
|
1120
|
+
const parts = typeof entry.archiveRelativePath === "string" ? entry.archiveRelativePath.split("/") : [];
|
|
1121
|
+
const sourceRelative = typeof entry.sourcePath === "string" && mount ? import_node_path.default.relative(mount.durabilityRootPath, entry.sourcePath) : "";
|
|
1122
|
+
if (!mount || typeof entry.sourcePath !== "string" || import_node_path.default.resolve(entry.sourcePath) !== entry.sourcePath || !sourceRelative || sourceRelative === ".." || sourceRelative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.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)) {
|
|
1123
|
+
throw new Error(`Invalid hydration retained-evidence entry: ${recordPath}`);
|
|
1124
|
+
}
|
|
1125
|
+
const archivedPath = import_node_path.default.join(transactionPath, ...parts);
|
|
1126
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1127
|
+
trustedRoot: transactionPath,
|
|
1128
|
+
candidate: import_node_path.default.dirname(archivedPath),
|
|
1129
|
+
label: "Archived hydration evidence"
|
|
1130
|
+
});
|
|
1131
|
+
if (!lstatIfExists(archivedPath)) throw new Error(`Archived hydration evidence is missing: ${archivedPath}`);
|
|
1132
|
+
seen.add(entry.archiveRelativePath);
|
|
1133
|
+
return { mountId: entry.mountId, sourcePath: entry.sourcePath, archiveRelativePath: entry.archiveRelativePath };
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
function captureHydrationRetainedEvidence(transactionPath, manifest) {
|
|
1137
|
+
const entries = readHydrationArchivedEvidence(transactionPath, manifest);
|
|
1138
|
+
const captureId = crypto.randomUUID();
|
|
1139
|
+
let captured = false;
|
|
1140
|
+
for (const mount of hydrationEvidenceMounts(manifest)) {
|
|
1141
|
+
const evidence = hydrationRetainedEvidence(mount);
|
|
1142
|
+
if (!evidence || evidence.paths.length === 0) continue;
|
|
1143
|
+
fsyncHydrationRetainedEvidence(mount);
|
|
1144
|
+
const relativeDirectory = `displaced/${captureId}/${mount.snapshotName}`;
|
|
1145
|
+
const destinationDirectory = import_node_path.default.join(transactionPath, relativeDirectory);
|
|
1146
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({ trustedRoot: transactionPath, candidate: destinationDirectory, label: "Hydration evidence capture" });
|
|
1147
|
+
import_node_fs.default.mkdirSync(destinationDirectory, { recursive: true, mode: 448 });
|
|
1148
|
+
for (const sourcePath of evidence.paths) {
|
|
1149
|
+
const archiveRelativePath = `${relativeDirectory}/${import_node_path.default.basename(sourcePath)}`;
|
|
1150
|
+
import_node_fs.default.cpSync(sourcePath, import_node_path.default.join(transactionPath, archiveRelativePath), {
|
|
1151
|
+
recursive: true,
|
|
1152
|
+
dereference: false,
|
|
1153
|
+
verbatimSymlinks: true,
|
|
1154
|
+
errorOnExist: true,
|
|
1155
|
+
force: false,
|
|
1156
|
+
preserveTimestamps: true
|
|
1157
|
+
});
|
|
1158
|
+
entries.push({ mountId: mount.id, sourcePath, archiveRelativePath });
|
|
1159
|
+
captured = true;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (captured) {
|
|
1163
|
+
fsyncTree(import_node_path.default.join(transactionPath, "displaced"));
|
|
1164
|
+
const temporaryPath = import_node_path.default.join(transactionPath, `.retained-evidence.${crypto.randomUUID()}.tmp`);
|
|
1165
|
+
writePrivateFileDurably(temporaryPath, `${JSON.stringify({ version: 1, entries })}
|
|
1166
|
+
`);
|
|
1167
|
+
import_node_fs.default.renameSync(temporaryPath, import_node_path.default.join(transactionPath, "retained-evidence.json"));
|
|
1168
|
+
fsyncDirectory(transactionPath);
|
|
1169
|
+
}
|
|
1170
|
+
return entries;
|
|
1171
|
+
}
|
|
1172
|
+
function hydrationRecoveryRequiredError(workspacePath, manifest, cause) {
|
|
1173
|
+
if (manifest.version !== 5 || !manifest.transactionId) throw new Error("Guarded hydration lacks a durable transaction identity");
|
|
1174
|
+
return new WorkspaceHydrationRecoveryRequiredError(
|
|
1175
|
+
{
|
|
1176
|
+
transactionId: manifest.transactionId,
|
|
1177
|
+
transactionPath: hydrationTransactionPath(workspacePath),
|
|
1178
|
+
mounts: hydrationEvidenceMounts(manifest).map(({ id, incarnationKey, sourcePath, durabilityRootPath, workspaceRelativePath }) => {
|
|
1179
|
+
const retainedPaths = hydrationRetainedEvidence({ id, sourcePath, durabilityRootPath })?.paths;
|
|
1180
|
+
return {
|
|
1181
|
+
id,
|
|
1182
|
+
incarnationKey,
|
|
1183
|
+
sourcePath,
|
|
1184
|
+
durabilityRootPath,
|
|
1185
|
+
workspaceRelativePath,
|
|
1186
|
+
...retainedPaths?.length ? { retainedPaths } : {}
|
|
1187
|
+
};
|
|
1188
|
+
}),
|
|
1189
|
+
receiptBefore: manifest.receiptBefore
|
|
1190
|
+
},
|
|
1191
|
+
cause === void 0 ? void 0 : { cause }
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
function runHydrationRecoveryInspectJob(input) {
|
|
1195
|
+
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
1196
|
+
const workspaceStatus = lstatIfExists(workspacePath);
|
|
1197
|
+
if (!workspaceStatus) return null;
|
|
1198
|
+
if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink())
|
|
1199
|
+
throw new Error(`Invalid workspace hydration root: ${workspacePath}`);
|
|
1200
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1201
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1202
|
+
trustedRoot: workspacePath,
|
|
1203
|
+
candidate: transactionPath,
|
|
1204
|
+
label: "Workspace hydration transaction inspection"
|
|
1205
|
+
});
|
|
1206
|
+
const status = lstatIfExists(transactionPath);
|
|
1207
|
+
if (!status) return null;
|
|
1208
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new Error(`Invalid workspace hydration transaction: ${transactionPath}`);
|
|
1209
|
+
const manifest = parseHydrationTransactionManifest(transactionPath);
|
|
1210
|
+
if (hydrationTransactionCommitted(transactionPath) || !manifest.mounts.some(({ recovery }) => recovery === "guarded")) return null;
|
|
1211
|
+
return hydrationRecoveryRequiredError(workspacePath, manifest).hydrationRecovery;
|
|
1212
|
+
}
|
|
1213
|
+
function readPreservedHydrationBases(workspacePath) {
|
|
1214
|
+
const recordPath = import_node_path.default.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
|
|
1215
|
+
const status = lstatIfExists(recordPath);
|
|
1216
|
+
if (!status) return null;
|
|
1217
|
+
if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Preserved hydration bases are not a regular file: ${recordPath}`);
|
|
1218
|
+
return parseWorkspaceHydrationReceiptContentUnverified(recordPath, import_node_fs.default.readFileSync(recordPath, "utf8")).receipt;
|
|
1219
|
+
}
|
|
1220
|
+
async function readPreservedHydrationBasesAsync(workspacePath) {
|
|
1221
|
+
const recordPath = import_node_path.default.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
|
|
1222
|
+
let status;
|
|
1223
|
+
try {
|
|
1224
|
+
status = await import_node_fs.default.promises.lstat(recordPath);
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
if (error.code === "ENOENT") return null;
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1229
|
+
if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Preserved hydration bases are not a regular file: ${recordPath}`);
|
|
1230
|
+
return parseWorkspaceHydrationReceiptContentUnverified(recordPath, await import_node_fs.default.promises.readFile(recordPath, "utf8")).receipt;
|
|
1231
|
+
}
|
|
1232
|
+
function preserveHydrationBases(workspacePath, manifest) {
|
|
1233
|
+
const current = readHydratedWorkspaceReceipt(workspacePath);
|
|
1234
|
+
if (!workspaceHydrationReceiptsEqual(current, manifest.receiptBefore)) {
|
|
1235
|
+
throw new Error(`Hydration receipt changed before preservation of transaction ${manifest.transactionId}`);
|
|
1236
|
+
}
|
|
1237
|
+
const previous = readPreservedHydrationBases(workspacePath);
|
|
1238
|
+
const affectedIds = new Set(hydrationEvidenceMounts(manifest).map(({ id }) => id));
|
|
1239
|
+
const mounts = (current?.mounts ?? []).filter(
|
|
1240
|
+
(entry) => affectedIds.has(entry.id) || previous?.mounts.some(
|
|
1241
|
+
(preserved) => workspaceHydrationReceiptsEqual({ version: 3, mounts: [entry] }, { version: 3, mounts: [preserved] })
|
|
1242
|
+
)
|
|
1243
|
+
);
|
|
1244
|
+
const recordPath = import_node_path.default.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
|
|
1245
|
+
const temporaryPath = `${recordPath}.${crypto.randomUUID()}.tmp`;
|
|
1246
|
+
writePrivateFileDurably(temporaryPath, `${JSON.stringify({ version: 3, mounts })}
|
|
1247
|
+
`);
|
|
1248
|
+
import_node_fs.default.renameSync(temporaryPath, recordPath);
|
|
1249
|
+
fsyncDirectory(import_node_path.default.dirname(recordPath));
|
|
1250
|
+
}
|
|
1251
|
+
function runHydrationPreserveInterruptedJob(input) {
|
|
1252
|
+
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
1253
|
+
if (!HYDRATION_TRANSACTION_ID.test(input.expectedTransactionId)) throw new Error("Invalid hydration recovery transaction identity");
|
|
1254
|
+
const stateDirectory = workspacePrivateStateDirectory(workspacePath);
|
|
1255
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1256
|
+
const archivesPath = import_node_path.default.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_RECOVERIES.split("/"));
|
|
1257
|
+
const archivePath = import_node_path.default.join(archivesPath, input.expectedTransactionId);
|
|
1258
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({ trustedRoot: stateDirectory, candidate: archivesPath, label: "Workspace hydration archives" });
|
|
1259
|
+
const pending = lstatIfExists(transactionPath);
|
|
1260
|
+
if (!pending) {
|
|
1261
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({ trustedRoot: stateDirectory, candidate: archivePath, label: "Workspace hydration archive" });
|
|
1262
|
+
if (!lstatIfExists(archivePath))
|
|
1263
|
+
throw new Error(`No pending or preserved hydration transaction matches ${input.expectedTransactionId}`);
|
|
1264
|
+
const archived = parseHydrationTransactionManifest(archivePath, workspacePath, { verifyReceiptCommits: false });
|
|
1265
|
+
const markerPath2 = import_node_path.default.join(archivePath, "preserved.json");
|
|
1266
|
+
const markerStatus = import_node_fs.default.lstatSync(markerPath2);
|
|
1267
|
+
if (!markerStatus.isFile() || markerStatus.isSymbolicLink()) throw new Error(`Invalid hydration preservation record: ${markerPath2}`);
|
|
1268
|
+
const marker = JSON.parse(import_node_fs.default.readFileSync(markerPath2, "utf8"));
|
|
1269
|
+
if (archived.version !== 5 || archived.transactionId !== input.expectedTransactionId || marker.version !== 1 || marker.transactionId !== archived.transactionId) {
|
|
1270
|
+
throw new Error(`Hydration archive identity does not match ${input.expectedTransactionId}`);
|
|
1271
|
+
}
|
|
1272
|
+
const retainedEntries2 = readHydrationArchivedEvidence(archivePath, archived).map(({ mountId, sourcePath, archiveRelativePath }) => ({
|
|
1273
|
+
mountId,
|
|
1274
|
+
sourcePath,
|
|
1275
|
+
archivePath: import_node_path.default.join(archivePath, archiveRelativePath)
|
|
1276
|
+
}));
|
|
1277
|
+
fsyncDirectory(archivePath);
|
|
1278
|
+
fsyncDirectory(archivesPath);
|
|
1279
|
+
fsyncDirectory(stateDirectory);
|
|
1280
|
+
return {
|
|
1281
|
+
transactionId: archived.transactionId,
|
|
1282
|
+
archivePath,
|
|
1283
|
+
receiptBefore: archived.receiptBefore,
|
|
1284
|
+
preservedMountIds: hydrationEvidenceMounts(archived).map(({ id }) => id).sort(),
|
|
1285
|
+
alreadyPreserved: true,
|
|
1286
|
+
...retainedEntries2.length > 0 ? { retainedEntries: retainedEntries2 } : {}
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
if (!pending.isDirectory() || pending.isSymbolicLink()) throw new Error(`Invalid workspace hydration transaction: ${transactionPath}`);
|
|
1290
|
+
const manifest = parseHydrationTransactionManifest(transactionPath);
|
|
1291
|
+
if (manifest.version !== 5 || manifest.transactionId !== input.expectedTransactionId) {
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
`Stale hydration recovery transaction identity: expected ${input.expectedTransactionId}, pending ${manifest.transactionId ?? "legacy transaction"}`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
if (hydrationTransactionCommitted(transactionPath))
|
|
1297
|
+
throw new Error(`Hydration transaction ${manifest.transactionId} is committed; ordinary recovery must finish its receipt`);
|
|
1298
|
+
if (!manifest.mounts.some(({ recovery }) => recovery === "guarded"))
|
|
1299
|
+
throw new Error(`Hydration transaction ${manifest.transactionId} has no interrupted guarded mounts`);
|
|
1300
|
+
if (lstatIfExists(archivePath))
|
|
1301
|
+
throw new Error(`Hydration archive already exists while transaction ${manifest.transactionId} is pending`);
|
|
1302
|
+
if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), manifest.receiptBefore)) {
|
|
1303
|
+
throw new Error(`Hydration receipt changed before preservation of transaction ${manifest.transactionId}`);
|
|
1304
|
+
}
|
|
1305
|
+
for (const mount of hydrationEvidenceMounts(manifest)) {
|
|
1306
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1307
|
+
trustedRoot: mount.durabilityRootPath,
|
|
1308
|
+
candidate: mount.sourcePath,
|
|
1309
|
+
label: `Hydration preservation mount ${mount.id}`
|
|
1310
|
+
});
|
|
1311
|
+
const source = lstatIfExists(mount.sourcePath);
|
|
1312
|
+
if (mount.existed && !source) throw new Error(`Hydration preservation mount ${mount.id} is missing: ${mount.sourcePath}`);
|
|
1313
|
+
const snapshotPath = import_node_path.default.join(transactionPath, "mounts", mount.snapshotName);
|
|
1314
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1315
|
+
trustedRoot: transactionPath,
|
|
1316
|
+
candidate: snapshotPath,
|
|
1317
|
+
label: `Hydration preservation snapshot ${mount.id}`
|
|
1318
|
+
});
|
|
1319
|
+
if (!lstatIfExists(snapshotPath)) throw new Error(`Hydration preservation snapshot ${mount.id} is missing`);
|
|
1320
|
+
const basis = manifest.receiptBefore?.mounts.find((entry) => entry.id === mount.id);
|
|
1321
|
+
if (!basis || basis.incarnationKey !== mount.incarnationKey || basis.sourcePath !== mount.sourcePath || basis.durabilityRootPath !== mount.durabilityRootPath || basis.workspaceRelativePath !== mount.workspaceRelativePath) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
`Hydration preservation mount ${mount.id} has no matching prior receipt basis; manual salvage is required. The transaction and all snapshots remain at ${transactionPath}`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
for (const mount of hydrationEvidenceMounts(manifest)) {
|
|
1328
|
+
fsyncHydratedWorkspaceMount({ ...mount, hydrationIncarnationKey: mount.incarnationKey, sourceMode: "all", hydrateDeletionMode: "all" });
|
|
1329
|
+
}
|
|
1330
|
+
const retainedEntries = captureHydrationRetainedEvidence(transactionPath, manifest).map(
|
|
1331
|
+
({ mountId, sourcePath, archiveRelativePath }) => ({
|
|
1332
|
+
mountId,
|
|
1333
|
+
sourcePath,
|
|
1334
|
+
archivePath: import_node_path.default.join(archivePath, archiveRelativePath)
|
|
1335
|
+
})
|
|
1336
|
+
);
|
|
1337
|
+
fsyncTree(transactionPath);
|
|
1338
|
+
preserveHydrationBases(workspacePath, manifest);
|
|
1339
|
+
repairWorkspaceBasisRefs(workspacePath, manifest.receiptBefore);
|
|
1340
|
+
const markerPath = import_node_path.default.join(transactionPath, "preserved.json");
|
|
1341
|
+
const markerContent = `${JSON.stringify({ version: 1, transactionId: manifest.transactionId })}
|
|
1342
|
+
`;
|
|
1343
|
+
const existingMarker = lstatIfExists(markerPath);
|
|
1344
|
+
if (existingMarker) {
|
|
1345
|
+
if (!existingMarker.isFile() || existingMarker.isSymbolicLink() || import_node_fs.default.readFileSync(markerPath, "utf8") !== markerContent) {
|
|
1346
|
+
throw new Error(`Invalid hydration preservation record: ${markerPath}`);
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
const markerTemporaryPath = import_node_path.default.join(transactionPath, `.preserved.${crypto.randomUUID()}.tmp`);
|
|
1350
|
+
writePrivateFileDurably(markerTemporaryPath, markerContent);
|
|
1351
|
+
import_node_fs.default.renameSync(markerTemporaryPath, markerPath);
|
|
1352
|
+
}
|
|
1353
|
+
fsyncDirectory(transactionPath);
|
|
1354
|
+
if (!lstatIfExists(archivesPath)) {
|
|
1355
|
+
import_node_fs.default.mkdirSync(archivesPath, { mode: 448 });
|
|
1356
|
+
fsyncDirectory(stateDirectory);
|
|
1357
|
+
}
|
|
1358
|
+
import_node_fs.default.renameSync(transactionPath, archivePath);
|
|
1359
|
+
fsyncDirectory(archivesPath);
|
|
1360
|
+
fsyncDirectory(stateDirectory);
|
|
1361
|
+
return {
|
|
1362
|
+
transactionId: manifest.transactionId,
|
|
1363
|
+
archivePath,
|
|
1364
|
+
receiptBefore: manifest.receiptBefore,
|
|
1365
|
+
preservedMountIds: hydrationEvidenceMounts(manifest).map(({ id }) => id).sort(),
|
|
1366
|
+
alreadyPreserved: false,
|
|
1367
|
+
...retainedEntries.length > 0 ? { retainedEntries } : {}
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1028
1370
|
function restoreHydrationTransaction(workspacePath, manifest) {
|
|
1371
|
+
if (manifest.mounts.some(({ recovery }) => recovery === "guarded")) {
|
|
1372
|
+
throw hydrationRecoveryRequiredError(workspacePath, manifest);
|
|
1373
|
+
}
|
|
1029
1374
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1030
1375
|
const durabilityScopes = /* @__PURE__ */ new Map();
|
|
1031
1376
|
for (const mount of manifest.mounts) {
|
|
@@ -1088,25 +1433,132 @@ function recoverPendingHydrationTransaction(workspacePath) {
|
|
|
1088
1433
|
return;
|
|
1089
1434
|
}
|
|
1090
1435
|
const receipt = readHydratedWorkspaceReceipt(workspacePath);
|
|
1091
|
-
if (manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
|
|
1436
|
+
if (manifest.version !== 5 && manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
|
|
1092
1437
|
removeHydrationTransaction(workspacePath);
|
|
1093
1438
|
return;
|
|
1094
1439
|
}
|
|
1095
1440
|
restoreHydrationTransaction(workspacePath, manifest);
|
|
1096
1441
|
removeHydrationTransaction(workspacePath);
|
|
1097
1442
|
}
|
|
1098
|
-
function
|
|
1443
|
+
function gitOutputSync(cwd, args, action, input) {
|
|
1444
|
+
const result = Bun.spawnSync(gitCommandArgs(args), {
|
|
1445
|
+
cwd,
|
|
1446
|
+
...input ? { stdin: input } : {},
|
|
1447
|
+
stdout: "pipe",
|
|
1448
|
+
stderr: "pipe",
|
|
1449
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
1450
|
+
});
|
|
1451
|
+
if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
1452
|
+
return Buffer.from(result.stdout);
|
|
1453
|
+
}
|
|
1454
|
+
function workspaceSubtreeChanges(workspacePath, projectedCommit, head, workspaceRelativePath) {
|
|
1455
|
+
const normalized = normalizedWorkspaceMountPath(workspaceRelativePath);
|
|
1456
|
+
const names = [`${projectedCommit}:${normalized}`, `${head}:${normalized}`];
|
|
1457
|
+
const resolved = resolveGitObjectsSync(workspacePath, names);
|
|
1458
|
+
const emptyTree = emptyTreeHash(workspacePath);
|
|
1459
|
+
const subtree = (name) => {
|
|
1460
|
+
const object = resolved.get(name);
|
|
1461
|
+
return object?.type === "tree" ? object.objectId : emptyTree;
|
|
1462
|
+
};
|
|
1463
|
+
const base = subtree(names[0]);
|
|
1464
|
+
const theirs = subtree(names[1]);
|
|
1465
|
+
if (base === theirs) return (0, import_workspace_hydration_merge.parseWorkspaceSubtreeChanges)(Buffer.alloc(0));
|
|
1466
|
+
return (0, import_workspace_hydration_merge.parseWorkspaceSubtreeChanges)(
|
|
1467
|
+
gitOutputSync(
|
|
1468
|
+
workspacePath,
|
|
1469
|
+
["diff-tree", "-r", "-z", "--no-renames", "--raw", base, theirs],
|
|
1470
|
+
"compare projected and hydration subtrees"
|
|
1471
|
+
)
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
function resolveGitObjectsSync(workspacePath, names) {
|
|
1475
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1476
|
+
const unique = [...new Set(names)];
|
|
1477
|
+
if (unique.length === 0) return resolved;
|
|
1478
|
+
const output = gitOutputSync(
|
|
1479
|
+
workspacePath,
|
|
1480
|
+
["cat-file", "--batch-check"],
|
|
1481
|
+
"resolve workspace objects",
|
|
1482
|
+
Buffer.from(`${unique.join("\n")}
|
|
1483
|
+
`)
|
|
1484
|
+
);
|
|
1485
|
+
const lines = output.toString().split("\n").filter((line) => line.length > 0);
|
|
1486
|
+
if (lines.length !== unique.length) {
|
|
1487
|
+
throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
|
|
1488
|
+
}
|
|
1489
|
+
unique.forEach((name, index) => {
|
|
1490
|
+
const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
|
|
1491
|
+
resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
|
|
1492
|
+
});
|
|
1493
|
+
return resolved;
|
|
1494
|
+
}
|
|
1495
|
+
function readWorkspaceBlobs(workspacePath, objectIds) {
|
|
1496
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
1497
|
+
const unique = [...new Set(objectIds)];
|
|
1498
|
+
if (unique.length === 0) return blobs;
|
|
1499
|
+
const output = gitOutputSync(
|
|
1500
|
+
workspacePath,
|
|
1501
|
+
["cat-file", "--batch"],
|
|
1502
|
+
"read projected workspace blobs",
|
|
1503
|
+
Buffer.from(`${unique.join("\n")}
|
|
1504
|
+
`)
|
|
1505
|
+
);
|
|
1506
|
+
let offset = 0;
|
|
1507
|
+
while (offset < output.length) {
|
|
1508
|
+
const newline = output.indexOf(10, offset);
|
|
1509
|
+
if (newline < 0) break;
|
|
1510
|
+
const header = output.subarray(offset, newline).toString();
|
|
1511
|
+
offset = newline + 1;
|
|
1512
|
+
const match = /^([0-9a-f]{40,64}) (\S+) (\d+)$/u.exec(header);
|
|
1513
|
+
if (!match) {
|
|
1514
|
+
if (/ missing$/u.test(header)) continue;
|
|
1515
|
+
throw new Error(`Unexpected cat-file record while reading projected blobs: ${header}`);
|
|
1516
|
+
}
|
|
1517
|
+
const size = Number(match[3]);
|
|
1518
|
+
if (match[2] === "blob") blobs.set(match[1], Buffer.from(output.subarray(offset, offset + size)));
|
|
1519
|
+
offset += size + 1;
|
|
1520
|
+
}
|
|
1521
|
+
return blobs;
|
|
1522
|
+
}
|
|
1523
|
+
function revertReceiptMounts(target, before, mountIds) {
|
|
1524
|
+
if (!target || mountIds.size === 0) return target;
|
|
1525
|
+
const beforeById = new Map(before?.mounts.map((entry) => [entry.id, entry]) ?? []);
|
|
1526
|
+
const mounts = target.mounts.flatMap((entry) => {
|
|
1527
|
+
if (!mountIds.has(entry.id)) return [entry];
|
|
1528
|
+
const previous = beforeById.get(entry.id);
|
|
1529
|
+
return previous ? [previous] : [];
|
|
1530
|
+
}).sort(compareWorkspaceHydrationMountBasis);
|
|
1531
|
+
return { version: 3, mounts };
|
|
1532
|
+
}
|
|
1533
|
+
function rewriteHydrationTransactionManifest(workspacePath, manifest) {
|
|
1534
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1535
|
+
const temporaryPath = import_node_path.default.join(transactionPath, `.manifest.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1536
|
+
try {
|
|
1537
|
+
writePrivateFileDurably(temporaryPath, `${JSON.stringify(manifest)}
|
|
1538
|
+
`);
|
|
1539
|
+
import_node_fs.default.renameSync(temporaryPath, import_node_path.default.join(transactionPath, "manifest.json"));
|
|
1540
|
+
fsyncDirectory(transactionPath);
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
import_node_fs.default.rmSync(temporaryPath, { force: true });
|
|
1543
|
+
throw error;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function beginHydrationTransaction(workspacePath, mounts, targetReceipt, mergeBases) {
|
|
1099
1547
|
recoverPendingHydrationTransaction(workspacePath);
|
|
1100
1548
|
const stateDirectory = workspacePrivateStateDirectory(workspacePath);
|
|
1101
1549
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1102
1550
|
const stagingPath = import_node_path.default.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1103
1551
|
import_node_fs.default.mkdirSync(stagingPath, { mode: 448 });
|
|
1104
1552
|
const durabilityScopes = /* @__PURE__ */ new Map();
|
|
1553
|
+
const mergePlans = /* @__PURE__ */ new Map();
|
|
1554
|
+
const skippedMounts = [];
|
|
1105
1555
|
try {
|
|
1106
1556
|
const snapshotRoot = import_node_path.default.join(stagingPath, "mounts");
|
|
1107
1557
|
import_node_fs.default.mkdirSync(snapshotRoot, { mode: 448 });
|
|
1108
1558
|
const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
|
|
1109
|
-
const
|
|
1559
|
+
const head = mergeBases && Object.keys(mergeBases).length > 0 ? revParse(workspacePath, "HEAD") : null;
|
|
1560
|
+
const manifestMounts = [];
|
|
1561
|
+
for (const mount of mounts) {
|
|
1110
1562
|
const sourcePath = import_node_path.default.resolve(mount.sourcePath);
|
|
1111
1563
|
const durabilityRootPath = import_node_path.default.resolve(mount.durabilityRootPath);
|
|
1112
1564
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
@@ -1118,45 +1570,89 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1118
1570
|
if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
|
|
1119
1571
|
throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
|
|
1120
1572
|
}
|
|
1121
|
-
const snapshotName = String(
|
|
1573
|
+
const snapshotName = String(manifestMounts.length);
|
|
1122
1574
|
const snapshotPath = import_node_path.default.join(snapshotRoot, snapshotName);
|
|
1123
|
-
import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
|
|
1124
1575
|
let scope;
|
|
1125
|
-
|
|
1576
|
+
const basis = mergeBases?.[mount.id];
|
|
1577
|
+
if (sourceStatus && mount.hydrateDeletionMode === "git" && basis && head) {
|
|
1126
1578
|
const outerPath = workspaceMountOuterPath(workspacePath, mount);
|
|
1127
1579
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1128
1580
|
trustedRoot: workspacePath,
|
|
1129
1581
|
candidate: outerPath,
|
|
1130
1582
|
label: `Workspace mount ${mount.id} outer source`
|
|
1131
1583
|
});
|
|
1132
|
-
const
|
|
1584
|
+
const prepared = (0, import_working_tree_mirror.prepareWorkingTreeMirror)({
|
|
1133
1585
|
sourceRoot: outerPath,
|
|
1134
1586
|
targetRoot: sourcePath,
|
|
1135
1587
|
sourceMode: "all",
|
|
1136
1588
|
deletionMode: "git",
|
|
1137
1589
|
sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
|
|
1138
1590
|
});
|
|
1591
|
+
const decision = (0, import_workspace_hydration_merge.decideWorkspaceHydrationMerge)({
|
|
1592
|
+
prepared,
|
|
1593
|
+
changes: workspaceSubtreeChanges(workspacePath, basis.projectedCommit, head, mount.workspaceRelativePath),
|
|
1594
|
+
projectedFiles: basis.projectedFiles,
|
|
1595
|
+
...basis.readAtMs === void 0 ? {} : { readAtMs: basis.readAtMs },
|
|
1596
|
+
readBaseBlobs: (objectIds) => readWorkspaceBlobs(workspacePath, objectIds),
|
|
1597
|
+
readBinaryPaths: (relativePaths) => (0, import_workspace_hydration_merge.readHydrationBinaryPaths)({ checkoutRoot: prepared.targetRoot, outerRoot: prepared.sourceRoot, relativePaths })
|
|
1598
|
+
});
|
|
1599
|
+
if (decision.kind !== "apply") {
|
|
1600
|
+
skippedMounts.push({
|
|
1601
|
+
mountId: mount.id,
|
|
1602
|
+
reason: decision.kind === "conflict" ? "hydration_merge_conflict" : "hydration_target_changed",
|
|
1603
|
+
paths: decision.paths
|
|
1604
|
+
});
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
|
|
1608
|
+
const plan = (0, import_working_tree_mirror.planPreparedWorkingTreeMirror)(prepared, { selection: decision.selection, overrides: decision.overrides });
|
|
1139
1609
|
scope = (0, import_working_tree_mirror.captureWorkingTreeMirrorPlan)({
|
|
1140
1610
|
plan,
|
|
1141
1611
|
targetRoot: sourcePath,
|
|
1142
1612
|
snapshotRoot: snapshotPath,
|
|
1143
|
-
// Same private-staging deferral as the complete snapshot below.
|
|
1144
1613
|
durability: "deferred_private_staging"
|
|
1145
1614
|
});
|
|
1146
|
-
durabilityScopes.set(mount.id, [...plan.desired.keys()]);
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1615
|
+
durabilityScopes.set(mount.id, [.../* @__PURE__ */ new Set([...plan.desired.keys(), ...decision.selection.remove])]);
|
|
1616
|
+
mergePlans.set(mount.id, { prepared, decision, plan, snapshotPath: import_node_path.default.join(transactionPath, "mounts", snapshotName), basis });
|
|
1617
|
+
} else if (sourceStatus && mount.hydrateDeletionMode === "git") {
|
|
1618
|
+
import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
|
|
1619
|
+
const outerPath = workspaceMountOuterPath(workspacePath, mount);
|
|
1620
|
+
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1621
|
+
trustedRoot: workspacePath,
|
|
1622
|
+
candidate: outerPath,
|
|
1623
|
+
label: `Workspace mount ${mount.id} outer source`
|
|
1624
|
+
});
|
|
1625
|
+
const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({
|
|
1626
|
+
sourceRoot: outerPath,
|
|
1627
|
+
targetRoot: sourcePath,
|
|
1151
1628
|
sourceMode: "all",
|
|
1152
|
-
deletionMode: "
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1629
|
+
deletionMode: "git",
|
|
1630
|
+
sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
|
|
1631
|
+
});
|
|
1632
|
+
scope = (0, import_working_tree_mirror.captureWorkingTreeMirrorPlan)({
|
|
1633
|
+
plan,
|
|
1634
|
+
targetRoot: sourcePath,
|
|
1635
|
+
snapshotRoot: snapshotPath,
|
|
1636
|
+
// Same private-staging deferral as the complete snapshot below.
|
|
1156
1637
|
durability: "deferred_private_staging"
|
|
1157
1638
|
});
|
|
1639
|
+
durabilityScopes.set(mount.id, [...plan.desired.keys()]);
|
|
1640
|
+
} else {
|
|
1641
|
+
import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
|
|
1642
|
+
if (sourceStatus) {
|
|
1643
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({
|
|
1644
|
+
sourceRoot: sourcePath,
|
|
1645
|
+
targetRoot: snapshotPath,
|
|
1646
|
+
sourceMode: "all",
|
|
1647
|
+
deletionMode: "all",
|
|
1648
|
+
// This tree is still private and the source authority is untouched.
|
|
1649
|
+
// The single recursive barrier below makes the complete snapshot
|
|
1650
|
+
// durable before its directory entry is published or hydration starts.
|
|
1651
|
+
durability: "deferred_private_staging"
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1158
1654
|
}
|
|
1159
|
-
|
|
1655
|
+
manifestMounts.push({
|
|
1160
1656
|
id: mount.id,
|
|
1161
1657
|
incarnationKey: mount.hydrationIncarnationKey,
|
|
1162
1658
|
sourcePath,
|
|
@@ -1164,23 +1660,27 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1164
1660
|
workspaceRelativePath: mount.workspaceRelativePath,
|
|
1165
1661
|
existed: Boolean(sourceStatus),
|
|
1166
1662
|
snapshotName,
|
|
1167
|
-
...scope ? { scope } : {}
|
|
1168
|
-
|
|
1169
|
-
|
|
1663
|
+
...scope ? { scope } : {},
|
|
1664
|
+
recovery: mergePlans.has(mount.id) ? "guarded" : "snapshot"
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
const effectiveTargetReceipt = revertReceiptMounts(targetReceipt, receiptBefore, new Set(skippedMounts.map(({ mountId }) => mountId)));
|
|
1668
|
+
const guarded = mergePlans.size > 0;
|
|
1170
1669
|
const manifest = {
|
|
1171
|
-
// Older workers reject
|
|
1172
|
-
//
|
|
1173
|
-
version: manifestMounts.some((mount) => mount.scope) ? 4 : 3,
|
|
1174
|
-
|
|
1670
|
+
// Older workers reject guarded transactions rather than replaying them
|
|
1671
|
+
// through an unguarded snapshot restore after a downgrade.
|
|
1672
|
+
version: guarded ? 5 : manifestMounts.some((mount) => mount.scope) ? 4 : 3,
|
|
1673
|
+
...guarded ? { transactionId: crypto.randomUUID() } : {},
|
|
1674
|
+
targetReceipt: effectiveTargetReceipt,
|
|
1175
1675
|
receiptBefore,
|
|
1176
|
-
mounts: manifestMounts
|
|
1676
|
+
mounts: guarded ? manifestMounts : manifestMounts.map(({ recovery: _recovery, ...mount }) => mount)
|
|
1177
1677
|
};
|
|
1178
1678
|
writePrivateFileDurably(import_node_path.default.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
|
|
1179
1679
|
`);
|
|
1180
1680
|
fsyncTree(stagingPath);
|
|
1181
1681
|
import_node_fs.default.renameSync(stagingPath, transactionPath);
|
|
1182
1682
|
fsyncDirectory(stateDirectory);
|
|
1183
|
-
return { manifest, durabilityScopes };
|
|
1683
|
+
return { manifest, durabilityScopes, mergePlans, skippedMounts, targetReceipt: effectiveTargetReceipt };
|
|
1184
1684
|
} catch (error) {
|
|
1185
1685
|
import_node_fs.default.rmSync(stagingPath, { recursive: true, force: true });
|
|
1186
1686
|
fsyncDirectory(stateDirectory);
|
|
@@ -1189,7 +1689,9 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1189
1689
|
}
|
|
1190
1690
|
function markHydrationTransactionDurable(workspacePath) {
|
|
1191
1691
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1192
|
-
|
|
1692
|
+
const temporaryPath = import_node_path.default.join(transactionPath, `.committed.${crypto.randomUUID()}.tmp`);
|
|
1693
|
+
writePrivateFileDurably(temporaryPath, "committed\n");
|
|
1694
|
+
import_node_fs.default.renameSync(temporaryPath, import_node_path.default.join(transactionPath, "committed"));
|
|
1193
1695
|
fsyncDirectory(transactionPath);
|
|
1194
1696
|
}
|
|
1195
1697
|
function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
|
|
@@ -1341,6 +1843,22 @@ async function configureWorkspaceRepository(input) {
|
|
|
1341
1843
|
"configure durable workspace commits and references"
|
|
1342
1844
|
);
|
|
1343
1845
|
await gitAsync(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
|
|
1846
|
+
const attributesPath = import_node_path.default.join(input.workspacePath, ".git", "info", "attributes");
|
|
1847
|
+
const attributes = [
|
|
1848
|
+
"# r5d workspace clone: checkout bytes are carried verbatim; no conversion or filter may change what projection reads or hydration writes.",
|
|
1849
|
+
"* -text -eol -filter -ident -working-tree-encoding",
|
|
1850
|
+
""
|
|
1851
|
+
].join("\n");
|
|
1852
|
+
let currentAttributes = null;
|
|
1853
|
+
try {
|
|
1854
|
+
currentAttributes = await import_node_fs.default.promises.readFile(attributesPath, "utf8");
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
if (error.code !== "ENOENT") throw error;
|
|
1857
|
+
}
|
|
1858
|
+
if (currentAttributes !== attributes) {
|
|
1859
|
+
await import_node_fs.default.promises.mkdir(import_node_path.default.dirname(attributesPath), { recursive: true });
|
|
1860
|
+
await import_node_fs.default.promises.writeFile(attributesPath, attributes, "utf8");
|
|
1861
|
+
}
|
|
1344
1862
|
}
|
|
1345
1863
|
async function configureExistingWorkspaceGitForRemediation(input) {
|
|
1346
1864
|
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
@@ -1472,7 +1990,7 @@ function activeMounts(mounts) {
|
|
|
1472
1990
|
}
|
|
1473
1991
|
return { active, tombstones, skipped };
|
|
1474
1992
|
}
|
|
1475
|
-
function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
1993
|
+
function mirrorMountsToWorkspace(workspacePath, mounts, projectedFiles) {
|
|
1476
1994
|
const projected = [];
|
|
1477
1995
|
for (const mount of mounts) {
|
|
1478
1996
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
@@ -1489,7 +2007,8 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
|
|
|
1489
2007
|
sourceRoot: mount.sourcePath,
|
|
1490
2008
|
targetRoot: import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
|
|
1491
2009
|
sourceMode: mount.sourceMode,
|
|
1492
|
-
deletionMode: "all"
|
|
2010
|
+
deletionMode: "all",
|
|
2011
|
+
...projectedFiles ? { projected: projectedFiles } : {}
|
|
1493
2012
|
});
|
|
1494
2013
|
projected.push({ mount, gitlinks: mirrored.gitlinks });
|
|
1495
2014
|
}
|
|
@@ -1600,11 +2119,60 @@ function outerSubtreeGitlinks(workspacePath, mount) {
|
|
|
1600
2119
|
function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
1601
2120
|
const hydratedMountIds = [];
|
|
1602
2121
|
const skippedMountIds = [];
|
|
2122
|
+
const mergeSkips = [];
|
|
2123
|
+
const rollbackScopes = /* @__PURE__ */ new Map();
|
|
2124
|
+
const preBlobs = {};
|
|
1603
2125
|
for (const mount of mounts) {
|
|
1604
2126
|
if (!options.ignoreBusy && mount.busy?.()) {
|
|
1605
2127
|
skippedMountIds.push(mount.id);
|
|
1606
2128
|
continue;
|
|
1607
2129
|
}
|
|
2130
|
+
const mergePlan = options.mergePlans?.get(mount.id);
|
|
2131
|
+
if (mergePlan) {
|
|
2132
|
+
const journal = { entries: [] };
|
|
2133
|
+
const runtimeJournal = { journal, snapshotPath: mergePlan.snapshotPath };
|
|
2134
|
+
options.journals?.set(mount.id, runtimeJournal);
|
|
2135
|
+
try {
|
|
2136
|
+
(0, import_working_tree_mirror.applyWorkingTreeMirror)(mergePlan.prepared, {
|
|
2137
|
+
selection: mergePlan.decision.selection,
|
|
2138
|
+
guards: {
|
|
2139
|
+
expectations: mergePlan.decision.expectations,
|
|
2140
|
+
overrides: mergePlan.decision.overrides,
|
|
2141
|
+
preBlobs: mergePlan.decision.preBlobs,
|
|
2142
|
+
journal
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
if (!(error instanceof import_working_tree_mirror.WorkingTreeTargetChangedError)) throw error;
|
|
2147
|
+
const rolledBack = (0, import_working_tree_mirror.rollbackWorkingTreeMirrorJournal)({
|
|
2148
|
+
journal,
|
|
2149
|
+
targetRoot: mount.sourcePath,
|
|
2150
|
+
snapshotRoot: mergePlan.snapshotPath
|
|
2151
|
+
});
|
|
2152
|
+
runtimeJournal.rolledBack = rolledBack;
|
|
2153
|
+
const retainedPaths = [
|
|
2154
|
+
...new Set([...error.retained, ...rolledBack.retained ?? []].map(({ path: retainedPath }) => retainedPath))
|
|
2155
|
+
].sort();
|
|
2156
|
+
if (retainedPaths.length > 0) {
|
|
2157
|
+
process.stderr.write(
|
|
2158
|
+
`[r5d-worker] hydration preserved displaced writer entries for mount ${mount.id}: ${retainedPaths.join(", ")}
|
|
2159
|
+
`
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
options.onMergeRollback?.(mount, rolledBack);
|
|
2163
|
+
mergeSkips.push({
|
|
2164
|
+
mountId: mount.id,
|
|
2165
|
+
reason: "hydration_target_changed",
|
|
2166
|
+
paths: [.../* @__PURE__ */ new Set([error.relativePath, ...rolledBack.abandoned])].sort(),
|
|
2167
|
+
...retainedPaths.length > 0 ? { retainedPaths } : {}
|
|
2168
|
+
});
|
|
2169
|
+
rollbackScopes.set(mount.id, rolledBack.paths);
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
hydratedMountIds.push(mount.id);
|
|
2173
|
+
preBlobs[mount.id] = Object.fromEntries(mergePlan.decision.preBlobs);
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
1608
2176
|
const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
|
|
1609
2177
|
(0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
|
|
1610
2178
|
trustedRoot: workspacePath,
|
|
@@ -1650,7 +2218,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1650
2218
|
});
|
|
1651
2219
|
hydratedMountIds.push(mount.id);
|
|
1652
2220
|
}
|
|
1653
|
-
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
|
|
2221
|
+
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort(), mergeSkips, rollbackScopes, preBlobs };
|
|
1654
2222
|
}
|
|
1655
2223
|
async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
|
|
1656
2224
|
const candidates = mounts.flatMap((mount) => {
|
|
@@ -1710,7 +2278,7 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1710
2278
|
};
|
|
1711
2279
|
const noopCandidate = computeSelection();
|
|
1712
2280
|
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)) {
|
|
1713
|
-
return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
|
|
2281
|
+
return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds, mergeSkips: [] };
|
|
1714
2282
|
}
|
|
1715
2283
|
const hooks = input.hydrationHooks;
|
|
1716
2284
|
const context = {
|
|
@@ -1725,13 +2293,30 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1725
2293
|
const heldMountIds = [...new Set([...hydrationMounts, ...advancedDurabilityMounts].map(({ id }) => id))];
|
|
1726
2294
|
const releaseHold = hooks?.holdMounts(heldMountIds, `hydration transaction ${context.transactionId}`);
|
|
1727
2295
|
try {
|
|
2296
|
+
const mergeBases = {};
|
|
2297
|
+
for (const mount of hydrationMounts) {
|
|
2298
|
+
const basis = input.mergeBases?.get(mount.id);
|
|
2299
|
+
if (basis) mergeBases[mount.id] = basis;
|
|
2300
|
+
}
|
|
1728
2301
|
const hydration = await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("hydration_transaction", {
|
|
1729
2302
|
workspacePath,
|
|
1730
2303
|
hydrationMounts: hydrationMounts.map(workspaceGitMountData),
|
|
1731
2304
|
advancedDurabilityMounts: advancedDurabilityMounts.map(workspaceGitMountData),
|
|
1732
|
-
targetReceipt
|
|
2305
|
+
targetReceipt,
|
|
2306
|
+
...Object.keys(mergeBases).length > 0 ? { mergeBases } : {}
|
|
1733
2307
|
});
|
|
1734
|
-
|
|
2308
|
+
const ledger = hydrationPreBlobLedger(workspacePath);
|
|
2309
|
+
for (const mount of hydrationMounts) {
|
|
2310
|
+
if (!hydration.hydratedMountIds.includes(mount.id)) continue;
|
|
2311
|
+
const preBlobs = mergeBases[mount.id] ? hydration.hydratedPreBlobs[mount.id] : void 0;
|
|
2312
|
+
if (preBlobs) ledger.record(mount, preBlobs);
|
|
2313
|
+
else ledger.clearMount(mount);
|
|
2314
|
+
}
|
|
2315
|
+
return {
|
|
2316
|
+
hydratedMountIds: hydration.hydratedMountIds,
|
|
2317
|
+
skippedMountIds: [.../* @__PURE__ */ new Set([...selection.skippedMountIds, ...hydration.skippedMounts.map(({ mountId }) => mountId)])].sort(),
|
|
2318
|
+
mergeSkips: hydration.skippedMounts
|
|
2319
|
+
};
|
|
1735
2320
|
} finally {
|
|
1736
2321
|
releaseHold?.();
|
|
1737
2322
|
}
|
|
@@ -1741,28 +2326,90 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1741
2326
|
}
|
|
1742
2327
|
function runHydrationTransactionJob(input) {
|
|
1743
2328
|
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
1744
|
-
const { hydrationMounts, advancedDurabilityMounts
|
|
1745
|
-
const
|
|
2329
|
+
const { hydrationMounts, advancedDurabilityMounts } = input;
|
|
2330
|
+
const begun = beginHydrationTransaction(workspacePath, hydrationMounts, input.targetReceipt, input.mergeBases);
|
|
2331
|
+
const { durabilityScopes, mergePlans, skippedMounts } = begun;
|
|
2332
|
+
let manifest = begun.manifest;
|
|
2333
|
+
let targetReceipt = begun.targetReceipt;
|
|
2334
|
+
const skippedIds = new Set(skippedMounts.map(({ mountId }) => mountId));
|
|
2335
|
+
const journals = /* @__PURE__ */ new Map();
|
|
2336
|
+
const excludeRolledBackMount = (mount, rollback) => {
|
|
2337
|
+
const settledMount = manifest.mounts.find(({ id }) => id === mount.id);
|
|
2338
|
+
if (!settledMount) return;
|
|
2339
|
+
fsyncHydratedWorkspaceMount(mount, rollback.paths);
|
|
2340
|
+
fsyncHydrationRetainedEvidence(mount);
|
|
2341
|
+
if (rollback.retained?.length) {
|
|
2342
|
+
process.stderr.write(
|
|
2343
|
+
`[r5d-worker] hydration rollback retained writer entries for mount ${mount.id}: ${rollback.retained.map(({ path: retainedPath }) => retainedPath).join(", ")}
|
|
2344
|
+
`
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
const nextReceipt = revertReceiptMounts(targetReceipt, manifest.receiptBefore, /* @__PURE__ */ new Set([mount.id]));
|
|
2348
|
+
const nextManifest = {
|
|
2349
|
+
...manifest,
|
|
2350
|
+
targetReceipt: nextReceipt,
|
|
2351
|
+
mounts: manifest.mounts.filter(({ id }) => id !== mount.id),
|
|
2352
|
+
preservedMounts: [...manifest.preservedMounts ?? [], settledMount]
|
|
2353
|
+
};
|
|
2354
|
+
rewriteHydrationTransactionManifest(workspacePath, nextManifest);
|
|
2355
|
+
manifest = nextManifest;
|
|
2356
|
+
targetReceipt = nextReceipt;
|
|
2357
|
+
};
|
|
1746
2358
|
try {
|
|
1747
|
-
const
|
|
1748
|
-
const
|
|
2359
|
+
const activeMounts2 = hydrationMounts.filter(({ id }) => !skippedIds.has(id));
|
|
2360
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, activeMounts2, {
|
|
2361
|
+
ignoreBusy: true,
|
|
2362
|
+
mergePlans,
|
|
2363
|
+
journals,
|
|
2364
|
+
onMergeRollback: excludeRolledBackMount
|
|
2365
|
+
});
|
|
2366
|
+
if (hydration.mergeSkips.length > 0) {
|
|
2367
|
+
for (const skip of hydration.mergeSkips) {
|
|
2368
|
+
skippedMounts.push(skip);
|
|
2369
|
+
skippedIds.add(skip.mountId);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
const expectedHydratedIds = activeMounts2.map(({ id }) => id).filter((id) => !skippedIds.has(id)).sort();
|
|
1749
2373
|
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
1750
2374
|
throw new Error("Workspace hydration did not include every required mount");
|
|
1751
2375
|
}
|
|
1752
|
-
fsyncHydratedWorkspaceMounts(
|
|
2376
|
+
fsyncHydratedWorkspaceMounts(
|
|
2377
|
+
advancedDurabilityMounts.filter(({ id }) => !skippedIds.has(id)),
|
|
2378
|
+
durabilityScopes
|
|
2379
|
+
);
|
|
1753
2380
|
markHydrationTransactionDurable(workspacePath);
|
|
1754
2381
|
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1755
2382
|
removeHydrationTransaction(workspacePath);
|
|
1756
|
-
return { hydratedMountIds: hydration.hydratedMountIds };
|
|
2383
|
+
return { hydratedMountIds: hydration.hydratedMountIds, skippedMounts, hydratedPreBlobs: hydration.preBlobs };
|
|
1757
2384
|
} catch (error) {
|
|
1758
2385
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1759
2386
|
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1760
2387
|
throw error;
|
|
1761
2388
|
}
|
|
1762
2389
|
try {
|
|
2390
|
+
for (const [mountId, mergePlan] of mergePlans) {
|
|
2391
|
+
const mount = hydrationMounts.find(({ id }) => id === mountId);
|
|
2392
|
+
if (!mount || !manifest.mounts.some(({ id }) => id === mountId)) continue;
|
|
2393
|
+
const runtimeJournal = journals.get(mountId);
|
|
2394
|
+
const rolledBack = runtimeJournal ? runtimeJournal.rolledBack ?? (0, import_working_tree_mirror.rollbackWorkingTreeMirrorJournal)({
|
|
2395
|
+
journal: runtimeJournal.journal,
|
|
2396
|
+
targetRoot: mount.sourcePath,
|
|
2397
|
+
snapshotRoot: mergePlan.snapshotPath
|
|
2398
|
+
}) : { paths: [], abandoned: [] };
|
|
2399
|
+
if (runtimeJournal) runtimeJournal.rolledBack = rolledBack;
|
|
2400
|
+
excludeRolledBackMount(mount, rolledBack);
|
|
2401
|
+
}
|
|
1763
2402
|
restoreHydrationTransaction(workspacePath, manifest);
|
|
1764
2403
|
removeHydrationTransaction(workspacePath);
|
|
1765
2404
|
} catch (restoreError) {
|
|
2405
|
+
const pendingManifest = parseHydrationTransactionManifest(transactionPath);
|
|
2406
|
+
if (pendingManifest.mounts.some(({ recovery }) => recovery === "guarded")) {
|
|
2407
|
+
throw hydrationRecoveryRequiredError(
|
|
2408
|
+
workspacePath,
|
|
2409
|
+
pendingManifest,
|
|
2410
|
+
new AggregateError([error, restoreError], "Guarded hydration rollback did not settle")
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
1766
2413
|
throw new AggregateError(
|
|
1767
2414
|
[error, restoreError],
|
|
1768
2415
|
`Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -1824,6 +2471,7 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
|
|
|
1824
2471
|
});
|
|
1825
2472
|
const currentHead = await revParseAsync(workspacePath, "HEAD");
|
|
1826
2473
|
const currentReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
2474
|
+
const preservedBases = await readPreservedHydrationBasesAsync(workspacePath);
|
|
1827
2475
|
if (!currentHead) return;
|
|
1828
2476
|
const outerHeadContainsMount = (mount) => tryGitAsync(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
|
|
1829
2477
|
const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
|
|
@@ -1833,7 +2481,7 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
|
|
|
1833
2481
|
if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
|
|
1834
2482
|
const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1835
2483
|
const recoveryMounts = uncoveredBasisMounts.filter(
|
|
1836
|
-
(mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
|
|
2484
|
+
(mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id) && workspaceHydrationReceiptMountBasisHead(preservedBases, mount) !== workspaceHydrationReceiptMountBasisHead(currentReceipt, mount)
|
|
1837
2485
|
);
|
|
1838
2486
|
const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
|
|
1839
2487
|
const hydrationTargets = [];
|
|
@@ -1863,6 +2511,7 @@ async function resetWorkspaceGit(input) {
|
|
|
1863
2511
|
if (busyResetMountIds.length > 0) {
|
|
1864
2512
|
throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
|
|
1865
2513
|
}
|
|
2514
|
+
hydrationPreBlobLedger(workspacePath).clear();
|
|
1866
2515
|
const initial = await ensureWorkspaceGitClone({ ...input, workspacePath });
|
|
1867
2516
|
await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("hydration_recovery", { workspacePath, preserveResolutionInProgress: false });
|
|
1868
2517
|
const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
|
|
@@ -2206,6 +2855,24 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2206
2855
|
const mergedProjectionMounts = [];
|
|
2207
2856
|
const projectionSkippedIds = /* @__PURE__ */ new Set();
|
|
2208
2857
|
let projectionWarning;
|
|
2858
|
+
const projectedMountFiles = /* @__PURE__ */ new Map();
|
|
2859
|
+
const mergedProjectionBases = /* @__PURE__ */ new Map();
|
|
2860
|
+
const projectedMountBases = /* @__PURE__ */ new Map();
|
|
2861
|
+
const mountSkipReasons = {};
|
|
2862
|
+
const staleRewriteLedger = hydrationPreBlobLedger(workspacePath);
|
|
2863
|
+
const staleRewriteBlobsFor = (mount) => {
|
|
2864
|
+
const preBlobs = staleRewriteLedger.preBlobs(mount);
|
|
2865
|
+
return preBlobs.size > 0 ? Object.fromEntries(preBlobs) : void 0;
|
|
2866
|
+
};
|
|
2867
|
+
const recordStaleRewrite = (mount, paths) => {
|
|
2868
|
+
projectionSkippedIds.add(mount.id);
|
|
2869
|
+
mountSkipReasons[mount.id] = { reason: "stale_rewrite_detected", paths: [...paths] };
|
|
2870
|
+
const holders = mount.describeHolders?.();
|
|
2871
|
+
process.stderr.write(
|
|
2872
|
+
`[r5d-worker] stale rewrite detected in mount ${mount.id}: ${(0, import_workspace_hydration_merge.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}` : ""}
|
|
2873
|
+
`
|
|
2874
|
+
);
|
|
2875
|
+
};
|
|
2209
2876
|
const refreshCycleSkippedMounts = (mounts = input.mounts) => {
|
|
2210
2877
|
for (const mount of mounts) {
|
|
2211
2878
|
const projectionToken = projectionMutationTokens.get(mount.id);
|
|
@@ -2228,7 +2895,8 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2228
2895
|
const token = projectionMutationTokens.get(id);
|
|
2229
2896
|
return token === void 0 ? [] : [[id, token]];
|
|
2230
2897
|
})
|
|
2231
|
-
)
|
|
2898
|
+
),
|
|
2899
|
+
...Object.keys(mountSkipReasons).length > 0 ? { mountSkipReasons: { ...mountSkipReasons } } : {}
|
|
2232
2900
|
};
|
|
2233
2901
|
};
|
|
2234
2902
|
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;
|
|
@@ -2258,6 +2926,7 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2258
2926
|
const releaseMergeHold = input.hydrationHooks?.holdMounts([mount.id], `merge projection of mount ${mount.id}`);
|
|
2259
2927
|
let merged;
|
|
2260
2928
|
try {
|
|
2929
|
+
const staleRewriteBlobs = staleRewriteBlobsFor(mount);
|
|
2261
2930
|
merged = await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("projection_merge", {
|
|
2262
2931
|
workspacePath,
|
|
2263
2932
|
mount: {
|
|
@@ -2268,11 +2937,22 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2268
2937
|
},
|
|
2269
2938
|
basisHead,
|
|
2270
2939
|
currentHead: projectionBasisHead,
|
|
2271
|
-
attemptId
|
|
2940
|
+
attemptId,
|
|
2941
|
+
...staleRewriteBlobs ? { staleRewriteBlobs } : {}
|
|
2272
2942
|
});
|
|
2273
2943
|
} finally {
|
|
2274
2944
|
releaseMergeHold?.();
|
|
2275
2945
|
}
|
|
2946
|
+
if (merged.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, merged.staleRewriteCleared);
|
|
2947
|
+
if (merged.kind === "stale_rewrite") {
|
|
2948
|
+
recordStaleRewrite(mount, merged.paths);
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
mergedProjectionBases.set(mount.id, {
|
|
2952
|
+
projectedCommit: merged.oursCommit,
|
|
2953
|
+
projectedFiles: merged.projectedFiles,
|
|
2954
|
+
readAtMs: merged.readAtMs
|
|
2955
|
+
});
|
|
2276
2956
|
if (merged.kind === "conflict") {
|
|
2277
2957
|
const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
|
|
2278
2958
|
return {
|
|
@@ -2325,17 +3005,44 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2325
3005
|
currentHeadBeforeHydration,
|
|
2326
3006
|
uncoveredCompletionMounts
|
|
2327
3007
|
) : /* @__PURE__ */ new Set();
|
|
3008
|
+
const hydrationMounts = uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id));
|
|
3009
|
+
const mergeBases = /* @__PURE__ */ new Map();
|
|
3010
|
+
if (workspaceMergeHydrationEnabled()) {
|
|
3011
|
+
for (const mount of hydrationMounts) {
|
|
3012
|
+
const basis = projectedMountBases.get(mount.id);
|
|
3013
|
+
if (basis) mergeBases.set(mount.id, basis);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
2328
3016
|
const hydration = await hydrateWorkspaceGitMountsTransactionally({
|
|
2329
3017
|
workspacePath,
|
|
2330
|
-
mounts:
|
|
3018
|
+
mounts: hydrationMounts,
|
|
2331
3019
|
durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2332
3020
|
carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
|
|
2333
3021
|
receiptMounts: completionReceiptMounts,
|
|
2334
3022
|
requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2335
3023
|
recordCurrentHead: true,
|
|
2336
|
-
hydrationHooks: input.hydrationHooks
|
|
3024
|
+
hydrationHooks: input.hydrationHooks,
|
|
3025
|
+
...mergeBases.size > 0 ? { mergeBases } : {}
|
|
2337
3026
|
});
|
|
2338
3027
|
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
3028
|
+
if (currentHeadBeforeHydration) {
|
|
3029
|
+
for (const id of hydration.hydratedMountIds) {
|
|
3030
|
+
if (mergeBases.has(id)) projectedMountBases.set(id, { projectedCommit: currentHeadBeforeHydration, projectedFiles: null });
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
for (const skip of hydration.mergeSkips) {
|
|
3034
|
+
const mount = hydrationMounts.find(({ id }) => id === skip.mountId);
|
|
3035
|
+
const basis = mount ? workspaceHydrationReceiptMountBasisHead(currentReceiptBeforeHydration, mount) : null;
|
|
3036
|
+
mountSkipReasons[skip.mountId] = {
|
|
3037
|
+
reason: skip.reason,
|
|
3038
|
+
paths: skip.paths,
|
|
3039
|
+
...skip.retainedPaths?.length ? { retainedPaths: skip.retainedPaths } : {}
|
|
3040
|
+
};
|
|
3041
|
+
process.stderr.write(
|
|
3042
|
+
`[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"} ${(0, import_workspace_hydration_merge.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(", ")}` : ""}
|
|
3043
|
+
`
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
2339
3046
|
refreshCycleSkippedMounts(completionReceiptMounts);
|
|
2340
3047
|
if (requireCurrentHeadReceipt) {
|
|
2341
3048
|
const currentHead = await revParseAsync(workspacePath, "HEAD");
|
|
@@ -2376,10 +3083,11 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2376
3083
|
};
|
|
2377
3084
|
const projectedFastMounts = [];
|
|
2378
3085
|
const currentProjectionMounts = [];
|
|
3086
|
+
const selectedActiveIdSet = new Set(selected.active.map(({ id }) => id));
|
|
2379
3087
|
if (!input.skipMountMirror) {
|
|
2380
3088
|
await beginWorkspaceCheckoutTransition(workspacePath);
|
|
2381
3089
|
try {
|
|
2382
|
-
const selectedActiveIds =
|
|
3090
|
+
const selectedActiveIds = selectedActiveIdSet;
|
|
2383
3091
|
const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
|
|
2384
3092
|
const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
|
|
2385
3093
|
for (const mount of fastActiveMounts) {
|
|
@@ -2392,15 +3100,27 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2392
3100
|
continue;
|
|
2393
3101
|
}
|
|
2394
3102
|
const releaseMirrorHold = input.hydrationHooks?.holdMounts([mount.id], `projection of mount ${mount.id}`);
|
|
3103
|
+
let staleRewritePaths;
|
|
2395
3104
|
try {
|
|
3105
|
+
const staleRewriteBlobs = staleRewriteBlobsFor(mount);
|
|
2396
3106
|
const mirrored = await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("projection_mirror", {
|
|
2397
3107
|
workspacePath,
|
|
2398
|
-
mount: workspaceGitMountData(mount)
|
|
3108
|
+
mount: workspaceGitMountData(mount),
|
|
3109
|
+
...staleRewriteBlobs ? { staleRewriteBlobs } : {}
|
|
2399
3110
|
});
|
|
2400
|
-
|
|
3111
|
+
staleRewritePaths = mirrored.staleRewritePaths;
|
|
3112
|
+
if (mirrored.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, mirrored.staleRewriteCleared);
|
|
3113
|
+
if (staleRewritePaths.length === 0) {
|
|
3114
|
+
projectedGitlinks.push({ mount, gitlinks: mirrored.gitlinks });
|
|
3115
|
+
projectedMountFiles.set(mount.id, { files: mirrored.projectedFiles, readAtMs: mirrored.readAtMs });
|
|
3116
|
+
}
|
|
2401
3117
|
} finally {
|
|
2402
3118
|
releaseMirrorHold?.();
|
|
2403
3119
|
}
|
|
3120
|
+
if (staleRewritePaths.length > 0) {
|
|
3121
|
+
recordStaleRewrite(mount, staleRewritePaths);
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
2404
3124
|
projectedFastMounts.push(mount);
|
|
2405
3125
|
}
|
|
2406
3126
|
await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("projection_outer_apply", {
|
|
@@ -2427,6 +3147,18 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
|
2427
3147
|
if (!input.skipMountMirror) {
|
|
2428
3148
|
const projectedHead = await revParseAsync(workspacePath, "HEAD");
|
|
2429
3149
|
if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
|
|
3150
|
+
for (const mount of projectedFastMounts) {
|
|
3151
|
+
if (!selectedActiveIdSet.has(mount.id)) continue;
|
|
3152
|
+
const read = projectedMountFiles.get(mount.id);
|
|
3153
|
+
projectedMountBases.set(mount.id, {
|
|
3154
|
+
projectedCommit: projectedHead,
|
|
3155
|
+
projectedFiles: read?.files ?? null,
|
|
3156
|
+
...read ? { readAtMs: read.readAtMs } : {}
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
for (const mount of currentProjectionMounts)
|
|
3160
|
+
projectedMountBases.set(mount.id, { projectedCommit: projectedHead, projectedFiles: null });
|
|
3161
|
+
for (const [mountId, basis] of mergedProjectionBases) projectedMountBases.set(mountId, basis);
|
|
2430
3162
|
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
2431
3163
|
projectionBasisReceipt,
|
|
2432
3164
|
projectedHead,
|
|
@@ -2606,9 +3338,52 @@ ${push.stdout}`)) {
|
|
|
2606
3338
|
}
|
|
2607
3339
|
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
2608
3340
|
}
|
|
3341
|
+
function detectStaleRewrites(workspacePath, mount, staleRewriteBlobs) {
|
|
3342
|
+
const outerRoot = workspaceMountOuterPath(workspacePath, mount);
|
|
3343
|
+
const stale = [];
|
|
3344
|
+
const cleared = [];
|
|
3345
|
+
for (const [relativePath, blob] of Object.entries(staleRewriteBlobs)) {
|
|
3346
|
+
const algorithm = (0, import_git_blob_hash.gitObjectHashAlgorithmFor)(blob);
|
|
3347
|
+
if (!algorithm) continue;
|
|
3348
|
+
const source = (0, import_working_tree_mirror.inspectWorkingTreePath)(mount.sourcePath, relativePath);
|
|
3349
|
+
if (source.kind !== "entry" || !source.stat.isFile()) {
|
|
3350
|
+
cleared.push(relativePath);
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
const content = import_node_fs.default.readFileSync(source.absolutePath);
|
|
3354
|
+
const outer = (0, import_working_tree_mirror.inspectWorkingTreePath)(outerRoot, relativePath);
|
|
3355
|
+
const outerEqual = outer.kind === "entry" && outer.stat.isFile() && import_node_fs.default.readFileSync(outer.absolutePath).equals(content);
|
|
3356
|
+
if ((0, import_git_blob_hash.gitBlobHash)(content, algorithm) !== blob) {
|
|
3357
|
+
if (!outerEqual) cleared.push(relativePath);
|
|
3358
|
+
continue;
|
|
3359
|
+
}
|
|
3360
|
+
if (outerEqual) {
|
|
3361
|
+
cleared.push(relativePath);
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
stale.push(relativePath);
|
|
3365
|
+
}
|
|
3366
|
+
return { stale: stale.sort(), cleared: cleared.sort() };
|
|
3367
|
+
}
|
|
2609
3368
|
function runProjectionMirrorJob(input) {
|
|
2610
|
-
const
|
|
2611
|
-
|
|
3369
|
+
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
3370
|
+
let staleRewriteCleared = [];
|
|
3371
|
+
if (input.staleRewriteBlobs) {
|
|
3372
|
+
const detected = detectStaleRewrites(workspacePath, input.mount, input.staleRewriteBlobs);
|
|
3373
|
+
staleRewriteCleared = detected.cleared;
|
|
3374
|
+
if (detected.stale.length > 0) {
|
|
3375
|
+
return { gitlinks: [], projectedFiles: {}, readAtMs: Date.now(), staleRewritePaths: detected.stale, staleRewriteCleared };
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
const projectedFiles = /* @__PURE__ */ new Map();
|
|
3379
|
+
const projected = mirrorMountsToWorkspace(workspacePath, [input.mount], projectedFiles);
|
|
3380
|
+
return {
|
|
3381
|
+
gitlinks: projected[0]?.gitlinks ?? [],
|
|
3382
|
+
projectedFiles: Object.fromEntries(projectedFiles),
|
|
3383
|
+
readAtMs: Date.now(),
|
|
3384
|
+
staleRewritePaths: [],
|
|
3385
|
+
staleRewriteCleared
|
|
3386
|
+
};
|
|
2612
3387
|
}
|
|
2613
3388
|
function runProjectionOuterApplyJob(input) {
|
|
2614
3389
|
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
@@ -2626,11 +3401,16 @@ function runProjectionReceiptJob(input) {
|
|
|
2626
3401
|
return null;
|
|
2627
3402
|
}
|
|
2628
3403
|
const workspaceGitSyncTestHarness = {
|
|
3404
|
+
beginHydrationTransaction,
|
|
2629
3405
|
commandArgs: gitCommandArgs,
|
|
2630
3406
|
workspaceCloneCommandArgs,
|
|
2631
3407
|
configureWorkspaceRepository,
|
|
2632
3408
|
mirrorMountsToWorkspace,
|
|
2633
|
-
hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts
|
|
3409
|
+
hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts,
|
|
3410
|
+
hydrationPreBlobLedger,
|
|
3411
|
+
resetHydrationPreBlobLedgers() {
|
|
3412
|
+
hydrationPreBlobLedgers.clear();
|
|
3413
|
+
}
|
|
2634
3414
|
};
|
|
2635
3415
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2636
3416
|
0 && (module.exports = {
|
|
@@ -2639,7 +3419,10 @@ const workspaceGitSyncTestHarness = {
|
|
|
2639
3419
|
WORKSPACE_GIT_CHECKOUT_DURABILITY,
|
|
2640
3420
|
WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
|
|
2641
3421
|
WORKSPACE_GIT_HYDRATED_RECEIPT,
|
|
3422
|
+
WORKSPACE_GIT_HYDRATION_RECOVERIES,
|
|
2642
3423
|
WORKSPACE_GIT_HYDRATION_TRANSACTION,
|
|
3424
|
+
WORKSPACE_MERGE_HYDRATION_SWITCH,
|
|
3425
|
+
WorkspaceHydrationRecoveryRequiredError,
|
|
2643
3426
|
WorkspaceRemediationAncestryError,
|
|
2644
3427
|
configureExistingWorkspaceGitForRemediation,
|
|
2645
3428
|
ensureWorkspaceGitClone,
|
|
@@ -2649,7 +3432,9 @@ const workspaceGitSyncTestHarness = {
|
|
|
2649
3432
|
runCheckoutDurabilityRecoveryJob,
|
|
2650
3433
|
runCheckoutTransitionBeginJob,
|
|
2651
3434
|
runCheckoutTransitionCompleteJob,
|
|
3435
|
+
runHydrationPreserveInterruptedJob,
|
|
2652
3436
|
runHydrationRawJob,
|
|
3437
|
+
runHydrationRecoveryInspectJob,
|
|
2653
3438
|
runHydrationRecoveryJob,
|
|
2654
3439
|
runHydrationTransactionJob,
|
|
2655
3440
|
runOuterCheckoutFsyncJob,
|