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.

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