import type { BobbinContext, XrpcRequestInit } from "./client"; import { createBobbinClient } from "./client"; import { buildUrl, jsonGet, rawGet } from "./_request"; import type { RepoTagResponse } from "./repo"; import type * as RepoBlob from "./lexicons/types/sh/tangled/repo/blob"; import type * as LegacyTree from "./lexicons/types/sh/tangled/git/temp/getTree"; import type * as Tree from "./lexicons/types/sh/tangled/repo/tree"; import { treeEntryKind, type BranchesResponse, type LogResponse, type TagsResponse } from "./repo"; const TREE_NSID = "sh.tangled.git.temp.getTree"; const ENTRY_NSID = "sh.tangled.git.temp.getEntry"; const BLOB_NSID = "sh.tangled.git.temp.getBlob"; const LOG_NSID = "sh.tangled.git.temp.listCommits"; const BRANCHES_NSID = "sh.tangled.git.temp.listBranches"; const TAGS_NSID = "sh.tangled.git.temp.listTags"; const LANGUAGES_NSID = "sh.tangled.git.temp.listLanguages"; const GET_TAG_NSID = "sh.tangled.git.temp.getTag"; const MERGE_BASE_NSID = "sh.tangled.git.temp.getMergeBase"; const GET_BRANCH_NSID = "sh.tangled.git.temp.getBranch"; const MAX_README_BYTES = 1 << 20; export interface KnotMirrorLanguagesResponse { languages?: { name: string; size: number }[]; total?: number; ref?: string; } export const createKnotMirrorClient = (serviceUrl: string, fetch: typeof globalThis.fetch) => createBobbinClient({ serviceUrl, fetch }); export interface KnotMirrorEntry { name: string; mode: string; oid?: string; size?: number; lastCommit?: { hash: string; message?: string; author?: { name: string; email: string; when: string }; committer?: { name: string; email: string; when: string }; }; } export const entry = ( ctx: BobbinContext, params: { repo: string; ref: string; path: string }, init?: XrpcRequestInit ) => jsonGet(ctx, ENTRY_NSID, params, init); export const blobRawUrl = ( serviceUrl: string, params: { repo: string; ref: string; path: string } ): string => buildUrl(serviceUrl, BLOB_NSID, params).toString(); // same heuristic as git: a null byte in the first chunk means binary const looksBinary = (bytes: Uint8Array): boolean => bytes.subarray(0, 8000).includes(0); // temp.getBlob answers raw bytes only, metadata comes from getEntry and // the binary flag from a content sniff export const blob = async ( ctx: BobbinContext, params: { repo: string; ref: string; path: string }, maxBytes: number, init?: XrpcRequestInit ): Promise => { const meta = await entry(ctx, params, init); const lastCommit = meta.lastCommit ? { hash: meta.lastCommit.hash, message: meta.lastCommit.message ?? "", when: meta.lastCommit.committer?.when ?? meta.lastCommit.author?.when ?? "", author: meta.lastCommit.author } : undefined; // a gitlink is a submodule pointer, not a file if (treeEntryKind(meta.mode) === "submodule") { return { path: params.path, ref: params.ref, size: meta.size, submodule: { name: meta.name, url: "" } }; } if (meta.size !== undefined && meta.size > maxBytes) { return { path: params.path, ref: params.ref, size: meta.size, fileTooLarge: true, lastCommit }; } const bytes = new Uint8Array(await (await rawGet(ctx, BLOB_NSID, params, init)).arrayBuffer()); if (looksBinary(bytes)) { return { path: params.path, ref: params.ref, size: meta.size ?? bytes.length, isBinary: true, lastCommit }; } return { path: params.path, ref: params.ref, size: meta.size ?? bytes.length, isBinary: false, encoding: "utf-8", content: new TextDecoder().decode(bytes), lastCommit }; }; const normalizeSignature = ( signature: LegacyTree.Signature | undefined ): Tree.Signature | undefined => signature ? { name: signature.name, email: signature.email, when: signature.when } : undefined; const normalizeCommit = (commit: LegacyTree.LastCommit | undefined): Tree.LastCommit | undefined => commit ? { hash: commit.hash, message: commit.message, when: commit.when, author: normalizeSignature(commit.author) } : undefined; const normalizeTree = (tree: LegacyTree.$output): Tree.$output => ({ ref: tree.ref, parent: tree.parent, dotdot: tree.dotdot, lastCommit: normalizeCommit(tree.lastCommit), files: tree.files.map((entry) => ({ name: entry.name, mode: entry.mode, size: entry.size, last_commit: normalizeCommit(entry.last_commit) })) }); const isReadmeFile = (name: string, mode: string): boolean => /^readme(?:\.[^.]+)?$/i.test(name) && treeEntryKind(mode) === "file"; const pathInTree = (path: string | undefined, name: string): string => path ? `${path}/${name}` : name; export const tree = async ( ctx: BobbinContext, params: { repo: string; ref: string; path?: string }, init?: XrpcRequestInit ): Promise => { const response = await jsonGet(ctx, TREE_NSID, params, init); const result = normalizeTree(response); const readme = response.files.find((entry) => isReadmeFile(entry.name, entry.mode)); if (!readme) return result; if (readme.size !== undefined && readme.size > MAX_README_BYTES) return result; try { const blob = await rawGet( ctx, BLOB_NSID, { repo: params.repo, ref: params.ref, path: pathInTree(params.path, readme.name) }, init ); return { ...result, readme: { filename: readme.name, contents: await blob.text() } }; } catch { return result; } }; export const log = ( ctx: BobbinContext, params: { repo: string; ref: string; limit: number; cursor?: string }, init?: XrpcRequestInit ) => jsonGet(ctx, LOG_NSID, params, init); export const branches = ( ctx: BobbinContext, params: { repo: string; limit: number; cursor?: string }, init?: XrpcRequestInit ) => jsonGet(ctx, BRANCHES_NSID, params, init); export const tags = ( ctx: BobbinContext, params: { repo: string; limit: number; cursor?: string }, init?: XrpcRequestInit ) => jsonGet(ctx, TAGS_NSID, params, init); export const getTag = ( ctx: BobbinContext, params: { repo: string; tag: string }, init?: XrpcRequestInit ) => jsonGet(ctx, GET_TAG_NSID, params, init); export const languages = ( ctx: BobbinContext, params: { repo: string; ref: string }, init?: XrpcRequestInit ) => jsonGet(ctx, LANGUAGES_NSID, params, init); export const getMergeBase = ( ctx: BobbinContext, params: { repo: string; base: string; head: string }, init?: XrpcRequestInit ) => jsonGet<{ commit: string }>(ctx, MERGE_BASE_NSID, params, init); export interface KnotMirrorBranch { name: string; hash: string; when: string; message?: string; author?: { name: string; email: string; when: string }; } export const getBranch = ( ctx: BobbinContext, params: { repo: string; name: string }, init?: XrpcRequestInit ) => jsonGet(ctx, GET_BRANCH_NSID, params, init);