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.
- package/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* The coexistence engine.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The workspace rule: every writer — human, agent, or remote machine —
|
|
7
|
+
* writes into the same working tree, and no writer's work is ever
|
|
8
|
+
* silently lost. This module is the mechanism. A peer's contribution to
|
|
9
|
+
* a local working tree is represented as a git tree oid ("base commit
|
|
10
|
+
* plus exactly the peer content folded in so far"), and the only verb is
|
|
11
|
+
* advance: move that contribution from the tree we hold to the tree the
|
|
12
|
+
* peer just published, file by file, around whatever else lives in the
|
|
13
|
+
* working tree.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
15
|
+
* Per file, in order:
|
|
16
|
+
* - working file already matches the peer's version → fold without
|
|
17
|
+
* writing (this is what makes echoes and replays no-ops);
|
|
18
|
+
* - working file untouched since the held version → take the peer's
|
|
19
|
+
* version verbatim (create, update, or delete);
|
|
20
|
+
* - both sides changed text → content-level three-way merge; a clean
|
|
21
|
+
* merge writes, an overlapping one holds the file aside untouched
|
|
22
|
+
* with a named conflict kind. Both versions survive: local in the
|
|
23
|
+
* tree, the peer's in their checkpoint.
|
|
14
24
|
*
|
|
15
|
-
* Everything computes against the object database with scratch
|
|
16
|
-
* the only worktree mutation is the final
|
|
25
|
+
* Everything here computes against the object database with scratch
|
|
26
|
+
* index files; the only worktree mutation is the final write set, which
|
|
27
|
+
* goes through transition.applyMergedFiles so it lands in the journal.
|
|
17
28
|
*/
|
|
18
29
|
|
|
30
|
+
const crypto = require('crypto');
|
|
19
31
|
const fs = require('fs');
|
|
20
32
|
const os = require('os');
|
|
21
33
|
const path = require('path');
|
|
@@ -72,15 +84,6 @@ async function hasTree(worktreeRoot, treeOid) {
|
|
|
72
84
|
return result.exitCode === 0;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
/** Gitlinks are a different repository and are not a file state we can apply. */
|
|
76
|
-
async function gitlinksInTree(worktreeRoot, treeOid) {
|
|
77
|
-
const { stdout } = await run(['ls-tree', '-r', '-z', treeOid], worktreeRoot);
|
|
78
|
-
return stdout.split('\0').filter(Boolean).flatMap((entry) => {
|
|
79
|
-
const match = entry.match(/^160000 commit [0-9a-f]+\t(.+)$/);
|
|
80
|
-
return match ? [match[1]] : [];
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
87
|
/**
|
|
85
88
|
* The tree a captured worktree patch describes: base commit's tree with
|
|
86
89
|
* the patch applied. Computed entirely in a scratch index — the worktree
|
|
@@ -144,7 +147,7 @@ async function readBlob(worktreeRoot, oid) {
|
|
|
144
147
|
|
|
145
148
|
/** The working file as bytes: { kind: 'file'|'symlink', content, exec } or null. */
|
|
146
149
|
function workFileState(worktreeRoot, relPath) {
|
|
147
|
-
const abs =
|
|
150
|
+
const abs = path.join(worktreeRoot, relPath);
|
|
148
151
|
let stat;
|
|
149
152
|
try {
|
|
150
153
|
stat = fs.lstatSync(abs);
|
|
@@ -154,7 +157,7 @@ function workFileState(worktreeRoot, relPath) {
|
|
|
154
157
|
if (stat.isSymbolicLink()) {
|
|
155
158
|
return { kind: 'symlink', content: Buffer.from(fs.readlinkSync(abs)), exec: false };
|
|
156
159
|
}
|
|
157
|
-
if (!stat.isFile()) return
|
|
160
|
+
if (!stat.isFile()) return null;
|
|
158
161
|
return { kind: 'file', content: fs.readFileSync(abs), exec: (stat.mode & 0o100) !== 0 };
|
|
159
162
|
}
|
|
160
163
|
|
|
@@ -168,84 +171,124 @@ function workMatches(work, entry, blobBytes) {
|
|
|
168
171
|
&& work.content.equals(blobBytes);
|
|
169
172
|
}
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
* state. Rail order alone decides the winner.
|
|
175
|
-
*/
|
|
176
|
-
async function reduceTree(worktreeRoot, currentTree, baseTree, targetTree) {
|
|
177
|
-
const updates = [];
|
|
178
|
-
for (const relPath of await changedPaths(worktreeRoot, baseTree, targetTree)) {
|
|
179
|
-
if (unsafePath(relPath)) throw new Error(`unsafe path: ${relPath}`);
|
|
180
|
-
const entry = await entryAt(worktreeRoot, targetTree, relPath);
|
|
181
|
-
if (entry && entry.type !== 'blob') {
|
|
182
|
-
throw new Error(`unsupported gitlink/submodule at ${relPath}`);
|
|
183
|
-
}
|
|
184
|
-
updates.push({ path: relPath, entry });
|
|
185
|
-
}
|
|
174
|
+
function isText(buffer) {
|
|
175
|
+
return buffer !== null && !buffer.includes(0);
|
|
176
|
+
}
|
|
186
177
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
/** Content-level three-way merge; { ok, content } — never writes markers to disk. */
|
|
179
|
+
async function mergeFileContents(ours, base, theirs, labels = {}) {
|
|
180
|
+
return withScratch(async (dir) => {
|
|
181
|
+
const oursFile = path.join(dir, 'ours');
|
|
182
|
+
const baseFile = path.join(dir, 'base');
|
|
183
|
+
const theirsFile = path.join(dir, 'theirs');
|
|
184
|
+
await fs.promises.writeFile(oursFile, ours);
|
|
185
|
+
await fs.promises.writeFile(baseFile, base);
|
|
186
|
+
await fs.promises.writeFile(theirsFile, theirs);
|
|
187
|
+
const conflictCodes = Array.from({ length: 127 }, (_, i) => i + 1);
|
|
188
|
+
const result = await run(
|
|
189
|
+
[
|
|
190
|
+
'merge-file', '-p',
|
|
191
|
+
'-L', labels.ours || 'current',
|
|
192
|
+
'-L', labels.base || 'base',
|
|
193
|
+
'-L', labels.theirs || 'incoming',
|
|
194
|
+
oursFile, baseFile, theirsFile,
|
|
195
|
+
],
|
|
196
|
+
dir,
|
|
197
|
+
{ okCodes: conflictCodes, binary: true },
|
|
198
|
+
);
|
|
199
|
+
return { ok: result.exitCode === 0, content: result.stdout };
|
|
200
|
+
});
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
function sameEntry(left, right) {
|
|
193
|
-
return
|
|
194
|
-
left === null
|
|
204
|
+
return left === null
|
|
195
205
|
? right === null
|
|
196
|
-
: right !== null
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
206
|
+
: right !== null && left.mode === right.mode && left.type === right.type && left.oid === right.oid;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function storedBlob(worktreeRoot, content, scratchDir, name) {
|
|
210
|
+
const file = path.join(scratchDir, name);
|
|
211
|
+
await fs.promises.writeFile(file, content);
|
|
212
|
+
const { stdout } = await run(['hash-object', '-w', file], worktreeRoot);
|
|
213
|
+
return stdout.trim();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function conflictCopyPath(relPath, label) {
|
|
217
|
+
const digest = crypto.createHash('sha256').update(`${label}\0${relPath}`).digest('hex').slice(0, 16);
|
|
218
|
+
return `.amalgm-conflicts/${digest}/${relPath}`;
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* sealed, so it remains on top of target.
|
|
222
|
+
* Pure checkpoint reduction. Apply one published transition
|
|
223
|
+
* (`baseTree → targetTree`) to the currently reduced tree. No working file
|
|
224
|
+
* is read or written. Disjoint changes combine, identical changes fold, and
|
|
225
|
+
* overlaps produce the same conflict bytes and paths on every machine.
|
|
209
226
|
*/
|
|
210
|
-
async function
|
|
211
|
-
worktreeRoot,
|
|
212
|
-
currentTree,
|
|
213
|
-
sourceTree,
|
|
214
|
-
targetTree,
|
|
215
|
-
{ sourceCrossed = false, crossedPaths = null } = {},
|
|
216
|
-
) {
|
|
217
|
-
const paths = new Set([
|
|
218
|
-
...await changedPaths(worktreeRoot, sourceTree, currentTree),
|
|
219
|
-
...await changedPaths(worktreeRoot, targetTree, currentTree),
|
|
220
|
-
]);
|
|
221
|
-
const local = [];
|
|
227
|
+
async function reduceTree(worktreeRoot, currentTree, baseTree, targetTree, { label = 'rail' } = {}) {
|
|
222
228
|
const updates = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
const conflicts = [];
|
|
230
|
+
|
|
231
|
+
await withScratch(async (dir) => {
|
|
232
|
+
let stored = 0;
|
|
233
|
+
for (const relPath of await changedPaths(worktreeRoot, baseTree, targetTree)) {
|
|
234
|
+
if (unsafePath(relPath)) {
|
|
235
|
+
conflicts.push({ path: relPath, kind: 'unsafe-path', label });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const base = await entryAt(worktreeRoot, baseTree, relPath);
|
|
239
|
+
const current = await entryAt(worktreeRoot, currentTree, relPath);
|
|
240
|
+
const target = await entryAt(worktreeRoot, targetTree, relPath);
|
|
241
|
+
|
|
242
|
+
if (sameEntry(current, target) || sameEntry(base, target)) continue;
|
|
243
|
+
if (sameEntry(current, base)) {
|
|
244
|
+
updates.push({ path: relPath, entry: target });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const baseBytes = base?.type === 'blob' ? await readBlob(worktreeRoot, base.oid) : null;
|
|
249
|
+
const currentBytes = current?.type === 'blob' ? await readBlob(worktreeRoot, current.oid) : null;
|
|
250
|
+
const targetBytes = target?.type === 'blob' ? await readBlob(worktreeRoot, target.oid) : null;
|
|
251
|
+
const mergeable = base && current && target
|
|
252
|
+
&& base.type === 'blob' && current.type === 'blob' && target.type === 'blob'
|
|
253
|
+
&& base.mode !== '120000' && current.mode !== '120000' && target.mode !== '120000'
|
|
254
|
+
&& isText(baseBytes) && isText(currentBytes) && isText(targetBytes);
|
|
255
|
+
|
|
256
|
+
if (mergeable) {
|
|
257
|
+
const merged = await mergeFileContents(currentBytes, baseBytes, targetBytes, {
|
|
258
|
+
ours: 'current',
|
|
259
|
+
base: 'shared-base',
|
|
260
|
+
theirs: `rail-${label}`,
|
|
261
|
+
});
|
|
262
|
+
const oid = await storedBlob(worktreeRoot, merged.content, dir, `merged-${stored++}`);
|
|
263
|
+
updates.push({ path: relPath, entry: { mode: current.mode, type: 'blob', oid } });
|
|
264
|
+
if (!merged.ok) conflicts.push({ path: relPath, kind: 'content-overlap', label });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Binary/delete/add conflicts retain the current path and, when the
|
|
269
|
+
// incoming side has bytes, preserve those bytes at one deterministic
|
|
270
|
+
// conflict path. The reducer's conflict record preserves delete intent.
|
|
271
|
+
const kind = !target
|
|
272
|
+
? 'delete-vs-change'
|
|
273
|
+
: !current
|
|
274
|
+
? 'change-vs-delete'
|
|
275
|
+
: !base
|
|
276
|
+
? 'add-add'
|
|
277
|
+
: 'binary-overlap';
|
|
278
|
+
const conflict = { path: relPath, kind, label };
|
|
279
|
+
if (targetBytes) {
|
|
280
|
+
const copyPath = conflictCopyPath(relPath, label);
|
|
281
|
+
const oid = await storedBlob(worktreeRoot, targetBytes, dir, `conflict-${stored++}`);
|
|
282
|
+
updates.push({ path: copyPath, entry: { mode: '100644', type: 'blob', oid } });
|
|
283
|
+
conflict.copyPath = copyPath;
|
|
233
284
|
}
|
|
285
|
+
conflicts.push(conflict);
|
|
234
286
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// Endpoint resemblance alone is not provenance. If the apply receipt
|
|
238
|
-
// proves this path crossed, returning to the source value is a later
|
|
239
|
-
// local write (including deleting an official create) and must remain.
|
|
240
|
-
if (sameEntry(current, source) && !crossed) continue;
|
|
241
|
-
local.push(relPath);
|
|
242
|
-
updates.push({ path: relPath, entry: current });
|
|
243
|
-
}
|
|
287
|
+
});
|
|
288
|
+
|
|
244
289
|
return {
|
|
245
|
-
tree: updates.length
|
|
246
|
-
|
|
247
|
-
: targetTree,
|
|
248
|
-
local,
|
|
290
|
+
tree: updates.length ? await updateHeldTree(worktreeRoot, currentTree, updates) : currentTree,
|
|
291
|
+
conflicts,
|
|
249
292
|
};
|
|
250
293
|
}
|
|
251
294
|
|
|
@@ -275,42 +318,81 @@ function unsafePath(relPath) {
|
|
|
275
318
|
}
|
|
276
319
|
|
|
277
320
|
/**
|
|
278
|
-
* Advance
|
|
279
|
-
*
|
|
280
|
-
*
|
|
321
|
+
* Advance the peer's contribution in this working tree from heldTree to
|
|
322
|
+
* targetTree. Returns { applied, conflicts, heldTree, wrote } — applied
|
|
323
|
+
* paths are folded into the returned heldTree; conflicted paths are left
|
|
324
|
+
* exactly as the working tree had them, each with a named kind.
|
|
281
325
|
*/
|
|
282
326
|
async function advance(worktreeRoot, heldTree, targetTree, { label } = {}) {
|
|
283
327
|
const applied = [];
|
|
328
|
+
const conflicts = [];
|
|
284
329
|
const writes = [];
|
|
285
330
|
|
|
286
331
|
for (const relPath of await changedPaths(worktreeRoot, heldTree, targetTree)) {
|
|
287
|
-
if (unsafePath(relPath))
|
|
332
|
+
if (unsafePath(relPath)) {
|
|
333
|
+
conflicts.push({ path: relPath, kind: 'unsafe-path' });
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
288
336
|
const held = await entryAt(worktreeRoot, heldTree, relPath);
|
|
289
337
|
const target = await entryAt(worktreeRoot, targetTree, relPath);
|
|
290
338
|
if ((held && held.type !== 'blob') || (target && target.type !== 'blob')) {
|
|
291
|
-
|
|
339
|
+
conflicts.push({ path: relPath, kind: 'unsupported-entry' });
|
|
340
|
+
continue;
|
|
292
341
|
}
|
|
293
342
|
const heldBytes = held ? await readBlob(worktreeRoot, held.oid) : null;
|
|
294
343
|
const targetBytes = target ? await readBlob(worktreeRoot, target.oid) : null;
|
|
295
344
|
const work = workFileState(worktreeRoot, relPath);
|
|
296
345
|
|
|
346
|
+
if (workMatches(work, target, targetBytes)) {
|
|
347
|
+
// Already there — a replayed or echoed change folds without a write.
|
|
348
|
+
applied.push({ path: relPath, entry: target });
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
297
351
|
if (workMatches(work, held, heldBytes)) {
|
|
352
|
+
// Untouched locally since what we hold — take the peer's version.
|
|
298
353
|
writes.push(target
|
|
299
|
-
? {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
354
|
+
? { path: relPath, content: targetBytes, mode: target.mode }
|
|
355
|
+
: { path: relPath, content: null });
|
|
356
|
+
applied.push({ path: relPath, entry: target });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// The working file differs from both what we hold and what the peer
|
|
361
|
+
// published: two writers touched it. Merge or preserve — never pick.
|
|
362
|
+
if (!target) {
|
|
363
|
+
conflicts.push({ path: relPath, kind: 'delete-vs-edit' });
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (!work) {
|
|
367
|
+
conflicts.push({ path: relPath, kind: 'edit-vs-delete' });
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!held) {
|
|
371
|
+
conflicts.push({ path: relPath, kind: 'add-add' });
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const mergeable = work.kind === 'file'
|
|
375
|
+
&& target.mode !== '120000'
|
|
376
|
+
&& held.mode !== '120000'
|
|
377
|
+
&& isText(work.content) && isText(heldBytes) && isText(targetBytes);
|
|
378
|
+
if (!mergeable) {
|
|
379
|
+
conflicts.push({ path: relPath, kind: 'binary-overlap' });
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const merged = await mergeFileContents(work.content, heldBytes, targetBytes);
|
|
383
|
+
if (!merged.ok) {
|
|
384
|
+
conflicts.push({ path: relPath, kind: 'content-overlap' });
|
|
385
|
+
continue;
|
|
306
386
|
}
|
|
387
|
+
writes.push({ path: relPath, content: merged.content, mode: work.exec ? '100755' : '100644' });
|
|
307
388
|
applied.push({ path: relPath, entry: target });
|
|
308
389
|
}
|
|
309
390
|
|
|
310
|
-
if (writes.length) await transition.
|
|
391
|
+
if (writes.length) await transition.applyMergedFiles(worktreeRoot, writes, label);
|
|
311
392
|
const newHeldTree = applied.length ? await updateHeldTree(worktreeRoot, heldTree, applied) : heldTree;
|
|
312
393
|
return {
|
|
313
394
|
applied: applied.map((a) => a.path),
|
|
395
|
+
conflicts,
|
|
314
396
|
heldTree: newHeldTree,
|
|
315
397
|
wrote: writes.length,
|
|
316
398
|
};
|
|
@@ -318,12 +400,9 @@ async function advance(worktreeRoot, heldTree, targetTree, { label } = {}) {
|
|
|
318
400
|
|
|
319
401
|
module.exports = {
|
|
320
402
|
advance,
|
|
321
|
-
changedPaths,
|
|
322
403
|
diffOfTree,
|
|
323
|
-
gitlinksInTree,
|
|
324
404
|
hasTree,
|
|
325
405
|
indexTree,
|
|
326
|
-
reconcileApplyingTree,
|
|
327
406
|
reduceTree,
|
|
328
407
|
treeOf,
|
|
329
408
|
treeOfPatch,
|