Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

symbols.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

symbols.tsxBlame369 lines · 1 contributor
4c8f666Claude1/**
2 * Block I8 — Symbol / xref navigation.
3 *
4 * GET /:owner/:repo/symbols — overview + "Reindex" button
5 * GET /:owner/:repo/symbols/search — search by name (prefix match)
6 * GET /:owner/:repo/symbols/:name — definitions list
7 * POST /:owner/:repo/symbols/reindex — owner-only, runs indexer
8 */
9
10import { Hono } from "hono";
11import { and, asc, eq, ilike, sql } from "drizzle-orm";
12import { db } from "../db";
13import { codeSymbols, repositories, users } from "../db/schema";
14import { Layout } from "../views/layout";
15import { RepoHeader, RepoNav } from "../views/components";
16import { softAuth, requireAuth } from "../middleware/auth";
17import type { AuthEnv } from "../middleware/auth";
18import { indexRepositorySymbols, findDefinitions } from "../lib/symbols";
19
20const symbols = new Hono<AuthEnv>();
21symbols.use("*", softAuth);
22
23async function loadRepo(ownerName: string, repoName: string) {
24 const [owner] = await db
25 .select()
26 .from(users)
27 .where(eq(users.username, ownerName))
28 .limit(1);
29 if (!owner) return null;
30 const [repo] = await db
31 .select()
32 .from(repositories)
33 .where(
34 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
35 )
36 .limit(1);
37 if (!repo) return null;
38 return { owner, repo };
39}
40
41// ---------- Overview ----------
42
43symbols.get("/:owner/:repo/symbols", async (c) => {
44 const user = c.get("user");
45 const { owner: ownerName, repo: repoName } = c.req.param();
46 const ctx = await loadRepo(ownerName, repoName);
47 if (!ctx) return c.notFound();
48 const { owner, repo } = ctx;
49 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
50 return c.notFound();
51 }
52
53 const [countRow] = await db
54 .select({ n: sql<number>`count(*)::int` })
55 .from(codeSymbols)
56 .where(eq(codeSymbols.repositoryId, repo.id));
57 const total = Number(countRow?.n || 0);
58
59 const byKindRaw = await db
60 .select({
61 kind: codeSymbols.kind,
62 n: sql<number>`count(*)::int`,
63 })
64 .from(codeSymbols)
65 .where(eq(codeSymbols.repositoryId, repo.id))
66 .groupBy(codeSymbols.kind);
67
68 const latest = await db
69 .select({
70 name: codeSymbols.name,
71 kind: codeSymbols.kind,
72 path: codeSymbols.path,
73 line: codeSymbols.line,
74 })
75 .from(codeSymbols)
76 .where(eq(codeSymbols.repositoryId, repo.id))
77 .orderBy(asc(codeSymbols.name))
78 .limit(50);
79
80 const isOwner = user && user.id === repo.ownerId;
81 const message = c.req.query("message");
82
83 return c.html(
84 <Layout title={`Symbols — ${ownerName}/${repoName}`} user={user}>
85 <RepoHeader owner={ownerName} repo={repoName} />
86 <RepoNav owner={ownerName} repo={repoName} active="code" />
87 <div class="settings-container">
88 <div style="display:flex;justify-content:space-between;align-items:center">
89 <h2 style="margin:0">Symbols</h2>
90 {isOwner && (
001af43Claude91 <form method="post" action={`/${ownerName}/${repoName}/symbols/reindex`}>
4c8f666Claude92 <button type="submit" class="btn btn-primary btn-sm">
93 Reindex
94 </button>
95 </form>
96 )}
97 </div>
98 {message && (
99 <div class="auth-success" style="margin-top:12px">
100 {decodeURIComponent(message)}
101 </div>
102 )}
103 <p style="color:var(--text-muted);margin-top:8px">
104 Top-level definitions from the default branch. Click a symbol to
105 see all its definitions.
106 </p>
107
108 <form
001af43Claude109 method="get"
4c8f666Claude110 action={`/${ownerName}/${repoName}/symbols/search`}
111 style="display:flex;gap:8px;margin:16px 0"
112 >
113 <input
114 type="text"
115 name="q"
116 placeholder="Search symbol name..."
117 required
118 style="flex:1"
119 />
120 <button type="submit" class="btn">
121 Search
122 </button>
123 </form>
124
125 <div
126 style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:16px"
127 >
128 <div class="panel" style="padding:12px;text-align:center">
129 <div style="font-size:20px;font-weight:700">{total}</div>
130 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
131 Symbols
132 </div>
133 </div>
134 {byKindRaw.map((r) => (
135 <div class="panel" style="padding:12px;text-align:center">
136 <div style="font-size:20px;font-weight:700">{Number(r.n)}</div>
137 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
138 {r.kind}
139 </div>
140 </div>
141 ))}
142 </div>
143
144 <h3>A–Z</h3>
145 {total === 0 ? (
146 <div class="panel-empty" style="padding:24px">
147 No symbols indexed yet.
148 {isOwner && " Click Reindex to scan the repository."}
149 </div>
150 ) : (
151 <div class="panel">
152 {latest.map((s) => (
153 <div class="panel-item" style="justify-content:space-between">
154 <div>
155 <a
156 href={`/${ownerName}/${repoName}/symbols/${encodeURIComponent(s.name)}`}
157 style="font-weight:600;font-family:var(--font-mono)"
158 >
159 {s.name}
160 </a>{" "}
161 <span
162 style="font-size:11px;color:var(--text-muted);text-transform:uppercase;margin-left:6px"
163 >
164 {s.kind}
165 </span>
166 </div>
167 <a
168 href={`/${ownerName}/${repoName}/blob/HEAD/${s.path}#L${s.line}`}
169 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
170 >
171 {s.path}:{s.line}
172 </a>
173 </div>
174 ))}
175 </div>
176 )}
177 </div>
178 </Layout>
179 );
180});
181
182// ---------- Search ----------
183
184symbols.get("/:owner/:repo/symbols/search", async (c) => {
185 const user = c.get("user");
186 const { owner: ownerName, repo: repoName } = c.req.param();
187 const ctx = await loadRepo(ownerName, repoName);
188 if (!ctx) return c.notFound();
189 const { repo } = ctx;
190 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
191 return c.notFound();
192 }
193
194 const q = (c.req.query("q") || "").trim();
195 const results = q
196 ? await db
197 .select({
198 name: codeSymbols.name,
199 kind: codeSymbols.kind,
200 path: codeSymbols.path,
201 line: codeSymbols.line,
202 })
203 .from(codeSymbols)
204 .where(
205 and(
206 eq(codeSymbols.repositoryId, repo.id),
207 ilike(codeSymbols.name, `${q}%`)
208 )
209 )
210 .orderBy(asc(codeSymbols.name))
211 .limit(200)
212 : [];
213
214 return c.html(
215 <Layout title={`Symbol search — ${ownerName}/${repoName}`} user={user}>
216 <RepoHeader owner={ownerName} repo={repoName} />
217 <RepoNav owner={ownerName} repo={repoName} active="code" />
218 <div class="settings-container">
219 <h2>Symbol search</h2>
220 <form
001af43Claude221 method="get"
4c8f666Claude222 action={`/${ownerName}/${repoName}/symbols/search`}
223 style="display:flex;gap:8px;margin:12px 0"
224 >
225 <input
226 type="text"
227 name="q"
228 value={q}
229 placeholder="Search symbol name..."
230 required
231 style="flex:1"
232 />
233 <button type="submit" class="btn">
234 Search
235 </button>
236 <a href={`/${ownerName}/${repoName}/symbols`} class="btn">
237 Back
238 </a>
239 </form>
240 {q === "" ? (
241 <p style="color:var(--text-muted)">Enter a prefix to search.</p>
242 ) : results.length === 0 ? (
243 <p style="color:var(--text-muted)">No symbols match "{q}".</p>
244 ) : (
245 <div class="panel">
246 {results.map((s) => (
247 <div class="panel-item" style="justify-content:space-between">
248 <div>
249 <a
250 href={`/${ownerName}/${repoName}/symbols/${encodeURIComponent(s.name)}`}
251 style="font-weight:600;font-family:var(--font-mono)"
252 >
253 {s.name}
254 </a>{" "}
255 <span
256 style="font-size:11px;color:var(--text-muted);text-transform:uppercase;margin-left:6px"
257 >
258 {s.kind}
259 </span>
260 </div>
261 <a
262 href={`/${ownerName}/${repoName}/blob/HEAD/${s.path}#L${s.line}`}
263 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
264 >
265 {s.path}:{s.line}
266 </a>
267 </div>
268 ))}
269 </div>
270 )}
271 </div>
272 </Layout>
273 );
274});
275
276// ---------- Symbol detail ----------
277
278symbols.get("/:owner/:repo/symbols/:name", async (c) => {
279 const user = c.get("user");
280 const { owner: ownerName, repo: repoName, name } = c.req.param();
281 const ctx = await loadRepo(ownerName, repoName);
282 if (!ctx) return c.notFound();
283 const { repo } = ctx;
284 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
285 return c.notFound();
286 }
287
288 const defs = await findDefinitions(repo.id, decodeURIComponent(name));
289
290 return c.html(
291 <Layout title={`${name} — ${ownerName}/${repoName}`} user={user}>
292 <RepoHeader owner={ownerName} repo={repoName} />
293 <RepoNav owner={ownerName} repo={repoName} active="code" />
294 <div class="settings-container">
295 <h2 style="font-family:var(--font-mono)">{decodeURIComponent(name)}</h2>
296 <p style="color:var(--text-muted)">
297 {defs.length} definition{defs.length === 1 ? "" : "s"}
298 </p>
299 {defs.length === 0 ? (
300 <div class="panel-empty" style="padding:24px">
301 No definitions found.{" "}
302 <a href={`/${ownerName}/${repoName}/symbols`}>Back to symbols</a>
303 </div>
304 ) : (
305 <div class="panel">
306 {defs.map((d) => (
307 <div class="panel-item" style="flex-direction:column;align-items:stretch;gap:4px">
308 <div style="display:flex;justify-content:space-between;align-items:center">
309 <span
310 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
311 >
312 {d.kind}
313 </span>
314 <a
315 href={`/${ownerName}/${repoName}/blob/HEAD/${d.path}#L${d.line}`}
316 style="font-size:12px;font-family:var(--font-mono)"
317 >
318 {d.path}:{d.line}
319 </a>
320 </div>
321 {d.signature && (
322 <pre
323 style="margin:0;padding:8px;background:var(--bg-subtle);border-radius:4px;font-size:12px;overflow-x:auto"
324 >
325 {d.signature}
326 </pre>
327 )}
328 </div>
329 ))}
330 </div>
331 )}
332 <p style="margin-top:20px">
333 <a href={`/${ownerName}/${repoName}/symbols`}>← Back to symbols</a>
334 </p>
335 </div>
336 </Layout>
337 );
338});
339
340// ---------- Reindex ----------
341
342symbols.post("/:owner/:repo/symbols/reindex", requireAuth, async (c) => {
343 const user = c.get("user")!;
344 const { owner: ownerName, repo: repoName } = c.req.param();
345 const ctx = await loadRepo(ownerName, repoName);
346 if (!ctx) return c.notFound();
347 const { repo } = ctx;
348 if (user.id !== repo.ownerId) {
349 return c.html(
350 <Layout title="Forbidden" user={user}>
351 <div class="empty-state">
352 <h2>403</h2>
353 <p>Only the repository owner can reindex symbols.</p>
354 </div>
355 </Layout>,
356 403
357 );
358 }
359
360 const result = await indexRepositorySymbols(repo.id);
361 const msg = result
362 ? `Indexed ${result.indexed} symbols across ${result.files} files.`
363 : "Indexing failed — see server logs.";
364 return c.redirect(
365 `/${ownerName}/${repoName}/symbols?message=${encodeURIComponent(msg)}`
366 );
367});
368
369export default symbols;