Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170import { type BobbinContext, ClientResponseError, createBobbinClient } from "#lib/api/client.ts";import { resolveEmailToDid } from "#lib/api/emailDid.ts";import { resolveMiniDoc } from "#lib/api/identity.ts";import { branches as gitBranches, type GitTarget } from "#lib/api/gitclient.ts";import { orNull } from "#lib/api/load.ts";import { branchSummaries, didFromSignature, type ImportState } from "#lib/api/repo.ts";import { listPubKeys, sshFingerprint } from "#lib/api/settings.ts";import type { CommitSummary, LanguageSlice } from "#lib/components/repo/types.ts";import type { Fetch } from "#lib/fetch.ts";
export const COMMIT_LIMIT = 3;export const BRANCH_LIMIT = 3;export const TAG_LIMIT = 5;// a knot only ever lists 100 refs, so any total we get is really a minimumexport const REF_LIMIT = 100;
export interface Attempt<T> { value: T | null; error: unknown | null;}
export const attempt = <T>(promise: Promise<T>): Promise<Attempt<T>> => promise.then( (value) => ({ value, error: null }), (error) => ({ value: null, error }) );
const isNotSynced = (cause: unknown): boolean => cause instanceof ClientResponseError && cause.error === "RepoNotFound";
export type RepoAvailability = "ok" | "empty" | "not-synced" | "error" | "importing" | "import-failed";
export const classifyRepoAvailability = ( attempts: readonly { value: unknown | null; error: unknown | null }[]): Exclude<RepoAvailability, "empty" | "importing" | "import-failed"> => { if (attempts.every((result) => result.value === null && isNotSynced(result.error))) { return "not-synced"; } return attempts.every((result) => result.value === null) ? "error" : "ok";};
export const withImportAvailability = ( availability: RepoAvailability, status: ImportState | null): RepoAvailability => { if (availability === "ok" || !status) { return availability; } return status.content === "failed" ? "import-failed" : "importing";};
export 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, size: language.size, 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);};
export const repoBranches = async (git: GitTarget) => branchSummaries(await gitBranches(git, REF_LIMIT));
// for the handle and the row reads like the rest of the site. a plain git// email that belongs to a verified signup resolves to that same did first, so// those rows get the identical treatment.export const withAuthorHandles = async ( commits: CommitSummary[], bobbinUrl: string, fetch: Fetch): Promise<CommitSummary[]> => { const emails = [ ...new Set( commits.flatMap((commit) => [ ...(commit.authorDid ? [] : [commit.authorEmail]), ...(commit.committerEmail && !didFromSignature(commit.committerEmail) ? [commit.committerEmail] : []) ]) ) ]; let emailDids = new Map<string, string>(); if (emails.length > 0) { // the lookups are best-effort: a kv/route failure keeps plain emails emailDids = (await orNull(resolveEmailToDid(fetch, emails))) ?? new Map(); } const hydrated = commits.map((commit) => { const authorDid = commit.authorDid ?? emailDids.get(commit.authorEmail); const committerDid = commit.committerEmail && (didFromSignature(commit.committerEmail) ?? emailDids.get(commit.committerEmail)); return { ...commit, ...(authorDid && { authorDid }), ...(committerDid && { committerDid }) }; });
const ctx = createBobbinClient({ serviceUrl: bobbinUrl, fetch }); const dids = [ ...new Set( hydrated .flatMap((commit) => [commit.authorDid, commit.committerDid]) .filter((did) => did !== undefined) ) ]; const [docs, verified] = await Promise.all([ Promise.all(dids.map((did) => orNull(resolveMiniDoc(ctx, did)))), withVerifiedSignatures(hydrated, ctx) ]); const handles = new Map<string, string>(); for (const doc of docs) { if (doc && !doc.handle.endsWith(".invalid")) handles.set(doc.did, doc.handle); }
return verified.map((commit) => ({ ...commit, ...(commit.authorDid && { authorHandle: handles.get(commit.authorDid) }), ...(commit.committerDid && { committerHandle: handles.get(commit.committerDid) }) }));};
// knotmirror only says the signature is intact. it counts as the committer's once the// key is one of the signing keys on the committer's accountconst withVerifiedSignatures = async ( commits: CommitSummary[], ctx: BobbinContext): Promise<CommitSummary[]> => { const dids = [ ...new Set( commits.flatMap((commit) => commit.signatureFingerprint && commit.committerDid ? [commit.committerDid] : [] ) ) ]; const keys = new Map( await Promise.all(dids.map(async (did) => [did, await signingKeys(ctx, did)] as const)) ); return commits.map((commit) => { if (!commit.signatureFingerprint) return commit; const verified = commit.committerDid !== undefined && !!keys.get(commit.committerDid)?.has(commit.signatureFingerprint); return { ...commit, signature: verified ? "verified" : "unverified" }; });};
const signingKeys = async (ctx: BobbinContext, did: string): Promise<Set<string>> => { const records = (await orNull(listPubKeys(ctx, did))) ?? []; const fingerprints = await Promise.all( records .filter(({ value }) => !value.usages || value.usages.includes("sign")) .map(({ value }) => sshFingerprint(value.publicKey)) ); return new Set(fingerprints.filter((fingerprint) => fingerprint !== null));};