CommitPage.tsx

  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> &lt;{commit.authorEmail}&gt;</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}