Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250import { error } from "@sveltejs/kit";import { ClientResponseError, createBobbinClient } from "$lib/api/client";import { languages as knotLanguages, tree as knotTree } from "$lib/api/knot";import { branches as knotMirrorBranches, createKnotMirrorClient, languages as knotMirrorLanguages, log as knotMirrorLog, tags as knotMirrorTags, tree as knotMirrorTree} from "$lib/api/knotmirror";import { parallel } from "$lib/api/load";import { branchesFor, logFor, sortTreeEntries, tagsByCommitHash, tagsFor, toBranchSummary, toCommitSummary, toTagSummary, toTreeCommitSummary, toTreeEntrySummary} from "$lib/api/repo";import { renderDocument } from "$lib/markup";import type { LanguageSlice, RepoInfo } from "$lib/components/repo/types";import type * as Tree from "./lexicons/types/sh/tangled/repo/tree";
// `/tree/{ref}` is this same page at another ref, so they share a load
const COMMIT_LIMIT = 10;const BRANCH_LIMIT = 5;const TAG_LIMIT = 5;// a knot only ever lists 100 refs, so any total we get is really a minimumconst REF_LIMIT = 100;
export interface RepoParent { publicConfig: { bobbinUrl: string; knotMirrorUrl: string; camoEnabled: boolean }; repo: RepoInfo;}
export interface RepoLoadEvent { fetch: typeof globalThis.fetch; url: URL;}
interface RepoDataSource { tree: (ref: string, path?: string) => Promise<Tree.$output>; log: (ref: string, limit: number) => Promise<Awaited<ReturnType<typeof logFor>>>; branches: (limit: number) => Promise<Awaited<ReturnType<typeof branchesFor>>>; tags: (limit: number) => Promise<Awaited<ReturnType<typeof tagsFor>>>; languages: (ref: string) => Promise<{ languages?: { name: string; size: number }[] }>;}
const orNull = <T>(promise: Promise<T>): Promise<T | null> => promise.catch(() => null);
interface Attempt<T> { value: T | null; error: unknown | null;}
const attempt = <T>(promise: Promise<T>): Promise<Attempt<T>> => promise.then( (value) => ({ value, error: null }), (error) => ({ value: null, error }) );
const isUnsupported = (cause: unknown): boolean => cause instanceof ClientResponseError && cause.status === 404;
export const classifyRepoAvailability = ( attempts: readonly { value: unknown | null; error: unknown | null }[]) => { const needsUpgrade = attempts.every( (result) => result.value === null && isUnsupported(result.error) ); return { needsUpgrade, knotUnreachable: !needsUpgrade && attempts.every((result) => result.value === null) };};
const toLanguageSlices = (languages: { name: string; size: number }[]): LanguageSlice[] => { const sized = languages.filter((language) => language.size > 0); const total = sized.reduce((sum, language) => sum + language.size, 0); if (total === 0) return [];
const slices = sized.map((language) => { const share = (language.size / total) * 100; return { name: language.name, share, percentage: Math.floor(share) }; });
const short = 100 - slices.reduce((sum, slice) => sum + slice.percentage, 0); [...slices] .sort((a, b) => (b.share % 1) - (a.share % 1) || b.share - a.share) .slice(0, Math.max(0, short)) .forEach((slice) => { slice.percentage += 1; });
return slices.sort((a, b) => b.share - a.share);};
const refNames = (branches: { name: string }[], tags: { name: string }[]) => ({ branches: branches.map((branch) => branch.name), tags: tags.map((tag) => tag.name), capped: branches.length >= REF_LIMIT || tags.length >= REF_LIMIT});
const renderReadme = ( readme: { filename: string; contents: string } | null, parent: RepoParent, event: RepoLoadEvent, ref: string, dir?: string) => readme ? renderDocument(readme.filename, readme.contents, { repo: `${parent.repo.ownerHandle}/${parent.repo.name}`, ref, dir, host: event.url.host, camo: parent.publicConfig.camoEnabled }) : Promise.resolve(null);
// the knot sends a readme with empty fields when a directory has noneconst readmeOf = (tree: { readme?: { filename: string; contents: string } } | null) => { const readme = tree?.readme; return readme?.filename ? readme : null;};
const repoDataSource = (event: RepoLoadEvent, parent: RepoParent): RepoDataSource => { if (parent.publicConfig.knotMirrorUrl && parent.repo.repoDid) { const ctx = createKnotMirrorClient(parent.publicConfig.knotMirrorUrl, event.fetch); const repo = parent.repo.repoDid; return { tree: (ref, path) => knotMirrorTree(ctx, { repo, ref, path }), log: (ref, limit) => knotMirrorLog(ctx, { repo, ref, limit }), branches: (limit) => knotMirrorBranches(ctx, { repo, limit }), tags: (limit) => knotMirrorTags(ctx, { repo, limit }), languages: (ref) => knotMirrorLanguages(ctx, { repo, ref }) }; }
const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch }); const repo = parent.repo.uri; return { tree: (ref, path) => knotTree(ctx, { repo, ref, path }), log: (ref, limit) => logFor(ctx, repo, ref, limit), branches: (limit) => branchesFor(ctx, repo, limit), tags: (limit) => tagsFor(ctx, repo, limit), languages: (ref) => knotLanguages(ctx, { repo, ref }) };};
export interface RepoIndexOptions { requireRef?: boolean;}
export const loadRepoIndex = async ( event: RepoLoadEvent, 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 }: RepoIndexOptions = {}) => { const source = repoDataSource(event, parent);
// each list falls back on its own, so half a page still renders const results = await parallel({ tree: attempt(source.tree(ref)), log: attempt(source.log(ref, COMMIT_LIMIT)), branches: attempt(source.branches(REF_LIMIT)), tags: attempt(source.tags(REF_LIMIT)), languages: attempt(source.languages(ref)) });
const branches = (results.branches.value?.branches ?? []).map(toBranchSummary); const tags = (results.tags.value?.tags ?? []).map(toTagSummary); const commits = (results.log.value?.commits ?? []).map(toCommitSummary); const files = sortTreeEntries((results.tree.value?.files ?? []).map(toTreeEntrySummary));
const languages = toLanguageSlices(results.languages.value?.languages ?? []);
const readme = readmeOf(results.tree.value); const readmeHtml = await renderReadme(readme, parent, event, ref);
const contentAttempts = [results.tree, results.log, results.branches]; const { needsUpgrade, knotUnreachable } = classifyRepoAvailability(contentAttempts); const isEmpty = !knotUnreachable && files.length === 0 && branches.length === 0;
// 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, `${ref} does not exist in this repository`); }
return { ref, isEmpty, needsUpgrade, knotUnreachable, files, readme, readmeHtml, commits, tagsByCommit: tagsByCommitHash(commits, tags), totalCommits: results.log.value?.total ?? commits.length, branches: branches.slice(0, BRANCH_LIMIT), totalBranches: branches.length, tags: tags.slice(0, TAG_LIMIT), totalTags: tags.length, // the switcher needs every ref, not just the visible slice refs: refNames(branches, tags), languages };};
export const loadRepoTree = async ( event: RepoLoadEvent, parent: RepoParent, ref: string, path: string) => { const source = repoDataSource(event, parent);
// the tree is the whole page here, so a miss is just a 404 const tree = await orNull(source.tree(ref, path)); const files = sortTreeEntries((tree?.files ?? []).map(toTreeEntrySummary)); // git cannot store an empty directory. so nothing here means the path is a // file, or was never there if (tree === null || files.length === 0) { error(404, `${path} does not exist at ${ref}`); }
const readme = readmeOf(tree); const readmeHtml = await renderReadme(readme, parent, event, ref, path);
return { ref, path, files, readme, readmeHtml, lastCommit: tree.lastCommit ? toTreeCommitSummary(tree.lastCommit) : null };};