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 +17 -0
- package/bin/wukong-gitlog-cli +1 -1
- package/package.json +3 -1
- package/src/cli/parseOptions.mjs +9 -0
- package/src/git.mjs +33 -24
- package/src/handlers/handleServe.mjs +203 -0
- package/src/{cli.mjs → index.mjs} +60 -276
- package/src/lib/configStore.mjs +11 -0
- package/src/lib/memoize.mjs +14 -0
- package/src/overtime/createOvertimeStats.mjs +11 -0
- package/src/utils/analyzeOvertimeCached.mjs +7 -0
- package/src/utils/authorNormalizer.mjs +22 -29
- package/src/utils/emoji.mjs +83 -0
- package/src/utils/exitWithTime.mjs +17 -0
- package/src/utils/getWeekRange.mjs +16 -0
- package/src/utils/handleSuccess.mjs +12 -0
- package/src/utils/logDev.mjs +19 -0
- package/src/utils/logger.mjs +145 -0
- package/src/utils/timer.mjs +35 -0
- /package/src/{overtime.mjs → overtime/overtime.mjs} +0 -0
|
@@ -7,6 +7,7 @@ import ora from 'ora'
|
|
|
7
7
|
import path from 'path'
|
|
8
8
|
import { fileURLToPath } from 'url'
|
|
9
9
|
|
|
10
|
+
import { parseOptions } from './cli/parseOptions.mjs'
|
|
10
11
|
// eslint-disable-next-line no-unused-vars
|
|
11
12
|
import { CLI_NAME } from './constants/index.mjs'
|
|
12
13
|
import {
|
|
@@ -15,23 +16,27 @@ import {
|
|
|
15
16
|
exportExcelPerPeriodSheets
|
|
16
17
|
} from './excel.mjs'
|
|
17
18
|
import { getGitLogsFast } from './git.mjs'
|
|
19
|
+
import { handleServe } from './handlers/handleServe.mjs'
|
|
18
20
|
import { renderAuthorChangesJson } from './json.mjs'
|
|
21
|
+
import { setConfig } from './lib/configStore.mjs'
|
|
22
|
+
import { createOvertimeStats } from './overtime/createOvertimeStats.mjs'
|
|
19
23
|
import {
|
|
20
|
-
analyzeOvertime,
|
|
21
24
|
renderOvertimeCsv,
|
|
22
25
|
renderOvertimeTab,
|
|
23
26
|
renderOvertimeText
|
|
24
|
-
} from './overtime.mjs'
|
|
27
|
+
} from './overtime/overtime.mjs'
|
|
25
28
|
import { renderAuthorMapText } from './renderAuthorMapText.mjs'
|
|
26
29
|
import { startServer } from './server.mjs'
|
|
27
30
|
import { renderChangedLinesText, renderText } from './text.mjs'
|
|
28
31
|
import { checkUpdateWithPatch } from './utils/checkUpdate.mjs'
|
|
32
|
+
import { handleSuccess } from './utils/handleSuccess.mjs'
|
|
29
33
|
import {
|
|
30
34
|
groupRecords,
|
|
31
35
|
outputFilePath,
|
|
32
36
|
writeJSON,
|
|
33
37
|
writeTextFile
|
|
34
38
|
} from './utils/index.mjs'
|
|
39
|
+
import { logDev } from './utils/logDev.mjs'
|
|
35
40
|
import { showVersionInfo } from './utils/showVersionInfo.mjs'
|
|
36
41
|
|
|
37
42
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
@@ -78,6 +83,8 @@ export function getWeekRange(periodStr) {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
const main = async () => {
|
|
86
|
+
const startTime = performance.now()
|
|
87
|
+
|
|
81
88
|
const program = new Command()
|
|
82
89
|
|
|
83
90
|
program
|
|
@@ -90,6 +97,7 @@ const main = async () => {
|
|
|
90
97
|
.option('--until <date>', '结束日期')
|
|
91
98
|
.option('--limit <n>', '限制数量', parseInt)
|
|
92
99
|
.option('--no-merges', '不包含 merge commit')
|
|
100
|
+
.option('--export', '导出统计数据')
|
|
93
101
|
.option('--json', '输出 JSON')
|
|
94
102
|
.option('--format <type>', '输出格式: text | excel | json', 'text')
|
|
95
103
|
.option('--group-by <type>', '按日期分组: day | month | week')
|
|
@@ -171,6 +179,7 @@ const main = async () => {
|
|
|
171
179
|
(v) => parseInt(v, 10),
|
|
172
180
|
3000
|
|
173
181
|
)
|
|
182
|
+
.option('--debug', 'enable debug logs')
|
|
174
183
|
.option(
|
|
175
184
|
'--serve-only',
|
|
176
185
|
'仅启动 web 服务,不导出或分析数据(使用 output-wukong/data 中已有的数据)'
|
|
@@ -179,6 +188,14 @@ const main = async () => {
|
|
|
179
188
|
.parse()
|
|
180
189
|
|
|
181
190
|
const opts = program.opts()
|
|
191
|
+
|
|
192
|
+
const config = parseOptions(opts)
|
|
193
|
+
|
|
194
|
+
// ❗只创建一次缓存实例
|
|
195
|
+
const getOvertimeStats = createOvertimeStats(config)
|
|
196
|
+
|
|
197
|
+
setConfig('debug', opts.debug === true)
|
|
198
|
+
|
|
182
199
|
// compute output directory root early (so serve-only can use it)
|
|
183
200
|
const outDir = opts.outParent
|
|
184
201
|
? path.resolve(process.cwd(), '..', 'output-wukong')
|
|
@@ -308,16 +325,14 @@ const main = async () => {
|
|
|
308
325
|
// --- 分组 ---
|
|
309
326
|
const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
|
|
310
327
|
|
|
328
|
+
// If serve mode is enabled, write data modules and launch the web server
|
|
329
|
+
if (opts.serve) {
|
|
330
|
+
handleServe({ opts, outDir, records, getOvertimeStats })
|
|
331
|
+
}
|
|
332
|
+
|
|
311
333
|
// --- Overtime analysis ---
|
|
312
334
|
if (opts.overtime) {
|
|
313
|
-
const stats =
|
|
314
|
-
startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
315
|
-
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
316
|
-
lunchStart:
|
|
317
|
-
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
318
|
-
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
319
|
-
country: opts.country || 'CN'
|
|
320
|
-
})
|
|
335
|
+
const stats = getOvertimeStats(records)
|
|
321
336
|
// Output to console
|
|
322
337
|
console.log('\n--- Overtime analysis ---\n')
|
|
323
338
|
console.log(renderOvertimeText(stats))
|
|
@@ -325,13 +340,13 @@ const main = async () => {
|
|
|
325
340
|
const authorMapText = renderAuthorMapText(authorMap)
|
|
326
341
|
console.log('\n Developers:\n', authorMapText, '\n')
|
|
327
342
|
writeTextFile(outputFilePath('authors.text', outDir), authorMapText)
|
|
328
|
-
|
|
343
|
+
|
|
329
344
|
// if user requested json format, write stats to file
|
|
330
345
|
if (opts.json || opts.format === 'json') {
|
|
331
346
|
const file = opts.out || 'overtime.json'
|
|
332
347
|
const filepath = outputFilePath(file, outDir)
|
|
333
348
|
writeJSON(filepath, stats)
|
|
334
|
-
|
|
349
|
+
logDev(`overtime JSON 已导出: ${filepath}`)
|
|
335
350
|
}
|
|
336
351
|
// Always write human readable overtime text to file (default: overtime.txt)
|
|
337
352
|
const outBase = opts.out
|
|
@@ -348,218 +363,9 @@ const main = async () => {
|
|
|
348
363
|
const overtimeCsvFileName = `overtime_${outBase}.csv`
|
|
349
364
|
const overtimeCsvFile = outputFilePath(overtimeCsvFileName, outDir)
|
|
350
365
|
writeTextFile(overtimeCsvFile, renderOvertimeCsv(stats))
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// If serve mode is enabled, write data modules and launch the web server
|
|
356
|
-
if (opts.serve) {
|
|
357
|
-
try {
|
|
358
|
-
const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
|
|
359
|
-
const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
|
|
360
|
-
writeTextFile(dataCommitsFile, commitsModule)
|
|
361
|
-
|
|
362
|
-
const dataCommitsChangedFile = outputFilePath(
|
|
363
|
-
'data/author-changes.mjs',
|
|
364
|
-
outDir
|
|
365
|
-
)
|
|
366
|
-
const jsonText = renderAuthorChangesJson(records)
|
|
367
|
-
|
|
368
|
-
const commitsChangedModule = `export default ${JSON.stringify(jsonText, null, 2)};\n`
|
|
369
|
-
writeTextFile(dataCommitsChangedFile, commitsChangedModule)
|
|
370
|
-
|
|
371
|
-
const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
|
|
372
|
-
const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`
|
|
373
|
-
writeTextFile(dataStatsFile, statsModule)
|
|
374
|
-
|
|
375
|
-
// 新增:每周趋势数据(用于前端图表)
|
|
376
|
-
const weekGroups = groupRecords(records, 'week')
|
|
377
|
-
const weekKeys = Object.keys(weekGroups).sort()
|
|
378
|
-
const weeklySeries = weekKeys.map((k) => {
|
|
379
|
-
const s = analyzeOvertime(weekGroups[k], {
|
|
380
|
-
startHour:
|
|
381
|
-
opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
382
|
-
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
383
|
-
lunchStart:
|
|
384
|
-
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
385
|
-
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
386
|
-
country: opts.country || 'CN'
|
|
387
|
-
})
|
|
388
|
-
return {
|
|
389
|
-
period: k,
|
|
390
|
-
range: getWeekRange(k),
|
|
391
|
-
total: s.total,
|
|
392
|
-
outsideWorkCount: s.outsideWorkCount,
|
|
393
|
-
outsideWorkRate: s.outsideWorkRate,
|
|
394
|
-
nonWorkdayCount: s.nonWorkdayCount,
|
|
395
|
-
nonWorkdayRate: s.nonWorkdayRate
|
|
396
|
-
}
|
|
397
|
-
})
|
|
398
|
-
const dataWeeklyFile = outputFilePath(
|
|
399
|
-
'data/overtime-weekly.mjs',
|
|
400
|
-
outDir
|
|
401
|
-
)
|
|
402
|
-
const weeklyModule = `export default ${JSON.stringify(weeklySeries, null, 2)};\n`
|
|
403
|
-
writeTextFile(dataWeeklyFile, weeklyModule)
|
|
404
|
-
console.log(chalk.green(`Weekly series 已导出: ${dataWeeklyFile}`))
|
|
405
|
-
|
|
406
|
-
// 新增:每月趋势数据(用于前端图表)
|
|
407
|
-
const monthGroups2 = groupRecords(records, 'month')
|
|
408
|
-
const monthKeys2 = Object.keys(monthGroups2).sort()
|
|
409
|
-
const monthlySeries = monthKeys2.map((k) => {
|
|
410
|
-
const s = analyzeOvertime(monthGroups2[k], {
|
|
411
|
-
startHour:
|
|
412
|
-
opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
413
|
-
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
414
|
-
lunchStart:
|
|
415
|
-
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
416
|
-
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
417
|
-
country: opts.country || 'CN'
|
|
418
|
-
})
|
|
419
|
-
return {
|
|
420
|
-
period: k,
|
|
421
|
-
total: s.total,
|
|
422
|
-
outsideWorkCount: s.outsideWorkCount,
|
|
423
|
-
outsideWorkRate: s.outsideWorkRate,
|
|
424
|
-
nonWorkdayCount: s.nonWorkdayCount,
|
|
425
|
-
nonWorkdayRate: s.nonWorkdayRate
|
|
426
|
-
}
|
|
427
|
-
})
|
|
428
|
-
const dataMonthlyFile = outputFilePath(
|
|
429
|
-
'data/overtime-monthly.mjs',
|
|
430
|
-
outDir
|
|
431
|
-
)
|
|
432
|
-
const monthlyModule = `export default ${JSON.stringify(monthlySeries, null, 2)};\n`
|
|
433
|
-
writeTextFile(dataMonthlyFile, monthlyModule)
|
|
434
|
-
console.log(chalk.green(`Monthly series 已导出: ${dataMonthlyFile}`))
|
|
435
|
-
|
|
436
|
-
// 新增:每日最晚提交小时(用于显著展示加班严重程度)
|
|
437
|
-
const dayGroups2 = groupRecords(records, 'day')
|
|
438
|
-
const dayKeys2 = Object.keys(dayGroups2).sort()
|
|
439
|
-
|
|
440
|
-
// 次日凌晨归并窗口(默认 6 点前仍算前一天的加班)
|
|
441
|
-
const overnightCutoff = Number.isFinite(opts.overnightCutoff)
|
|
442
|
-
? opts.overnightCutoff
|
|
443
|
-
: 6
|
|
444
|
-
// 次日上班时间(默认按 workStart,若未指定则 9 点)
|
|
445
|
-
const workStartHour =
|
|
446
|
-
opts.workStart || opts.workStart === 0 ? opts.workStart : 9
|
|
447
|
-
const workEndHour =
|
|
448
|
-
opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18
|
|
449
|
-
|
|
450
|
-
// 有些日期「本身没有 commit」,但第二天凌晨有提交要归并到这一天,
|
|
451
|
-
// 需要补出这些“虚拟日期”,否则 latestByDay 会漏掉这天。
|
|
452
|
-
const virtualPrevDays = new Set()
|
|
453
|
-
records.forEach((r) => {
|
|
454
|
-
const d = new Date(r.date)
|
|
455
|
-
if (Number.isNaN(d.valueOf())) return
|
|
456
|
-
const h = d.getHours()
|
|
457
|
-
if (h < 0 || h >= overnightCutoff || h >= workStartHour) return
|
|
458
|
-
const curDay = dayjs(d).format('YYYY-MM-DD')
|
|
459
|
-
const prevDay = dayjs(curDay).subtract(1, 'day').format('YYYY-MM-DD')
|
|
460
|
-
if (!dayGroups2[prevDay]) {
|
|
461
|
-
virtualPrevDays.add(prevDay)
|
|
462
|
-
}
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
const allDayKeys = Array.from(
|
|
466
|
-
new Set([...dayKeys2, ...virtualPrevDays])
|
|
467
|
-
).sort()
|
|
468
|
-
|
|
469
|
-
const latestByDay = allDayKeys.map((k) => {
|
|
470
|
-
const list = dayGroups2[k] || []
|
|
471
|
-
|
|
472
|
-
// 1) 当天「下班后」的提交:只统计 >= workEndHour 的小时
|
|
473
|
-
const sameDayHours = list
|
|
474
|
-
.map((r) => new Date(r.date))
|
|
475
|
-
.filter((d) => !Number.isNaN(d.valueOf()))
|
|
476
|
-
.map((d) => d.getHours())
|
|
477
|
-
.filter((h) => h >= workEndHour && h < 24)
|
|
478
|
-
|
|
479
|
-
// 2) 次日凌晨、但仍算前一日加班的提交
|
|
480
|
-
const nextKey = dayjs(k).add(1, 'day').format('YYYY-MM-DD')
|
|
481
|
-
const early = dayGroups2[nextKey] || []
|
|
482
|
-
const earlyHours = early
|
|
483
|
-
.map((r) => new Date(r.date))
|
|
484
|
-
.filter((d) => !Number.isNaN(d.valueOf()))
|
|
485
|
-
.map((d) => d.getHours())
|
|
486
|
-
// 只看 [0, overnightCutoff) 之间的小时,
|
|
487
|
-
// 并且默认认为 < workStartHour 属于「次日上班前」
|
|
488
|
-
.filter(
|
|
489
|
-
(h) =>
|
|
490
|
-
h >= 0 &&
|
|
491
|
-
h < overnightCutoff &&
|
|
492
|
-
// 保护性判断:若有人把 overnightCutoff 设得大于上班时间,
|
|
493
|
-
// 我们仍然只统计到上班时间为止
|
|
494
|
-
h < workStartHour
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
// 3) 计算「逻辑上的最晚加班时间」
|
|
498
|
-
// - 当天晚上的用原始小时(如 22 点)
|
|
499
|
-
// - 次日凌晨的用 24 + 小时(如 1 点 → 25)
|
|
500
|
-
const overtimeValues = [
|
|
501
|
-
...sameDayHours.map((h) => h),
|
|
502
|
-
...earlyHours.map((h) => 24 + h)
|
|
503
|
-
]
|
|
504
|
-
|
|
505
|
-
if (overtimeValues.length === 0) {
|
|
506
|
-
// 这一天没有任何「下班后到次日上班前」的提交
|
|
507
|
-
return {
|
|
508
|
-
date: k,
|
|
509
|
-
latestHour: null,
|
|
510
|
-
latestHourNormalized: null
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const latestHourNormalized = Math.max(...overtimeValues)
|
|
515
|
-
|
|
516
|
-
// latestHour 保留「当天自然日内」的最晚提交通常小时数,供前端需要时参考
|
|
517
|
-
const sameDayMax =
|
|
518
|
-
sameDayHours.length > 0 ? Math.max(...sameDayHours) : null
|
|
519
|
-
|
|
520
|
-
return {
|
|
521
|
-
date: k,
|
|
522
|
-
latestHour: sameDayMax,
|
|
523
|
-
latestHourNormalized
|
|
524
|
-
}
|
|
525
|
-
})
|
|
526
|
-
const dataLatestByDayFile = outputFilePath(
|
|
527
|
-
'data/overtime-latest-by-day.mjs',
|
|
528
|
-
outDir
|
|
529
|
-
)
|
|
530
|
-
const latestByDayModule = `export default ${JSON.stringify(latestByDay, null, 2)};\n`
|
|
531
|
-
writeTextFile(dataLatestByDayFile, latestByDayModule)
|
|
532
|
-
console.log(
|
|
533
|
-
chalk.green(`Latest-by-day series 已导出: ${dataLatestByDayFile}`)
|
|
534
|
-
)
|
|
535
|
-
|
|
536
|
-
// 导出配置(供前端显示)
|
|
537
|
-
try {
|
|
538
|
-
const configFile = outputFilePath('data/config.mjs', outDir)
|
|
539
|
-
const cfg = {
|
|
540
|
-
startHour: opts.workStart || 9,
|
|
541
|
-
endHour: opts.workEnd || 18,
|
|
542
|
-
lunchStart: opts.lunchStart || 12,
|
|
543
|
-
lunchEnd: opts.lunchEnd || 14,
|
|
544
|
-
overnightCutoff
|
|
545
|
-
}
|
|
546
|
-
writeTextFile(
|
|
547
|
-
configFile,
|
|
548
|
-
`export default ${JSON.stringify(cfg, null, 2)};\n`
|
|
549
|
-
)
|
|
550
|
-
console.log(chalk.green(`Config 已导出: ${configFile}`))
|
|
551
|
-
} catch (e) {
|
|
552
|
-
console.warn('Export config failed:', e && e.message ? e.message : e)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
startServer(opts.port || 3000, outDir).catch(() => {})
|
|
556
|
-
} catch (err) {
|
|
557
|
-
console.warn(
|
|
558
|
-
'Export data modules failed:',
|
|
559
|
-
err && err.message ? err.message : err
|
|
560
|
-
)
|
|
561
|
-
}
|
|
562
|
-
}
|
|
366
|
+
logDev(`Overtime text 已导出: ${overtimeFile}`)
|
|
367
|
+
logDev(`Overtime table (tabs) 已导出: ${overtimeTabFile}`)
|
|
368
|
+
logDev(`Overtime CSV 已导出: ${overtimeCsvFile}`)
|
|
563
369
|
|
|
564
370
|
// 按月输出 ... 保持原逻辑
|
|
565
371
|
const perPeriodFormats = String(opts.perPeriodFormats || '')
|
|
@@ -578,15 +384,7 @@ const main = async () => {
|
|
|
578
384
|
const monthKeys = Object.keys(monthGroups).sort()
|
|
579
385
|
monthKeys.forEach((k) => {
|
|
580
386
|
const groupRecs = monthGroups[k]
|
|
581
|
-
const s =
|
|
582
|
-
startHour:
|
|
583
|
-
opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
584
|
-
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
585
|
-
lunchStart:
|
|
586
|
-
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
587
|
-
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
588
|
-
country: opts.country || 'CN'
|
|
589
|
-
})
|
|
387
|
+
const s = getOvertimeStats(groupRecs)
|
|
590
388
|
monthlyContent += `===== ${k} =====\n`
|
|
591
389
|
monthlyContent += `${renderOvertimeText(s)}\n\n`
|
|
592
390
|
// Also write a single file per month under 'month/' folder
|
|
@@ -594,9 +392,7 @@ const main = async () => {
|
|
|
594
392
|
const perMonthFileName = `month/overtime_${outBase}_${k}.txt`
|
|
595
393
|
const perMonthFile = outputFilePath(perMonthFileName, outDir)
|
|
596
394
|
writeTextFile(perMonthFile, renderOvertimeText(s))
|
|
597
|
-
|
|
598
|
-
chalk.green(`Overtime 月度(${k}) 已导出: ${perMonthFile}`)
|
|
599
|
-
)
|
|
395
|
+
logDev(`Overtime 月度(${k}) 已导出: ${perMonthFile}`)
|
|
600
396
|
// per-period CSV / Tab format (按需生成)
|
|
601
397
|
if (perPeriodFormats.includes('csv')) {
|
|
602
398
|
try {
|
|
@@ -605,10 +401,8 @@ const main = async () => {
|
|
|
605
401
|
outputFilePath(perMonthCsvName, outDir),
|
|
606
402
|
renderOvertimeCsv(s)
|
|
607
403
|
)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
`Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`
|
|
611
|
-
)
|
|
404
|
+
logDev(
|
|
405
|
+
`Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`
|
|
612
406
|
)
|
|
613
407
|
} catch (err) {
|
|
614
408
|
console.warn(
|
|
@@ -624,10 +418,8 @@ const main = async () => {
|
|
|
624
418
|
outputFilePath(perMonthTabName, outDir),
|
|
625
419
|
renderOvertimeTab(s)
|
|
626
420
|
)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
`Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`
|
|
630
|
-
)
|
|
421
|
+
logDev(
|
|
422
|
+
`Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`
|
|
631
423
|
)
|
|
632
424
|
} catch (err) {
|
|
633
425
|
console.warn(
|
|
@@ -645,7 +437,7 @@ const main = async () => {
|
|
|
645
437
|
})
|
|
646
438
|
if (!opts.perPeriodOnly) {
|
|
647
439
|
writeTextFile(monthlyFile, monthlyContent)
|
|
648
|
-
|
|
440
|
+
logDev(`Overtime 月度汇总 已导出: ${monthlyFile}`)
|
|
649
441
|
}
|
|
650
442
|
// per-period Excel (sheets or files)
|
|
651
443
|
if (perPeriodFormats.includes('xlsx')) {
|
|
@@ -658,9 +450,7 @@ const main = async () => {
|
|
|
658
450
|
stats: opts.stats,
|
|
659
451
|
gerrit: opts.gerrit
|
|
660
452
|
})
|
|
661
|
-
|
|
662
|
-
chalk.green(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`)
|
|
663
|
-
)
|
|
453
|
+
logDev(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`)
|
|
664
454
|
} catch (err) {
|
|
665
455
|
console.warn(
|
|
666
456
|
'Export month XLSX (sheets) failed:',
|
|
@@ -710,22 +500,16 @@ const main = async () => {
|
|
|
710
500
|
const weekKeys = Object.keys(weekGroups).sort()
|
|
711
501
|
weekKeys.forEach((k) => {
|
|
712
502
|
const groupRecs = weekGroups[k]
|
|
713
|
-
const s =
|
|
714
|
-
startHour:
|
|
715
|
-
opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
716
|
-
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
717
|
-
lunchStart:
|
|
718
|
-
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
719
|
-
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
720
|
-
country: opts.country || 'CN'
|
|
721
|
-
})
|
|
503
|
+
const s = getOvertimeStats(groupRecs)
|
|
722
504
|
weeklyContent += `===== ${k} =====\n`
|
|
723
505
|
weeklyContent += `${renderOvertimeText(s)}\n\n`
|
|
724
506
|
try {
|
|
725
507
|
const perWeekFileName = `week/overtime_${outBase}_${k}.txt`
|
|
726
508
|
const perWeekFile = outputFilePath(perWeekFileName, outDir)
|
|
727
509
|
writeTextFile(perWeekFile, renderOvertimeText(s))
|
|
728
|
-
console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`))
|
|
510
|
+
// console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`))
|
|
511
|
+
logDev(`Overtime 周度(${k}) 已导出: ${perWeekFile}`)
|
|
512
|
+
|
|
729
513
|
// eslint-disable-next-line no-shadow
|
|
730
514
|
const perPeriodFormats = String(opts.perPeriodFormats || '')
|
|
731
515
|
.split(',')
|
|
@@ -743,10 +527,8 @@ const main = async () => {
|
|
|
743
527
|
outputFilePath(perWeekCsvName, outDir),
|
|
744
528
|
renderOvertimeCsv(s)
|
|
745
529
|
)
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
`Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`
|
|
749
|
-
)
|
|
530
|
+
logDev(
|
|
531
|
+
`Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`
|
|
750
532
|
)
|
|
751
533
|
} catch (err) {
|
|
752
534
|
console.warn(
|
|
@@ -762,10 +544,8 @@ const main = async () => {
|
|
|
762
544
|
outputFilePath(perWeekTabName, outDir),
|
|
763
545
|
renderOvertimeTab(s)
|
|
764
546
|
)
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
`Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`
|
|
768
|
-
)
|
|
547
|
+
logDev(
|
|
548
|
+
`Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`
|
|
769
549
|
)
|
|
770
550
|
} catch (err) {
|
|
771
551
|
console.warn(
|
|
@@ -782,7 +562,7 @@ const main = async () => {
|
|
|
782
562
|
}
|
|
783
563
|
})
|
|
784
564
|
writeTextFile(weeklyFile, weeklyContent)
|
|
785
|
-
|
|
565
|
+
logDev(`Overtime 周度汇总 已导出: ${weeklyFile}`)
|
|
786
566
|
} catch (err) {
|
|
787
567
|
console.warn(
|
|
788
568
|
'Generate weekly overtime failed:',
|
|
@@ -798,8 +578,8 @@ const main = async () => {
|
|
|
798
578
|
writeJSON(filepath, groups || records)
|
|
799
579
|
const jsonText = renderAuthorChangesJson(records)
|
|
800
580
|
writeJSON(outputFilePath('author-changes.json', outDir), jsonText)
|
|
801
|
-
|
|
802
|
-
spinner
|
|
581
|
+
logDev(`JSON 已导出: ${filepath}`)
|
|
582
|
+
handleSuccess({ startTime, spinner })
|
|
803
583
|
return
|
|
804
584
|
}
|
|
805
585
|
|
|
@@ -809,13 +589,16 @@ const main = async () => {
|
|
|
809
589
|
const text = renderText(records, groups, { showGerrit: !!opts.gerrit })
|
|
810
590
|
writeTextFile(filepath, text)
|
|
811
591
|
writeTextFile(
|
|
812
|
-
outputFilePath('author-changes.
|
|
592
|
+
outputFilePath('author-changes.txt', outDir),
|
|
813
593
|
renderChangedLinesText(records)
|
|
814
594
|
)
|
|
815
595
|
|
|
816
|
-
console.log(text)
|
|
817
|
-
|
|
818
|
-
|
|
596
|
+
console.log('\n Commits List:\n', text, '\n')
|
|
597
|
+
|
|
598
|
+
logDev(`文本已导出: ${filepath}`)
|
|
599
|
+
|
|
600
|
+
handleSuccess({ startTime, spinner })
|
|
601
|
+
|
|
819
602
|
return
|
|
820
603
|
}
|
|
821
604
|
|
|
@@ -835,9 +618,10 @@ const main = async () => {
|
|
|
835
618
|
)
|
|
836
619
|
const text = renderText(records, groups)
|
|
837
620
|
writeTextFile(txtPath, text)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
621
|
+
logDev(`Excel 已导出: ${excelPath}`)
|
|
622
|
+
logDev(`文本已自动导出: ${txtPath}`)
|
|
623
|
+
|
|
624
|
+
handleSuccess({ startTime, spinner })
|
|
841
625
|
}
|
|
842
626
|
|
|
843
627
|
await autoCheckUpdate()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// memoize.mjs
|
|
2
|
+
export function memoize(fn) {
|
|
3
|
+
const cache = new Map()
|
|
4
|
+
|
|
5
|
+
return function (...args) {
|
|
6
|
+
const key = JSON.stringify(args)
|
|
7
|
+
if (cache.has(key)) {
|
|
8
|
+
return cache.get(key)
|
|
9
|
+
}
|
|
10
|
+
const result = fn(...args)
|
|
11
|
+
cache.set(key, result)
|
|
12
|
+
return result
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { analyzeOvertime } from './overtime.mjs';
|
|
2
|
+
|
|
3
|
+
export function createOvertimeStats(defaultConfig = {}) {
|
|
4
|
+
return function getOvertimeStats(records, overrides) {
|
|
5
|
+
const config = overrides
|
|
6
|
+
? { ...defaultConfig, ...overrides }
|
|
7
|
+
: defaultConfig;
|
|
8
|
+
|
|
9
|
+
return analyzeOvertime(records, config);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -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 ->
|
|
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
|
-
|
|
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
|
|
32
|
-
const
|
|
25
|
+
const n = cleanName(name)
|
|
26
|
+
const e = cleanEmail(email)
|
|
33
27
|
|
|
34
|
-
if (!
|
|
28
|
+
if (!e) return n || 'Unknown'
|
|
35
29
|
|
|
36
|
-
// 记录原始 author
|
|
37
|
-
if (!originalMap[
|
|
38
|
-
if (
|
|
30
|
+
// 记录原始 author
|
|
31
|
+
if (!originalMap[e]) originalMap[e] = new Set()
|
|
32
|
+
if (n) originalMap[e].add(n)
|
|
39
33
|
|
|
40
|
-
const
|
|
34
|
+
const prev = map[e]
|
|
41
35
|
|
|
42
|
-
//
|
|
43
|
-
if (
|
|
44
|
-
map[
|
|
45
|
-
return
|
|
36
|
+
// 中文名优先覆盖
|
|
37
|
+
if (isChinese(n)) {
|
|
38
|
+
map[e] = n
|
|
39
|
+
return n
|
|
46
40
|
}
|
|
47
41
|
|
|
48
|
-
//
|
|
49
|
-
if (isChinese(
|
|
50
|
-
|
|
51
|
-
|
|
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 {
|