SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
3.9 KB · 114 lines javascript
Raw Blame History
1/**2 * ─────────────────────────────────────────────3 *  SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 *  Author  : Simon-Pierre Boucher6 *  Contact : contact@spboucher.ai7 *  File    : src/lib/overview.mjs8 *  Purpose : Aggregated per-repo overview (meta + git facts), cached9 *  License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { readFileSync, existsSync } from 'node:fs';14import { join } from 'node:path';15import { computeLanguages } from '../stats/languages.mjs';16import { mapLimit } from './util.mjs';1718/** @returns {Record<string, number>} clone counts by repo */19function cloneCounts(ctx) {20  const path = join(ctx.config.dataDir, 'clones.json');21  try {22    if (existsSync(path)) return JSON.parse(readFileSync(path, 'utf8'));23  } catch {24    /* ignore */25  }26  return {};27}2829/**30 * Build (or read from cache) the full overview of one repository.31 * @param {{config, repos, meta, cache}} ctx32 * @param {string} name33 * @returns {Promise<object|null>}34 */35export async function repoOverview(ctx, name) {36  if (!ctx.repos.exists(name)) return null;37  const cachePath = ctx.cache.path('repos', name, '_repo', 'overview.json');38  const cached = ctx.cache.getJSON(cachePath);39  const clones = cloneCounts(ctx)[name] ?? 0;40  if (cached) return { ...cached, cloneCount: clones };4142  const meta = ctx.meta.get(name) ?? {};43  const head = await ctx.repos.head(name);44  const defaultBranch = await ctx.repos.defaultBranch(name);45  const branches = await ctx.repos.branches(name);46  const tags = await ctx.repos.tags(name);47  let commitCount = 0;48  let languages = { languages: [], totalBytes: 0 };49  let license = null;50  if (head) {51    commitCount = await ctx.repos.commitCount(name, head);52    languages = computeLanguages(await ctx.repos.allFiles(name, head));53    license = await ctx.repos.license(name, head);54  }55  const overview = {56    name,57    description: meta.description ?? '',58    topics: meta.topics ?? [],59    homepage: meta.homepage ?? '',60    pinned: Boolean(meta.pinned),61    created: meta.created ?? null,62    defaultBranch,63    head,64    empty: head === null,65    lastPush: await ctx.repos.lastPushDate(name),66    commitCount,67    branchCount: branches.length,68    tagCount: tags.length,69    sizeBytes: await ctx.repos.sizeBytes(name),70    languages: languages.languages,71    languageBytes: languages.totalBytes,72    topLanguage: languages.languages[0]?.name ?? null,73    license: license?.name ?? null,74    licensePath: license?.path ?? null,75    cloneUrl: `${ctx.config.publicUrl}/${name}.git`,76    url: `${ctx.config.publicUrl}/${name}`,77  };78  ctx.cache.setJSON(cachePath, overview);79  return { ...overview, cloneCount: clones };80}8182/**83 * Overviews for every repo, most recently pushed first.84 * @param {{config, repos, meta, cache}} ctx85 * @returns {Promise<object[]>}86 */87export async function allOverviews(ctx) {88  const names = ctx.repos.list();89  const overviews = await mapLimit(names, 8, (name) => repoOverview(ctx, name));90  return overviews91    .filter(Boolean)92    .sort((a, b) => String(b.lastPush ?? '').localeCompare(String(a.lastPush ?? '')));93}9495/**96 * Site-wide stats for the home hero + /api/v1/stats.97 * @param {{config, repos, meta, cache}} ctx98 */99export async function siteStats(ctx) {100  const overviews = await allOverviews(ctx);101  const languages = new Set();102  let commits = 0;103  for (const o of overviews) {104    commits += o.commitCount;105    for (const lang of o.languages) languages.add(lang.name);106  }107  return {108    repos: overviews.length,109    commits,110    languages: languages.size,111    languageNames: [...languages].sort(),112  };113}114