git-fs-s3 0.8.0 → 0.10.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,189 @@ 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 readDiffEntry(entry) {
530
+ return entry ? {
531
+ type: await entry.type(),
532
+ oid: await entry.oid(),
533
+ mode: await entry.mode()
534
+ } : void 0;
535
+ }
536
+ function buildSubmoduleDiff(filepath, before, after) {
537
+ if (before?.type === after?.type && before?.oid === after?.oid)
538
+ return void 0;
539
+ const line = (entry) => entry ? `${entry.type === "commit" ? "Subproject commit" : entry.type === "tree" ? "Directory" : "File"} ${entry.oid}
540
+ ` : "";
541
+ return {
542
+ path: filepath,
543
+ status: !before ? "added" : !after ? "deleted" : "modified",
544
+ kind: "submodule",
545
+ additions: 0,
546
+ deletions: 0,
547
+ oldEntry: before,
548
+ newEntry: after,
549
+ patch: createTwoFilesPatch(
550
+ before ? `a/${filepath}` : "/dev/null",
551
+ after ? `b/${filepath}` : "/dev/null",
552
+ line(before),
553
+ line(after),
554
+ "",
555
+ "",
556
+ { context: 3 }
557
+ )
558
+ };
559
+ }
560
+ async function diffTrees(repo, oldTreeOid, newTreeOid) {
561
+ const readSide = async (entry) => {
562
+ if (!entry || await entry.type() !== "blob") return null;
563
+ const { blob } = await git4.readBlob({ ...repo, oid: await entry.oid() });
564
+ return readBlobContent(blob);
565
+ };
566
+ const trees = oldTreeOid ? [git4.TREE({ ref: oldTreeOid }), git4.TREE({ ref: newTreeOid })] : [git4.TREE({ ref: newTreeOid })];
590
567
  const changes = await git4.walk({
591
568
  ...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("")
569
+ trees,
570
+ map: async (filepath, entries) => {
571
+ const [oldEntry, newEntry] = oldTreeOid ? [entries[0] ?? null, entries[1] ?? null] : [null, entries[0] ?? null];
572
+ const [oldType, newType] = await Promise.all([
573
+ oldEntry?.type(),
574
+ newEntry?.type()
611
575
  ]);
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 })
576
+ if (oldType === "commit" || newType === "commit") {
577
+ const [before2, after2] = await Promise.all([
578
+ readDiffEntry(oldEntry),
579
+ readDiffEntry(newEntry)
580
+ ]);
581
+ return buildSubmoduleDiff(filepath, before2, after2);
582
+ }
583
+ if (oldType !== "blob" && newType !== "blob") return void 0;
584
+ if (oldEntry && newEntry && oldType === newType) {
585
+ const [oldOid, newOid] = await Promise.all([
586
+ oldEntry.oid(),
587
+ newEntry.oid()
616
588
  ]);
617
- const before = detectBlobContent(blobA);
618
- const after = detectBlobContent(blobB);
619
- return buildDiffFile(filepath, "modified", before, after);
589
+ if (oldOid === newOid) return void 0;
620
590
  }
621
- return null;
591
+ const [before, after] = await Promise.all([
592
+ readSide(oldEntry),
593
+ readSide(newEntry)
594
+ ]);
595
+ return before || after ? buildDiffFile(filepath, before, after) : void 0;
622
596
  }
623
597
  });
624
- return (changes ?? []).filter(
625
- (c) => c !== null && c !== void 0
626
- );
598
+ const files = (changes ?? []).filter((file) => !!file);
599
+ return {
600
+ files,
601
+ totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
602
+ totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
603
+ totalFiles: files.length
604
+ };
627
605
  }
628
606
  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
- }
607
+ const { commit } = await getCommit(repo, commitSha);
608
+ const parent = commit.parent[0];
609
+ const parentTree = parent ? (await getCommit(repo, parent)).commit.tree : null;
610
+ return {
611
+ ...await diffTrees(repo, parentTree, commit.tree),
612
+ oldRef: parent,
613
+ newRef: commitSha
614
+ };
663
615
  }
664
616
  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) })
617
+ const [base, compare] = await Promise.all([
618
+ resolveCommit(repo, baseRef),
619
+ resolveCommit(repo, compareRef)
668
620
  ]);
669
- const files = await walkTreeDiff(repo, baseOid, compareOid);
670
- return summarizeDiff(files);
621
+ const [mergeBase] = await git4.findMergeBase({
622
+ ...repo,
623
+ oids: [base.oid, compare.oid]
624
+ });
625
+ const baseTree = mergeBase === void 0 ? null : mergeBase === base.oid ? base.commit.tree : (await getCommit(repo, mergeBase)).commit.tree;
626
+ return {
627
+ ...await diffTrees(repo, baseTree, compare.commit.tree),
628
+ oldRef: mergeBase,
629
+ newRef: compare.oid
630
+ };
671
631
  }
672
632
 
673
633
  // src/ops/file-history.ts
674
- var HISTORY_WALK_DEPTH = 400;
675
634
  var BANNER_WALK_DEPTH = 60;
676
635
  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
636
  async function getFileHistory(repo, options, hooks) {
687
637
  const limit = options.limit ?? 30;
688
638
  const maxDepth = options.maxDepth ?? HISTORY_WALK_DEPTH;
@@ -707,7 +657,7 @@ async function getFileHistory(repo, options, hooks) {
707
657
  const cachedOid = oidByCommitTree.get(commitTreeOid);
708
658
  if (cachedOid !== void 0) return cachedOid;
709
659
  const entry = await findTreeEntry(repo, commitTreeOid, options.filePath);
710
- const oid = entry?.type === "blob" ? entry.oid : null;
660
+ const oid = entry && entry.type !== "tree" ? entry.oid : null;
711
661
  oidByCommitTree.set(commitTreeOid, oid);
712
662
  return oid;
713
663
  }
@@ -733,7 +683,7 @@ async function getFileHistory(repo, options, hooks) {
733
683
  parentCommit ? resolveOid(parentCommit.commit.tree) : Promise.resolve(null)
734
684
  ]);
735
685
  if (oid !== parentOid) {
736
- entries.push(toEntry(commit));
686
+ entries.push(summarizeCommit(commit));
737
687
  if (entries.length >= limit) {
738
688
  truncated = i < commits.length - 1;
739
689
  break outer;
@@ -747,20 +697,10 @@ async function getFileHistory(repo, options, hooks) {
747
697
  }
748
698
 
749
699
  // src/ops/last-commit.ts
750
- var HISTORY_WALK_DEPTH2 = 400;
751
700
  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
701
  async function getLastCommitsForTree(repo, options, hooks) {
762
702
  const treePath = options.treePath ?? "";
763
- const depth = options.depth ?? HISTORY_WALK_DEPTH2;
703
+ const depth = options.depth ?? HISTORY_WALK_DEPTH;
764
704
  const commits = await runStep(
765
705
  hooks,
766
706
  `getCommitLog depth=${depth}`,
@@ -840,7 +780,7 @@ async function getLastCommitsForTree(repo, options, hooks) {
840
780
  );
841
781
  for (const name of remaining) {
842
782
  if (childByName.get(name) !== parentChildByName.get(name)) {
843
- result[name] = toLastCommitInfo(commit);
783
+ result[name] = summarizeCommit(commit);
844
784
  }
845
785
  }
846
786
  for (const name of Object.keys(result)) {
@@ -912,6 +852,97 @@ async function fastForwardMerge(repo, sourceBranch, targetBranch) {
912
852
  });
913
853
  return { success: true, commitSha: sourceOid };
914
854
  }
855
+
856
+ // src/ops/submodules.ts
857
+ function parseValue(raw) {
858
+ let value = "";
859
+ let spaces = "";
860
+ let quoted = false;
861
+ const escapes = {
862
+ n: "\n",
863
+ t: " ",
864
+ b: "\b",
865
+ "\\": "\\",
866
+ '"': '"'
867
+ };
868
+ for (let i = 0; i < raw.length; i++) {
869
+ const c = raw.charAt(i);
870
+ if (c === "\\") {
871
+ const escaped = escapes[raw.charAt(++i)];
872
+ if (escaped === void 0) return null;
873
+ value += spaces + escaped;
874
+ spaces = "";
875
+ } else if (c === '"') {
876
+ value += spaces;
877
+ spaces = "";
878
+ quoted = !quoted;
879
+ } else if (!quoted && (c === "#" || c === ";")) break;
880
+ else if (!quoted && /\s/.test(c)) {
881
+ if (value) spaces += c;
882
+ } else {
883
+ value += spaces + c;
884
+ spaces = "";
885
+ }
886
+ }
887
+ return quoted ? null : value;
888
+ }
889
+ function parseGitmodules(content) {
890
+ const sections = /* @__PURE__ */ new Map();
891
+ let current;
892
+ const lines = [];
893
+ let pending = "";
894
+ for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
895
+ const slashes = /\\+$/.exec(line)?.[0].length ?? 0;
896
+ if (slashes % 2 === 1) pending += line.slice(0, -1);
897
+ else {
898
+ lines.push(pending + line);
899
+ pending = "";
900
+ }
901
+ }
902
+ for (const line of lines) {
903
+ const trimmed = line.trim();
904
+ if (!trimmed || /^[#;]/.test(trimmed)) continue;
905
+ if (trimmed.startsWith("[")) {
906
+ const match = /^\[submodule\s+"((?:[^"\\]|\\["\\])*)"\]\s*(?:[#;].*)?$/i.exec(
907
+ trimmed
908
+ );
909
+ current = void 0;
910
+ if (!match?.[1]) continue;
911
+ const name = match[1].replace(/\\(["\\])/g, "$1");
912
+ current = sections.get(name) ?? { name };
913
+ sections.set(name, current);
914
+ continue;
915
+ }
916
+ if (!current) continue;
917
+ const field = /^(path|url)\s*=\s*(.*)$/i.exec(trimmed);
918
+ if (!field?.[1] || field[2] === void 0) continue;
919
+ const key = field[1].toLowerCase();
920
+ const value = parseValue(field[2]);
921
+ if (value === null) delete current[key];
922
+ else current[key] = value;
923
+ }
924
+ const valid = [...sections.values()].filter(
925
+ (s) => !!s.name && !!s.path && !!s.url && ![...s.path + s.url].some(
926
+ (c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127
927
+ ) && !s.path.startsWith("/") && !s.path.includes("\\") && s.path.split("/").every(
928
+ (p) => p !== "" && p !== "." && p !== ".." && p.toLowerCase() !== ".git"
929
+ )
930
+ );
931
+ const counts = /* @__PURE__ */ new Map();
932
+ for (const s of valid) counts.set(s.path, (counts.get(s.path) ?? 0) + 1);
933
+ return valid.filter((s) => counts.get(s.path) === 1);
934
+ }
935
+ async function getSubmodules(repo, ref, hooks) {
936
+ let bytes;
937
+ try {
938
+ bytes = await getFileContent(repo, ".gitmodules", ref, hooks);
939
+ } catch (error) {
940
+ if (error instanceof GitPathNotFoundError) return [];
941
+ throw error;
942
+ }
943
+ if (bytes.length > 1024 * 1024) return [];
944
+ return parseGitmodules(new TextDecoder().decode(bytes));
945
+ }
915
946
  export {
916
947
  BANNER_WALK_DEPTH,
917
948
  HISTORY_WALK_DEPTH,
@@ -934,15 +965,17 @@ export {
934
965
  getCommitLog,
935
966
  getDiffBetweenRefs,
936
967
  getFileContent,
937
- getFileFromRef,
938
968
  getFileHistory,
939
969
  getLastCommitsForTree,
970
+ getSubmodules,
940
971
  getTreeFromRef,
941
972
  listBranches,
942
973
  listTreeEntries,
974
+ parseGitmodules,
943
975
  resolveCommit,
944
976
  resolveLooseRefFast,
945
977
  resultKeyPrefixes,
978
+ summarizeCommit,
946
979
  upsertTree,
947
980
  withPackedWrites,
948
981
  writeCommitToBare