/** * Tree & blob domain: wire types, row normalization and the tree/blob * endpoints. * * `GET /repos/{s}/{r}/tree/{rev}` is serialised as a bare `serde_json::Value` * on the server, so the generated client types it as `unknown`. The live shape * comes from gitbaby's `TreeEntry`: * • `oid` → plain hex today (`8358e08…`); older builds answered * `{ "Sha1": [20 raw bytes] }`, so both are accepted; * • `name` → the basename (`FUNDING.yml`), while some payloads carried the * repo-root-relative path — both are normalised to `path`. * Normalise both once, here so callers only see `TreeEntry`. */ import { unwrapApi } from '@app/lib' import type { EnvelopeBlobContentDto } from '@app/services/api' import Apis from '@app/services/api' export type BlobPayload = NonNullable /** gitbaby `TreeEntry` wire mode tag. */ export type TreeMode = 'Blob' | 'Exec' | 'Symlink' | 'Tree' | 'Submodule' | 'Commit' /** Normalized tree row — see `normalizeTreeRows` for what the raw payload looks like. */ export interface TreeEntry { /** Repo-root-relative path. */ path: string /** Basename, for display. */ name: string mode: TreeMode /** Full hex object id, or null when the server sent something unparseable. */ oid: string | null size: number | null } const MODES: TreeMode[] = ['Blob', 'Exec', 'Symlink', 'Tree', 'Submodule', 'Commit'] function bytesToHex(value: unknown): string | null { if (!Array.isArray(value) || value.length === 0) return null let out = '' for (const byte of value) { if (typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255) return null out += byte.toString(16).padStart(2, '0') } return out } /** `{ Sha1: […] }` / `[…]` / plain hex string → lowercase hex oid. */ function oidToHex(oid: unknown): string | null { if (typeof oid === 'string') return /^[0-9a-f]{4,64}$/i.test(oid) ? oid.toLowerCase() : null const direct = bytesToHex(oid) if (direct) return direct if (oid && typeof oid === 'object' && !Array.isArray(oid)) { for (const value of Object.values(oid as Record)) { const hex = bytesToHex(value) if (hex) return hex } } return null } function basename(path: string): string { const trimmed = path.replace(/\/+$/, '') const slash = trimmed.lastIndexOf('/') return slash === -1 ? trimmed : trimmed.slice(slash + 1) } function normalizeTreeRows(rows: unknown, dir: string): TreeEntry[] { if (!Array.isArray(rows)) return [] const out: TreeEntry[] = [] for (const raw of rows) { if (!raw || typeof raw !== 'object') continue const entry = raw as Record const rawName = typeof entry.name === 'string' ? entry.name.trim() : '' if (!rawName || rawName === '.') continue const path = dir && !rawName.startsWith(`${dir}/`) ? `${dir}/${rawName}` : rawName const mode = MODES.includes(entry.mode as TreeMode) ? (entry.mode as TreeMode) : 'Blob' out.push({ path, name: basename(path), mode, oid: oidToHex(entry.oid), size: typeof entry.size === 'number' ? entry.size : null, }) } return out } export async function fetchTreeJson(space: string, repo: string, revision: string, path: string): Promise { const data = await unwrapApi( Apis.trees.get_tree({ pathParams: { space_name: space, repo_name: repo, revision }, params: { path }, }), ) return normalizeTreeRows(data, path) } export const fetchBlob = (space: string, repo: string, oid: string) => unwrapApi(Apis.blobs.blob_content({ pathParams: { space_name: space, repo_name: repo, oid } }))