spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
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