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";
12
13const COMMIT_QUERY = gql`
14 query CommitPageDetail($repo: String, $hash: String!) {
15 repository(ref: $repo) {
16 commit(hash: $hash) {
17 hash
18 shortHash
19 message
20 fullMessage
21 authorName
22 authorEmail
23 date
24 parents
25 files {
26 nodes {
27 path
28 oldPath
29 status
30 }
31 }
32 }
33 }
34 }
35`;
36
37interface CommitQueryData {
38 repository: {
39 commit: {
40 hash: string;
41 shortHash: string;
42 message: string;
43 fullMessage: string;
44 authorName: string;
45 authorEmail: string | null;
46 date: string;
47 parents: string[];
48 files: {
49 nodes: { path: string; oldPath: string | null; status: string }[];
50 } | null;
51 } | null;
52 } | null;
53}
54
55export const Route = createFileRoute("/$repo/commit/$hash")({
56 component: RouteComponent,
57 pendingComponent: CommitPageSkeleton,
58 loader: async ({ context: { preloadQuery, ref }, params: { hash } }) => {
59 const commitRef = preloadQuery<CommitQueryData>(COMMIT_QUERY, {
60 variables: { repo: ref, hash },
61 });
62 return { commitRef: await preloadQuery.toPromise(commitRef) };
63 },
64});
65
66function RouteComponent() {
67 const { ref } = Route.useRouteContext();
68 const { repo } = Route.useParams();
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, 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 repo={ref}
141 hash={commit.hash}
142 path={file.path}
143 oldPath={file.oldPath ?? undefined}
144 status={file.status}
145 />
146 ))}
147 </div>
148 </div>
149 </div>
150 );
151}
152
153function CommitPageSkeleton() {
154 return (
155 <div className="space-y-6">
156 <Skeleton className="h-4 w-24" />
157 <div className="border-border space-y-3 rounded-md border p-5">
158 <Skeleton className="h-6 w-3/4" />
159 <Skeleton className="h-4 w-1/3" />
160 <Skeleton className="h-3 w-1/2" />
161 </div>
162 <div className="divide-border border-border divide-y rounded-md border">
163 {Array.from({ length: 5 }).map((_, i) => (
164 <div key={i} className="flex items-center gap-3 px-4 py-2.5">
165 <Skeleton className="size-4" />
166 <Skeleton className="h-4 flex-1" />
167 </div>
168 ))}
169 </div>
170 </div>
171 );
172}