import { first } from "../db"; import { notFoundError } from "../errors"; import { hasPermission, loadRepo, repoPermission } from "../repos"; import { scopedRepoPermission } from "../token-scope"; import type { RequestCredential } from "../token-scope"; import { diffTreesCached } from "./diff"; import { compareCommits } from "./history"; import { assertVisibleRevision } from "./reachable"; import { listRefs, resolveRevision } from "./refs"; import type { ResolvedRevision } from "./refs"; import { withStore } from "./store"; import type { Comparison } from "./history"; import type { DiffResult, RepoRow, Viewer } from "~/lib/types"; export type BranchComparison = { base: ResolvedRevision; head: ResolvedRevision; headRepo: RepoRow; comparison: Comparison; diff: DiffResult; baseBranches: string[]; headBranches: string[]; }; /** Who is comparing: a signed-in viewer (full access) or a request credential whose token scope also applies. */ export type CompareAccess = Viewer | null | RequestCredential; function isCredential(access: CompareAccess): access is RequestCredential { return access !== null && "grant" in access; } async function canReadHead(repo: RepoRow, access: CompareAccess): Promise { if (isCredential(access)) return hasPermission(await scopedRepoPermission(repo, access, "contents"), "read"); return (await repoPermission(repo, access)) !== "none"; } /** * Resolves an `owner:branch` head to a fork the caller can read; bare names stay in the base repository. Every * refusal is the same not-found, so the response never tells whether an unreadable private fork exists. */ async function resolveHead(base: RepoRow, head: string, access: CompareAccess) { const separator = head.indexOf(":"); if (separator === -1) return { repo: base, ref: head }; const owner = head.slice(0, separator); const ref = head.slice(separator + 1); const forks = await first<{ name: string }>( `SELECT r.name FROM repos r JOIN accounts a ON a.id = r.owner_id WHERE a.name = ? AND (r.id = ? OR r.fork_of_id = ? OR r.id = ?)`, owner, base.id, base.id, base.fork_of_id ?? "", ); const repo = forks ? await loadRepo(owner, forks.name) : null; if (!repo || !(await canReadHead(repo, access))) throw notFoundError("No readable fork found for that owner"); return { repo, ref }; } /** Commits and the merge-base diff between `base` in `repo` and `head` (optionally `owner:branch` in a fork). */ export async function compareBranches( repo: RepoRow, base: string, headRef: string, access: CompareAccess, ): Promise { const head = await resolveHead(repo, headRef, access); const [baseRefs, headRefs] = await Promise.all([ listRefs(repo), head.repo.id === repo.id ? null : listRefs(head.repo), ]); const baseRevision = resolveRevision(baseRefs, base); const headRevision = resolveRevision(headRefs ?? baseRefs, head.ref); if (!baseRevision || !headRevision) throw notFoundError("Unknown branch"); await Promise.all([assertVisibleRevision(repo, baseRevision), assertVisibleRevision(head.repo, headRevision)]); const result = await withStore(repo.artifacts_name, (baseStore) => withStore(head.repo.artifacts_name, async (headStore) => { const comparison = await compareCommits(baseStore, baseRevision.sha, headStore, headRevision.sha); const [mergeBase, headCommit] = await Promise.all([ comparison.mergeBase ? headStore.commit(comparison.mergeBase) : null, headStore.commit(headRevision.sha), ]); if (!headCommit) throw notFoundError("Head commit not found"); const diff = await diffTreesCached(headStore, mergeBase?.treeSha ?? null, headCommit.treeSha); return { comparison, diff }; }), ); return { base: baseRevision, head: headRevision, headRepo: head.repo, comparison: result.comparison, diff: result.diff, baseBranches: baseRefs.branches.map((branch) => branch.name), headBranches: (headRefs ?? baseRefs).branches.map((branch) => branch.name), }; }