git-edge 0.2.3 → 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 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) but neither imports the other; both just agree on isomorphic-git's `{ fs, gitdir, cache? }` shape.
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
 
@@ -17,10 +17,10 @@ Separately, isomorphic-git re-parses a packfile index from scratch on every `rea
17
17
  ## Install
18
18
 
19
19
  ```bash
20
- npm install git-edge isomorphic-git
20
+ npm install git-edge git-fs-s3 isomorphic-git # ESM only, Node.js 22+
21
21
  ```
22
22
 
23
- `git-fs-s3` is an optional peer — install it if you need an S3/R2-backed `fs` to pass in; git-edge itself works with any isomorphic-git-compatible `fs` (including plain `node:fs`).
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
- - Source is an ancestor of target, or vice versa → fast-forward: just moves `targetRef`, no merge commit.
71
- - Otherwise, flattens both trees (deep, recursive) against their merge base, takes non-conflicting changes automatically, and content-merges paths both sides touched with a line-level three-way merge.
72
- - Throws `GitMergeConflictError` (with `conflictingPaths: string[]`) if any file has unresolved conflicts. The conflict markers (`<<<<<<< ours` / `=======` / `>>>>>>> theirs`) are still written to a blob and included in the (unreached) result tree — a caller that wants "write the conflicted state so a human can resolve it" can catch the error, note the paths, and re-run its own resolution flow rather than losing that information.
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
- - No binary-file merge support — conflict markers are written as text into whatever bytes the paths held; binary content will produce a nonsensical merged blob, not a clean conflict signal. Detect binary paths upstream if that matters for your use case.
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
- function defaultAuthor() {
66
- return {
67
- name: "Git Edge",
68
- email: "git-edge@local",
69
- timestamp: Math.floor(Date.now() / 1e3),
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 flattenTreeDeep(repo, treeOid, prefix) {
74
- const map = /* @__PURE__ */ new Map();
75
- const entries = (await git.readTree({ ...repo, oid: treeOid })).tree;
76
- await Promise.all(
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
- async function writeBlob(repo, content) {
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: encodeLines(result), conflict };
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
- if (text === "") return [];
150
- return text.split("\n");
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 encodeLines(lines) {
153
- return new TextEncoder().encode(lines.join("\n"));
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: sourceOid };
175
+ if (sourceOid === targetOid || await git.isDescendent({ ...repo, oid: targetOid, ancestor: sourceOid })) {
176
+ return { commitOid: targetOid };
162
177
  }
163
- const isSourceAhead = await git.isDescendent({
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
- if (!baseOids || baseOids.length === 0) {
196
- throw new Error("No merge base found between source and target");
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
- const baseTreeOid = baseCommit.commit.tree;
205
- const sourceTreeOid = sourceCommit.commit.tree;
206
- const targetTreeOid = targetCommit.commit.tree;
207
- const [baseMap, sourceMap, targetMap] = await Promise.all([
208
- flattenTreeDeep(repo, baseTreeOid, ""),
209
- flattenTreeDeep(repo, sourceTreeOid, ""),
210
- flattenTreeDeep(repo, targetTreeOid, "")
211
- ]);
212
- const allPaths = /* @__PURE__ */ new Set([
213
- ...baseMap.keys(),
214
- ...sourceMap.keys(),
215
- ...targetMap.keys()
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
- if (conflictingPaths.length > 0) {
274
- throw new GitMergeConflictError(conflictingPaths);
275
- }
276
- const resultTreeOid = await writeTreeFromFlat(repo, resultEntries);
277
- const author = opts?.authorName ? {
278
- name: opts.authorName,
279
- email: opts.authorEmail || "git-edge@local",
280
- timestamp: Math.floor(Date.now() / 1e3),
281
- timezoneOffset: 0
282
- } : defaultAuthor();
283
- const message = opts?.message || `Merge ${sourceRef} into ${targetRef}`;
284
- const mergeCommitOid = await git.commit({
285
- ...repo,
286
- message,
287
- tree: resultTreeOid,
288
- parent: [targetOid, sourceOid],
289
- author,
290
- committer: author
291
- });
292
- await git.writeRef({
293
- ...repo,
294
- ref: targetRef,
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"]}
@@ -0,0 +1,14 @@
1
+ export interface ParsedObjectCacheOptions {
2
+ /** Maximum estimated size in bytes (default: 256 MB). */
3
+ maxSize?: number;
4
+ /** Time-to-live in milliseconds (default: 1 hour). */
5
+ ttl?: number;
6
+ }
7
+ export interface ParsedObjectStore {
8
+ get<T extends object>(key: string): T | null;
9
+ set<T extends object>(key: string, value: T): void;
10
+ delete(key: string): void;
11
+ /** Delete all keys matching a prefix. */
12
+ invalidatePrefix(prefix: string): void;
13
+ }
14
+ export declare function createParsedObjectCache(options?: ParsedObjectCacheOptions): ParsedObjectStore;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Error types for git-edge.
3
+ *
4
+ * Edge-compatible: no node: imports.
5
+ */
6
+ /** Base class for all git-edge errors. */
7
+ export declare class GitEdgeError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ /** Thrown when a three-way merge has unresolvable conflicts. */
11
+ export declare class GitMergeConflictError extends GitEdgeError {
12
+ readonly conflictingPaths: string[];
13
+ constructor(conflictingPaths: string[]);
14
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * git-edge
3
+ *
4
+ * High-level edge-compatible git operations.
5
+ * Composes with git-fs-s3 for storage.
6
+ */
7
+ export { createParsedObjectCache, type ParsedObjectCacheOptions, type ParsedObjectStore, } from "./cache.js";
8
+ export { GitEdgeError, GitMergeConflictError, } from "./errors.js";
9
+ export { type MergeOpts, type MergeResult, type Repo, threeWayMerge, } from "./merge.js";
10
+ export { getRepoCache, invalidateRepoCache } from "./repo.js";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Object-level three-way merge for isomorphic-git.
3
+ *
4
+ * Merges two commits by walking their trees directly — no worktree, no disk.
5
+ * Returns the merged commit OID, or throws GitMergeConflictError with the
6
+ * list of conflicting paths.
7
+ *
8
+ * Edge-compatible: no node: imports, uses only isomorphic-git object APIs.
9
+ */
10
+ import { type Repo } from "git-fs-s3/ops";
11
+ export type { Repo };
12
+ /** Options for threeWayMerge. */
13
+ export interface MergeOpts {
14
+ /** Merge commit message (default: "Merge <source> into <target>"). */
15
+ message?: string;
16
+ /** Author name (default: "Git Edge"). */
17
+ authorName?: string;
18
+ /** Author email (default: "git-edge@local"). */
19
+ authorEmail?: string;
20
+ }
21
+ /** Result of a successful merge (or of a no-op: target already up to date). */
22
+ export interface MergeResult {
23
+ commitOid: string;
24
+ }
25
+ /**
26
+ * Merge `sourceRef` into `targetRef`, entirely at the object level.
27
+ *
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.
38
+ *
39
+ * New objects are stored as one pack, written before the target ref moves.
40
+ * Serialize concurrent writers to the same repository externally.
41
+ */
42
+ export declare function threeWayMerge(repo: Repo, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * isomorphic-git per-repo parse cache management.
3
+ *
4
+ * No node: imports.
5
+ */
6
+ export declare function getRepoCache(ownerKey: string, repoName: string): object;
7
+ export declare function invalidateRepoCache(ownerKey: string, repoName: string): void;
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "git-edge",
3
- "version": "0.2.3",
4
- "description": "High-level edge-compatible git operations — parsed-object cache, object-level three-way merge, and repo management utilities. Composes with git-fs-s3.",
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
  ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js",
10
- "require": "./dist/index.cjs"
8
+ "types": "./dist/types/index.d.ts",
9
+ "import": "./dist/index.js"
11
10
  }
12
11
  },
13
12
  "files": [
@@ -15,7 +14,7 @@
15
14
  ],
16
15
  "sideEffects": false,
17
16
  "scripts": {
18
- "build": "tsup",
17
+ "build": "tsup && tsc -p tsconfig.build.json",
19
18
  "test": "vitest run",
20
19
  "test:watch": "vitest",
21
20
  "coverage": "vitest run --coverage",
@@ -48,23 +47,24 @@
48
47
  "access": "public"
49
48
  },
50
49
  "engines": {
51
- "node": ">=18"
50
+ "node": ">=22"
52
51
  },
53
52
  "dependencies": {
54
- "lru-cache": "^11.5.2",
53
+ "lru-cache": "^11.5.3",
55
54
  "node-diff3": "^3.2.1"
56
55
  },
57
56
  "peerDependencies": {
58
- "git-fs-s3": ">=0.3.0",
59
- "isomorphic-git": ">=1.24.0"
57
+ "git-fs-s3": ">=0.8.0",
58
+ "isomorphic-git": ">=1.43.0"
60
59
  },
61
60
  "devDependencies": {
62
- "@biomejs/biome": "^2.5.13",
63
- "@types/node": "^22.20.2",
64
- "git-fs-s3": "^0.3.13",
65
- "isomorphic-git": "^1.42.0",
61
+ "@biomejs/biome": "^2.5.15",
62
+ "@types/node": "^26.6.5",
63
+ "diff": "^9.0.0",
64
+ "git-fs-s3": "^0.8.0",
65
+ "isomorphic-git": "^1.43.3",
66
66
  "tsup": "^8.5.1",
67
- "typescript": "^5.9.3",
68
- "vitest": "^5.0.0"
67
+ "typescript": "^7.0.2",
68
+ "vitest": "^5.0.3"
69
69
  }
70
70
  }
package/dist/index.cjs DELETED
@@ -1,372 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- GitEdgeError: () => GitEdgeError,
34
- GitMergeConflictError: () => GitMergeConflictError,
35
- createParsedObjectCache: () => createParsedObjectCache,
36
- getRepoCache: () => getRepoCache,
37
- invalidateRepoCache: () => invalidateRepoCache,
38
- threeWayMerge: () => threeWayMerge
39
- });
40
- module.exports = __toCommonJS(index_exports);
41
-
42
- // src/cache.ts
43
- var import_lru_cache = require("lru-cache");
44
- function estimateSize(value) {
45
- if (typeof value === "string") return value.length;
46
- if (value === null || typeof value !== "object") return 8;
47
- if (ArrayBuffer.isView(value)) return value.byteLength;
48
- let size = 0;
49
- if (Array.isArray(value)) {
50
- for (const item of value) size += estimateSize(item);
51
- return size;
52
- }
53
- for (const [k, v] of Object.entries(value)) {
54
- size += k.length + estimateSize(v);
55
- }
56
- return size;
57
- }
58
- function createParsedObjectCache(options) {
59
- const maxSize = options?.maxSize ?? 256 * 1024 * 1024;
60
- const ttl = options?.ttl ?? 3600 * 1e3;
61
- const cache = new import_lru_cache.LRUCache({
62
- maxSize,
63
- sizeCalculation: (v) => Math.max(1, estimateSize(v)),
64
- ttl
65
- });
66
- return {
67
- get(key) {
68
- return cache.get(key) ?? null;
69
- },
70
- set(key, value) {
71
- cache.set(key, value);
72
- },
73
- delete(key) {
74
- cache.delete(key);
75
- },
76
- invalidatePrefix(prefix) {
77
- for (const k of cache.keys()) {
78
- if (k.startsWith(prefix)) cache.delete(k);
79
- }
80
- }
81
- };
82
- }
83
-
84
- // src/errors.ts
85
- var GitEdgeError = class extends Error {
86
- constructor(message) {
87
- super(message);
88
- this.name = "GitEdgeError";
89
- Error.captureStackTrace?.(this, this.constructor);
90
- }
91
- };
92
- var GitMergeConflictError = class extends GitEdgeError {
93
- conflictingPaths;
94
- constructor(conflictingPaths) {
95
- super(
96
- `Merge conflict in ${conflictingPaths.length} file(s): ${conflictingPaths.join(", ")}`
97
- );
98
- this.name = "GitMergeConflictError";
99
- this.conflictingPaths = conflictingPaths;
100
- }
101
- };
102
-
103
- // src/merge.ts
104
- var import_isomorphic_git = __toESM(require("isomorphic-git"), 1);
105
- var import_node_diff3 = require("node-diff3");
106
- function defaultAuthor() {
107
- return {
108
- name: "Git Edge",
109
- email: "git-edge@local",
110
- timestamp: Math.floor(Date.now() / 1e3),
111
- timezoneOffset: 0
112
- };
113
- }
114
- async function flattenTreeDeep(repo, treeOid, prefix) {
115
- const map = /* @__PURE__ */ new Map();
116
- const entries = (await import_isomorphic_git.default.readTree({ ...repo, oid: treeOid })).tree;
117
- await Promise.all(
118
- entries.map(async (entry) => {
119
- const fullPath = prefix ? `${prefix}/${entry.path}` : entry.path;
120
- if (entry.type === "tree") {
121
- const sub = await flattenTreeDeep(repo, entry.oid, fullPath);
122
- for (const [k, v] of sub) map.set(k, v);
123
- } else {
124
- map.set(fullPath, entry.oid);
125
- }
126
- })
127
- );
128
- return map;
129
- }
130
- async function readBlob(repo, oid) {
131
- const { blob } = await import_isomorphic_git.default.readBlob({ ...repo, oid });
132
- return blob;
133
- }
134
- async function writeBlob(repo, content) {
135
- return import_isomorphic_git.default.writeBlob({ ...repo, blob: content });
136
- }
137
- async function writeTreeObject(repo, entries) {
138
- return import_isomorphic_git.default.writeObject({
139
- ...repo,
140
- type: "tree",
141
- object: entries.map((e) => ({
142
- path: e.path,
143
- oid: e.oid,
144
- mode: e.mode,
145
- type: e.type
146
- })),
147
- format: "parsed"
148
- });
149
- }
150
- async function writeTreeFromFlat(repo, flat) {
151
- const root = /* @__PURE__ */ new Map();
152
- const children = /* @__PURE__ */ new Map();
153
- for (const [p, v] of flat) {
154
- const slash = p.indexOf("/");
155
- if (slash === -1) {
156
- root.set(p, v);
157
- } else {
158
- const dir = p.slice(0, slash);
159
- const rest = p.slice(slash + 1);
160
- if (!children.has(dir)) children.set(dir, /* @__PURE__ */ new Map());
161
- children.get(dir)?.set(rest, v);
162
- }
163
- }
164
- const treeEntries = [];
165
- for (const [name, { oid, mode }] of root) {
166
- treeEntries.push({ path: name, oid, mode, type: "blob" });
167
- }
168
- for (const [dirName, subMap] of children) {
169
- const subOid = await writeTreeFromFlat(repo, subMap);
170
- treeEntries.push({
171
- path: dirName,
172
- oid: subOid,
173
- mode: "040000",
174
- type: "tree"
175
- });
176
- }
177
- return writeTreeObject(repo, treeEntries);
178
- }
179
- function mergeContents(base, ours, theirs) {
180
- const { conflict, result } = (0, import_node_diff3.merge)(
181
- decodeLines(ours),
182
- decodeLines(base),
183
- decodeLines(theirs),
184
- { label: { a: "ours", b: "theirs" } }
185
- );
186
- return { content: encodeLines(result), conflict };
187
- }
188
- function decodeLines(data) {
189
- const text = new TextDecoder().decode(data);
190
- if (text === "") return [];
191
- return text.split("\n");
192
- }
193
- function encodeLines(lines) {
194
- return new TextEncoder().encode(lines.join("\n"));
195
- }
196
- async function threeWayMerge(repo, sourceRef, targetRef, opts) {
197
- const [sourceOid, targetOid] = await Promise.all([
198
- import_isomorphic_git.default.resolveRef({ ...repo, ref: sourceRef }),
199
- import_isomorphic_git.default.resolveRef({ ...repo, ref: targetRef })
200
- ]);
201
- if (sourceOid === targetOid) {
202
- return { commitOid: sourceOid };
203
- }
204
- const isSourceAhead = await import_isomorphic_git.default.isDescendent({
205
- ...repo,
206
- oid: sourceOid,
207
- ancestor: targetOid
208
- });
209
- if (isSourceAhead) {
210
- await import_isomorphic_git.default.writeRef({
211
- ...repo,
212
- ref: targetRef,
213
- value: sourceOid,
214
- force: true
215
- });
216
- return { commitOid: sourceOid };
217
- }
218
- const isTargetAhead = await import_isomorphic_git.default.isDescendent({
219
- ...repo,
220
- oid: targetOid,
221
- ancestor: sourceOid
222
- });
223
- if (isTargetAhead) {
224
- await import_isomorphic_git.default.writeRef({
225
- ...repo,
226
- ref: targetRef,
227
- value: sourceOid,
228
- force: true
229
- });
230
- return { commitOid: sourceOid };
231
- }
232
- const baseOids = await import_isomorphic_git.default.findMergeBase({
233
- ...repo,
234
- oids: [sourceOid, targetOid]
235
- });
236
- if (!baseOids || baseOids.length === 0) {
237
- throw new Error("No merge base found between source and target");
238
- }
239
- const baseOid = baseOids[0];
240
- const [baseCommit, sourceCommit, targetCommit] = await Promise.all([
241
- import_isomorphic_git.default.readCommit({ ...repo, oid: baseOid }),
242
- import_isomorphic_git.default.readCommit({ ...repo, oid: sourceOid }),
243
- import_isomorphic_git.default.readCommit({ ...repo, oid: targetOid })
244
- ]);
245
- const baseTreeOid = baseCommit.commit.tree;
246
- const sourceTreeOid = sourceCommit.commit.tree;
247
- const targetTreeOid = targetCommit.commit.tree;
248
- const [baseMap, sourceMap, targetMap] = await Promise.all([
249
- flattenTreeDeep(repo, baseTreeOid, ""),
250
- flattenTreeDeep(repo, sourceTreeOid, ""),
251
- flattenTreeDeep(repo, targetTreeOid, "")
252
- ]);
253
- const allPaths = /* @__PURE__ */ new Set([
254
- ...baseMap.keys(),
255
- ...sourceMap.keys(),
256
- ...targetMap.keys()
257
- ]);
258
- const resultEntries = /* @__PURE__ */ new Map();
259
- const conflictingPaths = [];
260
- for (const filepath of allPaths) {
261
- const baseOid2 = baseMap.get(filepath);
262
- const sourceOid2 = sourceMap.get(filepath);
263
- const targetOid2 = targetMap.get(filepath);
264
- if (sourceOid2 !== void 0 && targetOid2 === void 0) {
265
- resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
266
- } else if (sourceOid2 === void 0 && targetOid2 !== void 0) {
267
- resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
268
- } else if (sourceOid2 !== void 0 && targetOid2 !== void 0) {
269
- const sourceChanged = sourceOid2 !== baseOid2;
270
- const targetChanged = targetOid2 !== baseOid2;
271
- if (!sourceChanged && !targetChanged) {
272
- resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
273
- } else if (!sourceChanged) {
274
- resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
275
- } else if (!targetChanged) {
276
- resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
277
- } else if (baseOid2 === void 0) {
278
- if (sourceOid2 === targetOid2) {
279
- resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
280
- } else {
281
- const [sourceContent, targetContent] = await Promise.all([
282
- readBlob(repo, sourceOid2),
283
- readBlob(repo, targetOid2)
284
- ]);
285
- const { content, conflict } = mergeContents(
286
- new Uint8Array(),
287
- sourceContent,
288
- targetContent
289
- );
290
- if (conflict) conflictingPaths.push(filepath);
291
- const mergedOid = await writeBlob(repo, content);
292
- resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
293
- }
294
- } else if (sourceOid2 === targetOid2) {
295
- resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
296
- } else {
297
- const [baseContent, sourceContent, targetContent] = await Promise.all([
298
- readBlob(repo, baseOid2),
299
- readBlob(repo, sourceOid2),
300
- readBlob(repo, targetOid2)
301
- ]);
302
- const { content, conflict } = mergeContents(
303
- baseContent,
304
- sourceContent,
305
- targetContent
306
- );
307
- if (conflict) conflictingPaths.push(filepath);
308
- const mergedOid = await writeBlob(repo, content);
309
- resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
310
- }
311
- } else {
312
- }
313
- }
314
- if (conflictingPaths.length > 0) {
315
- throw new GitMergeConflictError(conflictingPaths);
316
- }
317
- const resultTreeOid = await writeTreeFromFlat(repo, resultEntries);
318
- const author = opts?.authorName ? {
319
- name: opts.authorName,
320
- email: opts.authorEmail || "git-edge@local",
321
- timestamp: Math.floor(Date.now() / 1e3),
322
- timezoneOffset: 0
323
- } : defaultAuthor();
324
- const message = opts?.message || `Merge ${sourceRef} into ${targetRef}`;
325
- const mergeCommitOid = await import_isomorphic_git.default.commit({
326
- ...repo,
327
- message,
328
- tree: resultTreeOid,
329
- parent: [targetOid, sourceOid],
330
- author,
331
- committer: author
332
- });
333
- await import_isomorphic_git.default.writeRef({
334
- ...repo,
335
- ref: targetRef,
336
- value: mergeCommitOid,
337
- force: true
338
- });
339
- return { commitOid: mergeCommitOid };
340
- }
341
-
342
- // src/repo.ts
343
- var MAX_REPO_CACHES = 32;
344
- var repoCaches = /* @__PURE__ */ new Map();
345
- function getRepoCache(ownerKey, repoName) {
346
- const key = `${ownerKey}/${repoName}`;
347
- let cache = repoCaches.get(key);
348
- if (cache) {
349
- repoCaches.delete(key);
350
- } else {
351
- cache = {};
352
- if (repoCaches.size >= MAX_REPO_CACHES) {
353
- const oldest = repoCaches.keys().next().value;
354
- if (oldest !== void 0) repoCaches.delete(oldest);
355
- }
356
- }
357
- repoCaches.set(key, cache);
358
- return cache;
359
- }
360
- function invalidateRepoCache(ownerKey, repoName) {
361
- repoCaches.delete(`${ownerKey}/${repoName}`);
362
- }
363
- // Annotate the CommonJS export names for ESM import in node:
364
- 0 && (module.exports = {
365
- GitEdgeError,
366
- GitMergeConflictError,
367
- createParsedObjectCache,
368
- getRepoCache,
369
- invalidateRepoCache,
370
- threeWayMerge
371
- });
372
- //# sourceMappingURL=index.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cache.ts","../src/errors.ts","../src/merge.ts","../src/repo.ts"],"sourcesContent":["/**\n * git-edge\n *\n * High-level edge-compatible git operations.\n * Composes with git-fs-s3 for storage.\n */\n\nexport {\n\tcreateParsedObjectCache,\n\ttype ParsedObjectCacheOptions,\n\ttype ParsedObjectStore,\n} from \"./cache.js\";\nexport {\n\tGitEdgeError,\n\tGitMergeConflictError,\n} from \"./errors.js\";\nexport {\n\ttype MergeOpts,\n\ttype MergeResult,\n\ttype Repo,\n\tthreeWayMerge,\n} from \"./merge.js\";\nexport { getRepoCache, invalidateRepoCache } from \"./repo.js\";\n","/**\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":["git","diff3Merge","baseOid","sourceOid","targetOid"]}
package/dist/index.d.cts DELETED
@@ -1,76 +0,0 @@
1
- import { Repo } from 'git-fs-s3/ops';
2
- export { Repo } from 'git-fs-s3/ops';
3
-
4
- interface ParsedObjectCacheOptions {
5
- /** Maximum estimated size in bytes (default: 256 MB). */
6
- maxSize?: number;
7
- /** Time-to-live in milliseconds (default: 1 hour). */
8
- ttl?: number;
9
- }
10
- interface ParsedObjectStore {
11
- get<T extends object>(key: string): T | null;
12
- set<T extends object>(key: string, value: T): void;
13
- delete(key: string): void;
14
- /** Delete all keys matching a prefix. */
15
- invalidatePrefix(prefix: string): void;
16
- }
17
- declare function createParsedObjectCache(options?: ParsedObjectCacheOptions): ParsedObjectStore;
18
-
19
- /**
20
- * Error types for git-edge.
21
- *
22
- * Edge-compatible: no node: imports.
23
- */
24
- /** Base class for all git-edge errors. */
25
- declare class GitEdgeError extends Error {
26
- constructor(message: string);
27
- }
28
- /** Thrown when a three-way merge has unresolvable conflicts. */
29
- declare class GitMergeConflictError extends GitEdgeError {
30
- readonly conflictingPaths: string[];
31
- constructor(conflictingPaths: string[]);
32
- }
33
-
34
- /**
35
- * Object-level three-way merge for isomorphic-git.
36
- *
37
- * Merges two commits by walking their trees directly — no worktree, no disk.
38
- * Returns the merged commit OID, or throws MergeConflictError with the list of
39
- * conflicting file paths.
40
- *
41
- * Edge-compatible: no node: imports, uses only isomorphic-git object APIs.
42
- */
43
-
44
- /** Options for threeWayMerge. */
45
- interface MergeOpts {
46
- /** Merge commit message (default: "Merge <source> into <target>"). */
47
- message?: string;
48
- /** Author name (default: "Git Edge"). */
49
- authorName?: string;
50
- /** Author email (default: "git-edge@local"). */
51
- authorEmail?: string;
52
- }
53
- /** Result of a successful merge. */
54
- interface MergeResult {
55
- commitOid: string;
56
- }
57
- /**
58
- * Perform an object-level three-way merge.
59
- *
60
- * - If source is an ancestor of target (fast-forward), moves target ref to source.
61
- * - If target is an ancestor of source (reverse FF), moves target ref to source.
62
- * - Otherwise, performs a true 3-way merge at the tree level.
63
- *
64
- * Throws `GitMergeConflictError` if the merge has unresolvable conflicts.
65
- */
66
- declare function threeWayMerge(repo: Repo, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
67
-
68
- /**
69
- * isomorphic-git per-repo parse cache management.
70
- *
71
- * No node: imports.
72
- */
73
- declare function getRepoCache(ownerKey: string, repoName: string): object;
74
- declare function invalidateRepoCache(ownerKey: string, repoName: string): void;
75
-
76
- export { GitEdgeError, GitMergeConflictError, type MergeOpts, type MergeResult, type ParsedObjectCacheOptions, type ParsedObjectStore, createParsedObjectCache, getRepoCache, invalidateRepoCache, threeWayMerge };
package/dist/index.d.ts DELETED
@@ -1,76 +0,0 @@
1
- import { Repo } from 'git-fs-s3/ops';
2
- export { Repo } from 'git-fs-s3/ops';
3
-
4
- interface ParsedObjectCacheOptions {
5
- /** Maximum estimated size in bytes (default: 256 MB). */
6
- maxSize?: number;
7
- /** Time-to-live in milliseconds (default: 1 hour). */
8
- ttl?: number;
9
- }
10
- interface ParsedObjectStore {
11
- get<T extends object>(key: string): T | null;
12
- set<T extends object>(key: string, value: T): void;
13
- delete(key: string): void;
14
- /** Delete all keys matching a prefix. */
15
- invalidatePrefix(prefix: string): void;
16
- }
17
- declare function createParsedObjectCache(options?: ParsedObjectCacheOptions): ParsedObjectStore;
18
-
19
- /**
20
- * Error types for git-edge.
21
- *
22
- * Edge-compatible: no node: imports.
23
- */
24
- /** Base class for all git-edge errors. */
25
- declare class GitEdgeError extends Error {
26
- constructor(message: string);
27
- }
28
- /** Thrown when a three-way merge has unresolvable conflicts. */
29
- declare class GitMergeConflictError extends GitEdgeError {
30
- readonly conflictingPaths: string[];
31
- constructor(conflictingPaths: string[]);
32
- }
33
-
34
- /**
35
- * Object-level three-way merge for isomorphic-git.
36
- *
37
- * Merges two commits by walking their trees directly — no worktree, no disk.
38
- * Returns the merged commit OID, or throws MergeConflictError with the list of
39
- * conflicting file paths.
40
- *
41
- * Edge-compatible: no node: imports, uses only isomorphic-git object APIs.
42
- */
43
-
44
- /** Options for threeWayMerge. */
45
- interface MergeOpts {
46
- /** Merge commit message (default: "Merge <source> into <target>"). */
47
- message?: string;
48
- /** Author name (default: "Git Edge"). */
49
- authorName?: string;
50
- /** Author email (default: "git-edge@local"). */
51
- authorEmail?: string;
52
- }
53
- /** Result of a successful merge. */
54
- interface MergeResult {
55
- commitOid: string;
56
- }
57
- /**
58
- * Perform an object-level three-way merge.
59
- *
60
- * - If source is an ancestor of target (fast-forward), moves target ref to source.
61
- * - If target is an ancestor of source (reverse FF), moves target ref to source.
62
- * - Otherwise, performs a true 3-way merge at the tree level.
63
- *
64
- * Throws `GitMergeConflictError` if the merge has unresolvable conflicts.
65
- */
66
- declare function threeWayMerge(repo: Repo, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
67
-
68
- /**
69
- * isomorphic-git per-repo parse cache management.
70
- *
71
- * No node: imports.
72
- */
73
- declare function getRepoCache(ownerKey: string, repoName: string): object;
74
- declare function invalidateRepoCache(ownerKey: string, repoName: string): void;
75
-
76
- export { GitEdgeError, GitMergeConflictError, type MergeOpts, type MergeResult, type ParsedObjectCacheOptions, type ParsedObjectStore, createParsedObjectCache, getRepoCache, invalidateRepoCache, threeWayMerge };