import { first } from "../db"; import { notFoundError } from "../errors"; import { loadRepo, repoPermission } from "../repos"; 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[]; }; /** Resolves an `owner:branch` head to a fork the viewer can read; bare names stay in the base repository. */ async function resolveHead(base: RepoRow, head: string, viewer: Viewer | null) { 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 ?? "", ); if (!forks) throw notFoundError("No fork found for that owner"); const repo = await loadRepo(owner, forks.name); if (!repo || (await repoPermission(repo, viewer)) === "none") throw notFoundError("Head repository not found"); 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, viewer: Viewer | null, ): Promise { const head = await resolveHead(repo, headRef, viewer); 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), }; }