import { error, redirect } from "@sveltejs/kit"; import { getRequestEvent } from "$app/server"; import { branch as gitBranch, branches as gitBranches, gitTarget, hasGit, languages as gitLanguages, log as gitLog, requireGit, tags as gitTags, tree as gitTree } from "#lib/api/gitclient.ts"; import { toMirrorCommitSummary } from "#lib/api/knotmirror.ts"; import { orNull, parallel, stream } from "#lib/api/load.ts"; import { branchSummaries, importState, sortTreeEntries, tagsByCommitHash, toTagSummary, toTreeCommitSummary, toTreeEntrySummary, type ImportState } from "#lib/api/repo.ts"; import { attempt, BRANCH_LIMIT, classifyRepoAvailability, COMMIT_LIMIT, REF_LIMIT, TAG_LIMIT, toLanguageSlices, withAuthorHandles, type RepoAvailability } from "#lib/api/repoIndex.ts"; import type { RepoInfo } from "#lib/components/repo/types.ts"; import { blobHref } from "#lib/components/repo/urls.ts"; export interface RepoParent { publicConfig: { bobbinUrl: string }; repo: RepoInfo | Promise; } export interface RepoLoadEvent { url: URL; } const currentFirst = (branches: T[], ref: string): T[] => { const current = branches.find((branch) => branch.name === ref); return current ? [current, ...branches.filter((branch) => branch !== current)] : branches; }; const refNames = (branches: { name: string }[], tags: { name: string }[]) => ({ branches: branches.map((branch) => branch.name), tags: tags.map((tag) => tag.name) }); // the knot sends a readme with empty fields when a directory has none const readmeOf = (tree: { readme?: { filename: string; contents: string } } | null) => { const readme = tree?.readme; return readme?.filename ? readme : null; }; export interface RepoIndexOptions { requireRef?: boolean; importStatus?: boolean; } // `/tree/{ref}` is this same page at another ref, so they share a load export const loadRepoIndex = ( parent: RepoParent, ref?: string, // a ref from the url has to resolve or a typo looks like a repo with no // files. the default branch renders whatever the knot managed to answer { requireRef = false, importStatus: withImportStatus = false }: RepoIndexOptions = {} ) => { const { fetch } = getRequestEvent(); const repoP = Promise.resolve(parent.repo); // each list falls back on its own, so half a page still renders // null content is a repo without git, nothing on this page applies to it const loaded = repoP.then(async (repo) => { if (requireRef) requireGit(repo); if (!hasGit(repo)) return null; const targetRef = ref ?? repo.git.defaultBranch; const git = gitTarget(parent.publicConfig, repo, fetch); const results = await parallel({ tree: attempt(gitTree(git, { ref: targetRef })), log: attempt(gitLog(git, { ref: targetRef, limit: COMMIT_LIMIT })), branch: attempt(gitBranch(git, targetRef)), branches: attempt(gitBranches(git, REF_LIMIT)), tags: attempt(gitTags(git, REF_LIMIT)) }); const branches = results.branches.value ? branchSummaries(results.branches.value) : []; const tags = (results.tags.value?.tags ?? []).map(toTagSummary); const commits = await withAuthorHandles( (results.log.value?.commits ?? []).map(toMirrorCommitSummary), parent.publicConfig.bobbinUrl, fetch ); const files = sortTreeEntries((results.tree.value?.files ?? []).map(toTreeEntrySummary)); const contentAttempts = [results.tree, results.log, results.branches]; const mirror = classifyRepoAvailability(contentAttempts); const availability: RepoAvailability = mirror === "ok" && files.length === 0 && branches.length === 0 ? "empty" : mirror; // there are refs but not this one, so it is not a real ref. an empty repo has // no refs at all and still gets a page if (requireRef && results.tree.value === null && branches.length > 0) { error(404, `${targetRef} does not exist in this repository`); } const readme = readmeOf(results.tree.value); return { content: { ref: targetRef, availability, files, readme, commits, tagsByCommit: tagsByCommitHash(commits, tags), totalCommits: results.branch.value?.totalCommits ?? commits.length, branches: currentFirst(branches, targetRef).slice(0, BRANCH_LIMIT), totalBranches: results.branches.value?.total ?? branches.length, tags: tags.slice(0, TAG_LIMIT), totalTags: results.tags.value?.total ?? tags.length, // the switcher needs every ref, not just the visible slice refs: refNames(branches, tags) } }; }); const content = loaded.then((resolved) => resolved?.content ?? null); const languages = repoP.then(async (repo) => { if (!hasGit(repo)) return []; const targetRef = ref ?? repo.git.defaultBranch; if (targetRef === repo.git.defaultBranch && repo.git.languages) { return toLanguageSlices(repo.git.languages); } const git = gitTarget(parent.publicConfig, repo, fetch); const result = await attempt(gitLanguages(git, targetRef)); return toLanguageSlices(result.value?.languages ?? []); }); const importStatusP = withImportStatus ? repoP.then(async (repo): Promise => { if (!repo.importSource || !hasGit(repo)) return null; const state = await orNull(importState(repo, { fetch })); return state ? { ...state, source: repo.importSource } : null; }) : Promise.resolve(null); return { ref: ref ?? "", content: stream(content), languages: stream(languages), importStatus: stream(importStatusP) }; }; export const loadRepoTree = ( event: RepoLoadEvent & { params: { handle: string; repo: string } }, parent: RepoParent, ref: string, path: string ) => { const { fetch } = getRequestEvent(); const repoP = Promise.resolve(parent.repo); const loaded = repoP.then(async (repo) => { const git = gitTarget(parent.publicConfig, requireGit(repo), fetch); const result = await orNull(gitTree(git, { ref, path })); const files = sortTreeEntries((result?.files ?? []).map(toTreeEntrySummary)); // the mirror lists nothing for a file and git can't keep an empty dir, so empty means blob or nothing if (result === null || files.length === 0) { redirect(307, blobHref(event.params.handle, event.params.repo, ref, path)); } // the same author resolution the blob page and commit log give their commits, so // the panel shows the account's avatar and handle rather than the raw git name const [lastCommit = null] = result.lastCommit ? await withAuthorHandles( [toTreeCommitSummary(result.lastCommit)], parent.publicConfig.bobbinUrl, fetch ) : []; return { files, readme: readmeOf(result), lastCommit }; }); return { ref, path, tree: stream(loaded) }; };