git-fs-s3 0.15.2 → 0.15.3

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.
@@ -0,0 +1,646 @@
1
+ import { A as GitRefNotFoundError, B as fromHex, C as GitConflictError, D as GitPathNotFoundError, E as GitObjectNotFoundError, F as concat, G as sha1, N as isAbsentError, R as deflate, T as GitInvalidRequestError, m as rawFs, n as isSafeBranchName, q as toHex, s as qualifyBranchRef, v as packFileName, z as encodeUtf8 } from "./refs-Cb_NfKWI.js";
2
+ import git from "isomorphic-git";
3
+ //#region src/ops/tree.ts
4
+ const joinPath = (prefix, name) => prefix ? `${prefix.replace(/\/+$/, "")}/${name}` : name;
5
+ /** File modes a blob entry can carry; anything else isn't a regular file. */
6
+ const BLOB_MODES = /* @__PURE__ */ new Set([
7
+ "100644",
8
+ "100755",
9
+ "120000"
10
+ ]);
11
+ /**
12
+ * Build/update a git tree by overlaying new blobs onto an existing tree,
13
+ * returning the new root tree oid. `entries` maps relative paths to blob oids.
14
+ *
15
+ * An updated file keeps its existing mode (executable bit, symlink); a path
16
+ * that names an existing directory, or runs through an existing file, is
17
+ * rejected rather than silently replacing what's there.
18
+ */
19
+ async function upsertTree(repo, treeOid, entries) {
20
+ const existing = treeOid ? (await git.readTree({
21
+ ...repo,
22
+ oid: treeOid
23
+ })).tree : [];
24
+ const byName = new Map(existing.map((e) => [e.path, e]));
25
+ const direct = /* @__PURE__ */ new Map();
26
+ const nested = /* @__PURE__ */ new Map();
27
+ for (const [filePath, blobOid] of entries) {
28
+ const slash = filePath.indexOf("/");
29
+ if (slash === -1) direct.set(filePath, blobOid);
30
+ else {
31
+ const dir = filePath.slice(0, slash);
32
+ const rest = filePath.slice(slash + 1);
33
+ if (!nested.has(dir)) nested.set(dir, /* @__PURE__ */ new Map());
34
+ nested.get(dir)?.set(rest, blobOid);
35
+ }
36
+ }
37
+ for (const [name, blobOid] of direct) {
38
+ const current = byName.get(name);
39
+ if (current && current.type !== "blob") throw new GitInvalidRequestError(`"${name}" is a directory, not a file`);
40
+ byName.set(name, {
41
+ mode: current && BLOB_MODES.has(current.mode) ? current.mode : "100644",
42
+ path: name,
43
+ oid: blobOid,
44
+ type: "blob"
45
+ });
46
+ }
47
+ const nestedResults = await Promise.all(Array.from(nested, async ([dir, subEntries]) => {
48
+ const entry = byName.get(dir);
49
+ if (entry && entry.type !== "tree") throw new GitInvalidRequestError(`"${dir}" is a file, not a directory`);
50
+ const subtreeOid = entry?.oid;
51
+ return [dir, await upsertTree(repo, subtreeOid, subEntries)];
52
+ }));
53
+ for (const [dir, newOid] of nestedResults) byName.set(dir, {
54
+ mode: "040000",
55
+ path: dir,
56
+ oid: newOid,
57
+ type: "tree"
58
+ });
59
+ return git.writeTree({
60
+ ...repo,
61
+ tree: Array.from(byName.values())
62
+ });
63
+ }
64
+ /**
65
+ * Remove a file path from a tree, returning the new root tree oid. Throws
66
+ * GitPathNotFoundError when there's no such file; directories left empty
67
+ * are dropped, as git never stores an empty subtree.
68
+ */
69
+ async function deleteFromTree(repo, treeOid, filePath) {
70
+ return await deleteFromSubtree(repo, treeOid, filePath, filePath) ?? git.writeTree({
71
+ ...repo,
72
+ tree: []
73
+ });
74
+ }
75
+ /** The new subtree oid, or null when removing the file left it empty. */
76
+ async function deleteFromSubtree(repo, treeOid, filePath, fullPath) {
77
+ const existing = (await git.readTree({
78
+ ...repo,
79
+ oid: treeOid
80
+ })).tree;
81
+ const byName = new Map(existing.map((e) => [e.path, e]));
82
+ const slash = filePath.indexOf("/");
83
+ const name = slash === -1 ? filePath : filePath.slice(0, slash);
84
+ const entry = byName.get(name);
85
+ if (slash === -1) {
86
+ if (entry?.type !== "blob") throw new GitPathNotFoundError(`No such file: ${fullPath}`);
87
+ byName.delete(name);
88
+ } else {
89
+ if (entry?.type !== "tree") throw new GitPathNotFoundError(`No such file: ${fullPath}`);
90
+ const newOid = await deleteFromSubtree(repo, entry.oid, filePath.slice(slash + 1), fullPath);
91
+ if (newOid === null) byName.delete(name);
92
+ else byName.set(name, {
93
+ ...entry,
94
+ oid: newOid
95
+ });
96
+ }
97
+ if (byName.size === 0) return null;
98
+ return git.writeTree({
99
+ ...repo,
100
+ tree: Array.from(byName.values())
101
+ });
102
+ }
103
+ /** Resolve a path inside a tree to its entry, or null when absent. */
104
+ async function findTreeEntry(repo, rootTreeOid, treePath) {
105
+ if (!treePath) return {
106
+ path: "",
107
+ mode: "040000",
108
+ type: "tree",
109
+ oid: rootTreeOid
110
+ };
111
+ const parts = treePath.split("/").filter(Boolean);
112
+ let currentTreeOid = rootTreeOid;
113
+ let currentPath = "";
114
+ for (const [index, part] of parts.entries()) {
115
+ const entry = (await git.readTree({
116
+ ...repo,
117
+ oid: currentTreeOid
118
+ })).tree.find((candidate) => candidate.path === part);
119
+ if (!entry) return null;
120
+ currentPath = currentPath ? joinPath(currentPath, entry.path) : entry.path;
121
+ if (index === parts.length - 1) return {
122
+ path: currentPath,
123
+ mode: entry.mode,
124
+ type: entry.type,
125
+ oid: entry.oid
126
+ };
127
+ if (entry.type !== "tree") return null;
128
+ currentTreeOid = entry.oid;
129
+ }
130
+ return null;
131
+ }
132
+ /** List a tree's direct entries, with paths prefixed by `prefix`. */
133
+ async function listTreeEntries(repo, treeOid, prefix = "") {
134
+ return (await git.readTree({
135
+ ...repo,
136
+ oid: treeOid
137
+ })).tree.map((entry) => ({
138
+ path: prefix ? joinPath(prefix, entry.path) : entry.path,
139
+ mode: entry.mode,
140
+ type: entry.type,
141
+ oid: entry.oid
142
+ }));
143
+ }
144
+ //#endregion
145
+ //#region src/ops/types.ts
146
+ const runStep = (hooks, label, fn) => hooks?.step ? hooks.step(label, fn) : fn();
147
+ /**
148
+ * The result-cache key prefixes holding entries for `gitdir` — evict these
149
+ * from your {@link ResultCache} when the repo's storage was rewritten outside
150
+ * a normal push (rename, repack).
151
+ */
152
+ function resultKeyPrefixes(gitdir) {
153
+ return [
154
+ `commitlog:${gitdir}:`,
155
+ `tree:${gitdir}:`,
156
+ `last-commits:${gitdir}:`,
157
+ `file-history:${gitdir}:`
158
+ ];
159
+ }
160
+ //#endregion
161
+ //#region src/ops/history.ts
162
+ /** How far back history walks look by default (commit count). */
163
+ const HISTORY_WALK_DEPTH = 400;
164
+ function summarizeCommit(commit) {
165
+ return {
166
+ sha: commit.oid,
167
+ message: commit.commit.message.trim(),
168
+ authorName: commit.commit.author.name,
169
+ authorEmail: commit.commit.author.email,
170
+ createdAt: (/* @__PURE__ */ new Date(commit.commit.author.timestamp * 1e3)).toISOString()
171
+ };
172
+ }
173
+ const isNotFound = (err) => err?.code === "NotFoundError";
174
+ /**
175
+ * A ref that doesn't resolve is an expected condition (empty repo, unborn
176
+ * branch) each caller handles. An object missing *underneath* a resolved ref
177
+ * means storage is inconsistent — raised as GitObjectNotFoundError so it is
178
+ * never mistaken for "empty".
179
+ */
180
+ function wrapMissingObject(promise, context) {
181
+ return promise.catch((err) => {
182
+ if (isNotFound(err)) throw new GitObjectNotFoundError(`Git data for ${context} is missing from storage`);
183
+ throw err;
184
+ });
185
+ }
186
+ /** The commit a branch name, full ref or sha points at; GitRefNotFoundError if none. */
187
+ /**
188
+ * A short name tries `refs/heads/<name>` first and then `refs/tags/<name>`,
189
+ * so a branch wins over a same-named tag. Annotated tags are peeled to the
190
+ * commit they point at, so every read accepts a tag wherever it accepts a
191
+ * branch.
192
+ */
193
+ async function resolveRefOid(repo, ref) {
194
+ const qualified = qualifyBranchRef(ref);
195
+ const candidates = qualified === `refs/heads/${ref}` ? [qualified, `refs/tags/${ref}`] : [qualified];
196
+ for (const candidate of candidates) try {
197
+ return await peelToCommit(repo, await git.resolveRef({
198
+ ...repo,
199
+ ref: candidate
200
+ }));
201
+ } catch (err) {
202
+ if (!isNotFound(err)) throw err;
203
+ }
204
+ throw new GitRefNotFoundError(`Ref ${ref} does not exist`);
205
+ }
206
+ /** Follow annotated tags (possibly nested) down to the object they name. */
207
+ async function peelToCommit(repo, oid) {
208
+ let current = oid;
209
+ for (let depth = 0; depth < 10; depth++) {
210
+ const { type, object } = await git.readObject({
211
+ ...repo,
212
+ oid: current,
213
+ format: "parsed"
214
+ });
215
+ if (type !== "tag") return current;
216
+ current = object.object;
217
+ }
218
+ throw new GitRefNotFoundError(`Tag chain at ${oid} is too deep`);
219
+ }
220
+ /** Resolve a branch name / full ref / sha to its commit. */
221
+ async function resolveCommit(repo, ref) {
222
+ const oid = await resolveRefOid(repo, ref);
223
+ const { commit } = await wrapMissingObject(git.readCommit({
224
+ ...repo,
225
+ oid
226
+ }), `${repo.gitdir} commit ${oid}`);
227
+ return {
228
+ oid,
229
+ commit
230
+ };
231
+ }
232
+ /** Read a blob's bytes by oid. */
233
+ async function getBlob(repo, sha) {
234
+ const { blob } = await wrapMissingObject(git.readBlob({
235
+ ...repo,
236
+ oid: sha
237
+ }), `${repo.gitdir} blob ${sha}`);
238
+ return blob;
239
+ }
240
+ /** Read a file's bytes at a ref. Throws GitPathNotFoundError when absent. */
241
+ async function getFileContent(repo, filePath, ref, hooks) {
242
+ if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
243
+ const { commit } = await resolveCommit(repo, ref);
244
+ const context = `${repo.gitdir}@${ref}:${filePath}`;
245
+ const entry = await wrapMissingObject(findTreeEntry(repo, commit.tree, filePath), context);
246
+ if (entry?.type !== "blob") throw new GitPathNotFoundError(`File not found: ${filePath}`);
247
+ const { blob } = await wrapMissingObject(git.readBlob({
248
+ ...repo,
249
+ oid: entry.oid
250
+ }), context);
251
+ return blob;
252
+ }
253
+ /** Read one commit by sha. */
254
+ async function getCommit(repo, sha) {
255
+ const result = await wrapMissingObject(git.readCommit({
256
+ ...repo,
257
+ oid: sha
258
+ }), `${repo.gitdir} commit ${sha}`);
259
+ return {
260
+ oid: result.oid,
261
+ commit: result.commit,
262
+ payload: result.payload
263
+ };
264
+ }
265
+ /**
266
+ * The commit chain from a ref, newest first; [] when the ref doesn't exist.
267
+ * Walking is sequential and round-trip-bound on object storage, so the
268
+ * deepest walk seen per head is memoized in `hooks.resultCache` and sliced
269
+ * for shallower requests.
270
+ */
271
+ async function getCommitLog(repo, options, hooks) {
272
+ const { ref } = options;
273
+ const depth = options.depth ?? 50;
274
+ let headSha = options.knownHeadSha;
275
+ if (headSha === void 0) try {
276
+ headSha = await resolveRefOid(repo, ref);
277
+ } catch (err) {
278
+ if (err instanceof GitRefNotFoundError) return [];
279
+ throw err;
280
+ }
281
+ const cacheKey = `commitlog:${repo.gitdir}:${headSha}`;
282
+ const cached = hooks?.resultCache?.get(cacheKey);
283
+ const fullyWalked = cached?.at(-1)?.commit.parent.length === 0;
284
+ if (cached && (cached.length >= depth || fullyWalked)) {
285
+ hooks?.onNote?.(`getCommitLog: cache hit ${cacheKey} (depth=${depth})`);
286
+ return cached.slice(0, depth);
287
+ }
288
+ hooks?.onNote?.(`getCommitLog: cache miss ${cacheKey} (depth=${depth})`);
289
+ if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
290
+ const result = (await wrapMissingObject(runStep(hooks, `git.log ${ref} depth=${depth}`, () => git.log({
291
+ ...repo,
292
+ ref: headSha,
293
+ depth
294
+ })), `${repo.gitdir}@${ref} history`)).map(({ oid, commit, payload }) => ({
295
+ oid,
296
+ commit,
297
+ payload: payload ?? ""
298
+ }));
299
+ if (!cached || result.length > cached.length) hooks?.resultCache?.set(cacheKey, result);
300
+ return result;
301
+ }
302
+ /**
303
+ * List a directory at the tip of a ref. Throws GitRefNotFoundError when the
304
+ * ref doesn't resolve and GitPathNotFoundError when `treePath` doesn't exist.
305
+ * Memoized per head sha.
306
+ */
307
+ async function getTreeFromRef(repo, options, hooks) {
308
+ const { ref } = options;
309
+ const treePath = options.treePath ?? "";
310
+ const { commit, oid: headSha } = await resolveCommit(repo, ref);
311
+ const cacheKey = `tree:${repo.gitdir}:${headSha}:${treePath}`;
312
+ const cached = hooks?.resultCache?.get(cacheKey);
313
+ if (cached) {
314
+ hooks?.onNote?.(`getTreeFromRef: cache hit ${cacheKey}`);
315
+ return cached;
316
+ }
317
+ hooks?.onNote?.(`getTreeFromRef: cache miss ${cacheKey}`);
318
+ if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
319
+ const context = `${repo.gitdir}@${ref}:${treePath || "/"}`;
320
+ const dir = treePath ? await wrapMissingObject(runStep(hooks, `findTreeEntry ${treePath}`, () => findTreeEntry(repo, commit.tree, treePath)), context) : {
321
+ path: "",
322
+ type: "tree",
323
+ oid: commit.tree
324
+ };
325
+ if (!dir) throw new GitPathNotFoundError(`Path "${treePath}" does not exist at ${ref}`);
326
+ const result = dir.type === "tree" ? await wrapMissingObject(runStep(hooks, `listTreeEntries ${treePath || "/"}`, () => listTreeEntries(repo, dir.oid, dir.path)), context) : [];
327
+ hooks?.resultCache?.set(cacheKey, result);
328
+ return result;
329
+ }
330
+ /**
331
+ * A page of commit history from a ref tip; [] when the ref doesn't exist.
332
+ * Pages are slices of {@link getCommitLog}'s memoized walk.
333
+ */
334
+ async function getCommitHistory(repo, options, hooks) {
335
+ const limit = options.limit ?? 50;
336
+ const skip = options.skip ?? 0;
337
+ return (await getCommitLog(repo, {
338
+ ref: options.ref,
339
+ depth: limit + skip
340
+ }, hooks)).slice(skip, skip + limit);
341
+ }
342
+ //#endregion
343
+ //#region src/ops/branch.ts
344
+ /**
345
+ * Defense in depth for every branch-name argument below: `git.deleteBranch`
346
+ * and raw resolveRef/writeRef reads don't validate ref names internally the
347
+ * way `git.branch` does (see refs.ts) — guard at the point the primitives are
348
+ * actually called, not just at an API boundary far above.
349
+ */
350
+ function assertSafeBranchName(name) {
351
+ if (!isSafeBranchName(name)) throw new GitInvalidRequestError(`Invalid branch name: ${name}`);
352
+ }
353
+ const FULL_SHA_RE = /^[0-9a-f]{40}$/i;
354
+ function hasRecursiveFileLister(fs) {
355
+ return typeof fs === "object" && fs !== null && "listFilesRecursively" in fs && typeof fs.listFilesRecursively === "function";
356
+ }
357
+ function getPromisesFs(fs) {
358
+ return fs.promises ?? null;
359
+ }
360
+ function decodeFileContent(content) {
361
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
362
+ }
363
+ /**
364
+ * Lists loose branch names without isomorphic-git's recursive readdir/stat
365
+ * traversal. Object stores already return only leaf keys in a recursive LIST,
366
+ * so this is one request regardless of branch-name nesting. Ordinary
367
+ * filesystems retain isomorphic-git's implementation and packed refs remain
368
+ * supported there.
369
+ */
370
+ async function listLooseBranches(repo) {
371
+ return listLooseRefNames(repo, "refs/heads");
372
+ }
373
+ /**
374
+ * Loose ref names under `dir` (e.g. `refs/tags`) in one recursive listing,
375
+ * or `null` when the fs can't list recursively (use isomorphic-git's walk).
376
+ */
377
+ async function listLooseRefNames(repo, dir) {
378
+ if (!hasRecursiveFileLister(repo.fs)) return null;
379
+ return (await repo.fs.listFilesRecursively(`${repo.gitdir}/${dir}`)).filter(isSafeBranchName);
380
+ }
381
+ /** Read packed branch refs so the object-store fast path preserves Git semantics. */
382
+ async function readPackedBranches(repo) {
383
+ if (!hasRecursiveFileLister(repo.fs)) return /* @__PURE__ */ new Map();
384
+ const promisesFs = getPromisesFs(repo.fs);
385
+ if (!promisesFs) return /* @__PURE__ */ new Map();
386
+ try {
387
+ const content = await promisesFs.readFile(`${repo.gitdir}/packed-refs`, "utf8");
388
+ const refs = /* @__PURE__ */ new Map();
389
+ for (const line of decodeFileContent(content).split("\n")) {
390
+ const match = /^([0-9a-f]{40}) refs\/heads\/(.+)$/i.exec(line);
391
+ if (match?.[1] && match[2] && isSafeBranchName(match[2])) refs.set(match[2], match[1]);
392
+ }
393
+ return refs;
394
+ } catch (error) {
395
+ if (isAbsentError(error)) return /* @__PURE__ */ new Map();
396
+ throw error;
397
+ }
398
+ }
399
+ /** Read the symbolic HEAD directly when the object-store fs is available. */
400
+ async function getCurrentLooseBranch(repo) {
401
+ if (!hasRecursiveFileLister(repo.fs)) return null;
402
+ const promisesFs = getPromisesFs(repo.fs);
403
+ if (!promisesFs) return null;
404
+ try {
405
+ const head = decodeFileContent(await promisesFs.readFile(`${repo.gitdir}/HEAD`, "utf8")).trim();
406
+ const match = /^ref: refs\/heads\/(.+)$/.exec(head);
407
+ return match?.[1] && isSafeBranchName(match[1]) ? match[1] : null;
408
+ } catch (error) {
409
+ if (isAbsentError(error)) return null;
410
+ throw error;
411
+ }
412
+ }
413
+ /**
414
+ * Resolve a fully-qualified ref's oid with one read instead of isomorphic-
415
+ * git's own `resolveRef`, which does a stat *then* a read for a loose ref
416
+ * (two round trips against object storage) — wasteful specifically here,
417
+ * where the caller already knows the ref exists (it came from a directory
418
+ * listing moments earlier). Reads the loose file directly and falls back to
419
+ * `git.resolveRef` only when that doesn't pan out (packed-refs, or anything
420
+ * else the plain-loose-file assumption doesn't cover), so correctness for
421
+ * those cases is unchanged — this only removes the redundant round trip on
422
+ * the common path.
423
+ */
424
+ async function resolveLooseRefFast(repo, ref) {
425
+ const promisesFs = getPromisesFs(repo.fs);
426
+ if (promisesFs) try {
427
+ const oid = decodeFileContent(await promisesFs.readFile(`${repo.gitdir}/${ref}`, "utf8")).trim();
428
+ if (FULL_SHA_RE.test(oid)) return oid;
429
+ } catch {}
430
+ return git.resolveRef({
431
+ ...repo,
432
+ ref
433
+ });
434
+ }
435
+ /** All branches with their tip commits; [] for an empty repository. */
436
+ async function listBranches(repo) {
437
+ try {
438
+ const [looseBranches, looseCurrentBranch, packedBranches] = await Promise.all([
439
+ listLooseBranches(repo),
440
+ getCurrentLooseBranch(repo),
441
+ readPackedBranches(repo)
442
+ ]);
443
+ const branches = looseBranches === null ? await git.listBranches(repo) : [.../* @__PURE__ */ new Set([...looseBranches, ...packedBranches.keys()])].sort();
444
+ const currentBranch = looseBranches === null ? await git.currentBranch({
445
+ ...repo,
446
+ fullname: false
447
+ }).catch((error) => {
448
+ if (isAbsentError(error)) return null;
449
+ throw error;
450
+ }) : looseCurrentBranch;
451
+ return Promise.all(branches.map(async (branch) => {
452
+ const packedCommit = packedBranches.get(branch);
453
+ return {
454
+ name: branch,
455
+ commit: packedCommit && !looseBranches?.includes(branch) ? packedCommit : await resolveLooseRefFast(repo, `refs/heads/${branch}`),
456
+ isDefault: branch === currentBranch
457
+ };
458
+ }));
459
+ } catch (err) {
460
+ if (err.code === "NotFoundError") return [];
461
+ throw err;
462
+ }
463
+ }
464
+ /** Create `name` pointing at the tip of `startPoint` (no checkout). */
465
+ async function createBranchFrom(repo, name, startPoint) {
466
+ assertSafeBranchName(name);
467
+ assertSafeBranchName(startPoint);
468
+ const object = await resolveLooseRefFast(repo, `refs/heads/${startPoint}`);
469
+ try {
470
+ await git.branch({
471
+ ...repo,
472
+ ref: name,
473
+ checkout: false,
474
+ object
475
+ });
476
+ } catch (err) {
477
+ if (err.code === "AlreadyExistsError") throw new GitConflictError(`Branch ${name} already exists`);
478
+ throw err;
479
+ }
480
+ }
481
+ /** Delete a branch ref (validated — deleteBranch has no internal ref check). */
482
+ async function deleteBranchByName(repo, name) {
483
+ assertSafeBranchName(name);
484
+ if ((await getCurrentLooseBranch(repo) ?? await git.currentBranch({
485
+ ...repo,
486
+ fullname: false
487
+ }).catch((error) => {
488
+ if (isAbsentError(error)) return null;
489
+ throw error;
490
+ })) === name) throw new GitInvalidRequestError(`Cannot delete ${name}: it is the default branch`);
491
+ await git.deleteBranch({
492
+ ...repo,
493
+ ref: name
494
+ });
495
+ }
496
+ /** Throws (NotFoundError) unless the branch resolves. */
497
+ async function assertBranchExists(repo, name) {
498
+ assertSafeBranchName(name);
499
+ await resolveLooseRefFast(repo, `refs/heads/${name}`);
500
+ }
501
+ /** All tags with the commits they name, sorted by name; [] when there are none. */
502
+ async function listTags(repo) {
503
+ const names = await listLooseRefNames(repo, "refs/tags") ?? [];
504
+ const all = /* @__PURE__ */ new Set([...names, ...await git.listTags(repo).catch((error) => {
505
+ if (isAbsentError(error)) return [];
506
+ throw error;
507
+ })]);
508
+ return (await Promise.all([...all].filter(isSafeBranchName).map(async (name) => {
509
+ try {
510
+ const oid = await resolveLooseRefFast(repo, `refs/tags/${name}`);
511
+ const commit = await peelToCommit(repo, oid);
512
+ return commit === oid ? {
513
+ name,
514
+ commit
515
+ } : {
516
+ name,
517
+ commit,
518
+ tagObject: oid
519
+ };
520
+ } catch (error) {
521
+ if (isAbsentError(error)) return null;
522
+ throw error;
523
+ }
524
+ }))).filter((tag) => tag !== null).sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true }));
525
+ }
526
+ //#endregion
527
+ //#region src/pack-write.ts
528
+ /**
529
+ * Writing packfiles: serialize whole (never deltified) objects into a pack,
530
+ * and store a pack plus its index in a repository. Shared by repack (the
531
+ * loose-object fallback) and packed writes (ops/packed-writes.ts).
532
+ *
533
+ * Edge-compatible: Web Crypto SHA-1, CompressionStream, Uint8Array only.
534
+ */
535
+ const PACK_OBJECT_TYPE_BITS = {
536
+ commit: 16,
537
+ tree: 32,
538
+ blob: 48,
539
+ tag: 64
540
+ };
541
+ function encodePackObjectHeader(type, length) {
542
+ const bytes = [];
543
+ let more = length > 15;
544
+ bytes.push((more ? 128 : 0) | PACK_OBJECT_TYPE_BITS[type] | length & 15);
545
+ let rest = Math.floor(length / 16);
546
+ while (more) {
547
+ more = rest > 127;
548
+ bytes.push((more ? 128 : 0) | rest & 127);
549
+ rest = Math.floor(rest / 128);
550
+ }
551
+ return new Uint8Array(bytes);
552
+ }
553
+ /** Git's object id: sha1("<type> <byte length>\0<content>"). */
554
+ function hashGitObject(type, content) {
555
+ return sha1(concat(encodeUtf8(`${type} ${content.length}\0`), content));
556
+ }
557
+ /** A version-2 pack holding `objects` as full entries, in order. */
558
+ async function encodePack(objects) {
559
+ const header = /* @__PURE__ */ new Uint8Array(12);
560
+ const view = new DataView(header.buffer);
561
+ header.set(encodeUtf8("PACK"), 0);
562
+ view.setUint32(4, 2, false);
563
+ view.setUint32(8, objects.length, false);
564
+ const chunks = [header];
565
+ const BATCH_SIZE = 100;
566
+ for (let i = 0; i < objects.length; i += BATCH_SIZE) {
567
+ const encoded = await Promise.all(objects.slice(i, i + BATCH_SIZE).map(async (object) => concat(encodePackObjectHeader(object.type, object.content.length), await deflate(object.content))));
568
+ chunks.push(...encoded);
569
+ }
570
+ const body = concat(...chunks);
571
+ return concat(body, fromHex(await sha1(body)));
572
+ }
573
+ /**
574
+ * A pack's conventional base name, `pack-<checksum>`: the trailer SHA-1 is a
575
+ * function of the content, so two writers can never pick the same name for
576
+ * different packs, and rewriting the same pack is idempotent.
577
+ */
578
+ function packBaseName(pack) {
579
+ return `pack-${toHex(pack.subarray(pack.length - 20))}`;
580
+ }
581
+ /**
582
+ * Store `pack` in `repo` and index it (isomorphic-git's `indexPack`, which
583
+ * re-hashes every object). The `.idx` is written last: a pack only becomes
584
+ * visible to readers once its index exists. Returns the base name and the
585
+ * oids the index lists.
586
+ */
587
+ async function storePack(repo, pack) {
588
+ const fsp = rawFs(repo);
589
+ const packDir = `${repo.gitdir}/objects/pack`;
590
+ const base = packBaseName(pack);
591
+ await fsp.mkdir(packDir, { recursive: true });
592
+ await fsp.writeFile(`${packDir}/${packFileName(base, "pack")}`, pack);
593
+ const { oids } = await git.indexPack({
594
+ ...repo,
595
+ dir: packDir,
596
+ filepath: packFileName(base, "pack")
597
+ });
598
+ return {
599
+ base,
600
+ oids
601
+ };
602
+ }
603
+ //#endregion
604
+ //#region src/semaphore.ts
605
+ /**
606
+ * Create a FIFO semaphore admitting at most `limit` concurrent `run`
607
+ * executions. Waiters are queued as promises and released in call order — no
608
+ * polling or timers — so a caller blocked on a full semaphore is woken the
609
+ * instant a permit frees, not on the next tick of a poll loop.
610
+ */
611
+ function createSemaphore(limit) {
612
+ if (!Number.isInteger(limit) || limit < 1) throw new Error(`createSemaphore: limit must be an integer >= 1, got ${limit}`);
613
+ let active = 0;
614
+ const waiting = [];
615
+ /**
616
+ * Hand the permit to the next waiter instead of dropping it, so `active`
617
+ * only decreases when nothing is queued and never exceeds `limit`.
618
+ */
619
+ function release() {
620
+ const next = waiting.shift();
621
+ if (next) next();
622
+ else active--;
623
+ }
624
+ function acquire() {
625
+ if (active < limit) {
626
+ active++;
627
+ return Promise.resolve();
628
+ }
629
+ return new Promise((resolve) => {
630
+ waiting.push(resolve);
631
+ });
632
+ }
633
+ return { run(fn) {
634
+ return acquire().then(async () => {
635
+ try {
636
+ return await fn();
637
+ } finally {
638
+ release();
639
+ }
640
+ });
641
+ } };
642
+ }
643
+ //#endregion
644
+ export { summarizeCommit as C, findTreeEntry as D, deleteFromTree as E, listTreeEntries as O, resolveCommit as S, runStep as T, getCommitHistory as _, storePack as a, getTreeFromRef as b, createBranchFrom as c, listLooseRefNames as d, listTags as f, getCommit as g, getBlob as h, packBaseName as i, upsertTree as k, deleteBranchByName as l, HISTORY_WALK_DEPTH as m, encodePack as n, assertBranchExists as o, resolveLooseRefFast as p, hashGitObject as r, assertSafeBranchName as s, createSemaphore as t, listBranches as u, getCommitLog as v, resultKeyPrefixes as w, peelToCommit as x, getFileContent as y };
645
+
646
+ //# sourceMappingURL=semaphore-BviNoQuJ.js.map