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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
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,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
+ }