import "server-only"; import { cache } from "react"; import { forbidden, notFound } from "next/navigation"; import { getViewer } from "./auth"; import { defaultBranch, isBranch, normalizeRepoName, resolveRefAndPath } from "./git"; import { canWrite, findRepo, repoAccess, type RepoRecord } from "./repos"; import { serverHost, serverOrigin } from "./request-origin"; import { sshCloneUrl } from "./ssh-config"; /** * Loads a repository for a page. Repositories the viewer can't read render the * 404 page, so private repositories don't reveal that they exist. */ export const getRepo = cache(async (owner: string, repoParam: string) => { const record = await findRepo(owner, normalizeRepoName(repoParam)); if (!record) notFound(); const viewer = await getViewer(); const access = await repoAccess(record, viewer?.id); if (!access) notFound(); return { ...record, viewer, access, canWrite: canWrite(access), isAdmin: access === "admin", defaultBranch: await defaultBranch(record.dir), base: `/${record.owner}/${record.name}`, }; }); export type Repo = Awaited>; /** Like getRepo, but also requires push access. */ export async function getWritableRepo(owner: string, repoParam: string) { const repo = await getRepo(owner, repoParam); if (!repo.canWrite) forbidden(); return repo; } /** Resolves catch-all URL segments into a ref and a path, or renders the 404 page. */ export async function getRefAndPath(repo: RepoRecord, segments: string[]) { const resolved = await resolveRefAndPath(repo.dir, segments); if (!resolved) notFound(); return resolved; } /** * For editor routes: the first segments must name an existing branch, except in * an empty repository where the (future) branch name is taken as-is. */ export async function getBranchAndPath(repo: Repo, segments: string[]) { if (!repo.defaultBranch) { const [branch = "main", ...rest] = segments; return { branch, path: rest.join("/"), exists: false }; } const resolved = await getRefAndPath(repo, segments); if (!(await isBranch(repo.dir, resolved.ref))) notFound(); return { branch: resolved.ref, path: resolved.path, exists: true }; } export async function cloneUrls(repo: RepoRecord) { const [origin, host] = await Promise.all([serverOrigin(), serverHost()]); return { https: `${origin}/${repo.owner}/${repo.name}.git`, ssh: sshCloneUrl(host, `${repo.owner}/${repo.name}.git`), }; } /** The repository's container image, e.g. vectrum.cc/alice/app. Image names are lowercase. */ export async function imageName(repo: RepoRecord) { return `${await serverHost()}/${repo.owner}/${repo.name}`.toLowerCase(); } export type { RepoRecord };