This repository has no description
1import { ClientResponseError, type BobbinContext, type XrpcRequestInit } from "./client";
2import { getRepoByName, type RecordView, type RepoRecord } from "./records";
3import { branches as knotBranches, log as knotLog, tags as knotTags } from "./knot";
4import { httpStatusFor } from "./load";
5import { rkeyFromUri } from "./uri";
6import type * as Tree from "./lexicons/types/sh/tangled/repo/tree";
7
8// log, branches and tags are `*/*` in the lexicons, so these shapes are copied
9// from core/types by hand. anything go-git touches keeps its go field names and
10// writes hashes as byte arrays, so the hex comes from a sibling field
11
12export interface GitSignature {
13 Name: string;
14 Email: string;
15 When: string;
16}
17
18export interface GitCommit {
19 Author?: GitSignature;
20 Committer?: GitSignature;
21 Message?: string;
22}
23
24// `this` and `parent` are the hex hashes, `hash` is the byte array
25export interface LogCommit {
26 this?: string;
27 parent?: string;
28 author?: GitSignature;
29 committer?: GitSignature;
30 message?: string;
31 tree?: string;
32 change_id?: string;
33}
34
35export interface LogResponse {
36 commits?: LogCommit[];
37 ref?: string;
38 total?: number;
39 page?: number;
40}
41
42export interface GitReference {
43 name: string;
44 hash: string;
45}
46
47export interface BranchEntry {
48 reference: GitReference;
49 commit?: GitCommit;
50 is_default?: boolean;
51}
52
53export interface BranchesResponse {
54 branches?: BranchEntry[];
55 total?: number;
56}
57
58export interface TagEntry {
59 name: string;
60 hash: string;
61 message?: string;
62 tag?: {
63 Tagger?: GitSignature;
64 Message?: string;
65 /** the commit an annotated tag points at, bytes like every go-git hash */
66 Target?: number[];
67 };
68}
69
70export interface TagsResponse {
71 tags?: TagEntry[];
72 total?: number;
73}
74
75export const repoNameOf = (view: RecordView<RepoRecord>): string => rkeyFromUri(view.uri);
76
77// a repo bobbin has never indexed is a miss, not an error
78export const resolveRepoByName = async (
79 ctx: BobbinContext,
80 ownerDid: string,
81 name: string,
82 init?: XrpcRequestInit
83): Promise<RecordView<RepoRecord> | null> => {
84 try {
85 return await getRepoByName(ctx, ownerDid, name, init);
86 } catch (cause) {
87 if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null;
88 throw cause;
89 }
90};
91
92export interface CommitSummary {
93 hash: string;
94 shortHash: string;
95 subject: string;
96 body: string;
97 authorName: string;
98 authorEmail: string;
99 when: string;
100 changeId?: string;
101}
102
103const splitMessage = (message: string): [string, string] => {
104 const separator = message.indexOf("\n\n");
105 if (separator === -1) return [message.trim(), ""];
106 return [message.slice(0, separator).trim(), message.slice(separator + 2).trim()];
107};
108
109export const toCommitSummary = (commit: LogCommit): CommitSummary => {
110 const [subject, body] = splitMessage(commit.message ?? "");
111 const hash = commit.this ?? "";
112 return {
113 hash,
114 shortHash: hash.slice(0, 8),
115 subject,
116 body,
117 authorName: commit.author?.Name ?? "",
118 authorEmail: commit.author?.Email ?? "",
119 when: commit.committer?.When ?? commit.author?.When ?? "",
120 changeId: commit.change_id
121 };
122};
123
124export interface BranchSummary {
125 name: string;
126 hash: string;
127 when?: string;
128 isDefault: boolean;
129}
130
131export const toBranchSummary = (branch: BranchEntry): BranchSummary => ({
132 name: branch.reference.name,
133 hash: branch.reference.hash,
134 when: branch.commit?.Committer?.When ?? branch.commit?.Author?.When,
135 isDefault: branch.is_default === true
136});
137
138export interface TagSummary {
139 name: string;
140 hash: string;
141 /** an annotated tag has its own hash, this is the commit it points at */
142 commitHash: string;
143 when?: string;
144 message?: string;
145}
146
147const hexFromBytes = (bytes: number[]): string =>
148 bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("");
149
150export const toTagSummary = (tag: TagEntry): TagSummary => {
151 const target = tag.tag?.Target;
152 return {
153 name: tag.name,
154 hash: tag.hash,
155 commitHash: target?.length ? hexFromBytes(target) : tag.hash,
156 when: tag.tag?.Tagger?.When,
157 message: tag.message ?? tag.tag?.Message
158 };
159};
160
161export type TreeEntryKind = "file" | "directory" | "symlink" | "submodule";
162
163// modes come back octal and zero padded
164export const treeEntryKind = (mode: string): TreeEntryKind => {
165 switch (mode.replace(/^0+/, "").padStart(6, "0")) {
166 case "040000":
167 return "directory";
168 case "120000":
169 return "symlink";
170 case "160000":
171 return "submodule";
172 default:
173 return "file";
174 }
175};
176
177export interface TreeEntrySummary {
178 name: string;
179 kind: TreeEntryKind;
180 size: number;
181 lastCommitHash?: string;
182 lastCommitWhen?: string;
183 lastCommitMessage?: string;
184}
185
186export const toTreeEntrySummary = (entry: Tree.TreeEntry): TreeEntrySummary => ({
187 name: entry.name,
188 kind: treeEntryKind(entry.mode),
189 size: entry.size,
190 lastCommitHash: entry.last_commit?.hash,
191 lastCommitWhen: entry.last_commit?.when,
192 lastCommitMessage: entry.last_commit?.message
193});
194
195export const tagsByCommitHash = (
196 commits: CommitSummary[],
197 tags: TagSummary[]
198): Record<string, string[]> => {
199 const shown = new Set(commits.map((commit) => commit.hash));
200 return tags.reduce<Record<string, string[]>>((acc, tag) => {
201 if (shown.has(tag.commitHash)) (acc[tag.commitHash] ??= []).push(tag.name);
202 return acc;
203 }, {});
204};
205
206export const sortTreeEntries = (entries: TreeEntrySummary[]): TreeEntrySummary[] =>
207 [...entries].sort((a, b) => {
208 const aDir = a.kind === "directory" || a.kind === "submodule";
209 const bDir = b.kind === "directory" || b.kind === "submodule";
210 if (aDir !== bDir) return aDir ? -1 : 1;
211 return a.name.localeCompare(b.name);
212 });
213
214export const logFor = (
215 ctx: BobbinContext,
216 repo: string,
217 ref: string,
218 limit: number,
219 init?: XrpcRequestInit
220) => knotLog<LogResponse>(ctx, { repo, ref, limit }, init);
221
222export const branchesFor = (
223 ctx: BobbinContext,
224 repo: string,
225 limit: number,
226 init?: XrpcRequestInit
227) => knotBranches<BranchesResponse>(ctx, { repo, limit }, init);
228
229export const tagsFor = (ctx: BobbinContext, repo: string, limit: number, init?: XrpcRequestInit) =>
230 knotTags<TagsResponse>(ctx, { repo, limit }, init);