git-fs-s3 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ops.js CHANGED
@@ -16,13 +16,10 @@ import {
16
16
  GitObjectNotFoundError,
17
17
  GitPathNotFoundError,
18
18
  GitRefNotFoundError,
19
- decodeUtf8,
20
- hasNullByte,
21
19
  inflate,
22
20
  isSafeTreePath,
23
21
  qualifyBranchRef,
24
- readBlobContent,
25
- toBase64
22
+ readBlobContent
26
23
  } from "./chunk-PASVUPKR.js";
27
24
 
28
25
  // src/ops/commit.ts
@@ -332,36 +329,49 @@ function resultKeyPrefixes(gitdir) {
332
329
  return [
333
330
  `commitlog:${gitdir}:`,
334
331
  `tree:${gitdir}:`,
335
- `commits:${gitdir}:`,
336
332
  `last-commits:${gitdir}:`,
337
333
  `file-history:${gitdir}:`
338
334
  ];
339
335
  }
340
336
 
341
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";
342
349
  function wrapMissingObject(promise, context) {
343
350
  return promise.catch((err) => {
344
- if (err?.code === "NotFoundError") {
351
+ if (isNotFound(err)) {
345
352
  throw new GitObjectNotFoundError(
346
- `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`
347
354
  );
348
355
  }
349
356
  throw err;
350
357
  });
351
358
  }
352
- var isNotFound = (err) => err?.code === "NotFoundError";
353
- async function resolveCommit(repo, ref) {
354
- const oid = await git3.resolveRef({ ...repo, ref: qualifyBranchRef(ref) }).catch((err) => {
355
- 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))
356
364
  throw new GitRefNotFoundError(`Ref ${ref} does not exist`);
357
- }
358
365
  throw err;
359
- });
360
- const result = await wrapMissingObject(
366
+ }
367
+ }
368
+ async function resolveCommit(repo, ref) {
369
+ const oid = await resolveRefOid(repo, ref);
370
+ const { commit } = await wrapMissingObject(
361
371
  git3.readCommit({ ...repo, oid }),
362
372
  `${repo.gitdir} commit ${oid}`
363
373
  );
364
- return { oid, commit: result.commit };
374
+ return { oid, commit };
365
375
  }
366
376
  async function getBlob(repo, sha) {
367
377
  const { blob } = await wrapMissingObject(
@@ -371,9 +381,7 @@ async function getBlob(repo, sha) {
371
381
  return blob;
372
382
  }
373
383
  async function getFileContent(repo, filePath, ref, hooks) {
374
- if (hooks?.prefetch) {
375
- await runStep(hooks, "prefetch", hooks.prefetch);
376
- }
384
+ if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
377
385
  const { commit } = await resolveCommit(repo, ref);
378
386
  const context = `${repo.gitdir}@${ref}:${filePath}`;
379
387
  const entry = await wrapMissingObject(
@@ -396,39 +404,28 @@ async function getCommit(repo, sha) {
396
404
  );
397
405
  return { oid: result.oid, commit: result.commit, payload: result.payload };
398
406
  }
399
- function isFullyWalked(commits) {
400
- const last = commits[commits.length - 1];
401
- return !!last && last.commit.parent.length === 0;
402
- }
403
407
  async function getCommitLog(repo, options, hooks) {
404
408
  const { ref } = options;
405
409
  const depth = options.depth ?? 50;
406
- let headSha;
407
- if (options.knownHeadSha) {
408
- headSha = options.knownHeadSha;
409
- } else {
410
+ let headSha = options.knownHeadSha;
411
+ if (headSha === void 0) {
410
412
  try {
411
- headSha = await git3.resolveRef({ ...repo, ref: qualifyBranchRef(ref) });
413
+ headSha = await resolveRefOid(repo, ref);
412
414
  } catch (err) {
413
- if (isNotFound(err)) return [];
415
+ if (err instanceof GitRefNotFoundError) return [];
414
416
  throw err;
415
417
  }
416
418
  }
417
419
  const cacheKey = `commitlog:${repo.gitdir}:${headSha}`;
418
420
  const cached = hooks?.resultCache?.get(cacheKey);
419
- if (cached && (cached.length >= depth || isFullyWalked(cached))) {
420
- hooks?.onNote?.(
421
- `getCommitLog: result-cache HIT for ${cacheKey} (depth=${depth})`
422
- );
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})`);
423
424
  return cached.slice(0, depth);
424
425
  }
425
- hooks?.onNote?.(
426
- `getCommitLog: result-cache MISS for ${cacheKey} (depth=${depth})`
427
- );
428
- if (hooks?.prefetch && depth >= (hooks.prefetchMinDepth ?? 5)) {
429
- await runStep(hooks, "prefetch", hooks.prefetch);
430
- }
431
- 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(
432
429
  runStep(
433
430
  hooks,
434
431
  `git.log ${ref} depth=${depth}`,
@@ -436,25 +433,16 @@ async function getCommitLog(repo, options, hooks) {
436
433
  ),
437
434
  `${repo.gitdir}@${ref} history`
438
435
  );
439
- const result = commits.map((commit) => ({
440
- oid: commit.oid,
441
- commit: commit.commit,
442
- payload: commit.payload || ""
436
+ const result = log.map(({ oid, commit, payload }) => ({
437
+ oid,
438
+ commit,
439
+ payload: payload ?? ""
443
440
  }));
444
441
  if (!cached || result.length > cached.length) {
445
442
  hooks?.resultCache?.set(cacheKey, result);
446
443
  }
447
444
  return result;
448
445
  }
449
- async function getFileFromRef(repo, filePath, ref, hooks) {
450
- const bytes = await getFileContent(repo, filePath, ref, hooks);
451
- const isBinary = hasNullByte(bytes);
452
- return {
453
- content: isBinary ? toBase64(bytes) : decodeUtf8(bytes),
454
- size: bytes.length,
455
- isBinary
456
- };
457
- }
458
446
  async function getTreeFromRef(repo, options, hooks) {
459
447
  const { ref } = options;
460
448
  const treePath = options.treePath ?? "";
@@ -462,227 +450,143 @@ async function getTreeFromRef(repo, options, hooks) {
462
450
  const cacheKey = `tree:${repo.gitdir}:${headSha}:${treePath}`;
463
451
  const cached = hooks?.resultCache?.get(cacheKey);
464
452
  if (cached) {
465
- hooks?.onNote?.(`getTreeFromRef: result-cache HIT for ${cacheKey}`);
453
+ hooks?.onNote?.(`getTreeFromRef: cache hit ${cacheKey}`);
466
454
  return cached;
467
455
  }
468
- hooks?.onNote?.(`getTreeFromRef: result-cache MISS for ${cacheKey}`);
469
- if (hooks?.prefetch) {
470
- await runStep(hooks, "prefetch", hooks.prefetch);
471
- }
456
+ hooks?.onNote?.(`getTreeFromRef: cache miss ${cacheKey}`);
457
+ if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
472
458
  const context = `${repo.gitdir}@${ref}:${treePath || "/"}`;
473
- let result;
474
- if (!treePath) {
475
- result = await wrapMissingObject(
476
- runStep(
477
- hooks,
478
- "listTreeEntries (root)",
479
- () => listTreeEntries(repo, commit.tree)
480
- ),
481
- context
482
- );
483
- } else {
484
- const entry = await wrapMissingObject(
485
- runStep(
486
- hooks,
487
- `findTreeEntry ${treePath}`,
488
- () => findTreeEntry(repo, commit.tree, treePath)
489
- ),
490
- context
491
- );
492
- if (!entry) {
493
- throw new GitPathNotFoundError(
494
- `Path "${treePath}" does not exist at ${ref}`
495
- );
496
- }
497
- result = entry.type !== "tree" ? [] : await wrapMissingObject(
498
- runStep(
499
- hooks,
500
- `listTreeEntries ${treePath}`,
501
- () => listTreeEntries(repo, entry.oid, entry.path)
502
- ),
503
- 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}`
504
470
  );
505
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
+ ) : [];
506
480
  hooks?.resultCache?.set(cacheKey, result);
507
481
  return result;
508
482
  }
509
483
  async function getCommitHistory(repo, options, hooks) {
510
484
  const limit = options.limit ?? 50;
511
485
  const skip = options.skip ?? 0;
512
- const headSha = await git3.resolveRef({ ...repo, ref: qualifyBranchRef(options.ref) }).catch(() => null);
513
- const cacheKey = headSha ? `commits:${repo.gitdir}:${headSha}:${limit}:${skip}` : null;
514
- if (cacheKey) {
515
- const cached = hooks?.resultCache?.get(cacheKey);
516
- if (cached) {
517
- hooks?.onNote?.(`getCommitHistory: result-cache HIT for ${cacheKey}`);
518
- return cached;
519
- }
520
- }
521
- hooks?.onNote?.(
522
- `getCommitHistory: result-cache MISS for ${cacheKey ?? "(no head)"}`
523
- );
524
- if (!headSha) return [];
525
- const all = await getCommitLog(
486
+ const log = await getCommitLog(
526
487
  repo,
527
- { ref: options.ref, depth: limit + skip, knownHeadSha: headSha },
488
+ { ref: options.ref, depth: limit + skip },
528
489
  hooks
529
490
  );
530
- const result = all.slice(skip, skip + limit);
531
- if (cacheKey) hooks?.resultCache?.set(cacheKey, result);
532
- return result;
491
+ return log.slice(skip, skip + limit);
533
492
  }
534
493
 
535
494
  // src/ops/diff.ts
536
495
  var MAX_DIFF_TEXT_BYTES = 1024 * 1024;
537
- function detectBlobContent(blob) {
538
- return readBlobContent(blob);
539
- }
540
496
  function countContentLines(content) {
541
497
  if (content.length === 0) return 0;
542
498
  const lines = content.split("\n");
543
- if (lines[lines.length - 1] === "") lines.pop();
499
+ if (lines.at(-1) === "") lines.pop();
544
500
  return lines.length;
545
501
  }
546
- function createUnifiedPatch(params) {
547
- const oldPath = params.oldPath ?? `a/${params.path}`;
548
- const newPath = params.newPath ?? `b/${params.path}`;
549
- const patchBody = createTwoFilesPatch(
550
- oldPath,
551
- newPath,
552
- params.before,
553
- 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 ?? "",
554
508
  "",
555
509
  "",
556
510
  { context: 3 }
557
511
  ).replace(/^=+\n/, "");
558
- return `diff --git a/${params.path} b/${params.path}
559
- ${patchBody}`;
512
+ return `diff --git a/${path} b/${path}
513
+ ${body}`;
560
514
  }
561
- function summarizeDiff(files) {
562
- return {
563
- files,
564
- totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
565
- totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
566
- totalFiles: files.length
567
- };
568
- }
569
- function buildDiffFile(path, status, before, after) {
515
+ function buildDiffFile(path, before, after) {
570
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";
571
517
  const isText = kind === "text";
572
518
  return {
573
519
  path,
574
- status,
520
+ status: !before ? "added" : !after ? "deleted" : "modified",
575
521
  kind,
576
522
  additions: isText && after ? countContentLines(after.text) : 0,
577
523
  deletions: isText && before ? countContentLines(before.text) : 0,
578
- patch: isText ? createUnifiedPatch({
579
- path,
580
- before: before?.text ?? "",
581
- after: after?.text ?? "",
582
- oldPath: before ? void 0 : "/dev/null",
583
- newPath: after ? void 0 : "/dev/null"
584
- }) : "",
524
+ patch: isText ? unifiedPatch(path, before, after) : "",
585
525
  oldSize: before?.bytes.length,
586
526
  newSize: after?.bytes.length
587
527
  };
588
528
  }
589
- 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 })];
590
536
  const changes = await git4.walk({
591
537
  ...repo,
592
- trees: [git4.TREE({ ref: oldOid }), git4.TREE({ ref: newOid })],
593
- map: async (filepath, [A, B]) => {
594
- const [typeA, typeB] = await Promise.all([A?.type(), B?.type()]);
595
- if (typeA === "tree" || typeB === "tree") return;
596
- if (typeA && !typeB) {
597
- const oidA2 = A ? await A.oid() : "";
598
- const { blob } = await git4.readBlob({ ...repo, oid: oidA2 });
599
- const before = detectBlobContent(blob);
600
- return buildDiffFile(filepath, "deleted", before, null);
601
- }
602
- if (!typeA && typeB) {
603
- const oidB2 = B ? await B.oid() : "";
604
- const { blob } = await git4.readBlob({ ...repo, oid: oidB2 });
605
- const after = detectBlobContent(blob);
606
- return buildDiffFile(filepath, "added", null, after);
607
- }
608
- const [oidA, oidB] = await Promise.all([
609
- A ? A.oid() : Promise.resolve(""),
610
- 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()
611
544
  ]);
612
- if (oidA !== oidB) {
613
- const [{ blob: blobA }, { blob: blobB }] = await Promise.all([
614
- git4.readBlob({ ...repo, oid: oidA }),
615
- 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()
616
550
  ]);
617
- const before = detectBlobContent(blobA);
618
- const after = detectBlobContent(blobB);
619
- return buildDiffFile(filepath, "modified", before, after);
551
+ if (oldOid === newOid) return void 0;
620
552
  }
621
- 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;
622
558
  }
623
559
  });
624
- return (changes ?? []).filter(
625
- (c) => c !== null && c !== void 0
626
- );
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
+ };
627
567
  }
628
568
  async function getCommitDiff(repo, commitSha) {
629
- try {
630
- const commit = await getCommit(repo, commitSha);
631
- const parent = commit.commit.parent[0];
632
- if (!parent) {
633
- const entries = [];
634
- const stack = [
635
- { treeOid: commit.commit.tree, prefix: "" }
636
- ];
637
- while (stack.length) {
638
- const { treeOid, prefix } = stack.pop();
639
- const { tree } = await git4.readTree({ ...repo, oid: treeOid });
640
- for (const entry of tree) {
641
- const full = prefix ? `${prefix}/${entry.path}` : entry.path;
642
- if (entry.type === "tree") {
643
- stack.push({ treeOid: entry.oid, prefix: full });
644
- } else if (entry.type === "blob") {
645
- entries.push({ path: full, oid: entry.oid });
646
- }
647
- }
648
- }
649
- const files2 = await Promise.all(
650
- entries.map(async ({ path, oid }) => {
651
- const { blob } = await git4.readBlob({ ...repo, oid });
652
- const after = detectBlobContent(blob);
653
- return buildDiffFile(path, "added", null, after);
654
- })
655
- );
656
- return summarizeDiff(files2);
657
- }
658
- const files = await walkTreeDiff(repo, parent, commitSha);
659
- return summarizeDiff(files);
660
- } catch (error) {
661
- throw new Error(`Failed to get commit diff: ${error}`);
662
- }
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);
663
573
  }
664
574
  async function getDiffBetweenRefs(repo, baseRef, compareRef) {
665
- const [baseOid, compareOid] = await Promise.all([
666
- git4.resolveRef({ ...repo, ref: qualifyBranchRef(baseRef) }),
667
- git4.resolveRef({ ...repo, ref: qualifyBranchRef(compareRef) })
575
+ const [base, compare] = await Promise.all([
576
+ resolveCommit(repo, baseRef),
577
+ resolveCommit(repo, compareRef)
668
578
  ]);
669
- const files = await walkTreeDiff(repo, baseOid, compareOid);
670
- 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);
671
585
  }
672
586
 
673
587
  // src/ops/file-history.ts
674
- var HISTORY_WALK_DEPTH = 400;
675
588
  var BANNER_WALK_DEPTH = 60;
676
589
  var PREFETCH_WINDOW = 24;
677
- function toEntry(commit) {
678
- return {
679
- sha: commit.oid,
680
- message: commit.commit.message.trim(),
681
- authorName: commit.commit.author.name,
682
- authorEmail: commit.commit.author.email,
683
- createdAt: new Date(commit.commit.author.timestamp * 1e3).toISOString()
684
- };
685
- }
686
590
  async function getFileHistory(repo, options, hooks) {
687
591
  const limit = options.limit ?? 30;
688
592
  const maxDepth = options.maxDepth ?? HISTORY_WALK_DEPTH;
@@ -733,7 +637,7 @@ async function getFileHistory(repo, options, hooks) {
733
637
  parentCommit ? resolveOid(parentCommit.commit.tree) : Promise.resolve(null)
734
638
  ]);
735
639
  if (oid !== parentOid) {
736
- entries.push(toEntry(commit));
640
+ entries.push(summarizeCommit(commit));
737
641
  if (entries.length >= limit) {
738
642
  truncated = i < commits.length - 1;
739
643
  break outer;
@@ -747,20 +651,10 @@ async function getFileHistory(repo, options, hooks) {
747
651
  }
748
652
 
749
653
  // src/ops/last-commit.ts
750
- var HISTORY_WALK_DEPTH2 = 400;
751
654
  var PREFETCH_WINDOW2 = 24;
752
- function toLastCommitInfo(commit) {
753
- return {
754
- sha: commit.oid,
755
- message: commit.commit.message.trim(),
756
- authorName: commit.commit.author.name,
757
- authorEmail: commit.commit.author.email,
758
- createdAt: new Date(commit.commit.author.timestamp * 1e3).toISOString()
759
- };
760
- }
761
655
  async function getLastCommitsForTree(repo, options, hooks) {
762
656
  const treePath = options.treePath ?? "";
763
- const depth = options.depth ?? HISTORY_WALK_DEPTH2;
657
+ const depth = options.depth ?? HISTORY_WALK_DEPTH;
764
658
  const commits = await runStep(
765
659
  hooks,
766
660
  `getCommitLog depth=${depth}`,
@@ -840,7 +734,7 @@ async function getLastCommitsForTree(repo, options, hooks) {
840
734
  );
841
735
  for (const name of remaining) {
842
736
  if (childByName.get(name) !== parentChildByName.get(name)) {
843
- result[name] = toLastCommitInfo(commit);
737
+ result[name] = summarizeCommit(commit);
844
738
  }
845
739
  }
846
740
  for (const name of Object.keys(result)) {
@@ -934,7 +828,6 @@ export {
934
828
  getCommitLog,
935
829
  getDiffBetweenRefs,
936
830
  getFileContent,
937
- getFileFromRef,
938
831
  getFileHistory,
939
832
  getLastCommitsForTree,
940
833
  getTreeFromRef,
@@ -943,6 +836,7 @@ export {
943
836
  resolveCommit,
944
837
  resolveLooseRefFast,
945
838
  resultKeyPrefixes,
839
+ summarizeCommit,
946
840
  upsertTree,
947
841
  withPackedWrites,
948
842
  writeCommitToBare