import { parseCanonicalResourceUri } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { mintServiceAuth, serviceDidForHost } from "#lib/auth/agent.ts"; import { attested, knotProcedure, type CobRef, type KnotWrite } from "#lib/api/cob.ts"; import { createTicket, eraseTicket, type TicketProse } from "#lib/api/ticket.ts"; import { didOf, nsidOf, type AtUri, type Did, type GitOid, type KnotOrigin, type Nsid } from "#lib/api/syntax.ts"; import { buildUrl, toResponseError } from "#lib/api/_request.ts"; export interface PatchRange { base: GitOid; head: GitOid; } export interface PatchSource { repo: Did; branch: string; } const SUBMIT_PATCH_NSID = nsidOf("org.tangled.review.submitPatch"); export const createPatch = async ( agent: OAuthUserAgent, knot: KnotOrigin, ticket: AtUri, { base, head }: PatchRange, source: PatchSource ): Promise => { const { repo, rkey } = parseCanonicalResourceUri(ticket); const patch = { ticket: rkey, base: { oid: base }, head: { oid: head }, sourceRepo: source.repo, sourceBranch: source.branch }; return attested( agent, knot, await knotProcedure(agent, knot, didOf(repo), SUBMIT_PATCH_NSID, { patch }) ); }; // if the knot refuses the patch, erase the ticket again so there's no pull without a diff export const openPull = async ( agent: OAuthUserAgent, knot: KnotOrigin, repo: Did, prose: TicketProse, targetBranch: string, range: PatchRange, source: PatchSource ): Promise => { const ticket = await createTicket(agent, knot, repo, { ...prose, targetBranch }); try { await createPatch(agent, knot, ticket.uri, range, source); } catch (cause) { await eraseTicket(agent, knot, ticket).catch(() => {}); throw cause; } return ticket; }; const MERGE_COMMIT_NSID = nsidOf("sh.tangled.git.mergeCommit"); const DELETE_BRANCH_NSID = nsidOf("org.tangled.temp.git.deleteBranch"); const PUSH_NSID = nsidOf("org.tangled.git.receivePack"); export interface MergeCommitInput { /** the repo and branch being merged into */ target: { repo: string; branch: string; commit: string }; /** the repo the head commit lives in, and the commit itself */ source: { repo: string; commit: string }; style: "rebase"; } const mirrorPush = async ( agent: OAuthUserAgent, bobbin: string, knot: string, nsid: Nsid, body: unknown, fetch: typeof globalThis.fetch ): Promise => { const token = await mintServiceAuth(agent, { aud: serviceDidForHost(knot), // knotmirror forwards this token to the knot's receive-pack lxm: PUSH_NSID }); const response = await fetch(buildUrl(bobbin, nsid), { method: "POST", headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(body) }); if (!response.ok) throw await toResponseError(response); }; export const mergeCommit = ( agent: OAuthUserAgent, bobbin: string, knot: string, input: MergeCommitInput, fetch: typeof globalThis.fetch = globalThis.fetch ): Promise => mirrorPush( agent, bobbin, knot, MERGE_COMMIT_NSID, { target: { $type: `${MERGE_COMMIT_NSID}#target`, ...input.target }, source: { $type: `${MERGE_COMMIT_NSID}#source`, ...input.source }, style: input.style }, fetch ); export const deleteBranch = ( agent: OAuthUserAgent, bobbin: string, knot: string, input: { repo: string; branch: string }, fetch: typeof globalThis.fetch = globalThis.fetch ): Promise => mirrorPush(agent, bobbin, knot, DELETE_BRANCH_NSID, input, fetch);