perf: make a single git call for timestamps instead of calling it for each file (#4958)

main
Divyansh Singh 22 hours ago committed by GitHub
parent 6d7422f8fa
commit 6dfcdd3fe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -9,6 +9,7 @@ import {
type NewsItem type NewsItem
} from 'sitemap' } from 'sitemap'
import type { SiteConfig } from '../config' import type { SiteConfig } from '../config'
import { slash } from '../shared'
import { getGitTimestamp } from '../utils/getGitTimestamp' import { getGitTimestamp } from '../utils/getGitTimestamp'
import { task } from '../utils/task' import { task } from '../utils/task'
@ -29,7 +30,7 @@ export async function generateSitemap(siteConfig: SiteConfig) {
if (data.lastUpdated === false) return undefined if (data.lastUpdated === false) return undefined
if (data.lastUpdated instanceof Date) return +data.lastUpdated if (data.lastUpdated instanceof Date) return +data.lastUpdated
return (await getGitTimestamp(file)) || undefined return (await getGitTimestamp(slash(file))) || undefined
} }
await task('generating sitemap', async () => { await task('generating sitemap', async () => {

@ -31,6 +31,7 @@ import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin' import { webFontsPlugin } from './plugins/webFontsPlugin'
import { slash, type PageDataPayload } from './shared' import { slash, type PageDataPayload } from './shared'
import { deserializeFunctions, serializeFunctions } from './utils/fnSerialize' import { deserializeFunctions, serializeFunctions } from './utils/fnSerialize'
import { cacheAllGitTimestamps } from './utils/getGitTimestamp'
declare module 'vite' { declare module 'vite' {
interface UserConfig { interface UserConfig {
@ -113,6 +114,8 @@ export async function createVitePressPlugin(
async configResolved(resolvedConfig) { async configResolved(resolvedConfig) {
config = resolvedConfig config = resolvedConfig
// pre-resolve git timestamps
if (lastUpdated) await cacheAllGitTimestamps(srcDir)
markdownToVue = await createMarkdownToVueRenderFn( markdownToVue = await createMarkdownToVueRenderFn(
srcDir, srcDir,
markdown, markdown,

@ -1,29 +1,121 @@
import { spawn } from 'cross-spawn' import { spawn, sync } from 'cross-spawn'
import fs from 'fs-extra' import _debug from 'debug'
import { basename, dirname } from 'node:path' import fs from 'node:fs'
import path from 'node:path'
import { slash } from '../shared'
const debug = _debug('vitepress:git')
const cache = new Map<string, number>() const cache = new Map<string, number>()
export function getGitTimestamp(file: string) { const RS = 0x1e
const NUL = 0x00
export async function cacheAllGitTimestamps(
root: string,
pathspec: string[] = ['*.md']
): Promise<void> {
const cp = sync('git', ['rev-parse', '--show-toplevel'], { cwd: root })
if (cp.error) throw cp.error
const gitRoot = cp.stdout.toString('utf8').trim()
const args = [
'log',
'--pretty=format:%x1e%at%x00', // RS + epoch + NUL
'--name-only',
'-z',
'--',
...pathspec
]
return new Promise((resolve, reject) => {
const out = new Map<string, number>()
const child = spawn('git', args, { cwd: root })
let buf = Buffer.alloc(0)
child.stdout.on('data', (chunk: Buffer<ArrayBuffer>) => {
buf = buf.length ? Buffer.concat([buf, chunk]) : chunk
let scanFrom = 0
let ts = 0
while (true) {
if (ts === 0) {
const rs = buf.indexOf(RS, scanFrom)
if (rs === -1) break
scanFrom = rs + 1
const nul = buf.indexOf(NUL, scanFrom)
if (nul === -1) break
scanFrom = nul + 2 // skip LF after NUL
const tsSec = buf.toString('utf8', rs + 1, nul)
ts = Number.parseInt(tsSec, 10) * 1000
}
let nextNul
while (true) {
nextNul = buf.indexOf(NUL, scanFrom)
if (nextNul === -1) break
// double NUL, move to next record
if (nextNul === scanFrom) {
scanFrom += 1
ts = 0
break
}
const file = buf.toString('utf8', scanFrom, nextNul)
if (file && !out.has(file)) out.set(file, ts)
scanFrom = nextNul + 1
}
if (nextNul === -1) break
}
if (scanFrom > 0) buf = buf.subarray(scanFrom)
})
child.on('close', async () => {
cache.clear()
for (const [file, ts] of out) {
const abs = path.resolve(gitRoot, file)
if (fs.existsSync(abs)) cache.set(slash(abs), ts)
}
out.clear()
resolve()
})
child.on('error', reject)
})
}
export async function getGitTimestamp(file: string): Promise<number> {
const cached = cache.get(file) const cached = cache.get(file)
if (cached) return cached if (cached) return cached
// most likely will never happen except for recently added files in dev
debug(`[cache miss] ${file}`)
if (!fs.existsSync(file)) return 0 if (!fs.existsSync(file)) return 0
return new Promise<number>((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn( const child = spawn(
'git', 'git',
['log', '-1', '--pretty="%ai"', basename(file)], ['log', '-1', '--pretty=%at', path.basename(file)],
{ cwd: dirname(file) } { cwd: path.dirname(file) }
) )
let output = '' let output = ''
child.stdout.on('data', (d) => (output += String(d))) child.stdout.on('data', (d) => (output += String(d)))
child.on('close', () => { child.on('close', () => {
const timestamp = +new Date(output) const ts = Number.parseInt(output.trim(), 10) * 1000
cache.set(file, timestamp) if (!(ts > 0)) return resolve(0)
resolve(timestamp)
cache.set(file, ts)
resolve(ts)
}) })
child.on('error', reject) child.on('error', reject)

Loading…
Cancel
Save