flagon-io/g1t

public

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/apps/web/app/routes/repo/commits.tsx

88 lines3,520 bytesCodeBlame
1import { GitCommitHorizontal } from "lucide-react";
2import { Link } from "react-router";
3
4import type { Commit } from "@g1t/contracts";
5
6import type { Route } from "./+types/commits";
7import { Avatar, EmptyState, TimeAgo } from "../../components/ui";
8import { repos } from "../../lib/services.server";
9import { getViewer, unwrap } from "../../lib/session.server";
10
11const PAGE_SIZE = 50;
12
13export function meta({ params }: Route.MetaArgs) {
14 return [{ title: `Commits · ${params.owner}/${params.repo} · g1t` }];
15}
16
17export async function loader({ params, context }: Route.LoaderArgs) {
18 const path = { namespace: params.owner, name: params.repo };
19 return {
20 commits: unwrap(await repos.log(path, getViewer(context), null, PAGE_SIZE)),
21 };
22}
23
24/** Commits by the day they were made, newest first. */
25function byDay(commits: Commit[]): [string, Commit[]][] {
26 const days = new Map<string, Commit[]>();
27 for (const commit of commits) {
28 const day = commit.authoredAt.slice(0, 10);
29 days.set(day, [...(days.get(day) ?? []), commit]);
30 }
31 return [...days];
32}
33
34function dayLabel(day: string): string {
35 const date = new Date(`${day}T12:00:00Z`);
36 return date.toLocaleDateString("en-US", {
37 weekday: "long",
38 month: "long",
39 day: "numeric",
40 year: "numeric",
41 timeZone: "UTC",
42 });
43}
44
45export default function Commits({ loaderData, params }: Route.ComponentProps) {
46 const { commits } = loaderData;
47 const base = `/${params.owner}/${params.repo}`;
48 if (commits.length === 0) {
49 return <EmptyState title="No commits yet" />;
50 }
51 return (
52 <div className="relative space-y-8 before:absolute before:top-3 before:bottom-3 before:left-1.75 before:w-px before:bg-line">
53 {byDay(commits).map(([day, list]) => (
54 <section key={day} className="relative pl-7">
55 <h2 className="flex items-center gap-2 text-xs font-medium text-muted">
56 <GitCommitHorizontal size={15} className="absolute left-0 bg-bg text-faint" />
57 {dayLabel(day)}
58 </h2>
59 <ol className="mt-3 divide-y divide-line overflow-hidden rounded-xl border border-line">
60 {list.map((commit) => {
61 const [subject, ...body] = commit.message.split("\n");
62 const rest = body.join("\n").trim();
63 const to = `${base}/commit/${commit.hash}`;
64 return (
65 <li key={commit.hash} className="group relative flex items-start gap-3 px-4 py-3 transition-colors hover:bg-surface">
66 <Avatar name={commit.author.name} size={24} />
67 <div className="min-w-0 grow">
68 <Link to={to} prefetch="intent" className="block truncate font-medium after:absolute after:inset-0 group-hover:text-accent">
69 {subject}
70 </Link>
71 {rest && <p className="mt-1 line-clamp-1 text-sm text-muted">{rest}</p>}
72 <p className="mt-1 text-xs text-faint">
73 {commit.author.name} committed <TimeAgo at={commit.authoredAt} />
74 {commit.parents.length > 1 && " · merge"}
75 </p>
76 </div>
77 <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">
78 {commit.hash.slice(0, 7)}
79 </span>
80 </li>
81 );
82 })}
83 </ol>
84 </section>
85 ))}
86 </div>
87 );
88}