git-fs-s3 0.6.0 → 0.8.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/http.js CHANGED
@@ -1,23 +1,26 @@
1
1
  import {
2
- packFileName,
3
- parsePackDirectory
4
- } from "./chunk-RMAOPWHJ.js";
5
- import {
2
+ encodePack,
3
+ hashGitObject,
6
4
  listBranches,
7
5
  listLooseRefNames,
8
- resolveLooseRefFast
9
- } from "./chunk-XNSZTA4Y.js";
6
+ packBaseName,
7
+ rawFs,
8
+ resolveLooseRefFast,
9
+ runStep,
10
+ storePack
11
+ } from "./chunk-HBOJR5D3.js";
10
12
  import {
11
13
  concat,
12
14
  decodeAscii,
13
15
  decodeUtf8,
14
- deflate,
15
16
  encodeUtf8,
16
17
  fromHex,
17
18
  isSafeFullRefName,
19
+ packFileName,
20
+ parsePackDirectory,
18
21
  sha1,
19
22
  toHex
20
- } from "./chunk-FIFWOMPM.js";
23
+ } from "./chunk-PASVUPKR.js";
21
24
 
22
25
  // src/http/info-refs.ts
23
26
  import git from "isomorphic-git";
@@ -66,18 +69,6 @@ function sideBandPackfile(packData) {
66
69
  return concat(...parts);
67
70
  }
68
71
 
69
- // src/http/types.ts
70
- var runStep = (hooks, label, fn) => hooks?.step ? hooks.step(label, fn) : fn();
71
- function rawFs(repo) {
72
- const fs = repo.fs;
73
- if (!fs?.promises) {
74
- throw new TypeError(
75
- "This operation needs a promise fs (`fs.promises`) \u2014 node:fs and createGitFs both provide one"
76
- );
77
- }
78
- return fs.promises;
79
- }
80
-
81
72
  // src/http/info-refs.ts
82
73
  async function listAllRefs(repo, defaultBranch) {
83
74
  const [branches, tagNames] = await Promise.all([
@@ -114,7 +105,7 @@ async function handleInfoRefs(repo, options, hooks) {
114
105
  () => listAllRefs(repo, options.defaultBranch)
115
106
  );
116
107
  const isUpload = service === "git-upload-pack";
117
- const caps = isUpload ? `no-progress side-band-64k symref=HEAD:${headSymref} allow-tip-sha1-in-want allow-reachable-sha1-in-want agent=${agent}` : `delete-refs report-status no-done agent=${agent}`;
108
+ const caps = isUpload ? `no-progress side-band-64k symref=HEAD:${headSymref} allow-tip-sha1-in-want allow-reachable-sha1-in-want agent=${agent}` : `report-status delete-refs side-band-64k agent=${agent}`;
118
109
  const parts = [pktLine(`# service=${service}
119
110
  `), FLUSH];
120
111
  if (refs.length === 0) {
@@ -203,6 +194,45 @@ async function collectReachableOids(repo, startOids, hooks, haveOids, options) {
203
194
  await Promise.all(startOids.map((oid) => visit(oid)));
204
195
  return { oids: Array.from(seen), complete };
205
196
  }
197
+ async function collectCommitAncestry(repo, startOids, stopAt) {
198
+ const commits = /* @__PURE__ */ new Set();
199
+ const boundary = /* @__PURE__ */ new Set();
200
+ const queued = /* @__PURE__ */ new Set();
201
+ let frontier = [...new Set(startOids)];
202
+ const BATCH = 50;
203
+ while (frontier.length > 0) {
204
+ const next = [];
205
+ for (let i = 0; i < frontier.length; i += BATCH) {
206
+ await Promise.all(
207
+ frontier.slice(i, i + BATCH).map(async (oid) => {
208
+ if (stopAt?.has(oid)) {
209
+ boundary.add(oid);
210
+ return;
211
+ }
212
+ const object = await git2.readObject({ ...repo, oid }).catch(() => null);
213
+ if (object?.type === "tag") {
214
+ const target = object.object.object;
215
+ if (!queued.has(target)) {
216
+ queued.add(target);
217
+ next.push(target);
218
+ }
219
+ return;
220
+ }
221
+ if (object?.type !== "commit") return;
222
+ commits.add(oid);
223
+ for (const parent of object.object.parent) {
224
+ if (!queued.has(parent)) {
225
+ queued.add(parent);
226
+ next.push(parent);
227
+ }
228
+ }
229
+ })
230
+ );
231
+ }
232
+ frontier = next;
233
+ }
234
+ return { commits, boundary };
235
+ }
206
236
 
207
237
  // src/http/receive-pack.ts
208
238
  import git4 from "isomorphic-git";
@@ -370,30 +400,6 @@ async function buildPackFromExistingPacks(repo, oids) {
370
400
  }
371
401
 
372
402
  // src/http/repack.ts
373
- var PACK_OBJECT_TYPE_BITS = {
374
- commit: 16,
375
- tree: 32,
376
- blob: 48,
377
- tag: 64
378
- };
379
- function encodePackObjectHeader(type, length) {
380
- const bytes = [];
381
- let more = length > 15;
382
- bytes.push(
383
- (more ? 128 : 0) | PACK_OBJECT_TYPE_BITS[type] | length & 15
384
- );
385
- length >>>= 4;
386
- while (more) {
387
- more = length > 127;
388
- bytes.push((more ? 128 : 0) | length & 127);
389
- length >>>= 7;
390
- }
391
- return new Uint8Array(bytes);
392
- }
393
- async function hashGitObject(type, content) {
394
- const prefix = encodeUtf8(`${type} ${content.length}\0`);
395
- return sha1(concat(prefix, content));
396
- }
397
403
  async function readAndVerifyObjects(repo, oids) {
398
404
  const objects = /* @__PURE__ */ new Map();
399
405
  const BATCH_SIZE = 100;
@@ -422,36 +428,6 @@ async function readAndVerifyObjects(repo, oids) {
422
428
  }
423
429
  return objects;
424
430
  }
425
- async function buildVerifiedPack(oids, objects) {
426
- const header = new Uint8Array(12);
427
- const view = new DataView(header.buffer);
428
- header.set(encodeUtf8("PACK"), 0);
429
- view.setUint32(4, 2, false);
430
- view.setUint32(8, oids.length, false);
431
- const chunks = [header];
432
- const BATCH_SIZE = 100;
433
- for (let i = 0; i < oids.length; i += BATCH_SIZE) {
434
- const batch = oids.slice(i, i + BATCH_SIZE);
435
- const encoded = await Promise.all(
436
- batch.map(async (oid) => {
437
- const entry = objects.get(oid);
438
- if (!entry) {
439
- throw new Error(`repack: missing verified object for ${oid}`);
440
- }
441
- const objHeader = encodePackObjectHeader(
442
- entry.type,
443
- entry.content.length
444
- );
445
- const compressed = await deflate(entry.content);
446
- return concat(objHeader, compressed);
447
- })
448
- );
449
- chunks.push(...encoded);
450
- }
451
- const packBody = concat(...chunks);
452
- const trailer = fromHex(await sha1(packBody));
453
- return concat(packBody, trailer);
454
- }
455
431
  var REPACK_PACK_COUNT_THRESHOLD = 4;
456
432
  async function readPackDirectory(repo) {
457
433
  const entries = await rawFs(repo).readdir(`${repo.gitdir}/objects/pack`).catch(() => []);
@@ -485,17 +461,30 @@ async function repackRepository(repo, options, hooks) {
485
461
  const { oids: reachable, complete } = await collectReachableOids(
486
462
  repo,
487
463
  tipOids,
488
- hooks
464
+ hooks,
465
+ void 0,
466
+ { verifyBlobs: false }
489
467
  );
490
468
  if (!complete || reachable.length === 0) return [];
491
469
  const packDir = `${repo.gitdir}/objects/pack`;
492
- const packBuffer = await buildPackFromExistingPacks(repo, reachable) ?? await buildVerifiedPack(
493
- reachable,
494
- await readAndVerifyObjects(repo, reachable)
495
- );
470
+ const packBuffer = await buildPackFromExistingPacks(repo, reachable) ?? await (async () => {
471
+ const objects = await readAndVerifyObjects(repo, reachable);
472
+ return encodePack(
473
+ reachable.map((oid) => {
474
+ const object = objects.get(oid);
475
+ if (!object) {
476
+ throw new Error(`repack: missing verified object for ${oid}`);
477
+ }
478
+ return object;
479
+ })
480
+ );
481
+ })();
496
482
  await fsp.mkdir(packDir, { recursive: true });
497
- const newBase = `pack-${Date.now()}`;
483
+ const newBase = packBaseName(packBuffer);
498
484
  const newPackFile = packFileName(newBase, "pack");
485
+ const preexisting = (await readPackDirectory(repo)).some(
486
+ (file) => file.base === newBase
487
+ );
499
488
  await fsp.writeFile(`${packDir}/${newPackFile}`, packBuffer);
500
489
  const { oids: indexedOids } = await git3.indexPack({
501
490
  ...repo,
@@ -504,12 +493,13 @@ async function repackRepository(repo, options, hooks) {
504
493
  });
505
494
  const indexed = new Set(indexedOids);
506
495
  if (reachable.some((oid) => !indexed.has(oid))) {
507
- await Promise.all(
508
- ["pack", "idx"].map(
509
- (kind) => fsp.unlink(`${packDir}/${packFileName(newBase, kind)}`).catch(() => {
510
- })
511
- )
512
- );
496
+ if (!preexisting)
497
+ await Promise.all(
498
+ ["pack", "idx"].map(
499
+ (kind) => fsp.unlink(`${packDir}/${packFileName(newBase, kind)}`).catch(() => {
500
+ })
501
+ )
502
+ );
513
503
  throw new Error(
514
504
  "repack: indexed pack is missing reachable objects \u2014 aborting"
515
505
  );
@@ -517,12 +507,14 @@ async function repackRepository(repo, options, hooks) {
517
507
  const stale = (await readPackDirectory(repo)).filter(
518
508
  (file) => file.base !== newBase
519
509
  );
520
- await Promise.all(
521
- stale.map(
510
+ const unlinkAll = (files) => Promise.all(
511
+ files.map(
522
512
  (file) => fsp.unlink(`${packDir}/${file.name}`).catch(() => {
523
513
  })
524
514
  )
525
515
  );
516
+ await unlinkAll(stale.filter((file) => file.kind === "idx"));
517
+ await unlinkAll(stale.filter((file) => file.kind !== "idx"));
526
518
  return stale.map((file) => `objects/pack/${file.name}`);
527
519
  } catch (err) {
528
520
  hooks?.onWarn?.("repack failed (non-fatal)", err);
@@ -532,8 +524,10 @@ async function repackRepository(repo, options, hooks) {
532
524
 
533
525
  // src/http/receive-pack.ts
534
526
  var ZERO_OID = "0".repeat(40);
527
+ var OID_RE = /^[0-9a-f]{40}$/;
535
528
  function parseReceivePackBody(body) {
536
529
  const refUpdates = [];
530
+ let capabilities;
537
531
  let pos = 0;
538
532
  while (pos + 4 <= body.length) {
539
533
  const len = Number.parseInt(decodeAscii(body.subarray(pos, pos + 4)), 16);
@@ -542,15 +536,21 @@ function parseReceivePackBody(body) {
542
536
  break;
543
537
  }
544
538
  if (len < 4) break;
545
- const line = decodeUtf8(body.subarray(pos + 4, pos + len)).replace(/\n$/, "").split("\0")[0];
539
+ const [line, caps] = decodeUtf8(body.subarray(pos + 4, pos + len)).replace(/\n$/, "").split("\0");
540
+ if (capabilities === void 0) {
541
+ capabilities = (caps ?? "").split(" ").filter(Boolean);
542
+ }
546
543
  pos += len;
547
- const parts = (line ?? "").split(" ");
548
- const [oldOid, newOid, refName] = parts;
549
- if (oldOid && newOid && refName) {
544
+ const [oldOid, newOid, refName] = (line ?? "").split(" ");
545
+ if (oldOid && newOid && refName && OID_RE.test(oldOid) && OID_RE.test(newOid)) {
550
546
  refUpdates.push({ oldOid, newOid, refName });
551
547
  }
552
548
  }
553
- return { refUpdates, packData: body.subarray(pos) };
549
+ return {
550
+ refUpdates,
551
+ packData: body.subarray(pos),
552
+ capabilities: capabilities ?? []
553
+ };
554
554
  }
555
555
  async function ensureRepoInitialized(repo, defaultBranch) {
556
556
  const fsp = rawFs(repo);
@@ -568,17 +568,8 @@ async function indexIncomingPack(repo, packData, hooks) {
568
568
  hooks,
569
569
  "write + indexPack incoming pack",
570
570
  async () => {
571
- const fsp = rawFs(repo);
572
- const packDir = `${repo.gitdir}/objects/pack`;
573
- await fsp.mkdir(packDir, { recursive: true });
574
- const packName2 = `recv-${Date.now()}`;
575
- await fsp.writeFile(`${packDir}/${packName2}.pack`, packData);
576
- await git4.indexPack({
577
- ...repo,
578
- dir: packDir,
579
- filepath: `${packName2}.pack`
580
- });
581
- return packName2;
571
+ const { base } = await storePack(repo, packData);
572
+ return base;
582
573
  }
583
574
  );
584
575
  return [
@@ -611,11 +602,27 @@ async function collectExistingRefTips(repo) {
611
602
  );
612
603
  return new Set(oids.filter((oid) => oid !== null));
613
604
  }
605
+ async function currentBranchRef(repo) {
606
+ return git4.currentBranch({ ...repo, fullname: true }).then((ref) => ref ?? null).catch(() => null);
607
+ }
614
608
  async function validateRefUpdates(repo, refUpdates) {
609
+ const headRef = await currentBranchRef(repo);
610
+ const seen = /* @__PURE__ */ new Set();
615
611
  return Promise.all(
616
- refUpdates.map(
617
- async ({ oldOid, refName }) => await checkRefCas(repo, refName, oldOid) ?? { refName, ok: true }
618
- )
612
+ refUpdates.map(async ({ oldOid, newOid, refName }) => {
613
+ if (seen.has(refName)) {
614
+ return { refName, ok: false, reason: "duplicate ref update" };
615
+ }
616
+ seen.add(refName);
617
+ if (newOid === ZERO_OID && refName === headRef) {
618
+ return {
619
+ refName,
620
+ ok: false,
621
+ reason: "deletion of the current branch prohibited"
622
+ };
623
+ }
624
+ return await checkRefCas(repo, refName, oldOid) ?? { refName, ok: true };
625
+ })
619
626
  );
620
627
  }
621
628
  async function applyRefUpdates(repo, refUpdates, hooks) {
@@ -630,6 +637,16 @@ async function applyRefUpdates(repo, refUpdates, hooks) {
630
637
  await git4.deleteRef({ ...repo, ref: refName }).catch(() => {
631
638
  });
632
639
  } else {
640
+ if (refName.startsWith("refs/heads/")) {
641
+ const { type } = await git4.readObject({ ...repo, oid: newOid, format: "content" }).catch(() => ({ type: "missing" }));
642
+ if (type !== "commit") {
643
+ return {
644
+ refName,
645
+ ok: false,
646
+ reason: `branch must point at a commit, not a ${type}`
647
+ };
648
+ }
649
+ }
633
650
  await git4.writeRef({
634
651
  ...repo,
635
652
  ref: refName,
@@ -642,16 +659,21 @@ async function applyRefUpdates(repo, refUpdates, hooks) {
642
659
  )
643
660
  );
644
661
  }
645
- function receivePackResponse(results) {
646
- const responseBody = concat(
647
- pktLine("unpack ok\n"),
662
+ function receivePackResponse(results, options) {
663
+ const capabilities = options?.capabilities ?? ["report-status"];
664
+ const report = capabilities.includes("report-status") ? concat(
665
+ pktLine(
666
+ options?.unpackError ? `unpack ${options.unpackError}
667
+ ` : "unpack ok\n"
668
+ ),
648
669
  ...results.map(
649
670
  ({ refName, ok, reason }) => pktLine(ok ? `ok ${refName}
650
671
  ` : `ng ${refName} ${reason}
651
672
  `)
652
673
  ),
653
674
  FLUSH
654
- );
675
+ ) : new Uint8Array();
676
+ const responseBody = capabilities.includes("side-band-64k") && report.length > 0 ? sideBandPackfile(report) : report;
655
677
  return {
656
678
  status: 200,
657
679
  headers: {
@@ -667,11 +689,29 @@ async function applyReceivePack(repo, parsed, options, hooks) {
667
689
  const acceptedUpdates = parsed.refUpdates.filter(
668
690
  (_, index) => preflight[index]?.ok
669
691
  );
670
- const incomingPackPaths = await indexIncomingPack(
671
- repo,
672
- acceptedUpdates.some((update) => update.newOid !== ZERO_OID) ? parsed.packData : new Uint8Array(),
673
- hooks
692
+ const fsp = rawFs(repo);
693
+ const packDir = `${repo.gitdir}/objects/pack`;
694
+ const existingPacks = new Set(
695
+ await fsp.readdir(packDir).catch(() => [])
674
696
  );
697
+ let incomingPackPaths;
698
+ try {
699
+ incomingPackPaths = await indexIncomingPack(
700
+ repo,
701
+ acceptedUpdates.some((update) => update.newOid !== ZERO_OID) ? parsed.packData : new Uint8Array(),
702
+ hooks
703
+ );
704
+ } catch (err) {
705
+ hooks?.onWarn?.("receive-pack: incoming pack rejected", err);
706
+ return {
707
+ results: preflight.map(
708
+ (result2) => result2.ok ? { ...result2, ok: false, reason: "unpacker error" } : result2
709
+ ),
710
+ stalePackPaths: [],
711
+ unpackError: "index-pack failed"
712
+ };
713
+ }
714
+ const removableIncoming = incomingPackPaths.filter((path) => !existingPacks.has(path.slice(packDir.length + 1))).sort((a, b) => Number(b.endsWith(".idx")) - Number(a.endsWith(".idx")));
675
715
  const newTips = acceptedUpdates.filter((update) => update.newOid !== ZERO_OID).map((update) => update.newOid);
676
716
  if (newTips.length > 0) {
677
717
  const haveOids = await collectExistingRefTips(repo);
@@ -685,15 +725,13 @@ async function applyReceivePack(repo, parsed, options, hooks) {
685
725
  haveOids
686
726
  );
687
727
  if (!complete) {
688
- await Promise.all(
689
- incomingPackPaths.map(
690
- (filepath) => rawFs(repo).unlink(filepath).catch(() => {
691
- })
692
- )
693
- );
728
+ for (const filepath of removableIncoming) {
729
+ await fsp.unlink(filepath).catch(() => {
730
+ });
731
+ }
694
732
  return {
695
733
  results: preflight.map(
696
- (result) => result.ok ? { ...result, ok: false, reason: "incomplete object graph" } : result
734
+ (result2) => result2.ok ? { ...result2, ok: false, reason: "incomplete object graph" } : result2
697
735
  ),
698
736
  stalePackPaths: []
699
737
  };
@@ -701,12 +739,12 @@ async function applyReceivePack(repo, parsed, options, hooks) {
701
739
  }
702
740
  const applied = await applyRefUpdates(repo, acceptedUpdates, hooks);
703
741
  const results = preflight.map(
704
- (result, index) => result.ok ? applied.find(
742
+ (result2, index) => result2.ok ? applied.find(
705
743
  (entry) => entry.refName === parsed.refUpdates[index]?.refName
706
- ) ?? result : result
744
+ ) ?? result2 : result2
707
745
  );
708
746
  const repackOptions = options.repack;
709
- const stalePackPaths = repackOptions === false || !results.some((result) => result.ok) ? [] : await runStep(
747
+ const stalePackPaths = repackOptions === false || !results.some((result2) => result2.ok) ? [] : await runStep(
710
748
  hooks,
711
749
  "repack",
712
750
  () => repackRepository(repo, repackOptions, hooks)
@@ -717,87 +755,161 @@ async function applyReceivePack(repo, parsed, options, hooks) {
717
755
  // src/http/upload-pack.ts
718
756
  import git5 from "isomorphic-git";
719
757
  var ZERO_OID2 = "0".repeat(40);
720
- async function handleUploadPack(repo, body, options, hooks) {
721
- const lines = parsePktLines(body);
758
+ var OID_RE2 = /^[0-9a-f]{40}$/;
759
+ var LOOSE_FANOUT_RE = /^[0-9a-f]{2}$/;
760
+ function result(body) {
761
+ return {
762
+ status: 200,
763
+ headers: {
764
+ "Content-Type": "application/x-git-upload-pack-result",
765
+ "Cache-Control": "no-cache"
766
+ },
767
+ body
768
+ };
769
+ }
770
+ function errorResult(message) {
771
+ return result(pktLine(`ERR ${message}
772
+ `));
773
+ }
774
+ function parseUploadPackRequest(body) {
722
775
  const wants = [];
723
776
  const haves = [];
724
777
  let done = false;
725
- for (const line of lines) {
778
+ let invalid = false;
779
+ let capabilities;
780
+ for (const line of parsePktLines(body)) {
726
781
  if (!line) continue;
727
- if (line.startsWith("want ")) {
728
- wants.push(line.slice(5, 45));
729
- }
730
- if (line.startsWith("have ")) {
731
- const sha = line.slice(5, 45);
732
- if (sha !== ZERO_OID2) {
733
- haves.push(sha);
782
+ const text = line.replace(/\n$/, "");
783
+ if (text.startsWith("want ")) {
784
+ const [oid, ...caps] = text.slice(5).split(" ");
785
+ if (capabilities === void 0) capabilities = caps.filter(Boolean);
786
+ if (oid !== void 0 && OID_RE2.test(oid)) wants.push(oid);
787
+ else invalid = true;
788
+ } else if (text.startsWith("have ")) {
789
+ const oid = text.slice(5).trim();
790
+ if (OID_RE2.test(oid)) {
791
+ if (oid !== ZERO_OID2) haves.push(oid);
792
+ } else {
793
+ invalid = true;
734
794
  }
735
- }
736
- if (line.startsWith("done")) {
795
+ } else if (text === "done") {
737
796
  done = true;
738
797
  }
739
798
  }
740
- if (wants.length === 0) {
741
- return {
742
- status: 200,
743
- headers: { "Content-Type": "application/x-git-upload-pack-result" },
744
- body: concat(pktLine("NAK\n"))
745
- };
799
+ return {
800
+ wants: [...new Set(wants)],
801
+ haves: [...new Set(haves)],
802
+ done,
803
+ capabilities: capabilities ?? [],
804
+ invalid
805
+ };
806
+ }
807
+ async function servableSinglePack(repo, wants, hooks) {
808
+ const fsp = rawFs(repo);
809
+ const objectsDir = `${repo.gitdir}/objects`;
810
+ const [objectEntries, packEntries] = await runStep(
811
+ hooks,
812
+ "readdir objects + objects/pack",
813
+ () => Promise.all([
814
+ fsp.readdir(objectsDir).catch(() => []),
815
+ fsp.readdir(`${objectsDir}/pack`).catch(() => [])
816
+ ])
817
+ );
818
+ if (objectEntries.some((name) => LOOSE_FANOUT_RE.test(name))) return null;
819
+ const packs = parsePackDirectory(packEntries).filter(
820
+ (file) => file.kind === "pack"
821
+ );
822
+ const pack = packs[0];
823
+ if (packs.length !== 1 || pack === void 0) return null;
824
+ const [packData, idxData] = await runStep(
825
+ hooks,
826
+ "read consolidated pack (fast path)",
827
+ () => Promise.all([
828
+ fsp.readFile(`${objectsDir}/pack/${pack.name}`),
829
+ fsp.readFile(`${objectsDir}/pack/${packFileName(pack.base, "idx")}`)
830
+ ]).catch(() => [null, null])
831
+ );
832
+ if (!(packData instanceof Uint8Array) || !(idxData instanceof Uint8Array)) {
833
+ return null;
746
834
  }
747
- if (haves.length > 0 && !done) {
748
- return {
749
- status: 200,
750
- headers: { "Content-Type": "application/x-git-upload-pack-result" },
751
- body: concat(pktLine("NAK\n"))
752
- };
835
+ const indexed = readPackIndex(idxData);
836
+ if (!indexed) return null;
837
+ const oids = new Set(indexed.map((entry) => entry.oid));
838
+ return wants.every((oid) => oids.has(oid)) ? packData : null;
839
+ }
840
+ async function handleUploadPack(repo, body, options, hooks) {
841
+ const request = parseUploadPackRequest(body);
842
+ if (request.invalid) {
843
+ return errorResult("upload-pack: protocol error, malformed object id");
753
844
  }
754
- if (haves.length === 0) {
755
- const packDirPath = `${repo.gitdir}/objects/pack`;
756
- const entries = await runStep(
845
+ const { wants, haves, done, capabilities } = request;
846
+ const frame = (pack) => capabilities.includes("side-band-64k") || capabilities.includes("side-band") ? sideBandPackfile(pack) : concat(pack);
847
+ if (wants.length === 0 || haves.length > 0 && !done) {
848
+ return result(concat(pktLine("NAK\n")));
849
+ }
850
+ const { refs } = await runStep(
851
+ hooks,
852
+ "listAllRefs",
853
+ () => listAllRefs(repo, "HEAD")
854
+ );
855
+ const tips = new Set(refs.map((ref) => ref.oid));
856
+ const untipped = wants.filter((oid) => !tips.has(oid));
857
+ if (untipped.length > 0) {
858
+ const { commits } = await runStep(
757
859
  hooks,
758
- "readdir objects/pack",
759
- () => rawFs(repo).readdir(packDirPath).catch(() => [])
860
+ "verify wants reachable",
861
+ () => collectCommitAncestry(repo, [...tips])
760
862
  );
761
- const packNames = parsePackDirectory(entries).filter((file) => file.kind === "pack").map((file) => file.name);
762
- const packName = packNames[0];
763
- if (packNames.length === 1 && packName !== void 0) {
764
- const packData = await runStep(
765
- hooks,
766
- "read consolidated pack (fast path)",
767
- () => rawFs(repo).readFile(`${packDirPath}/${packName}`)
768
- );
769
- const bytes = packData instanceof Uint8Array ? packData : new TextEncoder().encode(packData);
770
- return {
771
- status: 200,
772
- headers: {
773
- "Content-Type": "application/x-git-upload-pack-result",
774
- "Cache-Control": "no-cache"
775
- },
776
- body: concat(pktLine("NAK\n"), sideBandPackfile(bytes))
777
- };
863
+ const missing = untipped.find((oid) => !commits.has(oid));
864
+ if (missing !== void 0) {
865
+ return errorResult(`upload-pack: not our ref ${missing}`);
778
866
  }
779
867
  }
868
+ if (haves.length === 0) {
869
+ const pack = await servableSinglePack(repo, wants, hooks);
870
+ if (pack) return result(concat(pktLine("NAK\n"), frame(pack)));
871
+ }
780
872
  if (options?.beforeWalk) {
781
873
  await runStep(hooks, "beforeWalk", options.beforeWalk);
782
874
  }
783
875
  let haveSet;
784
876
  if (haves.length > 0) {
785
- const { oids: haveOids } = await runStep(
786
- hooks,
787
- "collectReachableOids(haves)",
788
- () => collectReachableOids(repo, haves, hooks, void 0, {
789
- verifyBlobs: false
790
- })
791
- );
792
- haveSet = new Set(haveOids);
877
+ haveSet = await runStep(hooks, "have boundary", async () => {
878
+ const { commits: clientCommits } = await collectCommitAncestry(
879
+ repo,
880
+ haves
881
+ );
882
+ const { boundary } = await collectCommitAncestry(
883
+ repo,
884
+ wants,
885
+ clientCommits
886
+ );
887
+ const boundaryTrees = await Promise.all(
888
+ [...boundary].map(async (oid) => {
889
+ const { commit } = await git5.readCommit({ ...repo, oid });
890
+ return commit.tree;
891
+ })
892
+ );
893
+ const { oids: boundaryObjects } = await collectReachableOids(
894
+ repo,
895
+ boundaryTrees,
896
+ hooks,
897
+ void 0,
898
+ { verifyBlobs: false }
899
+ );
900
+ return /* @__PURE__ */ new Set([...clientCommits, ...boundaryObjects]);
901
+ });
793
902
  }
794
- const { oids: wantOids } = await runStep(
903
+ const { oids: wantOids, complete } = await runStep(
795
904
  hooks,
796
905
  "collectReachableOids(wants)",
797
906
  () => collectReachableOids(repo, wants, hooks, haveSet, {
798
907
  verifyBlobs: false
799
908
  })
800
909
  );
910
+ if (!complete) {
911
+ return errorResult("upload-pack: repository is missing objects");
912
+ }
801
913
  const oids = haveSet ? wantOids.filter((oid) => !haveSet.has(oid)) : wantOids;
802
914
  const reused = await runStep(
803
915
  hooks,
@@ -808,14 +920,7 @@ async function handleUploadPack(repo, body, options, hooks) {
808
920
  const { packfile } = await git5.packObjects({ ...repo, oids });
809
921
  return packfile instanceof Uint8Array ? packfile : new Uint8Array(packfile ?? []);
810
922
  });
811
- return {
812
- status: 200,
813
- headers: {
814
- "Content-Type": "application/x-git-upload-pack-result",
815
- "Cache-Control": "no-cache"
816
- },
817
- body: concat(pktLine("NAK\n"), sideBandPackfile(packBytes))
818
- };
923
+ return result(concat(pktLine("NAK\n"), frame(packBytes)));
819
924
  }
820
925
  export {
821
926
  FLUSH,