| | import crypto from 'crypto' |
| | import fs from 'fs/promises' |
| |
|
| | import { RequestError } from '@octokit/request-error' |
| |
|
| | import { retryingGithub } from './github' |
| | const github = retryingGithub() |
| |
|
| | |
| | export async function getCommitSha(owner: string, repo: string, ref: string) { |
| | try { |
| | const { data } = await github.git.getRef({ |
| | owner, |
| | repo, |
| | ref, |
| | }) |
| | return data.object.sha |
| | } catch (err) { |
| | console.log('error getting commit sha', owner, repo, ref) |
| | throw err |
| | } |
| | } |
| |
|
| | |
| | export async function hasMatchingRef(owner: string, repo: string, ref: string) { |
| | try { |
| | await github.git.getRef({ |
| | owner, |
| | repo, |
| | ref, |
| | }) |
| | return true |
| | } catch (err) { |
| | if (err instanceof RequestError && err.status === 404) { |
| | return false |
| | } |
| | console.log('error getting matching ref', owner, repo, ref) |
| | throw err |
| | } |
| | } |
| |
|
| | |
| | export async function getTreeSha(owner: string, repo: string, commitSha: string) { |
| | try { |
| | const { data } = await github.git.getCommit({ |
| | owner, |
| | repo, |
| | commit_sha: commitSha, |
| | }) |
| | return data.tree.sha |
| | } catch (err) { |
| | console.log('error getting tree sha', owner, repo, commitSha) |
| | throw err |
| | } |
| | } |
| |
|
| | |
| | export async function getTree(owner: string, repo: string, ref: string) { |
| | const commitSha = await getCommitSha(owner, repo, ref) |
| | const treeSha = await getTreeSha(owner, repo, commitSha) |
| | try { |
| | const { data } = await github.git.getTree({ |
| | owner, |
| | repo, |
| | tree_sha: treeSha, |
| | recursive: 'true', |
| | }) |
| | |
| | |
| | return data.tree |
| | } catch (err) { |
| | console.log('error getting tree', owner, repo, ref) |
| | throw err |
| | } |
| | } |
| |
|
| | |
| | export async function getContentsForBlob(owner: string, repo: string, sha: string) { |
| | const { data } = await github.git.getBlob({ |
| | owner, |
| | repo, |
| | file_sha: sha, |
| | }) |
| | |
| | return Buffer.from(data.content, 'base64').toString() |
| | } |
| |
|
| | |
| | export async function getContents(owner: string, repo: string, ref: string, path: string) { |
| | const { data } = await getContent(owner, repo, ref, path) |
| | if (!data.content) { |
| | return await getContentsForBlob(owner, repo, data.sha) |
| | } |
| | |
| | return Buffer.from(data.content, 'base64').toString() |
| | } |
| |
|
| | |
| | export async function getContentAndData(owner: string, repo: string, ref: string, path: string) { |
| | const { data } = await getContent(owner, repo, ref, path) |
| | const content = data.content |
| | ? Buffer.from(data.content, 'base64').toString() |
| | : await getContentsForBlob(owner, repo, data.sha) |
| | |
| | return { content, blobSha: data.sha } |
| | } |
| |
|
| | async function getContent( |
| | owner: string, |
| | repo: string, |
| | ref: string, |
| | path: string, |
| | ): Promise<Record<string, any>> { |
| | try { |
| | return await github.repos.getContent({ |
| | owner, |
| | repo, |
| | ref, |
| | path, |
| | }) |
| | } catch (err) { |
| | console.log(`error getting ${path} from ${owner}/${repo} at ref ${ref}`) |
| | throw err |
| | } |
| | } |
| |
|
| | |
| | export async function listPulls(owner: string, repo: string) { |
| | try { |
| | const { data } = await github.pulls.list({ |
| | owner, |
| | repo, |
| | per_page: 100, |
| | }) |
| | return data |
| | } catch (err) { |
| | console.log(`error listing pulls in ${owner}/${repo}`) |
| | throw err |
| | } |
| | } |
| |
|
| | export async function createIssueComment( |
| | owner: string, |
| | repo: string, |
| | pullNumber: number, |
| | body: string, |
| | ) { |
| | try { |
| | const { data } = await github.issues.createComment({ |
| | owner, |
| | repo, |
| | issue_number: pullNumber, |
| | body, |
| | }) |
| | return data |
| | } catch (err) { |
| | console.log(`error creating a review comment on PR ${pullNumber} in ${owner}/${repo}`) |
| | throw err |
| | } |
| | } |
| |
|
| | |
| | export async function getPathsWithMatchingStrings( |
| | strArr: string[], |
| | org: string, |
| | repo: string, |
| | { cache = true, forceDownload = false } = {}, |
| | ) { |
| | const perPage = 100 |
| | const paths = new Set() |
| |
|
| | for (const str of strArr) { |
| | try { |
| | const q = `q=${str}+in:file+repo:${org}/${repo}` |
| | let currentPage = 1 |
| | let totalCount = 0 |
| | let currentCount = 0 |
| |
|
| | do { |
| | const data = await searchCode(q, perPage, currentPage, cache, forceDownload) |
| | data.items.map((el: Record<string, any>) => paths.add(el.path)) |
| | totalCount = data.total_count |
| | currentCount += data.items.length |
| | currentPage++ |
| | } while (currentCount < totalCount) |
| | } catch (err) { |
| | console.log(`error searching for ${str} in ${org}/${repo}`) |
| | throw err |
| | } |
| | } |
| |
|
| | return paths |
| | } |
| |
|
| | async function searchCode( |
| | q: string, |
| | perPage: number, |
| | currentPage: number, |
| | cache = true, |
| | forceDownload = false, |
| | ) { |
| | const cacheKey = `searchCode-${q}-${perPage}-${currentPage}` |
| | const tempFilename = `/tmp/searchCode-${crypto |
| | .createHash('md5') |
| | .update(cacheKey) |
| | .digest('hex')}.json` |
| |
|
| | if (!forceDownload && cache) { |
| | try { |
| | return JSON.parse(await fs.readFile(tempFilename, 'utf8')) |
| | } catch (error: any) { |
| | if (error.code !== 'ENOENT') { |
| | throw error |
| | } |
| | console.log(`Cache miss on ${tempFilename} (${cacheKey})`) |
| | } |
| | } |
| |
|
| | try { |
| | const { data } = await secondaryRateLimitRetry(github.rest.search.code, { |
| | q, |
| | per_page: perPage, |
| | page: currentPage, |
| | }) |
| | if (cache) { |
| | await fs.writeFile(tempFilename, JSON.stringify(data)) |
| | console.log(`Wrote search results to ${tempFilename}`) |
| | } |
| |
|
| | return data |
| | } catch (err) { |
| | console.log(`error searching for ${q} in code`) |
| | throw err |
| | } |
| | } |
| |
|
| | async function secondaryRateLimitRetry<T, TArgs = Record<string, unknown>>( |
| | callable: (args: TArgs) => Promise<T>, |
| | args: TArgs, |
| | maxAttempts = 10, |
| | sleepTime = 1000, |
| | ): Promise<T> { |
| | try { |
| | const response = await callable(args) |
| | return response |
| | } catch (err: any) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const lookFor = 'You have exceeded a secondary rate limit.' |
| | if ( |
| | err.status && |
| | err.status === 403 && |
| | err.response?.data?.message.includes(lookFor) && |
| | maxAttempts > 0 |
| | ) { |
| | console.warn( |
| | `Got secondary rate limit blocked. Sleeping for ${ |
| | sleepTime / 1000 |
| | } seconds. (attempts left: ${maxAttempts})`, |
| | ) |
| | return new Promise((resolve) => { |
| | setTimeout(() => { |
| | resolve(secondaryRateLimitRetry(callable, args, maxAttempts - 1, sleepTime * 2)) |
| | }, sleepTime) |
| | }) |
| | } |
| |
|
| | throw err |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export async function getDirectoryContents( |
| | owner: string, |
| | repo: string, |
| | branch: string, |
| | path: string, |
| | ) { |
| | const { data } = await getContent(owner, repo, branch, path) |
| | const files: any[] = [] |
| |
|
| | for (const blob of data) { |
| | if (blob.type === 'dir') { |
| | files.push(...(await getDirectoryContents(owner, repo, branch, blob.path))) |
| | } else if (blob.type === 'file') { |
| | if (!data.content) { |
| | const blobContents = await getContentsForBlob(owner, repo, blob.sha) |
| | files.push({ path: blob.path, content: blobContents }) |
| | } else { |
| | |
| | const decodedContent = Buffer.from(blob.content, 'base64').toString() |
| | files.push({ path: blob.path, content: decodedContent }) |
| | } |
| | } |
| | } |
| | return files |
| | } |
| |
|