Skip to content
Clone
98e06cb
100 lines 3.6 KB
/**
* 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<EnvelopeBlobContentDto['data']>
/** 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<string, unknown>)) {
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<string, unknown>
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<TreeEntry[]> {
const data = await unwrapApi<unknown>(
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<BlobPayload>(Apis.blobs.blob_content({ pathParams: { space_name: space, repo_name: repo, oid } }))