Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit43de941unknown_key

feat: Week 6 — contributors page, activity graph, full architecture docs

feat: Week 6 — contributors page, activity graph, full architecture docs

- Contributors page: commit leaderboard per repo with email + count
- Commit activity graph: 52-week bar chart (git log --since=1yr)
- CLAUDE.md: complete architecture documentation for all 36 source files
- Wired contributors into app router

Total: 36 source files, 7,779 lines of code, 60 passing tests

https://claude.ai/code/session_013wpQ5iX7qU6zy6PrtML3fP
Claude committed on April 12, 2026Parent: c81ab7a
3 files changed+1961443de941114aec36d6ce53278537e322caac4aa94
3 changed files+196−14
ModifiedCLAUDE.md+50−14View fileUnifiedSplit
2222
2323```
2424src/
25 index.ts Entry point (Bun server)
26 app.ts Hono app composition
27 lib/config.ts Environment config (getters, reads env at access time)
25 index.ts Entry point (Bun server)
26 app.tsx Hono app composition + error handlers
27 lib/
28 config.ts Environment config (getters, reads env at access time)
29 auth.ts Password hashing (bcrypt), session tokens
30 highlight.ts Syntax highlighting (highlight.js, 40+ languages)
31 markdown.ts Markdown rendering (GFM + syntax highlighting)
2832 db/
29 schema.ts Drizzle schema (users, repositories, ssh_keys)
30 index.ts Lazy DB connection (proxy pattern)
31 migrate.ts Migration runner
33 schema.ts Drizzle schema (all tables)
34 index.ts Lazy DB connection (proxy pattern)
35 migrate.ts Migration runner
3236 git/
33 repository.ts Git operations (tree, blob, commits, diff, branches)
34 protocol.ts Smart HTTP protocol (pkt-line, service RPC)
37 repository.ts Git operations (tree, blob, commits, diff, branches, blame, search, raw)
38 protocol.ts Smart HTTP protocol (pkt-line, service RPC)
3539 hooks/
36 post-receive.ts GateTest + Crontech webhooks on push
40 post-receive.ts GateTest + Crontech webhooks on push
41 middleware/
42 auth.ts softAuth + requireAuth middleware
3743 routes/
38 git.ts Git HTTP endpoints (clone/push)
39 api.ts REST API (repo CRUD, setup)
40 web.tsx Web UI (file browser, commits, diffs)
44 git.ts Git HTTP endpoints (clone/push)
45 api.ts REST API (repo CRUD, setup)
46 auth.tsx Register, login, logout (web + API)
47 web.tsx Web UI (file browser, commits, diffs, search, blame, raw)
48 issues.tsx Issue tracker (CRUD, comments, close/reopen)
49 pulls.tsx Pull requests (create, review, merge, close)
50 editor.tsx Web file editor (create/edit via git plumbing)
51 compare.tsx Branch comparison (diff + commit list)
52 settings.tsx User settings (profile, SSH keys)
53 repo-settings.tsx Repository settings (description, visibility, delete)
54 webhooks.tsx Webhook management + delivery engine
55 fork.ts Repository forking
56 explore.tsx Explore/discover public repos
57 tokens.tsx Personal access tokens
58 contributors.tsx Contributor list + commit activity graph
4159 views/
42 layout.tsx HTML shell + CSS
43 components.tsx UI components (file table, commit list, diff viewer)
60 layout.tsx HTML shell + CSS (dark theme) + auth-aware nav
61 components.tsx UI components (file table, commit list, diff viewer, etc.)
4462```
4563
64## Database Schema
65
66- `users` — accounts with bcrypt password hashing
67- `sessions` — cookie-based auth sessions (30 day expiry)
68- `repositories` — repos with fork tracking, star/fork/issue counts
69- `stars` — user-repo star relationships
70- `issues` — issue tracker with open/closed state
71- `issue_comments` — threaded comments on issues
72- `labels` + `issue_labels` — issue categorization
73- `pull_requests` — PRs with base/head branches, open/closed/merged state
74- `pr_comments` — PR comments with AI review flag + file/line annotations
75- `activity_feed` — event log for repos
76- `webhooks` — registered webhook URLs with HMAC secret + event filtering
77- `api_tokens` — personal access tokens with SHA-256 hashing
78- `repo_topics` — repository tags for discoverability
79- `ssh_keys` — user SSH public keys
80
4681## Integrations
4782
4883- **GateTest:** POST `https://gatetest.ai/api/scan/run` on every `git push`
4984- **Crontech:** POST `https://crontech.ai/api/trpc/tenant.deploy` on push to main
85- **Webhooks:** POST to registered URLs on push/issue/PR/star events with HMAC signatures
5086
5187## Environment Variables
5288
Modifiedsrc/app.tsx+4−0View fileUnifiedSplit
1515import webhookRoutes from "./routes/webhooks";
1616import exploreRoutes from "./routes/explore";
1717import tokenRoutes from "./routes/tokens";
18import contributorRoutes from "./routes/contributors";
1819import webRoutes from "./routes/web";
1920
2021const app = new Hono();
5960// Web file editor
6061app.route("/", editorRoutes);
6162
63// Contributors
64app.route("/", contributorRoutes);
65
6266// Explore page
6367app.route("/", exploreRoutes);
6468
Addedsrc/routes/contributors.tsx+142−0View fileUnifiedSplit
1/**
2 * Contributors page — who contributed to this repo, commit counts.
3 */
4
5import { Hono } from "hono";
6import { Layout } from "../views/layout";
7import { RepoHeader, RepoNav } from "../views/components";
8import { getRepoPath, repoExists, getDefaultBranch } from "../git/repository";
9import { softAuth } from "../middleware/auth";
10import type { AuthEnv } from "../middleware/auth";
11
12const contributors = new Hono<AuthEnv>();
13
14contributors.use("*", softAuth);
15
16interface Contributor {
17 name: string;
18 email: string;
19 commits: number;
20 additions: number;
21 deletions: number;
22}
23
24contributors.get("/:owner/:repo/contributors", async (c) => {
25 const { owner, repo } = c.req.param();
26 const user = c.get("user");
27
28 if (!(await repoExists(owner, repo))) return c.notFound();
29
30 const ref = (await getDefaultBranch(owner, repo)) || "main";
31 const repoDir = getRepoPath(owner, repo);
32
33 // Get shortlog for commit counts
34 const shortlogProc = Bun.spawn(
35 ["git", "shortlog", "-sne", ref],
36 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
37 );
38 const shortlogOut = await new Response(shortlogProc.stdout).text();
39 await shortlogProc.exited;
40
41 const contribs: Contributor[] = shortlogOut
42 .trim()
43 .split("\n")
44 .filter(Boolean)
45 .map((line) => {
46 const match = line.trim().match(/^(\d+)\t(.+?)\s+<(.+?)>$/);
47 if (!match) return null;
48 return {
49 name: match[2],
50 email: match[3],
51 commits: parseInt(match[1], 10),
52 additions: 0,
53 deletions: 0,
54 };
55 })
56 .filter((c): c is Contributor => c !== null)
57 .sort((a, b) => b.commits - a.commits);
58
59 // Get recent commit activity (last 52 weeks)
60 const activityProc = Bun.spawn(
61 [
62 "git",
63 "log",
64 "--format=%aI",
65 "--since=1 year ago",
66 ref,
67 ],
68 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
69 );
70 const activityOut = await new Response(activityProc.stdout).text();
71 await activityProc.exited;
72
73 // Build weekly commit counts
74 const weekCounts: number[] = new Array(52).fill(0);
75 const now = Date.now();
76 for (const line of activityOut.trim().split("\n").filter(Boolean)) {
77 const date = new Date(line);
78 const weeksAgo = Math.floor(
79 (now - date.getTime()) / (7 * 24 * 60 * 60 * 1000)
80 );
81 if (weeksAgo >= 0 && weeksAgo < 52) {
82 weekCounts[51 - weeksAgo]++;
83 }
84 }
85
86 const maxWeek = Math.max(...weekCounts, 1);
87
88 return c.html(
89 <Layout title={`Contributors — ${owner}/${repo}`} user={user}>
90 <RepoHeader owner={owner} repo={repo} />
91 <RepoNav owner={owner} repo={repo} active="code" />
92 <h2 style="margin-bottom: 16px">Contributors</h2>
93
94 <div
95 style="margin-bottom: 24px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px"
96 >
97 <div style="font-size: 13px; color: var(--text-muted); margin-bottom: 8px">
98 Commit activity — last year
99 </div>
100 <div style="display: flex; gap: 2px; align-items: flex-end; height: 60px">
101 {weekCounts.map((count) => (
102 <div
103 style={`flex: 1; background: var(--green); opacity: ${count === 0 ? "0.1" : Math.max(0.3, count / maxWeek).toFixed(2)}; height: ${count === 0 ? "2px" : Math.max(4, (count / maxWeek) * 60).toFixed(0) + "px"}; border-radius: 1px;`}
104 title={`${count} commits`}
105 />
106 ))}
107 </div>
108 </div>
109
110 <div class="issue-list">
111 {contribs.map((contrib, i) => (
112 <div class="issue-item">
113 <div style="display: flex; align-items: center; gap: 12px; flex: 1">
114 <div class="user-avatar" style="width: 36px; height: 36px; font-size: 16px; flex-shrink: 0">
115 {contrib.name[0].toUpperCase()}
116 </div>
117 <div>
118 <div style="font-weight: 600; font-size: 14px">
119 {contrib.name}
120 </div>
121 <div style="font-size: 12px; color: var(--text-muted)">
122 {contrib.email}
123 </div>
124 </div>
125 </div>
126 <div style="text-align: right">
127 <span style="font-weight: 600; font-size: 14px">
128 {contrib.commits}
129 </span>
130 <span style="color: var(--text-muted); font-size: 13px">
131 {" "}
132 commit{contrib.commits !== 1 ? "s" : ""}
133 </span>
134 </div>
135 </div>
136 ))}
137 </div>
138 </Layout>
139 );
140});
141
142export default contributors;
0143