diff --git a/scripts/generate-last-updated.mjs b/scripts/generate-last-updated.mjs index 4962a6d35..1425f554e 100644 --- a/scripts/generate-last-updated.mjs +++ b/scripts/generate-last-updated.mjs @@ -13,7 +13,7 @@ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' -import { getGitLastModified } from './git-dates.mjs' +import { buildGitDateMap } from './git-dates.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const ROOT = path.join(__dirname, '..') @@ -45,9 +45,10 @@ const entries = findMdxRoutes(PAGES_DIR) .filter((r) => r.route !== '/' && r.route !== '') .sort((a, b) => a.route.localeCompare(b.route)) +const gitDates = buildGitDateMap() const map = {} for (const { route, filePath } of entries) { - const date = getGitLastModified(filePath) + const date = gitDates.get(path.relative(ROOT, filePath)) if (date) map[route] = date } diff --git a/scripts/generate-sitemap.mjs b/scripts/generate-sitemap.mjs index daa81af01..b9c0615ab 100644 --- a/scripts/generate-sitemap.mjs +++ b/scripts/generate-sitemap.mjs @@ -10,7 +10,7 @@ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' -import { getGitLastModified } from './git-dates.mjs' +import { buildGitDateMap } from './git-dates.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const ROOT = path.join(__dirname, '..') @@ -56,10 +56,11 @@ function escapeXml(s) { .replace(/'/g, ''') } +const gitDates = buildGitDateMap() const entries = findMdxFiles(PAGES_DIR) .map(({ route, filePath }) => ({ url: toPublicUrl(route), - lastmod: getGitLastModified(filePath), + lastmod: gitDates.get(path.relative(ROOT, filePath)) ?? null, })) .sort((a, b) => a.url.localeCompare(b.url)) diff --git a/scripts/git-dates.mjs b/scripts/git-dates.mjs index 423967385..4912d3903 100644 --- a/scripts/git-dates.mjs +++ b/scripts/git-dates.mjs @@ -3,6 +3,9 @@ import { execSync } from 'child_process' /** * Get the last modified date for a file from git history. * Returns YYYY-MM-DD or null if the file is not tracked / git is unavailable. + * + * Prefer buildGitDateMap() when you need dates for many files — this spawns a + * git process per call, which is ~300x slower across a full page tree. */ export function getGitLastModified(filePath) { try { @@ -16,3 +19,47 @@ export function getGitLastModified(filePath) { return null } } + +let _dateMapCache + +/** + * Build a map of repo-relative path -> last commit date (YYYY-MM-DD) for every + * file in history, in a SINGLE git process, instead of one `git log` per file. + * Memoised for the lifetime of the process. + * + * git log is reverse-chronological, so the first date seen for a path is its + * most recent commit — the same value `git log -1 -- ` returns. Paths are + * repo-relative with forward slashes, matching path.relative(repoRoot, file). + * + * Returns an empty map if git is unavailable (e.g. inside the Docker image, + * which has no git binary); callers then fall back to no date, exactly as the + * per-file getGitLastModified() did. + */ +export function buildGitDateMap() { + if (_dateMapCache) return _dateMapCache + const map = new Map() + try { + // core.quotePath=false keeps non-ASCII paths unquoted so line parsing is safe. + const out = execSync( + 'git -c core.quotePath=false log --format=%cI --name-only', + { + encoding: 'utf-8', + maxBuffer: 128 * 1024 * 1024, + stdio: ['pipe', 'pipe', 'ignore'], + } + ) + let currentDate = null + for (const line of out.split('\n')) { + if (line === '') continue + if (/^\d{4}-\d{2}-\d{2}T/.test(line)) { + currentDate = line.slice(0, 10) + continue + } + if (currentDate && !map.has(line)) map.set(line, currentDate) + } + } catch { + // git unavailable — return the empty map, callers fall back to null. + } + _dateMapCache = map + return map +}