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.

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