amalgm 0.1.214 → 0.1.216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +31 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +270 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +26 -1
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +13 -1
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +24 -5
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +15 -0
package/package.json
CHANGED
|
@@ -243,6 +243,37 @@ test('a clean losing commit travels as parked metadata on a no-op checkpoint', (
|
|
|
243
243
|
assert.deepEqual(checkpoint.parked, parked);
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
test('only this machine keeps announcing its parked commits', () => {
|
|
247
|
+
const baseOid = 'a'.repeat(40);
|
|
248
|
+
const localOid = 'b'.repeat(40);
|
|
249
|
+
const peerOid = 'c'.repeat(40);
|
|
250
|
+
const pendingOid = 'd'.repeat(40);
|
|
251
|
+
const existing = {
|
|
252
|
+
branch: 'main',
|
|
253
|
+
baseOid,
|
|
254
|
+
parked: [{ oid: localOid, objects: { kind: 'pending' } }],
|
|
255
|
+
};
|
|
256
|
+
const reduced = {
|
|
257
|
+
parked: [
|
|
258
|
+
{ oid: localOid, version: 10, objects: { kind: 'pending' } },
|
|
259
|
+
{ oid: peerOid, version: 11, objects: { kind: 'bundle' } },
|
|
260
|
+
{ oid: pendingOid, objects: { kind: 'pending' } },
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
const card = { head: { branch: 'main', oid: baseOid } };
|
|
264
|
+
|
|
265
|
+
assert.deepEqual(
|
|
266
|
+
_test.localParkedEntries(existing, reduced, card).map((entry) => entry.oid),
|
|
267
|
+
[localOid, pendingOid],
|
|
268
|
+
);
|
|
269
|
+
assert.deepEqual(
|
|
270
|
+
_test.localParkedEntries(existing, reduced, {
|
|
271
|
+
head: { branch: 'main', oid: 'e'.repeat(40) },
|
|
272
|
+
}).map((entry) => entry.oid),
|
|
273
|
+
[pendingOid],
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
246
277
|
test('a server rail number does not turn the same parked checkpoint into new work', () => {
|
|
247
278
|
const value = { revision: null, indexDiff: '', worktreeDiff: '' };
|
|
248
279
|
const checkpoint = {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-publication-test-'));
|
|
10
|
+
process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
|
|
11
|
+
process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
|
|
12
|
+
process.env.AMALGM_COMPUTER_ID = 'machine-a';
|
|
13
|
+
process.env.AMALGM_DEVICE_ID = 'device-a';
|
|
14
|
+
fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(process.env.AMALGM_DIR, 'computer.json'),
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
computer_id: 'machine-a',
|
|
19
|
+
user_id: '11111111-1111-4111-8111-111111111111',
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const { closeLocalDb } = require('../state/db');
|
|
24
|
+
const {
|
|
25
|
+
_test: checkpointTest,
|
|
26
|
+
CHECKPOINT_CONTRACT,
|
|
27
|
+
checkpointFile,
|
|
28
|
+
sameTransition,
|
|
29
|
+
} = require('../workspace/checkpoint');
|
|
30
|
+
const { parkedForState } = require('../workspace/reducer');
|
|
31
|
+
const { compactWallText, hasLocalContribution, stampCards } = require('../workspace/wall');
|
|
32
|
+
|
|
33
|
+
test.after(() => {
|
|
34
|
+
closeLocalDb();
|
|
35
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const rules = {
|
|
39
|
+
card: {
|
|
40
|
+
identity: 'branch-and-commit-content',
|
|
41
|
+
ownership: 'originator-publishes',
|
|
42
|
+
liveness: 'shared-wall-outbox',
|
|
43
|
+
gating: 'defines-winning-card',
|
|
44
|
+
},
|
|
45
|
+
branch: {
|
|
46
|
+
identity: 'ref-name-and-commit-content',
|
|
47
|
+
ownership: 'originator-publishes',
|
|
48
|
+
liveness: 'shared-wall-outbox',
|
|
49
|
+
gating: 'per-ref-rail-order',
|
|
50
|
+
},
|
|
51
|
+
checkpoint: {
|
|
52
|
+
identity: 'from-and-to-content',
|
|
53
|
+
ownership: 'originator-publishes',
|
|
54
|
+
liveness: 'shared-wall-outbox',
|
|
55
|
+
gating: 'winning-card-only',
|
|
56
|
+
},
|
|
57
|
+
parked: {
|
|
58
|
+
identity: 'commit-and-object-content',
|
|
59
|
+
ownership: 'holder-publishes-until-acknowledged',
|
|
60
|
+
liveness: 'shared-wall-outbox',
|
|
61
|
+
gating: 'inside-winning-card-checkpoint',
|
|
62
|
+
},
|
|
63
|
+
compaction: {
|
|
64
|
+
identity: 'derived-deletion',
|
|
65
|
+
ownership: 'any-machine-may-compact',
|
|
66
|
+
liveness: 'not-semantic-state',
|
|
67
|
+
gating: 'commutative-with-writers',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
test('the publication model has one explicit rule for every value and property', () => {
|
|
72
|
+
assert.deepEqual(Object.keys(rules), ['card', 'branch', 'checkpoint', 'parked', 'compaction']);
|
|
73
|
+
for (const rule of Object.values(rules)) {
|
|
74
|
+
assert.deepEqual(Object.keys(rule), ['identity', 'ownership', 'liveness', 'gating']);
|
|
75
|
+
assert.equal(Object.values(rule).every(Boolean), true);
|
|
76
|
+
}
|
|
77
|
+
assert.deepEqual(
|
|
78
|
+
new Set(['card', 'branch', 'checkpoint', 'parked'].map((name) => rules[name].liveness)),
|
|
79
|
+
new Set(['shared-wall-outbox']),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('server rail numbers do not change checkpoint or parked identity', () => {
|
|
84
|
+
const oid = 'a'.repeat(40);
|
|
85
|
+
const transition = {
|
|
86
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
87
|
+
machine: 'machine-a',
|
|
88
|
+
repoKey: 'root:test',
|
|
89
|
+
branch: 'main',
|
|
90
|
+
baseOid: oid,
|
|
91
|
+
from: { revision: null, indexDiff: '', worktreeDiff: '' },
|
|
92
|
+
to: { revision: 'work-a', indexDiff: '', worktreeDiff: 'patch-a' },
|
|
93
|
+
parked: [{
|
|
94
|
+
oid: 'b'.repeat(40),
|
|
95
|
+
ref: `refs/amalgm/parked/${'b'.repeat(40)}`,
|
|
96
|
+
objects: { kind: 'bundle', checksum: 'c'.repeat(64) },
|
|
97
|
+
}],
|
|
98
|
+
};
|
|
99
|
+
assert.equal(sameTransition(transition, {
|
|
100
|
+
...transition,
|
|
101
|
+
mutationId: 'mut_server_metadata',
|
|
102
|
+
version: 41,
|
|
103
|
+
parked: transition.parked.map((entry) => ({ ...entry, version: 41 })),
|
|
104
|
+
}), true);
|
|
105
|
+
assert.equal(sameTransition(transition, {
|
|
106
|
+
...transition,
|
|
107
|
+
to: { ...transition.to, revision: 'different-work' },
|
|
108
|
+
}), false);
|
|
109
|
+
assert.equal(sameTransition(transition, {
|
|
110
|
+
...transition,
|
|
111
|
+
parked: transition.parked.map((entry) => ({
|
|
112
|
+
...entry,
|
|
113
|
+
objects: { ...entry.objects, checksum: 'd'.repeat(64) },
|
|
114
|
+
})),
|
|
115
|
+
}), false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('receivers stay silent and only unacknowledged local parked work republishes', () => {
|
|
119
|
+
const head = 'a'.repeat(40);
|
|
120
|
+
const local = {
|
|
121
|
+
head: { branch: 'main', oid: head, detached: false },
|
|
122
|
+
refs: [{ name: 'main', oid: head }],
|
|
123
|
+
};
|
|
124
|
+
const transition = {
|
|
125
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
126
|
+
machine: 'machine-a',
|
|
127
|
+
repoKey: 'root:test',
|
|
128
|
+
branch: 'main',
|
|
129
|
+
baseOid: head,
|
|
130
|
+
from: { revision: null, indexDiff: '', worktreeDiff: '' },
|
|
131
|
+
to: { revision: 'shared-work', indexDiff: '', worktreeDiff: 'patch' },
|
|
132
|
+
parked: [],
|
|
133
|
+
};
|
|
134
|
+
const plan = {
|
|
135
|
+
card: { head: { branch: 'main', oid: head, detached: false } },
|
|
136
|
+
branches: [{ name: 'main', oid: head }],
|
|
137
|
+
transitions: [{ ...transition, version: 9 }],
|
|
138
|
+
};
|
|
139
|
+
const baseline = {
|
|
140
|
+
branch: 'main',
|
|
141
|
+
baseOid: head,
|
|
142
|
+
branches: [{ name: 'main', oid: head }],
|
|
143
|
+
};
|
|
144
|
+
assert.equal(hasLocalContribution(local, plan, baseline, transition), false);
|
|
145
|
+
|
|
146
|
+
const localParked = { oid: 'b'.repeat(40), objects: { kind: 'bundle', checksum: 'local' } };
|
|
147
|
+
const unsentParked = { oid: 'c'.repeat(40), objects: { kind: 'bundle', checksum: 'unsent' } };
|
|
148
|
+
const peerParked = {
|
|
149
|
+
oid: 'd'.repeat(40),
|
|
150
|
+
objects: { kind: 'bundle', checksum: 'peer' },
|
|
151
|
+
version: 9,
|
|
152
|
+
};
|
|
153
|
+
assert.deepEqual(
|
|
154
|
+
checkpointTest.localParkedEntries(
|
|
155
|
+
{ ...transition, parked: [localParked] },
|
|
156
|
+
{ parked: [peerParked, unsentParked] },
|
|
157
|
+
local,
|
|
158
|
+
),
|
|
159
|
+
[localParked, unsentParked],
|
|
160
|
+
);
|
|
161
|
+
assert.deepEqual(parkedForState([localParked], [], []), [localParked]);
|
|
162
|
+
assert.deepEqual(
|
|
163
|
+
parkedForState([localParked], [{ ...localParked, version: 10 }], []),
|
|
164
|
+
[{ ...localParked, version: 10 }],
|
|
165
|
+
);
|
|
166
|
+
assert.deepEqual(parkedForState([{ ...localParked, version: 10 }], [], []), []);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('card, branch, checkpoint, and parked work ride one wall mutation', () => {
|
|
170
|
+
const repoKey = 'https://github.com/amalgm-inc/publication-invariant-test';
|
|
171
|
+
const head = 'a'.repeat(40);
|
|
172
|
+
const parkedOid = 'b'.repeat(40);
|
|
173
|
+
const checkpoint = {
|
|
174
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
175
|
+
machine: 'machine-a',
|
|
176
|
+
repoKey,
|
|
177
|
+
branch: 'main',
|
|
178
|
+
baseOid: head,
|
|
179
|
+
from: { revision: null, indexDiff: '', worktreeDiff: '' },
|
|
180
|
+
to: { revision: 'work-a', indexDiff: '', worktreeDiff: 'patch-a' },
|
|
181
|
+
parked: [{
|
|
182
|
+
oid: parkedOid,
|
|
183
|
+
ref: `refs/amalgm/parked/${parkedOid}`,
|
|
184
|
+
objects: { kind: 'bundle', checksum: 'c'.repeat(64) },
|
|
185
|
+
}],
|
|
186
|
+
};
|
|
187
|
+
const file = checkpointFile('machine-a', repoKey);
|
|
188
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
189
|
+
fs.writeFileSync(file, `${JSON.stringify(checkpoint)}\n`);
|
|
190
|
+
|
|
191
|
+
const mutationId = 'mut_one_shared_wall_outbox';
|
|
192
|
+
const stamped = stampCards([{
|
|
193
|
+
card: 'repo-card@2',
|
|
194
|
+
machine: 'machine-a',
|
|
195
|
+
replica: 'rpl_publication_test',
|
|
196
|
+
kind: 'primary',
|
|
197
|
+
repo: { remote: repoKey },
|
|
198
|
+
head: { branch: 'main', oid: head, detached: false },
|
|
199
|
+
refs: [
|
|
200
|
+
{ name: 'main', oid: head },
|
|
201
|
+
{ name: 'feature', oid: head },
|
|
202
|
+
],
|
|
203
|
+
health: 'healthy',
|
|
204
|
+
}], [], null, null, mutationId, {
|
|
205
|
+
repoKey,
|
|
206
|
+
resourceId: 'shr_publication_invariant_test',
|
|
207
|
+
})[0];
|
|
208
|
+
|
|
209
|
+
assert.equal(stamped.positionMutationId, mutationId);
|
|
210
|
+
assert.equal(stamped.branches.every((ref) => ref.mutationId === mutationId), true);
|
|
211
|
+
assert.equal(stamped.checkpoints[0].mutationId, mutationId);
|
|
212
|
+
assert.equal(stamped.checkpoints[0].parked[0].oid, parkedOid);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('two concurrent compacting publishers converge without losing either latest value', () => {
|
|
216
|
+
const Y = require('yjs');
|
|
217
|
+
const repoKey = 'root:compaction-collision';
|
|
218
|
+
const line = (machine, value) => JSON.stringify({
|
|
219
|
+
wall: 'code-project@3',
|
|
220
|
+
project: repoKey,
|
|
221
|
+
machine,
|
|
222
|
+
mutationId: `mut_${machine}_${value}`,
|
|
223
|
+
cards: [{ value }],
|
|
224
|
+
});
|
|
225
|
+
const baseText = [
|
|
226
|
+
line('machine-a', 'old-a'),
|
|
227
|
+
line('machine-b', 'old-b'),
|
|
228
|
+
line('machine-a', 'current-a'),
|
|
229
|
+
line('machine-b', 'current-b'),
|
|
230
|
+
'',
|
|
231
|
+
].join('\n');
|
|
232
|
+
const compacted = compactWallText(baseText, repoKey);
|
|
233
|
+
const removedBytes = baseText.length - compacted.length;
|
|
234
|
+
assert.ok(removedBytes > 0);
|
|
235
|
+
|
|
236
|
+
const base = new Y.Doc();
|
|
237
|
+
base.getText('content').insert(0, baseText);
|
|
238
|
+
const baseUpdate = Y.encodeStateAsUpdate(base);
|
|
239
|
+
const baseVector = Y.encodeStateVector(base);
|
|
240
|
+
const publisherUpdate = (machine, value) => {
|
|
241
|
+
const writer = new Y.Doc();
|
|
242
|
+
Y.applyUpdate(writer, baseUpdate);
|
|
243
|
+
const text = writer.getText('content');
|
|
244
|
+
text.delete(0, removedBytes);
|
|
245
|
+
text.insert(text.length, `${line(machine, value)}\n`);
|
|
246
|
+
return Y.encodeStateAsUpdate(writer, baseVector);
|
|
247
|
+
};
|
|
248
|
+
const updateA = publisherUpdate('machine-a', 'latest-a');
|
|
249
|
+
const updateB = publisherUpdate('machine-b', 'latest-b');
|
|
250
|
+
const replicas = [
|
|
251
|
+
[updateA, updateB],
|
|
252
|
+
[updateB, updateA],
|
|
253
|
+
].map((order) => {
|
|
254
|
+
const replica = new Y.Doc();
|
|
255
|
+
Y.applyUpdate(replica, baseUpdate);
|
|
256
|
+
for (const update of order) Y.applyUpdate(replica, update);
|
|
257
|
+
return replica.getText('content').toString();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
assert.equal(replicas[0], replicas[1]);
|
|
261
|
+
const latest = new Map();
|
|
262
|
+
for (const raw of replicas[0].trim().split('\n')) {
|
|
263
|
+
const entry = JSON.parse(raw);
|
|
264
|
+
latest.set(entry.machine, entry.cards[0].value);
|
|
265
|
+
}
|
|
266
|
+
assert.deepEqual(Object.fromEntries(latest), {
|
|
267
|
+
'machine-a': 'latest-a',
|
|
268
|
+
'machine-b': 'latest-b',
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
hydrateRepo,
|
|
29
29
|
localCardMayMaterialize,
|
|
30
30
|
materializeRepo,
|
|
31
|
+
parkedForState,
|
|
31
32
|
pathForRepo,
|
|
32
33
|
readRepoState,
|
|
33
34
|
reduceCheckpoint,
|
|
@@ -175,6 +176,17 @@ test('a ready parked bundle never regresses to pending', () => {
|
|
|
175
176
|
assert.equal(plan.parked[0].objects.kind, 'bundle');
|
|
176
177
|
});
|
|
177
178
|
|
|
179
|
+
test('an unpublished local parked commit stays active until the rail records it', () => {
|
|
180
|
+
const oid = 'f'.repeat(40);
|
|
181
|
+
const local = { oid, objects: { kind: 'bundle', checksum: 'local' } };
|
|
182
|
+
assert.deepEqual(parkedForState([local], [], []), [local]);
|
|
183
|
+
assert.deepEqual(parkedForState([local], [{ ...local, version: 7 }], []), [{
|
|
184
|
+
...local,
|
|
185
|
+
version: 7,
|
|
186
|
+
}]);
|
|
187
|
+
assert.deepEqual(parkedForState([{ ...local, version: 7 }], [], []), []);
|
|
188
|
+
});
|
|
189
|
+
|
|
178
190
|
test('a stale lower rail card cannot roll back a newer branch and commit', () => {
|
|
179
191
|
const newer = card('/newer', 'b'.repeat(40), 12);
|
|
180
192
|
newer.head.branch = 'feature';
|
|
@@ -508,6 +520,17 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
|
|
|
508
520
|
assert.equal((await git(repo, 'status', '--porcelain')).trim().length > 0, true);
|
|
509
521
|
assert.deepEqual(readRepoState('root:test').parked.map((entry) => entry.oid), [losingOid]);
|
|
510
522
|
|
|
523
|
+
const sharedParked = readRepoState('root:test').parked.map((entry) => ({
|
|
524
|
+
...entry,
|
|
525
|
+
version: 2,
|
|
526
|
+
}));
|
|
527
|
+
await materializeRepo(card(repo, winningOid, 2), {
|
|
528
|
+
repoKey: 'root:test',
|
|
529
|
+
card: winningCard,
|
|
530
|
+
transitions: [],
|
|
531
|
+
parked: sharedParked,
|
|
532
|
+
});
|
|
533
|
+
|
|
511
534
|
await git(repo, 'add', '.');
|
|
512
535
|
await git(repo, 'commit', '-m', 'fold parked work');
|
|
513
536
|
const foldedOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
|
|
@@ -521,7 +544,9 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
|
|
|
521
544
|
|
|
522
545
|
assert.equal(folded.state, 'in-sync');
|
|
523
546
|
assert.deepEqual(readRepoState('root:test').parked, []);
|
|
524
|
-
|
|
547
|
+
await assert.rejects(
|
|
548
|
+
git(repo, 'rev-parse', '--verify', `refs/amalgm/parked/${losingOid}`),
|
|
549
|
+
);
|
|
525
550
|
});
|
|
526
551
|
|
|
527
552
|
test('rail-parked peer commits become one staged checkpoint on every machine', async () => {
|
|
@@ -157,6 +157,17 @@ function checkpointForSharedState(existing, {
|
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
function localParkedEntries(existing, reduced, card) {
|
|
161
|
+
const held = existing?.baseOid === card.head?.oid
|
|
162
|
+
&& existing.branch === card.head?.branch
|
|
163
|
+
? existing.parked || []
|
|
164
|
+
: [];
|
|
165
|
+
const unpublished = (reduced?.parked || []).filter((entry) => (
|
|
166
|
+
!Number.isInteger(entry.version) || entry.version < 1
|
|
167
|
+
));
|
|
168
|
+
return [...new Map([...held, ...unpublished].map((entry) => [entry.oid, entry])).values()];
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
function readCheckpointValue(baseOid, value) {
|
|
161
172
|
let indexDiff = typeof value?.indexDiff === 'string' ? value.indexDiff : '';
|
|
162
173
|
let worktreeDiff = typeof value?.worktreeDiff === 'string' ? value.worktreeDiff : '';
|
|
@@ -228,7 +239,7 @@ async function publishPass(replicaId = null) {
|
|
|
228
239
|
const localRevision = revisionOf(card.head.oid, captured.indexDiff, captured.worktreeDiff);
|
|
229
240
|
const file = checkpointFile(machine, repoKey);
|
|
230
241
|
const existing = readCheckpoint(machine, repoKey);
|
|
231
|
-
const parked = await Promise.all((reduced
|
|
242
|
+
const parked = await Promise.all(localParkedEntries(existing, reduced, card).map(async (entry) => ({
|
|
232
243
|
...entry,
|
|
233
244
|
objects: await require('./objects').bundleSourceFor(card.path, repoKey, entry.oid),
|
|
234
245
|
})));
|
|
@@ -393,6 +404,7 @@ module.exports = {
|
|
|
393
404
|
checkpointBaseReady,
|
|
394
405
|
checkpointForSharedState,
|
|
395
406
|
checkpointMayPublish,
|
|
407
|
+
localParkedEntries,
|
|
396
408
|
},
|
|
397
409
|
CHECKPOINT_CONTRACT,
|
|
398
410
|
MAX_INLINE_DIFF_BYTES,
|
|
@@ -176,6 +176,18 @@ function uniqueParked(entries) {
|
|
|
176
176
|
return order.map((oid) => byOid.get(oid));
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function parkedForState(previous, shared, discovered) {
|
|
180
|
+
const sharedOids = new Set((shared || []).map((entry) => entry.oid));
|
|
181
|
+
const unpublished = (previous || []).filter((entry) => (
|
|
182
|
+
!validVersion(entry.version) && !sharedOids.has(entry.oid)
|
|
183
|
+
));
|
|
184
|
+
return uniqueParked([
|
|
185
|
+
...(shared || []),
|
|
186
|
+
...unpublished,
|
|
187
|
+
...(discovered || []),
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
|
|
179
191
|
async function obtainCardObjects(localCard, targetCard) {
|
|
180
192
|
const oid = targetCard.head?.oid;
|
|
181
193
|
if (!oid || await hasCommit(localCard.path, oid)) return { state: 'ready' };
|
|
@@ -348,7 +360,8 @@ async function moveToWinningCard(localCard, targetCard) {
|
|
|
348
360
|
|
|
349
361
|
async function materializeParked(localCard, entries) {
|
|
350
362
|
const waiting = [];
|
|
351
|
-
|
|
363
|
+
const active = uniqueParked(entries);
|
|
364
|
+
for (const entry of active) {
|
|
352
365
|
if (!(await hasCommit(localCard.path, entry.oid))) {
|
|
353
366
|
if (entry.objects?.kind === 'bundle') {
|
|
354
367
|
const imported = await require('./objects').importFromLocator(localCard.path, entry.objects);
|
|
@@ -363,6 +376,10 @@ async function materializeParked(localCard, entries) {
|
|
|
363
376
|
}
|
|
364
377
|
await transition.parkCommit(localCard.path, entry.oid);
|
|
365
378
|
}
|
|
379
|
+
await transition.pruneParkedCommits(
|
|
380
|
+
localCard.path,
|
|
381
|
+
new Set(active.map((entry) => entry.oid)),
|
|
382
|
+
);
|
|
366
383
|
return waiting;
|
|
367
384
|
}
|
|
368
385
|
|
|
@@ -512,10 +529,11 @@ async function materializeRepo(localCardInput, plan) {
|
|
|
512
529
|
? await refreshRepoCard(refreshed.replica) || refreshed
|
|
513
530
|
: refreshed;
|
|
514
531
|
const previous = readRepoState(plan.repoKey);
|
|
515
|
-
const parked =
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
532
|
+
const parked = parkedForState(
|
|
533
|
+
previous?.parked,
|
|
534
|
+
plan.parked,
|
|
535
|
+
cardResult.parked,
|
|
536
|
+
);
|
|
519
537
|
const parkedWaiting = await materializeParked(localCard, parked);
|
|
520
538
|
const planParked = new Set((plan.parked || []).map((entry) => entry.oid));
|
|
521
539
|
const requiredParkedWaiting = parkedWaiting.filter((entry) => planParked.has(entry.oid));
|
|
@@ -784,6 +802,7 @@ module.exports = {
|
|
|
784
802
|
localCardMayMaterialize,
|
|
785
803
|
materializeRepo,
|
|
786
804
|
pathForRepo,
|
|
805
|
+
parkedForState,
|
|
787
806
|
readRepoState,
|
|
788
807
|
reduceCheckpoint,
|
|
789
808
|
reduceNow,
|
|
@@ -236,6 +236,20 @@ function parkCommit(worktreeRoot, oid) {
|
|
|
236
236
|
]).then(() => ref);
|
|
237
237
|
}
|
|
238
238
|
|
|
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
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
239
253
|
function setBranchRef(worktreeRoot, branch, oid) {
|
|
240
254
|
return run('set-branch-ref', worktreeRoot, [
|
|
241
255
|
'-C', worktreeRoot, 'update-ref', `refs/heads/${branch}`, oid,
|
|
@@ -380,6 +394,7 @@ module.exports = {
|
|
|
380
394
|
initializeRepo,
|
|
381
395
|
moveToCard,
|
|
382
396
|
parkCommit,
|
|
397
|
+
pruneParkedCommits,
|
|
383
398
|
restoreInitialFiles,
|
|
384
399
|
setBranchRef,
|
|
385
400
|
setIndexTree,
|