wukong-gitlog-cli 1.0.33 → 1.0.35

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 CHANGED
@@ -2,6 +2,20 @@
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.35](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.34...v1.0.35) (2025-12-12)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 createAuthorNormalizer ([a23a80e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a23a80e6d664cc5db89b40a63456d8e8d4b2c321))
11
+
12
+ ### [1.0.34](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.33...v1.0.34) (2025-12-12)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * 🐛 eslint error ([3d48e25](https://github.com/tomatobybike/wukong-gitlog-cli/commit/3d48e25eb1217005ac2a525b54e307ca328b85e7))
18
+
5
19
  ### [1.0.33](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.32...v1.0.33) (2025-12-12)
6
20
 
7
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
package/src/cli.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  exportExcelAuthorChangeStats,
15
15
  exportExcelPerPeriodSheets
16
16
  } from './excel.mjs'
17
- import { getGitLogs } from './git.mjs'
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
- let records = await getGitLogs(opts)
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('data/author-changes.mjs', outDir)
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
- export async function getGitLogs(opts) {
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 } = await $`git show --numstat --format= ${c.hash}`.quiet()
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,69 @@
1
+ // authorNormalizer.js
2
+ /**
3
+ * 按邮箱自动归一化作者名字
4
+ * - 优先使用中文姓名
5
+ * - 如果同邮箱出现多个非中文名,保持第一个
6
+ * - 自动清理空格与不可见字符
7
+ * - 保留原始 author 以便 debug
8
+ */
9
+ export function createAuthorNormalizer() {
10
+ const map = {} // email -> canonical name
11
+ const originalMap = {} // email -> 原始所有 author 名
12
+
13
+ function isChinese(str) {
14
+ return /[\u4e00-\u9fa5]/.test(str)
15
+ }
16
+
17
+ function cleanName(str) {
18
+ if (!str) return ''
19
+ return str
20
+ // eslint-disable-next-line no-misleading-character-class
21
+ .replace(/[\u200B\u200C\u200D\uFEFF]/g, '') // 去零宽空格
22
+ .replace(/\s+/g, ' ') // 多空格 -> 单空格
23
+ .trim()
24
+ }
25
+
26
+ function cleanEmail(str) {
27
+ return (str || '').trim()
28
+ }
29
+
30
+ function getAuthor(name, email) {
31
+ const cleanedName = cleanName(name)
32
+ const cleanedEmail = cleanEmail(email)
33
+
34
+ if (!cleanedEmail) return cleanedName || 'Unknown'
35
+
36
+ // 记录原始 author 便于 debug
37
+ if (!originalMap[cleanedEmail]) originalMap[cleanedEmail] = new Set()
38
+ if (cleanedName) originalMap[cleanedEmail].add(cleanedName)
39
+
40
+ const canonical = map[cleanedEmail]
41
+
42
+ // 首次遇到这个邮箱 → 记录当前作者名
43
+ if (!canonical) {
44
+ map[cleanedEmail] = cleanedName || cleanedEmail
45
+ return map[cleanedEmail]
46
+ }
47
+
48
+ // 新名是中文 → 覆盖旧的
49
+ if (isChinese(cleanedName)) {
50
+ map[cleanedEmail] = cleanedName
51
+ return map[cleanedEmail]
52
+ }
53
+
54
+ // 新名非中文 → 保留已有中文或旧名
55
+ return canonical
56
+ }
57
+
58
+ return {
59
+ getAuthor,
60
+ getMap: () => map,
61
+ getOriginalMap: () => {
62
+ const res = {}
63
+ for (const [email, names] of Object.entries(originalMap)) {
64
+ res[email] = Array.from(names)
65
+ }
66
+ return res
67
+ }
68
+ }
69
+ }