"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import {
addCollaborator,
createRepo,
deleteRepo,
removeCollaborator,
removeRepoImage,
renameRepo,
RepoError,
setRepoImage,
setWikiEnabled,
updateRepo,
type Visibility,
} from "@/lib/repos";
import { getOrg, mayCreateRepos } from "@/lib/orgs";
import { ActionError, repoForAction, viewerOrThrow } from "./guard";
import type { FormState } from "./types";
const visibilityOf = (v: FormDataEntryValue | null): Visibility => (v === "private" ? "private" : "public");
async function attempt(fn: () => Promise<FormState | void> | FormState | void): Promise<FormState> {
try {
return (await fn()) ?? undefined;
} catch (e) {
if (e instanceof RepoError || e instanceof ActionError) return { error: e.message };
throw e;
}
}
export async function createRepoAction(_prev: FormState, formData: FormData): Promise<FormState> {
let path = "";
const result = await attempt(async () => {
const viewer = await viewerOrThrow();
const ownerName = String(formData.get("owner") ?? viewer.username);
const owner = ownerName.toLowerCase() === viewer.username.toLowerCase() ? viewer : await getOrg(ownerName);
if (!owner || !(await mayCreateRepos(owner, viewer))) throw new ActionError(`You can't create repositories in ${ownerName}.`);
const repo = await createRepo(
owner,
{
name: String(formData.get("name") ?? ""),
description: String(formData.get("description") ?? ""),
visibility: visibilityOf(formData.get("visibility")),
readme: formData.get("readme") === "on",
},
{ author: viewer },
);
path = `/${repo.owner}/${repo.name}`;
});
if (result?.error) return result;
redirect(path);
}
export async function updateRepoAction(owner: string, name: string, _prev: FormState, formData: FormData) {
return attempt(async () => {
const { repo } = await repoForAction(owner, name, "admin");
await updateRepo(repo.id, {
description: String(formData.get("description") ?? ""),
visibility: visibilityOf(formData.get("visibility")),
});
revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
return { success: "Settings saved." };
});
}
export async function updateFeaturesAction(owner: string, name: string, _prev: FormState, formData: FormData) {
return attempt(async () => {
const { repo } = await repoForAction(owner, name, "admin");
await setWikiEnabled(repo.id, formData.get("wiki") === "on");
revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
return { success: "Features saved." };
});
}
export async function updateRepoImageAction(owner: string, name: string, _prev: FormState, formData: FormData) {
return attempt(async () => {
const { repo } = await repoForAction(owner, name, "admin");
const file = formData.get("image");
if (!(file instanceof File)) throw new RepoError("Choose an image to upload.");
await setRepoImage(repo.id, new Uint8Array(await file.arrayBuffer()));
// The image shows up in sidebars and lists everywhere, not just on this repository's pages.
revalidatePath("/", "layout");
return { success: "Repository image updated." };
});
}
export async function removeRepoImageAction(owner: string, name: string) {
const { repo } = await repoForAction(owner, name, "admin");
await removeRepoImage(repo.id);
revalidatePath("/", "layout");
}
export async function renameRepoAction(owner: string, name: string, _prev: FormState, formData: FormData) {
let path = "";
const result = await attempt(async () => {
const { repo } = await repoForAction(owner, name, "admin");
const renamed = await renameRepo(repo, String(formData.get("name") ?? ""));
path = `/${renamed.owner}/${renamed.name}/settings`;
});
if (result?.error) return result;
redirect(path);
}
export async function deleteRepoAction(owner: string, name: string, _prev: FormState, formData: FormData) {
const result = await attempt(async () => {
const { repo } = await repoForAction(owner, name, "admin");
if (formData.get("confirm") !== `${repo.owner}/${repo.name}`) {
return { error: `Type ${repo.owner}/${repo.name} to confirm.` };
}
await deleteRepo(repo);
});
if (result?.error) return result;
redirect("/");
}
export async function addCollaboratorAction(owner: string, name: string, _prev: FormState, formData: FormData) {
return attempt(async () => {
const { repo } = await repoForAction(owner, name, "admin");
const username = String(formData.get("username") ?? "").trim();
await addCollaborator(repo, username);
revalidatePath(`/${repo.owner}/${repo.name}/settings`);
return { success: `${username} can now push to this repository.` };
});
}
export async function removeCollaboratorAction(owner: string, name: string, userId: string) {
const { repo } = await repoForAction(owner, name, "admin");
await removeCollaborator(repo.id, String(userId));
revalidatePath(`/${repo.owner}/${repo.name}/settings`);
}