@toon-protocol/client-mcp 0.13.0 → 0.13.2
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/dist/app/index.html +8 -8
- package/dist/{chunk-GQENVTED.js → chunk-BINI3PEC.js} +883 -877
- package/dist/chunk-BINI3PEC.js.map +1 -0
- package/dist/daemon.js +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-GQENVTED.js.map +0 -1
|
@@ -367,12 +367,13 @@ var STATUS_APPLIED_KIND = 1631;
|
|
|
367
367
|
var STATUS_CLOSED_KIND = 1632;
|
|
368
368
|
var STATUS_DRAFT_KIND = 1633;
|
|
369
369
|
|
|
370
|
-
// ../../node_modules/.pnpm/@toon-protocol+core@
|
|
370
|
+
// ../../node_modules/.pnpm/@toon-protocol+core@2.0.1_typescript@5.9.3/node_modules/@toon-protocol/core/dist/nip34/index.js
|
|
371
371
|
var REPOSITORY_ANNOUNCEMENT_KIND2 = 30617;
|
|
372
372
|
var PATCH_KIND = 1617;
|
|
373
373
|
var ISSUE_KIND = 1621;
|
|
374
374
|
|
|
375
|
-
// ../
|
|
375
|
+
// ../rig/dist/chunk-JBB7HBQC.js
|
|
376
|
+
import { decode as decodeToon } from "@toon-format/toon";
|
|
376
377
|
var REPOSITORY_STATE_KIND = 30618;
|
|
377
378
|
var COMMENT_KIND = 1622;
|
|
378
379
|
function buildRepoAnnouncement(repoId, name, description) {
|
|
@@ -432,12 +433,15 @@ function buildComment(repoOwnerPubkey, repoId, issueOrPrEventId, authorPubkey, b
|
|
|
432
433
|
created_at: Math.floor(Date.now() / 1e3)
|
|
433
434
|
};
|
|
434
435
|
}
|
|
435
|
-
function buildPatch(repoOwnerPubkey, repoId, title, commits, branchTag, content = "") {
|
|
436
|
+
function buildPatch(repoOwnerPubkey, repoId, title, commits, branchTag, content = "", description) {
|
|
436
437
|
const tags = [
|
|
437
438
|
["a", `${REPOSITORY_ANNOUNCEMENT_KIND2}:${repoOwnerPubkey}:${repoId}`],
|
|
438
439
|
["p", repoOwnerPubkey],
|
|
439
440
|
["subject", title]
|
|
440
441
|
];
|
|
442
|
+
if (description !== void 0 && description !== "") {
|
|
443
|
+
tags.push(["description", description]);
|
|
444
|
+
}
|
|
441
445
|
for (const commit of commits) {
|
|
442
446
|
tags.push(["commit", commit.sha]);
|
|
443
447
|
tags.push(["parent-commit", commit.parentSha]);
|
|
@@ -464,966 +468,966 @@ function buildStatus(targetEventId, statusKind, targetPubkey) {
|
|
|
464
468
|
created_at: Math.floor(Date.now() / 1e3)
|
|
465
469
|
};
|
|
466
470
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
var
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
import { promisify } from "util";
|
|
474
|
-
var execFileAsync = promisify(execFile);
|
|
475
|
-
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
476
|
-
var GitError = class extends Error {
|
|
477
|
-
constructor(message, exitCode, stderr) {
|
|
478
|
-
super(message);
|
|
479
|
-
this.exitCode = exitCode;
|
|
480
|
-
this.stderr = stderr;
|
|
481
|
-
this.name = "GitError";
|
|
482
|
-
}
|
|
483
|
-
exitCode;
|
|
484
|
-
stderr;
|
|
485
|
-
};
|
|
486
|
-
var FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
487
|
-
var REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;
|
|
488
|
-
function isValidRevision(rev) {
|
|
489
|
-
if (rev.length === 0 || rev.length > 1024) return false;
|
|
490
|
-
if (!REV_TOKEN_RE.test(rev)) return false;
|
|
491
|
-
if (rev.includes("..")) return false;
|
|
492
|
-
if (rev.endsWith(".lock") || rev.endsWith("/") || rev.endsWith(".")) return false;
|
|
493
|
-
return true;
|
|
471
|
+
var ARWEAVE_FETCH_TIMEOUT_MS = 15e3;
|
|
472
|
+
var ARWEAVE_GRAPHQL_URL = "https://arweave.net/graphql";
|
|
473
|
+
var SHA_CACHE_MAX_SIZE = 1e4;
|
|
474
|
+
var shaToTxIdCache = /* @__PURE__ */ new Map();
|
|
475
|
+
function isValidGitSha(sha) {
|
|
476
|
+
return /^[0-9a-f]{40}$/i.test(sha);
|
|
494
477
|
}
|
|
495
|
-
function
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
478
|
+
function sanitizeGraphQLValue(value) {
|
|
479
|
+
return value.replace(/["\\\n\r\u0000-\u001f`]/g, "");
|
|
480
|
+
}
|
|
481
|
+
function shaCacheKey(sha, repo) {
|
|
482
|
+
return `${sha}:${repo}`;
|
|
483
|
+
}
|
|
484
|
+
function seedShaCache(mappings) {
|
|
485
|
+
const entries = mappings instanceof Map ? mappings.entries() : mappings;
|
|
486
|
+
for (const [key2, txId] of entries) {
|
|
487
|
+
if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
|
|
488
|
+
const firstKey = shaToTxIdCache.keys().next().value;
|
|
489
|
+
if (firstKey !== void 0) {
|
|
490
|
+
shaToTxIdCache.delete(firstKey);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
shaToTxIdCache.set(key2, txId);
|
|
500
494
|
}
|
|
501
495
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
496
|
+
var ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;
|
|
497
|
+
function isValidArweaveTxId(txId) {
|
|
498
|
+
return ARWEAVE_TX_ID_RE.test(txId);
|
|
499
|
+
}
|
|
500
|
+
async function resolveGitSha(sha, repo) {
|
|
501
|
+
if (!isValidGitSha(sha)) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const cacheKey = shaCacheKey(sha, repo);
|
|
505
|
+
const cached = shaToTxIdCache.get(cacheKey);
|
|
506
|
+
if (cached !== void 0) {
|
|
507
|
+
return cached;
|
|
508
|
+
}
|
|
509
|
+
const safeSha = sanitizeGraphQLValue(sha);
|
|
510
|
+
const safeRepo = sanitizeGraphQLValue(repo);
|
|
511
|
+
const query = `query {
|
|
512
|
+
transactions(tags: [
|
|
513
|
+
{ name: "Git-SHA", values: ["${safeSha}"] },
|
|
514
|
+
{ name: "Repo", values: ["${safeRepo}"] }
|
|
515
|
+
]) {
|
|
516
|
+
edges { node { id } }
|
|
517
|
+
}
|
|
518
|
+
}`;
|
|
519
|
+
try {
|
|
520
|
+
const response = await fetch(ARWEAVE_GRAPHQL_URL, {
|
|
521
|
+
method: "POST",
|
|
522
|
+
headers: { "Content-Type": "application/json" },
|
|
523
|
+
body: JSON.stringify({ query }),
|
|
524
|
+
signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS)
|
|
525
|
+
});
|
|
526
|
+
if (!response.ok) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
const json = await response.json();
|
|
530
|
+
const edges = json.data?.transactions?.edges;
|
|
531
|
+
if (!edges || edges.length === 0) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const txId = edges[0]?.node?.id;
|
|
535
|
+
if (!txId || !isValidArweaveTxId(txId)) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
|
|
539
|
+
const firstKey = shaToTxIdCache.keys().next().value;
|
|
540
|
+
if (firstKey !== void 0) {
|
|
541
|
+
shaToTxIdCache.delete(firstKey);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
shaToTxIdCache.set(cacheKey, txId);
|
|
545
|
+
return txId;
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
507
548
|
}
|
|
508
549
|
}
|
|
509
|
-
function
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
550
|
+
function isValidRelayUrl(url) {
|
|
551
|
+
return /^wss?:\/\//i.test(url);
|
|
552
|
+
}
|
|
553
|
+
var WS_OPEN = 1;
|
|
554
|
+
function defaultWebSocketFactory2(url) {
|
|
555
|
+
const ctor = globalThis.WebSocket;
|
|
556
|
+
if (!ctor) {
|
|
514
557
|
throw new Error(
|
|
515
|
-
|
|
558
|
+
"No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
|
|
516
559
|
);
|
|
517
560
|
}
|
|
561
|
+
return new ctor(url);
|
|
518
562
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
pending = null;
|
|
523
|
-
objects = [];
|
|
524
|
-
missing = [];
|
|
525
|
-
push(chunk) {
|
|
526
|
-
this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
|
|
527
|
-
this.drain();
|
|
563
|
+
function decodeEventPayload(payload) {
|
|
564
|
+
if (payload !== null && typeof payload === "object") {
|
|
565
|
+
return payload;
|
|
528
566
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
return this.pending === null && this.buf.length === 0;
|
|
567
|
+
if (typeof payload !== "string") {
|
|
568
|
+
return null;
|
|
532
569
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
570
|
+
try {
|
|
571
|
+
const parsed = JSON.parse(payload);
|
|
572
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
573
|
+
return parsed;
|
|
574
|
+
}
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
return decodeToon(payload);
|
|
579
|
+
} catch {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function queryRelay(relayUrl, filter, timeoutMs, webSocketFactory) {
|
|
584
|
+
return new Promise((resolve2, reject) => {
|
|
585
|
+
const events = [];
|
|
586
|
+
const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
587
|
+
let ws;
|
|
588
|
+
let timeoutHandle;
|
|
589
|
+
let settled = false;
|
|
590
|
+
const settle = (outcome, error) => {
|
|
591
|
+
if (settled) return;
|
|
592
|
+
settled = true;
|
|
593
|
+
clearTimeout(timeoutHandle);
|
|
594
|
+
try {
|
|
595
|
+
if (ws.readyState === WS_OPEN) {
|
|
596
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
558
597
|
}
|
|
598
|
+
ws.close();
|
|
599
|
+
} catch {
|
|
559
600
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
601
|
+
if (outcome === "reject") {
|
|
602
|
+
reject(error);
|
|
603
|
+
} else {
|
|
604
|
+
resolve2(events);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
if (!isValidRelayUrl(relayUrl)) {
|
|
608
|
+
reject(
|
|
609
|
+
new Error(
|
|
610
|
+
`Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`
|
|
611
|
+
)
|
|
564
612
|
);
|
|
613
|
+
return;
|
|
565
614
|
}
|
|
566
|
-
}
|
|
567
|
-
};
|
|
568
|
-
var GitRepoReader = class {
|
|
569
|
-
constructor(repoPath) {
|
|
570
|
-
this.repoPath = repoPath;
|
|
571
|
-
}
|
|
572
|
-
repoPath;
|
|
573
|
-
/** Run git with argument-array safety; resolves stdout as UTF-8. */
|
|
574
|
-
async git(args, opts = {}) {
|
|
575
615
|
try {
|
|
576
|
-
|
|
577
|
-
cwd: this.repoPath,
|
|
578
|
-
maxBuffer: MAX_BUFFER,
|
|
579
|
-
encoding: "utf-8"
|
|
580
|
-
});
|
|
581
|
-
return { stdout, exitCode: 0 };
|
|
616
|
+
ws = webSocketFactory(relayUrl);
|
|
582
617
|
} catch (err) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (exitCode !== void 0 && opts.allowExitCodes?.includes(exitCode)) {
|
|
586
|
-
return { stdout: e.stdout ?? "", exitCode };
|
|
587
|
-
}
|
|
588
|
-
throw new GitError(
|
|
589
|
-
`git ${args[0]} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`,
|
|
590
|
-
exitCode,
|
|
591
|
-
(e.stderr ?? "").trim()
|
|
592
|
-
);
|
|
618
|
+
reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));
|
|
619
|
+
return;
|
|
593
620
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {
|
|
613
|
-
throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, void 0, "");
|
|
621
|
+
timeoutHandle = setTimeout(() => {
|
|
622
|
+
settle("resolve");
|
|
623
|
+
}, timeoutMs);
|
|
624
|
+
ws.addEventListener("open", () => {
|
|
625
|
+
ws.send(JSON.stringify(["REQ", subId, filter]));
|
|
626
|
+
});
|
|
627
|
+
ws.addEventListener("message", (msgEvent) => {
|
|
628
|
+
try {
|
|
629
|
+
const msg = JSON.parse(String(msgEvent.data));
|
|
630
|
+
if (!Array.isArray(msg) || msg.length < 2) return;
|
|
631
|
+
const msgType = msg[0];
|
|
632
|
+
if (msgType === "EVENT" && msg[1] === subId && msg[2] !== void 0) {
|
|
633
|
+
const event = decodeEventPayload(msg[2]);
|
|
634
|
+
if (event) events.push(event);
|
|
635
|
+
} else if (msgType === "EOSE" && msg[1] === subId) {
|
|
636
|
+
settle("resolve");
|
|
637
|
+
}
|
|
638
|
+
} catch {
|
|
614
639
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
640
|
+
});
|
|
641
|
+
ws.addEventListener("error", (event) => {
|
|
642
|
+
const detail = typeof event === "object" && event !== null && "message" in event ? String(event.message) : "unknown";
|
|
643
|
+
settle(
|
|
644
|
+
"reject",
|
|
645
|
+
new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
ws.addEventListener("close", () => {
|
|
649
|
+
settle("resolve");
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function latestReplaceable(events) {
|
|
654
|
+
let winner = null;
|
|
655
|
+
for (const event of events) {
|
|
656
|
+
if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
|
|
657
|
+
winner = event;
|
|
621
658
|
}
|
|
622
|
-
const head = headRes.exitCode === 0 ? headRes.stdout.trim() || void 0 : void 0;
|
|
623
|
-
return { head, refs };
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* SHAs of every object reachable from `want` but not from `have`
|
|
627
|
-
* (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.
|
|
628
|
-
*
|
|
629
|
-
* Haves that don't exist locally (e.g. remote tips we never fetched) are
|
|
630
|
-
* filtered out first via one `cat-file --batch-check` pass — rev-list
|
|
631
|
-
* would otherwise die on them.
|
|
632
|
-
*/
|
|
633
|
-
async objectsBetween(want, have) {
|
|
634
|
-
const objects = await this.objectsBetweenWithPaths(want, have);
|
|
635
|
-
return objects.map((o) => o.sha);
|
|
636
659
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (spaceIdx === -1) {
|
|
656
|
-
objects.push({ sha: line });
|
|
657
|
-
} else {
|
|
658
|
-
const path = line.slice(spaceIdx + 1);
|
|
659
|
-
objects.push({
|
|
660
|
-
sha: line.slice(0, spaceIdx),
|
|
661
|
-
...path ? { path } : {}
|
|
662
|
-
});
|
|
660
|
+
return winner;
|
|
661
|
+
}
|
|
662
|
+
function getTagValue(tags, name) {
|
|
663
|
+
const tag = tags.find((t) => t[0] === name);
|
|
664
|
+
return tag?.[1];
|
|
665
|
+
}
|
|
666
|
+
var MAX_REFS_PER_EVENT = 1e3;
|
|
667
|
+
var SYMREF_PREFIX = "ref: ";
|
|
668
|
+
function parseRefsEvent(event) {
|
|
669
|
+
const refs = /* @__PURE__ */ new Map();
|
|
670
|
+
const shaToTxId = /* @__PURE__ */ new Map();
|
|
671
|
+
let headSymref = null;
|
|
672
|
+
for (const tag of event.tags) {
|
|
673
|
+
const [tagName, v1, v2] = tag;
|
|
674
|
+
if (tagName === "r" && v1 && v2) {
|
|
675
|
+
if (v1 === "HEAD" && v2.startsWith(SYMREF_PREFIX)) {
|
|
676
|
+
headSymref = v2.slice(SYMREF_PREFIX.length);
|
|
677
|
+
continue;
|
|
663
678
|
}
|
|
679
|
+
if (refs.size >= MAX_REFS_PER_EVENT) continue;
|
|
680
|
+
refs.set(v1, v2);
|
|
681
|
+
} else if (tagName === "HEAD" && v1?.startsWith(SYMREF_PREFIX)) {
|
|
682
|
+
headSymref = v1.slice(SYMREF_PREFIX.length);
|
|
683
|
+
} else if (tagName === "arweave" && v1 && v2) {
|
|
684
|
+
shaToTxId.set(v1, v2);
|
|
664
685
|
}
|
|
665
|
-
return objects;
|
|
666
686
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
child.on("error", (err) => {
|
|
681
|
-
reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, void 0, ""));
|
|
682
|
-
});
|
|
683
|
-
child.on("close", (code) => {
|
|
684
|
-
if (code !== 0) {
|
|
685
|
-
return reject(
|
|
686
|
-
new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
resolve2(Buffer.concat(out));
|
|
690
|
-
});
|
|
691
|
-
child.stdin.on("error", () => {
|
|
692
|
-
});
|
|
693
|
-
child.stdin.write(input);
|
|
694
|
-
child.stdin.end();
|
|
695
|
-
});
|
|
687
|
+
return { refs, headSymref, shaToTxId };
|
|
688
|
+
}
|
|
689
|
+
async function fetchRemoteState(options) {
|
|
690
|
+
const {
|
|
691
|
+
relayUrls,
|
|
692
|
+
ownerPubkey,
|
|
693
|
+
repoId,
|
|
694
|
+
timeoutMs = 1e4,
|
|
695
|
+
resolveSha = resolveGitSha,
|
|
696
|
+
webSocketFactory = defaultWebSocketFactory2
|
|
697
|
+
} = options;
|
|
698
|
+
if (relayUrls.length === 0) {
|
|
699
|
+
throw new Error("fetchRemoteState: relayUrls must not be empty");
|
|
696
700
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (revs.length === 0) return [];
|
|
700
|
-
const { missing } = await this.batchCheck(revs);
|
|
701
|
-
const missingSet = new Set(missing);
|
|
702
|
-
return revs.filter((r) => !missingSet.has(r));
|
|
701
|
+
if (!ownerPubkey) {
|
|
702
|
+
throw new Error("fetchRemoteState: ownerPubkey is required");
|
|
703
703
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
704
|
+
if (!repoId) {
|
|
705
|
+
throw new Error("fetchRemoteState: repoId is required");
|
|
706
|
+
}
|
|
707
|
+
const filter = {
|
|
708
|
+
kinds: [REPOSITORY_ANNOUNCEMENT_KIND2, REPOSITORY_STATE_KIND],
|
|
709
|
+
authors: [ownerPubkey],
|
|
710
|
+
"#d": [repoId]
|
|
711
|
+
};
|
|
712
|
+
const results = await Promise.allSettled(
|
|
713
|
+
relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))
|
|
714
|
+
);
|
|
715
|
+
const failures = [];
|
|
716
|
+
const byId = /* @__PURE__ */ new Map();
|
|
717
|
+
for (const result of results) {
|
|
718
|
+
if (result.status === "rejected") {
|
|
719
|
+
failures.push(String(result.reason?.message ?? result.reason));
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
for (const event of result.value) {
|
|
723
|
+
if (event.pubkey !== ownerPubkey) continue;
|
|
724
|
+
if (getTagValue(event.tags, "d") !== repoId) continue;
|
|
725
|
+
if (typeof event.id === "string" && !byId.has(event.id)) {
|
|
726
|
+
byId.set(event.id, event);
|
|
727
|
+
}
|
|
713
728
|
}
|
|
714
|
-
return { missing };
|
|
715
729
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
* reported, not thrown.
|
|
720
|
-
*/
|
|
721
|
-
async statObjects(shas) {
|
|
722
|
-
for (const sha of shas) assertFullSha(sha, "sha");
|
|
723
|
-
if (shas.length === 0) return { objects: [], missing: [] };
|
|
724
|
-
const stdout = await this.runWithStdin(
|
|
725
|
-
["cat-file", "--batch-check"],
|
|
726
|
-
shas.join("\n") + "\n"
|
|
730
|
+
if (failures.length === relayUrls.length) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join("; ")}`
|
|
727
733
|
);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
734
|
+
}
|
|
735
|
+
const events = [...byId.values()];
|
|
736
|
+
const refsEvent = latestReplaceable(
|
|
737
|
+
events.filter((e) => e.kind === REPOSITORY_STATE_KIND)
|
|
738
|
+
);
|
|
739
|
+
const announceEvent = latestReplaceable(
|
|
740
|
+
events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND2)
|
|
741
|
+
);
|
|
742
|
+
const { refs, headSymref, shaToTxId } = refsEvent ? parseRefsEvent(refsEvent) : {
|
|
743
|
+
refs: /* @__PURE__ */ new Map(),
|
|
744
|
+
headSymref: null,
|
|
745
|
+
shaToTxId: /* @__PURE__ */ new Map()
|
|
746
|
+
};
|
|
747
|
+
if (shaToTxId.size > 0) {
|
|
748
|
+
seedShaCache(
|
|
749
|
+
[...shaToTxId].map(
|
|
750
|
+
([sha, txId]) => [shaCacheKey(sha, repoId), txId]
|
|
751
|
+
)
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
const name = announceEvent ? getTagValue(announceEvent.tags, "name") ?? null : null;
|
|
755
|
+
const description = announceEvent ? getTagValue(announceEvent.tags, "description") ?? announceEvent.content : null;
|
|
756
|
+
const relays = [];
|
|
757
|
+
if (announceEvent) {
|
|
758
|
+
for (const tag of announceEvent.tags) {
|
|
759
|
+
if (tag[0] === "relays") {
|
|
760
|
+
relays.push(...tag.slice(1).filter((url) => url.length > 0));
|
|
744
761
|
}
|
|
745
|
-
objects.push({ sha, type, size });
|
|
746
762
|
}
|
|
747
|
-
return { objects, missing };
|
|
748
763
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
764
|
+
const resolveMissing = async (shas) => {
|
|
765
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
766
|
+
const missing = [];
|
|
767
|
+
for (const sha of new Set(shas)) {
|
|
768
|
+
const known = shaToTxId.get(sha);
|
|
769
|
+
if (known !== void 0) {
|
|
770
|
+
resolved.set(sha, known);
|
|
771
|
+
} else {
|
|
772
|
+
missing.push(sha);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const lookups = await Promise.all(
|
|
776
|
+
missing.map(
|
|
777
|
+
async (sha) => [sha, await resolveSha(sha, repoId)]
|
|
778
|
+
)
|
|
779
|
+
);
|
|
780
|
+
for (const [sha, txId] of lookups) {
|
|
781
|
+
if (txId) resolved.set(sha, txId);
|
|
782
|
+
}
|
|
783
|
+
return resolved;
|
|
784
|
+
};
|
|
785
|
+
return {
|
|
786
|
+
announced: announceEvent !== null,
|
|
787
|
+
refs,
|
|
788
|
+
headSymref,
|
|
789
|
+
shaToTxId,
|
|
790
|
+
refsEvent,
|
|
791
|
+
announceEvent,
|
|
792
|
+
name,
|
|
793
|
+
description,
|
|
794
|
+
relays,
|
|
795
|
+
resolveMissing
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ../rig/dist/chunk-X2CZPPDM.js
|
|
800
|
+
var MAX_OBJECT_SIZE = 95 * 1024;
|
|
801
|
+
|
|
802
|
+
// ../rig/dist/chunk-PS5QOT62.js
|
|
803
|
+
import { execFile, spawn } from "child_process";
|
|
804
|
+
import { promisify } from "util";
|
|
805
|
+
import { execFile as execFile2, spawn as spawn2 } from "child_process";
|
|
806
|
+
import { promisify as promisify2 } from "util";
|
|
807
|
+
var execFileAsync = promisify(execFile);
|
|
808
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
809
|
+
var GitError = class extends Error {
|
|
810
|
+
constructor(message, exitCode, stderr) {
|
|
811
|
+
super(message);
|
|
812
|
+
this.exitCode = exitCode;
|
|
813
|
+
this.stderr = stderr;
|
|
814
|
+
this.name = "GitError";
|
|
800
815
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
816
|
+
exitCode;
|
|
817
|
+
stderr;
|
|
818
|
+
};
|
|
819
|
+
var FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
820
|
+
var REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;
|
|
821
|
+
function isValidRevision(rev) {
|
|
822
|
+
if (rev.length === 0 || rev.length > 1024) return false;
|
|
823
|
+
if (!REV_TOKEN_RE.test(rev)) return false;
|
|
824
|
+
if (rev.includes("..")) return false;
|
|
825
|
+
if (rev.endsWith(".lock") || rev.endsWith("/") || rev.endsWith(".")) return false;
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
function assertRevision(rev, what) {
|
|
829
|
+
if (!isValidRevision(rev)) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`${what} is not a valid git revision (got ${JSON.stringify(rev)}); expected a SHA or simple refname \u2014 options/ranges are rejected`
|
|
812
832
|
);
|
|
813
|
-
return exitCode === 0;
|
|
814
833
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
const { stdout } = await this.git(["format-patch", "--stdout", range, "--"]);
|
|
822
|
-
return stdout;
|
|
834
|
+
}
|
|
835
|
+
function assertFullSha(sha, what) {
|
|
836
|
+
if (!FULL_SHA_RE.test(sha)) {
|
|
837
|
+
throw new Error(
|
|
838
|
+
`${what} is not a full 40-hex SHA-1 (got ${JSON.stringify(sha)})`
|
|
839
|
+
);
|
|
823
840
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const { stdout } = await this.git([
|
|
834
|
-
"rev-list",
|
|
835
|
-
"--no-walk=unsorted",
|
|
836
|
-
"--parents",
|
|
837
|
-
...shas,
|
|
838
|
-
"--"
|
|
839
|
-
]);
|
|
840
|
-
const parents = /* @__PURE__ */ new Map();
|
|
841
|
-
for (const line of stdout.split("\n")) {
|
|
842
|
-
if (!line) continue;
|
|
843
|
-
const [sha, ...rest] = line.split(" ");
|
|
844
|
-
if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {
|
|
845
|
-
throw new GitError(
|
|
846
|
-
`unexpected rev-list --parents line: ${JSON.stringify(line)}`,
|
|
847
|
-
void 0,
|
|
848
|
-
""
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
parents.set(sha, rest);
|
|
852
|
-
}
|
|
853
|
-
return parents;
|
|
841
|
+
}
|
|
842
|
+
function assertRange(range, what) {
|
|
843
|
+
const parts = range.split(/\.{2,3}/);
|
|
844
|
+
const separators = range.match(/\.{2,3}/g) ?? [];
|
|
845
|
+
const ok = parts.length <= 2 && separators.length === parts.length - 1 && parts.every((p) => isValidRevision(p));
|
|
846
|
+
if (!ok) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`${what} is not a valid revision range (got ${JSON.stringify(range)}); expected <rev>, <rev>..<rev>, or <rev>...<rev>`
|
|
849
|
+
);
|
|
854
850
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
851
|
+
}
|
|
852
|
+
var OBJECT_TYPES = /* @__PURE__ */ new Set(["blob", "tree", "commit", "tag"]);
|
|
853
|
+
var BatchParser = class {
|
|
854
|
+
buf = Buffer.alloc(0);
|
|
855
|
+
pending = null;
|
|
856
|
+
objects = [];
|
|
857
|
+
missing = [];
|
|
858
|
+
push(chunk) {
|
|
859
|
+
this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
|
|
860
|
+
this.drain();
|
|
861
|
+
}
|
|
862
|
+
/** True when no partially-parsed record remains. */
|
|
863
|
+
isComplete() {
|
|
864
|
+
return this.pending === null && this.buf.length === 0;
|
|
865
|
+
}
|
|
866
|
+
drain() {
|
|
867
|
+
for (; ; ) {
|
|
868
|
+
if (this.pending) {
|
|
869
|
+
const needed = this.pending.size + 1;
|
|
870
|
+
if (this.buf.length < needed) return;
|
|
871
|
+
const body = Buffer.from(this.buf.subarray(0, this.pending.size));
|
|
872
|
+
this.objects.push({ sha: this.pending.sha, type: this.pending.type, body });
|
|
873
|
+
this.buf = this.buf.subarray(needed);
|
|
874
|
+
this.pending = null;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const nl = this.buf.indexOf(10);
|
|
878
|
+
if (nl === -1) return;
|
|
879
|
+
const header = this.buf.subarray(0, nl).toString("utf-8");
|
|
880
|
+
this.buf = this.buf.subarray(nl + 1);
|
|
881
|
+
const [name, second, third] = header.split(" ");
|
|
882
|
+
if (name && second === "missing" && third === void 0) {
|
|
883
|
+
this.missing.push(name);
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (name && second && third !== void 0 && OBJECT_TYPES.has(second)) {
|
|
887
|
+
const size = Number.parseInt(third, 10);
|
|
888
|
+
if (Number.isSafeInteger(size) && size >= 0) {
|
|
889
|
+
this.pending = { sha: name, type: second, size };
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
864
893
|
throw new GitError(
|
|
865
|
-
`
|
|
894
|
+
`unexpected cat-file --batch header: ${JSON.stringify(header)}`,
|
|
866
895
|
void 0,
|
|
867
896
|
""
|
|
868
897
|
);
|
|
869
898
|
}
|
|
870
|
-
return sha;
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
var NonFastForwardError = class extends Error {
|
|
874
|
-
constructor(refs) {
|
|
875
|
-
super(
|
|
876
|
-
`non-fast-forward update rejected for ${refs.map((r) => r.refname).join(", ")} \u2014 re-run with force to overwrite the remote ref(s)`
|
|
877
|
-
);
|
|
878
|
-
this.refs = refs;
|
|
879
|
-
this.name = "NonFastForwardError";
|
|
880
899
|
}
|
|
881
|
-
refs;
|
|
882
900
|
};
|
|
883
|
-
var
|
|
884
|
-
constructor(
|
|
885
|
-
|
|
886
|
-
`${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` + objects.map((o) => `${o.path ?? o.sha} (${o.size} bytes)`).join(", ")
|
|
887
|
-
);
|
|
888
|
-
this.objects = objects;
|
|
889
|
-
this.name = "OversizeObjectsError";
|
|
901
|
+
var GitRepoReader = class {
|
|
902
|
+
constructor(repoPath) {
|
|
903
|
+
this.repoPath = repoPath;
|
|
890
904
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
async
|
|
894
|
-
const { repoReader, remoteState, feeRates, repoId, force = false } = options;
|
|
895
|
-
const resolveMissing = options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);
|
|
896
|
-
const { head, refs: localRefs } = await repoReader.listRefs();
|
|
897
|
-
const localByName = new Map(localRefs.map((r) => [r.refname, r]));
|
|
898
|
-
let selected;
|
|
899
|
-
if (options.refs !== void 0) {
|
|
900
|
-
selected = options.refs.map((name) => {
|
|
901
|
-
const ref = localByName.get(name);
|
|
902
|
-
if (!ref) {
|
|
903
|
-
throw new Error(
|
|
904
|
-
`ref ${JSON.stringify(name)} does not exist locally (ref deletion is out of scope in v1)`
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
return ref;
|
|
908
|
-
});
|
|
909
|
-
} else {
|
|
910
|
-
selected = localRefs;
|
|
911
|
-
}
|
|
912
|
-
const refUpdates = [];
|
|
913
|
-
const rejected = [];
|
|
914
|
-
for (const ref of selected) {
|
|
915
|
-
const remoteSha = remoteState.refs.get(ref.refname) ?? null;
|
|
916
|
-
if (remoteSha === null) {
|
|
917
|
-
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "new" });
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
if (remoteSha === ref.sha) {
|
|
921
|
-
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "up-to-date" });
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
924
|
-
let fastForward = false;
|
|
905
|
+
repoPath;
|
|
906
|
+
/** Run git with argument-array safety; resolves stdout as UTF-8. */
|
|
907
|
+
async git(args, opts = {}) {
|
|
925
908
|
try {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
if (fastForward) {
|
|
932
|
-
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "fast-forward" });
|
|
933
|
-
} else if (force) {
|
|
934
|
-
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "forced" });
|
|
935
|
-
} else {
|
|
936
|
-
rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
if (rejected.length > 0) throw new NonFastForwardError(rejected);
|
|
940
|
-
const updates = refUpdates.filter((u) => u.kind !== "up-to-date");
|
|
941
|
-
const wantTips = [...new Set(updates.map((u) => u.localSha))];
|
|
942
|
-
const haveTips = [...new Set(remoteState.refs.values())];
|
|
943
|
-
const delta = wantTips.length > 0 ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips) : [];
|
|
944
|
-
const knownShaToTxId = new Map(remoteState.shaToTxId);
|
|
945
|
-
let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));
|
|
946
|
-
if (candidates.length > 0) {
|
|
947
|
-
const resolved = await resolveMissing(candidates.map((o) => o.sha));
|
|
948
|
-
for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);
|
|
949
|
-
candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));
|
|
950
|
-
}
|
|
951
|
-
const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));
|
|
952
|
-
const { objects: stats, missing } = await repoReader.statObjects(
|
|
953
|
-
candidates.map((c) => c.sha)
|
|
954
|
-
);
|
|
955
|
-
if (missing.length > 0) {
|
|
956
|
-
throw new Error(
|
|
957
|
-
`objects vanished from the local repository during planning: ${missing.join(", ")}`
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
const oversize = [];
|
|
961
|
-
for (const stat2 of stats) {
|
|
962
|
-
if (stat2.size > MAX_OBJECT_SIZE) {
|
|
963
|
-
const path = pathBySha.get(stat2.sha);
|
|
964
|
-
oversize.push({ ...stat2, ...path ? { path } : {} });
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
if (oversize.length > 0) throw new OversizeObjectsError(oversize);
|
|
968
|
-
const tipShas = new Set(updates.map((u) => u.localSha));
|
|
969
|
-
const planned = stats.map((stat2) => {
|
|
970
|
-
const path = pathBySha.get(stat2.sha);
|
|
971
|
-
return { ...stat2, ...path ? { path } : {}, isRefTip: tipShas.has(stat2.sha) };
|
|
972
|
-
});
|
|
973
|
-
const objects = [
|
|
974
|
-
...planned.filter((o) => !o.isRefTip),
|
|
975
|
-
...planned.filter((o) => o.isRefTip)
|
|
976
|
-
];
|
|
977
|
-
const newRefsMap = new Map(remoteState.refs);
|
|
978
|
-
for (const update of updates) newRefsMap.set(update.refname, update.localSha);
|
|
979
|
-
const headSymref = head && newRefsMap.has(head) ? head : remoteState.headSymref && newRefsMap.has(remoteState.headSymref) ? remoteState.headSymref : [...newRefsMap.keys()][0] ?? null;
|
|
980
|
-
const newRefs = {};
|
|
981
|
-
const headSha = headSymref ? newRefsMap.get(headSymref) : void 0;
|
|
982
|
-
if (headSymref && headSha) newRefs[headSymref] = headSha;
|
|
983
|
-
for (const [refname, sha] of newRefsMap) {
|
|
984
|
-
if (refname !== headSymref) newRefs[refname] = sha;
|
|
985
|
-
}
|
|
986
|
-
const announceNeeded = !remoteState.announced;
|
|
987
|
-
const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);
|
|
988
|
-
const uploadFee = BigInt(totalObjectBytes) * feeRates.uploadFeePerByte;
|
|
989
|
-
const eventCount = 1 + (announceNeeded ? 1 : 0);
|
|
990
|
-
const eventFees = BigInt(eventCount) * feeRates.eventFee;
|
|
991
|
-
return {
|
|
992
|
-
repoId,
|
|
993
|
-
refUpdates,
|
|
994
|
-
newRefs,
|
|
995
|
-
headSymref,
|
|
996
|
-
objects,
|
|
997
|
-
knownShaToTxId,
|
|
998
|
-
announceNeeded,
|
|
999
|
-
announcement: {
|
|
1000
|
-
name: options.announcement?.name ?? repoId,
|
|
1001
|
-
description: options.announcement?.description ?? ""
|
|
1002
|
-
},
|
|
1003
|
-
estimate: {
|
|
1004
|
-
objectCount: objects.length,
|
|
1005
|
-
totalObjectBytes,
|
|
1006
|
-
uploadFee,
|
|
1007
|
-
eventCount,
|
|
1008
|
-
eventFees,
|
|
1009
|
-
totalFee: uploadFee + eventFees
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
var READ_BATCH_SIZE = 100;
|
|
1014
|
-
async function executePush(options) {
|
|
1015
|
-
const { plan, publisher, remoteState, repoReader, relayUrls } = options;
|
|
1016
|
-
const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);
|
|
1017
|
-
const resultBySha = /* @__PURE__ */ new Map();
|
|
1018
|
-
let totalFeePaid = 0n;
|
|
1019
|
-
const pending = [];
|
|
1020
|
-
for (const object of plan.objects) {
|
|
1021
|
-
const knownTxId = merged.get(object.sha);
|
|
1022
|
-
if (knownTxId !== void 0) {
|
|
1023
|
-
resultBySha.set(object.sha, {
|
|
1024
|
-
sha: object.sha,
|
|
1025
|
-
txId: knownTxId,
|
|
1026
|
-
feePaid: 0n,
|
|
1027
|
-
skipped: true
|
|
909
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
910
|
+
cwd: this.repoPath,
|
|
911
|
+
maxBuffer: MAX_BUFFER,
|
|
912
|
+
encoding: "utf-8"
|
|
1028
913
|
});
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
`objects vanished from the local repository during push: ${missing.join(", ")}`
|
|
914
|
+
return { stdout, exitCode: 0 };
|
|
915
|
+
} catch (err) {
|
|
916
|
+
const e = err;
|
|
917
|
+
const exitCode = typeof e.code === "number" ? e.code : void 0;
|
|
918
|
+
if (exitCode !== void 0 && opts.allowExitCodes?.includes(exitCode)) {
|
|
919
|
+
return { stdout: e.stdout ?? "", exitCode };
|
|
920
|
+
}
|
|
921
|
+
throw new GitError(
|
|
922
|
+
`git ${args[0]} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`,
|
|
923
|
+
exitCode,
|
|
924
|
+
(e.stderr ?? "").trim()
|
|
1041
925
|
);
|
|
1042
926
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* List all branches and tags plus the symbolic HEAD.
|
|
930
|
+
*
|
|
931
|
+
* Annotated tags report the tag object's SHA/type with the peeled target
|
|
932
|
+
* in `peeledSha`. A detached HEAD is tolerated (`head` is `undefined`).
|
|
933
|
+
*/
|
|
934
|
+
async listRefs() {
|
|
935
|
+
const format = "%(refname)%00%(objectname)%00%(objecttype)%00%(*objectname)";
|
|
936
|
+
const [refsRes, headRes] = await Promise.all([
|
|
937
|
+
this.git(["for-each-ref", `--format=${format}`, "refs/heads", "refs/tags"]),
|
|
938
|
+
// Exit 1 = detached HEAD (or unborn branch pointer oddities) — tolerated.
|
|
939
|
+
this.git(["symbolic-ref", "--quiet", "HEAD"], { allowExitCodes: [1] })
|
|
940
|
+
]);
|
|
941
|
+
const refs = [];
|
|
942
|
+
for (const line of refsRes.stdout.split("\n")) {
|
|
943
|
+
if (!line) continue;
|
|
944
|
+
const [refname, sha, objecttype, peeled] = line.split("\0");
|
|
945
|
+
if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {
|
|
946
|
+
throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, void 0, "");
|
|
1050
947
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
});
|
|
1057
|
-
merged.set(object.sha, receipt.txId);
|
|
1058
|
-
totalFeePaid += receipt.feePaid;
|
|
1059
|
-
resultBySha.set(object.sha, {
|
|
1060
|
-
sha: object.sha,
|
|
1061
|
-
txId: receipt.txId,
|
|
1062
|
-
feePaid: receipt.feePaid,
|
|
1063
|
-
skipped: false
|
|
948
|
+
refs.push({
|
|
949
|
+
refname,
|
|
950
|
+
sha,
|
|
951
|
+
type: objecttype,
|
|
952
|
+
...peeled ? { peeledSha: peeled } : {}
|
|
1064
953
|
});
|
|
1065
954
|
}
|
|
955
|
+
const head = headRes.exitCode === 0 ? headRes.stdout.trim() || void 0 : void 0;
|
|
956
|
+
return { head, refs };
|
|
1066
957
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
958
|
+
/**
|
|
959
|
+
* SHAs of every object reachable from `want` but not from `have`
|
|
960
|
+
* (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.
|
|
961
|
+
*
|
|
962
|
+
* Haves that don't exist locally (e.g. remote tips we never fetched) are
|
|
963
|
+
* filtered out first via one `cat-file --batch-check` pass — rev-list
|
|
964
|
+
* would otherwise die on them.
|
|
965
|
+
*/
|
|
966
|
+
async objectsBetween(want, have) {
|
|
967
|
+
const objects = await this.objectsBetweenWithPaths(want, have);
|
|
968
|
+
return objects.map((o) => o.sha);
|
|
1076
969
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
var ARWEAVE_GRAPHQL_URL = "https://arweave.net/graphql";
|
|
1104
|
-
var SHA_CACHE_MAX_SIZE = 1e4;
|
|
1105
|
-
var shaToTxIdCache = /* @__PURE__ */ new Map();
|
|
1106
|
-
function isValidGitSha(sha) {
|
|
1107
|
-
return /^[0-9a-f]{40}$/i.test(sha);
|
|
1108
|
-
}
|
|
1109
|
-
function sanitizeGraphQLValue(value) {
|
|
1110
|
-
return value.replace(/["\\\n\r\u0000-\u001f`]/g, "");
|
|
1111
|
-
}
|
|
1112
|
-
function shaCacheKey(sha, repo) {
|
|
1113
|
-
return `${sha}:${repo}`;
|
|
1114
|
-
}
|
|
1115
|
-
function seedShaCache(mappings) {
|
|
1116
|
-
const entries = mappings instanceof Map ? mappings.entries() : mappings;
|
|
1117
|
-
for (const [key2, txId] of entries) {
|
|
1118
|
-
if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
|
|
1119
|
-
const firstKey = shaToTxIdCache.keys().next().value;
|
|
1120
|
-
if (firstKey !== void 0) {
|
|
1121
|
-
shaToTxIdCache.delete(firstKey);
|
|
970
|
+
/**
|
|
971
|
+
* Like {@link objectsBetween} but keeps the path each object was reached
|
|
972
|
+
* by (`rev-list --objects` emits `<sha> <path>` for blobs and non-root
|
|
973
|
+
* trees) — used by push planning to report actionable oversize errors.
|
|
974
|
+
*/
|
|
975
|
+
async objectsBetweenWithPaths(want, have) {
|
|
976
|
+
for (const w of want) assertRevision(w, "want");
|
|
977
|
+
for (const h of have) assertRevision(h, "have");
|
|
978
|
+
if (want.length === 0) return [];
|
|
979
|
+
const knownHaves = await this.filterExisting(have);
|
|
980
|
+
const args = ["rev-list", "--objects", ...want];
|
|
981
|
+
if (knownHaves.length > 0) args.push("--not", ...knownHaves);
|
|
982
|
+
args.push("--");
|
|
983
|
+
const { stdout } = await this.git(args);
|
|
984
|
+
const objects = [];
|
|
985
|
+
for (const line of stdout.split("\n")) {
|
|
986
|
+
if (!line) continue;
|
|
987
|
+
const spaceIdx = line.indexOf(" ");
|
|
988
|
+
if (spaceIdx === -1) {
|
|
989
|
+
objects.push({ sha: line });
|
|
990
|
+
} else {
|
|
991
|
+
const path = line.slice(spaceIdx + 1);
|
|
992
|
+
objects.push({
|
|
993
|
+
sha: line.slice(0, spaceIdx),
|
|
994
|
+
...path ? { path } : {}
|
|
995
|
+
});
|
|
1122
996
|
}
|
|
1123
997
|
}
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
var ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;
|
|
1128
|
-
function isValidArweaveTxId(txId) {
|
|
1129
|
-
return ARWEAVE_TX_ID_RE.test(txId);
|
|
1130
|
-
}
|
|
1131
|
-
async function resolveGitSha(sha, repo) {
|
|
1132
|
-
if (!isValidGitSha(sha)) {
|
|
1133
|
-
return null;
|
|
1134
|
-
}
|
|
1135
|
-
const cacheKey = shaCacheKey(sha, repo);
|
|
1136
|
-
const cached = shaToTxIdCache.get(cacheKey);
|
|
1137
|
-
if (cached !== void 0) {
|
|
1138
|
-
return cached;
|
|
1139
|
-
}
|
|
1140
|
-
const safeSha = sanitizeGraphQLValue(sha);
|
|
1141
|
-
const safeRepo = sanitizeGraphQLValue(repo);
|
|
1142
|
-
const query = `query {
|
|
1143
|
-
transactions(tags: [
|
|
1144
|
-
{ name: "Git-SHA", values: ["${safeSha}"] },
|
|
1145
|
-
{ name: "Repo", values: ["${safeRepo}"] }
|
|
1146
|
-
]) {
|
|
1147
|
-
edges { node { id } }
|
|
998
|
+
return objects;
|
|
1148
999
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1000
|
+
/** Run git feeding `input` on stdin; resolves collected stdout bytes. */
|
|
1001
|
+
runWithStdin(args, input) {
|
|
1002
|
+
return new Promise((resolve2, reject) => {
|
|
1003
|
+
const child = spawn("git", args, {
|
|
1004
|
+
cwd: this.repoPath,
|
|
1005
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1006
|
+
});
|
|
1007
|
+
const out = [];
|
|
1008
|
+
let stderr = "";
|
|
1009
|
+
child.stdout.on("data", (chunk) => out.push(chunk));
|
|
1010
|
+
child.stderr.on("data", (chunk) => {
|
|
1011
|
+
stderr += chunk.toString("utf-8");
|
|
1012
|
+
});
|
|
1013
|
+
child.on("error", (err) => {
|
|
1014
|
+
reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, void 0, ""));
|
|
1015
|
+
});
|
|
1016
|
+
child.on("close", (code) => {
|
|
1017
|
+
if (code !== 0) {
|
|
1018
|
+
return reject(
|
|
1019
|
+
new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
resolve2(Buffer.concat(out));
|
|
1023
|
+
});
|
|
1024
|
+
child.stdin.on("error", () => {
|
|
1025
|
+
});
|
|
1026
|
+
child.stdin.write(input);
|
|
1027
|
+
child.stdin.end();
|
|
1156
1028
|
});
|
|
1157
|
-
if (!response.ok) {
|
|
1158
|
-
return null;
|
|
1159
|
-
}
|
|
1160
|
-
const json = await response.json();
|
|
1161
|
-
const edges = json.data?.transactions?.edges;
|
|
1162
|
-
if (!edges || edges.length === 0) {
|
|
1163
|
-
return null;
|
|
1164
|
-
}
|
|
1165
|
-
const txId = edges[0]?.node?.id;
|
|
1166
|
-
if (!txId || !isValidArweaveTxId(txId)) {
|
|
1167
|
-
return null;
|
|
1168
|
-
}
|
|
1169
|
-
if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
|
|
1170
|
-
const firstKey = shaToTxIdCache.keys().next().value;
|
|
1171
|
-
if (firstKey !== void 0) {
|
|
1172
|
-
shaToTxIdCache.delete(firstKey);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
shaToTxIdCache.set(cacheKey, txId);
|
|
1176
|
-
return txId;
|
|
1177
|
-
} catch {
|
|
1178
|
-
return null;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
function isValidRelayUrl(url) {
|
|
1182
|
-
return /^wss?:\/\//i.test(url);
|
|
1183
|
-
}
|
|
1184
|
-
var WS_OPEN = 1;
|
|
1185
|
-
function defaultWebSocketFactory2(url) {
|
|
1186
|
-
const ctor = globalThis.WebSocket;
|
|
1187
|
-
if (!ctor) {
|
|
1188
|
-
throw new Error(
|
|
1189
|
-
"No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
return new ctor(url);
|
|
1193
|
-
}
|
|
1194
|
-
function decodeEventPayload(payload) {
|
|
1195
|
-
if (payload !== null && typeof payload === "object") {
|
|
1196
|
-
return payload;
|
|
1197
1029
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1030
|
+
/** Of the given revisions, keep only those resolvable locally. */
|
|
1031
|
+
async filterExisting(revs) {
|
|
1032
|
+
if (revs.length === 0) return [];
|
|
1033
|
+
const { missing } = await this.batchCheck(revs);
|
|
1034
|
+
const missingSet = new Set(missing);
|
|
1035
|
+
return revs.filter((r) => !missingSet.has(r));
|
|
1200
1036
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1037
|
+
/** One `cat-file --batch-check` pass; returns names reported missing. */
|
|
1038
|
+
async batchCheck(names) {
|
|
1039
|
+
const stdout = await this.runWithStdin(
|
|
1040
|
+
["cat-file", "--batch-check"],
|
|
1041
|
+
names.join("\n") + "\n"
|
|
1042
|
+
);
|
|
1043
|
+
const missing = [];
|
|
1044
|
+
for (const line of stdout.toString("utf-8").split("\n")) {
|
|
1045
|
+
if (line.endsWith(" missing")) missing.push(line.slice(0, -" missing".length));
|
|
1205
1046
|
}
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1208
|
-
try {
|
|
1209
|
-
return decodeToon(payload);
|
|
1210
|
-
} catch {
|
|
1211
|
-
return null;
|
|
1047
|
+
return { missing };
|
|
1212
1048
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1049
|
+
/**
|
|
1050
|
+
* Object metadata (type + body size) for a batch of SHAs via one
|
|
1051
|
+
* `cat-file --batch-check` pass — no bodies are read. Missing objects are
|
|
1052
|
+
* reported, not thrown.
|
|
1053
|
+
*/
|
|
1054
|
+
async statObjects(shas) {
|
|
1055
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
1056
|
+
if (shas.length === 0) return { objects: [], missing: [] };
|
|
1057
|
+
const stdout = await this.runWithStdin(
|
|
1058
|
+
["cat-file", "--batch-check"],
|
|
1059
|
+
shas.join("\n") + "\n"
|
|
1060
|
+
);
|
|
1061
|
+
const objects = [];
|
|
1062
|
+
const missing = [];
|
|
1063
|
+
for (const line of stdout.toString("utf-8").split("\n")) {
|
|
1064
|
+
if (!line) continue;
|
|
1065
|
+
if (line.endsWith(" missing")) {
|
|
1066
|
+
missing.push(line.slice(0, -" missing".length));
|
|
1067
|
+
continue;
|
|
1231
1068
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1069
|
+
const [sha, type, sizeStr] = line.split(" ");
|
|
1070
|
+
const size = Number.parseInt(sizeStr ?? "", 10);
|
|
1071
|
+
if (!sha || !type || !OBJECT_TYPES.has(type) || !Number.isSafeInteger(size) || size < 0) {
|
|
1072
|
+
throw new GitError(
|
|
1073
|
+
`unexpected cat-file --batch-check line: ${JSON.stringify(line)}`,
|
|
1074
|
+
void 0,
|
|
1075
|
+
""
|
|
1076
|
+
);
|
|
1236
1077
|
}
|
|
1237
|
-
|
|
1238
|
-
if (!isValidRelayUrl(relayUrl)) {
|
|
1239
|
-
reject(
|
|
1240
|
-
new Error(
|
|
1241
|
-
`Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`
|
|
1242
|
-
)
|
|
1243
|
-
);
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
try {
|
|
1247
|
-
ws = webSocketFactory(relayUrl);
|
|
1248
|
-
} catch (err) {
|
|
1249
|
-
reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));
|
|
1250
|
-
return;
|
|
1078
|
+
objects.push({ sha, type, size });
|
|
1251
1079
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1080
|
+
return { objects, missing };
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Read raw object bodies via a single streaming `git cat-file --batch`
|
|
1084
|
+
* child process. Bodies may be binary and are parsed size-driven across
|
|
1085
|
+
* chunk boundaries. Missing objects are reported, not thrown.
|
|
1086
|
+
*/
|
|
1087
|
+
async readObjects(shas) {
|
|
1088
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
1089
|
+
if (shas.length === 0) return { objects: [], missing: [] };
|
|
1090
|
+
const parser = new BatchParser();
|
|
1091
|
+
await new Promise((resolve2, reject) => {
|
|
1092
|
+
const child = spawn("git", ["cat-file", "--batch"], {
|
|
1093
|
+
cwd: this.repoPath,
|
|
1094
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1095
|
+
});
|
|
1096
|
+
let stderr = "";
|
|
1097
|
+
let parseError = null;
|
|
1098
|
+
child.stderr.on("data", (chunk) => {
|
|
1099
|
+
stderr += chunk.toString("utf-8");
|
|
1100
|
+
});
|
|
1101
|
+
child.stdout.on("data", (chunk) => {
|
|
1102
|
+
if (parseError) return;
|
|
1103
|
+
try {
|
|
1104
|
+
parser.push(chunk);
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
parseError = err;
|
|
1107
|
+
child.kill();
|
|
1268
1108
|
}
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1109
|
+
});
|
|
1110
|
+
child.on("error", (err) => {
|
|
1111
|
+
reject(new GitError(`failed to spawn git cat-file: ${err.message}`, void 0, ""));
|
|
1112
|
+
});
|
|
1113
|
+
child.on("close", (code) => {
|
|
1114
|
+
if (parseError) return reject(parseError);
|
|
1115
|
+
if (code !== 0) {
|
|
1116
|
+
return reject(
|
|
1117
|
+
new GitError(`git cat-file --batch failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
if (!parser.isComplete()) {
|
|
1121
|
+
return reject(
|
|
1122
|
+
new GitError("git cat-file --batch output ended mid-record", code ?? void 0, stderr.trim())
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
resolve2();
|
|
1126
|
+
});
|
|
1127
|
+
child.stdin.on("error", () => {
|
|
1128
|
+
});
|
|
1129
|
+
child.stdin.write(shas.join("\n") + "\n");
|
|
1130
|
+
child.stdin.end();
|
|
1281
1131
|
});
|
|
1282
|
-
|
|
1283
|
-
}
|
|
1284
|
-
function latestReplaceable(events) {
|
|
1285
|
-
let winner = null;
|
|
1286
|
-
for (const event of events) {
|
|
1287
|
-
if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
|
|
1288
|
-
winner = event;
|
|
1289
|
-
}
|
|
1132
|
+
return { objects: parser.objects, missing: parser.missing };
|
|
1290
1133
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1134
|
+
/**
|
|
1135
|
+
* `git merge-base --is-ancestor <a> <b>` — true when `a` is an ancestor
|
|
1136
|
+
* of `b` (fast-forward check / force detection). Exit codes other than
|
|
1137
|
+
* 0/1 (e.g. unknown revisions) throw.
|
|
1138
|
+
*/
|
|
1139
|
+
async isAncestor(a, b) {
|
|
1140
|
+
assertRevision(a, "ancestor candidate");
|
|
1141
|
+
assertRevision(b, "descendant candidate");
|
|
1142
|
+
const { exitCode } = await this.git(
|
|
1143
|
+
["merge-base", "--is-ancestor", a, b],
|
|
1144
|
+
{ allowExitCodes: [1] }
|
|
1145
|
+
);
|
|
1146
|
+
return exitCode === 0;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* `git format-patch --stdout <range>` — the full mbox-formatted patch
|
|
1150
|
+
* series text (empty string when the range selects no commits).
|
|
1151
|
+
*/
|
|
1152
|
+
async formatPatch(range) {
|
|
1153
|
+
assertRange(range, "range");
|
|
1154
|
+
const { stdout } = await this.git(["format-patch", "--stdout", range, "--"]);
|
|
1155
|
+
return stdout;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Parent SHAs for a batch of commit SHAs via one
|
|
1159
|
+
* `git rev-list --no-walk=unsorted --parents` pass. Root commits map to an
|
|
1160
|
+
* empty array. Used to derive the kind:1617 `commit`/`parent-commit` tag
|
|
1161
|
+
* pairs for exactly the commits a format-patch series carries.
|
|
1162
|
+
*/
|
|
1163
|
+
async commitParents(shas) {
|
|
1164
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
1165
|
+
if (shas.length === 0) return /* @__PURE__ */ new Map();
|
|
1166
|
+
const { stdout } = await this.git([
|
|
1167
|
+
"rev-list",
|
|
1168
|
+
"--no-walk=unsorted",
|
|
1169
|
+
"--parents",
|
|
1170
|
+
...shas,
|
|
1171
|
+
"--"
|
|
1172
|
+
]);
|
|
1173
|
+
const parents = /* @__PURE__ */ new Map();
|
|
1174
|
+
for (const line of stdout.split("\n")) {
|
|
1175
|
+
if (!line) continue;
|
|
1176
|
+
const [sha, ...rest] = line.split(" ");
|
|
1177
|
+
if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {
|
|
1178
|
+
throw new GitError(
|
|
1179
|
+
`unexpected rev-list --parents line: ${JSON.stringify(line)}`,
|
|
1180
|
+
void 0,
|
|
1181
|
+
""
|
|
1182
|
+
);
|
|
1309
1183
|
}
|
|
1310
|
-
|
|
1311
|
-
refs.set(v1, v2);
|
|
1312
|
-
} else if (tagName === "HEAD" && v1?.startsWith(SYMREF_PREFIX)) {
|
|
1313
|
-
headSymref = v1.slice(SYMREF_PREFIX.length);
|
|
1314
|
-
} else if (tagName === "arweave" && v1 && v2) {
|
|
1315
|
-
shaToTxId.set(v1, v2);
|
|
1184
|
+
parents.set(sha, rest);
|
|
1316
1185
|
}
|
|
1186
|
+
return parents;
|
|
1317
1187
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1188
|
+
/**
|
|
1189
|
+
* Resolve a ref/revision to a full SHA via `git rev-parse --verify`.
|
|
1190
|
+
* Throws {@link GitError} when the name doesn't resolve.
|
|
1191
|
+
*/
|
|
1192
|
+
async resolveRef(name) {
|
|
1193
|
+
assertRevision(name, "ref name");
|
|
1194
|
+
const { stdout } = await this.git(["rev-parse", "--verify", "--quiet", name]);
|
|
1195
|
+
const sha = stdout.trim();
|
|
1196
|
+
if (!FULL_SHA_RE.test(sha)) {
|
|
1197
|
+
throw new GitError(
|
|
1198
|
+
`rev-parse --verify returned unexpected output for ${JSON.stringify(name)}: ${JSON.stringify(sha)}`,
|
|
1199
|
+
void 0,
|
|
1200
|
+
""
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
return sha;
|
|
1331
1204
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1205
|
+
};
|
|
1206
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1207
|
+
var NonFastForwardError = class extends Error {
|
|
1208
|
+
constructor(refs) {
|
|
1209
|
+
super(
|
|
1210
|
+
`non-fast-forward update rejected for ${refs.map((r) => r.refname).join(", ")} \u2014 re-run with force to overwrite the remote ref(s)`
|
|
1211
|
+
);
|
|
1212
|
+
this.refs = refs;
|
|
1213
|
+
this.name = "NonFastForwardError";
|
|
1334
1214
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1215
|
+
refs;
|
|
1216
|
+
};
|
|
1217
|
+
var OversizeObjectsError = class extends Error {
|
|
1218
|
+
constructor(objects) {
|
|
1219
|
+
super(
|
|
1220
|
+
`${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` + objects.map((o) => `${o.path ?? o.sha} (${o.size} bytes)`).join(", ")
|
|
1221
|
+
);
|
|
1222
|
+
this.objects = objects;
|
|
1223
|
+
this.name = "OversizeObjectsError";
|
|
1224
|
+
}
|
|
1225
|
+
objects;
|
|
1226
|
+
};
|
|
1227
|
+
async function planPush(options) {
|
|
1228
|
+
const { repoReader, remoteState, feeRates, repoId, force = false } = options;
|
|
1229
|
+
const resolveMissing = options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);
|
|
1230
|
+
const { head, refs: localRefs } = await repoReader.listRefs();
|
|
1231
|
+
const localByName = new Map(localRefs.map((r) => [r.refname, r]));
|
|
1232
|
+
let selected;
|
|
1233
|
+
if (options.refs !== void 0) {
|
|
1234
|
+
selected = options.refs.map((name) => {
|
|
1235
|
+
const ref = localByName.get(name);
|
|
1236
|
+
if (!ref) {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`ref ${JSON.stringify(name)} does not exist locally (ref deletion is out of scope in v1)`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
return ref;
|
|
1242
|
+
});
|
|
1243
|
+
} else {
|
|
1244
|
+
selected = localRefs;
|
|
1337
1245
|
}
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))
|
|
1345
|
-
);
|
|
1346
|
-
const failures = [];
|
|
1347
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1348
|
-
for (const result of results) {
|
|
1349
|
-
if (result.status === "rejected") {
|
|
1350
|
-
failures.push(String(result.reason?.message ?? result.reason));
|
|
1246
|
+
const refUpdates = [];
|
|
1247
|
+
const rejected = [];
|
|
1248
|
+
for (const ref of selected) {
|
|
1249
|
+
const remoteSha = remoteState.refs.get(ref.refname) ?? null;
|
|
1250
|
+
if (remoteSha === null) {
|
|
1251
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "new" });
|
|
1351
1252
|
continue;
|
|
1352
1253
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1254
|
+
if (remoteSha === ref.sha) {
|
|
1255
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "up-to-date" });
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
let fastForward = false;
|
|
1259
|
+
try {
|
|
1260
|
+
fastForward = await repoReader.isAncestor(remoteSha, ref.sha);
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
if (!(err instanceof GitError)) throw err;
|
|
1263
|
+
fastForward = false;
|
|
1264
|
+
}
|
|
1265
|
+
if (fastForward) {
|
|
1266
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "fast-forward" });
|
|
1267
|
+
} else if (force) {
|
|
1268
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "forced" });
|
|
1269
|
+
} else {
|
|
1270
|
+
rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });
|
|
1359
1271
|
}
|
|
1360
1272
|
}
|
|
1361
|
-
if (
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1273
|
+
if (rejected.length > 0) throw new NonFastForwardError(rejected);
|
|
1274
|
+
const updates = refUpdates.filter((u) => u.kind !== "up-to-date");
|
|
1275
|
+
const wantTips = [...new Set(updates.map((u) => u.localSha))];
|
|
1276
|
+
const haveTips = [...new Set(remoteState.refs.values())];
|
|
1277
|
+
const delta = wantTips.length > 0 ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips) : [];
|
|
1278
|
+
const knownShaToTxId = new Map(remoteState.shaToTxId);
|
|
1279
|
+
let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));
|
|
1280
|
+
if (candidates.length > 0) {
|
|
1281
|
+
const resolved = await resolveMissing(candidates.map((o) => o.sha));
|
|
1282
|
+
for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);
|
|
1283
|
+
candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));
|
|
1365
1284
|
}
|
|
1366
|
-
const
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
);
|
|
1370
|
-
const announceEvent = latestReplaceable(
|
|
1371
|
-
events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND2)
|
|
1285
|
+
const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));
|
|
1286
|
+
const { objects: stats, missing } = await repoReader.statObjects(
|
|
1287
|
+
candidates.map((c) => c.sha)
|
|
1372
1288
|
);
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
shaToTxId: /* @__PURE__ */ new Map()
|
|
1377
|
-
};
|
|
1378
|
-
if (shaToTxId.size > 0) {
|
|
1379
|
-
seedShaCache(
|
|
1380
|
-
[...shaToTxId].map(
|
|
1381
|
-
([sha, txId]) => [shaCacheKey(sha, repoId), txId]
|
|
1382
|
-
)
|
|
1289
|
+
if (missing.length > 0) {
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
`objects vanished from the local repository during planning: ${missing.join(", ")}`
|
|
1383
1292
|
);
|
|
1384
1293
|
}
|
|
1385
|
-
const
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
if (tag[0] === "relays") {
|
|
1391
|
-
relays.push(...tag.slice(1).filter((url) => url.length > 0));
|
|
1392
|
-
}
|
|
1294
|
+
const oversize = [];
|
|
1295
|
+
for (const stat2 of stats) {
|
|
1296
|
+
if (stat2.size > MAX_OBJECT_SIZE) {
|
|
1297
|
+
const path = pathBySha.get(stat2.sha);
|
|
1298
|
+
oversize.push({ ...stat2, ...path ? { path } : {} });
|
|
1393
1299
|
}
|
|
1394
1300
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1301
|
+
if (oversize.length > 0) throw new OversizeObjectsError(oversize);
|
|
1302
|
+
const tipShas = new Set(updates.map((u) => u.localSha));
|
|
1303
|
+
const planned = stats.map((stat2) => {
|
|
1304
|
+
const path = pathBySha.get(stat2.sha);
|
|
1305
|
+
return { ...stat2, ...path ? { path } : {}, isRefTip: tipShas.has(stat2.sha) };
|
|
1306
|
+
});
|
|
1307
|
+
const objects = [
|
|
1308
|
+
...planned.filter((o) => !o.isRefTip),
|
|
1309
|
+
...planned.filter((o) => o.isRefTip)
|
|
1310
|
+
];
|
|
1311
|
+
const newRefsMap = new Map(remoteState.refs);
|
|
1312
|
+
for (const update of updates) newRefsMap.set(update.refname, update.localSha);
|
|
1313
|
+
const headSymref = head && newRefsMap.has(head) ? head : remoteState.headSymref && newRefsMap.has(remoteState.headSymref) ? remoteState.headSymref : [...newRefsMap.keys()][0] ?? null;
|
|
1314
|
+
const newRefs = {};
|
|
1315
|
+
const headSha = headSymref ? newRefsMap.get(headSymref) : void 0;
|
|
1316
|
+
if (headSymref && headSha) newRefs[headSymref] = headSha;
|
|
1317
|
+
for (const [refname, sha] of newRefsMap) {
|
|
1318
|
+
if (refname !== headSymref) newRefs[refname] = sha;
|
|
1319
|
+
}
|
|
1320
|
+
const announceNeeded = !remoteState.announced;
|
|
1321
|
+
const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);
|
|
1322
|
+
const uploadFee = BigInt(totalObjectBytes) * feeRates.uploadFeePerByte;
|
|
1323
|
+
const eventCount = 1 + (announceNeeded ? 1 : 0);
|
|
1324
|
+
const eventFees = BigInt(eventCount) * feeRates.eventFee;
|
|
1325
|
+
return {
|
|
1326
|
+
repoId,
|
|
1327
|
+
refUpdates,
|
|
1328
|
+
newRefs,
|
|
1329
|
+
headSymref,
|
|
1330
|
+
objects,
|
|
1331
|
+
knownShaToTxId,
|
|
1332
|
+
announceNeeded,
|
|
1333
|
+
announcement: {
|
|
1334
|
+
name: options.announcement?.name ?? repoId,
|
|
1335
|
+
description: options.announcement?.description ?? ""
|
|
1336
|
+
},
|
|
1337
|
+
estimate: {
|
|
1338
|
+
objectCount: objects.length,
|
|
1339
|
+
totalObjectBytes,
|
|
1340
|
+
uploadFee,
|
|
1341
|
+
eventCount,
|
|
1342
|
+
eventFees,
|
|
1343
|
+
totalFee: uploadFee + eventFees
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
var READ_BATCH_SIZE = 100;
|
|
1348
|
+
async function executePush(options) {
|
|
1349
|
+
const { plan, publisher, remoteState, repoReader, relayUrls } = options;
|
|
1350
|
+
const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);
|
|
1351
|
+
const resultBySha = /* @__PURE__ */ new Map();
|
|
1352
|
+
let totalFeePaid = 0n;
|
|
1353
|
+
const pending = [];
|
|
1354
|
+
for (const object of plan.objects) {
|
|
1355
|
+
const knownTxId = merged.get(object.sha);
|
|
1356
|
+
if (knownTxId !== void 0) {
|
|
1357
|
+
resultBySha.set(object.sha, {
|
|
1358
|
+
sha: object.sha,
|
|
1359
|
+
txId: knownTxId,
|
|
1360
|
+
feePaid: 0n,
|
|
1361
|
+
skipped: true
|
|
1362
|
+
});
|
|
1363
|
+
} else {
|
|
1364
|
+
pending.push(object);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
for (let i = 0; i < pending.length; i += READ_BATCH_SIZE) {
|
|
1368
|
+
const batch = pending.slice(i, i + READ_BATCH_SIZE);
|
|
1369
|
+
const { objects: read, missing } = await repoReader.readObjects(
|
|
1370
|
+
batch.map((o) => o.sha)
|
|
1371
|
+
);
|
|
1372
|
+
if (missing.length > 0) {
|
|
1373
|
+
throw new Error(
|
|
1374
|
+
`objects vanished from the local repository during push: ${missing.join(", ")}`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
const bodyBySha = new Map(read.map((r) => [r.sha, r.body]));
|
|
1378
|
+
for (const object of batch) {
|
|
1379
|
+
const body = bodyBySha.get(object.sha);
|
|
1380
|
+
if (!body) {
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
`internal: cat-file returned no body for ${object.sha}`
|
|
1383
|
+
);
|
|
1404
1384
|
}
|
|
1385
|
+
const receipt = await publisher.uploadGitObject({
|
|
1386
|
+
sha: object.sha,
|
|
1387
|
+
type: object.type,
|
|
1388
|
+
body,
|
|
1389
|
+
repoId: plan.repoId
|
|
1390
|
+
});
|
|
1391
|
+
merged.set(object.sha, receipt.txId);
|
|
1392
|
+
totalFeePaid += receipt.feePaid;
|
|
1393
|
+
resultBySha.set(object.sha, {
|
|
1394
|
+
sha: object.sha,
|
|
1395
|
+
txId: receipt.txId,
|
|
1396
|
+
feePaid: receipt.feePaid,
|
|
1397
|
+
skipped: false
|
|
1398
|
+
});
|
|
1405
1399
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1400
|
+
}
|
|
1401
|
+
let announceReceipt = null;
|
|
1402
|
+
if (plan.announceNeeded && !remoteState.announced) {
|
|
1403
|
+
const announceEvent = buildRepoAnnouncement(
|
|
1404
|
+
plan.repoId,
|
|
1405
|
+
plan.announcement.name,
|
|
1406
|
+
plan.announcement.description
|
|
1410
1407
|
);
|
|
1411
|
-
|
|
1412
|
-
|
|
1408
|
+
announceReceipt = await publisher.publishEvent(announceEvent, relayUrls);
|
|
1409
|
+
totalFeePaid += announceReceipt.feePaid;
|
|
1410
|
+
}
|
|
1411
|
+
const refsEvent = buildRepoRefs(
|
|
1412
|
+
plan.repoId,
|
|
1413
|
+
plan.newRefs,
|
|
1414
|
+
Object.fromEntries(merged)
|
|
1415
|
+
);
|
|
1416
|
+
const refsReceipt = await publisher.publishEvent(refsEvent, relayUrls);
|
|
1417
|
+
totalFeePaid += refsReceipt.feePaid;
|
|
1418
|
+
const uploads = plan.objects.map((o) => {
|
|
1419
|
+
const step = resultBySha.get(o.sha);
|
|
1420
|
+
if (!step) {
|
|
1421
|
+
throw new Error(`internal: no upload result recorded for ${o.sha}`);
|
|
1413
1422
|
}
|
|
1414
|
-
return
|
|
1415
|
-
};
|
|
1423
|
+
return step;
|
|
1424
|
+
});
|
|
1416
1425
|
return {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
announceEvent,
|
|
1423
|
-
name,
|
|
1424
|
-
description,
|
|
1425
|
-
relays,
|
|
1426
|
-
resolveMissing
|
|
1426
|
+
uploads,
|
|
1427
|
+
announceReceipt,
|
|
1428
|
+
refsReceipt,
|
|
1429
|
+
arweaveMap: merged,
|
|
1430
|
+
totalFeePaid
|
|
1427
1431
|
};
|
|
1428
1432
|
}
|
|
1429
1433
|
|
|
@@ -3471,7 +3475,7 @@ var ClientRunner = class {
|
|
|
3471
3475
|
}
|
|
3472
3476
|
// ── Git write path (/git/*, epic #222 ticket #227) ────────────────────────
|
|
3473
3477
|
/**
|
|
3474
|
-
* The daemon `Publisher` implementation (see @toon-protocol/
|
|
3478
|
+
* The daemon `Publisher` implementation (see @toon-protocol/rig) for one
|
|
3475
3479
|
* apex. Maps the interface onto the runner's production paid-write
|
|
3476
3480
|
* machinery:
|
|
3477
3481
|
*
|
|
@@ -3672,7 +3676,9 @@ var ClientRunner = class {
|
|
|
3672
3676
|
req.title,
|
|
3673
3677
|
req.commits ?? [],
|
|
3674
3678
|
req.branch,
|
|
3675
|
-
content
|
|
3679
|
+
content,
|
|
3680
|
+
// PR body → `description` tag; never the content (git am safety, #280).
|
|
3681
|
+
typeof req.description === "string" && req.description !== "" ? req.description : void 0
|
|
3676
3682
|
);
|
|
3677
3683
|
return this.gitPublishSigned(event);
|
|
3678
3684
|
}
|
|
@@ -4322,4 +4328,4 @@ export {
|
|
|
4322
4328
|
PublishRejectedError,
|
|
4323
4329
|
registerRoutes
|
|
4324
4330
|
};
|
|
4325
|
-
//# sourceMappingURL=chunk-
|
|
4331
|
+
//# sourceMappingURL=chunk-BINI3PEC.js.map
|