Skip to content
104 linesCodeBlameRaw
1import { GitCommitHorizontal } from "lucide-react";
2import { Link } from "react-router";
3
4import type { ShownCommit } from "../../lib/commit-people";
5import { parseCommitMessage } from "../../lib/commit-message";
6
7import type { Route } from "./+types/commits";
8import { page } from "../../lib/meta";
9import { CommitChecksBadge } from "../../components/commit-checks";
10import { CommitAvatars, CommitNames } from "../../components/commit-person";
11import { EmptyState, TimeAgo } from "../../components/ui";
12import { commitChecksFor } from "../../lib/commit-checks.server";
13import { showCommits } from "../../lib/commit-people.server";
14import { repos } from "../../lib/services.server";
15import { getViewer, unwrap } from "../../lib/session.server";
16
17const PAGE_SIZE = 50;
18
19export function meta({ params, ...args }: Route.MetaArgs) {
20 return page(args, { title: `Commits · ${params.owner}/${params.repo} · g1t` });
21}
22
23export async function loader({ params, context }: Route.LoaderArgs) {
24 const path = { namespace: params.owner, name: params.repo };
25 const viewer = getViewer(context);
26 const log = unwrap(await repos.log(path, viewer, null, PAGE_SIZE));
27 // Every commit's checks in one call, streamed in beside each.
28 const checks = commitChecksFor(path, viewer, log.map((commit) => commit.hash));
29 // Who wrote each commit, as their account where there is one.
30 const commits = await showCommits(log);
31 return { commits, checks };
32}
33
34/** Commits by the day they were made, newest first. */
35function byDay(commits: ShownCommit[]): [string, ShownCommit[]][] {
36 const days = new Map<string, ShownCommit[]>();
37 for (const commit of commits) {
38 const day = commit.authoredAt.slice(0, 10);
39 days.set(day, [...(days.get(day) ?? []), commit]);
40 }
41 return [...days];
42}
43
44function dayLabel(day: string): string {
45 const date = new Date(`${day}T12:00:00Z`);
46 return date.toLocaleDateString("en-US", {
47 weekday: "long",
48 month: "long",
49 day: "numeric",
50 year: "numeric",
51 timeZone: "UTC",
52 });
53}
54
55export default function Commits({ loaderData, params }: Route.ComponentProps) {
56 const { commits, checks } = loaderData;
57 const base = `/${params.owner}/${params.repo}`;
58 if (commits.length === 0) {
59 return <EmptyState title="No commits yet" />;
60 }
61 return (
62 <div className="relative space-y-8 before:absolute before:top-3 before:bottom-3 before:left-1.75 before:w-px before:bg-line">
63 {byDay(commits).map(([day, list]) => (
64 <section key={day} className="relative pl-7">
65 <h2 className="flex items-center gap-2 text-xs font-medium text-muted">
66 <GitCommitHorizontal size={15} className="absolute left-0 bg-bg text-faint" />
67 {dayLabel(day)}
68 </h2>
69 <ol className="mt-3 divide-y divide-line overflow-hidden rounded-xl border border-line">
70 {list.map((commit) => {
71 // The body's prose: trailers (co-authors and their addresses) are not shown.
72 const { subject, body: rest } = parseCommitMessage(commit.message);
73 const to = `${base}/commit/${commit.hash}`;
74 return (
75 <li key={commit.hash} className="group relative flex items-start gap-3 px-4 py-3 transition-colors hover:bg-surface">
76 <span className="mt-0.5 flex">
77 <CommitAvatars commit={commit} size={24} />
78 </span>
79 <div className="min-w-0 grow">
80 <div className="flex min-w-0 items-center gap-1.5">
81 <Link to={to} prefetch="intent" className="truncate font-medium after:absolute after:inset-0 group-hover:text-accent">
82 {subject}
83 </Link>
84 <CommitChecksBadge checks={checks} sha={commit.hash} />
85 </div>
86 {rest && <p className="mt-1 line-clamp-1 text-sm text-muted">{rest}</p>}
87 <p className="mt-1 text-xs text-faint">
88 <CommitNames commit={commit} className="text-muted hover:text-fg" />{" "}
89 committed <TimeAgo at={commit.authoredAt} />
90 {commit.parents.length > 1 && " · merge"}
91 </p>
92 </div>
93 <span className="shrink-0 rounded-md border border-line px-2 py-0.5 font-mono text-xs text-muted group-hover:border-line-strong group-hover:text-fg">
94 {commit.hash.slice(0, 7)}
95 </span>
96 </li>
97 );
98 })}
99 </ol>
100 </section>
101 ))}
102 </div>
103 );
104}