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<ReturnType<typeof getRepo>>;
/** 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 wiki is cloned like the repository, as <name>.wiki.git. */
export async function wikiCloneUrls(repo: RepoRecord) {
const [origin, host] = await Promise.all([serverOrigin(), serverHost()]);
return {
https: `${origin}/${repo.owner}/${repo.name}.wiki.git`,
ssh: sshCloneUrl(host, `${repo.owner}/${repo.name}.wiki.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 };