import { Elysia, t } from "elysia"; import { sql } from "kysely"; import { COMMITTER_EMAIL, COMMITTER_NAME, MAX_USER_UPLOAD_BYTES, } from "../config.ts"; import { ALLOWED_REACTIONS, PATCHES_PER_PAGE } from "../constants.ts"; import { db, getRepo } from "../db/index.ts"; import { requireAdmin, requireAuth, resolveSession, } from "../middleware/session.ts"; import { extractPatchMeta, git } from "../services/git.ts"; import { prepareDiff } from "../services/highlightWorker.ts"; import { renderMarkdown } from "../services/markdown.ts"; import { patchCache } from "../services/patchCache.ts"; import { buildReactionCounts } from "../services/reactions.ts"; import { NewPatch } from "../views/patches/NewPatch.tsx"; import { PatchDetail } from "../views/patches/PatchDetail.tsx"; import { PatchList } from "../views/patches/PatchList.tsx"; import { html } from "../views/render.tsx"; function isValidPatch(content: string): boolean { const lines = content.split("\n"); return lines.some( (l) => l.startsWith("diff --git ") || l.startsWith("--- ") || l.startsWith("+++ ") || l.startsWith("@@ ") || l.startsWith("Index: "), ); } async function runPatchCheck( repoName: string, patchId: number, patchContent: string, ) { const result = await git.checkPatch(repoName, patchContent); const applyResult = { status: result.clean ? ("clean" as const) : ("conflict" as const), output: result.output, }; patchCache.set(patchId, applyResult); return applyResult; } export const patchRoutes = new Elysia() .guard({ cookie: t.Cookie({ session: t.Optional(t.String()) }), }) .get( "/:repo/patches", async ({ params, query, cookie }) => { const user = await resolveSession(cookie.session.value); const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); const status = ["open", "merged", "closed"].includes( query.status ?? "", ) ? query.status! : "open"; const page = Math.max(1, query.page ?? 1); const allCounts = await db .selectFrom("patches") .select(["status", db.fn.countAll().as("count")]) .where("repo_id", "=", repo.id) .groupBy("status") .execute(); const counts: Record = Object.fromEntries( allCounts.map((r) => [r.status, Number(r.count)]), ); const totalPages = Math.max( 1, Math.ceil((counts[status] ?? 0) / PATCHES_PER_PAGE), ); const safePage = Math.min(page, totalPages); const offset = (safePage - 1) * PATCHES_PER_PAGE; const patches = await db .selectFrom("patches") .leftJoin("users", "users.id", "patches.author_id") .select([ "patches.id", "patches.repo_id", "patches.author_id", "patches.number", "patches.title", "patches.description", "patches.patch_content", "patches.status", "patches.author_name", "patches.author_email", "patches.created_at", "patches.updated_at", "patches.edited_at", "patches.version", "users.username as author_username", "users.avatar_version as author_avatar_version", ]) .where("patches.repo_id", "=", repo.id) .where("patches.status", "=", status) .orderBy("patches.number", "desc") .limit(PATCHES_PER_PAGE) .offset(offset) .execute(); const pagination = { page: safePage, totalPages, pageUrlTemplate: `/${repo.name}/patches?status=${status}&page={page}`, }; return html( , ); }, { query: t.Object({ status: t.Optional(t.String()), page: t.Optional(t.Numeric()), }), }, ) .get("/:repo/patches/new", async ({ params, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAuth(user); if (deny) return deny; const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); return html( , ); }) .post( "/:repo/patches", async ({ params, body, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAuth(user); if (deny) return deny; const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); if (!body.title?.trim()) { return html( , ); } if (!body.patch_file) { return html( , ); } if (body.patch_file.size > MAX_USER_UPLOAD_BYTES) { return html( , ); } const patchContent = await body.patch_file.text(); if (!patchContent.trim()) { return html( , ); } // Validate it looks like a patch file if (!isValidPatch(patchContent)) { return html( , ); } const uploadMeta = extractPatchMeta(patchContent); if (!uploadMeta.subject) { return html( , ); } if (!uploadMeta.author || !uploadMeta.email) { return html( , ); } if (!uploadMeta.date) { return html( , ); } const now = new Date().toISOString(); const { number, result } = await db .transaction() .execute(async (trx) => { const { patch_seq } = await trx .updateTable("repositories") .set({ patch_seq: sql`patch_seq + 1` }) .where("id", "=", repo.id) .returning("patch_seq") .executeTakeFirstOrThrow(); const inserted = await trx .insertInto("patches") .values({ repo_id: repo.id, author_id: user?.id, number: patch_seq, title: body.title!.trim(), description: body.description?.trim() ?? "", patch_content: patchContent, status: "open", author_name: uploadMeta.author, author_email: uploadMeta.email, created_at: now, updated_at: now, version: crypto.randomUUID(), }) .returning("id") .executeTakeFirstOrThrow(); return { number: patch_seq, result: inserted }; }); await runPatchCheck(repo.name, result.id, patchContent); return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches/${number}` }, }); }, { body: t.Object({ title: t.Optional(t.String()), description: t.Optional(t.String()), patch_file: t.Optional(t.File()), }), }, ) .get( "/:repo/patches/:number", async ({ params, query, cookie }) => { const user = await resolveSession(cookie.session.value); const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); const patchNum = parseInt(params.number, 10); const patch = await db .selectFrom("patches") .leftJoin("users", "users.id", "patches.author_id") .select([ "patches.id", "patches.repo_id", "patches.author_id", "patches.number", "patches.title", "patches.description", "patches.patch_content", "patches.status", "patches.author_name", "patches.author_email", "patches.created_at", "patches.updated_at", "patches.edited_at", "patches.version", "users.username as author_username", "users.avatar_version as author_avatar_version", ]) .where("patches.repo_id", "=", repo.id) .where("patches.number", "=", patchNum) .executeTakeFirst(); if (!patch) return new Response("Not found", { status: 404 }); const descriptionHtml = patch.description ? renderMarkdown(patch.description) : ""; let applyResult = patchCache.get(patch.id) ?? null; // Cold cache (e.g. server restart) — re-check synchronously for open patches only if (!applyResult && patch.status === "open") { applyResult = await runPatchCheck( repo.name, patch.id, patch.patch_content, ); } const files = await prepareDiff( patch.patch_content, `patch:${patch.id}`, ); const comments = await db .selectFrom("patch_comments") .leftJoin("users", "users.id", "patch_comments.author_id") .select([ "patch_comments.id", "patch_comments.patch_id", "patch_comments.author_id", "patch_comments.body", "patch_comments.created_at", "patch_comments.edited_at", "users.username as author_username", "users.avatar_version as author_avatar_version", ]) .where("patch_comments.patch_id", "=", patch.id) .orderBy("patch_comments.created_at", "asc") .execute(); const commentsWithHtml = comments.map((c) => ({ ...c, bodyHtml: renderMarkdown(c.body), })); const allReactions = await db .selectFrom("patch_reactions") .selectAll() .where("patch_id", "=", patch.id) .execute(); const reactions = buildReactionCounts(allReactions, null, user?.id); const commentReactions = new Map( comments.map((c) => [ c.id, buildReactionCounts(allReactions, c.id, user?.id), ]), ); const tab = query.tab === "changes" ? ("changes" as const) : ("conversation" as const); const patchMeta = extractPatchMeta(patch.patch_content); return html( , ); }, { query: t.Object({ tab: t.Optional(t.String()) }), }, ) .post( "/:repo/patches/:number/merge", async ({ params, body, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAdmin(user); if (deny) return deny; const repo = await getRepo(params.repo, true); if (!repo) return new Response("Not found", { status: 404 }); const patchNum = parseInt(params.number, 10); const patch = await db .selectFrom("patches") .select(["id", "patch_content", "status", "version"]) .where("repo_id", "=", repo.id) .where("number", "=", patchNum) .executeTakeFirst(); if (!patch) return new Response("Not found", { status: 404 }); // Reject if the patch file was changed after the admin loaded the page if (body.version !== patch.version) { return new Response( "The patch file was updated after you loaded this page. Please review the new version before merging.", { status: 409 }, ); } // Atomically claim the merge slot before the slow git operation to // prevent two concurrent requests from both applying the same patch. const claimed = await db .updateTable("patches") .set({ status: "merged", updated_at: new Date().toISOString() }) .where("id", "=", patch.id) .where("status", "=", "open") .where("version", "=", patch.version) .executeTakeFirst(); if (!claimed || claimed.numUpdatedRows === 0n) return new Response("Patch is not open", { status: 400 }); const mergeMeta = extractPatchMeta(patch.patch_content); try { await git.applyPatch( repo.name, patch.patch_content, mergeMeta.author, mergeMeta.email, COMMITTER_NAME, COMMITTER_EMAIL, ); } catch (err) { // Roll back the status if the git operation fails await db .updateTable("patches") .set({ status: "open", updated_at: new Date().toISOString(), }) .where("id", "=", patch.id) .execute(); throw err; } patchCache.invalidate(patch.id); return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches/${patchNum}` }, }); }, { body: t.Object({ version: t.String() }), }, ) .post( "/:repo/patches/:number/upload", async ({ params, body, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAuth(user); if (deny) return deny; const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); const patchNum = parseInt(params.number, 10); const patch = await db .selectFrom("patches") .select(["id", "author_id", "status"]) .where("repo_id", "=", repo.id) .where("number", "=", patchNum) .executeTakeFirst(); if (!patch) return new Response("Not found", { status: 404 }); if (patch.author_id !== user?.id && !user?.isAdmin) return new Response("Forbidden", { status: 403 }); if (patch.status !== "open") return new Response("Patch is not open", { status: 400 }); if (!body.patch_file || body.patch_file.size === 0) { return new Response("Patch file is required", { status: 400 }); } if (body.patch_file.size > MAX_USER_UPLOAD_BYTES) { return new Response("Patch file is too large", { status: 400 }); } const patchContent = await body.patch_file.text(); if (!patchContent.trim()) { return new Response("Patch file is empty", { status: 400 }); } if (!isValidPatch(patchContent)) { return new Response( "File does not appear to be a valid patch file", { status: 400 }, ); } const uploadMeta = extractPatchMeta(patchContent); if ( !uploadMeta.subject || !uploadMeta.author || !uploadMeta.email || !uploadMeta.date ) { return new Response( "Patch is missing required headers (Subject, From, Date)", { status: 400 }, ); } const newVersion = crypto.randomUUID(); await db .updateTable("patches") .set({ patch_content: patchContent, author_name: uploadMeta.author, author_email: uploadMeta.email, version: newVersion, updated_at: new Date().toISOString(), }) .where("id", "=", patch.id) .execute(); patchCache.invalidate(patch.id); runPatchCheck(repo.name, patch.id, patchContent); return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches/${patchNum}` }, }); }, { body: t.Object({ patch_file: t.Optional(t.File()), }), }, ) .post("/:repo/patches/:number/close", async ({ params, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAdmin(user); if (deny) return deny; const repo = await getRepo(params.repo, true); if (!repo) return new Response("Not found", { status: 404 }); const patchNum = parseInt(params.number, 10); const patch = await db .selectFrom("patches") .select("id") .where("repo_id", "=", repo.id) .where("number", "=", patchNum) .executeTakeFirst(); if (!patch) return new Response("Not found", { status: 404 }); // Toggle open↔closed atomically; exclude merged patches from the WHERE // so that numUpdatedRows = 0 means the patch is merged (or gone). const toggled = await db .updateTable("patches") .set({ status: sql`CASE WHEN status = 'open' THEN 'closed' ELSE 'open' END`, updated_at: new Date().toISOString(), }) .where("id", "=", patch.id) .where("status", "!=", "merged") .executeTakeFirst(); if (!toggled || toggled.numUpdatedRows === 0n) return new Response("Patch is merged", { status: 400 }); return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches/${patchNum}` }, }); }) .post("/:repo/patches/:number/delete", async ({ params, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAuth(user); if (deny) return deny; const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); const patchNum = parseInt(params.number, 10); const patch = await db .selectFrom("patches") .select(["id", "author_id"]) .where("repo_id", "=", repo.id) .where("number", "=", patchNum) .executeTakeFirst(); if (!patch) return new Response("Not found", { status: 404 }); if (patch.author_id !== user?.id && !user?.isAdmin) return new Response("Forbidden", { status: 403 }); patchCache.invalidate(patch.id); await db.deleteFrom("patches").where("id", "=", patch.id).execute(); return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches` }, }); }) .post( "/:repo/patches/:number/comments", async ({ params, body, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAuth(user); if (deny) return deny; const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); const patchNum = parseInt(params.number, 10); const patch = await db .selectFrom("patches") .select(["id", "status"]) .where("repo_id", "=", repo.id) .where("number", "=", patchNum) .executeTakeFirst(); if (!patch) return new Response("Not found", { status: 404 }); const { body: commentBody } = body; if (!commentBody?.trim()) { return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches/${patchNum}` }, }); } await db.transaction().execute(async (trx) => { const now = new Date().toISOString(); await trx .insertInto("patch_comments") .values({ patch_id: patch.id, author_id: user?.id, body: commentBody.trim(), created_at: now, }) .execute(); await trx .updateTable("patches") .set({ updated_at: now }) .where("id", "=", patch.id) .execute(); }); return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches/${patchNum}` }, }); }, { body: t.Object({ body: t.String() }), }, ) .post( "/:repo/patches/:number/react", async ({ params, body, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAuth(user); if (deny) return deny; const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); const { emoji, comment_id } = body; if (!ALLOWED_REACTIONS.has(emoji)) { return new Response("Invalid emoji", { status: 400 }); } const patchNum = parseInt(params.number, 10); const patch = await db .selectFrom("patches") .select(["id"]) .where("repo_id", "=", repo.id) .where("number", "=", patchNum) .executeTakeFirst(); if (!patch) return new Response("Not found", { status: 404 }); const commentId = comment_id ? parseInt(comment_id, 10) : null; await db.transaction().execute(async (trx) => { const existing = await trx .selectFrom("patch_reactions") .select(["id", "emoji"]) .where("patch_id", "=", patch.id) .where((eb) => commentId !== null ? eb("comment_id", "=", commentId) : eb("comment_id", "is", null), ) .where("user_id", "=", user!.id) .executeTakeFirst(); if (existing) { if (existing.emoji === emoji) { await trx .deleteFrom("patch_reactions") .where("id", "=", existing.id) .execute(); } else { await trx .updateTable("patch_reactions") .set({ emoji }) .where("id", "=", existing.id) .execute(); } } else { await trx .insertInto("patch_reactions") .values({ patch_id: patch.id, comment_id: commentId, user_id: user!.id, emoji, }) .execute(); } }); return new Response(null, { status: 303, headers: { Location: `/${repo.name}/patches/${patchNum}` }, }); }, { body: t.Object({ emoji: t.String(), comment_id: t.Optional(t.String()), }), }, ) .post( "/:repo/patches/:number/comments/:id/edit", async ({ params, body, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAuth(user); if (deny) return deny; const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); const comment = await db .selectFrom("patch_comments") .select(["id", "author_id", "patch_id"]) .where("id", "=", params.id) .executeTakeFirst(); if (!comment) return new Response("Not found", { status: 404 }); if (comment.author_id !== user?.id && !user?.isAdmin) return new Response("Forbidden", { status: 403 }); const parentPatch = await db .selectFrom("patches") .select("status") .where("id", "=", comment.patch_id) .executeTakeFirst(); if (parentPatch?.status !== "open" && !user?.isAdmin) return new Response("Forbidden", { status: 403 }); const patchNum = parseInt(params.number, 10); await db .updateTable("patch_comments") .set({ body: body.edit_body.trim(), edited_at: new Date().toISOString(), }) .where("id", "=", comment.id) .execute(); return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches/${patchNum}` }, }); }, { params: t.Object({ repo: t.String(), number: t.String(), id: t.Numeric(), }), body: t.Object({ edit_body: t.String() }), }, ) .post( "/:repo/patches/:number/edit", async ({ params, body, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAuth(user); if (deny) return deny; const repo = await getRepo(params.repo, user?.isAdmin ?? false); if (!repo) return new Response("Not found", { status: 404 }); const patchNum = parseInt(params.number, 10); const patch = await db .selectFrom("patches") .select(["id", "author_id", "status"]) .where("repo_id", "=", repo.id) .where("number", "=", patchNum) .executeTakeFirst(); if (!patch) return new Response("Not found", { status: 404 }); if (patch.author_id !== user?.id && !user?.isAdmin) return new Response("Forbidden", { status: 403 }); if (patch.status !== "open" && !user?.isAdmin) return new Response("Forbidden", { status: 403 }); await db .updateTable("patches") .set({ title: body.title.trim(), description: body.edit_description ?? "", edited_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) .where("id", "=", patch.id) .execute(); return new Response(null, { status: 302, headers: { Location: `/${repo.name}/patches/${patchNum}` }, }); }, { body: t.Object({ title: t.String(), edit_description: t.Optional(t.String()), }), }, );