wukong-gitlog-cli 1.0.33 → 1.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/cli.mjs +15 -4
- package/src/git.mjs +176 -3
- package/src/renderAuthorMapText.mjs +21 -0
- package/src/text.mjs +4 -2
- package/src/utils/authorNormalizer.mjs +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.0.34](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.33...v1.0.34) (2025-12-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* 🐛 eslint error ([3d48e25](https://github.com/tomatobybike/wukong-gitlog-cli/commit/3d48e25eb1217005ac2a525b54e307ca328b85e7))
|
|
11
|
+
|
|
5
12
|
### [1.0.33](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.32...v1.0.33) (2025-12-12)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
exportExcelAuthorChangeStats,
|
|
15
15
|
exportExcelPerPeriodSheets
|
|
16
16
|
} from './excel.mjs'
|
|
17
|
-
import {
|
|
17
|
+
import { getGitLogsFast } from './git.mjs'
|
|
18
18
|
import { renderAuthorChangesJson } from './json.mjs'
|
|
19
19
|
import {
|
|
20
20
|
analyzeOvertime,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
renderOvertimeTab,
|
|
23
23
|
renderOvertimeText
|
|
24
24
|
} from './overtime.mjs'
|
|
25
|
+
import { renderAuthorMapText } from './renderAuthorMapText.mjs'
|
|
25
26
|
import { startServer } from './server.mjs'
|
|
26
27
|
import { renderChangedLinesText, renderText } from './text.mjs'
|
|
27
28
|
import { checkUpdateWithPatch } from './utils/checkUpdate.mjs'
|
|
@@ -203,8 +204,9 @@ const main = async () => {
|
|
|
203
204
|
|
|
204
205
|
const spinner = ora('Loading...').start()
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
const gitCommits = await getGitLogsFast(opts)
|
|
208
|
+
let { commits: records } = gitCommits
|
|
209
|
+
const { authorMap } = gitCommits
|
|
208
210
|
// compute output directory root if user provided one or wants parent
|
|
209
211
|
|
|
210
212
|
// --- Gerrit 地址处理(若提供) ---
|
|
@@ -319,6 +321,11 @@ const main = async () => {
|
|
|
319
321
|
// Output to console
|
|
320
322
|
console.log('\n--- Overtime analysis ---\n')
|
|
321
323
|
console.log(renderOvertimeText(stats))
|
|
324
|
+
|
|
325
|
+
const authorMapText = renderAuthorMapText(authorMap)
|
|
326
|
+
console.log('\n Developers:\n', authorMapText, '\n')
|
|
327
|
+
writeTextFile(outputFilePath('authors.text', outDir), authorMapText)
|
|
328
|
+
|
|
322
329
|
// if user requested json format, write stats to file
|
|
323
330
|
if (opts.json || opts.format === 'json') {
|
|
324
331
|
const file = opts.out || 'overtime.json'
|
|
@@ -352,7 +359,10 @@ const main = async () => {
|
|
|
352
359
|
const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
|
|
353
360
|
writeTextFile(dataCommitsFile, commitsModule)
|
|
354
361
|
|
|
355
|
-
const dataCommitsChangedFile = outputFilePath(
|
|
362
|
+
const dataCommitsChangedFile = outputFilePath(
|
|
363
|
+
'data/author-changes.mjs',
|
|
364
|
+
outDir
|
|
365
|
+
)
|
|
356
366
|
const jsonText = renderAuthorChangesJson(records)
|
|
357
367
|
|
|
358
368
|
const commitsChangedModule = `export default ${JSON.stringify(jsonText, null, 2)};\n`
|
|
@@ -802,6 +812,7 @@ const main = async () => {
|
|
|
802
812
|
outputFilePath('author-changes.text', outDir),
|
|
803
813
|
renderChangedLinesText(records)
|
|
804
814
|
)
|
|
815
|
+
|
|
805
816
|
console.log(text)
|
|
806
817
|
console.log(chalk.green(`文本已导出: ${filepath}`))
|
|
807
818
|
spinner.succeed('Done')
|
package/src/git.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { $ } from 'zx'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { createAuthorNormalizer } from './utils/authorNormalizer.mjs'
|
|
4
|
+
|
|
5
|
+
const normalizer = createAuthorNormalizer()
|
|
6
|
+
|
|
7
|
+
export async function getGitLogsSlow(opts) {
|
|
4
8
|
const { author, email, since, until, limit, merges } = opts
|
|
5
9
|
|
|
6
10
|
const pretty = '%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%B%x1e'
|
|
@@ -31,7 +35,8 @@ export async function getGitLogs(opts) {
|
|
|
31
35
|
|
|
32
36
|
return {
|
|
33
37
|
hash,
|
|
34
|
-
author: authorName,
|
|
38
|
+
author: normalizer.getAuthor(authorName, emailAddr),
|
|
39
|
+
originalAuthor: authorName,
|
|
35
40
|
email: emailAddr,
|
|
36
41
|
date,
|
|
37
42
|
message: subject,
|
|
@@ -43,7 +48,8 @@ export async function getGitLogs(opts) {
|
|
|
43
48
|
// === 新增:为每个 commit 计算代码增量 ===
|
|
44
49
|
for (const c of commits) {
|
|
45
50
|
try {
|
|
46
|
-
const { stdout: diffOut } =
|
|
51
|
+
const { stdout: diffOut } =
|
|
52
|
+
await $`git show --numstat --format= ${c.hash}`.quiet()
|
|
47
53
|
// numstat 格式: "12 5 path/file.js"
|
|
48
54
|
const lines = diffOut
|
|
49
55
|
.split('\n')
|
|
@@ -72,3 +78,170 @@ export async function getGitLogs(opts) {
|
|
|
72
78
|
|
|
73
79
|
return commits
|
|
74
80
|
}
|
|
81
|
+
|
|
82
|
+
export async function getGitLogsQuick(opts) {
|
|
83
|
+
const { author, email, since, until, limit, merges } = opts
|
|
84
|
+
|
|
85
|
+
const pretty =
|
|
86
|
+
`${[
|
|
87
|
+
'%H', // hash
|
|
88
|
+
'%an', // author name
|
|
89
|
+
'%ae', // email
|
|
90
|
+
'%ad', // date
|
|
91
|
+
'%s', // subject
|
|
92
|
+
'%B' // body
|
|
93
|
+
].join('%x1f') }%x1e`
|
|
94
|
+
|
|
95
|
+
const args = [
|
|
96
|
+
'log',
|
|
97
|
+
`--pretty=format:${pretty}`,
|
|
98
|
+
'--date=iso-local',
|
|
99
|
+
'--numstat'
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
if (author) args.push(`--author=${author}`)
|
|
103
|
+
if (email) args.push(`--author=${email}`)
|
|
104
|
+
if (since) args.push(`--since=${since}`)
|
|
105
|
+
if (until) args.push(`--until=${until}`)
|
|
106
|
+
if (merges === false) args.push(`--no-merges`)
|
|
107
|
+
if (limit) args.push('-n', `${limit}`)
|
|
108
|
+
|
|
109
|
+
const { stdout } = await $`git ${args}`.quiet()
|
|
110
|
+
|
|
111
|
+
const rawCommits = stdout.split('\x1e').filter(Boolean)
|
|
112
|
+
const commits = []
|
|
113
|
+
|
|
114
|
+
for (const raw of rawCommits) {
|
|
115
|
+
const block = raw.replace(/\r/g, '').trim()
|
|
116
|
+
// eslint-disable-next-line no-continue
|
|
117
|
+
if (!block) continue
|
|
118
|
+
|
|
119
|
+
// header: 6 个字段用 \x1f 分隔
|
|
120
|
+
const headerMatch = block.match(
|
|
121
|
+
// eslint-disable-next-line no-control-regex
|
|
122
|
+
/^([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([\s\S]*)$/
|
|
123
|
+
)
|
|
124
|
+
// eslint-disable-next-line no-continue
|
|
125
|
+
if (!headerMatch) continue
|
|
126
|
+
|
|
127
|
+
const [, hash, authorName, emailAddr, date, subject, body] = headerMatch
|
|
128
|
+
|
|
129
|
+
const [, changeId] = body.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
|
|
130
|
+
|
|
131
|
+
const c = {
|
|
132
|
+
hash,
|
|
133
|
+
author: normalizer.getAuthor(authorName, emailAddr),
|
|
134
|
+
originalAuthor: authorName,
|
|
135
|
+
email: emailAddr,
|
|
136
|
+
date,
|
|
137
|
+
message: subject,
|
|
138
|
+
body,
|
|
139
|
+
changeId,
|
|
140
|
+
added: 0,
|
|
141
|
+
deleted: 0,
|
|
142
|
+
changed: 0,
|
|
143
|
+
files: []
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 匹配所有 numstat 行
|
|
147
|
+
const numstatLines = block
|
|
148
|
+
.split('\n')
|
|
149
|
+
.filter((l) => /^\d+\s+\d+\s+/.test(l))
|
|
150
|
+
for (const line of numstatLines) {
|
|
151
|
+
const [a, d, file] = line.trim().split(/\s+/)
|
|
152
|
+
const added = parseInt(a, 10) || 0
|
|
153
|
+
const deleted = parseInt(d, 10) || 0
|
|
154
|
+
c.added += added
|
|
155
|
+
c.deleted += deleted
|
|
156
|
+
c.changed += added + deleted
|
|
157
|
+
c.files.push({ file, added, deleted })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
commits.push(c)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
commits,
|
|
165
|
+
authorMap: normalizer.getMap()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function getGitLogsFast(opts) {
|
|
170
|
+
const { author, email, since, until, limit, merges } = opts
|
|
171
|
+
|
|
172
|
+
const pretty = `${[
|
|
173
|
+
'%H', // hash
|
|
174
|
+
'%an', // author name
|
|
175
|
+
'%ae', // email
|
|
176
|
+
'%ad', // date
|
|
177
|
+
'%s', // subject
|
|
178
|
+
'%B' // body
|
|
179
|
+
].join('%x1f')}%x1e`
|
|
180
|
+
|
|
181
|
+
const args = [
|
|
182
|
+
'log',
|
|
183
|
+
`--pretty=format:${pretty}`,
|
|
184
|
+
'--date=iso-local',
|
|
185
|
+
'--numstat'
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
if (author) args.push(`--author=${author}`)
|
|
189
|
+
if (email) args.push(`--author=${email}`)
|
|
190
|
+
if (since) args.push(`--since=${since}`)
|
|
191
|
+
if (until) args.push(`--until=${until}`)
|
|
192
|
+
if (merges === false) args.push(`--no-merges`)
|
|
193
|
+
if (limit) args.push('-n', `${limit}`)
|
|
194
|
+
|
|
195
|
+
const { stdout } = await $`git ${args}`.quiet()
|
|
196
|
+
|
|
197
|
+
const commits = []
|
|
198
|
+
const raw = stdout.replace(/\r/g, '')
|
|
199
|
+
|
|
200
|
+
// 正则匹配每个 commit header + numstat
|
|
201
|
+
const commitRegex =
|
|
202
|
+
// eslint-disable-next-line no-control-regex
|
|
203
|
+
/([0-9a-f]+)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([\s\S]*?)(?=(?:[0-9a-f]{7,40}\x1f)|\x1e$)/g
|
|
204
|
+
let match
|
|
205
|
+
// eslint-disable-next-line no-cond-assign
|
|
206
|
+
while ((match = commitRegex.exec(raw)) !== null) {
|
|
207
|
+
const [_, hash, authorName, emailAddr, date, subject, bodyAndNumstat] =
|
|
208
|
+
match
|
|
209
|
+
const [, changeId] =
|
|
210
|
+
bodyAndNumstat.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
|
|
211
|
+
|
|
212
|
+
const c = {
|
|
213
|
+
hash,
|
|
214
|
+
author: normalizer.getAuthor(authorName, emailAddr),
|
|
215
|
+
originalAuthor: authorName,
|
|
216
|
+
email: emailAddr,
|
|
217
|
+
date,
|
|
218
|
+
message: subject,
|
|
219
|
+
body: bodyAndNumstat,
|
|
220
|
+
changeId,
|
|
221
|
+
added: 0,
|
|
222
|
+
deleted: 0,
|
|
223
|
+
changed: 0,
|
|
224
|
+
files: []
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 匹配 numstat 行
|
|
228
|
+
const numstatRegex = /^(\d+)\s+(\d+)\s+(.+)$/gm
|
|
229
|
+
|
|
230
|
+
for (const nsMatch of bodyAndNumstat.matchAll(numstatRegex)) {
|
|
231
|
+
const added = parseInt(nsMatch[1], 10) || 0
|
|
232
|
+
const deleted = parseInt(nsMatch[2], 10) || 0
|
|
233
|
+
const file = nsMatch[3]
|
|
234
|
+
c.added += added
|
|
235
|
+
c.deleted += deleted
|
|
236
|
+
c.changed += added + deleted
|
|
237
|
+
c.files.push({ file, added, deleted })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
commits.push(c)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
commits,
|
|
245
|
+
authorMap: normalizer.getMap()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 输出 authorMap 映射表文本
|
|
3
|
+
* @param {Record<string,string>} authorMap - email -> normalized author name
|
|
4
|
+
* @param {Object} opts
|
|
5
|
+
* @param {boolean} opts.showHeader - 是否显示表头
|
|
6
|
+
* @returns {string} 文本内容
|
|
7
|
+
*/
|
|
8
|
+
export function renderAuthorMapText(authorMap, opts = {}) {
|
|
9
|
+
const { showHeader = true } = opts
|
|
10
|
+
const pad = (s, n) => (s.length >= n ? `${s.slice(0, n - 1)}…` : s + ' '.repeat(n - s.length))
|
|
11
|
+
|
|
12
|
+
const header = showHeader ? `${pad('Email', 30)} | ${pad('Author', 20)}` : ''
|
|
13
|
+
const line = showHeader ? '-'.repeat(header.length) : ''
|
|
14
|
+
|
|
15
|
+
const rows = []
|
|
16
|
+
for (const [email, author] of Object.entries(authorMap)) {
|
|
17
|
+
rows.push(`${pad(email, 30)} | ${pad(author, 20)}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return showHeader ? [header, line, ...rows].join('\n') : rows.join('\n')
|
|
21
|
+
}
|
package/src/text.mjs
CHANGED
|
@@ -3,8 +3,10 @@ import { buildAuthorChangeStats } from './stats.mjs'
|
|
|
3
3
|
|
|
4
4
|
export function renderText(records, groups = null, opts = {}) {
|
|
5
5
|
const { showGerrit = false } = opts
|
|
6
|
-
const pad = (s, n) =>
|
|
7
|
-
s.length >= n ? `${s.slice(0, n - 1)}…` : s + ' '.repeat(n - s.length)
|
|
6
|
+
const pad = (s, n) => {
|
|
7
|
+
return s.length >= n ? `${s.slice(0, n - 1)}…` : s + ' '.repeat(n - s.length)
|
|
8
|
+
}
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
const baseHeader = `${pad('Hash', 10)} | ${pad('Author', 18)} | ${pad(
|
|
10
12
|
'Date',
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// authorNormalizer.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 一个按邮箱自动归一化作者名字的工具。
|
|
5
|
+
* - 优先使用中文姓名
|
|
6
|
+
* - 如果同邮箱出现多个非中文名,保持第一个
|
|
7
|
+
* - 保留原始 author 以防 debug
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function createAuthorNormalizer() {
|
|
11
|
+
const map = {} // email -> canonical name
|
|
12
|
+
|
|
13
|
+
function isChinese(str) {
|
|
14
|
+
return /[\u4e00-\u9fa5]/.test(str)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getAuthor(name, email) {
|
|
18
|
+
if (!email) return name
|
|
19
|
+
|
|
20
|
+
const canonical = map[email]
|
|
21
|
+
|
|
22
|
+
// 首次遇到这个邮箱 → 记录当前作者名
|
|
23
|
+
if (!canonical) {
|
|
24
|
+
map[email] = name
|
|
25
|
+
return name
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 如果新的作者名是中文 → 覆盖旧的
|
|
29
|
+
if (isChinese(name)) {
|
|
30
|
+
map[email] = name
|
|
31
|
+
return name
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 新名不是中文 → 返回已有中文(或旧名)
|
|
35
|
+
return canonical
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
getAuthor,
|
|
40
|
+
getMap: () => map
|
|
41
|
+
}
|
|
42
|
+
}
|