11import { execSync , spawn , spawnSync } from 'node:child_process'
22import { existsSync , mkdirSync , readFileSync , writeFileSync } from 'node:fs'
33import { resolve } from 'node:path'
4- import { Transform , type TransformCallback } from 'node:stream'
54import { basename , dirname } from 'path'
65
76
@@ -32,138 +31,20 @@ export const saveCache = (cacheDir: string) => {
3231 )
3332}
3433
35- const RS = 0x1e
36- const NUL = 0x00
37- const LF = 0x0a
38-
39- interface GitLogRecord {
40- ts : number
41- files : string [ ]
42- }
43-
44- type State = 'READ_TS' | 'READ_FILE'
45-
46- class GitLogParser extends Transform {
47- private state : State = 'READ_TS'
48- private tsBytes : number [ ] = [ ]
49- private fileBytes : number [ ] = [ ]
50- private files : string [ ] = [ ]
51-
52- constructor ( ) {
53- super ( { readableObjectMode : true } )
54- }
55-
56- override _transform ( chunk : Buffer , _enc : BufferEncoding , cb : TransformCallback ) : void {
57- try {
58- for ( let i = 0 ; i < chunk . length ; i ++ ) {
59- const b = chunk [ i ] === LF ? NUL : chunk [ i ] // treat LF as NUL
60-
61- switch ( this . state ) {
62- case 'READ_TS' : {
63- if ( b === RS ) {
64- // ignore
65- } else if ( b === NUL ) {
66- this . state = 'READ_FILE'
67- } else {
68- this . tsBytes . push ( b )
69- }
70- break
71- }
72-
73- case 'READ_FILE' : {
74- if ( b === RS ) {
75- this . emitRecord ( )
76- } else if ( b === NUL ) {
77- if ( this . fileBytes . length > 0 ) {
78- this . files . push ( Buffer . from ( this . fileBytes ) . toString ( 'utf8' ) )
79- this . fileBytes . length = 0
80- }
81- } else {
82- this . fileBytes . push ( b )
83- }
84- break
85- }
86- }
87- }
88-
89- cb ( )
90- } catch ( err ) {
91- cb ( err as Error )
92- }
93- }
94-
95- override _flush ( cb : TransformCallback ) : void {
96- if ( this . state === 'READ_FILE' ) {
97- if ( this . fileBytes . length > 0 ) {
98- return cb ( new Error ( 'GitLogParser: unexpected EOF while reading filename' ) )
99- } else {
100- this . emitRecord ( )
101- }
102- }
103-
104- cb ( )
105- }
106-
107- private emitRecord ( ) : void {
108- const ts = Buffer . from ( this . tsBytes ) . toString ( 'utf8' )
109- const rec : GitLogRecord = {
110- ts : Number . parseInt ( ts , 10 ) * 1000 ,
111- files : this . files . slice ( ) ,
112- }
113- if ( rec . ts > 0 && rec . files . length > 0 ) this . push ( rec )
114-
115- this . tsBytes . length = 0
116- this . fileBytes . length = 0
117- this . files . length = 0
118- this . state = 'READ_TS'
119- }
120- }
121-
12234
12335export const slash = ( p : string ) : string => p . replace ( / \\ / g, '/' )
12436
125-
126- export async function cacheAllGitTimestamps (
127- root : string ,
128- spec : string [ ] = [ '*.md' ] ,
129- ) : Promise < Map < string , GitFileTimes > > {
130- const cp = spawnSync ( 'git' , [ 'rev-parse' , '--show-toplevel' ] , { cwd : root } )
131- if ( cp . error ) throw cp . error
132- const gitRoot = cp . stdout . toString ( 'utf8' ) . trim ( )
133-
134- const args = [
135- 'log' ,
136- '--pretty=format:%x1e%at%x00' , // RS + epoch + NUL
137- '--name-only' ,
138- '-z' ,
139- '--' ,
140- ...spec ,
141- ]
142-
143- return new Promise ( ( res , rej ) => {
144- cache . clear ( )
145- const child = spawn ( 'git' , args , { cwd : root } )
146-
147- child . stdout
148- . pipe ( new GitLogParser ( ) )
149- . on ( 'data' , ( rec : GitLogRecord ) => {
150- for ( const file of rec . files ) {
151- const slashed = slash ( resolve ( gitRoot , file ) )
152- const cached = cache . get ( slashed )
153-
154- if ( ! cached ) {
155- cache . set ( slashed , { createdDate : rec . ts , updatedDate : rec . ts } )
156- } else {
157- cached . updatedDate = rec . ts
158- }
159- }
160- } )
161- . on ( 'error' , rej )
162- . on ( 'end' , ( ) => res ( cache ) )
163-
164- child . on ( 'error' , rej )
165- } )
166- }
37+ // const inner = (file: string) => {
38+ // const [ cwd, filename ] = [ dirname(file), basename(file) ]
39+ // const [ createdDate, updatedDate ] = [
40+ // `git log -1 --pretty="%at" --diff-filter=A --follow -- "${ filename }"`,
41+ // `git log -1 --pretty="%at" -- "${ filename }"`,
42+ // ]
43+ // .map(cmd => execSync(cmd, { cwd, encoding: 'utf8' }))
44+ // .map(x => Number(x) * 1000)
45+ //
46+ // return { createdDate, updatedDate }
47+ // }
16748
16849
16950export async function getGitTimestamp ( file : string ) : Promise < GitFileTimes > {
@@ -176,13 +57,10 @@ export async function getGitTimestamp(file: string): Promise<GitFileTimes> {
17657
17758 const inner = ( file : string ) => {
17859 const [ cwd , filename ] = [ dirname ( file ) , basename ( file ) ]
179- const [ createdDate , updatedDate ] = [
180- `git log -1 --pretty="%at" --diff-filter=A --follow -- "${ filename } "` ,
181- `git log -1 --pretty="%at" -- "${ filename } "` ,
182- ]
183- . map ( cmd => execSync ( cmd , { cwd, encoding : 'utf8' } ) )
184- . map ( x => Number ( x ) * 1000 )
60+ const res = execSync ( `git log --follow --pretty="%at" -- "${ filename } "` , { cwd, encoding : 'utf8' } )
61+ const times = res . trim ( ) . split ( '\n' ) . map ( t => Number ( t ) * 1000 ) . reverse ( )
18562
63+ const [ createdDate , updatedDate ] = [ times [ 0 ] , times [ times . length - 1 ] ]
18664 return { createdDate, updatedDate }
18765 }
18866
@@ -191,3 +69,43 @@ export async function getGitTimestamp(file: string): Promise<GitFileTimes> {
19169
19270 return res
19371}
72+
73+ export async function cacheAllGitTimestamps ( root : string , patterns : string [ ] = [ '*.md' ] ) {
74+ // git 根目录
75+ const cp = spawnSync ( 'git' , [ 'rev-parse' , '--show-toplevel' ] , { encoding : 'utf8' } )
76+ if ( cp . error ) throw cp . error
77+ const gitRoot = cp . stdout . trim ( )
78+
79+ // 文件列表
80+ const args = [ 'ls-files' , ...patterns ]
81+ const { stdout : lsOut } = spawnSync ( 'git' , args , { cwd : root , encoding : 'utf8' } )
82+ const files = lsOut . split ( '\n' ) . filter ( Boolean )
83+
84+ const asyncFn = ( file : string ) => {
85+ return new Promise < GitFileTimes > ( ( resolve , reject ) => {
86+ const [ cwd , filename ] = [ dirname ( file ) , basename ( file ) ]
87+ const child = spawn (
88+ 'git' ,
89+ [ 'log' , '--follow' , '--pretty=%at' , '--' , filename ] ,
90+ { cwd } ,
91+ )
92+
93+ child . stdout
94+ . on ( 'data' , ( data : Buffer ) => {
95+ const times = data . toString ( 'utf8' ) . trim ( ) . split ( '\n' ) . map ( t => Number ( t ) * 1000 ) . reverse ( )
96+ const [ createdDate , updatedDate ] = [ times [ 0 ] , times [ times . length - 1 ] ]
97+ resolve ( { createdDate, updatedDate } )
98+ } )
99+ . on ( 'error' , reject )
100+ } )
101+ }
102+
103+ return Promise . all ( files . map ( async file => {
104+ const slashed = slash ( resolve ( root , file ) )
105+ const cached = cache . get ( slashed )
106+ if ( ! cached ) {
107+ const res = await asyncFn ( slashed )
108+ cache . set ( slashed , res )
109+ }
110+ } ) )
111+ }
0 commit comments