git-fs-s3 0.3.12 → 0.4.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.cjs CHANGED
@@ -111,15 +111,24 @@ var BAD_REF_COMPONENT = (
111
111
  // biome-ignore lint/suspicious/noControlCharactersInRegex: control chars are exactly what git's own ref-name rules reject — this needs to match the same range.
112
112
  /(^|[/.])([/.]|$)|^@$|@\{|[\x00-\x20\x7f~^:?*[\\]|\.lock(\/|$)/
113
113
  );
114
+ var FULL_SHA_RE = /^[0-9a-f]{40}$/i;
114
115
  function isSafeFullRefName(ref) {
115
116
  if (!ref.startsWith("refs/heads/") && !ref.startsWith("refs/tags/")) {
116
117
  return false;
117
118
  }
118
119
  return !BAD_REF_COMPONENT.test(ref);
119
120
  }
121
+ function isSafeBranchName(name) {
122
+ if (!name || name.startsWith("refs/") || name === "HEAD") return false;
123
+ if (FULL_SHA_RE.test(name)) return false;
124
+ return !BAD_REF_COMPONENT.test(name);
125
+ }
120
126
 
121
127
  // src/ops/branch.ts
122
- var FULL_SHA_RE = /^[0-9a-f]{40}$/i;
128
+ var FULL_SHA_RE2 = /^[0-9a-f]{40}$/i;
129
+ function hasRecursiveFileLister(fs) {
130
+ return typeof fs === "object" && fs !== null && "listFilesRecursively" in fs && typeof fs.listFilesRecursively === "function";
131
+ }
123
132
  function getPromisesFs(fs) {
124
133
  const promises = fs.promises;
125
134
  return promises ?? null;
@@ -127,6 +136,48 @@ function getPromisesFs(fs) {
127
136
  function decodeFileContent(content) {
128
137
  return typeof content === "string" ? content : new TextDecoder().decode(content);
129
138
  }
139
+ async function listLooseBranches(repo) {
140
+ return listLooseRefNames(repo, "refs/heads");
141
+ }
142
+ async function listLooseRefNames(repo, dir) {
143
+ if (!hasRecursiveFileLister(repo.fs)) return null;
144
+ const names = await repo.fs.listFilesRecursively(`${repo.gitdir}/${dir}`);
145
+ return names.filter(isSafeBranchName);
146
+ }
147
+ async function readPackedBranches(repo) {
148
+ if (!hasRecursiveFileLister(repo.fs)) return /* @__PURE__ */ new Map();
149
+ const promisesFs = getPromisesFs(repo.fs);
150
+ if (!promisesFs) return /* @__PURE__ */ new Map();
151
+ try {
152
+ const content = await promisesFs.readFile(
153
+ `${repo.gitdir}/packed-refs`,
154
+ "utf8"
155
+ );
156
+ const refs = /* @__PURE__ */ new Map();
157
+ for (const line of decodeFileContent(content).split("\n")) {
158
+ const match = /^([0-9a-f]{40}) refs\/heads\/(.+)$/i.exec(line);
159
+ if (match?.[1] && match[2] && isSafeBranchName(match[2])) {
160
+ refs.set(match[2], match[1]);
161
+ }
162
+ }
163
+ return refs;
164
+ } catch {
165
+ return /* @__PURE__ */ new Map();
166
+ }
167
+ }
168
+ async function getCurrentLooseBranch(repo) {
169
+ if (!hasRecursiveFileLister(repo.fs)) return null;
170
+ const promisesFs = getPromisesFs(repo.fs);
171
+ if (!promisesFs) return null;
172
+ try {
173
+ const content = await promisesFs.readFile(`${repo.gitdir}/HEAD`, "utf8");
174
+ const head = decodeFileContent(content).trim();
175
+ const match = /^ref: refs\/heads\/(.+)$/.exec(head);
176
+ return match?.[1] && isSafeBranchName(match[1]) ? match[1] : null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
130
181
  async function resolveLooseRefFast(repo, ref) {
131
182
  const promisesFs = getPromisesFs(repo.fs);
132
183
  if (promisesFs) {
@@ -136,12 +187,36 @@ async function resolveLooseRefFast(repo, ref) {
136
187
  "utf8"
137
188
  );
138
189
  const oid = decodeFileContent(content).trim();
139
- if (FULL_SHA_RE.test(oid)) return oid;
190
+ if (FULL_SHA_RE2.test(oid)) return oid;
140
191
  } catch {
141
192
  }
142
193
  }
143
194
  return import_isomorphic_git.default.resolveRef({ ...repo, ref });
144
195
  }
196
+ async function listBranches(repo) {
197
+ try {
198
+ const [looseBranches, looseCurrentBranch, packedBranches] = await Promise.all([
199
+ listLooseBranches(repo),
200
+ getCurrentLooseBranch(repo),
201
+ readPackedBranches(repo)
202
+ ]);
203
+ const branches = looseBranches === null ? await import_isomorphic_git.default.listBranches(repo) : [.../* @__PURE__ */ new Set([...looseBranches, ...packedBranches.keys()])].sort();
204
+ const currentBranch = looseBranches === null ? await import_isomorphic_git.default.currentBranch({ ...repo, fullname: false }).catch(() => null) : looseCurrentBranch;
205
+ return Promise.all(
206
+ branches.map(async (branch) => {
207
+ const packedCommit = packedBranches.get(branch);
208
+ return {
209
+ name: branch,
210
+ commit: packedCommit && !looseBranches?.includes(branch) ? packedCommit : await resolveLooseRefFast(repo, `refs/heads/${branch}`),
211
+ isDefault: branch === currentBranch
212
+ };
213
+ })
214
+ );
215
+ } catch (err) {
216
+ if (err.code === "NotFoundError") return [];
217
+ throw err;
218
+ }
219
+ }
145
220
 
146
221
  // src/http/pkt-line.ts
147
222
  var FLUSH = new Uint8Array([
@@ -201,34 +276,24 @@ function rawFs(repo) {
201
276
 
202
277
  // src/http/info-refs.ts
203
278
  async function listAllRefs(repo, defaultBranch = "main") {
204
- const [branches, tags, headSymref] = await Promise.all([
205
- import_isomorphic_git2.default.listBranches(repo),
206
- import_isomorphic_git2.default.listTags(repo),
207
- // Wrap with Promise.resolve so a mock/stub returning undefined doesn't crash .then()
208
- Promise.resolve(import_isomorphic_git2.default.currentBranch({ ...repo, fullname: true })).then((cb) => cb ?? `refs/heads/${defaultBranch}`).catch(() => `refs/heads/${defaultBranch}`)
209
- ]);
210
- const [branchRefs, tagRefs] = await Promise.all([
211
- Promise.all(
212
- branches.map(async (branch) => {
213
- try {
214
- const oid = await resolveLooseRefFast(repo, `refs/heads/${branch}`);
215
- return { name: `refs/heads/${branch}`, oid };
216
- } catch {
217
- return null;
218
- }
219
- })
220
- ),
221
- Promise.all(
222
- tags.map(async (tag) => {
223
- try {
224
- const oid = await resolveLooseRefFast(repo, `refs/tags/${tag}`);
225
- return { name: `refs/tags/${tag}`, oid };
226
- } catch {
227
- return null;
228
- }
229
- })
279
+ const [branches, tagNames] = await Promise.all([
280
+ listBranches(repo),
281
+ listLooseRefNames(repo, "refs/tags").then(
282
+ (names) => names ?? import_isomorphic_git2.default.listTags(repo)
230
283
  )
231
284
  ]);
285
+ const tagRefs = await Promise.all(
286
+ tagNames.map(async (tag) => {
287
+ const name = `refs/tags/${tag}`;
288
+ return resolveLooseRefFast(repo, name).then((oid) => ({ name, oid })).catch(() => null);
289
+ })
290
+ );
291
+ const branchRefs = branches.map((b) => ({
292
+ name: `refs/heads/${b.name}`,
293
+ oid: b.commit
294
+ }));
295
+ const defaultRef = branches.find((b) => b.isDefault);
296
+ const headSymref = defaultRef ? `refs/heads/${defaultRef.name}` : await Promise.resolve(import_isomorphic_git2.default.currentBranch({ ...repo, fullname: true })).then((cb) => cb ?? `refs/heads/${defaultBranch}`).catch(() => `refs/heads/${defaultBranch}`);
232
297
  const headOid = branchRefs.find((r) => r?.name === headSymref)?.oid ?? await import_isomorphic_git2.default.resolveRef({ ...repo, ref: "HEAD" }).catch(() => null);
233
298
  const refs = [];
234
299
  if (headOid) refs.push({ name: "HEAD", oid: headOid });
@@ -281,7 +346,8 @@ async function handleInfoRefs(repo, options, hooks) {
281
346
  var import_isomorphic_git3 = __toESM(require("isomorphic-git"), 1);
282
347
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
283
348
  var MISSING_OBJECT_RETRY_DELAY_MS = 200;
284
- async function collectReachableOids(repo, startOids, hooks, haveOids) {
349
+ async function collectReachableOids(repo, startOids, hooks, haveOids, options) {
350
+ const verifyBlobs = options?.verifyBlobs ?? true;
285
351
  const have = haveOids instanceof Set ? haveOids : new Set(haveOids ?? []);
286
352
  const seen = /* @__PURE__ */ new Set();
287
353
  let complete = true;
@@ -289,23 +355,26 @@ async function collectReachableOids(repo, startOids, hooks, haveOids) {
289
355
  async function readAndVisitChildren(oid) {
290
356
  const obj = await import_isomorphic_git3.default.readObject({ ...repo, oid });
291
357
  seen.add(oid);
292
- let children = [];
358
+ const children = [];
293
359
  if (obj.type === "commit") {
294
- const { commit } = await import_isomorphic_git3.default.readCommit({ ...repo, oid });
295
- children = [commit.tree, ...commit.parent];
360
+ const commit = obj.object;
361
+ children.push([commit.tree, "tree"]);
362
+ for (const parent of commit.parent) children.push([parent, "commit"]);
296
363
  } else if (obj.type === "tree") {
297
- const { tree } = await import_isomorphic_git3.default.readTree({ ...repo, oid });
298
- children = tree.map((e) => e.oid);
364
+ const tree = obj.object;
365
+ for (const entry of tree) {
366
+ if (entry.type === "commit") continue;
367
+ children.push([entry.oid, entry.type]);
368
+ }
299
369
  } else if (obj.type === "tag") {
300
- const { tag } = await import_isomorphic_git3.default.readTag({ ...repo, oid });
301
- children = [tag.object];
370
+ children.push([obj.object.object, void 0]);
302
371
  }
303
- await Promise.all(children.map(visit));
372
+ await Promise.all(children.map(([child, type]) => visit(child, type)));
304
373
  }
305
- function visit(oid) {
374
+ function visit(oid, type) {
306
375
  const existing = promises.get(oid);
307
376
  if (existing) return existing;
308
- if (have.has(oid)) {
377
+ if (have.has(oid) || type === "blob" && !verifyBlobs) {
309
378
  seen.add(oid);
310
379
  const p2 = Promise.resolve();
311
380
  promises.set(oid, p2);
@@ -327,7 +396,7 @@ async function collectReachableOids(repo, startOids, hooks, haveOids) {
327
396
  promises.set(oid, p);
328
397
  return p;
329
398
  }
330
- await Promise.all(startOids.map(visit));
399
+ await Promise.all(startOids.map((oid) => visit(oid)));
331
400
  return { oids: Array.from(seen), complete };
332
401
  }
333
402
 
@@ -336,6 +405,193 @@ var import_isomorphic_git5 = __toESM(require("isomorphic-git"), 1);
336
405
 
337
406
  // src/http/repack.ts
338
407
  var import_isomorphic_git4 = __toESM(require("isomorphic-git"), 1);
408
+
409
+ // src/pack-files.ts
410
+ var PACK_FILE_KINDS = [
411
+ "pack",
412
+ "idx",
413
+ "keep",
414
+ "bitmap",
415
+ "rev"
416
+ ];
417
+ var KNOWN_KINDS = new Set(PACK_FILE_KINDS);
418
+ function isPackFileKind(value) {
419
+ return KNOWN_KINDS.has(value);
420
+ }
421
+ function parsePackFileName(name) {
422
+ const dot = name.lastIndexOf(".");
423
+ if (dot <= 0) return null;
424
+ const kind = name.slice(dot + 1);
425
+ if (!isPackFileKind(kind)) return null;
426
+ return { base: name.slice(0, dot), kind, name };
427
+ }
428
+ function packFileName(base, kind) {
429
+ return `${base}.${kind}`;
430
+ }
431
+ function parsePackDirectory(entries) {
432
+ return entries.map(parsePackFileName).filter((file) => file !== null);
433
+ }
434
+
435
+ // src/http/pack-reuse.ts
436
+ var OBJ_OFS_DELTA = 6;
437
+ var OBJ_REF_DELTA = 7;
438
+ var OID_BYTES = 20;
439
+ var PACK_TRAILER_BYTES = 20;
440
+ var IDX_V2 = {
441
+ magic: 4285812579,
442
+ version: 2,
443
+ headerBytes: 8,
444
+ fanoutEntries: 256,
445
+ /** Offsets with this bit set index the 64-bit large-offset table. */
446
+ largeOffsetFlag: 2147483648
447
+ };
448
+ function readPackIndex(idx) {
449
+ const view = new DataView(idx.buffer, idx.byteOffset, idx.byteLength);
450
+ const namesStart = IDX_V2.headerBytes + IDX_V2.fanoutEntries * 4;
451
+ if (idx.byteLength < namesStart || view.getUint32(0) !== IDX_V2.magic || view.getUint32(4) !== IDX_V2.version) {
452
+ return null;
453
+ }
454
+ const count = view.getUint32(namesStart - 4);
455
+ const crcStart = namesStart + count * OID_BYTES;
456
+ const offsetsStart = crcStart + count * 4;
457
+ if (idx.byteLength < offsetsStart + count * 4) return null;
458
+ const entries = [];
459
+ for (let i = 0; i < count; i++) {
460
+ const offset = view.getUint32(offsetsStart + i * 4);
461
+ if (offset & IDX_V2.largeOffsetFlag) return null;
462
+ const at = namesStart + i * OID_BYTES;
463
+ entries.push({ oid: toHex(idx.subarray(at, at + OID_BYTES)), offset });
464
+ }
465
+ return entries;
466
+ }
467
+ function indexPack(idx, pack) {
468
+ const located = readPackIndex(idx);
469
+ if (!located) return null;
470
+ located.sort((a, b) => a.offset - b.offset);
471
+ const entries = /* @__PURE__ */ new Map();
472
+ const oidAtOffset = /* @__PURE__ */ new Map();
473
+ const packEnd = pack.byteLength - PACK_TRAILER_BYTES;
474
+ for (let i = 0; i < located.length; i++) {
475
+ const { oid, offset } = located[i];
476
+ const end = located[i + 1]?.offset ?? packEnd;
477
+ entries.set(oid, { pack, start: offset, end });
478
+ oidAtOffset.set(offset, oid);
479
+ }
480
+ return { entries, oidAtOffset };
481
+ }
482
+ function readEntryHeader(bytes, start) {
483
+ let byte = bytes[start];
484
+ const type = byte >> 4 & 7;
485
+ let size = byte & 15;
486
+ let shift = 4;
487
+ let at = start + 1;
488
+ while (byte & 128) {
489
+ byte = bytes[at++];
490
+ size += (byte & 127) * 2 ** shift;
491
+ shift += 7;
492
+ }
493
+ return { type, size, length: at - start };
494
+ }
495
+ function encodeEntryHeader(type, size) {
496
+ const bytes = [];
497
+ let rest = Math.floor(size / 16);
498
+ bytes.push((rest > 0 ? 128 : 0) | type << 4 | size % 16);
499
+ while (rest > 0) {
500
+ const low = rest % 128;
501
+ rest = Math.floor(rest / 128);
502
+ bytes.push((rest > 0 ? 128 : 0) | low);
503
+ }
504
+ return new Uint8Array(bytes);
505
+ }
506
+ function readOfsDeltaOffset(bytes, start) {
507
+ let at = start;
508
+ let byte = bytes[at++];
509
+ let distance = byte & 127;
510
+ while (byte & 128) {
511
+ byte = bytes[at++];
512
+ distance = (distance + 1) * 128 + (byte & 127);
513
+ }
514
+ return { distance, length: at - start };
515
+ }
516
+ function reusableEntry(entry, indexed) {
517
+ const { pack, start, end } = entry;
518
+ const header = readEntryHeader(pack, start);
519
+ const bodyStart = start + header.length;
520
+ if (header.type === OBJ_REF_DELTA) {
521
+ return {
522
+ bytes: pack.subarray(start, end),
523
+ baseOid: toHex(pack.subarray(bodyStart, bodyStart + OID_BYTES))
524
+ };
525
+ }
526
+ if (header.type !== OBJ_OFS_DELTA) {
527
+ return { bytes: pack.subarray(start, end), baseOid: null };
528
+ }
529
+ const { distance, length } = readOfsDeltaOffset(pack, bodyStart);
530
+ const baseOid = indexed.oidAtOffset.get(start - distance);
531
+ if (baseOid === void 0) return null;
532
+ return {
533
+ bytes: concat(
534
+ encodeEntryHeader(OBJ_REF_DELTA, header.size),
535
+ fromHex(baseOid),
536
+ pack.subarray(bodyStart + length, end)
537
+ ),
538
+ baseOid
539
+ };
540
+ }
541
+ async function buildPackFromExistingPacks(repo, oids) {
542
+ const fsp = rawFs(repo);
543
+ const packDir = `${repo.gitdir}/objects/pack`;
544
+ const packs = parsePackDirectory(
545
+ await fsp.readdir(packDir).catch(() => [])
546
+ ).filter((file) => file.kind === "pack");
547
+ const indexes = [];
548
+ for (const { base } of packs) {
549
+ const [pack, idx] = await Promise.all([
550
+ fsp.readFile(`${packDir}/${packFileName(base, "pack")}`),
551
+ fsp.readFile(`${packDir}/${packFileName(base, "idx")}`)
552
+ ]).catch(() => [null, null]);
553
+ if (!(pack instanceof Uint8Array) || !(idx instanceof Uint8Array)) {
554
+ return null;
555
+ }
556
+ const indexed = indexPack(idx, pack);
557
+ if (!indexed) return null;
558
+ indexes.push(indexed);
559
+ }
560
+ const locate = (oid) => {
561
+ for (const indexed of indexes) {
562
+ const entry = indexed.entries.get(oid);
563
+ if (entry) return reusableEntry(entry, indexed);
564
+ }
565
+ return null;
566
+ };
567
+ const ordered = [];
568
+ const done = /* @__PURE__ */ new Set();
569
+ const pending = /* @__PURE__ */ new Set();
570
+ const visit = (oid) => {
571
+ if (done.has(oid)) return true;
572
+ if (pending.has(oid)) return false;
573
+ const entry = locate(oid);
574
+ if (!entry) return false;
575
+ pending.add(oid);
576
+ if (entry.baseOid !== null && !visit(entry.baseOid)) return false;
577
+ pending.delete(oid);
578
+ done.add(oid);
579
+ ordered.push(entry.bytes);
580
+ return true;
581
+ };
582
+ for (const oid of oids) {
583
+ if (!visit(oid)) return null;
584
+ }
585
+ const header = new Uint8Array(12);
586
+ header.set(encodeUtf8("PACK"), 0);
587
+ const view = new DataView(header.buffer);
588
+ view.setUint32(4, 2);
589
+ view.setUint32(8, ordered.length);
590
+ const body = concat(header, ...ordered);
591
+ return concat(body, fromHex(await sha1(body)));
592
+ }
593
+
594
+ // src/http/repack.ts
339
595
  var PACK_OBJECT_TYPE_BITS = {
340
596
  commit: 16,
341
597
  tree: 32,
@@ -419,13 +675,13 @@ async function buildVerifiedPack(oids, objects) {
419
675
  return concat(packBody, trailer);
420
676
  }
421
677
  var REPACK_PACK_COUNT_THRESHOLD = 4;
678
+ async function readPackDirectory(repo) {
679
+ const entries = await rawFs(repo).readdir(`${repo.gitdir}/objects/pack`).catch(() => []);
680
+ return parsePackDirectory(entries);
681
+ }
422
682
  async function countPacks(repo) {
423
- try {
424
- const entries = await rawFs(repo).readdir(`${repo.gitdir}/objects/pack`);
425
- return entries.filter((f) => f.endsWith(".pack")).length;
426
- } catch {
427
- return 0;
428
- }
683
+ const files = await readPackDirectory(repo);
684
+ return files.filter((file) => file.kind === "pack").length;
429
685
  }
430
686
  async function repackRepository(repo, options, hooks) {
431
687
  const threshold = options?.threshold ?? REPACK_PACK_COUNT_THRESHOLD;
@@ -448,41 +704,48 @@ async function repackRepository(repo, options, hooks) {
448
704
  )
449
705
  )).filter((oid) => oid !== null);
450
706
  if (tipOids.length === 0) return [];
451
- const { oids, complete } = await collectReachableOids(repo, tipOids, hooks);
452
- if (!complete || oids.length === 0) return [];
453
- const objects = await readAndVerifyObjects(repo, oids);
454
- const packBuffer = await buildVerifiedPack(oids, objects);
707
+ const { oids: reachable, complete } = await collectReachableOids(
708
+ repo,
709
+ tipOids,
710
+ hooks
711
+ );
712
+ if (!complete || reachable.length === 0) return [];
455
713
  const packDir = `${repo.gitdir}/objects/pack`;
714
+ const packBuffer = await buildPackFromExistingPacks(repo, reachable) ?? await buildVerifiedPack(
715
+ reachable,
716
+ await readAndVerifyObjects(repo, reachable)
717
+ );
456
718
  await fsp.mkdir(packDir, { recursive: true });
457
719
  const newBase = `pack-${Date.now()}`;
458
- const newPackFile = `${newBase}.pack`;
459
- const newIdxFile = `${newBase}.idx`;
720
+ const newPackFile = packFileName(newBase, "pack");
460
721
  await fsp.writeFile(`${packDir}/${newPackFile}`, packBuffer);
461
722
  const { oids: indexedOids } = await import_isomorphic_git4.default.indexPack({
462
723
  ...repo,
463
724
  dir: packDir,
464
725
  filepath: newPackFile
465
726
  });
466
- const expected = new Set(oids);
467
727
  const indexed = new Set(indexedOids);
468
- if (indexed.size !== expected.size || oids.some((oid) => !indexed.has(oid))) {
469
- await fsp.unlink(`${packDir}/${newPackFile}`).catch(() => {
470
- });
471
- await fsp.unlink(`${packDir}/${newIdxFile}`).catch(() => {
472
- });
728
+ if (reachable.some((oid) => !indexed.has(oid))) {
729
+ await Promise.all(
730
+ ["pack", "idx"].map(
731
+ (kind) => fsp.unlink(`${packDir}/${packFileName(newBase, kind)}`).catch(() => {
732
+ })
733
+ )
734
+ );
473
735
  throw new Error(
474
- "repack: indexed pack's oid set didn't match the verified reachable set \u2014 aborting"
736
+ "repack: indexed pack is missing reachable objects \u2014 aborting"
475
737
  );
476
738
  }
477
- const allEntries = await fsp.readdir(packDir).catch(() => []);
478
- const staleFiles = allEntries.filter(
479
- (f) => f !== newPackFile && f !== newIdxFile && (f.endsWith(".pack") || f.endsWith(".idx") || f.endsWith(".keep"))
739
+ const stale = (await readPackDirectory(repo)).filter(
740
+ (file) => file.base !== newBase
480
741
  );
481
742
  await Promise.all(
482
- staleFiles.map((f) => fsp.unlink(`${packDir}/${f}`).catch(() => {
483
- }))
743
+ stale.map(
744
+ (file) => fsp.unlink(`${packDir}/${file.name}`).catch(() => {
745
+ })
746
+ )
484
747
  );
485
- return staleFiles.map((f) => `objects/pack/${f}`);
748
+ return stale.map((file) => `objects/pack/${file.name}`);
486
749
  } catch (err) {
487
750
  hooks?.onWarn?.("repack failed (non-fatal)", err);
488
751
  return [];
@@ -556,6 +819,20 @@ async function checkRefCas(repo, refName, oldOid) {
556
819
  reason: "non-fast-forward, ref updated by another push"
557
820
  };
558
821
  }
822
+ async function collectExistingRefTips(repo) {
823
+ const [branches, tags] = await Promise.all([
824
+ import_isomorphic_git5.default.listBranches(repo),
825
+ import_isomorphic_git5.default.listTags(repo)
826
+ ]);
827
+ const refs = [
828
+ ...branches.map((b) => `refs/heads/${b}`),
829
+ ...tags.map((t) => `refs/tags/${t}`)
830
+ ];
831
+ const oids = await Promise.all(
832
+ refs.map((ref) => import_isomorphic_git5.default.resolveRef({ ...repo, ref }).catch(() => null))
833
+ );
834
+ return new Set(oids.filter((oid) => oid !== null));
835
+ }
559
836
  async function validateRefUpdates(repo, refUpdates) {
560
837
  return Promise.all(
561
838
  refUpdates.map(
@@ -619,7 +896,10 @@ async function applyReceivePack(repo, parsed, options, hooks) {
619
896
  );
620
897
  const newTips = acceptedUpdates.filter((update) => update.newOid !== ZERO_OID).map((update) => update.newOid);
621
898
  if (newTips.length > 0) {
622
- const haveOids = acceptedUpdates.map((update) => update.oldOid).filter((oid) => oid !== ZERO_OID);
899
+ const haveOids = await collectExistingRefTips(repo);
900
+ for (const update of acceptedUpdates) {
901
+ if (update.oldOid !== ZERO_OID) haveOids.add(update.oldOid);
902
+ }
623
903
  const { complete } = await collectReachableOids(
624
904
  repo,
625
905
  newTips,
@@ -700,7 +980,7 @@ async function handleUploadPack(repo, body, options, hooks) {
700
980
  "readdir objects/pack",
701
981
  () => rawFs(repo).readdir(packDirPath).catch(() => [])
702
982
  );
703
- const packNames = entries.filter((f) => f.endsWith(".pack"));
983
+ const packNames = parsePackDirectory(entries).filter((file) => file.kind === "pack").map((file) => file.name);
704
984
  const packName = packNames[0];
705
985
  if (packNames.length === 1 && packName !== void 0) {
706
986
  const packData = await runStep(
@@ -722,27 +1002,34 @@ async function handleUploadPack(repo, body, options, hooks) {
722
1002
  if (options?.beforeWalk) {
723
1003
  await runStep(hooks, "beforeWalk", options.beforeWalk);
724
1004
  }
725
- const { oids: wantOids } = await runStep(
726
- hooks,
727
- "collectReachableOids(wants)",
728
- () => collectReachableOids(repo, wants, hooks)
729
- );
730
- let oids = wantOids;
1005
+ let haveSet;
731
1006
  if (haves.length > 0) {
732
1007
  const { oids: haveOids } = await runStep(
733
1008
  hooks,
734
1009
  "collectReachableOids(haves)",
735
- () => collectReachableOids(repo, haves, hooks)
1010
+ () => collectReachableOids(repo, haves, hooks, void 0, {
1011
+ verifyBlobs: false
1012
+ })
736
1013
  );
737
- const haveSet = new Set(haveOids);
738
- oids = oids.filter((oid) => !haveSet.has(oid));
1014
+ haveSet = new Set(haveOids);
739
1015
  }
740
- const { packfile } = await runStep(
1016
+ const { oids: wantOids } = await runStep(
1017
+ hooks,
1018
+ "collectReachableOids(wants)",
1019
+ () => collectReachableOids(repo, wants, hooks, haveSet, {
1020
+ verifyBlobs: false
1021
+ })
1022
+ );
1023
+ const oids = haveSet ? wantOids.filter((oid) => !haveSet.has(oid)) : wantOids;
1024
+ const reused = await runStep(
741
1025
  hooks,
742
- `packObjects (${oids.length} oids)`,
743
- () => import_isomorphic_git6.default.packObjects({ ...repo, oids })
1026
+ `reuse pack entries (${oids.length} oids)`,
1027
+ () => buildPackFromExistingPacks(repo, oids)
744
1028
  );
745
- const packBytes = packfile instanceof Uint8Array ? packfile : new Uint8Array(packfile ?? []);
1029
+ const packBytes = reused ?? await runStep(hooks, `packObjects (${oids.length} oids)`, async () => {
1030
+ const { packfile } = await import_isomorphic_git6.default.packObjects({ ...repo, oids });
1031
+ return packfile instanceof Uint8Array ? packfile : new Uint8Array(packfile ?? []);
1032
+ });
746
1033
  return {
747
1034
  status: 200,
748
1035
  headers: {