This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / api / repoIndex.ts
5.7 kB 179 lines
1import { error } from "@sveltejs/kit"; 2import { createBobbinClient } from "$lib/api/client"; 3import { languages as knotLanguages, tree as knotTree } from "$lib/api/knot"; 4import { parallel } from "$lib/api/load"; 5import { 6 branchesFor, 7 logFor, 8 sortTreeEntries, 9 tagsByCommitHash, 10 tagsFor, 11 toBranchSummary, 12 toCommitSummary, 13 toTagSummary, 14 toTreeCommitSummary, 15 toTreeEntrySummary 16} from "$lib/api/repo"; 17import { renderDocument } from "$lib/markup"; 18import type { LanguageSlice, RepoInfo } from "$lib/components/repo/types"; 19 20// `/tree/{ref}` is this same page at another ref, so they share a load 21 22const COMMIT_LIMIT = 10; 23const BRANCH_LIMIT = 5; 24const TAG_LIMIT = 5; 25// a knot only ever lists 100 refs, so any total we get is really a minimum 26const REF_LIMIT = 100; 27 28export interface RepoParent { 29 publicConfig: { bobbinUrl: string; camoEnabled: boolean }; 30 repo: RepoInfo; 31} 32 33export interface RepoLoadEvent { 34 fetch: typeof globalThis.fetch; 35 url: URL; 36} 37 38const orNull = <T>(promise: Promise<T>): Promise<T | null> => promise.catch(() => null); 39 40const toLanguageSlices = (languages: { name: string; size: number }[]): LanguageSlice[] => { 41 const sized = languages.filter((language) => language.size > 0); 42 const total = sized.reduce((sum, language) => sum + language.size, 0); 43 if (total === 0) return []; 44 45 const slices = sized.map((language) => { 46 const share = (language.size / total) * 100; 47 return { name: language.name, share, percentage: Math.floor(share) }; 48 }); 49 50 const short = 100 - slices.reduce((sum, slice) => sum + slice.percentage, 0); 51 [...slices] 52 .sort((a, b) => (b.share % 1) - (a.share % 1) || b.share - a.share) 53 .slice(0, Math.max(0, short)) 54 .forEach((slice) => { 55 slice.percentage += 1; 56 }); 57 58 return slices.sort((a, b) => b.share - a.share); 59}; 60 61const refNames = (branches: { name: string }[], tags: { name: string }[]) => ({ 62 branches: branches.map((branch) => branch.name), 63 tags: tags.map((tag) => tag.name), 64 capped: branches.length >= REF_LIMIT || tags.length >= REF_LIMIT 65}); 66 67const renderReadme = ( 68 readme: { filename: string; contents: string } | null, 69 parent: RepoParent, 70 event: RepoLoadEvent, 71 ref: string, 72 dir?: string 73) => 74 readme 75 ? renderDocument(readme.filename, readme.contents, { 76 repo: `${parent.repo.ownerHandle}/${parent.repo.name}`, 77 ref, 78 dir, 79 host: event.url.host, 80 camo: parent.publicConfig.camoEnabled 81 }) 82 : Promise.resolve(null); 83 84// the knot sends a readme with empty fields when a directory has none 85const readmeOf = (tree: { readme?: { filename: string; contents: string } } | null) => { 86 const readme = tree?.readme; 87 return readme?.filename ? readme : null; 88}; 89 90export const loadRepoIndex = async ( 91 event: RepoLoadEvent, 92 parent: RepoParent, 93 ref: string, 94 // a ref from the url has to resolve or a typo looks like a repo with no 95 // files. the default branch renders whatever the knot managed to answer 96 { requireRef = false }: { requireRef?: boolean } = {} 97) => { 98 const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch }); 99 const repo = parent.repo.uri; 100 101 // each list falls back on its own, so half a page still renders 102 const results = await parallel({ 103 tree: orNull(knotTree(ctx, { repo, ref })), 104 log: orNull(logFor(ctx, repo, ref, COMMIT_LIMIT)), 105 branches: orNull(branchesFor(ctx, repo, REF_LIMIT)), 106 tags: orNull(tagsFor(ctx, repo, REF_LIMIT)), 107 languages: orNull(knotLanguages(ctx, { repo, ref })) 108 }); 109 110 const branches = (results.branches?.branches ?? []).map(toBranchSummary); 111 const tags = (results.tags?.tags ?? []).map(toTagSummary); 112 const commits = (results.log?.commits ?? []).map(toCommitSummary); 113 const files = sortTreeEntries((results.tree?.files ?? []).map(toTreeEntrySummary)); 114 115 const languages = toLanguageSlices(results.languages?.languages ?? []); 116 117 const readme = readmeOf(results.tree); 118 const readmeHtml = await renderReadme(readme, parent, event, ref); 119 120 const knotUnreachable = 121 results.tree === null && results.log === null && results.branches === null; 122 const isEmpty = !knotUnreachable && files.length === 0 && branches.length === 0; 123 124 // there are refs but not this one, so it is not a real ref. an empty repo has 125 // no refs at all and still gets a page 126 if (requireRef && results.tree === null && branches.length > 0) { 127 error(404, `${ref} does not exist in this repository`); 128 } 129 130 return { 131 ref, 132 isEmpty, 133 knotUnreachable, 134 files, 135 readme, 136 readmeHtml, 137 commits, 138 tagsByCommit: tagsByCommitHash(commits, tags), 139 totalCommits: results.log?.total ?? commits.length, 140 branches: branches.slice(0, BRANCH_LIMIT), 141 totalBranches: branches.length, 142 tags: tags.slice(0, TAG_LIMIT), 143 totalTags: tags.length, 144 // the switcher needs every ref, not just the visible slice 145 refs: refNames(branches, tags), 146 languages 147 }; 148}; 149 150export const loadRepoTree = async ( 151 event: RepoLoadEvent, 152 parent: RepoParent, 153 ref: string, 154 path: string 155) => { 156 const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch }); 157 const repo = parent.repo.uri; 158 159 // the tree is the whole page here, so a miss is just a 404 160 const tree = await orNull(knotTree(ctx, { repo, ref, path })); 161 const files = sortTreeEntries((tree?.files ?? []).map(toTreeEntrySummary)); 162 // git cannot store an empty directory. so nothing here means the path is a 163 // file, or was never there 164 if (tree === null || files.length === 0) { 165 error(404, `${path} does not exist at ${ref}`); 166 } 167 168 const readme = readmeOf(tree); 169 const readmeHtml = await renderReadme(readme, parent, event, ref, path); 170 171 return { 172 ref, 173 path, 174 files, 175 readme, 176 readmeHtml, 177 lastCommit: tree.lastCommit ? toTreeCommitSummary(tree.lastCommit) : null 178 }; 179};