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