Skip to content

Commit a4c107c

Browse files
committed
Updated version of git repositoryMap
1 parent d4ca7b2 commit a4c107c

2 files changed

Lines changed: 55 additions & 137 deletions

File tree

docs/.vitepress/config/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Plugin } from 'vite'
2-
import type { DefaultTheme, SiteConfig } from 'vitepress'
2+
import { DefaultTheme, SiteConfig } from 'vitepress'
33
import { cacheAllGitTimestamps, saveCache } from '../utils/getGitTimestamp'
44

55

Lines changed: 54 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { execSync, spawn, spawnSync } from 'node:child_process'
22
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
33
import { resolve } from 'node:path'
4-
import { Transform, type TransformCallback } from 'node:stream'
54
import { 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

12335
export 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

16950
export 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

Comments
 (0)