import { readFileSync, rmSync } from "node:fs"; import path from "node:path"; import { Elysia, t } from "elysia"; import { fileTypeFromBuffer } from "file-type"; import { ALLOWED_SIGNERS_PATH, COMMITS_PER_PAGE, REPOS_PER_PAGE, VALID_REPO_NAME_RE, } from "../constants.ts"; import { db } from "../db/index.ts"; import { requireAdmin, resolveSession } from "../middleware/session.ts"; import { git, repoPath } from "../services/git.ts"; import { hasBinaryContent, prepareDiff, serveFile, } from "../services/highlightWorker.ts"; import { renderMarkdown } from "../services/markdown.ts"; import { ensureRepoRecord, repoDiskExists } from "../services/repoSync.ts"; import { redirect } from "../lib/redirect.ts"; import { html } from "../views/render.tsx"; import { CommitDetail } from "../views/repos/CommitDetail.tsx"; import { CommitLog } from "../views/repos/CommitLog.tsx"; import { FileBlob } from "../views/repos/FileBlob.tsx"; import { FileTree } from "../views/repos/FileTree.tsx"; import { NewRepo } from "../views/repos/NewRepo.tsx"; import { RepoHome } from "../views/repos/RepoHome.tsx"; import { RepoList } from "../views/repos/RepoList.tsx"; import { RepoSettings } from "../views/repos/RepoSettings.tsx"; async function getRepo(name: string, isAdmin: boolean) { if (!repoDiskExists(name)) return null; const repo = await ensureRepoRecord(name); if (repo.is_private && !isAdmin) return null; return repo; } async function mimeForContent(content: Buffer): Promise { const result = await fileTypeFromBuffer(content); if (result) return result.mime; return hasBinaryContent(content.subarray(0, 8000)) ? "application/octet-stream" : "text/plain; charset=utf-8"; } async function readReadme( repo: string, ref: string, dir = "", ): Promise { const prefix = dir ? `${dir}/` : ""; const [md, mdLc, readme, readmeLc] = await Promise.all([ git.show(repo, ref, `${prefix}README.md`), git.show(repo, ref, `${prefix}readme.md`), git.show(repo, ref, `${prefix}README`), git.show(repo, ref, `${prefix}readme`), ]); return md ?? mdLc ?? readme ?? readmeLc; } export const repoRoutes = new Elysia() .get("/allowed_signers", () => { return new Response(readFileSync(ALLOWED_SIGNERS_PATH), { headers: { "Content-Type": "text/plain; charset=utf-8" }, }); }) .guard({ cookie: t.Cookie({ session: t.Optional(t.String()), repo_sort: t.Optional(t.String()), }), }) .post( "/sort", ({ body }) => { const sort = body.sort === "name" ? "name" : "created"; return redirect( "/", `repo_sort=${sort}; Path=/; SameSite=Lax; Max-Age=${365 * 24 * 60 * 60}`, ); }, { body: t.Object({ sort: t.String() }) }, ) .get( "/", async ({ cookie, query }) => { const user = await resolveSession(cookie.session.value); const search = query.q?.trim() || undefined; const page = Math.max(1, query.page ?? 1); const sort = cookie.repo_sort.value === "name" ? "name" : "created"; const isAdmin = user?.isAdmin ?? false; const countResult = await db .selectFrom("repositories") .select(db.fn.countAll().as("count")) .where((eb) => isAdmin ? eb.or([ eb("is_private", "=", 0), eb("is_private", "=", 1), ]) : eb("is_private", "=", 0), ) .$if(!!search, (qb) => qb.where((eb) => eb.or([ eb("name", "like", `%${search}%`), eb("description", "like", `%${search}%`), ]), ), ) .executeTakeFirst(); const totalCount = Number(countResult?.count ?? 0); const totalPages = Math.max( 1, Math.ceil(totalCount / REPOS_PER_PAGE), ); const safePage = Math.min(page, totalPages); const repos = await db .selectFrom("repositories") .selectAll() .where((eb) => isAdmin ? eb.or([ eb("is_private", "=", 0), eb("is_private", "=", 1), ]) : eb("is_private", "=", 0), ) .$if(!!search, (qb) => qb.where((eb) => eb.or([ eb("name", "like", `%${search}%`), eb("description", "like", `%${search}%`), ]), ), ) .orderBy("is_pinned", "desc") .$if(sort === "name", (qb) => qb.orderBy("name", "asc")) .$if(sort === "created", (qb) => qb.orderBy("created_at", "desc"), ) .limit(REPOS_PER_PAGE) .offset((safePage - 1) * REPOS_PER_PAGE) .execute(); const searchParam = search ? `&q=${encodeURIComponent(search)}` : ""; const pagination = { page: safePage, totalPages, pageUrlTemplate: `/?page={page}${searchParam}`, }; return html( , ); }, { query: t.Object({ q: t.Optional(t.String()), page: t.Optional(t.Numeric()), }), }, ) .get("/new", async ({ cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAdmin(user); if (deny) return deny; return html(); }) .post( "/new", async ({ body, cookie }) => { const user = await resolveSession(cookie.session.value); const deny = requireAdmin(user); if (deny) return deny; const { name, description, is_private, default_branch } = body; if (!VALID_REPO_NAME_RE.test(name)) { return html( , ); } const branch = (default_branch?.trim() || "main").replace( /[^a-zA-Z0-9._/-]/g, "", ); const existing = await db .selectFrom("repositories") .select("id") .where("name", "=", name) .executeTakeFirst(); if (existing) { return html( , ); } const now = new Date().toISOString(); await db .insertInto("repositories") .values({ name, description: description || null, is_private: is_private === "1" ? 1 : 0, default_branch: branch, created_at: now, }) .execute(); // Initialise the git repo after the DB record is committed. If // git.init fails we roll back the DB record so the two stay in sync. try { await git.init(name, branch); } catch (err) { await db .deleteFrom("repositories") .where("name", "=", name) .execute(); throw err; } return new Response(null, { status: 302, headers: { Location: `/${name}` }, }); }, { body: t.Object({ name: t.String(), description: t.Optional(t.String()), is_private: t.Optional(t.String()), default_branch: t.Optional(t.String()), }), }, ) .get("/:repo", async ({ params, 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 hasContent = await git.hasCommits(repo.name); let readmeHtml: string | null = null; let entries: Awaited> = []; let branches: string[] = []; if (hasContent) { const [lsResult, branchResult, resolved] = await Promise.all([ git.lsTree(repo.name, repo.default_branch), git.branches(repo.name), git.resolveRef(repo.name, repo.default_branch), ]); entries = lsResult; branches = branchResult; const readmeBuf = await readReadme(repo.name, repo.default_branch); if (readmeBuf) { const key = resolved ? `readme:${repo.name}:${resolved}:` : undefined; readmeHtml = renderMarkdown(readmeBuf.toString("utf-8"), key); } } return html( , ); }) .get( "/:repo/branch-switch", 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 ref = query.rev?.trim(); if (!ref) return new Response(null, { status: 302, headers: { Location: `/${repo.name}` }, }); const view = query.view; const subpath = query.path ?? ""; if (view === "commits") { return new Response(null, { status: 302, headers: { Location: `/${repo.name}/commits/${ref}` }, }); } if (view === "blob" && subpath) { return new Response(null, { status: 302, headers: { Location: `/${repo.name}/blob/${ref}/${subpath}`, }, }); } const location = subpath ? `/${repo.name}/tree/${ref}/${subpath}` : `/${repo.name}/tree/${ref}`; return new Response(null, { status: 302, headers: { Location: location }, }); }, { query: t.Object({ rev: t.Optional(t.String()), view: t.Optional(t.String()), path: t.Optional(t.String()), }), }, ) .get("/:repo/tree/:ref", async ({ params, 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 resolved = await git.resolveRef(repo.name, params.ref); if (!resolved) return new Response("Not found", { status: 404 }); const [entries, branches] = await Promise.all([ git.lsTree(repo.name, params.ref), git.branches(repo.name), ]); const readmeBuf = await readReadme(repo.name, params.ref); const readmeHtml = readmeBuf ? renderMarkdown( readmeBuf.toString("utf-8"), `readme:${repo.name}:${resolved}:`, ) : null; return html( , ); }) .get("/:repo/tree/:ref/*", async ({ params, 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 resolved = await git.resolveRef(repo.name, params.ref); if (!resolved) return new Response("Not found", { status: 404 }); const subpath = decodeURIComponent(params["*"]); const [entries, branches] = await Promise.all([ git.lsTree(repo.name, params.ref, subpath), git.branches(repo.name), ]); if (entries.length === 0) { // Could be a file — redirect to blob return new Response(null, { status: 302, headers: { Location: `/${repo.name}/blob/${params.ref}/${subpath}`, }, }); } const readmeBuf = await readReadme(repo.name, params.ref, subpath); const readmeHtml = readmeBuf ? renderMarkdown( readmeBuf.toString("utf-8"), `readme:${repo.name}:${resolved}:${subpath}`, ) : null; return html( , ); }) .get("/:repo/blob/:ref/*", async ({ params, 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 filePath = decodeURIComponent(params["*"]); const [content, branches, commitSHA] = await Promise.all([ git.show(repo.name, params.ref, filePath), git.branches(repo.name), git.resolveRef(repo.name, params.ref), ]); if (!content || !commitSHA) return new Response("Not found", { status: 404 }); const filename = path.basename(filePath); const view = await serveFile( content, filename, `${repo.name}:${commitSHA}:${filePath}`, ); return html( , ); }) .get("/:repo/raw/:ref/*", async ({ params, cookie, request }) => { 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 filePath = decodeURIComponent(params["*"]); const content = await git.show(repo.name, params.ref, filePath); if (!content) return new Response("Not found", { status: 404 }); const filename = path.basename(filePath); const contentType = await mimeForContent(content); const total = content.length; const rangeHeader = request.headers.get("Range"); if (rangeHeader) { const match = rangeHeader.match(/bytes=(\d*)-(\d*)/); if (match) { const start = match[1] ? parseInt(match[1], 10) : 0; const end = match[2] ? parseInt(match[2], 10) : total - 1; const clampedEnd = Math.min(end, total - 1); return new Response(content.subarray(start, clampedEnd + 1), { status: 206, headers: { "Content-Type": contentType, "Content-Range": `bytes ${start}-${clampedEnd}/${total}`, "Accept-Ranges": "bytes", "Content-Length": String(clampedEnd - start + 1), }, }); } } return new Response(content, { headers: { "Content-Type": contentType, "Content-Disposition": `inline; filename="${filename}"`, "Content-Length": String(total), "Accept-Ranges": "bytes", }, }); }) .get( "/:repo/commits/:ref", async ({ params, cookie, query }) => { 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 }); // Cursor-based pagination — O(1) regardless of history depth. // `after` = SHA of the last commit on the previous page (resume cursor). // `prev` = the `after` value used on the page that linked here, so we can // reconstruct a "← Newer" link without a full history traversal. const after = query.after?.trim() || null; const prev = query.prev?.trim() || null; const [rawCommits, branches] = await Promise.all([ // When `after` is set: start at that SHA and skip it (--skip=1 is O(1)), // then fetch LIMIT+1 to detect whether another page exists. after ? git.log(repo.name, after, COMMITS_PER_PAGE + 1, 1) : git.log(repo.name, params.ref, COMMITS_PER_PAGE + 1, 0), git.branches(repo.name), ]); const hasNext = rawCommits.length > COMMITS_PER_PAGE; const commits = rawCommits.slice(0, COMMITS_PER_PAGE); // Build cursor URLs. // "Older" advances past the last commit on this page. // "Newer" goes back one page using the `prev` cursor saved in the URL, // or to the first page if we're on page 2. const base = `/${repo.name}/commits/${params.ref}`; const olderUrl = hasNext ? `${base}?after=${commits[commits.length - 1]?.hash}&prev=${after ?? ""}` : null; const newerUrl = after ? prev ? `${base}?after=${prev}` : base : null; return html( , ); }, { query: t.Object({ after: t.Optional(t.String()), prev: t.Optional(t.String()), }), }, ) .get("/:repo/commit/:sha", async ({ params, 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 [meta, rawDiff] = await Promise.all([ git.commitMeta(repo.name, params.sha), git.diff(repo.name, params.sha), ]); if (!meta) return new Response("Commit not found", { status: 404 }); const files = await prepareDiff( rawDiff, `commit:${repo.name}:${params.sha}`, repo.name, ); return html( , ); }) .get("/:repo/settings", 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 branches = await git.branches(repo.name); return html( , ); }) .post( "/:repo/settings", 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 { description, is_private, is_pinned, default_branch, issue_template, patch_template, } = body; const branches = await git.branches(repo.name); const newBranch = default_branch?.trim() || repo.default_branch; // Validate the selected branch exists (only if repo has commits) if (branches.length > 0 && !branches.includes(newBranch)) { return html( , ); } await db .updateTable("repositories") .set({ description: description?.trim() || null, is_private: is_private === "1" ? 1 : 0, is_pinned: is_pinned === "1" ? 1 : 0, default_branch: newBranch, issue_template: issue_template?.trim() || null, patch_template: patch_template?.trim() || null, }) .where("id", "=", repo.id) .execute(); // Keep git HEAD in sync if the branch actually exists if (branches.includes(newBranch)) { await git.setHead(repo.name, newBranch).catch(() => {}); } const updated = await db .selectFrom("repositories") .selectAll() .where("id", "=", repo.id) .executeTakeFirstOrThrow(); return html( , ); }, { body: t.Object({ description: t.Optional(t.String()), is_private: t.Optional(t.String()), is_pinned: t.Optional(t.String()), default_branch: t.Optional(t.String()), issue_template: t.Optional(t.String()), patch_template: t.Optional(t.String()), }), }, ) .post("/:repo/settings/delete", 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 }); // Remove the on-disk repo first. If this fails (e.g. permission error), // we abort before touching the DB so the repo remains accessible. rmSync(repoPath(repo.name), { recursive: true, force: true }); await db.deleteFrom("repositories").where("id", "=", repo.id).execute(); return new Response(null, { status: 302, headers: { Location: "/" } }); });