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";
import type { Fetch } from "#lib/fetch.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<CobRef> => {
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<KnotWrite>(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<CobRef> => {
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: Fetch
): Promise<void> => {
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: Fetch = globalThis.fetch
): Promise<void> =>
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: Fetch = globalThis.fetch
): Promise<void> => mirrorPush(agent, bobbin, knot, DELETE_BRANCH_NSID, input, fetch);