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