import { File, Folder, GitBranch, GitCommitHorizontal, List } from 'lucide-solid';
import { A, useNavigate, useParams } from '@solidjs/router';
import { createQuery, keepPreviousData, useQueryClient } from '@tanstack/solid-query';
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, type Accessor, type Component } from 'solid-js';
import { buildBlobDataUrl, decodeBlobBytes, decodeBlobText, getRepoBlob, getRepoBranches, getRepoDefaultBranch, getRepoLanguages, getRepoLog, getRepoTags, getRepoTree, type CommitHeadline } from '../../lib/api/repos';
import { buildBlobDataUrl, decodeBlobBytes, decodeBlobText, getRepoBlob, getRepoBranches, getRepoDefaultBranch, getRepoLanguages, getRepoLog, getRepoTags, getRepoTree, type CommitHeadline, type RepoContext, type BranchEntry } from '../../lib/api/repos';
import { Avatar, ErrorState, PlaceholderAvatar, buttonStyles, cardStyles } from '../../components/common';
import { CloneDropdown, CodeView, FileRow, MarkdownBlock, OverviewFileRow, ReadmeCard, RepoBlobSkeleton, RepoTreeSkeleton, PathBreadcrumbs, TreeLatestCommitPanel, RepoLanguageBar, LoadingFilePath } from '../../components/repo';
import { RepoFrame, useRepoQuery } from './shared';
import { blobHref, commitHref, commitsHref, countLines, decodeRoutePath, formatBytes, formatRelativeTime, getErrorMessage, getParentPath, imageLike, isDirectory, joinPath, markdownLike, safeDecode, sortedTreeEntries, svgLike, treeHref, updateRecentRepoLanguage, videoLike } from '../../lib/repo-utils';
import { LOADING_DELAY_MS, hasNamedRef, normalizeLanguages, resolveDefaultBranchName, resolveRouteRefAndPath, shouldAnimateNavigation, useDelayedLoading } from './code-helpers';
import { useAuth } from '../../lib/auth';
const readmeIsAvailable = (
readme: { filename: string; contents: string } | undefined,
entries: Array<{ name: string; mode: string }>,
219 unmodified lines
Expand all
};
void RepoCodePageLegacy;
const getKnotHost = (knotUrl: string): string => {
try {
const url = new URL(knotUrl);
let host = url.hostname;
if (host === 'knot1.tangled.sh') {
host = 'tangled.org';
}
return host;
} catch {
return knotUrl;
}
};
interface EmptyRepoViewProps {
repo: RepoContext;
branches: BranchEntry[];
}
const EmptyRepoView: Component<EmptyRepoViewProps> = (props) => {
const auth = useAuth();
const isOwner = createMemo(() => {
const current = auth.currentDid();
return Boolean(current && current === props.repo.owner.did);
});
return (
<section class="bg-white dark:bg-gray-800 px-6 py-6 rounded relative w-full mx-auto dark:text-white min-h-[250px] flex items-center justify-center">
<Switch>
<Match when={props.branches.length > 0}>
<div class="flex flex-col items-center w-full max-w-xl mx-auto">
<p class="text-center text-gray-500 dark:text-gray-400">
This branch is empty. Other branches in this repository are populated:
</p>
<div class="mt-4 grid grid-cols-1 divide-y divide-gray-200 dark:divide-gray-700 rounded border border-gray-200 dark:border-gray-700 w-full bg-gray-50 dark:bg-gray-900/50">
<For each={props.branches}>
{(branch) => (
<A
href={treeHref(props.repo, branch.reference.name)}
class="no-underline hover:bg-gray-100 dark:hover:bg-gray-800 block p-3 transition-colors duration-150"
>
<div class="flex items-center justify-between">
<span class="font-medium text-blue-600 dark:text-blue-400">{branch.reference.name}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{branch.commit?.Committer?.When ? formatRelativeTime(branch.commit.Committer.When) : ''}
</span>
</div>
</A>
)}
</For>
</div>
</div>
</Match>
<Match when={isOwner()}>
<div class="w-full max-w-2xl mx-auto">
<div class="py-4 flex flex-col gap-5 text-gray-700 dark:text-gray-300">
<p class="font-semibold text-lg text-gray-900 dark:text-white">This is an empty repository. To get started:</p>
<div class="space-y-4">
<p class="flex items-start gap-3">
<span class="text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full w-5 h-5 flex items-center justify-center font-mono flex-shrink-0 mt-0.5">1</span>
<span>
First, generate a new{' '}
<a
href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key"
target="_blank"
rel="noopener noreferrer"
class="underline text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
SSH key pair
</a>
.
</span>
</p>
<p class="flex items-start gap-3">
<span class="text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full w-5 h-5 flex items-center justify-center font-mono flex-shrink-0 mt-0.5">2</span>
<span>
Then add the public key to your account from the{' '}
<A
href="/settings/keys"
class="underline text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
keys page
</A>{' '}
in your settings.
</span>
</p>
<p class="flex items-start gap-3">
<span class="text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full w-5 h-5 flex items-center justify-center font-mono flex-shrink-0 mt-0.5">3</span>
<span class="break-all">
Configure your remote to:{' '}
<code class="bg-gray-100 dark:bg-gray-900 px-2 py-1 rounded font-mono text-sm border border-gray-200 dark:border-gray-800 text-gray-900 dark:text-gray-100 select-all block mt-1.5 w-fit">
git@{getKnotHost(props.repo.knot)}:{props.repo.repoDid}
</code>
</span>
</p>
<p class="flex items-start gap-3">
<span class="text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full w-5 h-5 flex items-center justify-center font-mono flex-shrink-0 mt-0.5">4</span>
<span>Push!</span>
</p>
</div>
</div>
</div>
</Match>
<Match when={true}>
<p class="text-gray-400 dark:text-gray-500 py-12 text-center text-lg">
This is an empty repository.
</p>
</Match>
</Switch>
</section>
);
};
export const RepoCodePage: Component = () => {
const params = useParams();
const navigate = useNavigate();
const queryClient = useQueryClient();
queryKey: ['repo-branches', repo?.repoDid],
enabled: Boolean(repo),
queryFn: async () => getRepoBranches(repo!),
};
});
const defaultBranchQuery = createQuery(() => {
}); const defaultBranchQuery = createQuery(() => {
const repo = repoQuery.data;
const hasBranches = branchesQuery.data && branchesQuery.data.branches.length > 0;
return {
queryKey: ['repo-default-branch', repo?.repoDid],
enabled: Boolean(repo),
enabled: Boolean(repo && hasBranches),
queryFn: async () => getRepoDefaultBranch(repo!),
};
});
const defaultBranch = createMemo(
),
);
const ref = createMemo(() => routeTarget().ref);
const path = createMemo(() => routeTarget().path);
const effectiveRef = createMemo(() => (ref() === 'HEAD' ? (defaultBranch() ?? (defaultBranchReady() ? 'HEAD' : undefined)) : ref()));
const effectiveRef = createMemo(() => {
const branches = branchesQuery.data?.branches;
if (!branches || branches.length === 0) {
return undefined;
}
return (ref() === 'HEAD' ? (defaultBranch() ?? (defaultBranchReady() ? 'HEAD' : undefined)) : ref());
});
const treeQuery = createQuery(() => {
const repo = repoQuery.data;
const currentRef = effectiveRef();
return {
return currentRouteRef === 'HEAD' ? (defaultBranch() ?? currentRouteRef) : currentRouteRef;
});
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),
enabled: Boolean(repo && hasBranches),
queryFn: async () => getRepoTags(repo!),
};
});
const logQuery = createQuery(() => {
);
const languagesLoading = createMemo(() => !languagesQuery.data && (languagesQuery.isLoading || languagesQuery.isFetching));
const showOverviewSkeleton = useDelayedLoading(() => !overviewReady() && !overviewError());
const isRefNotFoundError = (err: unknown): boolean => {
if (!err) return false;
if (typeof err === 'object') {
const e = err as any;
if (e.error === 'RefNotFound' || e.status === 404 || e.name === 'RefNotFound' || e.code === 'RefNotFound') {
return true;
}
}
const msg = getErrorMessage(err).toLowerCase();
return (
msg.includes('refnotfound') ||
msg.includes('failed to access ref') ||
msg.includes('reference not found') ||
msg.includes('ref not found')
);
};
const isEmptyRepo = createMemo(() => {
return Boolean(branchesQuery.data && branchesQuery.data.branches.length === 0);
});
const isEmptyBranch = createMemo(() => {
if (branchesQuery.data && branchesQuery.data.branches.length > 0) {
if (treeQuery.error && isRefNotFoundError(treeQuery.error)) {
return true;
}
}
return false;
});
createEffect(() => {
if (!treeQuery.isFetching) setPendingTreeHref(undefined);
});
return (
<RepoFrame active="code" loadingFallback={<RepoTreeSkeleton nested={Boolean(routePath())} />}>
<Switch>
<Match when={!overviewReady() && !overviewError() && showOverviewSkeleton()}>
<Match when={!overviewReady() && !overviewError() && !isEmptyRepo() && !isEmptyBranch() && showOverviewSkeleton()}>
<RepoTreeSkeleton nested={Boolean(path())} />
</Match>
<Match when={isEmptyRepo()}>
<EmptyRepoView repo={repoQuery.data!} branches={[]} />
</Match>
<Match when={isEmptyBranch()}>
<EmptyRepoView repo={repoQuery.data!} branches={branchesQuery.data?.branches ?? []} />
</Match>
<Match when={overviewError()}>
<div class={cardStyles('p-6')}>
<ErrorState message={getErrorMessage(overviewError())} />
</div>
470 unmodified lines
Expand all