import { ok } from "@atcute/client"; import { mainSchema as getRecordSchema } from "@atcute/atproto/types/repo/getRecord"; import type { Nsid, RecordKey } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { now as tidNow } from "@atcute/tid"; import { createClient, mintServiceAuth, serviceDidForHost } from "$lib/auth/agent"; import { deleteRecord, putRecord } from "$lib/api/write"; import { buildUrl, toResponseError } from "$lib/api/_request"; import type { PullRecord, PullState, PullStatusRecord, RecordView } from "$lib/api/records"; const PULL_COLLECTION = "sh.tangled.repo.pull" as Nsid; const PULL_STATUS_COLLECTION = "sh.tangled.repo.pull.status" as Nsid; export const putPull = async ( agent: OAuthUserAgent, rkey: string, record: PullRecord ): Promise> => { const { uri, cid } = await putRecord(agent, PULL_COLLECTION, rkey, record); return { uri, cid, value: record }; }; /** read-modify-write against the live record */ const updatePull = async ( agent: OAuthUserAgent, rkey: string, mutate: (record: PullRecord) => PullRecord ): Promise> => { const rpc = createClient(agent); const existing = await ok( rpc.call(getRecordSchema, { params: { repo: agent.sub, collection: PULL_COLLECTION, rkey: rkey as RecordKey } }) ); const record = mutate(existing.value as PullRecord); const { uri, cid } = await putRecord(agent, PULL_COLLECTION, rkey, record, existing.cid); return { uri, cid, value: record }; }; export const editPull = ( agent: OAuthUserAgent, rkey: string, ticket: { title: string; body: string } ): Promise> => updatePull(agent, rkey, (record) => ({ ...record, ...ticket })); export const resubmitPull = ( agent: OAuthUserAgent, rkey: string, version: PullRecord["versions"][number] ): Promise> => updatePull(agent, rkey, (record) => ({ ...record, versions: [...(record.versions ?? []), version] })); export const deletePull = async (agent: OAuthUserAgent, rkey: string): Promise => { await deleteRecord(agent, PULL_COLLECTION, rkey); }; /** * Build the status record for a pull transition. Statuses are append-only: every transition is * a fresh record at a new tid, and readers take the newest one they trust. */ export const pullStatusRecord = (pull: string, status: PullState): PullStatusRecord => ({ $type: "sh.tangled.repo.pull.status", pull: pull as PullStatusRecord["pull"], status: `sh.tangled.repo.pull.status.${status}`, createdAt: new Date().toISOString() as PullStatusRecord["createdAt"] }); /** Writes into the acting user's repo, not the pull author's. */ export const putPullStatus = async ( agent: OAuthUserAgent, pull: string, status: PullState ): Promise> => { const record = pullStatusRecord(pull, status); const { uri, cid } = await putRecord(agent, PULL_STATUS_COLLECTION, tidNow(), record); return { uri, cid, value: record }; }; const KEEP_COMMIT_NSID = "sh.tangled.git.keepCommit"; export interface KeepCommitInput { /** repo did the commit lives in */ repo: string; /** the commit to pin */ oid: string; /** at-uri of the record that owns the keep ref; the rkey has to be final */ record: string; } export const keepCommit = async ( agent: OAuthUserAgent, knot: string, input: KeepCommitInput, fetch: typeof globalThis.fetch = globalThis.fetch ): Promise<{ commit: string }> => { const token = await mintServiceAuth(agent, { aud: serviceDidForHost(knot), lxm: KEEP_COMMIT_NSID }); const response = await fetch(`https://${knot}/xrpc/${KEEP_COMMIT_NSID}`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify({ repo: input.repo, record: input.record, source: { $type: `${KEEP_COMMIT_NSID}#commit`, repo: input.repo, oid: input.oid } }) }); if (!response.ok) throw await toResponseError(response); return (await response.json()) as { commit: string }; }; const MERGE_COMMIT_NSID = "sh.tangled.git.mergeCommit"; const REPO_PUSH_NSID = "sh.tangled.repo.push"; 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"; } export const mergeCommit = async ( agent: OAuthUserAgent, mirror: string, knot: string, input: MergeCommitInput, fetch: typeof globalThis.fetch = globalThis.fetch ): Promise => { const token = await mintServiceAuth(agent, { aud: serviceDidForHost(knot), // `sh.tangled.git.mergeCommit` expects service-auth token minted for `sh.tangled.repo.push` // so it can reuse it on git-push. lxm: REPO_PUSH_NSID }); const response = await fetch(buildUrl(mirror, MERGE_COMMIT_NSID), { method: "POST", headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify({ target: { $type: `${MERGE_COMMIT_NSID}#target`, ...input.target }, source: { $type: `${MERGE_COMMIT_NSID}#source`, ...input.source }, style: input.style }) }); if (!response.ok) throw await toResponseError(response); };