git-fs-s3 0.3.13 → 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.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import {
2
+ packFileName,
3
+ parsePackDirectory
4
+ } from "./chunk-RMAOPWHJ.js";
5
+ import {
6
+ listBranches,
7
+ listLooseRefNames,
2
8
  resolveLooseRefFast
3
- } from "./chunk-3BDDEGAP.js";
9
+ } from "./chunk-Z4656PGE.js";
4
10
  import {
5
11
  concat,
6
12
  decodeAscii,
@@ -9,7 +15,8 @@ import {
9
15
  encodeUtf8,
10
16
  fromHex,
11
17
  isSafeFullRefName,
12
- sha1
18
+ sha1,
19
+ toHex
13
20
  } from "./chunk-YQFNY6PG.js";
14
21
 
15
22
  // src/http/info-refs.ts
@@ -73,34 +80,24 @@ function rawFs(repo) {
73
80
 
74
81
  // src/http/info-refs.ts
75
82
  async function listAllRefs(repo, defaultBranch = "main") {
76
- const [branches, tags, headSymref] = await Promise.all([
77
- git.listBranches(repo),
78
- git.listTags(repo),
79
- // Wrap with Promise.resolve so a mock/stub returning undefined doesn't crash .then()
80
- Promise.resolve(git.currentBranch({ ...repo, fullname: true })).then((cb) => cb ?? `refs/heads/${defaultBranch}`).catch(() => `refs/heads/${defaultBranch}`)
81
- ]);
82
- const [branchRefs, tagRefs] = await Promise.all([
83
- Promise.all(
84
- branches.map(async (branch) => {
85
- try {
86
- const oid = await resolveLooseRefFast(repo, `refs/heads/${branch}`);
87
- return { name: `refs/heads/${branch}`, oid };
88
- } catch {
89
- return null;
90
- }
91
- })
92
- ),
93
- Promise.all(
94
- tags.map(async (tag) => {
95
- try {
96
- const oid = await resolveLooseRefFast(repo, `refs/tags/${tag}`);
97
- return { name: `refs/tags/${tag}`, oid };
98
- } catch {
99
- return null;
100
- }
101
- })
83
+ const [branches, tagNames] = await Promise.all([
84
+ listBranches(repo),
85
+ listLooseRefNames(repo, "refs/tags").then(
86
+ (names) => names ?? git.listTags(repo)
102
87
  )
103
88
  ]);
89
+ const tagRefs = await Promise.all(
90
+ tagNames.map(async (tag) => {
91
+ const name = `refs/tags/${tag}`;
92
+ return resolveLooseRefFast(repo, name).then((oid) => ({ name, oid })).catch(() => null);
93
+ })
94
+ );
95
+ const branchRefs = branches.map((b) => ({
96
+ name: `refs/heads/${b.name}`,
97
+ oid: b.commit
98
+ }));
99
+ 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}`);
104
101
  const headOid = branchRefs.find((r) => r?.name === headSymref)?.oid ?? await git.resolveRef({ ...repo, ref: "HEAD" }).catch(() => null);
105
102
  const refs = [];
106
103
  if (headOid) refs.push({ name: "HEAD", oid: headOid });
@@ -153,7 +150,8 @@ async function handleInfoRefs(repo, options, hooks) {
153
150
  import git2 from "isomorphic-git";
154
151
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
155
152
  var MISSING_OBJECT_RETRY_DELAY_MS = 200;
156
- async function collectReachableOids(repo, startOids, hooks, haveOids) {
153
+ async function collectReachableOids(repo, startOids, hooks, haveOids, options) {
154
+ const verifyBlobs = options?.verifyBlobs ?? true;
157
155
  const have = haveOids instanceof Set ? haveOids : new Set(haveOids ?? []);
158
156
  const seen = /* @__PURE__ */ new Set();
159
157
  let complete = true;
@@ -161,23 +159,26 @@ async function collectReachableOids(repo, startOids, hooks, haveOids) {
161
159
  async function readAndVisitChildren(oid) {
162
160
  const obj = await git2.readObject({ ...repo, oid });
163
161
  seen.add(oid);
164
- let children = [];
162
+ const children = [];
165
163
  if (obj.type === "commit") {
166
- const { commit } = await git2.readCommit({ ...repo, oid });
167
- children = [commit.tree, ...commit.parent];
164
+ const commit = obj.object;
165
+ children.push([commit.tree, "tree"]);
166
+ for (const parent of commit.parent) children.push([parent, "commit"]);
168
167
  } else if (obj.type === "tree") {
169
- const { tree } = await git2.readTree({ ...repo, oid });
170
- children = tree.map((e) => e.oid);
168
+ const tree = obj.object;
169
+ for (const entry of tree) {
170
+ if (entry.type === "commit") continue;
171
+ children.push([entry.oid, entry.type]);
172
+ }
171
173
  } else if (obj.type === "tag") {
172
- const { tag } = await git2.readTag({ ...repo, oid });
173
- children = [tag.object];
174
+ children.push([obj.object.object, void 0]);
174
175
  }
175
- await Promise.all(children.map(visit));
176
+ await Promise.all(children.map(([child, type]) => visit(child, type)));
176
177
  }
177
- function visit(oid) {
178
+ function visit(oid, type) {
178
179
  const existing = promises.get(oid);
179
180
  if (existing) return existing;
180
- if (have.has(oid)) {
181
+ if (have.has(oid) || type === "blob" && !verifyBlobs) {
181
182
  seen.add(oid);
182
183
  const p2 = Promise.resolve();
183
184
  promises.set(oid, p2);
@@ -199,7 +200,7 @@ async function collectReachableOids(repo, startOids, hooks, haveOids) {
199
200
  promises.set(oid, p);
200
201
  return p;
201
202
  }
202
- await Promise.all(startOids.map(visit));
203
+ await Promise.all(startOids.map((oid) => visit(oid)));
203
204
  return { oids: Array.from(seen), complete };
204
205
  }
205
206
 
@@ -208,6 +209,167 @@ import git4 from "isomorphic-git";
208
209
 
209
210
  // src/http/repack.ts
210
211
  import git3 from "isomorphic-git";
212
+
213
+ // src/http/pack-reuse.ts
214
+ var OBJ_OFS_DELTA = 6;
215
+ var OBJ_REF_DELTA = 7;
216
+ var OID_BYTES = 20;
217
+ var PACK_TRAILER_BYTES = 20;
218
+ var IDX_V2 = {
219
+ magic: 4285812579,
220
+ version: 2,
221
+ headerBytes: 8,
222
+ fanoutEntries: 256,
223
+ /** Offsets with this bit set index the 64-bit large-offset table. */
224
+ largeOffsetFlag: 2147483648
225
+ };
226
+ function readPackIndex(idx) {
227
+ const view = new DataView(idx.buffer, idx.byteOffset, idx.byteLength);
228
+ const namesStart = IDX_V2.headerBytes + IDX_V2.fanoutEntries * 4;
229
+ if (idx.byteLength < namesStart || view.getUint32(0) !== IDX_V2.magic || view.getUint32(4) !== IDX_V2.version) {
230
+ return null;
231
+ }
232
+ const count = view.getUint32(namesStart - 4);
233
+ const crcStart = namesStart + count * OID_BYTES;
234
+ const offsetsStart = crcStart + count * 4;
235
+ if (idx.byteLength < offsetsStart + count * 4) return null;
236
+ const entries = [];
237
+ for (let i = 0; i < count; i++) {
238
+ const offset = view.getUint32(offsetsStart + i * 4);
239
+ if (offset & IDX_V2.largeOffsetFlag) return null;
240
+ const at = namesStart + i * OID_BYTES;
241
+ entries.push({ oid: toHex(idx.subarray(at, at + OID_BYTES)), offset });
242
+ }
243
+ return entries;
244
+ }
245
+ function indexPack(idx, pack) {
246
+ const located = readPackIndex(idx);
247
+ if (!located) return null;
248
+ located.sort((a, b) => a.offset - b.offset);
249
+ const entries = /* @__PURE__ */ new Map();
250
+ const oidAtOffset = /* @__PURE__ */ new Map();
251
+ const packEnd = pack.byteLength - PACK_TRAILER_BYTES;
252
+ for (let i = 0; i < located.length; i++) {
253
+ const { oid, offset } = located[i];
254
+ const end = located[i + 1]?.offset ?? packEnd;
255
+ entries.set(oid, { pack, start: offset, end });
256
+ oidAtOffset.set(offset, oid);
257
+ }
258
+ return { entries, oidAtOffset };
259
+ }
260
+ function readEntryHeader(bytes, start) {
261
+ let byte = bytes[start];
262
+ const type = byte >> 4 & 7;
263
+ let size = byte & 15;
264
+ let shift = 4;
265
+ let at = start + 1;
266
+ while (byte & 128) {
267
+ byte = bytes[at++];
268
+ size += (byte & 127) * 2 ** shift;
269
+ shift += 7;
270
+ }
271
+ return { type, size, length: at - start };
272
+ }
273
+ function encodeEntryHeader(type, size) {
274
+ const bytes = [];
275
+ let rest = Math.floor(size / 16);
276
+ bytes.push((rest > 0 ? 128 : 0) | type << 4 | size % 16);
277
+ while (rest > 0) {
278
+ const low = rest % 128;
279
+ rest = Math.floor(rest / 128);
280
+ bytes.push((rest > 0 ? 128 : 0) | low);
281
+ }
282
+ return new Uint8Array(bytes);
283
+ }
284
+ function readOfsDeltaOffset(bytes, start) {
285
+ let at = start;
286
+ let byte = bytes[at++];
287
+ let distance = byte & 127;
288
+ while (byte & 128) {
289
+ byte = bytes[at++];
290
+ distance = (distance + 1) * 128 + (byte & 127);
291
+ }
292
+ return { distance, length: at - start };
293
+ }
294
+ function reusableEntry(entry, indexed) {
295
+ const { pack, start, end } = entry;
296
+ const header = readEntryHeader(pack, start);
297
+ const bodyStart = start + header.length;
298
+ if (header.type === OBJ_REF_DELTA) {
299
+ return {
300
+ bytes: pack.subarray(start, end),
301
+ baseOid: toHex(pack.subarray(bodyStart, bodyStart + OID_BYTES))
302
+ };
303
+ }
304
+ if (header.type !== OBJ_OFS_DELTA) {
305
+ return { bytes: pack.subarray(start, end), baseOid: null };
306
+ }
307
+ const { distance, length } = readOfsDeltaOffset(pack, bodyStart);
308
+ const baseOid = indexed.oidAtOffset.get(start - distance);
309
+ if (baseOid === void 0) return null;
310
+ return {
311
+ bytes: concat(
312
+ encodeEntryHeader(OBJ_REF_DELTA, header.size),
313
+ fromHex(baseOid),
314
+ pack.subarray(bodyStart + length, end)
315
+ ),
316
+ baseOid
317
+ };
318
+ }
319
+ async function buildPackFromExistingPacks(repo, oids) {
320
+ const fsp = rawFs(repo);
321
+ const packDir = `${repo.gitdir}/objects/pack`;
322
+ const packs = parsePackDirectory(
323
+ await fsp.readdir(packDir).catch(() => [])
324
+ ).filter((file) => file.kind === "pack");
325
+ const indexes = [];
326
+ for (const { base } of packs) {
327
+ const [pack, idx] = await Promise.all([
328
+ fsp.readFile(`${packDir}/${packFileName(base, "pack")}`),
329
+ fsp.readFile(`${packDir}/${packFileName(base, "idx")}`)
330
+ ]).catch(() => [null, null]);
331
+ if (!(pack instanceof Uint8Array) || !(idx instanceof Uint8Array)) {
332
+ return null;
333
+ }
334
+ const indexed = indexPack(idx, pack);
335
+ if (!indexed) return null;
336
+ indexes.push(indexed);
337
+ }
338
+ const locate = (oid) => {
339
+ for (const indexed of indexes) {
340
+ const entry = indexed.entries.get(oid);
341
+ if (entry) return reusableEntry(entry, indexed);
342
+ }
343
+ return null;
344
+ };
345
+ const ordered = [];
346
+ const done = /* @__PURE__ */ new Set();
347
+ const pending = /* @__PURE__ */ new Set();
348
+ const visit = (oid) => {
349
+ if (done.has(oid)) return true;
350
+ if (pending.has(oid)) return false;
351
+ const entry = locate(oid);
352
+ if (!entry) return false;
353
+ pending.add(oid);
354
+ if (entry.baseOid !== null && !visit(entry.baseOid)) return false;
355
+ pending.delete(oid);
356
+ done.add(oid);
357
+ ordered.push(entry.bytes);
358
+ return true;
359
+ };
360
+ for (const oid of oids) {
361
+ if (!visit(oid)) return null;
362
+ }
363
+ const header = new Uint8Array(12);
364
+ header.set(encodeUtf8("PACK"), 0);
365
+ const view = new DataView(header.buffer);
366
+ view.setUint32(4, 2);
367
+ view.setUint32(8, ordered.length);
368
+ const body = concat(header, ...ordered);
369
+ return concat(body, fromHex(await sha1(body)));
370
+ }
371
+
372
+ // src/http/repack.ts
211
373
  var PACK_OBJECT_TYPE_BITS = {
212
374
  commit: 16,
213
375
  tree: 32,
@@ -291,13 +453,13 @@ async function buildVerifiedPack(oids, objects) {
291
453
  return concat(packBody, trailer);
292
454
  }
293
455
  var REPACK_PACK_COUNT_THRESHOLD = 4;
456
+ async function readPackDirectory(repo) {
457
+ const entries = await rawFs(repo).readdir(`${repo.gitdir}/objects/pack`).catch(() => []);
458
+ return parsePackDirectory(entries);
459
+ }
294
460
  async function countPacks(repo) {
295
- try {
296
- const entries = await rawFs(repo).readdir(`${repo.gitdir}/objects/pack`);
297
- return entries.filter((f) => f.endsWith(".pack")).length;
298
- } catch {
299
- return 0;
300
- }
461
+ const files = await readPackDirectory(repo);
462
+ return files.filter((file) => file.kind === "pack").length;
301
463
  }
302
464
  async function repackRepository(repo, options, hooks) {
303
465
  const threshold = options?.threshold ?? REPACK_PACK_COUNT_THRESHOLD;
@@ -320,41 +482,48 @@ async function repackRepository(repo, options, hooks) {
320
482
  )
321
483
  )).filter((oid) => oid !== null);
322
484
  if (tipOids.length === 0) return [];
323
- const { oids, complete } = await collectReachableOids(repo, tipOids, hooks);
324
- if (!complete || oids.length === 0) return [];
325
- const objects = await readAndVerifyObjects(repo, oids);
326
- const packBuffer = await buildVerifiedPack(oids, objects);
485
+ const { oids: reachable, complete } = await collectReachableOids(
486
+ repo,
487
+ tipOids,
488
+ hooks
489
+ );
490
+ if (!complete || reachable.length === 0) return [];
327
491
  const packDir = `${repo.gitdir}/objects/pack`;
492
+ const packBuffer = await buildPackFromExistingPacks(repo, reachable) ?? await buildVerifiedPack(
493
+ reachable,
494
+ await readAndVerifyObjects(repo, reachable)
495
+ );
328
496
  await fsp.mkdir(packDir, { recursive: true });
329
497
  const newBase = `pack-${Date.now()}`;
330
- const newPackFile = `${newBase}.pack`;
331
- const newIdxFile = `${newBase}.idx`;
498
+ const newPackFile = packFileName(newBase, "pack");
332
499
  await fsp.writeFile(`${packDir}/${newPackFile}`, packBuffer);
333
500
  const { oids: indexedOids } = await git3.indexPack({
334
501
  ...repo,
335
502
  dir: packDir,
336
503
  filepath: newPackFile
337
504
  });
338
- const expected = new Set(oids);
339
505
  const indexed = new Set(indexedOids);
340
- if (indexed.size !== expected.size || oids.some((oid) => !indexed.has(oid))) {
341
- await fsp.unlink(`${packDir}/${newPackFile}`).catch(() => {
342
- });
343
- await fsp.unlink(`${packDir}/${newIdxFile}`).catch(() => {
344
- });
506
+ 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
+ );
345
513
  throw new Error(
346
- "repack: indexed pack's oid set didn't match the verified reachable set \u2014 aborting"
514
+ "repack: indexed pack is missing reachable objects \u2014 aborting"
347
515
  );
348
516
  }
349
- const allEntries = await fsp.readdir(packDir).catch(() => []);
350
- const staleFiles = allEntries.filter(
351
- (f) => f !== newPackFile && f !== newIdxFile && (f.endsWith(".pack") || f.endsWith(".idx") || f.endsWith(".keep"))
517
+ const stale = (await readPackDirectory(repo)).filter(
518
+ (file) => file.base !== newBase
352
519
  );
353
520
  await Promise.all(
354
- staleFiles.map((f) => fsp.unlink(`${packDir}/${f}`).catch(() => {
355
- }))
521
+ stale.map(
522
+ (file) => fsp.unlink(`${packDir}/${file.name}`).catch(() => {
523
+ })
524
+ )
356
525
  );
357
- return staleFiles.map((f) => `objects/pack/${f}`);
526
+ return stale.map((file) => `objects/pack/${file.name}`);
358
527
  } catch (err) {
359
528
  hooks?.onWarn?.("repack failed (non-fatal)", err);
360
529
  return [];
@@ -589,7 +758,7 @@ async function handleUploadPack(repo, body, options, hooks) {
589
758
  "readdir objects/pack",
590
759
  () => rawFs(repo).readdir(packDirPath).catch(() => [])
591
760
  );
592
- const packNames = entries.filter((f) => f.endsWith(".pack"));
761
+ const packNames = parsePackDirectory(entries).filter((file) => file.kind === "pack").map((file) => file.name);
593
762
  const packName = packNames[0];
594
763
  if (packNames.length === 1 && packName !== void 0) {
595
764
  const packData = await runStep(
@@ -611,27 +780,34 @@ async function handleUploadPack(repo, body, options, hooks) {
611
780
  if (options?.beforeWalk) {
612
781
  await runStep(hooks, "beforeWalk", options.beforeWalk);
613
782
  }
614
- const { oids: wantOids } = await runStep(
615
- hooks,
616
- "collectReachableOids(wants)",
617
- () => collectReachableOids(repo, wants, hooks)
618
- );
619
- let oids = wantOids;
783
+ let haveSet;
620
784
  if (haves.length > 0) {
621
785
  const { oids: haveOids } = await runStep(
622
786
  hooks,
623
787
  "collectReachableOids(haves)",
624
- () => collectReachableOids(repo, haves, hooks)
788
+ () => collectReachableOids(repo, haves, hooks, void 0, {
789
+ verifyBlobs: false
790
+ })
625
791
  );
626
- const haveSet = new Set(haveOids);
627
- oids = oids.filter((oid) => !haveSet.has(oid));
792
+ haveSet = new Set(haveOids);
628
793
  }
629
- const { packfile } = await runStep(
794
+ const { oids: wantOids } = await runStep(
795
+ hooks,
796
+ "collectReachableOids(wants)",
797
+ () => collectReachableOids(repo, wants, hooks, haveSet, {
798
+ verifyBlobs: false
799
+ })
800
+ );
801
+ const oids = haveSet ? wantOids.filter((oid) => !haveSet.has(oid)) : wantOids;
802
+ const reused = await runStep(
630
803
  hooks,
631
- `packObjects (${oids.length} oids)`,
632
- () => git5.packObjects({ ...repo, oids })
804
+ `reuse pack entries (${oids.length} oids)`,
805
+ () => buildPackFromExistingPacks(repo, oids)
633
806
  );
634
- const packBytes = packfile instanceof Uint8Array ? packfile : new Uint8Array(packfile ?? []);
807
+ const packBytes = reused ?? await runStep(hooks, `packObjects (${oids.length} oids)`, async () => {
808
+ const { packfile } = await git5.packObjects({ ...repo, oids });
809
+ return packfile instanceof Uint8Array ? packfile : new Uint8Array(packfile ?? []);
810
+ });
635
811
  return {
636
812
  status: 200,
637
813
  headers: {