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.tsxBlame371 lines · 2 contributors
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
2c3ba6ecopilot-swe-agent[bot]118 aria-label="Search symbol name"
4c8f666Claude119 style="flex:1"
120 />
121 <button type="submit" class="btn">
122 Search
123 </button>
124 </form>
125
126 <div
127 style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:16px"
128 >
129 <div class="panel" style="padding:12px;text-align:center">
130 <div style="font-size:20px;font-weight:700">{total}</div>
131 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
132 Symbols
133 </div>
134 </div>
135 {byKindRaw.map((r) => (
136 <div class="panel" style="padding:12px;text-align:center">
137 <div style="font-size:20px;font-weight:700">{Number(r.n)}</div>
138 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
139 {r.kind}
140 </div>
141 </div>
142 ))}
143 </div>
144
145 <h3>A–Z</h3>
146 {total === 0 ? (
147 <div class="panel-empty" style="padding:24px">
148 No symbols indexed yet.
149 {isOwner && " Click Reindex to scan the repository."}
150 </div>
151 ) : (
152 <div class="panel">
153 {latest.map((s) => (
154 <div class="panel-item" style="justify-content:space-between">
155 <div>
156 <a
157 href={`/${ownerName}/${repoName}/symbols/${encodeURIComponent(s.name)}`}
158 style="font-weight:600;font-family:var(--font-mono)"
159 >
160 {s.name}
161 </a>{" "}
162 <span
163 style="font-size:11px;color:var(--text-muted);text-transform:uppercase;margin-left:6px"
164 >
165 {s.kind}
166 </span>
167 </div>
168 <a
169 href={`/${ownerName}/${repoName}/blob/HEAD/${s.path}#L${s.line}`}
170 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
171 >
172 {s.path}:{s.line}
173 </a>
174 </div>
175 ))}
176 </div>
177 )}
178 </div>
179 </Layout>
180 );
181});
182
183// ---------- Search ----------
184
185symbols.get("/:owner/:repo/symbols/search", async (c) => {
186 const user = c.get("user");
187 const { owner: ownerName, repo: repoName } = c.req.param();
188 const ctx = await loadRepo(ownerName, repoName);
189 if (!ctx) return c.notFound();
190 const { repo } = ctx;
191 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
192 return c.notFound();
193 }
194
195 const q = (c.req.query("q") || "").trim();
196 const results = q
197 ? await db
198 .select({
199 name: codeSymbols.name,
200 kind: codeSymbols.kind,
201 path: codeSymbols.path,
202 line: codeSymbols.line,
203 })
204 .from(codeSymbols)
205 .where(
206 and(
207 eq(codeSymbols.repositoryId, repo.id),
208 ilike(codeSymbols.name, `${q}%`)
209 )
210 )
211 .orderBy(asc(codeSymbols.name))
212 .limit(200)
213 : [];
214
215 return c.html(
216 <Layout title={`Symbol search — ${ownerName}/${repoName}`} user={user}>
217 <RepoHeader owner={ownerName} repo={repoName} />
218 <RepoNav owner={ownerName} repo={repoName} active="code" />
219 <div class="settings-container">
220 <h2>Symbol search</h2>
221 <form
001af43Claude222 method="get"
4c8f666Claude223 action={`/${ownerName}/${repoName}/symbols/search`}
224 style="display:flex;gap:8px;margin:12px 0"
225 >
226 <input
227 type="text"
228 name="q"
229 value={q}
230 placeholder="Search symbol name..."
231 required
2c3ba6ecopilot-swe-agent[bot]232 aria-label="Search symbol name"
4c8f666Claude233 style="flex:1"
234 />
235 <button type="submit" class="btn">
236 Search
237 </button>
238 <a href={`/${ownerName}/${repoName}/symbols`} class="btn">
239 Back
240 </a>
241 </form>
242 {q === "" ? (
243 <p style="color:var(--text-muted)">Enter a prefix to search.</p>
244 ) : results.length === 0 ? (
245 <p style="color:var(--text-muted)">No symbols match "{q}".</p>
246 ) : (
247 <div class="panel">
248 {results.map((s) => (
249 <div class="panel-item" style="justify-content:space-between">
250 <div>
251 <a
252 href={`/${ownerName}/${repoName}/symbols/${encodeURIComponent(s.name)}`}
253 style="font-weight:600;font-family:var(--font-mono)"
254 >
255 {s.name}
256 </a>{" "}
257 <span
258 style="font-size:11px;color:var(--text-muted);text-transform:uppercase;margin-left:6px"
259 >
260 {s.kind}
261 </span>
262 </div>
263 <a
264 href={`/${ownerName}/${repoName}/blob/HEAD/${s.path}#L${s.line}`}
265 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
266 >
267 {s.path}:{s.line}
268 </a>
269 </div>
270 ))}
271 </div>
272 )}
273 </div>
274 </Layout>
275 );
276});
277
278// ---------- Symbol detail ----------
279
280symbols.get("/:owner/:repo/symbols/:name", async (c) => {
281 const user = c.get("user");
282 const { owner: ownerName, repo: repoName, name } = c.req.param();
283 const ctx = await loadRepo(ownerName, repoName);
284 if (!ctx) return c.notFound();
285 const { repo } = ctx;
286 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
287 return c.notFound();
288 }
289
290 const defs = await findDefinitions(repo.id, decodeURIComponent(name));
291
292 return c.html(
293 <Layout title={`${name} — ${ownerName}/${repoName}`} user={user}>
294 <RepoHeader owner={ownerName} repo={repoName} />
295 <RepoNav owner={ownerName} repo={repoName} active="code" />
296 <div class="settings-container">
297 <h2 style="font-family:var(--font-mono)">{decodeURIComponent(name)}</h2>
298 <p style="color:var(--text-muted)">
299 {defs.length} definition{defs.length === 1 ? "" : "s"}
300 </p>
301 {defs.length === 0 ? (
302 <div class="panel-empty" style="padding:24px">
303 No definitions found.{" "}
304 <a href={`/${ownerName}/${repoName}/symbols`}>Back to symbols</a>
305 </div>
306 ) : (
307 <div class="panel">
308 {defs.map((d) => (
309 <div class="panel-item" style="flex-direction:column;align-items:stretch;gap:4px">
310 <div style="display:flex;justify-content:space-between;align-items:center">
311 <span
312 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
313 >
314 {d.kind}
315 </span>
316 <a
317 href={`/${ownerName}/${repoName}/blob/HEAD/${d.path}#L${d.line}`}
318 style="font-size:12px;font-family:var(--font-mono)"
319 >
320 {d.path}:{d.line}
321 </a>
322 </div>
323 {d.signature && (
324 <pre
325 style="margin:0;padding:8px;background:var(--bg-subtle);border-radius:4px;font-size:12px;overflow-x:auto"
326 >
327 {d.signature}
328 </pre>
329 )}
330 </div>
331 ))}
332 </div>
333 )}
334 <p style="margin-top:20px">
335 <a href={`/${ownerName}/${repoName}/symbols`}>← Back to symbols</a>
336 </p>
337 </div>
338 </Layout>
339 );
340});
341
342// ---------- Reindex ----------
343
344symbols.post("/:owner/:repo/symbols/reindex", requireAuth, async (c) => {
345 const user = c.get("user")!;
346 const { owner: ownerName, repo: repoName } = c.req.param();
347 const ctx = await loadRepo(ownerName, repoName);
348 if (!ctx) return c.notFound();
349 const { repo } = ctx;
350 if (user.id !== repo.ownerId) {
351 return c.html(
352 <Layout title="Forbidden" user={user}>
353 <div class="empty-state">
354 <h2>403</h2>
355 <p>Only the repository owner can reindex symbols.</p>
356 </div>
357 </Layout>,
358 403
359 );
360 }
361
362 const result = await indexRepositorySymbols(repo.id);
363 const msg = result
364 ? `Indexed ${result.indexed} symbols across ${result.files} files.`
365 : "Indexing failed — see server logs.";
366 return c.redirect(
367 `/${ownerName}/${repoName}/symbols?message=${encodeURIComponent(msg)}`
368 );
369});
370
371export default symbols;