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.
- package/README.md +5 -3
- package/dist/fetch-s3.js +137 -160
- package/dist/fetch-s3.js.map +1 -1
- package/dist/http.js +905 -849
- package/dist/http.js.map +1 -1
- package/dist/index.js +578 -658
- package/dist/index.js.map +1 -1
- package/dist/memory-M2cCuVF-.js +61 -0
- package/dist/memory-M2cCuVF-.js.map +1 -0
- package/dist/node.js +75 -72
- package/dist/node.js.map +1 -1
- package/dist/object-size-BJAsnZzw.js +191 -0
- package/dist/object-size-BJAsnZzw.js.map +1 -0
- package/dist/ops.js +662 -692
- package/dist/ops.js.map +1 -1
- package/dist/refs-Cb_NfKWI.js +550 -0
- package/dist/refs-Cb_NfKWI.js.map +1 -0
- package/dist/s3.js +111 -116
- package/dist/s3.js.map +1 -1
- package/dist/semaphore-BviNoQuJ.js +646 -0
- package/dist/semaphore-BviNoQuJ.js.map +1 -0
- package/dist/types/git-errors.d.ts +6 -0
- package/dist/types/http/repack.d.ts +7 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +8 -13
- package/dist/chunk-HLYCSNJ2.js +0 -53
- package/dist/chunk-HLYCSNJ2.js.map +0 -1
- package/dist/chunk-IAKZRWBZ.js +0 -598
- package/dist/chunk-IAKZRWBZ.js.map +0 -1
- package/dist/chunk-OKBQISUC.js +0 -442
- package/dist/chunk-OKBQISUC.js.map +0 -1
- package/dist/chunk-T5M5EYCZ.js +0 -143
- package/dist/chunk-T5M5EYCZ.js.map +0 -1
|
@@ -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
|