git-fs-s3 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/dist/{chunk-XNSZTA4Y.js → chunk-HBOJR5D3.js} +108 -5
- package/dist/chunk-HBOJR5D3.js.map +1 -0
- package/dist/{chunk-FIFWOMPM.js → chunk-PASVUPKR.js} +64 -2
- package/dist/chunk-PASVUPKR.js.map +1 -0
- package/dist/chunk-ZB6GNJ2J.js +59 -0
- package/dist/chunk-ZB6GNJ2J.js.map +1 -0
- package/dist/http.js +299 -192
- package/dist/http.js.map +1 -1
- package/dist/index.js +79 -76
- package/dist/index.js.map +1 -1
- package/dist/ops.js +282 -253
- package/dist/ops.js.map +1 -1
- package/dist/types/edge-utils.d.ts +2 -0
- package/dist/types/errors.d.ts +0 -1
- package/dist/types/http/index.d.ts +2 -2
- package/dist/types/http/pack-reuse.d.ts +2 -1
- package/dist/types/http/reachability.d.ts +12 -0
- package/dist/types/http/receive-pack.d.ts +28 -9
- package/dist/types/http/upload-pack.d.ts +19 -3
- package/dist/types/index.d.ts +1 -1
- package/dist/types/ops/commit.d.ts +9 -1
- package/dist/types/ops/diff.d.ts +7 -3
- package/dist/types/ops/file-history.d.ts +4 -18
- package/dist/types/ops/history.d.ts +30 -43
- package/dist/types/ops/index.d.ts +4 -3
- package/dist/types/ops/last-commit.d.ts +2 -8
- package/dist/types/ops/packed-writes.d.ts +9 -0
- package/dist/types/ops/tree.d.ts +9 -1
- package/dist/types/ops/types.d.ts +10 -16
- package/dist/types/pack-write.d.ts +26 -0
- package/dist/types/refs.d.ts +9 -0
- package/package.json +2 -2
- package/dist/chunk-FIFWOMPM.js.map +0 -1
- package/dist/chunk-RMAOPWHJ.js +0 -38
- package/dist/chunk-RMAOPWHJ.js.map +0 -1
- package/dist/chunk-XNSZTA4Y.js.map +0 -1
package/dist/ops.js
CHANGED
|
@@ -1,28 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseLooseObjectPath
|
|
3
|
+
} from "./chunk-ZB6GNJ2J.js";
|
|
1
4
|
import {
|
|
2
5
|
assertBranchExists,
|
|
3
6
|
assertSafeBranchName,
|
|
4
7
|
createBranchFrom,
|
|
5
8
|
deleteBranchByName,
|
|
9
|
+
encodePack,
|
|
6
10
|
listBranches,
|
|
7
|
-
resolveLooseRefFast
|
|
8
|
-
|
|
11
|
+
resolveLooseRefFast,
|
|
12
|
+
storePack
|
|
13
|
+
} from "./chunk-HBOJR5D3.js";
|
|
9
14
|
import {
|
|
15
|
+
GitInvalidRequestError,
|
|
10
16
|
GitObjectNotFoundError,
|
|
11
17
|
GitPathNotFoundError,
|
|
12
18
|
GitRefNotFoundError,
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
inflate,
|
|
20
|
+
isSafeTreePath,
|
|
15
21
|
qualifyBranchRef,
|
|
16
|
-
readBlobContent
|
|
17
|
-
|
|
18
|
-
} from "./chunk-FIFWOMPM.js";
|
|
22
|
+
readBlobContent
|
|
23
|
+
} from "./chunk-PASVUPKR.js";
|
|
19
24
|
|
|
20
25
|
// src/ops/commit.ts
|
|
21
26
|
import git2 from "isomorphic-git";
|
|
22
27
|
|
|
28
|
+
// src/ops/packed-writes.ts
|
|
29
|
+
var OBJECT_TYPES = /* @__PURE__ */ new Set(["commit", "tree", "blob", "tag"]);
|
|
30
|
+
function parseLooseObject(raw) {
|
|
31
|
+
const nul = raw.indexOf(0);
|
|
32
|
+
const header = new TextDecoder().decode(raw.subarray(0, nul));
|
|
33
|
+
const [type, size] = header.split(" ");
|
|
34
|
+
const content = raw.slice(nul + 1);
|
|
35
|
+
if (!OBJECT_TYPES.has(type) || Number(size) !== content.length) {
|
|
36
|
+
throw new Error(`packed writes: malformed object header "${header}"`);
|
|
37
|
+
}
|
|
38
|
+
return { type, content };
|
|
39
|
+
}
|
|
40
|
+
function fileStat(size) {
|
|
41
|
+
const epoch = /* @__PURE__ */ new Date(0);
|
|
42
|
+
return {
|
|
43
|
+
type: "file",
|
|
44
|
+
mode: 33188,
|
|
45
|
+
size,
|
|
46
|
+
ino: 0,
|
|
47
|
+
mtimeMs: 0,
|
|
48
|
+
ctimeMs: 0,
|
|
49
|
+
uid: 0,
|
|
50
|
+
gid: 0,
|
|
51
|
+
dev: 0,
|
|
52
|
+
mtime: epoch,
|
|
53
|
+
ctime: epoch,
|
|
54
|
+
isFile: () => true,
|
|
55
|
+
isDirectory: () => false,
|
|
56
|
+
isSymbolicLink: () => false
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function withPackedWrites(repo, fn) {
|
|
60
|
+
const base = repo.fs.promises;
|
|
61
|
+
const pending = /* @__PURE__ */ new Map();
|
|
62
|
+
async function flush() {
|
|
63
|
+
if (pending.size === 0) return;
|
|
64
|
+
const objects = await Promise.all(
|
|
65
|
+
[...pending.values()].map(
|
|
66
|
+
async (deflated) => parseLooseObject(await inflate(deflated))
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
await storePack(repo, await encodePack(objects));
|
|
70
|
+
pending.clear();
|
|
71
|
+
}
|
|
72
|
+
const promises = new Proxy(base, {
|
|
73
|
+
get(target, property, receiver) {
|
|
74
|
+
switch (property) {
|
|
75
|
+
case "writeFile":
|
|
76
|
+
return async (path, data, options) => {
|
|
77
|
+
if (parseLooseObjectPath(path) !== null && typeof data !== "string") {
|
|
78
|
+
pending.set(path, new Uint8Array(data));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await flush();
|
|
82
|
+
return target.writeFile(path, data, options);
|
|
83
|
+
};
|
|
84
|
+
case "readFile":
|
|
85
|
+
return async (path, options) => {
|
|
86
|
+
const data = pending.get(path);
|
|
87
|
+
if (data === void 0) return target.readFile(path, options);
|
|
88
|
+
return data;
|
|
89
|
+
};
|
|
90
|
+
case "stat":
|
|
91
|
+
case "lstat":
|
|
92
|
+
return async (path) => {
|
|
93
|
+
const data = pending.get(path);
|
|
94
|
+
if (data !== void 0) return fileStat(data.length);
|
|
95
|
+
return target[property](path);
|
|
96
|
+
};
|
|
97
|
+
default: {
|
|
98
|
+
const value = Reflect.get(target, property, receiver);
|
|
99
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const packedRepo = { ...repo, fs: { promises } };
|
|
105
|
+
const result = await fn(packedRepo);
|
|
106
|
+
await flush();
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
23
110
|
// src/ops/tree.ts
|
|
24
111
|
import git from "isomorphic-git";
|
|
25
112
|
var joinPath = (prefix, name) => prefix ? `${prefix.replace(/\/+$/, "")}/${name}` : name;
|
|
113
|
+
var BLOB_MODES = /* @__PURE__ */ new Set(["100644", "100755", "120000"]);
|
|
26
114
|
async function upsertTree(repo, treeOid, entries) {
|
|
27
115
|
const existing = treeOid ? (await git.readTree({ ...repo, oid: treeOid })).tree : [];
|
|
28
116
|
const byName = new Map(existing.map((e) => [e.path, e]));
|
|
@@ -40,8 +128,12 @@ async function upsertTree(repo, treeOid, entries) {
|
|
|
40
128
|
}
|
|
41
129
|
}
|
|
42
130
|
for (const [name, blobOid] of direct) {
|
|
131
|
+
const current = byName.get(name);
|
|
132
|
+
if (current && current.type !== "blob") {
|
|
133
|
+
throw new GitInvalidRequestError(`"${name}" is a directory, not a file`);
|
|
134
|
+
}
|
|
43
135
|
byName.set(name, {
|
|
44
|
-
mode: "100644",
|
|
136
|
+
mode: current && BLOB_MODES.has(current.mode) ? current.mode : "100644",
|
|
45
137
|
path: name,
|
|
46
138
|
oid: blobOid,
|
|
47
139
|
type: "blob"
|
|
@@ -50,7 +142,10 @@ async function upsertTree(repo, treeOid, entries) {
|
|
|
50
142
|
const nestedResults = await Promise.all(
|
|
51
143
|
Array.from(nested, async ([dir, subEntries]) => {
|
|
52
144
|
const entry = byName.get(dir);
|
|
53
|
-
|
|
145
|
+
if (entry && entry.type !== "tree") {
|
|
146
|
+
throw new GitInvalidRequestError(`"${dir}" is a file, not a directory`);
|
|
147
|
+
}
|
|
148
|
+
const subtreeOid = entry?.oid;
|
|
54
149
|
const newOid = await upsertTree(repo, subtreeOid, subEntries);
|
|
55
150
|
return [dir, newOid];
|
|
56
151
|
})
|
|
@@ -61,20 +156,34 @@ async function upsertTree(repo, treeOid, entries) {
|
|
|
61
156
|
return git.writeTree({ ...repo, tree: Array.from(byName.values()) });
|
|
62
157
|
}
|
|
63
158
|
async function deleteFromTree(repo, treeOid, filePath) {
|
|
159
|
+
const result = await deleteFromSubtree(repo, treeOid, filePath, filePath);
|
|
160
|
+
return result ?? git.writeTree({ ...repo, tree: [] });
|
|
161
|
+
}
|
|
162
|
+
async function deleteFromSubtree(repo, treeOid, filePath, fullPath) {
|
|
64
163
|
const existing = (await git.readTree({ ...repo, oid: treeOid })).tree;
|
|
65
164
|
const byName = new Map(existing.map((e) => [e.path, e]));
|
|
66
165
|
const slash = filePath.indexOf("/");
|
|
166
|
+
const name = slash === -1 ? filePath : filePath.slice(0, slash);
|
|
167
|
+
const entry = byName.get(name);
|
|
67
168
|
if (slash === -1) {
|
|
68
|
-
|
|
169
|
+
if (entry?.type !== "blob") {
|
|
170
|
+
throw new GitPathNotFoundError(`No such file: ${fullPath}`);
|
|
171
|
+
}
|
|
172
|
+
byName.delete(name);
|
|
69
173
|
} else {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const entry = byName.get(dir);
|
|
73
|
-
if (entry?.type === "tree") {
|
|
74
|
-
const newOid = await deleteFromTree(repo, entry.oid, rest);
|
|
75
|
-
byName.set(dir, { ...entry, oid: newOid });
|
|
174
|
+
if (entry?.type !== "tree") {
|
|
175
|
+
throw new GitPathNotFoundError(`No such file: ${fullPath}`);
|
|
76
176
|
}
|
|
177
|
+
const newOid = await deleteFromSubtree(
|
|
178
|
+
repo,
|
|
179
|
+
entry.oid,
|
|
180
|
+
filePath.slice(slash + 1),
|
|
181
|
+
fullPath
|
|
182
|
+
);
|
|
183
|
+
if (newOid === null) byName.delete(name);
|
|
184
|
+
else byName.set(name, { ...entry, oid: newOid });
|
|
77
185
|
}
|
|
186
|
+
if (byName.size === 0) return null;
|
|
78
187
|
return git.writeTree({ ...repo, tree: Array.from(byName.values()) });
|
|
79
188
|
}
|
|
80
189
|
async function findTreeEntry(repo, rootTreeOid, treePath) {
|
|
@@ -123,6 +232,9 @@ function authorNow(name, email) {
|
|
|
123
232
|
}
|
|
124
233
|
async function writeCommitToBare(repo, options) {
|
|
125
234
|
assertSafeBranchName(options.branch);
|
|
235
|
+
return withPackedWrites(repo, (packed) => writeCommit(packed, options));
|
|
236
|
+
}
|
|
237
|
+
async function writeCommit(repo, options) {
|
|
126
238
|
let parentOid;
|
|
127
239
|
let parentTreeOid;
|
|
128
240
|
try {
|
|
@@ -137,7 +249,7 @@ async function writeCommitToBare(repo, options) {
|
|
|
137
249
|
throw err;
|
|
138
250
|
}
|
|
139
251
|
}
|
|
140
|
-
const treeOid = await options.buildTree(parentTreeOid);
|
|
252
|
+
const treeOid = await options.buildTree(parentTreeOid, repo);
|
|
141
253
|
const commitOid = await git2.writeCommit({
|
|
142
254
|
...repo,
|
|
143
255
|
commit: {
|
|
@@ -156,34 +268,50 @@ async function writeCommitToBare(repo, options) {
|
|
|
156
268
|
});
|
|
157
269
|
return commitOid;
|
|
158
270
|
}
|
|
159
|
-
function commitFilesToBare(repo, options) {
|
|
271
|
+
async function commitFilesToBare(repo, options) {
|
|
272
|
+
const paths = /* @__PURE__ */ new Set();
|
|
273
|
+
for (const file of options.files) {
|
|
274
|
+
if (!isSafeTreePath(file.path)) {
|
|
275
|
+
throw new GitInvalidRequestError(`Invalid file path: ${file.path}`);
|
|
276
|
+
}
|
|
277
|
+
if (paths.has(file.path)) {
|
|
278
|
+
throw new GitInvalidRequestError(`Duplicate file path: ${file.path}`);
|
|
279
|
+
}
|
|
280
|
+
paths.add(file.path);
|
|
281
|
+
}
|
|
282
|
+
if (paths.size === 0) {
|
|
283
|
+
throw new GitInvalidRequestError("A commit needs at least one file");
|
|
284
|
+
}
|
|
160
285
|
return writeCommitToBare(repo, {
|
|
161
286
|
branch: options.branch,
|
|
162
287
|
message: options.message,
|
|
163
288
|
author: options.author,
|
|
164
|
-
buildTree: async (parentTreeOid) => {
|
|
289
|
+
buildTree: async (parentTreeOid, writeRepo) => {
|
|
165
290
|
const blobs = /* @__PURE__ */ new Map();
|
|
166
291
|
await Promise.all(
|
|
167
292
|
options.files.map(async (file) => {
|
|
168
293
|
const content = typeof file.content === "string" ? new TextEncoder().encode(file.content) : file.content;
|
|
169
|
-
const oid = await git2.writeBlob({ ...
|
|
294
|
+
const oid = await git2.writeBlob({ ...writeRepo, blob: content });
|
|
170
295
|
blobs.set(file.path, oid);
|
|
171
296
|
})
|
|
172
297
|
);
|
|
173
|
-
return upsertTree(
|
|
298
|
+
return upsertTree(writeRepo, parentTreeOid, blobs);
|
|
174
299
|
}
|
|
175
300
|
});
|
|
176
301
|
}
|
|
177
|
-
function deleteFileFromBare(repo, options) {
|
|
302
|
+
async function deleteFileFromBare(repo, options) {
|
|
303
|
+
if (!isSafeTreePath(options.filePath)) {
|
|
304
|
+
throw new GitInvalidRequestError(`Invalid file path: ${options.filePath}`);
|
|
305
|
+
}
|
|
178
306
|
return writeCommitToBare(repo, {
|
|
179
307
|
branch: options.branch,
|
|
180
308
|
message: options.message,
|
|
181
309
|
author: options.author,
|
|
182
|
-
buildTree: async (parentTreeOid) => {
|
|
310
|
+
buildTree: async (parentTreeOid, writeRepo) => {
|
|
183
311
|
if (!parentTreeOid) {
|
|
184
|
-
throw new
|
|
312
|
+
throw new GitPathNotFoundError(`Branch ${options.branch} is empty`);
|
|
185
313
|
}
|
|
186
|
-
return deleteFromTree(
|
|
314
|
+
return deleteFromTree(writeRepo, parentTreeOid, options.filePath);
|
|
187
315
|
}
|
|
188
316
|
});
|
|
189
317
|
}
|
|
@@ -201,36 +329,49 @@ function resultKeyPrefixes(gitdir) {
|
|
|
201
329
|
return [
|
|
202
330
|
`commitlog:${gitdir}:`,
|
|
203
331
|
`tree:${gitdir}:`,
|
|
204
|
-
`commits:${gitdir}:`,
|
|
205
332
|
`last-commits:${gitdir}:`,
|
|
206
333
|
`file-history:${gitdir}:`
|
|
207
334
|
];
|
|
208
335
|
}
|
|
209
336
|
|
|
210
337
|
// src/ops/history.ts
|
|
338
|
+
var HISTORY_WALK_DEPTH = 400;
|
|
339
|
+
function summarizeCommit(commit) {
|
|
340
|
+
return {
|
|
341
|
+
sha: commit.oid,
|
|
342
|
+
message: commit.commit.message.trim(),
|
|
343
|
+
authorName: commit.commit.author.name,
|
|
344
|
+
authorEmail: commit.commit.author.email,
|
|
345
|
+
createdAt: new Date(commit.commit.author.timestamp * 1e3).toISOString()
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
var isNotFound = (err) => err?.code === "NotFoundError";
|
|
211
349
|
function wrapMissingObject(promise, context) {
|
|
212
350
|
return promise.catch((err) => {
|
|
213
|
-
if (err
|
|
351
|
+
if (isNotFound(err)) {
|
|
214
352
|
throw new GitObjectNotFoundError(
|
|
215
|
-
`Git data for ${context} is missing from storage
|
|
353
|
+
`Git data for ${context} is missing from storage`
|
|
216
354
|
);
|
|
217
355
|
}
|
|
218
356
|
throw err;
|
|
219
357
|
});
|
|
220
358
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
359
|
+
async function resolveRefOid(repo, ref) {
|
|
360
|
+
try {
|
|
361
|
+
return await git3.resolveRef({ ...repo, ref: qualifyBranchRef(ref) });
|
|
362
|
+
} catch (err) {
|
|
363
|
+
if (isNotFound(err))
|
|
225
364
|
throw new GitRefNotFoundError(`Ref ${ref} does not exist`);
|
|
226
|
-
}
|
|
227
365
|
throw err;
|
|
228
|
-
}
|
|
229
|
-
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function resolveCommit(repo, ref) {
|
|
369
|
+
const oid = await resolveRefOid(repo, ref);
|
|
370
|
+
const { commit } = await wrapMissingObject(
|
|
230
371
|
git3.readCommit({ ...repo, oid }),
|
|
231
372
|
`${repo.gitdir} commit ${oid}`
|
|
232
373
|
);
|
|
233
|
-
return { oid, commit
|
|
374
|
+
return { oid, commit };
|
|
234
375
|
}
|
|
235
376
|
async function getBlob(repo, sha) {
|
|
236
377
|
const { blob } = await wrapMissingObject(
|
|
@@ -240,9 +381,7 @@ async function getBlob(repo, sha) {
|
|
|
240
381
|
return blob;
|
|
241
382
|
}
|
|
242
383
|
async function getFileContent(repo, filePath, ref, hooks) {
|
|
243
|
-
if (hooks?.prefetch)
|
|
244
|
-
await runStep(hooks, "prefetch", hooks.prefetch);
|
|
245
|
-
}
|
|
384
|
+
if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
|
|
246
385
|
const { commit } = await resolveCommit(repo, ref);
|
|
247
386
|
const context = `${repo.gitdir}@${ref}:${filePath}`;
|
|
248
387
|
const entry = await wrapMissingObject(
|
|
@@ -265,39 +404,28 @@ async function getCommit(repo, sha) {
|
|
|
265
404
|
);
|
|
266
405
|
return { oid: result.oid, commit: result.commit, payload: result.payload };
|
|
267
406
|
}
|
|
268
|
-
function isFullyWalked(commits) {
|
|
269
|
-
const last = commits[commits.length - 1];
|
|
270
|
-
return !!last && last.commit.parent.length === 0;
|
|
271
|
-
}
|
|
272
407
|
async function getCommitLog(repo, options, hooks) {
|
|
273
408
|
const { ref } = options;
|
|
274
409
|
const depth = options.depth ?? 50;
|
|
275
|
-
let headSha;
|
|
276
|
-
if (
|
|
277
|
-
headSha = options.knownHeadSha;
|
|
278
|
-
} else {
|
|
410
|
+
let headSha = options.knownHeadSha;
|
|
411
|
+
if (headSha === void 0) {
|
|
279
412
|
try {
|
|
280
|
-
headSha = await
|
|
413
|
+
headSha = await resolveRefOid(repo, ref);
|
|
281
414
|
} catch (err) {
|
|
282
|
-
if (
|
|
415
|
+
if (err instanceof GitRefNotFoundError) return [];
|
|
283
416
|
throw err;
|
|
284
417
|
}
|
|
285
418
|
}
|
|
286
419
|
const cacheKey = `commitlog:${repo.gitdir}:${headSha}`;
|
|
287
420
|
const cached = hooks?.resultCache?.get(cacheKey);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
);
|
|
421
|
+
const fullyWalked = cached?.at(-1)?.commit.parent.length === 0;
|
|
422
|
+
if (cached && (cached.length >= depth || fullyWalked)) {
|
|
423
|
+
hooks?.onNote?.(`getCommitLog: cache hit ${cacheKey} (depth=${depth})`);
|
|
292
424
|
return cached.slice(0, depth);
|
|
293
425
|
}
|
|
294
|
-
hooks?.onNote?.(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (hooks?.prefetch && depth >= (hooks.prefetchMinDepth ?? 5)) {
|
|
298
|
-
await runStep(hooks, "prefetch", hooks.prefetch);
|
|
299
|
-
}
|
|
300
|
-
const commits = await wrapMissingObject(
|
|
426
|
+
hooks?.onNote?.(`getCommitLog: cache miss ${cacheKey} (depth=${depth})`);
|
|
427
|
+
if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
|
|
428
|
+
const log = await wrapMissingObject(
|
|
301
429
|
runStep(
|
|
302
430
|
hooks,
|
|
303
431
|
`git.log ${ref} depth=${depth}`,
|
|
@@ -305,25 +433,16 @@ async function getCommitLog(repo, options, hooks) {
|
|
|
305
433
|
),
|
|
306
434
|
`${repo.gitdir}@${ref} history`
|
|
307
435
|
);
|
|
308
|
-
const result =
|
|
309
|
-
oid
|
|
310
|
-
commit
|
|
311
|
-
payload:
|
|
436
|
+
const result = log.map(({ oid, commit, payload }) => ({
|
|
437
|
+
oid,
|
|
438
|
+
commit,
|
|
439
|
+
payload: payload ?? ""
|
|
312
440
|
}));
|
|
313
441
|
if (!cached || result.length > cached.length) {
|
|
314
442
|
hooks?.resultCache?.set(cacheKey, result);
|
|
315
443
|
}
|
|
316
444
|
return result;
|
|
317
445
|
}
|
|
318
|
-
async function getFileFromRef(repo, filePath, ref, hooks) {
|
|
319
|
-
const bytes = await getFileContent(repo, filePath, ref, hooks);
|
|
320
|
-
const isBinary = hasNullByte(bytes);
|
|
321
|
-
return {
|
|
322
|
-
content: isBinary ? toBase64(bytes) : decodeUtf8(bytes),
|
|
323
|
-
size: bytes.length,
|
|
324
|
-
isBinary
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
446
|
async function getTreeFromRef(repo, options, hooks) {
|
|
328
447
|
const { ref } = options;
|
|
329
448
|
const treePath = options.treePath ?? "";
|
|
@@ -331,227 +450,143 @@ async function getTreeFromRef(repo, options, hooks) {
|
|
|
331
450
|
const cacheKey = `tree:${repo.gitdir}:${headSha}:${treePath}`;
|
|
332
451
|
const cached = hooks?.resultCache?.get(cacheKey);
|
|
333
452
|
if (cached) {
|
|
334
|
-
hooks?.onNote?.(`getTreeFromRef:
|
|
453
|
+
hooks?.onNote?.(`getTreeFromRef: cache hit ${cacheKey}`);
|
|
335
454
|
return cached;
|
|
336
455
|
}
|
|
337
|
-
hooks?.onNote?.(`getTreeFromRef:
|
|
338
|
-
if (hooks?.prefetch)
|
|
339
|
-
await runStep(hooks, "prefetch", hooks.prefetch);
|
|
340
|
-
}
|
|
456
|
+
hooks?.onNote?.(`getTreeFromRef: cache miss ${cacheKey}`);
|
|
457
|
+
if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
|
|
341
458
|
const context = `${repo.gitdir}@${ref}:${treePath || "/"}`;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const entry = await wrapMissingObject(
|
|
354
|
-
runStep(
|
|
355
|
-
hooks,
|
|
356
|
-
`findTreeEntry ${treePath}`,
|
|
357
|
-
() => findTreeEntry(repo, commit.tree, treePath)
|
|
358
|
-
),
|
|
359
|
-
context
|
|
360
|
-
);
|
|
361
|
-
if (!entry) {
|
|
362
|
-
throw new GitPathNotFoundError(
|
|
363
|
-
`Path "${treePath}" does not exist at ${ref}`
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
result = entry.type !== "tree" ? [] : await wrapMissingObject(
|
|
367
|
-
runStep(
|
|
368
|
-
hooks,
|
|
369
|
-
`listTreeEntries ${treePath}`,
|
|
370
|
-
() => listTreeEntries(repo, entry.oid, entry.path)
|
|
371
|
-
),
|
|
372
|
-
context
|
|
459
|
+
const dir = treePath ? await wrapMissingObject(
|
|
460
|
+
runStep(
|
|
461
|
+
hooks,
|
|
462
|
+
`findTreeEntry ${treePath}`,
|
|
463
|
+
() => findTreeEntry(repo, commit.tree, treePath)
|
|
464
|
+
),
|
|
465
|
+
context
|
|
466
|
+
) : { path: "", type: "tree", oid: commit.tree };
|
|
467
|
+
if (!dir) {
|
|
468
|
+
throw new GitPathNotFoundError(
|
|
469
|
+
`Path "${treePath}" does not exist at ${ref}`
|
|
373
470
|
);
|
|
374
471
|
}
|
|
472
|
+
const result = dir.type === "tree" ? await wrapMissingObject(
|
|
473
|
+
runStep(
|
|
474
|
+
hooks,
|
|
475
|
+
`listTreeEntries ${treePath || "/"}`,
|
|
476
|
+
() => listTreeEntries(repo, dir.oid, dir.path)
|
|
477
|
+
),
|
|
478
|
+
context
|
|
479
|
+
) : [];
|
|
375
480
|
hooks?.resultCache?.set(cacheKey, result);
|
|
376
481
|
return result;
|
|
377
482
|
}
|
|
378
483
|
async function getCommitHistory(repo, options, hooks) {
|
|
379
484
|
const limit = options.limit ?? 50;
|
|
380
485
|
const skip = options.skip ?? 0;
|
|
381
|
-
const
|
|
382
|
-
const cacheKey = headSha ? `commits:${repo.gitdir}:${headSha}:${limit}:${skip}` : null;
|
|
383
|
-
if (cacheKey) {
|
|
384
|
-
const cached = hooks?.resultCache?.get(cacheKey);
|
|
385
|
-
if (cached) {
|
|
386
|
-
hooks?.onNote?.(`getCommitHistory: result-cache HIT for ${cacheKey}`);
|
|
387
|
-
return cached;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
hooks?.onNote?.(
|
|
391
|
-
`getCommitHistory: result-cache MISS for ${cacheKey ?? "(no head)"}`
|
|
392
|
-
);
|
|
393
|
-
if (!headSha) return [];
|
|
394
|
-
const all = await getCommitLog(
|
|
486
|
+
const log = await getCommitLog(
|
|
395
487
|
repo,
|
|
396
|
-
{ ref: options.ref, depth: limit + skip
|
|
488
|
+
{ ref: options.ref, depth: limit + skip },
|
|
397
489
|
hooks
|
|
398
490
|
);
|
|
399
|
-
|
|
400
|
-
if (cacheKey) hooks?.resultCache?.set(cacheKey, result);
|
|
401
|
-
return result;
|
|
491
|
+
return log.slice(skip, skip + limit);
|
|
402
492
|
}
|
|
403
493
|
|
|
404
494
|
// src/ops/diff.ts
|
|
405
495
|
var MAX_DIFF_TEXT_BYTES = 1024 * 1024;
|
|
406
|
-
function detectBlobContent(blob) {
|
|
407
|
-
return readBlobContent(blob);
|
|
408
|
-
}
|
|
409
496
|
function countContentLines(content) {
|
|
410
497
|
if (content.length === 0) return 0;
|
|
411
498
|
const lines = content.split("\n");
|
|
412
|
-
if (lines
|
|
499
|
+
if (lines.at(-1) === "") lines.pop();
|
|
413
500
|
return lines.length;
|
|
414
501
|
}
|
|
415
|
-
function
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
params.before,
|
|
422
|
-
params.after,
|
|
502
|
+
function unifiedPatch(path, before, after) {
|
|
503
|
+
const body = createTwoFilesPatch(
|
|
504
|
+
before ? `a/${path}` : "/dev/null",
|
|
505
|
+
after ? `b/${path}` : "/dev/null",
|
|
506
|
+
before?.text ?? "",
|
|
507
|
+
after?.text ?? "",
|
|
423
508
|
"",
|
|
424
509
|
"",
|
|
425
510
|
{ context: 3 }
|
|
426
511
|
).replace(/^=+\n/, "");
|
|
427
|
-
return `diff --git a/${
|
|
428
|
-
${
|
|
429
|
-
}
|
|
430
|
-
function summarizeDiff(files) {
|
|
431
|
-
return {
|
|
432
|
-
files,
|
|
433
|
-
totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
|
|
434
|
-
totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
|
|
435
|
-
totalFiles: files.length
|
|
436
|
-
};
|
|
512
|
+
return `diff --git a/${path} b/${path}
|
|
513
|
+
${body}`;
|
|
437
514
|
}
|
|
438
|
-
function buildDiffFile(path,
|
|
515
|
+
function buildDiffFile(path, before, after) {
|
|
439
516
|
const kind = before?.isBinary || after?.isBinary ? "binary" : Math.max(before?.bytes.length ?? 0, after?.bytes.length ?? 0) > MAX_DIFF_TEXT_BYTES ? "too-large" : "text";
|
|
440
517
|
const isText = kind === "text";
|
|
441
518
|
return {
|
|
442
519
|
path,
|
|
443
|
-
status,
|
|
520
|
+
status: !before ? "added" : !after ? "deleted" : "modified",
|
|
444
521
|
kind,
|
|
445
522
|
additions: isText && after ? countContentLines(after.text) : 0,
|
|
446
523
|
deletions: isText && before ? countContentLines(before.text) : 0,
|
|
447
|
-
patch: isText ?
|
|
448
|
-
path,
|
|
449
|
-
before: before?.text ?? "",
|
|
450
|
-
after: after?.text ?? "",
|
|
451
|
-
oldPath: before ? void 0 : "/dev/null",
|
|
452
|
-
newPath: after ? void 0 : "/dev/null"
|
|
453
|
-
}) : "",
|
|
524
|
+
patch: isText ? unifiedPatch(path, before, after) : "",
|
|
454
525
|
oldSize: before?.bytes.length,
|
|
455
526
|
newSize: after?.bytes.length
|
|
456
527
|
};
|
|
457
528
|
}
|
|
458
|
-
async function
|
|
529
|
+
async function diffTrees(repo, oldTreeOid, newTreeOid) {
|
|
530
|
+
const readSide = async (entry) => {
|
|
531
|
+
if (!entry || await entry.type() !== "blob") return null;
|
|
532
|
+
const { blob } = await git4.readBlob({ ...repo, oid: await entry.oid() });
|
|
533
|
+
return readBlobContent(blob);
|
|
534
|
+
};
|
|
535
|
+
const trees = oldTreeOid ? [git4.TREE({ ref: oldTreeOid }), git4.TREE({ ref: newTreeOid })] : [git4.TREE({ ref: newTreeOid })];
|
|
459
536
|
const changes = await git4.walk({
|
|
460
537
|
...repo,
|
|
461
|
-
trees
|
|
462
|
-
map: async (filepath,
|
|
463
|
-
const [
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const { blob } = await git4.readBlob({ ...repo, oid: oidA2 });
|
|
468
|
-
const before = detectBlobContent(blob);
|
|
469
|
-
return buildDiffFile(filepath, "deleted", before, null);
|
|
470
|
-
}
|
|
471
|
-
if (!typeA && typeB) {
|
|
472
|
-
const oidB2 = B ? await B.oid() : "";
|
|
473
|
-
const { blob } = await git4.readBlob({ ...repo, oid: oidB2 });
|
|
474
|
-
const after = detectBlobContent(blob);
|
|
475
|
-
return buildDiffFile(filepath, "added", null, after);
|
|
476
|
-
}
|
|
477
|
-
const [oidA, oidB] = await Promise.all([
|
|
478
|
-
A ? A.oid() : Promise.resolve(""),
|
|
479
|
-
B ? B.oid() : Promise.resolve("")
|
|
538
|
+
trees,
|
|
539
|
+
map: async (filepath, entries) => {
|
|
540
|
+
const [oldEntry, newEntry] = oldTreeOid ? [entries[0] ?? null, entries[1] ?? null] : [null, entries[0] ?? null];
|
|
541
|
+
const [oldType, newType] = await Promise.all([
|
|
542
|
+
oldEntry?.type(),
|
|
543
|
+
newEntry?.type()
|
|
480
544
|
]);
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
545
|
+
if (oldType !== "blob" && newType !== "blob") return void 0;
|
|
546
|
+
if (oldEntry && newEntry && oldType === newType) {
|
|
547
|
+
const [oldOid, newOid] = await Promise.all([
|
|
548
|
+
oldEntry.oid(),
|
|
549
|
+
newEntry.oid()
|
|
485
550
|
]);
|
|
486
|
-
|
|
487
|
-
const after = detectBlobContent(blobB);
|
|
488
|
-
return buildDiffFile(filepath, "modified", before, after);
|
|
551
|
+
if (oldOid === newOid) return void 0;
|
|
489
552
|
}
|
|
490
|
-
|
|
553
|
+
const [before, after] = await Promise.all([
|
|
554
|
+
readSide(oldEntry),
|
|
555
|
+
readSide(newEntry)
|
|
556
|
+
]);
|
|
557
|
+
return before || after ? buildDiffFile(filepath, before, after) : void 0;
|
|
491
558
|
}
|
|
492
559
|
});
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
560
|
+
const files = (changes ?? []).filter((file) => !!file);
|
|
561
|
+
return {
|
|
562
|
+
files,
|
|
563
|
+
totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
|
|
564
|
+
totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
|
|
565
|
+
totalFiles: files.length
|
|
566
|
+
};
|
|
496
567
|
}
|
|
497
568
|
async function getCommitDiff(repo, commitSha) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const entries = [];
|
|
503
|
-
const stack = [
|
|
504
|
-
{ treeOid: commit.commit.tree, prefix: "" }
|
|
505
|
-
];
|
|
506
|
-
while (stack.length) {
|
|
507
|
-
const { treeOid, prefix } = stack.pop();
|
|
508
|
-
const { tree } = await git4.readTree({ ...repo, oid: treeOid });
|
|
509
|
-
for (const entry of tree) {
|
|
510
|
-
const full = prefix ? `${prefix}/${entry.path}` : entry.path;
|
|
511
|
-
if (entry.type === "tree") {
|
|
512
|
-
stack.push({ treeOid: entry.oid, prefix: full });
|
|
513
|
-
} else if (entry.type === "blob") {
|
|
514
|
-
entries.push({ path: full, oid: entry.oid });
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
const files2 = await Promise.all(
|
|
519
|
-
entries.map(async ({ path, oid }) => {
|
|
520
|
-
const { blob } = await git4.readBlob({ ...repo, oid });
|
|
521
|
-
const after = detectBlobContent(blob);
|
|
522
|
-
return buildDiffFile(path, "added", null, after);
|
|
523
|
-
})
|
|
524
|
-
);
|
|
525
|
-
return summarizeDiff(files2);
|
|
526
|
-
}
|
|
527
|
-
const files = await walkTreeDiff(repo, parent, commitSha);
|
|
528
|
-
return summarizeDiff(files);
|
|
529
|
-
} catch (error) {
|
|
530
|
-
throw new Error(`Failed to get commit diff: ${error}`);
|
|
531
|
-
}
|
|
569
|
+
const { commit } = await getCommit(repo, commitSha);
|
|
570
|
+
const parent = commit.parent[0];
|
|
571
|
+
const parentTree = parent ? (await getCommit(repo, parent)).commit.tree : null;
|
|
572
|
+
return diffTrees(repo, parentTree, commit.tree);
|
|
532
573
|
}
|
|
533
574
|
async function getDiffBetweenRefs(repo, baseRef, compareRef) {
|
|
534
|
-
const [
|
|
535
|
-
|
|
536
|
-
|
|
575
|
+
const [base, compare] = await Promise.all([
|
|
576
|
+
resolveCommit(repo, baseRef),
|
|
577
|
+
resolveCommit(repo, compareRef)
|
|
537
578
|
]);
|
|
538
|
-
const
|
|
539
|
-
|
|
579
|
+
const [mergeBase] = await git4.findMergeBase({
|
|
580
|
+
...repo,
|
|
581
|
+
oids: [base.oid, compare.oid]
|
|
582
|
+
});
|
|
583
|
+
const baseTree = mergeBase === void 0 ? null : mergeBase === base.oid ? base.commit.tree : (await getCommit(repo, mergeBase)).commit.tree;
|
|
584
|
+
return diffTrees(repo, baseTree, compare.commit.tree);
|
|
540
585
|
}
|
|
541
586
|
|
|
542
587
|
// src/ops/file-history.ts
|
|
543
|
-
var HISTORY_WALK_DEPTH = 400;
|
|
544
588
|
var BANNER_WALK_DEPTH = 60;
|
|
545
589
|
var PREFETCH_WINDOW = 24;
|
|
546
|
-
function toEntry(commit) {
|
|
547
|
-
return {
|
|
548
|
-
sha: commit.oid,
|
|
549
|
-
message: commit.commit.message.trim(),
|
|
550
|
-
authorName: commit.commit.author.name,
|
|
551
|
-
authorEmail: commit.commit.author.email,
|
|
552
|
-
createdAt: new Date(commit.commit.author.timestamp * 1e3).toISOString()
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
590
|
async function getFileHistory(repo, options, hooks) {
|
|
556
591
|
const limit = options.limit ?? 30;
|
|
557
592
|
const maxDepth = options.maxDepth ?? HISTORY_WALK_DEPTH;
|
|
@@ -602,7 +637,7 @@ async function getFileHistory(repo, options, hooks) {
|
|
|
602
637
|
parentCommit ? resolveOid(parentCommit.commit.tree) : Promise.resolve(null)
|
|
603
638
|
]);
|
|
604
639
|
if (oid !== parentOid) {
|
|
605
|
-
entries.push(
|
|
640
|
+
entries.push(summarizeCommit(commit));
|
|
606
641
|
if (entries.length >= limit) {
|
|
607
642
|
truncated = i < commits.length - 1;
|
|
608
643
|
break outer;
|
|
@@ -616,20 +651,10 @@ async function getFileHistory(repo, options, hooks) {
|
|
|
616
651
|
}
|
|
617
652
|
|
|
618
653
|
// src/ops/last-commit.ts
|
|
619
|
-
var HISTORY_WALK_DEPTH2 = 400;
|
|
620
654
|
var PREFETCH_WINDOW2 = 24;
|
|
621
|
-
function toLastCommitInfo(commit) {
|
|
622
|
-
return {
|
|
623
|
-
sha: commit.oid,
|
|
624
|
-
message: commit.commit.message.trim(),
|
|
625
|
-
authorName: commit.commit.author.name,
|
|
626
|
-
authorEmail: commit.commit.author.email,
|
|
627
|
-
createdAt: new Date(commit.commit.author.timestamp * 1e3).toISOString()
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
655
|
async function getLastCommitsForTree(repo, options, hooks) {
|
|
631
656
|
const treePath = options.treePath ?? "";
|
|
632
|
-
const depth = options.depth ??
|
|
657
|
+
const depth = options.depth ?? HISTORY_WALK_DEPTH;
|
|
633
658
|
const commits = await runStep(
|
|
634
659
|
hooks,
|
|
635
660
|
`getCommitLog depth=${depth}`,
|
|
@@ -709,7 +734,7 @@ async function getLastCommitsForTree(repo, options, hooks) {
|
|
|
709
734
|
);
|
|
710
735
|
for (const name of remaining) {
|
|
711
736
|
if (childByName.get(name) !== parentChildByName.get(name)) {
|
|
712
|
-
result[name] =
|
|
737
|
+
result[name] = summarizeCommit(commit);
|
|
713
738
|
}
|
|
714
739
|
}
|
|
715
740
|
for (const name of Object.keys(result)) {
|
|
@@ -734,7 +759,7 @@ async function analyzeMerge(repo, sourceBranch, targetBranch) {
|
|
|
734
759
|
git5.resolveRef({ ...repo, ref: qualifyBranchRef(sourceBranch) }),
|
|
735
760
|
git5.resolveRef({ ...repo, ref: qualifyBranchRef(targetBranch) })
|
|
736
761
|
]);
|
|
737
|
-
const isDescendant = await git5.isDescendent({
|
|
762
|
+
const isDescendant = sourceOid === targetOid || await git5.isDescendent({
|
|
738
763
|
...repo,
|
|
739
764
|
oid: sourceOid,
|
|
740
765
|
ancestor: targetOid
|
|
@@ -764,6 +789,9 @@ async function fastForwardMerge(repo, sourceBranch, targetBranch) {
|
|
|
764
789
|
git5.resolveRef({ ...repo, ref: `refs/heads/${sourceBranch}` }),
|
|
765
790
|
git5.resolveRef({ ...repo, ref: `refs/heads/${targetBranch}` })
|
|
766
791
|
]);
|
|
792
|
+
if (sourceOid === targetOid || await git5.isDescendent({ ...repo, oid: targetOid, ancestor: sourceOid })) {
|
|
793
|
+
return { success: true, commitSha: targetOid };
|
|
794
|
+
}
|
|
767
795
|
const isFF = await git5.isDescendent({
|
|
768
796
|
...repo,
|
|
769
797
|
oid: sourceOid,
|
|
@@ -800,7 +828,6 @@ export {
|
|
|
800
828
|
getCommitLog,
|
|
801
829
|
getDiffBetweenRefs,
|
|
802
830
|
getFileContent,
|
|
803
|
-
getFileFromRef,
|
|
804
831
|
getFileHistory,
|
|
805
832
|
getLastCommitsForTree,
|
|
806
833
|
getTreeFromRef,
|
|
@@ -809,7 +836,9 @@ export {
|
|
|
809
836
|
resolveCommit,
|
|
810
837
|
resolveLooseRefFast,
|
|
811
838
|
resultKeyPrefixes,
|
|
839
|
+
summarizeCommit,
|
|
812
840
|
upsertTree,
|
|
841
|
+
withPackedWrites,
|
|
813
842
|
writeCommitToBare
|
|
814
843
|
};
|
|
815
844
|
//# sourceMappingURL=ops.js.map
|