Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/node/build/generateSitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type NewsItem
} from 'sitemap'
import type { SiteConfig } from '../config'
import { slash } from '../shared'
import { getGitTimestamp } from '../utils/getGitTimestamp'
import { task } from '../utils/task'

Expand All @@ -29,7 +30,7 @@ export async function generateSitemap(siteConfig: SiteConfig) {
if (data.lastUpdated === false) return undefined
if (data.lastUpdated instanceof Date) return +data.lastUpdated

return (await getGitTimestamp(file)) || undefined
return (await getGitTimestamp(slash(file))) || undefined
}

await task('generating sitemap', async () => {
Expand Down
3 changes: 3 additions & 0 deletions src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin'
import { slash, type PageDataPayload } from './shared'
import { deserializeFunctions, serializeFunctions } from './utils/fnSerialize'
import { cacheAllGitTimestamps } from './utils/getGitTimestamp'

declare module 'vite' {
interface UserConfig {
Expand Down Expand Up @@ -113,6 +114,8 @@ export async function createVitePressPlugin(

async configResolved(resolvedConfig) {
config = resolvedConfig
// pre-resolve git timestamps
if (lastUpdated) await cacheAllGitTimestamps(srcDir)
markdownToVue = await createMarkdownToVueRenderFn(
srcDir,
markdown,
Expand Down
112 changes: 102 additions & 10 deletions src/node/utils/getGitTimestamp.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,121 @@
import { spawn } from 'cross-spawn'
import fs from 'fs-extra'
import { basename, dirname } from 'node:path'
import { spawn, sync } from 'cross-spawn'
import _debug from 'debug'
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>()

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)
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

return new Promise<number>((resolve, reject) => {
return new Promise((resolve, reject) => {
const child = spawn(
'git',
['log', '-1', '--pretty="%ai"', basename(file)],
{ cwd: dirname(file) }
['log', '-1', '--pretty=%at', path.basename(file)],
{ cwd: path.dirname(file) }
)

let output = ''
child.stdout.on('data', (d) => (output += String(d)))

child.on('close', () => {
const timestamp = +new Date(output)
cache.set(file, timestamp)
resolve(timestamp)
const ts = Number.parseInt(output.trim(), 10) * 1000
if (!(ts > 0)) return resolve(0)

cache.set(file, ts)
resolve(ts)
})

child.on('error', reject)
Expand Down
Loading