1/** The Commits tab: paged history of the selected ref. */
2import { useEffect, useState } from "react";
3import { getLog, type CommitInfo } from "../client.ts";
4import { Button, CopyButton, ErrorBox, Spinner } from "../components.tsx";
5import { shortOid, subject, timeAgo } from "../format.ts";
6import { href, Link } from "../router.tsx";
7import type { RepoContext } from "./Repo.tsx";
8
9const CommitRow = ({
10 context,
11 commit,
12}: {
13 context: RepoContext;
14 commit: CommitInfo;
15}) => {
16 const [expanded, setExpanded] = useState(false);
17 const body = commit.message.split("\n").slice(1).join("\n").trim();
18 const commitHref = href(
19 context.repo.owner,
20 context.repo.name,
21 "commit",
22 commit.oid,
23 );
24 return (
25 <li className="border-b border-border-muted px-4 py-3 last:border-b-0">
26 <div className="flex items-start justify-between gap-4">
27 <div className="min-w-0">
28 <Link
29 to={commitHref}
30 className="text-left text-sm font-semibold hover:text-accent hover:underline"
31 >
32 {subject(commit.message)}
33 </Link>
34 {body.length > 0 && (
35 <button
36 type="button"
37 onClick={() => setExpanded((value) => !value)}
38 className="ml-2 cursor-pointer rounded border border-border-muted px-1 text-xs text-fg-muted hover:text-accent"
39 title={expanded ? "Hide description" : "Show description"}
40 >
41 …
42 </button>
43 )}
44 <p className="mt-0.5 text-xs text-fg-muted">
45 {commit.author.name} committed {timeAgo(commit.author.date * 1000)}
46 </p>
47 {expanded && body.length > 0 && (
48 <pre className="mt-2 overflow-x-auto rounded bg-canvas-subtle p-3 font-mono text-xs whitespace-pre-wrap">
49 {body}
50 </pre>
51 )}
52 </div>
53 <div className="flex shrink-0 items-center gap-1">
54 <Link
55 to={commitHref}
56 className="rounded border border-border-muted px-2 py-0.5 font-mono text-xs text-accent hover:underline"
57 >
58 {shortOid(commit.oid)}
59 </Link>
60 <CopyButton text={commit.oid} />
61 </div>
62 </div>
63 </li>
64 );
65};
66
67export const CommitsTab = ({ context }: { context: RepoContext }) => {
68 const [commits, setCommits] = useState<CommitInfo[] | null>(null);
69 const [cursor, setCursor] = useState<string | null>(null);
70 const [error, setError] = useState<unknown>(null);
71 const { connection, repo, refName } = context;
72
73 const load = async (nextCursor?: string) => {
74 try {
75 const page = await getLog(connection, repo.owner, repo.name, {
76 ref: refName,
77 limit: 50,
78 ...(nextCursor ? { cursor: nextCursor } : {}),
79 });
80 setCommits((existing) =>
81 nextCursor ? [...(existing ?? []), ...page.items] : page.items,
82 );
83 setCursor(page.hasMore ? page.nextCursor : null);
84 } catch (cause) {
85 setError(cause);
86 }
87 };
88
89 useEffect(() => {
90 setCommits(null);
91 void load();
92 // eslint-disable-next-line react-hooks/exhaustive-deps
93 }, [refName, repo.repoId]);
94
95 if (error != null) return <ErrorBox error={error} />;
96 if (commits === null) return <Spinner />;
97
98 return (
99 <div>
100 <ul className="overflow-hidden rounded-md border border-border-muted">
101 {commits.map((commit) => (
102 <CommitRow key={commit.oid} context={context} commit={commit} />
103 ))}
104 {commits.length === 0 && (
105 <li className="px-4 py-8 text-center text-sm text-fg-muted">
106 No commits on {refName}
107 </li>
108 )}
109 </ul>
110 {cursor !== null && (
111 <div className="mt-4 flex justify-center">
112 <Button onClick={() => void load(cursor)}>Older commits</Button>
113 </div>
114 )}
115 </div>
116 );
117};