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