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<RepoInfo>;
}
export interface RepoLoadEvent {
url: URL;
}
const currentFirst = <T extends { name: string }>(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<ImportState | null> => {
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)
};
};