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 / knotmirror.ts
6.2 kB 198 lines
1import type { BobbinContext, XrpcRequestInit } from "./client"; 2import { createBobbinClient } from "./client"; 3import { buildUrl, jsonGet, rawGet } from "./_request"; 4import type { RepoTagResponse } from "./repo"; 5import type * as RepoBlob from "./lexicons/types/sh/tangled/repo/blob"; 6import type * as LegacyTree from "./lexicons/types/sh/tangled/git/temp/getTree"; 7import type * as Tree from "./lexicons/types/sh/tangled/repo/tree"; 8import { treeEntryKind, type BranchesResponse, type LogResponse, type TagsResponse } from "./repo"; 9 10const TREE_NSID = "sh.tangled.git.temp.getTree"; 11const ENTRY_NSID = "sh.tangled.git.temp.getEntry"; 12const BLOB_NSID = "sh.tangled.git.temp.getBlob"; 13const LOG_NSID = "sh.tangled.git.temp.listCommits"; 14const BRANCHES_NSID = "sh.tangled.git.temp.listBranches"; 15const TAGS_NSID = "sh.tangled.git.temp.listTags"; 16const LANGUAGES_NSID = "sh.tangled.git.temp.listLanguages"; 17const GET_TAG_NSID = "sh.tangled.git.temp.getTag"; 18 19const MAX_README_BYTES = 1 << 20; 20 21export interface KnotMirrorLanguagesResponse { 22 languages?: { name: string; size: number }[]; 23 total?: number; 24 ref?: string; 25} 26 27export const createKnotMirrorClient = (serviceUrl: string, fetch: typeof globalThis.fetch) => 28 createBobbinClient({ serviceUrl, fetch }); 29 30export interface KnotMirrorEntry { 31 name: string; 32 mode: string; 33 oid?: string; 34 size?: number; 35 lastCommit?: { 36 hash: string; 37 message?: string; 38 author?: { name: string; email: string; when: string }; 39 committer?: { name: string; email: string; when: string }; 40 }; 41} 42 43export const entry = ( 44 ctx: BobbinContext, 45 params: { repo: string; ref: string; path: string }, 46 init?: XrpcRequestInit 47) => jsonGet<KnotMirrorEntry>(ctx, ENTRY_NSID, params, init); 48 49export const blobRawUrl = ( 50 serviceUrl: string, 51 params: { repo: string; ref: string; path: string } 52): string => buildUrl(serviceUrl, BLOB_NSID, params).toString(); 53 54// same heuristic as git: a null byte in the first chunk means binary 55const looksBinary = (bytes: Uint8Array): boolean => bytes.subarray(0, 8000).includes(0); 56 57// temp.getBlob answers raw bytes only, metadata comes from getEntry and 58// the binary flag from a content sniff 59export const blob = async ( 60 ctx: BobbinContext, 61 params: { repo: string; ref: string; path: string }, 62 maxBytes: number, 63 init?: XrpcRequestInit 64): Promise<RepoBlob.$output> => { 65 const meta = await entry(ctx, params, init); 66 67 const lastCommit = meta.lastCommit 68 ? { 69 hash: meta.lastCommit.hash, 70 message: meta.lastCommit.message ?? "", 71 when: meta.lastCommit.committer?.when ?? meta.lastCommit.author?.when ?? "", 72 author: meta.lastCommit.author 73 } 74 : undefined; 75 76 // a gitlink is a submodule pointer, not a file 77 if (treeEntryKind(meta.mode) === "submodule") { 78 return { 79 path: params.path, 80 ref: params.ref, 81 size: meta.size, 82 submodule: { name: meta.name, url: "" } 83 }; 84 } 85 86 if (meta.size !== undefined && meta.size > maxBytes) { 87 return { path: params.path, ref: params.ref, size: meta.size, fileTooLarge: true, lastCommit }; 88 } 89 90 const bytes = new Uint8Array(await (await rawGet(ctx, BLOB_NSID, params, init)).arrayBuffer()); 91 if (looksBinary(bytes)) { 92 return { 93 path: params.path, 94 ref: params.ref, 95 size: meta.size ?? bytes.length, 96 isBinary: true, 97 lastCommit 98 }; 99 } 100 101 return { 102 path: params.path, 103 ref: params.ref, 104 size: meta.size ?? bytes.length, 105 isBinary: false, 106 encoding: "utf-8", 107 content: new TextDecoder().decode(bytes), 108 lastCommit 109 }; 110}; 111 112const normalizeSignature = ( 113 signature: LegacyTree.Signature | undefined 114): Tree.Signature | undefined => 115 signature ? { name: signature.name, email: signature.email, when: signature.when } : undefined; 116 117const normalizeCommit = (commit: LegacyTree.LastCommit | undefined): Tree.LastCommit | undefined => 118 commit 119 ? { 120 hash: commit.hash, 121 message: commit.message, 122 when: commit.when, 123 author: normalizeSignature(commit.author) 124 } 125 : undefined; 126 127const normalizeTree = (tree: LegacyTree.$output): Tree.$output => ({ 128 ref: tree.ref, 129 parent: tree.parent, 130 dotdot: tree.dotdot, 131 lastCommit: normalizeCommit(tree.lastCommit), 132 files: tree.files.map((entry) => ({ 133 name: entry.name, 134 mode: entry.mode, 135 size: entry.size, 136 last_commit: normalizeCommit(entry.last_commit) 137 })) 138}); 139 140const isReadmeFile = (name: string, mode: string): boolean => 141 /^readme(?:\.[^.]+)?$/i.test(name) && treeEntryKind(mode) === "file"; 142 143const pathInTree = (path: string | undefined, name: string): string => 144 path ? `${path}/${name}` : name; 145 146export const tree = async ( 147 ctx: BobbinContext, 148 params: { repo: string; ref: string; path?: string }, 149 init?: XrpcRequestInit 150): Promise<Tree.$output> => { 151 const response = await jsonGet<LegacyTree.$output>(ctx, TREE_NSID, params, init); 152 const result = normalizeTree(response); 153 const readme = response.files.find((entry) => isReadmeFile(entry.name, entry.mode)); 154 if (!readme) return result; 155 if (readme.size !== undefined && readme.size > MAX_README_BYTES) return result; 156 157 try { 158 const blob = await rawGet( 159 ctx, 160 BLOB_NSID, 161 { repo: params.repo, ref: params.ref, path: pathInTree(params.path, readme.name) }, 162 init 163 ); 164 return { ...result, readme: { filename: readme.name, contents: await blob.text() } }; 165 } catch { 166 return result; 167 } 168}; 169 170export const log = ( 171 ctx: BobbinContext, 172 params: { repo: string; ref: string; limit: number; cursor?: string }, 173 init?: XrpcRequestInit 174) => jsonGet<LogResponse>(ctx, LOG_NSID, params, init); 175 176export const branches = ( 177 ctx: BobbinContext, 178 params: { repo: string; limit: number; cursor?: string }, 179 init?: XrpcRequestInit 180) => jsonGet<BranchesResponse>(ctx, BRANCHES_NSID, params, init); 181 182export const tags = ( 183 ctx: BobbinContext, 184 params: { repo: string; limit: number; cursor?: string }, 185 init?: XrpcRequestInit 186) => jsonGet<TagsResponse>(ctx, TAGS_NSID, params, init); 187 188export const getTag = ( 189 ctx: BobbinContext, 190 params: { repo: string; tag: string }, 191 init?: XrpcRequestInit 192) => jsonGet<RepoTagResponse>(ctx, GET_TAG_NSID, params, init); 193 194export const languages = ( 195 ctx: BobbinContext, 196 params: { repo: string; ref: string }, 197 init?: XrpcRequestInit 198) => jsonGet<KnotMirrorLanguagesResponse>(ctx, LANGUAGES_NSID, params, init);