web/app/%5Bowner%5D/%5Brepo%5D/(tabs)/page.tsxblame
View source
e4a233c1import type { Metadata } from "next";
3e3af552import Link from "next/link";
bf5fc333import { FileIcon } from "@/app/components/file-icon";
bf5fc334import { Markdown } from "@/app/components/markdown";
59a80f95import { GitImportForm } from "@/app/components/git-import-form";
bc1b2ba6import { encodePath, timeAgo } from "@/lib/utils";
bc1b2ba7import {
bc1b2ba8 getRepoBranches,
bc1b2ba9 getRepoTree,
bc1b2ba10 getRepoBlob,
bc1b2ba11 getRepoCommits,
bc1b2ba12} from "@/lib/grove-api";
3e3af5513
3e3af5514interface Props {
3e3af5515 params: Promise<{ owner: string; repo: string }>;
3e3af5516}
3e3af5517
e4a233c18export async function generateMetadata({ params }: Props): Promise<Metadata> {
86450dc19 const { repo } = await params;
86450dc20 return { title: `Grove · ${repo}` };
e4a233c21}
e4a233c22
80fafdf23async function findReadme(owner: string, repo: string, ref: string, entries: any[]) {
bf5fc3324 const readmeNames = ["README.md", "README", "readme.md", "README.txt"];
bf5fc3325 const readmeEntry = entries.find(
f0bb19226 (e: any) => e.type !== "tree" && readmeNames.includes(e.name)
bf5fc3327 );
bf5fc3328 if (!readmeEntry) return null;
bc1b2ba29 const blob = await getRepoBlob(owner, repo, ref, readmeEntry.name);
bf5fc3330 return blob?.content ?? null;
bf5fc3331}
bf5fc3332
bf5fc3333function sortEntries(entries: any[]) {
bf5fc3334 return [...entries].sort((a, b) => {
bf5fc3335 if (a.type === b.type) return a.name.localeCompare(b.name);
bf5fc3336 return a.type === "tree" ? -1 : 1;
bf5fc3337 });
bf5fc3338}
bf5fc3339
3e3af5540export default async function RepoPage({ params }: Props) {
3e3af5541 const { owner, repo } = await params;
3e3af5542
bc1b2ba43 const bookmarks = await getRepoBranches(owner, repo);
bf5fc3344 if (!bookmarks) {
3e3af5545 return (
530592e46 <div className="py-10">
cf89d3c47 <h1 className="text-lg" style={{ color: "var(--text-secondary)" }}>
135dfe548 Repository not found
135dfe549 </h1>
135dfe550 <p className="text-sm mt-1" style={{ color: "var(--text-muted)" }}>
3e3af5551 {owner}/{repo} does not exist or you don&apos;t have access.
3e3af5552 </p>
3e3af5553 </div>
3e3af5554 );
3e3af5555 }
3e3af5556
f0bb19257 const branches = bookmarks.branches ?? bookmarks.bookmarks ?? [];
f0bb19258 const mainBookmark = branches.find((b: any) => b.name === "main");
f0bb19259 const ref = mainBookmark?.name ?? branches[0]?.name ?? "main";
2dc32fd60 const [tree, latestCommitData] = await Promise.all([
bc1b2ba61 getRepoTree(owner, repo, ref),
bc1b2ba62 getRepoCommits(owner, repo, ref, { limit: 1 }),
2dc32fd63 ]);
80fafdf64 const readme = tree ? await findReadme(owner, repo, ref, tree.entries) : null;
bf5fc3365 const isMarkdown = readme !== null;
2dc32fd66 const latestCommit = latestCommitData?.commits?.[0] ?? null;
bc2f20567 const sortedEntries = tree?.entries ? sortEntries(tree.entries) : [];
bc2f20568 const isEmptyRepo = !latestCommit && sortedEntries.length === 0;
2dc32fd69 const latestCommitAuthor =
2dc32fd70 latestCommit?.author?.split("<")[0]?.trim() ?? latestCommit?.author ?? "";
2dc32fd71 const latestCommitInitial = latestCommitAuthor?.[0]?.toUpperCase() ?? "?";
2dc32fd72 const latestCommitSubject =
2dc32fd73 latestCommit?.subject ?? latestCommit?.message?.split("\n")[0] ?? "Commit";
bc2f20574 const emptyRepoOptions = [
bc2f20575 {
bc2f20576 title: "Clone empty and start",
bc2f20577 description: "Create the first commit in this repository.",
bc2f20578 commands: [
8d8e81579 `grove clone ${owner}/${repo}`,
bc2f20580 `cd ${repo}`,
bc2f20581 `echo "# ${repo}" > README.md`,
bc2f20582 "sl add README.md",
bc2f20583 'sl commit -m "Initial commit"',
bc2f20584 `sl push --to ${ref}`,
bc2f20585 ],
bc2f20586 },
bc2f20587 {
bc2f20588 title: "Push existing Sapling repo",
bc2f20589 description: "Point your current Sapling repo at this remote and push.",
bc2f20590 commands: [
bc2f20591 "cd ~/src/my-existing-repo",
4ab131792 `sl path default "slapi:${repo}"`,
bc2f20593 `sl push --to ${ref}`,
bc2f20594 ],
bc2f20595 },
bc2f20596 ];
3e3af5597
3e3af5598 return (
530592e99 <>
2dc32fd100 {latestCommit && (
2dc32fd101 <div className="mb-3 text-sm" style={{ border: "1px solid var(--border-subtle)" }}>
2dc32fd102 <div className="flex items-center gap-3 py-2 pl-3 pr-3">
2dc32fd103 <span
2dc32fd104 title={latestCommit.author}
2dc32fd105 style={{
2dc32fd106 display: "inline-flex",
2dc32fd107 alignItems: "center",
2dc32fd108 justifyContent: "center",
2dc32fd109 width: 22,
2dc32fd110 height: 22,
2dc32fd111 borderRadius: "50%",
2dc32fd112 backgroundColor: "var(--bg-inset)",
2dc32fd113 border: "1px solid var(--border-subtle)",
2dc32fd114 color: "var(--text-muted)",
2dc32fd115 fontSize: "0.65rem",
2dc32fd116 cursor: "default",
2dc32fd117 }}
2dc32fd118 >
2dc32fd119 {latestCommitInitial}
2dc32fd120 </span>
2dc32fd121 <div className="min-w-0 flex-1">
2dc32fd122 <div className="truncate">
2dc32fd123 <Link
2dc32fd124 href={`/${owner}/${repo}/commit/${latestCommit.hash}`}
2dc32fd125 className="hover:underline"
2dc32fd126 style={{ color: "var(--text-primary)" }}
2dc32fd127 >
2dc32fd128 {latestCommitSubject}
2dc32fd129 </Link>
2dc32fd130 </div>
2dc32fd131 <div className="text-xs" style={{ color: "var(--text-faint)" }}>
2dc32fd132 Most recent commit {latestCommitAuthor ? `by ${latestCommitAuthor}` : ""}
2dc32fd133 </div>
2dc32fd134 </div>
2dc32fd135 <Link
2dc32fd136 href={`/${owner}/${repo}/commit/${latestCommit.hash}`}
2dc32fd137 className="font-mono text-xs hover:underline"
2dc32fd138 style={{ color: "var(--accent)" }}
2dc32fd139 >
2dc32fd140 {latestCommit.hash.slice(0, 7)}
2dc32fd141 </Link>
2dc32fd142 <span className="text-xs shrink-0" style={{ color: "var(--text-faint)" }}>
2dc32fd143 {timeAgo(latestCommit.timestamp)}
2dc32fd144 </span>
2dc32fd145 </div>
2dc32fd146 </div>
2dc32fd147 )}
2dc32fd148
bc2f205149 {isEmptyRepo ? (
bc2f205150 <div
bc2f205151 className="px-4 py-6"
bc2f205152 style={{
bc2f205153 backgroundColor: "var(--bg-card)",
bc2f205154 border: "1px solid var(--border-subtle)",
bc2f205155 }}
bc2f205156 >
bc2f205157 <p className="text-sm" style={{ color: "var(--text-secondary)" }}>
bc2f205158 This repository is empty
bc2f205159 </p>
bc2f205160 <p className="text-xs mt-1 mb-4" style={{ color: "var(--text-faint)" }}>
bc2f205161 Push your first commit to <span className="font-mono">{ref}</span> to get started.
bc2f205162 </p>
bc2f205163 <div className="grid gap-3">
59a80f9164 <GitImportForm owner={owner} repo={repo} />
bc2f205165 {emptyRepoOptions.map((option) => (
bc2f205166 <div
bc2f205167 key={option.title}
bdf6540168 className="text-left p-3 min-w-0 overflow-hidden"
bc2f205169 style={{
bc2f205170 backgroundColor: "var(--bg-inset)",
bc2f205171 border: "1px solid var(--border-subtle)",
bc2f205172 }}
bc2f205173 >
bc2f205174 <p className="text-xs mb-1" style={{ color: "var(--text-secondary)" }}>
bc2f205175 {option.title}
bc2f205176 </p>
bc2f205177 <p className="text-xs mb-2" style={{ color: "var(--text-faint)" }}>
bc2f205178 {option.description}
bc2f205179 </p>
bc2f205180 <pre
bc2f205181 className="text-xs whitespace-pre overflow-x-auto"
bc2f205182 style={{ color: "var(--text-muted)" }}
bc2f205183 >
bc2f205184{option.commands.join("\n")}
bc2f205185 </pre>
bc2f205186 </div>
bc2f205187 ))}
bc2f205188 </div>
bc2f205189 </div>
bc2f205190 ) : tree && sortedEntries.length > 0 ? (
135dfe5191 <div>
bf5fc33192 <div
bf5fc33193 className="text-sm"
bf5fc33194 style={{
bf5fc33195 border: "1px solid var(--border-subtle)",
bf5fc33196 }}
bf5fc33197 >
bc2f205198 {sortedEntries.map((entry: any, i: number) => (
bf5fc33199 <Link
bf5fc33200 key={entry.name}
bf5fc33201 href={
bf5fc33202 entry.type === "tree"
d744b82203 ? `/${owner}/${repo}/tree/${ref}/${encodePath(entry.name)}`
d744b82204 : `/${owner}/${repo}/blob/${ref}/${encodePath(entry.name)}`
bf5fc33205 }
bf5fc33206 className="flex items-center gap-2 py-1.5 pl-3 pr-3 hover-row"
bf5fc33207 style={{
bf5fc33208 borderTop:
bf5fc33209 i > 0 ? "1px solid var(--divide)" : undefined,
bf5fc33210 color: "var(--accent)",
bf5fc33211 }}
bf5fc33212 >
bf5fc33213 <FileIcon type={entry.type} name={entry.name} />
bf5fc33214 <span className="hover:underline">{entry.name}</span>
bf5fc33215 </Link>
bf5fc33216 ))}
bf5fc33217 </div>
3e3af55218 </div>
bc2f205219 ) : null}
3e3af55220
bf5fc33221 {readme && (
135dfe5222 <div className="mt-8">
bf5fc33223 {isMarkdown ? (
bf5fc33224 <Markdown content={readme} />
bf5fc33225 ) : (
bf5fc33226 <pre
bf5fc33227 className="whitespace-pre-wrap text-sm p-4"
bf5fc33228 style={{
bf5fc33229 color: "var(--text-secondary)",
bf5fc33230 backgroundColor: "var(--bg-card)",
bf5fc33231 border: "1px solid var(--border-subtle)",
bf5fc33232 }}
bf5fc33233 >
bf5fc33234 {readme}
bf5fc33235 </pre>
bf5fc33236 )}
3e3af55237 </div>
3e3af55238 )}
530592e239 </>
3e3af55240 );
3e3af55241}