| 'use strict'; | |
| // Generates scripts/last-update/data.json: a { "<repo-relative md path>": | |
| // "<ISO commit date>" } map of every built document's last update time, | |
| // derived from this repo's git history. | |
| // | |
| // docusaurus.config.js reads this map at build time (markdown.parseFrontMatter) | |
| // and injects it as each document's `last_update` front matter, so the build | |
| // never does a per-file git lookup. The 2-hourly Cron Deploy runs this script | |
| // itself (its checkout uses fetch-depth: 0 + filter: blob:none) and builds from | |
| // the result, so published dates track master. The committed data.json is the | |
| // local-build copy and the deploy's fallback; the "Refresh Docs Last-Update | |
| // Map" workflow (.github/workflows/refresh-docs-last-update.yml) keeps it fresh | |
| // via PR. | |
| // | |
| // Requires full git history to be accurate. In a shallow clone every file | |
| // resolves to the single HEAD commit, so run this only where the history is | |
| // available (locally, or the workflow's fetch-depth: 0 checkout). | |
| const { execFileSync } = require('child_process'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const REPO_ROOT = path.resolve(__dirname, '../..'); | |
| const OUTPUT = path.join(__dirname, 'data.json'); | |
| // Versions the site actually builds: docs/ (the "current"/Dev version) plus | |
| // the versions listed in versions.json. Other version-* dirs on disk (e.g. | |
| // version-1.2) are not built, so we don't spend map entries on them. | |
| const VERSIONS = require(path.join(REPO_ROOT, 'versions.json')); | |
| // Repo-relative directories whose Markdown we timestamp. These paths match | |
| // what path.relative(siteDir, absFilePath) yields at build time, so the map | |
| // keys line up with parseFrontMatter's lookup. zh-CN translations carry their | |
| // own git history, hence their own (correct) timestamps. | |
| function targetDirs() { | |
| const dirs = ['docs']; | |
| for (const v of VERSIONS) dirs.push(`versioned_docs/version-${v}`); | |
| dirs.push('community'); | |
| dirs.push('i18n/zh-CN/docusaurus-plugin-content-docs/current'); | |
| for (const v of VERSIONS) { | |
| dirs.push(`i18n/zh-CN/docusaurus-plugin-content-docs/version-${v}`); | |
| } | |
| dirs.push('i18n/zh-CN/docusaurus-plugin-content-docs-community/current'); | |
| return dirs.filter((d) => fs.existsSync(path.join(REPO_ROOT, d))); | |
| } | |
| // One pass over history: `git log` is newest-first, so the first date we see | |
| // for a path is its most recent commit. Merges are skipped (Doris lands PRs as | |
| // squashed single commits; --name-only shows nothing for merges anyway). | |
| function buildHistoryMap(dirs) { | |
| const out = execFileSync( | |
| 'git', | |
| [ | |
| '-c', 'core.quotePath=false', | |
| 'log', '--no-merges', '--name-only', '--format=__C__%cI', | |
| '--', ...dirs, | |
| ], | |
| { cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 512 * 1024 * 1024 }, | |
| ); | |
| const map = new Map(); | |
| let date = null; | |
| for (const line of out.split('\n')) { | |
| if (line.startsWith('__C__')) { | |
| date = line.slice(5); | |
| } else if (line && date && !map.has(line)) { | |
| map.set(line, date); | |
| } | |
| } | |
| return map; | |
| } | |
| function walkMarkdown(relDir) { | |
| const files = []; | |
| for (const entry of fs.readdirSync(path.join(REPO_ROOT, relDir), { withFileTypes: true })) { | |
| const relPath = `${relDir}/${entry.name}`; | |
| if (entry.isDirectory()) { | |
| files.push(...walkMarkdown(relPath)); | |
| } else if (/\.mdx?$/.test(entry.name)) { | |
| files.push(relPath); | |
| } | |
| } | |
| return files; | |
| } | |
| // Per-file dates need the full history; a shallow clone collapses every file | |
| // to the single HEAD commit. Fail loud rather than write a misleading map. | |
| function assertFullHistory() { | |
| let shallow; | |
| try { | |
| shallow = execFileSync('git', ['rev-parse', '--is-shallow-repository'], { | |
| cwd: REPO_ROOT, encoding: 'utf8', | |
| }).trim(); | |
| } catch (e) { | |
| console.error('last-update: not a git repository, or git is not installed.'); | |
| process.exit(1); | |
| } | |
| if (shallow === 'true') { | |
| console.error( | |
| 'last-update: shallow clone detected — every file would resolve to ' | |
| + 'the latest commit.\nRun `git fetch --unshallow` and retry.', | |
| ); | |
| process.exit(1); | |
| } | |
| } | |
| function main() { | |
| assertFullHistory(); | |
| const dirs = targetDirs(); | |
| const history = buildHistoryMap(dirs); | |
| const result = {}; | |
| let missing = 0; | |
| for (const dir of dirs) { | |
| for (const file of walkMarkdown(dir)) { | |
| const date = history.get(file); | |
| if (date) result[file] = date; | |
| else missing += 1; | |
| } | |
| } | |
| // Sorted keys keep the committed JSON stable so each refresh produces a | |
| // minimal diff (only the documents whose dates actually changed). | |
| const sorted = {}; | |
| for (const key of Object.keys(result).sort()) sorted[key] = result[key]; | |
| fs.writeFileSync(OUTPUT, `${JSON.stringify(sorted, null, 2)}\n`, 'utf8'); | |
| console.log( | |
| `last-update: wrote ${Object.keys(sorted).length} entries to ` | |
| + `${path.relative(REPO_ROOT, OUTPUT)}` | |
| + (missing ? ` (${missing} files had no git history, skipped)` : ''), | |
| ); | |
| } | |
| main(); |