@@ -2,13 +2,106 @@ import { spawn, sync } from 'cross-spawn'
22import _debug from 'debug'
33import fs from 'node:fs'
44import path from 'node:path'
5+ import { Transform , type TransformCallback } from 'node:stream'
56import { slash } from '../shared'
67
78const debug = _debug ( 'vitepress:git' )
89const cache = new Map < string , number > ( )
910
1011const RS = 0x1e
1112const NUL = 0x00
13+ const LF = 0x0a
14+
15+ interface GitLogRecord {
16+ ts : number
17+ files : string [ ]
18+ }
19+
20+ type State = 'READ_TS' | 'READ_FILE'
21+
22+ class GitLogParser extends Transform {
23+ #state: State = 'READ_TS'
24+ #tsBytes: number [ ] = [ ]
25+ #fileBytes: number [ ] = [ ]
26+ #files: string [ ] = [ ]
27+
28+ constructor ( ) {
29+ super ( { readableObjectMode : true } )
30+ }
31+
32+ override _transform (
33+ chunk : Buffer ,
34+ _enc : BufferEncoding ,
35+ cb : TransformCallback
36+ ) : void {
37+ try {
38+ for ( let i = 0 ; i < chunk . length ; i ++ ) {
39+ const b = chunk [ i ] === LF ? NUL : chunk [ i ] // treat LF as NUL
40+
41+ switch ( this . #state) {
42+ case 'READ_TS' : {
43+ if ( b === RS ) {
44+ // ignore
45+ } else if ( b === NUL ) {
46+ this . #state = 'READ_FILE'
47+ } else {
48+ this . #tsBytes. push ( b )
49+ }
50+ break
51+ }
52+
53+ case 'READ_FILE' : {
54+ if ( b === RS ) {
55+ this . #emitRecord( )
56+ } else if ( b === NUL ) {
57+ if ( this . #fileBytes. length > 0 ) {
58+ this . #files. push ( Buffer . from ( this . #fileBytes) . toString ( 'utf8' ) )
59+ this . #fileBytes. length = 0
60+ }
61+ } else {
62+ this . #fileBytes. push ( b )
63+ }
64+ break
65+ }
66+ }
67+ }
68+
69+ cb ( )
70+ } catch ( err ) {
71+ cb ( err as Error )
72+ }
73+ }
74+
75+ override _flush ( cb : TransformCallback ) : void {
76+ try {
77+ if ( this . #state === 'READ_FILE' ) {
78+ if ( this . #fileBytes. length > 0 ) {
79+ throw new Error ( 'GitLogParser: unexpected EOF while reading filename' )
80+ } else {
81+ this . #emitRecord( )
82+ }
83+ }
84+
85+ cb ( )
86+ } catch ( err ) {
87+ cb ( err as Error )
88+ }
89+ }
90+
91+ #emitRecord( ) : void {
92+ const ts = Buffer . from ( this . #tsBytes) . toString ( 'utf8' )
93+ const rec : GitLogRecord = {
94+ ts : Number . parseInt ( ts , 10 ) * 1000 ,
95+ files : this . #files. slice ( )
96+ }
97+ if ( rec . ts > 0 && rec . files . length > 0 ) this . push ( rec )
98+
99+ this . #tsBytes. length = 0
100+ this . #fileBytes. length = 0
101+ this . #files. length = 0
102+ this . #state = 'READ_TS'
103+ }
104+ }
12105
13106export async function cacheAllGitTimestamps (
14107 root : string ,
@@ -28,64 +121,19 @@ export async function cacheAllGitTimestamps(
28121 ]
29122
30123 return new Promise ( ( resolve , reject ) => {
31- const out = new Map < string , number > ( )
124+ cache . clear ( )
32125 const child = spawn ( 'git' , args , { cwd : root } )
33126
34- let buf = Buffer . alloc ( 0 )
35- child . stdout . on ( 'data' , ( chunk : Buffer < ArrayBuffer > ) => {
36- buf = buf . length ? Buffer . concat ( [ buf , chunk ] ) : chunk
37-
38- let scanFrom = 0
39- let ts = 0
40-
41- while ( true ) {
42- if ( ts === 0 ) {
43- const rs = buf . indexOf ( RS , scanFrom )
44- if ( rs === - 1 ) break
45- scanFrom = rs + 1
46-
47- const nul = buf . indexOf ( NUL , scanFrom )
48- if ( nul === - 1 ) break
49- scanFrom = nul + 2 // skip LF after NUL
50-
51- const tsSec = buf . toString ( 'utf8' , rs + 1 , nul )
52- ts = Number . parseInt ( tsSec , 10 ) * 1000
127+ child . stdout
128+ . pipe ( new GitLogParser ( ) )
129+ . on ( 'data' , ( rec : GitLogRecord ) => {
130+ for ( const file of rec . files ) {
131+ const slashed = slash ( path . resolve ( gitRoot , file ) )
132+ if ( ! cache . has ( slashed ) ) cache . set ( slashed , rec . ts )
53133 }
54-
55- let nextNul
56- while ( true ) {
57- nextNul = buf . indexOf ( NUL , scanFrom )
58- if ( nextNul === - 1 ) break
59-
60- // double NUL, move to next record
61- if ( nextNul === scanFrom ) {
62- scanFrom += 1
63- ts = 0
64- break
65- }
66-
67- const file = buf . toString ( 'utf8' , scanFrom , nextNul )
68- if ( file && ! out . has ( file ) ) out . set ( file , ts )
69- scanFrom = nextNul + 1
70- }
71-
72- if ( nextNul === - 1 ) break
73- }
74-
75- if ( scanFrom > 0 ) buf = buf . subarray ( scanFrom )
76- } )
77-
78- child . on ( 'close' , async ( ) => {
79- cache . clear ( )
80-
81- for ( const [ file , ts ] of out ) {
82- const abs = path . resolve ( gitRoot , file )
83- if ( fs . existsSync ( abs ) ) cache . set ( slash ( abs ) , ts )
84- }
85-
86- out . clear ( )
87- resolve ( )
88- } )
134+ } )
135+ . on ( 'error' , reject )
136+ . on ( 'end' , resolve )
89137
90138 child . on ( 'error' , reject )
91139 } )
@@ -103,7 +151,7 @@ export async function getGitTimestamp(file: string): Promise<number> {
103151 return new Promise ( ( resolve , reject ) => {
104152 const child = spawn (
105153 'git' ,
106- [ 'log' , '-1' , '--pretty=%at' , path . basename ( file ) ] ,
154+ [ 'log' , '-1' , '--pretty=%at' , '--' , path . basename ( file ) ] ,
107155 { cwd : path . dirname ( file ) }
108156 )
109157
0 commit comments