git-edge 0.1.3 → 0.2.1

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/dist/index.cjs CHANGED
@@ -32,11 +32,8 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  GitEdgeError: () => GitEdgeError,
34
34
  GitMergeConflictError: () => GitMergeConflictError,
35
- analyzeMerge: () => analyzeMerge,
36
35
  createParsedObjectCache: () => createParsedObjectCache,
37
- estimateRepoSize: () => estimateRepoSize,
38
36
  getRepoCache: () => getRepoCache,
39
- initBareRepo: () => initBareRepo,
40
37
  invalidateRepoCache: () => invalidateRepoCache,
41
38
  threeWayMerge: () => threeWayMerge
42
39
  });
@@ -91,6 +88,7 @@ var GitMergeConflictError = class extends GitEdgeError {
91
88
 
92
89
  // src/merge.ts
93
90
  var import_isomorphic_git = __toESM(require("isomorphic-git"), 1);
91
+ var import_node_diff3 = require("node-diff3");
94
92
  function defaultAuthor() {
95
93
  return {
96
94
  name: "Git Edge",
@@ -165,45 +163,13 @@ async function writeTreeFromFlat(repo, flat) {
165
163
  return writeTreeObject(repo, treeEntries);
166
164
  }
167
165
  function mergeContents(base, ours, theirs) {
168
- const baseLines = decodeLines(base);
169
- const oursLines = decodeLines(ours);
170
- const theirsLines = decodeLines(theirs);
171
- if (arraysEqual(baseLines, oursLines)) return theirs;
172
- if (arraysEqual(baseLines, theirsLines)) return ours;
173
- if (arraysEqual(oursLines, theirsLines)) return ours;
174
- const result = [];
175
- let bi = 0, oi = 0, ti = 0;
176
- while (bi < baseLines.length || oi < oursLines.length || ti < theirsLines.length) {
177
- const b = bi < baseLines.length ? baseLines[bi] : void 0;
178
- const o = oi < oursLines.length ? oursLines[oi] : void 0;
179
- const t = ti < theirsLines.length ? theirsLines[ti] : void 0;
180
- if (b === o && b === t) {
181
- result.push(b ?? "");
182
- bi++;
183
- oi++;
184
- ti++;
185
- } else if (b === o) {
186
- result.push(t ?? "");
187
- bi++;
188
- oi++;
189
- ti++;
190
- } else if (b === t) {
191
- result.push(o ?? "");
192
- bi++;
193
- oi++;
194
- ti++;
195
- } else {
196
- result.push(`<<<<<<< ours`);
197
- result.push(o ?? "");
198
- result.push(`=======`);
199
- result.push(t ?? "");
200
- result.push(`>>>>>>> theirs`);
201
- bi++;
202
- oi++;
203
- ti++;
204
- }
205
- }
206
- return encodeLines(result);
166
+ const { conflict, result } = (0, import_node_diff3.merge)(
167
+ decodeLines(ours),
168
+ decodeLines(base),
169
+ decodeLines(theirs),
170
+ { label: { a: "ours", b: "theirs" } }
171
+ );
172
+ return { content: encodeLines(result), conflict };
207
173
  }
208
174
  function decodeLines(data) {
209
175
  const text = new TextDecoder().decode(data);
@@ -213,34 +179,6 @@ function decodeLines(data) {
213
179
  function encodeLines(lines) {
214
180
  return new TextEncoder().encode(lines.join("\n"));
215
181
  }
216
- function arraysEqual(a, b) {
217
- if (a.length !== b.length) return false;
218
- for (let i = 0; i < a.length; i++) {
219
- if (a[i] !== b[i]) return false;
220
- }
221
- return true;
222
- }
223
- async function analyzeMerge(repo, sourceRef, targetRef) {
224
- try {
225
- const sourceOid = await import_isomorphic_git.default.resolveRef({ ...repo, ref: sourceRef });
226
- const targetOid = await import_isomorphic_git.default.resolveRef({ ...repo, ref: targetRef });
227
- const isDescendant = await import_isomorphic_git.default.isDescendent({
228
- ...repo,
229
- oid: sourceOid,
230
- ancestor: targetOid
231
- });
232
- return {
233
- canMerge: true,
234
- fastForward: isDescendant,
235
- diverged: !isDescendant
236
- };
237
- } catch (err) {
238
- if (err?.code !== "NotFoundError") {
239
- throw err;
240
- }
241
- return { canMerge: false, fastForward: false, diverged: false };
242
- }
243
- }
244
182
  async function threeWayMerge(repo, sourceRef, targetRef, opts) {
245
183
  const [sourceOid, targetOid] = await Promise.all([
246
184
  import_isomorphic_git.default.resolveRef({ ...repo, ref: sourceRef }),
@@ -330,43 +268,31 @@ async function threeWayMerge(repo, sourceRef, targetRef, opts) {
330
268
  readBlob(repo, sourceOid2),
331
269
  readBlob(repo, targetOid2)
332
270
  ]);
333
- const merged = mergeContents(
271
+ const { content, conflict } = mergeContents(
334
272
  new Uint8Array(),
335
273
  sourceContent,
336
274
  targetContent
337
275
  );
338
- conflictingPaths.push(filepath);
339
- const mergedOid = await writeBlob(repo, merged);
276
+ if (conflict) conflictingPaths.push(filepath);
277
+ const mergedOid = await writeBlob(repo, content);
340
278
  resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
341
279
  }
280
+ } else if (sourceOid2 === targetOid2) {
281
+ resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
342
282
  } else {
343
283
  const [baseContent, sourceContent, targetContent] = await Promise.all([
344
284
  readBlob(repo, baseOid2),
345
285
  readBlob(repo, sourceOid2),
346
286
  readBlob(repo, targetOid2)
347
287
  ]);
348
- if (baseContent.length === sourceContent.length && baseContent.every((b, i) => b === sourceContent[i])) {
349
- resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
350
- } else if (baseContent.length === targetContent.length && baseContent.every((b, i) => b === targetContent[i])) {
351
- resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
352
- } else if (sourceContent.length === targetContent.length && sourceContent.every((b, i) => b === targetContent[i])) {
353
- resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
354
- } else {
355
- const merged = mergeContents(
356
- baseContent,
357
- sourceContent,
358
- targetContent
359
- );
360
- const mergedText = new TextDecoder().decode(merged);
361
- if (mergedText.includes("<<<<<<< ours") || mergedText.includes(">>>>>>> theirs")) {
362
- conflictingPaths.push(filepath);
363
- const mergedOid = await writeBlob(repo, merged);
364
- resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
365
- } else {
366
- const mergedOid = await writeBlob(repo, merged);
367
- resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
368
- }
369
- }
288
+ const { content, conflict } = mergeContents(
289
+ baseContent,
290
+ sourceContent,
291
+ targetContent
292
+ );
293
+ if (conflict) conflictingPaths.push(filepath);
294
+ const mergedOid = await writeBlob(repo, content);
295
+ resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
370
296
  }
371
297
  } else {
372
298
  }
@@ -400,7 +326,6 @@ async function threeWayMerge(repo, sourceRef, targetRef, opts) {
400
326
  }
401
327
 
402
328
  // src/repo.ts
403
- var import_isomorphic_git2 = __toESM(require("isomorphic-git"), 1);
404
329
  var repoCaches = /* @__PURE__ */ new Map();
405
330
  function getRepoCache(ownerKey, repoName) {
406
331
  const key = `${ownerKey}/${repoName}`;
@@ -414,48 +339,12 @@ function getRepoCache(ownerKey, repoName) {
414
339
  function invalidateRepoCache(ownerKey, repoName) {
415
340
  repoCaches.delete(`${ownerKey}/${repoName}`);
416
341
  }
417
- async function initBareRepo(repo, defaultBranch = "main") {
418
- await import_isomorphic_git2.default.init({
419
- ...repo,
420
- dir: repo.gitdir,
421
- defaultBranch,
422
- bare: true
423
- });
424
- }
425
- async function estimateRepoSize(repo, stat, list) {
426
- let total = 0;
427
- async function walk(dir) {
428
- let entries;
429
- try {
430
- entries = await list(dir);
431
- } catch {
432
- return;
433
- }
434
- const sizes = await Promise.all(
435
- entries.map(async (name) => {
436
- const fullPath = `${dir}/${name}`;
437
- try {
438
- const s = await stat(fullPath);
439
- return s.size;
440
- } catch {
441
- return 0;
442
- }
443
- })
444
- );
445
- for (const size of sizes) total += size;
446
- }
447
- await walk(`${repo.gitdir}/objects`);
448
- return total;
449
- }
450
342
  // Annotate the CommonJS export names for ESM import in node:
451
343
  0 && (module.exports = {
452
344
  GitEdgeError,
453
345
  GitMergeConflictError,
454
- analyzeMerge,
455
346
  createParsedObjectCache,
456
- estimateRepoSize,
457
347
  getRepoCache,
458
- initBareRepo,
459
348
  invalidateRepoCache,
460
349
  threeWayMerge
461
350
  });
@@ -1 +1 @@
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\tanalyzeMerge,\n\ttype MergeAnalysis,\n\ttype MergeOpts,\n\ttype MergeResult,\n\ttype Repo,\n\tthreeWayMerge,\n} from \"./merge.js\";\nexport {\n\testimateRepoSize,\n\tgetRepoCache,\n\tinitBareRepo,\n\tinvalidateRepoCache,\n} 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 (bytes estimate via JSON.stringify.length) 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` uses `JSON.stringify(v).length` as a byte estimate.\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) => JSON.stringify(v).length,\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 { FsClient } from \"isomorphic-git\";\nimport git from \"isomorphic-git\";\nimport { GitMergeConflictError } from \"./errors.js\";\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/** Analysis of whether a merge is possible. */\nexport interface MergeAnalysis {\n\t/** True if both refs resolved. */\n\tcanMerge: boolean;\n\t/** True if source is an ancestor of target (fast-forward possible). */\n\tfastForward: boolean;\n\t/** True if source and target diverged. */\n\tdiverged: boolean;\n}\n\n/** The repo fs+gitdir shape used by isomorphic-git. */\nexport interface Repo {\n\tfs: FsClient;\n\tgitdir: string;\n\tcache?: object;\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/** Read a tree object into a flat map of path → oid. */\nasync function _flattenTree(\n\trepo: Repo,\n\ttreeOid: string,\n): Promise<Map<string, string>> {\n\tconst map = new Map<string, string>();\n\tconst entries = (await git.readTree({ ...repo, oid: treeOid })).tree;\n\tfor (const entry of entries) {\n\t\tmap.set(entry.path, entry.oid);\n\t}\n\treturn map;\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 * Simple byte-level 3-way merge on two Uint8Array contents.\n *\n * This is a line-level merge using a basic diff algorithm.\n * For production quality, callers should use the `diff` library —\n * this is the fallback for edge environments where `diff` is not available.\n */\nfunction mergeContents(\n\tbase: Uint8Array,\n\tours: Uint8Array,\n\ttheirs: Uint8Array,\n): Uint8Array {\n\tconst baseLines = decodeLines(base);\n\tconst oursLines = decodeLines(ours);\n\tconst theirsLines = decodeLines(theirs);\n\n\t// If either side is identical to base, take the other\n\tif (arraysEqual(baseLines, oursLines)) return theirs;\n\tif (arraysEqual(baseLines, theirsLines)) return ours;\n\tif (arraysEqual(oursLines, theirsLines)) return ours;\n\n\t// Simple line-by-line merge: walk all three in lockstep\n\tconst result: string[] = [];\n\tlet bi = 0,\n\t\toi = 0,\n\t\tti = 0;\n\n\twhile (\n\t\tbi < baseLines.length ||\n\t\toi < oursLines.length ||\n\t\tti < theirsLines.length\n\t) {\n\t\tconst b = bi < baseLines.length ? baseLines[bi] : undefined;\n\t\tconst o = oi < oursLines.length ? oursLines[oi] : undefined;\n\t\tconst t = ti < theirsLines.length ? theirsLines[ti] : undefined;\n\n\t\tif (b === o && b === t) {\n\t\t\t// All three agree\n\t\t\tresult.push(b ?? \"\");\n\t\t\tbi++;\n\t\t\toi++;\n\t\t\tti++;\n\t\t} else if (b === o) {\n\t\t\t// Only theirs changed — including the case where theirs ran out\n\t\t\t// of lines here (t undefined, i.e. a deletion): `?? \"\"` matches\n\t\t\t// what pushing `t` directly already did once joined below\n\t\t\t// (Array.prototype.join renders an undefined element as \"\"),\n\t\t\t// just without relying on `!` to silence the type checker\n\t\t\t// about it.\n\t\t\tresult.push(t ?? \"\");\n\t\t\tbi++;\n\t\t\toi++;\n\t\t\tti++;\n\t\t} else if (b === t) {\n\t\t\t// Only ours changed — symmetric to the theirs case above.\n\t\t\tresult.push(o ?? \"\");\n\t\t\tbi++;\n\t\t\toi++;\n\t\t\tti++;\n\t\t} else {\n\t\t\t// Both changed — conflict: take ours + theirs separated by\n\t\t\t// conflict markers. o or t can individually be undefined here\n\t\t\t// too (one side ran out of lines while the other still\n\t\t\t// conflicts with base) — same `?? \"\"` reasoning as above.\n\t\t\tresult.push(`<<<<<<< ours`);\n\t\t\tresult.push(o ?? \"\");\n\t\t\tresult.push(`=======`);\n\t\t\tresult.push(t ?? \"\");\n\t\t\tresult.push(`>>>>>>> theirs`);\n\t\t\tbi++;\n\t\t\toi++;\n\t\t\tti++;\n\t\t}\n\t}\n\n\treturn encodeLines(result);\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\nfunction arraysEqual(a: string[], b: string[]): boolean {\n\tif (a.length !== b.length) return false;\n\tfor (let i = 0; i < a.length; i++) {\n\t\tif (a[i] !== b[i]) return false;\n\t}\n\treturn true;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Analyze whether a merge is possible between two refs.\n *\n * Returns `canMerge`, `fastForward`, and `diverged` — does not attempt\n * a real content merge (no conflict detection in analysis).\n *\n * `canMerge: false` means one of the refs didn't resolve (deleted branch,\n * typo) — a `NotFoundError` from `git.resolveRef`. Any other failure (a\n * network/storage error reading the ref, a corrupted repo) is rethrown\n * rather than folded into `canMerge: false`, which would otherwise report\n * \"this branch doesn't exist\" for a transient failure that has nothing to\n * do with whether the branches can merge.\n */\nexport async function analyzeMerge(\n\trepo: Repo,\n\tsourceRef: string,\n\ttargetRef: string,\n): Promise<MergeAnalysis> {\n\ttry {\n\t\tconst sourceOid = await git.resolveRef({ ...repo, ref: sourceRef });\n\t\tconst targetOid = await git.resolveRef({ ...repo, ref: targetRef });\n\n\t\tconst isDescendant = await git.isDescendent({\n\t\t\t...repo,\n\t\t\toid: sourceOid,\n\t\t\tancestor: targetOid,\n\t\t});\n\n\t\treturn {\n\t\t\tcanMerge: true,\n\t\t\tfastForward: isDescendant,\n\t\t\tdiverged: !isDescendant,\n\t\t};\n\t} catch (err) {\n\t\tif ((err as { code?: string })?.code !== \"NotFoundError\") {\n\t\t\tthrow err;\n\t\t}\n\t\treturn { canMerge: false, fastForward: false, diverged: false };\n\t}\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 merged = 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\tconflictingPaths.push(filepath);\n\t\t\t\t\tconst mergedOid = await writeBlob(repo, merged);\n\t\t\t\t\tresultEntries.set(filepath, { oid: mergedOid, mode: \"100644\" });\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Both changed — attempt content 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\n\t\t\t\tif (\n\t\t\t\t\tbaseContent.length === sourceContent.length &&\n\t\t\t\t\tbaseContent.every((b, i) => b === sourceContent[i])\n\t\t\t\t) {\n\t\t\t\t\t// Source is identical to base — take target\n\t\t\t\t\tresultEntries.set(filepath, { oid: targetOid, mode: \"100644\" });\n\t\t\t\t} else if (\n\t\t\t\t\tbaseContent.length === targetContent.length &&\n\t\t\t\t\tbaseContent.every((b, i) => b === targetContent[i])\n\t\t\t\t) {\n\t\t\t\t\t// Target is identical to base — take source\n\t\t\t\t\tresultEntries.set(filepath, { oid: sourceOid, mode: \"100644\" });\n\t\t\t\t} else if (\n\t\t\t\t\tsourceContent.length === targetContent.length &&\n\t\t\t\t\tsourceContent.every((b, i) => b === targetContent[i])\n\t\t\t\t) {\n\t\t\t\t\t// Source and target are identical — keep either\n\t\t\t\t\tresultEntries.set(filepath, { oid: sourceOid, mode: \"100644\" });\n\t\t\t\t} else {\n\t\t\t\t\t// Both truly changed — attempt line-level merge\n\t\t\t\t\tconst merged = mergeContents(\n\t\t\t\t\t\tbaseContent,\n\t\t\t\t\t\tsourceContent,\n\t\t\t\t\t\ttargetContent,\n\t\t\t\t\t);\n\n\t\t\t\t\t// Check for conflict markers\n\t\t\t\t\tconst mergedText = new TextDecoder().decode(merged);\n\t\t\t\t\tif (\n\t\t\t\t\t\tmergedText.includes(\"<<<<<<< ours\") ||\n\t\t\t\t\t\tmergedText.includes(\">>>>>>> theirs\")\n\t\t\t\t\t) {\n\t\t\t\t\t\tconflictingPaths.push(filepath);\n\t\t\t\t\t\t// Still take the merged result with conflict markers\n\t\t\t\t\t\t// so the caller can see what happened\n\t\t\t\t\t\tconst mergedOid = await writeBlob(repo, merged);\n\t\t\t\t\t\tresultEntries.set(filepath, { oid: mergedOid, mode: \"100644\" });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst mergedOid = await writeBlob(repo, merged);\n\t\t\t\t\t\tresultEntries.set(filepath, { oid: mergedOid, mode: \"100644\" });\n\t\t\t\t\t}\n\t\t\t\t}\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 * Repository management utilities.\n *\n * Edge-compatible helpers for isomorphic-git repos: bare repo init,\n * packfile-only disk usage, and isomorphic-git cache management.\n *\n * No node: imports. The `fs` parameter is caller-provided.\n */\n\nimport type { FsClient } from \"isomorphic-git\";\nimport git from \"isomorphic-git\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** The repo fs+gitdir shape used by isomorphic-git. */\nexport interface Repo {\n\tfs: FsClient;\n\tgitdir: string;\n\tcache?: object;\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 */\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\tcache = {};\n\t\trepoCaches.set(key, cache);\n\t}\n\treturn cache;\n}\n\nexport function invalidateRepoCache(ownerKey: string, repoName: string): void {\n\trepoCaches.delete(`${ownerKey}/${repoName}`);\n}\n\n// ---------------------------------------------------------------------------\n// Repo init\n// ---------------------------------------------------------------------------\n\n/**\n * Initialize a new bare git repository.\n *\n * Accepts any fs implementation — works with node:fs, S3-backed fs,\n * or any isomorphic-git-compatible filesystem.\n *\n * Returns the gitdir path.\n */\nexport async function initBareRepo(\n\trepo: Repo,\n\tdefaultBranch: string = \"main\",\n): Promise<void> {\n\tawait git.init({\n\t\t...repo,\n\t\tdir: repo.gitdir,\n\t\tdefaultBranch,\n\t\tbare: true,\n\t});\n}\n\n// ---------------------------------------------------------------------------\n// Disk usage\n// ---------------------------------------------------------------------------\n\n/**\n * Estimate the disk usage of a bare repo's git objects.\n *\n * This walks the loose object directories and reads `.pack` file sizes\n * from the `objects/` directory. For S3-backed repos, this may be\n * expensive — prefer tracking size at write time if possible.\n *\n * The caller must provide a `stat` function that works with their fs.\n * For node:fs this is `fs.stat`; for edge runtimes it may be a HEAD\n * request to the S3 object.\n */\nexport async function estimateRepoSize(\n\trepo: Repo,\n\tstat: (path: string) => Promise<{ size: number }>,\n\tlist: (path: string) => Promise<string[]>,\n): Promise<number> {\n\tlet total = 0;\n\n\tasync function walk(dir: string): Promise<void> {\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = await list(dir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst sizes = await Promise.all(\n\t\t\tentries.map(async (name) => {\n\t\t\t\tconst fullPath = `${dir}/${name}`;\n\t\t\t\ttry {\n\t\t\t\t\tconst s = await stat(fullPath);\n\t\t\t\t\treturn s.size;\n\t\t\t\t} catch {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\tfor (const size of sizes) total += size;\n\t}\n\n\tawait walk(`${repo.gitdir}/objects`);\n\treturn total;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACQA,uBAAyB;AAuBlB,SAAS,wBACf,SACoB;AACpB,QAAM,UAAU,SAAS,WAAW,MAAM,OAAO;AACjD,QAAM,MAAM,SAAS,OAAO,OAAO;AAEnC,QAAM,QAAQ,IAAI,0BAAyB;AAAA,IAC1C;AAAA,IACA,iBAAiB,CAAC,MAAM,KAAK,UAAU,CAAC,EAAE;AAAA,IAC1C;AAAA,EACD,CAAC;AAED,SAAO;AAAA,IACN,IAAsB,KAAuB;AAC5C,aAAQ,MAAM,IAAI,GAAG,KAAW;AAAA,IACjC;AAAA,IACA,IAAsB,KAAa,OAAgB;AAClD,YAAM,IAAI,KAAK,KAAK;AAAA,IACrB;AAAA,IACA,OAAO,KAAmB;AACzB,YAAM,OAAO,GAAG;AAAA,IACjB;AAAA,IACA,iBAAiB,QAAsB;AACtC,iBAAW,KAAK,MAAM,KAAK,GAAG;AAC7B,YAAI,EAAE,WAAW,MAAM,EAAG,OAAM,OAAO,CAAC;AAAA,MACzC;AAAA,IACD;AAAA,EACD;AACD;;;ACpDO,IAAM,eAAN,cAA2B,MAAM;AAAA,EACvC,YAAY,SAAiB;AAC5B,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,UAAM,oBAAoB,MAAM,KAAK,WAAW;AAAA,EACjD;AACD;AAGO,IAAM,wBAAN,cAAoC,aAAa;AAAA,EAC9C;AAAA,EAET,YAAY,kBAA4B;AACvC;AAAA,MACC,qBAAqB,iBAAiB,MAAM,aAAa,iBAAiB,KAAK,IAAI,CAAC;AAAA,IACrF;AACA,SAAK,OAAO;AACZ,SAAK,mBAAmB;AAAA,EACzB;AACD;;;ACfA,4BAAgB;AA2ChB,SAAS,gBAAgB;AACxB,SAAO;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,IACvC,gBAAgB;AAAA,EACjB;AACD;AAgBA,eAAe,gBACd,MACA,SACA,QAC+B;AAC/B,QAAM,MAAM,oBAAI,IAAoB;AACpC,QAAM,WAAW,MAAM,sBAAAA,QAAI,SAAS,EAAE,GAAG,MAAM,KAAK,QAAQ,CAAC,GAAG;AAChE,QAAM,QAAQ;AAAA,IACb,QAAQ,IAAI,OAAO,UAAU;AAC5B,YAAM,WAAW,SAAS,GAAG,MAAM,IAAI,MAAM,IAAI,KAAK,MAAM;AAC5D,UAAI,MAAM,SAAS,QAAQ;AAC1B,cAAM,MAAM,MAAM,gBAAgB,MAAM,MAAM,KAAK,QAAQ;AAC3D,mBAAW,CAAC,GAAG,CAAC,KAAK,IAAK,KAAI,IAAI,GAAG,CAAC;AAAA,MACvC,OAAO;AACN,YAAI,IAAI,UAAU,MAAM,GAAG;AAAA,MAC5B;AAAA,IACD,CAAC;AAAA,EACF;AACA,SAAO;AACR;AAGA,eAAe,SAAS,MAAY,KAAkC;AACrE,QAAM,EAAE,KAAK,IAAI,MAAM,sBAAAA,QAAI,SAAS,EAAE,GAAG,MAAM,IAAI,CAAC;AACpD,SAAO;AACR;AAGA,eAAe,UAAU,MAAY,SAAsC;AAC1E,SAAO,sBAAAA,QAAI,UAAU,EAAE,GAAG,MAAM,MAAM,QAAQ,CAAC;AAChD;AAGA,eAAe,gBACd,MACA,SAMkB;AAClB,SAAO,sBAAAA,QAAI,YAAY;AAAA,IACtB,GAAG;AAAA,IACH,MAAM;AAAA,IACN,QAAQ,QAAQ,IAAI,CAAC,OAAO;AAAA,MAC3B,MAAM,EAAE;AAAA,MACR,KAAK,EAAE;AAAA,MACP,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,IACT,EAAE;AAAA,IACF,QAAQ;AAAA,EACT,CAAC;AACF;AAMA,eAAe,kBACd,MACA,MACkB;AAElB,QAAM,OAAO,oBAAI,IAA2C;AAC5D,QAAM,WAAW,oBAAI,IAGnB;AAEF,aAAW,CAAC,GAAG,CAAC,KAAK,MAAM;AAC1B,UAAM,QAAQ,EAAE,QAAQ,GAAG;AAC3B,QAAI,UAAU,IAAI;AACjB,WAAK,IAAI,GAAG,CAAC;AAAA,IACd,OAAO;AACN,YAAM,MAAM,EAAE,MAAM,GAAG,KAAK;AAC5B,YAAM,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC9B,UAAI,CAAC,SAAS,IAAI,GAAG,EAAG,UAAS,IAAI,KAAK,oBAAI,IAAI,CAAC;AACnD,eAAS,IAAI,GAAG,GAAG,IAAI,MAAM,CAAC;AAAA,IAC/B;AAAA,EACD;AAEA,QAAM,cAKD,CAAC;AAEN,aAAW,CAAC,MAAM,EAAE,KAAK,KAAK,CAAC,KAAK,MAAM;AACzC,gBAAY,KAAK,EAAE,MAAM,MAAM,KAAK,MAAM,MAAM,OAAO,CAAC;AAAA,EACzD;AAEA,aAAW,CAAC,SAAS,MAAM,KAAK,UAAU;AACzC,UAAM,SAAS,MAAM,kBAAkB,MAAM,MAAM;AACnD,gBAAY,KAAK;AAAA,MAChB,MAAM;AAAA,MACN,KAAK;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,IACP,CAAC;AAAA,EACF;AAEA,SAAO,gBAAgB,MAAM,WAAW;AACzC;AASA,SAAS,cACR,MACA,MACA,QACa;AACb,QAAM,YAAY,YAAY,IAAI;AAClC,QAAM,YAAY,YAAY,IAAI;AAClC,QAAM,cAAc,YAAY,MAAM;AAGtC,MAAI,YAAY,WAAW,SAAS,EAAG,QAAO;AAC9C,MAAI,YAAY,WAAW,WAAW,EAAG,QAAO;AAChD,MAAI,YAAY,WAAW,WAAW,EAAG,QAAO;AAGhD,QAAM,SAAmB,CAAC;AAC1B,MAAI,KAAK,GACR,KAAK,GACL,KAAK;AAEN,SACC,KAAK,UAAU,UACf,KAAK,UAAU,UACf,KAAK,YAAY,QAChB;AACD,UAAM,IAAI,KAAK,UAAU,SAAS,UAAU,EAAE,IAAI;AAClD,UAAM,IAAI,KAAK,UAAU,SAAS,UAAU,EAAE,IAAI;AAClD,UAAM,IAAI,KAAK,YAAY,SAAS,YAAY,EAAE,IAAI;AAEtD,QAAI,MAAM,KAAK,MAAM,GAAG;AAEvB,aAAO,KAAK,KAAK,EAAE;AACnB;AACA;AACA;AAAA,IACD,WAAW,MAAM,GAAG;AAOnB,aAAO,KAAK,KAAK,EAAE;AACnB;AACA;AACA;AAAA,IACD,WAAW,MAAM,GAAG;AAEnB,aAAO,KAAK,KAAK,EAAE;AACnB;AACA;AACA;AAAA,IACD,OAAO;AAKN,aAAO,KAAK,cAAc;AAC1B,aAAO,KAAK,KAAK,EAAE;AACnB,aAAO,KAAK,SAAS;AACrB,aAAO,KAAK,KAAK,EAAE;AACnB,aAAO,KAAK,gBAAgB;AAC5B;AACA;AACA;AAAA,IACD;AAAA,EACD;AAEA,SAAO,YAAY,MAAM;AAC1B;AAEA,SAAS,YAAY,MAA4B;AAChD,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,IAAI;AAC1C,MAAI,SAAS,GAAI,QAAO,CAAC;AACzB,SAAO,KAAK,MAAM,IAAI;AACvB;AAEA,SAAS,YAAY,OAA6B;AACjD,SAAO,IAAI,YAAY,EAAE,OAAO,MAAM,KAAK,IAAI,CAAC;AACjD;AAEA,SAAS,YAAY,GAAa,GAAsB;AACvD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAClC,QAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAAA,EAC3B;AACA,SAAO;AACR;AAmBA,eAAsB,aACrB,MACA,WACA,WACyB;AACzB,MAAI;AACH,UAAM,YAAY,MAAM,sBAAAA,QAAI,WAAW,EAAE,GAAG,MAAM,KAAK,UAAU,CAAC;AAClE,UAAM,YAAY,MAAM,sBAAAA,QAAI,WAAW,EAAE,GAAG,MAAM,KAAK,UAAU,CAAC;AAElE,UAAM,eAAe,MAAM,sBAAAA,QAAI,aAAa;AAAA,MAC3C,GAAG;AAAA,MACH,KAAK;AAAA,MACL,UAAU;AAAA,IACX,CAAC;AAED,WAAO;AAAA,MACN,UAAU;AAAA,MACV,aAAa;AAAA,MACb,UAAU,CAAC;AAAA,IACZ;AAAA,EACD,SAAS,KAAK;AACb,QAAK,KAA2B,SAAS,iBAAiB;AACzD,YAAM;AAAA,IACP;AACA,WAAO,EAAE,UAAU,OAAO,aAAa,OAAO,UAAU,MAAM;AAAA,EAC/D;AACD;AAWA,eAAsB,cACrB,MACA,WACA,WACA,MACuB;AACvB,QAAM,CAAC,WAAW,SAAS,IAAI,MAAM,QAAQ,IAAI;AAAA,IAChD,sBAAAA,QAAI,WAAW,EAAE,GAAG,MAAM,KAAK,UAAU,CAAC;AAAA,IAC1C,sBAAAA,QAAI,WAAW,EAAE,GAAG,MAAM,KAAK,UAAU,CAAC;AAAA,EAC3C,CAAC;AAED,MAAI,cAAc,WAAW;AAC5B,WAAO,EAAE,WAAW,UAAU;AAAA,EAC/B;AAGA,QAAM,gBAAgB,MAAM,sBAAAA,QAAI,aAAa;AAAA,IAC5C,GAAG;AAAA,IACH,KAAK;AAAA,IACL,UAAU;AAAA,EACX,CAAC;AACD,MAAI,eAAe;AAClB,UAAM,sBAAAA,QAAI,SAAS;AAAA,MAClB,GAAG;AAAA,MACH,KAAK;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,IACR,CAAC;AACD,WAAO,EAAE,WAAW,UAAU;AAAA,EAC/B;AAGA,QAAM,gBAAgB,MAAM,sBAAAA,QAAI,aAAa;AAAA,IAC5C,GAAG;AAAA,IACH,KAAK;AAAA,IACL,UAAU;AAAA,EACX,CAAC;AACD,MAAI,eAAe;AAClB,UAAM,sBAAAA,QAAI,SAAS;AAAA,MAClB,GAAG;AAAA,MACH,KAAK;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,IACR,CAAC;AACD,WAAO,EAAE,WAAW,UAAU;AAAA,EAC/B;AAGA,QAAM,WAAW,MAAM,sBAAAA,QAAI,cAAc;AAAA,IACxC,GAAG;AAAA,IACH,MAAM,CAAC,WAAW,SAAS;AAAA,EAC5B,CAAC;AAED,MAAI,CAAC,YAAY,SAAS,WAAW,GAAG;AACvC,UAAM,IAAI,MAAM,+CAA+C;AAAA,EAChE;AAEA,QAAM,UAAU,SAAS,CAAC;AAG1B,QAAM,CAAC,YAAY,cAAc,YAAY,IAAI,MAAM,QAAQ,IAAI;AAAA,IAClE,sBAAAA,QAAI,WAAW,EAAE,GAAG,MAAM,KAAK,QAAQ,CAAC;AAAA,IACxC,sBAAAA,QAAI,WAAW,EAAE,GAAG,MAAM,KAAK,UAAU,CAAC;AAAA,IAC1C,sBAAAA,QAAI,WAAW,EAAE,GAAG,MAAM,KAAK,UAAU,CAAC;AAAA,EAC3C,CAAC;AAED,QAAM,cAAc,WAAW,OAAO;AACtC,QAAM,gBAAgB,aAAa,OAAO;AAC1C,QAAM,gBAAgB,aAAa,OAAO;AAG1C,QAAM,CAAC,SAAS,WAAW,SAAS,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzD,gBAAgB,MAAM,aAAa,EAAE;AAAA,IACrC,gBAAgB,MAAM,eAAe,EAAE;AAAA,IACvC,gBAAgB,MAAM,eAAe,EAAE;AAAA,EACxC,CAAC;AAGD,QAAM,WAAW,oBAAI,IAAI;AAAA,IACxB,GAAG,QAAQ,KAAK;AAAA,IAChB,GAAG,UAAU,KAAK;AAAA,IAClB,GAAG,UAAU,KAAK;AAAA,EACnB,CAAC;AAED,QAAM,gBAAgB,oBAAI,IAA2C;AACrE,QAAM,mBAA6B,CAAC;AAEpC,aAAW,YAAY,UAAU;AAChC,UAAMC,WAAU,QAAQ,IAAI,QAAQ;AACpC,UAAMC,aAAY,UAAU,IAAI,QAAQ;AACxC,UAAMC,aAAY,UAAU,IAAI,QAAQ;AAExC,QAAID,eAAc,UAAaC,eAAc,QAAW;AAEvD,oBAAc,IAAI,UAAU,EAAE,KAAKD,YAAW,MAAM,SAAS,CAAC;AAAA,IAC/D,WAAWA,eAAc,UAAaC,eAAc,QAAW;AAE9D,oBAAc,IAAI,UAAU,EAAE,KAAKA,YAAW,MAAM,SAAS,CAAC;AAAA,IAC/D,WAAWD,eAAc,UAAaC,eAAc,QAAW;AAM9D,YAAM,gBAAgBD,eAAcD;AACpC,YAAM,gBAAgBE,eAAcF;AAEpC,UAAI,CAAC,iBAAiB,CAAC,eAAe;AAErC,sBAAc,IAAI,UAAU,EAAE,KAAKE,YAAW,MAAM,SAAS,CAAC;AAAA,MAC/D,WAAW,CAAC,eAAe;AAE1B,sBAAc,IAAI,UAAU,EAAE,KAAKA,YAAW,MAAM,SAAS,CAAC;AAAA,MAC/D,WAAW,CAAC,eAAe;AAE1B,sBAAc,IAAI,UAAU,EAAE,KAAKD,YAAW,MAAM,SAAS,CAAC;AAAA,MAC/D,WAAWD,aAAY,QAAW;AAQjC,YAAIC,eAAcC,YAAW;AAC5B,wBAAc,IAAI,UAAU,EAAE,KAAKD,YAAW,MAAM,SAAS,CAAC;AAAA,QAC/D,OAAO;AACN,gBAAM,CAAC,eAAe,aAAa,IAAI,MAAM,QAAQ,IAAI;AAAA,YACxD,SAAS,MAAMA,UAAS;AAAA,YACxB,SAAS,MAAMC,UAAS;AAAA,UACzB,CAAC;AACD,gBAAM,SAAS;AAAA,YACd,IAAI,WAAW;AAAA,YACf;AAAA,YACA;AAAA,UACD;AACA,2BAAiB,KAAK,QAAQ;AAC9B,gBAAM,YAAY,MAAM,UAAU,MAAM,MAAM;AAC9C,wBAAc,IAAI,UAAU,EAAE,KAAK,WAAW,MAAM,SAAS,CAAC;AAAA,QAC/D;AAAA,MACD,OAAO;AAEN,cAAM,CAAC,aAAa,eAAe,aAAa,IAAI,MAAM,QAAQ,IAAI;AAAA,UACrE,SAAS,MAAMF,QAAO;AAAA,UACtB,SAAS,MAAMC,UAAS;AAAA,UACxB,SAAS,MAAMC,UAAS;AAAA,QACzB,CAAC;AAED,YACC,YAAY,WAAW,cAAc,UACrC,YAAY,MAAM,CAAC,GAAG,MAAM,MAAM,cAAc,CAAC,CAAC,GACjD;AAED,wBAAc,IAAI,UAAU,EAAE,KAAKA,YAAW,MAAM,SAAS,CAAC;AAAA,QAC/D,WACC,YAAY,WAAW,cAAc,UACrC,YAAY,MAAM,CAAC,GAAG,MAAM,MAAM,cAAc,CAAC,CAAC,GACjD;AAED,wBAAc,IAAI,UAAU,EAAE,KAAKD,YAAW,MAAM,SAAS,CAAC;AAAA,QAC/D,WACC,cAAc,WAAW,cAAc,UACvC,cAAc,MAAM,CAAC,GAAG,MAAM,MAAM,cAAc,CAAC,CAAC,GACnD;AAED,wBAAc,IAAI,UAAU,EAAE,KAAKA,YAAW,MAAM,SAAS,CAAC;AAAA,QAC/D,OAAO;AAEN,gBAAM,SAAS;AAAA,YACd;AAAA,YACA;AAAA,YACA;AAAA,UACD;AAGA,gBAAM,aAAa,IAAI,YAAY,EAAE,OAAO,MAAM;AAClD,cACC,WAAW,SAAS,cAAc,KAClC,WAAW,SAAS,gBAAgB,GACnC;AACD,6BAAiB,KAAK,QAAQ;AAG9B,kBAAM,YAAY,MAAM,UAAU,MAAM,MAAM;AAC9C,0BAAc,IAAI,UAAU,EAAE,KAAK,WAAW,MAAM,SAAS,CAAC;AAAA,UAC/D,OAAO;AACN,kBAAM,YAAY,MAAM,UAAU,MAAM,MAAM;AAC9C,0BAAc,IAAI,UAAU,EAAE,KAAK,WAAW,MAAM,SAAS,CAAC;AAAA,UAC/D;AAAA,QACD;AAAA,MACD;AAAA,IACD,OAAO;AAAA,IAEP;AAAA,EACD;AAEA,MAAI,iBAAiB,SAAS,GAAG;AAChC,UAAM,IAAI,sBAAsB,gBAAgB;AAAA,EACjD;AAGA,QAAM,gBAAgB,MAAM,kBAAkB,MAAM,aAAa;AAGjE,QAAM,SAAS,MAAM,aAClB;AAAA,IACA,MAAM,KAAK;AAAA,IACX,OAAO,KAAK,eAAe;AAAA,IAC3B,WAAW,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,IACvC,gBAAgB;AAAA,EACjB,IACC,cAAc;AAEjB,QAAM,UAAU,MAAM,WAAW,SAAS,SAAS,SAAS,SAAS;AAErE,QAAM,iBAAiB,MAAM,sBAAAF,QAAI,OAAO;AAAA,IACvC,GAAG;AAAA,IACH;AAAA,IACA,MAAM;AAAA,IACN,QAAQ,CAAC,WAAW,SAAS;AAAA,IAC7B;AAAA,IACA,WAAW;AAAA,EACZ,CAAC;AAGD,QAAM,sBAAAA,QAAI,SAAS;AAAA,IAClB,GAAG;AAAA,IACH,KAAK;AAAA,IACL,OAAO;AAAA,IACP,OAAO;AAAA,EACR,CAAC;AAED,SAAO,EAAE,WAAW,eAAe;AACpC;;;AC5iBA,IAAAI,yBAAgB;AA2BhB,IAAM,aAAa,oBAAI,IAAoB;AAEpC,SAAS,aAAa,UAAkB,UAA0B;AACxE,QAAM,MAAM,GAAG,QAAQ,IAAI,QAAQ;AACnC,MAAI,QAAQ,WAAW,IAAI,GAAG;AAC9B,MAAI,CAAC,OAAO;AACX,YAAQ,CAAC;AACT,eAAW,IAAI,KAAK,KAAK;AAAA,EAC1B;AACA,SAAO;AACR;AAEO,SAAS,oBAAoB,UAAkB,UAAwB;AAC7E,aAAW,OAAO,GAAG,QAAQ,IAAI,QAAQ,EAAE;AAC5C;AAcA,eAAsB,aACrB,MACA,gBAAwB,QACR;AAChB,QAAM,uBAAAC,QAAI,KAAK;AAAA,IACd,GAAG;AAAA,IACH,KAAK,KAAK;AAAA,IACV;AAAA,IACA,MAAM;AAAA,EACP,CAAC;AACF;AAiBA,eAAsB,iBACrB,MACA,MACA,MACkB;AAClB,MAAI,QAAQ;AAEZ,iBAAe,KAAK,KAA4B;AAC/C,QAAI;AACJ,QAAI;AACH,gBAAU,MAAM,KAAK,GAAG;AAAA,IACzB,QAAQ;AACP;AAAA,IACD;AAEA,UAAM,QAAQ,MAAM,QAAQ;AAAA,MAC3B,QAAQ,IAAI,OAAO,SAAS;AAC3B,cAAM,WAAW,GAAG,GAAG,IAAI,IAAI;AAC/B,YAAI;AACH,gBAAM,IAAI,MAAM,KAAK,QAAQ;AAC7B,iBAAO,EAAE;AAAA,QACV,QAAQ;AACP,iBAAO;AAAA,QACR;AAAA,MACD,CAAC;AAAA,IACF;AAEA,eAAW,QAAQ,MAAO,UAAS;AAAA,EACpC;AAEA,QAAM,KAAK,GAAG,KAAK,MAAM,UAAU;AACnC,SAAO;AACR;","names":["git","baseOid","sourceOid","targetOid","import_isomorphic_git","git"]}
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 (bytes estimate via JSON.stringify.length) 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` uses `JSON.stringify(v).length` as a byte estimate.\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) => JSON.stringify(v).length,\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 */\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\tcache = {};\n\t\trepoCaches.set(key, cache);\n\t}\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 CHANGED
@@ -1,4 +1,5 @@
1
- import { FsClient } from 'isomorphic-git';
1
+ import { Repo } from 'git-fs-s3/ops';
2
+ export { Repo } from 'git-fs-s3/ops';
2
3
 
3
4
  interface ParsedObjectCacheOptions {
4
5
  /** Maximum estimated size in bytes (default: 256 MB). */
@@ -59,35 +60,6 @@ interface MergeOpts {
59
60
  interface MergeResult {
60
61
  commitOid: string;
61
62
  }
62
- /** Analysis of whether a merge is possible. */
63
- interface MergeAnalysis {
64
- /** True if both refs resolved. */
65
- canMerge: boolean;
66
- /** True if source is an ancestor of target (fast-forward possible). */
67
- fastForward: boolean;
68
- /** True if source and target diverged. */
69
- diverged: boolean;
70
- }
71
- /** The repo fs+gitdir shape used by isomorphic-git. */
72
- interface Repo$1 {
73
- fs: FsClient;
74
- gitdir: string;
75
- cache?: object;
76
- }
77
- /**
78
- * Analyze whether a merge is possible between two refs.
79
- *
80
- * Returns `canMerge`, `fastForward`, and `diverged` — does not attempt
81
- * a real content merge (no conflict detection in analysis).
82
- *
83
- * `canMerge: false` means one of the refs didn't resolve (deleted branch,
84
- * typo) — a `NotFoundError` from `git.resolveRef`. Any other failure (a
85
- * network/storage error reading the ref, a corrupted repo) is rethrown
86
- * rather than folded into `canMerge: false`, which would otherwise report
87
- * "this branch doesn't exist" for a transient failure that has nothing to
88
- * do with whether the branches can merge.
89
- */
90
- declare function analyzeMerge(repo: Repo$1, sourceRef: string, targetRef: string): Promise<MergeAnalysis>;
91
63
  /**
92
64
  * Perform an object-level three-way merge.
93
65
  *
@@ -97,47 +69,14 @@ declare function analyzeMerge(repo: Repo$1, sourceRef: string, targetRef: string
97
69
  *
98
70
  * Throws `GitMergeConflictError` if the merge has unresolvable conflicts.
99
71
  */
100
- declare function threeWayMerge(repo: Repo$1, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
72
+ declare function threeWayMerge(repo: Repo, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
101
73
 
102
74
  /**
103
- * Repository management utilities.
104
- *
105
- * Edge-compatible helpers for isomorphic-git repos: bare repo init,
106
- * packfile-only disk usage, and isomorphic-git cache management.
75
+ * isomorphic-git per-repo parse cache management.
107
76
  *
108
- * No node: imports. The `fs` parameter is caller-provided.
77
+ * No node: imports.
109
78
  */
110
-
111
- /** The repo fs+gitdir shape used by isomorphic-git. */
112
- interface Repo {
113
- fs: FsClient;
114
- gitdir: string;
115
- cache?: object;
116
- }
117
79
  declare function getRepoCache(ownerKey: string, repoName: string): object;
118
80
  declare function invalidateRepoCache(ownerKey: string, repoName: string): void;
119
- /**
120
- * Initialize a new bare git repository.
121
- *
122
- * Accepts any fs implementation — works with node:fs, S3-backed fs,
123
- * or any isomorphic-git-compatible filesystem.
124
- *
125
- * Returns the gitdir path.
126
- */
127
- declare function initBareRepo(repo: Repo, defaultBranch?: string): Promise<void>;
128
- /**
129
- * Estimate the disk usage of a bare repo's git objects.
130
- *
131
- * This walks the loose object directories and reads `.pack` file sizes
132
- * from the `objects/` directory. For S3-backed repos, this may be
133
- * expensive — prefer tracking size at write time if possible.
134
- *
135
- * The caller must provide a `stat` function that works with their fs.
136
- * For node:fs this is `fs.stat`; for edge runtimes it may be a HEAD
137
- * request to the S3 object.
138
- */
139
- declare function estimateRepoSize(repo: Repo, stat: (path: string) => Promise<{
140
- size: number;
141
- }>, list: (path: string) => Promise<string[]>): Promise<number>;
142
81
 
143
- export { GitEdgeError, GitMergeConflictError, type MergeAnalysis, type MergeOpts, type MergeResult, type ParsedObjectCacheOptions, type ParsedObjectStore, type Repo$1 as Repo, analyzeMerge, createParsedObjectCache, estimateRepoSize, getRepoCache, initBareRepo, invalidateRepoCache, threeWayMerge };
82
+ export { GitEdgeError, GitMergeConflictError, type MergeOpts, type MergeResult, type ParsedObjectCacheOptions, type ParsedObjectStore, createParsedObjectCache, getRepoCache, invalidateRepoCache, threeWayMerge };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { FsClient } from 'isomorphic-git';
1
+ import { Repo } from 'git-fs-s3/ops';
2
+ export { Repo } from 'git-fs-s3/ops';
2
3
 
3
4
  interface ParsedObjectCacheOptions {
4
5
  /** Maximum estimated size in bytes (default: 256 MB). */
@@ -59,35 +60,6 @@ interface MergeOpts {
59
60
  interface MergeResult {
60
61
  commitOid: string;
61
62
  }
62
- /** Analysis of whether a merge is possible. */
63
- interface MergeAnalysis {
64
- /** True if both refs resolved. */
65
- canMerge: boolean;
66
- /** True if source is an ancestor of target (fast-forward possible). */
67
- fastForward: boolean;
68
- /** True if source and target diverged. */
69
- diverged: boolean;
70
- }
71
- /** The repo fs+gitdir shape used by isomorphic-git. */
72
- interface Repo$1 {
73
- fs: FsClient;
74
- gitdir: string;
75
- cache?: object;
76
- }
77
- /**
78
- * Analyze whether a merge is possible between two refs.
79
- *
80
- * Returns `canMerge`, `fastForward`, and `diverged` — does not attempt
81
- * a real content merge (no conflict detection in analysis).
82
- *
83
- * `canMerge: false` means one of the refs didn't resolve (deleted branch,
84
- * typo) — a `NotFoundError` from `git.resolveRef`. Any other failure (a
85
- * network/storage error reading the ref, a corrupted repo) is rethrown
86
- * rather than folded into `canMerge: false`, which would otherwise report
87
- * "this branch doesn't exist" for a transient failure that has nothing to
88
- * do with whether the branches can merge.
89
- */
90
- declare function analyzeMerge(repo: Repo$1, sourceRef: string, targetRef: string): Promise<MergeAnalysis>;
91
63
  /**
92
64
  * Perform an object-level three-way merge.
93
65
  *
@@ -97,47 +69,14 @@ declare function analyzeMerge(repo: Repo$1, sourceRef: string, targetRef: string
97
69
  *
98
70
  * Throws `GitMergeConflictError` if the merge has unresolvable conflicts.
99
71
  */
100
- declare function threeWayMerge(repo: Repo$1, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
72
+ declare function threeWayMerge(repo: Repo, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
101
73
 
102
74
  /**
103
- * Repository management utilities.
104
- *
105
- * Edge-compatible helpers for isomorphic-git repos: bare repo init,
106
- * packfile-only disk usage, and isomorphic-git cache management.
75
+ * isomorphic-git per-repo parse cache management.
107
76
  *
108
- * No node: imports. The `fs` parameter is caller-provided.
77
+ * No node: imports.
109
78
  */
110
-
111
- /** The repo fs+gitdir shape used by isomorphic-git. */
112
- interface Repo {
113
- fs: FsClient;
114
- gitdir: string;
115
- cache?: object;
116
- }
117
79
  declare function getRepoCache(ownerKey: string, repoName: string): object;
118
80
  declare function invalidateRepoCache(ownerKey: string, repoName: string): void;
119
- /**
120
- * Initialize a new bare git repository.
121
- *
122
- * Accepts any fs implementation — works with node:fs, S3-backed fs,
123
- * or any isomorphic-git-compatible filesystem.
124
- *
125
- * Returns the gitdir path.
126
- */
127
- declare function initBareRepo(repo: Repo, defaultBranch?: string): Promise<void>;
128
- /**
129
- * Estimate the disk usage of a bare repo's git objects.
130
- *
131
- * This walks the loose object directories and reads `.pack` file sizes
132
- * from the `objects/` directory. For S3-backed repos, this may be
133
- * expensive — prefer tracking size at write time if possible.
134
- *
135
- * The caller must provide a `stat` function that works with their fs.
136
- * For node:fs this is `fs.stat`; for edge runtimes it may be a HEAD
137
- * request to the S3 object.
138
- */
139
- declare function estimateRepoSize(repo: Repo, stat: (path: string) => Promise<{
140
- size: number;
141
- }>, list: (path: string) => Promise<string[]>): Promise<number>;
142
81
 
143
- export { GitEdgeError, GitMergeConflictError, type MergeAnalysis, type MergeOpts, type MergeResult, type ParsedObjectCacheOptions, type ParsedObjectStore, type Repo$1 as Repo, analyzeMerge, createParsedObjectCache, estimateRepoSize, getRepoCache, initBareRepo, invalidateRepoCache, threeWayMerge };
82
+ export { GitEdgeError, GitMergeConflictError, type MergeOpts, type MergeResult, type ParsedObjectCacheOptions, type ParsedObjectStore, createParsedObjectCache, getRepoCache, invalidateRepoCache, threeWayMerge };
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ var GitMergeConflictError = class extends GitEdgeError {
47
47
 
48
48
  // src/merge.ts
49
49
  import git from "isomorphic-git";
50
+ import { merge as diff3Merge } from "node-diff3";
50
51
  function defaultAuthor() {
51
52
  return {
52
53
  name: "Git Edge",
@@ -121,45 +122,13 @@ async function writeTreeFromFlat(repo, flat) {
121
122
  return writeTreeObject(repo, treeEntries);
122
123
  }
123
124
  function mergeContents(base, ours, theirs) {
124
- const baseLines = decodeLines(base);
125
- const oursLines = decodeLines(ours);
126
- const theirsLines = decodeLines(theirs);
127
- if (arraysEqual(baseLines, oursLines)) return theirs;
128
- if (arraysEqual(baseLines, theirsLines)) return ours;
129
- if (arraysEqual(oursLines, theirsLines)) return ours;
130
- const result = [];
131
- let bi = 0, oi = 0, ti = 0;
132
- while (bi < baseLines.length || oi < oursLines.length || ti < theirsLines.length) {
133
- const b = bi < baseLines.length ? baseLines[bi] : void 0;
134
- const o = oi < oursLines.length ? oursLines[oi] : void 0;
135
- const t = ti < theirsLines.length ? theirsLines[ti] : void 0;
136
- if (b === o && b === t) {
137
- result.push(b ?? "");
138
- bi++;
139
- oi++;
140
- ti++;
141
- } else if (b === o) {
142
- result.push(t ?? "");
143
- bi++;
144
- oi++;
145
- ti++;
146
- } else if (b === t) {
147
- result.push(o ?? "");
148
- bi++;
149
- oi++;
150
- ti++;
151
- } else {
152
- result.push(`<<<<<<< ours`);
153
- result.push(o ?? "");
154
- result.push(`=======`);
155
- result.push(t ?? "");
156
- result.push(`>>>>>>> theirs`);
157
- bi++;
158
- oi++;
159
- ti++;
160
- }
161
- }
162
- return encodeLines(result);
125
+ const { conflict, result } = diff3Merge(
126
+ decodeLines(ours),
127
+ decodeLines(base),
128
+ decodeLines(theirs),
129
+ { label: { a: "ours", b: "theirs" } }
130
+ );
131
+ return { content: encodeLines(result), conflict };
163
132
  }
164
133
  function decodeLines(data) {
165
134
  const text = new TextDecoder().decode(data);
@@ -169,34 +138,6 @@ function decodeLines(data) {
169
138
  function encodeLines(lines) {
170
139
  return new TextEncoder().encode(lines.join("\n"));
171
140
  }
172
- function arraysEqual(a, b) {
173
- if (a.length !== b.length) return false;
174
- for (let i = 0; i < a.length; i++) {
175
- if (a[i] !== b[i]) return false;
176
- }
177
- return true;
178
- }
179
- async function analyzeMerge(repo, sourceRef, targetRef) {
180
- try {
181
- const sourceOid = await git.resolveRef({ ...repo, ref: sourceRef });
182
- const targetOid = await git.resolveRef({ ...repo, ref: targetRef });
183
- const isDescendant = await git.isDescendent({
184
- ...repo,
185
- oid: sourceOid,
186
- ancestor: targetOid
187
- });
188
- return {
189
- canMerge: true,
190
- fastForward: isDescendant,
191
- diverged: !isDescendant
192
- };
193
- } catch (err) {
194
- if (err?.code !== "NotFoundError") {
195
- throw err;
196
- }
197
- return { canMerge: false, fastForward: false, diverged: false };
198
- }
199
- }
200
141
  async function threeWayMerge(repo, sourceRef, targetRef, opts) {
201
142
  const [sourceOid, targetOid] = await Promise.all([
202
143
  git.resolveRef({ ...repo, ref: sourceRef }),
@@ -286,43 +227,31 @@ async function threeWayMerge(repo, sourceRef, targetRef, opts) {
286
227
  readBlob(repo, sourceOid2),
287
228
  readBlob(repo, targetOid2)
288
229
  ]);
289
- const merged = mergeContents(
230
+ const { content, conflict } = mergeContents(
290
231
  new Uint8Array(),
291
232
  sourceContent,
292
233
  targetContent
293
234
  );
294
- conflictingPaths.push(filepath);
295
- const mergedOid = await writeBlob(repo, merged);
235
+ if (conflict) conflictingPaths.push(filepath);
236
+ const mergedOid = await writeBlob(repo, content);
296
237
  resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
297
238
  }
239
+ } else if (sourceOid2 === targetOid2) {
240
+ resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
298
241
  } else {
299
242
  const [baseContent, sourceContent, targetContent] = await Promise.all([
300
243
  readBlob(repo, baseOid2),
301
244
  readBlob(repo, sourceOid2),
302
245
  readBlob(repo, targetOid2)
303
246
  ]);
304
- if (baseContent.length === sourceContent.length && baseContent.every((b, i) => b === sourceContent[i])) {
305
- resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
306
- } else if (baseContent.length === targetContent.length && baseContent.every((b, i) => b === targetContent[i])) {
307
- resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
308
- } else if (sourceContent.length === targetContent.length && sourceContent.every((b, i) => b === targetContent[i])) {
309
- resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
310
- } else {
311
- const merged = mergeContents(
312
- baseContent,
313
- sourceContent,
314
- targetContent
315
- );
316
- const mergedText = new TextDecoder().decode(merged);
317
- if (mergedText.includes("<<<<<<< ours") || mergedText.includes(">>>>>>> theirs")) {
318
- conflictingPaths.push(filepath);
319
- const mergedOid = await writeBlob(repo, merged);
320
- resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
321
- } else {
322
- const mergedOid = await writeBlob(repo, merged);
323
- resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
324
- }
325
- }
247
+ const { content, conflict } = mergeContents(
248
+ baseContent,
249
+ sourceContent,
250
+ targetContent
251
+ );
252
+ if (conflict) conflictingPaths.push(filepath);
253
+ const mergedOid = await writeBlob(repo, content);
254
+ resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
326
255
  }
327
256
  } else {
328
257
  }
@@ -356,7 +285,6 @@ async function threeWayMerge(repo, sourceRef, targetRef, opts) {
356
285
  }
357
286
 
358
287
  // src/repo.ts
359
- import git2 from "isomorphic-git";
360
288
  var repoCaches = /* @__PURE__ */ new Map();
361
289
  function getRepoCache(ownerKey, repoName) {
362
290
  const key = `${ownerKey}/${repoName}`;
@@ -370,47 +298,11 @@ function getRepoCache(ownerKey, repoName) {
370
298
  function invalidateRepoCache(ownerKey, repoName) {
371
299
  repoCaches.delete(`${ownerKey}/${repoName}`);
372
300
  }
373
- async function initBareRepo(repo, defaultBranch = "main") {
374
- await git2.init({
375
- ...repo,
376
- dir: repo.gitdir,
377
- defaultBranch,
378
- bare: true
379
- });
380
- }
381
- async function estimateRepoSize(repo, stat, list) {
382
- let total = 0;
383
- async function walk(dir) {
384
- let entries;
385
- try {
386
- entries = await list(dir);
387
- } catch {
388
- return;
389
- }
390
- const sizes = await Promise.all(
391
- entries.map(async (name) => {
392
- const fullPath = `${dir}/${name}`;
393
- try {
394
- const s = await stat(fullPath);
395
- return s.size;
396
- } catch {
397
- return 0;
398
- }
399
- })
400
- );
401
- for (const size of sizes) total += size;
402
- }
403
- await walk(`${repo.gitdir}/objects`);
404
- return total;
405
- }
406
301
  export {
407
302
  GitEdgeError,
408
303
  GitMergeConflictError,
409
- analyzeMerge,
410
304
  createParsedObjectCache,
411
- estimateRepoSize,
412
305
  getRepoCache,
413
- initBareRepo,
414
306
  invalidateRepoCache,
415
307
  threeWayMerge
416
308
  };
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 (bytes estimate via JSON.stringify.length) 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` uses `JSON.stringify(v).length` as a byte estimate.\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) => JSON.stringify(v).length,\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 { FsClient } from \"isomorphic-git\";\nimport git from \"isomorphic-git\";\nimport { GitMergeConflictError } from \"./errors.js\";\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/** Analysis of whether a merge is possible. */\nexport interface MergeAnalysis {\n\t/** True if both refs resolved. */\n\tcanMerge: boolean;\n\t/** True if source is an ancestor of target (fast-forward possible). */\n\tfastForward: boolean;\n\t/** True if source and target diverged. */\n\tdiverged: boolean;\n}\n\n/** The repo fs+gitdir shape used by isomorphic-git. */\nexport interface Repo {\n\tfs: FsClient;\n\tgitdir: string;\n\tcache?: object;\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/** Read a tree object into a flat map of path → oid. */\nasync function _flattenTree(\n\trepo: Repo,\n\ttreeOid: string,\n): Promise<Map<string, string>> {\n\tconst map = new Map<string, string>();\n\tconst entries = (await git.readTree({ ...repo, oid: treeOid })).tree;\n\tfor (const entry of entries) {\n\t\tmap.set(entry.path, entry.oid);\n\t}\n\treturn map;\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 * Simple byte-level 3-way merge on two Uint8Array contents.\n *\n * This is a line-level merge using a basic diff algorithm.\n * For production quality, callers should use the `diff` library —\n * this is the fallback for edge environments where `diff` is not available.\n */\nfunction mergeContents(\n\tbase: Uint8Array,\n\tours: Uint8Array,\n\ttheirs: Uint8Array,\n): Uint8Array {\n\tconst baseLines = decodeLines(base);\n\tconst oursLines = decodeLines(ours);\n\tconst theirsLines = decodeLines(theirs);\n\n\t// If either side is identical to base, take the other\n\tif (arraysEqual(baseLines, oursLines)) return theirs;\n\tif (arraysEqual(baseLines, theirsLines)) return ours;\n\tif (arraysEqual(oursLines, theirsLines)) return ours;\n\n\t// Simple line-by-line merge: walk all three in lockstep\n\tconst result: string[] = [];\n\tlet bi = 0,\n\t\toi = 0,\n\t\tti = 0;\n\n\twhile (\n\t\tbi < baseLines.length ||\n\t\toi < oursLines.length ||\n\t\tti < theirsLines.length\n\t) {\n\t\tconst b = bi < baseLines.length ? baseLines[bi] : undefined;\n\t\tconst o = oi < oursLines.length ? oursLines[oi] : undefined;\n\t\tconst t = ti < theirsLines.length ? theirsLines[ti] : undefined;\n\n\t\tif (b === o && b === t) {\n\t\t\t// All three agree\n\t\t\tresult.push(b ?? \"\");\n\t\t\tbi++;\n\t\t\toi++;\n\t\t\tti++;\n\t\t} else if (b === o) {\n\t\t\t// Only theirs changed — including the case where theirs ran out\n\t\t\t// of lines here (t undefined, i.e. a deletion): `?? \"\"` matches\n\t\t\t// what pushing `t` directly already did once joined below\n\t\t\t// (Array.prototype.join renders an undefined element as \"\"),\n\t\t\t// just without relying on `!` to silence the type checker\n\t\t\t// about it.\n\t\t\tresult.push(t ?? \"\");\n\t\t\tbi++;\n\t\t\toi++;\n\t\t\tti++;\n\t\t} else if (b === t) {\n\t\t\t// Only ours changed — symmetric to the theirs case above.\n\t\t\tresult.push(o ?? \"\");\n\t\t\tbi++;\n\t\t\toi++;\n\t\t\tti++;\n\t\t} else {\n\t\t\t// Both changed — conflict: take ours + theirs separated by\n\t\t\t// conflict markers. o or t can individually be undefined here\n\t\t\t// too (one side ran out of lines while the other still\n\t\t\t// conflicts with base) — same `?? \"\"` reasoning as above.\n\t\t\tresult.push(`<<<<<<< ours`);\n\t\t\tresult.push(o ?? \"\");\n\t\t\tresult.push(`=======`);\n\t\t\tresult.push(t ?? \"\");\n\t\t\tresult.push(`>>>>>>> theirs`);\n\t\t\tbi++;\n\t\t\toi++;\n\t\t\tti++;\n\t\t}\n\t}\n\n\treturn encodeLines(result);\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\nfunction arraysEqual(a: string[], b: string[]): boolean {\n\tif (a.length !== b.length) return false;\n\tfor (let i = 0; i < a.length; i++) {\n\t\tif (a[i] !== b[i]) return false;\n\t}\n\treturn true;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Analyze whether a merge is possible between two refs.\n *\n * Returns `canMerge`, `fastForward`, and `diverged` — does not attempt\n * a real content merge (no conflict detection in analysis).\n *\n * `canMerge: false` means one of the refs didn't resolve (deleted branch,\n * typo) — a `NotFoundError` from `git.resolveRef`. Any other failure (a\n * network/storage error reading the ref, a corrupted repo) is rethrown\n * rather than folded into `canMerge: false`, which would otherwise report\n * \"this branch doesn't exist\" for a transient failure that has nothing to\n * do with whether the branches can merge.\n */\nexport async function analyzeMerge(\n\trepo: Repo,\n\tsourceRef: string,\n\ttargetRef: string,\n): Promise<MergeAnalysis> {\n\ttry {\n\t\tconst sourceOid = await git.resolveRef({ ...repo, ref: sourceRef });\n\t\tconst targetOid = await git.resolveRef({ ...repo, ref: targetRef });\n\n\t\tconst isDescendant = await git.isDescendent({\n\t\t\t...repo,\n\t\t\toid: sourceOid,\n\t\t\tancestor: targetOid,\n\t\t});\n\n\t\treturn {\n\t\t\tcanMerge: true,\n\t\t\tfastForward: isDescendant,\n\t\t\tdiverged: !isDescendant,\n\t\t};\n\t} catch (err) {\n\t\tif ((err as { code?: string })?.code !== \"NotFoundError\") {\n\t\t\tthrow err;\n\t\t}\n\t\treturn { canMerge: false, fastForward: false, diverged: false };\n\t}\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 merged = 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\tconflictingPaths.push(filepath);\n\t\t\t\t\tconst mergedOid = await writeBlob(repo, merged);\n\t\t\t\t\tresultEntries.set(filepath, { oid: mergedOid, mode: \"100644\" });\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Both changed — attempt content 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\n\t\t\t\tif (\n\t\t\t\t\tbaseContent.length === sourceContent.length &&\n\t\t\t\t\tbaseContent.every((b, i) => b === sourceContent[i])\n\t\t\t\t) {\n\t\t\t\t\t// Source is identical to base — take target\n\t\t\t\t\tresultEntries.set(filepath, { oid: targetOid, mode: \"100644\" });\n\t\t\t\t} else if (\n\t\t\t\t\tbaseContent.length === targetContent.length &&\n\t\t\t\t\tbaseContent.every((b, i) => b === targetContent[i])\n\t\t\t\t) {\n\t\t\t\t\t// Target is identical to base — take source\n\t\t\t\t\tresultEntries.set(filepath, { oid: sourceOid, mode: \"100644\" });\n\t\t\t\t} else if (\n\t\t\t\t\tsourceContent.length === targetContent.length &&\n\t\t\t\t\tsourceContent.every((b, i) => b === targetContent[i])\n\t\t\t\t) {\n\t\t\t\t\t// Source and target are identical — keep either\n\t\t\t\t\tresultEntries.set(filepath, { oid: sourceOid, mode: \"100644\" });\n\t\t\t\t} else {\n\t\t\t\t\t// Both truly changed — attempt line-level merge\n\t\t\t\t\tconst merged = mergeContents(\n\t\t\t\t\t\tbaseContent,\n\t\t\t\t\t\tsourceContent,\n\t\t\t\t\t\ttargetContent,\n\t\t\t\t\t);\n\n\t\t\t\t\t// Check for conflict markers\n\t\t\t\t\tconst mergedText = new TextDecoder().decode(merged);\n\t\t\t\t\tif (\n\t\t\t\t\t\tmergedText.includes(\"<<<<<<< ours\") ||\n\t\t\t\t\t\tmergedText.includes(\">>>>>>> theirs\")\n\t\t\t\t\t) {\n\t\t\t\t\t\tconflictingPaths.push(filepath);\n\t\t\t\t\t\t// Still take the merged result with conflict markers\n\t\t\t\t\t\t// so the caller can see what happened\n\t\t\t\t\t\tconst mergedOid = await writeBlob(repo, merged);\n\t\t\t\t\t\tresultEntries.set(filepath, { oid: mergedOid, mode: \"100644\" });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst mergedOid = await writeBlob(repo, merged);\n\t\t\t\t\t\tresultEntries.set(filepath, { oid: mergedOid, mode: \"100644\" });\n\t\t\t\t\t}\n\t\t\t\t}\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 * Repository management utilities.\n *\n * Edge-compatible helpers for isomorphic-git repos: bare repo init,\n * packfile-only disk usage, and isomorphic-git cache management.\n *\n * No node: imports. The `fs` parameter is caller-provided.\n */\n\nimport type { FsClient } from \"isomorphic-git\";\nimport git from \"isomorphic-git\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** The repo fs+gitdir shape used by isomorphic-git. */\nexport interface Repo {\n\tfs: FsClient;\n\tgitdir: string;\n\tcache?: object;\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 */\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\tcache = {};\n\t\trepoCaches.set(key, cache);\n\t}\n\treturn cache;\n}\n\nexport function invalidateRepoCache(ownerKey: string, repoName: string): void {\n\trepoCaches.delete(`${ownerKey}/${repoName}`);\n}\n\n// ---------------------------------------------------------------------------\n// Repo init\n// ---------------------------------------------------------------------------\n\n/**\n * Initialize a new bare git repository.\n *\n * Accepts any fs implementation — works with node:fs, S3-backed fs,\n * or any isomorphic-git-compatible filesystem.\n *\n * Returns the gitdir path.\n */\nexport async function initBareRepo(\n\trepo: Repo,\n\tdefaultBranch: string = \"main\",\n): Promise<void> {\n\tawait git.init({\n\t\t...repo,\n\t\tdir: repo.gitdir,\n\t\tdefaultBranch,\n\t\tbare: true,\n\t});\n}\n\n// ---------------------------------------------------------------------------\n// Disk usage\n// ---------------------------------------------------------------------------\n\n/**\n * Estimate the disk usage of a bare repo's git objects.\n *\n * This walks the loose object directories and reads `.pack` file sizes\n * from the `objects/` directory. For S3-backed repos, this may be\n * expensive — prefer tracking size at write time if possible.\n *\n * The caller must provide a `stat` function that works with their fs.\n * For node:fs this is `fs.stat`; for edge runtimes it may be a HEAD\n * request to the S3 object.\n */\nexport async function estimateRepoSize(\n\trepo: Repo,\n\tstat: (path: string) => Promise<{ size: number }>,\n\tlist: (path: string) => Promise<string[]>,\n): Promise<number> {\n\tlet total = 0;\n\n\tasync function walk(dir: string): Promise<void> {\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = await list(dir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst sizes = await Promise.all(\n\t\t\tentries.map(async (name) => {\n\t\t\t\tconst fullPath = `${dir}/${name}`;\n\t\t\t\ttry {\n\t\t\t\t\tconst s = await stat(fullPath);\n\t\t\t\t\treturn s.size;\n\t\t\t\t} catch {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\tfor (const size of sizes) total += size;\n\t}\n\n\tawait walk(`${repo.gitdir}/objects`);\n\treturn total;\n}\n"],"mappings":"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","names":["baseOid","sourceOid","targetOid","git"]}
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 (bytes estimate via JSON.stringify.length) 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` uses `JSON.stringify(v).length` as a byte estimate.\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) => JSON.stringify(v).length,\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 */\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\tcache = {};\n\t\trepoCaches.set(key, cache);\n\t}\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-edge",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
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.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -51,7 +51,8 @@
51
51
  "node": ">=18"
52
52
  },
53
53
  "dependencies": {
54
- "lru-cache": "^11.2.1"
54
+ "lru-cache": "^11.2.1",
55
+ "node-diff3": "^3.2.1"
55
56
  },
56
57
  "peerDependencies": {
57
58
  "git-fs-s3": ">=0.3.0",
@@ -59,8 +60,8 @@
59
60
  },
60
61
  "devDependencies": {
61
62
  "@biomejs/biome": "^2.3.8",
62
- "git-fs-s3": "^0.3.0",
63
63
  "@types/node": "^22.15.21",
64
+ "git-fs-s3": "^0.3.0",
64
65
  "isomorphic-git": "^1.27.0",
65
66
  "tsup": "^8.5.0",
66
67
  "typescript": "^5.8.3",