/** * Token-scoped discovery for onboarding. Given just a base URL + PAT (no repo * chosen yet), enumerate the owners and repositories the token can reach, so the * connect screen can offer them as dropdowns instead of free-text. These * endpoints are user-scoped, not repo-scoped, so they don't belong on the * repo-bound `GiteaClient` — this is a standalone call with the same injected * `fetch` seam (real fetch in main, stub in tests). */ import { GiteaApiError } from './types.js' import type { FetchLike } from './types.js' /** What onboarding needs to render the owner + repo pickers. */ export interface DiscoveredRepos { /** The token's own login (its default owner). */ user: string /** Owners with at least one accessible repo — the user first, then the rest, sorted. */ owners: string[] /** Repo names per owner, sorted. Keys match `owners`. */ reposByOwner: Record } interface RawRepo { name: string owner?: { login?: string } | null } const PAGE_LIMIT = 50 async function getJson(url: string, token: string, fetchImpl: FetchLike): Promise { const res = await fetchImpl(url, { headers: { Authorization: `token ${token}`, Accept: 'application/json' } }) if (!res.ok) { const body = await res.text().catch(() => '') throw new GiteaApiError(res.status, `GET ${url} failed (${res.status})`, body) } return res.json() } /** * Enumerate the owners + repos a token can reach. Hits `/user` (to learn the * token's own login and prove the token works) and paginates `/user/repos` * (every repo the token can access across personal + org namespaces). */ export async function discoverRepos(conn: { baseUrl: string; token: string }, fetchImpl: FetchLike): Promise { const apiBase = `${conn.baseUrl.replace(/\/+$/, '')}/api/v1` const me = (await getJson(`${apiBase}/user`, conn.token, fetchImpl)) as { login?: string } const user = me.login ?? '' const repos: RawRepo[] = [] for (let page = 1; ; page++) { const batch = (await getJson(`${apiBase}/user/repos?page=${page}&limit=${PAGE_LIMIT}`, conn.token, fetchImpl)) as RawRepo[] repos.push(...batch) if (batch.length < PAGE_LIMIT) break } const reposByOwner: Record = {} for (const r of repos) { const owner = r.owner?.login if (!owner || !r.name) continue ;(reposByOwner[owner] ??= []).push(r.name) } for (const owner of Object.keys(reposByOwner)) { reposByOwner[owner] = [...new Set(reposByOwner[owner])].sort((a, b) => a.localeCompare(b)) } // owners sorted alphabetically, but the token's own login always leads. const owners = Object.keys(reposByOwner).sort((a, b) => { if (a === user) return -1 if (b === user) return 1 return a.localeCompare(b) }) return { user, owners, reposByOwner } }