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.
Files changed (37) hide show
  1. package/README.md +10 -6
  2. package/dist/{chunk-XNSZTA4Y.js → chunk-HBOJR5D3.js} +108 -5
  3. package/dist/chunk-HBOJR5D3.js.map +1 -0
  4. package/dist/{chunk-FIFWOMPM.js → chunk-PASVUPKR.js} +64 -2
  5. package/dist/chunk-PASVUPKR.js.map +1 -0
  6. package/dist/chunk-ZB6GNJ2J.js +59 -0
  7. package/dist/chunk-ZB6GNJ2J.js.map +1 -0
  8. package/dist/http.js +299 -192
  9. package/dist/http.js.map +1 -1
  10. package/dist/index.js +79 -76
  11. package/dist/index.js.map +1 -1
  12. package/dist/ops.js +282 -253
  13. package/dist/ops.js.map +1 -1
  14. package/dist/types/edge-utils.d.ts +2 -0
  15. package/dist/types/errors.d.ts +0 -1
  16. package/dist/types/http/index.d.ts +2 -2
  17. package/dist/types/http/pack-reuse.d.ts +2 -1
  18. package/dist/types/http/reachability.d.ts +12 -0
  19. package/dist/types/http/receive-pack.d.ts +28 -9
  20. package/dist/types/http/upload-pack.d.ts +19 -3
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/ops/commit.d.ts +9 -1
  23. package/dist/types/ops/diff.d.ts +7 -3
  24. package/dist/types/ops/file-history.d.ts +4 -18
  25. package/dist/types/ops/history.d.ts +30 -43
  26. package/dist/types/ops/index.d.ts +4 -3
  27. package/dist/types/ops/last-commit.d.ts +2 -8
  28. package/dist/types/ops/packed-writes.d.ts +9 -0
  29. package/dist/types/ops/tree.d.ts +9 -1
  30. package/dist/types/ops/types.d.ts +10 -16
  31. package/dist/types/pack-write.d.ts +26 -0
  32. package/dist/types/refs.d.ts +9 -0
  33. package/package.json +2 -2
  34. package/dist/chunk-FIFWOMPM.js.map +0 -1
  35. package/dist/chunk-RMAOPWHJ.js +0 -38
  36. package/dist/chunk-RMAOPWHJ.js.map +0 -1
  37. 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
- } from "./chunk-XNSZTA4Y.js";
11
+ resolveLooseRefFast,
12
+ storePack
13
+ } from "./chunk-HBOJR5D3.js";
9
14
  import {
15
+ GitInvalidRequestError,
10
16
  GitObjectNotFoundError,
11
17
  GitPathNotFoundError,
12
18
  GitRefNotFoundError,
13
- decodeUtf8,
14
- hasNullByte,
19
+ inflate,
20
+ isSafeTreePath,
15
21
  qualifyBranchRef,
16
- readBlobContent,
17
- toBase64
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
- const subtreeOid = entry?.type === "tree" ? entry.oid : void 0;
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
- byName.delete(filePath);
169
+ if (entry?.type !== "blob") {
170
+ throw new GitPathNotFoundError(`No such file: ${fullPath}`);
171
+ }
172
+ byName.delete(name);
69
173
  } else {
70
- const dir = filePath.slice(0, slash);
71
- const rest = filePath.slice(slash + 1);
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({ ...repo, blob: content });
294
+ const oid = await git2.writeBlob({ ...writeRepo, blob: content });
170
295
  blobs.set(file.path, oid);
171
296
  })
172
297
  );
173
- return upsertTree(repo, parentTreeOid, blobs);
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 Error(`Branch ${options.branch} is empty`);
312
+ throw new GitPathNotFoundError(`Branch ${options.branch} is empty`);
185
313
  }
186
- return deleteFromTree(repo, parentTreeOid, options.filePath);
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?.code === "NotFoundError") {
351
+ if (isNotFound(err)) {
214
352
  throw new GitObjectNotFoundError(
215
- `Git data for ${context} is missing from storage. The repository may need to be re-pushed to repair it.`
353
+ `Git data for ${context} is missing from storage`
216
354
  );
217
355
  }
218
356
  throw err;
219
357
  });
220
358
  }
221
- var isNotFound = (err) => err?.code === "NotFoundError";
222
- async function resolveCommit(repo, ref) {
223
- const oid = await git3.resolveRef({ ...repo, ref: qualifyBranchRef(ref) }).catch((err) => {
224
- if (isNotFound(err)) {
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
- const result = await wrapMissingObject(
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: result.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 (options.knownHeadSha) {
277
- headSha = options.knownHeadSha;
278
- } else {
410
+ let headSha = options.knownHeadSha;
411
+ if (headSha === void 0) {
279
412
  try {
280
- headSha = await git3.resolveRef({ ...repo, ref: qualifyBranchRef(ref) });
413
+ headSha = await resolveRefOid(repo, ref);
281
414
  } catch (err) {
282
- if (isNotFound(err)) return [];
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
- if (cached && (cached.length >= depth || isFullyWalked(cached))) {
289
- hooks?.onNote?.(
290
- `getCommitLog: result-cache HIT for ${cacheKey} (depth=${depth})`
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
- `getCommitLog: result-cache MISS for ${cacheKey} (depth=${depth})`
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 = commits.map((commit) => ({
309
- oid: commit.oid,
310
- commit: commit.commit,
311
- payload: commit.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: result-cache HIT for ${cacheKey}`);
453
+ hooks?.onNote?.(`getTreeFromRef: cache hit ${cacheKey}`);
335
454
  return cached;
336
455
  }
337
- hooks?.onNote?.(`getTreeFromRef: result-cache MISS for ${cacheKey}`);
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
- let result;
343
- if (!treePath) {
344
- result = await wrapMissingObject(
345
- runStep(
346
- hooks,
347
- "listTreeEntries (root)",
348
- () => listTreeEntries(repo, commit.tree)
349
- ),
350
- context
351
- );
352
- } else {
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 headSha = await git3.resolveRef({ ...repo, ref: qualifyBranchRef(options.ref) }).catch(() => null);
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, knownHeadSha: headSha },
488
+ { ref: options.ref, depth: limit + skip },
397
489
  hooks
398
490
  );
399
- const result = all.slice(skip, skip + limit);
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[lines.length - 1] === "") lines.pop();
499
+ if (lines.at(-1) === "") lines.pop();
413
500
  return lines.length;
414
501
  }
415
- function createUnifiedPatch(params) {
416
- const oldPath = params.oldPath ?? `a/${params.path}`;
417
- const newPath = params.newPath ?? `b/${params.path}`;
418
- const patchBody = createTwoFilesPatch(
419
- oldPath,
420
- newPath,
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/${params.path} b/${params.path}
428
- ${patchBody}`;
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, status, before, after) {
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 ? createUnifiedPatch({
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 walkTreeDiff(repo, oldOid, newOid) {
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: [git4.TREE({ ref: oldOid }), git4.TREE({ ref: newOid })],
462
- map: async (filepath, [A, B]) => {
463
- const [typeA, typeB] = await Promise.all([A?.type(), B?.type()]);
464
- if (typeA === "tree" || typeB === "tree") return;
465
- if (typeA && !typeB) {
466
- const oidA2 = A ? await A.oid() : "";
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 (oidA !== oidB) {
482
- const [{ blob: blobA }, { blob: blobB }] = await Promise.all([
483
- git4.readBlob({ ...repo, oid: oidA }),
484
- git4.readBlob({ ...repo, oid: oidB })
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
- const before = detectBlobContent(blobA);
487
- const after = detectBlobContent(blobB);
488
- return buildDiffFile(filepath, "modified", before, after);
551
+ if (oldOid === newOid) return void 0;
489
552
  }
490
- return null;
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
- return (changes ?? []).filter(
494
- (c) => c !== null && c !== void 0
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
- try {
499
- const commit = await getCommit(repo, commitSha);
500
- const parent = commit.commit.parent[0];
501
- if (!parent) {
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 [baseOid, compareOid] = await Promise.all([
535
- git4.resolveRef({ ...repo, ref: qualifyBranchRef(baseRef) }),
536
- git4.resolveRef({ ...repo, ref: qualifyBranchRef(compareRef) })
575
+ const [base, compare] = await Promise.all([
576
+ resolveCommit(repo, baseRef),
577
+ resolveCommit(repo, compareRef)
537
578
  ]);
538
- const files = await walkTreeDiff(repo, baseOid, compareOid);
539
- return summarizeDiff(files);
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(toEntry(commit));
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 ?? HISTORY_WALK_DEPTH2;
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] = toLastCommitInfo(commit);
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