syntaxai/tdd.md · main · src / d21_handlers_commit_view.ts

d21_handlers_commit_view.ts 95 lines · 3372 bytes raw
// 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<Response> => {
  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);
};