wukong-gitlog-cli 1.0.35 → 1.0.36

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.36](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.35...v1.0.36) (2025-12-12)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * 🐛 createAuthorNormalizer ([12f70ba](https://github.com/tomatobybike/wukong-gitlog-cli/commit/12f70ba10013cbba988d3ae1d7fed6f0949c6cbc))
11
+
5
12
  ### [1.0.35](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.34...v1.0.35) (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.35",
3
+ "version": "1.0.36",
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/git.mjs CHANGED
@@ -166,7 +166,10 @@ export async function getGitLogsQuick(opts) {
166
166
  }
167
167
  }
168
168
 
169
- export async function getGitLogsFast(opts) {
169
+ /**
170
+ * 高性能 git log + numstat 获取 commit
171
+ */
172
+ export async function getGitLogsFast(opts = {}) {
170
173
  const { author, email, since, until, limit, merges } = opts
171
174
 
172
175
  const pretty = `${[
@@ -174,16 +177,11 @@ export async function getGitLogsFast(opts) {
174
177
  '%an', // author name
175
178
  '%ae', // email
176
179
  '%ad', // date
177
- '%s', // subject
178
- '%B' // body
179
- ].join('%x1f')}%x1e`
180
+ '%s', // subject
181
+ '%B' // body
182
+ ].join('%x1f') }%x1e`
180
183
 
181
- const args = [
182
- 'log',
183
- `--pretty=format:${pretty}`,
184
- '--date=iso-local',
185
- '--numstat'
186
- ]
184
+ const args = ['log', `--pretty=format:${pretty}`, '--date=iso-local', '--numstat']
187
185
 
188
186
  if (author) args.push(`--author=${author}`)
189
187
  if (email) args.push(`--author=${email}`)
@@ -193,21 +191,18 @@ export async function getGitLogsFast(opts) {
193
191
  if (limit) args.push('-n', `${limit}`)
194
192
 
195
193
  const { stdout } = await $`git ${args}`.quiet()
194
+ const raw = stdout.replace(/\r/g, '')
196
195
 
197
196
  const commits = []
198
- const raw = stdout.replace(/\r/g, '')
199
197
 
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
198
+ // 匹配每个 commit header + numstat
199
+ // eslint-disable-next-line no-control-regex
200
+ const commitRegex = /([0-9a-f]+)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([\s\S]*?)(?=(?:[0-9a-f]{7,40}\x1f)|\x1e$)/g
204
201
  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]+)/) || []
202
+
203
+ for (const ns of raw.matchAll(commitRegex)) {
204
+ const [_, hash, authorName, emailAddr, date, subject, bodyAndNumstat] = ns
205
+ const [, changeId] = bodyAndNumstat.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
211
206
 
212
207
  const c = {
213
208
  hash,
@@ -224,13 +219,12 @@ export async function getGitLogsFast(opts) {
224
219
  files: []
225
220
  }
226
221
 
227
- // 匹配 numstat
222
+ // 匹配 numstat
228
223
  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]
224
+ for (const m of bodyAndNumstat.matchAll(numstatRegex)) {
225
+ const added = parseInt(m[1], 10) || 0
226
+ const deleted = parseInt(m[2], 10) || 0
227
+ const file = m[3]
234
228
  c.added += added
235
229
  c.deleted += deleted
236
230
  c.changed += added + deleted
@@ -240,8 +234,17 @@ export async function getGitLogsFast(opts) {
240
234
  commits.push(c)
241
235
  }
242
236
 
237
+ // 最终统一覆盖 author,保证所有 commit 都使用中文名(如存在)
238
+ const finalMap = normalizer.getMap()
239
+ for (const c of commits) {
240
+ if (c.email && finalMap[c.email]) {
241
+ c.author = finalMap[c.email]
242
+ }
243
+ }
244
+
243
245
  return {
244
246
  commits,
245
- authorMap: normalizer.getMap()
247
+ authorMap: finalMap,
248
+ originalMap: normalizer.getOriginalMap()
246
249
  }
247
250
  }
@@ -1,26 +1,20 @@
1
- // authorNormalizer.js
2
1
  /**
3
2
  * 按邮箱自动归一化作者名字
4
- * - 优先使用中文姓名
5
- * - 如果同邮箱出现多个非中文名,保持第一个
6
- * - 自动清理空格与不可见字符
3
+ * - 中文名优先覆盖
4
+ * - 同邮箱多个英文名保持第一个
7
5
  * - 保留原始 author 以便 debug
8
6
  */
9
7
  export function createAuthorNormalizer() {
10
8
  const map = {} // email -> canonical name
11
- const originalMap = {} // email -> 原始所有 author
9
+ const originalMap = {} // email -> Set of original author names
12
10
 
13
11
  function isChinese(str) {
14
12
  return /[\u4e00-\u9fa5]/.test(str)
15
13
  }
16
14
 
17
15
  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()
16
+ // eslint-disable-next-line no-misleading-character-class
17
+ return (str || '').replace(/[\u200B\u200C\u200D\uFEFF]/g, '').trim()
24
18
  }
25
19
 
26
20
  function cleanEmail(str) {
@@ -28,31 +22,30 @@ export function createAuthorNormalizer() {
28
22
  }
29
23
 
30
24
  function getAuthor(name, email) {
31
- const cleanedName = cleanName(name)
32
- const cleanedEmail = cleanEmail(email)
25
+ const n = cleanName(name)
26
+ const e = cleanEmail(email)
33
27
 
34
- if (!cleanedEmail) return cleanedName || 'Unknown'
28
+ if (!e) return n || 'Unknown'
35
29
 
36
- // 记录原始 author 便于 debug
37
- if (!originalMap[cleanedEmail]) originalMap[cleanedEmail] = new Set()
38
- if (cleanedName) originalMap[cleanedEmail].add(cleanedName)
30
+ // 记录原始 author
31
+ if (!originalMap[e]) originalMap[e] = new Set()
32
+ if (n) originalMap[e].add(n)
39
33
 
40
- const canonical = map[cleanedEmail]
34
+ const prev = map[e]
41
35
 
42
- // 首次遇到这个邮箱 → 记录当前作者名
43
- if (!canonical) {
44
- map[cleanedEmail] = cleanedName || cleanedEmail
45
- return map[cleanedEmail]
36
+ // 中文名优先覆盖
37
+ if (isChinese(n)) {
38
+ map[e] = n
39
+ return n
46
40
  }
47
41
 
48
- // 新名是中文覆盖旧的
49
- if (isChinese(cleanedName)) {
50
- map[cleanedEmail] = cleanedName
51
- return map[cleanedEmail]
52
- }
42
+ // 已有中文名返回中文
43
+ if (prev && isChinese(prev)) return prev
44
+
45
+ // 首次出现英文名 → 记录
46
+ if (!prev) map[e] = n
53
47
 
54
- // 新名非中文 → 保留已有中文或旧名
55
- return canonical
48
+ return map[e]
56
49
  }
57
50
 
58
51
  return {