import type { ResourceUri } from "@atcute/lexicons"; import { isDid, type Did } from "@atcute/lexicons/syntax"; import { ClientResponseError, type BobbinContext, type XrpcRequestInit } from "$lib/api/client"; import { resolveEmailToDid } from "$lib/api/emailDid"; import { getBlame } from "$lib/api/gitmirror"; import { resolveMiniDoc } from "$lib/api/identity"; import { blame as knotBlame } from "$lib/api/knot"; import { didFromSignature, splitMessage } from "$lib/api/repo"; import type * as GetBlame from "$lib/api/lexicons/types/sh/tangled/git/temp2/getBlame"; import type * as KnotBlame from "$lib/api/lexicons/types/sh/tangled/repo/blame"; import type { Blame, BlameCommit } from "$lib/components/repo/blame/types"; type RunTable = { start: number; count: number; commit: string }; // knot and mirror payloads share the run-table contract: commits must be unique // and runs must be a gap-free, sorted cover of the file starting at line 1 const toBlame = (commits: readonly BlameCommit[], runs: readonly RunTable[]): Blame => { const byShort = new Map(); for (const commit of commits) { if (byShort.has(commit.sha)) throw new Error(`duplicate blame commit: ${commit.sha}`); byShort.set(commit.sha, commit); } const blame: Blame = []; let nextLine = 1; for (const run of runs) { if (run.start !== nextLine || run.count < 1 || !Number.isSafeInteger(run.count)) { throw new Error(`invalid blame run at line ${run.start}`); } const commit = byShort.get(run.commit); if (!commit) throw new Error(`unknown blame commit: ${run.commit}`); blame.push({ start: run.start, count: run.count, commit }); nextLine += run.count; } return blame; }; // a did in the author slot is a tangled account signing its own commits const toAuthor = ({ name, email }: { name: string; email: string }) => ({ name, email, did: didFromSignature(email) }); const fromKnot = (result: KnotBlame.$output): Blame => toBlame( result.commits.map((commit) => ({ sha: commit.sha, subject: commit.subject, when: commit.authoredAt, author: toAuthor(commit.author) })), result.runs ); const fromMirror = (result: GetBlame.$output): Blame => toBlame( result.commits.map((commit) => ({ sha: commit.oid, subject: splitMessage(commit.message)[0], when: commit.author.when, author: toAuthor(commit.author) })), result.runs ); // the mirror only speaks for what it has fetched: a repo it doesn't mirror is // RepoNotFound, a commit it hasn't caught up to is RefNotFound, and the knot can // still answer both const mirrorMiss = (cause: unknown): boolean => cause instanceof ClientResponseError && (cause.error === "RepoNotFound" || cause.error === "RefNotFound"); // the mirror keys repos by bobbin's repo did, the knot by the record's at-uri, so // blame needs both names for the same repo export const blameFor = async ( ctx: BobbinContext, repo: { repo: ResourceUri; repoDid?: Did }, ref: string, path: string, init?: XrpcRequestInit ): Promise => { if (repo.repoDid) { try { return fromMirror(await getBlame(ctx, { repo: repo.repoDid, ref, path }, init)); } catch (cause) { if (!mirrorMiss(cause)) throw cause; } } return fromKnot(await knotBlame(ctx, { repo: repo.repo, ref, path }, init)); }; const MAX_AUTHOR_HYDRATIONS = 250; const AUTHOR_HYDRATION_CONCURRENCY = 8; const mapConcurrent = async ( items: T[], concurrency: number, fn: (item: T) => Promise ): Promise => { const results = new Array(items.length); let next = 0; await Promise.all( Array.from({ length: Math.min(concurrency, items.length) }, async () => { while (next < items.length) { const index = next++; results[index] = await fn(items[index]); } }) ); return results; }; export const hydrateBlameAuthors = async (blame: Blame, ctx: BobbinContext): Promise => { const emails = [ ...new Set( blame.flatMap(({ commit }) => commit.author.did || !commit.author.email ? [] : [commit.author.email] ) ) ].slice(0, MAX_AUTHOR_HYDRATIONS); const emailDids = await resolveEmailToDid(ctx.fetch, emails); const didFor = (email: string): string | undefined => isDid(email) ? email : emailDids.get(email); const dids = [ ...new Set( blame.flatMap(({ commit }) => { const did = commit.author.did ?? didFor(commit.author.email); return did ? [did] : []; }) ) ].slice(0, MAX_AUTHOR_HYDRATIONS); const docs = await mapConcurrent( dids, AUTHOR_HYDRATION_CONCURRENCY, async (did) => [did, await resolveMiniDoc(ctx, did).catch(() => null)] as const ); const handles = new Map( docs.flatMap(([did, doc]) => doc && !doc.handle.endsWith(".invalid") ? [[did, doc.handle] as const] : [] ) ); return blame.map((run) => { const did = run.commit.author.did ?? didFor(run.commit.author.email); return { ...run, commit: { ...run.commit, author: { ...run.commit.author, did, handle: did ? handles.get(did) : undefined } } }; }); };