git-fs-s3 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ops.cjs ADDED
@@ -0,0 +1,1021 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/ops/index.ts
31
+ var ops_exports = {};
32
+ __export(ops_exports, {
33
+ BANNER_WALK_DEPTH: () => BANNER_WALK_DEPTH,
34
+ HISTORY_WALK_DEPTH: () => HISTORY_WALK_DEPTH,
35
+ analyzeMerge: () => analyzeMerge,
36
+ assertBranchExists: () => assertBranchExists,
37
+ assertSafeBranchName: () => assertSafeBranchName,
38
+ authorNow: () => authorNow,
39
+ commitFilesToBare: () => commitFilesToBare,
40
+ createBranchFrom: () => createBranchFrom,
41
+ deleteBranchByName: () => deleteBranchByName,
42
+ deleteFileFromBare: () => deleteFileFromBare,
43
+ deleteFromTree: () => deleteFromTree,
44
+ fastForwardMerge: () => fastForwardMerge,
45
+ findTreeEntry: () => findTreeEntry,
46
+ getBlob: () => getBlob,
47
+ getCommit: () => getCommit,
48
+ getCommitDiff: () => getCommitDiff,
49
+ getCommitHistory: () => getCommitHistory,
50
+ getCommitLog: () => getCommitLog,
51
+ getDiffBetweenRefs: () => getDiffBetweenRefs,
52
+ getFileContent: () => getFileContent,
53
+ getFileFromRef: () => getFileFromRef,
54
+ getFileHistory: () => getFileHistory,
55
+ getLastCommitsForTree: () => getLastCommitsForTree,
56
+ getTreeFromRef: () => getTreeFromRef,
57
+ listBranches: () => listBranches,
58
+ listTreeEntries: () => listTreeEntries,
59
+ resolveCommit: () => resolveCommit,
60
+ resultKeyPrefixes: () => resultKeyPrefixes,
61
+ upsertTree: () => upsertTree,
62
+ writeCommitToBare: () => writeCommitToBare
63
+ });
64
+ module.exports = __toCommonJS(ops_exports);
65
+
66
+ // src/ops/branch.ts
67
+ var import_isomorphic_git = __toESM(require("isomorphic-git"), 1);
68
+
69
+ // src/git-errors.ts
70
+ var GitError = class extends Error {
71
+ statusCode;
72
+ retryable;
73
+ constructor(message, statusCode = 500, retryable = false) {
74
+ super(message);
75
+ this.name = this.constructor.name;
76
+ this.statusCode = statusCode;
77
+ this.retryable = retryable;
78
+ Error.captureStackTrace?.(this, this.constructor);
79
+ }
80
+ toJSON() {
81
+ return {
82
+ error: this.name,
83
+ message: this.message,
84
+ statusCode: this.statusCode,
85
+ retryable: this.retryable
86
+ };
87
+ }
88
+ };
89
+ var GitPathNotFoundError = class extends GitError {
90
+ constructor(message) {
91
+ super(message, 404, false);
92
+ }
93
+ };
94
+ var GitObjectNotFoundError = class extends GitError {
95
+ constructor(message) {
96
+ super(message, 404, false);
97
+ }
98
+ };
99
+ var GitInvalidRequestError = class extends GitError {
100
+ constructor(message) {
101
+ super(message, 400, false);
102
+ }
103
+ };
104
+
105
+ // src/refs.ts
106
+ var BAD_REF_COMPONENT = (
107
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control chars are exactly what git's own ref-name rules reject — this needs to match the same range.
108
+ /(^|[/.])([/.]|$)|^@$|@\{|[\x00-\x20\x7f~^:?*[\\]|\.lock(\/|$)/
109
+ );
110
+ var FULL_SHA_RE = /^[0-9a-f]{40}$/i;
111
+ function isSafeBranchName(name) {
112
+ if (!name || name.startsWith("refs/") || name === "HEAD") return false;
113
+ if (FULL_SHA_RE.test(name)) return false;
114
+ return !BAD_REF_COMPONENT.test(name);
115
+ }
116
+ function qualifyBranchRef(ref) {
117
+ if (ref.startsWith("refs/") || ref === "HEAD" || FULL_SHA_RE.test(ref)) {
118
+ return ref;
119
+ }
120
+ return `refs/heads/${ref}`;
121
+ }
122
+
123
+ // src/ops/branch.ts
124
+ function assertSafeBranchName(name) {
125
+ if (!isSafeBranchName(name)) {
126
+ throw new GitInvalidRequestError(`Invalid branch name: ${name}`);
127
+ }
128
+ }
129
+ async function listBranches(repo) {
130
+ try {
131
+ const [branches, currentBranch] = await Promise.all([
132
+ import_isomorphic_git.default.listBranches(repo),
133
+ import_isomorphic_git.default.currentBranch({ ...repo, fullname: false }).catch(() => null)
134
+ ]);
135
+ return Promise.all(
136
+ branches.map(async (branch) => ({
137
+ name: branch,
138
+ commit: await import_isomorphic_git.default.resolveRef({ ...repo, ref: `refs/heads/${branch}` }),
139
+ isDefault: branch === currentBranch
140
+ }))
141
+ );
142
+ } catch (err) {
143
+ if (err.code === "NotFoundError") return [];
144
+ throw err;
145
+ }
146
+ }
147
+ async function createBranchFrom(repo, name, startPoint = "main") {
148
+ assertSafeBranchName(name);
149
+ assertSafeBranchName(startPoint);
150
+ const object = await import_isomorphic_git.default.resolveRef({
151
+ ...repo,
152
+ ref: `refs/heads/${startPoint}`
153
+ });
154
+ await import_isomorphic_git.default.branch({ ...repo, ref: name, checkout: false, object });
155
+ }
156
+ async function deleteBranchByName(repo, name) {
157
+ assertSafeBranchName(name);
158
+ await import_isomorphic_git.default.deleteBranch({ ...repo, ref: name });
159
+ }
160
+ async function assertBranchExists(repo, name) {
161
+ assertSafeBranchName(name);
162
+ await import_isomorphic_git.default.resolveRef({ ...repo, ref: `refs/heads/${name}` });
163
+ }
164
+
165
+ // src/ops/commit.ts
166
+ var import_isomorphic_git3 = __toESM(require("isomorphic-git"), 1);
167
+
168
+ // src/ops/tree.ts
169
+ var import_isomorphic_git2 = __toESM(require("isomorphic-git"), 1);
170
+ var joinPath = (prefix, name) => prefix ? `${prefix.replace(/\/+$/, "")}/${name}` : name;
171
+ async function upsertTree(repo, treeOid, entries) {
172
+ const existing = treeOid ? (await import_isomorphic_git2.default.readTree({ ...repo, oid: treeOid })).tree : [];
173
+ const byName = new Map(existing.map((e) => [e.path, e]));
174
+ const direct = /* @__PURE__ */ new Map();
175
+ const nested = /* @__PURE__ */ new Map();
176
+ for (const [filePath, blobOid] of entries) {
177
+ const slash = filePath.indexOf("/");
178
+ if (slash === -1) {
179
+ direct.set(filePath, blobOid);
180
+ } else {
181
+ const dir = filePath.slice(0, slash);
182
+ const rest = filePath.slice(slash + 1);
183
+ if (!nested.has(dir)) nested.set(dir, /* @__PURE__ */ new Map());
184
+ nested.get(dir)?.set(rest, blobOid);
185
+ }
186
+ }
187
+ for (const [name, blobOid] of direct) {
188
+ byName.set(name, {
189
+ mode: "100644",
190
+ path: name,
191
+ oid: blobOid,
192
+ type: "blob"
193
+ });
194
+ }
195
+ const nestedResults = await Promise.all(
196
+ Array.from(nested, async ([dir, subEntries]) => {
197
+ const entry = byName.get(dir);
198
+ const subtreeOid = entry?.type === "tree" ? entry.oid : void 0;
199
+ const newOid = await upsertTree(repo, subtreeOid, subEntries);
200
+ return [dir, newOid];
201
+ })
202
+ );
203
+ for (const [dir, newOid] of nestedResults) {
204
+ byName.set(dir, { mode: "040000", path: dir, oid: newOid, type: "tree" });
205
+ }
206
+ return import_isomorphic_git2.default.writeTree({ ...repo, tree: Array.from(byName.values()) });
207
+ }
208
+ async function deleteFromTree(repo, treeOid, filePath) {
209
+ const existing = (await import_isomorphic_git2.default.readTree({ ...repo, oid: treeOid })).tree;
210
+ const byName = new Map(existing.map((e) => [e.path, e]));
211
+ const slash = filePath.indexOf("/");
212
+ if (slash === -1) {
213
+ byName.delete(filePath);
214
+ } else {
215
+ const dir = filePath.slice(0, slash);
216
+ const rest = filePath.slice(slash + 1);
217
+ const entry = byName.get(dir);
218
+ if (entry?.type === "tree") {
219
+ const newOid = await deleteFromTree(repo, entry.oid, rest);
220
+ byName.set(dir, { ...entry, oid: newOid });
221
+ }
222
+ }
223
+ return import_isomorphic_git2.default.writeTree({ ...repo, tree: Array.from(byName.values()) });
224
+ }
225
+ async function findTreeEntry(repo, rootTreeOid, treePath) {
226
+ if (!treePath) {
227
+ return { path: "", mode: "040000", type: "tree", oid: rootTreeOid };
228
+ }
229
+ const parts = treePath.split("/").filter(Boolean);
230
+ let currentTreeOid = rootTreeOid;
231
+ let currentPath = "";
232
+ for (const [index, part] of parts.entries()) {
233
+ const tree = await import_isomorphic_git2.default.readTree({ ...repo, oid: currentTreeOid });
234
+ const entry = tree.tree.find((candidate) => candidate.path === part);
235
+ if (!entry) return null;
236
+ currentPath = currentPath ? joinPath(currentPath, entry.path) : entry.path;
237
+ if (index === parts.length - 1) {
238
+ return {
239
+ path: currentPath,
240
+ mode: entry.mode,
241
+ type: entry.type,
242
+ oid: entry.oid
243
+ };
244
+ }
245
+ if (entry.type !== "tree") return null;
246
+ currentTreeOid = entry.oid;
247
+ }
248
+ return null;
249
+ }
250
+ async function listTreeEntries(repo, treeOid, prefix = "") {
251
+ const tree = await import_isomorphic_git2.default.readTree({ ...repo, oid: treeOid });
252
+ return tree.tree.map((entry) => ({
253
+ path: prefix ? joinPath(prefix, entry.path) : entry.path,
254
+ mode: entry.mode,
255
+ type: entry.type,
256
+ oid: entry.oid
257
+ }));
258
+ }
259
+
260
+ // src/ops/commit.ts
261
+ function authorNow(name, email) {
262
+ return {
263
+ name,
264
+ email,
265
+ timestamp: Math.floor(Date.now() / 1e3),
266
+ timezoneOffset: 0
267
+ };
268
+ }
269
+ async function writeCommitToBare(repo, options) {
270
+ assertSafeBranchName(options.branch);
271
+ let parentOid;
272
+ let parentTreeOid;
273
+ try {
274
+ parentOid = await import_isomorphic_git3.default.resolveRef({
275
+ ...repo,
276
+ ref: `refs/heads/${options.branch}`
277
+ });
278
+ const { commit } = await import_isomorphic_git3.default.readCommit({ ...repo, oid: parentOid });
279
+ parentTreeOid = commit.tree;
280
+ } catch (err) {
281
+ if (err?.code !== "NotFoundError") {
282
+ throw err;
283
+ }
284
+ }
285
+ const treeOid = await options.buildTree(parentTreeOid);
286
+ const commitOid = await import_isomorphic_git3.default.writeCommit({
287
+ ...repo,
288
+ commit: {
289
+ message: options.message,
290
+ tree: treeOid,
291
+ parent: parentOid ? [parentOid] : [],
292
+ author: options.author,
293
+ committer: options.author
294
+ }
295
+ });
296
+ await import_isomorphic_git3.default.writeRef({
297
+ ...repo,
298
+ ref: `refs/heads/${options.branch}`,
299
+ value: commitOid,
300
+ force: true
301
+ });
302
+ return commitOid;
303
+ }
304
+ function commitFilesToBare(repo, options) {
305
+ return writeCommitToBare(repo, {
306
+ branch: options.branch,
307
+ message: options.message,
308
+ author: options.author,
309
+ buildTree: async (parentTreeOid) => {
310
+ const blobs = /* @__PURE__ */ new Map();
311
+ await Promise.all(
312
+ options.files.map(async (file) => {
313
+ const content = typeof file.content === "string" ? new TextEncoder().encode(file.content) : file.content;
314
+ const oid = await import_isomorphic_git3.default.writeBlob({ ...repo, blob: content });
315
+ blobs.set(file.path, oid);
316
+ })
317
+ );
318
+ return upsertTree(repo, parentTreeOid, blobs);
319
+ }
320
+ });
321
+ }
322
+ function deleteFileFromBare(repo, options) {
323
+ return writeCommitToBare(repo, {
324
+ branch: options.branch,
325
+ message: options.message,
326
+ author: options.author,
327
+ buildTree: async (parentTreeOid) => {
328
+ if (!parentTreeOid) {
329
+ throw new Error(`Branch ${options.branch} is empty`);
330
+ }
331
+ return deleteFromTree(repo, parentTreeOid, options.filePath);
332
+ }
333
+ });
334
+ }
335
+
336
+ // src/ops/diff.ts
337
+ var import_diff = require("diff");
338
+ var import_isomorphic_git5 = __toESM(require("isomorphic-git"), 1);
339
+
340
+ // src/edge-utils.ts
341
+ var textEncoder = new TextEncoder();
342
+ var textDecoder = new TextDecoder();
343
+ function decodeUtf8(data) {
344
+ return textDecoder.decode(data);
345
+ }
346
+ function toBase64(data) {
347
+ let binary = "";
348
+ for (let i = 0; i < data.length; i++)
349
+ binary += String.fromCharCode(data[i]);
350
+ return btoa(binary);
351
+ }
352
+ function hasNullByte(data) {
353
+ return data.includes(0);
354
+ }
355
+ function readBlobContent(blob) {
356
+ const isBinary = hasNullByte(blob);
357
+ return {
358
+ isBinary,
359
+ text: isBinary ? "" : decodeUtf8(blob),
360
+ bytes: blob
361
+ };
362
+ }
363
+
364
+ // src/ops/history.ts
365
+ var import_isomorphic_git4 = __toESM(require("isomorphic-git"), 1);
366
+
367
+ // src/ops/types.ts
368
+ var runStep = (hooks, label, fn) => hooks?.step ? hooks.step(label, fn) : fn();
369
+ function resultKeyPrefixes(gitdir) {
370
+ return [
371
+ `commitlog:${gitdir}:`,
372
+ `tree:${gitdir}:`,
373
+ `commits:${gitdir}:`,
374
+ `last-commits:${gitdir}:`,
375
+ `file-history:${gitdir}:`
376
+ ];
377
+ }
378
+
379
+ // src/ops/history.ts
380
+ function wrapMissingObject(promise, context) {
381
+ return promise.catch((err) => {
382
+ if (err?.code === "NotFoundError") {
383
+ throw new GitObjectNotFoundError(
384
+ `Git data for ${context} is missing from storage. The repository may need to be re-pushed to repair it.`
385
+ );
386
+ }
387
+ throw err;
388
+ });
389
+ }
390
+ var isNotFound = (err) => err?.code === "NotFoundError";
391
+ async function resolveCommit(repo, ref) {
392
+ const oid = await import_isomorphic_git4.default.resolveRef({ ...repo, ref: qualifyBranchRef(ref) });
393
+ const result = await wrapMissingObject(
394
+ import_isomorphic_git4.default.readCommit({ ...repo, oid }),
395
+ `${repo.gitdir} commit ${oid}`
396
+ );
397
+ return { oid, commit: result.commit };
398
+ }
399
+ async function getBlob(repo, sha) {
400
+ const { blob } = await wrapMissingObject(
401
+ import_isomorphic_git4.default.readBlob({ ...repo, oid: sha }),
402
+ `${repo.gitdir} blob ${sha}`
403
+ );
404
+ return blob;
405
+ }
406
+ async function getFileContent(repo, filePath, ref = "main") {
407
+ const { commit } = await resolveCommit(repo, ref);
408
+ const context = `${repo.gitdir}@${ref}:${filePath}`;
409
+ const entry = await wrapMissingObject(
410
+ findTreeEntry(repo, commit.tree, filePath),
411
+ context
412
+ );
413
+ if (entry?.type !== "blob") {
414
+ throw new GitPathNotFoundError(`File not found: ${filePath}`);
415
+ }
416
+ const { blob } = await wrapMissingObject(
417
+ import_isomorphic_git4.default.readBlob({ ...repo, oid: entry.oid }),
418
+ context
419
+ );
420
+ return blob;
421
+ }
422
+ async function getCommit(repo, sha) {
423
+ const result = await wrapMissingObject(
424
+ import_isomorphic_git4.default.readCommit({ ...repo, oid: sha }),
425
+ `${repo.gitdir} commit ${sha}`
426
+ );
427
+ return { oid: result.oid, commit: result.commit, payload: result.payload };
428
+ }
429
+ function isFullyWalked(commits) {
430
+ const last = commits[commits.length - 1];
431
+ return !!last && last.commit.parent.length === 0;
432
+ }
433
+ async function getCommitLog(repo, options = {}, hooks) {
434
+ const ref = options.ref ?? "main";
435
+ const depth = options.depth ?? 50;
436
+ let headSha;
437
+ if (options.knownHeadSha) {
438
+ headSha = options.knownHeadSha;
439
+ } else {
440
+ try {
441
+ headSha = await import_isomorphic_git4.default.resolveRef({ ...repo, ref: qualifyBranchRef(ref) });
442
+ } catch (err) {
443
+ if (isNotFound(err)) return [];
444
+ throw err;
445
+ }
446
+ }
447
+ const cacheKey = `commitlog:${repo.gitdir}:${headSha}`;
448
+ const cached = hooks?.resultCache?.get(cacheKey);
449
+ if (cached && (cached.length >= depth || isFullyWalked(cached))) {
450
+ hooks?.onNote?.(
451
+ `getCommitLog: result-cache HIT for ${cacheKey} (depth=${depth})`
452
+ );
453
+ return cached.slice(0, depth);
454
+ }
455
+ hooks?.onNote?.(
456
+ `getCommitLog: result-cache MISS for ${cacheKey} (depth=${depth})`
457
+ );
458
+ if (hooks?.prefetch && depth >= (hooks.prefetchMinDepth ?? 5)) {
459
+ await runStep(hooks, "prefetch", hooks.prefetch);
460
+ }
461
+ const commits = await wrapMissingObject(
462
+ runStep(
463
+ hooks,
464
+ `git.log ${ref} depth=${depth}`,
465
+ () => import_isomorphic_git4.default.log({ ...repo, ref: headSha, depth })
466
+ ),
467
+ `${repo.gitdir}@${ref} history`
468
+ );
469
+ const result = commits.map((commit) => ({
470
+ oid: commit.oid,
471
+ commit: commit.commit,
472
+ payload: commit.payload || ""
473
+ }));
474
+ if (!cached || result.length > cached.length) {
475
+ hooks?.resultCache?.set(cacheKey, result);
476
+ }
477
+ return result;
478
+ }
479
+ async function getFileFromRef(repo, filePath, ref) {
480
+ const bytes = await getFileContent(repo, filePath, ref);
481
+ const isBinary = hasNullByte(bytes);
482
+ return {
483
+ content: isBinary ? toBase64(bytes) : decodeUtf8(bytes),
484
+ size: bytes.length,
485
+ isBinary
486
+ };
487
+ }
488
+ async function getTreeFromRef(repo, options = {}, hooks) {
489
+ const ref = options.ref ?? "main";
490
+ const treePath = options.treePath ?? "";
491
+ let commit;
492
+ let headSha;
493
+ try {
494
+ const resolved = await resolveCommit(repo, ref);
495
+ commit = resolved.commit;
496
+ headSha = resolved.oid;
497
+ } catch (err) {
498
+ if (isNotFound(err)) return [];
499
+ throw err;
500
+ }
501
+ const cacheKey = `tree:${repo.gitdir}:${headSha}:${treePath}`;
502
+ const cached = hooks?.resultCache?.get(cacheKey);
503
+ if (cached) {
504
+ hooks?.onNote?.(`getTreeFromRef: result-cache HIT for ${cacheKey}`);
505
+ return cached;
506
+ }
507
+ hooks?.onNote?.(`getTreeFromRef: result-cache MISS for ${cacheKey}`);
508
+ if (hooks?.prefetch) {
509
+ await runStep(hooks, "prefetch", hooks.prefetch);
510
+ }
511
+ const context = `${repo.gitdir}@${ref}:${treePath || "/"}`;
512
+ let result;
513
+ if (!treePath) {
514
+ result = await wrapMissingObject(
515
+ runStep(
516
+ hooks,
517
+ "listTreeEntries (root)",
518
+ () => listTreeEntries(repo, commit.tree)
519
+ ),
520
+ context
521
+ );
522
+ } else {
523
+ const entry = await wrapMissingObject(
524
+ runStep(
525
+ hooks,
526
+ `findTreeEntry ${treePath}`,
527
+ () => findTreeEntry(repo, commit.tree, treePath)
528
+ ),
529
+ context
530
+ );
531
+ if (!entry) {
532
+ throw new GitPathNotFoundError(
533
+ `Path "${treePath}" does not exist at ${ref}`
534
+ );
535
+ }
536
+ result = entry.type !== "tree" ? [] : await wrapMissingObject(
537
+ runStep(
538
+ hooks,
539
+ `listTreeEntries ${treePath}`,
540
+ () => listTreeEntries(repo, entry.oid, entry.path)
541
+ ),
542
+ context
543
+ );
544
+ }
545
+ hooks?.resultCache?.set(cacheKey, result);
546
+ return result;
547
+ }
548
+ async function getCommitHistory(repo, options, hooks) {
549
+ const limit = options.limit ?? 50;
550
+ const skip = options.skip ?? 0;
551
+ const headSha = await import_isomorphic_git4.default.resolveRef({ ...repo, ref: qualifyBranchRef(options.ref) }).catch(() => null);
552
+ const cacheKey = headSha ? `commits:${repo.gitdir}:${headSha}:${limit}:${skip}` : null;
553
+ if (cacheKey) {
554
+ const cached = hooks?.resultCache?.get(cacheKey);
555
+ if (cached) {
556
+ hooks?.onNote?.(`getCommitHistory: result-cache HIT for ${cacheKey}`);
557
+ return cached;
558
+ }
559
+ }
560
+ hooks?.onNote?.(
561
+ `getCommitHistory: result-cache MISS for ${cacheKey ?? "(no head)"}`
562
+ );
563
+ if (!headSha) return [];
564
+ const all = await getCommitLog(
565
+ repo,
566
+ { ref: options.ref, depth: limit + skip, knownHeadSha: headSha },
567
+ hooks
568
+ );
569
+ const result = all.slice(skip, skip + limit);
570
+ if (cacheKey) hooks?.resultCache?.set(cacheKey, result);
571
+ return result;
572
+ }
573
+
574
+ // src/ops/diff.ts
575
+ function detectBlobContent(blob) {
576
+ return readBlobContent(blob);
577
+ }
578
+ function countContentLines(content) {
579
+ if (content.length === 0) return 0;
580
+ const lines = content.split("\n");
581
+ if (lines[lines.length - 1] === "") lines.pop();
582
+ return lines.length;
583
+ }
584
+ function createUnifiedPatch(params) {
585
+ const oldPath = params.oldPath ?? `a/${params.path}`;
586
+ const newPath = params.newPath ?? `b/${params.path}`;
587
+ const patchBody = (0, import_diff.createTwoFilesPatch)(
588
+ oldPath,
589
+ newPath,
590
+ params.before,
591
+ params.after,
592
+ "",
593
+ "",
594
+ { context: 3 }
595
+ ).replace(/^=+\n/, "");
596
+ return `diff --git a/${params.path} b/${params.path}
597
+ ${patchBody}`;
598
+ }
599
+ function summarizeDiff(files) {
600
+ return {
601
+ files,
602
+ totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
603
+ totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
604
+ totalFiles: files.length
605
+ };
606
+ }
607
+ async function walkTreeDiff(repo, oldOid, newOid) {
608
+ const changes = await import_isomorphic_git5.default.walk({
609
+ ...repo,
610
+ trees: [import_isomorphic_git5.default.TREE({ ref: oldOid }), import_isomorphic_git5.default.TREE({ ref: newOid })],
611
+ map: async (filepath, [A, B]) => {
612
+ const [typeA, typeB] = await Promise.all([A?.type(), B?.type()]);
613
+ if (typeA === "tree" || typeB === "tree") return;
614
+ if (typeA && !typeB) {
615
+ const oidA2 = A ? await A.oid() : "";
616
+ const { blob } = await import_isomorphic_git5.default.readBlob({ ...repo, oid: oidA2 });
617
+ const before = detectBlobContent(blob);
618
+ return {
619
+ path: filepath,
620
+ status: "deleted",
621
+ additions: 0,
622
+ deletions: before.isBinary ? 0 : countContentLines(before.text),
623
+ patch: before.isBinary ? "" : createUnifiedPatch({
624
+ path: filepath,
625
+ before: before.text,
626
+ after: "",
627
+ newPath: "/dev/null"
628
+ }),
629
+ isBinary: before.isBinary,
630
+ oldContent: before.isBinary ? toBase64(before.bytes) : void 0,
631
+ oldSize: before.bytes.length
632
+ };
633
+ }
634
+ if (!typeA && typeB) {
635
+ const oidB2 = B ? await B.oid() : "";
636
+ const { blob } = await import_isomorphic_git5.default.readBlob({ ...repo, oid: oidB2 });
637
+ const after = detectBlobContent(blob);
638
+ return {
639
+ path: filepath,
640
+ status: "added",
641
+ additions: after.isBinary ? 0 : countContentLines(after.text),
642
+ deletions: 0,
643
+ patch: after.isBinary ? "" : createUnifiedPatch({
644
+ path: filepath,
645
+ before: "",
646
+ after: after.text,
647
+ oldPath: "/dev/null"
648
+ }),
649
+ isBinary: after.isBinary,
650
+ newContent: after.isBinary ? toBase64(after.bytes) : void 0,
651
+ newSize: after.bytes.length
652
+ };
653
+ }
654
+ const [oidA, oidB] = await Promise.all([
655
+ A ? A.oid() : Promise.resolve(""),
656
+ B ? B.oid() : Promise.resolve("")
657
+ ]);
658
+ if (oidA !== oidB) {
659
+ const [{ blob: blobA }, { blob: blobB }] = await Promise.all([
660
+ import_isomorphic_git5.default.readBlob({ ...repo, oid: oidA }),
661
+ import_isomorphic_git5.default.readBlob({ ...repo, oid: oidB })
662
+ ]);
663
+ const before = detectBlobContent(blobA);
664
+ const after = detectBlobContent(blobB);
665
+ const isBinary = before.isBinary || after.isBinary;
666
+ return {
667
+ path: filepath,
668
+ status: "modified",
669
+ additions: isBinary ? 0 : countContentLines(after.text),
670
+ deletions: isBinary ? 0 : countContentLines(before.text),
671
+ patch: isBinary ? "" : createUnifiedPatch({
672
+ path: filepath,
673
+ before: before.text,
674
+ after: after.text
675
+ }),
676
+ isBinary,
677
+ oldContent: isBinary ? toBase64(before.bytes) : void 0,
678
+ newContent: isBinary ? toBase64(after.bytes) : void 0,
679
+ oldSize: before.bytes.length,
680
+ newSize: after.bytes.length
681
+ };
682
+ }
683
+ return null;
684
+ }
685
+ });
686
+ return (changes ?? []).filter(
687
+ (c) => c !== null && c !== void 0
688
+ );
689
+ }
690
+ async function getCommitDiff(repo, commitSha) {
691
+ try {
692
+ const commit = await getCommit(repo, commitSha);
693
+ const parent = commit.commit.parent[0];
694
+ if (!parent) {
695
+ const entries = [];
696
+ const stack = [
697
+ { treeOid: commit.commit.tree, prefix: "" }
698
+ ];
699
+ while (stack.length) {
700
+ const { treeOid, prefix } = stack.pop();
701
+ const { tree } = await import_isomorphic_git5.default.readTree({ ...repo, oid: treeOid });
702
+ for (const entry of tree) {
703
+ const full = prefix ? `${prefix}/${entry.path}` : entry.path;
704
+ if (entry.type === "tree") {
705
+ stack.push({ treeOid: entry.oid, prefix: full });
706
+ } else if (entry.type === "blob") {
707
+ entries.push({ path: full, oid: entry.oid });
708
+ }
709
+ }
710
+ }
711
+ const files2 = await Promise.all(
712
+ entries.map(async ({ path, oid }) => {
713
+ const { blob } = await import_isomorphic_git5.default.readBlob({ ...repo, oid });
714
+ const after = detectBlobContent(blob);
715
+ return {
716
+ path,
717
+ status: "added",
718
+ additions: after.isBinary ? 0 : countContentLines(after.text),
719
+ deletions: 0,
720
+ patch: after.isBinary ? "" : createUnifiedPatch({
721
+ path,
722
+ before: "",
723
+ after: after.text,
724
+ oldPath: "/dev/null"
725
+ }),
726
+ isBinary: after.isBinary,
727
+ newContent: after.isBinary ? toBase64(after.bytes) : void 0,
728
+ newSize: after.bytes.length
729
+ };
730
+ })
731
+ );
732
+ return summarizeDiff(files2);
733
+ }
734
+ const files = await walkTreeDiff(repo, parent, commitSha);
735
+ return summarizeDiff(files);
736
+ } catch (error) {
737
+ throw new Error(`Failed to get commit diff: ${error}`);
738
+ }
739
+ }
740
+ async function getDiffBetweenRefs(repo, baseRef, compareRef) {
741
+ const [baseOid, compareOid] = await Promise.all([
742
+ import_isomorphic_git5.default.resolveRef({ ...repo, ref: qualifyBranchRef(baseRef) }),
743
+ import_isomorphic_git5.default.resolveRef({ ...repo, ref: qualifyBranchRef(compareRef) })
744
+ ]);
745
+ const files = await walkTreeDiff(repo, baseOid, compareOid);
746
+ return summarizeDiff(files);
747
+ }
748
+
749
+ // src/ops/file-history.ts
750
+ var HISTORY_WALK_DEPTH = 400;
751
+ var BANNER_WALK_DEPTH = 60;
752
+ var PREFETCH_WINDOW = 24;
753
+ function toEntry(commit) {
754
+ return {
755
+ sha: commit.oid,
756
+ message: commit.commit.message.trim(),
757
+ authorName: commit.commit.author.name,
758
+ authorEmail: commit.commit.author.email,
759
+ createdAt: new Date(commit.commit.author.timestamp * 1e3).toISOString()
760
+ };
761
+ }
762
+ async function getFileHistory(repo, options, hooks) {
763
+ const limit = options.limit ?? 30;
764
+ const maxDepth = options.maxDepth ?? HISTORY_WALK_DEPTH;
765
+ const walkDepth = Math.max(maxDepth, limit);
766
+ const commits = await runStep(
767
+ hooks,
768
+ `getCommitLog depth=${walkDepth}`,
769
+ () => getCommitLog(repo, { ref: options.ref, depth: walkDepth }, hooks)
770
+ );
771
+ const head = commits[0];
772
+ if (!head) return { entries: [], truncated: false };
773
+ const cacheKey = `file-history:${repo.gitdir}:${head.oid}:${options.filePath}:${limit}:${maxDepth}`;
774
+ const cached = hooks?.resultCache?.get(cacheKey);
775
+ if (cached) {
776
+ hooks?.onNote?.("getFileHistory: result-cache HIT, skipping history walk");
777
+ return cached;
778
+ }
779
+ hooks?.onNote?.("getFileHistory: result-cache MISS, walking history");
780
+ const byOid = new Map(commits.map((commit) => [commit.oid, commit]));
781
+ const oidByCommitTree = /* @__PURE__ */ new Map();
782
+ async function resolveOid(commitTreeOid) {
783
+ const cachedOid = oidByCommitTree.get(commitTreeOid);
784
+ if (cachedOid !== void 0) return cachedOid;
785
+ const entry = await findTreeEntry(repo, commitTreeOid, options.filePath);
786
+ const oid = entry?.type === "blob" ? entry.oid : null;
787
+ oidByCommitTree.set(commitTreeOid, oid);
788
+ return oid;
789
+ }
790
+ const entries = [];
791
+ let truncated = false;
792
+ outer: for (let windowStart = 0; windowStart < commits.length; windowStart += PREFETCH_WINDOW) {
793
+ const windowEnd = Math.min(windowStart + PREFETCH_WINDOW, commits.length);
794
+ const prefetchEnd = Math.min(windowEnd + 1, commits.length);
795
+ await Promise.all(
796
+ commits.slice(windowStart, prefetchEnd).map((commit) => resolveOid(commit.commit.tree))
797
+ );
798
+ for (let i = windowStart; i < windowEnd; i++) {
799
+ const commit = commits[i];
800
+ if (!commit) break outer;
801
+ const parentSha = commit.commit.parent[0];
802
+ const parentCommit = parentSha ? byOid.get(parentSha) : void 0;
803
+ if (parentSha && !parentCommit) {
804
+ truncated = true;
805
+ break outer;
806
+ }
807
+ const [oid, parentOid] = await Promise.all([
808
+ resolveOid(commit.commit.tree),
809
+ parentCommit ? resolveOid(parentCommit.commit.tree) : Promise.resolve(null)
810
+ ]);
811
+ if (oid !== parentOid) {
812
+ entries.push(toEntry(commit));
813
+ if (entries.length >= limit) {
814
+ truncated = i < commits.length - 1;
815
+ break outer;
816
+ }
817
+ }
818
+ }
819
+ }
820
+ const result = { entries, truncated };
821
+ hooks?.resultCache?.set(cacheKey, result);
822
+ return result;
823
+ }
824
+
825
+ // src/ops/last-commit.ts
826
+ var HISTORY_WALK_DEPTH2 = 400;
827
+ var PREFETCH_WINDOW2 = 24;
828
+ function toLastCommitInfo(commit) {
829
+ return {
830
+ sha: commit.oid,
831
+ message: commit.commit.message.trim(),
832
+ authorName: commit.commit.author.name,
833
+ authorEmail: commit.commit.author.email,
834
+ createdAt: new Date(commit.commit.author.timestamp * 1e3).toISOString()
835
+ };
836
+ }
837
+ async function getLastCommitsForTree(repo, options, hooks) {
838
+ const treePath = options.treePath ?? "";
839
+ const depth = options.depth ?? HISTORY_WALK_DEPTH2;
840
+ const commits = await runStep(
841
+ hooks,
842
+ `getCommitLog depth=${depth}`,
843
+ () => getCommitLog(repo, { ref: options.ref, depth }, hooks)
844
+ );
845
+ hooks?.onNote?.(`getLastCommitsForTree: ${commits.length} commits in log`);
846
+ const head = commits[0];
847
+ if (!head) return {};
848
+ const cacheKey = `last-commits:${repo.gitdir}:${head.oid}:${treePath}`;
849
+ const cachedResult = hooks?.resultCache?.get(cacheKey);
850
+ if (cachedResult) {
851
+ hooks?.onNote?.(
852
+ "getLastCommitsForTree: result-cache HIT, skipping history walk"
853
+ );
854
+ return cachedResult;
855
+ }
856
+ hooks?.onNote?.("getLastCommitsForTree: result-cache MISS, walking history");
857
+ const byOid = new Map(commits.map((commit) => [commit.oid, commit]));
858
+ const dirOidByCommitTree = /* @__PURE__ */ new Map();
859
+ const childrenByDirOid = /* @__PURE__ */ new Map();
860
+ async function resolveDirOid(commitTreeOid) {
861
+ const cached = dirOidByCommitTree.get(commitTreeOid);
862
+ if (cached !== void 0) return cached;
863
+ const entry = await findTreeEntry(repo, commitTreeOid, treePath);
864
+ const dirOid = entry?.type === "tree" ? entry.oid : null;
865
+ dirOidByCommitTree.set(commitTreeOid, dirOid);
866
+ return dirOid;
867
+ }
868
+ async function resolveChildren(dirOid) {
869
+ if (dirOid === null) return [];
870
+ const cached = childrenByDirOid.get(dirOid);
871
+ if (cached) return cached;
872
+ const children = await listTreeEntries(repo, dirOid, treePath);
873
+ childrenByDirOid.set(dirOid, children);
874
+ return children;
875
+ }
876
+ const headDirOid = await resolveDirOid(head.commit.tree);
877
+ if (headDirOid === null) return {};
878
+ const headChildren = await resolveChildren(headDirOid);
879
+ const remaining = new Set(headChildren.map((entry) => entry.path));
880
+ const result = {};
881
+ let commitsWalked = 0;
882
+ let prefetchWindows = 0;
883
+ const walkStart = performance.now();
884
+ outer: for (let windowStart = 0; windowStart < commits.length && remaining.size > 0; windowStart += PREFETCH_WINDOW2) {
885
+ const windowEnd = Math.min(windowStart + PREFETCH_WINDOW2, commits.length);
886
+ const prefetchEnd = Math.min(windowEnd + 1, commits.length);
887
+ prefetchWindows++;
888
+ const dirOids = await Promise.all(
889
+ commits.slice(windowStart, prefetchEnd).map((commit) => resolveDirOid(commit.commit.tree))
890
+ );
891
+ await Promise.all(
892
+ [...new Set(dirOids)].map((dirOid) => resolveChildren(dirOid))
893
+ );
894
+ for (let i = windowStart; i < windowEnd; i++) {
895
+ if (remaining.size === 0) break outer;
896
+ const commit = commits[i];
897
+ if (!commit) break outer;
898
+ commitsWalked++;
899
+ const parentSha = commit.commit.parent[0];
900
+ const parentCommit = parentSha ? byOid.get(parentSha) : void 0;
901
+ if (parentSha && !parentCommit) break outer;
902
+ const [dirOid, parentDirOid] = await Promise.all([
903
+ resolveDirOid(commit.commit.tree),
904
+ parentCommit ? resolveDirOid(parentCommit.commit.tree) : Promise.resolve(null)
905
+ ]);
906
+ if (dirOid === parentDirOid) continue;
907
+ const [children, parentChildren] = await Promise.all([
908
+ resolveChildren(dirOid),
909
+ resolveChildren(parentDirOid)
910
+ ]);
911
+ const childByName = new Map(
912
+ children.map((entry) => [entry.path, entry.oid])
913
+ );
914
+ const parentChildByName = new Map(
915
+ parentChildren.map((entry) => [entry.path, entry.oid])
916
+ );
917
+ for (const name of remaining) {
918
+ if (childByName.get(name) !== parentChildByName.get(name)) {
919
+ result[name] = toLastCommitInfo(commit);
920
+ }
921
+ }
922
+ for (const name of Object.keys(result)) {
923
+ remaining.delete(name);
924
+ }
925
+ }
926
+ }
927
+ hooks?.onNote?.(
928
+ `getLastCommitsForTree: walked ${commitsWalked}/${commits.length} commits across ${prefetchWindows} prefetch windows in ${(performance.now() - walkStart).toFixed(1)}ms, ${dirOidByCommitTree.size} unique tree lookups, ${remaining.size} entries never resolved`
929
+ );
930
+ hooks?.resultCache?.set(cacheKey, result);
931
+ return result;
932
+ }
933
+
934
+ // src/ops/merge.ts
935
+ var import_isomorphic_git6 = __toESM(require("isomorphic-git"), 1);
936
+ async function analyzeMerge(repo, sourceBranch, targetBranch) {
937
+ assertSafeBranchName(sourceBranch);
938
+ assertSafeBranchName(targetBranch);
939
+ try {
940
+ const [sourceOid, targetOid] = await Promise.all([
941
+ import_isomorphic_git6.default.resolveRef({ ...repo, ref: qualifyBranchRef(sourceBranch) }),
942
+ import_isomorphic_git6.default.resolveRef({ ...repo, ref: qualifyBranchRef(targetBranch) })
943
+ ]);
944
+ const isDescendant = await import_isomorphic_git6.default.isDescendent({
945
+ ...repo,
946
+ oid: sourceOid,
947
+ ancestor: targetOid
948
+ });
949
+ return {
950
+ canMerge: true,
951
+ hasConflicts: false,
952
+ conflictingFiles: [],
953
+ fastForward: isDescendant
954
+ };
955
+ } catch (err) {
956
+ if (err?.code !== "NotFoundError") {
957
+ throw err;
958
+ }
959
+ return {
960
+ canMerge: false,
961
+ hasConflicts: true,
962
+ conflictingFiles: [],
963
+ fastForward: false
964
+ };
965
+ }
966
+ }
967
+ async function fastForwardMerge(repo, sourceBranch, targetBranch) {
968
+ assertSafeBranchName(sourceBranch);
969
+ assertSafeBranchName(targetBranch);
970
+ const [sourceOid, targetOid] = await Promise.all([
971
+ import_isomorphic_git6.default.resolveRef({ ...repo, ref: `refs/heads/${sourceBranch}` }),
972
+ import_isomorphic_git6.default.resolveRef({ ...repo, ref: `refs/heads/${targetBranch}` })
973
+ ]);
974
+ const isFF = await import_isomorphic_git6.default.isDescendent({
975
+ ...repo,
976
+ oid: sourceOid,
977
+ ancestor: targetOid
978
+ });
979
+ if (!isFF) return null;
980
+ await import_isomorphic_git6.default.writeRef({
981
+ ...repo,
982
+ ref: `refs/heads/${targetBranch}`,
983
+ value: sourceOid,
984
+ force: true
985
+ });
986
+ return { success: true, commitSha: sourceOid };
987
+ }
988
+ // Annotate the CommonJS export names for ESM import in node:
989
+ 0 && (module.exports = {
990
+ BANNER_WALK_DEPTH,
991
+ HISTORY_WALK_DEPTH,
992
+ analyzeMerge,
993
+ assertBranchExists,
994
+ assertSafeBranchName,
995
+ authorNow,
996
+ commitFilesToBare,
997
+ createBranchFrom,
998
+ deleteBranchByName,
999
+ deleteFileFromBare,
1000
+ deleteFromTree,
1001
+ fastForwardMerge,
1002
+ findTreeEntry,
1003
+ getBlob,
1004
+ getCommit,
1005
+ getCommitDiff,
1006
+ getCommitHistory,
1007
+ getCommitLog,
1008
+ getDiffBetweenRefs,
1009
+ getFileContent,
1010
+ getFileFromRef,
1011
+ getFileHistory,
1012
+ getLastCommitsForTree,
1013
+ getTreeFromRef,
1014
+ listBranches,
1015
+ listTreeEntries,
1016
+ resolveCommit,
1017
+ resultKeyPrefixes,
1018
+ upsertTree,
1019
+ writeCommitToBare
1020
+ });
1021
+ //# sourceMappingURL=ops.cjs.map