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