1// Commit detail page (/:repo/commit/:hash). Shows commit metadata, full
2// message, parent links, and changed files with lazy diffs.
3
4import { gql, useQuery } from "@apollo/client";
5import { format } from "date-fns";
6import { ArrowLeft, GitCommit } from "lucide-react";
7import { Link, useParams, useNavigate } from "react-router-dom";
8
9import { FileDiffView } from "@/components/code/FileDiffView";
10import { Skeleton } from "@/components/ui/skeleton";
11import { useRepo } from "@/lib/repo";
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
37export function CommitPage() {
38 const { hash } = useParams<{ hash: string }>();
39 const navigate = useNavigate();
40 const repo = useRepo();
41
42 const { data, loading, error } = useQuery(COMMIT_QUERY, {
43 variables: { repo, hash },
44 skip: !hash,
45 });
46
47 if (loading) return <CommitPageSkeleton />;
48
49 if (error) {
50 return (
51 <div className="py-16 text-center text-sm text-destructive">
52 Failed to load commit: {error.message}
53 </div>
54 );
55 }
56
57 const commit = data?.repository?.commit;
58 if (!commit) return null;
59
60 const date = new Date(commit.date);
61 const files = commit.files?.nodes ?? [];
62
63 return (
64 <div>
65 <button
66 onClick={() => navigate(-1)}
67 className="mb-6 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
68 >
69 <ArrowLeft className="size-3.5" />
70 Back
71 </button>
72
73 <div className="mb-6 rounded-md border border-border p-5">
74 <div className="mb-1 flex items-start gap-3">
75 <GitCommit className="mt-1 size-5 shrink-0 text-muted-foreground" />
76 <h1 className="text-lg font-semibold leading-snug">{commit.message}</h1>
77 </div>
78
79 {commit.fullMessage.includes("\n") && (
80 <pre className="mb-4 ml-8 mt-3 whitespace-pre-wrap font-sans text-sm text-muted-foreground">
81 {commit.fullMessage.split("\n").slice(1).join("\n").trim()}
82 </pre>
83 )}
84
85 <div className="ml-8 mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
86 <span>
87 <span className="font-medium text-foreground">{commit.authorName}</span>
88 {commit.authorEmail && <span> <{commit.authorEmail}></span>}
89 </span>
90 <span title={date.toISOString()}>{format(date, "PPP")}</span>
91 </div>
92
93 <div className="ml-8 mt-3 flex flex-wrap gap-3 text-xs">
94 <span className="text-muted-foreground">
95 commit <code className="font-mono text-foreground">{commit.hash}</code>
96 </span>
97 {commit.parents.map((p: string) => (
98 <span key={p} className="text-muted-foreground">
99 parent{" "}
100 <Link
101 to={repo ? `/${repo}/commit/${p}` : `/commit/${p}`}
102 className="font-mono text-foreground hover:underline"
103 >
104 {p.slice(0, 7)}
105 </Link>
106 </span>
107 ))}
108 </div>
109 </div>
110
111 <div>
112 <h2 className="mb-3 text-sm font-semibold text-muted-foreground">
113 {files.length} file{files.length !== 1 ? "s" : ""} changed
114 </h2>
115 <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
116 {files.length === 0 && (
117 <p className="px-4 py-4 text-sm text-muted-foreground">No file changes.</p>
118 )}
119 {files.map((file: { path: string; oldPath?: string | null; status: string }) => (
120 <FileDiffView
121 key={file.path}
122 hash={commit.hash}
123 path={file.path}
124 oldPath={file.oldPath ?? undefined}
125 status={file.status}
126 />
127 ))}
128 </div>
129 </div>
130 </div>
131 );
132}
133
134function CommitPageSkeleton() {
135 return (
136 <div className="space-y-6">
137 <Skeleton className="h-4 w-24" />
138 <div className="space-y-3 rounded-md border border-border p-5">
139 <Skeleton className="h-6 w-3/4" />
140 <Skeleton className="h-4 w-1/3" />
141 <Skeleton className="h-3 w-1/2" />
142 </div>
143 <div className="divide-y divide-border rounded-md border border-border">
144 {Array.from({ length: 5 }).map((_, i) => (
145 <div key={i} className="flex items-center gap-3 px-4 py-2.5">
146 <Skeleton className="size-4" />
147 <Skeleton className="h-4 flex-1" />
148 </div>
149 ))}
150 </div>
151 </div>
152 );
153}