← Public packages
@kentcdodds/lineage
Interactive commit-level lines of kody, minus generated noise, as an offline-ready chart.
src/github.ts
126 lines · 3.7 KB · TypeScriptimport 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<BranchHistory> {
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<GithubCommitDetail> {
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<string | null> {
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
}