┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ // Build-time git readers for the /git/ section. Pages are generated │
│ // from real repositories at build time - stagit-style, but rendered │
│ // through the site's own components. Anything that can land in a │
│ // frame passes toGrid() so history written before the og-ascii rule │
│ // can't fail the build. Every reader takes a ref so branch pages are │
│ // first-class; urls follow the github shape (tree/<branch>/<path>). │
│ import { execFileSync } from "node:child_process"; │
│ import { existsSync } from "node:fs"; │
│ import { toGrid } from "./ascii"; │
│ import type { TimelineEntry } from "../components/Timeline/Timeline"; │
│ │
│ export interface GitRepo { │
│ /** Url segment. */ │
│ slug: string; │
│ /** Display name. */ │
│ name: string; │
│ /** Local checkout or bare repo path, resolved from ooknet-design/. */ │
│ path: string; │
│ desc: string; │
│ clone?: string; │
│ /** Path prefixes that get no blob/history pages - committed data │
│ * dumps would explode the page count. Listed but unlinked. */ │
│ exclude?: string[]; │
│ } │
│ │
│ const MAX_BLOB = 200 * 1024; │
│ │
│ const git = (path: string, args: string[]): string => │
│ execFileSync("git", ["-C", path, ...args], { │
│ encoding: "utf8", │
│ maxBuffer: 32 * 1024 * 1024, │
│ }); │
│ │
│ export const repoAvailable = (r: GitRepo): boolean => existsSync(r.path); │
│ │
│ /** True when a path sits under one of the repo's excluded prefixes. */ │
│ export const excluded = (r: GitRepo, path: string): boolean => │
│ (r.exclude ?? []).some((p) => path === p || path.startsWith(p + "/")); │
│ │
│ /** Local branches, default branch first. */ │
│ export function branches(path: string): string[] { │
│ const head = defaultBranch(path); │
│ const all = git(path, ["for-each-ref", "refs/heads", "--format=%(refname:short)" │
│ ]) │
│ .split("\n") │
│ .filter(Boolean); │
│ return [head, ...all.filter((b) => b !== head)]; │
│ } │
│ │
│ export const defaultBranch = (path: string): string => │
│ git(path, ["symbolic-ref", "--short", "HEAD"]).trim(); │
│ │
│ export interface RepoInfo { │
│ branch: string; │
│ commits: number; │
│ lastHash: string; │
│ lastDate: string; │
│ lastSubject: string; │
│ } │
│ │
│ export function repoInfo(path: string, ref?: string): RepoInfo { │
│ const branch = ref ?? defaultBranch(path); │
│ const [lastHash, lastDate, ...subject] = git(path, ["log", "-1", "--format=%h|%a │
│ s|%s", branch]) │
│ .trim() │
│ .split("|"); │
│ return { │
│ branch, │
│ commits: parseInt(git(path, ["rev-list", "--count", branch]).trim(), 10), │
│ lastHash, │
│ lastDate, │
│ lastSubject: toGrid(subject.join("|")), │
│ }; │
│ } │
│ │
│ export function repoFiles(path: string, ref = "HEAD"): string[] { │
│ return git(path, ["ls-tree", "-r", "--name-only", ref]) │
│ .split("\n") │
│ .filter(Boolean); │
│ } │
│ │
│ export function repoLog(path: string, limit: number, base: string, ref = "HEAD"): │
│ TimelineEntry[] { │
│ return git(path, ["log", `-${limit}`, "--format=%h|%as|%s", ref]) │
│ .split("\n") │
│ .filter(Boolean) │
│ .map((line) => { │
│ const [hash, date, ...subject] = line.split("|"); │
│ return { │
│ date, │
│ title: toGrid(hash), │
│ desc: toGrid(subject.join("|")), │
│ href: `${base}commit/${hash}/`, │
│ }; │
│ }); │
│ } │
│ │
│ /** Commits touching one path - the file history page. */ │
│ export function pathLog( │
│ path: string, │
│ file: string, │
│ limit: number, │
│ base: string, │
│ ref = "HEAD", │
│ ): TimelineEntry[] { │
│ return git(path, ["log", `-${limit}`, "--format=%h|%as|%s", ref, "--", file]) │
│ .split("\n") │
│ .filter(Boolean) │
│ .map((line) => { │
│ const [hash, date, ...subject] = line.split("|"); │
│ return { │
│ date, │
│ title: toGrid(hash), │
│ desc: toGrid(subject.join("|")), │
│ href: `${base}commit/${hash}/`, │
│ }; │
│ }); │
│ } │
│ │
│ export interface DirEntry { │
│ name: string; │
│ path: string; │
│ kind: "tree" | "blob"; │
│ /** Last commit touching this path. */ │
│ hash: string; │
│ date: string; │
│ subject: string; │
│ } │
│ │
│ /** Immediate children of a directory, github-style: dirs first, each │
│ * with the last commit that touched it. */ │
│ export function dirEntries(path: string, dir = "", ref = "HEAD"): DirEntry[] { │
│ const spec = dir ? `${ref}:${dir}` : ref; │
│ const rows = git(path, ["ls-tree", spec]) │
│ .split("\n") │
│ .filter(Boolean) │
│ .map((line) => { │
│ const [meta, name] = line.split("\t"); │
│ const kind = meta.split(" ")[1] as "tree" | "blob" | "commit"; │
│ return { name, kind }; │
│ }) │
│ .filter((e): e is { name: string; kind: "tree" | "blob" } => e.kind !== "commi │
│ t"); │
│ return rows │
│ .sort((a, b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === │
│ "tree" ? -1 : 1)) │
│ .map(({ name, kind }) => { │
│ const full = dir ? `${dir}/${name}` : name; │
│ const [hash, date, ...subject] = git(path, ["log", "-1", "--format=%h|%as|%s │
│ ", ref, "--", full]) │
│ .trim() │
│ .split("|"); │
│ return { name: toGrid(name), path: full, kind, hash, date, subject: toGrid(s │
│ ubject.join("|")) }; │
│ }); │
│ } │
│ │
│ /** Every directory path in the repo, for tree page generation. */ │
│ export function repoDirs(files: string[]): string[] { │
│ const dirs = new Set<string>(); │
│ for (const f of files) { │
│ const segments = f.split("/"); │
│ for (let i = 1; i < segments.length; i++) { │
│ dirs.add(segments.slice(0, i).join("/")); │
│ } │
│ } │
│ return [...dirs].sort(); │
│ } │
│ │
│ /** Repo README content, if present. */ │
│ export function readReadme(path: string, ref = "HEAD"): string | null { │
│ for (const name of ["README.md", "readme.md", "README"]) { │
│ try { │
│ return git(path, ["show", `${ref}:${name}`]); │
│ } catch { │
│ continue; │
│ } │
│ } │
│ return null; │
│ } │
│ │
│ export interface Blob { │
│ text: string; │
│ binary: boolean; │
│ truncated: boolean; │
│ lines: number; │
│ } │
│ │
│ export function readBlob(path: string, file: string, ref = "HEAD"): Blob { │
│ const raw = execFileSync("git", ["-C", path, "show", `${ref}:${file}`], { │
│ maxBuffer: 32 * 1024 * 1024, │
│ }); │
│ if (raw.subarray(0, 8000).includes(0)) { │
│ return { text: "", binary: true, truncated: false, lines: 0 }; │
│ } │
│ const truncated = raw.length > MAX_BLOB; │
│ const text = raw.subarray(0, MAX_BLOB).toString("utf8"); │
│ return { │
│ text, │
│ binary: false, │
│ truncated, │
│ lines: text.split("\n").length, │
│ }; │
│ } │
│ │
│ export interface CommitFile { │
│ path: string; │
│ diff: string; │
│ } │
│ │
│ export interface Commit { │
│ hash: string; │
│ date: string; │
│ subject: string; │
│ body: string; │
│ files: CommitFile[]; │
│ /** Set instead of files when the patch is too large to render. */ │
│ stat?: string; │
│ } │
│ │
│ /** True when a numstat says the patch is too big for a page. Data │
│ * regen commits produced 5MB pages at looser thresholds - a human │
│ * reads neither a thousand changed lines nor fifty files. */ │
│ export function oversizedNumstat(numstat: string): boolean { │
│ const rows = numstat.split("\n").filter(Boolean); │
│ let lines = 0; │
│ for (const r of rows) { │
│ const [a, d] = r.split("\t"); │
│ lines += (parseInt(a, 10) || 0) + (parseInt(d, 10) || 0); │
│ } │
│ return rows.length > 50 || lines > 1000; │
│ } │
│ │
│ /** One commit with its patch split per file, github-style. Oversized │
│ * commits carry a diffstat summary instead of patches. */ │
│ export function readCommit(path: string, hash: string): Commit { │
│ const meta = git(path, ["show", "-s", "--format=%H|%as|%s|%b", hash]).trim(); │
│ const [full, date, subject, ...body] = meta.split("|"); │
│ const base = { │
│ hash: full, │
│ date, │
│ subject: toGrid(subject), │
│ body: toGrid(body.join("|").trim()), │
│ }; │
│ const numstat = git(path, ["show", "--numstat", "--format=", hash]); │
│ if (oversizedNumstat(numstat)) { │
│ const stat = git(path, ["show", "--stat", "--format=", hash]); │
│ return { ...base, files: [], stat: stat.length > MAX_BLOB ? stat.slice(0, MAX_ │
│ BLOB) : stat }; │
│ } │
│ const patch = git(path, ["show", "--patch", "--format=", hash]); │
│ const files: CommitFile[] = []; │
│ for (const chunk of patch.split(/^(?=diff --git )/m)) { │
│ if (!chunk.trim()) continue; │
│ const m = chunk.match(/^diff --git a\/.+? b\/(.+)$/m); │
│ const file = m ? m[1] : "?"; │
│ const MAX_FILE_DIFF = 64 * 1024; │
│ const trimmed = │
│ chunk.length > MAX_FILE_DIFF │
│ ? chunk.slice(0, MAX_FILE_DIFF) + "\n[diff truncated]" │
│ : chunk; │
│ files.push({ path: file, diff: trimmed }); │
│ } │
│ return { ...base, files }; │
│ } │
│ │
│ /** Frame label for a file: the uppercased extension, same convention │
│ * as the pipeline's code fences (label = uppercase of the token you │
│ * have). No language table to maintain. */ │
│ export function langOf(file: string): string { │
│ const name = file.split("/").pop() ?? file; │
│ const ext = name.includes(".") ? name.split(".").pop()! : ""; │
│ return ext ? ext.toUpperCase().slice(0, 8) : "TXT"; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ // Build-time git readers for the /git/ sect │
│ ion. Pages are generated │
│ // from real repositories at build time - st │
│ agit-style, but rendered │
│ // through the site's own components. Anythi │
│ ng that can land in a │
│ // frame passes toGrid() so history written │
│ before the og-ascii rule │
│ // can't fail the build. Every reader takes │
│ a ref so branch pages are │
│ // first-class; urls follow the github shape │
│ (tree/<branch>/<path>). │
│ import { execFileSync } from "node:child_pro │
│ cess"; │
│ import { existsSync } from "node:fs"; │
│ import { toGrid } from "./ascii"; │
│ import type { TimelineEntry } from "../compo │
│ nents/Timeline/Timeline"; │
│ │
│ export interface GitRepo { │
│ /** Url segment. */ │
│ slug: string; │
│ /** Display name. */ │
│ name: string; │
│ /** Local checkout or bare repo path, reso │
│ lved from ooknet-design/. */ │
│ path: string; │
│ desc: string; │
│ clone?: string; │
│ /** Path prefixes that get no blob/history │
│ pages - committed data │
│ * dumps would explode the page count. Li │
│ sted but unlinked. */ │
│ exclude?: string[]; │
│ } │
│ │
│ const MAX_BLOB = 200 * 1024; │
│ │
│ const git = (path: string, args: string[]): │
│ string => │
│ execFileSync("git", ["-C", path, ...args], │
│ { │
│ encoding: "utf8", │
│ maxBuffer: 32 * 1024 * 1024, │
│ }); │
│ │
│ export const repoAvailable = (r: GitRepo): b │
│ oolean => existsSync(r.path); │
│ │
│ /** True when a path sits under one of the r │
│ epo's excluded prefixes. */ │
│ export const excluded = (r: GitRepo, path: s │
│ tring): boolean => │
│ (r.exclude ?? []).some((p) => path === p | │
│ | path.startsWith(p + "/")); │
│ │
│ /** Local branches, default branch first. */ │
│ export function branches(path: string): stri │
│ ng[] { │
│ const head = defaultBranch(path); │
│ const all = git(path, ["for-each-ref", "re │
│ fs/heads", "--format=%(refname:short)"]) │
│ .split("\n") │
│ .filter(Boolean); │
│ return [head, ...all.filter((b) => b !== h │
│ ead)]; │
│ } │
│ │
│ export const defaultBranch = (path: string): │
│ string => │
│ git(path, ["symbolic-ref", "--short", "HEA │
│ D"]).trim(); │
│ │
│ export interface RepoInfo { │
│ branch: string; │
│ commits: number; │
│ lastHash: string; │
│ lastDate: string; │
│ lastSubject: string; │
│ } │
│ │
│ export function repoInfo(path: string, ref?: │
│ string): RepoInfo { │
│ const branch = ref ?? defaultBranch(path); │
│ const [lastHash, lastDate, ...subject] = g │
│ it(path, ["log", "-1", "--format=%h|%as|%s", │
│ branch]) │
│ .trim() │
│ .split("|"); │
│ return { │
│ branch, │
│ commits: parseInt(git(path, ["rev-list", │
│ "--count", branch]).trim(), 10), │
│ lastHash, │
│ lastDate, │
│ lastSubject: toGrid(subject.join("|")), │
│ }; │
│ } │
│ │
│ export function repoFiles(path: string, ref │
│ = "HEAD"): string[] { │
│ return git(path, ["ls-tree", "-r", "--name │
│ -only", ref]) │
│ .split("\n") │
│ .filter(Boolean); │
│ } │
│ │
│ export function repoLog(path: string, limit: │
│ number, base: string, ref = "HEAD"): Timeli │
│ neEntry[] { │
│ return git(path, ["log", `-${limit}`, "--f │
│ ormat=%h|%as|%s", ref]) │
│ .split("\n") │
│ .filter(Boolean) │
│ .map((line) => { │
│ const [hash, date, ...subject] = line. │
│ split("|"); │
│ return { │
│ date, │
│ title: toGrid(hash), │
│ desc: toGrid(subject.join("|")), │
│ href: `${base}commit/${hash}/`, │
│ }; │
│ }); │
│ } │
│ │
│ /** Commits touching one path - the file his │
│ tory page. */ │
│ export function pathLog( │
│ path: string, │
│ file: string, │
│ limit: number, │
│ base: string, │
│ ref = "HEAD", │
│ ): TimelineEntry[] { │
│ return git(path, ["log", `-${limit}`, "--f │
│ ormat=%h|%as|%s", ref, "--", file]) │
│ .split("\n") │
│ .filter(Boolean) │
│ .map((line) => { │
│ const [hash, date, ...subject] = line. │
│ split("|"); │
│ return { │
│ date, │
│ title: toGrid(hash), │
│ desc: toGrid(subject.join("|")), │
│ href: `${base}commit/${hash}/`, │
│ }; │
│ }); │
│ } │
│ │
│ export interface DirEntry { │
│ name: string; │
│ path: string; │
│ kind: "tree" | "blob"; │
│ /** Last commit touching this path. */ │
│ hash: string; │
│ date: string; │
│ subject: string; │
│ } │
│ │
│ /** Immediate children of a directory, githu │
│ b-style: dirs first, each │
│ * with the last commit that touched it. */ │
│ export function dirEntries(path: string, dir │
│ = "", ref = "HEAD"): DirEntry[] { │
│ const spec = dir ? `${ref}:${dir}` : ref; │
│ const rows = git(path, ["ls-tree", spec]) │
│ .split("\n") │
│ .filter(Boolean) │
│ .map((line) => { │
│ const [meta, name] = line.split("\t"); │
│ const kind = meta.split(" ")[1] as "tr │
│ ee" | "blob" | "commit"; │
│ return { name, kind }; │
│ }) │
│ .filter((e): e is { name: string; kind: │
│ "tree" | "blob" } => e.kind !== "commit"); │
│ return rows │
│ .sort((a, b) => (a.kind === b.kind ? a.n │
│ ame.localeCompare(b.name) : a.kind === "tree │
│ " ? -1 : 1)) │
│ .map(({ name, kind }) => { │
│ const full = dir ? `${dir}/${name}` : │
│ name; │
│ const [hash, date, ...subject] = git(p │
│ ath, ["log", "-1", "--format=%h|%as|%s", ref │
│ , "--", full]) │
│ .trim() │
│ .split("|"); │
│ return { name: toGrid(name), path: ful │
│ l, kind, hash, date, subject: toGrid(subject │
│ .join("|")) }; │
│ }); │
│ } │
│ │
│ /** Every directory path in the repo, for tr │
│ ee page generation. */ │
│ export function repoDirs(files: string[]): s │
│ tring[] { │
│ const dirs = new Set<string>(); │
│ for (const f of files) { │
│ const segments = f.split("/"); │
│ for (let i = 1; i < segments.length; i++ │
│ ) { │
│ dirs.add(segments.slice(0, i).join("/" │
│ )); │
│ } │
│ } │
│ return [...dirs].sort(); │
│ } │
│ │
│ /** Repo README content, if present. */ │
│ export function readReadme(path: string, ref │
│ = "HEAD"): string | null { │
│ for (const name of ["README.md", "readme.m │
│ d", "README"]) { │
│ try { │
│ return git(path, ["show", `${ref}:${na │
│ me}`]); │
│ } catch { │
│ continue; │
│ } │
│ } │
│ return null; │
│ } │
│ │
│ export interface Blob { │
│ text: string; │
│ binary: boolean; │
│ truncated: boolean; │
│ lines: number; │
│ } │
│ │
│ export function readBlob(path: string, file: │
│ string, ref = "HEAD"): Blob { │
│ const raw = execFileSync("git", ["-C", pat │
│ h, "show", `${ref}:${file}`], { │
│ maxBuffer: 32 * 1024 * 1024, │
│ }); │
│ if (raw.subarray(0, 8000).includes(0)) { │
│ return { text: "", binary: true, truncat │
│ ed: false, lines: 0 }; │
│ } │
│ const truncated = raw.length > MAX_BLOB; │
│ const text = raw.subarray(0, MAX_BLOB).toS │
│ tring("utf8"); │
│ return { │
│ text, │
│ binary: false, │
│ truncated, │
│ lines: text.split("\n").length, │
│ }; │
│ } │
│ │
│ export interface CommitFile { │
│ path: string; │
│ diff: string; │
│ } │
│ │
│ export interface Commit { │
│ hash: string; │
│ date: string; │
│ subject: string; │
│ body: string; │
│ files: CommitFile[]; │
│ /** Set instead of files when the patch is │
│ too large to render. */ │
│ stat?: string; │
│ } │
│ │
│ /** True when a numstat says the patch is to │
│ o big for a page. Data │
│ * regen commits produced 5MB pages at loos │
│ er thresholds - a human │
│ * reads neither a thousand changed lines n │
│ or fifty files. */ │
│ export function oversizedNumstat(numstat: st │
│ ring): boolean { │
│ const rows = numstat.split("\n").filter(Bo │
│ olean); │
│ let lines = 0; │
│ for (const r of rows) { │
│ const [a, d] = r.split("\t"); │
│ lines += (parseInt(a, 10) || 0) + (parse │
│ Int(d, 10) || 0); │
│ } │
│ return rows.length > 50 || lines > 1000; │
│ } │
│ │
│ /** One commit with its patch split per file │
│ , github-style. Oversized │
│ * commits carry a diffstat summary instead │
│ of patches. */ │
│ export function readCommit(path: string, has │
│ h: string): Commit { │
│ const meta = git(path, ["show", "-s", "--f │
│ ormat=%H|%as|%s|%b", hash]).trim(); │
│ const [full, date, subject, ...body] = met │
│ a.split("|"); │
│ const base = { │
│ hash: full, │
│ date, │
│ subject: toGrid(subject), │
│ body: toGrid(body.join("|").trim()), │
│ }; │
│ const numstat = git(path, ["show", "--nums │
│ tat", "--format=", hash]); │
│ if (oversizedNumstat(numstat)) { │
│ const stat = git(path, ["show", "--stat" │
│ , "--format=", hash]); │
│ return { ...base, files: [], stat: stat. │
│ length > MAX_BLOB ? stat.slice(0, MAX_BLOB) │
│ : stat }; │
│ } │
│ const patch = git(path, ["show", "--patch" │
│ , "--format=", hash]); │
│ const files: CommitFile[] = []; │
│ for (const chunk of patch.split(/^(?=diff │
│ --git )/m)) { │
│ if (!chunk.trim()) continue; │
│ const m = chunk.match(/^diff --git a\/.+ │
│ ? b\/(.+)$/m); │
│ const file = m ? m[1] : "?"; │
│ const MAX_FILE_DIFF = 64 * 1024; │
│ const trimmed = │
│ chunk.length > MAX_FILE_DIFF │
│ ? chunk.slice(0, MAX_FILE_DIFF) + "\ │
│ n[diff truncated]" │
│ : chunk; │
│ files.push({ path: file, diff: trimmed } │
│ ); │
│ } │
│ return { ...base, files }; │
│ } │
│ │
│ /** Frame label for a file: the uppercased e │
│ xtension, same convention │
│ * as the pipeline's code fences (label = u │
│ ppercase of the token you │
│ * have). No language table to maintain. */ │
│ export function langOf(file: string): string │
│ { │
│ const name = file.split("/").pop() ?? file │
│ ; │
│ const ext = name.includes(".") ? name.spli │
│ t(".").pop()! : ""; │
│ return ext ? ext.toUpperCase().slice(0, 8) │
│ : "TXT"; │
│ } │
└──────────────────────────────────────────────┘