/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/lib/overview.mjs * Purpose : Aggregated per-repo overview (meta + git facts), cached * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { readFileSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { computeLanguages } from '../stats/languages.mjs'; import { mapLimit } from './util.mjs'; /** @returns {Record} clone counts by repo */ function cloneCounts(ctx) { const path = join(ctx.config.dataDir, 'clones.json'); try { if (existsSync(path)) return JSON.parse(readFileSync(path, 'utf8')); } catch { /* ignore */ } return {}; } /** * Build (or read from cache) the full overview of one repository. * @param {{config, repos, meta, cache}} ctx * @param {string} name * @returns {Promise} */ export async function repoOverview(ctx, name) { if (!ctx.repos.exists(name)) return null; const cachePath = ctx.cache.path('repos', name, '_repo', 'overview.json'); const cached = ctx.cache.getJSON(cachePath); const clones = cloneCounts(ctx)[name] ?? 0; if (cached) return { ...cached, cloneCount: clones }; const meta = ctx.meta.get(name) ?? {}; const head = await ctx.repos.head(name); const defaultBranch = await ctx.repos.defaultBranch(name); const branches = await ctx.repos.branches(name); const tags = await ctx.repos.tags(name); let commitCount = 0; let languages = { languages: [], totalBytes: 0 }; let license = null; if (head) { commitCount = await ctx.repos.commitCount(name, head); languages = computeLanguages(await ctx.repos.allFiles(name, head)); license = await ctx.repos.license(name, head); } const overview = { name, description: meta.description ?? '', topics: meta.topics ?? [], homepage: meta.homepage ?? '', pinned: Boolean(meta.pinned), created: meta.created ?? null, defaultBranch, head, empty: head === null, lastPush: await ctx.repos.lastPushDate(name), commitCount, branchCount: branches.length, tagCount: tags.length, sizeBytes: await ctx.repos.sizeBytes(name), languages: languages.languages, languageBytes: languages.totalBytes, topLanguage: languages.languages[0]?.name ?? null, license: license?.name ?? null, licensePath: license?.path ?? null, cloneUrl: `${ctx.config.publicUrl}/${name}.git`, url: `${ctx.config.publicUrl}/${name}`, }; ctx.cache.setJSON(cachePath, overview); return { ...overview, cloneCount: clones }; } /** * Overviews for every repo, most recently pushed first. * @param {{config, repos, meta, cache}} ctx * @returns {Promise} */ export async function allOverviews(ctx) { const names = ctx.repos.list(); const overviews = await mapLimit(names, 8, (name) => repoOverview(ctx, name)); return overviews .filter(Boolean) .sort((a, b) => String(b.lastPush ?? '').localeCompare(String(a.lastPush ?? ''))); } /** * Site-wide stats for the home hero + /api/v1/stats. * @param {{config, repos, meta, cache}} ctx */ export async function siteStats(ctx) { const overviews = await allOverviews(ctx); const languages = new Set(); let commits = 0; for (const o of overviews) { commits += o.commitCount; for (const lang of o.languages) languages.add(lang.name); } return { repos: overviews.length, commits, languages: languages.size, languageNames: [...languages].sort(), }; }