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/dist/index.d.cts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { FsClient } from 'isomorphic-git';
|
|
2
|
+
|
|
3
|
+
interface ParsedObjectCacheOptions {
|
|
4
|
+
/** Maximum estimated size in bytes (default: 256 MB). */
|
|
5
|
+
maxSize?: number;
|
|
6
|
+
/** Time-to-live in milliseconds (default: 1 hour). */
|
|
7
|
+
ttl?: number;
|
|
8
|
+
}
|
|
9
|
+
interface ParsedObjectStore {
|
|
10
|
+
get<T extends object>(key: string): T | null;
|
|
11
|
+
set<T extends object>(key: string, value: T): void;
|
|
12
|
+
delete(key: string): void;
|
|
13
|
+
/** Delete all keys matching a prefix. */
|
|
14
|
+
invalidatePrefix(prefix: string): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create a parsed-object cache instance.
|
|
18
|
+
*
|
|
19
|
+
* `key` convention: `"${oid}:${format}"` or any caller-chosen string.
|
|
20
|
+
* `sizeCalculation` uses `JSON.stringify(v).length` as a byte estimate.
|
|
21
|
+
*/
|
|
22
|
+
declare function createParsedObjectCache(options?: ParsedObjectCacheOptions): ParsedObjectStore;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Error types for git-edge.
|
|
26
|
+
*
|
|
27
|
+
* Edge-compatible: no node: imports.
|
|
28
|
+
*/
|
|
29
|
+
/** Base class for all git-edge errors. */
|
|
30
|
+
declare class GitEdgeError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
/** Thrown when a three-way merge has unresolvable conflicts. */
|
|
34
|
+
declare class GitMergeConflictError extends GitEdgeError {
|
|
35
|
+
readonly conflictingPaths: string[];
|
|
36
|
+
constructor(conflictingPaths: string[]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Object-level three-way merge for isomorphic-git.
|
|
41
|
+
*
|
|
42
|
+
* Merges two commits by walking their trees directly — no worktree, no disk.
|
|
43
|
+
* Returns the merged commit OID, or throws MergeConflictError with the list of
|
|
44
|
+
* conflicting file paths.
|
|
45
|
+
*
|
|
46
|
+
* Edge-compatible: no node: imports, uses only isomorphic-git object APIs.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** Options for threeWayMerge. */
|
|
50
|
+
interface MergeOpts {
|
|
51
|
+
/** Merge commit message (default: "Merge <source> into <target>"). */
|
|
52
|
+
message?: string;
|
|
53
|
+
/** Author name (default: "Git Edge"). */
|
|
54
|
+
authorName?: string;
|
|
55
|
+
/** Author email (default: "git-edge@local"). */
|
|
56
|
+
authorEmail?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Result of a successful merge. */
|
|
59
|
+
interface MergeResult {
|
|
60
|
+
commitOid: string;
|
|
61
|
+
}
|
|
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
|
+
/**
|
|
92
|
+
* Perform an object-level three-way merge.
|
|
93
|
+
*
|
|
94
|
+
* - If source is an ancestor of target (fast-forward), moves target ref to source.
|
|
95
|
+
* - If target is an ancestor of source (reverse FF), moves target ref to source.
|
|
96
|
+
* - Otherwise, performs a true 3-way merge at the tree level.
|
|
97
|
+
*
|
|
98
|
+
* Throws `GitMergeConflictError` if the merge has unresolvable conflicts.
|
|
99
|
+
*/
|
|
100
|
+
declare function threeWayMerge(repo: Repo$1, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
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.
|
|
107
|
+
*
|
|
108
|
+
* No node: imports. The `fs` parameter is caller-provided.
|
|
109
|
+
*/
|
|
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
|
+
declare function getRepoCache(ownerKey: string, repoName: string): object;
|
|
118
|
+
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
|
+
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { FsClient } from 'isomorphic-git';
|
|
2
|
+
|
|
3
|
+
interface ParsedObjectCacheOptions {
|
|
4
|
+
/** Maximum estimated size in bytes (default: 256 MB). */
|
|
5
|
+
maxSize?: number;
|
|
6
|
+
/** Time-to-live in milliseconds (default: 1 hour). */
|
|
7
|
+
ttl?: number;
|
|
8
|
+
}
|
|
9
|
+
interface ParsedObjectStore {
|
|
10
|
+
get<T extends object>(key: string): T | null;
|
|
11
|
+
set<T extends object>(key: string, value: T): void;
|
|
12
|
+
delete(key: string): void;
|
|
13
|
+
/** Delete all keys matching a prefix. */
|
|
14
|
+
invalidatePrefix(prefix: string): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create a parsed-object cache instance.
|
|
18
|
+
*
|
|
19
|
+
* `key` convention: `"${oid}:${format}"` or any caller-chosen string.
|
|
20
|
+
* `sizeCalculation` uses `JSON.stringify(v).length` as a byte estimate.
|
|
21
|
+
*/
|
|
22
|
+
declare function createParsedObjectCache(options?: ParsedObjectCacheOptions): ParsedObjectStore;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Error types for git-edge.
|
|
26
|
+
*
|
|
27
|
+
* Edge-compatible: no node: imports.
|
|
28
|
+
*/
|
|
29
|
+
/** Base class for all git-edge errors. */
|
|
30
|
+
declare class GitEdgeError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
/** Thrown when a three-way merge has unresolvable conflicts. */
|
|
34
|
+
declare class GitMergeConflictError extends GitEdgeError {
|
|
35
|
+
readonly conflictingPaths: string[];
|
|
36
|
+
constructor(conflictingPaths: string[]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Object-level three-way merge for isomorphic-git.
|
|
41
|
+
*
|
|
42
|
+
* Merges two commits by walking their trees directly — no worktree, no disk.
|
|
43
|
+
* Returns the merged commit OID, or throws MergeConflictError with the list of
|
|
44
|
+
* conflicting file paths.
|
|
45
|
+
*
|
|
46
|
+
* Edge-compatible: no node: imports, uses only isomorphic-git object APIs.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** Options for threeWayMerge. */
|
|
50
|
+
interface MergeOpts {
|
|
51
|
+
/** Merge commit message (default: "Merge <source> into <target>"). */
|
|
52
|
+
message?: string;
|
|
53
|
+
/** Author name (default: "Git Edge"). */
|
|
54
|
+
authorName?: string;
|
|
55
|
+
/** Author email (default: "git-edge@local"). */
|
|
56
|
+
authorEmail?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Result of a successful merge. */
|
|
59
|
+
interface MergeResult {
|
|
60
|
+
commitOid: string;
|
|
61
|
+
}
|
|
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
|
+
/**
|
|
92
|
+
* Perform an object-level three-way merge.
|
|
93
|
+
*
|
|
94
|
+
* - If source is an ancestor of target (fast-forward), moves target ref to source.
|
|
95
|
+
* - If target is an ancestor of source (reverse FF), moves target ref to source.
|
|
96
|
+
* - Otherwise, performs a true 3-way merge at the tree level.
|
|
97
|
+
*
|
|
98
|
+
* Throws `GitMergeConflictError` if the merge has unresolvable conflicts.
|
|
99
|
+
*/
|
|
100
|
+
declare function threeWayMerge(repo: Repo$1, sourceRef: string, targetRef: string, opts?: MergeOpts): Promise<MergeResult>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
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.
|
|
107
|
+
*
|
|
108
|
+
* No node: imports. The `fs` parameter is caller-provided.
|
|
109
|
+
*/
|
|
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
|
+
declare function getRepoCache(ownerKey: string, repoName: string): object;
|
|
118
|
+
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
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
import { LRUCache } from "lru-cache";
|
|
3
|
+
function createParsedObjectCache(options) {
|
|
4
|
+
const maxSize = options?.maxSize ?? 256 * 1024 * 1024;
|
|
5
|
+
const ttl = options?.ttl ?? 3600 * 1e3;
|
|
6
|
+
const cache = new LRUCache({
|
|
7
|
+
maxSize,
|
|
8
|
+
sizeCalculation: (v) => JSON.stringify(v).length,
|
|
9
|
+
ttl
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
get(key) {
|
|
13
|
+
return cache.get(key) ?? null;
|
|
14
|
+
},
|
|
15
|
+
set(key, value) {
|
|
16
|
+
cache.set(key, value);
|
|
17
|
+
},
|
|
18
|
+
delete(key) {
|
|
19
|
+
cache.delete(key);
|
|
20
|
+
},
|
|
21
|
+
invalidatePrefix(prefix) {
|
|
22
|
+
for (const k of cache.keys()) {
|
|
23
|
+
if (k.startsWith(prefix)) cache.delete(k);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/errors.ts
|
|
30
|
+
var GitEdgeError = class extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "GitEdgeError";
|
|
34
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var GitMergeConflictError = class extends GitEdgeError {
|
|
38
|
+
conflictingPaths;
|
|
39
|
+
constructor(conflictingPaths) {
|
|
40
|
+
super(
|
|
41
|
+
`Merge conflict in ${conflictingPaths.length} file(s): ${conflictingPaths.join(", ")}`
|
|
42
|
+
);
|
|
43
|
+
this.name = "GitMergeConflictError";
|
|
44
|
+
this.conflictingPaths = conflictingPaths;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/merge.ts
|
|
49
|
+
import git from "isomorphic-git";
|
|
50
|
+
function defaultAuthor() {
|
|
51
|
+
return {
|
|
52
|
+
name: "Git Edge",
|
|
53
|
+
email: "git-edge@local",
|
|
54
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
55
|
+
timezoneOffset: 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function flattenTreeDeep(repo, treeOid, prefix) {
|
|
59
|
+
const map = /* @__PURE__ */ new Map();
|
|
60
|
+
const entries = (await git.readTree({ ...repo, oid: treeOid })).tree;
|
|
61
|
+
await Promise.all(
|
|
62
|
+
entries.map(async (entry) => {
|
|
63
|
+
const fullPath = prefix ? `${prefix}/${entry.path}` : entry.path;
|
|
64
|
+
if (entry.type === "tree") {
|
|
65
|
+
const sub = await flattenTreeDeep(repo, entry.oid, fullPath);
|
|
66
|
+
for (const [k, v] of sub) map.set(k, v);
|
|
67
|
+
} else {
|
|
68
|
+
map.set(fullPath, entry.oid);
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
return map;
|
|
73
|
+
}
|
|
74
|
+
async function readBlob(repo, oid) {
|
|
75
|
+
const { blob } = await git.readBlob({ ...repo, oid });
|
|
76
|
+
return blob;
|
|
77
|
+
}
|
|
78
|
+
async function writeBlob(repo, content) {
|
|
79
|
+
return git.writeBlob({ ...repo, blob: content });
|
|
80
|
+
}
|
|
81
|
+
async function writeTreeObject(repo, entries) {
|
|
82
|
+
return git.writeObject({
|
|
83
|
+
...repo,
|
|
84
|
+
type: "tree",
|
|
85
|
+
object: entries.map((e) => ({
|
|
86
|
+
path: e.path,
|
|
87
|
+
oid: e.oid,
|
|
88
|
+
mode: e.mode,
|
|
89
|
+
type: e.type
|
|
90
|
+
})),
|
|
91
|
+
format: "parsed"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function writeTreeFromFlat(repo, flat) {
|
|
95
|
+
const root = /* @__PURE__ */ new Map();
|
|
96
|
+
const children = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const [p, v] of flat) {
|
|
98
|
+
const slash = p.indexOf("/");
|
|
99
|
+
if (slash === -1) {
|
|
100
|
+
root.set(p, v);
|
|
101
|
+
} else {
|
|
102
|
+
const dir = p.slice(0, slash);
|
|
103
|
+
const rest = p.slice(slash + 1);
|
|
104
|
+
if (!children.has(dir)) children.set(dir, /* @__PURE__ */ new Map());
|
|
105
|
+
children.get(dir)?.set(rest, v);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const treeEntries = [];
|
|
109
|
+
for (const [name, { oid, mode }] of root) {
|
|
110
|
+
treeEntries.push({ path: name, oid, mode, type: "blob" });
|
|
111
|
+
}
|
|
112
|
+
for (const [dirName, subMap] of children) {
|
|
113
|
+
const subOid = await writeTreeFromFlat(repo, subMap);
|
|
114
|
+
treeEntries.push({
|
|
115
|
+
path: dirName,
|
|
116
|
+
oid: subOid,
|
|
117
|
+
mode: "040000",
|
|
118
|
+
type: "tree"
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return writeTreeObject(repo, treeEntries);
|
|
122
|
+
}
|
|
123
|
+
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);
|
|
163
|
+
}
|
|
164
|
+
function decodeLines(data) {
|
|
165
|
+
const text = new TextDecoder().decode(data);
|
|
166
|
+
if (text === "") return [];
|
|
167
|
+
return text.split("\n");
|
|
168
|
+
}
|
|
169
|
+
function encodeLines(lines) {
|
|
170
|
+
return new TextEncoder().encode(lines.join("\n"));
|
|
171
|
+
}
|
|
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
|
+
async function threeWayMerge(repo, sourceRef, targetRef, opts) {
|
|
201
|
+
const [sourceOid, targetOid] = await Promise.all([
|
|
202
|
+
git.resolveRef({ ...repo, ref: sourceRef }),
|
|
203
|
+
git.resolveRef({ ...repo, ref: targetRef })
|
|
204
|
+
]);
|
|
205
|
+
if (sourceOid === targetOid) {
|
|
206
|
+
return { commitOid: sourceOid };
|
|
207
|
+
}
|
|
208
|
+
const isSourceAhead = await git.isDescendent({
|
|
209
|
+
...repo,
|
|
210
|
+
oid: sourceOid,
|
|
211
|
+
ancestor: targetOid
|
|
212
|
+
});
|
|
213
|
+
if (isSourceAhead) {
|
|
214
|
+
await git.writeRef({
|
|
215
|
+
...repo,
|
|
216
|
+
ref: targetRef,
|
|
217
|
+
value: sourceOid,
|
|
218
|
+
force: true
|
|
219
|
+
});
|
|
220
|
+
return { commitOid: sourceOid };
|
|
221
|
+
}
|
|
222
|
+
const isTargetAhead = await git.isDescendent({
|
|
223
|
+
...repo,
|
|
224
|
+
oid: targetOid,
|
|
225
|
+
ancestor: sourceOid
|
|
226
|
+
});
|
|
227
|
+
if (isTargetAhead) {
|
|
228
|
+
await git.writeRef({
|
|
229
|
+
...repo,
|
|
230
|
+
ref: targetRef,
|
|
231
|
+
value: sourceOid,
|
|
232
|
+
force: true
|
|
233
|
+
});
|
|
234
|
+
return { commitOid: sourceOid };
|
|
235
|
+
}
|
|
236
|
+
const baseOids = await git.findMergeBase({
|
|
237
|
+
...repo,
|
|
238
|
+
oids: [sourceOid, targetOid]
|
|
239
|
+
});
|
|
240
|
+
if (!baseOids || baseOids.length === 0) {
|
|
241
|
+
throw new Error("No merge base found between source and target");
|
|
242
|
+
}
|
|
243
|
+
const baseOid = baseOids[0];
|
|
244
|
+
const [baseCommit, sourceCommit, targetCommit] = await Promise.all([
|
|
245
|
+
git.readCommit({ ...repo, oid: baseOid }),
|
|
246
|
+
git.readCommit({ ...repo, oid: sourceOid }),
|
|
247
|
+
git.readCommit({ ...repo, oid: targetOid })
|
|
248
|
+
]);
|
|
249
|
+
const baseTreeOid = baseCommit.commit.tree;
|
|
250
|
+
const sourceTreeOid = sourceCommit.commit.tree;
|
|
251
|
+
const targetTreeOid = targetCommit.commit.tree;
|
|
252
|
+
const [baseMap, sourceMap, targetMap] = await Promise.all([
|
|
253
|
+
flattenTreeDeep(repo, baseTreeOid, ""),
|
|
254
|
+
flattenTreeDeep(repo, sourceTreeOid, ""),
|
|
255
|
+
flattenTreeDeep(repo, targetTreeOid, "")
|
|
256
|
+
]);
|
|
257
|
+
const allPaths = /* @__PURE__ */ new Set([
|
|
258
|
+
...baseMap.keys(),
|
|
259
|
+
...sourceMap.keys(),
|
|
260
|
+
...targetMap.keys()
|
|
261
|
+
]);
|
|
262
|
+
const resultEntries = /* @__PURE__ */ new Map();
|
|
263
|
+
const conflictingPaths = [];
|
|
264
|
+
for (const filepath of allPaths) {
|
|
265
|
+
const baseOid2 = baseMap.get(filepath);
|
|
266
|
+
const sourceOid2 = sourceMap.get(filepath);
|
|
267
|
+
const targetOid2 = targetMap.get(filepath);
|
|
268
|
+
const _baseExists = baseOid2 !== void 0;
|
|
269
|
+
const sourceExists = sourceOid2 !== void 0;
|
|
270
|
+
const targetExists = targetOid2 !== void 0;
|
|
271
|
+
if (sourceExists && !targetExists) {
|
|
272
|
+
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
273
|
+
} else if (!sourceExists && targetExists) {
|
|
274
|
+
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
275
|
+
} else if (sourceExists && targetExists) {
|
|
276
|
+
const sourceChanged = sourceOid2 !== baseOid2;
|
|
277
|
+
const targetChanged = targetOid2 !== baseOid2;
|
|
278
|
+
if (!sourceChanged && !targetChanged) {
|
|
279
|
+
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
280
|
+
} else if (!sourceChanged) {
|
|
281
|
+
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
282
|
+
} else if (!targetChanged) {
|
|
283
|
+
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
284
|
+
} else {
|
|
285
|
+
const [baseContent, sourceContent, targetContent] = await Promise.all([
|
|
286
|
+
readBlob(repo, baseOid2),
|
|
287
|
+
readBlob(repo, sourceOid2),
|
|
288
|
+
readBlob(repo, targetOid2)
|
|
289
|
+
]);
|
|
290
|
+
if (baseContent.length === sourceContent.length && baseContent.every((b, i) => b === sourceContent[i])) {
|
|
291
|
+
resultEntries.set(filepath, { oid: targetOid2, mode: "100644" });
|
|
292
|
+
} else if (baseContent.length === targetContent.length && baseContent.every((b, i) => b === targetContent[i])) {
|
|
293
|
+
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
294
|
+
} else if (sourceContent.length === targetContent.length && sourceContent.every((b, i) => b === targetContent[i])) {
|
|
295
|
+
resultEntries.set(filepath, { oid: sourceOid2, mode: "100644" });
|
|
296
|
+
} else {
|
|
297
|
+
const merged = mergeContents(
|
|
298
|
+
baseContent,
|
|
299
|
+
sourceContent,
|
|
300
|
+
targetContent
|
|
301
|
+
);
|
|
302
|
+
const mergedText = new TextDecoder().decode(merged);
|
|
303
|
+
if (mergedText.includes("<<<<<<< ours") || mergedText.includes(">>>>>>> theirs")) {
|
|
304
|
+
conflictingPaths.push(filepath);
|
|
305
|
+
const mergedOid = await writeBlob(repo, merged);
|
|
306
|
+
resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
|
|
307
|
+
} else {
|
|
308
|
+
const mergedOid = await writeBlob(repo, merged);
|
|
309
|
+
resultEntries.set(filepath, { oid: mergedOid, mode: "100644" });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (conflictingPaths.length > 0) {
|
|
317
|
+
throw new GitMergeConflictError(conflictingPaths);
|
|
318
|
+
}
|
|
319
|
+
const resultTreeOid = await writeTreeFromFlat(repo, resultEntries);
|
|
320
|
+
const author = opts?.authorName ? {
|
|
321
|
+
name: opts.authorName,
|
|
322
|
+
email: opts.authorEmail || "git-edge@local",
|
|
323
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
324
|
+
timezoneOffset: 0
|
|
325
|
+
} : defaultAuthor();
|
|
326
|
+
const message = opts?.message || `Merge ${sourceRef} into ${targetRef}`;
|
|
327
|
+
const mergeCommitOid = await git.commit({
|
|
328
|
+
...repo,
|
|
329
|
+
message,
|
|
330
|
+
tree: resultTreeOid,
|
|
331
|
+
parent: [targetOid, sourceOid],
|
|
332
|
+
author,
|
|
333
|
+
committer: author
|
|
334
|
+
});
|
|
335
|
+
await git.writeRef({
|
|
336
|
+
...repo,
|
|
337
|
+
ref: targetRef,
|
|
338
|
+
value: mergeCommitOid,
|
|
339
|
+
force: true
|
|
340
|
+
});
|
|
341
|
+
return { commitOid: mergeCommitOid };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/repo.ts
|
|
345
|
+
import git2 from "isomorphic-git";
|
|
346
|
+
var repoCaches = /* @__PURE__ */ new Map();
|
|
347
|
+
function getRepoCache(ownerKey, repoName) {
|
|
348
|
+
const key = `${ownerKey}/${repoName}`;
|
|
349
|
+
let cache = repoCaches.get(key);
|
|
350
|
+
if (!cache) {
|
|
351
|
+
cache = {};
|
|
352
|
+
repoCaches.set(key, cache);
|
|
353
|
+
}
|
|
354
|
+
return cache;
|
|
355
|
+
}
|
|
356
|
+
function invalidateRepoCache(ownerKey, repoName) {
|
|
357
|
+
repoCaches.delete(`${ownerKey}/${repoName}`);
|
|
358
|
+
}
|
|
359
|
+
async function initBareRepo(repo, defaultBranch = "main") {
|
|
360
|
+
await git2.init({
|
|
361
|
+
...repo,
|
|
362
|
+
dir: repo.gitdir,
|
|
363
|
+
defaultBranch,
|
|
364
|
+
bare: true
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async function estimateRepoSize(repo, stat, list) {
|
|
368
|
+
let total = 0;
|
|
369
|
+
async function walk(dir) {
|
|
370
|
+
let entries;
|
|
371
|
+
try {
|
|
372
|
+
entries = await list(dir);
|
|
373
|
+
} catch {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const sizes = await Promise.all(
|
|
377
|
+
entries.map(async (name) => {
|
|
378
|
+
const fullPath = `${dir}/${name}`;
|
|
379
|
+
try {
|
|
380
|
+
const s = await stat(fullPath);
|
|
381
|
+
return s.size;
|
|
382
|
+
} catch {
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
for (const size of sizes) total += size;
|
|
388
|
+
}
|
|
389
|
+
await walk(`${repo.gitdir}/objects`);
|
|
390
|
+
return total;
|
|
391
|
+
}
|
|
392
|
+
export {
|
|
393
|
+
GitEdgeError,
|
|
394
|
+
GitMergeConflictError,
|
|
395
|
+
analyzeMerge,
|
|
396
|
+
createParsedObjectCache,
|
|
397
|
+
estimateRepoSize,
|
|
398
|
+
getRepoCache,
|
|
399
|
+
initBareRepo,
|
|
400
|
+
invalidateRepoCache,
|
|
401
|
+
threeWayMerge
|
|
402
|
+
};
|
|
403
|
+
//# sourceMappingURL=index.js.map
|