{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "get-github-data",
  "title": "Get GitHub Data",
  "author": "Om Patil <patilom1906@gmail.com>",
  "description": "Fetch a GitHub user's repositories and authored pull requests, with caching and rate-limit handling.",
  "dependencies": [],
  "files": [
    {
      "path": "src/registry/lib/get-github-data/github-data.ts",
      "content": "export type GitHubRepo = {\n  id: number\n  name: string\n  /** `owner/name` as it lives on GitHub. */\n  fullName: string\n  description: string | null\n  /** GitHub HTML URL. */\n  url: string\n  /** Primary language, if any. */\n  language: string | null\n  /** Star count. For forks this is the upstream repo's star count. */\n  stars: number\n  isFork: boolean\n  /** `owner/name` of the upstream repo, when this is a fork. */\n  upstream: string | null\n  /** ISO timestamp of the last push. */\n  pushedAt: string\n}\n\nexport type GitHubContributionState = \"merged\" | \"closed\" | \"open\"\n\nexport type GitHubContribution = {\n  id: number\n  /** `owner/name` of the repository the pull request targets. */\n  repo: string\n  title: string\n  url: string\n  state: GitHubContributionState\n  createdAt: string\n}\n\nexport type GitHubData = {\n  /** Authored pull requests against repositories the viewer does not own. */\n  contributions: GitHubContribution[]\n  /** Repositories the viewer forked, ranked by the original repo's stars. */\n  forks: GitHubRepo[]\n  /** Repositories the viewer owns (non-forks). */\n  owned: GitHubRepo[]\n}\n\nexport type RawGitHubRepo = {\n  id: number\n  name: string\n  full_name: string\n  description: string | null\n  html_url: string\n  language: string | null\n  stargazers_count: number\n  fork: boolean\n  pushed_at: string\n  /** Immediate upstream repo. Only returned by single-repo endpoints. */\n  parent?: { full_name: string; stargazers_count: number } | null\n  /** Root repo of the fork network. Only returned by single-repo endpoints. */\n  source?: { full_name: string; stargazers_count: number } | null\n}\n\nexport type RawGitHubIssue = {\n  id: number\n  title: string\n  html_url: string\n  state: string\n  created_at: string\n  repository_url: string\n  pull_request?: { merged_at: string | null } | null\n}\n\nconst GITHUB_REPOS_API = \"https://api.github.com/repos/\"\n\n/** GitHub list endpoints return a bare array; search endpoints wrap items. */\nexport function extractRepoPage(data: unknown): RawGitHubRepo[] {\n  return Array.isArray(data) ? (data as RawGitHubRepo[]) : []\n}\n\nexport function extractIssuePage(data: unknown): RawGitHubIssue[] {\n  const items = (data as { items?: unknown }).items\n  return Array.isArray(items) ? (items as RawGitHubIssue[]) : []\n}\n\n/** Reads the `rel=\"next\"` URL from a GitHub `Link` header, if present. */\nexport function getNextLink(link: string | null | undefined): string | null {\n  if (!link) {\n    return null\n  }\n\n  for (const segment of link.split(\",\")) {\n    const match = segment.match(/<([^>]+)>;\\s*rel=\"next\"/)\n    if (match) {\n      return match[1]\n    }\n  }\n\n  return null\n}\n\nexport function mapRepo(raw: RawGitHubRepo): GitHubRepo {\n  // Prefer the fork network's root (`source`) so the label and star count point\n  // at the original project rather than an intermediate fork.\n  const upstream = raw.fork ? (raw.source ?? raw.parent ?? null) : null\n\n  return {\n    id: raw.id,\n    name: raw.name,\n    fullName: raw.full_name,\n    description: raw.description,\n    url: raw.html_url,\n    language: raw.language,\n    stars: upstream ? upstream.stargazers_count : raw.stargazers_count,\n    isFork: raw.fork,\n    upstream: upstream ? upstream.full_name : null,\n    pushedAt: raw.pushed_at,\n  }\n}\n\n/**\n * Maps a search-issue result to a contribution. Pull requests against the\n * viewer's own repositories are dropped: those are fork-sync or self-authored\n * PRs, not outside contributions.\n */\nexport function mapContribution(\n  raw: RawGitHubIssue,\n  viewer: string\n): GitHubContribution | null {\n  const repo = raw.repository_url.startsWith(GITHUB_REPOS_API)\n    ? raw.repository_url.slice(GITHUB_REPOS_API.length)\n    : raw.repository_url\n\n  const owner = repo.split(\"/\")[0]\n  if (owner.toLowerCase() === viewer.toLowerCase()) {\n    return null\n  }\n\n  const merged = Boolean(raw.pull_request?.merged_at)\n  const state: GitHubContributionState = merged\n    ? \"merged\"\n    : raw.state === \"open\"\n      ? \"open\"\n      : \"closed\"\n\n  return {\n    id: raw.id,\n    repo,\n    title: raw.title,\n    url: raw.html_url,\n    state,\n    createdAt: raw.created_at,\n  }\n}\n\nfunction byStarsThenName(a: GitHubRepo, b: GitHubRepo): number {\n  return b.stars - a.stars || a.name.localeCompare(b.name)\n}\n\nexport function groupGitHubData(\n  repos: RawGitHubRepo[],\n  issues: RawGitHubIssue[],\n  viewer: string\n): GitHubData {\n  const mapped = repos.map(mapRepo)\n\n  return {\n    contributions: issues\n      .map((issue) => mapContribution(issue, viewer))\n      .filter((item): item is GitHubContribution => item !== null)\n      .sort((a, b) => b.createdAt.localeCompare(a.createdAt)),\n    forks: mapped.filter((repo) => repo.isFork).sort(byStarsThenName),\n    owned: mapped.filter((repo) => !repo.isFork).sort(byStarsThenName),\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/get-github-data/github-data.ts"
    },
    {
      "path": "src/registry/lib/get-github-data/get-cached-github-data.ts",
      "content": "import { unstable_cache } from \"next/cache\"\n\nimport {\n  extractIssuePage,\n  extractRepoPage,\n  getNextLink,\n  groupGitHubData,\n  type GitHubData,\n  type RawGitHubIssue,\n  type RawGitHubRepo,\n} from \"@/registry/lib/get-github-data/github-data\"\n\nconst GITHUB_API = \"https://api.github.com\"\n\nfunction getHeaders(): Record<string, string> {\n  const headers: Record<string, string> = {\n    Accept: \"application/vnd.github+json\",\n    \"X-GitHub-Api-Version\": \"2022-11-28\",\n  }\n\n  const token = process.env.GITHUB_API_TOKEN\n  if (token) {\n    headers.Authorization = `Bearer ${token}`\n  }\n\n  return headers\n}\n\n/** Follows `Link` pagination until exhausted, with a safety cap. */\nasync function fetchAllPages<T>(\n  url: string,\n  extract: (data: unknown) => T[]\n): Promise<T[]> {\n  const items: T[] = []\n  let next: string | null = url\n\n  while (next) {\n    const res = await fetch(next, { headers: getHeaders() })\n\n    if (!res.ok) {\n      throw new Error(`GitHub API request failed (${res.status})`)\n    }\n\n    items.push(...extract(await res.json()))\n    next = getNextLink(res.headers.get(\"link\"))\n\n    if (items.length >= 1000) {\n      break\n    }\n  }\n\n  return items\n}\n\n/**\n * Number of per-fork upstream requests to run at once, to stay clear of\n * GitHub's secondary (burst) rate limits.\n */\nconst UPSTREAM_FETCH_CONCURRENCY = 8\n\n/**\n * The user-repos list endpoint omits `parent`/`source`, so forks arrive with no\n * upstream. Fetch each fork's own record to recover the original project (the\n * network root, `source`) so forks can be labelled and ranked by the original\n * repo's stars. Failures leave the fork unenriched rather than failing the run.\n */\nasync function withUpstreamRepos(\n  repos: RawGitHubRepo[]\n): Promise<RawGitHubRepo[]> {\n  const forks = repos.filter(\n    (repo) => repo.fork && !repo.parent && !repo.source\n  )\n\n  if (forks.length === 0) {\n    return repos\n  }\n\n  const upstreamByFullName = new Map<\n    string,\n    RawGitHubRepo[\"parent\"] | undefined\n  >()\n\n  for (let i = 0; i < forks.length; i += UPSTREAM_FETCH_CONCURRENCY) {\n    const chunk = forks.slice(i, i + UPSTREAM_FETCH_CONCURRENCY)\n\n    await Promise.all(\n      chunk.map(async (repo) => {\n        try {\n          const res = await fetch(`${GITHUB_API}/repos/${repo.full_name}`, {\n            headers: getHeaders(),\n          })\n\n          if (!res.ok) {\n            return\n          }\n\n          const data = (await res.json()) as RawGitHubRepo\n          upstreamByFullName.set(repo.full_name, data.source ?? data.parent)\n        } catch {\n          // Ignore; the fork renders without upstream metadata.\n        }\n      })\n    )\n  }\n\n  return repos.map((repo) => {\n    const upstream = upstreamByFullName.get(repo.full_name)\n    return upstream ? { ...repo, parent: upstream } : repo\n  })\n}\n\n/**\n * Fetches a user's repositories (owned + forks) and their authored pull\n * requests. Throws on failure so the cache layer never stores an error result;\n * callers of `getCachedGithubData` receive `null` instead.\n */\nasync function fetchGithubData(username: string): Promise<GitHubData> {\n  const [repos, issues] = await Promise.all([\n    fetchAllPages<RawGitHubRepo>(\n      `${GITHUB_API}/users/${username}/repos?per_page=100&sort=updated&type=owner`,\n      extractRepoPage\n    ),\n    fetchAllPages<RawGitHubIssue>(\n      `${GITHUB_API}/search/issues?q=${encodeURIComponent(\n        `author:${username} type:pr`\n      )}&per_page=100&sort=created&order=desc`,\n      extractIssuePage\n    ),\n  ])\n\n  return groupGitHubData(await withUpstreamRepos(repos), issues, username)\n}\n\nconst getCachedGithubDataUncached = unstable_cache(\n  fetchGithubData,\n  [\"github-data\"],\n  { revalidate: 86400 } // 1 day\n)\n\n/**\n * Cached wrapper around {@link fetchGithubData}. Returns `null` when the API is\n * unreachable or rate-limited, so callers can render nothing instead of an\n * empty shell. Failures are not cached, so a transient error does not hide the\n * data for a full revalidation window.\n */\nexport async function getCachedGithubData(\n  username: string\n): Promise<GitHubData | null> {\n  if (!username) {\n    return null\n  }\n\n  try {\n    return await getCachedGithubDataUncached(username)\n  } catch {\n    return null\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/get-github-data/get-cached-github-data.ts"
    }
  ],
  "categories": [
    "data-display"
  ],
  "type": "registry:lib"
}