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

gists.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.

gists.tsxBlame810 lines · 2 contributors
1e162a8Claude1/**
2 * Block E4 — Gists: user-owned tiny multi-file repos.
3 *
4 * DB-backed v1 (no git bare repo). Each gist owns a collection of gist_files,
5 * and every edit appends a gist_revisions row with a JSON snapshot of the
6 * full file set at that revision.
7 *
8 * Never throws — all DB paths wrapped in try/catch; any failure redirects.
9 */
10
11import { Hono } from "hono";
12import { and, eq, desc, sql } from "drizzle-orm";
13import { randomBytes } from "crypto";
14import { db } from "../db";
15import {
16 gists,
17 gistFiles,
18 gistRevisions,
19 gistStars,
20 users,
21} from "../db/schema";
22import { Layout } from "../views/layout";
23import { highlightCode } from "../lib/highlight";
24import { softAuth, requireAuth } from "../middleware/auth";
25import type { AuthEnv } from "../middleware/auth";
26import { html } from "hono/html";
27
28export function generateSlug(): string {
29 return randomBytes(4).toString("hex");
30}
31
32export function snapshotOf(
33 files: { filename: string; content: string }[]
34): string {
35 const map: Record<string, string> = {};
36 for (const f of files) map[f.filename] = f.content;
37 return JSON.stringify(map);
38}
39
40const gistRoutes = new Hono<AuthEnv>();
41
42function notFound(user: any, label = "Gist not found") {
43 return (
44 <Layout title={label} user={user}>
45 <div class="empty-state">
46 <h2>{label}</h2>
47 </div>
48 </Layout>
49 );
50}
51
52// Discover / list public gists
53gistRoutes.get("/gists", softAuth, async (c) => {
54 const user = c.get("user");
55 const page = Math.max(1, Number(c.req.query("page")) || 1);
56 const limit = 30;
57 const offset = (page - 1) * limit;
58
59 let rows: any[] = [];
60 try {
61 rows = await db
62 .select({
63 g: gists,
64 owner: { username: users.username },
65 fileCount: sql<number>`(SELECT count(*) FROM gist_files WHERE gist_id = ${gists.id})`,
66 starCount: sql<number>`(SELECT count(*) FROM gist_stars WHERE gist_id = ${gists.id})`,
67 })
68 .from(gists)
69 .innerJoin(users, eq(gists.ownerId, users.id))
70 .where(eq(gists.isPublic, true))
71 .orderBy(desc(gists.updatedAt))
72 .limit(limit)
73 .offset(offset);
74 } catch {
75 rows = [];
76 }
77
78 return c.html(
79 <Layout title="Discover gists" user={user}>
80 <div style="display: flex; justify-content: space-between; align-items: center; margin: 16px 0;">
81 <h1 style="margin: 0;">Discover gists</h1>
82 {user && (
83 <a href="/gists/new" class="btn btn-primary">
84 + New gist
85 </a>
86 )}
87 </div>
88 {rows.length === 0 ? (
89 <div class="empty-state">
90 <p>No public gists yet.</p>
91 </div>
92 ) : (
93 <div class="commit-list">
94 {rows.map((r) => (
95 <div class="commit-item">
96 <div>
97 <div class="commit-message">
98 <a href={`/gists/${r.g.slug}`}>
99 <strong>{r.g.title || r.g.slug}</strong>
100 </a>
101 </div>
102 <div class="commit-meta">
103 by <a href={`/${r.owner.username}`}>@{r.owner.username}</a>{" "}
104 · {r.fileCount} file{r.fileCount !== 1 ? "s" : ""} · ★{" "}
105 {r.starCount}
106 {r.g.description && ` · ${r.g.description}`}
107 </div>
108 </div>
109 <a href={`/gists/${r.g.slug}`} class="commit-sha">
110 {r.g.slug}
111 </a>
112 </div>
113 ))}
114 </div>
115 )}
116 {(rows.length === limit || page > 1) && (
117 <div style="margin-top: 16px;">
118 {page > 1 && <a href={`/gists?page=${page - 1}`}>← prev</a>}
119 {" "}
120 {rows.length === limit && (
121 <a href={`/gists?page=${page + 1}`}>next →</a>
122 )}
123 </div>
124 )}
125 </Layout>
126 );
127});
128
129// New gist form
130gistRoutes.get("/gists/new", requireAuth, async (c) => {
131 const user = c.get("user");
132 return c.html(
133 <Layout title="New gist" user={user}>
134 <h1 style="margin-top: 20px;">Create a gist</h1>
135 <form
cce7944Claude136 method="post"
1e162a8Claude137 action="/gists"
138 style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;"
139 >
140 <input
141 type="text"
142 name="description"
143 placeholder="Gist description..."
2c3ba6ecopilot-swe-agent[bot]144 aria-label="Gist description"
1e162a8Claude145 style="padding: 8px;"
146 />
147 <div style="display: flex; gap: 16px;">
148 <label>
149 <input type="radio" name="is_public" value="true" checked />{" "}
150 Public
151 </label>
152 <label>
153 <input type="radio" name="is_public" value="false" /> Secret
154 </label>
155 </div>
156 <div id="files">
157 <div class="gist-file" style="border: 1px solid var(--border); padding: 12px; margin-bottom: 8px;">
158 <input
159 type="text"
160 name="filename[]"
161 placeholder="filename.ext"
162 required
2c3ba6ecopilot-swe-agent[bot]163 aria-label="Filename"
1e162a8Claude164 style="padding: 6px; width: 300px;"
165 />
166 <textarea
167 name="content[]"
168 rows={12}
169 placeholder="File contents..."
170 required
171 style="width: 100%; padding: 8px; font-family: monospace; margin-top: 8px;"
172 ></textarea>
173 </div>
174 </div>
175 <button
176 type="button"
177 class="btn"
178 id="add-file"
179 style="align-self: flex-start;"
180 >
181 + Add file
182 </button>
183 <button type="submit" class="btn btn-primary">
184 Create gist
185 </button>
186 </form>
187 {html`
188 <script>
189 document.getElementById("add-file").addEventListener("click", () => {
190 const div = document.createElement("div");
191 div.className = "gist-file";
192 div.style.cssText = "border: 1px solid var(--border); padding: 12px; margin-bottom: 8px;";
193 div.innerHTML =
194 '<input type="text" name="filename[]" placeholder="filename.ext" required style="padding: 6px; width: 300px;" />' +
195 '<textarea name="content[]" rows="12" placeholder="File contents..." required style="width: 100%; padding: 8px; font-family: monospace; margin-top: 8px;"></textarea>';
196 document.getElementById("files").appendChild(div);
197 });
198 </script>
199 `}
200 </Layout>
201 );
202});
203
204// Create gist
205gistRoutes.post("/gists", requireAuth, async (c) => {
206 const user = c.get("user")!;
207 const form = await c.req.formData();
208 const description = (form.get("description") as string || "").trim();
209 const isPublic = (form.get("is_public") as string) !== "false";
210 const filenames = form.getAll("filename[]") as string[];
211 const contents = form.getAll("content[]") as string[];
212
213 const files = filenames
214 .map((fn, i) => ({
215 filename: (fn || "").trim(),
216 content: contents[i] || "",
217 }))
218 .filter((f) => f.filename && f.content);
219
220 if (files.length === 0) {
221 return c.text("At least one file is required", 400);
222 }
223
224 // Retry on unique slug collision
225 for (let attempt = 0; attempt < 5; attempt++) {
226 const slug = generateSlug();
227 try {
228 const [gist] = await db
229 .insert(gists)
230 .values({
231 ownerId: user.id,
232 slug,
233 title: files[0].filename,
234 description,
235 isPublic,
236 })
237 .returning({ id: gists.id });
238 await db.insert(gistFiles).values(
239 files.map((f) => ({
240 gistId: gist.id,
241 filename: f.filename,
242 content: f.content,
243 sizeBytes: new TextEncoder().encode(f.content).length,
244 }))
245 );
246 await db.insert(gistRevisions).values({
247 gistId: gist.id,
248 revision: 1,
249 snapshot: snapshotOf(files),
250 authorId: user.id,
251 message: "Initial",
252 });
253 return c.redirect(`/gists/${slug}`);
254 } catch (err: any) {
255 if (attempt === 4) {
256 return c.text("Could not create gist", 500);
257 }
258 // Otherwise assume slug collision, retry with fresh slug.
259 }
260 }
261 return c.redirect("/gists");
262});
263
264// View gist
265gistRoutes.get("/gists/:slug", softAuth, async (c) => {
266 const user = c.get("user");
267 const slug = c.req.param("slug");
268
269 let gist: any = null;
270 let files: any[] = [];
271 let starCount = 0;
272 let isStarred = false;
273 try {
274 const [row] = await db
275 .select({ g: gists, owner: { username: users.username } })
276 .from(gists)
277 .innerJoin(users, eq(gists.ownerId, users.id))
278 .where(eq(gists.slug, slug))
279 .limit(1);
280 if (row) {
281 gist = row;
282 files = await db
283 .select()
284 .from(gistFiles)
285 .where(eq(gistFiles.gistId, gist.g.id))
286 .orderBy(gistFiles.filename);
287 const [cnt] = await db
288 .select({ n: sql<number>`count(*)` })
289 .from(gistStars)
290 .where(eq(gistStars.gistId, gist.g.id));
291 starCount = Number(cnt?.n || 0);
292 if (user) {
293 const [has] = await db
294 .select()
295 .from(gistStars)
296 .where(
297 and(
298 eq(gistStars.gistId, gist.g.id),
299 eq(gistStars.userId, user.id)
300 )
301 )
302 .limit(1);
303 isStarred = !!has;
304 }
305 }
306 } catch {
307 // leave null
308 }
309
310 if (!gist) return c.html(notFound(user), 404);
311
312 const isOwner = user && user.id === gist.g.ownerId;
313 if (!gist.g.isPublic && !isOwner) {
314 return c.html(notFound(user), 404);
315 }
316
317 return c.html(
318 <Layout title={gist.g.title || slug} user={user}>
319 <div style="display: flex; justify-content: space-between; align-items: center; margin: 16px 0;">
320 <div>
321 <h1 style="margin: 0;">
322 <a href={`/${gist.owner.username}`}>@{gist.owner.username}</a>{" "}
323 <span style="color: var(--text-muted);">/</span>{" "}
324 {gist.g.title || slug}
325 {!gist.g.isPublic && <span class="badge">Secret</span>}
326 </h1>
327 {gist.g.description && (
328 <div style="color: var(--text-muted); margin-top: 4px;">
329 {gist.g.description}
330 </div>
331 )}
332 </div>
333 <div style="display: flex; gap: 8px;">
334 {user && !isOwner && (
335 <form
cce7944Claude336 method="post"
1e162a8Claude337 action={`/gists/${slug}/star`}
338 style="display: inline;"
339 >
340 <button
341 type="submit"
342 class={`star-btn${isStarred ? " starred" : ""}`}
343 >
344 {isStarred ? "★" : "☆"} {starCount}
345 </button>
346 </form>
347 )}
348 {!user && (
349 <span class="star-btn">☆ {starCount}</span>
350 )}
351 <a href={`/gists/${slug}/revisions`} class="btn">
352 Revisions
353 </a>
354 {isOwner && (
355 <>
356 <a href={`/gists/${slug}/edit`} class="btn">
357 Edit
358 </a>
359 <form
cce7944Claude360 method="post"
1e162a8Claude361 action={`/gists/${slug}/delete`}
362 style="display: inline;"
363 onsubmit="return confirm('Delete this gist?')"
364 >
365 <button type="submit" class="btn">
366 Delete
367 </button>
368 </form>
369 </>
370 )}
371 </div>
372 </div>
373 {files.map((f) => {
374 const { html: highlighted } = highlightCode(f.content, f.filename);
375 return (
376 <div style="margin-top: 16px; border: 1px solid var(--border); border-radius: 6px;">
377 <div class="diff-file-header">{f.filename}</div>
378 <div class="blob-code">
379 <pre style="margin: 0; padding: 12px; font-size: 13px; line-height: 1.6; overflow-x: auto;">
380 {html([highlighted] as unknown as TemplateStringsArray)}
381 </pre>
382 </div>
383 </div>
384 );
385 })}
386 </Layout>
387 );
388});
389
390// Edit form
391gistRoutes.get("/gists/:slug/edit", requireAuth, async (c) => {
392 const user = c.get("user")!;
393 const slug = c.req.param("slug");
394
395 let gist: any = null;
396 let files: any[] = [];
397 try {
398 const [row] = await db
399 .select()
400 .from(gists)
401 .where(eq(gists.slug, slug))
402 .limit(1);
403 if (row && row.ownerId === user.id) {
404 gist = row;
405 files = await db
406 .select()
407 .from(gistFiles)
408 .where(eq(gistFiles.gistId, gist.id))
409 .orderBy(gistFiles.filename);
410 }
411 } catch {
412 // leave null
413 }
414
415 if (!gist) return c.html(notFound(user, "Not found or not yours"), 404);
416
417 return c.html(
418 <Layout title={`Edit ${gist.slug}`} user={user}>
419 <h1 style="margin-top: 20px;">Edit gist</h1>
420 <form
cce7944Claude421 method="post"
1e162a8Claude422 action={`/gists/${slug}/edit`}
423 style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;"
424 >
425 <input
426 type="text"
427 name="description"
428 value={gist.description}
429 placeholder="Description"
2c3ba6ecopilot-swe-agent[bot]430 aria-label="Gist description"
1e162a8Claude431 style="padding: 8px;"
432 />
433 <input
434 type="text"
435 name="message"
436 placeholder="Revision message (optional)"
2c3ba6ecopilot-swe-agent[bot]437 aria-label="Revision message"
1e162a8Claude438 style="padding: 8px;"
439 />
440 <div id="files">
441 {files.map((f) => (
442 <div class="gist-file" style="border: 1px solid var(--border); padding: 12px; margin-bottom: 8px;">
443 <input
444 type="text"
445 name="filename[]"
446 value={f.filename}
447 required
2c3ba6ecopilot-swe-agent[bot]448 aria-label="Filename"
1e162a8Claude449 style="padding: 6px; width: 300px;"
450 />
451 <textarea
452 name="content[]"
453 rows={12}
454 required
455 style="width: 100%; padding: 8px; font-family: monospace; margin-top: 8px;"
456 >
457 {f.content}
458 </textarea>
459 </div>
460 ))}
461 </div>
462 <button
463 type="button"
464 class="btn"
465 id="add-file"
466 style="align-self: flex-start;"
467 >
468 + Add file
469 </button>
470 <button type="submit" class="btn btn-primary">
471 Save revision
472 </button>
473 </form>
474 {html`
475 <script>
476 document.getElementById("add-file").addEventListener("click", () => {
477 const div = document.createElement("div");
478 div.className = "gist-file";
479 div.style.cssText = "border: 1px solid var(--border); padding: 12px; margin-bottom: 8px;";
480 div.innerHTML =
481 '<input type="text" name="filename[]" placeholder="filename.ext" required style="padding: 6px; width: 300px;" />' +
482 '<textarea name="content[]" rows="12" required style="width: 100%; padding: 8px; font-family: monospace; margin-top: 8px;"></textarea>';
483 document.getElementById("files").appendChild(div);
484 });
485 </script>
486 `}
487 </Layout>
488 );
489});
490
491// Save edit
492gistRoutes.post("/gists/:slug/edit", requireAuth, async (c) => {
493 const user = c.get("user")!;
494 const slug = c.req.param("slug");
495 const form = await c.req.formData();
496 const description = (form.get("description") as string || "").trim();
497 const message = (form.get("message") as string || "").trim();
498 const filenames = form.getAll("filename[]") as string[];
499 const contents = form.getAll("content[]") as string[];
500
501 const files = filenames
502 .map((fn, i) => ({
503 filename: (fn || "").trim(),
504 content: contents[i] || "",
505 }))
506 .filter((f) => f.filename && f.content);
507
508 if (files.length === 0) {
509 return c.text("At least one file is required", 400);
510 }
511
512 try {
513 const [row] = await db
514 .select()
515 .from(gists)
516 .where(eq(gists.slug, slug))
517 .limit(1);
518 if (!row || row.ownerId !== user.id) {
519 return c.redirect("/gists");
520 }
521 // Replace file set: delete all, re-insert.
522 await db.delete(gistFiles).where(eq(gistFiles.gistId, row.id));
523 await db.insert(gistFiles).values(
524 files.map((f) => ({
525 gistId: row.id,
526 filename: f.filename,
527 content: f.content,
528 sizeBytes: new TextEncoder().encode(f.content).length,
529 }))
530 );
531 // Bump revision.
532 const [last] = await db
533 .select({ r: sql<number>`max(${gistRevisions.revision})` })
534 .from(gistRevisions)
535 .where(eq(gistRevisions.gistId, row.id));
536 const nextRev = Number(last?.r || 0) + 1;
537 await db.insert(gistRevisions).values({
538 gistId: row.id,
539 revision: nextRev,
540 snapshot: snapshotOf(files),
541 authorId: user.id,
542 message: message || null,
543 });
544 await db
545 .update(gists)
546 .set({ description, updatedAt: new Date() })
547 .where(eq(gists.id, row.id));
548 } catch {
549 // swallow
550 }
551 return c.redirect(`/gists/${slug}`);
552});
553
554// Delete
555gistRoutes.post("/gists/:slug/delete", requireAuth, async (c) => {
556 const user = c.get("user")!;
557 const slug = c.req.param("slug");
558 try {
559 const [row] = await db
560 .select()
561 .from(gists)
562 .where(eq(gists.slug, slug))
563 .limit(1);
564 if (row && row.ownerId === user.id) {
565 await db.delete(gists).where(eq(gists.id, row.id));
566 }
567 } catch {
568 // swallow
569 }
570 return c.redirect("/gists");
571});
572
573// Toggle star
574gistRoutes.post("/gists/:slug/star", requireAuth, async (c) => {
575 const user = c.get("user")!;
576 const slug = c.req.param("slug");
577 try {
578 const [row] = await db
579 .select()
580 .from(gists)
581 .where(eq(gists.slug, slug))
582 .limit(1);
583 if (row && row.ownerId !== user.id) {
584 const [existing] = await db
585 .select()
586 .from(gistStars)
587 .where(
588 and(
589 eq(gistStars.gistId, row.id),
590 eq(gistStars.userId, user.id)
591 )
592 )
593 .limit(1);
594 if (existing) {
595 await db.delete(gistStars).where(eq(gistStars.id, existing.id));
596 } else {
597 await db.insert(gistStars).values({
598 gistId: row.id,
599 userId: user.id,
600 });
601 }
602 }
603 } catch {
604 // swallow
605 }
606 return c.redirect(`/gists/${slug}`);
607});
608
609// Revisions list
610gistRoutes.get("/gists/:slug/revisions", softAuth, async (c) => {
611 const user = c.get("user");
612 const slug = c.req.param("slug");
613
614 let gist: any = null;
615 let revs: any[] = [];
616 try {
617 const [row] = await db
618 .select()
619 .from(gists)
620 .where(eq(gists.slug, slug))
621 .limit(1);
622 if (row && (row.isPublic || (user && user.id === row.ownerId))) {
623 gist = row;
624 revs = await db
625 .select({
626 r: gistRevisions,
627 author: { username: users.username },
628 })
629 .from(gistRevisions)
630 .innerJoin(users, eq(gistRevisions.authorId, users.id))
631 .where(eq(gistRevisions.gistId, gist.id))
632 .orderBy(desc(gistRevisions.revision));
633 }
634 } catch {
635 // leave null
636 }
637
638 if (!gist) return c.html(notFound(user), 404);
639
640 return c.html(
641 <Layout title={`${gist.slug} — revisions`} user={user}>
642 <h1 style="margin-top: 16px;">
643 <a href={`/gists/${slug}`}>{gist.title || slug}</a> — revisions
644 </h1>
645 <div class="commit-list" style="margin-top: 16px;">
646 {revs.map((rv) => (
647 <div class="commit-item">
648 <div>
649 <div class="commit-message">
650 <a href={`/gists/${slug}/revisions/${rv.r.revision}`}>
651 <strong>Revision {rv.r.revision}</strong>
652 </a>
653 {rv.r.message ? ` — ${rv.r.message}` : ""}
654 </div>
655 <div class="commit-meta">
656 by @{rv.author.username}
657 </div>
658 </div>
659 <a
660 href={`/gists/${slug}/revisions/${rv.r.revision}`}
661 class="commit-sha"
662 >
663 r{rv.r.revision}
664 </a>
665 </div>
666 ))}
667 </div>
668 </Layout>
669 );
670});
671
672// Revision detail
673gistRoutes.get(
674 "/gists/:slug/revisions/:rev",
675 softAuth,
676 async (c) => {
677 const user = c.get("user");
678 const slug = c.req.param("slug");
679 const rev = Number(c.req.param("rev"));
680
681 let gist: any = null;
682 let snapshot: Record<string, string> | null = null;
683 try {
684 const [row] = await db
685 .select()
686 .from(gists)
687 .where(eq(gists.slug, slug))
688 .limit(1);
689 if (row && (row.isPublic || (user && user.id === row.ownerId))) {
690 gist = row;
691 const [rv] = await db
692 .select()
693 .from(gistRevisions)
694 .where(
695 and(
696 eq(gistRevisions.gistId, gist.id),
697 eq(gistRevisions.revision, rev)
698 )
699 )
700 .limit(1);
701 if (rv) {
702 try {
703 snapshot = JSON.parse(rv.snapshot);
704 } catch {
705 snapshot = {};
706 }
707 }
708 }
709 } catch {
710 // leave null
711 }
712
713 if (!gist || !snapshot)
714 return c.html(notFound(user, "Revision not found"), 404);
715
716 return c.html(
717 <Layout title={`${slug} @ r${rev}`} user={user}>
718 <h1 style="margin-top: 16px;">
719 <a href={`/gists/${slug}`}>{gist.title || slug}</a> @ revision {rev}
720 </h1>
721 {Object.entries(snapshot).map(([filename, content]) => {
722 const { html: highlighted } = highlightCode(content, filename);
723 return (
724 <div style="margin-top: 16px; border: 1px solid var(--border); border-radius: 6px;">
725 <div class="diff-file-header">{filename}</div>
726 <div class="blob-code">
727 <pre style="margin: 0; padding: 12px; font-size: 13px; line-height: 1.6; overflow-x: auto;">
728 {html([highlighted] as unknown as TemplateStringsArray)}
729 </pre>
730 </div>
731 </div>
732 );
733 })}
734 </Layout>
735 );
736 }
737);
738
739// User's public gists
740gistRoutes.get("/:username/gists", softAuth, async (c) => {
741 const user = c.get("user");
742 const username = c.req.param("username");
743
744 let ownerUser: any = null;
745 let rows: any[] = [];
746 try {
747 const [u] = await db
748 .select()
749 .from(users)
750 .where(eq(users.username, username))
751 .limit(1);
752 if (u) {
753 ownerUser = u;
754 const showPrivate = user && user.id === u.id;
755 rows = await db
756 .select({
757 g: gists,
758 fileCount: sql<number>`(SELECT count(*) FROM gist_files WHERE gist_id = ${gists.id})`,
759 })
760 .from(gists)
761 .where(
762 showPrivate
763 ? eq(gists.ownerId, u.id)
764 : and(eq(gists.ownerId, u.id), eq(gists.isPublic, true))
765 )
766 .orderBy(desc(gists.updatedAt));
767 }
768 } catch {
769 rows = [];
770 }
771
772 if (!ownerUser) return c.html(notFound(user, "User not found"), 404);
773
774 return c.html(
775 <Layout title={`@${username}'s gists`} user={user}>
776 <h1 style="margin-top: 16px;">
777 <a href={`/${username}`}>@{username}</a>'s gists
778 </h1>
779 {rows.length === 0 ? (
780 <div class="empty-state">
781 <p>No gists yet.</p>
782 </div>
783 ) : (
784 <div class="commit-list" style="margin-top: 16px;">
785 {rows.map((r) => (
786 <div class="commit-item">
787 <div>
788 <div class="commit-message">
789 <a href={`/gists/${r.g.slug}`}>
790 <strong>{r.g.title || r.g.slug}</strong>
791 </a>
792 {!r.g.isPublic && <span class="badge">Secret</span>}
793 </div>
794 <div class="commit-meta">
795 {r.fileCount} file{r.fileCount !== 1 ? "s" : ""}
796 {r.g.description && ` · ${r.g.description}`}
797 </div>
798 </div>
799 <a href={`/gists/${r.g.slug}`} class="commit-sha">
800 {r.g.slug}
801 </a>
802 </div>
803 ))}
804 </div>
805 )}
806 </Layout>
807 );
808});
809
810export default gistRoutes;