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

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.tsxBlame147 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";
bb0f894Claude11import {
12 Avatar,
13 Card,
14 Flex,
15 List,
16 ListItem,
17 PageHeader,
18 Text,
19 Tooltip,
20} from "../views/ui";
43de941Claude21
22const contributors = new Hono<AuthEnv>();
23
24contributors.use("*", softAuth);
25
26interface Contributor {
27 name: string;
28 email: string;
29 commits: number;
30 additions: number;
31 deletions: number;
32}
33
34contributors.get("/:owner/:repo/contributors", async (c) => {
35 const { owner, repo } = c.req.param();
36 const user = c.get("user");
37
38 if (!(await repoExists(owner, repo))) return c.notFound();
39
40 const ref = (await getDefaultBranch(owner, repo)) || "main";
41 const repoDir = getRepoPath(owner, repo);
42
43 // Get shortlog for commit counts
44 const shortlogProc = Bun.spawn(
45 ["git", "shortlog", "-sne", ref],
46 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
47 );
48 const shortlogOut = await new Response(shortlogProc.stdout).text();
49 await shortlogProc.exited;
50
51 const contribs: Contributor[] = shortlogOut
52 .trim()
53 .split("\n")
54 .filter(Boolean)
55 .map((line) => {
56 const match = line.trim().match(/^(\d+)\t(.+?)\s+<(.+?)>$/);
57 if (!match) return null;
58 return {
59 name: match[2],
60 email: match[3],
61 commits: parseInt(match[1], 10),
62 additions: 0,
63 deletions: 0,
64 };
65 })
66 .filter((c): c is Contributor => c !== null)
67 .sort((a, b) => b.commits - a.commits);
68
69 // Get recent commit activity (last 52 weeks)
70 const activityProc = Bun.spawn(
71 [
72 "git",
73 "log",
74 "--format=%aI",
75 "--since=1 year ago",
76 ref,
77 ],
78 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
79 );
80 const activityOut = await new Response(activityProc.stdout).text();
81 await activityProc.exited;
82
83 // Build weekly commit counts
84 const weekCounts: number[] = new Array(52).fill(0);
85 const now = Date.now();
86 for (const line of activityOut.trim().split("\n").filter(Boolean)) {
87 const date = new Date(line);
88 const weeksAgo = Math.floor(
89 (now - date.getTime()) / (7 * 24 * 60 * 60 * 1000)
90 );
91 if (weeksAgo >= 0 && weeksAgo < 52) {
92 weekCounts[51 - weeksAgo]++;
93 }
94 }
95
96 const maxWeek = Math.max(...weekCounts, 1);
97
98 return c.html(
99 <Layout title={`Contributors — ${owner}/${repo}`} user={user}>
100 <RepoHeader owner={owner} repo={repo} />
101 <RepoNav owner={owner} repo={repo} active="code" />
bb0f894Claude102 <PageHeader title="Contributors" />
103
104 <Card style="margin-bottom:24px;padding:16px">
105 <Text size={13} muted>Commit activity — last year</Text>
106 <Flex gap={2} align="flex-end" style="height:60px;margin-top:8px">
43de941Claude107 {weekCounts.map((count) => (
bb0f894Claude108 <Tooltip text={`${count} commits`}>
109 <div
110 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;`}
111 />
112 </Tooltip>
43de941Claude113 ))}
bb0f894Claude114 </Flex>
115 </Card>
116
117 <List>
118 {contribs.map((contrib) => (
119 <ListItem>
120 <Flex align="center" gap={12} style="flex:1">
121 <Avatar name={contrib.name} size={36} />
43de941Claude122 <div>
bb0f894Claude123 <Text size={14} weight={600}>
43de941Claude124 {contrib.name}
bb0f894Claude125 </Text>
126 <br />
127 <Text size={12} muted>
43de941Claude128 {contrib.email}
bb0f894Claude129 </Text>
43de941Claude130 </div>
bb0f894Claude131 </Flex>
132 <div style="text-align:right">
133 <Text size={14} weight={600}>
43de941Claude134 {contrib.commits}
bb0f894Claude135 </Text>
136 <Text size={13} muted>
137 {" "}commit{contrib.commits !== 1 ? "s" : ""}
138 </Text>
43de941Claude139 </div>
bb0f894Claude140 </ListItem>
43de941Claude141 ))}
bb0f894Claude142 </List>
43de941Claude143 </Layout>
144 );
145});
146
147export default contributors;