Vercel/src/platforms/github.ts
2026-02-19 10:27:34 -06:00

205 lines
5.4 KiB
TypeScript

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<RepoParams> => {
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,
};
};