amalgm 0.1.245 → 0.1.246

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 (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -7,47 +7,23 @@
7
7
  * no shell, pinned cwd, no prompts, and config armor so repo-configured
8
8
  * programs (hooks, fsmonitor) can never run under an executed transition.
9
9
  *
10
- * The verbs are deliberately few and exact — import verified objects,
11
- * move Git metadata with expected-old guards, and replace file/index
12
- * state only when the observed local state is unchanged. Anything messier
13
- * is a human act on one machine; the rail carries its settled outcome.
14
- * Every execution, success or failure, lands as one line in a visible
15
- * journal before control returns.
10
+ * The verbs are deliberately few and exact — fetch one branch from a
11
+ * named remote, fast-forward to an exact commit, apply or reverse a
12
+ * captured patch. Anything messier is a human act on one machine; the
13
+ * rail carries its settled outcome. Every execution, success or failure,
14
+ * lands as one line in a visible journal before control returns.
16
15
  */
17
16
 
18
17
  const fs = require('fs');
19
18
  const path = require('path');
20
- const crypto = require('crypto');
21
- const { execFile, execFileSync } = require('child_process');
19
+ const { execFile } = require('child_process');
22
20
  const { promisify } = require('util');
23
21
  const { AMALGM_DIR } = require('../config');
24
- const durable = require('./durable');
25
- const nativeFileTransaction = require('./native-file-transaction/client');
26
22
 
27
23
  const execFileAsync = promisify(execFile);
28
24
 
29
25
  const JOURNAL_FILE = path.join(AMALGM_DIR, 'state', 'workspace-transitions.jsonl');
30
- const FILE_TRANSACTION_CONTRACT = 'workspace-file-transaction@4';
31
- const GIT_METADATA_TRANSACTION_CONTRACT = 'workspace-git-metadata-transaction@1';
32
- const GIT_WORKTREE_NAMESPACE_GUARD_CONTRACT = 'workspace-git-worktree-namespace-guard@2';
33
- const APPLY_RECEIPT_DIRECTORY = /^\.amalgm-apply-[0-9a-f]{64}$/;
34
- const APPLY_RECEIPT_EXCLUDE = `.amalgm-apply-${'[0-9a-f]'.repeat(64)}/`;
35
-
36
- function processLifetime(pid) {
37
- return nativeFileTransaction.processLifetime(pid);
38
- }
39
-
40
- const PROCESS_LIFETIME = processLifetime(process.pid);
41
- if (!PROCESS_LIFETIME?.live) {
42
- throw new Error('workspace transitions cannot identify their process lifetime');
43
- }
44
- const PROCESS_START_IDENTITY = PROCESS_LIFETIME.identity;
45
-
46
- function guardOwnerIsActive(transaction) {
47
- const owner = processLifetime(transaction.ownerPid);
48
- if (owner === null) return true;
49
- return owner.live && owner.identity === transaction.ownerStarted;
50
- }
26
+ const HYDRATION_DIR = path.join(AMALGM_DIR, 'state', 'repo-hydration');
51
27
 
52
28
  const TRANSITION_ENV = {
53
29
  GIT_TERMINAL_PROMPT: '0',
@@ -78,43 +54,14 @@ function journal(entry) {
78
54
  }
79
55
  }
80
56
 
81
- function execFileWithInput(file, args, options, input) {
82
- return new Promise((resolve, reject) => {
83
- const child = execFile(file, args, options, (error, stdout, stderr) => {
84
- if (error) {
85
- error.stdout = stdout;
86
- error.stderr = stderr;
87
- reject(error);
88
- return;
89
- }
90
- resolve({ stdout, stderr });
91
- });
92
- child.stdin.on('error', () => {});
93
- child.stdin.end(input);
94
- });
95
- }
96
-
97
- async function run(
98
- action,
99
- worktreeRoot,
100
- args,
101
- {
102
- timeoutMs = 120_000,
103
- journalResult = true,
104
- input = null,
105
- env = {},
106
- } = {},
107
- ) {
57
+ async function run(action, worktreeRoot, args, { timeoutMs = 120_000, journalResult = true } = {}) {
108
58
  try {
109
- const options = {
59
+ const { stdout } = await execFileAsync('git', args, {
110
60
  cwd: worktreeRoot,
111
- env: { ...process.env, ...TRANSITION_ENV, ...env },
61
+ env: { ...process.env, ...TRANSITION_ENV },
112
62
  timeout: timeoutMs,
113
63
  maxBuffer: 32 * 1024 * 1024,
114
- };
115
- const { stdout } = input === null
116
- ? await execFileAsync('git', args, options)
117
- : await execFileWithInput('git', args, options, input);
64
+ });
118
65
  if (journalResult) journal({ action, worktree: worktreeRoot, args, ok: true });
119
66
  return stdout;
120
67
  } catch (error) {
@@ -126,6 +73,14 @@ async function run(
126
73
  }
127
74
  }
128
75
 
76
+ /** Bring one branch's objects and remote-tracking ref in from a named remote. */
77
+ function fetchBranch(worktreeRoot, remote, branch) {
78
+ return run('fetch-branch', worktreeRoot, [
79
+ '-C', worktreeRoot, 'fetch', '--no-tags', remote,
80
+ `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`,
81
+ ]);
82
+ }
83
+
129
84
  async function createBundle(worktreeRoot, oid, bundleFile, exportRef) {
130
85
  await run('prepare-bundle-ref', worktreeRoot, [
131
86
  '-C', worktreeRoot, 'update-ref', exportRef, oid,
@@ -173,18 +128,55 @@ async function initializeRepo(worktreeRoot, remote) {
173
128
  }
174
129
  }
175
130
 
176
- async function currentHead(worktreeRoot) {
177
- let oid = null;
178
- try {
179
- oid = (await run(
180
- 'observe-head',
131
+ async function checkoutInitialCard(worktreeRoot, { branch, oid }) {
132
+ await run('checkout-initial-card', worktreeRoot, branch
133
+ ? ['-C', worktreeRoot, 'checkout', '-B', branch, oid]
134
+ : ['-C', worktreeRoot, 'checkout', '--detach', oid], {
135
+ timeoutMs: 60_000,
136
+ });
137
+ const after = await currentHead(worktreeRoot);
138
+ if (after.branch !== (branch || null) || after.oid !== oid) {
139
+ throw new TransitionError(
140
+ 'checkout-initial-card',
181
141
  worktreeRoot,
182
- ['-C', worktreeRoot, 'rev-parse', '--verify', 'HEAD'],
183
- { journalResult: false },
184
- )).trim();
185
- } catch (error) {
186
- if (!(error instanceof TransitionError)) throw error;
142
+ `expected ${branch || '(detached)'}@${oid}, observed ${after.branch || '(detached)'}@${after.oid}`,
143
+ );
144
+ }
145
+ }
146
+
147
+ function holdInitialFiles(worktreeRoot) {
148
+ const entries = fs.readdirSync(worktreeRoot).filter((entry) => entry !== '.git');
149
+ if (!entries.length) return null;
150
+ fs.mkdirSync(HYDRATION_DIR, { recursive: true });
151
+ const holding = fs.mkdtempSync(path.join(HYDRATION_DIR, 'hold-'));
152
+ for (const entry of entries) {
153
+ fs.renameSync(path.join(worktreeRoot, entry), path.join(holding, entry));
187
154
  }
155
+ journal({ action: 'hold-initial-files', worktree: worktreeRoot, holding, entries, ok: true });
156
+ return holding;
157
+ }
158
+
159
+ function restoreInitialFiles(worktreeRoot, holding) {
160
+ if (!holding) return;
161
+ fs.cpSync(holding, worktreeRoot, { recursive: true, force: true, dereference: false });
162
+ fs.rmSync(holding, { recursive: true, force: true });
163
+ journal({ action: 'restore-initial-files', worktree: worktreeRoot, holding, ok: true });
164
+ }
165
+
166
+ /** Fast-forward the current branch to an exact commit. Refuses anything but a ff. */
167
+ function fastForwardTo(worktreeRoot, oid) {
168
+ return run('fast-forward', worktreeRoot, ['-C', worktreeRoot, 'merge', '--ff-only', oid], {
169
+ timeoutMs: 60_000,
170
+ });
171
+ }
172
+
173
+ async function currentHead(worktreeRoot) {
174
+ const oid = (await run(
175
+ 'observe-head',
176
+ worktreeRoot,
177
+ ['-C', worktreeRoot, 'rev-parse', '--verify', 'HEAD'],
178
+ { journalResult: false },
179
+ )).trim();
188
180
  let branch = null;
189
181
  try {
190
182
  branch = (await run(
@@ -199,1745 +191,211 @@ async function currentHead(worktreeRoot) {
199
191
  return { branch, oid };
200
192
  }
201
193
 
202
- async function branchOid(worktreeRoot, branch) {
203
- try {
204
- return (await run(
205
- 'observe-branch-oid',
206
- worktreeRoot,
207
- ['-C', worktreeRoot, 'rev-parse', '--verify', `refs/heads/${branch}`],
208
- { journalResult: false },
209
- )).trim();
210
- } catch (error) {
211
- if (error instanceof TransitionError) return null;
212
- throw error;
213
- }
214
- }
215
-
216
- function syncDirectory(directory) {
217
- const descriptor = fs.openSync(directory, 'r');
218
- try {
219
- fs.fsyncSync(descriptor);
220
- } finally {
221
- fs.closeSync(descriptor);
222
- }
223
- }
224
-
225
194
  /**
226
- * Change only HEAD's attachment, under Git's own HEAD lock. The index and
227
- * working files are deliberately untouched. A process death therefore
228
- * cannot discard local bytes; the reducer's durable apply record can replay
229
- * the remaining content-addressed steps.
195
+ * Materialize one exact card. Branch and OID are one instruction:
196
+ * either the expected old pair still exists and the target pair lands,
197
+ * or the command refuses without moving another branch.
230
198
  */
231
- async function compareAndSwapHead(worktreeRoot, {
199
+ async function moveToCard(worktreeRoot, {
232
200
  expectedBranch,
233
201
  expectedOid,
234
202
  targetBranch,
235
203
  targetOid,
236
- operation,
237
- }) {
238
- if (!/^[0-9a-f]{64}$/.test(operation || '')) {
239
- throw new Error('HEAD movement has no durable operation identity');
240
- }
241
- const expected = Buffer.from(expectedBranch
242
- ? `ref: refs/heads/${expectedBranch}\n`
243
- : `${expectedOid}\n`);
244
- const target = Buffer.from(targetBranch
245
- ? `ref: refs/heads/${targetBranch}\n`
246
- : `${targetOid}\n`);
247
- if (expected.equals(target)) return;
248
- applyGitMetadataTransaction(
249
- worktreeRoot,
250
- 'HEAD',
251
- expected,
252
- target,
253
- operation,
254
- );
255
- }
256
-
257
- /**
258
- * Move only repository metadata to an exact card.
259
- *
260
- * Branch refs use compare-and-swap. Switching attachment uses Git's HEAD
261
- * lock. Neither operation touches the index or working tree, so replay after
262
- * a crash is idempotent and cannot erase pending local work.
263
- */
264
- async function moveHeadToCard(worktreeRoot, {
265
- expectedBranch,
266
- expectedOid,
267
- expectedTargetOid,
268
- targetBranch,
269
- targetOid,
270
- operation,
271
204
  }) {
272
205
  const before = await currentHead(worktreeRoot);
273
- if (before.branch === (targetBranch || null) && before.oid === targetOid) {
274
- if (
275
- expectedBranch === (targetBranch || null)
276
- && expectedOid === targetOid
277
- ) return;
278
- if (
279
- /^[0-9a-f]{64}$/.test(operation || '')
280
- && (
281
- (
282
- expectedBranch === (targetBranch || null)
283
- && await branchReceiptCrossed(worktreeRoot, operation, targetBranch)
284
- )
285
- || (
286
- expectedBranch !== (targetBranch || null)
287
- && gitMetadataReceiptCrossed(worktreeRoot, operation, 'HEAD')
288
- )
289
- )
290
- ) return;
291
- throw new TransitionError(
292
- 'move-head-to-card',
293
- worktreeRoot,
294
- 'local HEAD reached the target without this ordered apply',
295
- );
296
- }
297
206
  if (before.branch !== expectedBranch || before.oid !== expectedOid) {
298
- throw new TransitionError('move-head-to-card', worktreeRoot, 'local card changed before movement');
299
- }
300
-
301
- if (targetBranch) {
302
- const observedTarget = await branchOid(worktreeRoot, targetBranch);
303
- if (observedTarget !== targetOid) {
304
- if (observedTarget !== (expectedTargetOid || null)) {
305
- throw new TransitionError(
306
- 'move-head-to-card',
307
- worktreeRoot,
308
- `target branch changed from ${expectedTargetOid || '(absent)'} to ${observedTarget || '(absent)'}`,
309
- );
310
- }
311
- await updateBranchRefs(worktreeRoot, [{
312
- branch: targetBranch,
313
- expectedOid: observedTarget,
314
- oid: targetOid,
315
- }], operation);
316
- }
207
+ throw new TransitionError('move-to-card', worktreeRoot, 'local card changed before movement');
317
208
  }
318
209
 
319
- if (!targetBranch || expectedBranch !== targetBranch) {
320
- await compareAndSwapHead(worktreeRoot, {
321
- expectedBranch,
322
- expectedOid,
323
- targetBranch,
324
- targetOid,
325
- operation,
326
- });
210
+ if (before.branch === targetBranch) {
211
+ await fastForwardTo(worktreeRoot, targetOid);
212
+ } else {
213
+ await run(
214
+ 'switch-card',
215
+ worktreeRoot,
216
+ ['-C', worktreeRoot, 'checkout', '-B', targetBranch, targetOid],
217
+ { timeoutMs: 60_000 },
218
+ );
327
219
  }
328
220
 
329
221
  const after = await currentHead(worktreeRoot);
330
- if (after.branch !== (targetBranch || null) || after.oid !== targetOid) {
222
+ if (after.branch !== targetBranch || after.oid !== targetOid) {
331
223
  throw new TransitionError(
332
- 'move-head-to-card',
224
+ 'move-to-card',
333
225
  worktreeRoot,
334
- `expected ${targetBranch || '(detached)'}@${targetOid}, observed ${after.branch || '(detached)'}@${after.oid}`,
226
+ `expected ${targetBranch}@${targetOid}, observed ${after.branch}@${after.oid}`,
335
227
  );
336
228
  }
337
229
  }
338
230
 
339
- async function emptyTree(worktreeRoot) {
340
- return (await run(
341
- 'empty-tree',
342
- worktreeRoot,
343
- ['-C', worktreeRoot, 'mktree'],
344
- { input: '', journalResult: false },
345
- )).trim();
231
+ /** Keep a commit reachable under one content-derived internal ref. */
232
+ function parkCommit(worktreeRoot, oid) {
233
+ const ref = `refs/amalgm/parked/${oid}`;
234
+ return run('park-commit', worktreeRoot, [
235
+ '-C', worktreeRoot, 'update-ref', ref, oid,
236
+ ]).then(() => ref);
346
237
  }
347
238
 
348
- async function ensureEmptyIndex(worktreeRoot, operation, testHooks = {}) {
349
- if (!/^[0-9a-f]{64}$/.test(operation || '')) {
350
- throw new Error('index initialization has no durable operation identity');
351
- }
352
- const value = (await run(
353
- 'observe-index-file',
354
- worktreeRoot,
355
- ['-C', worktreeRoot, 'rev-parse', '--git-path', 'index'],
356
- { journalResult: false },
357
- )).trim();
358
- const file = path.isAbsolute(value) ? value : path.resolve(worktreeRoot, value);
359
- if (gitMetadataReceiptCrossed(worktreeRoot, operation, 'index')) {
360
- if (testHooks.retainReceipt !== true) {
361
- releaseGitMetadataReceipts(worktreeRoot, operation, 'index');
362
- }
363
- return;
364
- }
365
- if (fs.existsSync(file)) return;
366
- if (typeof testHooks.afterAbsent === 'function') {
367
- await testHooks.afterAbsent();
368
- }
369
- const stateDir = path.join(AMALGM_DIR, 'state');
370
- fs.mkdirSync(stateDir, { recursive: true });
371
- const scratch = path.join(
372
- stateDir,
373
- `empty-index-${process.pid}-${crypto.randomBytes(8).toString('hex')}`,
374
- );
375
- try {
376
- await run(
377
- 'prepare-empty-index',
378
- worktreeRoot,
379
- ['-C', worktreeRoot, 'read-tree', '--empty'],
380
- { env: { GIT_INDEX_FILE: scratch } },
381
- );
382
- applyGitMetadataTransaction(
383
- worktreeRoot,
384
- 'index',
385
- null,
386
- fs.readFileSync(scratch),
387
- operation,
388
- );
389
- if (testHooks.retainReceipt !== true) {
390
- releaseGitMetadataReceipts(worktreeRoot, operation, 'index');
391
- }
392
- } finally {
393
- fs.rmSync(scratch, { force: true });
239
+ /** Delete internal parked refs that are no longer part of shared checkpoint state. */
240
+ async function pruneParkedCommits(worktreeRoot, keepOids) {
241
+ const output = await run('list-parked-commits', worktreeRoot, [
242
+ '-C', worktreeRoot, 'for-each-ref', '--format=%(refname)', 'refs/amalgm/parked',
243
+ ], { journalResult: false });
244
+ const prefix = 'refs/amalgm/parked/';
245
+ for (const ref of output.trim().split('\n').filter(Boolean)) {
246
+ if (!ref.startsWith(prefix) || keepOids.has(ref.slice(prefix.length))) continue;
247
+ await run('remove-parked-commit', worktreeRoot, [
248
+ '-C', worktreeRoot, 'update-ref', '-d', ref,
249
+ ]);
394
250
  }
395
251
  }
396
252
 
253
+ function setBranchRef(worktreeRoot, branch, oid) {
254
+ return run('set-branch-ref', worktreeRoot, [
255
+ '-C', worktreeRoot, 'update-ref', `refs/heads/${branch}`, oid,
256
+ ]);
257
+ }
258
+
259
+ function deleteBranchRef(worktreeRoot, branch) {
260
+ return run('delete-branch-ref', worktreeRoot, [
261
+ '-C', worktreeRoot, 'update-ref', '-d', `refs/heads/${branch}`,
262
+ ]);
263
+ }
264
+
397
265
  /**
398
- * Give an unborn repository its first exact card without checking out files.
399
- * Existing files remain local pending work and are materialized separately.
266
+ * Exact card movement after the caller has durably parked the old commit
267
+ * and captured the working tree. This deliberately discards the local
268
+ * checkout copy; the caller immediately pours the preserved work back as
269
+ * the checkpoint layer.
400
270
  */
401
- async function initializeHeadToCard(worktreeRoot, {
271
+ async function forceMoveToCard(worktreeRoot, {
272
+ expectedBranch,
273
+ expectedOid,
402
274
  targetBranch,
403
275
  targetOid,
404
- operation,
405
276
  }) {
406
- try {
407
- const current = await currentHead(worktreeRoot);
408
- if (current.branch === (targetBranch || null) && current.oid === targetOid) return;
409
- throw new TransitionError('initialize-head-to-card', worktreeRoot, 'repository already has another card');
410
- } catch (error) {
411
- if (!(error instanceof TransitionError) || error.action !== 'observe-head') throw error;
412
- }
413
-
414
- let expectedBranch = null;
415
- try {
416
- expectedBranch = (await run(
417
- 'observe-unborn-branch',
418
- worktreeRoot,
419
- ['-C', worktreeRoot, 'symbolic-ref', '--quiet', '--short', 'HEAD'],
420
- { journalResult: false },
421
- )).trim() || null;
422
- } catch (error) {
423
- if (!(error instanceof TransitionError)) throw error;
277
+ const before = await currentHead(worktreeRoot);
278
+ if (before.branch !== expectedBranch || before.oid !== expectedOid) {
279
+ throw new TransitionError('force-move-to-card', worktreeRoot, 'local card changed before movement');
424
280
  }
425
281
  if (targetBranch) {
426
- const observedTarget = await branchOid(worktreeRoot, targetBranch);
427
- if (observedTarget !== null && observedTarget !== targetOid) {
428
- throw new TransitionError(
429
- 'initialize-head-to-card',
430
- worktreeRoot,
431
- 'target branch already has another commit',
432
- );
433
- }
434
- if (observedTarget === null) {
435
- await updateBranchRefs(worktreeRoot, [{
436
- branch: targetBranch,
437
- expectedOid: null,
438
- oid: targetOid,
439
- }], operation);
440
- }
282
+ await run('force-move-to-card', worktreeRoot, [
283
+ '-C', worktreeRoot, 'checkout', '--force', '-B', targetBranch, targetOid,
284
+ ], { timeoutMs: 60_000 });
285
+ } else {
286
+ await run('force-move-to-card', worktreeRoot, [
287
+ '-C', worktreeRoot, 'checkout', '--force', '--detach', targetOid,
288
+ ], { timeoutMs: 60_000 });
441
289
  }
442
- await compareAndSwapHead(worktreeRoot, {
443
- expectedBranch,
444
- expectedOid: null,
445
- targetBranch,
446
- targetOid,
447
- operation,
448
- });
449
290
  const after = await currentHead(worktreeRoot);
450
291
  if (after.branch !== (targetBranch || null) || after.oid !== targetOid) {
451
- throw new TransitionError('initialize-head-to-card', worktreeRoot, 'initial card did not land');
452
- }
453
- }
454
-
455
- /**
456
- * Move a set of branch refs as one compare-and-swap transaction.
457
- * Each change is { branch, expectedOid, oid }; null expectedOid means create
458
- * only if absent, and null oid means delete only from the expected commit.
459
- */
460
- function branchReceiptPrefix(operation) {
461
- if (!/^[0-9a-f]{64}$/.test(operation || '')) {
462
- throw new Error('branch receipt has no operation identity');
463
- }
464
- return `refs/amalgm/apply/${operation}/branches`;
465
- }
466
-
467
- function branchReceiptRef(operation, branch) {
468
- if (typeof branch !== 'string' || branch.length === 0) {
469
- throw new Error('branch receipt has no branch identity');
470
- }
471
- const digest = crypto.createHash('sha256').update(branch).digest('hex');
472
- return `${branchReceiptPrefix(operation)}/${digest}`;
473
- }
474
-
475
- async function updateBranchRefs(
476
- worktreeRoot,
477
- changes,
478
- operation = null,
479
- testHooks = {},
480
- ) {
481
- if (changes.length === 0) return Promise.resolve('');
482
- const deletions = changes.filter((change) => change.oid === null);
483
- const nonDeletions = changes.filter((change) => change.oid !== null);
484
- if (deletions.length > 0 && nonDeletions.length > 0) {
485
- const checked = new Set(
486
- (await listedWorktreeHeads(worktreeRoot))
487
- .map((entry) => entry.branch)
488
- .filter(Boolean),
489
- );
490
- if (nonDeletions.some((change) => checked.has(change.branch))) {
491
- await updateBranchRefs(worktreeRoot, nonDeletions, operation);
492
- if (typeof testHooks.afterNonDeletionBatch === 'function') {
493
- await testHooks.afterNonDeletionBatch();
494
- }
495
- return updateBranchRefs(worktreeRoot, deletions, operation, testHooks);
496
- }
497
- }
498
- let input = 'start\0';
499
- for (const { branch, expectedOid, oid } of changes) {
500
- const ref = `refs/heads/${branch}`;
501
- if (oid === null) {
502
- input += `delete ${ref}\0${expectedOid}\0`;
503
- } else if (expectedOid === null) {
504
- input += `create ${ref}\0${oid}\0`;
505
- } else {
506
- input += `update ${ref}\0${oid}\0${expectedOid}\0`;
507
- }
508
- }
509
- if (operation !== null) {
510
- for (const change of changes) {
511
- const receiptOid = change.oid || change.expectedOid;
512
- if (!/^[0-9a-f]{40,64}$/.test(receiptOid || '')) {
513
- throw new Error('branch receipt has no object');
514
- }
515
- input += `create ${branchReceiptRef(operation, change.branch)}\0${receiptOid}\0`;
516
- }
517
- }
518
- input += 'prepare\0commit\0';
519
- const apply = (commonRoot = null) => run(
520
- 'update-branch-refs',
521
- worktreeRoot,
522
- commonRoot === null
523
- ? ['-C', worktreeRoot, 'update-ref', '--stdin', '-z']
524
- : [`--git-dir=${commonRoot}`, 'update-ref', '--stdin', '-z'],
525
- { input },
526
- );
527
- if (!changes.some((change) => change.oid === null)) return apply();
528
- return withGitHeadGuard(
529
- worktreeRoot,
530
- changes,
531
- testHooks,
532
- apply,
533
- );
534
- }
535
-
536
- async function branchReceiptCrossed(worktreeRoot, operation, branch = null) {
537
- try {
538
- if (branch === null) {
539
- const observed = await run(
540
- 'observe-branch-receipts',
541
- worktreeRoot,
542
- [
543
- '-C', worktreeRoot,
544
- 'for-each-ref',
545
- '--count=1',
546
- '--format=%(refname)',
547
- `${branchReceiptPrefix(operation)}/`,
548
- ],
549
- { journalResult: false },
550
- );
551
- return observed.trim() !== '';
552
- }
553
- await run(
554
- 'observe-branch-receipt',
292
+ throw new TransitionError(
293
+ 'force-move-to-card',
555
294
  worktreeRoot,
556
- ['-C', worktreeRoot, 'rev-parse', '--verify', branchReceiptRef(operation, branch)],
557
- { journalResult: false },
295
+ `expected ${targetBranch || '(detached)'}@${targetOid}, observed ${after.branch || '(detached)'}@${after.oid}`,
558
296
  );
559
- return true;
560
- } catch (error) {
561
- if (error instanceof TransitionError) return false;
562
- throw error;
563
297
  }
564
298
  }
565
299
 
566
- async function releaseBranchReceipt(worktreeRoot, operation) {
567
- const observed = await run(
568
- 'observe-branch-receipts',
569
- worktreeRoot,
570
- [
571
- '-C', worktreeRoot,
572
- 'for-each-ref',
573
- '--format=%(refname)',
574
- `${branchReceiptPrefix(operation)}/`,
575
- ],
576
- { journalResult: false },
577
- );
578
- for (const ref of observed.trim().split('\n').filter(Boolean)) {
579
- await run('release-branch-receipt', worktreeRoot, [
580
- '-C', worktreeRoot, 'update-ref', '-d', ref,
581
- ]);
582
- }
300
+ /**
301
+ * Apply a captured worktree patch — or reverse one — from a file on disk.
302
+ * The patch file lives outside the repo (in the state dir), so what was
303
+ * applied stays inspectable after the fact.
304
+ */
305
+ function applyPatch(worktreeRoot, patchFile, { reverse = false } = {}) {
306
+ return run(reverse ? 'reverse-patch' : 'apply-patch', worktreeRoot, [
307
+ '-C', worktreeRoot, 'apply', ...(reverse ? ['-R'] : []), '--whitespace=nowarn', patchFile,
308
+ ], { timeoutMs: 60_000 });
583
309
  }
584
310
 
585
311
  /**
586
- * Apply only the index change the reducer computed. The current exact index
587
- * bytes must first derive the reducer's sealed source tree. The patch must
588
- * then derive the named target tree before Git's index lock may replace those
589
- * exact bytes. A concurrent staged change therefore stops the movement
590
- * instead of becoming a new expected-old value.
312
+ * Apply only the index change the reducer computed. Git locks the index and
313
+ * verifies the patch's preimage, so a concurrent commit is serialized instead
314
+ * of being replaced by an old whole-index snapshot.
591
315
  */
592
- async function applyIndexPatch(
593
- worktreeRoot,
594
- patch,
595
- operation,
596
- { expectedTree, targetTree } = {},
597
- ) {
316
+ async function applyIndexPatch(worktreeRoot, patch) {
598
317
  if (!patch) return;
599
- if (!/^[0-9a-f]{64}$/.test(operation || '')) {
600
- throw new Error('index movement has no durable operation identity');
601
- }
602
- if (
603
- !/^[0-9a-f]{40,64}$/.test(expectedTree || '')
604
- || !/^[0-9a-f]{40,64}$/.test(targetTree || '')
605
- ) {
606
- throw new Error('index movement has no sealed tree endpoints');
607
- }
608
318
  const stateDir = path.join(AMALGM_DIR, 'state');
609
319
  fs.mkdirSync(stateDir, { recursive: true });
610
320
  const dir = fs.mkdtempSync(path.join(stateDir, 'index-patch-'));
611
321
  const patchFile = path.join(dir, 'patch');
612
- const scratchIndex = path.join(dir, 'index');
613
- const index = gitPath(worktreeRoot, 'index');
614
322
  try {
615
323
  fs.writeFileSync(patchFile, patch);
616
- const expected = fs.existsSync(index) ? fs.readFileSync(index) : null;
617
- if (expected === null) {
618
- await run(
619
- 'prepare-empty-index',
620
- worktreeRoot,
621
- ['-C', worktreeRoot, 'read-tree', '--empty'],
622
- { env: { GIT_INDEX_FILE: scratchIndex } },
623
- );
624
- } else {
625
- fs.writeFileSync(scratchIndex, expected);
626
- }
627
- const observedTree = (await run(
628
- 'verify-index-patch-source',
629
- worktreeRoot,
630
- ['-C', worktreeRoot, 'write-tree'],
631
- {
632
- env: { GIT_INDEX_FILE: scratchIndex },
633
- journalResult: false,
634
- },
635
- )).trim();
636
- if (observedTree !== expectedTree) {
637
- throw new TransitionError(
638
- 'apply-index-patch',
639
- worktreeRoot,
640
- `local index changed from sealed tree ${expectedTree} to ${observedTree}`,
641
- );
642
- }
643
324
  await run('apply-index-patch', worktreeRoot, [
644
325
  '-C', worktreeRoot, 'apply', '--cached', '--whitespace=nowarn', patchFile,
645
- ], { env: { GIT_INDEX_FILE: scratchIndex } });
646
- const appliedTree = (await run(
647
- 'verify-index-patch-target',
648
- worktreeRoot,
649
- ['-C', worktreeRoot, 'write-tree'],
650
- {
651
- env: { GIT_INDEX_FILE: scratchIndex },
652
- journalResult: false,
653
- },
654
- )).trim();
655
- if (appliedTree !== targetTree) {
656
- throw new Error(
657
- `index patch target mismatch: expected ${targetTree}, derived ${appliedTree}`,
658
- );
659
- }
660
- applyGitMetadataTransaction(
661
- worktreeRoot,
662
- 'index',
663
- expected,
664
- fs.readFileSync(scratchIndex),
665
- operation,
666
- );
326
+ ]);
667
327
  } finally {
668
328
  fs.rmSync(dir, { recursive: true, force: true });
669
329
  }
670
330
  }
671
331
 
332
+ /** Set only Git's index; working files are left exactly as they are. */
333
+ function setIndexTree(worktreeRoot, treeOid) {
334
+ return run('set-index-tree', worktreeRoot, [
335
+ '-C', worktreeRoot, 'read-tree', treeOid,
336
+ ]);
337
+ }
338
+
672
339
  /**
673
- * Apply one ordered file write set to a working tree. Each entry is
674
- * { path, content, mode } (content a
340
+ * Land a merged write set in a working tree — the coexistence engine's
341
+ * only mutation path. Each entry is { path, content, mode } (content a
675
342
  * Buffer, mode '100644' | '100755' | '120000') or { path, content: null }
676
343
  * for a deletion. Paths are confined to the worktree; the whole set lands
677
344
  * as one journal line.
678
345
  */
679
- function applyFiles(worktreeRoot, writes, label) {
680
- const root = fs.realpathSync(worktreeRoot);
681
- const operation = typeof label === 'string' && label
682
- ? label
683
- : `anonymous-${crypto.randomBytes(32).toString('hex')}`;
346
+ function applyMergedFiles(worktreeRoot, writes, label) {
347
+ const root = path.resolve(worktreeRoot);
684
348
  const written = [];
685
349
  const removed = [];
686
350
  try {
687
- ensureFileTransactionExcluded(root);
688
- recoverFileTransactions(root);
689
351
  for (const write of writes) {
690
- applyFileTransaction(root, write, operation);
691
- if (write.content === null) removed.push(write.path);
692
- else written.push(write.path);
693
- }
694
- journal({ action: 'apply-files', worktree: root, operation, written, removed, ok: true });
695
- return { operation };
696
- } catch (error) {
697
- journal({
698
- action: 'apply-files', worktree: root, operation, written, removed, ok: false,
699
- error: String(error.message).slice(0, 500),
700
- });
701
- throw new TransitionError('apply-files', root, String(error.message));
702
- }
703
- }
704
-
705
- function gitDirectory(worktreeRoot) {
706
- const marker = path.join(worktreeRoot, '.git');
707
- if (fs.statSync(marker).isDirectory()) return marker;
708
- const match = fs.readFileSync(marker, 'utf8').trim().match(/^gitdir:\s*(.+)$/);
709
- if (!match) throw new Error('worktree has no Git directory');
710
- return path.resolve(worktreeRoot, match[1]);
711
- }
712
-
713
- function gitPath(worktreeRoot, relativePath) {
714
- const value = execFileSync(
715
- 'git',
716
- ['-C', worktreeRoot, 'rev-parse', '--git-path', relativePath],
717
- {
718
- cwd: worktreeRoot,
719
- env: { ...process.env, ...TRANSITION_ENV },
720
- encoding: 'utf8',
721
- timeout: 120_000,
722
- maxBuffer: 32 * 1024 * 1024,
723
- },
724
- ).trim();
725
- return path.isAbsolute(value) ? value : path.resolve(worktreeRoot, value);
726
- }
727
-
728
- function fileTransactionDirectory(worktreeRoot) {
729
- return path.join(gitDirectory(worktreeRoot), 'amalgm-apply');
730
- }
731
-
732
- /**
733
- * Receipts live beside their destination so the kernel can consume prepared
734
- * targets atomically even when a linked worktree's Git directory is on
735
- * another device. They are implementation state, never workspace content.
736
- */
737
- function isApplyReceiptPath(relativePath) {
738
- return String(relativePath)
739
- .split(/[\\/]/)
740
- .some((component) => APPLY_RECEIPT_DIRECTORY.test(component));
741
- }
742
-
743
- function ensureFileTransactionExcluded(worktreeRoot) {
744
- // Linked worktrees have a private Git directory, but Git reads exclusions
745
- // from the common path named by `--git-path`. The command is the authority;
746
- // reconstructing this path from `.git` makes receipt bytes look user-owned.
747
- const exclude = gitPath(worktreeRoot, 'info/exclude');
748
- let content = '';
749
- let mode = 0o644;
750
- try {
751
- content = fs.readFileSync(exclude, 'utf8');
752
- mode = fs.statSync(exclude).mode & 0o777;
753
- } catch (error) {
754
- if (error?.code !== 'ENOENT') throw error;
755
- }
756
- if (content.split(/\r?\n/).includes(APPLY_RECEIPT_EXCLUDE)) return;
757
- fs.mkdirSync(path.dirname(exclude), { recursive: true });
758
- // Append instead of replacing a read snapshot: a concurrent user edit to
759
- // info/exclude must survive. A racing apply may append the same harmless
760
- // rule twice, while every later call observes it and stops.
761
- const descriptor = fs.openSync(exclude, 'a', mode);
762
- try {
763
- fs.writeFileSync(descriptor, `\n${APPLY_RECEIPT_EXCLUDE}\n`);
764
- fs.fsyncSync(descriptor);
765
- } finally {
766
- fs.closeSync(descriptor);
767
- }
768
- syncDirectory(path.dirname(exclude));
769
- }
770
-
771
- function ensureDurableDirectory(directory) {
772
- try {
773
- fs.mkdirSync(directory);
774
- syncDirectory(path.dirname(directory));
775
- } catch (error) {
776
- if (error?.code !== 'EEXIST') throw error;
777
- if (!fs.statSync(directory).isDirectory()) {
778
- throw new Error(`workspace transaction path is not a directory: ${directory}`);
779
- }
780
- }
781
- }
782
-
783
- function confinedPath(worktreeRoot, relativePath) {
784
- if (
785
- typeof relativePath !== 'string'
786
- || !relativePath
787
- || path.isAbsolute(relativePath)
788
- || relativePath.split(/[\\/]/).includes('..')
789
- ) throw new Error(`unsafe path: ${relativePath}`);
790
- const root = fs.realpathSync(worktreeRoot);
791
- const absolute = path.resolve(root, relativePath);
792
- if (!absolute.startsWith(root + path.sep)) throw new Error(`unsafe path: ${relativePath}`);
793
- let current = root;
794
- for (const component of relativePath.split(/[\\/]/).slice(0, -1)) {
795
- if (!component || component === '.') continue;
796
- current = path.join(current, component);
797
- let stat;
798
- try {
799
- stat = fs.lstatSync(current);
800
- } catch (error) {
801
- if (error?.code === 'ENOENT') break;
802
- throw error;
803
- }
804
- if (stat.isSymbolicLink()) {
805
- throw new Error(`unsafe symlink parent: ${relativePath}`);
806
- }
807
- if (!stat.isDirectory()) throw new Error(`unsafe non-directory parent: ${relativePath}`);
808
- }
809
- return absolute;
810
- }
811
-
812
- function fileStateIdentity(state) {
813
- if (state === null) return null;
814
- if (state?.kind === 'other') return { kind: 'other' };
815
- if (!Buffer.isBuffer(state?.content)) throw new Error('file state has no exact bytes');
816
- return {
817
- kind: state.kind,
818
- exec: state.exec === true,
819
- bytes: state.content.length,
820
- sha256: crypto.createHash('sha256').update(state.content).digest('hex'),
821
- };
822
- }
823
-
824
- function transactionPaths(worktreeRoot, operation, relativePath, rootStat) {
825
- const recordDirectory = fileTransactionDirectory(worktreeRoot);
826
- ensureDurableDirectory(recordDirectory);
827
- const id = crypto.createHash('sha256')
828
- .update(
829
- `${FILE_TRANSACTION_CONTRACT}\0${rootStat.dev}\0${rootStat.ino}`
830
- + `\0${operation}\0${relativePath}`,
831
- )
832
- .digest('hex');
833
- return {
834
- id,
835
- recordDirectory,
836
- record: path.join(recordDirectory, `${id}.json`),
837
- };
838
- }
839
-
840
- function removeTransactionRecord(record) {
841
- try {
842
- fs.unlinkSync(record);
843
- } catch (error) {
844
- if (error?.code !== 'ENOENT') throw error;
845
- }
846
- syncDirectory(path.dirname(record));
847
- }
848
-
849
- function readFileTransaction(record) {
850
- try {
851
- const value = JSON.parse(fs.readFileSync(record, 'utf8'));
852
- if (
853
- value?.contract !== FILE_TRANSACTION_CONTRACT
854
- || typeof value.root !== 'string'
855
- || !/^\d+$/.test(value.rootDev)
856
- || !/^\d+$/.test(value.rootIno)
857
- || typeof value.path !== 'string'
858
- || typeof value.id !== 'string'
859
- || typeof value.operation !== 'string'
860
- || !value.operation
861
- || value.id !== path.basename(record, '.json')
862
- || !/^[a-f0-9]{64}$/.test(value.id)
863
- || (value.expected !== null && typeof value.expected !== 'object')
864
- || (value.target !== null && typeof value.target !== 'object')
865
- ) throw new Error('invalid file transaction');
866
- return { ...value, record };
867
- } catch (error) {
868
- throw new Error(`invalid workspace file transaction ${record}: ${error.message}`);
869
- }
870
- }
871
-
872
- function runNativeFileTransaction(transaction, action, content = null, testHooks = {}) {
873
- try {
874
- return nativeFileTransaction.run(transaction, action, content, testHooks);
875
- } catch (error) {
876
- throw new Error(
877
- String(error?.message || error).replace(
878
- /^native file transaction/,
879
- 'native workspace file transaction',
880
- ),
881
- );
882
- }
883
- }
884
-
885
- function fileTransactionRecords(worktreeRoot, operation = null) {
886
- const directory = fileTransactionDirectory(worktreeRoot);
887
- if (!fs.existsSync(directory)) return [];
888
- return fs.readdirSync(directory)
889
- .filter((name) => name.endsWith('.json'))
890
- .sort()
891
- .map((name) => readFileTransaction(path.join(directory, name)))
892
- .filter((transaction) => operation === null || transaction.operation === operation);
893
- }
894
-
895
- /**
896
- * Resolve an interrupted expected-source → official-target file movement
897
- * before any new repo observation. The retained source is either the exact
898
- * expected floor (safe to finish/rollback) or a third value (a local save
899
- * that is restored without overwriting anything newer).
900
- */
901
- function recoverFileTransactions(worktreeRoot) {
902
- const root = fs.realpathSync(worktreeRoot);
903
- ensureFileTransactionExcluded(root);
904
- for (const transaction of fileTransactionRecords(root)) {
905
- if (transaction.root !== root) {
906
- throw new Error(`workspace file transaction escaped its repository: ${transaction.record}`);
907
- }
908
- const result = runNativeFileTransaction(transaction, 'recover');
909
- if (result.status === 'pending') {
910
- throw new Error(
911
- `workspace file transaction is waiting for local writer ownership: `
912
- + `${transaction.path}: ${result.reason || 'pending'}`,
913
- );
914
- }
915
- // A crossed operation is evidence until the verified cursor passes it.
916
- // Only a transaction proven not to have crossed can retire here.
917
- if (result.status === 'changed') removeTransactionRecord(transaction.record);
918
- }
919
- }
920
-
921
- function sameTransaction(left, right) {
922
- return (
923
- left.contract === right.contract
924
- && left.root === right.root
925
- && left.rootDev === right.rootDev
926
- && left.rootIno === right.rootIno
927
- && left.path === right.path
928
- && left.id === right.id
929
- && left.operation === right.operation
930
- && JSON.stringify(left.expected) === JSON.stringify(right.expected)
931
- && JSON.stringify(left.target) === JSON.stringify(right.target)
932
- );
933
- }
934
-
935
- function applyFileTransaction(worktreeRoot, write, operation) {
936
- if (typeof operation !== 'string' || !operation || operation.length > 1024) {
937
- throw new Error('workspace file transaction has no operation identity');
938
- }
939
- const rootStat = fs.statSync(worktreeRoot, { bigint: true });
940
- const paths = transactionPaths(worktreeRoot, operation, write.path, rootStat);
941
- const target = write.content === null
942
- ? null
943
- : {
944
- ...fileStateIdentity({
945
- kind: write.mode === '120000' ? 'symlink' : 'file',
946
- content: write.content,
947
- exec: write.mode === '100755',
948
- }),
949
- };
950
- const transaction = {
951
- contract: FILE_TRANSACTION_CONTRACT,
952
- root: worktreeRoot,
953
- rootDev: rootStat.dev.toString(),
954
- rootIno: rootStat.ino.toString(),
955
- path: write.path,
956
- id: paths.id,
957
- operation,
958
- expected: fileStateIdentity(write.expected),
959
- target,
960
- };
961
- if (fs.existsSync(paths.record)) {
962
- const retained = readFileTransaction(paths.record);
963
- if (!sameTransaction(retained, transaction)) {
964
- throw new Error(`workspace file transaction identity changed: ${write.path}`);
965
- }
966
- const recovered = runNativeFileTransaction(retained, 'recover');
967
- if (recovered.status === 'applied') return;
968
- if (recovered.status === 'pending') {
969
- throw new Error(
970
- `local writer ownership is unresolved: ${write.path}: `
971
- + `${recovered.reason || 'pending'}`,
972
- );
973
- }
974
- removeTransactionRecord(paths.record);
975
- }
976
- durable.writeJson(paths.record, transaction);
977
- const result = runNativeFileTransaction(
978
- transaction,
979
- 'apply',
980
- write.content,
981
- {
982
- pauseAfterParentOpen: Number(write.testPauseAfterParentOpenMillis || 0),
983
- pauseAfterExtraction: Number(write.testPauseAfterExtractionMillis || 0),
984
- pauseBeforeInstall: Number(write.testPauseBeforeInstallMillis || 0),
985
- crashAfterExtraction: write.testCrashAfterExtraction === true,
986
- crashBeforeInstall: write.testCrashBeforeInstall === true,
987
- },
988
- );
989
- if (result.status === 'pending') {
990
- throw new Error(
991
- `local writer ownership is unresolved: ${write.path}: ${result.reason || 'pending'}`,
992
- );
993
- }
994
- if (result.status === 'changed') {
995
- removeTransactionRecord(paths.record);
996
- throw new Error(`local file changed before ordered apply: ${write.path}`);
997
- }
998
- }
999
-
1000
- /**
1001
- * The transaction artifact, not ambient file metadata, says whether an
1002
- * operation crossed. A later local write may return the pathname to either
1003
- * endpoint without changing this answer.
1004
- */
1005
- function crossedFileReceiptPaths(worktreeRoot, operation, relativePaths) {
1006
- const root = fs.realpathSync(worktreeRoot);
1007
- const requested = new Set(relativePaths);
1008
- const crossed = new Set();
1009
- recoverFileTransactions(root);
1010
- for (const transaction of fileTransactionRecords(root, operation)) {
1011
- if (!requested.has(transaction.path)) continue;
1012
- const result = runNativeFileTransaction(transaction, 'receipt');
1013
- if (result.status === 'crossed') crossed.add(transaction.path);
1014
- else if (result.status !== 'not-crossed') {
1015
- throw new Error(
1016
- `workspace file receipt is unresolved: ${transaction.path}: `
1017
- + `${result.reason || result.status}`,
1018
- );
1019
- }
1020
- }
1021
- return crossed;
1022
- }
1023
-
1024
- /**
1025
- * Receipt retirement is safe only after the caller's verified cursor is
1026
- * durable. Repeating release after any crash is harmless.
1027
- */
1028
- function releaseFileReceipts(worktreeRoot, operation) {
1029
- const root = fs.realpathSync(worktreeRoot);
1030
- for (const transaction of fileTransactionRecords(root, operation)) {
1031
- const result = runNativeFileTransaction(transaction, 'release');
1032
- if (result.status === 'pending') {
1033
- throw new Error(
1034
- `workspace file receipt cannot be released: ${transaction.path}: `
1035
- + `${result.reason || 'pending'}`,
1036
- );
1037
- }
1038
- if (result.status !== 'released') {
1039
- throw new Error(
1040
- `workspace file receipt release failed: ${transaction.path}: ${result.status}`,
1041
- );
1042
- }
1043
- removeTransactionRecord(transaction.record);
1044
- }
1045
- }
1046
-
1047
- function gitMetadataTransactionDirectoryForRoot(root) {
1048
- return path.join(fs.realpathSync(root), 'amalgm-metadata-apply');
1049
- }
1050
-
1051
- function gitMetadataTransactionDirectory(worktreeRoot) {
1052
- return gitMetadataTransactionDirectoryForRoot(gitDirectory(worktreeRoot));
1053
- }
1054
-
1055
- async function listedWorktreeHeads(worktreeRoot) {
1056
- const output = await run(
1057
- 'observe-worktree-heads',
1058
- worktreeRoot,
1059
- ['-C', worktreeRoot, 'worktree', 'list', '--porcelain', '-z'],
1060
- { journalResult: false },
1061
- );
1062
- const entries = [];
1063
- let current = null;
1064
- for (const token of output.split('\0')) {
1065
- if (!token) {
1066
- if (current?.path) entries.push(current);
1067
- current = null;
1068
- } else if (token.startsWith('worktree ')) {
1069
- current = { path: token.slice('worktree '.length), branch: null };
1070
- } else if (current && token.startsWith('branch refs/heads/')) {
1071
- current.branch = token.slice('branch refs/heads/'.length);
1072
- }
1073
- }
1074
- if (current?.path) entries.push(current);
1075
- return entries;
1076
- }
1077
-
1078
- async function withOneGitHeadGuard(
1079
- gitRoot,
1080
- label,
1081
- changes,
1082
- guarded,
1083
- ) {
1084
- const root = fs.realpathSync(gitRoot);
1085
- recoverGitMetadataTransactionsAtRoot(root, label);
1086
- const head = path.join(root, 'HEAD');
1087
- const expected = fs.readFileSync(head);
1088
- const attached = expected.toString('utf8').match(/^ref: refs\/heads\/(.+)\n$/)?.[1] || null;
1089
- const deleted = new Set(
1090
- changes.filter((change) => change.oid === null).map((change) => change.branch),
1091
- );
1092
- if (attached && deleted.has(attached)) {
1093
- throw new TransitionError(
1094
- 'guard-branch-delete',
1095
- label,
1096
- `HEAD is attached to branch ${attached}`,
1097
- );
1098
- }
1099
- // A logical branch operation may be attempted concurrently. Its transient
1100
- // guard artifacts must still have exactly one process owner.
1101
- const guardOperation = crypto.randomBytes(32).toString('hex');
1102
- const rootStat = fs.statSync(root, { bigint: true });
1103
- const paths = gitMetadataTransactionPathsForRoot(
1104
- root,
1105
- guardOperation,
1106
- 'HEAD',
1107
- rootStat,
1108
- );
1109
- const identity = gitMetadataIdentity(expected);
1110
- const transaction = {
1111
- contract: GIT_METADATA_TRANSACTION_CONTRACT,
1112
- backend: 'git-guard',
1113
- root,
1114
- rootDev: rootStat.dev.toString(),
1115
- rootIno: rootStat.ino.toString(),
1116
- path: 'HEAD',
1117
- id: paths.id,
1118
- operation: guardOperation,
1119
- ownerPid: process.pid,
1120
- ownerStarted: PROCESS_START_IDENTITY,
1121
- expected: identity,
1122
- target: identity,
1123
- record: paths.record,
1124
- prepared: paths.target,
1125
- claimed: paths.claimed,
1126
- };
1127
-
1128
- if (fs.existsSync(paths.record)) {
1129
- const retained = readGitMetadataTransaction(paths.record);
1130
- if (!sameGitMetadataTransaction(retained, transaction)) {
1131
- throw new Error('Git HEAD guard identity changed');
1132
- }
1133
- if (guardOwnerIsActive(retained)) {
1134
- throw new TransitionError(
1135
- 'recover-head-guard',
1136
- label,
1137
- `guard owner ${retained.ownerPid} is still active`,
1138
- );
1139
- }
1140
- const recovered = runNativeFileTransaction(retained, 'recover');
1141
- if (recovered.status !== 'released') {
1142
- throw new TransitionError(
1143
- 'recover-head-guard',
1144
- label,
1145
- recovered.reason || recovered.status,
1146
- );
1147
- }
1148
- removeGitMetadataTransaction(retained);
1149
- }
1150
-
1151
- durable.writeJson(paths.record, {
1152
- contract: transaction.contract,
1153
- backend: transaction.backend,
1154
- root: transaction.root,
1155
- rootDev: transaction.rootDev,
1156
- rootIno: transaction.rootIno,
1157
- path: transaction.path,
1158
- id: transaction.id,
1159
- operation: transaction.operation,
1160
- ownerPid: transaction.ownerPid,
1161
- ownerStarted: transaction.ownerStarted,
1162
- expected: transaction.expected,
1163
- target: transaction.target,
1164
- });
1165
- durable.writeFile(paths.target, expected, 0o644);
1166
- const claimed = runNativeFileTransaction(transaction, 'guard');
1167
- if (claimed.status !== 'guarded') {
1168
- removeGitMetadataTransaction(transaction);
1169
- throw new TransitionError(
1170
- 'guard-branch-delete',
1171
- label,
1172
- claimed.reason || claimed.status,
1173
- );
1174
- }
1175
- try {
1176
- return await guarded();
1177
- } finally {
1178
- const released = runNativeFileTransaction(transaction, 'unguard');
1179
- if (released.status !== 'released') {
1180
- throw new Error(`Git HEAD guard release failed: ${released.reason || released.status}`);
1181
- }
1182
- removeGitMetadataTransaction(transaction);
1183
- }
1184
- }
1185
-
1186
- function gitWorktreeNamespaceTransactionDirectory() {
1187
- return path.join(AMALGM_DIR, 'state', 'workspace-git-worktree-guards');
1188
- }
1189
-
1190
- function physicalDirectoryIdentity(directory) {
1191
- const realpath = fs.realpathSync(directory);
1192
- const stat = fs.statSync(realpath, { bigint: true });
1193
- if (!stat.isDirectory()) {
1194
- throw new Error(`physical directory identity is not a directory: ${realpath}`);
1195
- }
1196
- return {
1197
- realpath,
1198
- dev: stat.dev.toString(),
1199
- ino: stat.ino.toString(),
1200
- };
1201
- }
1202
-
1203
- function pinPhysicalDirectory(directory) {
1204
- const realpath = fs.realpathSync(directory);
1205
- const fd = fs.openSync(
1206
- realpath,
1207
- fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_DIRECTORY,
1208
- );
1209
- try {
1210
- const stat = fs.fstatSync(fd, { bigint: true });
1211
- if (!stat.isDirectory()) {
1212
- throw new Error(`physical directory pin is not a directory: ${realpath}`);
1213
- }
1214
- return {
1215
- fd,
1216
- realpath,
1217
- dev: stat.dev.toString(),
1218
- ino: stat.ino.toString(),
1219
- };
1220
- } catch (error) {
1221
- fs.closeSync(fd);
1222
- throw error;
1223
- }
1224
- }
1225
-
1226
- function closePhysicalDirectoryPins(pins) {
1227
- for (const pin of [...pins].reverse()) fs.closeSync(pin.fd);
1228
- }
1229
-
1230
- function samePhysicalDirectory(left, right) {
1231
- return (
1232
- left !== null
1233
- && right !== null
1234
- && left.dev === right.dev
1235
- && left.ino === right.ino
1236
- );
1237
- }
1238
-
1239
- function gitCommonDirectory(worktreeRoot) {
1240
- const value = execFileSync(
1241
- 'git',
1242
- ['-C', worktreeRoot, 'rev-parse', '--git-common-dir'],
1243
- {
1244
- cwd: worktreeRoot,
1245
- env: { ...process.env, ...TRANSITION_ENV },
1246
- encoding: 'utf8',
1247
- timeout: 120_000,
1248
- maxBuffer: 32 * 1024 * 1024,
1249
- },
1250
- ).trim();
1251
- return fs.realpathSync(path.isAbsolute(value) ? value : path.resolve(worktreeRoot, value));
1252
- }
1253
-
1254
- function gitCommonDirectoryFromMarker(worktreeRoot) {
1255
- const root = fs.realpathSync(worktreeRoot);
1256
- const marker = path.join(root, '.git');
1257
- if (fs.statSync(marker).isDirectory()) return fs.realpathSync(marker);
1258
- const match = fs.readFileSync(marker, 'utf8').trim().match(/^gitdir:\s*(.+)$/);
1259
- if (!match) throw new Error('worktree has no Git directory');
1260
- const privateDirectory = path.resolve(root, match[1]);
1261
- const registrations = path.dirname(privateDirectory);
1262
- return fs.realpathSync(
1263
- path.basename(registrations) === 'worktrees'
1264
- ? path.dirname(registrations)
1265
- : privateDirectory,
1266
- );
1267
- }
1268
-
1269
- function physicalGitWorktreeRegistrations(directory) {
1270
- return fs.readdirSync(directory)
1271
- .sort()
1272
- .map((name) => {
1273
- const stat = fs.lstatSync(path.join(directory, name), { bigint: true });
1274
- if (!stat.isDirectory()) {
1275
- throw new Error(`Git worktree registration is not a directory: ${name}`);
1276
- }
1277
- return {
1278
- name,
1279
- dev: stat.dev.toString(),
1280
- ino: stat.ino.toString(),
1281
- };
1282
- });
1283
- }
1284
-
1285
- function ensureGitWorktreeNamespace(commonRoot) {
1286
- const directory = path.join(commonRoot, 'worktrees');
1287
- try {
1288
- fs.statSync(directory);
1289
- } catch (error) {
1290
- if (error?.code !== 'ENOENT') throw error;
1291
- fs.mkdirSync(directory, { mode: 0o755 });
1292
- syncDirectory(commonRoot);
1293
- }
1294
- return directory;
1295
- }
1296
-
1297
- function pinGitWorktreeNamespace(commonRoot) {
1298
- const directory = ensureGitWorktreeNamespace(commonRoot);
1299
- const pins = [];
1300
- try {
1301
- const namespace = pinPhysicalDirectory(directory);
1302
- pins.push(namespace);
1303
- const entries = fs.readdirSync(directory)
1304
- .sort()
1305
- .map((name) => {
1306
- const pin = pinPhysicalDirectory(path.join(directory, name));
1307
- pins.push(pin);
1308
- return { name, dev: pin.dev, ino: pin.ino };
1309
- });
1310
- return { namespace, entries, pins };
1311
- } catch (error) {
1312
- closePhysicalDirectoryPins(pins);
1313
- throw error;
1314
- }
1315
- }
1316
-
1317
- function gitWorktreeNamespacePaths(commonRoot, operation, rootStat) {
1318
- const receiptDirectory = path.join(commonRoot, 'amalgm-metadata-apply');
1319
- const recordDirectory = gitWorktreeNamespaceTransactionDirectory();
1320
- ensureDurableDirectory(receiptDirectory);
1321
- ensureDurableDirectory(AMALGM_DIR);
1322
- ensureDurableDirectory(path.join(AMALGM_DIR, 'state'));
1323
- ensureDurableDirectory(recordDirectory);
1324
- const id = crypto.createHash('sha256')
1325
- .update(
1326
- `${GIT_WORKTREE_NAMESPACE_GUARD_CONTRACT}\0${rootStat.dev}\0${rootStat.ino}`
1327
- + `\0${operation}\0worktrees`,
1328
- )
1329
- .digest('hex');
1330
- return {
1331
- id,
1332
- record: path.join(recordDirectory, `${id}.json`),
1333
- target: path.join(receiptDirectory, `${id}.target`),
1334
- claimed: path.join(receiptDirectory, `${id}.claimed`),
1335
- };
1336
- }
1337
-
1338
- function readGitWorktreeNamespaceTransaction(record) {
1339
- try {
1340
- const value = JSON.parse(fs.readFileSync(record, 'utf8'));
1341
- if (
1342
- value?.contract !== GIT_WORKTREE_NAMESPACE_GUARD_CONTRACT
1343
- || value.backend !== 'git-worktree-guard'
1344
- || typeof value.worktree !== 'string'
1345
- || !path.isAbsolute(value.worktree)
1346
- || !/^\d+$/.test(value.worktreeDev)
1347
- || !/^\d+$/.test(value.worktreeIno)
1348
- || typeof value.root !== 'string'
1349
- || !path.isAbsolute(value.root)
1350
- || !/^\d+$/.test(value.rootDev)
1351
- || !/^\d+$/.test(value.rootIno)
1352
- || value.path !== 'worktrees'
1353
- || value.id !== path.basename(record, '.json')
1354
- || !/^[a-f0-9]{64}$/.test(value.id || '')
1355
- || !/^[a-f0-9]{64}$/.test(value.operation || '')
1356
- || !Number.isSafeInteger(value.ownerPid)
1357
- || value.ownerPid < 1
1358
- || typeof value.ownerStarted !== 'string'
1359
- || !value.ownerStarted
1360
- || value.expected?.kind !== 'directory'
1361
- || !/^\d+$/.test(value.expected?.dev || '')
1362
- || !/^\d+$/.test(value.expected?.ino || '')
1363
- || value.target?.kind !== 'file'
1364
- || !Array.isArray(value.entries)
1365
- || value.entries.some((entry) => (
1366
- entry === null
1367
- || typeof entry !== 'object'
1368
- || typeof entry.name !== 'string'
1369
- || !entry.name
1370
- || entry.name === '.'
1371
- || entry.name === '..'
1372
- || entry.name.includes('/')
1373
- || entry.name.includes('\\')
1374
- || !/^\d+$/.test(entry.dev || '')
1375
- || !/^\d+$/.test(entry.ino || '')
1376
- ))
1377
- ) throw new Error('invalid Git worktree namespace transaction');
1378
- return {
1379
- ...value,
1380
- record,
1381
- prepared: path.join(value.root, 'amalgm-metadata-apply', `${value.id}.target`),
1382
- claimed: path.join(value.root, 'amalgm-metadata-apply', `${value.id}.claimed`),
1383
- };
1384
- } catch (error) {
1385
- throw new Error(
1386
- `invalid workspace Git worktree namespace transaction ${record}: ${error.message}`,
1387
- );
1388
- }
1389
- }
1390
-
1391
- function gitWorktreeNamespaceTransactionRecords(worktreeRoot) {
1392
- const directory = gitWorktreeNamespaceTransactionDirectory();
1393
- if (!fs.existsSync(directory)) return [];
1394
- const requested = physicalDirectoryIdentity(worktreeRoot);
1395
- let common = null;
1396
- try {
1397
- common = physicalDirectoryIdentity(gitCommonDirectoryFromMarker(worktreeRoot));
1398
- } catch {
1399
- // A nonstandard Git layout may require Git's own common-dir resolution.
1400
- // That is available whenever the shared registration namespace is open.
1401
- try {
1402
- common = physicalDirectoryIdentity(gitCommonDirectory(worktreeRoot));
1403
- } catch {
1404
- // The checkout identity may still surface a record for diagnosis, but
1405
- // only a resolved matching common repository can authorize recovery.
1406
- }
1407
- }
1408
- return fs.readdirSync(directory)
1409
- .filter((name) => name.endsWith('.json'))
1410
- .sort()
1411
- .map((name) => readGitWorktreeNamespaceTransaction(path.join(directory, name)))
1412
- .map((transaction) => {
1413
- const worktreeMatches = samePhysicalDirectory(
1414
- { dev: transaction.worktreeDev, ino: transaction.worktreeIno },
1415
- requested,
1416
- );
1417
- const commonMatches = samePhysicalDirectory(
1418
- { dev: transaction.rootDev, ino: transaction.rootIno },
1419
- common,
1420
- );
1421
- if (!worktreeMatches && !commonMatches) return null;
1422
- if (!commonMatches) {
1423
- throw new Error(
1424
- `workspace Git worktree guard belongs to another common repository: `
1425
- + `${transaction.record}`,
1426
- );
352
+ const abs = path.resolve(root, write.path);
353
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
354
+ throw new Error(`unsafe path: ${write.path}`);
1427
355
  }
1428
- return {
1429
- ...transaction,
1430
- root: common.realpath,
1431
- prepared: path.join(
1432
- common.realpath,
1433
- 'amalgm-metadata-apply',
1434
- `${transaction.id}.target`,
1435
- ),
1436
- claimed: path.join(
1437
- common.realpath,
1438
- 'amalgm-metadata-apply',
1439
- `${transaction.id}.claimed`,
1440
- ),
1441
- };
1442
- })
1443
- .filter(Boolean);
1444
- }
1445
-
1446
- function removeGitWorktreeNamespaceTransaction(transaction) {
1447
- try {
1448
- fs.unlinkSync(transaction.record);
1449
- } catch (error) {
1450
- if (error?.code !== 'ENOENT') throw error;
1451
- }
1452
- if (fs.existsSync(path.dirname(transaction.record))) {
1453
- syncDirectory(path.dirname(transaction.record));
1454
- }
1455
- }
1456
-
1457
- function recoverGitWorktreeNamespaceTransactions(worktreeRoot) {
1458
- for (const transaction of gitWorktreeNamespaceTransactionRecords(worktreeRoot)) {
1459
- if (guardOwnerIsActive(transaction)) {
1460
- throw new TransitionError(
1461
- 'recover-git-worktree-guard',
1462
- transaction.worktree,
1463
- `guard owner ${transaction.ownerPid} is still active`,
1464
- );
1465
- }
1466
- const result = runNativeFileTransaction(transaction, 'recover');
1467
- if (result.status !== 'released') {
1468
- throw new TransitionError(
1469
- 'recover-git-worktree-guard',
1470
- transaction.root,
1471
- result.reason || result.status,
1472
- );
1473
- }
1474
- removeGitWorktreeNamespaceTransaction(transaction);
1475
- }
1476
- }
1477
-
1478
- function prepareGitWorktreeNamespaceGuard(worktreeRoot) {
1479
- recoverGitWorktreeNamespaceTransactions(worktreeRoot);
1480
- const worktree = physicalDirectoryIdentity(worktreeRoot);
1481
- const common = physicalDirectoryIdentity(gitCommonDirectory(worktree.realpath));
1482
- const root = common.realpath;
1483
- const pinned = pinGitWorktreeNamespace(root);
1484
- const guardOperation = crypto.randomBytes(32).toString('hex');
1485
- const paths = gitWorktreeNamespacePaths(root, guardOperation, {
1486
- dev: common.dev,
1487
- ino: common.ino,
1488
- });
1489
- const target = gitMetadataIdentity(Buffer.alloc(0));
1490
- return {
1491
- contract: GIT_WORKTREE_NAMESPACE_GUARD_CONTRACT,
1492
- backend: 'git-worktree-guard',
1493
- worktree: worktree.realpath,
1494
- worktreeDev: worktree.dev,
1495
- worktreeIno: worktree.ino,
1496
- root,
1497
- rootDev: common.dev,
1498
- rootIno: common.ino,
1499
- path: 'worktrees',
1500
- id: paths.id,
1501
- operation: guardOperation,
1502
- ownerPid: process.pid,
1503
- ownerStarted: PROCESS_START_IDENTITY,
1504
- expected: {
1505
- kind: 'directory',
1506
- dev: pinned.namespace.dev,
1507
- ino: pinned.namespace.ino,
1508
- },
1509
- target,
1510
- entries: pinned.entries,
1511
- pins: pinned.pins,
1512
- gitRoots: [
1513
- root,
1514
- ...pinned.pins.slice(1).map((pin) => pin.realpath),
1515
- ].sort(),
1516
- record: paths.record,
1517
- prepared: paths.target,
1518
- claimed: paths.claimed,
1519
- };
1520
- }
1521
-
1522
- async function withGitWorktreeNamespaceGuard(transaction, testHooks, guarded) {
1523
- try {
1524
- durable.writeJson(transaction.record, {
1525
- contract: transaction.contract,
1526
- backend: transaction.backend,
1527
- worktree: transaction.worktree,
1528
- worktreeDev: transaction.worktreeDev,
1529
- worktreeIno: transaction.worktreeIno,
1530
- root: transaction.root,
1531
- rootDev: transaction.rootDev,
1532
- rootIno: transaction.rootIno,
1533
- path: transaction.path,
1534
- id: transaction.id,
1535
- operation: transaction.operation,
1536
- ownerPid: transaction.ownerPid,
1537
- ownerStarted: transaction.ownerStarted,
1538
- expected: transaction.expected,
1539
- target: transaction.target,
1540
- entries: transaction.entries,
1541
- });
1542
- durable.writeFile(transaction.prepared, Buffer.alloc(0), 0o644);
1543
- const result = runNativeFileTransaction(transaction, 'guard');
1544
- if (result.status !== 'guarded') {
1545
- throw new TransitionError(
1546
- 'guard-git-worktree-namespace',
1547
- transaction.root,
1548
- result.reason || result.status,
1549
- );
1550
- }
1551
- const guardedEntries = physicalGitWorktreeRegistrations(transaction.prepared);
1552
- if (JSON.stringify(guardedEntries) !== JSON.stringify(transaction.entries)) {
1553
- throw new TransitionError(
1554
- 'guard-git-worktree-namespace',
1555
- transaction.root,
1556
- 'Git worktree registrations changed before namespace exclusion',
1557
- );
1558
- }
1559
- if (typeof testHooks.afterWorktreeNamespaceGuard === 'function') {
1560
- await testHooks.afterWorktreeNamespaceGuard();
1561
- }
1562
- return await guarded(transaction.root);
1563
- } finally {
1564
- const released = runNativeFileTransaction(transaction, 'unguard');
1565
- if (released.status !== 'released') {
1566
- throw new Error(
1567
- `Git worktree namespace guard release failed: ${released.reason || released.status}`,
1568
- );
1569
- }
1570
- removeGitWorktreeNamespaceTransaction(transaction);
1571
- }
1572
- }
1573
-
1574
- async function withGitHeadGuard(
1575
- worktreeRoot,
1576
- changes,
1577
- testHooks,
1578
- guarded,
1579
- ) {
1580
- const namespace = prepareGitWorktreeNamespaceGuard(worktreeRoot);
1581
- try {
1582
- const acquire = (index) => (
1583
- index === namespace.gitRoots.length
1584
- ? (async () => {
1585
- if (typeof testHooks.afterHeadGuard === 'function') {
1586
- await testHooks.afterHeadGuard();
1587
- }
1588
- return withGitWorktreeNamespaceGuard(namespace, testHooks, guarded);
1589
- })()
1590
- : withOneGitHeadGuard(
1591
- namespace.gitRoots[index],
1592
- worktreeRoot,
1593
- changes,
1594
- () => acquire(index + 1),
1595
- )
1596
- );
1597
- return await acquire(0);
1598
- } finally {
1599
- closePhysicalDirectoryPins(namespace.pins);
1600
- }
1601
- }
1602
-
1603
- function gitMetadataIdentity(content) {
1604
- if (content === null) return null;
1605
- if (!Buffer.isBuffer(content)) throw new Error('Git metadata has no exact bytes');
1606
- return {
1607
- kind: 'file',
1608
- exec: false,
1609
- bytes: content.length,
1610
- sha256: crypto.createHash('sha256').update(content).digest('hex'),
1611
- };
1612
- }
1613
-
1614
- function gitMetadataTransactionPathsForRoot(root, operation, component, rootStat) {
1615
- if (!/^[0-9a-f]{64}$/.test(operation || '')) {
1616
- throw new Error('Git metadata transaction has no operation identity');
1617
- }
1618
- if (component !== 'index' && component !== 'HEAD') {
1619
- throw new Error(`unsupported Git metadata component: ${component}`);
1620
- }
1621
- const directory = gitMetadataTransactionDirectoryForRoot(root);
1622
- ensureDurableDirectory(directory);
1623
- const id = crypto.createHash('sha256')
1624
- .update(
1625
- `${GIT_METADATA_TRANSACTION_CONTRACT}\0${rootStat.dev}\0${rootStat.ino}`
1626
- + `\0${operation}\0${component}`,
1627
- )
1628
- .digest('hex');
1629
- return {
1630
- id,
1631
- directory,
1632
- record: path.join(directory, `${id}.json`),
1633
- target: path.join(directory, `${id}.target`),
1634
- claimed: path.join(directory, `${id}.claimed`),
1635
- };
1636
- }
1637
-
1638
- function gitMetadataTransactionPaths(worktreeRoot, operation, component, rootStat) {
1639
- return gitMetadataTransactionPathsForRoot(
1640
- gitDirectory(worktreeRoot),
1641
- operation,
1642
- component,
1643
- rootStat,
1644
- );
1645
- }
1646
-
1647
- function readGitMetadataTransaction(record) {
1648
- try {
1649
- const value = JSON.parse(fs.readFileSync(record, 'utf8'));
1650
- if (
1651
- value?.contract !== GIT_METADATA_TRANSACTION_CONTRACT
1652
- || (value.backend !== 'git-lock' && value.backend !== 'git-guard')
1653
- || typeof value.root !== 'string'
1654
- || !/^\d+$/.test(value.rootDev)
1655
- || !/^\d+$/.test(value.rootIno)
1656
- || (value.path !== 'index' && value.path !== 'HEAD')
1657
- || typeof value.id !== 'string'
1658
- || value.id !== path.basename(record, '.json')
1659
- || !/^[a-f0-9]{64}$/.test(value.id)
1660
- || !/^[a-f0-9]{64}$/.test(value.operation || '')
1661
- || (
1662
- value.backend === 'git-guard'
1663
- && (
1664
- !Number.isSafeInteger(value.ownerPid)
1665
- || value.ownerPid < 1
1666
- || typeof value.ownerStarted !== 'string'
1667
- || !value.ownerStarted
1668
- )
1669
- )
1670
- || (value.expected !== null && typeof value.expected !== 'object')
1671
- || typeof value.target !== 'object'
1672
- ) throw new Error('invalid Git metadata transaction');
1673
- return {
1674
- ...value,
1675
- record,
1676
- prepared: path.join(path.dirname(record), `${value.id}.target`),
1677
- claimed: path.join(path.dirname(record), `${value.id}.claimed`),
1678
- };
1679
- } catch (error) {
1680
- throw new Error(`invalid workspace Git metadata transaction ${record}: ${error.message}`);
1681
- }
1682
- }
1683
-
1684
- function gitMetadataTransactionRecordsAtRoot(rootInput, operation = null, component = null) {
1685
- const root = physicalDirectoryIdentity(rootInput);
1686
- const directory = gitMetadataTransactionDirectoryForRoot(root.realpath);
1687
- if (!fs.existsSync(directory)) return [];
1688
- return fs.readdirSync(directory)
1689
- .filter((name) => name.endsWith('.json'))
1690
- .sort()
1691
- .map((name) => readGitMetadataTransaction(path.join(directory, name)))
1692
- .map((transaction) => {
1693
- if (!samePhysicalDirectory(
1694
- { dev: transaction.rootDev, ino: transaction.rootIno },
1695
- root,
1696
- )) {
1697
- throw new Error(`Git metadata transaction escaped its worktree: ${transaction.record}`);
356
+ if (write.content === null) {
357
+ fs.rmSync(abs, { force: true });
358
+ removed.push(write.path);
359
+ continue;
1698
360
  }
1699
- return { ...transaction, root: root.realpath };
1700
- })
1701
- .filter((transaction) => operation === null || transaction.operation === operation)
1702
- .filter((transaction) => component === null || transaction.path === component);
1703
- }
1704
-
1705
- function gitMetadataTransactionRecords(worktreeRoot, operation = null, component = null) {
1706
- return gitMetadataTransactionRecordsAtRoot(
1707
- gitDirectory(worktreeRoot),
1708
- operation,
1709
- component,
1710
- );
1711
- }
1712
-
1713
- function sameGitMetadataTransaction(left, right) {
1714
- return (
1715
- left.contract === right.contract
1716
- && left.backend === right.backend
1717
- && left.rootDev === right.rootDev
1718
- && left.rootIno === right.rootIno
1719
- && left.path === right.path
1720
- && left.id === right.id
1721
- && left.operation === right.operation
1722
- && JSON.stringify(left.expected) === JSON.stringify(right.expected)
1723
- && JSON.stringify(left.target) === JSON.stringify(right.target)
1724
- );
1725
- }
1726
-
1727
- function removeGitMetadataTransaction(transaction) {
1728
- for (const file of [transaction.record, transaction.prepared, transaction.claimed]) {
1729
- try {
1730
- fs.unlinkSync(file);
1731
- } catch (error) {
1732
- if (error?.code !== 'ENOENT') throw error;
1733
- }
1734
- }
1735
- if (fs.existsSync(path.dirname(transaction.record))) {
1736
- syncDirectory(path.dirname(transaction.record));
1737
- }
1738
- }
1739
-
1740
- /**
1741
- * Apply one exact index or HEAD byte transition through Git's canonical lock.
1742
- * The prepared bytes stay in the per-worktree Git directory and are hard
1743
- * linked to index.lock/HEAD.lock by the native helper, so even very large
1744
- * indexes never travel through JSON.
1745
- */
1746
- function applyGitMetadataTransaction(
1747
- worktreeRoot,
1748
- component,
1749
- expected,
1750
- target,
1751
- operation,
1752
- testHooks = {},
1753
- ) {
1754
- if (expected !== null && !Buffer.isBuffer(expected)) {
1755
- throw new Error('Git metadata expected value has no exact bytes');
1756
- }
1757
- if (!Buffer.isBuffer(target)) throw new Error('Git metadata target has no exact bytes');
1758
- const root = fs.realpathSync(gitDirectory(worktreeRoot));
1759
- const rootStat = fs.statSync(root, { bigint: true });
1760
- const paths = gitMetadataTransactionPaths(worktreeRoot, operation, component, rootStat);
1761
- const transaction = {
1762
- contract: GIT_METADATA_TRANSACTION_CONTRACT,
1763
- backend: 'git-lock',
1764
- root,
1765
- rootDev: rootStat.dev.toString(),
1766
- rootIno: rootStat.ino.toString(),
1767
- path: component,
1768
- id: paths.id,
1769
- operation,
1770
- expected: gitMetadataIdentity(expected),
1771
- target: gitMetadataIdentity(target),
1772
- record: paths.record,
1773
- prepared: paths.target,
1774
- claimed: paths.claimed,
1775
- };
1776
-
1777
- if (fs.existsSync(paths.record)) {
1778
- const retained = readGitMetadataTransaction(paths.record);
1779
- if (!sameGitMetadataTransaction(retained, transaction)) {
1780
- throw new Error(`Git metadata transaction identity changed: ${component}`);
1781
- }
1782
- const recovered = runNativeFileTransaction(retained, 'recover');
1783
- if (recovered.status === 'applied') return;
1784
- if (recovered.status === 'pending') {
1785
- throw new TransitionError(
1786
- 'apply-git-metadata',
1787
- worktreeRoot,
1788
- `${component}: ${recovered.reason || 'ownership is pending'}`,
1789
- );
1790
- }
1791
- removeGitMetadataTransaction(retained);
1792
- }
1793
-
1794
- durable.writeJson(paths.record, {
1795
- contract: transaction.contract,
1796
- backend: transaction.backend,
1797
- root: transaction.root,
1798
- rootDev: transaction.rootDev,
1799
- rootIno: transaction.rootIno,
1800
- path: transaction.path,
1801
- id: transaction.id,
1802
- operation: transaction.operation,
1803
- expected: transaction.expected,
1804
- target: transaction.target,
1805
- });
1806
- durable.writeFile(paths.target, target, 0o644);
1807
- const result = runNativeFileTransaction(transaction, 'apply', null, {
1808
- crashBeforeInstall: testHooks.crashBeforeInstall === true,
1809
- crashAfterInstall: testHooks.crashAfterInstall === true,
1810
- });
1811
- if (result.status === 'pending') {
1812
- throw new TransitionError(
1813
- 'apply-git-metadata',
1814
- worktreeRoot,
1815
- `${component}: ${result.reason || 'ownership is pending'}`,
1816
- );
1817
- }
1818
- if (result.status === 'changed') {
1819
- removeGitMetadataTransaction(transaction);
1820
- throw new TransitionError(
1821
- 'apply-git-metadata',
1822
- worktreeRoot,
1823
- `${component} changed before ordered apply`,
1824
- );
1825
- }
1826
- }
1827
-
1828
- function recoverGitMetadataTransactionsAtRoot(root, label) {
1829
- for (const transaction of gitMetadataTransactionRecordsAtRoot(root)) {
1830
- if (transaction.backend === 'git-guard' && guardOwnerIsActive(transaction)) {
1831
- throw new TransitionError(
1832
- 'recover-git-head-guard',
1833
- label,
1834
- `guard owner ${transaction.ownerPid} is still active`,
1835
- );
1836
- }
1837
- const result = runNativeFileTransaction(transaction, 'recover');
1838
- if (transaction.backend === 'git-guard') {
1839
- if (result.status !== 'released') {
1840
- throw new TransitionError(
1841
- 'recover-git-head-guard',
1842
- label,
1843
- result.reason || result.status,
1844
- );
361
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
362
+ if (write.mode === '120000') {
363
+ fs.rmSync(abs, { force: true });
364
+ fs.symlinkSync(write.content.toString('utf8'), abs);
365
+ } else {
366
+ fs.writeFileSync(abs, write.content);
367
+ fs.chmodSync(abs, write.mode === '100755' ? 0o755 : 0o644);
1845
368
  }
1846
- removeGitMetadataTransaction(transaction);
1847
- continue;
369
+ written.push(write.path);
1848
370
  }
1849
- if (result.status === 'pending') {
1850
- throw new TransitionError(
1851
- 'recover-git-metadata',
1852
- label,
1853
- `${transaction.path}: ${result.reason || 'ownership is pending'}`,
1854
- );
1855
- }
1856
- if (result.status === 'changed') removeGitMetadataTransaction(transaction);
1857
- }
1858
- }
1859
-
1860
- function gitHeadRoots(worktreeRoot) {
1861
- const common = gitCommonDirectoryFromMarker(worktreeRoot);
1862
- const registrations = path.join(common, 'worktrees');
1863
- let names = [];
1864
- try {
1865
- names = fs.readdirSync(registrations).sort();
371
+ journal({ action: 'merge-files', worktree: root, label, written, removed, ok: true });
1866
372
  } catch (error) {
1867
- if (error?.code !== 'ENOENT') throw error;
1868
- }
1869
- return [
1870
- common,
1871
- ...names.map((name) => path.join(registrations, name)),
1872
- ];
1873
- }
1874
-
1875
- function recoverGitMetadataTransactions(worktreeRoot) {
1876
- recoverGitWorktreeNamespaceTransactions(worktreeRoot);
1877
- for (const root of gitHeadRoots(worktreeRoot)) {
1878
- try {
1879
- recoverGitMetadataTransactionsAtRoot(root, worktreeRoot);
1880
- } catch (error) {
1881
- if (error?.code === 'ENOENT') continue;
1882
- throw error;
1883
- }
1884
- }
1885
- }
1886
-
1887
- function gitMetadataReceiptCrossed(worktreeRoot, operation, component) {
1888
- recoverGitMetadataTransactions(worktreeRoot);
1889
- const records = gitMetadataTransactionRecords(worktreeRoot, operation, component);
1890
- if (records.length > 1) {
1891
- throw new Error(`multiple Git metadata receipts exist for ${component}`);
1892
- }
1893
- if (records.length === 0) return false;
1894
- const result = runNativeFileTransaction(records[0], 'receipt');
1895
- if (result.status === 'crossed') return true;
1896
- if (result.status === 'not-crossed') return false;
1897
- throw new Error(
1898
- `Git metadata receipt is unresolved: ${component}: ${result.reason || result.status}`,
1899
- );
1900
- }
1901
-
1902
- function releaseGitMetadataReceipts(worktreeRoot, operation, component = null) {
1903
- for (const transaction of gitMetadataTransactionRecords(worktreeRoot, operation, component)) {
1904
- const result = runNativeFileTransaction(transaction, 'release');
1905
- if (result.status === 'pending') {
1906
- throw new Error(
1907
- `Git metadata receipt cannot be released: ${transaction.path}: `
1908
- + `${result.reason || 'pending'}`,
1909
- );
1910
- }
1911
- if (result.status !== 'released') {
1912
- throw new Error(
1913
- `Git metadata receipt release failed: ${transaction.path}: ${result.status}`,
1914
- );
1915
- }
1916
- removeGitMetadataTransaction(transaction);
373
+ journal({
374
+ action: 'merge-files', worktree: root, label, written, removed, ok: false,
375
+ error: String(error.message).slice(0, 500),
376
+ });
377
+ throw new TransitionError('merge-files', root, String(error.message));
1917
378
  }
1918
379
  }
1919
380
 
1920
381
  module.exports = {
1921
382
  TransitionError,
1922
- applyGitMetadataTransaction,
1923
383
  applyIndexPatch,
1924
- applyFiles,
1925
- branchReceiptCrossed,
1926
- confinedPath,
384
+ applyMergedFiles,
385
+ applyPatch,
386
+ checkoutInitialCard,
1927
387
  createBundle,
1928
- emptyTree,
1929
- ensureEmptyIndex,
1930
- gitMetadataReceiptCrossed,
388
+ deleteBranchRef,
389
+ fastForwardTo,
390
+ fetchBranch,
391
+ forceMoveToCard,
392
+ holdInitialFiles,
1931
393
  importBundle,
1932
- initializeHeadToCard,
1933
394
  initializeRepo,
1934
- isApplyReceiptPath,
1935
- moveHeadToCard,
1936
- crossedFileReceiptPaths,
1937
- recoverFileTransactions,
1938
- recoverGitMetadataTransactions,
1939
- releaseBranchReceipt,
1940
- releaseFileReceipts,
1941
- releaseGitMetadataReceipts,
1942
- updateBranchRefs,
395
+ moveToCard,
396
+ parkCommit,
397
+ pruneParkedCommits,
398
+ restoreInitialFiles,
399
+ setBranchRef,
400
+ setIndexTree,
1943
401
  };