Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

131 linesCodeBlameRaw
1import type { Contributor, WeekCommits } from "@g1t/contracts";
2
3import type { Route } from "./+types/contributors";
4import { CommitAvatar, CommitName } from "../../components/commit-person";
5import { EmptyState, TimeAgo } from "../../components/ui";
6import { Card } from "../../components/ui/card";
7import { Hint } from "../../components/ui/hint";
8import { requireRepo } from "../../lib/access.server";
9import { alignWeeks, count, peak } from "../../lib/about";
10import { contributorPerson } from "../../lib/commit-people";
11import { page } from "../../lib/meta";
12import { repos } from "../../lib/services.server";
13import { unwrap } from "../../lib/session.server";
14
15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `Contributors · ${params.owner}/${params.repo} · g1t` });
17}
18
19export async function loader({ params, context }: Route.LoaderArgs) {
20 const { viewer } = await requireRepo(context, params, "read");
21 return { contributors: unwrap(await repos.contributors({ namespace: params.owner, name: params.repo }, viewer)) };
22}
23
24/** The most weeks a chart shows: the latest. */
25const WEEKS = 52;
26
27function weekLabel(week: string): string {
28 return new Date(`${week}T00:00:00Z`).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
29}
30
31/** Commits per week as bars, newest on the right, each with its week on hover. */
32function WeekBars({ weeks, values, height, label, scale }: { weeks: WeekCommits[]; values: number[]; height: number; label: string; scale?: number }) {
33 // One scale for every contributor, so their bars compare.
34 const top = scale ?? Math.max(1, ...values);
35 return (
36 <div role="img" aria-label={label} className="flex items-end gap-0.5" style={{ height }}>
37 {values.map((value, at) => (
38 <Hint key={weeks[at].week} label={`${count(value, "commit")} · week of ${weekLabel(weeks[at].week)}`}>
39 <span className="flex h-full min-w-0 flex-1 items-end">
40 <span
41 className={`w-full rounded-t-xs ${value > 0 ? "bg-accent" : "bg-line"}`}
42 style={{ height: value > 0 ? `${Math.max(6, (value / top) * 100)}%` : 2 }}
43 />
44 </span>
45 </Hint>
46 ))}
47 </div>
48 );
49}
50
51function Person({ contributor, rank, weeks, scale }: { contributor: Contributor; rank: number; weeks: WeekCommits[]; scale: number }) {
52 // The same person, avatar, link and card as on their commits.
53 const person = contributorPerson(contributor);
54 return (
55 <Card asChild className="p-4">
56 <li>
57 <div className="flex items-center gap-3">
58 <CommitAvatar person={person} size={36} />
59 <div className="min-w-0 grow">
60 <p className="flex items-center gap-2 truncate text-sm">
61 <CommitName person={person} className="font-medium" />
62 </p>
63 <p className="text-xs text-muted">
64 {count(contributor.commits, "commit")} · last <TimeAgo at={contributor.lastAt} />
65 </p>
66 </div>
67 <span className="text-xs text-faint tabular-nums">#{rank}</span>
68 </div>
69 {contributor.weeks.length > 0 && weeks.length > 0 && (
70 <div className="mt-3">
71 <WeekBars weeks={weeks} values={alignWeeks(weeks, contributor.weeks)} height={40} label={`${contributor.name}'s commits by week`} scale={scale} />
72 </div>
73 )}
74 </li>
75 </Card>
76 );
77}
78
79export default function Contributors({ loaderData }: Route.ComponentProps) {
80 const { contributors } = loaderData;
81 const weeks = contributors.weeks.slice(-WEEKS);
82 const all = weeks.map((week) => week.commits);
83 const scale = Math.max(1, ...contributors.contributors.flatMap((contributor) => alignWeeks(weeks, contributor.weeks)));
84 return (
85 <div className="space-y-6">
86 <div>
87 <h2 className="text-lg font-semibold tracking-tight">
88 Contributors <span className="font-normal text-faint">{contributors.total}</span>
89 </h2>
90 <p className="mt-1 text-sm text-muted">
91 Commits on the default branch by who made them: people by the addresses they confirmed, g1t as itself, anyone else by the name on their commits.
92 {contributors.partial && ` The newest ${contributors.commits.toLocaleString("en-US")} commits are counted.`}
93 </p>
94 </div>
95 {contributors.pending ? (
96 <EmptyState title="Reading the history">The default branch is being read for the first time. This page has it in a few seconds.</EmptyState>
97 ) : contributors.contributors.length === 0 ? (
98 <EmptyState title="No commits yet">Push a first commit and who made it shows here.</EmptyState>
99 ) : (
100 <>
101 <Card asChild className="p-5">
102 <section>
103 <div className="mb-4 flex flex-wrap items-baseline justify-between gap-2">
104 <h3 className="text-sm font-semibold">Commits per week</h3>
105 <span className="text-xs text-muted">
106 {count(contributors.commits, "commit")} · peak {peak(weeks)} a week
107 </span>
108 </div>
109 <WeekBars weeks={weeks} values={all} height={120} label="Commits per week" />
110 <div className="mt-2 flex justify-between text-xs text-faint">
111 <span>{weeks[0] && weekLabel(weeks[0].week)}</span>
112 <span>{weeks.at(-1) && weekLabel(weeks.at(-1)!.week)}</span>
113 </div>
114 </section>
115 </Card>
116 <ul className="grid gap-3 md:grid-cols-2">
117 {contributors.contributors.map((contributor, at) => (
118 <Person key={`${contributor.kind}:${contributor.name}`} contributor={contributor} rank={at + 1} weeks={weeks} scale={scale} />
119 ))}
120 </ul>
121 {contributors.computedAt && (
122 <p className="text-xs text-faint">
123 Counted <TimeAgo at={contributors.computedAt} />
124 {contributors.commit && contributors.head && contributors.commit !== contributors.head && ", and being counted again for the newest commits"}.
125 </p>
126 )}
127 </>
128 )}
129 </div>
130 );
131}