This repository has no description
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};