wukong-gitlog-cli 1.0.35 → 1.0.37

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,23 @@
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.37](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.36...v1.0.37) (2025-12-12)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 add logDev ([a85905b](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a85905b038014fb62c7fd21713f63e2405e3cfa7))
11
+ * 🎸 createOvertimeStats ([abb0051](https://github.com/tomatobybike/wukong-gitlog-cli/commit/abb0051efc91cceb9845a1c5c9a00f9d6747baf4))
12
+ * 🎸 logDev ([e122042](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e12204203a077630784caa16b01c566fdd72b84a))
13
+ * 🎸 logger debug ([32a55fe](https://github.com/tomatobybike/wukong-gitlog-cli/commit/32a55fe8d191969ca887cb46789d14b65b446f6d))
14
+
15
+ ### [1.0.36](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.35...v1.0.36) (2025-12-12)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * 🐛 createAuthorNormalizer ([12f70ba](https://github.com/tomatobybike/wukong-gitlog-cli/commit/12f70ba10013cbba988d3ae1d7fed6f0949c6cbc))
21
+
5
22
  ### [1.0.35](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.34...v1.0.35) (2025-12-12)
6
23
 
7
24
 
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import('../src/cli.mjs')
2
+ import('../src/index.mjs')
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.37",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
@@ -96,6 +96,8 @@
96
96
  "commander": "12.1.0",
97
97
  "date-holidays": "2.1.1",
98
98
  "dayjs": "1.11.19",
99
+ "date-fns": "4.1.0",
100
+ "dotenv": "17.2.2",
99
101
  "exceljs": "4.4.0",
100
102
  "is-online": "12.0.2",
101
103
  "ora": "9.0.0",
@@ -0,0 +1,9 @@
1
+ export function parseOptions(opts) {
2
+ return {
3
+ startHour: opts.workStart ?? 9,
4
+ endHour: opts.workEnd ?? 18,
5
+ lunchStart: opts.lunchStart ?? 12,
6
+ lunchEnd: opts.lunchEnd ?? 14,
7
+ country: opts.country ?? 'CN'
8
+ };
9
+ }
package/src/git.mjs CHANGED
@@ -76,21 +76,20 @@ export async function getGitLogsSlow(opts) {
76
76
  }
77
77
  }
78
78
 
79
- return commits
79
+ return { commits, authorMap: {} }
80
80
  }
81
81
 
82
82
  export async function getGitLogsQuick(opts) {
83
83
  const { author, email, since, until, limit, merges } = opts
84
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`
85
+ const pretty = `${[
86
+ '%H', // hash
87
+ '%an', // author name
88
+ '%ae', // email
89
+ '%ad', // date
90
+ '%s', // subject
91
+ '%B' // body
92
+ ].join('%x1f')}%x1e`
94
93
 
95
94
  const args = [
96
95
  'log',
@@ -166,7 +165,10 @@ export async function getGitLogsQuick(opts) {
166
165
  }
167
166
  }
168
167
 
169
- export async function getGitLogsFast(opts) {
168
+ /**
169
+ * 高性能 git log + numstat 获取 commit
170
+ */
171
+ export async function getGitLogsFast(opts = {}) {
170
172
  const { author, email, since, until, limit, merges } = opts
171
173
 
172
174
  const pretty = `${[
@@ -193,19 +195,18 @@ export async function getGitLogsFast(opts) {
193
195
  if (limit) args.push('-n', `${limit}`)
194
196
 
195
197
  const { stdout } = await $`git ${args}`.quiet()
198
+ const raw = stdout.replace(/\r/g, '')
196
199
 
197
200
  const commits = []
198
- const raw = stdout.replace(/\r/g, '')
199
201
 
200
- // 正则匹配每个 commit header + numstat
202
+ // 匹配每个 commit header + numstat
201
203
  const commitRegex =
202
204
  // eslint-disable-next-line no-control-regex
203
205
  /([0-9a-f]+)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([\s\S]*?)(?=(?:[0-9a-f]{7,40}\x1f)|\x1e$)/g
204
206
  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
207
+
208
+ for (const ns of raw.matchAll(commitRegex)) {
209
+ const [_, hash, authorName, emailAddr, date, subject, bodyAndNumstat] = ns
209
210
  const [, changeId] =
210
211
  bodyAndNumstat.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
211
212
 
@@ -224,13 +225,12 @@ export async function getGitLogsFast(opts) {
224
225
  files: []
225
226
  }
226
227
 
227
- // 匹配 numstat
228
+ // 匹配 numstat
228
229
  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]
230
+ for (const m of bodyAndNumstat.matchAll(numstatRegex)) {
231
+ const added = parseInt(m[1], 10) || 0
232
+ const deleted = parseInt(m[2], 10) || 0
233
+ const file = m[3]
234
234
  c.added += added
235
235
  c.deleted += deleted
236
236
  c.changed += added + deleted
@@ -240,8 +240,17 @@ export async function getGitLogsFast(opts) {
240
240
  commits.push(c)
241
241
  }
242
242
 
243
+ // 最终统一覆盖 author,保证所有 commit 都使用中文名(如存在)
244
+ const finalMap = normalizer.getMap()
245
+ for (const c of commits) {
246
+ if (c.email && finalMap[c.email]) {
247
+ c.author = finalMap[c.email]
248
+ }
249
+ }
250
+
243
251
  return {
244
252
  commits,
245
- authorMap: normalizer.getMap()
253
+ authorMap: finalMap,
254
+ originalMap: normalizer.getOriginalMap()
246
255
  }
247
256
  }
@@ -0,0 +1,203 @@
1
+ import dayjs from 'dayjs'
2
+
3
+ // eslint-disable-next-line no-unused-vars
4
+ import { renderAuthorChangesJson } from '../json.mjs'
5
+ import { startServer } from '../server.mjs'
6
+ import { getWeekRange } from '../utils/getWeekRange.mjs'
7
+ import { groupRecords, outputFilePath, writeTextFile } from '../utils/index.mjs'
8
+ import { logDev } from '../utils/logDev.mjs'
9
+
10
+ export const handleServe = async ({
11
+ opts,
12
+ outDir,
13
+ records,
14
+ getOvertimeStats
15
+ }) => {
16
+ try {
17
+ const stats = getOvertimeStats(records)
18
+
19
+ const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
20
+ const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
21
+ writeTextFile(dataCommitsFile, commitsModule)
22
+
23
+ const dataCommitsChangedFile = outputFilePath(
24
+ 'data/author-changes.mjs',
25
+ outDir
26
+ )
27
+ const jsonText = renderAuthorChangesJson(records)
28
+
29
+ const commitsChangedModule = `export default ${JSON.stringify(jsonText, null, 2)};\n`
30
+ writeTextFile(dataCommitsChangedFile, commitsChangedModule)
31
+
32
+ const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
33
+ const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`
34
+ writeTextFile(dataStatsFile, statsModule)
35
+
36
+ // 新增:每周趋势数据(用于前端图表)
37
+ const weekGroups = groupRecords(records, 'week')
38
+ const weekKeys = Object.keys(weekGroups).sort()
39
+ const weeklySeries = weekKeys.map((k) => {
40
+ const s = getOvertimeStats(weekGroups[k])
41
+ return {
42
+ period: k,
43
+ range: getWeekRange(k),
44
+ total: s.total,
45
+ outsideWorkCount: s.outsideWorkCount,
46
+ outsideWorkRate: s.outsideWorkRate,
47
+ nonWorkdayCount: s.nonWorkdayCount,
48
+ nonWorkdayRate: s.nonWorkdayRate
49
+ }
50
+ })
51
+ const dataWeeklyFile = outputFilePath('data/overtime-weekly.mjs', outDir)
52
+ const weeklyModule = `export default ${JSON.stringify(weeklySeries, null, 2)};\n`
53
+ writeTextFile(dataWeeklyFile, weeklyModule)
54
+ logDev(`Weekly series 已导出: ${dataWeeklyFile}`)
55
+
56
+ // 新增:每月趋势数据(用于前端图表)
57
+ const monthGroups2 = groupRecords(records, 'month')
58
+ const monthKeys2 = Object.keys(monthGroups2).sort()
59
+ const monthlySeries = monthKeys2.map((k) => {
60
+ const s = getOvertimeStats(monthGroups2[k])
61
+ return {
62
+ period: k,
63
+ total: s.total,
64
+ outsideWorkCount: s.outsideWorkCount,
65
+ outsideWorkRate: s.outsideWorkRate,
66
+ nonWorkdayCount: s.nonWorkdayCount,
67
+ nonWorkdayRate: s.nonWorkdayRate
68
+ }
69
+ })
70
+ const dataMonthlyFile = outputFilePath('data/overtime-monthly.mjs', outDir)
71
+ const monthlyModule = `export default ${JSON.stringify(monthlySeries, null, 2)};\n`
72
+ writeTextFile(dataMonthlyFile, monthlyModule)
73
+ logDev(`Monthly series 已导出: ${dataMonthlyFile}`)
74
+
75
+ // 新增:每日最晚提交小时(用于显著展示加班严重程度)
76
+ const dayGroups2 = groupRecords(records, 'day')
77
+ const dayKeys2 = Object.keys(dayGroups2).sort()
78
+
79
+ // 次日凌晨归并窗口(默认 6 点前仍算前一天的加班)
80
+ const overnightCutoff = Number.isFinite(opts.overnightCutoff)
81
+ ? opts.overnightCutoff
82
+ : 6
83
+ // 次日上班时间(默认按 workStart,若未指定则 9 点)
84
+ const workStartHour =
85
+ opts.workStart || opts.workStart === 0 ? opts.workStart : 9
86
+ const workEndHour = opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18
87
+
88
+ // 有些日期「本身没有 commit」,但第二天凌晨有提交要归并到这一天,
89
+ // 需要补出这些“虚拟日期”,否则 latestByDay 会漏掉这天。
90
+ const virtualPrevDays = new Set()
91
+ records.forEach((r) => {
92
+ const d = new Date(r.date)
93
+ if (Number.isNaN(d.valueOf())) return
94
+ const h = d.getHours()
95
+ if (h < 0 || h >= overnightCutoff || h >= workStartHour) return
96
+ const curDay = dayjs(d).format('YYYY-MM-DD')
97
+ const prevDay = dayjs(curDay).subtract(1, 'day').format('YYYY-MM-DD')
98
+ if (!dayGroups2[prevDay]) {
99
+ virtualPrevDays.add(prevDay)
100
+ }
101
+ })
102
+
103
+ const allDayKeys = Array.from(
104
+ new Set([...dayKeys2, ...virtualPrevDays])
105
+ ).sort()
106
+
107
+ const latestByDay = allDayKeys.map((k) => {
108
+ const list = dayGroups2[k] || []
109
+
110
+ // 1) 当天「下班后」的提交:只统计 >= workEndHour 的小时
111
+ const sameDayHours = list
112
+ .map((r) => new Date(r.date))
113
+ .filter((d) => !Number.isNaN(d.valueOf()))
114
+ .map((d) => d.getHours())
115
+ .filter((h) => h >= workEndHour && h < 24)
116
+
117
+ // 2) 次日凌晨、但仍算前一日加班的提交
118
+ const nextKey = dayjs(k).add(1, 'day').format('YYYY-MM-DD')
119
+ const early = dayGroups2[nextKey] || []
120
+ const earlyHours = early
121
+ .map((r) => new Date(r.date))
122
+ .filter((d) => !Number.isNaN(d.valueOf()))
123
+ .map((d) => d.getHours())
124
+ // 只看 [0, overnightCutoff) 之间的小时,
125
+ // 并且默认认为 < workStartHour 属于「次日上班前」
126
+ .filter(
127
+ (h) =>
128
+ h >= 0 &&
129
+ h < overnightCutoff &&
130
+ // 保护性判断:若有人把 overnightCutoff 设得大于上班时间,
131
+ // 我们仍然只统计到上班时间为止
132
+ h < workStartHour
133
+ )
134
+
135
+ // 3) 计算「逻辑上的最晚加班时间」
136
+ // - 当天晚上的用原始小时(如 22 点)
137
+ // - 次日凌晨的用 24 + 小时(如 1 点 → 25)
138
+ const overtimeValues = [
139
+ ...sameDayHours.map((h) => h),
140
+ ...earlyHours.map((h) => 24 + h)
141
+ ]
142
+
143
+ if (overtimeValues.length === 0) {
144
+ // 这一天没有任何「下班后到次日上班前」的提交
145
+ return {
146
+ date: k,
147
+ latestHour: null,
148
+ latestHourNormalized: null
149
+ }
150
+ }
151
+
152
+ const latestHourNormalized = Math.max(...overtimeValues)
153
+
154
+ // latestHour 保留「当天自然日内」的最晚提交通常小时数,供前端需要时参考
155
+ const sameDayMax =
156
+ sameDayHours.length > 0 ? Math.max(...sameDayHours) : null
157
+
158
+ return {
159
+ date: k,
160
+ latestHour: sameDayMax,
161
+ latestHourNormalized
162
+ }
163
+ })
164
+ const dataLatestByDayFile = outputFilePath(
165
+ 'data/overtime-latest-by-day.mjs',
166
+ outDir
167
+ )
168
+ const latestByDayModule = `export default ${JSON.stringify(latestByDay, null, 2)};\n`
169
+ writeTextFile(dataLatestByDayFile, latestByDayModule)
170
+ logDev(`Latest-by-day series 已导出: ${dataLatestByDayFile}`)
171
+
172
+ // 导出配置(供前端显示)
173
+ try {
174
+ const configFile = outputFilePath('data/config.mjs', outDir)
175
+ const cfg = {
176
+ startHour: opts.workStart || 9,
177
+ endHour: opts.workEnd || 18,
178
+ lunchStart: opts.lunchStart || 12,
179
+ lunchEnd: opts.lunchEnd || 14,
180
+ overnightCutoff
181
+ }
182
+ writeTextFile(
183
+ configFile,
184
+ `export default ${JSON.stringify(cfg, null, 2)};\n`
185
+ )
186
+ logDev(`Config 已导出: ${configFile}`)
187
+ } catch (e) {
188
+ console.warn('Export config failed:', e && e.message ? e.message : e)
189
+ }
190
+
191
+ startServer(opts.port || 3000, outDir).catch((err) => {
192
+ console.warn(
193
+ 'Start server failed.',
194
+ err && err.message ? err.message : err
195
+ )
196
+ })
197
+ } catch (err) {
198
+ console.warn(
199
+ 'Export data modules failed:',
200
+ err && err.message ? err.message : err
201
+ )
202
+ }
203
+ }