git-edge 0.1.2
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/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/index.cjs +448 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +403 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nandan Varma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# git-edge
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/git-edge)
|
|
4
|
+
[](https://github.com/nandan-varma/git-edge/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
High-level edge-compatible git operations on top of [isomorphic-git](https://isomorphic-git.org): a parsed-object LRU cache, an object-level three-way merge that never needs a worktree, and repo-cache/init utilities. No `node:*` imports anywhere in `src/` — runs on Cloudflare Workers, Vercel Edge, Deno Deploy, and Node.
|
|
8
|
+
|
|
9
|
+
Extracted from the same production git-hosting service as [`git-fs-s3`](https://www.npmjs.com/package/git-fs-s3) — the two compose (see below) but neither imports the other; both just agree on isomorphic-git's `{ fs, gitdir, cache? }` shape.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
isomorphic-git's own `git.merge` needs a worktree — a real (or hydrated-to-`/tmp`) checkout to run its merge driver against. That's a problem for a bare repo living entirely in object storage with no durable disk: merging means materializing the whole tree locally first. `threeWayMerge` here works directly on the object graph (trees, blobs, commits) — no checkout, no worktree, safe to run inside a Lambda/Worker/Edge function that only ever sees `{ fs, gitdir }`.
|
|
14
|
+
|
|
15
|
+
Separately, isomorphic-git re-parses a packfile index from scratch on every `readTree`/`log`/`readObject` call unless callers share a `cache` object across calls — and even with that shared cache, higher-level *parsed* results (a rendered commit list, a resolved merge base) get recomputed every time. `createParsedObjectCache` is a generic LRU for exactly those parsed values, keyed however the caller likes.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install git-edge isomorphic-git
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`git-fs-s3` is an optional peer — install it if you need an S3/R2-backed `fs` to pass in; git-edge itself works with any isomorphic-git-compatible `fs` (including plain `node:fs`).
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import git from "isomorphic-git";
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import { threeWayMerge, GitMergeConflictError } from "git-edge";
|
|
31
|
+
|
|
32
|
+
const repo = { fs, gitdir: "/repo.git", cache: {} };
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const { commitOid } = await threeWayMerge(repo, "feature", "main", {
|
|
36
|
+
authorName: "Ada",
|
|
37
|
+
authorEmail: "ada@example.com",
|
|
38
|
+
});
|
|
39
|
+
console.log("merged:", commitOid);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err instanceof GitMergeConflictError) {
|
|
42
|
+
console.log("conflicts in:", err.conflictingPaths);
|
|
43
|
+
} else {
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### With git-fs-s3
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { createGitFs, MemoryObjectStore } from "git-fs-s3";
|
|
53
|
+
import { threeWayMerge, initBareRepo } from "git-edge";
|
|
54
|
+
|
|
55
|
+
const fs = createGitFs(new MemoryObjectStore());
|
|
56
|
+
const repo = { fs, gitdir: "/repo.git", cache: {} };
|
|
57
|
+
|
|
58
|
+
await initBareRepo(repo);
|
|
59
|
+
// ... commits land on "feature" and "main" via git-fs-s3's fs ...
|
|
60
|
+
await threeWayMerge(repo, "feature", "main");
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
### `threeWayMerge(repo, sourceRef, targetRef, opts?)`
|
|
66
|
+
|
|
67
|
+
Merges `sourceRef` into `targetRef` at the object level — no worktree.
|
|
68
|
+
|
|
69
|
+
- Source is an ancestor of target, or vice versa → fast-forward: just moves `targetRef`, no merge commit.
|
|
70
|
+
- Otherwise, flattens both trees (deep, recursive) against their merge base, takes non-conflicting changes automatically, and content-merges paths both sides touched with a line-level three-way merge.
|
|
71
|
+
- Throws `GitMergeConflictError` (with `conflictingPaths: string[]`) if any file has unresolved conflicts. The conflict markers (`<<<<<<< ours` / `=======` / `>>>>>>> theirs`) are still written to a blob and included in the (unreached) result tree — a caller that wants "write the conflicted state so a human can resolve it" can catch the error, note the paths, and re-run its own resolution flow rather than losing that information.
|
|
72
|
+
- `opts.message`, `opts.authorName`/`opts.authorEmail` (default `"Git Edge" <git-edge@local>`).
|
|
73
|
+
|
|
74
|
+
Returns `{ commitOid }` — the new merge commit, or the fast-forwarded `targetRef`'s new oid.
|
|
75
|
+
|
|
76
|
+
### `analyzeMerge(repo, sourceRef, targetRef)`
|
|
77
|
+
|
|
78
|
+
Cheap pre-merge check: resolves both refs and checks ancestry. Returns `{ canMerge, fastForward, diverged }` — `canMerge: false` only means a ref failed to resolve, not that a real merge would conflict (that's only knowable by attempting one; this doesn't walk trees at all). Safe to call before deciding whether to show a "conflicts likely" hint in a UI.
|
|
79
|
+
|
|
80
|
+
### `createParsedObjectCache(options?)`
|
|
81
|
+
|
|
82
|
+
A generic in-memory LRU for any JS value, keyed by caller-chosen strings (convention: `` `${oid}:${format}` ``).
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const cache = createParsedObjectCache({ maxSize: 128 * 1024 * 1024, ttl: 3600_000 });
|
|
86
|
+
cache.set(`${oid}:commit`, parsedCommit);
|
|
87
|
+
const hit = cache.get<ParsedCommit>(`${oid}:commit`);
|
|
88
|
+
cache.invalidatePrefix(gitdir); // drop everything under a repo after a rewrite
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `options.maxSize` — byte budget, estimated via `JSON.stringify(value).length` (default 256 MiB).
|
|
92
|
+
- `options.ttl` — entry TTL in ms (default 1 h).
|
|
93
|
+
- `.invalidatePrefix(prefix)` — drop every key starting with `prefix`; O(cache size), fine for occasional invalidation, not a hot-path operation.
|
|
94
|
+
|
|
95
|
+
### `getRepoCache(ownerKey, repoName)` / `invalidateRepoCache(ownerKey, repoName)`
|
|
96
|
+
|
|
97
|
+
Per-repo isomorphic-git packfile `cache` object management, keyed `` `${ownerKey}/${repoName}` ``. isomorphic-git treats this `cache` as opaque and safe to share indefinitely (git objects are content-addressed/immutable), so a long-lived per-repo instance turns "reparse this pack's index" from once-per-call into once-per-process. Call `invalidateRepoCache` after anything rewrites a repo's storage out from under a live process (a rename, a bulk resync) so stale parsed state can't leak into the next read.
|
|
98
|
+
|
|
99
|
+
### `initBareRepo(repo, defaultBranch?)`
|
|
100
|
+
|
|
101
|
+
Thin wrapper over `git.init({ ...repo, bare: true })` — accepts any fs (git-fs-s3-backed, `node:fs`, in-memory), defaults `defaultBranch` to `"main"`.
|
|
102
|
+
|
|
103
|
+
### `estimateRepoSize(repo, stat, list)`
|
|
104
|
+
|
|
105
|
+
Sums `.pack`/loose-object file sizes under `objects/`. Caller supplies `stat`/`list` since "size of a file" isn't part of isomorphic-git's own `fs` contract — for `node:fs` that's `fs.stat`/`fs.readdir`; for an object-storage-backed fs it's typically a HEAD request per key. Can be expensive against remote storage — prefer tracking size incrementally at write time where possible.
|
|
106
|
+
|
|
107
|
+
### Errors
|
|
108
|
+
|
|
109
|
+
`GitEdgeError` — base class. `GitMergeConflictError extends GitEdgeError` — `conflictingPaths: string[]`, thrown only by `threeWayMerge`.
|
|
110
|
+
|
|
111
|
+
## Semantics & limitations
|
|
112
|
+
|
|
113
|
+
- `threeWayMerge`'s content merge is a from-scratch line-level three-way merge (not `diff3`/libgit2), used only for paths both sides changed from the merge base — most changes (added/deleted/single-side-modified) resolve without touching it at all.
|
|
114
|
+
- No binary-file merge support — conflict markers are written as text into whatever bytes the paths held; binary content will produce a nonsensical merged blob, not a clean conflict signal. Detect binary paths upstream if that matters for your use case.
|
|
115
|
+
- `estimateRepoSize` and the parsed-object cache are general-purpose helpers, not required by `threeWayMerge`/`analyzeMerge` — use whichever pieces you need independently.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
[MIT](LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
GitEdgeError: () => GitEdgeError,
|
|
34
|
+
GitMergeConflictError: () => GitMergeConflictError,
|
|
35
|
+
analyzeMerge: () => analyzeMerge,
|
|
36
|
+
createParsedObjectCache: () => createParsedObjectCache,
|
|
37
|
+
estimateRepoSize: () => estimateRepoSize,
|
|
38
|
+
getRepoCache: () => getRepoCache,
|
|
39
|
+
initBareRepo: () => initBareRepo,
|
|
40
|
+
invalidateRepoCache: () => invalidateRepoCache,
|
|
41
|
+
threeWayMerge: () => threeWayMerge
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/cache.ts
|
|
46
|
+
var import_lru_cache = require("lru-cache");
|
|
47
|
+
function createParsedObjectCache(options) {
|
|
48
|
+
const maxSize = options?.maxSize ?? 256 * 1024 * 1024;
|
|
49
|
+
const ttl = options?.ttl ?? 3600 * 1e3;
|
|
50
|
+
const cache = new import_lru_cache.LRUCache({
|
|
51
|
+
maxSize,
|
|
52
|
+
sizeCalculation: (v) => JSON.stringify(v).length,
|
|
53
|
+
ttl
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
get(key) {
|
|
57
|
+
return cache.get(key) ?? null;
|
|
58
|
+
},
|
|
59
|
+
set(key, value) {
|
|
60
|
+
cache.set(key, value);
|
|
61
|
+
},
|
|
62
|
+
delete(key) {
|
|
63
|
+
cache.delete(key);
|
|
64
|
+
},
|
|
65
|
+
invalidatePrefix(prefix) {
|
|
66
|
+
for (const k of cache.keys()) {
|
|
67
|
+
if (k.startsWith(prefix)) cache.delete(k);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/errors.ts
|
|
74
|
+
var GitEdgeError = class extends Error {
|
|
75
|
+
constructor(message) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = "GitEdgeError";
|
|
78
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var GitMergeConflictError = class extends GitEdgeError {
|
|
82
|
+
conflictingPaths;
|
|
83
|
+
constructor(conflictingPaths) {
|
|
84
|
+
super(
|
|
85
|
+
`Merge conflict in ${conflictingPaths.length} file(s): ${conflictingPaths.join(", ")}`
|
|
86
|
+
);
|
|
87
|
+
this.name = "GitMergeConflictError";
|
|
88
|
+
this.conflictingPaths = conflictingPaths;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/merge.ts
|
|
93
|
+
var import_isomorphic_git = __toESM(require("isomorphic-git"), 1);
|
|
94
|
+
function defaultAuthor() {
|
|
95
|
+
return {
|
|
96
|
+
name: "Git Edge",
|
|
97
|
+
email: "git-edge@local",
|
|
98
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
99
|
+
timezoneOffset: 0
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function flattenTreeDeep(repo, treeOid, prefix) {
|
|
103
|
+
const map = /* @__PURE__ */ new Map();
|
|
104
|
+
const entries = (await import_isomorphic_git.default.readTree({ ...repo, oid: treeOid })).tree;
|
|
105
|
+
await Promise.all(
|
|
106
|
+
entries.map(async (entry) => {
|
|
107
|
+
const fullPath = prefix ? `${prefix}/${entry.path}` : entry.path;
|
|
108
|
+
if (entry.type === "tree") {
|
|
109
|
+
const sub = await flattenTreeDeep(repo, entry.oid, fullPath);
|
|
110
|
+
for (const [k, v] of sub) map.set(k, v);
|
|
111
|
+
} else {
|
|
112
|
+
map.set(fullPath, entry.oid);
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
return map;
|
|
117
|
+
}
|
|
118
|
+
async function readBlob(repo, oid) {
|
|
119
|
+
const { blob } = await import_isomorphic_git.default.readBlob({ ...repo, oid });
|
|
120
|
+
return blob;
|
|
121
|
+
}
|
|
122
|
+
async function writeBlob(repo, content) {
|
|
123
|
+
return import_isomorphic_git.default.writeBlob({ ...repo, blob: content });
|
|
124
|
+
}
|
|
125
|
+
async function writeTreeObject(repo, entries) {
|
|
126
|
+
return import_isomorphic_git.default.writeObject({
|
|
127
|
+
...repo,
|
|
128
|
+
type: "tree",
|
|
129
|
+
object: entries.map((e) => ({
|
|
130
|
+
path: e.path,
|
|
131
|
+
oid: e.oid,
|
|
132
|
+
mode: e.mode,
|
|
133
|
+
type: e.type
|
|
134
|
+
})),
|
|
135
|
+
format: "parsed"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async function writeTreeFromFlat(repo, flat) {
|
|
139
|
+
const root = /* @__PURE__ */ new Map();
|
|
140
|
+
const children = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const [p, v] of flat) {
|
|
142
|
+
const slash = p.indexOf("/");
|
|
143
|
+
if (slash === -1) {
|
|
144
|
+
root.set(p, v);
|
|
145
|
+
} else {
|
|
146
|
+
const dir = p.slice(0, slash);
|
|
147
|
+
const rest = p.slice(slash + 1);
|
|
148
|
+
if (!children.has(dir)) children.set(dir, /* @__PURE__ */ new Map());
|
|
149
|
+
children.get(dir)?.set(rest, v);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const treeEntries = [];
|
|
153
|
+
for (const [name, { oid, mode }] of root) {
|
|
154
|
+
treeEntries.push({ path: name, oid, mode, type: "blob" });
|
|
155
|
+
}
|
|
156
|
+
for (const [dirName, subMap] of children) {
|
|
157
|
+
const subOid = await writeTreeFromFlat(repo, subMap);
|
|
158
|
+
treeEntries.push({
|
|
159
|
+
path: dirName,
|
|
160
|
+
oid: subOid,
|
|
161
|
+
mode: "040000",
|
|
162
|
+
type: "tree"
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return writeTreeObject(repo, treeEntries);
|
|
166
|
+
}
|
|
167
|
+
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);
|
|
207
|
+
}
|
|
208
|
+
function decodeLines(data) {
|
|
209
|
+
const text = new TextDecoder().decode(data);
|
|
210
|
+
if (text === "") return [];
|
|
211
|
+
return text.split("\n");
|
|
212
|
+
}
|
|
213
|
+
function encodeLines(lines) {
|
|
214
|
+
return new TextEncoder().encode(lines.join("\n"));
|
|
215
|
+
}
|
|
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
|
+
async function threeWayMerge(repo, sourceRef, targetRef, opts) {
|
|
245
|
+
const [sourceOid, targetOid] = await Promise.all([
|
|
246
|
+
import_isomorphic_git.default.resolveRef({ ...repo, ref: sourceRef }),
|
|
247
|
+
import_isomorphic_git.default.resolveRef({ ...repo, ref: targetRef })
|
|
248
|
+
]);
|
|
249
|
+
if (sourceOid === targetOid) {
|
|
250
|
+
return { commitOid: sourceOid };
|
|
251
|
+
}
|
|
252
|
+
const isSourceAhead = await import_isomorphic_git.default.isDescendent({
|
|
253
|
+
...repo,
|
|
254
|
+
oid: sourceOid,
|
|
255
|
+
ancestor: targetOid
|
|
256
|
+
});
|
|
257
|
+
if (isSourceAhead) {
|
|
258
|
+
await import_isomorphic_git.default.writeRef({
|
|
259
|
+
...repo,
|
|
260
|
+
ref: targetRef,
|
|
261
|
+
value: sourceOid,
|
|
262
|
+
force: true
|
|
263
|
+
});
|
|
264
|
+
return { commitOid: sourceOid };
|
|
265
|
+
}
|
|
266
|
+
const isTargetAhead = await import_isomorphic_git.default.isDescendent({
|
|
267
|
+
...repo,
|
|
268
|
+
oid: targetOid,
|
|
269
|
+
ancestor: sourceOid
|
|
270
|
+
});
|
|
271
|
+
if (isTargetAhead) {
|
|
272
|
+
await import_isomorphic_git.default.writeRef({
|
|
273
|
+
...repo,
|
|
274
|
+
ref: targetRef,
|
|
275
|
+
value: sourceOid,
|
|
276
|
+
force: true
|
|
277
|
+
});
|
|
278
|
+
return { commitOid: sourceOid };
|
|
279
|
+
}
|
|
280
|
+
const baseOids = await import_isomorphic_git.default.findMergeBase({
|
|
281
|
+
...repo,
|
|
282
|
+
oids: [sourceOid, targetOid]
|
|
283
|
+
});
|
|
284
|
+
if (!baseOids || baseOids.length === 0) {
|
|
285
|
+
throw new Error("No merge base found between source and target");
|
|
286
|
+
}
|
|
287
|
+
const baseOid = baseOids[0];
|
|
288
|
+
const [baseCommit, sourceCommit, targetCommit] = await Promise.all([
|
|
289
|
+
import_isomorphic_git.default.readCommit({ ...repo, oid: baseOid }),
|
|
290
|
+
import_isomorphic_git.default.readCommit({ ...repo, oid: sourceOid }),
|
|
291
|
+
import_isomorphic_git.default.readCommit({ ...repo, oid: targetOid })
|
|
292
|
+
]);
|
|
293
|
+
const baseTreeOid = baseCommit.commit.tree;
|
|
294
|
+
const sourceTreeOid = sourceCommit.commit.tree;
|
|
295
|
+
const targetTreeOid = targetCommit.commit.tree;
|
|
296
|
+
const [baseMap, sourceMap, targetMap] = await Promise.all([
|
|
297
|
+
flattenTreeDeep(repo, baseTreeOid, ""),
|
|
298
|
+
flattenTreeDeep(repo, sourceTreeOid, ""),
|
|
299
|
+
flattenTreeDeep(repo, targetTreeOid, "")
|
|
300
|
+
]);
|
|
301
|
+
const allPaths = /* @__PURE__ */ new Set([
|
|
302
|
+
...baseMap.keys(),
|
|
303
|
+
...sourceMap.keys(),
|
|
304
|
+
...targetMap.keys()
|
|
305
|
+
]);
|
|
306
|
+
const resultEntries = /* @__PURE__ */ new Map();
|
|
307
|
+
const conflictingPaths = [];
|
|
308
|
+
for (const filepath of allPaths) {
|
|
309
|
+
const baseOid2 = baseMap.get(filepath);
|
|
310
|
+
const sourceOid2 = sourceMap.get(filepath);
|
|
311
|
+
const targetOid2 = targetMap.get(filepath);
|
|
312
|
+
const _baseExists = baseOid2 !== void 0;
|
|
313
|
+
const sourceExists = sourceOid2 !== void 0;
|
|
314
|
+
const targetExists = targetOid2 !== void 0;
|
|
315
|
+
if (sourceExists && !targetExists) {
|
|
316
|
+
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
317
|
+
} else if (!sourceExists && targetExists) {
|
|
318
|
+
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
319
|
+
} else if (sourceExists && targetExists) {
|
|
320
|
+
const sourceChanged = sourceOid2 !== baseOid2;
|
|
321
|
+
const targetChanged = targetOid2 !== baseOid2;
|
|
322
|
+
if (!sourceChanged && !targetChanged) {
|
|
323
|
+
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
324
|
+
} else if (!sourceChanged) {
|
|
325
|
+
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
326
|
+
} else if (!targetChanged) {
|
|
327
|
+
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
328
|
+
} else {
|
|
329
|
+
const [baseContent, sourceContent, targetContent] = await Promise.all([
|
|
330
|
+
readBlob(repo, baseOid2),
|
|
331
|
+
readBlob(repo, sourceOid2),
|
|
332
|
+
readBlob(repo, targetOid2)
|
|
333
|
+
]);
|
|
334
|
+
if (baseContent.length === sourceContent.length && baseContent.every((b, i) => b === sourceContent[i])) {
|
|
335
|
+
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
336
|
+
} else if (baseContent.length === targetContent.length && baseContent.every((b, i) => b === targetContent[i])) {
|
|
337
|
+
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
338
|
+
} else if (sourceContent.length === targetContent.length && sourceContent.every((b, i) => b === targetContent[i])) {
|
|
339
|
+
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
340
|
+
} else {
|
|
341
|
+
const merged = mergeContents(
|
|
342
|
+
baseContent,
|
|
343
|
+
sourceContent,
|
|
344
|
+
targetContent
|
|
345
|
+
);
|
|
346
|
+
const mergedText = new TextDecoder().decode(merged);
|
|
347
|
+
if (mergedText.includes("<<<<<<< ours") || mergedText.includes(">>>>>>> theirs")) {
|
|
348
|
+
conflictingPaths.push(filepath);
|
|
349
|
+
const mergedOid = await writeBlob(repo, merged);
|
|
350
|
+
resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
|
|
351
|
+
} else {
|
|
352
|
+
const mergedOid = await writeBlob(repo, merged);
|
|
353
|
+
resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (conflictingPaths.length > 0) {
|
|
361
|
+
throw new GitMergeConflictError(conflictingPaths);
|
|
362
|
+
}
|
|
363
|
+
const resultTreeOid = await writeTreeFromFlat(repo, resultEntries);
|
|
364
|
+
const author = opts?.authorName ? {
|
|
365
|
+
name: opts.authorName,
|
|
366
|
+
email: opts.authorEmail || "git-edge@local",
|
|
367
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
368
|
+
timezoneOffset: 0
|
|
369
|
+
} : defaultAuthor();
|
|
370
|
+
const message = opts?.message || `Merge ${sourceRef} into ${targetRef}`;
|
|
371
|
+
const mergeCommitOid = await import_isomorphic_git.default.commit({
|
|
372
|
+
...repo,
|
|
373
|
+
message,
|
|
374
|
+
tree: resultTreeOid,
|
|
375
|
+
parent: [targetOid, sourceOid],
|
|
376
|
+
author,
|
|
377
|
+
committer: author
|
|
378
|
+
});
|
|
379
|
+
await import_isomorphic_git.default.writeRef({
|
|
380
|
+
...repo,
|
|
381
|
+
ref: targetRef,
|
|
382
|
+
value: mergeCommitOid,
|
|
383
|
+
force: true
|
|
384
|
+
});
|
|
385
|
+
return { commitOid: mergeCommitOid };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/repo.ts
|
|
389
|
+
var import_isomorphic_git2 = __toESM(require("isomorphic-git"), 1);
|
|
390
|
+
var repoCaches = /* @__PURE__ */ new Map();
|
|
391
|
+
function getRepoCache(ownerKey, repoName) {
|
|
392
|
+
const key = `${ownerKey}/${repoName}`;
|
|
393
|
+
let cache = repoCaches.get(key);
|
|
394
|
+
if (!cache) {
|
|
395
|
+
cache = {};
|
|
396
|
+
repoCaches.set(key, cache);
|
|
397
|
+
}
|
|
398
|
+
return cache;
|
|
399
|
+
}
|
|
400
|
+
function invalidateRepoCache(ownerKey, repoName) {
|
|
401
|
+
repoCaches.delete(`${ownerKey}/${repoName}`);
|
|
402
|
+
}
|
|
403
|
+
async function initBareRepo(repo, defaultBranch = "main") {
|
|
404
|
+
await import_isomorphic_git2.default.init({
|
|
405
|
+
...repo,
|
|
406
|
+
dir: repo.gitdir,
|
|
407
|
+
defaultBranch,
|
|
408
|
+
bare: true
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
async function estimateRepoSize(repo, stat, list) {
|
|
412
|
+
let total = 0;
|
|
413
|
+
async function walk(dir) {
|
|
414
|
+
let entries;
|
|
415
|
+
try {
|
|
416
|
+
entries = await list(dir);
|
|
417
|
+
} catch {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const sizes = await Promise.all(
|
|
421
|
+
entries.map(async (name) => {
|
|
422
|
+
const fullPath = `${dir}/${name}`;
|
|
423
|
+
try {
|
|
424
|
+
const s = await stat(fullPath);
|
|
425
|
+
return s.size;
|
|
426
|
+
} catch {
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
);
|
|
431
|
+
for (const size of sizes) total += size;
|
|
432
|
+
}
|
|
433
|
+
await walk(`${repo.gitdir}/objects`);
|
|
434
|
+
return total;
|
|
435
|
+
}
|
|
436
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
437
|
+
0 && (module.exports = {
|
|
438
|
+
GitEdgeError,
|
|
439
|
+
GitMergeConflictError,
|
|
440
|
+
analyzeMerge,
|
|
441
|
+
createParsedObjectCache,
|
|
442
|
+
estimateRepoSize,
|
|
443
|
+
getRepoCache,
|
|
444
|
+
initBareRepo,
|
|
445
|
+
invalidateRepoCache,
|
|
446
|
+
threeWayMerge
|
|
447
|
+
});
|
|
448
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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\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\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 conflict markers\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\tconst _baseExists = baseOid !== undefined;\n\t\tconst sourceExists = sourceOid !== undefined;\n\t\tconst targetExists = targetOid !== undefined;\n\n\t\tif (sourceExists && !targetExists) {\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 (!sourceExists && targetExists) {\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 (sourceExists && targetExists) {\n\t\t\t// Exists in both — check if both changed from base\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 {\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":["git","baseOid","sourceOid","targetOid","import_isomorphic_git","git"]}
|