1// Commit detail page (/:repo/commit/:hash). Shows commit metadata, full
2// message, parent links, and changed files with lazy diffs.
3
4import { gql } from "@apollo/client";
5import { useReadQuery } from "@apollo/client/react";
6import { createFileRoute, Link } from "@tanstack/react-router";
7import { format } from "date-fns";
8import { ArrowLeft, GitCommit } from "lucide-react";
9
10import { FileDiffView } from "@/components/code/FileDiffView";
11import { Skeleton } from "@/components/ui/skeleton";
12import { useRepo } from "@/lib/repo";
13
14const COMMIT_QUERY = gql`
15 query CommitPageDetail($repo: String, $hash: String!) {
16 repository(ref: $repo) {
17 commit(hash: $hash) {
18 hash
19 shortHash
20 message
21 fullMessage
22 authorName
23 authorEmail
24 date
25 parents
26 files {
27 nodes {
28 path
29 oldPath
30 status
31 }
32 }
33 }
34 }
35 }
36`;
37
38interface CommitQueryData {
39 repository: {
40 commit: {
41 hash: string;
42 shortHash: string;
43 message: string;
44 fullMessage: string;
45 authorName: string;
46 authorEmail: string | null;
47 date: string;
48 parents: string[];
49 files: {
50 nodes: { path: string; oldPath: string | null; status: string }[];
51 } | null;
52 } | null;
53 } | null;
54}
55
56export const Route = createFileRoute("/$repo/commit/$hash")({
57 component: RouteComponent,
58 pendingComponent: CommitPageSkeleton,
59 loader: async ({ context: { preloadQuery, ref }, params: { hash } }) => {
60 const commitRef = preloadQuery<CommitQueryData>(COMMIT_QUERY, {
61 variables: { repo: ref, hash },
62 });
63 return { commitRef: await preloadQuery.toPromise(commitRef) };
64 },
65});
66
67function RouteComponent() {
68 const repo = useRepo();
69 const { commitRef } = Route.useLoaderData();
70 const { data } = useReadQuery(commitRef);
71
72 const commit = data?.repository?.commit;
73 if (!commit) return null;
74
75 const date = new Date(commit.date);
76 const files = commit.files?.nodes ?? [];
77
78 return (
79 <div>
80 <button
81 onClick={() => {
82 window.history.back();
83 }}
84 className="text-muted-foreground hover:text-foreground mb-6 flex items-center gap-1.5 text-sm"
85 >
86 <ArrowLeft className="size-3.5" />
87 Back
88 </button>
89
90 <div className="border-border mb-6 rounded-md border p-5">
91 <div className="mb-1 flex items-start gap-3">
92 <GitCommit className="text-muted-foreground mt-1 size-5 shrink-0" />
93 <h1 className="text-lg leading-snug font-semibold">{commit.message}</h1>
94 </div>
95
96 {commit.fullMessage.includes("\n") && (
97 <pre className="text-muted-foreground mt-3 mb-4 ml-8 font-sans text-sm whitespace-pre-wrap">
98 {commit.fullMessage.split("\n").slice(1).join("\n").trim()}
99 </pre>
100 )}
101
102 <div className="text-muted-foreground mt-3 ml-8 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
103 <span>
104 <span className="text-foreground font-medium">{commit.authorName}</span>
105 {commit.authorEmail && <span> <{commit.authorEmail}></span>}
106 </span>
107 <span title={date.toISOString()}>{format(date, "PPP")}</span>
108 </div>
109
110 <div className="mt-3 ml-8 flex flex-wrap gap-3 text-xs">
111 <span className="text-muted-foreground">
112 commit <code className="text-foreground font-mono">{commit.hash}</code>
113 </span>
114 {commit.parents.map((p: string) => (
115 <span key={p} className="text-muted-foreground">
116 parent{" "}
117 <Link
118 to="/$repo/commit/$hash"
119 params={{ repo: repo!, hash: p }}
120 className="text-foreground font-mono hover:underline"
121 >
122 {p.slice(0, 7)}
123 </Link>
124 </span>
125 ))}
126 </div>
127 </div>
128
129 <div>
130 <h2 className="text-muted-foreground mb-3 text-sm font-semibold">
131 {files.length} file{files.length !== 1 ? "s" : ""} changed
132 </h2>
133 <div className="divide-border border-border divide-y overflow-hidden rounded-md border">
134 {files.length === 0 && (
135 <p className="text-muted-foreground px-4 py-4 text-sm">No file changes.</p>
136 )}
137 {files.map((file: { path: string; oldPath?: string | null; status: string }) => (
138 <FileDiffView
139 key={file.path}
140 hash={commit.hash}
141 path={file.path}
142 oldPath={file.oldPath ?? undefined}
143 status={file.status}
144 />
145 ))}
146 </div>
147 </div>
148 </div>
149 );
150}
151
152function CommitPageSkeleton() {
153 return (
154 <div className="space-y-6">
155 <Skeleton className="h-4 w-24" />
156 <div className="border-border space-y-3 rounded-md border p-5">
157 <Skeleton className="h-6 w-3/4" />
158 <Skeleton className="h-4 w-1/3" />
159 <Skeleton className="h-3 w-1/2" />
160 </div>
161 <div className="divide-border border-border divide-y rounded-md border">
162 {Array.from({ length: 5 }).map((_, i) => (
163 <div key={i} className="flex items-center gap-3 px-4 py-2.5">
164 <Skeleton className="size-4" />
165 <Skeleton className="h-4 flex-1" />
166 </div>
167 ))}
168 </div>
169 </div>
170 );
171}