git-edge 0.3.0 → 0.4.0
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 +8 -6
- package/dist/index.js +131 -191
- package/dist/index.js.map +1 -1
- package/dist/types/merge.d.ts +17 -9
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
High-level edge-compatible git operations on top of [isomorphic-git](https://isomorphic-git.org): a parsed-object LRU cache, an object-level three-way merge that never needs a worktree, and per-repo packfile-cache management. No `node:*` imports anywhere in `src/` — runs on Cloudflare Workers, Vercel Edge, Deno Deploy, and Node.
|
|
8
8
|
|
|
9
|
-
Extracted from the same production git-hosting service as [`git-fs-s3`](https://www.npmjs.com/package/git-fs-s3) — the two compose (see below)
|
|
9
|
+
Extracted from the same production git-hosting service as [`git-fs-s3`](https://www.npmjs.com/package/git-fs-s3) — the two compose (see below): both work on isomorphic-git's `{ fs, gitdir, cache? }` shape, and `threeWayMerge` stores its objects through git-fs-s3's `withPackedWrites`.
|
|
10
10
|
|
|
11
11
|
## Why
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ Separately, isomorphic-git re-parses a packfile index from scratch on every `rea
|
|
|
20
20
|
npm install git-edge git-fs-s3 isomorphic-git # ESM only, Node.js 22+
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`git-fs-s3` is
|
|
23
|
+
`git-fs-s3` (≥ 0.8) is a required peer — `threeWayMerge` uses its `withPackedWrites`. git-edge works with any isomorphic-git-compatible `fs` (including plain `node:fs`), not just git-fs-s3's.
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
@@ -67,9 +67,11 @@ await threeWayMerge(repo, "feature", "main");
|
|
|
67
67
|
|
|
68
68
|
Merges `sourceRef` into `targetRef` at the object level — no worktree.
|
|
69
69
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
70
|
+
- Target already contains source (same commit, or source is an ancestor) → nothing to do: returns target's oid, no ref moves.
|
|
71
|
+
- Source is a descendant of target → fast-forward: just moves `targetRef`, no merge commit.
|
|
72
|
+
- Otherwise, merges the trees recursively against their merge base: a change only one side made wins (deletions and mode changes included), subtrees only one side touched are taken without being read, and text files both sides changed are content-merged with diff3. Binary files, symlinks, submodules, modify/delete and file/directory clashes that both sides touched are conflicts. Unrelated histories throw `GitEdgeError`.
|
|
73
|
+
- Throws `GitMergeConflictError` (with sorted `conflictingPaths: string[]`) if anything conflicts — before writing anything.
|
|
74
|
+
- Only `targetRef` moves (never the branch `HEAD` names); new objects are stored as one pack via git-fs-s3's `withPackedWrites`, before the ref.
|
|
73
75
|
- `opts.message`, `opts.authorName`/`opts.authorEmail` (default `"Git Edge" <git-edge@local>`).
|
|
74
76
|
|
|
75
77
|
Returns `{ commitOid }` — the new merge commit, or the fast-forwarded `targetRef`'s new oid.
|
|
@@ -106,7 +108,7 @@ Per-repo isomorphic-git packfile `cache` object management, keyed `` `${ownerKey
|
|
|
106
108
|
## Semantics & limitations
|
|
107
109
|
|
|
108
110
|
- `threeWayMerge`'s content merge (for paths both sides changed from the merge base — most changes are added/deleted/single-side-modified and resolve without touching it at all) uses [`node-diff3`](https://www.npmjs.com/package/node-diff3)'s LCS-based diff3 algorithm, the same approach GNU diffutils' `diff3` and git's own default merge driver use.
|
|
109
|
-
-
|
|
111
|
+
- Binary files (a NUL byte in any version) are never line-merged; both sides changing one is a conflict. No rename detection; criss-cross histories use the first merge base.
|
|
110
112
|
- The parsed-object cache and `getRepoCache`/`invalidateRepoCache` are general-purpose helpers, not required by `threeWayMerge` — use whichever pieces you need independently.
|
|
111
113
|
|
|
112
114
|
## License
|
package/dist/index.js
CHANGED
|
@@ -60,126 +60,122 @@ var GitMergeConflictError = class extends GitEdgeError {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
// src/merge.ts
|
|
63
|
+
import { withPackedWrites } from "git-fs-s3/ops";
|
|
63
64
|
import git from "isomorphic-git";
|
|
64
65
|
import { merge as diff3Merge } from "node-diff3";
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
timezoneOffset: 0
|
|
71
|
-
};
|
|
66
|
+
var isTree = (entry) => entry?.type === "tree";
|
|
67
|
+
var isFile = (entry) => entry?.type === "blob";
|
|
68
|
+
function same(a, b) {
|
|
69
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
70
|
+
return a.oid === b.oid && a.mode === b.mode;
|
|
72
71
|
}
|
|
73
|
-
async function
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
entries.map(async (entry) => {
|
|
78
|
-
const fullPath = prefix ? `${prefix}/${entry.path}` : entry.path;
|
|
79
|
-
if (entry.type === "tree") {
|
|
80
|
-
const sub = await flattenTreeDeep(repo, entry.oid, fullPath);
|
|
81
|
-
for (const [k, v] of sub) map.set(k, v);
|
|
82
|
-
} else {
|
|
83
|
-
map.set(fullPath, entry.oid);
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
);
|
|
87
|
-
return map;
|
|
72
|
+
async function readEntries(repo, treeOid) {
|
|
73
|
+
if (treeOid === void 0) return /* @__PURE__ */ new Map();
|
|
74
|
+
const { tree } = await git.readTree({ ...repo, oid: treeOid });
|
|
75
|
+
return new Map(tree.map((entry) => [entry.path, entry]));
|
|
88
76
|
}
|
|
89
77
|
async function readBlob(repo, oid) {
|
|
90
78
|
const { blob } = await git.readBlob({ ...repo, oid });
|
|
91
79
|
return blob;
|
|
92
80
|
}
|
|
93
|
-
|
|
94
|
-
return git.writeBlob({ ...repo, blob: content });
|
|
95
|
-
}
|
|
96
|
-
async function writeTreeObject(repo, entries) {
|
|
97
|
-
return git.writeObject({
|
|
98
|
-
...repo,
|
|
99
|
-
type: "tree",
|
|
100
|
-
object: entries.map((e) => ({
|
|
101
|
-
path: e.path,
|
|
102
|
-
oid: e.oid,
|
|
103
|
-
mode: e.mode,
|
|
104
|
-
type: e.type
|
|
105
|
-
})),
|
|
106
|
-
format: "parsed"
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
async function writeTreeFromFlat(repo, flat) {
|
|
110
|
-
const root = /* @__PURE__ */ new Map();
|
|
111
|
-
const children = /* @__PURE__ */ new Map();
|
|
112
|
-
for (const [p, v] of flat) {
|
|
113
|
-
const slash = p.indexOf("/");
|
|
114
|
-
if (slash === -1) {
|
|
115
|
-
root.set(p, v);
|
|
116
|
-
} else {
|
|
117
|
-
const dir = p.slice(0, slash);
|
|
118
|
-
const rest = p.slice(slash + 1);
|
|
119
|
-
if (!children.has(dir)) children.set(dir, /* @__PURE__ */ new Map());
|
|
120
|
-
children.get(dir)?.set(rest, v);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
const treeEntries = [];
|
|
124
|
-
for (const [name, { oid, mode }] of root) {
|
|
125
|
-
treeEntries.push({ path: name, oid, mode, type: "blob" });
|
|
126
|
-
}
|
|
127
|
-
for (const [dirName, subMap] of children) {
|
|
128
|
-
const subOid = await writeTreeFromFlat(repo, subMap);
|
|
129
|
-
treeEntries.push({
|
|
130
|
-
path: dirName,
|
|
131
|
-
oid: subOid,
|
|
132
|
-
mode: "040000",
|
|
133
|
-
type: "tree"
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
return writeTreeObject(repo, treeEntries);
|
|
137
|
-
}
|
|
138
|
-
function mergeContents(base, ours, theirs) {
|
|
81
|
+
function mergeText(base, ours, theirs) {
|
|
139
82
|
const { conflict, result } = diff3Merge(
|
|
140
83
|
decodeLines(ours),
|
|
141
84
|
decodeLines(base),
|
|
142
85
|
decodeLines(theirs),
|
|
143
86
|
{ label: { a: "ours", b: "theirs" } }
|
|
144
87
|
);
|
|
145
|
-
return { content:
|
|
88
|
+
return { content: new TextEncoder().encode(result.join("\n")), conflict };
|
|
146
89
|
}
|
|
147
90
|
function decodeLines(data) {
|
|
148
91
|
const text = new TextDecoder().decode(data);
|
|
149
|
-
|
|
150
|
-
|
|
92
|
+
return text === "" ? [] : text.split("\n");
|
|
93
|
+
}
|
|
94
|
+
var isBinary = (data) => data.includes(0);
|
|
95
|
+
function mergeMode(base, ours, theirs) {
|
|
96
|
+
if (ours.mode === theirs.mode) return ours.mode;
|
|
97
|
+
if (base?.mode === ours.mode) return theirs.mode;
|
|
98
|
+
if (base?.mode === theirs.mode) return ours.mode;
|
|
99
|
+
return null;
|
|
151
100
|
}
|
|
152
|
-
function
|
|
153
|
-
|
|
101
|
+
async function mergeChangedEntry(repo, path, base, ours, theirs, conflicts) {
|
|
102
|
+
if (isTree(ours) && isTree(theirs)) {
|
|
103
|
+
const oid2 = await mergeTrees(
|
|
104
|
+
repo,
|
|
105
|
+
path,
|
|
106
|
+
isTree(base) ? base.oid : void 0,
|
|
107
|
+
ours.oid,
|
|
108
|
+
theirs.oid,
|
|
109
|
+
conflicts
|
|
110
|
+
);
|
|
111
|
+
return oid2 === null ? null : { mode: "040000", oid: oid2, type: "tree" };
|
|
112
|
+
}
|
|
113
|
+
if (!isFile(ours) || !isFile(theirs) || ours.mode === "120000") {
|
|
114
|
+
conflicts.push(path);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const mode = mergeMode(base, ours, theirs);
|
|
118
|
+
if (mode === null) {
|
|
119
|
+
conflicts.push(path);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (ours.oid === theirs.oid) return { mode, oid: ours.oid, type: "blob" };
|
|
123
|
+
const [baseContent, oursContent, theirsContent] = await Promise.all([
|
|
124
|
+
isFile(base) ? readBlob(repo, base.oid) : new Uint8Array(),
|
|
125
|
+
readBlob(repo, ours.oid),
|
|
126
|
+
readBlob(repo, theirs.oid)
|
|
127
|
+
]);
|
|
128
|
+
if (isBinary(baseContent) || isBinary(oursContent) || isBinary(theirsContent)) {
|
|
129
|
+
conflicts.push(path);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const { content, conflict } = mergeText(
|
|
133
|
+
baseContent,
|
|
134
|
+
oursContent,
|
|
135
|
+
theirsContent
|
|
136
|
+
);
|
|
137
|
+
if (conflict) {
|
|
138
|
+
conflicts.push(path);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const oid = await git.writeBlob({ ...repo, blob: content });
|
|
142
|
+
return { mode, oid, type: "blob" };
|
|
143
|
+
}
|
|
144
|
+
async function mergeTrees(repo, prefix, baseOid, oursOid, theirsOid, conflicts) {
|
|
145
|
+
const [base, ours, theirs] = await Promise.all([
|
|
146
|
+
readEntries(repo, baseOid),
|
|
147
|
+
readEntries(repo, oursOid),
|
|
148
|
+
readEntries(repo, theirsOid)
|
|
149
|
+
]);
|
|
150
|
+
const names = /* @__PURE__ */ new Set([...base.keys(), ...ours.keys(), ...theirs.keys()]);
|
|
151
|
+
const merged = [];
|
|
152
|
+
const before = conflicts.length;
|
|
153
|
+
await Promise.all(
|
|
154
|
+
[...names].map(async (name) => {
|
|
155
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
156
|
+
const b = base.get(name);
|
|
157
|
+
const o = ours.get(name);
|
|
158
|
+
const t = theirs.get(name);
|
|
159
|
+
let result;
|
|
160
|
+
if (same(o, t)) result = o;
|
|
161
|
+
else if (same(b, o)) result = t;
|
|
162
|
+
else if (same(b, t)) result = o;
|
|
163
|
+
else result = await mergeChangedEntry(repo, path, b, o, t, conflicts);
|
|
164
|
+
if (result) merged.push({ ...result, path: name });
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
if (conflicts.length > before) return null;
|
|
168
|
+
return git.writeTree({ ...repo, tree: merged });
|
|
154
169
|
}
|
|
155
170
|
async function threeWayMerge(repo, sourceRef, targetRef, opts) {
|
|
156
171
|
const [sourceOid, targetOid] = await Promise.all([
|
|
157
172
|
git.resolveRef({ ...repo, ref: sourceRef }),
|
|
158
173
|
git.resolveRef({ ...repo, ref: targetRef })
|
|
159
174
|
]);
|
|
160
|
-
if (sourceOid === targetOid) {
|
|
161
|
-
return { commitOid:
|
|
175
|
+
if (sourceOid === targetOid || await git.isDescendent({ ...repo, oid: targetOid, ancestor: sourceOid })) {
|
|
176
|
+
return { commitOid: targetOid };
|
|
162
177
|
}
|
|
163
|
-
|
|
164
|
-
...repo,
|
|
165
|
-
oid: sourceOid,
|
|
166
|
-
ancestor: targetOid
|
|
167
|
-
});
|
|
168
|
-
if (isSourceAhead) {
|
|
169
|
-
await git.writeRef({
|
|
170
|
-
...repo,
|
|
171
|
-
ref: targetRef,
|
|
172
|
-
value: sourceOid,
|
|
173
|
-
force: true
|
|
174
|
-
});
|
|
175
|
-
return { commitOid: sourceOid };
|
|
176
|
-
}
|
|
177
|
-
const isTargetAhead = await git.isDescendent({
|
|
178
|
-
...repo,
|
|
179
|
-
oid: targetOid,
|
|
180
|
-
ancestor: sourceOid
|
|
181
|
-
});
|
|
182
|
-
if (isTargetAhead) {
|
|
178
|
+
if (await git.isDescendent({ ...repo, oid: sourceOid, ancestor: targetOid })) {
|
|
183
179
|
await git.writeRef({
|
|
184
180
|
...repo,
|
|
185
181
|
ref: targetRef,
|
|
@@ -192,110 +188,54 @@ async function threeWayMerge(repo, sourceRef, targetRef, opts) {
|
|
|
192
188
|
...repo,
|
|
193
189
|
oids: [sourceOid, targetOid]
|
|
194
190
|
});
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
const baseOid = baseOids?.[0];
|
|
192
|
+
if (baseOid === void 0) {
|
|
193
|
+
throw new GitEdgeError(
|
|
194
|
+
`Refusing to merge unrelated histories (${sourceRef} and ${targetRef} share no commit)`
|
|
195
|
+
);
|
|
197
196
|
}
|
|
198
|
-
const baseOid = baseOids[0];
|
|
199
197
|
const [baseCommit, sourceCommit, targetCommit] = await Promise.all([
|
|
200
198
|
git.readCommit({ ...repo, oid: baseOid }),
|
|
201
199
|
git.readCommit({ ...repo, oid: sourceOid }),
|
|
202
200
|
git.readCommit({ ...repo, oid: targetOid })
|
|
203
201
|
]);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
]);
|
|
217
|
-
const resultEntries = /* @__PURE__ */ new Map();
|
|
218
|
-
const conflictingPaths = [];
|
|
219
|
-
for (const filepath of allPaths) {
|
|
220
|
-
const baseOid2 = baseMap.get(filepath);
|
|
221
|
-
const sourceOid2 = sourceMap.get(filepath);
|
|
222
|
-
const targetOid2 = targetMap.get(filepath);
|
|
223
|
-
if (sourceOid2 !== void 0 && targetOid2 === void 0) {
|
|
224
|
-
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
225
|
-
} else if (sourceOid2 === void 0 && targetOid2 !== void 0) {
|
|
226
|
-
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
227
|
-
} else if (sourceOid2 !== void 0 && targetOid2 !== void 0) {
|
|
228
|
-
const sourceChanged = sourceOid2 !== baseOid2;
|
|
229
|
-
const targetChanged = targetOid2 !== baseOid2;
|
|
230
|
-
if (!sourceChanged && !targetChanged) {
|
|
231
|
-
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
232
|
-
} else if (!sourceChanged) {
|
|
233
|
-
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
234
|
-
} else if (!targetChanged) {
|
|
235
|
-
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
236
|
-
} else if (baseOid2 === void 0) {
|
|
237
|
-
if (sourceOid2 === targetOid2) {
|
|
238
|
-
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
239
|
-
} else {
|
|
240
|
-
const [sourceContent, targetContent] = await Promise.all([
|
|
241
|
-
readBlob(repo, sourceOid2),
|
|
242
|
-
readBlob(repo, targetOid2)
|
|
243
|
-
]);
|
|
244
|
-
const { content, conflict } = mergeContents(
|
|
245
|
-
new Uint8Array(),
|
|
246
|
-
sourceContent,
|
|
247
|
-
targetContent
|
|
248
|
-
);
|
|
249
|
-
if (conflict) conflictingPaths.push(filepath);
|
|
250
|
-
const mergedOid = await writeBlob(repo, content);
|
|
251
|
-
resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
|
|
252
|
-
}
|
|
253
|
-
} else if (sourceOid2 === targetOid2) {
|
|
254
|
-
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
255
|
-
} else {
|
|
256
|
-
const [baseContent, sourceContent, targetContent] = await Promise.all([
|
|
257
|
-
readBlob(repo, baseOid2),
|
|
258
|
-
readBlob(repo, sourceOid2),
|
|
259
|
-
readBlob(repo, targetOid2)
|
|
260
|
-
]);
|
|
261
|
-
const { content, conflict } = mergeContents(
|
|
262
|
-
baseContent,
|
|
263
|
-
sourceContent,
|
|
264
|
-
targetContent
|
|
265
|
-
);
|
|
266
|
-
if (conflict) conflictingPaths.push(filepath);
|
|
267
|
-
const mergedOid = await writeBlob(repo, content);
|
|
268
|
-
resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
|
|
269
|
-
}
|
|
270
|
-
} else {
|
|
202
|
+
return withPackedWrites(repo, async (packed) => {
|
|
203
|
+
const conflicts = [];
|
|
204
|
+
const treeOid = await mergeTrees(
|
|
205
|
+
packed,
|
|
206
|
+
"",
|
|
207
|
+
baseCommit.commit.tree,
|
|
208
|
+
targetCommit.commit.tree,
|
|
209
|
+
sourceCommit.commit.tree,
|
|
210
|
+
conflicts
|
|
211
|
+
);
|
|
212
|
+
if (treeOid === null) {
|
|
213
|
+
throw new GitMergeConflictError(conflicts.sort());
|
|
271
214
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
value: mergeCommitOid,
|
|
296
|
-
force: true
|
|
215
|
+
const author = {
|
|
216
|
+
name: opts?.authorName || "Git Edge",
|
|
217
|
+
email: opts?.authorEmail || "git-edge@local",
|
|
218
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
219
|
+
timezoneOffset: 0
|
|
220
|
+
};
|
|
221
|
+
const commitOid = await git.writeCommit({
|
|
222
|
+
...packed,
|
|
223
|
+
commit: {
|
|
224
|
+
message: opts?.message || `Merge ${sourceRef} into ${targetRef}`,
|
|
225
|
+
tree: treeOid,
|
|
226
|
+
parent: [targetOid, sourceOid],
|
|
227
|
+
author,
|
|
228
|
+
committer: author
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
await git.writeRef({
|
|
232
|
+
...packed,
|
|
233
|
+
ref: targetRef,
|
|
234
|
+
value: commitOid,
|
|
235
|
+
force: true
|
|
236
|
+
});
|
|
237
|
+
return { commitOid };
|
|
297
238
|
});
|
|
298
|
-
return { commitOid: mergeCommitOid };
|
|
299
239
|
}
|
|
300
240
|
|
|
301
241
|
// src/repo.ts
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cache.ts","../src/errors.ts","../src/merge.ts","../src/repo.ts"],"sourcesContent":["/**\n * Generalized parsed-object LRU cache for isomorphic-git.\n *\n * Stores JS values directly — avoids JSON.parse on every hot-path read.\n * Configurable maxSize (rough byte estimate) and TTL.\n *\n * Edge-compatible: no node: imports.\n */\nimport { LRUCache } from \"lru-cache\";\n\nexport interface ParsedObjectCacheOptions {\n\t/** Maximum estimated size in bytes (default: 256 MB). */\n\tmaxSize?: number;\n\t/** Time-to-live in milliseconds (default: 1 hour). */\n\tttl?: number;\n}\n\nexport interface ParsedObjectStore {\n\tget<T extends object>(key: string): T | null;\n\tset<T extends object>(key: string, value: T): void;\n\tdelete(key: string): void;\n\t/** Delete all keys matching a prefix. */\n\tinvalidatePrefix(prefix: string): void;\n}\n\n/**\n * Create a parsed-object cache instance.\n *\n * `key` convention: `\"${oid}:${format}\"` or any caller-chosen string.\n * `sizeCalculation` walks the value for a rough byte estimate.\n */\n// Rough byte estimate without serializing: JSON.stringify on every set\n// allocated a full copy of large values (commit logs, trees) and expanded\n// byte arrays into decimal text, both on the hot path.\nfunction estimateSize(value: unknown): number {\n\tif (typeof value === \"string\") return value.length;\n\tif (value === null || typeof value !== \"object\") return 8;\n\tif (ArrayBuffer.isView(value)) return value.byteLength;\n\tlet size = 0;\n\tif (Array.isArray(value)) {\n\t\tfor (const item of value) size += estimateSize(item);\n\t\treturn size;\n\t}\n\tfor (const [k, v] of Object.entries(value)) {\n\t\tsize += k.length + estimateSize(v);\n\t}\n\treturn size;\n}\n\nexport function createParsedObjectCache(\n\toptions?: ParsedObjectCacheOptions,\n): ParsedObjectStore {\n\tconst maxSize = options?.maxSize ?? 256 * 1024 * 1024;\n\tconst ttl = options?.ttl ?? 3600 * 1000;\n\n\tconst cache = new LRUCache<string, object>({\n\t\tmaxSize,\n\t\tsizeCalculation: (v) => Math.max(1, estimateSize(v)),\n\t\tttl,\n\t});\n\n\treturn {\n\t\tget<T extends object>(key: string): T | null {\n\t\t\treturn (cache.get(key) as T) ?? null;\n\t\t},\n\t\tset<T extends object>(key: string, value: T): void {\n\t\t\tcache.set(key, value);\n\t\t},\n\t\tdelete(key: string): void {\n\t\t\tcache.delete(key);\n\t\t},\n\t\tinvalidatePrefix(prefix: string): void {\n\t\t\tfor (const k of cache.keys()) {\n\t\t\t\tif (k.startsWith(prefix)) cache.delete(k);\n\t\t\t}\n\t\t},\n\t};\n}\n","/**\n * Error types for git-edge.\n *\n * Edge-compatible: no node: imports.\n */\n\n/** Base class for all git-edge errors. */\nexport class GitEdgeError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"GitEdgeError\";\n\t\tError.captureStackTrace?.(this, this.constructor);\n\t}\n}\n\n/** Thrown when a three-way merge has unresolvable conflicts. */\nexport class GitMergeConflictError extends GitEdgeError {\n\treadonly conflictingPaths: string[];\n\n\tconstructor(conflictingPaths: string[]) {\n\t\tsuper(\n\t\t\t`Merge conflict in ${conflictingPaths.length} file(s): ${conflictingPaths.join(\", \")}`,\n\t\t);\n\t\tthis.name = \"GitMergeConflictError\";\n\t\tthis.conflictingPaths = conflictingPaths;\n\t}\n}\n","/**\n * Object-level three-way merge for isomorphic-git.\n *\n * Merges two commits by walking their trees directly — no worktree, no disk.\n * Returns the merged commit OID, or throws MergeConflictError with the list of\n * conflicting file paths.\n *\n * Edge-compatible: no node: imports, uses only isomorphic-git object APIs.\n */\n\nimport type { Repo } from \"git-fs-s3/ops\";\nimport git from \"isomorphic-git\";\nimport { merge as diff3Merge } from \"node-diff3\";\nimport { GitMergeConflictError } from \"./errors.js\";\n\nexport type { Repo };\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Options for threeWayMerge. */\nexport interface MergeOpts {\n\t/** Merge commit message (default: \"Merge <source> into <target>\"). */\n\tmessage?: string;\n\t/** Author name (default: \"Git Edge\"). */\n\tauthorName?: string;\n\t/** Author email (default: \"git-edge@local\"). */\n\tauthorEmail?: string;\n}\n\n/** Result of a successful merge. */\nexport interface MergeResult {\n\tcommitOid: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction defaultAuthor() {\n\treturn {\n\t\tname: \"Git Edge\",\n\t\temail: \"git-edge@local\",\n\t\ttimestamp: Math.floor(Date.now() / 1000),\n\t\ttimezoneOffset: 0,\n\t};\n}\n\n/** Recursively flatten a tree into path → oid entries. */\nasync function flattenTreeDeep(\n\trepo: Repo,\n\ttreeOid: string,\n\tprefix: string,\n): Promise<Map<string, string>> {\n\tconst map = new Map<string, string>();\n\tconst entries = (await git.readTree({ ...repo, oid: treeOid })).tree;\n\tawait Promise.all(\n\t\tentries.map(async (entry) => {\n\t\t\tconst fullPath = prefix ? `${prefix}/${entry.path}` : entry.path;\n\t\t\tif (entry.type === \"tree\") {\n\t\t\t\tconst sub = await flattenTreeDeep(repo, entry.oid, fullPath);\n\t\t\t\tfor (const [k, v] of sub) map.set(k, v);\n\t\t\t} else {\n\t\t\t\tmap.set(fullPath, entry.oid);\n\t\t\t}\n\t\t}),\n\t);\n\treturn map;\n}\n\n/** Read a blob and return it as a Uint8Array. */\nasync function readBlob(repo: Repo, oid: string): Promise<Uint8Array> {\n\tconst { blob } = await git.readBlob({ ...repo, oid });\n\treturn blob;\n}\n\n/** Write a blob into the object store. */\nasync function writeBlob(repo: Repo, content: Uint8Array): Promise<string> {\n\treturn git.writeBlob({ ...repo, blob: content });\n}\n\n/** Write a tree object from a flat path→oid map. */\nasync function writeTreeObject(\n\trepo: Repo,\n\tentries: Array<{\n\t\tpath: string;\n\t\toid: string;\n\t\tmode: string;\n\t\ttype: \"blob\" | \"tree\";\n\t}>,\n): Promise<string> {\n\treturn git.writeObject({\n\t\t...repo,\n\t\ttype: \"tree\",\n\t\tobject: entries.map((e) => ({\n\t\t\tpath: e.path,\n\t\t\toid: e.oid,\n\t\t\tmode: e.mode,\n\t\t\ttype: e.type,\n\t\t})),\n\t\tformat: \"parsed\",\n\t});\n}\n\n/**\n * Write a tree from a flat path→{oid,mode} map by recursively building\n * intermediate trees for subdirectories.\n */\nasync function writeTreeFromFlat(\n\trepo: Repo,\n\tflat: Map<string, { oid: string; mode: string }>,\n): Promise<string> {\n\t// Group entries by first path segment\n\tconst root = new Map<string, { oid: string; mode: string }>();\n\tconst children = new Map<\n\t\tstring,\n\t\tMap<string, { oid: string; mode: string }>\n\t>();\n\n\tfor (const [p, v] of flat) {\n\t\tconst slash = p.indexOf(\"/\");\n\t\tif (slash === -1) {\n\t\t\troot.set(p, v);\n\t\t} else {\n\t\t\tconst dir = p.slice(0, slash);\n\t\t\tconst rest = p.slice(slash + 1);\n\t\t\tif (!children.has(dir)) children.set(dir, new Map());\n\t\t\tchildren.get(dir)?.set(rest, v);\n\t\t}\n\t}\n\n\tconst treeEntries: Array<{\n\t\tpath: string;\n\t\toid: string;\n\t\tmode: string;\n\t\ttype: \"blob\" | \"tree\";\n\t}> = [];\n\n\tfor (const [name, { oid, mode }] of root) {\n\t\ttreeEntries.push({ path: name, oid, mode, type: \"blob\" });\n\t}\n\n\tfor (const [dirName, subMap] of children) {\n\t\tconst subOid = await writeTreeFromFlat(repo, subMap);\n\t\ttreeEntries.push({\n\t\t\tpath: dirName,\n\t\t\toid: subOid,\n\t\t\tmode: \"040000\",\n\t\t\ttype: \"tree\",\n\t\t});\n\t}\n\n\treturn writeTreeObject(repo, treeEntries);\n}\n\n/**\n * Line-level 3-way merge, via node-diff3's LCS-based diff3 algorithm — the\n * same approach GNU diffutils' `diff3` (and git's own default merge driver)\n * use. Conflicts are marked with git's standard `<<<<<<< ours` / `=======` /\n * `>>>>>>> theirs` markers; `conflict` reports whether any were needed.\n *\n * Previously a hand-rolled lockstep line-by-line walk (compare base[i] vs\n * ours[i] vs theirs[i] at the same index across all three): correct only\n * when no side ever inserts or deletes a line, since any such edit shifts\n * every later index out of alignment — which the walk had no way to detect\n * or recover from, so it could misreport unrelated later lines as\n * conflicting, or — with no conflict raised — silently merge misaligned\n * lines into corrupted content.\n */\nfunction mergeContents(\n\tbase: Uint8Array,\n\tours: Uint8Array,\n\ttheirs: Uint8Array,\n): { content: Uint8Array; conflict: boolean } {\n\tconst { conflict, result } = diff3Merge(\n\t\tdecodeLines(ours),\n\t\tdecodeLines(base),\n\t\tdecodeLines(theirs),\n\t\t{ label: { a: \"ours\", b: \"theirs\" } },\n\t);\n\treturn { content: encodeLines(result), conflict };\n}\n\nfunction decodeLines(data: Uint8Array): string[] {\n\tconst text = new TextDecoder().decode(data);\n\tif (text === \"\") return [];\n\treturn text.split(\"\\n\");\n}\n\nfunction encodeLines(lines: string[]): Uint8Array {\n\treturn new TextEncoder().encode(lines.join(\"\\n\"));\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Perform an object-level three-way merge.\n *\n * - If source is an ancestor of target (fast-forward), moves target ref to source.\n * - If target is an ancestor of source (reverse FF), moves target ref to source.\n * - Otherwise, performs a true 3-way merge at the tree level.\n *\n * Throws `GitMergeConflictError` if the merge has unresolvable conflicts.\n */\nexport async function threeWayMerge(\n\trepo: Repo,\n\tsourceRef: string,\n\ttargetRef: string,\n\topts?: MergeOpts,\n): Promise<MergeResult> {\n\tconst [sourceOid, targetOid] = await Promise.all([\n\t\tgit.resolveRef({ ...repo, ref: sourceRef }),\n\t\tgit.resolveRef({ ...repo, ref: targetRef }),\n\t]);\n\n\tif (sourceOid === targetOid) {\n\t\treturn { commitOid: sourceOid };\n\t}\n\n\t// Fast-forward: source is ahead of target\n\tconst isSourceAhead = await git.isDescendent({\n\t\t...repo,\n\t\toid: sourceOid,\n\t\tancestor: targetOid,\n\t});\n\tif (isSourceAhead) {\n\t\tawait git.writeRef({\n\t\t\t...repo,\n\t\t\tref: targetRef,\n\t\t\tvalue: sourceOid,\n\t\t\tforce: true,\n\t\t});\n\t\treturn { commitOid: sourceOid };\n\t}\n\n\t// Reverse fast-forward: target is ahead of source\n\tconst isTargetAhead = await git.isDescendent({\n\t\t...repo,\n\t\toid: targetOid,\n\t\tancestor: sourceOid,\n\t});\n\tif (isTargetAhead) {\n\t\tawait git.writeRef({\n\t\t\t...repo,\n\t\t\tref: targetRef,\n\t\t\tvalue: sourceOid,\n\t\t\tforce: true,\n\t\t});\n\t\treturn { commitOid: sourceOid };\n\t}\n\n\t// True 3-way merge\n\tconst baseOids = await git.findMergeBase({\n\t\t...repo,\n\t\toids: [sourceOid, targetOid],\n\t});\n\n\tif (!baseOids || baseOids.length === 0) {\n\t\tthrow new Error(\"No merge base found between source and target\");\n\t}\n\n\tconst baseOid = baseOids[0];\n\n\t// Read commit objects to get their trees\n\tconst [baseCommit, sourceCommit, targetCommit] = await Promise.all([\n\t\tgit.readCommit({ ...repo, oid: baseOid }),\n\t\tgit.readCommit({ ...repo, oid: sourceOid }),\n\t\tgit.readCommit({ ...repo, oid: targetOid }),\n\t]);\n\n\tconst baseTreeOid = baseCommit.commit.tree;\n\tconst sourceTreeOid = sourceCommit.commit.tree;\n\tconst targetTreeOid = targetCommit.commit.tree;\n\n\t// Flatten all three trees\n\tconst [baseMap, sourceMap, targetMap] = await Promise.all([\n\t\tflattenTreeDeep(repo, baseTreeOid, \"\"),\n\t\tflattenTreeDeep(repo, sourceTreeOid, \"\"),\n\t\tflattenTreeDeep(repo, targetTreeOid, \"\"),\n\t]);\n\n\t// Collect all unique paths\n\tconst allPaths = new Set([\n\t\t...baseMap.keys(),\n\t\t...sourceMap.keys(),\n\t\t...targetMap.keys(),\n\t]);\n\n\tconst resultEntries = new Map<string, { oid: string; mode: string }>();\n\tconst conflictingPaths: string[] = [];\n\n\tfor (const filepath of allPaths) {\n\t\tconst baseOid = baseMap.get(filepath);\n\t\tconst sourceOid = sourceMap.get(filepath);\n\t\tconst targetOid = targetMap.get(filepath);\n\n\t\tif (sourceOid !== undefined && targetOid === undefined) {\n\t\t\t// Added in source, not in target — take source\n\t\t\tresultEntries.set(filepath, { oid: sourceOid, mode: \"100644\" });\n\t\t} else if (sourceOid === undefined && targetOid !== undefined) {\n\t\t\t// Deleted in source, exists in target — keep target\n\t\t\tresultEntries.set(filepath, { oid: targetOid, mode: \"100644\" });\n\t\t} else if (sourceOid !== undefined && targetOid !== undefined) {\n\t\t\t// Exists in both — check if both changed from base. A path with\n\t\t\t// no base entry (both sides independently added it — no common\n\t\t\t// ancestor content to diff against) is its own case below,\n\t\t\t// compared directly against each other instead of a base blob\n\t\t\t// that doesn't exist.\n\t\t\tconst sourceChanged = sourceOid !== baseOid;\n\t\t\tconst targetChanged = targetOid !== baseOid;\n\n\t\t\tif (!sourceChanged && !targetChanged) {\n\t\t\t\t// Neither changed — keep as-is\n\t\t\t\tresultEntries.set(filepath, { oid: targetOid, mode: \"100644\" });\n\t\t\t} else if (!sourceChanged) {\n\t\t\t\t// Only target changed — take target\n\t\t\t\tresultEntries.set(filepath, { oid: targetOid, mode: \"100644\" });\n\t\t\t} else if (!targetChanged) {\n\t\t\t\t// Only source changed — take source\n\t\t\t\tresultEntries.set(filepath, { oid: sourceOid, mode: \"100644\" });\n\t\t\t} else if (baseOid === undefined) {\n\t\t\t\t// Both sides independently added this path. Same oid means\n\t\t\t\t// identical content on both sides — no real conflict. Different\n\t\t\t\t// oids is a genuine conflict with no common ancestor to 3-way\n\t\t\t\t// merge against, so run the line-level merge against an empty\n\t\t\t\t// \"base\" — mergeContents already treats an exhausted side as\n\t\t\t\t// producing full conflict markers around whatever content the\n\t\t\t\t// other side has.\n\t\t\t\tif (sourceOid === targetOid) {\n\t\t\t\t\tresultEntries.set(filepath, { oid: sourceOid, mode: \"100644\" });\n\t\t\t\t} else {\n\t\t\t\t\tconst [sourceContent, targetContent] = await Promise.all([\n\t\t\t\t\t\treadBlob(repo, sourceOid),\n\t\t\t\t\t\treadBlob(repo, targetOid),\n\t\t\t\t\t]);\n\t\t\t\t\tconst { content, conflict } = mergeContents(\n\t\t\t\t\t\tnew Uint8Array(),\n\t\t\t\t\t\tsourceContent,\n\t\t\t\t\t\ttargetContent,\n\t\t\t\t\t);\n\t\t\t\t\tif (conflict) conflictingPaths.push(filepath);\n\t\t\t\t\tconst mergedOid = await writeBlob(repo, content);\n\t\t\t\t\tresultEntries.set(filepath, { oid: mergedOid, mode: \"100644\" });\n\t\t\t\t}\n\t\t\t} else if (sourceOid === targetOid) {\n\t\t\t\t// Both changed from base, but to identical content (a real,\n\t\t\t\t// reachable case — e.g. both sides applied the same\n\t\t\t\t// formatter) — oid equality already proves this, no blob\n\t\t\t\t// read needed. (The reverse isn't checked: sourceOid or\n\t\t\t\t// targetOid equaling baseOid was already ruled out by\n\t\t\t\t// sourceChanged/targetChanged above, so source/target can\n\t\t\t\t// never equal base content here — no byte comparison against\n\t\t\t\t// base is reachable either.)\n\t\t\t\tresultEntries.set(filepath, { oid: sourceOid, mode: \"100644\" });\n\t\t\t} else {\n\t\t\t\t// Both changed from base, to different content — attempt a\n\t\t\t\t// real line-level merge.\n\t\t\t\tconst [baseContent, sourceContent, targetContent] = await Promise.all([\n\t\t\t\t\treadBlob(repo, baseOid),\n\t\t\t\t\treadBlob(repo, sourceOid),\n\t\t\t\t\treadBlob(repo, targetOid),\n\t\t\t\t]);\n\t\t\t\tconst { content, conflict } = mergeContents(\n\t\t\t\t\tbaseContent,\n\t\t\t\t\tsourceContent,\n\t\t\t\t\ttargetContent,\n\t\t\t\t);\n\t\t\t\tif (conflict) conflictingPaths.push(filepath);\n\t\t\t\tconst mergedOid = await writeBlob(repo, content);\n\t\t\t\tresultEntries.set(filepath, { oid: mergedOid, mode: \"100644\" });\n\t\t\t}\n\t\t} else {\n\t\t\t// Deleted in both — no-op\n\t\t}\n\t}\n\n\tif (conflictingPaths.length > 0) {\n\t\tthrow new GitMergeConflictError(conflictingPaths);\n\t}\n\n\t// Build the result tree\n\tconst resultTreeOid = await writeTreeFromFlat(repo, resultEntries);\n\n\t// Create the merge commit\n\tconst author = opts?.authorName\n\t\t? {\n\t\t\t\tname: opts.authorName,\n\t\t\t\temail: opts.authorEmail || \"git-edge@local\",\n\t\t\t\ttimestamp: Math.floor(Date.now() / 1000),\n\t\t\t\ttimezoneOffset: 0,\n\t\t\t}\n\t\t: defaultAuthor();\n\n\tconst message = opts?.message || `Merge ${sourceRef} into ${targetRef}`;\n\n\tconst mergeCommitOid = await git.commit({\n\t\t...repo,\n\t\tmessage,\n\t\ttree: resultTreeOid,\n\t\tparent: [targetOid, sourceOid],\n\t\tauthor,\n\t\tcommitter: author,\n\t});\n\n\t// Update the target ref\n\tawait git.writeRef({\n\t\t...repo,\n\t\tref: targetRef,\n\t\tvalue: mergeCommitOid,\n\t\tforce: true,\n\t});\n\n\treturn { commitOid: mergeCommitOid };\n}\n","/**\n * isomorphic-git per-repo parse cache management.\n *\n * No node: imports.\n */\n\n// ---------------------------------------------------------------------------\n// isomorphic-git cache management\n// ---------------------------------------------------------------------------\n\n/**\n * Per-repo isomorphic-git packfile cache.\n *\n * isomorphic-git re-parses a packfile index from scratch on every\n * readTree/log/readObject call unless callers share a `cache` object\n * across calls. Without this, operations that touch many objects\n * (e.g. walking commit history) pay that parse cost hundreds of times.\n * Objects are content-addressed/immutable so a long-lived per-repo\n * cache is safe.\n */\n// Each cache retains every packfile it has read, in full. Bound the number of\n// repos held so a long-lived process serving many repos doesn't grow without\n// limit; least-recently-used repos are dropped first (Map keeps insertion\n// order, and a hit re-inserts its key at the end).\nconst MAX_REPO_CACHES = 32;\nconst repoCaches = new Map<string, object>();\n\nexport function getRepoCache(ownerKey: string, repoName: string): object {\n\tconst key = `${ownerKey}/${repoName}`;\n\tlet cache = repoCaches.get(key);\n\tif (cache) {\n\t\trepoCaches.delete(key);\n\t} else {\n\t\tcache = {};\n\t\tif (repoCaches.size >= MAX_REPO_CACHES) {\n\t\t\tconst oldest = repoCaches.keys().next().value;\n\t\t\tif (oldest !== undefined) repoCaches.delete(oldest);\n\t\t}\n\t}\n\trepoCaches.set(key, cache);\n\treturn cache;\n}\n\nexport function invalidateRepoCache(ownerKey: string, repoName: string): void {\n\trepoCaches.delete(`${ownerKey}/${repoName}`);\n}\n"],"mappings":"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","names":["baseOid","sourceOid","targetOid"]}
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts","../src/errors.ts","../src/merge.ts","../src/repo.ts"],"sourcesContent":["/**\n * Generalized parsed-object LRU cache for isomorphic-git.\n *\n * Stores JS values directly — avoids JSON.parse on every hot-path read.\n * Configurable maxSize (rough byte estimate) and TTL.\n *\n * Edge-compatible: no node: imports.\n */\nimport { LRUCache } from \"lru-cache\";\n\nexport interface ParsedObjectCacheOptions {\n\t/** Maximum estimated size in bytes (default: 256 MB). */\n\tmaxSize?: number;\n\t/** Time-to-live in milliseconds (default: 1 hour). */\n\tttl?: number;\n}\n\nexport interface ParsedObjectStore {\n\tget<T extends object>(key: string): T | null;\n\tset<T extends object>(key: string, value: T): void;\n\tdelete(key: string): void;\n\t/** Delete all keys matching a prefix. */\n\tinvalidatePrefix(prefix: string): void;\n}\n\n/**\n * Create a parsed-object cache instance.\n *\n * `key` convention: `\"${oid}:${format}\"` or any caller-chosen string.\n * `sizeCalculation` walks the value for a rough byte estimate.\n */\n// Rough byte estimate without serializing: JSON.stringify on every set\n// allocated a full copy of large values (commit logs, trees) and expanded\n// byte arrays into decimal text, both on the hot path.\nfunction estimateSize(value: unknown): number {\n\tif (typeof value === \"string\") return value.length;\n\tif (value === null || typeof value !== \"object\") return 8;\n\tif (ArrayBuffer.isView(value)) return value.byteLength;\n\tlet size = 0;\n\tif (Array.isArray(value)) {\n\t\tfor (const item of value) size += estimateSize(item);\n\t\treturn size;\n\t}\n\tfor (const [k, v] of Object.entries(value)) {\n\t\tsize += k.length + estimateSize(v);\n\t}\n\treturn size;\n}\n\nexport function createParsedObjectCache(\n\toptions?: ParsedObjectCacheOptions,\n): ParsedObjectStore {\n\tconst maxSize = options?.maxSize ?? 256 * 1024 * 1024;\n\tconst ttl = options?.ttl ?? 3600 * 1000;\n\n\tconst cache = new LRUCache<string, object>({\n\t\tmaxSize,\n\t\tsizeCalculation: (v) => Math.max(1, estimateSize(v)),\n\t\tttl,\n\t});\n\n\treturn {\n\t\tget<T extends object>(key: string): T | null {\n\t\t\treturn (cache.get(key) as T) ?? null;\n\t\t},\n\t\tset<T extends object>(key: string, value: T): void {\n\t\t\tcache.set(key, value);\n\t\t},\n\t\tdelete(key: string): void {\n\t\t\tcache.delete(key);\n\t\t},\n\t\tinvalidatePrefix(prefix: string): void {\n\t\t\tfor (const k of cache.keys()) {\n\t\t\t\tif (k.startsWith(prefix)) cache.delete(k);\n\t\t\t}\n\t\t},\n\t};\n}\n","/**\n * Error types for git-edge.\n *\n * Edge-compatible: no node: imports.\n */\n\n/** Base class for all git-edge errors. */\nexport class GitEdgeError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"GitEdgeError\";\n\t\tError.captureStackTrace?.(this, this.constructor);\n\t}\n}\n\n/** Thrown when a three-way merge has unresolvable conflicts. */\nexport class GitMergeConflictError extends GitEdgeError {\n\treadonly conflictingPaths: string[];\n\n\tconstructor(conflictingPaths: string[]) {\n\t\tsuper(\n\t\t\t`Merge conflict in ${conflictingPaths.length} file(s): ${conflictingPaths.join(\", \")}`,\n\t\t);\n\t\tthis.name = \"GitMergeConflictError\";\n\t\tthis.conflictingPaths = conflictingPaths;\n\t}\n}\n","/**\n * Object-level three-way merge for isomorphic-git.\n *\n * Merges two commits by walking their trees directly — no worktree, no disk.\n * Returns the merged commit OID, or throws GitMergeConflictError with the\n * list of conflicting paths.\n *\n * Edge-compatible: no node: imports, uses only isomorphic-git object APIs.\n */\n\nimport { type Repo, withPackedWrites } from \"git-fs-s3/ops\";\nimport git from \"isomorphic-git\";\nimport { merge as diff3Merge } from \"node-diff3\";\nimport { GitEdgeError, GitMergeConflictError } from \"./errors.js\";\n\nexport type { Repo };\n\n/** Options for threeWayMerge. */\nexport interface MergeOpts {\n\t/** Merge commit message (default: \"Merge <source> into <target>\"). */\n\tmessage?: string;\n\t/** Author name (default: \"Git Edge\"). */\n\tauthorName?: string;\n\t/** Author email (default: \"git-edge@local\"). */\n\tauthorEmail?: string;\n}\n\n/** Result of a successful merge (or of a no-op: target already up to date). */\nexport interface MergeResult {\n\tcommitOid: string;\n}\n\ninterface Entry {\n\tmode: string;\n\toid: string;\n\ttype: \"blob\" | \"tree\" | \"commit\";\n}\n\nconst isTree = (entry: Entry | undefined): entry is Entry =>\n\tentry?.type === \"tree\";\nconst isFile = (entry: Entry | undefined): entry is Entry =>\n\tentry?.type === \"blob\";\n\n/** Same tree entry: both absent, or identical oid and mode. */\nfunction same(a: Entry | undefined, b: Entry | undefined): boolean {\n\tif (a === undefined || b === undefined) return a === b;\n\treturn a.oid === b.oid && a.mode === b.mode;\n}\n\nasync function readEntries(\n\trepo: Repo,\n\ttreeOid: string | undefined,\n): Promise<Map<string, Entry>> {\n\tif (treeOid === undefined) return new Map();\n\tconst { tree } = await git.readTree({ ...repo, oid: treeOid });\n\treturn new Map(tree.map((entry) => [entry.path, entry]));\n}\n\nasync function readBlob(repo: Repo, oid: string): Promise<Uint8Array> {\n\tconst { blob } = await git.readBlob({ ...repo, oid });\n\treturn blob;\n}\n\n/**\n * Line-level 3-way merge, via node-diff3's LCS-based diff3 algorithm — the\n * same approach GNU diffutils' `diff3` (and git's own default merge driver)\n * use. `conflict` reports whether any hunk couldn't be merged.\n */\nfunction mergeText(\n\tbase: Uint8Array,\n\tours: Uint8Array,\n\ttheirs: Uint8Array,\n): { content: Uint8Array; conflict: boolean } {\n\tconst { conflict, result } = diff3Merge(\n\t\tdecodeLines(ours),\n\t\tdecodeLines(base),\n\t\tdecodeLines(theirs),\n\t\t{ label: { a: \"ours\", b: \"theirs\" } },\n\t);\n\treturn { content: new TextEncoder().encode(result.join(\"\\n\")), conflict };\n}\n\nfunction decodeLines(data: Uint8Array): string[] {\n\tconst text = new TextDecoder().decode(data);\n\treturn text === \"\" ? [] : text.split(\"\\n\");\n}\n\n/** Git's own heuristic: a NUL byte means binary, which is never line-merged. */\nconst isBinary = (data: Uint8Array) => data.includes(0);\n\n/**\n * The merged mode when both sides changed a file: a mode only one side\n * changed wins; both changing it differently is a conflict (null).\n */\nfunction mergeMode(\n\tbase: Entry | undefined,\n\tours: Entry,\n\ttheirs: Entry,\n): string | null {\n\tif (ours.mode === theirs.mode) return ours.mode;\n\tif (base?.mode === ours.mode) return theirs.mode;\n\tif (base?.mode === theirs.mode) return ours.mode;\n\treturn null;\n}\n\n/**\n * Merge one path whose two sides both changed it differently. Returns the\n * merged entry, or null for a conflict (pushed onto `conflicts`).\n */\nasync function mergeChangedEntry(\n\trepo: Repo,\n\tpath: string,\n\tbase: Entry | undefined,\n\tours: Entry | undefined,\n\ttheirs: Entry | undefined,\n\tconflicts: string[],\n): Promise<Entry | null> {\n\tif (isTree(ours) && isTree(theirs)) {\n\t\tconst oid = await mergeTrees(\n\t\t\trepo,\n\t\t\tpath,\n\t\t\tisTree(base) ? base.oid : undefined,\n\t\t\tours.oid,\n\t\t\ttheirs.oid,\n\t\t\tconflicts,\n\t\t);\n\t\treturn oid === null ? null : { mode: \"040000\", oid, type: \"tree\" };\n\t}\n\t// Anything else — modify/delete, file/directory, submodule changes — is a\n\t// conflict unless both sides are regular files with mergeable content.\n\tif (!isFile(ours) || !isFile(theirs) || ours.mode === \"120000\") {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst mode = mergeMode(base, ours, theirs);\n\tif (mode === null) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tif (ours.oid === theirs.oid) return { mode, oid: ours.oid, type: \"blob\" };\n\tconst [baseContent, oursContent, theirsContent] = await Promise.all([\n\t\tisFile(base) ? readBlob(repo, base.oid) : new Uint8Array(),\n\t\treadBlob(repo, ours.oid),\n\t\treadBlob(repo, theirs.oid),\n\t]);\n\tif (\n\t\tisBinary(baseContent) ||\n\t\tisBinary(oursContent) ||\n\t\tisBinary(theirsContent)\n\t) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst { content, conflict } = mergeText(\n\t\tbaseContent,\n\t\toursContent,\n\t\ttheirsContent,\n\t);\n\tif (conflict) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst oid = await git.writeBlob({ ...repo, blob: content });\n\treturn { mode, oid, type: \"blob\" };\n}\n\n/**\n * Three-way merge of trees, recursing only into subtrees both sides changed\n * — an untouched or one-sided subtree is taken by oid without being read.\n * Returns the merged tree's oid, or null when any path conflicted.\n */\nasync function mergeTrees(\n\trepo: Repo,\n\tprefix: string,\n\tbaseOid: string | undefined,\n\toursOid: string,\n\ttheirsOid: string,\n\tconflicts: string[],\n): Promise<string | null> {\n\tconst [base, ours, theirs] = await Promise.all([\n\t\treadEntries(repo, baseOid),\n\t\treadEntries(repo, oursOid),\n\t\treadEntries(repo, theirsOid),\n\t]);\n\tconst names = new Set([...base.keys(), ...ours.keys(), ...theirs.keys()]);\n\tconst merged: Array<Entry & { path: string }> = [];\n\tconst before = conflicts.length;\n\n\tawait Promise.all(\n\t\t[...names].map(async (name) => {\n\t\t\tconst path = prefix ? `${prefix}/${name}` : name;\n\t\t\tconst b = base.get(name);\n\t\t\tconst o = ours.get(name);\n\t\t\tconst t = theirs.get(name);\n\t\t\tlet result: Entry | undefined | null;\n\t\t\tif (same(o, t)) result = o;\n\t\t\telse if (same(b, o)) result = t;\n\t\t\telse if (same(b, t)) result = o;\n\t\t\telse result = await mergeChangedEntry(repo, path, b, o, t, conflicts);\n\t\t\tif (result) merged.push({ ...result, path: name });\n\t\t}),\n\t);\n\n\tif (conflicts.length > before) return null;\n\treturn git.writeTree({ ...repo, tree: merged });\n}\n\n/**\n * Merge `sourceRef` into `targetRef`, entirely at the object level.\n *\n * - Target already contains source (same commit, or source is an ancestor):\n * nothing to do — returns the target's commit, refs untouched.\n * - Source contains target: fast-forward the target ref to source.\n * - Otherwise: a real three-way merge against the merge base. Paths only one\n * side changed take that side (deletions included); paths both changed are\n * merged — subtrees recursively, text files line by line (diff3), modes\n * kept unless both sides changed them differently. Binary files, symlinks,\n * submodules, modify/delete and file/directory clashes that both sides\n * touched are conflicts. Any conflict throws `GitMergeConflictError`\n * without writing anything.\n *\n * New objects are stored as one pack, written before the target ref moves.\n * Serialize concurrent writers to the same repository externally.\n */\nexport async function threeWayMerge(\n\trepo: Repo,\n\tsourceRef: string,\n\ttargetRef: string,\n\topts?: MergeOpts,\n): Promise<MergeResult> {\n\tconst [sourceOid, targetOid] = await Promise.all([\n\t\tgit.resolveRef({ ...repo, ref: sourceRef }),\n\t\tgit.resolveRef({ ...repo, ref: targetRef }),\n\t]);\n\n\tif (\n\t\tsourceOid === targetOid ||\n\t\t(await git.isDescendent({ ...repo, oid: targetOid, ancestor: sourceOid }))\n\t) {\n\t\treturn { commitOid: targetOid };\n\t}\n\n\tif (\n\t\tawait git.isDescendent({ ...repo, oid: sourceOid, ancestor: targetOid })\n\t) {\n\t\tawait git.writeRef({\n\t\t\t...repo,\n\t\t\tref: targetRef,\n\t\t\tvalue: sourceOid,\n\t\t\tforce: true,\n\t\t});\n\t\treturn { commitOid: sourceOid };\n\t}\n\n\tconst baseOids = await git.findMergeBase({\n\t\t...repo,\n\t\toids: [sourceOid, targetOid],\n\t});\n\tconst baseOid = baseOids?.[0];\n\tif (baseOid === undefined) {\n\t\tthrow new GitEdgeError(\n\t\t\t`Refusing to merge unrelated histories (${sourceRef} and ${targetRef} share no commit)`,\n\t\t);\n\t}\n\n\tconst [baseCommit, sourceCommit, targetCommit] = await Promise.all([\n\t\tgit.readCommit({ ...repo, oid: baseOid }),\n\t\tgit.readCommit({ ...repo, oid: sourceOid }),\n\t\tgit.readCommit({ ...repo, oid: targetOid }),\n\t]);\n\n\treturn withPackedWrites(repo, async (packed) => {\n\t\tconst conflicts: string[] = [];\n\t\tconst treeOid = await mergeTrees(\n\t\t\tpacked,\n\t\t\t\"\",\n\t\t\tbaseCommit.commit.tree,\n\t\t\ttargetCommit.commit.tree,\n\t\t\tsourceCommit.commit.tree,\n\t\t\tconflicts,\n\t\t);\n\t\tif (treeOid === null) {\n\t\t\tthrow new GitMergeConflictError(conflicts.sort());\n\t\t}\n\n\t\tconst author = {\n\t\t\tname: opts?.authorName || \"Git Edge\",\n\t\t\temail: opts?.authorEmail || \"git-edge@local\",\n\t\t\ttimestamp: Math.floor(Date.now() / 1000),\n\t\t\ttimezoneOffset: 0,\n\t\t};\n\t\t// writeCommit, not git.commit: git.commit also advances whatever\n\t\t// branch HEAD names, which may not be the target.\n\t\tconst commitOid = await git.writeCommit({\n\t\t\t...packed,\n\t\t\tcommit: {\n\t\t\t\tmessage: opts?.message || `Merge ${sourceRef} into ${targetRef}`,\n\t\t\t\ttree: treeOid,\n\t\t\t\tparent: [targetOid, sourceOid],\n\t\t\t\tauthor,\n\t\t\t\tcommitter: author,\n\t\t\t},\n\t\t});\n\t\tawait git.writeRef({\n\t\t\t...packed,\n\t\t\tref: targetRef,\n\t\t\tvalue: commitOid,\n\t\t\tforce: true,\n\t\t});\n\t\treturn { commitOid };\n\t});\n}\n","/**\n * isomorphic-git per-repo parse cache management.\n *\n * No node: imports.\n */\n\n// ---------------------------------------------------------------------------\n// isomorphic-git cache management\n// ---------------------------------------------------------------------------\n\n/**\n * Per-repo isomorphic-git packfile cache.\n *\n * isomorphic-git re-parses a packfile index from scratch on every\n * readTree/log/readObject call unless callers share a `cache` object\n * across calls. Without this, operations that touch many objects\n * (e.g. walking commit history) pay that parse cost hundreds of times.\n * Objects are content-addressed/immutable so a long-lived per-repo\n * cache is safe.\n */\n// Each cache retains every packfile it has read, in full. Bound the number of\n// repos held so a long-lived process serving many repos doesn't grow without\n// limit; least-recently-used repos are dropped first (Map keeps insertion\n// order, and a hit re-inserts its key at the end).\nconst MAX_REPO_CACHES = 32;\nconst repoCaches = new Map<string, object>();\n\nexport function getRepoCache(ownerKey: string, repoName: string): object {\n\tconst key = `${ownerKey}/${repoName}`;\n\tlet cache = repoCaches.get(key);\n\tif (cache) {\n\t\trepoCaches.delete(key);\n\t} else {\n\t\tcache = {};\n\t\tif (repoCaches.size >= MAX_REPO_CACHES) {\n\t\t\tconst oldest = repoCaches.keys().next().value;\n\t\t\tif (oldest !== undefined) repoCaches.delete(oldest);\n\t\t}\n\t}\n\trepoCaches.set(key, cache);\n\treturn cache;\n}\n\nexport function invalidateRepoCache(ownerKey: string, repoName: string): void {\n\trepoCaches.delete(`${ownerKey}/${repoName}`);\n}\n"],"mappings":"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","names":["oid"]}
|
package/dist/types/merge.d.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Object-level three-way merge for isomorphic-git.
|
|
3
3
|
*
|
|
4
4
|
* Merges two commits by walking their trees directly — no worktree, no disk.
|
|
5
|
-
* Returns the merged commit OID, or throws
|
|
6
|
-
* conflicting
|
|
5
|
+
* Returns the merged commit OID, or throws GitMergeConflictError with the
|
|
6
|
+
* list of conflicting paths.
|
|
7
7
|
*
|
|
8
8
|
* Edge-compatible: no node: imports, uses only isomorphic-git object APIs.
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
10
|
+
import { type Repo } from "git-fs-s3/ops";
|
|
11
11
|
export type { Repo };
|
|
12
12
|
/** Options for threeWayMerge. */
|
|
13
13
|
export interface MergeOpts {
|
|
@@ -18,17 +18,25 @@ export interface MergeOpts {
|
|
|
18
18
|
/** Author email (default: "git-edge@local"). */
|
|
19
19
|
authorEmail?: string;
|
|
20
20
|
}
|
|
21
|
-
/** Result of a successful merge. */
|
|
21
|
+
/** Result of a successful merge (or of a no-op: target already up to date). */
|
|
22
22
|
export interface MergeResult {
|
|
23
23
|
commitOid: string;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Merge `sourceRef` into `targetRef`, entirely at the object level.
|
|
27
27
|
*
|
|
28
|
-
* -
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
28
|
+
* - Target already contains source (same commit, or source is an ancestor):
|
|
29
|
+
* nothing to do — returns the target's commit, refs untouched.
|
|
30
|
+
* - Source contains target: fast-forward the target ref to source.
|
|
31
|
+
* - Otherwise: a real three-way merge against the merge base. Paths only one
|
|
32
|
+
* side changed take that side (deletions included); paths both changed are
|
|
33
|
+
* merged — subtrees recursively, text files line by line (diff3), modes
|
|
34
|
+
* kept unless both sides changed them differently. Binary files, symlinks,
|
|
35
|
+
* submodules, modify/delete and file/directory clashes that both sides
|
|
36
|
+
* touched are conflicts. Any conflict throws `GitMergeConflictError`
|
|
37
|
+
* without writing anything.
|
|
31
38
|
*
|
|
32
|
-
*
|
|
39
|
+
* New objects are stored as one pack, written before the target ref moves.
|
|
40
|
+
* Serialize concurrent writers to the same repository externally.
|
|
33
41
|
*/
|
|
34
42
|
export declare function threeWayMerge(repo: Repo, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-edge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "High-level edge-compatible git operations
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "High-level edge-compatible git operations \u2014 parsed-object cache, object-level three-way merge, and repo management utilities. Composes with git-fs-s3.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -54,13 +54,14 @@
|
|
|
54
54
|
"node-diff3": "^3.2.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"git-fs-s3": ">=0.
|
|
57
|
+
"git-fs-s3": ">=0.8.0",
|
|
58
58
|
"isomorphic-git": ">=1.43.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@biomejs/biome": "^2.5.15",
|
|
62
62
|
"@types/node": "^26.6.5",
|
|
63
|
-
"
|
|
63
|
+
"diff": "^9.0.0",
|
|
64
|
+
"git-fs-s3": "^0.8.0",
|
|
64
65
|
"isomorphic-git": "^1.43.3",
|
|
65
66
|
"tsup": "^8.5.1",
|
|
66
67
|
"typescript": "^7.0.2",
|