Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

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

contributors.tsxBlame142 lines · 1 contributor
43de941Claude1/**
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;