git-fs-s3 0.7.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.
Files changed (37) hide show
  1. package/README.md +10 -6
  2. package/dist/{chunk-XNSZTA4Y.js → chunk-HBOJR5D3.js} +108 -5
  3. package/dist/chunk-HBOJR5D3.js.map +1 -0
  4. package/dist/{chunk-FIFWOMPM.js → chunk-PASVUPKR.js} +64 -2
  5. package/dist/chunk-PASVUPKR.js.map +1 -0
  6. package/dist/chunk-ZB6GNJ2J.js +59 -0
  7. package/dist/chunk-ZB6GNJ2J.js.map +1 -0
  8. package/dist/http.js +299 -192
  9. package/dist/http.js.map +1 -1
  10. package/dist/index.js +79 -76
  11. package/dist/index.js.map +1 -1
  12. package/dist/ops.js +282 -253
  13. package/dist/ops.js.map +1 -1
  14. package/dist/types/edge-utils.d.ts +2 -0
  15. package/dist/types/errors.d.ts +0 -1
  16. package/dist/types/http/index.d.ts +2 -2
  17. package/dist/types/http/pack-reuse.d.ts +2 -1
  18. package/dist/types/http/reachability.d.ts +12 -0
  19. package/dist/types/http/receive-pack.d.ts +28 -9
  20. package/dist/types/http/upload-pack.d.ts +19 -3
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/ops/commit.d.ts +9 -1
  23. package/dist/types/ops/diff.d.ts +7 -3
  24. package/dist/types/ops/file-history.d.ts +4 -18
  25. package/dist/types/ops/history.d.ts +30 -43
  26. package/dist/types/ops/index.d.ts +4 -3
  27. package/dist/types/ops/last-commit.d.ts +2 -8
  28. package/dist/types/ops/packed-writes.d.ts +9 -0
  29. package/dist/types/ops/tree.d.ts +9 -1
  30. package/dist/types/ops/types.d.ts +10 -16
  31. package/dist/types/pack-write.d.ts +26 -0
  32. package/dist/types/refs.d.ts +9 -0
  33. package/package.json +2 -2
  34. package/dist/chunk-FIFWOMPM.js.map +0 -1
  35. package/dist/chunk-RMAOPWHJ.js +0 -38
  36. package/dist/chunk-RMAOPWHJ.js.map +0 -1
  37. package/dist/chunk-XNSZTA4Y.js.map +0 -1
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([
@@ -97,11 +88,12 @@ async function listAllRefs(repo, defaultBranch) {
97
88
  oid: b.commit
98
89
  }));
99
90
  const defaultRef = branches.find((b) => b.isDefault);
100
- const headSymref = defaultRef ? `refs/heads/${defaultRef.name}` : await Promise.resolve(git.currentBranch({ ...repo, fullname: true })).then((cb) => cb ?? `refs/heads/${defaultBranch}`).catch(() => `refs/heads/${defaultBranch}`);
101
- const headOid = branchRefs.find((r) => r?.name === headSymref)?.oid ?? await git.resolveRef({ ...repo, ref: "HEAD" }).catch(() => null);
91
+ const current = defaultRef ? null : await git.currentBranch({ ...repo, fullname: true }).catch(() => void 0);
92
+ const headSymref = defaultRef ? `refs/heads/${defaultRef.name}` : current ?? `refs/heads/${defaultBranch}`;
93
+ const headOid = branchRefs.find((r) => r.name === headSymref)?.oid ?? (defaultRef || current ? null : await git.resolveRef({ ...repo, ref: "HEAD" }).catch(() => null));
102
94
  const refs = [];
103
95
  if (headOid) refs.push({ name: "HEAD", oid: headOid });
104
- for (const r of branchRefs) if (r) refs.push(r);
96
+ refs.push(...branchRefs);
105
97
  for (const r of tagRefs) if (r) refs.push(r);
106
98
  return { refs, headSymref };
107
99
  }
@@ -114,7 +106,7 @@ async function handleInfoRefs(repo, options, hooks) {
114
106
  () => listAllRefs(repo, options.defaultBranch)
115
107
  );
116
108
  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}`;
109
+ 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
110
  const parts = [pktLine(`# service=${service}
119
111
  `), FLUSH];
120
112
  if (refs.length === 0) {
@@ -203,6 +195,45 @@ async function collectReachableOids(repo, startOids, hooks, haveOids, options) {
203
195
  await Promise.all(startOids.map((oid) => visit(oid)));
204
196
  return { oids: Array.from(seen), complete };
205
197
  }
198
+ async function collectCommitAncestry(repo, startOids, stopAt) {
199
+ const commits = /* @__PURE__ */ new Set();
200
+ const boundary = /* @__PURE__ */ new Set();
201
+ const queued = /* @__PURE__ */ new Set();
202
+ let frontier = [...new Set(startOids)];
203
+ const BATCH = 50;
204
+ while (frontier.length > 0) {
205
+ const next = [];
206
+ for (let i = 0; i < frontier.length; i += BATCH) {
207
+ await Promise.all(
208
+ frontier.slice(i, i + BATCH).map(async (oid) => {
209
+ if (stopAt?.has(oid)) {
210
+ boundary.add(oid);
211
+ return;
212
+ }
213
+ const object = await git2.readObject({ ...repo, oid }).catch(() => null);
214
+ if (object?.type === "tag") {
215
+ const target = object.object.object;
216
+ if (!queued.has(target)) {
217
+ queued.add(target);
218
+ next.push(target);
219
+ }
220
+ return;
221
+ }
222
+ if (object?.type !== "commit") return;
223
+ commits.add(oid);
224
+ for (const parent of object.object.parent) {
225
+ if (!queued.has(parent)) {
226
+ queued.add(parent);
227
+ next.push(parent);
228
+ }
229
+ }
230
+ })
231
+ );
232
+ }
233
+ frontier = next;
234
+ }
235
+ return { commits, boundary };
236
+ }
206
237
 
207
238
  // src/http/receive-pack.ts
208
239
  import git4 from "isomorphic-git";
@@ -370,30 +401,6 @@ async function buildPackFromExistingPacks(repo, oids) {
370
401
  }
371
402
 
372
403
  // 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
404
  async function readAndVerifyObjects(repo, oids) {
398
405
  const objects = /* @__PURE__ */ new Map();
399
406
  const BATCH_SIZE = 100;
@@ -422,36 +429,6 @@ async function readAndVerifyObjects(repo, oids) {
422
429
  }
423
430
  return objects;
424
431
  }
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
432
  var REPACK_PACK_COUNT_THRESHOLD = 4;
456
433
  async function readPackDirectory(repo) {
457
434
  const entries = await rawFs(repo).readdir(`${repo.gitdir}/objects/pack`).catch(() => []);
@@ -485,17 +462,30 @@ async function repackRepository(repo, options, hooks) {
485
462
  const { oids: reachable, complete } = await collectReachableOids(
486
463
  repo,
487
464
  tipOids,
488
- hooks
465
+ hooks,
466
+ void 0,
467
+ { verifyBlobs: false }
489
468
  );
490
469
  if (!complete || reachable.length === 0) return [];
491
470
  const packDir = `${repo.gitdir}/objects/pack`;
492
- const packBuffer = await buildPackFromExistingPacks(repo, reachable) ?? await buildVerifiedPack(
493
- reachable,
494
- await readAndVerifyObjects(repo, reachable)
495
- );
471
+ const packBuffer = await buildPackFromExistingPacks(repo, reachable) ?? await (async () => {
472
+ const objects = await readAndVerifyObjects(repo, reachable);
473
+ return encodePack(
474
+ reachable.map((oid) => {
475
+ const object = objects.get(oid);
476
+ if (!object) {
477
+ throw new Error(`repack: missing verified object for ${oid}`);
478
+ }
479
+ return object;
480
+ })
481
+ );
482
+ })();
496
483
  await fsp.mkdir(packDir, { recursive: true });
497
- const newBase = `pack-${Date.now()}`;
484
+ const newBase = packBaseName(packBuffer);
498
485
  const newPackFile = packFileName(newBase, "pack");
486
+ const preexisting = (await readPackDirectory(repo)).some(
487
+ (file) => file.base === newBase
488
+ );
499
489
  await fsp.writeFile(`${packDir}/${newPackFile}`, packBuffer);
500
490
  const { oids: indexedOids } = await git3.indexPack({
501
491
  ...repo,
@@ -504,12 +494,13 @@ async function repackRepository(repo, options, hooks) {
504
494
  });
505
495
  const indexed = new Set(indexedOids);
506
496
  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
- );
497
+ if (!preexisting)
498
+ await Promise.all(
499
+ ["pack", "idx"].map(
500
+ (kind) => fsp.unlink(`${packDir}/${packFileName(newBase, kind)}`).catch(() => {
501
+ })
502
+ )
503
+ );
513
504
  throw new Error(
514
505
  "repack: indexed pack is missing reachable objects \u2014 aborting"
515
506
  );
@@ -517,12 +508,14 @@ async function repackRepository(repo, options, hooks) {
517
508
  const stale = (await readPackDirectory(repo)).filter(
518
509
  (file) => file.base !== newBase
519
510
  );
520
- await Promise.all(
521
- stale.map(
511
+ const unlinkAll = (files) => Promise.all(
512
+ files.map(
522
513
  (file) => fsp.unlink(`${packDir}/${file.name}`).catch(() => {
523
514
  })
524
515
  )
525
516
  );
517
+ await unlinkAll(stale.filter((file) => file.kind === "idx"));
518
+ await unlinkAll(stale.filter((file) => file.kind !== "idx"));
526
519
  return stale.map((file) => `objects/pack/${file.name}`);
527
520
  } catch (err) {
528
521
  hooks?.onWarn?.("repack failed (non-fatal)", err);
@@ -532,8 +525,10 @@ async function repackRepository(repo, options, hooks) {
532
525
 
533
526
  // src/http/receive-pack.ts
534
527
  var ZERO_OID = "0".repeat(40);
528
+ var OID_RE = /^[0-9a-f]{40}$/;
535
529
  function parseReceivePackBody(body) {
536
530
  const refUpdates = [];
531
+ let capabilities;
537
532
  let pos = 0;
538
533
  while (pos + 4 <= body.length) {
539
534
  const len = Number.parseInt(decodeAscii(body.subarray(pos, pos + 4)), 16);
@@ -542,15 +537,21 @@ function parseReceivePackBody(body) {
542
537
  break;
543
538
  }
544
539
  if (len < 4) break;
545
- const line = decodeUtf8(body.subarray(pos + 4, pos + len)).replace(/\n$/, "").split("\0")[0];
540
+ const [line, caps] = decodeUtf8(body.subarray(pos + 4, pos + len)).replace(/\n$/, "").split("\0");
541
+ if (capabilities === void 0) {
542
+ capabilities = (caps ?? "").split(" ").filter(Boolean);
543
+ }
546
544
  pos += len;
547
- const parts = (line ?? "").split(" ");
548
- const [oldOid, newOid, refName] = parts;
549
- if (oldOid && newOid && refName) {
545
+ const [oldOid, newOid, refName] = (line ?? "").split(" ");
546
+ if (oldOid && newOid && refName && OID_RE.test(oldOid) && OID_RE.test(newOid)) {
550
547
  refUpdates.push({ oldOid, newOid, refName });
551
548
  }
552
549
  }
553
- return { refUpdates, packData: body.subarray(pos) };
550
+ return {
551
+ refUpdates,
552
+ packData: body.subarray(pos),
553
+ capabilities: capabilities ?? []
554
+ };
554
555
  }
555
556
  async function ensureRepoInitialized(repo, defaultBranch) {
556
557
  const fsp = rawFs(repo);
@@ -568,17 +569,8 @@ async function indexIncomingPack(repo, packData, hooks) {
568
569
  hooks,
569
570
  "write + indexPack incoming pack",
570
571
  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;
572
+ const { base } = await storePack(repo, packData);
573
+ return base;
582
574
  }
583
575
  );
584
576
  return [
@@ -611,11 +603,27 @@ async function collectExistingRefTips(repo) {
611
603
  );
612
604
  return new Set(oids.filter((oid) => oid !== null));
613
605
  }
606
+ async function currentBranchRef(repo) {
607
+ return git4.currentBranch({ ...repo, fullname: true }).then((ref) => ref ?? null).catch(() => null);
608
+ }
614
609
  async function validateRefUpdates(repo, refUpdates) {
610
+ const headRef = await currentBranchRef(repo);
611
+ const seen = /* @__PURE__ */ new Set();
615
612
  return Promise.all(
616
- refUpdates.map(
617
- async ({ oldOid, refName }) => await checkRefCas(repo, refName, oldOid) ?? { refName, ok: true }
618
- )
613
+ refUpdates.map(async ({ oldOid, newOid, refName }) => {
614
+ if (seen.has(refName)) {
615
+ return { refName, ok: false, reason: "duplicate ref update" };
616
+ }
617
+ seen.add(refName);
618
+ if (newOid === ZERO_OID && refName === headRef) {
619
+ return {
620
+ refName,
621
+ ok: false,
622
+ reason: "deletion of the current branch prohibited"
623
+ };
624
+ }
625
+ return await checkRefCas(repo, refName, oldOid) ?? { refName, ok: true };
626
+ })
619
627
  );
620
628
  }
621
629
  async function applyRefUpdates(repo, refUpdates, hooks) {
@@ -630,6 +638,16 @@ async function applyRefUpdates(repo, refUpdates, hooks) {
630
638
  await git4.deleteRef({ ...repo, ref: refName }).catch(() => {
631
639
  });
632
640
  } else {
641
+ if (refName.startsWith("refs/heads/")) {
642
+ const { type } = await git4.readObject({ ...repo, oid: newOid, format: "content" }).catch(() => ({ type: "missing" }));
643
+ if (type !== "commit") {
644
+ return {
645
+ refName,
646
+ ok: false,
647
+ reason: `branch must point at a commit, not a ${type}`
648
+ };
649
+ }
650
+ }
633
651
  await git4.writeRef({
634
652
  ...repo,
635
653
  ref: refName,
@@ -642,16 +660,21 @@ async function applyRefUpdates(repo, refUpdates, hooks) {
642
660
  )
643
661
  );
644
662
  }
645
- function receivePackResponse(results) {
646
- const responseBody = concat(
647
- pktLine("unpack ok\n"),
663
+ function receivePackResponse(results, options) {
664
+ const capabilities = options?.capabilities ?? ["report-status"];
665
+ const report = capabilities.includes("report-status") ? concat(
666
+ pktLine(
667
+ options?.unpackError ? `unpack ${options.unpackError}
668
+ ` : "unpack ok\n"
669
+ ),
648
670
  ...results.map(
649
671
  ({ refName, ok, reason }) => pktLine(ok ? `ok ${refName}
650
672
  ` : `ng ${refName} ${reason}
651
673
  `)
652
674
  ),
653
675
  FLUSH
654
- );
676
+ ) : new Uint8Array();
677
+ const responseBody = capabilities.includes("side-band-64k") && report.length > 0 ? sideBandPackfile(report) : report;
655
678
  return {
656
679
  status: 200,
657
680
  headers: {
@@ -667,11 +690,29 @@ async function applyReceivePack(repo, parsed, options, hooks) {
667
690
  const acceptedUpdates = parsed.refUpdates.filter(
668
691
  (_, index) => preflight[index]?.ok
669
692
  );
670
- const incomingPackPaths = await indexIncomingPack(
671
- repo,
672
- acceptedUpdates.some((update) => update.newOid !== ZERO_OID) ? parsed.packData : new Uint8Array(),
673
- hooks
693
+ const fsp = rawFs(repo);
694
+ const packDir = `${repo.gitdir}/objects/pack`;
695
+ const existingPacks = new Set(
696
+ await fsp.readdir(packDir).catch(() => [])
674
697
  );
698
+ let incomingPackPaths;
699
+ try {
700
+ incomingPackPaths = await indexIncomingPack(
701
+ repo,
702
+ acceptedUpdates.some((update) => update.newOid !== ZERO_OID) ? parsed.packData : new Uint8Array(),
703
+ hooks
704
+ );
705
+ } catch (err) {
706
+ hooks?.onWarn?.("receive-pack: incoming pack rejected", err);
707
+ return {
708
+ results: preflight.map(
709
+ (result2) => result2.ok ? { ...result2, ok: false, reason: "unpacker error" } : result2
710
+ ),
711
+ stalePackPaths: [],
712
+ unpackError: "index-pack failed"
713
+ };
714
+ }
715
+ const removableIncoming = incomingPackPaths.filter((path) => !existingPacks.has(path.slice(packDir.length + 1))).sort((a, b) => Number(b.endsWith(".idx")) - Number(a.endsWith(".idx")));
675
716
  const newTips = acceptedUpdates.filter((update) => update.newOid !== ZERO_OID).map((update) => update.newOid);
676
717
  if (newTips.length > 0) {
677
718
  const haveOids = await collectExistingRefTips(repo);
@@ -685,15 +726,13 @@ async function applyReceivePack(repo, parsed, options, hooks) {
685
726
  haveOids
686
727
  );
687
728
  if (!complete) {
688
- await Promise.all(
689
- incomingPackPaths.map(
690
- (filepath) => rawFs(repo).unlink(filepath).catch(() => {
691
- })
692
- )
693
- );
729
+ for (const filepath of removableIncoming) {
730
+ await fsp.unlink(filepath).catch(() => {
731
+ });
732
+ }
694
733
  return {
695
734
  results: preflight.map(
696
- (result) => result.ok ? { ...result, ok: false, reason: "incomplete object graph" } : result
735
+ (result2) => result2.ok ? { ...result2, ok: false, reason: "incomplete object graph" } : result2
697
736
  ),
698
737
  stalePackPaths: []
699
738
  };
@@ -701,12 +740,12 @@ async function applyReceivePack(repo, parsed, options, hooks) {
701
740
  }
702
741
  const applied = await applyRefUpdates(repo, acceptedUpdates, hooks);
703
742
  const results = preflight.map(
704
- (result, index) => result.ok ? applied.find(
743
+ (result2, index) => result2.ok ? applied.find(
705
744
  (entry) => entry.refName === parsed.refUpdates[index]?.refName
706
- ) ?? result : result
745
+ ) ?? result2 : result2
707
746
  );
708
747
  const repackOptions = options.repack;
709
- const stalePackPaths = repackOptions === false || !results.some((result) => result.ok) ? [] : await runStep(
748
+ const stalePackPaths = repackOptions === false || !results.some((result2) => result2.ok) ? [] : await runStep(
710
749
  hooks,
711
750
  "repack",
712
751
  () => repackRepository(repo, repackOptions, hooks)
@@ -717,87 +756,161 @@ async function applyReceivePack(repo, parsed, options, hooks) {
717
756
  // src/http/upload-pack.ts
718
757
  import git5 from "isomorphic-git";
719
758
  var ZERO_OID2 = "0".repeat(40);
720
- async function handleUploadPack(repo, body, options, hooks) {
721
- const lines = parsePktLines(body);
759
+ var OID_RE2 = /^[0-9a-f]{40}$/;
760
+ var LOOSE_FANOUT_RE = /^[0-9a-f]{2}$/;
761
+ function result(body) {
762
+ return {
763
+ status: 200,
764
+ headers: {
765
+ "Content-Type": "application/x-git-upload-pack-result",
766
+ "Cache-Control": "no-cache"
767
+ },
768
+ body
769
+ };
770
+ }
771
+ function errorResult(message) {
772
+ return result(pktLine(`ERR ${message}
773
+ `));
774
+ }
775
+ function parseUploadPackRequest(body) {
722
776
  const wants = [];
723
777
  const haves = [];
724
778
  let done = false;
725
- for (const line of lines) {
779
+ let invalid = false;
780
+ let capabilities;
781
+ for (const line of parsePktLines(body)) {
726
782
  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);
783
+ const text = line.replace(/\n$/, "");
784
+ if (text.startsWith("want ")) {
785
+ const [oid, ...caps] = text.slice(5).split(" ");
786
+ if (capabilities === void 0) capabilities = caps.filter(Boolean);
787
+ if (oid !== void 0 && OID_RE2.test(oid)) wants.push(oid);
788
+ else invalid = true;
789
+ } else if (text.startsWith("have ")) {
790
+ const oid = text.slice(5).trim();
791
+ if (OID_RE2.test(oid)) {
792
+ if (oid !== ZERO_OID2) haves.push(oid);
793
+ } else {
794
+ invalid = true;
734
795
  }
735
- }
736
- if (line.startsWith("done")) {
796
+ } else if (text === "done") {
737
797
  done = true;
738
798
  }
739
799
  }
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
- };
800
+ return {
801
+ wants: [...new Set(wants)],
802
+ haves: [...new Set(haves)],
803
+ done,
804
+ capabilities: capabilities ?? [],
805
+ invalid
806
+ };
807
+ }
808
+ async function servableSinglePack(repo, wants, hooks) {
809
+ const fsp = rawFs(repo);
810
+ const objectsDir = `${repo.gitdir}/objects`;
811
+ const [objectEntries, packEntries] = await runStep(
812
+ hooks,
813
+ "readdir objects + objects/pack",
814
+ () => Promise.all([
815
+ fsp.readdir(objectsDir).catch(() => []),
816
+ fsp.readdir(`${objectsDir}/pack`).catch(() => [])
817
+ ])
818
+ );
819
+ if (objectEntries.some((name) => LOOSE_FANOUT_RE.test(name))) return null;
820
+ const packs = parsePackDirectory(packEntries).filter(
821
+ (file) => file.kind === "pack"
822
+ );
823
+ const pack = packs[0];
824
+ if (packs.length !== 1 || pack === void 0) return null;
825
+ const [packData, idxData] = await runStep(
826
+ hooks,
827
+ "read consolidated pack (fast path)",
828
+ () => Promise.all([
829
+ fsp.readFile(`${objectsDir}/pack/${pack.name}`),
830
+ fsp.readFile(`${objectsDir}/pack/${packFileName(pack.base, "idx")}`)
831
+ ]).catch(() => [null, null])
832
+ );
833
+ if (!(packData instanceof Uint8Array) || !(idxData instanceof Uint8Array)) {
834
+ return null;
746
835
  }
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
- };
836
+ const indexed = readPackIndex(idxData);
837
+ if (!indexed) return null;
838
+ const oids = new Set(indexed.map((entry) => entry.oid));
839
+ return wants.every((oid) => oids.has(oid)) ? packData : null;
840
+ }
841
+ async function handleUploadPack(repo, body, options, hooks) {
842
+ const request = parseUploadPackRequest(body);
843
+ if (request.invalid) {
844
+ return errorResult("upload-pack: protocol error, malformed object id");
753
845
  }
754
- if (haves.length === 0) {
755
- const packDirPath = `${repo.gitdir}/objects/pack`;
756
- const entries = await runStep(
846
+ const { wants, haves, done, capabilities } = request;
847
+ const frame = (pack) => capabilities.includes("side-band-64k") || capabilities.includes("side-band") ? sideBandPackfile(pack) : concat(pack);
848
+ if (wants.length === 0 || haves.length > 0 && !done) {
849
+ return result(concat(pktLine("NAK\n")));
850
+ }
851
+ const { refs } = await runStep(
852
+ hooks,
853
+ "listAllRefs",
854
+ () => listAllRefs(repo, "HEAD")
855
+ );
856
+ const tips = new Set(refs.map((ref) => ref.oid));
857
+ const untipped = wants.filter((oid) => !tips.has(oid));
858
+ if (untipped.length > 0) {
859
+ const { commits } = await runStep(
757
860
  hooks,
758
- "readdir objects/pack",
759
- () => rawFs(repo).readdir(packDirPath).catch(() => [])
861
+ "verify wants reachable",
862
+ () => collectCommitAncestry(repo, [...tips])
760
863
  );
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
- };
864
+ const missing = untipped.find((oid) => !commits.has(oid));
865
+ if (missing !== void 0) {
866
+ return errorResult(`upload-pack: not our ref ${missing}`);
778
867
  }
779
868
  }
869
+ if (haves.length === 0) {
870
+ const pack = await servableSinglePack(repo, wants, hooks);
871
+ if (pack) return result(concat(pktLine("NAK\n"), frame(pack)));
872
+ }
780
873
  if (options?.beforeWalk) {
781
874
  await runStep(hooks, "beforeWalk", options.beforeWalk);
782
875
  }
783
876
  let haveSet;
784
877
  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);
878
+ haveSet = await runStep(hooks, "have boundary", async () => {
879
+ const { commits: clientCommits } = await collectCommitAncestry(
880
+ repo,
881
+ haves
882
+ );
883
+ const { boundary } = await collectCommitAncestry(
884
+ repo,
885
+ wants,
886
+ clientCommits
887
+ );
888
+ const boundaryTrees = await Promise.all(
889
+ [...boundary].map(async (oid) => {
890
+ const { commit } = await git5.readCommit({ ...repo, oid });
891
+ return commit.tree;
892
+ })
893
+ );
894
+ const { oids: boundaryObjects } = await collectReachableOids(
895
+ repo,
896
+ boundaryTrees,
897
+ hooks,
898
+ void 0,
899
+ { verifyBlobs: false }
900
+ );
901
+ return /* @__PURE__ */ new Set([...clientCommits, ...boundaryObjects]);
902
+ });
793
903
  }
794
- const { oids: wantOids } = await runStep(
904
+ const { oids: wantOids, complete } = await runStep(
795
905
  hooks,
796
906
  "collectReachableOids(wants)",
797
907
  () => collectReachableOids(repo, wants, hooks, haveSet, {
798
908
  verifyBlobs: false
799
909
  })
800
910
  );
911
+ if (!complete) {
912
+ return errorResult("upload-pack: repository is missing objects");
913
+ }
801
914
  const oids = haveSet ? wantOids.filter((oid) => !haveSet.has(oid)) : wantOids;
802
915
  const reused = await runStep(
803
916
  hooks,
@@ -808,14 +921,7 @@ async function handleUploadPack(repo, body, options, hooks) {
808
921
  const { packfile } = await git5.packObjects({ ...repo, oids });
809
922
  return packfile instanceof Uint8Array ? packfile : new Uint8Array(packfile ?? []);
810
923
  });
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
- };
924
+ return result(concat(pktLine("NAK\n"), frame(packBytes)));
819
925
  }
820
926
  export {
821
927
  FLUSH,
@@ -830,6 +936,7 @@ export {
830
936
  listAllRefs,
831
937
  parsePktLines,
832
938
  parseReceivePackBody,
939
+ parseUploadPackRequest,
833
940
  pktLine,
834
941
  pktLineBuffer,
835
942
  rawFs,