import Link from "next/link";
import { commitCount, lastCommit, listTree, type Commit, type TreeEntry } from "@/lib/git";
import { encodePath, shortSha, timeAgo } from "@/lib/format";
import type { Repo } from "@/lib/repo";
import { ClockIcon, CommitIcon, DirIcon, FileIcon, SubmoduleIcon } from "./icons";
import { liftRowClass } from "./repo-rows";
import { Avatar, Box } from "./ui";
// Looking up the last commit is one git call per entry; skip it for huge directories.
const MAX_ENTRIES_WITH_COMMITS = 200;
/** A short sha on a small raised key: the commit glyph and the mono hash. */
export function ShaChip({ sha, href, className = "" }: { sha: string; href: string; className?: string }) {
return (
<Link
href={href}
className={`material-cap inline-flex h-6 shrink-0 items-center gap-1 rounded-key px-1.5 font-mono text-[11px] text-ink2 transition-colors hover:text-ink focus-visible:focus-ring ${className}`}
>
<CommitIcon width={12} height={12} className="text-ink3" /> {shortSha(sha)}
</Link>
);
}
/** The last commit to touch a path, sunk into the head of its plate: who, what, which and when, and the way to the history. */
export function LatestCommitBar({ repo, commit, count, gitRef, path }: {
repo: Repo;
commit: Commit;
count?: number;
gitRef: string;
path?: string;
}) {
const historyHref = `${repo.base}/commits/${encodePath(gitRef)}${path ? `/${encodePath(path)}` : ""}`;
return (
<div className="material-well flex min-h-11 items-center gap-2.5 rounded-plate py-1.5 pr-1.5 pl-3 type-ui">
<Avatar name={commit.authorName} size={20} />
<span className="shrink-0 font-semibold text-ink">{commit.authorName}</span>
<Link href={`${repo.base}/commit/${commit.sha}`} className="truncate text-ink2 transition-colors hover:text-ink">
{commit.subject}
</Link>
<span className="ml-auto flex shrink-0 items-center gap-2.5">
<ShaChip sha={commit.sha} href={`${repo.base}/commit/${commit.sha}`} className="hidden sm:inline-flex" />
<time dateTime={new Date(commit.date).toISOString()} className="hidden type-readout text-ink3 sm:inline">
{timeAgo(commit.date)}
</time>
<Link
href={historyHref}
className="mu-icon-trigger flex h-7 items-center gap-1.5 rounded-pill px-2.5 text-ink transition-colors hover:recipe-icon-button-ghost-hover focus-visible:focus-ring"
>
<ClockIcon width={14} height={14} className="text-ink2" />
{count !== undefined ? (
<>
<span className="type-readout">{count}</span> commit{count === 1 ? "" : "s"}
</>
) : (
"History"
)}
</Link>
</span>
</div>
);
}
function entryIcon(entry: TreeEntry) {
if (entry.type === "tree") return <DirIcon width={15} height={15} className="shrink-0 fill-ink3/20 text-ink3" />;
if (entry.type === "commit") return <SubmoduleIcon width={15} height={15} className="shrink-0 text-ink3" />;
return <FileIcon width={15} height={15} className="shrink-0 text-ink3" />;
}
// One row of the listing: the name, the last commit to touch it (md and up) and when.
const ROW = `${liftRowClass} grid h-9 grid-cols-[minmax(0,1fr)_8.5rem] items-center gap-4 px-3 type-ui md:grid-cols-[minmax(0,1fr)_minmax(0,2fr)_8.5rem]`;
export async function FileTable({ repo, gitRef, path }: { repo: Repo; gitRef: string; path: string }) {
const entries = await listTree(repo.dir, gitRef, path);
const withCommits = entries.length <= MAX_ENTRIES_WITH_COMMITS;
const [latest, count, commits] = await Promise.all([
lastCommit(repo.dir, gitRef, path || undefined),
path ? undefined : commitCount(repo.dir, gitRef),
withCommits ? Promise.all(entries.map((e) => lastCommit(repo.dir, gitRef, e.path))) : [],
]);
const parent = path.split("/").slice(0, -1).join("/");
return (
<Box className="p-1.5">
{latest && <LatestCommitBar repo={repo} commit={latest} count={count} gitRef={gitRef} path={path} />}
<ul aria-label="Files" className={latest ? "mt-1.5" : ""}>
{path && (
<li className={ROW}>
<Link
href={`${repo.base}/tree/${encodePath(gitRef)}${parent ? `/${encodePath(parent)}` : ""}`}
aria-label="Parent directory"
className="col-span-full flex items-center gap-2.5 text-ink2 transition-colors hover:text-ink"
>
<DirIcon width={15} height={15} className="fill-ink3/20 text-ink3" /> ..
</Link>
</li>
)}
{entries.map((entry, i) => {
const commit = commits[i];
const kind = entry.type === "tree" ? "tree" : "blob";
return (
<li key={entry.name} className={ROW}>
<span className="flex min-w-0 items-center gap-2.5">
{entryIcon(entry)}
{entry.type === "commit" ? (
<span className="truncate text-ink">
{entry.name} <span className="font-mono text-[11px] text-ink3">@ {shortSha(entry.sha)}</span>
</span>
) : (
<Link
href={`${repo.base}/${kind}/${encodePath(gitRef)}/${encodePath(entry.path)}`}
className="truncate text-ink decoration-ink3 underline-offset-4 hover:underline"
>
{entry.name}
</Link>
)}
</span>
<span className="hidden min-w-0 truncate text-ink2 md:block">
{commit && (
<Link href={`${repo.base}/commit/${commit.sha}`} className="transition-colors hover:text-ink">
{commit.subject}
</Link>
)}
</span>
<span className="text-right whitespace-nowrap type-readout text-ink3">
{commit && <time dateTime={new Date(commit.date).toISOString()}>{timeAgo(commit.date)}</time>}
</span>
</li>
);
})}
</ul>
</Box>
);
}