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

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