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.
@@ -0,0 +1,38 @@
1
+ // src/pack-files.ts
2
+ var PACK_FILE_KINDS = [
3
+ "pack",
4
+ "idx",
5
+ "keep",
6
+ "bitmap",
7
+ "rev"
8
+ ];
9
+ var READABLE_PACK_FILE_KINDS = /* @__PURE__ */ new Set([
10
+ "pack",
11
+ "idx"
12
+ ]);
13
+ var KNOWN_KINDS = new Set(PACK_FILE_KINDS);
14
+ function isPackFileKind(value) {
15
+ return KNOWN_KINDS.has(value);
16
+ }
17
+ function parsePackFileName(name) {
18
+ const dot = name.lastIndexOf(".");
19
+ if (dot <= 0) return null;
20
+ const kind = name.slice(dot + 1);
21
+ if (!isPackFileKind(kind)) return null;
22
+ return { base: name.slice(0, dot), kind, name };
23
+ }
24
+ function packFileName(base, kind) {
25
+ return `${base}.${kind}`;
26
+ }
27
+ function parsePackDirectory(entries) {
28
+ return entries.map(parsePackFileName).filter((file) => file !== null);
29
+ }
30
+
31
+ export {
32
+ PACK_FILE_KINDS,
33
+ READABLE_PACK_FILE_KINDS,
34
+ parsePackFileName,
35
+ packFileName,
36
+ parsePackDirectory
37
+ };
38
+ //# sourceMappingURL=chunk-RMAOPWHJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pack-files.ts"],"sourcesContent":["/**\n * Typed view of a git `objects/pack` directory listing.\n *\n * Every pack is a set of sibling files sharing one base name\n * (`pack-<hash>.pack`, `.idx`, and optionally `.keep`/`.bitmap`/`.rev`).\n * Parsing names once into {@link PackFileName} lets callers group, filter and\n * rename by kind instead of re-matching filename strings at each call site.\n *\n * Edge-compatible: no node: imports.\n */\n\n/** Every file kind git writes into `objects/pack`. */\nexport const PACK_FILE_KINDS = [\n\t\"pack\",\n\t\"idx\",\n\t\"keep\",\n\t\"bitmap\",\n\t\"rev\",\n] as const;\nexport type PackFileKind = (typeof PACK_FILE_KINDS)[number];\n\n/** Kinds isomorphic-git actually reads; the rest are git-CLI accelerators. */\nexport const READABLE_PACK_FILE_KINDS: ReadonlySet<PackFileKind> = new Set([\n\t\"pack\",\n\t\"idx\",\n]);\n\nexport interface PackFileName {\n\t/** Shared base of a pack's sibling files, e.g. `pack-1a2b…`. */\n\treadonly base: string;\n\treadonly kind: PackFileKind;\n\t/** The file name as listed, e.g. `pack-1a2b….idx`. */\n\treadonly name: string;\n}\n\nconst KNOWN_KINDS: ReadonlySet<string> = new Set(PACK_FILE_KINDS);\n\nfunction isPackFileKind(value: string): value is PackFileKind {\n\treturn KNOWN_KINDS.has(value);\n}\n\n/** Parse a pack-directory entry; `null` for anything git didn't put there. */\nexport function parsePackFileName(name: string): PackFileName | null {\n\tconst dot = name.lastIndexOf(\".\");\n\tif (dot <= 0) return null;\n\tconst kind = name.slice(dot + 1);\n\tif (!isPackFileKind(kind)) return null;\n\treturn { base: name.slice(0, dot), kind, name };\n}\n\n/** File name of the `kind` sibling of the pack with this base name. */\nexport function packFileName(base: string, kind: PackFileKind): string {\n\treturn `${base}.${kind}`;\n}\n\n/** Parse a directory listing, dropping entries that aren't pack files. */\nexport function parsePackDirectory(entries: readonly string[]): PackFileName[] {\n\treturn entries\n\t\t.map(parsePackFileName)\n\t\t.filter((file): file is PackFileName => file !== null);\n}\n"],"mappings":";AAYO,IAAM,kBAAkB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAIO,IAAM,2BAAsD,oBAAI,IAAI;AAAA,EAC1E;AAAA,EACA;AACD,CAAC;AAUD,IAAM,cAAmC,IAAI,IAAI,eAAe;AAEhE,SAAS,eAAe,OAAsC;AAC7D,SAAO,YAAY,IAAI,KAAK;AAC7B;AAGO,SAAS,kBAAkB,MAAmC;AACpE,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,MAAI,OAAO,EAAG,QAAO;AACrB,QAAM,OAAO,KAAK,MAAM,MAAM,CAAC;AAC/B,MAAI,CAAC,eAAe,IAAI,EAAG,QAAO;AAClC,SAAO,EAAE,MAAM,KAAK,MAAM,GAAG,GAAG,GAAG,MAAM,KAAK;AAC/C;AAGO,SAAS,aAAa,MAAc,MAA4B;AACtE,SAAO,GAAG,IAAI,IAAI,IAAI;AACvB;AAGO,SAAS,mBAAmB,SAA4C;AAC9E,SAAO,QACL,IAAI,iBAAiB,EACrB,OAAO,CAAC,SAA+B,SAAS,IAAI;AACvD;","names":[]}
@@ -22,8 +22,11 @@ function decodeFileContent(content) {
22
22
  return typeof content === "string" ? content : new TextDecoder().decode(content);
23
23
  }
24
24
  async function listLooseBranches(repo) {
25
+ return listLooseRefNames(repo, "refs/heads");
26
+ }
27
+ async function listLooseRefNames(repo, dir) {
25
28
  if (!hasRecursiveFileLister(repo.fs)) return null;
26
- const names = await repo.fs.listFilesRecursively(`${repo.gitdir}/refs/heads`);
29
+ const names = await repo.fs.listFilesRecursively(`${repo.gitdir}/${dir}`);
27
30
  return names.filter(isSafeBranchName);
28
31
  }
29
32
  async function readPackedBranches(repo) {
@@ -116,10 +119,11 @@ async function assertBranchExists(repo, name) {
116
119
 
117
120
  export {
118
121
  assertSafeBranchName,
122
+ listLooseRefNames,
119
123
  resolveLooseRefFast,
120
124
  listBranches,
121
125
  createBranchFrom,
122
126
  deleteBranchByName,
123
127
  assertBranchExists
124
128
  };
125
- //# sourceMappingURL=chunk-3BDDEGAP.js.map
129
+ //# sourceMappingURL=chunk-Z4656PGE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ops/branch.ts"],"sourcesContent":["import git from \"isomorphic-git\";\nimport { GitInvalidRequestError } from \"../git-errors.js\";\nimport { isSafeBranchName } from \"../refs.js\";\nimport type { Repo } from \"./types.js\";\n\nexport interface Branch {\n\tname: string;\n\tcommit: string;\n\tisDefault: boolean;\n}\n\n/**\n * Defense in depth for every branch-name argument below: `git.deleteBranch`\n * and raw resolveRef/writeRef reads don't validate ref names internally the\n * way `git.branch` does (see refs.ts) — guard at the point the primitives are\n * actually called, not just at an API boundary far above.\n */\nexport function assertSafeBranchName(name: string): void {\n\tif (!isSafeBranchName(name)) {\n\t\tthrow new GitInvalidRequestError(`Invalid branch name: ${name}`);\n\t}\n}\n\nconst FULL_SHA_RE = /^[0-9a-f]{40}$/i;\n\ntype RecursiveFileLister = {\n\tlistFilesRecursively(dirpath: string): Promise<string[]>;\n};\n\nfunction hasRecursiveFileLister(\n\tfs: Repo[\"fs\"],\n): fs is Repo[\"fs\"] & RecursiveFileLister {\n\treturn (\n\t\ttypeof fs === \"object\" &&\n\t\tfs !== null &&\n\t\t\"listFilesRecursively\" in fs &&\n\t\ttypeof (fs as Partial<RecursiveFileLister>).listFilesRecursively ===\n\t\t\t\"function\"\n\t);\n}\n\ntype PromisesFs = {\n\treadFile(path: string, encoding: \"utf8\"): Promise<string | Uint8Array>;\n};\n\n// Repo.fs is typed as isomorphic-git's own fs union (promise- or\n// callback-based) since that's what git.readTree's signature allows, but\n// every fs this package actually constructs (git-fs.ts) is promise-based —\n// this just keeps the callback-style branch honest instead of crashing on\n// it. Shared by every loose-ref fast path below instead of each repeating\n// the same cast + presence check.\nfunction getPromisesFs(fs: Repo[\"fs\"]): PromisesFs | null {\n\tconst promises = (fs as { promises?: PromisesFs }).promises;\n\treturn promises ?? null;\n}\n\nfunction decodeFileContent(content: string | Uint8Array): string {\n\treturn typeof content === \"string\"\n\t\t? content\n\t\t: new TextDecoder().decode(content);\n}\n\n/**\n * Lists loose branch names without isomorphic-git's recursive readdir/stat\n * traversal. Object stores already return only leaf keys in a recursive LIST,\n * so this is one request regardless of branch-name nesting. Ordinary\n * filesystems retain isomorphic-git's implementation and packed refs remain\n * supported there.\n */\nasync function listLooseBranches(repo: Repo): Promise<string[] | null> {\n\treturn listLooseRefNames(repo, \"refs/heads\");\n}\n\n/**\n * Loose ref names under `dir` (e.g. `refs/tags`) in one recursive listing,\n * or `null` when the fs can't list recursively (use isomorphic-git's walk).\n */\nexport async function listLooseRefNames(\n\trepo: Repo,\n\tdir: \"refs/heads\" | \"refs/tags\",\n): Promise<string[] | null> {\n\tif (!hasRecursiveFileLister(repo.fs)) return null;\n\tconst names = await repo.fs.listFilesRecursively(`${repo.gitdir}/${dir}`);\n\treturn names.filter(isSafeBranchName);\n}\n\n/** Read packed branch refs so the object-store fast path preserves Git semantics. */\nasync function readPackedBranches(repo: Repo): Promise<Map<string, string>> {\n\tif (!hasRecursiveFileLister(repo.fs)) return new Map();\n\tconst promisesFs = getPromisesFs(repo.fs);\n\tif (!promisesFs) return new Map();\n\ttry {\n\t\tconst content = await promisesFs.readFile(\n\t\t\t`${repo.gitdir}/packed-refs`,\n\t\t\t\"utf8\",\n\t\t);\n\t\tconst refs = new Map<string, string>();\n\t\tfor (const line of decodeFileContent(content).split(\"\\n\")) {\n\t\t\tconst match = /^([0-9a-f]{40}) refs\\/heads\\/(.+)$/i.exec(line);\n\t\t\tif (match?.[1] && match[2] && isSafeBranchName(match[2])) {\n\t\t\t\trefs.set(match[2], match[1]);\n\t\t\t}\n\t\t}\n\t\treturn refs;\n\t} catch {\n\t\treturn new Map();\n\t}\n}\n\n/** Read the symbolic HEAD directly when the object-store fs is available. */\nasync function getCurrentLooseBranch(repo: Repo): Promise<string | null> {\n\tif (!hasRecursiveFileLister(repo.fs)) return null;\n\tconst promisesFs = getPromisesFs(repo.fs);\n\tif (!promisesFs) return null;\n\ttry {\n\t\tconst content = await promisesFs.readFile(`${repo.gitdir}/HEAD`, \"utf8\");\n\t\tconst head = decodeFileContent(content).trim();\n\t\tconst match = /^ref: refs\\/heads\\/(.+)$/.exec(head);\n\t\treturn match?.[1] && isSafeBranchName(match[1]) ? match[1] : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Resolve a fully-qualified ref's oid with one read instead of isomorphic-\n * git's own `resolveRef`, which does a stat *then* a read for a loose ref\n * (two round trips against object storage) — wasteful specifically here,\n * where the caller already knows the ref exists (it came from a directory\n * listing moments earlier). Reads the loose file directly and falls back to\n * `git.resolveRef` only when that doesn't pan out (packed-refs, or anything\n * else the plain-loose-file assumption doesn't cover), so correctness for\n * those cases is unchanged — this only removes the redundant round trip on\n * the common path.\n */\nexport async function resolveLooseRefFast(\n\trepo: Repo,\n\tref: string,\n): Promise<string> {\n\tconst promisesFs = getPromisesFs(repo.fs);\n\tif (promisesFs) {\n\t\ttry {\n\t\t\tconst content = await promisesFs.readFile(\n\t\t\t\t`${repo.gitdir}/${ref}`,\n\t\t\t\t\"utf8\",\n\t\t\t);\n\t\t\tconst oid = decodeFileContent(content).trim();\n\t\t\tif (FULL_SHA_RE.test(oid)) return oid;\n\t\t} catch {\n\t\t\t// Not a loose file (packed-refs, or genuinely absent) — fall through.\n\t\t}\n\t}\n\treturn git.resolveRef({ ...repo, ref });\n}\n\n/** All branches with their tip commits; [] for an empty repository. */\nexport async function listBranches(repo: Repo): Promise<Branch[]> {\n\ttry {\n\t\tconst [looseBranches, looseCurrentBranch, packedBranches] =\n\t\t\tawait Promise.all([\n\t\t\t\tlistLooseBranches(repo),\n\t\t\t\tgetCurrentLooseBranch(repo),\n\t\t\t\treadPackedBranches(repo),\n\t\t\t]);\n\t\tconst branches =\n\t\t\tlooseBranches === null\n\t\t\t\t? await git.listBranches(repo)\n\t\t\t\t: [...new Set([...looseBranches, ...packedBranches.keys()])].sort();\n\t\tconst currentBranch =\n\t\t\tlooseBranches === null\n\t\t\t\t? await git\n\t\t\t\t\t\t.currentBranch({ ...repo, fullname: false })\n\t\t\t\t\t\t.catch(() => null)\n\t\t\t\t: looseCurrentBranch;\n\n\t\treturn Promise.all(\n\t\t\tbranches.map(async (branch) => {\n\t\t\t\tconst packedCommit = packedBranches.get(branch);\n\t\t\t\treturn {\n\t\t\t\t\tname: branch,\n\t\t\t\t\tcommit:\n\t\t\t\t\t\tpackedCommit && !looseBranches?.includes(branch)\n\t\t\t\t\t\t\t? packedCommit\n\t\t\t\t\t\t\t: await resolveLooseRefFast(repo, `refs/heads/${branch}`),\n\t\t\t\t\tisDefault: branch === currentBranch,\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\t} catch (err: unknown) {\n\t\tif ((err as { code?: string }).code === \"NotFoundError\") return [];\n\t\tthrow err;\n\t}\n}\n\n/** Create `name` pointing at the tip of `startPoint` (no checkout). */\nexport async function createBranchFrom(\n\trepo: Repo,\n\tname: string,\n\tstartPoint = \"main\",\n): Promise<void> {\n\tassertSafeBranchName(name);\n\tassertSafeBranchName(startPoint);\n\tconst object = await resolveLooseRefFast(repo, `refs/heads/${startPoint}`);\n\tawait git.branch({ ...repo, ref: name, checkout: false, object });\n}\n\n/** Delete a branch ref (validated — deleteBranch has no internal ref check). */\nexport async function deleteBranchByName(\n\trepo: Repo,\n\tname: string,\n): Promise<void> {\n\tassertSafeBranchName(name);\n\tawait git.deleteBranch({ ...repo, ref: name });\n}\n\n/** Throws (NotFoundError) unless the branch resolves. */\nexport async function assertBranchExists(\n\trepo: Repo,\n\tname: string,\n): Promise<void> {\n\tassertSafeBranchName(name);\n\tawait resolveLooseRefFast(repo, `refs/heads/${name}`);\n}\n"],"mappings":"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","names":[]}
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 [];
@@ -717,7 +980,7 @@ async function handleUploadPack(repo, body, options, hooks) {
717
980
  "readdir objects/pack",
718
981
  () => rawFs(repo).readdir(packDirPath).catch(() => [])
719
982
  );
720
- const packNames = entries.filter((f) => f.endsWith(".pack"));
983
+ const packNames = parsePackDirectory(entries).filter((file) => file.kind === "pack").map((file) => file.name);
721
984
  const packName = packNames[0];
722
985
  if (packNames.length === 1 && packName !== void 0) {
723
986
  const packData = await runStep(
@@ -739,27 +1002,34 @@ async function handleUploadPack(repo, body, options, hooks) {
739
1002
  if (options?.beforeWalk) {
740
1003
  await runStep(hooks, "beforeWalk", options.beforeWalk);
741
1004
  }
742
- const { oids: wantOids } = await runStep(
743
- hooks,
744
- "collectReachableOids(wants)",
745
- () => collectReachableOids(repo, wants, hooks)
746
- );
747
- let oids = wantOids;
1005
+ let haveSet;
748
1006
  if (haves.length > 0) {
749
1007
  const { oids: haveOids } = await runStep(
750
1008
  hooks,
751
1009
  "collectReachableOids(haves)",
752
- () => collectReachableOids(repo, haves, hooks)
1010
+ () => collectReachableOids(repo, haves, hooks, void 0, {
1011
+ verifyBlobs: false
1012
+ })
753
1013
  );
754
- const haveSet = new Set(haveOids);
755
- oids = oids.filter((oid) => !haveSet.has(oid));
1014
+ haveSet = new Set(haveOids);
756
1015
  }
757
- 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(
758
1025
  hooks,
759
- `packObjects (${oids.length} oids)`,
760
- () => import_isomorphic_git6.default.packObjects({ ...repo, oids })
1026
+ `reuse pack entries (${oids.length} oids)`,
1027
+ () => buildPackFromExistingPacks(repo, oids)
761
1028
  );
762
- 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
+ });
763
1033
  return {
764
1034
  status: 200,
765
1035
  headers: {