wukong-gitlog-cli 1.0.34 → 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,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.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
+
12
+ ### [1.0.35](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.34...v1.0.35) (2025-12-12)
13
+
14
+
15
+ ### Features
16
+
17
+ * 🎸 createAuthorNormalizer ([a23a80e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a23a80e6d664cc5db89b40a63456d8e8d4b2c321))
18
+
5
19
  ### [1.0.34](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.33...v1.0.34) (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.34",
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,42 +1,62 @@
1
- // authorNormalizer.js
2
-
3
1
  /**
4
- * 一个按邮箱自动归一化作者名字的工具。
5
- * - 优先使用中文姓名
6
- * - 如果同邮箱出现多个非中文名,保持第一个
7
- * - 保留原始 author 以防 debug
2
+ * 按邮箱自动归一化作者名字
3
+ * - 中文名优先覆盖
4
+ * - 同邮箱多个英文名保持第一个
5
+ * - 保留原始 author 以便 debug
8
6
  */
9
-
10
7
  export function createAuthorNormalizer() {
11
8
  const map = {} // email -> canonical name
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
 
15
+ function cleanName(str) {
16
+ // eslint-disable-next-line no-misleading-character-class
17
+ return (str || '').replace(/[\u200B\u200C\u200D\uFEFF]/g, '').trim()
18
+ }
19
+
20
+ function cleanEmail(str) {
21
+ return (str || '').trim()
22
+ }
23
+
17
24
  function getAuthor(name, email) {
18
- if (!email) return name
25
+ const n = cleanName(name)
26
+ const e = cleanEmail(email)
19
27
 
20
- const canonical = map[email]
28
+ if (!e) return n || 'Unknown'
21
29
 
22
- // 首次遇到这个邮箱 → 记录当前作者名
23
- if (!canonical) {
24
- map[email] = name
25
- return name
26
- }
30
+ // 记录原始 author
31
+ if (!originalMap[e]) originalMap[e] = new Set()
32
+ if (n) originalMap[e].add(n)
33
+
34
+ const prev = map[e]
27
35
 
28
- // 如果新的作者名是中文 → 覆盖旧的
29
- if (isChinese(name)) {
30
- map[email] = name
31
- return name
36
+ // 中文名优先覆盖
37
+ if (isChinese(n)) {
38
+ map[e] = n
39
+ return n
32
40
  }
33
41
 
34
- // 新名不是中文返回已有中文(或旧名)
35
- return canonical
42
+ // 已有中文名返回中文
43
+ if (prev && isChinese(prev)) return prev
44
+
45
+ // 首次出现英文名 → 记录
46
+ if (!prev) map[e] = n
47
+
48
+ return map[e]
36
49
  }
37
50
 
38
51
  return {
39
52
  getAuthor,
40
- getMap: () => map
53
+ getMap: () => map,
54
+ getOriginalMap: () => {
55
+ const res = {}
56
+ for (const [email, names] of Object.entries(originalMap)) {
57
+ res[email] = Array.from(names)
58
+ }
59
+ return res
60
+ }
41
61
  }
42
62
  }