import clsx from 'clsx';
import {
ArrowLeft,
Copy,
CopyCheck,
Ellipsis,
FolderCode,
} from 'lucide-solid';
import { A, useNavigate, useParams, useSearchParams } from '@solidjs/router';
import { createQuery } from '@tanstack/solid-query';
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js';
import {
getRepoBranches,
getRepoCommit,
getRepoLog,
getRepoTags,
type CommitEntry,
type CommitSignature,
type DiffTextLine,
type GitHashValue,
type RepoContext,
type RepoDiff,
type RepoDiffFile,
type TagResponse,
} from '../../lib/api/repos';
import { Avatar, ErrorState, PaginationControls, PlaceholderAvatar, SkeletonBlock, cardStyles } from '../../components/common';
import { DiffView } from '../../components/repo';
import { RepoFrame, useRepoQuery } from './shared';
import {
commitHref,
commitsHref,
formatRelativeTime,
getErrorMessage,
parseIntegerSearchParam,
safeDecode,
treeHref,
} from '../../lib/repo-utils';
const COMMITS_PAGE_LIMIT = 60;
const commitListSkeletonRows = Array.from({ length: COMMITS_PAGE_LIMIT });
const commitDiffSkeletonFiles = Array.from({ length: 2 });
const commitDiffSkeletonTreeRows = Array.from({ length: 8 });
const commitDiffSkeletonLines = Array.from({ length: 9 });
const hashBytesToHex = (value: number[]): string =>
value.map((byte) => byte.toString(16).padStart(2, '0')).join('');
const normalizeHashValue = (value?: GitHashValue): string => {
if (!value) return '';
if (typeof value === 'string') return value;
if (Array.isArray(value)) return hashBytesToHex(value);
return value.String ?? value.string ?? '';
};
const commitHash = (commit: CommitEntry): string => commit.this || normalizeHashValue(commit.hash);
const primaryParentHash = (commit: CommitEntry): string =>
commit.parent || commit.parent_hashes?.map(normalizeHashValue).find(Boolean) || '';
const splitCommitMessage = (message: string) => {
const trimmed = message.trimEnd();
const [subject, ...bodyParts] = trimmed.split(/\n\n/);
return {
subject: subject?.trim() || 'no commit message',
body: bodyParts.join('\n\n').trim(),
};
};
const commitTime = (commit: CommitEntry): string =>
commit.committer?.When || commit.author?.When || '';
const commitAuthor = (commit: CommitEntry): CommitSignature | undefined =>
commit.author ?? commit.committer;
const personLabel = (person?: CommitSignature): string =>
person?.Name || person?.Email || 'unknown author';
const personDid = (person?: CommitSignature): string | undefined =>
person?.Email?.startsWith('did:') ? person.Email : undefined;
const formatFullTime = (value?: string): string => {
if (!value) return 'unknown time';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
}).format(date);
};
const commitListPageHref = (repo: RepoContext, ref: string, page: number): string =>
page <= 1 ? commitsHref(repo, ref) : `${commitsHref(repo, ref)}?page=${page}`;
const addRefLabel = (map: Map<string, string[]>, hash: string | undefined, label: string | undefined) => {
if (!hash || !label) return;
const existing = map.get(hash) ?? [];
if (!existing.includes(label)) {
map.set(hash, [...existing, label]);
}
};
const tagTargetHash = (tag: TagResponse['tags'][number]): string => {
const fullTag = tag.tag as (TagResponse['tags'][number]['tag'] & {
Target?: GitHashValue;
target?: GitHashValue;
}) | undefined;
return normalizeHashValue(fullTag?.Target ?? fullTag?.target) || tag.hash;
};
const formatRange = (start: number, count: number): string =>
count === 1 ? String(start) : `${start},${count}`;
const diffLinePrefix = (line: DiffTextLine): ' ' | '-' | '+' => {
if (line.Op === 1 || line.Op === '-') return '-';
if (line.Op === 2 || line.Op === '+') return '+';
return ' ';
};
const renderFragment = (fragment: RepoDiffFile['text_fragments'][number]): string => {
let text = `@@ -${formatRange(fragment.OldPosition, fragment.OldLines)} +${formatRange(fragment.NewPosition, fragment.NewLines)} @@`;
if (fragment.Comment) {
text += ` ${fragment.Comment}`;
}
text += '\n';
for (const line of fragment.Lines ?? []) {
const rendered = `${diffLinePrefix(line)}${line.Line ?? ''}`;
text += rendered.endsWith('\n') ? rendered : `${rendered}\n`;
}
return text;
};
const renderDiffFile = (file: RepoDiffFile): string => {
let oldName = file.name.old;
let newName = file.name.new;
if (!oldName) oldName = newName;
if (!newName) newName = oldName;
let text = `diff --git a/${oldName} b/${newName}\n`;
if (file.is_new) {
text += 'new file mode 100644\n';
text += '--- /dev/null\n';
text += `+++ b/${newName}\n`;
} else if (file.is_delete) {
text += 'deleted file mode 100644\n';
text += `--- a/${oldName}\n`;
text += '+++ /dev/null\n';
} else if (file.is_rename) {
text += `rename from ${oldName}\n`;
text += `rename to ${newName}\n`;
text += `--- a/${oldName}\n`;
text += `+++ b/${newName}\n`;
} else {
text += `--- a/${oldName}\n`;
text += `+++ b/${newName}\n`;
}
if (file.is_binary) {
text += `Binary files a/${oldName} and b/${newName} differ\n`;
return text;
}
for (const fragment of file.text_fragments ?? []) {
text += renderFragment(fragment);
}
return text;
};
const renderUnifiedDiff = (diff?: RepoDiff): string =>
diff?.diff.map(renderDiffFile).join('') ?? '';
const CommitIdentity: Component<{ person?: CommitSignature; size?: string }> = (props) => (
<span class="flex items-center gap-1 min-w-0">
<Show
when={personDid(props.person)}
fallback={<PlaceholderAvatar size={props.size ?? 'size-6'} iconSize="size-4" />}
>
{(did) => <Avatar did={did()} size={props.size ?? 'size-6'} />}
</Show>
<span class="truncate">{personLabel(props.person)}</span>
</span>
);
const CopyShaButton: Component<{ hash: string }> = (props) => {
const [copied, setCopied] = createSignal(false);
const copy = () => {
void navigator.clipboard?.writeText(props.hash);
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
};
return (
<button
type="button"
class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
title="Copy SHA"
aria-label="Copy SHA"
onClick={copy}
>
<Show when={copied()} fallback={<Copy class="w-4 h-4" />}>
<CopyCheck class="w-4 h-4" />
</Show>
</button>
);
};
const CommitHashLink: Component<{ repo: RepoContext; hash: string }> = (props) => (
<A
href={commitHref(props.repo, props.hash)}
class="no-underline hover:underline text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900 px-2 py-1 rounded flex items-center gap-2"
>
{props.hash.slice(0, 8)}
</A>
);
const RefLabels: Component<{ labels: string[] }> = (props) => (
<For each={props.labels}>
{(label) => (
<span class="ml-2 text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 inline-flex items-center">
{label}
</span>
)}
</For>
);
const CommitMessageCell: Component<{
repo: RepoContext;
commit: CommitEntry;
hash: string;
labels: string[];
}> = (props) => {
const [expanded, setExpanded] = createSignal(false);
const message = createMemo(() => splitCommitMessage(props.commit.message));
return (
<div>
<A href={commitHref(props.repo, props.hash)} class="dark:text-white no-underline hover:underline">
{message().subject}
</A>
<Show when={message().body}>
<button
type="button"
class="ml-1 py-0.5 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded"
aria-label={expanded() ? 'Hide commit message body' : 'Show commit message body'}
onClick={() => setExpanded(!expanded())}
>
<Ellipsis class="w-3 h-3" />
</button>
</Show>
<RefLabels labels={props.labels} />
<Show when={expanded() && message().body}>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap">{message().body}</p>
</Show>
</div>
);
};
const CommitDesktopRow: Component<{
repo: RepoContext;
commit: CommitEntry;
labels: string[];
}> = (props) => {
const hash = createMemo(() => commitHash(props.commit));
const when = createMemo(() => commitTime(props.commit));
return (
<div class="grid grid-cols-14 gap-4 py-3">
<div class="align-top col-span-3">
<CommitIdentity person={commitAuthor(props.commit)} />
</div>
<div class="align-top font-mono flex items-start col-span-3">
<CommitHashLink repo={props.repo} hash={hash()} />
<div class="inline-flex ml-1">
<CopyShaButton hash={hash()} />
<A
href={treeHref(props.repo, hash())}
class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
title="Browse repository at this commit"
aria-label="Browse repository at this commit"
>
<FolderCode class="w-4 h-4" />
</A>
</div>
</div>
<div class="align-top col-span-6">
<CommitMessageCell repo={props.repo} commit={props.commit} hash={hash()} labels={props.labels} />
</div>
<div class="align-top justify-self-end text-gray-500 dark:text-gray-400 col-span-2">
<time datetime={when()} title={formatFullTime(when())}>
{formatRelativeTime(when())}
</time>
</div>
</div>
);
};
const CommitMobileRow: Component<{
repo: RepoContext;
commit: CommitEntry;
labels: string[];
last: boolean;
}> = (props) => {
const hash = createMemo(() => commitHash(props.commit));
const when = createMemo(() => commitTime(props.commit));
return (
<div
class={clsx(
'relative p-2 mb-2',
!props.last && 'border-b border-gray-200 dark:border-gray-700',
)}
>
<div class="text-base">
<div class="flex items-center justify-between gap-2">
<div class="flex-1 min-w-0">
<CommitMessageCell repo={props.repo} commit={props.commit} hash={hash()} labels={props.labels} />
</div>
<A
href={treeHref(props.repo, hash())}
class="p-1 mr-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
title="Browse repository at this commit"
aria-label="Browse repository at this commit"
>
<FolderCode class="w-4 h-4" />
</A>
</div>
</div>
<div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center flex-wrap gap-y-2">
<span class="font-mono">
<CommitHashLink repo={props.repo} hash={hash()} />
</span>
<span class="mx-1 before:content-['·'] before:select-none" />
<CommitIdentity person={commitAuthor(props.commit)} />
<div class="inline-block px-1 select-none after:content-['·']" />
<time datetime={when()} title={formatFullTime(when())}>
{formatRelativeTime(when())}
</time>
</div>
</div>
);
};
const CommitListSkeleton: Component = () => (
<>
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
<div class="grid grid-cols-14 gap-4">
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
</div>
<For each={commitListSkeletonRows}>
{(_, index) => (
<div class="grid grid-cols-14 gap-4 py-3">
<div class="col-span-3 flex min-w-0 items-center gap-2">
<SkeletonBlock class="size-6 rounded-full" />
<SkeletonBlock class={index() % 3 === 0 ? 'h-4 w-24' : 'h-4 w-32'} />
</div>
<div class="col-span-3 flex items-center gap-2">
<SkeletonBlock class="h-8 w-24" />
<SkeletonBlock class="size-6" />
<SkeletonBlock class="size-6" />
</div>
<div class="col-span-6 flex min-w-0 items-center gap-2">
<SkeletonBlock class={index() % 2 === 0 ? 'h-5 w-3/4' : 'h-5 w-1/2'} />
<Show when={index() % 4 === 0}>
<SkeletonBlock class="h-5 w-14" />
</Show>
</div>
<div class="col-span-2 justify-self-end">
<SkeletonBlock class="h-4 w-16" />
</div>
</div>
)}
</For>
</div>
<div class="md:hidden">
<For each={commitListSkeletonRows}>
{(_, index) => (
<div class={clsx('relative p-2 mb-2', index() < commitListSkeletonRows.length - 1 && 'border-b border-gray-200 dark:border-gray-700')}>
<div class="flex items-center justify-between gap-2">
<SkeletonBlock class={index() % 2 === 0 ? 'h-5 w-2/3' : 'h-5 w-1/2'} />
<SkeletonBlock class="size-6" />
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<SkeletonBlock class="h-7 w-24" />
<SkeletonBlock class="size-6 rounded-full" />
<SkeletonBlock class="h-4 w-24" />
<SkeletonBlock class="h-4 w-16" />
</div>
</div>
)}
</For>
</div>
</>
);
const CommitListContent: Component<{
repo: RepoContext;
commits: CommitEntry[];
labelsByHash: Map<string, string[]>;
}> = (props) => (
<Show
when={props.commits.length > 0}
fallback={<p class="text-sm text-gray-500 dark:text-gray-400">No commits found.</p>}
>
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
<div class="grid grid-cols-14 gap-4">
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
</div>
<For each={props.commits}>
{(commit) => (
<CommitDesktopRow
repo={props.repo}
commit={commit}
labels={props.labelsByHash.get(commitHash(commit)) ?? []}
/>
)}
</For>
</div>
<div class="md:hidden">
<For each={props.commits}>
{(commit, index) => (
<CommitMobileRow
repo={props.repo}
commit={commit}
labels={props.labelsByHash.get(commitHash(commit)) ?? []}
last={index() === props.commits.length - 1}
/>
)}
</For>
</div>
</Show>
);
export const CommitsPage: Component = () => {
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const repoQuery = useRepoQuery();
const ref = createMemo(() => safeDecode(params.ref ?? 'HEAD'));
const page = createMemo(() => parseIntegerSearchParam(searchParams.page, 1, 1));
const pageOffset = createMemo(() => (page() - 1) * COMMITS_PAGE_LIMIT);
const cursor = createMemo(() => String(pageOffset()));
const branchesQuery = createQuery(() => {
const repo = repoQuery.data;
return {
queryKey: ['repo-branches', repo?.repoDid],
enabled: Boolean(repo),
queryFn: async () => getRepoBranches(repo!),
};
});
const logQuery = createQuery(() => {
const repo = repoQuery.data;
const hasBranches = branchesQuery.data && branchesQuery.data.branches.length > 0;
return {
queryKey: ['repo-commits', repo?.repoDid, ref(), page()],
enabled: Boolean(repo && hasBranches),
queryFn: async () => getRepoLog(repo!, ref(), {
limit: COMMITS_PAGE_LIMIT,
cursor: page() > 1 ? cursor() : undefined,
}),
};
});
const tagsQuery = createQuery(() => {
const repo = repoQuery.data;
const hasBranches = branchesQuery.data && branchesQuery.data.branches.length > 0;
return {
queryKey: ['repo-tags', repo?.repoDid],
enabled: Boolean(repo && hasBranches),
queryFn: async () => getRepoTags(repo!),
};
});
const labelsByHash = createMemo(() => {
const map = new Map<string, string[]>();
for (const branch of branchesQuery.data?.branches ?? []) {
addRefLabel(map, branch.reference.hash, branch.reference.name);
}
for (const tag of tagsQuery.data?.tags ?? []) {
addRefLabel(map, tagTargetHash(tag), tag.name);
}
return map;
});
const commits = createMemo(() => logQuery.data?.commits ?? []);
const totalCount = createMemo(() => {
const total = logQuery.data?.total;
return total && total > 0 ? total : undefined;
});
const hasNextPage = createMemo(() => {
const data = logQuery.data;
if (!data) return false;
if (totalCount()) {
return pageOffset() + COMMITS_PAGE_LIMIT < totalCount()!;
}
return data.commits.length === COMMITS_PAGE_LIMIT;
});
createEffect(() => {
if (repoQuery.data && branchesQuery.data && branchesQuery.data.branches.length === 0) {
navigate(`/${repoQuery.data.owner.handle}/${repoQuery.data.slug}`, { replace: true });
}
});
return (
<RepoFrame
active="code"
loadingFallback={
<section id="commit-table" class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white overflow-x-auto">
<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">commits</h2>
<CommitListSkeleton />
</section>
}
>
<section id="commit-table" class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white overflow-x-auto">
<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">commits</h2>
<Switch>
<Match when={logQuery.isLoading && !logQuery.data}>
<CommitListSkeleton />
</Match>
<Match when={logQuery.error}>
<ErrorState message={getErrorMessage(logQuery.error)} />
</Match>
<Match when={logQuery.data && repoQuery.data}>
<CommitListContent repo={repoQuery.data!} commits={commits()} labelsByHash={labelsByHash()} />
</Match>
</Switch>
</section>
<Show when={repoQuery.data && logQuery.data}>
<PaginationControls
offset={pageOffset()}
limit={COMMITS_PAGE_LIMIT}
totalCount={totalCount()}
hasNext={hasNextPage()}
hrefForOffset={(offset) =>
commitListPageHref(repoQuery.data!, ref(), Math.floor(Math.max(0, offset) / COMMITS_PAGE_LIMIT) + 1)
}
/>
</Show>
</RepoFrame>
);
};
const CommitAttribution: Component<{ commit: CommitEntry }> = (props) => {
const showCommitter = createMemo(
() => props.commit.author?.Email && props.commit.committer?.Email && props.commit.author.Email !== props.commit.committer.Email,
);
return (
<Show
when={showCommitter()}
fallback={<CommitIdentity person={commitAuthor(props.commit)} />}
>
<span class="inline-flex flex-wrap items-center gap-1">
<span>authored by</span>
<CommitIdentity person={props.commit.author} />
<span>and committed by</span>
<CommitIdentity person={props.commit.committer} />
</span>
</Show>
);
};
const CommitDiffSkeleton: Component = () => (
<section class="untangled-commit-diff mt-2 w-full">
<div class="untangled-diff-view untangled-diff-view-commit">
<div class="untangled-diff-panel">
<div class="untangled-diff-toolbar">
<div class="flex min-w-0 items-center gap-2">
<SkeletonBlock class="h-8 w-8" />
<SkeletonBlock class="h-7 w-20" />
<SkeletonBlock class="h-4 w-28" />
</div>
<div class="flex items-center gap-2">
<SkeletonBlock class="h-8 w-28" />
<SkeletonBlock class="h-8 w-32" />
</div>
</div>
<div class="untangled-diff-panel-body untangled-diff-panel-body-with-tree">
<aside class="untangled-pr-file-tree" aria-label="changed files loading">
<div class="untangled-pr-file-tree-list">
<For each={commitDiffSkeletonTreeRows}>
{(_, index) => (
<div class="untangled-pr-file-tree-entry">
<SkeletonBlock class="size-4 shrink-0 rounded-sm" />
<SkeletonBlock class={index() % 3 === 0 ? 'h-4 w-20' : 'h-4 w-32'} />
</div>
)}
</For>
</div>
</aside>
<div class="untangled-pr-diff-files">
<For each={commitDiffSkeletonFiles}>
{(_, fileIndex) => (
<div class="untangled-diff-file">
<div class="untangled-diff-file-header">
<div class="flex min-w-0 items-center gap-2">
<SkeletonBlock class="size-4 shrink-0" />
<SkeletonBlock class="h-7 w-20" />
<SkeletonBlock class={fileIndex() === 0 ? 'h-5 w-2/3' : 'h-5 w-1/2'} />
</div>
</div>
<div class="space-y-2 bg-white p-4 dark:bg-gray-900">
<For each={commitDiffSkeletonLines}>
{(_, index) => (
<div class="untangled-skeleton-code-line">
<SkeletonBlock class="h-4 w-8" />
<SkeletonBlock class={index() % 4 === 0 ? 'h-4 w-1/2' : index() % 3 === 0 ? 'h-4 w-2/3' : 'h-4 w-11/12'} />
</div>
)}
</For>
</div>
</div>
)}
</For>
</div>
</div>
</div>
</div>
</section>
);
const CommitDetailSkeleton: Component = () => (
<div class="untangled-commit-view w-full">
<section class="untangled-commit-detail bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white">
<SkeletonBlock class="h-6 w-2/3" />
<div class="mt-3 space-y-2">
<SkeletonBlock class="h-4 w-5/6" />
<SkeletonBlock class="h-4 w-1/2" />
</div>
<div class="mt-4 flex flex-wrap items-center gap-2">
<SkeletonBlock class="size-6 rounded-full" />
<SkeletonBlock class="h-4 w-28" />
<SkeletonBlock class="h-4 w-32" />
<SkeletonBlock class="h-4 w-16" />
<SkeletonBlock class="h-4 w-16" />
</div>
</section>
<CommitDiffSkeleton />
</div>
);
const CommitDetailContent: Component<{
repo: RepoContext;
commit: CommitEntry;
hash: string;
parentHash: string;
message: ReturnType<typeof splitCommitMessage>;
patch: string;
}> = (props) => {
const when = createMemo(() => commitTime(props.commit));
return (
<div class="untangled-commit-view w-full">
<section class="untangled-commit-detail bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white">
<div id="commit-message">
<p class="pb-2">{props.message.subject}</p>
<Show when={props.message.body}>
<p class="mt-1 cursor-text pb-2 text-sm whitespace-pre-wrap">{props.message.body}</p>
</Show>
</div>
<div class="flex flex-wrap items-center gap-2 text-sm">
<p class="flex flex-wrap items-center gap-1 text-gray-500 dark:text-gray-300">
<CommitAttribution commit={props.commit} />
<span class="px-1 select-none before:content-['·']" />
<time datetime={when()} title={formatFullTime(when())}>
{formatFullTime(when())}
</time>
<span class="px-1 select-none before:content-['·']" />
<A
href={commitHref(props.repo, props.hash)}
class="no-underline hover:underline text-gray-500 dark:text-gray-300"
>
{props.hash.slice(0, 8)}
</A>
<Show when={props.parentHash}>
{(parent) => (
<>
<ArrowLeft class="w-3 h-3 mx-1" />
<A
href={commitHref(props.repo, parent())}
class="no-underline hover:underline text-gray-500 dark:text-gray-300"
>
{parent().slice(0, 8)}
</A>
</>
)}
</Show>
</p>
</div>
</section>
<section class="untangled-commit-diff mt-2 w-full">
<DiffView patch={props.patch} wrap defaultExpanded showFileTree variant="commit" />
</section>
</div>
);
};
export const CommitPage: Component = () => {
const params = useParams();
const repoQuery = useRepoQuery();
const ref = createMemo(() => safeDecode(params.ref ?? ''));
const commitQuery = createQuery(() => {
const repo = repoQuery.data;
return {
queryKey: ['repo-commit', repo?.repoDid, ref()],
enabled: Boolean(repo && ref()),
queryFn: async () => getRepoCommit(repo!, ref()),
};
});
const commit = createMemo(() => commitQuery.data?.diff?.commit);
const hash = createMemo(() => {
const current = commit();
return current ? commitHash(current) : ref();
});
const parentHash = createMemo(() => {
const current = commit();
return current ? primaryParentHash(current) : '';
});
const message = createMemo(() => splitCommitMessage(commit()?.message ?? ''));
const patch = createMemo(() => renderUnifiedDiff(commitQuery.data?.diff));
return (
<RepoFrame active="code" fullWidth loadingFallback={<CommitDetailSkeleton />}>
<Switch>
<Match when={commitQuery.isLoading}>
<CommitDetailSkeleton />
</Match>
<Match when={commitQuery.error}>
<div class={cardStyles('p-6')}>
<ErrorState message={getErrorMessage(commitQuery.error)} />
</div>
</Match>
<Match when={commit() && repoQuery.data}>
<CommitDetailContent
repo={repoQuery.data!}
commit={commit()!}
hash={hash()}
parentHash={parentHash()}
message={message()}
patch={patch()}
/>
</Match>
</Switch>
</RepoFrame>
);
};