import { repoDb } from "./scoped-db"; import { withStore } from "./git/store"; import { compareCommits } from "./git/history"; import { diffTreesCached } from "./git/diff"; import { isReachableFrom } from "./git/reachable"; import { badRequest, notFoundError } from "./errors"; import type { PullRow } from "./pulls"; import type { DiffResult, RepoRow } from "~/lib/types"; import type { GitStore } from "./git/store"; const MAX_PICKER_COMMITS = 100; /** * A "changes since" commit must belong to this pull request: a commit of the pull request, its merge * base, a head the pull request had before (reviews and head_pushed events record them), or an ancestor * of the current head or base. Anything else in the head store, e.g. a private fork's other branches, is * refused (AZ-A3). */ async function sinceBelongsToPull(pull: PullRow, headStore: GitStore, since: string, mergeBase: string | null) { if (since === mergeBase || since === pull.merge_base_sha || since === pull.base_sha) return true; const recorded = await repoDb(pull.repo_id).first( `SELECT 1 AS ok FROM reviews WHERE issue_id = ?1 AND commit_sha = ?2 UNION SELECT 1 FROM issue_events WHERE issue_id = ?1 AND kind = 'head_pushed' AND (json_extract(data, '$.before') = ?2 OR json_extract(data, '$.after') = ?2) LIMIT 1`, pull.issue_id, since, ); if (recorded) return true; return isReachableFrom(headStore, [pull.head_sha, pull.base_sha], since); } export type DiffRange = { commit?: string; since?: string }; export type PullDiff = { diff: DiffResult; commits: Array<{ sha: string; shortSha: string; subject: string }>; range: { kind: "all" } | { kind: "commit"; sha: string } | { kind: "since"; sha: string }; }; /** * Diff for the Files tab: the whole pull request (merge base to head), a single commit, or the * changes since a reviewed commit ("changes since last review"). */ export async function pullDiff(pull: PullRow, repo: RepoRow, headRepo: RepoRow, range: DiffRange): Promise { return withStore(repo.artifacts_name, (baseStore) => withStore(headRepo.artifacts_name, async (headStore) => { const comparison = await compareCommits( baseStore, pull.merged_at && pull.merge_base_sha ? pull.merge_base_sha : pull.base_sha, headStore, pull.head_sha, ); const commits = comparison.commits .slice(0, MAX_PICKER_COMMITS) .map((commit) => ({ sha: commit.sha, shortSha: commit.shortSha, subject: commit.subject })); const head = await headStore.commit(pull.head_sha); if (!head) throw notFoundError("Head commit missing"); if (range.commit) { const commit = comparison.commits.find((candidate) => candidate.sha === range.commit); if (!commit) throw badRequest("That commit is not part of this pull request"); const parent = commit.parents[0] ? await headStore.commit(commit.parents[0]) : null; return { diff: await diffTreesCached(headStore, parent?.treeSha ?? null, commit.treeSha), commits, range: { kind: "commit", sha: commit.sha }, }; } if (range.since && range.since !== pull.head_sha) { if (!(await sinceBelongsToPull(pull, headStore, range.since, comparison.mergeBase))) { throw badRequest("That commit is not part of this pull request"); } const since = await headStore.commit(range.since); if (!since) throw badRequest("The reviewed commit is no longer available"); return { diff: await diffTreesCached(headStore, since.treeSha, head.treeSha), commits, range: { kind: "since", sha: since.sha }, }; } const mergeBaseSha = comparison.mergeBase ?? pull.merge_base_sha; if (mergeBaseSha && mergeBaseSha !== pull.merge_base_sha && !pull.merged_at) { await repoDb(pull.repo_id).run( "UPDATE pull_requests SET merge_base_sha = ? WHERE issue_id = ?", mergeBaseSha, pull.issue_id, ); } const mergeBase = mergeBaseSha ? await headStore.commit(mergeBaseSha) : null; return { diff: await diffTreesCached(headStore, mergeBase?.treeSha ?? null, head.treeSha), commits, range: { kind: "all" }, }; }), ); }