Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251import { ClientResponseError, type BobbinContext, type XrpcRequestInit } from "./client";import { getRepoByName, type RecordView, type RepoRecord } from "./records";import { branches as knotBranches, log as knotLog, tags as knotTags } from "./knot";import { httpStatusFor } from "./load";import { rkeyFromUri } from "./uri";import type * as Tree from "./lexicons/types/sh/tangled/repo/tree";
// log, branches and tags are `*/*` in the lexicons, so these shapes are copied// from core/types by hand. anything go-git touches keeps its go field names and// writes hashes as byte arrays, so the hex comes from a sibling field
export interface GitSignature { Name: string; Email: string; When: string;}
export interface GitCommit { Author?: GitSignature; Committer?: GitSignature; Message?: string;}
// `this` and `parent` are the hex hashes, `hash` is the byte arrayexport interface LogCommit { this?: string; parent?: string; author?: GitSignature; committer?: GitSignature; message?: string; tree?: string; change_id?: string;}
export interface LogResponse { commits?: LogCommit[]; ref?: string; total?: number; page?: number;}
export interface GitReference { name: string; hash: string;}
export interface BranchEntry { reference: GitReference; commit?: GitCommit; is_default?: boolean;}
export interface BranchesResponse { branches?: BranchEntry[]; total?: number;}
export interface TagEntry { name: string; hash: string; message?: string; tag?: { Tagger?: GitSignature; Message?: string; /** the commit an annotated tag points at, bytes like every go-git hash */ Target?: number[]; };}
export interface TagsResponse { tags?: TagEntry[]; total?: number;}
// newer repos get tid rkeys and keep their display name in the recordexport const repoNameOf = (view: RecordView<RepoRecord>): string => view.value.name ?? rkeyFromUri(view.uri);
// a repo bobbin has never indexed is a miss, not an errorexport const resolveRepoByName = async ( ctx: BobbinContext, ownerDid: string, name: string, init?: XrpcRequestInit): Promise<RecordView<RepoRecord> | null> => { try { return await getRepoByName(ctx, ownerDid, name, init); } catch (cause) { if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null; throw cause; }};
export interface CommitSummary { hash: string; shortHash: string; subject: string; body: string; authorName: string; authorEmail: string; when: string; changeId?: string;}
const splitMessage = (message: string): [string, string] => { const separator = message.indexOf("\n\n"); if (separator === -1) return [message.trim(), ""]; return [message.slice(0, separator).trim(), message.slice(separator + 2).trim()];};
/** the subject is the first paragraph, not the first line */export const subjectOf = (message: string): string => splitMessage(message)[0];
export const toCommitSummary = (commit: LogCommit): CommitSummary => { const [subject, body] = splitMessage(commit.message ?? ""); const hash = commit.this ?? ""; return { hash, shortHash: hash.slice(0, 8), subject, body, authorName: commit.author?.Name ?? "", authorEmail: commit.author?.Email ?? "", when: commit.committer?.When ?? commit.author?.When ?? "", changeId: commit.change_id };};
// the tree endpoint uses lexicon casing where the log ones pass through go field// names, so this cannot share `toCommitSummary`export const toTreeCommitSummary = (commit: Tree.LastCommit): CommitSummary => { const [subject, body] = splitMessage(commit.message ?? ""); return { hash: commit.hash, shortHash: commit.hash.slice(0, 8), subject, body, authorName: commit.author?.name ?? "", authorEmail: commit.author?.email ?? "", when: commit.when ?? commit.author?.when ?? "" };};
export interface BranchSummary { name: string; hash: string; when?: string; isDefault: boolean;}
export const toBranchSummary = (branch: BranchEntry): BranchSummary => ({ name: branch.reference.name, hash: branch.reference.hash, when: branch.commit?.Committer?.When ?? branch.commit?.Author?.When, isDefault: branch.is_default === true});
export interface TagSummary { name: string; hash: string; /** an annotated tag has its own hash, this is the commit it points at */ commitHash: string; when?: string; message?: string;}
const hexFromBytes = (bytes: number[]): string => bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("");
export const toTagSummary = (tag: TagEntry): TagSummary => { const target = tag.tag?.Target; return { name: tag.name, hash: tag.hash, commitHash: target?.length ? hexFromBytes(target) : tag.hash, when: tag.tag?.Tagger?.When, message: tag.message ?? tag.tag?.Message };};
export type TreeEntryKind = "file" | "directory" | "symlink" | "submodule";
// modes come back octal and zero paddedexport const treeEntryKind = (mode: string): TreeEntryKind => { switch (mode.replace(/^0+/, "").padStart(6, "0")) { case "040000": return "directory"; case "120000": return "symlink"; case "160000": return "submodule"; default: return "file"; }};
export interface TreeEntrySummary { name: string; kind: TreeEntryKind; size: number; lastCommitHash?: string; lastCommitWhen?: string; lastCommitMessage?: string;}
export const toTreeEntrySummary = (entry: Tree.TreeEntry): TreeEntrySummary => ({ name: entry.name, kind: treeEntryKind(entry.mode), size: entry.size, lastCommitHash: entry.last_commit?.hash, lastCommitWhen: entry.last_commit?.when, lastCommitMessage: entry.last_commit?.message});
export const tagsByCommitHash = ( commits: CommitSummary[], tags: TagSummary[]): Record<string, string[]> => { const shown = new Set(commits.map((commit) => commit.hash)); return tags.reduce<Record<string, string[]>>((acc, tag) => { if (shown.has(tag.commitHash)) (acc[tag.commitHash] ??= []).push(tag.name); return acc; }, {});};
export const sortTreeEntries = (entries: TreeEntrySummary[]): TreeEntrySummary[] => [...entries].sort((a, b) => { const aDir = a.kind === "directory" || a.kind === "submodule"; const bDir = b.kind === "directory" || b.kind === "submodule"; if (aDir !== bDir) return aDir ? -1 : 1; return a.name.localeCompare(b.name); });
export const logFor = ( ctx: BobbinContext, repo: string, ref: string, limit: number, init?: XrpcRequestInit) => knotLog<LogResponse>(ctx, { repo, ref, limit }, init);
export const branchesFor = ( ctx: BobbinContext, repo: string, limit: number, init?: XrpcRequestInit) => knotBranches<BranchesResponse>(ctx, { repo, limit }, init);
export const tagsFor = (ctx: BobbinContext, repo: string, limit: number, init?: XrpcRequestInit) => knotTags<TagsResponse>(ctx, { repo, limit }, init);