import { resolveGitHubRepoFromDirectory } from './index.js'; const REPO_METADATA_TTL_MS = 5 * 60_000; const REPO_METADATA_CACHE_MAX_ENTRIES = 200; const repoMetadataCache = new Map(); const setRepoMetadataCache = (repoKey, data) => { if (repoMetadataCache.size >= REPO_METADATA_CACHE_MAX_ENTRIES && !repoMetadataCache.has(repoKey)) { const oldest = repoMetadataCache.entries().next().value; if (oldest) { repoMetadataCache.delete(oldest[0]); } } repoMetadataCache.set(repoKey, { data, fetchedAt: Date.now() }); }; const normalizeRepoKey = (owner, repo) => { const o = typeof owner === 'string' ? owner.trim().toLowerCase() : ''; const r = typeof repo === 'string' ? repo.trim().toLowerCase() : ''; if (!o || !r) return ''; return `${o}/${r}`; }; const getRepoMetadata = async (octokit, repo) => { const repoKey = normalizeRepoKey(repo?.owner, repo?.repo); if (!repoKey) return null; const cached = repoMetadataCache.get(repoKey); if (cached && Date.now() - cached.fetchedAt < REPO_METADATA_TTL_MS) { return cached.data; } try { const response = await octokit.rest.repos.get({ owner: repo.owner, repo: repo.repo, }); const data = response?.data ?? null; setRepoMetadataCache(repoKey, data); return data; } catch (error) { if (error?.status === 403 || error?.status === 404) { setRepoMetadataCache(repoKey, null); return null; } throw error; } }; /** * Resolve the repo network for a directory. If the origin repo is a fork, * includes the parent/source (upstream) repo in the result. * * @param {import('@octokit/rest').Octokit} octokit * @param {string} directory * @param {string} [remoteName='origin'] * @returns {Promise | null>} * Array of repos to query (origin first, then upstream), or null if not a fork. */ export async function resolveRepoNetwork(octokit, directory, remoteName = 'origin') { const { repo } = await resolveGitHubRepoFromDirectory(directory, remoteName).catch(() => ({ repo: null })); if (!repo) return null; const metadata = await getRepoMetadata(octokit, repo); if (!metadata) return [{ ...repo, source: 'origin' }]; const result = [{ ...repo, source: 'origin' }]; const seenKeys = new Set([normalizeRepoKey(repo.owner, repo.repo)]); const parent = metadata?.parent; if (parent?.owner?.login && parent?.name) { const key = normalizeRepoKey(parent.owner.login, parent.name); if (!seenKeys.has(key)) { seenKeys.add(key); result.push({ owner: parent.owner.login, repo: parent.name, url: parent.html_url || `https://github.com/${parent.owner.login}/${parent.name}`, source: 'upstream', }); } } const source = metadata?.source; if (source?.owner?.login && source?.name) { const key = normalizeRepoKey(source.owner.login, source.name); if (!seenKeys.has(key)) { seenKeys.add(key); result.push({ owner: source.owner.login, repo: source.name, url: source.html_url || `https://github.com/${source.owner.login}/${source.name}`, source: 'upstream', }); } } // If no parent/source found, repo is not a fork if (result.length === 1) return null; return result; }