// c21 — handler: SAMA-native commit view at // GET /GIT/:repo/commit/:sha // and a raw-diff sibling at // GET /GIT/:repo/commit/:sha.diff // // Composes c14 (Forgejo HTTP), c31 (diff parser), c51 (render). The // route prefix is uppercase /GIT/ to make it visually distinct from // the markdown content sections (/sama, /blog, /guides). Owner is // implicit (single-tenant — LIVE_REPO_OWNER) and never appears in // the URL surface. import { renderNotFound, htmlResponse } from "./b51_render_layout.ts"; import { getCommit, getCommitDiff } from "./c14_git.ts"; import { LIVE_REPO_OWNER, LIVE_REPO_NAME } from "./a31_site_config.ts"; import { parseUnifiedDiff } from "./a31_diff_parse.ts"; import { renderCommitView } from "./b51_render_commit.ts"; // Repo + sha shape — paranoid because these go straight into a // Forgejo URL. Repo allows letters/digits/hyphens/underscores/dots; // sha is hex 7-64 (Forgejo accepts shortened SHAs but our render assumes // full ones because we use them in URLs). const SAFE_OWNER_REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/; const SAFE_SHA = /^[a-f0-9]{7,64}$/; const isValid = (repo: string, sha: string): boolean => SAFE_OWNER_REPO.test(repo) && SAFE_SHA.test(sha); export const commitViewHandler = async ( req: Request & { params: { repo: string; sha: string } }, ): Promise => { const { repo } = req.params; // The :sha param may carry a trailing ".diff" because the route // pattern doesn't have a separate one. Normalise + branch. const rawSha = req.params.sha; const wantsDiff = rawSha.endsWith(".diff"); const sha = wantsDiff ? rawSha.slice(0, -5) : rawSha; const fullPath = `/GIT/${repo}/commit/${rawSha}`; if (!isValid(repo, sha)) { const html = await renderNotFound(fullPath); return htmlResponse(html, 404); } // /GIT/ now serves only the local bare repo (LIVE_REPO_NAME via // c14_git). Other repos would historically have been proxied to // Forgejo for agent katas — that's a separate concern and // currently 404s. if (repo !== LIVE_REPO_NAME) { const html = await renderNotFound(fullPath); return htmlResponse(html, 404); } if (wantsDiff) { const diffText = await getCommitDiff(sha); if (diffText === null) { const html = await renderNotFound(fullPath); return htmlResponse(html, 404); } return new Response(diffText, { headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "public, max-age=300", }, }); } const commit = await getCommit(sha); if (commit === null) { const html = await renderNotFound(fullPath); return htmlResponse(html, 404); } const diffText = (await getCommitDiff(sha)) ?? ""; const diff = parseUnifiedDiff(diffText); // c14_git's GitCommit shape matches what c51_render_commit needs // (it used to take ForgejoCommitDetail; same field names + types). const detail = { sha: commit.sha, parents: commit.parents, authorName: commit.authorName, authorEmail: commit.authorEmail, authorDate: commit.authorDate, committerName: commit.committerName, committerEmail: commit.committerEmail, committerDate: commit.committerDate, message: commit.message, }; const html = await renderCommitView({ owner: LIVE_REPO_OWNER, repo, detail, diff, }); return htmlResponse(html); };