import githubRequest from 'kody:@kentcdodds/github/request' import { withTransientGithubRetry } from './github-errors.ts' import { MIN_GITHUB_REQUEST_MS, nextLinkFrom, requestTimeoutWithin } from './github-paging.ts' import { repoHtmlUrl } from './repo.ts' import type { RepoSettings } from './types.ts' export { repoHtmlUrl } export type GithubCommitListItem = { sha: string html_url: string commit: { message: string author: { name?: string; date?: string } | null committer: { name?: string; date?: string } | null } parents: Array<{ sha: string }> } export type GithubCommitFile = { filename: string status: string additions: number deletions: number changes: number patch?: string } export type GithubCommitDetail = GithubCommitListItem & { stats?: { additions: number; deletions: number; total: number } files?: GithubCommitFile[] } const COMMITS_PER_PAGE = 100 export type BranchHistory = { items: GithubCommitListItem[] pages: number /** False when the deadline stopped paging before the branch history ended. */ complete: boolean } /** * List tracked-branch commits newest-first, one page per request, stopping at * `deadline`. Each page gets a bounded fetch timeout and its own transient * retry, so a slow GitHub page shortens the listing instead of restarting it * or running past the platform cap. Throws only when the first page fails. */ export async function listBranchCommits( repo: RepoSettings, options: { maxPages?: number; deadline?: number } = {}, ): Promise { const maxPages = options.maxPages ?? 40 const items: GithubCommitListItem[] = [] let pages = 0 // Follow GitHub's Link cursor; deep `page=N` offsets are several times slower. let nextPath: string | null = `/repos/${repo.owner}/${repo.repo}/commits` while (nextPath && pages < maxPages) { const path: string = nextPath let response try { response = await withTransientGithubRetry(() => { const timeoutMs = requestTimeoutWithin(options.deadline) if (timeoutMs === null) throw new BranchListingDeadlineError() return githubRequest({ account: 'bot', path, query: pages === 0 ? { sha: repo.branch, per_page: COMMITS_PER_PAGE } : undefined, throwOnError: true, timeoutMs, }) }) } catch (error) { if (pages === 0 && !(error instanceof BranchListingDeadlineError)) throw error return { items, pages, complete: false } } const page = Array.isArray(response.data) ? (response.data as GithubCommitListItem[]) : [] items.push(...page) pages += 1 nextPath = nextLinkFrom(response.headers?.link) } return { items, pages, complete: nextPath === null } } class BranchListingDeadlineError extends Error { constructor() { super('Lineage branch listing deadline reached') this.name = 'BranchListingDeadlineError' } } export async function getCommitDetail( repo: RepoSettings, sha: string, options: { deadline?: number } = {}, ): Promise { const response = await withTransientGithubRetry(() => githubRequest({ account: 'bot', path: `/repos/${repo.owner}/${repo.repo}/commits/${sha}`, throwOnError: true, timeoutMs: requestTimeoutWithin(options.deadline) ?? MIN_GITHUB_REQUEST_MS, }), ) return response.data as GithubCommitDetail } export async function getRepoTextFile( repo: RepoSettings, path: string, ): Promise { const response = await githubRequest({ account: 'bot', path: `/repos/${repo.owner}/${repo.repo}/contents/${path}`, headers: { Accept: 'application/vnd.github.raw' }, query: { ref: repo.branch }, }) if (!response.ok) return null if (typeof response.text === 'string' && response.text && !response.text.startsWith('{')) { return response.text } if (typeof response.data === 'string') return response.data return null }