Skip to content
File

Blob: src/worker/routes/ui/overview.ts

typescript136 lines
1import type { HeadInfo, Ref } from "@/worker/git";
2import { readPath } from "@/worker/git";
3import { classifyRef, formatRefOption, shortRefName } from "@/shared/git/ref-display";
4import { isValidOwnerRepo, bytesToText } from "@/shared/web";
5import { buildCacheKeyFrom, cacheOrLoadJSONForRequest } from "@/worker/cache";
6import { findNamespaceBySlug } from "@/worker/db/d1/dal/namespaces";
7import { listRepositoriesForNamespace } from "@/worker/db/d1/dal/repositories";
8import { loadViewer } from "@/worker/auth/session";
9import {
10 badRequest,
11 loadHeadAndRefsCached,
12 loadUiRepoActivity,
13 notFound,
14 resolveUiRepoAccess,
15} from "./helpers";
16import type { AppContext } from "../hono";
17import { renderUiDocumentResponse } from "../uiResponse";
18 
19export async function handleOwnerOverview(c: AppContext<"/:owner">) {
20 const env = c.env;
21 const owner = c.req.param("owner");
22 if (!isValidOwnerRepo(owner)) {
23 return badRequest(env, "Invalid owner", "Owner contains invalid characters or length");
24 }
25 const db = c.var.db;
26 const namespace = await findNamespaceBySlug(db, owner);
27 if (!namespace) {
28 // Namespace rows are the owner-listing authority.
29 return await notFound(c);
30 }
31 const viewer = await loadViewer(c);
32 const repos = await listRepositoriesForNamespace(db, namespace.id, viewer?.userId ?? null);
33 const includesPrivate = repos.some((row) => row.visibility === "private");
34 return renderUiDocumentResponse(
35 env,
36 "owner",
37 {
38 title: `${owner} · Repositories`,
39 owner,
40 repos: repos.map((row) => ({
41 slug: row.slug,
42 visibility: row.visibility,
43 })),
44 },
45 {
46 // Public-only listings cache briefly per-colo. As soon as a private
47 // row enters the response, we must not cache (membership-derived).
48 cacheControl: includesPrivate ? "no-store" : "public, max-age=60",
49 failureBody: "Failed to render view",
50 viewer,
51 }
52 );
53}
54 
55export async function handleRepoOverview(c: AppContext<"/:owner/:repo">) {
56 const env = c.env;
57 const owner = c.req.param("owner");
58 const repo = c.req.param("repo");
59 const access = await resolveUiRepoAccess(c, owner, repo);
60 if (access.kind === "response") return access.response;
61 const { route, cacheCtx, viewer } = access;
62 const repoId = route.doName;
63 
64 const refsData = await loadHeadAndRefsCached(env, cacheCtx, repoId);
65 const head: HeadInfo | undefined = refsData?.head;
66 const refs: Ref[] = refsData?.refs || [];
67 
68 const defaultRef = head?.target || (refs[0]?.name ?? "refs/heads/main");
69 const refShort = shortRefName(defaultRef);
70 const refEnc = encodeURIComponent(refShort);
71 const branchesData = refs
72 .filter((ref) => classifyRef(ref.name) === "branch")
73 .map(formatRefOption);
74 const tagsData = refs.filter((ref) => classifyRef(ref.name) === "tag").map(formatRefOption);
75 
76 const readReadme = async (): Promise<{ md: string } | null> => {
77 try {
78 const candidates = ["README.md", "README.MD", "Readme.md", "README", "readme.md"];
79 const results = await Promise.all(
80 candidates.map(async (name) => {
81 try {
82 const res = await readPath(env, repoId, refShort, name, cacheCtx);
83 if (res.type === "blob") {
84 return { name, content: res.content };
85 }
86 } catch {}
87 return null;
88 })
89 );
90 const found = results.find((r) => r !== null) as {
91 name: string;
92 content: Uint8Array;
93 } | null;
94 if (!found) return null;
95 const text = bytesToText(found.content);
96 return { md: text };
97 } catch {
98 return null;
99 }
100 };
101 
102 const cacheKeyReadme = buildCacheKeyFrom(c.req.raw, "/_cache/readme", {
103 repo: repoId,
104 ref: refShort,
105 });
106 const readmeData = await cacheOrLoadJSONForRequest<{ md: string }>(
107 cacheCtx,
108 cacheKeyReadme,
109 readReadme,
110 300
111 );
112 const readmeMd = readmeData?.md || "";
113 const progress = await loadUiRepoActivity(env, access);
114 
115 return renderUiDocumentResponse(
116 env,
117 "overview",
118 {
119 title: `${owner}/${repo}`,
120 owner,
121 repo,
122 refShort,
123 refEnc,
124 branches: branchesData,
125 tags: tagsData,
126 readmeMd,
127 progress,
128 },
129 {
130 cacheControl: route.visibility === "private" ? "no-store" : undefined,
131 failureBody: "Failed to render view",
132 viewer,
133 }
134 );
135}