import { createServerFn } from "@tanstack/react-start"; import { withAuthorAccount, withAuthorAccounts } from "~/server/commit-authors"; import type { WithAuthorAccount } from "~/server/commit-authors"; import { assertVisibleRevision, isReachableCommit } from "~/server/git/reachable"; import { limitBrowse, limitHeavy, MAX_PAGE } from "~/server/limits"; import { z } from "zod"; import { viewerMiddleware } from "./middleware"; import { repoDb } from "~/server/scoped-db"; import { assertRepoReady, requireRepoAccess } from "~/server/repos"; import { branchSha, listRefs, resolveRevision, splitRevisionPath } from "~/server/git/refs"; import { withStore } from "~/server/git/store"; import type { GitStore } from "~/server/git/store"; import { diffTreesCached } from "~/server/git/diff"; import { renderMarkdown } from "~/server/markdown"; import { notFoundError } from "~/server/errors"; import { lfsBlobPreview } from "~/server/lfs/content"; import { combinedState, listCommitStatuses } from "~/server/statuses"; import { repoRef } from "~/lib/validation"; import type { CommitSummary, RepoRefs, RepoRow, StatusState, TreeEntry } from "~/lib/types"; const README_NAMES = ["readme.md", "readme.markdown", "readme", "readme.txt"]; const MAX_VIEW_BYTES = 1024 * 1024; const COMMITS_PAGE = 30; const BINARY_SNIFF = 8000; const decoder = new TextDecoder(); const FULL_SHA_LENGTH = 40; const MAX_LISTED_REFS = 90; function sortEntries(entries: TreeEntry[]): TreeEntry[] { return [...entries].sort((left, right) => { const leftDir = left.type === "tree" ? 0 : 1; const rightDir = right.type === "tree" ? 0 : 1; return leftDir - rightDir || left.name.localeCompare(right.name); }); } function resolveSplat(repo: RepoRow, refs: RepoRefs, splat: string) { if (!splat) { const sha = branchSha(refs, repo.default_branch) ?? refs.branches[0]?.sha; if (!sha) return null; const name = branchSha(refs, repo.default_branch) ? repo.default_branch : (refs.branches[0]?.name ?? repo.default_branch); return { revision: { kind: "branch" as const, name, sha }, path: "" }; } return splitRevisionPath(refs, splat); } async function readme(store: GitStore, repo: RepoRow, ref: string, entries: TreeEntry[], basePath: string) { const entry = entries.find( (candidate) => candidate.type !== "tree" && README_NAMES.includes(candidate.name.toLowerCase()), ); if (!entry) return null; const bytes = await store.blob(entry.sha); if (!bytes || bytes.byteLength > MAX_VIEW_BYTES) return null; const text = decoder.decode(bytes); const isMarkdown = /\.(md|markdown)$/i.test(entry.name); return { path: entry.path, html: isMarkdown ? renderMarkdown(text, { owner: repo.owner_name, repo: repo.name, ref, basePath, }) : null, text: isMarkdown ? null : text, }; } async function statusMap(repoId: string, shas: string[]): Promise> { if (shas.length === 0) return {}; const rows = await repoDb(repoId).all<{ sha: string; state: StatusState }>( `SELECT sha, state FROM commit_statuses WHERE repo_id = ? AND sha IN (${shas.map(() => "?").join(", ")})`, repoId, ...shas, ); const grouped = new Map>(); for (const row of rows) grouped.set(row.sha, [...(grouped.get(row.sha) ?? []), row]); const out: Record = {}; for (const [sha, statuses] of grouped) { const state = combinedState(statuses); if (state) out[sha] = state; } return out; } async function latestRunNumber(repoId: string, sha: string): Promise { const row = await repoDb(repoId).first<{ run_number: number }>( "SELECT run_number FROM workflow_runs WHERE repo_id = ? AND sha = ? ORDER BY created_at DESC LIMIT 1", repoId, sha, ); return row?.run_number ?? null; } const splatInput = repoRef.extend({ splat: z.string().default("") }); export const getTree = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(splatInput) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); if (repo.status !== "ready") return { kind: "empty" as const }; const refs = await listRefs(repo); if (refs.branches.length === 0) return { kind: "empty" as const }; const resolved = resolveSplat(repo, refs, data.splat); if (!resolved) throw notFoundError("Branch or path not found"); await assertVisibleRevision(repo, resolved.revision); return withStore(repo.artifacts_name, async (store) => { const commit = await store.commit(resolved.revision.sha); if (!commit) throw notFoundError("Commit not found"); const entry = await store.entryAtPath(commit.treeSha, resolved.path); if (!entry) throw notFoundError("Path not found"); if (entry.type !== "tree") return { kind: "file" as const, ref: resolved.revision.name, path: resolved.path }; const entries = sortEntries(await store.listTree(entry.sha, resolved.path)); const [statuses, commitRun] = await Promise.all([ statusMap(repo.id, [commit.sha]), latestRunNumber(repo.id, commit.sha), ]); return { kind: "tree" as const, ref: resolved.revision, path: resolved.path, entries, commit: await withAuthorAccount(commit), commitStatus: statuses[commit.sha] ?? null, commitRun, readme: await readme(store, repo, resolved.revision.name, entries, resolved.path), branches: refs.branches.map((branch) => branch.name), tags: refs.tags.map((tag) => tag.name), }; }); }); export const getBlob = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(splatInput) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); assertRepoReady(repo); const refs = await listRefs(repo); const resolved = splitRevisionPath(refs, data.splat); if (!resolved || !resolved.path) throw notFoundError("File not found"); await assertVisibleRevision(repo, resolved.revision); return withStore(repo.artifacts_name, async (store) => { const commit = await store.commit(resolved.revision.sha); const entry = commit ? await store.entryAtPath(commit.treeSha, resolved.path) : null; if (!entry || entry.type === "tree") throw notFoundError("File not found"); const stored = await store.blob(entry.sha); if (!stored) throw notFoundError("File not found"); const lfs = await lfsBlobPreview(repo, stored, MAX_VIEW_BYTES); const bytes = lfs?.content ?? stored; const binary = bytes.subarray(0, BINARY_SNIFF).includes(0); const tooLarge = lfs ? lfs.content === null : bytes.byteLength > MAX_VIEW_BYTES; const content = binary || tooLarge ? null : decoder.decode(bytes); const isMarkdown = /\.(md|markdown|mdx)$/i.test(resolved.path); const directory = resolved.path.split("/").slice(0, -1).join("/"); return { ref: resolved.revision, path: resolved.path, sha: entry.sha, mode: entry.mode, size: lfs?.info.size ?? bytes.byteLength, lfs: lfs?.info ?? null, binary, tooLarge, content, markdownHtml: isMarkdown && content !== null ? renderMarkdown(content, { owner: repo.owner_name, repo: repo.name, ref: resolved.revision.name, basePath: directory, }) : null, canEdit: permission !== "read" && resolved.revision.kind === "branch" && repo.is_archived === 0, branches: refs.branches.map((branch) => branch.name), }; }); }); export const getCommits = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ ref: z.string().optional(), page: z.number().int().min(1).max(MAX_PAGE).default(1) })) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); assertRepoReady(repo); const refs = await listRefs(repo); const refName = data.ref ?? repo.default_branch; const revision = resolveRevision(refs, refName); if (!revision) { const commits: Array> = []; return { ref: refName, commits, statuses: {}, hasMore: false, branches: [] }; } await assertVisibleRevision(repo, revision); const commits = await withStore(repo.artifacts_name, (store) => store.log(revision.sha, COMMITS_PAGE + 1, (data.page - 1) * COMMITS_PAGE), ); const page = commits.slice(0, COMMITS_PAGE); return { ref: revision.name, commits: await withAuthorAccounts(page), statuses: await statusMap( repo.id, page.map((commit) => commit.sha), ), hasMore: commits.length > COMMITS_PAGE, branches: refs.branches.map((branch) => branch.name), }; }); export const getCommit = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ sha: z.string().regex(/^[0-9a-f]{4,40}$/) })) .handler(async ({ data, context }) => { await limitHeavy(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); assertRepoReady(repo); return withStore(repo.artifacts_name, async (store) => { let sha = data.sha; if (sha.length < FULL_SHA_LENGTH) { const refs = await listRefs(repo); const match = [...refs.branches, ...refs.tags].find((ref) => ref.sha.startsWith(sha)); if (!match) throw notFoundError("Use the full commit SHA"); sha = match.sha; } if (!(await isReachableCommit(repo, sha))) throw notFoundError("Commit not found"); const commit = await store.commit(sha); if (!commit) throw notFoundError("Commit not found"); const parent = commit.parents[0] ? await store.commit(commit.parents[0]) : null; const diff = await diffTreesCached(store, parent?.treeSha ?? null, commit.treeSha); const statuses = await listCommitStatuses(repo.id, commit.sha); return { commit: await withAuthorAccount(commit), diff, statuses }; }); }); export const getBranches = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); assertRepoReady(repo); const refs = await listRefs(repo); const branches = await withStore(repo.artifacts_name, (store) => Promise.all( refs.branches.slice(0, MAX_LISTED_REFS).map(async (branch) => ({ ...branch, commit: await store.commit(branch.sha), })), ), ); // Pull requests live with their base repository: this one, or the parent for pulls opened from a fork. const pullStores = [...new Set([repo.id, ...(repo.fork_of_id ? [repo.fork_of_id] : [])])]; const pulls = ( await Promise.all( pullStores.map((storeId) => repoDb(storeId).all<{ head_ref: string; number: number; state: string; }>( `SELECT p.head_ref, i.number, i.state FROM pull_requests p JOIN issues i ON i.id = p.issue_id WHERE p.head_repo_id = ? ORDER BY i.number DESC`, repo.id, ), ), ) ).flat(); const statuses = await statusMap( repo.id, refs.branches.slice(0, MAX_LISTED_REFS).map((branch) => branch.sha), ); return { defaultBranch: repo.default_branch, canWrite: permission === "write" || permission === "admin", branches: branches .map((branch) => ({ name: branch.name, sha: branch.sha, commit: branch.commit, status: statuses[branch.sha] ?? null, pull: pulls.find((pull) => pull.head_ref === branch.name) ?? null, })) .sort((left, right) => (right.commit?.committedAt ?? 0) - (left.commit?.committedAt ?? 0)), }; }); export const getTags = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); assertRepoReady(repo); const refs = await listRefs(repo); return withStore(repo.artifacts_name, (store) => Promise.all( refs.tags.slice(0, MAX_LISTED_REFS).map(async (tag) => ({ ...tag, commit: await store.commit(tag.sha) })), ), ); });