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