@ricsam/r5d-worker 0.0.137 → 0.0.138

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.
Files changed (236) hide show
  1. package/README.md +4 -94
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/mjs/main.mjs +46 -7543
  4. package/dist/mjs/package.json +1 -1
  5. package/dist/mjs/personal/action-scheduler.mjs +31 -0
  6. package/dist/mjs/personal/client.mjs +215 -0
  7. package/dist/mjs/personal/runtime.mjs +328 -0
  8. package/dist/mjs/personal/tcp.mjs +217 -0
  9. package/dist/mjs/runtime/adapter.mjs +42 -0
  10. package/dist/mjs/runtime/client.mjs +95 -0
  11. package/dist/mjs/runtime/daemon.mjs +131 -0
  12. package/dist/mjs/runtime/executor.mjs +436 -0
  13. package/dist/mjs/runtime/index.mjs +15 -0
  14. package/dist/mjs/runtime/protocol.mjs +139 -0
  15. package/dist/mjs/runtime/pty-host.mjs +156 -0
  16. package/dist/mjs/runtime/pty.mjs +127 -0
  17. package/dist/mjs/runtime/releases/artifacts.mjs +201 -0
  18. package/dist/mjs/runtime/releases/grants.mjs +93 -0
  19. package/dist/mjs/runtime/releases/main.mjs +101 -0
  20. package/dist/mjs/runtime/releases/manager.mjs +541 -0
  21. package/dist/mjs/runtime/releases/process.mjs +88 -0
  22. package/dist/mjs/runtime/releases/retirement.mjs +18 -0
  23. package/dist/mjs/runtime/releases/rpc-client.mjs +184 -0
  24. package/dist/mjs/runtime/releases/rpc-main.mjs +213 -0
  25. package/dist/mjs/runtime/releases/rpc-protocol.mjs +103 -0
  26. package/dist/mjs/runtime/storage.mjs +156 -0
  27. package/dist/mjs/runtime/workspace/artifacts.mjs +121 -0
  28. package/dist/mjs/runtime/workspace/authority.mjs +1355 -0
  29. package/dist/mjs/runtime/workspace/contracts.mjs +60 -0
  30. package/dist/mjs/runtime/workspace/file-write.mjs +23 -0
  31. package/dist/mjs/runtime/workspace/files.mjs +308 -0
  32. package/dist/mjs/runtime/workspace/http-body.mjs +45 -0
  33. package/dist/mjs/runtime/workspace/storage-client.mjs +59 -0
  34. package/dist/mjs/runtime/workspace/storage-wire.mjs +53 -0
  35. package/dist/types/main.d.ts +0 -1167
  36. package/dist/types/personal/action-scheduler.d.ts +23 -0
  37. package/dist/types/personal/client.d.ts +14 -0
  38. package/dist/types/personal/runtime.d.ts +121 -0
  39. package/dist/types/personal/tcp.d.ts +14 -0
  40. package/dist/types/runtime/client.d.ts +3 -1
  41. package/dist/types/runtime/executor.d.ts +6 -1
  42. package/dist/types/runtime/protocol.d.ts +176 -4
  43. package/dist/types/runtime/pty.d.ts +1 -0
  44. package/dist/types/runtime/releases/artifacts.d.ts +51 -0
  45. package/dist/types/runtime/releases/grants.d.ts +16 -0
  46. package/dist/types/runtime/releases/manager.d.ts +145 -0
  47. package/dist/types/runtime/releases/process.d.ts +14 -0
  48. package/dist/types/runtime/releases/retirement.d.ts +15 -0
  49. package/dist/types/runtime/releases/rpc-client.d.ts +52 -0
  50. package/dist/types/runtime/releases/rpc-main.d.ts +10 -0
  51. package/dist/types/runtime/releases/rpc-protocol.d.ts +1139 -0
  52. package/dist/types/runtime/workspace/artifacts.d.ts +30 -0
  53. package/dist/types/runtime/workspace/authority.d.ts +338 -0
  54. package/dist/types/runtime/workspace/contracts.d.ts +86 -0
  55. package/dist/types/runtime/workspace/file-write.d.ts +14 -0
  56. package/dist/types/runtime/workspace/files.d.ts +21 -0
  57. package/dist/types/runtime/workspace/http-body.d.ts +9 -0
  58. package/dist/types/runtime/workspace/storage-client.d.ts +23 -0
  59. package/dist/types/runtime/workspace/storage-wire.d.ts +138 -0
  60. package/package.json +8 -6
  61. package/dist/cjs/atomic-rename.cjs +0 -303
  62. package/dist/cjs/browser-upload.cjs +0 -73
  63. package/dist/cjs/cli-update.cjs +0 -108
  64. package/dist/cjs/command-launcher.cjs +0 -423
  65. package/dist/cjs/control-command-policy.cjs +0 -177
  66. package/dist/cjs/git-blob-hash.cjs +0 -41
  67. package/dist/cjs/git-identity.cjs +0 -39
  68. package/dist/cjs/git-process-environment.cjs +0 -133
  69. package/dist/cjs/inline-output.cjs +0 -62
  70. package/dist/cjs/main.cjs +0 -7517
  71. package/dist/cjs/managed-paths.cjs +0 -215
  72. package/dist/cjs/plan-lint.cjs +0 -165
  73. package/dist/cjs/plan-parser.cjs +0 -127
  74. package/dist/cjs/plan-paths.cjs +0 -53
  75. package/dist/cjs/port-forward-client.cjs +0 -206
  76. package/dist/cjs/process-tree.cjs +0 -117
  77. package/dist/cjs/project-checkout-garbage.cjs +0 -219
  78. package/dist/cjs/project-mirror-fetch-policy.cjs +0 -82
  79. package/dist/cjs/project-mirror-refs-token.cjs +0 -85
  80. package/dist/cjs/project-workspace-state.cjs +0 -972
  81. package/dist/cjs/project-worktrees.cjs +0 -1713
  82. package/dist/cjs/pty-output-coalescer.cjs +0 -87
  83. package/dist/cjs/recovery-journal-protocol.cjs +0 -56
  84. package/dist/cjs/recovery-journal-runtime.cjs +0 -133
  85. package/dist/cjs/recovery-journal-thread.cjs +0 -9
  86. package/dist/cjs/recovery-journal.cjs +0 -742
  87. package/dist/cjs/recovery-output-window.cjs +0 -77
  88. package/dist/cjs/recovery-protocol.cjs +0 -31
  89. package/dist/cjs/recovery-store.cjs +0 -252
  90. package/dist/cjs/registry-auth.cjs +0 -310
  91. package/dist/cjs/repository-transition-policy.cjs +0 -49
  92. package/dist/cjs/runtime-version.cjs +0 -88
  93. package/dist/cjs/session-file-mutations.cjs +0 -61
  94. package/dist/cjs/supervisor.cjs +0 -147
  95. package/dist/cjs/three-way-merge.cjs +0 -346
  96. package/dist/cjs/working-tree-mirror.cjs +0 -1872
  97. package/dist/cjs/workspace-automatic-sync-policy.cjs +0 -79
  98. package/dist/cjs/workspace-branch-incarnation-policy.cjs +0 -37
  99. package/dist/cjs/workspace-command-sync-policy.cjs +0 -112
  100. package/dist/cjs/workspace-command-targets.cjs +0 -63
  101. package/dist/cjs/workspace-filesystem-executor-thread.cjs +0 -36
  102. package/dist/cjs/workspace-filesystem-executor.cjs +0 -327
  103. package/dist/cjs/workspace-filesystem-job-types.cjs +0 -144
  104. package/dist/cjs/workspace-filesystem-jobs.cjs +0 -59
  105. package/dist/cjs/workspace-git-sync.cjs +0 -3595
  106. package/dist/cjs/workspace-hydration-ledger.cjs +0 -83
  107. package/dist/cjs/workspace-hydration-merge.cjs +0 -448
  108. package/dist/cjs/workspace-hydration-recovery-state.cjs +0 -53
  109. package/dist/cjs/workspace-incident-state.cjs +0 -37
  110. package/dist/cjs/workspace-merge-projection.cjs +0 -628
  111. package/dist/cjs/workspace-mount-boundary.cjs +0 -71
  112. package/dist/cjs/workspace-mount-hold-fence.cjs +0 -120
  113. package/dist/cjs/workspace-mutation-gate.cjs +0 -127
  114. package/dist/cjs/workspace-path-move.cjs +0 -195
  115. package/dist/cjs/workspace-preserve-only-policy.cjs +0 -36
  116. package/dist/cjs/workspace-project-config-policy.cjs +0 -53
  117. package/dist/cjs/workspace-projection-ledger.cjs +0 -81
  118. package/dist/cjs/workspace-publication-evidence.cjs +0 -46
  119. package/dist/cjs/workspace-sync-coalescer.cjs +0 -58
  120. package/dist/mjs/atomic-rename.mjs +0 -261
  121. package/dist/mjs/browser-upload.mjs +0 -42
  122. package/dist/mjs/command-launcher.mjs +0 -393
  123. package/dist/mjs/control-command-policy.mjs +0 -146
  124. package/dist/mjs/git-blob-hash.mjs +0 -16
  125. package/dist/mjs/git-identity.mjs +0 -15
  126. package/dist/mjs/git-process-environment.mjs +0 -105
  127. package/dist/mjs/inline-output.mjs +0 -37
  128. package/dist/mjs/managed-paths.mjs +0 -174
  129. package/dist/mjs/plan-lint.mjs +0 -129
  130. package/dist/mjs/plan-parser.mjs +0 -94
  131. package/dist/mjs/plan-paths.mjs +0 -25
  132. package/dist/mjs/port-forward-client.mjs +0 -172
  133. package/dist/mjs/project-checkout-garbage.mjs +0 -178
  134. package/dist/mjs/project-mirror-fetch-policy.mjs +0 -56
  135. package/dist/mjs/project-mirror-refs-token.mjs +0 -53
  136. package/dist/mjs/project-workspace-state.mjs +0 -937
  137. package/dist/mjs/project-worktrees.mjs +0 -1657
  138. package/dist/mjs/pty-output-coalescer.mjs +0 -61
  139. package/dist/mjs/recovery-journal-protocol.mjs +0 -30
  140. package/dist/mjs/recovery-journal-runtime.mjs +0 -112
  141. package/dist/mjs/recovery-journal-thread.mjs +0 -8
  142. package/dist/mjs/recovery-journal.mjs +0 -694
  143. package/dist/mjs/recovery-output-window.mjs +0 -53
  144. package/dist/mjs/recovery-protocol.mjs +0 -6
  145. package/dist/mjs/recovery-store.mjs +0 -217
  146. package/dist/mjs/registry-auth.mjs +0 -269
  147. package/dist/mjs/repository-transition-policy.mjs +0 -22
  148. package/dist/mjs/runtime-version.mjs +0 -54
  149. package/dist/mjs/session-file-mutations.mjs +0 -37
  150. package/dist/mjs/supervisor.mjs +0 -114
  151. package/dist/mjs/three-way-merge.mjs +0 -318
  152. package/dist/mjs/working-tree-mirror.mjs +0 -1817
  153. package/dist/mjs/workspace-automatic-sync-policy.mjs +0 -48
  154. package/dist/mjs/workspace-branch-incarnation-policy.mjs +0 -13
  155. package/dist/mjs/workspace-command-sync-policy.mjs +0 -84
  156. package/dist/mjs/workspace-command-targets.mjs +0 -37
  157. package/dist/mjs/workspace-filesystem-executor-thread.mjs +0 -38
  158. package/dist/mjs/workspace-filesystem-executor.mjs +0 -287
  159. package/dist/mjs/workspace-filesystem-job-types.mjs +0 -116
  160. package/dist/mjs/workspace-filesystem-jobs.mjs +0 -55
  161. package/dist/mjs/workspace-git-sync.mjs +0 -3553
  162. package/dist/mjs/workspace-hydration-ledger.mjs +0 -58
  163. package/dist/mjs/workspace-hydration-merge.mjs +0 -414
  164. package/dist/mjs/workspace-hydration-recovery-state.mjs +0 -29
  165. package/dist/mjs/workspace-incident-state.mjs +0 -12
  166. package/dist/mjs/workspace-merge-projection.mjs +0 -594
  167. package/dist/mjs/workspace-mount-boundary.mjs +0 -37
  168. package/dist/mjs/workspace-mount-hold-fence.mjs +0 -95
  169. package/dist/mjs/workspace-mutation-gate.mjs +0 -103
  170. package/dist/mjs/workspace-path-move.mjs +0 -160
  171. package/dist/mjs/workspace-preserve-only-policy.mjs +0 -11
  172. package/dist/mjs/workspace-project-config-policy.mjs +0 -27
  173. package/dist/mjs/workspace-projection-ledger.mjs +0 -57
  174. package/dist/mjs/workspace-publication-evidence.mjs +0 -21
  175. package/dist/mjs/workspace-sync-coalescer.mjs +0 -34
  176. package/dist/types/atomic-rename.d.ts +0 -78
  177. package/dist/types/browser-upload.d.ts +0 -7
  178. package/dist/types/command-launcher.d.ts +0 -104
  179. package/dist/types/control-command-policy.d.ts +0 -53
  180. package/dist/types/git-blob-hash.d.ts +0 -10
  181. package/dist/types/git-identity.d.ts +0 -9
  182. package/dist/types/git-process-environment.d.ts +0 -9
  183. package/dist/types/inline-output.d.ts +0 -3
  184. package/dist/types/managed-paths.d.ts +0 -19
  185. package/dist/types/plan-lint.d.ts +0 -16
  186. package/dist/types/plan-parser.d.ts +0 -38
  187. package/dist/types/plan-paths.d.ts +0 -36
  188. package/dist/types/port-forward-client.d.ts +0 -8
  189. package/dist/types/project-checkout-garbage.d.ts +0 -86
  190. package/dist/types/project-mirror-fetch-policy.d.ts +0 -84
  191. package/dist/types/project-mirror-refs-token.d.ts +0 -53
  192. package/dist/types/project-workspace-state.d.ts +0 -188
  193. package/dist/types/project-worktrees.d.ts +0 -397
  194. package/dist/types/pty-output-coalescer.d.ts +0 -20
  195. package/dist/types/recovery-journal-protocol.d.ts +0 -35
  196. package/dist/types/recovery-journal-runtime.d.ts +0 -12
  197. package/dist/types/recovery-journal-stall-fixture.d.ts +0 -1
  198. package/dist/types/recovery-journal-thread.d.ts +0 -1
  199. package/dist/types/recovery-journal.d.ts +0 -248
  200. package/dist/types/recovery-output-window.d.ts +0 -19
  201. package/dist/types/recovery-protocol.d.ts +0 -59
  202. package/dist/types/recovery-store.d.ts +0 -52
  203. package/dist/types/registry-auth.d.ts +0 -47
  204. package/dist/types/repository-transition-policy.d.ts +0 -26
  205. package/dist/types/runtime-version.d.ts +0 -1
  206. package/dist/types/session-file-mutations.d.ts +0 -22
  207. package/dist/types/supervisor-daemonized-fixture.d.ts +0 -1
  208. package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +0 -1
  209. package/dist/types/supervisor-signal-fixture.d.ts +0 -1
  210. package/dist/types/supervisor.d.ts +0 -23
  211. package/dist/types/three-way-merge.d.ts +0 -77
  212. package/dist/types/working-tree-mirror.d.ts +0 -424
  213. package/dist/types/workspace-automatic-sync-policy.d.ts +0 -57
  214. package/dist/types/workspace-branch-incarnation-policy.d.ts +0 -14
  215. package/dist/types/workspace-command-sync-policy.d.ts +0 -57
  216. package/dist/types/workspace-command-targets.d.ts +0 -37
  217. package/dist/types/workspace-filesystem-executor-thread.d.ts +0 -1
  218. package/dist/types/workspace-filesystem-executor.d.ts +0 -123
  219. package/dist/types/workspace-filesystem-job-types.d.ts +0 -302
  220. package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
  221. package/dist/types/workspace-git-sync.d.ts +0 -514
  222. package/dist/types/workspace-hydration-ledger.d.ts +0 -76
  223. package/dist/types/workspace-hydration-merge.d.ts +0 -103
  224. package/dist/types/workspace-hydration-recovery-state.d.ts +0 -10
  225. package/dist/types/workspace-incident-state.d.ts +0 -19
  226. package/dist/types/workspace-merge-projection.d.ts +0 -69
  227. package/dist/types/workspace-mount-boundary.d.ts +0 -10
  228. package/dist/types/workspace-mount-hold-fence.d.ts +0 -42
  229. package/dist/types/workspace-mutation-gate.d.ts +0 -42
  230. package/dist/types/workspace-path-move.d.ts +0 -21
  231. package/dist/types/workspace-preserve-only-policy.d.ts +0 -15
  232. package/dist/types/workspace-project-config-policy.d.ts +0 -48
  233. package/dist/types/workspace-projection-ledger.d.ts +0 -59
  234. package/dist/types/workspace-publication-evidence.d.ts +0 -17
  235. package/dist/types/workspace-sync-coalescer.d.ts +0 -28
  236. /package/dist/types/{credential-authority-lock-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
@@ -1,3553 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
4
- import {
5
- materializeWorkspaceProjectionTree,
6
- workspaceMergeProjectionSupport
7
- } from "./workspace-merge-projection.mjs";
8
- import { workspaceFilesystemExecutor } from "./workspace-filesystem-executor.mjs";
9
- import { gitBlobHash, gitObjectHashAlgorithmFor } from "./git-blob-hash.mjs";
10
- import {
11
- applyWorkingTreeMirror,
12
- captureWorkingTreeMirrorPlan,
13
- fsyncWorkingTreePaths,
14
- inspectWorkingTree,
15
- inspectWorkingTreePath,
16
- projectedFileEntry,
17
- projectedFileEntryMatches,
18
- mirrorWorkingTree,
19
- normalizeWorkingTreeRelativePath,
20
- planPreparedWorkingTreeMirror,
21
- planWorkingTreeMirror,
22
- prepareWorkingTreeMirror,
23
- restoreWorkingTreeMirrorScope,
24
- rollbackWorkingTreeMirrorJournal,
25
- workingTreeDisplacedRetentionDirectory,
26
- WorkingTreeTargetChangedError
27
- } from "./working-tree-mirror.mjs";
28
- import {
29
- decideWorkspaceHydrationMerge,
30
- readHydrationBinaryPaths,
31
- describeHydrationSkipPaths,
32
- parseWorkspaceSubtreeChanges
33
- } from "./workspace-hydration-merge.mjs";
34
- import { WorkspaceHydrationLedger } from "./workspace-hydration-ledger.mjs";
35
- import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
36
- const WORKSPACE_GIT_BRANCH = "main";
37
- const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
38
- const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v1";
39
- const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
40
- const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
41
- const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
42
- const WORKSPACE_GIT_HYDRATION_RECOVERIES = "r5d/workspace-hydration-recoveries";
43
- const WORKSPACE_GIT_HYDRATION_PRESERVED_BASES = "r5d/workspace-hydration-preserved-bases";
44
- const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
45
- const WORKSPACE_MERGE_HYDRATION_SWITCH = "R5D_MERGE_HYDRATION";
46
- function workspaceMergeHydrationEnabled() {
47
- return process.env[WORKSPACE_MERGE_HYDRATION_SWITCH] !== "0";
48
- }
49
- const WORKSPACE_PROCESS_BUSY_FENCE_SWITCH = "R5D_PROCESS_BUSY_FENCE";
50
- function workspaceProcessBusyFenceEnabled() {
51
- return process.env[WORKSPACE_PROCESS_BUSY_FENCE_SWITCH] === "1";
52
- }
53
- function mountBusy(mount) {
54
- if (mount.busy !== void 0 && mount.busy()) return true;
55
- return workspaceProcessBusyFenceEnabled() && mount.processLive?.() === true;
56
- }
57
- const hydrationPreBlobLedgers = /* @__PURE__ */ new Map();
58
- function hydrationPreBlobLedger(workspacePath) {
59
- const key = path.resolve(workspacePath);
60
- let ledger = hydrationPreBlobLedgers.get(key);
61
- if (!ledger) {
62
- ledger = new WorkspaceHydrationLedger();
63
- hydrationPreBlobLedgers.set(key, ledger);
64
- }
65
- return ledger;
66
- }
67
- class WorkspaceHydrationRecoveryRequiredError extends Error {
68
- constructor(hydrationRecovery, options) {
69
- super(
70
- `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(", ")}` : ""}`,
71
- options
72
- );
73
- this.hydrationRecovery = hydrationRecovery;
74
- this.name = "WorkspaceHydrationRecoveryRequiredError";
75
- }
76
- hydrationRecovery;
77
- code = "hydration_recovery_required";
78
- }
79
- const HYDRATION_TRANSACTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
80
- function workspaceGitMountData(mount) {
81
- return {
82
- id: mount.id,
83
- hydrationIncarnationKey: mount.hydrationIncarnationKey,
84
- sourcePath: mount.sourcePath,
85
- durabilityRootPath: mount.durabilityRootPath,
86
- workspaceRelativePath: mount.workspaceRelativePath,
87
- sourceMode: mount.sourceMode,
88
- hydrateDeletionMode: mount.hydrateDeletionMode,
89
- ...mount.preserveLocalOnInitialOuterAbsence === void 0 ? {} : { preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence },
90
- ...mount.preserveLocalOnHydrationBasisChange === void 0 ? {} : { preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange },
91
- ...mount.initialPublicationSourceRelativePath === void 0 ? {} : { initialPublicationSourceRelativePath: mount.initialPublicationSourceRelativePath },
92
- ...mount.deleteWhenSourceMissing === void 0 ? {} : { deleteWhenSourceMissing: mount.deleteWhenSourceMissing },
93
- ...mount.lastProjectedMutationToken === void 0 ? {} : { lastProjectedMutationToken: mount.lastProjectedMutationToken }
94
- };
95
- }
96
- function workspaceGitMountsForRemediation(mounts) {
97
- return mounts.map((mount) => ({ ...mount, busy: mount.busyForRecovery ?? mount.busy }));
98
- }
99
- class WorkspaceRemediationAncestryError extends Error {
100
- constructor(message) {
101
- super(message);
102
- this.name = "WorkspaceRemediationAncestryError";
103
- }
104
- }
105
- const NON_RECURSIVE_GIT_CONFIG = [
106
- "-c",
107
- "submodule.recurse=false",
108
- "-c",
109
- "fetch.recurseSubmodules=false",
110
- "-c",
111
- "push.recurseSubmodules=false"
112
- ];
113
- function gitCommandArgs(args) {
114
- return ["git", ...NON_RECURSIVE_GIT_CONFIG, ...args];
115
- }
116
- function workspaceCloneCommandArgs(args, remoteUrl, credentialHelper, credentialUsername) {
117
- return gitCommandArgs([...gitTransportSecurityArgs(remoteUrl, credentialHelper, credentialUsername), ...args]);
118
- }
119
- function gitResult(cwd, args) {
120
- const result = Bun.spawnSync(gitCommandArgs(args), {
121
- cwd,
122
- stdout: "pipe",
123
- stderr: "pipe",
124
- env: workerGitProcessEnvironment()
125
- });
126
- return {
127
- exitCode: result.exitCode,
128
- stdout: result.stdout.toString().trim(),
129
- stderr: result.stderr.toString().trim()
130
- };
131
- }
132
- function git(cwd, args, action) {
133
- const result = gitResult(cwd, args);
134
- if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
135
- return result.stdout;
136
- }
137
- function tryGit(cwd, args) {
138
- return gitResult(cwd, args).exitCode === 0;
139
- }
140
- function revParse(workspacePath, revision) {
141
- const result = gitResult(workspacePath, ["rev-parse", "--verify", revision]);
142
- return result.exitCode === 0 ? result.stdout : null;
143
- }
144
- async function gitResultAsync(cwd, args) {
145
- const subprocess = Bun.spawn(gitCommandArgs(args), {
146
- cwd,
147
- stdin: "ignore",
148
- stdout: "pipe",
149
- stderr: "pipe",
150
- env: workerGitProcessEnvironment()
151
- });
152
- const [stdout, stderr, exitCode] = await Promise.all([
153
- new Response(subprocess.stdout).text(),
154
- new Response(subprocess.stderr).text(),
155
- subprocess.exited
156
- ]);
157
- return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
158
- }
159
- async function gitAsync(cwd, args, action) {
160
- const result = await gitResultAsync(cwd, args);
161
- if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
162
- return result.stdout;
163
- }
164
- async function gitOutputAsync(cwd, args, action, input) {
165
- const subprocess = Bun.spawn(gitCommandArgs(args), {
166
- cwd,
167
- stdin: input ?? "ignore",
168
- stdout: "pipe",
169
- stderr: "pipe",
170
- env: workerGitProcessEnvironment()
171
- });
172
- const [stdout, stderr, exitCode] = await Promise.all([
173
- new Response(subprocess.stdout).arrayBuffer(),
174
- new Response(subprocess.stderr).text(),
175
- subprocess.exited
176
- ]);
177
- if (exitCode !== 0) throw new Error(`${action}: ${stderr.trim() || `git exited ${exitCode}`}`);
178
- return Buffer.from(stdout);
179
- }
180
- async function tryGitAsync(cwd, args) {
181
- return (await gitResultAsync(cwd, args)).exitCode === 0;
182
- }
183
- async function revParseAsync(workspacePath, revision) {
184
- const result = await gitResultAsync(workspacePath, ["rev-parse", "--verify", revision]);
185
- return result.exitCode === 0 ? result.stdout : null;
186
- }
187
- function workspaceCommitsExist(workspacePath, heads) {
188
- const uniqueHeads = [...new Set(heads)];
189
- if (uniqueHeads.length === 0) return true;
190
- const batch = Bun.spawnSync(gitCommandArgs(["cat-file", "--batch-check"]), {
191
- cwd: workspacePath,
192
- stdin: Buffer.from(uniqueHeads.map((head) => `${head}^{commit}
193
- `).join("")),
194
- stdout: "pipe",
195
- stderr: "pipe",
196
- env: workerGitProcessEnvironment()
197
- });
198
- if (batch.exitCode !== 0) return false;
199
- const lines = batch.stdout.toString().split("\n").filter(Boolean);
200
- return lines.length === uniqueHeads.length && lines.every((line) => !line.endsWith(" missing"));
201
- }
202
- async function workspaceCommitsExistAsync(workspacePath, heads) {
203
- const uniqueHeads = [...new Set(heads)];
204
- if (uniqueHeads.length === 0) return true;
205
- const subprocess = Bun.spawn(gitCommandArgs(["cat-file", "--batch-check"]), {
206
- cwd: workspacePath,
207
- stdin: Buffer.from(uniqueHeads.map((head) => `${head}^{commit}
208
- `).join("")),
209
- stdout: "pipe",
210
- stderr: "pipe",
211
- env: workerGitProcessEnvironment()
212
- });
213
- const [stdout, , exitCode] = await Promise.all([
214
- new Response(subprocess.stdout).text(),
215
- new Response(subprocess.stderr).text(),
216
- subprocess.exited
217
- ]);
218
- if (exitCode !== 0) return false;
219
- const lines = stdout.split("\n").filter(Boolean);
220
- return lines.length === uniqueHeads.length && lines.every((line) => !line.endsWith(" missing"));
221
- }
222
- function requiredWorkspaceAncestorHeads(value) {
223
- if (value === void 0) return [];
224
- if (!Array.isArray(value) || value.length === 0 || value.some((head) => typeof head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head)) || new Set(value).size !== value.length) {
225
- throw new WorkspaceRemediationAncestryError("Workspace remediation ancestor requirements are invalid");
226
- }
227
- return [...value];
228
- }
229
- function assertWorkspaceRemediationAncestry(input) {
230
- if (input.requiredAncestorHeads.length === 0) return;
231
- if (!input.currentHead) throw new WorkspaceRemediationAncestryError("Workspace remediation has no resolved HEAD to verify");
232
- for (const requiredHead of input.requiredAncestorHeads) {
233
- if (!tryGit(input.workspacePath, ["cat-file", "-e", `${requiredHead}^{commit}`]) || !tryGit(input.workspacePath, ["merge-base", "--is-ancestor", requiredHead, input.currentHead])) {
234
- throw new WorkspaceRemediationAncestryError(
235
- `Workspace remediation HEAD does not descend from required conflict commit ${requiredHead}`
236
- );
237
- }
238
- }
239
- if (!input.remoteHead || !tryGit(input.workspacePath, ["cat-file", "-e", `${input.remoteHead}^{commit}`]) || !tryGit(input.workspacePath, ["merge-base", "--is-ancestor", input.remoteHead, input.currentHead])) {
240
- throw new WorkspaceRemediationAncestryError(
241
- "Fetched workspace head is not an ancestor of the resolved remediation HEAD; merge it explicitly before synchronizing"
242
- );
243
- }
244
- }
245
- async function updateIntegratedWorkspaceHead(workspacePath, head) {
246
- await gitAsync(workspacePath, ["update-ref", WORKSPACE_GIT_INTEGRATED_REF, head], "record integrated workspace head");
247
- }
248
- function fsyncDirectory(directoryPath) {
249
- const descriptor = fs.openSync(directoryPath, fs.constants.O_RDONLY);
250
- try {
251
- fs.fsyncSync(descriptor);
252
- } finally {
253
- fs.closeSync(descriptor);
254
- }
255
- }
256
- function lstatIfExists(targetPath) {
257
- try {
258
- return fs.lstatSync(targetPath);
259
- } catch (error) {
260
- if (error.code === "ENOENT") return null;
261
- throw error;
262
- }
263
- }
264
- function hydratedWorkspaceReceiptPath(workspacePath) {
265
- return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATED_RECEIPT.split("/"));
266
- }
267
- function workspaceCheckoutDurabilityPath(workspacePath) {
268
- return path.join(workspacePath, ".git", ...WORKSPACE_GIT_CHECKOUT_DURABILITY.split("/"));
269
- }
270
- function normalizedWorkspaceMountPath(workspaceRelativePath) {
271
- return workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
272
- }
273
- function workspaceHydrationMountBasis(mount) {
274
- return {
275
- id: mount.id,
276
- incarnationKey: mount.hydrationIncarnationKey,
277
- sourcePath: path.resolve(mount.sourcePath),
278
- durabilityRootPath: path.resolve(mount.durabilityRootPath),
279
- workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
280
- sourceMode: mount.sourceMode,
281
- hydrateDeletionMode: mount.hydrateDeletionMode,
282
- preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence === true,
283
- preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange === true,
284
- deleteWhenSourceMissing: mount.deleteWhenSourceMissing === true
285
- };
286
- }
287
- function compareWorkspaceHydrationMountBasis(left, right) {
288
- return left.id.localeCompare(right.id);
289
- }
290
- function workspaceHydrationReceipt(head, mounts) {
291
- const bases = mounts.map((mount) => ({ ...workspaceHydrationMountBasis(mount), head })).sort(compareWorkspaceHydrationMountBasis);
292
- const ids = /* @__PURE__ */ new Set();
293
- for (const basis of bases) {
294
- if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
295
- ids.add(basis.id);
296
- }
297
- return { version: 3, mounts: bases };
298
- }
299
- function receiptWithMountsAtHead(previous, head, mounts, allConfiguredMounts) {
300
- const advanceIds = new Set(mounts.map(({ id }) => id));
301
- const previousById = new Map(previous?.mounts.map((entry) => [entry.id, entry]) ?? []);
302
- const entries = allConfiguredMounts.flatMap((mount) => {
303
- const basis = workspaceHydrationMountBasis(mount);
304
- if (advanceIds.has(mount.id)) return [{ ...basis, head }];
305
- const carried = previousById.get(mount.id);
306
- if (!carried) return [];
307
- return [carried];
308
- });
309
- entries.sort(compareWorkspaceHydrationMountBasis);
310
- const ids = /* @__PURE__ */ new Set();
311
- for (const entry of entries) {
312
- if (ids.has(entry.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${entry.id}`);
313
- ids.add(entry.id);
314
- }
315
- return { version: 3, mounts: entries };
316
- }
317
- function workspaceHydrationReceiptsEqual(left, right) {
318
- if (JSON.stringify(left) !== JSON.stringify(right)) return false;
319
- const effectiveLegacyHead = (receipt) => receipt?.mounts.length === 0 ? receipt.legacyHead ?? null : null;
320
- return effectiveLegacyHead(left) === effectiveLegacyHead(right);
321
- }
322
- function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
323
- if (!receipt) return false;
324
- const expected = workspaceHydrationMountBasis(mount);
325
- const actual = receipt.mounts.find(({ id }) => id === expected.id);
326
- if (!actual || actual.head !== head) return false;
327
- const { head: _head, ...actualBasis } = actual;
328
- return JSON.stringify(actualBasis) === JSON.stringify(expected);
329
- }
330
- function workspaceHydrationReceiptMountBasisHead(receipt, mount) {
331
- if (!receipt) return null;
332
- const expected = workspaceHydrationMountBasis(mount);
333
- const actual = receipt.mounts.find(({ id }) => id === expected.id);
334
- if (!actual) return null;
335
- const { head, ...actualBasis } = actual;
336
- return JSON.stringify(actualBasis) === JSON.stringify(expected) ? head : null;
337
- }
338
- function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
339
- return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
340
- }
341
- function configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, deferMountIds) {
342
- const resolvedWorkspacePath = path.resolve(workspacePath);
343
- return mounts.filter((mount) => {
344
- if (deferMountIds?.has(mount.id)) return false;
345
- if (mount.deleteWhenSourceMissing) return !fs.existsSync(mount.sourcePath);
346
- const outerPath = path.join(resolvedWorkspacePath, ...normalizedWorkspaceMountPath(mount.workspaceRelativePath).split("/"));
347
- return fs.existsSync(mount.sourcePath) || fs.existsSync(outerPath) || mount.sourceMode === "all";
348
- });
349
- }
350
- function parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content) {
351
- const legacyHead = content.endsWith("\n") ? content.slice(0, -1) : "";
352
- if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(legacyHead) && content === `${legacyHead}
353
- `) {
354
- const receipt2 = { version: 3, mounts: [] };
355
- Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
356
- return { receipt: receipt2, headsToVerify: [legacyHead] };
357
- }
358
- let parsed;
359
- try {
360
- parsed = JSON.parse(content);
361
- } catch {
362
- throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
363
- }
364
- if (!parsed || typeof parsed !== "object") {
365
- throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
366
- }
367
- const candidate = parsed;
368
- const liftedV2Head = candidate.version === 2 && typeof candidate.head === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head) ? candidate.head : null;
369
- if (candidate.version !== 3 && liftedV2Head === null) {
370
- throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
371
- }
372
- if (!Array.isArray(candidate.mounts)) {
373
- throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
374
- }
375
- const seenIds = /* @__PURE__ */ new Set();
376
- const mounts = candidate.mounts.map((rawMount) => {
377
- if (!rawMount || typeof rawMount !== "object") {
378
- throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
379
- }
380
- const mount = rawMount;
381
- const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
382
- const durabilityRelative = typeof mount.sourcePath === "string" && typeof mount.durabilityRootPath === "string" ? path.relative(mount.durabilityRootPath, mount.sourcePath) : "..";
383
- if (typeof mount.id !== "string" || !mount.id || seenIds.has(mount.id) || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || path.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || path.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.isAbsolute(durabilityRelative) || typeof mount.workspaceRelativePath !== "string" || mount.workspaceRelativePath !== normalizedRelativePath || !normalizedRelativePath || normalizedRelativePath === "." || normalizedRelativePath.split("/").some((segment) => segment === ".." || segment === ".git") || mount.sourceMode !== "all" && mount.sourceMode !== "git" || mount.hydrateDeletionMode !== "all" && mount.hydrateDeletionMode !== "git" || typeof mount.preserveLocalOnInitialOuterAbsence !== "boolean" || typeof mount.preserveLocalOnHydrationBasisChange !== "boolean" || typeof mount.deleteWhenSourceMissing !== "boolean" || candidate.version === 3 && (typeof mount.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head))) {
384
- throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
385
- }
386
- seenIds.add(mount.id);
387
- return {
388
- id: mount.id,
389
- incarnationKey: mount.incarnationKey,
390
- sourcePath: mount.sourcePath,
391
- durabilityRootPath: mount.durabilityRootPath,
392
- workspaceRelativePath: mount.workspaceRelativePath,
393
- sourceMode: mount.sourceMode,
394
- hydrateDeletionMode: mount.hydrateDeletionMode,
395
- preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence,
396
- preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange,
397
- deleteWhenSourceMissing: mount.deleteWhenSourceMissing,
398
- head: candidate.version === 3 ? mount.head : liftedV2Head
399
- };
400
- });
401
- const sortedMounts = mounts.sort(compareWorkspaceHydrationMountBasis);
402
- if (liftedV2Head) {
403
- const legacyReceipt = {
404
- version: 2,
405
- head: liftedV2Head,
406
- mounts: sortedMounts.map(({ head: _head, ...basis }) => basis)
407
- };
408
- if (content !== `${JSON.stringify(legacyReceipt)}
409
- `) {
410
- throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
411
- }
412
- }
413
- const receipt = {
414
- version: 3,
415
- mounts: sortedMounts
416
- };
417
- if (liftedV2Head) Object.defineProperty(receipt, "legacyHead", { value: liftedV2Head, enumerable: false });
418
- if (candidate.version === 3 && content !== `${JSON.stringify(receipt)}
419
- `) {
420
- throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
421
- }
422
- const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
423
- if (liftedV2Head) receiptHeads.add(liftedV2Head);
424
- return { receipt, headsToVerify: [...receiptHeads] };
425
- }
426
- function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content) {
427
- const { receipt, headsToVerify } = parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content);
428
- if (!workspaceCommitsExist(workspacePath, headsToVerify)) {
429
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
430
- }
431
- return receipt;
432
- }
433
- function readHydratedWorkspaceReceiptUnverified(workspacePath) {
434
- const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
435
- const status = lstatIfExists(receiptPath);
436
- if (!status) return null;
437
- if (!status.isFile() || status.isSymbolicLink()) {
438
- throw new Error(`Workspace hydration receipt is not a regular file: ${receiptPath}`);
439
- }
440
- const content = fs.readFileSync(receiptPath, "utf8");
441
- return { ...parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content), receiptPath };
442
- }
443
- function readHydratedWorkspaceReceipt(workspacePath) {
444
- const unverified = readHydratedWorkspaceReceiptUnverified(workspacePath);
445
- if (!unverified) return null;
446
- if (!workspaceCommitsExist(workspacePath, unverified.headsToVerify)) {
447
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${unverified.receiptPath}`);
448
- }
449
- return unverified.receipt;
450
- }
451
- async function readHydratedWorkspaceReceiptUnverifiedAsync(workspacePath) {
452
- const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
453
- let status;
454
- try {
455
- status = await fs.promises.lstat(receiptPath);
456
- } catch (error) {
457
- if (error.code === "ENOENT") return null;
458
- throw error;
459
- }
460
- if (!status.isFile() || status.isSymbolicLink()) {
461
- throw new Error(`Workspace hydration receipt is not a regular file: ${receiptPath}`);
462
- }
463
- const content = await fs.promises.readFile(receiptPath, "utf8");
464
- return { ...parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content), receiptPath };
465
- }
466
- async function readHydratedWorkspaceReceiptAsync(workspacePath) {
467
- const unverified = await readHydratedWorkspaceReceiptUnverifiedAsync(workspacePath);
468
- if (!unverified) return null;
469
- if (!await workspaceCommitsExistAsync(workspacePath, unverified.headsToVerify)) {
470
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${unverified.receiptPath}`);
471
- }
472
- return unverified.receipt;
473
- }
474
- async function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
475
- const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
476
- try {
477
- const status = await fs.promises.lstat(receiptPath);
478
- if (!status.isFile() || status.isSymbolicLink()) return false;
479
- return JSON.parse(await fs.promises.readFile(receiptPath, "utf8")).version === 3;
480
- } catch {
481
- return false;
482
- }
483
- }
484
- function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
485
- return workspaceGitHydrationHolds(workspacePath, mounts, false);
486
- }
487
- function workspaceGitHydrationIsRecoverable(workspacePath, mounts) {
488
- return workspaceGitHydrationHolds(workspacePath, mounts, true);
489
- }
490
- function workspaceGitHydrationHolds(workspacePath, mounts, tolerateIdleRecordedBasis) {
491
- const resolvedWorkspacePath = path.resolve(workspacePath);
492
- if (!fs.existsSync(path.join(resolvedWorkspacePath, ".git"))) return true;
493
- if (mounts) validateMounts(resolvedWorkspacePath, mounts);
494
- const head = revParse(resolvedWorkspacePath, "HEAD");
495
- if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
496
- if (tolerateIdleRecordedBasis && lstatIfExists(hydrationTransactionPath(resolvedWorkspacePath)) !== null) return false;
497
- const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
498
- if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
499
- if (head === null) return true;
500
- if (!receipt) return false;
501
- return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
502
- (mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean((tolerateIdleRecordedBasis || mountBusy(mount)) && workspaceHydrationReceiptMountBasisHead(receipt, mount))
503
- ) : Boolean(receipt && receipt.mounts.every((mount) => tolerateIdleRecordedBasis || mount.head === head));
504
- }
505
- const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
506
- const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
507
- function workspaceBasisRefName(mountId) {
508
- const sanitized = Buffer.from(mountId, "utf8").toString("base64url");
509
- if (!sanitized) throw new Error(`Cannot derive a workspace basis ref for mount id: ${mountId}`);
510
- return `${WORKSPACE_GIT_BASIS_REF_PREFIX}${sanitized}`;
511
- }
512
- function listWorkspaceRefs(workspacePath, prefix) {
513
- return git(workspacePath, ["for-each-ref", "--format=%(refname)", prefix], `list ${prefix} refs`).split("\n").filter(Boolean).sort();
514
- }
515
- function desiredWorkspaceBasisRefs(receipt) {
516
- const desired = /* @__PURE__ */ new Map();
517
- for (const mount of receipt.mounts) {
518
- const ref = workspaceBasisRefName(mount.id);
519
- if (desired.has(ref)) throw new Error(`Workspace hydration mount ids collide at basis ref ${ref}`);
520
- desired.set(ref, mount.head);
521
- }
522
- return desired;
523
- }
524
- function workspaceBasisRefsMatch(workspacePath, receipt) {
525
- const desired = desiredWorkspaceBasisRefs(receipt);
526
- const actual = git(
527
- workspacePath,
528
- ["for-each-ref", "--format=%(refname) %(objectname)", WORKSPACE_GIT_BASIS_REF_PREFIX],
529
- `list ${WORKSPACE_GIT_BASIS_REF_PREFIX} refs`
530
- ).split("\n").filter(Boolean);
531
- if (actual.length !== desired.size) return false;
532
- return actual.every((line) => {
533
- const separator = line.lastIndexOf(" ");
534
- if (separator <= 0) return false;
535
- return desired.get(line.slice(0, separator)) === line.slice(separator + 1);
536
- });
537
- }
538
- function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
539
- return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
540
- }
541
- async function workspaceBasisRefsAreCurrentAsync(workspacePath, receipt) {
542
- const desired = desiredWorkspaceBasisRefs(receipt);
543
- const listing = await gitAsync(
544
- workspacePath,
545
- ["for-each-ref", "--format=%(refname) %(objectname)", WORKSPACE_GIT_BASIS_REF_PREFIX, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX],
546
- "list workspace hydration basis refs"
547
- );
548
- const stable = /* @__PURE__ */ new Map();
549
- for (const line of listing.split("\n").filter(Boolean)) {
550
- const separator = line.lastIndexOf(" ");
551
- if (separator <= 0) return false;
552
- const ref = line.slice(0, separator);
553
- if (ref.startsWith(WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) return false;
554
- stable.set(ref, line.slice(separator + 1));
555
- }
556
- if (stable.size !== desired.size) return false;
557
- return [...stable].every(([ref, head]) => desired.get(ref) === head);
558
- }
559
- function repairWorkspaceBasisRefs(workspacePath, receipt) {
560
- const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
561
- if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
562
- }
563
- function runWorkspaceRefUpdateBatch(workspacePath, instructions, action) {
564
- if (instructions.length === 0) return;
565
- const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
566
- cwd: workspacePath,
567
- stdin: Buffer.from([...instructions, ""].join("\n")),
568
- stdout: "pipe",
569
- stderr: "pipe",
570
- env: workerGitProcessEnvironment()
571
- });
572
- if (updated.exitCode !== 0) {
573
- throw new Error(`${action}: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
574
- }
575
- }
576
- function workspaceBasisRefTransaction(receipt, stableRefs) {
577
- const desired = desiredWorkspaceBasisRefs(receipt);
578
- const transaction = ["start"];
579
- for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
580
- for (const ref of stableRefs) {
581
- if (!desired.has(ref)) transaction.push(`delete ${ref}`);
582
- }
583
- transaction.push("prepare", "commit");
584
- return transaction;
585
- }
586
- function updateWorkspaceBasisRefs(workspacePath, receipt) {
587
- runWorkspaceRefUpdateBatch(
588
- workspacePath,
589
- workspaceBasisRefTransaction(receipt, listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)),
590
- "Synchronize workspace hydration basis refs"
591
- );
592
- runWorkspaceRefUpdateBatch(
593
- workspacePath,
594
- listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).map((ref) => `delete ${ref}`),
595
- "remove staged workspace hydration basis refs"
596
- );
597
- }
598
- function assertCanonicalHydrationReceiptHeads(receipt) {
599
- for (const mount of receipt.mounts) {
600
- if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
601
- throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
602
- }
603
- }
604
- }
605
- function throwMissingHydrationReceiptCommit(workspacePath, receipt) {
606
- const missing = receipt.mounts.find(({ head }) => !tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`]));
607
- throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${missing?.head ?? "unknown"}`);
608
- }
609
- function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
610
- assertCanonicalHydrationReceiptHeads(receipt);
611
- if (!workspaceCommitsExist(
612
- workspacePath,
613
- receipt.mounts.map(({ head }) => head)
614
- )) {
615
- throwMissingHydrationReceiptCommit(workspacePath, receipt);
616
- }
617
- readHydratedWorkspaceReceipt(workspacePath);
618
- writeHydratedWorkspaceReceiptCore(workspacePath, receipt);
619
- }
620
- function prepareHydratedWorkspaceReceiptWrite(workspacePath) {
621
- const gitDirectory = path.join(workspacePath, ".git");
622
- const gitDirectoryStatus = fs.lstatSync(gitDirectory);
623
- if (!gitDirectoryStatus.isDirectory() || gitDirectoryStatus.isSymbolicLink()) {
624
- throw new Error(`Workspace Git directory is not a regular directory: ${gitDirectory}`);
625
- }
626
- const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
627
- const receiptDirectory = path.dirname(receiptPath);
628
- const existingReceiptDirectory = lstatIfExists(receiptDirectory);
629
- if (!existingReceiptDirectory) {
630
- fs.mkdirSync(receiptDirectory, { recursive: false, mode: 448 });
631
- fsyncDirectory(gitDirectory);
632
- }
633
- const receiptDirectoryStatus = fs.lstatSync(receiptDirectory);
634
- if (!receiptDirectoryStatus.isDirectory() || receiptDirectoryStatus.isSymbolicLink()) {
635
- throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
636
- }
637
- return { receiptPath, receiptDirectory };
638
- }
639
- function stagedWorkspaceBasisRefInstructions(receipt, stagingPrefix) {
640
- desiredWorkspaceBasisRefs(receipt);
641
- return receipt.mounts.map(
642
- (mount) => `update ${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)} ${mount.head}`
643
- );
644
- }
645
- function publishHydratedWorkspaceReceiptFile(workspacePath, receipt, paths) {
646
- const temporaryPath = path.join(paths.receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
647
- let descriptor;
648
- try {
649
- descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
650
- fs.writeFileSync(descriptor, `${JSON.stringify(receipt)}
651
- `, "utf8");
652
- fs.fsyncSync(descriptor);
653
- fs.closeSync(descriptor);
654
- descriptor = void 0;
655
- fs.renameSync(temporaryPath, paths.receiptPath);
656
- fsyncDirectory(paths.receiptDirectory);
657
- } catch (error) {
658
- if (descriptor !== void 0) fs.closeSync(descriptor);
659
- fs.rmSync(temporaryPath, { force: true });
660
- throw error;
661
- }
662
- }
663
- function writeHydratedWorkspaceReceiptCore(workspacePath, receipt) {
664
- const paths = prepareHydratedWorkspaceReceiptWrite(workspacePath);
665
- const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
666
- runWorkspaceRefUpdateBatch(
667
- workspacePath,
668
- stagedWorkspaceBasisRefInstructions(receipt, stagingPrefix),
669
- "stage workspace hydration basis refs"
670
- );
671
- publishHydratedWorkspaceReceiptFile(workspacePath, receipt, paths);
672
- updateWorkspaceBasisRefs(workspacePath, receipt);
673
- }
674
- function hydrationTransactionPath(workspacePath) {
675
- return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_TRANSACTION.split("/"));
676
- }
677
- function workspacePrivateStateDirectory(workspacePath) {
678
- const gitDirectory = path.join(workspacePath, ".git");
679
- const gitDirectoryStatus = fs.lstatSync(gitDirectory);
680
- if (!gitDirectoryStatus.isDirectory() || gitDirectoryStatus.isSymbolicLink()) {
681
- throw new Error(`Workspace Git directory is not a regular directory: ${gitDirectory}`);
682
- }
683
- const stateDirectory = path.dirname(hydrationTransactionPath(workspacePath));
684
- const existing = lstatIfExists(stateDirectory);
685
- if (!existing) {
686
- fs.mkdirSync(stateDirectory, { recursive: false, mode: 448 });
687
- fsyncDirectory(gitDirectory);
688
- } else if (!existing.isDirectory() || existing.isSymbolicLink()) {
689
- throw new Error(`Workspace Git private state is not a regular directory: ${stateDirectory}`);
690
- }
691
- return stateDirectory;
692
- }
693
- function writePrivateFileDurably(filePath, content) {
694
- const descriptor = fs.openSync(
695
- filePath,
696
- fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
697
- 384
698
- );
699
- try {
700
- fs.writeFileSync(descriptor, content, "utf8");
701
- fs.fsyncSync(descriptor);
702
- } finally {
703
- fs.closeSync(descriptor);
704
- }
705
- }
706
- function fsyncTree(rootPath) {
707
- const syncEntry = (entryPath) => {
708
- const status = fs.lstatSync(entryPath);
709
- if (status.isSymbolicLink()) return;
710
- if (status.isDirectory()) {
711
- for (const name of fs.readdirSync(entryPath)) syncEntry(path.join(entryPath, name));
712
- fsyncDirectory(entryPath);
713
- return;
714
- }
715
- if (!status.isFile()) return;
716
- const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
717
- try {
718
- fs.fsyncSync(descriptor);
719
- } finally {
720
- fs.closeSync(descriptor);
721
- }
722
- };
723
- syncEntry(rootPath);
724
- }
725
- function parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, content) {
726
- let parsed;
727
- try {
728
- parsed = JSON.parse(content);
729
- } catch {
730
- throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
731
- }
732
- if (!parsed || typeof parsed !== "object") {
733
- throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
734
- }
735
- const candidate = parsed;
736
- if (candidate.version !== 1 || candidate.state !== "durable" && candidate.state !== "transition" || candidate.head !== null && (typeof candidate.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head))) {
737
- throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
738
- }
739
- const record = {
740
- version: 1,
741
- state: candidate.state,
742
- head: candidate.head
743
- };
744
- if (content !== `${JSON.stringify(record)}
745
- `) {
746
- throw new Error(`Workspace checkout durability record is noncanonical: ${recordPath}`);
747
- }
748
- return record;
749
- }
750
- function readWorkspaceCheckoutDurabilityRecordUnverified(workspacePath) {
751
- const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
752
- const status = lstatIfExists(recordPath);
753
- if (!status) return null;
754
- if (!status.isFile() || status.isSymbolicLink()) {
755
- throw new Error(`Workspace checkout durability record is not a regular file: ${recordPath}`);
756
- }
757
- return { record: parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, fs.readFileSync(recordPath, "utf8")), recordPath };
758
- }
759
- function readWorkspaceCheckoutDurabilityRecord(workspacePath) {
760
- const unverified = readWorkspaceCheckoutDurabilityRecordUnverified(workspacePath);
761
- if (!unverified) return null;
762
- if (unverified.record.head && !tryGit(workspacePath, ["cat-file", "-e", `${unverified.record.head}^{commit}`])) {
763
- throw new Error(`Workspace checkout durability record names a missing commit: ${unverified.recordPath}`);
764
- }
765
- return unverified.record;
766
- }
767
- async function readWorkspaceCheckoutDurabilityRecordAsync(workspacePath) {
768
- const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
769
- let status;
770
- try {
771
- status = await fs.promises.lstat(recordPath);
772
- } catch (error) {
773
- if (error.code === "ENOENT") return null;
774
- throw error;
775
- }
776
- if (!status.isFile() || status.isSymbolicLink()) {
777
- throw new Error(`Workspace checkout durability record is not a regular file: ${recordPath}`);
778
- }
779
- const unverified = {
780
- record: parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, await fs.promises.readFile(recordPath, "utf8")),
781
- recordPath
782
- };
783
- if (unverified.record.head && !await tryGitAsync(workspacePath, ["cat-file", "-e", `${unverified.record.head}^{commit}`])) {
784
- throw new Error(`Workspace checkout durability record names a missing commit: ${unverified.recordPath}`);
785
- }
786
- return unverified.record;
787
- }
788
- function writeWorkspaceCheckoutDurabilityRecordFile(workspacePath, record) {
789
- const stateDirectory = workspacePrivateStateDirectory(workspacePath);
790
- const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
791
- const temporaryPath = path.join(stateDirectory, `.workspace-checkout-durability.${process.pid}.${crypto.randomUUID()}.tmp`);
792
- try {
793
- writePrivateFileDurably(temporaryPath, `${JSON.stringify(record)}
794
- `);
795
- fs.renameSync(temporaryPath, recordPath);
796
- fsyncDirectory(stateDirectory);
797
- } catch (error) {
798
- fs.rmSync(temporaryPath, { force: true });
799
- throw error;
800
- }
801
- }
802
- function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
803
- if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
804
- throw new Error(`Cannot record workspace checkout durability for a missing commit: ${record.head}`);
805
- }
806
- readWorkspaceCheckoutDurabilityRecord(workspacePath);
807
- writeWorkspaceCheckoutDurabilityRecordFile(workspacePath, record);
808
- }
809
- function fsyncWorkspaceCheckoutTree(workspacePath) {
810
- const status = fs.lstatSync(workspacePath);
811
- if (!status.isDirectory() || status.isSymbolicLink()) {
812
- throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
813
- }
814
- for (const name of fs.readdirSync(workspacePath)) {
815
- if (name === ".git") continue;
816
- fsyncTree(path.join(workspacePath, name));
817
- }
818
- fsyncDirectory(workspacePath);
819
- }
820
- function workspaceRebaseInProgress(workspacePath) {
821
- const gitDirectory = path.join(workspacePath, ".git");
822
- return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
823
- }
824
- function workspaceResolutionInProgress(workspacePath) {
825
- return workspaceRebaseInProgress(workspacePath) || fs.existsSync(path.join(workspacePath, ".git", "MERGE_HEAD"));
826
- }
827
- async function workspaceHasUnmergedEntries(workspacePath) {
828
- const result = await gitResultAsync(workspacePath, ["ls-files", "-u", "-z"]);
829
- if (result.exitCode !== 0) {
830
- throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
831
- }
832
- return result.stdout.length > 0;
833
- }
834
- function clearWorkspaceCheckoutTree(workspacePath) {
835
- for (const name of fs.readdirSync(workspacePath)) {
836
- if (name === ".git") continue;
837
- fs.rmSync(path.join(workspacePath, name), { recursive: true, force: true });
838
- }
839
- fsyncDirectory(workspacePath);
840
- }
841
- function recoverWorkspaceCheckoutDurability(workspacePath, preserveResolutionInProgress = false) {
842
- workspacePath = path.resolve(workspacePath);
843
- const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
844
- const observedHead = revParse(workspacePath, "HEAD");
845
- if (record?.state === "durable" && record.head === observedHead) return;
846
- if (preserveResolutionInProgress && workspaceResolutionInProgress(workspacePath)) {
847
- throw new Error("Workspace resolution is in progress; finish or abort it before synchronizing");
848
- }
849
- let targetHead = record?.state === "transition" ? record.head : observedHead;
850
- if (record?.state === "transition" && observedHead && observedHead !== record.head) {
851
- const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
852
- if (hydrationReceipt?.legacyHead === observedHead || hydrationReceipt?.mounts.some((mount) => mount.head === observedHead)) {
853
- targetHead = observedHead;
854
- }
855
- }
856
- if (workspaceRebaseInProgress(workspacePath)) {
857
- const abort = gitResult(workspacePath, ["rebase", "--abort"]);
858
- if (abort.exitCode !== 0) {
859
- throw new Error(`Abort interrupted workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
860
- }
861
- if (record?.state !== "transition") targetHead = revParse(workspacePath, "HEAD");
862
- }
863
- clearWorkspaceCheckoutTree(workspacePath);
864
- if (targetHead) {
865
- git(workspacePath, ["reset", "--hard", targetHead], "reconstruct durable workspace checkout");
866
- if (revParse(workspacePath, "HEAD") !== targetHead) {
867
- throw new Error(`Workspace checkout reconstruction did not restore ${targetHead}`);
868
- }
869
- } else {
870
- git(workspacePath, ["symbolic-ref", "HEAD", `refs/heads/${WORKSPACE_GIT_BRANCH}`], "restore unborn workspace HEAD");
871
- git(workspacePath, ["update-ref", "-d", `refs/heads/${WORKSPACE_GIT_BRANCH}`], "remove interrupted workspace branch ref");
872
- git(workspacePath, ["read-tree", "--empty"], "reconstruct unborn workspace checkout");
873
- if (revParse(workspacePath, "HEAD") !== null) {
874
- throw new Error("Workspace checkout reconstruction did not restore its unborn HEAD");
875
- }
876
- }
877
- fsyncWorkspaceCheckoutTree(workspacePath);
878
- updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "durable", head: targetHead });
879
- }
880
- function workspaceCheckoutDurabilityIsCurrent(workspacePath) {
881
- const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
882
- return Boolean(record?.state === "durable" && record.head === revParse(workspacePath, "HEAD"));
883
- }
884
- async function workspaceCheckoutDurabilityIsCurrentAsync(workspacePath) {
885
- const record = await readWorkspaceCheckoutDurabilityRecordAsync(workspacePath);
886
- return Boolean(record?.state === "durable" && record.head === await revParseAsync(workspacePath, "HEAD"));
887
- }
888
- function runCheckoutTransitionBeginJob(input) {
889
- const workspacePath = path.resolve(input.workspacePath);
890
- recoverWorkspaceCheckoutDurability(workspacePath);
891
- const head = revParse(workspacePath, "HEAD");
892
- updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "transition", head });
893
- return null;
894
- }
895
- function runCheckoutTransitionCompleteJob(input) {
896
- const workspacePath = path.resolve(input.workspacePath);
897
- fsyncWorkspaceCheckoutTree(workspacePath);
898
- updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "durable", head: revParse(workspacePath, "HEAD") });
899
- return null;
900
- }
901
- function runCheckoutDurabilityRecoveryJob(input) {
902
- recoverWorkspaceCheckoutDurability(path.resolve(input.workspacePath), input.preserveResolutionInProgress);
903
- return null;
904
- }
905
- function runOuterCheckoutFsyncJob(input) {
906
- fsyncWorkspaceCheckoutTree(path.resolve(input.workspacePath));
907
- return null;
908
- }
909
- async function beginWorkspaceCheckoutTransition(workspacePath) {
910
- await workspaceFilesystemExecutor().run("checkout_transition_begin", { workspacePath });
911
- }
912
- async function completeWorkspaceCheckoutTransition(workspacePath) {
913
- await workspaceFilesystemExecutor().run("checkout_transition_complete", { workspacePath });
914
- }
915
- async function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
916
- try {
917
- await workspaceFilesystemExecutor().run("checkout_durability_recovery", { workspacePath, preserveResolutionInProgress: false });
918
- } catch (recoveryError) {
919
- throw new AggregateError(
920
- [error, recoveryError],
921
- `Workspace checkout mutation failed and its durable head could not be restored: ${error instanceof Error ? error.message : String(error)}`
922
- );
923
- }
924
- throw error;
925
- }
926
- async function runWorkspaceCheckoutTransition(workspacePath, mutate) {
927
- await beginWorkspaceCheckoutTransition(workspacePath);
928
- try {
929
- await mutate();
930
- await completeWorkspaceCheckoutTransition(workspacePath);
931
- } catch (error) {
932
- await throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
933
- }
934
- }
935
- function parseHydrationTransactionManifest(transactionPath, workspacePath = path.resolve(transactionPath, "..", "..", ".."), options = {}) {
936
- const manifestPath = path.join(transactionPath, "manifest.json");
937
- const status = fs.lstatSync(manifestPath);
938
- if (!status.isFile() || status.isSymbolicLink()) {
939
- throw new Error(`Workspace hydration transaction manifest is not a regular file: ${manifestPath}`);
940
- }
941
- let parsed;
942
- try {
943
- parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
944
- } catch (error) {
945
- throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`, { cause: error });
946
- }
947
- if (!parsed || typeof parsed !== "object") {
948
- throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
949
- }
950
- const candidate = parsed;
951
- 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))) {
952
- throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
953
- }
954
- if (candidate.version === 5 && (typeof candidate.transactionId !== "string" || !HYDRATION_TRANSACTION_ID.test(candidate.transactionId))) {
955
- throw new Error(`Workspace hydration transaction identity is invalid: ${manifestPath}`);
956
- }
957
- const parseReceipt = (rawReceipt) => {
958
- if (rawReceipt === null) return null;
959
- if (!rawReceipt || typeof rawReceipt !== "object") {
960
- throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
961
- }
962
- const content = `${JSON.stringify(rawReceipt)}
963
- `;
964
- return options.verifyReceiptCommits === false ? parseWorkspaceHydrationReceiptContentUnverified(manifestPath, content).receipt : parseWorkspaceHydrationReceiptContent(workspacePath, manifestPath, content);
965
- };
966
- const targetReceipt = parseReceipt(candidate.targetReceipt);
967
- const receiptBefore = parseReceipt(candidate.receiptBefore);
968
- const parseScope = (rawScope, existed) => {
969
- if (rawScope === void 0) return void 0;
970
- if (candidate.version !== 4 && candidate.version !== 5 || !existed || !rawScope || typeof rawScope !== "object") {
971
- throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
972
- }
973
- const parsePaths = (rawPaths) => {
974
- if (!Array.isArray(rawPaths)) throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
975
- const seen = /* @__PURE__ */ new Set();
976
- return rawPaths.map((rawPath) => {
977
- if (typeof rawPath !== "string" || normalizeWorkingTreeRelativePath(rawPath) !== rawPath || seen.has(rawPath)) {
978
- throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
979
- }
980
- seen.add(rawPath);
981
- return rawPath;
982
- });
983
- };
984
- const { absent, trees } = rawScope;
985
- return { absent: parsePaths(absent), trees: parsePaths(trees) };
986
- };
987
- const seenIds = /* @__PURE__ */ new Set();
988
- const seenSnapshots = /* @__PURE__ */ new Set();
989
- const parsedMounts = [...candidate.mounts, ...candidate.preservedMounts ?? []].map((rawMount) => {
990
- if (!rawMount || typeof rawMount !== "object") {
991
- throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
992
- }
993
- const mount = rawMount;
994
- if (typeof mount.id !== "string" || !mount.id || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || path.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || path.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || typeof mount.workspaceRelativePath !== "string" || typeof mount.existed !== "boolean" || typeof mount.snapshotName !== "string" || !/^[0-9]+$/.test(mount.snapshotName) || seenIds.has(mount.id) || seenSnapshots.has(mount.snapshotName)) {
995
- throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
996
- }
997
- const relative = path.relative(mount.durabilityRootPath, mount.sourcePath);
998
- if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
999
- throw new Error(`Workspace hydration transaction source escapes its durability root: ${mount.sourcePath}`);
1000
- }
1001
- seenIds.add(mount.id);
1002
- seenSnapshots.add(mount.snapshotName);
1003
- const scope = parseScope(rawMount.scope, mount.existed);
1004
- if (candidate.version === 5 && mount.recovery !== "snapshot" && mount.recovery !== "guarded" || candidate.version !== 5 && mount.recovery !== void 0 || mount.recovery === "guarded" && (!mount.existed || !scope)) {
1005
- throw new Error(`Workspace hydration recovery policy is invalid: ${manifestPath}`);
1006
- }
1007
- return {
1008
- id: mount.id,
1009
- incarnationKey: mount.incarnationKey,
1010
- sourcePath: mount.sourcePath,
1011
- durabilityRootPath: mount.durabilityRootPath,
1012
- workspaceRelativePath: mount.workspaceRelativePath,
1013
- existed: mount.existed,
1014
- snapshotName: mount.snapshotName,
1015
- ...scope ? { scope } : {},
1016
- ...candidate.version === 5 ? { recovery: mount.recovery } : {}
1017
- };
1018
- });
1019
- return {
1020
- version: candidate.version === 5 ? 5 : candidate.version === 4 ? 4 : 3,
1021
- ...candidate.version === 5 ? { transactionId: candidate.transactionId } : {},
1022
- targetReceipt,
1023
- receiptBefore,
1024
- mounts: parsedMounts.slice(0, candidate.mounts.length),
1025
- ...candidate.preservedMounts !== void 0 ? { preservedMounts: parsedMounts.slice(candidate.mounts.length) } : {}
1026
- };
1027
- }
1028
- function hydrationEvidenceMounts(manifest) {
1029
- return [...manifest.mounts, ...manifest.preservedMounts ?? []];
1030
- }
1031
- function removeHydrationTransaction(workspacePath) {
1032
- const transactionPath = hydrationTransactionPath(workspacePath);
1033
- fs.rmSync(transactionPath, { recursive: true, force: true });
1034
- fsyncDirectory(path.dirname(transactionPath));
1035
- }
1036
- function hydrationTransactionCommitted(transactionPath) {
1037
- const markerPath = path.join(transactionPath, "committed");
1038
- const markerStatus = lstatIfExists(markerPath);
1039
- if (!markerStatus) return false;
1040
- if (!markerStatus.isFile() || markerStatus.isSymbolicLink() || fs.readFileSync(markerPath, "utf8") !== "committed\n") {
1041
- throw new Error(`Workspace hydration transaction commit marker is invalid: ${markerPath}`);
1042
- }
1043
- return true;
1044
- }
1045
- function hydrationRetainedEvidence(mount) {
1046
- if (!lstatIfExists(mount.sourcePath)) return null;
1047
- assertManagedDirectoryPath({
1048
- trustedRoot: mount.durabilityRootPath,
1049
- candidate: mount.sourcePath,
1050
- label: `Hydration evidence mount ${mount.id}`
1051
- });
1052
- const dotGit = path.join(mount.sourcePath, ".git");
1053
- if (lstatIfExists(dotGit)?.isSymbolicLink()) throw new Error(`Hydration evidence Git directory is a symlink: ${dotGit}`);
1054
- const directory = workingTreeDisplacedRetentionDirectory(mount.sourcePath);
1055
- if (!directory) return null;
1056
- assertManagedDirectoryPath({
1057
- trustedRoot: mount.durabilityRootPath,
1058
- candidate: directory,
1059
- label: `Hydration retained evidence ${mount.id}`
1060
- });
1061
- if (!lstatIfExists(directory)) return null;
1062
- return {
1063
- directory,
1064
- paths: fs.readdirSync(directory).sort().map((name) => path.join(directory, name))
1065
- };
1066
- }
1067
- function fsyncHydrationRetainedEvidence(mount) {
1068
- const evidence = hydrationRetainedEvidence(mount);
1069
- if (!evidence || evidence.paths.length === 0) return;
1070
- fsyncTree(evidence.directory);
1071
- for (let directory = path.dirname(evidence.directory); ; directory = path.dirname(directory)) {
1072
- fsyncDirectory(directory);
1073
- if (directory === path.resolve(mount.durabilityRootPath)) break;
1074
- }
1075
- }
1076
- function readHydrationArchivedEvidence(transactionPath, manifest) {
1077
- const recordPath = path.join(transactionPath, "retained-evidence.json");
1078
- const status = lstatIfExists(recordPath);
1079
- if (!status) return [];
1080
- if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Invalid hydration retained-evidence record: ${recordPath}`);
1081
- const record = JSON.parse(fs.readFileSync(recordPath, "utf8"));
1082
- if (record.version !== 1 || !Array.isArray(record.entries)) throw new Error(`Invalid hydration retained-evidence record: ${recordPath}`);
1083
- const seen = /* @__PURE__ */ new Set();
1084
- return record.entries.map((raw) => {
1085
- if (!raw || typeof raw !== "object") throw new Error(`Invalid hydration retained-evidence entry: ${recordPath}`);
1086
- const entry = raw;
1087
- const mount = hydrationEvidenceMounts(manifest).find(({ id }) => id === entry.mountId);
1088
- const parts = typeof entry.archiveRelativePath === "string" ? entry.archiveRelativePath.split("/") : [];
1089
- const sourceRelative = typeof entry.sourcePath === "string" && mount ? path.relative(mount.durabilityRootPath, entry.sourcePath) : "";
1090
- 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)) {
1091
- throw new Error(`Invalid hydration retained-evidence entry: ${recordPath}`);
1092
- }
1093
- const archivedPath = path.join(transactionPath, ...parts);
1094
- assertManagedDirectoryPath({
1095
- trustedRoot: transactionPath,
1096
- candidate: path.dirname(archivedPath),
1097
- label: "Archived hydration evidence"
1098
- });
1099
- if (!lstatIfExists(archivedPath)) throw new Error(`Archived hydration evidence is missing: ${archivedPath}`);
1100
- seen.add(entry.archiveRelativePath);
1101
- return { mountId: entry.mountId, sourcePath: entry.sourcePath, archiveRelativePath: entry.archiveRelativePath };
1102
- });
1103
- }
1104
- function captureHydrationRetainedEvidence(transactionPath, manifest) {
1105
- const entries = readHydrationArchivedEvidence(transactionPath, manifest);
1106
- const captureId = crypto.randomUUID();
1107
- let captured = false;
1108
- for (const mount of hydrationEvidenceMounts(manifest)) {
1109
- const evidence = hydrationRetainedEvidence(mount);
1110
- if (!evidence || evidence.paths.length === 0) continue;
1111
- fsyncHydrationRetainedEvidence(mount);
1112
- const relativeDirectory = `displaced/${captureId}/${mount.snapshotName}`;
1113
- const destinationDirectory = path.join(transactionPath, relativeDirectory);
1114
- assertManagedDirectoryPath({ trustedRoot: transactionPath, candidate: destinationDirectory, label: "Hydration evidence capture" });
1115
- fs.mkdirSync(destinationDirectory, { recursive: true, mode: 448 });
1116
- for (const sourcePath of evidence.paths) {
1117
- const archiveRelativePath = `${relativeDirectory}/${path.basename(sourcePath)}`;
1118
- fs.cpSync(sourcePath, path.join(transactionPath, archiveRelativePath), {
1119
- recursive: true,
1120
- dereference: false,
1121
- verbatimSymlinks: true,
1122
- errorOnExist: true,
1123
- force: false,
1124
- preserveTimestamps: true
1125
- });
1126
- entries.push({ mountId: mount.id, sourcePath, archiveRelativePath });
1127
- captured = true;
1128
- }
1129
- }
1130
- if (captured) {
1131
- fsyncTree(path.join(transactionPath, "displaced"));
1132
- const temporaryPath = path.join(transactionPath, `.retained-evidence.${crypto.randomUUID()}.tmp`);
1133
- writePrivateFileDurably(temporaryPath, `${JSON.stringify({ version: 1, entries })}
1134
- `);
1135
- fs.renameSync(temporaryPath, path.join(transactionPath, "retained-evidence.json"));
1136
- fsyncDirectory(transactionPath);
1137
- }
1138
- return entries;
1139
- }
1140
- function hydrationRecoveryRequiredError(workspacePath, manifest, cause) {
1141
- if (manifest.version !== 5 || !manifest.transactionId) throw new Error("Guarded hydration lacks a durable transaction identity");
1142
- return new WorkspaceHydrationRecoveryRequiredError(
1143
- {
1144
- transactionId: manifest.transactionId,
1145
- transactionPath: hydrationTransactionPath(workspacePath),
1146
- mounts: hydrationEvidenceMounts(manifest).map(({ id, incarnationKey, sourcePath, durabilityRootPath, workspaceRelativePath }) => {
1147
- const retainedPaths = hydrationRetainedEvidence({ id, sourcePath, durabilityRootPath })?.paths;
1148
- return {
1149
- id,
1150
- incarnationKey,
1151
- sourcePath,
1152
- durabilityRootPath,
1153
- workspaceRelativePath,
1154
- ...retainedPaths?.length ? { retainedPaths } : {}
1155
- };
1156
- }),
1157
- receiptBefore: manifest.receiptBefore
1158
- },
1159
- cause === void 0 ? void 0 : { cause }
1160
- );
1161
- }
1162
- function runHydrationRecoveryInspectJob(input) {
1163
- const workspacePath = path.resolve(input.workspacePath);
1164
- const workspaceStatus = lstatIfExists(workspacePath);
1165
- if (!workspaceStatus) return null;
1166
- if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink())
1167
- throw new Error(`Invalid workspace hydration root: ${workspacePath}`);
1168
- const transactionPath = hydrationTransactionPath(workspacePath);
1169
- assertManagedDirectoryPath({
1170
- trustedRoot: workspacePath,
1171
- candidate: transactionPath,
1172
- label: "Workspace hydration transaction inspection"
1173
- });
1174
- const status = lstatIfExists(transactionPath);
1175
- if (!status) return null;
1176
- if (!status.isDirectory() || status.isSymbolicLink()) throw new Error(`Invalid workspace hydration transaction: ${transactionPath}`);
1177
- const manifest = parseHydrationTransactionManifest(transactionPath);
1178
- if (hydrationTransactionCommitted(transactionPath) || !manifest.mounts.some(({ recovery }) => recovery === "guarded")) return null;
1179
- return hydrationRecoveryRequiredError(workspacePath, manifest).hydrationRecovery;
1180
- }
1181
- function readPreservedHydrationBases(workspacePath) {
1182
- const recordPath = path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
1183
- const status = lstatIfExists(recordPath);
1184
- if (!status) return null;
1185
- if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Preserved hydration bases are not a regular file: ${recordPath}`);
1186
- return parseWorkspaceHydrationReceiptContentUnverified(recordPath, fs.readFileSync(recordPath, "utf8")).receipt;
1187
- }
1188
- async function readPreservedHydrationBasesAsync(workspacePath) {
1189
- const recordPath = path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
1190
- let status;
1191
- try {
1192
- status = await fs.promises.lstat(recordPath);
1193
- } catch (error) {
1194
- if (error.code === "ENOENT") return null;
1195
- throw error;
1196
- }
1197
- if (!status.isFile() || status.isSymbolicLink()) throw new Error(`Preserved hydration bases are not a regular file: ${recordPath}`);
1198
- return parseWorkspaceHydrationReceiptContentUnverified(recordPath, await fs.promises.readFile(recordPath, "utf8")).receipt;
1199
- }
1200
- function preserveHydrationBases(workspacePath, manifest) {
1201
- const current = readHydratedWorkspaceReceipt(workspacePath);
1202
- if (!workspaceHydrationReceiptsEqual(current, manifest.receiptBefore)) {
1203
- throw new Error(`Hydration receipt changed before preservation of transaction ${manifest.transactionId}`);
1204
- }
1205
- const previous = readPreservedHydrationBases(workspacePath);
1206
- const affectedIds = new Set(hydrationEvidenceMounts(manifest).map(({ id }) => id));
1207
- const mounts = (current?.mounts ?? []).filter(
1208
- (entry) => affectedIds.has(entry.id) || previous?.mounts.some(
1209
- (preserved) => workspaceHydrationReceiptsEqual({ version: 3, mounts: [entry] }, { version: 3, mounts: [preserved] })
1210
- )
1211
- );
1212
- const recordPath = path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_PRESERVED_BASES.split("/"));
1213
- const temporaryPath = `${recordPath}.${crypto.randomUUID()}.tmp`;
1214
- writePrivateFileDurably(temporaryPath, `${JSON.stringify({ version: 3, mounts })}
1215
- `);
1216
- fs.renameSync(temporaryPath, recordPath);
1217
- fsyncDirectory(path.dirname(recordPath));
1218
- }
1219
- function runHydrationPreserveInterruptedJob(input) {
1220
- const workspacePath = path.resolve(input.workspacePath);
1221
- if (!HYDRATION_TRANSACTION_ID.test(input.expectedTransactionId)) throw new Error("Invalid hydration recovery transaction identity");
1222
- const stateDirectory = workspacePrivateStateDirectory(workspacePath);
1223
- const transactionPath = hydrationTransactionPath(workspacePath);
1224
- const archivesPath = path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_RECOVERIES.split("/"));
1225
- const archivePath = path.join(archivesPath, input.expectedTransactionId);
1226
- assertManagedDirectoryPath({ trustedRoot: stateDirectory, candidate: archivesPath, label: "Workspace hydration archives" });
1227
- const pending = lstatIfExists(transactionPath);
1228
- if (!pending) {
1229
- assertManagedDirectoryPath({ trustedRoot: stateDirectory, candidate: archivePath, label: "Workspace hydration archive" });
1230
- if (!lstatIfExists(archivePath))
1231
- throw new Error(`No pending or preserved hydration transaction matches ${input.expectedTransactionId}`);
1232
- const archived = parseHydrationTransactionManifest(archivePath, workspacePath, { verifyReceiptCommits: false });
1233
- const markerPath2 = path.join(archivePath, "preserved.json");
1234
- const markerStatus = fs.lstatSync(markerPath2);
1235
- if (!markerStatus.isFile() || markerStatus.isSymbolicLink()) throw new Error(`Invalid hydration preservation record: ${markerPath2}`);
1236
- const marker = JSON.parse(fs.readFileSync(markerPath2, "utf8"));
1237
- if (archived.version !== 5 || archived.transactionId !== input.expectedTransactionId || marker.version !== 1 || marker.transactionId !== archived.transactionId) {
1238
- throw new Error(`Hydration archive identity does not match ${input.expectedTransactionId}`);
1239
- }
1240
- const retainedEntries2 = readHydrationArchivedEvidence(archivePath, archived).map(({ mountId, sourcePath, archiveRelativePath }) => ({
1241
- mountId,
1242
- sourcePath,
1243
- archivePath: path.join(archivePath, archiveRelativePath)
1244
- }));
1245
- fsyncDirectory(archivePath);
1246
- fsyncDirectory(archivesPath);
1247
- fsyncDirectory(stateDirectory);
1248
- return {
1249
- transactionId: archived.transactionId,
1250
- archivePath,
1251
- receiptBefore: archived.receiptBefore,
1252
- preservedMountIds: hydrationEvidenceMounts(archived).map(({ id }) => id).sort(),
1253
- alreadyPreserved: true,
1254
- ...retainedEntries2.length > 0 ? { retainedEntries: retainedEntries2 } : {}
1255
- };
1256
- }
1257
- if (!pending.isDirectory() || pending.isSymbolicLink()) throw new Error(`Invalid workspace hydration transaction: ${transactionPath}`);
1258
- const manifest = parseHydrationTransactionManifest(transactionPath);
1259
- if (manifest.version !== 5 || manifest.transactionId !== input.expectedTransactionId) {
1260
- throw new Error(
1261
- `Stale hydration recovery transaction identity: expected ${input.expectedTransactionId}, pending ${manifest.transactionId ?? "legacy transaction"}`
1262
- );
1263
- }
1264
- if (hydrationTransactionCommitted(transactionPath))
1265
- throw new Error(`Hydration transaction ${manifest.transactionId} is committed; ordinary recovery must finish its receipt`);
1266
- if (!manifest.mounts.some(({ recovery }) => recovery === "guarded"))
1267
- throw new Error(`Hydration transaction ${manifest.transactionId} has no interrupted guarded mounts`);
1268
- if (lstatIfExists(archivePath))
1269
- throw new Error(`Hydration archive already exists while transaction ${manifest.transactionId} is pending`);
1270
- if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), manifest.receiptBefore)) {
1271
- throw new Error(`Hydration receipt changed before preservation of transaction ${manifest.transactionId}`);
1272
- }
1273
- for (const mount of hydrationEvidenceMounts(manifest)) {
1274
- assertManagedDirectoryPath({
1275
- trustedRoot: mount.durabilityRootPath,
1276
- candidate: mount.sourcePath,
1277
- label: `Hydration preservation mount ${mount.id}`
1278
- });
1279
- const source = lstatIfExists(mount.sourcePath);
1280
- if (mount.existed && !source) throw new Error(`Hydration preservation mount ${mount.id} is missing: ${mount.sourcePath}`);
1281
- const snapshotPath = path.join(transactionPath, "mounts", mount.snapshotName);
1282
- assertManagedDirectoryPath({
1283
- trustedRoot: transactionPath,
1284
- candidate: snapshotPath,
1285
- label: `Hydration preservation snapshot ${mount.id}`
1286
- });
1287
- if (!lstatIfExists(snapshotPath)) throw new Error(`Hydration preservation snapshot ${mount.id} is missing`);
1288
- const basis = manifest.receiptBefore?.mounts.find((entry) => entry.id === mount.id);
1289
- if (!basis || basis.incarnationKey !== mount.incarnationKey || basis.sourcePath !== mount.sourcePath || basis.durabilityRootPath !== mount.durabilityRootPath || basis.workspaceRelativePath !== mount.workspaceRelativePath) {
1290
- throw new Error(
1291
- `Hydration preservation mount ${mount.id} has no matching prior receipt basis; manual salvage is required. The transaction and all snapshots remain at ${transactionPath}`
1292
- );
1293
- }
1294
- }
1295
- for (const mount of hydrationEvidenceMounts(manifest)) {
1296
- fsyncHydratedWorkspaceMount({ ...mount, hydrationIncarnationKey: mount.incarnationKey, sourceMode: "all", hydrateDeletionMode: "all" });
1297
- }
1298
- const retainedEntries = captureHydrationRetainedEvidence(transactionPath, manifest).map(
1299
- ({ mountId, sourcePath, archiveRelativePath }) => ({
1300
- mountId,
1301
- sourcePath,
1302
- archivePath: path.join(archivePath, archiveRelativePath)
1303
- })
1304
- );
1305
- fsyncTree(transactionPath);
1306
- preserveHydrationBases(workspacePath, manifest);
1307
- repairWorkspaceBasisRefs(workspacePath, manifest.receiptBefore);
1308
- const markerPath = path.join(transactionPath, "preserved.json");
1309
- const markerContent = `${JSON.stringify({ version: 1, transactionId: manifest.transactionId })}
1310
- `;
1311
- const existingMarker = lstatIfExists(markerPath);
1312
- if (existingMarker) {
1313
- if (!existingMarker.isFile() || existingMarker.isSymbolicLink() || fs.readFileSync(markerPath, "utf8") !== markerContent) {
1314
- throw new Error(`Invalid hydration preservation record: ${markerPath}`);
1315
- }
1316
- } else {
1317
- const markerTemporaryPath = path.join(transactionPath, `.preserved.${crypto.randomUUID()}.tmp`);
1318
- writePrivateFileDurably(markerTemporaryPath, markerContent);
1319
- fs.renameSync(markerTemporaryPath, markerPath);
1320
- }
1321
- fsyncDirectory(transactionPath);
1322
- if (!lstatIfExists(archivesPath)) {
1323
- fs.mkdirSync(archivesPath, { mode: 448 });
1324
- fsyncDirectory(stateDirectory);
1325
- }
1326
- fs.renameSync(transactionPath, archivePath);
1327
- fsyncDirectory(archivesPath);
1328
- fsyncDirectory(stateDirectory);
1329
- return {
1330
- transactionId: manifest.transactionId,
1331
- archivePath,
1332
- receiptBefore: manifest.receiptBefore,
1333
- preservedMountIds: hydrationEvidenceMounts(manifest).map(({ id }) => id).sort(),
1334
- alreadyPreserved: false,
1335
- ...retainedEntries.length > 0 ? { retainedEntries } : {}
1336
- };
1337
- }
1338
- function restoreHydrationTransaction(workspacePath, manifest) {
1339
- if (manifest.mounts.some(({ recovery }) => recovery === "guarded")) {
1340
- throw hydrationRecoveryRequiredError(workspacePath, manifest);
1341
- }
1342
- const transactionPath = hydrationTransactionPath(workspacePath);
1343
- const durabilityScopes = /* @__PURE__ */ new Map();
1344
- for (const mount of manifest.mounts) {
1345
- assertManagedDirectoryPath({
1346
- trustedRoot: mount.durabilityRootPath,
1347
- candidate: mount.sourcePath,
1348
- label: `Workspace hydration recovery mount ${mount.id}`
1349
- });
1350
- if (!mount.existed) {
1351
- fs.rmSync(mount.sourcePath, { recursive: true, force: true });
1352
- continue;
1353
- }
1354
- const snapshotPath = path.join(transactionPath, "mounts", mount.snapshotName);
1355
- const snapshotStatus = fs.lstatSync(snapshotPath);
1356
- if (!snapshotStatus.isDirectory() || snapshotStatus.isSymbolicLink()) {
1357
- throw new Error(`Workspace hydration snapshot is not a regular directory: ${snapshotPath}`);
1358
- }
1359
- if (mount.scope) {
1360
- const restored = restoreWorkingTreeMirrorScope({ snapshotRoot: snapshotPath, targetRoot: mount.sourcePath, scope: mount.scope });
1361
- durabilityScopes.set(mount.id, restored.paths);
1362
- continue;
1363
- }
1364
- mirrorWorkingTree({ sourceRoot: snapshotPath, targetRoot: mount.sourcePath, sourceMode: "all", deletionMode: "all" });
1365
- }
1366
- fsyncHydratedWorkspaceMounts(
1367
- manifest.mounts.map((mount) => ({
1368
- ...mount,
1369
- hydrationIncarnationKey: mount.incarnationKey,
1370
- sourceMode: "all",
1371
- hydrateDeletionMode: "all"
1372
- })),
1373
- durabilityScopes
1374
- );
1375
- }
1376
- function recoverPendingHydrationTransaction(workspacePath) {
1377
- const stateDirectory = path.dirname(hydrationTransactionPath(workspacePath));
1378
- const stateStatus = lstatIfExists(stateDirectory);
1379
- if (!stateStatus) return;
1380
- if (!stateStatus.isDirectory() || stateStatus.isSymbolicLink()) {
1381
- throw new Error(`Workspace Git private state is not a regular directory: ${stateDirectory}`);
1382
- }
1383
- for (const name of fs.readdirSync(stateDirectory)) {
1384
- if (!name.startsWith(".workspace-hydration-transaction.") || !name.endsWith(".tmp")) continue;
1385
- fs.rmSync(path.join(stateDirectory, name), { recursive: true, force: true });
1386
- fsyncDirectory(stateDirectory);
1387
- }
1388
- const transactionPath = hydrationTransactionPath(workspacePath);
1389
- const transactionStatus = lstatIfExists(transactionPath);
1390
- if (!transactionStatus) return;
1391
- if (!transactionStatus.isDirectory() || transactionStatus.isSymbolicLink()) {
1392
- throw new Error(`Workspace hydration transaction is not a regular directory: ${transactionPath}`);
1393
- }
1394
- const manifest = parseHydrationTransactionManifest(transactionPath);
1395
- if (hydrationTransactionCommitted(transactionPath)) {
1396
- const receipt2 = readHydratedWorkspaceReceipt(workspacePath);
1397
- if (manifest.targetReceipt && !workspaceHydrationReceiptsEqual(receipt2, manifest.targetReceipt)) {
1398
- updateHydratedWorkspaceReceipt(workspacePath, manifest.targetReceipt);
1399
- }
1400
- removeHydrationTransaction(workspacePath);
1401
- return;
1402
- }
1403
- const receipt = readHydratedWorkspaceReceipt(workspacePath);
1404
- if (manifest.version !== 5 && manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
1405
- removeHydrationTransaction(workspacePath);
1406
- return;
1407
- }
1408
- restoreHydrationTransaction(workspacePath, manifest);
1409
- removeHydrationTransaction(workspacePath);
1410
- }
1411
- function gitOutputSync(cwd, args, action, input) {
1412
- const result = Bun.spawnSync(gitCommandArgs(args), {
1413
- cwd,
1414
- ...input ? { stdin: input } : {},
1415
- stdout: "pipe",
1416
- stderr: "pipe",
1417
- env: workerGitProcessEnvironment()
1418
- });
1419
- if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
1420
- return Buffer.from(result.stdout);
1421
- }
1422
- function workspaceSubtreeChanges(workspacePath, projectedCommit, head, workspaceRelativePath) {
1423
- const normalized = normalizedWorkspaceMountPath(workspaceRelativePath);
1424
- const names = [`${projectedCommit}:${normalized}`, `${head}:${normalized}`];
1425
- const resolved = resolveGitObjectsSync(workspacePath, names);
1426
- const emptyTree = emptyTreeHash(workspacePath);
1427
- const subtree = (name) => {
1428
- const object = resolved.get(name);
1429
- return object?.type === "tree" ? object.objectId : emptyTree;
1430
- };
1431
- const base = subtree(names[0]);
1432
- const theirs = subtree(names[1]);
1433
- if (base === theirs) return parseWorkspaceSubtreeChanges(Buffer.alloc(0));
1434
- return parseWorkspaceSubtreeChanges(
1435
- gitOutputSync(
1436
- workspacePath,
1437
- ["diff-tree", "-r", "-z", "--no-renames", "--raw", base, theirs],
1438
- "compare projected and hydration subtrees"
1439
- )
1440
- );
1441
- }
1442
- function resolveGitObjectsSync(workspacePath, names) {
1443
- const resolved = /* @__PURE__ */ new Map();
1444
- const unique = [...new Set(names)];
1445
- if (unique.length === 0) return resolved;
1446
- const output = gitOutputSync(
1447
- workspacePath,
1448
- ["cat-file", "--batch-check"],
1449
- "resolve workspace objects",
1450
- Buffer.from(`${unique.join("\n")}
1451
- `)
1452
- );
1453
- const lines = output.toString().split("\n").filter((line) => line.length > 0);
1454
- if (lines.length !== unique.length) {
1455
- throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1456
- }
1457
- unique.forEach((name, index) => {
1458
- const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
1459
- resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
1460
- });
1461
- return resolved;
1462
- }
1463
- function storeWorkspaceBlobs(workspacePath, blobs) {
1464
- for (const [objectId, content] of blobs) {
1465
- const written = gitOutputSync(workspacePath, ["hash-object", "-w", "-t", "blob", "--no-filters", "--stdin"], "store hydration ledger blob", content).toString().trim();
1466
- if (written !== objectId) throw new Error(`Hydration ledger blob ${objectId} was stored as ${written}`);
1467
- }
1468
- }
1469
- function readWorkspaceBlobs(workspacePath, objectIds) {
1470
- const blobs = /* @__PURE__ */ new Map();
1471
- const unique = [...new Set(objectIds)];
1472
- if (unique.length === 0) return blobs;
1473
- const output = gitOutputSync(
1474
- workspacePath,
1475
- ["cat-file", "--batch"],
1476
- "read projected workspace blobs",
1477
- Buffer.from(`${unique.join("\n")}
1478
- `)
1479
- );
1480
- let offset = 0;
1481
- while (offset < output.length) {
1482
- const newline = output.indexOf(10, offset);
1483
- if (newline < 0) break;
1484
- const header = output.subarray(offset, newline).toString();
1485
- offset = newline + 1;
1486
- const match = /^([0-9a-f]{40,64}) (\S+) (\d+)$/u.exec(header);
1487
- if (!match) {
1488
- if (/ missing$/u.test(header)) continue;
1489
- throw new Error(`Unexpected cat-file record while reading projected blobs: ${header}`);
1490
- }
1491
- const size = Number(match[3]);
1492
- if (match[2] === "blob") blobs.set(match[1], Buffer.from(output.subarray(offset, offset + size)));
1493
- offset += size + 1;
1494
- }
1495
- return blobs;
1496
- }
1497
- function revertReceiptMounts(target, before, mountIds) {
1498
- if (!target || mountIds.size === 0) return target;
1499
- const beforeById = new Map(before?.mounts.map((entry) => [entry.id, entry]) ?? []);
1500
- const mounts = target.mounts.flatMap((entry) => {
1501
- if (!mountIds.has(entry.id)) return [entry];
1502
- const previous = beforeById.get(entry.id);
1503
- return previous ? [previous] : [];
1504
- }).sort(compareWorkspaceHydrationMountBasis);
1505
- return { version: 3, mounts };
1506
- }
1507
- function rewriteHydrationTransactionManifest(workspacePath, manifest) {
1508
- const transactionPath = hydrationTransactionPath(workspacePath);
1509
- const temporaryPath = path.join(transactionPath, `.manifest.${process.pid}.${crypto.randomUUID()}.tmp`);
1510
- try {
1511
- writePrivateFileDurably(temporaryPath, `${JSON.stringify(manifest)}
1512
- `);
1513
- fs.renameSync(temporaryPath, path.join(transactionPath, "manifest.json"));
1514
- fsyncDirectory(transactionPath);
1515
- } catch (error) {
1516
- fs.rmSync(temporaryPath, { force: true });
1517
- throw error;
1518
- }
1519
- }
1520
- function beginHydrationTransaction(workspacePath, mounts, targetReceipt, mergeBases) {
1521
- recoverPendingHydrationTransaction(workspacePath);
1522
- const stateDirectory = workspacePrivateStateDirectory(workspacePath);
1523
- const transactionPath = hydrationTransactionPath(workspacePath);
1524
- const stagingPath = path.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
1525
- fs.mkdirSync(stagingPath, { mode: 448 });
1526
- const durabilityScopes = /* @__PURE__ */ new Map();
1527
- const mergePlans = /* @__PURE__ */ new Map();
1528
- const skippedMounts = [];
1529
- try {
1530
- const snapshotRoot = path.join(stagingPath, "mounts");
1531
- fs.mkdirSync(snapshotRoot, { mode: 448 });
1532
- const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
1533
- const head = mergeBases && Object.keys(mergeBases).length > 0 ? revParse(workspacePath, "HEAD") : null;
1534
- const manifestMounts = [];
1535
- for (const mount of mounts) {
1536
- const sourcePath = path.resolve(mount.sourcePath);
1537
- const durabilityRootPath = path.resolve(mount.durabilityRootPath);
1538
- assertManagedDirectoryPath({
1539
- trustedRoot: durabilityRootPath,
1540
- candidate: sourcePath,
1541
- label: `Workspace hydration snapshot mount ${mount.id}`
1542
- });
1543
- const sourceStatus = lstatIfExists(sourcePath);
1544
- if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
1545
- throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
1546
- }
1547
- const snapshotName = String(manifestMounts.length);
1548
- const snapshotPath = path.join(snapshotRoot, snapshotName);
1549
- let scope;
1550
- const basis = mergeBases?.[mount.id];
1551
- if (sourceStatus && mount.hydrateDeletionMode === "git" && basis && head) {
1552
- const outerPath = workspaceMountOuterPath(workspacePath, mount);
1553
- assertManagedDirectoryPath({
1554
- trustedRoot: workspacePath,
1555
- candidate: outerPath,
1556
- label: `Workspace mount ${mount.id} outer source`
1557
- });
1558
- const prepared = prepareWorkingTreeMirror({
1559
- sourceRoot: outerPath,
1560
- targetRoot: sourcePath,
1561
- sourceMode: "all",
1562
- deletionMode: "git",
1563
- sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
1564
- });
1565
- const decision = decideWorkspaceHydrationMerge({
1566
- prepared,
1567
- changes: workspaceSubtreeChanges(workspacePath, basis.projectedCommit, head, mount.workspaceRelativePath),
1568
- projectedFiles: basis.projectedFiles,
1569
- ...basis.readAtMs === void 0 ? {} : { readAtMs: basis.readAtMs },
1570
- readBaseBlobs: (objectIds) => readWorkspaceBlobs(workspacePath, objectIds),
1571
- readBinaryPaths: (relativePaths) => readHydrationBinaryPaths({ checkoutRoot: prepared.targetRoot, outerRoot: prepared.sourceRoot, relativePaths })
1572
- });
1573
- if (decision.kind !== "apply") {
1574
- skippedMounts.push({
1575
- mountId: mount.id,
1576
- reason: decision.kind === "conflict" ? "hydration_merge_conflict" : "hydration_target_changed",
1577
- paths: decision.paths
1578
- });
1579
- continue;
1580
- }
1581
- fs.mkdirSync(snapshotPath, { mode: 448 });
1582
- const plan = planPreparedWorkingTreeMirror(prepared, { selection: decision.selection, overrides: decision.overrides });
1583
- scope = captureWorkingTreeMirrorPlan({
1584
- plan,
1585
- targetRoot: sourcePath,
1586
- snapshotRoot: snapshotPath,
1587
- durability: "deferred_private_staging"
1588
- });
1589
- durabilityScopes.set(mount.id, [.../* @__PURE__ */ new Set([...plan.desired.keys(), ...decision.selection.remove])]);
1590
- mergePlans.set(mount.id, { prepared, decision, plan, snapshotPath: path.join(transactionPath, "mounts", snapshotName), basis });
1591
- } else if (sourceStatus && mount.hydrateDeletionMode === "git") {
1592
- fs.mkdirSync(snapshotPath, { mode: 448 });
1593
- const outerPath = workspaceMountOuterPath(workspacePath, mount);
1594
- assertManagedDirectoryPath({
1595
- trustedRoot: workspacePath,
1596
- candidate: outerPath,
1597
- label: `Workspace mount ${mount.id} outer source`
1598
- });
1599
- const plan = planWorkingTreeMirror({
1600
- sourceRoot: outerPath,
1601
- targetRoot: sourcePath,
1602
- sourceMode: "all",
1603
- deletionMode: "git",
1604
- sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount)
1605
- });
1606
- scope = captureWorkingTreeMirrorPlan({
1607
- plan,
1608
- targetRoot: sourcePath,
1609
- snapshotRoot: snapshotPath,
1610
- // Same private-staging deferral as the complete snapshot below.
1611
- durability: "deferred_private_staging"
1612
- });
1613
- durabilityScopes.set(mount.id, [...plan.desired.keys()]);
1614
- } else {
1615
- fs.mkdirSync(snapshotPath, { mode: 448 });
1616
- if (sourceStatus) {
1617
- mirrorWorkingTree({
1618
- sourceRoot: sourcePath,
1619
- targetRoot: snapshotPath,
1620
- sourceMode: "all",
1621
- deletionMode: "all",
1622
- // This tree is still private and the source authority is untouched.
1623
- // The single recursive barrier below makes the complete snapshot
1624
- // durable before its directory entry is published or hydration starts.
1625
- durability: "deferred_private_staging"
1626
- });
1627
- }
1628
- }
1629
- manifestMounts.push({
1630
- id: mount.id,
1631
- incarnationKey: mount.hydrationIncarnationKey,
1632
- sourcePath,
1633
- durabilityRootPath,
1634
- workspaceRelativePath: mount.workspaceRelativePath,
1635
- existed: Boolean(sourceStatus),
1636
- snapshotName,
1637
- ...scope ? { scope } : {},
1638
- recovery: mergePlans.has(mount.id) ? "guarded" : "snapshot"
1639
- });
1640
- }
1641
- const effectiveTargetReceipt = revertReceiptMounts(targetReceipt, receiptBefore, new Set(skippedMounts.map(({ mountId }) => mountId)));
1642
- const guarded = mergePlans.size > 0;
1643
- const manifest = {
1644
- // Older workers reject guarded transactions rather than replaying them
1645
- // through an unguarded snapshot restore after a downgrade.
1646
- version: guarded ? 5 : manifestMounts.some((mount) => mount.scope) ? 4 : 3,
1647
- ...guarded ? { transactionId: crypto.randomUUID() } : {},
1648
- targetReceipt: effectiveTargetReceipt,
1649
- receiptBefore,
1650
- mounts: guarded ? manifestMounts : manifestMounts.map(({ recovery: _recovery, ...mount }) => mount)
1651
- };
1652
- writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
1653
- `);
1654
- fsyncTree(stagingPath);
1655
- fs.renameSync(stagingPath, transactionPath);
1656
- fsyncDirectory(stateDirectory);
1657
- return { manifest, durabilityScopes, mergePlans, skippedMounts, targetReceipt: effectiveTargetReceipt };
1658
- } catch (error) {
1659
- fs.rmSync(stagingPath, { recursive: true, force: true });
1660
- fsyncDirectory(stateDirectory);
1661
- throw error;
1662
- }
1663
- }
1664
- function markHydrationTransactionDurable(workspacePath) {
1665
- const transactionPath = hydrationTransactionPath(workspacePath);
1666
- const temporaryPath = path.join(transactionPath, `.committed.${crypto.randomUUID()}.tmp`);
1667
- writePrivateFileDurably(temporaryPath, "committed\n");
1668
- fs.renameSync(temporaryPath, path.join(transactionPath, "committed"));
1669
- fsyncDirectory(transactionPath);
1670
- }
1671
- function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
1672
- if (mount.hydrateDeletionMode !== "git" || !sourceStatus.isDirectory() || !lstatIfExists(path.join(sourcePath, ".git"))) return null;
1673
- return [...inspectWorkingTree(sourcePath, "git").keys()];
1674
- }
1675
- function fsyncHydratedWorkspaceMount(mount, scope) {
1676
- assertManagedDirectoryPath({
1677
- trustedRoot: mount.durabilityRootPath,
1678
- candidate: mount.sourcePath,
1679
- label: `Workspace mount ${mount.id} source`
1680
- });
1681
- const syncEntry = (entryPath, root) => {
1682
- const status = fs.lstatSync(entryPath);
1683
- if (status.isSymbolicLink()) return;
1684
- if (status.isDirectory()) {
1685
- for (const name of fs.readdirSync(entryPath)) {
1686
- if (root && name === ".git") continue;
1687
- syncEntry(path.join(entryPath, name), false);
1688
- }
1689
- fsyncDirectory(entryPath);
1690
- return;
1691
- }
1692
- if (!status.isFile()) return;
1693
- const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY);
1694
- try {
1695
- fs.fsyncSync(descriptor);
1696
- } finally {
1697
- fs.closeSync(descriptor);
1698
- }
1699
- };
1700
- const sourcePath = path.resolve(mount.sourcePath);
1701
- const sourceStatus = lstatIfExists(sourcePath);
1702
- if (sourceStatus) {
1703
- const paths = scope ?? gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus);
1704
- if (paths) {
1705
- if (!sourceStatus.isDirectory()) throw new Error(`Workspace mount ${mount.id} source is not a regular directory: ${sourcePath}`);
1706
- fsyncWorkingTreePaths(sourcePath, paths);
1707
- } else {
1708
- syncEntry(sourcePath, true);
1709
- }
1710
- }
1711
- const durabilityRoot = path.resolve(mount.durabilityRootPath);
1712
- let boundaryParent = path.dirname(path.resolve(mount.sourcePath));
1713
- while (true) {
1714
- if (fs.existsSync(boundaryParent)) {
1715
- const boundaryStatus = fs.lstatSync(boundaryParent);
1716
- if (!boundaryStatus.isDirectory() || boundaryStatus.isSymbolicLink()) {
1717
- throw new Error(`Workspace mount durability boundary is not a regular directory: ${boundaryParent}`);
1718
- }
1719
- fsyncDirectory(boundaryParent);
1720
- }
1721
- if (boundaryParent === durabilityRoot) break;
1722
- const nextParent = path.dirname(boundaryParent);
1723
- if (nextParent === boundaryParent) {
1724
- throw new Error(`Workspace mount ${mount.sourcePath} does not descend from durability root ${durabilityRoot}`);
1725
- }
1726
- boundaryParent = nextParent;
1727
- }
1728
- }
1729
- function fsyncHydratedWorkspaceMounts(mounts, scopes = /* @__PURE__ */ new Map()) {
1730
- for (const mount of mounts) fsyncHydratedWorkspaceMount(mount, scopes.get(mount.id));
1731
- }
1732
- function workspaceIsShallow(workspacePath) {
1733
- return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
1734
- }
1735
- function workspaceIsClean(workspacePath) {
1736
- const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
1737
- return status.exitCode === 0 && status.stdout === "";
1738
- }
1739
- function validateMounts(workspacePath, mounts) {
1740
- const ids = /* @__PURE__ */ new Set();
1741
- const paths = [];
1742
- for (const mount of mounts) {
1743
- if (!mount.id || ids.has(mount.id)) throw new Error(`Workspace mount id must be unique: ${mount.id}`);
1744
- if (!mount.hydrationIncarnationKey || mount.hydrationIncarnationKey.includes("\0")) {
1745
- throw new Error(`Workspace mount hydration incarnation is invalid: ${mount.id}`);
1746
- }
1747
- ids.add(mount.id);
1748
- const sourcePath = path.resolve(mount.sourcePath);
1749
- const durabilityRoot = path.resolve(mount.durabilityRootPath);
1750
- const durabilityRelative = path.relative(durabilityRoot, sourcePath);
1751
- const durabilityRootStatus = fs.lstatSync(durabilityRoot);
1752
- if (!durabilityRootStatus.isDirectory() || durabilityRootStatus.isSymbolicLink() || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.isAbsolute(durabilityRelative)) {
1753
- throw new Error(`Workspace mount source escapes its durability root: ${mount.sourcePath}`);
1754
- }
1755
- assertManagedDirectoryPath({
1756
- trustedRoot: durabilityRoot,
1757
- candidate: sourcePath,
1758
- label: `Workspace mount ${mount.id} source`
1759
- });
1760
- const normalized = mount.workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
1761
- if (!normalized || normalized === "." || normalized.split("/").some((segment) => segment === ".." || segment === ".git")) {
1762
- throw new Error(`Invalid workspace mount path: ${mount.workspaceRelativePath}`);
1763
- }
1764
- const targetPath = path.resolve(workspacePath, ...normalized.split("/"));
1765
- const relative = path.relative(path.resolve(workspacePath), targetPath);
1766
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
1767
- throw new Error(`Workspace mount escapes the workspace clone: ${mount.workspaceRelativePath}`);
1768
- }
1769
- paths.push(normalized);
1770
- }
1771
- paths.sort();
1772
- for (let index = 0; index < paths.length; index += 1) {
1773
- const current = paths[index];
1774
- const overlap = paths.slice(index + 1).find((candidate) => candidate.startsWith(`${current}/`));
1775
- if (overlap) throw new Error(`Workspace mounts overlap: ${current} and ${overlap}`);
1776
- }
1777
- }
1778
- async function configureWorkspaceRepository(input) {
1779
- if (await tryGitAsync(input.workspacePath, ["remote", "get-url", "origin"])) {
1780
- await gitAsync(input.workspacePath, ["remote", "set-url", "origin", input.remoteUrl], "configure workspace origin");
1781
- } else {
1782
- await gitAsync(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
1783
- }
1784
- await tryGitAsync(input.workspacePath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
1785
- await gitAsync(
1786
- input.workspacePath,
1787
- ["config", "--local", "--replace-all", "credential.helper", ""],
1788
- "reset workspace credential helpers"
1789
- );
1790
- await gitAsync(
1791
- input.workspacePath,
1792
- ["config", "--local", "credential.useHttpPath", "false"],
1793
- "configure workspace credential path matching"
1794
- );
1795
- if (input.credentialUsername) {
1796
- await gitAsync(
1797
- input.workspacePath,
1798
- ["config", "--local", "--replace-all", gitCredentialUsernameConfigKey(input.remoteUrl), input.credentialUsername],
1799
- "configure workspace credential username"
1800
- );
1801
- }
1802
- if (input.credentialHelper) {
1803
- await gitAsync(
1804
- input.workspacePath,
1805
- ["config", "--local", "--add", "credential.helper", input.credentialHelper],
1806
- "configure workspace credential helper"
1807
- );
1808
- }
1809
- const name = input.gitIdentity.name.trim();
1810
- const email = input.gitIdentity.email.trim();
1811
- if (!name || !email) throw new Error("Workspace Git identity must include name and email");
1812
- await gitAsync(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
1813
- await gitAsync(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
1814
- await gitAsync(
1815
- input.workspacePath,
1816
- ["config", "--local", "core.fsync", "committed,reference"],
1817
- "configure durable workspace commits and references"
1818
- );
1819
- await gitAsync(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
1820
- const attributesPath = path.join(input.workspacePath, ".git", "info", "attributes");
1821
- const attributes = [
1822
- "# r5d workspace clone: checkout bytes are carried verbatim; no conversion or filter may change what projection reads or hydration writes.",
1823
- "* -text -eol -filter -ident -working-tree-encoding",
1824
- ""
1825
- ].join("\n");
1826
- let currentAttributes = null;
1827
- try {
1828
- currentAttributes = await fs.promises.readFile(attributesPath, "utf8");
1829
- } catch (error) {
1830
- if (error.code !== "ENOENT") throw error;
1831
- }
1832
- if (currentAttributes !== attributes) {
1833
- await fs.promises.mkdir(path.dirname(attributesPath), { recursive: true });
1834
- await fs.promises.writeFile(attributesPath, attributes, "utf8");
1835
- }
1836
- }
1837
- async function configureExistingWorkspaceGitForRemediation(input) {
1838
- const workspacePath = path.resolve(input.workspacePath);
1839
- const workspaceStatus = lstatIfExists(workspacePath);
1840
- const gitStatus = lstatIfExists(path.join(workspacePath, ".git"));
1841
- if (!workspaceStatus?.isDirectory() || workspaceStatus.isSymbolicLink() || !gitStatus?.isDirectory() || gitStatus.isSymbolicLink()) {
1842
- throw new Error("Active workspace remediation requires a regular existing canonical synchronization checkout");
1843
- }
1844
- await configureWorkspaceRepository({ ...input, workspacePath });
1845
- }
1846
- async function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
1847
- const result = await gitResultAsync(workspacePath, [
1848
- ...gitTransportSecurityArgs(remoteUrl, credentialHelper, credentialUsername),
1849
- "fetch",
1850
- "--no-recurse-submodules",
1851
- "--prune",
1852
- "--update-shallow",
1853
- "origin",
1854
- `+refs/heads/${WORKSPACE_GIT_BRANCH}:refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`
1855
- ]);
1856
- if (result.exitCode !== 0) {
1857
- const detail = `${result.stderr}
1858
- ${result.stdout}`;
1859
- if (!/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(detail)) {
1860
- throw new Error(`Fetch workspace main: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
1861
- }
1862
- return null;
1863
- }
1864
- return revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
1865
- }
1866
- async function resolveWorkspaceRemoteHead(workspacePath, input) {
1867
- const advertised = input.advertisedRemoteHead;
1868
- if (advertised && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(advertised)) {
1869
- const tracked = await revParseAsync(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
1870
- if (tracked === advertised && await tryGitAsync(workspacePath, ["cat-file", "-e", `${tracked}^{commit}`])) {
1871
- return { head: tracked, fetched: false };
1872
- }
1873
- }
1874
- return {
1875
- head: await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername),
1876
- fetched: true
1877
- };
1878
- }
1879
- async function ensureWorkspaceGitClone(input) {
1880
- const workspacePath = path.resolve(input.workspacePath);
1881
- const workspaceParent = path.dirname(workspacePath);
1882
- const parentStatus = fs.lstatSync(workspaceParent);
1883
- if (!parentStatus.isDirectory() || parentStatus.isSymbolicLink()) {
1884
- throw new Error(`Workspace clone parent is not a regular directory: ${workspaceParent}`);
1885
- }
1886
- const workspaceStatus = lstatIfExists(workspacePath);
1887
- if (workspaceStatus) {
1888
- if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink()) {
1889
- throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
1890
- }
1891
- }
1892
- const gitPath = path.join(workspacePath, ".git");
1893
- const gitStatus = lstatIfExists(gitPath);
1894
- if (gitStatus) {
1895
- if (!gitStatus.isDirectory() || gitStatus.isSymbolicLink()) {
1896
- throw new Error(`Workspace clone Git metadata is not a regular directory: ${gitPath}`);
1897
- }
1898
- }
1899
- if (!fs.existsSync(gitPath)) {
1900
- fs.rmSync(workspacePath, { recursive: true, force: true });
1901
- fs.mkdirSync(path.dirname(workspacePath), { recursive: true });
1902
- const cloneCommand = workspaceCloneCommandArgs(
1903
- ["clone", "--no-recurse-submodules", "--branch", WORKSPACE_GIT_BRANCH, input.remoteUrl, workspacePath],
1904
- input.remoteUrl,
1905
- input.credentialHelper,
1906
- input.credentialUsername
1907
- );
1908
- const clone = Bun.spawn(cloneCommand, {
1909
- stdin: "ignore",
1910
- stdout: "pipe",
1911
- stderr: "pipe",
1912
- env: workerGitProcessEnvironment()
1913
- });
1914
- const [, , cloneExitCode] = await Promise.all([new Response(clone.stdout).text(), new Response(clone.stderr).text(), clone.exited]);
1915
- if (cloneExitCode !== 0) {
1916
- fs.rmSync(workspacePath, { recursive: true, force: true });
1917
- fs.mkdirSync(workspacePath, { recursive: true });
1918
- git(workspacePath, ["init", `--initial-branch=${WORKSPACE_GIT_BRANCH}`], "initialize workspace clone");
1919
- }
1920
- }
1921
- await configureWorkspaceRepository({ ...input, workspacePath });
1922
- await workspaceFilesystemExecutor().run("checkout_durability_recovery", {
1923
- workspacePath,
1924
- preserveResolutionInProgress: input.preserveResolutionInProgress === true
1925
- });
1926
- const previousRemoteHead = await revParseAsync(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
1927
- const localHeadBeforeFetch = await revParseAsync(workspacePath, "HEAD");
1928
- if (!await revParseAsync(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && await tryGitAsync(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
1929
- await updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
1930
- }
1931
- const resolvedRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
1932
- const remoteHead = resolvedRemoteHead.head;
1933
- let localHead = await revParseAsync(workspacePath, "HEAD");
1934
- if (!localHead && remoteHead) {
1935
- await runWorkspaceCheckoutTransition(workspacePath, async () => {
1936
- await gitAsync(
1937
- workspacePath,
1938
- ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
1939
- "check out workspace main"
1940
- );
1941
- });
1942
- localHead = remoteHead;
1943
- }
1944
- if (localHead && remoteHead && localHead === remoteHead) {
1945
- await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
1946
- } else if (localHead && remoteHead && workspaceIsShallow(workspacePath) && !tryGit(workspacePath, ["merge-base", localHead, remoteHead]) && revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) === localHead && workspaceIsClean(workspacePath)) {
1947
- await runWorkspaceCheckoutTransition(workspacePath, async () => {
1948
- await gitAsync(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
1949
- });
1950
- await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
1951
- localHead = remoteHead;
1952
- }
1953
- return { localHead, remoteHead, remoteHeadFetched: resolvedRemoteHead.fetched };
1954
- }
1955
- function activeMounts(mounts) {
1956
- const active = [];
1957
- const tombstones = [];
1958
- const skipped = [];
1959
- for (const mount of mounts) {
1960
- if (mountBusy(mount)) skipped.push(mount);
1961
- else if (fs.existsSync(mount.sourcePath)) active.push(mount);
1962
- else if (mount.deleteWhenSourceMissing) tombstones.push(mount);
1963
- else skipped.push(mount);
1964
- }
1965
- return { active, tombstones, skipped };
1966
- }
1967
- function mirrorMountsToWorkspace(workspacePath, mounts, projectedFiles) {
1968
- const projected = [];
1969
- for (const mount of mounts) {
1970
- assertManagedDirectoryPath({
1971
- trustedRoot: mount.durabilityRootPath,
1972
- candidate: mount.sourcePath,
1973
- label: `Workspace mount ${mount.id} source`
1974
- });
1975
- assertManagedDirectoryPath({
1976
- trustedRoot: workspacePath,
1977
- candidate: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1978
- label: `Workspace mount ${mount.id} outer target`
1979
- });
1980
- const mirrored = mirrorWorkingTree({
1981
- sourceRoot: mount.sourcePath,
1982
- targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
1983
- sourceMode: mount.sourceMode,
1984
- deletionMode: "all",
1985
- ...projectedFiles ? { projected: projectedFiles } : {}
1986
- });
1987
- projected.push({ mount, gitlinks: mirrored.gitlinks });
1988
- }
1989
- return projected;
1990
- }
1991
- async function stageWorkspaceGitlinks(workspacePath, projected) {
1992
- if (projected.length === 0) return;
1993
- const mountPaths = projected.map(({ mount }) => normalizedWorkspaceMountPath(mount.workspaceRelativePath));
1994
- const desired = /* @__PURE__ */ new Map();
1995
- projected.forEach(({ gitlinks }, index) => {
1996
- for (const gitlink of gitlinks) desired.set(`${mountPaths[index]}/${gitlink.relativePath}`, gitlink.objectId);
1997
- });
1998
- const desiredRoots = new Set(desired.keys());
1999
- const listed = await gitOutputAsync(
2000
- workspacePath,
2001
- ["ls-files", "-z", "--stage", "--", ...mountPaths.map((mountPath) => `:(literal)${mountPath}`)],
2002
- "inspect staged workspace mount entries"
2003
- );
2004
- const feed = [];
2005
- const current = /* @__PURE__ */ new Map();
2006
- const drop = (objectId, relativePath) => {
2007
- feed.push(Buffer.concat([Buffer.from(`0 ${"0".repeat(objectId.length)} `), Buffer.from(relativePath), Buffer.from([0])]));
2008
- };
2009
- for (const record of listed.toString().split("\0")) {
2010
- const match = /^([0-7]{6}) ([0-9a-f]{40,64}) [0-3]\t(.+)$/su.exec(record);
2011
- if (!match) continue;
2012
- const mode = match[1];
2013
- const objectId = match[2];
2014
- const relativePath = match[3];
2015
- if (mode === "160000") {
2016
- current.set(relativePath, objectId);
2017
- if (!desired.has(relativePath)) drop(objectId, relativePath);
2018
- continue;
2019
- }
2020
- if (desired.has(relativePath) || hasAncestorIn(relativePath, desiredRoots)) drop(objectId, relativePath);
2021
- }
2022
- for (const [relativePath, objectId] of desired) {
2023
- if (current.get(relativePath) === objectId) continue;
2024
- feed.push(Buffer.concat([Buffer.from(`160000 ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]));
2025
- }
2026
- if (feed.length === 0) return;
2027
- await gitOutputAsync(workspacePath, ["update-index", "-z", "--index-info"], "stage workspace gitlinks", Buffer.concat(feed));
2028
- }
2029
- function hasAncestorIn(relativePath, ancestors) {
2030
- let parent = path.posix.dirname(relativePath);
2031
- while (parent !== ".") {
2032
- if (ancestors.has(parent)) return true;
2033
- parent = path.posix.dirname(parent);
2034
- }
2035
- return false;
2036
- }
2037
- function removeWorkspaceMounts(workspacePath, mounts) {
2038
- for (const mount of mounts) {
2039
- const targetPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
2040
- assertManagedDirectoryPath({
2041
- trustedRoot: workspacePath,
2042
- candidate: targetPath,
2043
- label: `Workspace mount ${mount.id} outer target`
2044
- });
2045
- const relative = path.relative(path.resolve(workspacePath), path.resolve(targetPath));
2046
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
2047
- throw new Error(`Workspace mount escapes the workspace clone: ${mount.workspaceRelativePath}`);
2048
- }
2049
- fs.rmSync(targetPath, { recursive: true, force: true });
2050
- }
2051
- }
2052
- function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
2053
- const head = revParse(workspacePath, "HEAD");
2054
- for (const mount of mounts) {
2055
- const normalized = normalizedWorkspaceMountPath(mount.workspaceRelativePath);
2056
- const targetPath = path.join(workspacePath, ...normalized.split("/"));
2057
- assertManagedDirectoryPath({
2058
- trustedRoot: workspacePath,
2059
- candidate: targetPath,
2060
- label: `Deferred workspace mount ${mount.id} outer target`
2061
- });
2062
- fs.rmSync(targetPath, { recursive: true, force: true });
2063
- if (head && tryGit(workspacePath, ["cat-file", "-e", `${head}:${normalized}`])) {
2064
- git(
2065
- workspacePath,
2066
- ["checkout", "--no-recurse-submodules", head, "--", `:(literal)${normalized}`],
2067
- `restore deferred workspace mount ${mount.id} from its integration basis`
2068
- );
2069
- }
2070
- }
2071
- }
2072
- function workspaceMountOuterPath(workspacePath, mount) {
2073
- return path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
2074
- }
2075
- function outerSubtreeGitlinks(workspacePath, mount) {
2076
- const gitlinks = /* @__PURE__ */ new Map();
2077
- const head = revParse(workspacePath, "HEAD");
2078
- if (!head) return gitlinks;
2079
- const subtree = `${head}:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`;
2080
- const listed = Bun.spawnSync(gitCommandArgs(["ls-tree", "-r", "-z", subtree]), {
2081
- cwd: workspacePath,
2082
- stdout: "pipe",
2083
- stderr: "pipe",
2084
- env: workerGitProcessEnvironment()
2085
- });
2086
- if (listed.exitCode !== 0) return gitlinks;
2087
- for (const record of Buffer.from(listed.stdout).toString().split("\0")) {
2088
- const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record);
2089
- if (match) gitlinks.set(match[2], match[1]);
2090
- }
2091
- return gitlinks;
2092
- }
2093
- function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
2094
- const hydratedMountIds = [];
2095
- const skippedMountIds = [];
2096
- const mergeSkips = [];
2097
- const rollbackScopes = /* @__PURE__ */ new Map();
2098
- const ledgerEntries = {};
2099
- for (const mount of mounts) {
2100
- if (!options.ignoreBusy && mountBusy(mount)) {
2101
- skippedMountIds.push(mount.id);
2102
- continue;
2103
- }
2104
- const mergePlan = options.mergePlans?.get(mount.id);
2105
- if (mergePlan) {
2106
- const journal = { entries: [] };
2107
- const runtimeJournal = { journal, snapshotPath: mergePlan.snapshotPath };
2108
- options.journals?.set(mount.id, runtimeJournal);
2109
- try {
2110
- applyWorkingTreeMirror(mergePlan.prepared, {
2111
- selection: mergePlan.decision.selection,
2112
- guards: {
2113
- expectations: mergePlan.decision.expectations,
2114
- overrides: mergePlan.decision.overrides,
2115
- preBlobs: mergePlan.decision.preBlobs,
2116
- journal
2117
- }
2118
- });
2119
- } catch (error) {
2120
- if (!(error instanceof WorkingTreeTargetChangedError)) throw error;
2121
- const rolledBack = rollbackWorkingTreeMirrorJournal({
2122
- journal,
2123
- targetRoot: mount.sourcePath,
2124
- snapshotRoot: mergePlan.snapshotPath
2125
- });
2126
- runtimeJournal.rolledBack = rolledBack;
2127
- const retainedPaths = [
2128
- ...new Set([...error.retained, ...rolledBack.retained ?? []].map(({ path: retainedPath }) => retainedPath))
2129
- ].sort();
2130
- if (retainedPaths.length > 0) {
2131
- process.stderr.write(
2132
- `[r5d-worker] hydration preserved displaced writer entries for mount ${mount.id}: ${retainedPaths.join(", ")}
2133
- `
2134
- );
2135
- }
2136
- options.onMergeRollback?.(mount, rolledBack);
2137
- mergeSkips.push({
2138
- mountId: mount.id,
2139
- reason: "hydration_target_changed",
2140
- paths: [.../* @__PURE__ */ new Set([error.relativePath, ...rolledBack.abandoned])].sort(),
2141
- ...retainedPaths.length > 0 ? { retainedPaths } : {}
2142
- });
2143
- rollbackScopes.set(mount.id, rolledBack.paths);
2144
- continue;
2145
- }
2146
- hydratedMountIds.push(mount.id);
2147
- storeWorkspaceBlobs(workspacePath, mergePlan.decision.blobsToStore);
2148
- const left = /* @__PURE__ */ new Map();
2149
- for (const entry of journal.entries) left.set(entry.relativePath, entry.after);
2150
- const entries = {};
2151
- for (const [relativePath, preBlob] of mergePlan.decision.preBlobs) {
2152
- const postBlob = mergePlan.decision.postBlobs.get(relativePath) ?? null;
2153
- let after = left.get(relativePath);
2154
- if (after === void 0) {
2155
- const current = inspectWorkingTreePath(mount.sourcePath, relativePath);
2156
- after = current.kind === "entry" ? current.stat : null;
2157
- }
2158
- entries[relativePath] = { preBlob, postBlob, stat: after && after.isFile() ? projectedFileEntry(after) : null };
2159
- }
2160
- ledgerEntries[mount.id] = entries;
2161
- continue;
2162
- }
2163
- const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
2164
- assertManagedDirectoryPath({
2165
- trustedRoot: workspacePath,
2166
- candidate: sourceRoot,
2167
- label: `Workspace mount ${mount.id} outer source`
2168
- });
2169
- assertManagedDirectoryPath({
2170
- trustedRoot: mount.durabilityRootPath,
2171
- candidate: mount.sourcePath,
2172
- label: `Workspace mount ${mount.id} hydration target`
2173
- });
2174
- if (!fs.existsSync(sourceRoot)) {
2175
- if (mount.hydrateDeletionMode === "all") {
2176
- fs.rmSync(mount.sourcePath, { recursive: true, force: true });
2177
- } else if (fs.existsSync(mount.sourcePath)) {
2178
- const emptyRoot = fs.mkdtempSync(path.join(path.dirname(workspacePath), ".r5d-empty-mount-"));
2179
- try {
2180
- mirrorWorkingTree({
2181
- sourceRoot: emptyRoot,
2182
- targetRoot: mount.sourcePath,
2183
- sourceMode: "all",
2184
- deletionMode: "git"
2185
- });
2186
- } finally {
2187
- fs.rmSync(emptyRoot, { recursive: true, force: true });
2188
- }
2189
- }
2190
- hydratedMountIds.push(mount.id);
2191
- continue;
2192
- }
2193
- const targetExists = fs.existsSync(mount.sourcePath);
2194
- mirrorWorkingTree({
2195
- sourceRoot,
2196
- targetRoot: mount.sourcePath,
2197
- sourceMode: "all",
2198
- // The outer worktree shows a submodule as an empty directory; the tree
2199
- // carries its pointer, which a Git checkout applies to its index (D4).
2200
- sourceGitlinks: outerSubtreeGitlinks(workspacePath, mount),
2201
- // A missing checkout has no Git index to define git-only deletions. Its
2202
- // newly created directory is empty, so all-mode is equivalent and lets
2203
- // bootstrap hydration preserve the fetched tree before Git initializes.
2204
- deletionMode: targetExists ? mount.hydrateDeletionMode : "all"
2205
- });
2206
- hydratedMountIds.push(mount.id);
2207
- }
2208
- return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort(), mergeSkips, rollbackScopes, ledgerEntries };
2209
- }
2210
- async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
2211
- const candidates = mounts.flatMap((mount) => {
2212
- const basisHead = workspaceHydrationReceiptMountBasisHead(receipt, mount);
2213
- return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
2214
- });
2215
- if (candidates.length === 0) return /* @__PURE__ */ new Set();
2216
- const objects = await resolveGitObjectsAsync(
2217
- workspacePath,
2218
- candidates.flatMap(({ mount, basisHead }) => [
2219
- workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
2220
- workspaceTreeObjectName(head, mount.workspaceRelativePath)
2221
- ])
2222
- );
2223
- const unchanged = /* @__PURE__ */ new Set();
2224
- for (const { mount, basisHead } of candidates) {
2225
- const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath))?.objectId ?? null;
2226
- const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath))?.objectId ?? null;
2227
- if (before === after) unchanged.add(mount.id);
2228
- }
2229
- return unchanged;
2230
- }
2231
- async function hydrateWorkspaceGitMountsTransactionally(input) {
2232
- const workspacePath = path.resolve(input.workspacePath);
2233
- const durabilityMounts = input.durabilityMounts ?? input.mounts;
2234
- const receiptMounts = input.receiptMounts ?? durabilityMounts;
2235
- const requiredMounts = input.requiredMounts ?? input.mounts;
2236
- const carryMounts = input.carryMounts ?? [];
2237
- if (carryMounts.length > 0 && !input.recordCurrentHead) {
2238
- throw new Error("Workspace hydration can only carry unchanged mounts while recording the current head");
2239
- }
2240
- const candidateMounts = [
2241
- ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts, ...carryMounts].map((mount) => [mount.id, mount])).values()
2242
- ];
2243
- if (input.recordCurrentHead && !await workspaceCheckoutDurabilityIsCurrentAsync(workspacePath)) {
2244
- throw new Error("Workspace checkout is not durably materialized at its current HEAD");
2245
- }
2246
- const targetHead = input.recordCurrentHead ? await revParseAsync(workspacePath, "HEAD") : null;
2247
- const receiptBeforeTransaction = targetHead ? await readHydratedWorkspaceReceiptAsync(workspacePath) : null;
2248
- const computeSelection = () => {
2249
- const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mountBusy(mount)).map(({ id }) => id));
2250
- const requestedIds = new Set(input.mounts.map(({ id }) => id));
2251
- const requiredIds = new Set(requiredMounts.map(({ id }) => id));
2252
- const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
2253
- const advancedDurabilityMounts = durabilityMounts.filter(
2254
- ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
2255
- );
2256
- const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
2257
- const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
2258
- return {
2259
- busyMountIds,
2260
- hydrationMounts,
2261
- advancedDurabilityMounts,
2262
- skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
2263
- targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts, ...carriedMounts], receiptMounts) : null
2264
- };
2265
- };
2266
- const noopCandidate = computeSelection();
2267
- 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)) {
2268
- return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds, mergeSkips: [], hydratedEntryPaths: {} };
2269
- }
2270
- const hooks = input.hydrationHooks;
2271
- const context = {
2272
- transactionId: crypto.randomUUID(),
2273
- workspacePath,
2274
- mountIds: [...new Set([...noopCandidate.hydrationMounts, ...noopCandidate.advancedDurabilityMounts].map(({ id }) => id))].sort()
2275
- };
2276
- if (hooks) await hooks.beforeTransaction(context);
2277
- try {
2278
- const selection = computeSelection();
2279
- const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
2280
- const heldMountIds = [...new Set([...hydrationMounts, ...advancedDurabilityMounts].map(({ id }) => id))];
2281
- const releaseHold = hooks?.holdMounts(heldMountIds, `hydration transaction ${context.transactionId}`);
2282
- try {
2283
- const mergeBases = {};
2284
- for (const mount of hydrationMounts) {
2285
- const basis = input.mergeBases?.get(mount.id);
2286
- if (basis) mergeBases[mount.id] = basis;
2287
- }
2288
- const liveBefore = new Set(hydrationMounts.filter((mount) => mergeBases[mount.id] && mount.processLive?.()).map(({ id }) => id));
2289
- const hydration = await workspaceFilesystemExecutor().run("hydration_transaction", {
2290
- workspacePath,
2291
- hydrationMounts: hydrationMounts.map(workspaceGitMountData),
2292
- advancedDurabilityMounts: advancedDurabilityMounts.map(workspaceGitMountData),
2293
- targetReceipt,
2294
- ...Object.keys(mergeBases).length > 0 ? { mergeBases } : {}
2295
- });
2296
- const ledger = hydrationPreBlobLedger(workspacePath);
2297
- const hydratedEntryPaths = {};
2298
- for (const mount of hydrationMounts) {
2299
- if (!hydration.hydratedMountIds.includes(mount.id)) continue;
2300
- const entries = mergeBases[mount.id] ? hydration.hydratedEntries[mount.id] : void 0;
2301
- if (!entries) {
2302
- ledger.clearMount(mount);
2303
- continue;
2304
- }
2305
- const processLiveAcrossHydration = liveBefore.has(mount.id) || mount.processLive?.() === true;
2306
- hydratedEntryPaths[mount.id] = Object.keys(entries).sort();
2307
- ledger.record(
2308
- mount,
2309
- Object.fromEntries(Object.entries(entries).map(([relativePath, entry]) => [relativePath, { ...entry, processLiveAcrossHydration }])),
2310
- { replacePreBlobFor: input.ledgerMergedPaths?.get(mount.id) ?? [] }
2311
- );
2312
- }
2313
- return {
2314
- hydratedMountIds: hydration.hydratedMountIds,
2315
- skippedMountIds: [.../* @__PURE__ */ new Set([...selection.skippedMountIds, ...hydration.skippedMounts.map(({ mountId }) => mountId)])].sort(),
2316
- mergeSkips: hydration.skippedMounts,
2317
- hydratedEntryPaths
2318
- };
2319
- } finally {
2320
- releaseHold?.();
2321
- }
2322
- } finally {
2323
- hooks?.afterTransaction(context);
2324
- }
2325
- }
2326
- function runHydrationTransactionJob(input) {
2327
- const workspacePath = path.resolve(input.workspacePath);
2328
- const { hydrationMounts, advancedDurabilityMounts } = input;
2329
- const begun = beginHydrationTransaction(workspacePath, hydrationMounts, input.targetReceipt, input.mergeBases);
2330
- const { durabilityScopes, mergePlans, skippedMounts } = begun;
2331
- let manifest = begun.manifest;
2332
- let targetReceipt = begun.targetReceipt;
2333
- const skippedIds = new Set(skippedMounts.map(({ mountId }) => mountId));
2334
- const journals = /* @__PURE__ */ new Map();
2335
- const excludeRolledBackMount = (mount, rollback) => {
2336
- const settledMount = manifest.mounts.find(({ id }) => id === mount.id);
2337
- if (!settledMount) return;
2338
- fsyncHydratedWorkspaceMount(mount, rollback.paths);
2339
- fsyncHydrationRetainedEvidence(mount);
2340
- if (rollback.retained?.length) {
2341
- process.stderr.write(
2342
- `[r5d-worker] hydration rollback retained writer entries for mount ${mount.id}: ${rollback.retained.map(({ path: retainedPath }) => retainedPath).join(", ")}
2343
- `
2344
- );
2345
- }
2346
- const nextReceipt = revertReceiptMounts(targetReceipt, manifest.receiptBefore, /* @__PURE__ */ new Set([mount.id]));
2347
- const nextManifest = {
2348
- ...manifest,
2349
- targetReceipt: nextReceipt,
2350
- mounts: manifest.mounts.filter(({ id }) => id !== mount.id),
2351
- preservedMounts: [...manifest.preservedMounts ?? [], settledMount]
2352
- };
2353
- rewriteHydrationTransactionManifest(workspacePath, nextManifest);
2354
- manifest = nextManifest;
2355
- targetReceipt = nextReceipt;
2356
- };
2357
- try {
2358
- const activeMounts2 = hydrationMounts.filter(({ id }) => !skippedIds.has(id));
2359
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, activeMounts2, {
2360
- ignoreBusy: true,
2361
- mergePlans,
2362
- journals,
2363
- onMergeRollback: excludeRolledBackMount
2364
- });
2365
- if (hydration.mergeSkips.length > 0) {
2366
- for (const skip of hydration.mergeSkips) {
2367
- skippedMounts.push(skip);
2368
- skippedIds.add(skip.mountId);
2369
- }
2370
- }
2371
- const expectedHydratedIds = activeMounts2.map(({ id }) => id).filter((id) => !skippedIds.has(id)).sort();
2372
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
2373
- throw new Error("Workspace hydration did not include every required mount");
2374
- }
2375
- fsyncHydratedWorkspaceMounts(
2376
- advancedDurabilityMounts.filter(({ id }) => !skippedIds.has(id)),
2377
- durabilityScopes
2378
- );
2379
- markHydrationTransactionDurable(workspacePath);
2380
- if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
2381
- removeHydrationTransaction(workspacePath);
2382
- return { hydratedMountIds: hydration.hydratedMountIds, skippedMounts, hydratedEntries: hydration.ledgerEntries };
2383
- } catch (error) {
2384
- const transactionPath = hydrationTransactionPath(workspacePath);
2385
- if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
2386
- throw error;
2387
- }
2388
- try {
2389
- for (const [mountId, mergePlan] of mergePlans) {
2390
- const mount = hydrationMounts.find(({ id }) => id === mountId);
2391
- if (!mount || !manifest.mounts.some(({ id }) => id === mountId)) continue;
2392
- const runtimeJournal = journals.get(mountId);
2393
- const rolledBack = runtimeJournal ? runtimeJournal.rolledBack ?? rollbackWorkingTreeMirrorJournal({
2394
- journal: runtimeJournal.journal,
2395
- targetRoot: mount.sourcePath,
2396
- snapshotRoot: mergePlan.snapshotPath
2397
- }) : { paths: [], abandoned: [] };
2398
- if (runtimeJournal) runtimeJournal.rolledBack = rolledBack;
2399
- excludeRolledBackMount(mount, rolledBack);
2400
- }
2401
- restoreHydrationTransaction(workspacePath, manifest);
2402
- removeHydrationTransaction(workspacePath);
2403
- } catch (restoreError) {
2404
- const pendingManifest = parseHydrationTransactionManifest(transactionPath);
2405
- if (pendingManifest.mounts.some(({ recovery }) => recovery === "guarded")) {
2406
- throw hydrationRecoveryRequiredError(
2407
- workspacePath,
2408
- pendingManifest,
2409
- new AggregateError([error, restoreError], "Guarded hydration rollback did not settle")
2410
- );
2411
- }
2412
- throw new AggregateError(
2413
- [error, restoreError],
2414
- `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
2415
- );
2416
- }
2417
- throw error;
2418
- }
2419
- }
2420
- function runHydrationRawJob(input) {
2421
- return {
2422
- hydratedMountIds: hydrateWorkspaceGitMountsRaw(path.resolve(input.workspacePath), input.mounts, { ignoreBusy: true }).hydratedMountIds
2423
- };
2424
- }
2425
- function runHydrationRecoveryJob(input) {
2426
- const workspacePath = path.resolve(input.workspacePath);
2427
- recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
2428
- recoverPendingHydrationTransaction(workspacePath);
2429
- repairWorkspaceBasisRefs(workspacePath, readHydratedWorkspaceReceipt(workspacePath));
2430
- return null;
2431
- }
2432
- async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
2433
- const resolvedWorkspacePath = path.resolve(workspacePath);
2434
- const gitDirectory = path.join(resolvedWorkspacePath, ".git");
2435
- if (!fs.existsSync(gitDirectory)) {
2436
- const skippedMountIds = options.ignoreBusy ? [] : mounts.filter((mount) => mountBusy(mount)).map(({ id }) => id).sort();
2437
- const skipped = new Set(skippedMountIds);
2438
- const selected = mounts.filter(({ id }) => !skipped.has(id));
2439
- const releaseHold = options.hydrationHooks?.holdMounts(
2440
- selected.map(({ id }) => id),
2441
- "pre-clone hydration"
2442
- );
2443
- try {
2444
- const hydration = await workspaceFilesystemExecutor().run("hydration_raw", {
2445
- workspacePath: resolvedWorkspacePath,
2446
- mounts: selected.map(workspaceGitMountData)
2447
- });
2448
- return { hydratedMountIds: hydration.hydratedMountIds, skippedMountIds };
2449
- } finally {
2450
- releaseHold?.();
2451
- }
2452
- }
2453
- await workspaceFilesystemExecutor().run("checkout_durability_recovery", {
2454
- workspacePath: resolvedWorkspacePath,
2455
- preserveResolutionInProgress: false
2456
- });
2457
- return hydrateWorkspaceGitMountsTransactionally({
2458
- workspacePath,
2459
- mounts,
2460
- ignoreBusy: options.ignoreBusy,
2461
- hydrationHooks: options.hydrationHooks
2462
- });
2463
- }
2464
- async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
2465
- workspacePath = path.resolve(workspacePath);
2466
- validateMounts(workspacePath, mounts);
2467
- await workspaceFilesystemExecutor().run("hydration_recovery", {
2468
- workspacePath,
2469
- preserveResolutionInProgress: options.preserveResolutionInProgress === true
2470
- });
2471
- const currentHead = await revParseAsync(workspacePath, "HEAD");
2472
- const currentReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
2473
- const preservedBases = await readPreservedHydrationBasesAsync(workspacePath);
2474
- if (!currentHead) return;
2475
- const outerHeadContainsMount = (mount) => tryGitAsync(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
2476
- const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
2477
- const uncoveredBasisMounts = configuredBasisMounts.filter(
2478
- (mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
2479
- );
2480
- if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
2481
- const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mountBusy(mount)).map(({ id }) => id));
2482
- const recoveryMounts = uncoveredBasisMounts.filter(
2483
- (mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id) && workspaceHydrationReceiptMountBasisHead(preservedBases, mount) !== workspaceHydrationReceiptMountBasisHead(currentReceipt, mount)
2484
- );
2485
- const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
2486
- const hydrationTargets = [];
2487
- for (const mount of idleBasislessMounts) {
2488
- if (mount.deleteWhenSourceMissing) continue;
2489
- if (mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) {
2490
- continue;
2491
- }
2492
- if (await outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true) hydrationTargets.push(mount);
2493
- }
2494
- const hydration = await hydrateWorkspaceGitMountsTransactionally({
2495
- workspacePath,
2496
- mounts: hydrationTargets,
2497
- durabilityMounts: idleBasislessMounts,
2498
- receiptMounts: configuredBasisMounts,
2499
- requiredMounts: hydrationTargets,
2500
- ignoreBusy: options.ignoreBusy,
2501
- recordCurrentHead: true,
2502
- hydrationHooks: options.hydrationHooks
2503
- });
2504
- void hydration;
2505
- }
2506
- async function resetWorkspaceGit(input) {
2507
- const workspacePath = path.resolve(input.workspacePath);
2508
- validateMounts(workspacePath, input.mounts);
2509
- const busyResetMountIds = input.mounts.filter((mount) => mountBusy(mount)).map(({ id }) => id);
2510
- if (busyResetMountIds.length > 0) {
2511
- throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
2512
- }
2513
- hydrationPreBlobLedger(workspacePath).clear();
2514
- const initial = await ensureWorkspaceGitClone({ ...input, workspacePath });
2515
- await workspaceFilesystemExecutor().run("hydration_recovery", { workspacePath, preserveResolutionInProgress: false });
2516
- const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
2517
- const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
2518
- const remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
2519
- if (remoteHead) {
2520
- await runWorkspaceCheckoutTransition(workspacePath, async () => {
2521
- await gitAsync(
2522
- workspacePath,
2523
- ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead],
2524
- "reset workspace main"
2525
- );
2526
- await gitAsync(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
2527
- });
2528
- } else if (revParse(workspacePath, "HEAD")) {
2529
- await runWorkspaceCheckoutTransition(workspacePath, async () => {
2530
- await gitAsync(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
2531
- await gitAsync(
2532
- workspacePath,
2533
- ["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })],
2534
- "record empty workspace reset"
2535
- );
2536
- });
2537
- }
2538
- const selected = activeMounts(input.mounts);
2539
- const requiredLiveMountsBeforeHydration = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
2540
- const bootstrapMounts = selected.skipped.filter((mount) => {
2541
- if (mount.deleteWhenSourceMissing || fs.existsSync(mount.sourcePath) || mountBusy(mount)) return false;
2542
- const outerPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
2543
- return fs.existsSync(outerPath);
2544
- });
2545
- const bootstrapMountIds = new Set(bootstrapMounts.map(({ id }) => id));
2546
- const durablyAbsentMounts = selected.skipped.filter((mount) => !bootstrapMountIds.has(mount.id) && mount.sourceMode === "all");
2547
- const uncoveredSkippedMounts = selected.skipped.filter((mount) => !bootstrapMountIds.has(mount.id) && mount.sourceMode !== "all");
2548
- const hydrationMounts = [...selected.active, ...bootstrapMounts];
2549
- const requiredLiveMounts = [
2550
- ...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
2551
- ];
2552
- const hydration = await hydrateWorkspaceGitMountsTransactionally({
2553
- workspacePath,
2554
- mounts: hydrationMounts,
2555
- durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
2556
- receiptMounts: input.mounts,
2557
- requiredMounts: requiredLiveMounts,
2558
- recordCurrentHead: true,
2559
- hydrationHooks: input.hydrationHooks
2560
- });
2561
- const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
2562
- const resetHead = revParse(workspacePath, "HEAD");
2563
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredLiveIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredLiveIds[index]) || resetHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), resetHead, input.mounts)) {
2564
- throw new Error("Workspace reset could not durably hydrate every live mount; remediation remains active");
2565
- }
2566
- return {
2567
- startingHead: initial.localHead,
2568
- localHead: revParse(workspacePath, "HEAD"),
2569
- remoteHead,
2570
- discardedPaths,
2571
- activeMountIds: hydration.hydratedMountIds,
2572
- skippedMountIds: [.../* @__PURE__ */ new Set([...uncoveredSkippedMounts.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
2573
- };
2574
- }
2575
- async function stagedPaths(workspacePath) {
2576
- return (await gitAsync(workspacePath, ["diff", "--cached", "--name-only", "--no-renames", "-z"], "inspect staged workspace paths")).split("\0").filter(Boolean).sort();
2577
- }
2578
- function emptyTreeHash(workspacePath) {
2579
- const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, "hash-object", "-t", "tree", "--stdin"], {
2580
- cwd: workspacePath,
2581
- stdin: Buffer.alloc(0),
2582
- stdout: "pipe",
2583
- stderr: "pipe",
2584
- env: workerGitProcessEnvironment()
2585
- });
2586
- if (result.exitCode !== 0)
2587
- throw new Error(`Create empty workspace tree: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
2588
- return result.stdout.toString().trim();
2589
- }
2590
- async function changedPaths(workspacePath, baseRevision, headRevision) {
2591
- const base = baseRevision ?? emptyTreeHash(workspacePath);
2592
- return (await gitAsync(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths")).split("\0").filter(Boolean).sort();
2593
- }
2594
- async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, pathspecs = []) {
2595
- const base = baseRevision ?? emptyTreeHash(workspacePath);
2596
- const subprocess = Bun.spawn(
2597
- [
2598
- "git",
2599
- ...NON_RECURSIVE_GIT_CONFIG,
2600
- "diff",
2601
- "--binary",
2602
- "--no-ext-diff",
2603
- base,
2604
- headRevision,
2605
- ...pathspecs.length > 0 ? ["--", ...pathspecs] : []
2606
- ],
2607
- {
2608
- cwd: workspacePath,
2609
- stdout: "pipe",
2610
- stderr: "pipe",
2611
- env: workerGitProcessEnvironment()
2612
- }
2613
- );
2614
- const stderrPromise = new Response(subprocess.stderr).text();
2615
- const reader = subprocess.stdout.getReader();
2616
- let total = 0;
2617
- while (true) {
2618
- const chunk = await reader.read();
2619
- if (chunk.done) break;
2620
- total += chunk.value.byteLength;
2621
- if (total > limit) {
2622
- subprocess.kill();
2623
- await subprocess.exited;
2624
- await stderrPromise;
2625
- return limit + 1;
2626
- }
2627
- }
2628
- const exitCode = await subprocess.exited;
2629
- const stderr = (await stderrPromise).trim();
2630
- if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
2631
- return total;
2632
- }
2633
- async function resolveGitObjectsAsync(workspacePath, names) {
2634
- const resolved = /* @__PURE__ */ new Map();
2635
- const unique = [...new Set(names)];
2636
- if (unique.length === 0) return resolved;
2637
- const subprocess = Bun.spawn(gitCommandArgs(["cat-file", "--batch-check"]), {
2638
- cwd: workspacePath,
2639
- stdin: Buffer.from(`${unique.join("\n")}
2640
- `),
2641
- stdout: "pipe",
2642
- stderr: "pipe",
2643
- env: workerGitProcessEnvironment()
2644
- });
2645
- const [stdout, stderr, exitCode] = await Promise.all([
2646
- new Response(subprocess.stdout).text(),
2647
- new Response(subprocess.stderr).text(),
2648
- subprocess.exited
2649
- ]);
2650
- if (exitCode !== 0) throw new Error(`Resolve workspace objects: ${stderr.trim() || `git exited ${exitCode}`}`);
2651
- const lines = stdout.split("\n").filter((line) => line.length > 0);
2652
- if (lines.length !== unique.length) {
2653
- throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
2654
- }
2655
- unique.forEach((name, index) => {
2656
- const match = /^([0-9a-f]{40,64}) (\S+) \d+$/.exec(lines[index]);
2657
- resolved.set(name, match ? { objectId: match[1], type: match[2] } : null);
2658
- });
2659
- return resolved;
2660
- }
2661
- function workspaceTreeObjectName(revision, workspaceRelativePath) {
2662
- return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
2663
- }
2664
- async function pathsReplacedByGitlinks(workspacePath, baseRevision, headRevision) {
2665
- const base = baseRevision ?? emptyTreeHash(workspacePath);
2666
- const raw = await gitOutputAsync(
2667
- workspacePath,
2668
- ["diff", "--raw", "--no-renames", "-z", base, headRevision],
2669
- "inspect workspace gitlink changes"
2670
- );
2671
- const records = raw.toString().split("\0");
2672
- const addedGitlinks = [];
2673
- for (let index = 0; index + 1 < records.length; index += 2) {
2674
- if (/^:[0-7]{6} 160000 \S+ \S+ A$/u.test(records[index])) addedGitlinks.push(records[index + 1]);
2675
- }
2676
- if (addedGitlinks.length === 0) return [];
2677
- const objects = await resolveGitObjectsAsync(
2678
- workspacePath,
2679
- addedGitlinks.map((relativePath) => `${base}:${relativePath}`)
2680
- );
2681
- return addedGitlinks.filter((relativePath) => objects.get(`${base}:${relativePath}`)?.type === "tree").sort();
2682
- }
2683
- function gitlinkMigrationPathspecs(paths) {
2684
- return paths.map((relativePath) => `:(exclude,literal)${relativePath}`);
2685
- }
2686
- async function measureInitialPublicationDiffSize(input) {
2687
- const candidates = input.mounts.filter(
2688
- (mount) => typeof mount.initialPublicationSourceRelativePath === "string"
2689
- );
2690
- if (candidates.length === 0) return null;
2691
- const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
2692
- const objects = await resolveGitObjectsAsync(
2693
- input.workspacePath,
2694
- candidates.flatMap(
2695
- (mount) => revisions.flatMap((revision) => [
2696
- workspaceTreeObjectName(revision, mount.workspaceRelativePath),
2697
- workspaceTreeObjectName(revision, mount.initialPublicationSourceRelativePath)
2698
- ])
2699
- )
2700
- );
2701
- const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath))?.objectId ?? null : null;
2702
- const initialPublications = candidates.flatMap((mount) => {
2703
- const published = lookup(input.localHead, mount.workspaceRelativePath);
2704
- if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
2705
- const source = lookup(input.localHead, mount.initialPublicationSourceRelativePath) ?? lookup(input.remoteHead, mount.initialPublicationSourceRelativePath);
2706
- return [{ mount, published, source }];
2707
- });
2708
- if (initialPublications.length === 0) return null;
2709
- let size = await input.measure(input.workspacePath, input.remoteHead, input.localHead, input.limit, [
2710
- ...initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`),
2711
- ...input.excludePathspecs ?? []
2712
- ]);
2713
- for (const { published, source } of initialPublications) {
2714
- if (size > input.limit) break;
2715
- const migrations = source ? await pathsReplacedByGitlinks(input.workspacePath, source, published) : [];
2716
- size += await input.measure(input.workspacePath, source, published, input.limit - size, gitlinkMigrationPathspecs(migrations));
2717
- }
2718
- return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
2719
- }
2720
- function conflictPaths(workspacePath) {
2721
- const result = gitResult(workspacePath, ["diff", "--name-only", "--diff-filter=U", "-z"]);
2722
- return result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
2723
- }
2724
- function snapshotConflict(input) {
2725
- const suffix = input.attemptId.replace(/[^a-zA-Z0-9._-]/g, "-");
2726
- const refs = {
2727
- local: `refs/r5d/workspace-conflicts/${suffix}/local`,
2728
- remote: `refs/r5d/workspace-conflicts/${suffix}/remote`
2729
- };
2730
- git(input.workspacePath, ["update-ref", refs.local, input.localHead], "snapshot local workspace conflict head");
2731
- git(input.workspacePath, ["update-ref", refs.remote, input.remoteHead], "snapshot remote workspace conflict head");
2732
- return refs;
2733
- }
2734
- async function rebaseWorkspace(input) {
2735
- const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
2736
- await beginWorkspaceCheckoutTransition(input.workspacePath);
2737
- const rebase = await gitResultAsync(
2738
- input.workspacePath,
2739
- input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
2740
- );
2741
- if (rebase.exitCode === 0) {
2742
- await completeWorkspaceCheckoutTransition(input.workspacePath);
2743
- return { ok: true };
2744
- }
2745
- const conflicts = conflictPaths(input.workspacePath);
2746
- const refs = snapshotConflict(input);
2747
- const abort = gitResult(input.workspacePath, ["rebase", "--abort"]);
2748
- if (abort.exitCode !== 0) {
2749
- throw new Error(`Abort workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
2750
- }
2751
- await completeWorkspaceCheckoutTransition(input.workspacePath);
2752
- return {
2753
- ok: false,
2754
- conflictPaths: conflicts,
2755
- refs,
2756
- error: rebase.stderr || rebase.stdout || `Workspace rebase onto ${input.remoteHead} conflicted`
2757
- };
2758
- }
2759
- async function synchronizeWithFetchedHead(input) {
2760
- const localHead = await revParseAsync(input.workspacePath, "HEAD");
2761
- const remoteHead = input.remoteHead;
2762
- if (!remoteHead) return { kind: "ready", updated: false, rebased: false };
2763
- if (!localHead) {
2764
- await runWorkspaceCheckoutTransition(input.workspacePath, async () => {
2765
- await gitAsync(
2766
- input.workspacePath,
2767
- ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
2768
- "check out fetched workspace main"
2769
- );
2770
- });
2771
- return { kind: "ready", updated: true, rebased: false };
2772
- }
2773
- if (localHead === remoteHead) {
2774
- await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
2775
- return { kind: "ready", updated: false, rebased: false };
2776
- }
2777
- if (await tryGitAsync(input.workspacePath, ["merge-base", "--is-ancestor", localHead, remoteHead])) {
2778
- await runWorkspaceCheckoutTransition(input.workspacePath, async () => {
2779
- await gitAsync(input.workspacePath, ["reset", "--hard", remoteHead], "fast-forward workspace main");
2780
- });
2781
- await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
2782
- return { kind: "ready", updated: true, rebased: false };
2783
- }
2784
- if (await tryGitAsync(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
2785
- await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
2786
- return { kind: "ready", updated: false, rebased: false };
2787
- }
2788
- const integratedHead = await revParseAsync(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
2789
- const truncatedHistoryBase = workspaceIsShallow(input.workspacePath) && integratedHead && tryGit(input.workspacePath, ["merge-base", "--is-ancestor", integratedHead, localHead]) ? integratedHead : void 0;
2790
- if (workspaceIsShallow(input.workspacePath) && !truncatedHistoryBase) {
2791
- throw new Error(
2792
- "Workspace history was truncated before this clone's integrated base could be identified; reset the workspace clone before retrying"
2793
- );
2794
- }
2795
- const rebased = await rebaseWorkspace({
2796
- workspacePath: input.workspacePath,
2797
- localHead,
2798
- remoteHead,
2799
- attemptId: input.attemptId,
2800
- truncatedHistoryBase
2801
- });
2802
- if (rebased.ok) await updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
2803
- return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
2804
- }
2805
- async function synchronizeWorkspaceGit(input) {
2806
- const remoteHeadFetches = { count: 0 };
2807
- const result = await runWorkspaceGitSynchronization(input, remoteHeadFetches);
2808
- return { ...result, remoteHeadFetches: remoteHeadFetches.count };
2809
- }
2810
- async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2811
- const workspacePath = path.resolve(input.workspacePath);
2812
- const attemptId = input.attemptId ?? crypto.randomUUID();
2813
- const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
2814
- const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
2815
- validateMounts(workspacePath, input.mounts);
2816
- const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
2817
- const preserveResolutionInProgress = input.skipMountMirror === true;
2818
- const initial = await ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
2819
- if (initial.remoteHeadFetched) remoteHeadFetches.count += 1;
2820
- assertWorkspaceRemediationAncestry({
2821
- workspacePath,
2822
- currentHead: initial.localHead,
2823
- remoteHead: initial.remoteHead,
2824
- requiredAncestorHeads
2825
- });
2826
- const verifiedAncestorHeads = requiredAncestorHeads.length > 0 ? requiredAncestorHeads : void 0;
2827
- const startingHead = initial.localHead;
2828
- const receiptBeforeInitialHydration = await readHydratedWorkspaceReceiptAsync(workspacePath);
2829
- const deferredMountIds = /* @__PURE__ */ new Set();
2830
- if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
2831
- for (const mount of input.mounts) {
2832
- if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && workspaceHydrationReceiptMountBasisHead(receiptBeforeInitialHydration, mount) === null) {
2833
- deferredMountIds.add(mount.id);
2834
- }
2835
- }
2836
- }
2837
- await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
2838
- ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
2839
- preserveResolutionInProgress,
2840
- preserveStaleBases: true,
2841
- hydrationHooks: input.hydrationHooks
2842
- });
2843
- const selected = activeMounts(input.mounts);
2844
- const projectionMutationTokens = new Map(
2845
- input.mounts.flatMap((mount) => mount.mutationToken ? [[mount.id, mount.mutationToken()]] : [])
2846
- );
2847
- const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
2848
- const receiptRequiredLiveMounts = selected.active;
2849
- const projectionBasisHead = await revParseAsync(workspacePath, "HEAD");
2850
- const projectionBasisReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
2851
- const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
2852
- const fastProjectionMounts = [];
2853
- const mergedProjectionTrees = /* @__PURE__ */ new Map();
2854
- const mergedProjectionMounts = [];
2855
- const projectionSkippedIds = /* @__PURE__ */ new Set();
2856
- let projectionWarning;
2857
- const projectedMountFiles = /* @__PURE__ */ new Map();
2858
- const mergedProjectionBases = /* @__PURE__ */ new Map();
2859
- const projectedMountBases = /* @__PURE__ */ new Map();
2860
- const mountSkipReasons = {};
2861
- const staleRewriteLedger = hydrationPreBlobLedger(workspacePath);
2862
- const hydrationLedgerFor = (mount) => {
2863
- const entries = staleRewriteLedger.entries(mount);
2864
- return entries.size > 0 ? Object.fromEntries(entries) : void 0;
2865
- };
2866
- const forceHydrationMountIds = /* @__PURE__ */ new Set();
2867
- const ledgerMergedPathsByMount = /* @__PURE__ */ new Map();
2868
- const recordStaleRewrite = (mount, paths) => {
2869
- projectionSkippedIds.add(mount.id);
2870
- mountSkipReasons[mount.id] = { reason: "stale_rewrite_detected", paths: [...paths] };
2871
- const holders = mount.describeHolders?.();
2872
- process.stderr.write(
2873
- `[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}` : ""}
2874
- `
2875
- );
2876
- };
2877
- const refreshCycleSkippedMounts = (mounts = input.mounts) => {
2878
- for (const mount of mounts) {
2879
- const projectionToken = projectionMutationTokens.get(mount.id);
2880
- if (projectionToken !== void 0 && mount.mutationToken?.() !== projectionToken) cycleSkippedMountIds.add(mount.id);
2881
- if (mountBusy(mount)) cycleSkippedMountIds.add(mount.id);
2882
- }
2883
- };
2884
- const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
2885
- refreshCycleSkippedMounts();
2886
- const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
2887
- const activeMountIds = [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort();
2888
- return {
2889
- activeMountIds,
2890
- skippedMountIds: [...skippedIds].sort(),
2891
- // An active mount's token is unchanged since capture (a moved token
2892
- // lands it in cycleSkippedMountIds above), so this is the token its
2893
- // projected bytes belong to.
2894
- projectedMutationTokens: Object.fromEntries(
2895
- activeMountIds.flatMap((id) => {
2896
- const token = projectionMutationTokens.get(id);
2897
- return token === void 0 ? [] : [[id, token]];
2898
- })
2899
- ),
2900
- ...Object.keys(mountSkipReasons).length > 0 ? { mountSkipReasons: { ...mountSkipReasons } } : {}
2901
- };
2902
- };
2903
- 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;
2904
- const mergeProjectMount = async (mount, basisHead, currentHead) => {
2905
- const releaseMergeHold = input.hydrationHooks?.holdMounts([mount.id], `merge projection of mount ${mount.id}`);
2906
- let merged;
2907
- try {
2908
- const hydrationLedger = hydrationLedgerFor(mount);
2909
- merged = await workspaceFilesystemExecutor().run("projection_merge", {
2910
- workspacePath,
2911
- mount: {
2912
- id: mount.id,
2913
- sourcePath: mount.sourcePath,
2914
- workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
2915
- sourceMode: mount.sourceMode
2916
- },
2917
- basisHead,
2918
- currentHead,
2919
- attemptId,
2920
- ...hydrationLedger ? { hydrationLedger } : {}
2921
- });
2922
- } finally {
2923
- releaseMergeHold?.();
2924
- }
2925
- if (merged.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, merged.staleRewriteCleared);
2926
- if (merged.kind === "stale_rewrite") {
2927
- recordStaleRewrite(mount, merged.paths);
2928
- return { kind: "skipped" };
2929
- }
2930
- if (merged.kind === "conflict" && mount.processLive?.()) {
2931
- projectionSkippedIds.add(mount.id);
2932
- mountSkipReasons[mount.id] = { reason: "projection_conflict_deferred", paths: merged.conflictPaths };
2933
- const holders = mount.describeHolders?.();
2934
- process.stderr.write(
2935
- `[r5d-worker] projection of mount ${mount.id} conflicts with the workspace head at ${merged.conflictPaths.join(", ")}; deferred while a process runs in the checkout${holders ? ` (${holders})` : ""}, reported once the mount is idle
2936
- `
2937
- );
2938
- return { kind: "skipped" };
2939
- }
2940
- mergedProjectionBases.set(mount.id, {
2941
- projectedCommit: merged.oursCommit,
2942
- projectedFiles: merged.projectedFiles,
2943
- readAtMs: merged.readAtMs
2944
- });
2945
- if (merged.kind === "conflict") {
2946
- const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: currentHead });
2947
- return {
2948
- kind: "conflict",
2949
- result: {
2950
- outcome: "conflict_blocked",
2951
- startingHead,
2952
- localHead: merged.oursCommit,
2953
- remoteHead: initial.remoteHead,
2954
- publishedHead: initial.remoteHead,
2955
- rebaseCount: 0,
2956
- diffSizeBytes: 0,
2957
- affectedPaths: merged.conflictPaths,
2958
- activeMountIds: [],
2959
- skippedMountIds: input.mounts.map(({ id }) => id).sort(),
2960
- conflictPaths: merged.conflictPaths,
2961
- conflictSnapshotRefs: refs,
2962
- conflictKind: "projection_merge",
2963
- ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
2964
- error: merged.error
2965
- }
2966
- };
2967
- }
2968
- mergedProjectionTrees.set(mount.id, merged.resultTree);
2969
- mergedProjectionMounts.push(mount);
2970
- if (merged.ledgerMergedPaths.length > 0) {
2971
- forceHydrationMountIds.add(mount.id);
2972
- ledgerMergedPathsByMount.set(mount.id, merged.ledgerMergedPaths);
2973
- }
2974
- return { kind: "projected" };
2975
- };
2976
- if (!input.skipMountMirror) {
2977
- const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
2978
- const mergeSupport = mergeProjectionEnabled ? workspaceMergeProjectionSupport() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
2979
- const activeProjectionIds = new Set(selected.active.map(({ id }) => id));
2980
- for (const mount of [...selected.active, ...selected.tombstones]) {
2981
- if (deferredMountIds.has(mount.id)) continue;
2982
- const basisHead = workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount);
2983
- if (projectionBasisHead === null) {
2984
- fastProjectionMounts.push(mount);
2985
- continue;
2986
- }
2987
- if (basisHead === projectionBasisHead) {
2988
- const hydrationLedger = activeProjectionIds.has(mount.id) ? hydrationLedgerFor(mount) : void 0;
2989
- if (!hydrationLedger || !Object.values(hydrationLedger).some(({ processLiveAcrossHydration }) => processLiveAcrossHydration) || !mergeSupport.supported) {
2990
- fastProjectionMounts.push(mount);
2991
- continue;
2992
- }
2993
- if (cycleSkippedMountIds.has(mount.id) || mountBusy(mount)) {
2994
- cycleSkippedMountIds.add(mount.id);
2995
- continue;
2996
- }
2997
- const releaseCheckHold = input.hydrationHooks?.holdMounts([mount.id], `hydration ledger check of mount ${mount.id}`);
2998
- let checked;
2999
- try {
3000
- checked = await workspaceFilesystemExecutor().run("projection_mirror", {
3001
- workspacePath,
3002
- mount: workspaceGitMountData(mount),
3003
- hydrationLedger,
3004
- checkOnly: true
3005
- });
3006
- } finally {
3007
- releaseCheckHold?.();
3008
- }
3009
- if (checked.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, checked.staleRewriteCleared);
3010
- if (checked.staleRewritePaths.length > 0) {
3011
- recordStaleRewrite(mount, checked.staleRewritePaths);
3012
- continue;
3013
- }
3014
- if (checked.ledgerMergePaths.length === 0) {
3015
- fastProjectionMounts.push(mount);
3016
- continue;
3017
- }
3018
- if (cycleSkippedMountIds.has(mount.id) || mountBusy(mount)) {
3019
- cycleSkippedMountIds.add(mount.id);
3020
- continue;
3021
- }
3022
- const outcome2 = await mergeProjectMount(mount, projectionBasisHead, projectionBasisHead);
3023
- if (outcome2.kind === "conflict") return outcome2.result;
3024
- continue;
3025
- }
3026
- if (basisHead === null) {
3027
- deferredMountIds.add(mount.id);
3028
- continue;
3029
- }
3030
- if (!mergeSupport.supported) {
3031
- projectionSkippedIds.add(mount.id);
3032
- projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
3033
- continue;
3034
- }
3035
- if (cycleSkippedMountIds.has(mount.id) || mountBusy(mount)) {
3036
- cycleSkippedMountIds.add(mount.id);
3037
- continue;
3038
- }
3039
- const outcome = await mergeProjectMount(mount, basisHead, projectionBasisHead);
3040
- if (outcome.kind === "conflict") return outcome.result;
3041
- }
3042
- }
3043
- if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
3044
- `);
3045
- const completedMountSelection = async (requireCurrentHeadReceipt = false) => {
3046
- const currentHeadBeforeHydration = await revParseAsync(workspacePath, "HEAD");
3047
- const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
3048
- const currentReceiptBeforeHydration = await readHydratedWorkspaceReceiptAsync(workspacePath);
3049
- refreshCycleSkippedMounts(completionReceiptMounts);
3050
- const mountIsAlreadyCovered = (mount) => !forceHydrationMountIds.has(mount.id) && Boolean(
3051
- currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
3052
- );
3053
- if (currentHeadBeforeHydration && completionReceiptMounts.every((mount) => cycleSkippedMountIds.has(mount.id) || mountIsAlreadyCovered(mount))) {
3054
- return classifyMounts([...selected.active, ...selected.tombstones]);
3055
- }
3056
- const uncoveredCompletionMounts = selected.active.filter(
3057
- (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
3058
- );
3059
- const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter(
3060
- (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
3061
- );
3062
- const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
3063
- (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
3064
- );
3065
- const unchangedMountIds = currentHeadBeforeHydration ? await selectMountsWithUnchangedOuterSubtree(
3066
- workspacePath,
3067
- currentReceiptBeforeHydration,
3068
- currentHeadBeforeHydration,
3069
- uncoveredCompletionMounts
3070
- ) : /* @__PURE__ */ new Set();
3071
- for (const id of forceHydrationMountIds) unchangedMountIds.delete(id);
3072
- const hydrationMounts = uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id));
3073
- const mergeBases = /* @__PURE__ */ new Map();
3074
- if (workspaceMergeHydrationEnabled()) {
3075
- for (const mount of hydrationMounts) {
3076
- const basis = projectedMountBases.get(mount.id);
3077
- if (basis) mergeBases.set(mount.id, basis);
3078
- }
3079
- }
3080
- const hydration = await hydrateWorkspaceGitMountsTransactionally({
3081
- workspacePath,
3082
- mounts: hydrationMounts,
3083
- durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
3084
- carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
3085
- receiptMounts: completionReceiptMounts,
3086
- requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
3087
- recordCurrentHead: true,
3088
- hydrationHooks: input.hydrationHooks,
3089
- ...mergeBases.size > 0 ? { mergeBases } : {},
3090
- ...ledgerMergedPathsByMount.size > 0 ? { ledgerMergedPaths: ledgerMergedPathsByMount } : {}
3091
- });
3092
- for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
3093
- for (const mount of hydrationMounts) {
3094
- const mergedPaths = ledgerMergedPathsByMount.get(mount.id);
3095
- if (!mergedPaths || !hydration.hydratedMountIds.includes(mount.id)) continue;
3096
- const written = new Set(hydration.hydratedEntryPaths[mount.id] ?? []);
3097
- const settled = mergedPaths.filter((relativePath) => !written.has(relativePath));
3098
- if (settled.length > 0) staleRewriteLedger.forget(mount, settled);
3099
- ledgerMergedPathsByMount.delete(mount.id);
3100
- forceHydrationMountIds.delete(mount.id);
3101
- }
3102
- if (currentHeadBeforeHydration) {
3103
- for (const id of hydration.hydratedMountIds) {
3104
- if (mergeBases.has(id)) projectedMountBases.set(id, { projectedCommit: currentHeadBeforeHydration, projectedFiles: null });
3105
- }
3106
- }
3107
- for (const skip of hydration.mergeSkips) {
3108
- const mount = hydrationMounts.find(({ id }) => id === skip.mountId);
3109
- const basis = mount ? workspaceHydrationReceiptMountBasisHead(currentReceiptBeforeHydration, mount) : null;
3110
- mountSkipReasons[skip.mountId] = {
3111
- reason: skip.reason,
3112
- paths: skip.paths,
3113
- ...skip.retainedPaths?.length ? { retainedPaths: skip.retainedPaths } : {}
3114
- };
3115
- process.stderr.write(
3116
- `[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(", ")}` : ""}
3117
- `
3118
- );
3119
- }
3120
- refreshCycleSkippedMounts(completionReceiptMounts);
3121
- if (requireCurrentHeadReceipt) {
3122
- const currentHead = await revParseAsync(workspacePath, "HEAD");
3123
- const receipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
3124
- const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
3125
- const idleMounts = completionReceiptMounts.filter(
3126
- (mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mountBusy(mount) && !projectionSkippedIds.has(mount.id)
3127
- );
3128
- if (currentHead && !idleMounts.every((mount) => workspaceHydrationReceiptCoversMount(receipt, currentHead, mount))) {
3129
- throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
3130
- }
3131
- }
3132
- return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
3133
- };
3134
- let newlyStagedPaths = [];
3135
- const projectedGitlinks = [];
3136
- const stageAndCommitWorkspace = async () => {
3137
- if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
3138
- throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
3139
- }
3140
- await stageWorkspaceGitlinks(workspacePath, projectedGitlinks);
3141
- await gitAsync(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
3142
- newlyStagedPaths = await stagedPaths(workspacePath);
3143
- if (newlyStagedPaths.length > 0) {
3144
- if (workspaceResolutionInProgress(workspacePath) || await workspaceHasUnmergedEntries(workspacePath)) {
3145
- throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
3146
- }
3147
- await gitAsync(
3148
- workspacePath,
3149
- [
3150
- "commit",
3151
- "-m",
3152
- JSON.stringify({ type: "workspace_sync", worker: input.workerLabel, attemptId, detail: input.commitDetail ?? null })
3153
- ],
3154
- "commit workspace working trees"
3155
- );
3156
- }
3157
- };
3158
- const projectedFastMounts = [];
3159
- const currentProjectionMounts = [];
3160
- const selectedActiveIdSet = new Set(selected.active.map(({ id }) => id));
3161
- if (!input.skipMountMirror) {
3162
- await beginWorkspaceCheckoutTransition(workspacePath);
3163
- try {
3164
- const selectedActiveIds = selectedActiveIdSet;
3165
- const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
3166
- const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
3167
- for (const mount of fastActiveMounts) {
3168
- if (cycleSkippedMountIds.has(mount.id) || mountBusy(mount)) {
3169
- cycleSkippedMountIds.add(mount.id);
3170
- continue;
3171
- }
3172
- if (mountProjectionIsCurrent(mount)) {
3173
- currentProjectionMounts.push(mount);
3174
- continue;
3175
- }
3176
- const releaseMirrorHold = input.hydrationHooks?.holdMounts([mount.id], `projection of mount ${mount.id}`);
3177
- let staleRewritePaths;
3178
- let ledgerMergePaths;
3179
- try {
3180
- const hydrationLedger = hydrationLedgerFor(mount);
3181
- const mirrored = await workspaceFilesystemExecutor().run("projection_mirror", {
3182
- workspacePath,
3183
- mount: workspaceGitMountData(mount),
3184
- ...hydrationLedger ? { hydrationLedger } : {}
3185
- });
3186
- staleRewritePaths = mirrored.staleRewritePaths;
3187
- ledgerMergePaths = mirrored.ledgerMergePaths;
3188
- if (mirrored.staleRewriteCleared.length > 0) staleRewriteLedger.forget(mount, mirrored.staleRewriteCleared);
3189
- if (staleRewritePaths.length === 0 && ledgerMergePaths.length === 0) {
3190
- projectedGitlinks.push({ mount, gitlinks: mirrored.gitlinks });
3191
- projectedMountFiles.set(mount.id, { files: mirrored.projectedFiles, readAtMs: mirrored.readAtMs });
3192
- }
3193
- } finally {
3194
- releaseMirrorHold?.();
3195
- }
3196
- if (ledgerMergePaths.length > 0) {
3197
- projectionSkippedIds.add(mount.id);
3198
- mountSkipReasons[mount.id] = { reason: "hydration_target_changed", paths: ledgerMergePaths };
3199
- process.stderr.write(
3200
- `[r5d-worker] projection of mount ${mount.id} skipped: a running process rewrote ${describeHydrationSkipPaths(mount.sourcePath, ledgerMergePaths)} while the cycle prepared the mirror; the next cycle merges it
3201
- `
3202
- );
3203
- continue;
3204
- }
3205
- if (staleRewritePaths.length > 0) {
3206
- recordStaleRewrite(mount, staleRewritePaths);
3207
- continue;
3208
- }
3209
- projectedFastMounts.push(mount);
3210
- }
3211
- await workspaceFilesystemExecutor().run("projection_outer_apply", {
3212
- workspacePath,
3213
- removeMounts: fastTombstones.map(workspaceGitMountData),
3214
- materialize: mergedProjectionMounts.map((mount) => ({
3215
- workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
3216
- resultTree: mergedProjectionTrees.get(mount.id)
3217
- })),
3218
- restoreFromHead: input.mounts.filter(
3219
- (mount) => deferredMountIds.has(mount.id) || mount.preserveLocalOnHydrationBasisChange === true && cycleSkippedMountIds.has(mount.id)
3220
- ).map(workspaceGitMountData)
3221
- });
3222
- projectedFastMounts.push(...fastTombstones);
3223
- await stageAndCommitWorkspace();
3224
- await workspaceFilesystemExecutor().run("outer_checkout_fsync", { workspacePath });
3225
- } catch (error) {
3226
- await throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
3227
- }
3228
- } else {
3229
- await stageAndCommitWorkspace();
3230
- if (newlyStagedPaths.length > 0) await completeWorkspaceCheckoutTransition(workspacePath);
3231
- }
3232
- if (!input.skipMountMirror) {
3233
- const projectedHead = await revParseAsync(workspacePath, "HEAD");
3234
- if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
3235
- for (const mount of projectedFastMounts) {
3236
- if (!selectedActiveIdSet.has(mount.id)) continue;
3237
- const read = projectedMountFiles.get(mount.id);
3238
- projectedMountBases.set(mount.id, {
3239
- projectedCommit: projectedHead,
3240
- projectedFiles: read?.files ?? null,
3241
- ...read ? { readAtMs: read.readAtMs } : {}
3242
- });
3243
- }
3244
- for (const mount of currentProjectionMounts)
3245
- projectedMountBases.set(mount.id, { projectedCommit: projectedHead, projectedFiles: null });
3246
- for (const [mountId, basis] of mergedProjectionBases) projectedMountBases.set(mountId, basis);
3247
- const targetProjectionReceipt = receiptWithMountsAtHead(
3248
- projectionBasisReceipt,
3249
- projectedHead,
3250
- [...projectedFastMounts, ...currentProjectionMounts],
3251
- allProjectionReceiptMounts
3252
- );
3253
- const currentProjectionReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
3254
- if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !await hydratedWorkspaceReceiptFileIsV3(workspacePath) || !await workspaceBasisRefsAreCurrentAsync(workspacePath, targetProjectionReceipt)) {
3255
- await workspaceFilesystemExecutor().run("projection_receipt", {
3256
- workspacePath,
3257
- fsyncMounts: projectedFastMounts.map(workspaceGitMountData),
3258
- receipt: targetProjectionReceipt
3259
- });
3260
- }
3261
- await completeWorkspaceCheckoutTransition(workspacePath);
3262
- }
3263
- const prePublishRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
3264
- if (prePublishRemoteHead.fetched) remoteHeadFetches.count += 1;
3265
- let remoteHead = prePublishRemoteHead.head;
3266
- let rebaseCount = 0;
3267
- let updated = false;
3268
- for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
3269
- assertWorkspaceRemediationAncestry({
3270
- workspacePath,
3271
- currentHead: await revParseAsync(workspacePath, "HEAD"),
3272
- remoteHead,
3273
- requiredAncestorHeads
3274
- });
3275
- const reconciled = await synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
3276
- if (reconciled.kind === "conflict") {
3277
- const localHead2 = await revParseAsync(workspacePath, "HEAD");
3278
- const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
3279
- return {
3280
- outcome: "conflict_blocked",
3281
- startingHead,
3282
- localHead: localHead2,
3283
- remoteHead,
3284
- publishedHead: remoteHead,
3285
- rebaseCount: rebaseCount + 1,
3286
- diffSizeBytes: 0,
3287
- affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
3288
- ...classifiedMounts,
3289
- conflictPaths: reconciled.conflictPaths,
3290
- conflictSnapshotRefs: reconciled.refs,
3291
- conflictKind: "integration_rebase",
3292
- ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
3293
- error: reconciled.error
3294
- };
3295
- }
3296
- updated ||= reconciled.updated;
3297
- if (reconciled.rebased) rebaseCount += 1;
3298
- if (reconciled.updated || reconciled.rebased) await completedMountSelection(true);
3299
- const localHead = await revParseAsync(workspacePath, "HEAD");
3300
- if (!localHead) {
3301
- const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
3302
- return {
3303
- outcome: "no_change",
3304
- startingHead,
3305
- localHead: null,
3306
- remoteHead,
3307
- publishedHead: remoteHead,
3308
- rebaseCount,
3309
- diffSizeBytes: 0,
3310
- affectedPaths: [],
3311
- ...classifiedMounts,
3312
- ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
3313
- ...projectionWarning ? { error: projectionWarning } : {}
3314
- };
3315
- }
3316
- const paths = await changedPaths(workspacePath, remoteHead, localHead);
3317
- const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
3318
- const gitlinkMigrations = paths.length > 0 ? await pathsReplacedByGitlinks(workspacePath, remoteHead, localHead) : [];
3319
- const gitlinkMigrationExcludes = gitlinkMigrationPathspecs(gitlinkMigrations);
3320
- let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes, gitlinkMigrationExcludes) : 0;
3321
- input.assertStillAdmitted?.();
3322
- let initialPublicationMountIds;
3323
- if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
3324
- const initialPublication = await measureInitialPublicationDiffSize({
3325
- workspacePath,
3326
- remoteHead,
3327
- localHead,
3328
- mounts: input.mounts,
3329
- limit: maxDiffBytes,
3330
- measure: measureDiffSize,
3331
- excludePathspecs: gitlinkMigrationExcludes
3332
- });
3333
- input.assertStillAdmitted?.();
3334
- if (initialPublication && initialPublication.size <= maxDiffBytes) {
3335
- size = initialPublication.size;
3336
- initialPublicationMountIds = initialPublication.mountIds;
3337
- }
3338
- }
3339
- let gitlinkMigrationConfirmed = false;
3340
- if (gitlinkMigrations.length > 0 && size <= maxDiffBytes && !input.allowLargeDiff) {
3341
- gitlinkMigrationConfirmed = await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) > maxDiffBytes;
3342
- input.assertStillAdmitted?.();
3343
- }
3344
- if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
3345
- const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
3346
- return {
3347
- outcome: "large_diff_blocked",
3348
- startingHead,
3349
- localHead,
3350
- remoteHead,
3351
- publishedHead: remoteHead,
3352
- rebaseCount,
3353
- diffSizeBytes: size,
3354
- affectedPaths: paths,
3355
- ...classifiedMounts,
3356
- ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
3357
- error: `Workspace diff exceeds the ${maxDiffBytes}-byte automatic publication limit`
3358
- };
3359
- }
3360
- if (localHead === remoteHead) {
3361
- assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
3362
- const completedMounts = await completedMountSelection(Boolean(input.skipMountMirror));
3363
- await input.afterWorkspacePublished?.({
3364
- publishedHead: localHead,
3365
- activeMountIds: completedMounts.activeMountIds
3366
- });
3367
- input.assertStillAdmitted?.();
3368
- return {
3369
- outcome: updated ? "updated" : "no_change",
3370
- startingHead,
3371
- localHead,
3372
- remoteHead,
3373
- publishedHead: localHead,
3374
- rebaseCount,
3375
- diffSizeBytes: size,
3376
- affectedPaths: paths,
3377
- ...completedMounts,
3378
- ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
3379
- ...projectionWarning ? { error: projectionWarning } : {}
3380
- };
3381
- }
3382
- assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
3383
- const pushArgs = [
3384
- ...gitTransportSecurityArgs(input.remoteUrl, input.credentialHelper, input.credentialUsername),
3385
- "push",
3386
- "--no-recurse-submodules"
3387
- ];
3388
- if (input.allowLargeDiff || initialPublicationMountIds || gitlinkMigrationConfirmed) {
3389
- pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
3390
- }
3391
- pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
3392
- const push = await gitResultAsync(workspacePath, pushArgs);
3393
- if (push.exitCode === 0) {
3394
- assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
3395
- await updateIntegratedWorkspaceHead(workspacePath, localHead);
3396
- const completedMounts = await completedMountSelection(Boolean(input.skipMountMirror));
3397
- await input.afterWorkspacePublished?.({
3398
- publishedHead: localHead,
3399
- activeMountIds: completedMounts.activeMountIds
3400
- });
3401
- input.assertStillAdmitted?.();
3402
- return {
3403
- outcome: "pushed",
3404
- startingHead,
3405
- localHead,
3406
- remoteHead,
3407
- publishedHead: localHead,
3408
- rebaseCount,
3409
- diffSizeBytes: size,
3410
- affectedPaths: paths,
3411
- ...completedMounts,
3412
- ...initialPublicationMountIds ? { initialPublicationMountIds } : {},
3413
- ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
3414
- ...projectionWarning ? { error: projectionWarning } : {}
3415
- };
3416
- }
3417
- if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
3418
- ${push.stdout}`)) {
3419
- throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
3420
- }
3421
- remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
3422
- remoteHeadFetches.count += 1;
3423
- }
3424
- throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
3425
- }
3426
- function inspectHydrationLedger(workspacePath, mount, hydrationLedger) {
3427
- const outerRoot = workspaceMountOuterPath(workspacePath, mount);
3428
- const stale = [];
3429
- const merge = [];
3430
- const cleared = [];
3431
- for (const [relativePath, entry] of Object.entries(hydrationLedger)) {
3432
- const algorithm = gitObjectHashAlgorithmFor(entry.preBlob);
3433
- if (!algorithm) continue;
3434
- const source = inspectWorkingTreePath(mount.sourcePath, relativePath);
3435
- if (source.kind !== "entry" || !source.stat.isFile()) {
3436
- if (entry.postBlob !== null) cleared.push(relativePath);
3437
- continue;
3438
- }
3439
- if (entry.stat && projectedFileEntryMatches(entry.stat, source.stat)) continue;
3440
- const content = fs.readFileSync(source.absolutePath);
3441
- const hash = gitBlobHash(content, algorithm);
3442
- if (hash === entry.postBlob) continue;
3443
- const outer = inspectWorkingTreePath(outerRoot, relativePath);
3444
- const outerEqual = outer.kind === "entry" && outer.stat.isFile() && fs.readFileSync(outer.absolutePath).equals(content);
3445
- if (outerEqual) {
3446
- cleared.push(relativePath);
3447
- continue;
3448
- }
3449
- if (entry.processLiveAcrossHydration) {
3450
- merge.push(relativePath);
3451
- continue;
3452
- }
3453
- if (hash !== entry.preBlob) {
3454
- cleared.push(relativePath);
3455
- continue;
3456
- }
3457
- stale.push(relativePath);
3458
- }
3459
- return { stale: stale.sort(), merge: merge.sort(), cleared: cleared.sort() };
3460
- }
3461
- function runProjectionMirrorJob(input) {
3462
- const workspacePath = path.resolve(input.workspacePath);
3463
- let staleRewriteCleared = [];
3464
- if (input.hydrationLedger) {
3465
- const inspected = inspectHydrationLedger(workspacePath, input.mount, input.hydrationLedger);
3466
- staleRewriteCleared = inspected.cleared;
3467
- if (inspected.stale.length > 0 || inspected.merge.length > 0 || input.checkOnly) {
3468
- return {
3469
- gitlinks: [],
3470
- projectedFiles: {},
3471
- readAtMs: Date.now(),
3472
- staleRewritePaths: inspected.stale,
3473
- ledgerMergePaths: inspected.merge,
3474
- staleRewriteCleared
3475
- };
3476
- }
3477
- } else if (input.checkOnly) {
3478
- return { gitlinks: [], projectedFiles: {}, readAtMs: Date.now(), staleRewritePaths: [], ledgerMergePaths: [], staleRewriteCleared };
3479
- }
3480
- const projectedFiles = /* @__PURE__ */ new Map();
3481
- const projected = mirrorMountsToWorkspace(workspacePath, [input.mount], projectedFiles);
3482
- return {
3483
- gitlinks: projected[0]?.gitlinks ?? [],
3484
- projectedFiles: Object.fromEntries(projectedFiles),
3485
- readAtMs: Date.now(),
3486
- staleRewritePaths: [],
3487
- ledgerMergePaths: [],
3488
- staleRewriteCleared
3489
- };
3490
- }
3491
- function runProjectionOuterApplyJob(input) {
3492
- const workspacePath = path.resolve(input.workspacePath);
3493
- removeWorkspaceMounts(workspacePath, input.removeMounts);
3494
- for (const { workspaceRelativePath, resultTree } of input.materialize) {
3495
- materializeWorkspaceProjectionTree({ workspacePath, workspaceRelativePath, resultTree });
3496
- }
3497
- restoreDeferredWorkspaceMountsFromHead(workspacePath, input.restoreFromHead);
3498
- return null;
3499
- }
3500
- function runProjectionReceiptJob(input) {
3501
- const workspacePath = path.resolve(input.workspacePath);
3502
- for (const mount of input.fsyncMounts) fsyncHydratedWorkspaceMount(mount);
3503
- updateHydratedWorkspaceReceipt(workspacePath, input.receipt);
3504
- return null;
3505
- }
3506
- const workspaceGitSyncTestHarness = {
3507
- beginHydrationTransaction,
3508
- commandArgs: gitCommandArgs,
3509
- workspaceCloneCommandArgs,
3510
- configureWorkspaceRepository,
3511
- mirrorMountsToWorkspace,
3512
- hydrateMountsFromWorkspace: hydrateWorkspaceGitMounts,
3513
- hydrationPreBlobLedger,
3514
- resetHydrationPreBlobLedgers() {
3515
- hydrationPreBlobLedgers.clear();
3516
- }
3517
- };
3518
- export {
3519
- MAX_WORKSPACE_GIT_DIFF_BYTES,
3520
- WORKSPACE_GIT_BRANCH,
3521
- WORKSPACE_GIT_CHECKOUT_DURABILITY,
3522
- WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
3523
- WORKSPACE_GIT_HYDRATED_RECEIPT,
3524
- WORKSPACE_GIT_HYDRATION_RECOVERIES,
3525
- WORKSPACE_GIT_HYDRATION_TRANSACTION,
3526
- WORKSPACE_MERGE_HYDRATION_SWITCH,
3527
- WORKSPACE_PROCESS_BUSY_FENCE_SWITCH,
3528
- WorkspaceHydrationRecoveryRequiredError,
3529
- WorkspaceRemediationAncestryError,
3530
- configureExistingWorkspaceGitForRemediation,
3531
- ensureWorkspaceGitClone,
3532
- hydrateWorkspaceGitMounts,
3533
- recoverWorkspaceGitHydration,
3534
- resetWorkspaceGit,
3535
- runCheckoutDurabilityRecoveryJob,
3536
- runCheckoutTransitionBeginJob,
3537
- runCheckoutTransitionCompleteJob,
3538
- runHydrationPreserveInterruptedJob,
3539
- runHydrationRawJob,
3540
- runHydrationRecoveryInspectJob,
3541
- runHydrationRecoveryJob,
3542
- runHydrationTransactionJob,
3543
- runOuterCheckoutFsyncJob,
3544
- runProjectionMirrorJob,
3545
- runProjectionOuterApplyJob,
3546
- runProjectionReceiptJob,
3547
- synchronizeWorkspaceGit,
3548
- workspaceGitHydrationIsCurrent,
3549
- workspaceGitHydrationIsRecoverable,
3550
- workspaceGitMountData,
3551
- workspaceGitMountsForRemediation,
3552
- workspaceGitSyncTestHarness
3553
- };