@toon-protocol/git 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/chunk-4WFGAICZ.js +707 -0
- package/dist/chunk-4WFGAICZ.js.map +1 -0
- package/dist/chunk-KXXHAUXL.js +109 -0
- package/dist/chunk-KXXHAUXL.js.map +1 -0
- package/dist/chunk-LJA7PPZI.js +144 -0
- package/dist/chunk-LJA7PPZI.js.map +1 -0
- package/dist/chunk-M7O4SEVW.js +56 -0
- package/dist/chunk-M7O4SEVW.js.map +1 -0
- package/dist/chunk-R3JVS6SX.js +345 -0
- package/dist/chunk-R3JVS6SX.js.map +1 -0
- package/dist/chunk-SBMFWVCP.js +265 -0
- package/dist/chunk-SBMFWVCP.js.map +1 -0
- package/dist/cli/rig.d.ts +1 -0
- package/dist/cli/rig.js +1430 -0
- package/dist/cli/rig.js.map +1 -0
- package/dist/index.d.ts +742 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/publisher-VEIEQHl6.d.ts +254 -0
- package/dist/standalone/index.d.ts +272 -0
- package/dist/standalone/index.js +30 -0
- package/dist/standalone/index.js.map +1 -0
- package/dist/standalone-mode-UFMHGUOM.js +132 -0
- package/dist/standalone-mode-UFMHGUOM.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildRepoAnnouncement,
|
|
3
|
+
buildRepoRefs
|
|
4
|
+
} from "./chunk-KXXHAUXL.js";
|
|
5
|
+
import {
|
|
6
|
+
MAX_OBJECT_SIZE
|
|
7
|
+
} from "./chunk-M7O4SEVW.js";
|
|
8
|
+
|
|
9
|
+
// src/repo-reader.ts
|
|
10
|
+
import { execFile, spawn } from "child_process";
|
|
11
|
+
import { promisify } from "util";
|
|
12
|
+
var execFileAsync = promisify(execFile);
|
|
13
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
14
|
+
var GitError = class extends Error {
|
|
15
|
+
constructor(message, exitCode, stderr) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.exitCode = exitCode;
|
|
18
|
+
this.stderr = stderr;
|
|
19
|
+
this.name = "GitError";
|
|
20
|
+
}
|
|
21
|
+
exitCode;
|
|
22
|
+
stderr;
|
|
23
|
+
};
|
|
24
|
+
var FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
25
|
+
var REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;
|
|
26
|
+
function isValidRevision(rev) {
|
|
27
|
+
if (rev.length === 0 || rev.length > 1024) return false;
|
|
28
|
+
if (!REV_TOKEN_RE.test(rev)) return false;
|
|
29
|
+
if (rev.includes("..")) return false;
|
|
30
|
+
if (rev.endsWith(".lock") || rev.endsWith("/") || rev.endsWith(".")) return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function assertRevision(rev, what) {
|
|
34
|
+
if (!isValidRevision(rev)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${what} is not a valid git revision (got ${JSON.stringify(rev)}); expected a SHA or simple refname \u2014 options/ranges are rejected`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function assertFullSha(sha, what) {
|
|
41
|
+
if (!FULL_SHA_RE.test(sha)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${what} is not a full 40-hex SHA-1 (got ${JSON.stringify(sha)})`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function assertRange(range, what) {
|
|
48
|
+
const parts = range.split(/\.{2,3}/);
|
|
49
|
+
const separators = range.match(/\.{2,3}/g) ?? [];
|
|
50
|
+
const ok = parts.length <= 2 && separators.length === parts.length - 1 && parts.every((p) => isValidRevision(p));
|
|
51
|
+
if (!ok) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${what} is not a valid revision range (got ${JSON.stringify(range)}); expected <rev>, <rev>..<rev>, or <rev>...<rev>`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
var OBJECT_TYPES = /* @__PURE__ */ new Set(["blob", "tree", "commit", "tag"]);
|
|
58
|
+
var BatchParser = class {
|
|
59
|
+
buf = Buffer.alloc(0);
|
|
60
|
+
pending = null;
|
|
61
|
+
objects = [];
|
|
62
|
+
missing = [];
|
|
63
|
+
push(chunk) {
|
|
64
|
+
this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
|
|
65
|
+
this.drain();
|
|
66
|
+
}
|
|
67
|
+
/** True when no partially-parsed record remains. */
|
|
68
|
+
isComplete() {
|
|
69
|
+
return this.pending === null && this.buf.length === 0;
|
|
70
|
+
}
|
|
71
|
+
drain() {
|
|
72
|
+
for (; ; ) {
|
|
73
|
+
if (this.pending) {
|
|
74
|
+
const needed = this.pending.size + 1;
|
|
75
|
+
if (this.buf.length < needed) return;
|
|
76
|
+
const body = Buffer.from(this.buf.subarray(0, this.pending.size));
|
|
77
|
+
this.objects.push({ sha: this.pending.sha, type: this.pending.type, body });
|
|
78
|
+
this.buf = this.buf.subarray(needed);
|
|
79
|
+
this.pending = null;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const nl = this.buf.indexOf(10);
|
|
83
|
+
if (nl === -1) return;
|
|
84
|
+
const header = this.buf.subarray(0, nl).toString("utf-8");
|
|
85
|
+
this.buf = this.buf.subarray(nl + 1);
|
|
86
|
+
const [name, second, third] = header.split(" ");
|
|
87
|
+
if (name && second === "missing" && third === void 0) {
|
|
88
|
+
this.missing.push(name);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (name && second && third !== void 0 && OBJECT_TYPES.has(second)) {
|
|
92
|
+
const size = Number.parseInt(third, 10);
|
|
93
|
+
if (Number.isSafeInteger(size) && size >= 0) {
|
|
94
|
+
this.pending = { sha: name, type: second, size };
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw new GitError(
|
|
99
|
+
`unexpected cat-file --batch header: ${JSON.stringify(header)}`,
|
|
100
|
+
void 0,
|
|
101
|
+
""
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var GitRepoReader = class {
|
|
107
|
+
constructor(repoPath) {
|
|
108
|
+
this.repoPath = repoPath;
|
|
109
|
+
}
|
|
110
|
+
repoPath;
|
|
111
|
+
/** Run git with argument-array safety; resolves stdout as UTF-8. */
|
|
112
|
+
async git(args, opts = {}) {
|
|
113
|
+
try {
|
|
114
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
115
|
+
cwd: this.repoPath,
|
|
116
|
+
maxBuffer: MAX_BUFFER,
|
|
117
|
+
encoding: "utf-8"
|
|
118
|
+
});
|
|
119
|
+
return { stdout, exitCode: 0 };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const e = err;
|
|
122
|
+
const exitCode = typeof e.code === "number" ? e.code : void 0;
|
|
123
|
+
if (exitCode !== void 0 && opts.allowExitCodes?.includes(exitCode)) {
|
|
124
|
+
return { stdout: e.stdout ?? "", exitCode };
|
|
125
|
+
}
|
|
126
|
+
throw new GitError(
|
|
127
|
+
`git ${args[0]} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`,
|
|
128
|
+
exitCode,
|
|
129
|
+
(e.stderr ?? "").trim()
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* List all branches and tags plus the symbolic HEAD.
|
|
135
|
+
*
|
|
136
|
+
* Annotated tags report the tag object's SHA/type with the peeled target
|
|
137
|
+
* in `peeledSha`. A detached HEAD is tolerated (`head` is `undefined`).
|
|
138
|
+
*/
|
|
139
|
+
async listRefs() {
|
|
140
|
+
const format = "%(refname)%00%(objectname)%00%(objecttype)%00%(*objectname)";
|
|
141
|
+
const [refsRes, headRes] = await Promise.all([
|
|
142
|
+
this.git(["for-each-ref", `--format=${format}`, "refs/heads", "refs/tags"]),
|
|
143
|
+
// Exit 1 = detached HEAD (or unborn branch pointer oddities) — tolerated.
|
|
144
|
+
this.git(["symbolic-ref", "--quiet", "HEAD"], { allowExitCodes: [1] })
|
|
145
|
+
]);
|
|
146
|
+
const refs = [];
|
|
147
|
+
for (const line of refsRes.stdout.split("\n")) {
|
|
148
|
+
if (!line) continue;
|
|
149
|
+
const [refname, sha, objecttype, peeled] = line.split("\0");
|
|
150
|
+
if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {
|
|
151
|
+
throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, void 0, "");
|
|
152
|
+
}
|
|
153
|
+
refs.push({
|
|
154
|
+
refname,
|
|
155
|
+
sha,
|
|
156
|
+
type: objecttype,
|
|
157
|
+
...peeled ? { peeledSha: peeled } : {}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const head = headRes.exitCode === 0 ? headRes.stdout.trim() || void 0 : void 0;
|
|
161
|
+
return { head, refs };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* SHAs of every object reachable from `want` but not from `have`
|
|
165
|
+
* (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.
|
|
166
|
+
*
|
|
167
|
+
* Haves that don't exist locally (e.g. remote tips we never fetched) are
|
|
168
|
+
* filtered out first via one `cat-file --batch-check` pass — rev-list
|
|
169
|
+
* would otherwise die on them.
|
|
170
|
+
*/
|
|
171
|
+
async objectsBetween(want, have) {
|
|
172
|
+
const objects = await this.objectsBetweenWithPaths(want, have);
|
|
173
|
+
return objects.map((o) => o.sha);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Like {@link objectsBetween} but keeps the path each object was reached
|
|
177
|
+
* by (`rev-list --objects` emits `<sha> <path>` for blobs and non-root
|
|
178
|
+
* trees) — used by push planning to report actionable oversize errors.
|
|
179
|
+
*/
|
|
180
|
+
async objectsBetweenWithPaths(want, have) {
|
|
181
|
+
for (const w of want) assertRevision(w, "want");
|
|
182
|
+
for (const h of have) assertRevision(h, "have");
|
|
183
|
+
if (want.length === 0) return [];
|
|
184
|
+
const knownHaves = await this.filterExisting(have);
|
|
185
|
+
const args = ["rev-list", "--objects", ...want];
|
|
186
|
+
if (knownHaves.length > 0) args.push("--not", ...knownHaves);
|
|
187
|
+
args.push("--");
|
|
188
|
+
const { stdout } = await this.git(args);
|
|
189
|
+
const objects = [];
|
|
190
|
+
for (const line of stdout.split("\n")) {
|
|
191
|
+
if (!line) continue;
|
|
192
|
+
const spaceIdx = line.indexOf(" ");
|
|
193
|
+
if (spaceIdx === -1) {
|
|
194
|
+
objects.push({ sha: line });
|
|
195
|
+
} else {
|
|
196
|
+
const path = line.slice(spaceIdx + 1);
|
|
197
|
+
objects.push({
|
|
198
|
+
sha: line.slice(0, spaceIdx),
|
|
199
|
+
...path ? { path } : {}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return objects;
|
|
204
|
+
}
|
|
205
|
+
/** Run git feeding `input` on stdin; resolves collected stdout bytes. */
|
|
206
|
+
runWithStdin(args, input) {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const child = spawn("git", args, {
|
|
209
|
+
cwd: this.repoPath,
|
|
210
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
211
|
+
});
|
|
212
|
+
const out = [];
|
|
213
|
+
let stderr = "";
|
|
214
|
+
child.stdout.on("data", (chunk) => out.push(chunk));
|
|
215
|
+
child.stderr.on("data", (chunk) => {
|
|
216
|
+
stderr += chunk.toString("utf-8");
|
|
217
|
+
});
|
|
218
|
+
child.on("error", (err) => {
|
|
219
|
+
reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, void 0, ""));
|
|
220
|
+
});
|
|
221
|
+
child.on("close", (code) => {
|
|
222
|
+
if (code !== 0) {
|
|
223
|
+
return reject(
|
|
224
|
+
new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
resolve(Buffer.concat(out));
|
|
228
|
+
});
|
|
229
|
+
child.stdin.on("error", () => {
|
|
230
|
+
});
|
|
231
|
+
child.stdin.write(input);
|
|
232
|
+
child.stdin.end();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** Of the given revisions, keep only those resolvable locally. */
|
|
236
|
+
async filterExisting(revs) {
|
|
237
|
+
if (revs.length === 0) return [];
|
|
238
|
+
const { missing } = await this.batchCheck(revs);
|
|
239
|
+
const missingSet = new Set(missing);
|
|
240
|
+
return revs.filter((r) => !missingSet.has(r));
|
|
241
|
+
}
|
|
242
|
+
/** One `cat-file --batch-check` pass; returns names reported missing. */
|
|
243
|
+
async batchCheck(names) {
|
|
244
|
+
const stdout = await this.runWithStdin(
|
|
245
|
+
["cat-file", "--batch-check"],
|
|
246
|
+
names.join("\n") + "\n"
|
|
247
|
+
);
|
|
248
|
+
const missing = [];
|
|
249
|
+
for (const line of stdout.toString("utf-8").split("\n")) {
|
|
250
|
+
if (line.endsWith(" missing")) missing.push(line.slice(0, -" missing".length));
|
|
251
|
+
}
|
|
252
|
+
return { missing };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Object metadata (type + body size) for a batch of SHAs via one
|
|
256
|
+
* `cat-file --batch-check` pass — no bodies are read. Missing objects are
|
|
257
|
+
* reported, not thrown.
|
|
258
|
+
*/
|
|
259
|
+
async statObjects(shas) {
|
|
260
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
261
|
+
if (shas.length === 0) return { objects: [], missing: [] };
|
|
262
|
+
const stdout = await this.runWithStdin(
|
|
263
|
+
["cat-file", "--batch-check"],
|
|
264
|
+
shas.join("\n") + "\n"
|
|
265
|
+
);
|
|
266
|
+
const objects = [];
|
|
267
|
+
const missing = [];
|
|
268
|
+
for (const line of stdout.toString("utf-8").split("\n")) {
|
|
269
|
+
if (!line) continue;
|
|
270
|
+
if (line.endsWith(" missing")) {
|
|
271
|
+
missing.push(line.slice(0, -" missing".length));
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const [sha, type, sizeStr] = line.split(" ");
|
|
275
|
+
const size = Number.parseInt(sizeStr ?? "", 10);
|
|
276
|
+
if (!sha || !type || !OBJECT_TYPES.has(type) || !Number.isSafeInteger(size) || size < 0) {
|
|
277
|
+
throw new GitError(
|
|
278
|
+
`unexpected cat-file --batch-check line: ${JSON.stringify(line)}`,
|
|
279
|
+
void 0,
|
|
280
|
+
""
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
objects.push({ sha, type, size });
|
|
284
|
+
}
|
|
285
|
+
return { objects, missing };
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Read raw object bodies via a single streaming `git cat-file --batch`
|
|
289
|
+
* child process. Bodies may be binary and are parsed size-driven across
|
|
290
|
+
* chunk boundaries. Missing objects are reported, not thrown.
|
|
291
|
+
*/
|
|
292
|
+
async readObjects(shas) {
|
|
293
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
294
|
+
if (shas.length === 0) return { objects: [], missing: [] };
|
|
295
|
+
const parser = new BatchParser();
|
|
296
|
+
await new Promise((resolve, reject) => {
|
|
297
|
+
const child = spawn("git", ["cat-file", "--batch"], {
|
|
298
|
+
cwd: this.repoPath,
|
|
299
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
300
|
+
});
|
|
301
|
+
let stderr = "";
|
|
302
|
+
let parseError = null;
|
|
303
|
+
child.stderr.on("data", (chunk) => {
|
|
304
|
+
stderr += chunk.toString("utf-8");
|
|
305
|
+
});
|
|
306
|
+
child.stdout.on("data", (chunk) => {
|
|
307
|
+
if (parseError) return;
|
|
308
|
+
try {
|
|
309
|
+
parser.push(chunk);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
parseError = err;
|
|
312
|
+
child.kill();
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
child.on("error", (err) => {
|
|
316
|
+
reject(new GitError(`failed to spawn git cat-file: ${err.message}`, void 0, ""));
|
|
317
|
+
});
|
|
318
|
+
child.on("close", (code) => {
|
|
319
|
+
if (parseError) return reject(parseError);
|
|
320
|
+
if (code !== 0) {
|
|
321
|
+
return reject(
|
|
322
|
+
new GitError(`git cat-file --batch failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (!parser.isComplete()) {
|
|
326
|
+
return reject(
|
|
327
|
+
new GitError("git cat-file --batch output ended mid-record", code ?? void 0, stderr.trim())
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
resolve();
|
|
331
|
+
});
|
|
332
|
+
child.stdin.on("error", () => {
|
|
333
|
+
});
|
|
334
|
+
child.stdin.write(shas.join("\n") + "\n");
|
|
335
|
+
child.stdin.end();
|
|
336
|
+
});
|
|
337
|
+
return { objects: parser.objects, missing: parser.missing };
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* `git merge-base --is-ancestor <a> <b>` — true when `a` is an ancestor
|
|
341
|
+
* of `b` (fast-forward check / force detection). Exit codes other than
|
|
342
|
+
* 0/1 (e.g. unknown revisions) throw.
|
|
343
|
+
*/
|
|
344
|
+
async isAncestor(a, b) {
|
|
345
|
+
assertRevision(a, "ancestor candidate");
|
|
346
|
+
assertRevision(b, "descendant candidate");
|
|
347
|
+
const { exitCode } = await this.git(
|
|
348
|
+
["merge-base", "--is-ancestor", a, b],
|
|
349
|
+
{ allowExitCodes: [1] }
|
|
350
|
+
);
|
|
351
|
+
return exitCode === 0;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* `git format-patch --stdout <range>` — the full mbox-formatted patch
|
|
355
|
+
* series text (empty string when the range selects no commits).
|
|
356
|
+
*/
|
|
357
|
+
async formatPatch(range) {
|
|
358
|
+
assertRange(range, "range");
|
|
359
|
+
const { stdout } = await this.git(["format-patch", "--stdout", range, "--"]);
|
|
360
|
+
return stdout;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Parent SHAs for a batch of commit SHAs via one
|
|
364
|
+
* `git rev-list --no-walk=unsorted --parents` pass. Root commits map to an
|
|
365
|
+
* empty array. Used to derive the kind:1617 `commit`/`parent-commit` tag
|
|
366
|
+
* pairs for exactly the commits a format-patch series carries.
|
|
367
|
+
*/
|
|
368
|
+
async commitParents(shas) {
|
|
369
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
370
|
+
if (shas.length === 0) return /* @__PURE__ */ new Map();
|
|
371
|
+
const { stdout } = await this.git([
|
|
372
|
+
"rev-list",
|
|
373
|
+
"--no-walk=unsorted",
|
|
374
|
+
"--parents",
|
|
375
|
+
...shas,
|
|
376
|
+
"--"
|
|
377
|
+
]);
|
|
378
|
+
const parents = /* @__PURE__ */ new Map();
|
|
379
|
+
for (const line of stdout.split("\n")) {
|
|
380
|
+
if (!line) continue;
|
|
381
|
+
const [sha, ...rest] = line.split(" ");
|
|
382
|
+
if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {
|
|
383
|
+
throw new GitError(
|
|
384
|
+
`unexpected rev-list --parents line: ${JSON.stringify(line)}`,
|
|
385
|
+
void 0,
|
|
386
|
+
""
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
parents.set(sha, rest);
|
|
390
|
+
}
|
|
391
|
+
return parents;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Resolve a ref/revision to a full SHA via `git rev-parse --verify`.
|
|
395
|
+
* Throws {@link GitError} when the name doesn't resolve.
|
|
396
|
+
*/
|
|
397
|
+
async resolveRef(name) {
|
|
398
|
+
assertRevision(name, "ref name");
|
|
399
|
+
const { stdout } = await this.git(["rev-parse", "--verify", "--quiet", name]);
|
|
400
|
+
const sha = stdout.trim();
|
|
401
|
+
if (!FULL_SHA_RE.test(sha)) {
|
|
402
|
+
throw new GitError(
|
|
403
|
+
`rev-parse --verify returned unexpected output for ${JSON.stringify(name)}: ${JSON.stringify(sha)}`,
|
|
404
|
+
void 0,
|
|
405
|
+
""
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return sha;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// src/routes.ts
|
|
413
|
+
function serializeFeeEstimate(plan) {
|
|
414
|
+
return {
|
|
415
|
+
objectCount: plan.estimate.objectCount,
|
|
416
|
+
totalObjectBytes: plan.estimate.totalObjectBytes,
|
|
417
|
+
uploadFee: plan.estimate.uploadFee.toString(),
|
|
418
|
+
eventCount: plan.estimate.eventCount,
|
|
419
|
+
eventFees: plan.estimate.eventFees.toString(),
|
|
420
|
+
totalFee: plan.estimate.totalFee.toString()
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function serializePushPlan(plan) {
|
|
424
|
+
return {
|
|
425
|
+
repoId: plan.repoId,
|
|
426
|
+
refUpdates: plan.refUpdates,
|
|
427
|
+
newRefs: plan.newRefs,
|
|
428
|
+
headSymref: plan.headSymref,
|
|
429
|
+
objects: plan.objects,
|
|
430
|
+
knownShaToTxId: Object.fromEntries(plan.knownShaToTxId),
|
|
431
|
+
announceNeeded: plan.announceNeeded,
|
|
432
|
+
announcement: plan.announcement,
|
|
433
|
+
estimate: serializeFeeEstimate(plan)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function serializeEventReceipt(kind, receipt) {
|
|
437
|
+
return {
|
|
438
|
+
eventId: receipt.eventId,
|
|
439
|
+
feePaid: receipt.feePaid.toString(),
|
|
440
|
+
kind
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function serializePushResult(plan, result) {
|
|
444
|
+
return {
|
|
445
|
+
repoId: plan.repoId,
|
|
446
|
+
refUpdates: plan.refUpdates,
|
|
447
|
+
uploads: result.uploads.map((u) => ({
|
|
448
|
+
sha: u.sha,
|
|
449
|
+
txId: u.txId,
|
|
450
|
+
feePaid: u.feePaid.toString(),
|
|
451
|
+
skipped: u.skipped
|
|
452
|
+
})),
|
|
453
|
+
announceReceipt: result.announceReceipt ? {
|
|
454
|
+
eventId: result.announceReceipt.eventId,
|
|
455
|
+
feePaid: result.announceReceipt.feePaid.toString()
|
|
456
|
+
} : null,
|
|
457
|
+
refsReceipt: {
|
|
458
|
+
eventId: result.refsReceipt.eventId,
|
|
459
|
+
feePaid: result.refsReceipt.feePaid.toString()
|
|
460
|
+
},
|
|
461
|
+
arweaveMap: Object.fromEntries(result.arweaveMap),
|
|
462
|
+
totalFeePaid: result.totalFeePaid.toString(),
|
|
463
|
+
estimate: serializeFeeEstimate(plan)
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/push.ts
|
|
468
|
+
var NonFastForwardError = class extends Error {
|
|
469
|
+
constructor(refs) {
|
|
470
|
+
super(
|
|
471
|
+
`non-fast-forward update rejected for ${refs.map((r) => r.refname).join(", ")} \u2014 re-run with force to overwrite the remote ref(s)`
|
|
472
|
+
);
|
|
473
|
+
this.refs = refs;
|
|
474
|
+
this.name = "NonFastForwardError";
|
|
475
|
+
}
|
|
476
|
+
refs;
|
|
477
|
+
};
|
|
478
|
+
var OversizeObjectsError = class extends Error {
|
|
479
|
+
constructor(objects) {
|
|
480
|
+
super(
|
|
481
|
+
`${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` + objects.map((o) => `${o.path ?? o.sha} (${o.size} bytes)`).join(", ")
|
|
482
|
+
);
|
|
483
|
+
this.objects = objects;
|
|
484
|
+
this.name = "OversizeObjectsError";
|
|
485
|
+
}
|
|
486
|
+
objects;
|
|
487
|
+
};
|
|
488
|
+
async function planPush(options) {
|
|
489
|
+
const { repoReader, remoteState, feeRates, repoId, force = false } = options;
|
|
490
|
+
const resolveMissing = options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);
|
|
491
|
+
const { head, refs: localRefs } = await repoReader.listRefs();
|
|
492
|
+
const localByName = new Map(localRefs.map((r) => [r.refname, r]));
|
|
493
|
+
let selected;
|
|
494
|
+
if (options.refs !== void 0) {
|
|
495
|
+
selected = options.refs.map((name) => {
|
|
496
|
+
const ref = localByName.get(name);
|
|
497
|
+
if (!ref) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`ref ${JSON.stringify(name)} does not exist locally (ref deletion is out of scope in v1)`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return ref;
|
|
503
|
+
});
|
|
504
|
+
} else {
|
|
505
|
+
selected = localRefs;
|
|
506
|
+
}
|
|
507
|
+
const refUpdates = [];
|
|
508
|
+
const rejected = [];
|
|
509
|
+
for (const ref of selected) {
|
|
510
|
+
const remoteSha = remoteState.refs.get(ref.refname) ?? null;
|
|
511
|
+
if (remoteSha === null) {
|
|
512
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "new" });
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (remoteSha === ref.sha) {
|
|
516
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "up-to-date" });
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
let fastForward = false;
|
|
520
|
+
try {
|
|
521
|
+
fastForward = await repoReader.isAncestor(remoteSha, ref.sha);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (!(err instanceof GitError)) throw err;
|
|
524
|
+
fastForward = false;
|
|
525
|
+
}
|
|
526
|
+
if (fastForward) {
|
|
527
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "fast-forward" });
|
|
528
|
+
} else if (force) {
|
|
529
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "forced" });
|
|
530
|
+
} else {
|
|
531
|
+
rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (rejected.length > 0) throw new NonFastForwardError(rejected);
|
|
535
|
+
const updates = refUpdates.filter((u) => u.kind !== "up-to-date");
|
|
536
|
+
const wantTips = [...new Set(updates.map((u) => u.localSha))];
|
|
537
|
+
const haveTips = [...new Set(remoteState.refs.values())];
|
|
538
|
+
const delta = wantTips.length > 0 ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips) : [];
|
|
539
|
+
const knownShaToTxId = new Map(remoteState.shaToTxId);
|
|
540
|
+
let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));
|
|
541
|
+
if (candidates.length > 0) {
|
|
542
|
+
const resolved = await resolveMissing(candidates.map((o) => o.sha));
|
|
543
|
+
for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);
|
|
544
|
+
candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));
|
|
545
|
+
}
|
|
546
|
+
const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));
|
|
547
|
+
const { objects: stats, missing } = await repoReader.statObjects(
|
|
548
|
+
candidates.map((c) => c.sha)
|
|
549
|
+
);
|
|
550
|
+
if (missing.length > 0) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`objects vanished from the local repository during planning: ${missing.join(", ")}`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
const oversize = [];
|
|
556
|
+
for (const stat of stats) {
|
|
557
|
+
if (stat.size > MAX_OBJECT_SIZE) {
|
|
558
|
+
const path = pathBySha.get(stat.sha);
|
|
559
|
+
oversize.push({ ...stat, ...path ? { path } : {} });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (oversize.length > 0) throw new OversizeObjectsError(oversize);
|
|
563
|
+
const tipShas = new Set(updates.map((u) => u.localSha));
|
|
564
|
+
const planned = stats.map((stat) => {
|
|
565
|
+
const path = pathBySha.get(stat.sha);
|
|
566
|
+
return { ...stat, ...path ? { path } : {}, isRefTip: tipShas.has(stat.sha) };
|
|
567
|
+
});
|
|
568
|
+
const objects = [
|
|
569
|
+
...planned.filter((o) => !o.isRefTip),
|
|
570
|
+
...planned.filter((o) => o.isRefTip)
|
|
571
|
+
];
|
|
572
|
+
const newRefsMap = new Map(remoteState.refs);
|
|
573
|
+
for (const update of updates) newRefsMap.set(update.refname, update.localSha);
|
|
574
|
+
const headSymref = head && newRefsMap.has(head) ? head : remoteState.headSymref && newRefsMap.has(remoteState.headSymref) ? remoteState.headSymref : [...newRefsMap.keys()][0] ?? null;
|
|
575
|
+
const newRefs = {};
|
|
576
|
+
const headSha = headSymref ? newRefsMap.get(headSymref) : void 0;
|
|
577
|
+
if (headSymref && headSha) newRefs[headSymref] = headSha;
|
|
578
|
+
for (const [refname, sha] of newRefsMap) {
|
|
579
|
+
if (refname !== headSymref) newRefs[refname] = sha;
|
|
580
|
+
}
|
|
581
|
+
const announceNeeded = !remoteState.announced;
|
|
582
|
+
const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);
|
|
583
|
+
const uploadFee = BigInt(totalObjectBytes) * feeRates.uploadFeePerByte;
|
|
584
|
+
const eventCount = 1 + (announceNeeded ? 1 : 0);
|
|
585
|
+
const eventFees = BigInt(eventCount) * feeRates.eventFee;
|
|
586
|
+
return {
|
|
587
|
+
repoId,
|
|
588
|
+
refUpdates,
|
|
589
|
+
newRefs,
|
|
590
|
+
headSymref,
|
|
591
|
+
objects,
|
|
592
|
+
knownShaToTxId,
|
|
593
|
+
announceNeeded,
|
|
594
|
+
announcement: {
|
|
595
|
+
name: options.announcement?.name ?? repoId,
|
|
596
|
+
description: options.announcement?.description ?? ""
|
|
597
|
+
},
|
|
598
|
+
estimate: {
|
|
599
|
+
objectCount: objects.length,
|
|
600
|
+
totalObjectBytes,
|
|
601
|
+
uploadFee,
|
|
602
|
+
eventCount,
|
|
603
|
+
eventFees,
|
|
604
|
+
totalFee: uploadFee + eventFees
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
var READ_BATCH_SIZE = 100;
|
|
609
|
+
async function executePush(options) {
|
|
610
|
+
const { plan, publisher, remoteState, repoReader, relayUrls } = options;
|
|
611
|
+
const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);
|
|
612
|
+
const resultBySha = /* @__PURE__ */ new Map();
|
|
613
|
+
let totalFeePaid = 0n;
|
|
614
|
+
const pending = [];
|
|
615
|
+
for (const object of plan.objects) {
|
|
616
|
+
const knownTxId = merged.get(object.sha);
|
|
617
|
+
if (knownTxId !== void 0) {
|
|
618
|
+
resultBySha.set(object.sha, {
|
|
619
|
+
sha: object.sha,
|
|
620
|
+
txId: knownTxId,
|
|
621
|
+
feePaid: 0n,
|
|
622
|
+
skipped: true
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
pending.push(object);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (let i = 0; i < pending.length; i += READ_BATCH_SIZE) {
|
|
629
|
+
const batch = pending.slice(i, i + READ_BATCH_SIZE);
|
|
630
|
+
const { objects: read, missing } = await repoReader.readObjects(
|
|
631
|
+
batch.map((o) => o.sha)
|
|
632
|
+
);
|
|
633
|
+
if (missing.length > 0) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`objects vanished from the local repository during push: ${missing.join(", ")}`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
const bodyBySha = new Map(read.map((r) => [r.sha, r.body]));
|
|
639
|
+
for (const object of batch) {
|
|
640
|
+
const body = bodyBySha.get(object.sha);
|
|
641
|
+
if (!body) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
`internal: cat-file returned no body for ${object.sha}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const receipt = await publisher.uploadGitObject({
|
|
647
|
+
sha: object.sha,
|
|
648
|
+
type: object.type,
|
|
649
|
+
body,
|
|
650
|
+
repoId: plan.repoId
|
|
651
|
+
});
|
|
652
|
+
merged.set(object.sha, receipt.txId);
|
|
653
|
+
totalFeePaid += receipt.feePaid;
|
|
654
|
+
resultBySha.set(object.sha, {
|
|
655
|
+
sha: object.sha,
|
|
656
|
+
txId: receipt.txId,
|
|
657
|
+
feePaid: receipt.feePaid,
|
|
658
|
+
skipped: false
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
let announceReceipt = null;
|
|
663
|
+
if (plan.announceNeeded && !remoteState.announced) {
|
|
664
|
+
const announceEvent = buildRepoAnnouncement(
|
|
665
|
+
plan.repoId,
|
|
666
|
+
plan.announcement.name,
|
|
667
|
+
plan.announcement.description
|
|
668
|
+
);
|
|
669
|
+
announceReceipt = await publisher.publishEvent(announceEvent, relayUrls);
|
|
670
|
+
totalFeePaid += announceReceipt.feePaid;
|
|
671
|
+
}
|
|
672
|
+
const refsEvent = buildRepoRefs(
|
|
673
|
+
plan.repoId,
|
|
674
|
+
plan.newRefs,
|
|
675
|
+
Object.fromEntries(merged)
|
|
676
|
+
);
|
|
677
|
+
const refsReceipt = await publisher.publishEvent(refsEvent, relayUrls);
|
|
678
|
+
totalFeePaid += refsReceipt.feePaid;
|
|
679
|
+
const uploads = plan.objects.map((o) => {
|
|
680
|
+
const step = resultBySha.get(o.sha);
|
|
681
|
+
if (!step) {
|
|
682
|
+
throw new Error(`internal: no upload result recorded for ${o.sha}`);
|
|
683
|
+
}
|
|
684
|
+
return step;
|
|
685
|
+
});
|
|
686
|
+
return {
|
|
687
|
+
uploads,
|
|
688
|
+
announceReceipt,
|
|
689
|
+
refsReceipt,
|
|
690
|
+
arweaveMap: merged,
|
|
691
|
+
totalFeePaid
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export {
|
|
696
|
+
GitError,
|
|
697
|
+
GitRepoReader,
|
|
698
|
+
serializeFeeEstimate,
|
|
699
|
+
serializePushPlan,
|
|
700
|
+
serializeEventReceipt,
|
|
701
|
+
serializePushResult,
|
|
702
|
+
NonFastForwardError,
|
|
703
|
+
OversizeObjectsError,
|
|
704
|
+
planPush,
|
|
705
|
+
executePush
|
|
706
|
+
};
|
|
707
|
+
//# sourceMappingURL=chunk-4WFGAICZ.js.map
|