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.

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