git-fs-s3 0.15.2 → 0.16.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.
@@ -0,0 +1,686 @@
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
+ const HEX_SHA_RE = /^[0-9a-f]{4,40}$/i;
221
+ /**
222
+ * The commit oid a revision names: a branch, tag, `HEAD`, a full ref, a full
223
+ * sha or an abbreviated sha (4+ hex digits). Refs win over an abbreviated sha
224
+ * of the same spelling, as in git. Throws GitRefNotFoundError when nothing
225
+ * matches or an abbreviation is ambiguous. An abbreviation scans the pack
226
+ * indexes, so resolve once and read by the returned (immutable) oid.
227
+ */
228
+ async function resolveCommitOid(repo, rev) {
229
+ const oid = await lookupCommitOid(repo, rev);
230
+ try {
231
+ await git.readCommit({
232
+ ...repo,
233
+ oid
234
+ });
235
+ } catch (err) {
236
+ if (isNotFound(err) || err?.code === "ObjectTypeError") throw new GitRefNotFoundError(`Ref ${rev} does not exist`);
237
+ throw err;
238
+ }
239
+ return oid;
240
+ }
241
+ async function lookupCommitOid(repo, rev) {
242
+ try {
243
+ return await resolveRefOid(repo, rev);
244
+ } catch (err) {
245
+ if (!(err instanceof GitRefNotFoundError) || !HEX_SHA_RE.test(rev)) throw err;
246
+ }
247
+ const sha = rev.toLowerCase();
248
+ if (sha.length === 40) return peelToCommit(repo, sha).catch(notFoundAsRef(rev));
249
+ return git.expandOid({
250
+ ...repo,
251
+ oid: sha
252
+ }).then((full) => peelToCommit(repo, full)).catch(notFoundAsRef(rev));
253
+ }
254
+ function notFoundAsRef(rev) {
255
+ return (err) => {
256
+ if (isNotFound(err) || err?.code === "AmbiguousError") throw new GitRefNotFoundError(`Ref ${rev} does not exist`);
257
+ throw err;
258
+ };
259
+ }
260
+ /** Resolve a branch name / full ref / sha to its commit. */
261
+ async function resolveCommit(repo, ref) {
262
+ const oid = await resolveRefOid(repo, ref);
263
+ const { commit } = await wrapMissingObject(git.readCommit({
264
+ ...repo,
265
+ oid
266
+ }), `${repo.gitdir} commit ${oid}`);
267
+ return {
268
+ oid,
269
+ commit
270
+ };
271
+ }
272
+ /** Read a blob's bytes by oid. */
273
+ async function getBlob(repo, sha) {
274
+ const { blob } = await wrapMissingObject(git.readBlob({
275
+ ...repo,
276
+ oid: sha
277
+ }), `${repo.gitdir} blob ${sha}`);
278
+ return blob;
279
+ }
280
+ /** Read a file's bytes at a ref. Throws GitPathNotFoundError when absent. */
281
+ async function getFileContent(repo, filePath, ref, hooks) {
282
+ if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
283
+ const { commit } = await resolveCommit(repo, ref);
284
+ const context = `${repo.gitdir}@${ref}:${filePath}`;
285
+ const entry = await wrapMissingObject(findTreeEntry(repo, commit.tree, filePath), context);
286
+ if (entry?.type !== "blob") throw new GitPathNotFoundError(`File not found: ${filePath}`);
287
+ const { blob } = await wrapMissingObject(git.readBlob({
288
+ ...repo,
289
+ oid: entry.oid
290
+ }), context);
291
+ return blob;
292
+ }
293
+ /** Read one commit by sha. */
294
+ async function getCommit(repo, sha) {
295
+ const result = await wrapMissingObject(git.readCommit({
296
+ ...repo,
297
+ oid: sha
298
+ }), `${repo.gitdir} commit ${sha}`);
299
+ return {
300
+ oid: result.oid,
301
+ commit: result.commit,
302
+ payload: result.payload
303
+ };
304
+ }
305
+ /**
306
+ * The commit chain from a ref, newest first; [] when the ref doesn't exist.
307
+ * Walking is sequential and round-trip-bound on object storage, so the
308
+ * deepest walk seen per head is memoized in `hooks.resultCache` and sliced
309
+ * for shallower requests.
310
+ */
311
+ async function getCommitLog(repo, options, hooks) {
312
+ const { ref } = options;
313
+ const depth = options.depth ?? 50;
314
+ let headSha = options.knownHeadSha;
315
+ if (headSha === void 0) try {
316
+ headSha = await resolveRefOid(repo, ref);
317
+ } catch (err) {
318
+ if (err instanceof GitRefNotFoundError) return [];
319
+ throw err;
320
+ }
321
+ const cacheKey = `commitlog:${repo.gitdir}:${headSha}`;
322
+ const cached = hooks?.resultCache?.get(cacheKey);
323
+ const fullyWalked = cached?.at(-1)?.commit.parent.length === 0;
324
+ if (cached && (cached.length >= depth || fullyWalked)) {
325
+ hooks?.onNote?.(`getCommitLog: cache hit ${cacheKey} (depth=${depth})`);
326
+ return cached.slice(0, depth);
327
+ }
328
+ hooks?.onNote?.(`getCommitLog: cache miss ${cacheKey} (depth=${depth})`);
329
+ if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
330
+ const result = (await wrapMissingObject(runStep(hooks, `git.log ${ref} depth=${depth}`, () => git.log({
331
+ ...repo,
332
+ ref: headSha,
333
+ depth
334
+ })), `${repo.gitdir}@${ref} history`)).map(({ oid, commit, payload }) => ({
335
+ oid,
336
+ commit,
337
+ payload: payload ?? ""
338
+ }));
339
+ if (!cached || result.length > cached.length) hooks?.resultCache?.set(cacheKey, result);
340
+ return result;
341
+ }
342
+ /**
343
+ * List a directory at the tip of a ref. Throws GitRefNotFoundError when the
344
+ * ref doesn't resolve and GitPathNotFoundError when `treePath` doesn't exist.
345
+ * Memoized per head sha.
346
+ */
347
+ async function getTreeFromRef(repo, options, hooks) {
348
+ const { ref } = options;
349
+ const treePath = options.treePath ?? "";
350
+ const { commit, oid: headSha } = await resolveCommit(repo, ref);
351
+ const cacheKey = `tree:${repo.gitdir}:${headSha}:${treePath}`;
352
+ const cached = hooks?.resultCache?.get(cacheKey);
353
+ if (cached) {
354
+ hooks?.onNote?.(`getTreeFromRef: cache hit ${cacheKey}`);
355
+ return cached;
356
+ }
357
+ hooks?.onNote?.(`getTreeFromRef: cache miss ${cacheKey}`);
358
+ if (hooks?.prefetch) await runStep(hooks, "prefetch", hooks.prefetch);
359
+ const context = `${repo.gitdir}@${ref}:${treePath || "/"}`;
360
+ const dir = treePath ? await wrapMissingObject(runStep(hooks, `findTreeEntry ${treePath}`, () => findTreeEntry(repo, commit.tree, treePath)), context) : {
361
+ path: "",
362
+ type: "tree",
363
+ oid: commit.tree
364
+ };
365
+ if (!dir) throw new GitPathNotFoundError(`Path "${treePath}" does not exist at ${ref}`);
366
+ const result = dir.type === "tree" ? await wrapMissingObject(runStep(hooks, `listTreeEntries ${treePath || "/"}`, () => listTreeEntries(repo, dir.oid, dir.path)), context) : [];
367
+ hooks?.resultCache?.set(cacheKey, result);
368
+ return result;
369
+ }
370
+ /**
371
+ * A page of commit history from a ref tip; [] when the ref doesn't exist.
372
+ * Pages are slices of {@link getCommitLog}'s memoized walk.
373
+ */
374
+ async function getCommitHistory(repo, options, hooks) {
375
+ const limit = options.limit ?? 50;
376
+ const skip = options.skip ?? 0;
377
+ return (await getCommitLog(repo, {
378
+ ref: options.ref,
379
+ depth: limit + skip
380
+ }, hooks)).slice(skip, skip + limit);
381
+ }
382
+ //#endregion
383
+ //#region src/ops/branch.ts
384
+ /**
385
+ * Defense in depth for every branch-name argument below: `git.deleteBranch`
386
+ * and raw resolveRef/writeRef reads don't validate ref names internally the
387
+ * way `git.branch` does (see refs.ts) — guard at the point the primitives are
388
+ * actually called, not just at an API boundary far above.
389
+ */
390
+ function assertSafeBranchName(name) {
391
+ if (!isSafeBranchName(name)) throw new GitInvalidRequestError(`Invalid branch name: ${name}`);
392
+ }
393
+ const FULL_SHA_RE = /^[0-9a-f]{40}$/i;
394
+ function hasRecursiveFileLister(fs) {
395
+ return typeof fs === "object" && fs !== null && "listFilesRecursively" in fs && typeof fs.listFilesRecursively === "function";
396
+ }
397
+ function getPromisesFs(fs) {
398
+ return fs.promises ?? null;
399
+ }
400
+ function decodeFileContent(content) {
401
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
402
+ }
403
+ /**
404
+ * Lists loose branch names without isomorphic-git's recursive readdir/stat
405
+ * traversal. Object stores already return only leaf keys in a recursive LIST,
406
+ * so this is one request regardless of branch-name nesting. Ordinary
407
+ * filesystems retain isomorphic-git's implementation and packed refs remain
408
+ * supported there.
409
+ */
410
+ async function listLooseBranches(repo) {
411
+ return listLooseRefNames(repo, "refs/heads");
412
+ }
413
+ /**
414
+ * Loose ref names under `dir` (e.g. `refs/tags`) in one recursive listing,
415
+ * or `null` when the fs can't list recursively (use isomorphic-git's walk).
416
+ */
417
+ async function listLooseRefNames(repo, dir) {
418
+ if (!hasRecursiveFileLister(repo.fs)) return null;
419
+ return (await repo.fs.listFilesRecursively(`${repo.gitdir}/${dir}`)).filter(isSafeBranchName);
420
+ }
421
+ /** Read packed branch refs so the object-store fast path preserves Git semantics. */
422
+ async function readPackedBranches(repo) {
423
+ if (!hasRecursiveFileLister(repo.fs)) return /* @__PURE__ */ new Map();
424
+ const promisesFs = getPromisesFs(repo.fs);
425
+ if (!promisesFs) return /* @__PURE__ */ new Map();
426
+ try {
427
+ const content = await promisesFs.readFile(`${repo.gitdir}/packed-refs`, "utf8");
428
+ const refs = /* @__PURE__ */ new Map();
429
+ for (const line of decodeFileContent(content).split("\n")) {
430
+ const match = /^([0-9a-f]{40}) refs\/heads\/(.+)$/i.exec(line);
431
+ if (match?.[1] && match[2] && isSafeBranchName(match[2])) refs.set(match[2], match[1]);
432
+ }
433
+ return refs;
434
+ } catch (error) {
435
+ if (isAbsentError(error)) return /* @__PURE__ */ new Map();
436
+ throw error;
437
+ }
438
+ }
439
+ /** Read the symbolic HEAD directly when the object-store fs is available. */
440
+ async function getCurrentLooseBranch(repo) {
441
+ if (!hasRecursiveFileLister(repo.fs)) return null;
442
+ const promisesFs = getPromisesFs(repo.fs);
443
+ if (!promisesFs) return null;
444
+ try {
445
+ const head = decodeFileContent(await promisesFs.readFile(`${repo.gitdir}/HEAD`, "utf8")).trim();
446
+ const match = /^ref: refs\/heads\/(.+)$/.exec(head);
447
+ return match?.[1] && isSafeBranchName(match[1]) ? match[1] : null;
448
+ } catch (error) {
449
+ if (isAbsentError(error)) return null;
450
+ throw error;
451
+ }
452
+ }
453
+ /**
454
+ * Resolve a fully-qualified ref's oid with one read instead of isomorphic-
455
+ * git's own `resolveRef`, which does a stat *then* a read for a loose ref
456
+ * (two round trips against object storage) — wasteful specifically here,
457
+ * where the caller already knows the ref exists (it came from a directory
458
+ * listing moments earlier). Reads the loose file directly and falls back to
459
+ * `git.resolveRef` only when that doesn't pan out (packed-refs, or anything
460
+ * else the plain-loose-file assumption doesn't cover), so correctness for
461
+ * those cases is unchanged — this only removes the redundant round trip on
462
+ * the common path.
463
+ */
464
+ async function resolveLooseRefFast(repo, ref) {
465
+ const promisesFs = getPromisesFs(repo.fs);
466
+ if (promisesFs) try {
467
+ const oid = decodeFileContent(await promisesFs.readFile(`${repo.gitdir}/${ref}`, "utf8")).trim();
468
+ if (FULL_SHA_RE.test(oid)) return oid;
469
+ } catch {}
470
+ return git.resolveRef({
471
+ ...repo,
472
+ ref
473
+ });
474
+ }
475
+ /** All branches with their tip commits; [] for an empty repository. */
476
+ async function listBranches(repo) {
477
+ try {
478
+ const [looseBranches, looseCurrentBranch, packedBranches] = await Promise.all([
479
+ listLooseBranches(repo),
480
+ getCurrentLooseBranch(repo),
481
+ readPackedBranches(repo)
482
+ ]);
483
+ const branches = looseBranches === null ? await git.listBranches(repo) : [.../* @__PURE__ */ new Set([...looseBranches, ...packedBranches.keys()])].sort();
484
+ const currentBranch = looseBranches === null ? await git.currentBranch({
485
+ ...repo,
486
+ fullname: false
487
+ }).catch((error) => {
488
+ if (isAbsentError(error)) return null;
489
+ throw error;
490
+ }) : looseCurrentBranch;
491
+ return Promise.all(branches.map(async (branch) => {
492
+ const packedCommit = packedBranches.get(branch);
493
+ return {
494
+ name: branch,
495
+ commit: packedCommit && !looseBranches?.includes(branch) ? packedCommit : await resolveLooseRefFast(repo, `refs/heads/${branch}`),
496
+ isDefault: branch === currentBranch
497
+ };
498
+ }));
499
+ } catch (err) {
500
+ if (err.code === "NotFoundError") return [];
501
+ throw err;
502
+ }
503
+ }
504
+ /** Create `name` pointing at the tip of `startPoint` (no checkout). */
505
+ async function createBranchFrom(repo, name, startPoint) {
506
+ assertSafeBranchName(name);
507
+ assertSafeBranchName(startPoint);
508
+ const object = await resolveLooseRefFast(repo, `refs/heads/${startPoint}`);
509
+ try {
510
+ await git.branch({
511
+ ...repo,
512
+ ref: name,
513
+ checkout: false,
514
+ object
515
+ });
516
+ } catch (err) {
517
+ if (err.code === "AlreadyExistsError") throw new GitConflictError(`Branch ${name} already exists`);
518
+ throw err;
519
+ }
520
+ }
521
+ /** Delete a branch ref (validated — deleteBranch has no internal ref check). */
522
+ async function deleteBranchByName(repo, name) {
523
+ assertSafeBranchName(name);
524
+ if ((await getCurrentLooseBranch(repo) ?? await git.currentBranch({
525
+ ...repo,
526
+ fullname: false
527
+ }).catch((error) => {
528
+ if (isAbsentError(error)) return null;
529
+ throw error;
530
+ })) === name) throw new GitInvalidRequestError(`Cannot delete ${name}: it is the default branch`);
531
+ await git.deleteBranch({
532
+ ...repo,
533
+ ref: name
534
+ });
535
+ }
536
+ /** Throws (NotFoundError) unless the branch resolves. */
537
+ async function assertBranchExists(repo, name) {
538
+ assertSafeBranchName(name);
539
+ await resolveLooseRefFast(repo, `refs/heads/${name}`);
540
+ }
541
+ /** All tags with the commits they name, sorted by name; [] when there are none. */
542
+ async function listTags(repo) {
543
+ const names = await listLooseRefNames(repo, "refs/tags") ?? [];
544
+ const all = /* @__PURE__ */ new Set([...names, ...await git.listTags(repo).catch((error) => {
545
+ if (isAbsentError(error)) return [];
546
+ throw error;
547
+ })]);
548
+ return (await Promise.all([...all].filter(isSafeBranchName).map(async (name) => {
549
+ try {
550
+ const oid = await resolveLooseRefFast(repo, `refs/tags/${name}`);
551
+ const commit = await peelToCommit(repo, oid);
552
+ return commit === oid ? {
553
+ name,
554
+ commit
555
+ } : {
556
+ name,
557
+ commit,
558
+ tagObject: oid
559
+ };
560
+ } catch (error) {
561
+ if (isAbsentError(error)) return null;
562
+ throw error;
563
+ }
564
+ }))).filter((tag) => tag !== null).sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true }));
565
+ }
566
+ //#endregion
567
+ //#region src/pack-write.ts
568
+ /**
569
+ * Writing packfiles: serialize whole (never deltified) objects into a pack,
570
+ * and store a pack plus its index in a repository. Shared by repack (the
571
+ * loose-object fallback) and packed writes (ops/packed-writes.ts).
572
+ *
573
+ * Edge-compatible: Web Crypto SHA-1, CompressionStream, Uint8Array only.
574
+ */
575
+ const PACK_OBJECT_TYPE_BITS = {
576
+ commit: 16,
577
+ tree: 32,
578
+ blob: 48,
579
+ tag: 64
580
+ };
581
+ function encodePackObjectHeader(type, length) {
582
+ const bytes = [];
583
+ let more = length > 15;
584
+ bytes.push((more ? 128 : 0) | PACK_OBJECT_TYPE_BITS[type] | length & 15);
585
+ let rest = Math.floor(length / 16);
586
+ while (more) {
587
+ more = rest > 127;
588
+ bytes.push((more ? 128 : 0) | rest & 127);
589
+ rest = Math.floor(rest / 128);
590
+ }
591
+ return new Uint8Array(bytes);
592
+ }
593
+ /** Git's object id: sha1("<type> <byte length>\0<content>"). */
594
+ function hashGitObject(type, content) {
595
+ return sha1(concat(encodeUtf8(`${type} ${content.length}\0`), content));
596
+ }
597
+ /** A version-2 pack holding `objects` as full entries, in order. */
598
+ async function encodePack(objects) {
599
+ const header = /* @__PURE__ */ new Uint8Array(12);
600
+ const view = new DataView(header.buffer);
601
+ header.set(encodeUtf8("PACK"), 0);
602
+ view.setUint32(4, 2, false);
603
+ view.setUint32(8, objects.length, false);
604
+ const chunks = [header];
605
+ const BATCH_SIZE = 100;
606
+ for (let i = 0; i < objects.length; i += BATCH_SIZE) {
607
+ 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))));
608
+ chunks.push(...encoded);
609
+ }
610
+ const body = concat(...chunks);
611
+ return concat(body, fromHex(await sha1(body)));
612
+ }
613
+ /**
614
+ * A pack's conventional base name, `pack-<checksum>`: the trailer SHA-1 is a
615
+ * function of the content, so two writers can never pick the same name for
616
+ * different packs, and rewriting the same pack is idempotent.
617
+ */
618
+ function packBaseName(pack) {
619
+ return `pack-${toHex(pack.subarray(pack.length - 20))}`;
620
+ }
621
+ /**
622
+ * Store `pack` in `repo` and index it (isomorphic-git's `indexPack`, which
623
+ * re-hashes every object). The `.idx` is written last: a pack only becomes
624
+ * visible to readers once its index exists. Returns the base name and the
625
+ * oids the index lists.
626
+ */
627
+ async function storePack(repo, pack) {
628
+ const fsp = rawFs(repo);
629
+ const packDir = `${repo.gitdir}/objects/pack`;
630
+ const base = packBaseName(pack);
631
+ await fsp.mkdir(packDir, { recursive: true });
632
+ await fsp.writeFile(`${packDir}/${packFileName(base, "pack")}`, pack);
633
+ const { oids } = await git.indexPack({
634
+ ...repo,
635
+ dir: packDir,
636
+ filepath: packFileName(base, "pack")
637
+ });
638
+ return {
639
+ base,
640
+ oids
641
+ };
642
+ }
643
+ //#endregion
644
+ //#region src/semaphore.ts
645
+ /**
646
+ * Create a FIFO semaphore admitting at most `limit` concurrent `run`
647
+ * executions. Waiters are queued as promises and released in call order — no
648
+ * polling or timers — so a caller blocked on a full semaphore is woken the
649
+ * instant a permit frees, not on the next tick of a poll loop.
650
+ */
651
+ function createSemaphore(limit) {
652
+ if (!Number.isInteger(limit) || limit < 1) throw new Error(`createSemaphore: limit must be an integer >= 1, got ${limit}`);
653
+ let active = 0;
654
+ const waiting = [];
655
+ /**
656
+ * Hand the permit to the next waiter instead of dropping it, so `active`
657
+ * only decreases when nothing is queued and never exceeds `limit`.
658
+ */
659
+ function release() {
660
+ const next = waiting.shift();
661
+ if (next) next();
662
+ else active--;
663
+ }
664
+ function acquire() {
665
+ if (active < limit) {
666
+ active++;
667
+ return Promise.resolve();
668
+ }
669
+ return new Promise((resolve) => {
670
+ waiting.push(resolve);
671
+ });
672
+ }
673
+ return { run(fn) {
674
+ return acquire().then(async () => {
675
+ try {
676
+ return await fn();
677
+ } finally {
678
+ release();
679
+ }
680
+ });
681
+ } };
682
+ }
683
+ //#endregion
684
+ export { upsertTree as A, resolveCommitOid as C, deleteFromTree as D, runStep as E, findTreeEntry as O, resolveCommit as S, resultKeyPrefixes 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, listTreeEntries 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, summarizeCommit as w, peelToCommit as x, getFileContent as y };
685
+
686
+ //# sourceMappingURL=semaphore-CPjn7Iqh.js.map