import { TEMPLATE_REPO_OWNER, TEMPLATE_REPO_NAME, TEMPLATE_REPO_BRANCH, } from '@/app/config'; const DEFAULT_BRANCH = 'main'; const GITHUB_API_ERROR = 'API rate limit exceeded'; interface RepoParams { owner?: string repo?: string branch?: string commit?: string }; interface CommitDetails { commit: { committer: { date: string } } stats: { total: number, additions: number, deletions: number }, } const fetchGitHub = async ( url: string, cacheRequest = true, ) => { const data = await fetch( url, // Cache all results for 5 minutes to avoid rate limiting // GitHub API requests limited to 60 requests per hour cacheRequest ? { next: { revalidate: 300 } } : undefined, ) .then(response => response.json()); if ((data.message ?? '').includes(GITHUB_API_ERROR)) { throw new Error(GITHUB_API_ERROR); } return data; }; // Website urls export const getGitHubUrlOwner = ({ owner = TEMPLATE_REPO_OWNER, }: RepoParams = {}) => `https://github.com/${owner}`; export const getGitHubUrlRepo = ({ owner = TEMPLATE_REPO_OWNER, repo = TEMPLATE_REPO_NAME, }: RepoParams = {}) => `${getGitHubUrlOwner({ owner })}/${repo}`; export const getGitHubUrlBranch = ({ owner = TEMPLATE_REPO_OWNER, repo = TEMPLATE_REPO_NAME, branch = DEFAULT_BRANCH, }: RepoParams = {}) => `${getGitHubUrlRepo({ owner, repo })}/tree/${branch}`; export const getGitHubUrlCommit = ({ owner = TEMPLATE_REPO_OWNER, repo = TEMPLATE_REPO_NAME, commit, }: RepoParams = {}) => commit ? `${getGitHubUrlRepo({ owner, repo })}/commit/${commit}` : undefined; export const getGitHubUrlCompare = ({ owner, repo, branch = DEFAULT_BRANCH, }: RepoParams = {}) => // eslint-disable-next-line max-len `${getGitHubUrlRepo({ owner, repo })}/compare/${branch}...${TEMPLATE_REPO_OWNER}:${TEMPLATE_REPO_NAME}:${TEMPLATE_REPO_BRANCH}`; // API urls const getGitHubApiRepoUrl = ({ owner = TEMPLATE_REPO_OWNER, repo = TEMPLATE_REPO_NAME, }: RepoParams = {}) => `https://api.github.com/repos/${owner}/${repo}`; const getGitHubApiCommitUrl = (params?: RepoParams) => `${getGitHubApiRepoUrl(params)}/commits/${params?.commit}`; const getGitHubApiCommitsUrl = (params?: RepoParams) => `${getGitHubApiRepoUrl(params)}/commits/${params?.branch || DEFAULT_BRANCH}`; const getGitHubApiForksUrl = (params?: RepoParams) => `${getGitHubApiRepoUrl(params)}/forks`; const getGitHubApiCompareToRepoUrl = ({ owner, repo, branch = DEFAULT_BRANCH, }: RepoParams = {}) => // eslint-disable-next-line max-len `${getGitHubApiRepoUrl()}/compare/${TEMPLATE_REPO_BRANCH}...${owner}:${repo}:${branch}`; const getGitHubApiCompareToCommitUrl = ({ commit }: RepoParams = {}) => `${getGitHubApiRepoUrl()}/compare/${TEMPLATE_REPO_BRANCH}...${commit}`; // Requests export const getLatestBaseRepoCommitSha = async () => { const data = await fetchGitHub(getGitHubApiCommitsUrl()); return data.sha ? data.sha.slice(0, 7) as string : undefined; }; const getIsRepoForkedFromBase = async (params: RepoParams) => { const data = await fetchGitHub(getGitHubApiRepoUrl(params)); return ( Boolean(data.fork) && data.source?.full_name === `${TEMPLATE_REPO_OWNER}/${TEMPLATE_REPO_NAME}` ); }; const getCommitDetails = async (params: RepoParams) => { const data: CommitDetails = await fetchGitHub(getGitHubApiCommitUrl(params)); return data?.commit?.committer?.date && data.stats ? { date: new Date(data.commit.committer.date), stats: data.stats, } : undefined; }; const getGitHubCommitsBehindFromRepo = async (params?: RepoParams) => { const data = await fetchGitHub(getGitHubApiCompareToRepoUrl(params)); return data.behind_by as number; }; const getGitHubCommitsBehindFromCommit = async (params?: RepoParams) => { const data = await fetchGitHub(getGitHubApiCompareToCommitUrl(params)); return data.behind_by as number; }; const isRepoBaseRepo = ({ owner, repo }: RepoParams) => owner?.toLowerCase() === TEMPLATE_REPO_OWNER && repo?.toLowerCase() === TEMPLATE_REPO_NAME; export const getGitHubPublicFork = async (): Promise => { const data = await fetchGitHub(getGitHubApiForksUrl()); const fork = data[0]; return { owner: fork?.owner.login, repo: fork?.name, }; }; export const getGitHubMeta = async (params: RepoParams) => { const urlOwner = getGitHubUrlOwner(params); const urlRepo = getGitHubUrlRepo(params); const urlBranch = getGitHubUrlBranch(params); const urlCommit = getGitHubUrlCommit(params); const isBaseRepo = isRepoBaseRepo(params); let commitDate: Date | undefined; let isForkedFromBase: boolean | undefined; let isBehind: boolean | undefined; let behindBy: number | undefined; let didError: boolean = false; try { const results = await Promise.all([ getCommitDetails(params), getIsRepoForkedFromBase(params), isBaseRepo && params.commit ? getGitHubCommitsBehindFromCommit(params) : getGitHubCommitsBehindFromRepo(params), ]); commitDate = results[0]?.date; isForkedFromBase = results[1]; behindBy = results[2]; isBehind = behindBy === undefined ? undefined : behindBy > 0; } catch (error) { didError = true; console.error('Error retrieving GitHub meta', { params, error }); } return { ...params, urlOwner, urlRepo, urlBranch, urlCommit, commitDate, isForkedFromBase, isBaseRepo, behindBy, isBehind, didError, }; };