wukong-gitlog-cli 1.0.37 → 1.0.39

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/.eslintrc CHANGED
@@ -24,6 +24,7 @@
24
24
  "consistent-return": "off",
25
25
  "no-await-in-loop": "warn",
26
26
  "no-unused-vars": "warn",
27
+ "no-nested-ternary": "warn",
27
28
  "no-console": "off",
28
29
  "no-plusplus": "off",
29
30
  "no-unresolved": "off",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
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.39](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.38...v1.0.39) (2025-12-29)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 web report add date filter ([d5968d8](https://github.com/tomatobybike/wukong-gitlog-cli/commit/d5968d8222aa1129e60894234ac9fca6c91e5a73))
11
+ * 🎸 wukong-gitlog command entry ([48b5698](https://github.com/tomatobybike/wukong-gitlog-cli/commit/48b569842114cd11336ee23580104048342e085b))
12
+
13
+ ### [1.0.38](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.37...v1.0.38) (2025-12-27)
14
+
15
+
16
+ ### Features
17
+
18
+ * 🎸 增加性能分析 ([ec925cc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/ec925cc3d20be2a0eee6e20aaee99b4449eff9be))
19
+ * 🎸 flame 和 显示 flame-like 日志 ([7b1a00c](https://github.com/tomatobybike/wukong-gitlog-cli/commit/7b1a00c26afa1757637e6c4a3a2be92d74e0ad65))
20
+ * 🎸 ignore ([eb3e5bc](https://github.com/tomatobybike/wukong-gitlog-cli/commit/eb3e5bc89183e0722056036f14f74806e3c3b1e3))
21
+ * 🎸 profiler ([48fd0a5](https://github.com/tomatobybike/wukong-gitlog-cli/commit/48fd0a5a8b25b55bbca7bbdbd2df11158049adcd))
22
+ * 🎸 Profiler ([a7f4623](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a7f4623a162947d31f25702052f54a4ff0406fcb))
23
+
5
24
  ### [1.0.37](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.36...v1.0.37) (2025-12-12)
6
25
 
7
26
 
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
@@ -34,43 +34,46 @@
34
34
  "author": "Tom <tomatobybike@gmail.com>",
35
35
  "type": "module",
36
36
  "bin": {
37
+ "wukong-gitlog": "./bin/wukong-gitlog-cli",
37
38
  "wukong-gitlog-cli": "./bin/wukong-gitlog-cli"
38
39
  },
39
40
  "scripts": {
41
+ "build": "node scripts/esbuild.config.mjs",
42
+ "check:npm:login": "node scripts/check-npm-login.mjs",
40
43
  "cli:excel-demo": "node ./src/cli.mjs --overtime --format excel --stats --limit 5 --out commits.xlsx",
41
44
  "cli:excel-demo-parent": "node ./src/cli.mjs --out-parent --format excel --stats --limit 5 --out commits-parent.xlsx",
42
45
  "cli:gerrit-changeid-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{changeId}}\" --limit 5 --out commits-gerrit-changeid.txt",
43
46
  "cli:gerrit-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{hash}}\" --limit 5 --out commits-gerrit.txt",
44
47
  "cli:help": "node ./src/cli.mjs --help",
45
48
  "cli:json-demo": "node ./src/cli.mjs --json --limit 5 --out commits.json",
46
- "cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
47
- "cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
48
49
  "cli:overtime-demo": "node ./src/cli.mjs --overtime --format text --limit 200",
49
- "cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
50
50
  "cli:overtime-excel": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx",
51
- "cli:overtime-text-us": "node ./src/cli.mjs --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
52
- "cli:overtime-text-us-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
53
- "cli:overtime-text-us-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
54
51
  "cli:overtime-excel-cn": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx --country CN",
55
- "cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
56
52
  "cli:overtime-excel-cn-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format excel --out commits.xlsx --country CN",
53
+ "cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
57
54
  "cli:overtime-per-period-csv-tab": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab",
58
- "cli:overtime-per-period-xlsx-sheets": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets",
59
- "cli:overtime-per-period-xlsx-files": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files",
60
55
  "cli:overtime-per-period-only": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-only",
56
+ "cli:overtime-per-period-xlsx-files": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files",
57
+ "cli:overtime-per-period-xlsx-sheets": "node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets",
58
+ "cli:overtime-text": "node ./src/cli.mjs --overtime --limit 100 --format text --out commits.txt",
59
+ "cli:overtime-text-us": "node ./src/cli.mjs --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
60
+ "cli:overtime-text-us-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
61
+ "cli:overtime-text-us-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format text --out commits.txt --country US --work-start 10 --work-end 19 --lunch-start 12 --lunch-end 13",
61
62
  "cli:serve": "node ./src/cli.mjs --serve --overtime --format text --out commits.txt",
62
63
  "cli:serve-only": "node ./src/cli.mjs --serve-only",
64
+ "cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
65
+ "cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
63
66
  "format": "prettier --write \"src/**/*.{js,mjs}\"",
64
67
  "lint": "eslint src --ext .js,.mjs src",
65
68
  "lint:fix": "eslint src --ext .js,.mjs --fix",
66
69
  "pack": "npm pack",
67
- "build": "node scripts/esbuild.config.mjs",
68
70
  "prepare": "husky install",
69
- "prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
70
71
  "prepublishOnly": "yarn lint && yarn build",
71
- "check:npm:login": "node scripts/check-npm-login.mjs",
72
- "release": "yarn check:npm:login && yarn release:patch && yarn push:tags && npm publish",
72
+ "prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
73
73
  "push:tags": "git push origin main --follow-tags",
74
+ "prerelease": "yarn lint",
75
+ "release": "yarn check:npm:login && yarn release:patch && npm publish && yarn push:tags",
76
+ "release:rollback": "git reset --hard HEAD~1 && git tag -d $(git tag --points-at HEAD~1)",
74
77
  "release:major": "yarn lint && standard-version --release-as major",
75
78
  "release:minor": "yarn lint && standard-version --release-as minor",
76
79
  "release:patch": "yarn lint && standard-version --release-as patch",
@@ -94,15 +97,16 @@
94
97
  "boxen": "8.0.1",
95
98
  "chalk": "5.6.2",
96
99
  "commander": "12.1.0",
100
+ "date-fns": "4.1.0",
97
101
  "date-holidays": "2.1.1",
98
102
  "dayjs": "1.11.19",
99
- "date-fns": "4.1.0",
100
103
  "dotenv": "17.2.2",
101
104
  "exceljs": "4.4.0",
102
105
  "is-online": "12.0.2",
103
106
  "ora": "9.0.0",
104
107
  "semver": "6.3.1",
105
108
  "string-width": "5.1.2",
109
+ "wukong-profiler": "^1.0.6",
106
110
  "zx": "7.2.4"
107
111
  },
108
112
  "devDependencies": {
package/src/index.mjs CHANGED
@@ -6,6 +6,7 @@ import fs from 'fs'
6
6
  import ora from 'ora'
7
7
  import path from 'path'
8
8
  import { fileURLToPath } from 'url'
9
+ import { createProfiler } from 'wukong-profiler'
9
10
 
10
11
  import { parseOptions } from './cli/parseOptions.mjs'
11
12
  // eslint-disable-next-line no-unused-vars
@@ -50,6 +51,8 @@ dayjs.extend(isoWeek)
50
51
  const PKG_NAME = pkg.name
51
52
  const VERSION = pkg.version
52
53
 
54
+ let profiler
55
+
53
56
  const autoCheckUpdate = async () => {
54
57
  // === CLI 主逻辑完成后提示更新 ===
55
58
  await checkUpdateWithPatch({
@@ -83,8 +86,6 @@ export function getWeekRange(periodStr) {
83
86
  }
84
87
 
85
88
  const main = async () => {
86
- const startTime = performance.now()
87
-
88
89
  const program = new Command()
89
90
 
90
91
  program
@@ -185,12 +186,31 @@ const main = async () => {
185
186
  '仅启动 web 服务,不导出或分析数据(使用 output-wukong/data 中已有的数据)'
186
187
  )
187
188
  .option('--version', 'show version information')
189
+ .option('--profile', '输出性能分析 JSON')
190
+ .option('--verbose', '显示详细性能日志')
191
+ .option('--flame', '显示 flame-like 日志')
192
+ .option('--trace <file>', '生成 Chrome Trace')
193
+ .option('--hot-threshold <n>', 'HOT 比例阈值', parseFloat, 0.8)
194
+ .option('--fail-on-hot', 'HOT 时 CI 失败')
195
+ .option('--diff-base <file>', '基线 profile.json')
196
+ .option('--diff-threshold <n>', '回归阈值', parseFloat, 0.2)
188
197
  .parse()
189
198
 
190
199
  const opts = program.opts()
191
200
 
192
201
  const config = parseOptions(opts)
193
202
 
203
+ profiler = createProfiler({
204
+ enabled: opts.profile,
205
+ verbose: opts.verbose,
206
+ flame: opts.flame,
207
+ traceFile: opts.trace,
208
+ hotThreshold: opts.hotThreshold,
209
+ failOnHot: opts.failOnHot,
210
+ diffBaseFile: opts.diffBase,
211
+ diffThreshold: opts.diffThreshold
212
+ })
213
+
194
214
  // ❗只创建一次缓存实例
195
215
  const getOvertimeStats = createOvertimeStats(config)
196
216
 
@@ -324,6 +344,7 @@ const main = async () => {
324
344
 
325
345
  // --- 分组 ---
326
346
  const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
347
+ profiler.step('load config')
327
348
 
328
349
  // If serve mode is enabled, write data modules and launch the web server
329
350
  if (opts.serve) {
@@ -332,7 +353,12 @@ const main = async () => {
332
353
 
333
354
  // --- Overtime analysis ---
334
355
  if (opts.overtime) {
356
+ await profiler.stepAsync('getOvertimeStats', async () => {
357
+ await getOvertimeStats(records)
358
+ })
335
359
  const stats = getOvertimeStats(records)
360
+
361
+ profiler.step('load getOvertimeStats')
336
362
  // Output to console
337
363
  console.log('\n--- Overtime analysis ---\n')
338
364
  console.log(renderOvertimeText(stats))
@@ -579,7 +605,7 @@ const main = async () => {
579
605
  const jsonText = renderAuthorChangesJson(records)
580
606
  writeJSON(outputFilePath('author-changes.json', outDir), jsonText)
581
607
  logDev(`JSON 已导出: ${filepath}`)
582
- handleSuccess({ startTime, spinner })
608
+ handleSuccess({ spinner })
583
609
  return
584
610
  }
585
611
 
@@ -593,11 +619,11 @@ const main = async () => {
593
619
  renderChangedLinesText(records)
594
620
  )
595
621
 
596
- console.log('\n Commits List:\n', text, '\n')
622
+ // console.log('\n Commits List:\n', text, '\n')
597
623
 
598
624
  logDev(`文本已导出: ${filepath}`)
599
625
 
600
- handleSuccess({ startTime, spinner })
626
+ handleSuccess({ spinner })
601
627
 
602
628
  return
603
629
  }
@@ -621,10 +647,32 @@ const main = async () => {
621
647
  logDev(`Excel 已导出: ${excelPath}`)
622
648
  logDev(`文本已自动导出: ${txtPath}`)
623
649
 
624
- handleSuccess({ startTime, spinner })
650
+ handleSuccess({ spinner })
625
651
  }
626
652
 
627
653
  await autoCheckUpdate()
654
+
655
+ handleSuccess({ spinner })
628
656
  }
629
657
 
630
- main()
658
+ try {
659
+ await main()
660
+ } catch (err) {
661
+ console.error(err)
662
+ process.exitCode = 1
663
+ } finally {
664
+ if (profiler) {
665
+ const result = profiler.end('git-commits')
666
+
667
+ // --profile 时输出 JSON
668
+ if (process.argv.includes('--profile')) {
669
+ const json = {
670
+ command: 'git-commits',
671
+ version: VERSION,
672
+ timestamp: Date.now(),
673
+ profile: result
674
+ }
675
+ console.log(JSON.stringify(json, null, 2))
676
+ }
677
+ }
678
+ }
@@ -4,9 +4,6 @@ import { performance } from 'perf_hooks'
4
4
  import { exitWithTime } from './exitWithTime.mjs'
5
5
  import { costTimer } from './timer.mjs'
6
6
 
7
- export const handleSuccess = ({ message = 'Done', startTime, spinner }) => {
7
+ export const handleSuccess = ({ message = 'Done', spinner }) => {
8
8
  spinner.succeed(message)
9
-
10
- // 如果需要退出,可以调用 exitWithTime
11
- exitWithTime(startTime, 0)
12
9
  }
@@ -0,0 +1,26 @@
1
+ export const diffProfiles = (prev, curr, threshold = 0.2) => {
2
+ const map = new Map()
3
+
4
+ prev.events.forEach(e => {
5
+ map.set(e.name, e.duration)
6
+ })
7
+
8
+ const regressions = []
9
+
10
+ curr.events.forEach(e => {
11
+ const before = map.get(e.name)
12
+ if (!before) return
13
+
14
+ const diff = (e.duration - before) / before
15
+ if (diff >= threshold) {
16
+ regressions.push({
17
+ name: e.name,
18
+ before,
19
+ after: e.duration,
20
+ diff
21
+ })
22
+ }
23
+ })
24
+
25
+ return regressions
26
+ }
@@ -0,0 +1,11 @@
1
+ export const formatTime = (ms) => {
2
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
3
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
4
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
5
+ return `${(ms / 60000).toFixed(2)} min`
6
+ }
7
+
8
+ export const makeBar = (ratio, width = 32) => {
9
+ const len = Math.max(1, Math.round(ratio * width))
10
+ return '█'.repeat(len)
11
+ }
@@ -0,0 +1,144 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs'
3
+ import { formatTime, makeBar } from './format.mjs'
4
+ import { exportChromeTrace } from './trace.mjs'
5
+ import { diffProfiles } from './diff.mjs'
6
+
7
+ export const createProfiler = ({
8
+ enabled = false,
9
+ verbose = false,
10
+ flame = false,
11
+ slowThreshold = 500,
12
+ hotThreshold = 0.8,
13
+ traceFile,
14
+ failOnHot = false,
15
+ diffBaseFile,
16
+ diffThreshold = 0.2
17
+ } = {}) => {
18
+ const start = process.hrtime.bigint()
19
+
20
+
21
+ const stack = []
22
+ const events = []
23
+
24
+ const toMs = (a, b) => Number(b - a) / 1e6
25
+
26
+ /* ================= step ================= */
27
+
28
+ const step = (name, fn) => {
29
+ const parent = stack[stack.length - 1]
30
+ const startTime = process.hrtime.bigint()
31
+
32
+ const node = {
33
+ name,
34
+ start: toMs(start, startTime),
35
+ duration: 0,
36
+ depth: stack.length,
37
+ children: []
38
+ }
39
+
40
+ if (parent) parent.children.push(node)
41
+ stack.push(node)
42
+
43
+ const finish = () => {
44
+ const endTime = process.hrtime.bigint()
45
+ node.duration = toMs(startTime, endTime)
46
+ events.push(node)
47
+ stack.pop()
48
+ }
49
+
50
+ if (typeof fn === 'function') {
51
+ try {
52
+ return fn()
53
+ } finally {
54
+ finish()
55
+ }
56
+ } else {
57
+ finish()
58
+ }
59
+ }
60
+
61
+ /* ================= end ================= */
62
+
63
+ const end = (label = 'Total') => {
64
+ const total = toMs(start, process.hrtime.bigint())
65
+
66
+ let hasHot = false
67
+
68
+ if (enabled || verbose) {
69
+ console.log(
70
+ chalk.cyan('⏱'),
71
+ chalk.bold(label),
72
+ chalk.yellow(formatTime(total))
73
+ )
74
+
75
+ if (flame) {
76
+ for (const e of events) {
77
+ const ratio = e.duration / total
78
+ const hot = ratio >= hotThreshold
79
+ const slow = e.duration >= slowThreshold
80
+
81
+ if (hot) hasHot = true
82
+
83
+ console.log(
84
+ chalk.gray(`${' '.repeat(e.depth) }├─`),
85
+ chalk.white(e.name.padEnd(22)),
86
+ chalk.yellow(formatTime(e.duration)),
87
+ chalk.gray(makeBar(ratio)),
88
+ hot
89
+ ? chalk.red.bold(' 🔥 HOT')
90
+ : slow
91
+ ? chalk.yellow(' ⚠ SLOW')
92
+ : ''
93
+ )
94
+ }
95
+ }
96
+ }
97
+
98
+ const profile = { total, events }
99
+
100
+ /* ---------- Chrome Trace ---------- */
101
+
102
+ if (traceFile) {
103
+ exportChromeTrace(events, traceFile)
104
+ }
105
+
106
+ /* ---------- diff ---------- */
107
+ if (diffBaseFile && fs.existsSync(diffBaseFile)) {
108
+ const base = JSON.parse(fs.readFileSync(diffBaseFile, 'utf8'))
109
+ const regressions = diffProfiles(base, profile, diffThreshold)
110
+
111
+ if (regressions.length) {
112
+ console.log(chalk.red('\n⚠ Performance Regression Detected:\n'))
113
+ regressions.forEach(r => {
114
+ console.log(
115
+ chalk.red(
116
+ ` ${r.name}: ${formatTime(r.before)} → ${formatTime(
117
+ r.after
118
+ )} (+${(r.diff * 100).toFixed(1)}%)`
119
+ )
120
+ )
121
+ })
122
+ process.exitCode = 1
123
+ }
124
+ }
125
+
126
+ /* ---------- save profile ---------- */
127
+ if (enabled) {
128
+ fs.writeFileSync(
129
+ 'profile.json',
130
+ JSON.stringify(profile, null, 2)
131
+ )
132
+ }
133
+
134
+ /* ---------- CI HOT fail ---------- */
135
+ if (failOnHot && hasHot) {
136
+ console.log(chalk.red('\n🔥 HOT step detected — failing CI'))
137
+ process.exitCode = 1
138
+ }
139
+
140
+ return profile
141
+ }
142
+
143
+ return { step, end }
144
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'fs'
2
+
3
+ export const exportChromeTrace = (events, file) => {
4
+ const traceEvents = []
5
+
6
+ for (const e of events) {
7
+ traceEvents.push({
8
+ name: e.name,
9
+ cat: 'cli',
10
+ ph: 'X', // complete event
11
+ ts: e.start * 1000, // μs
12
+ dur: e.duration * 1000, // μs
13
+ pid: 1,
14
+ tid: e.depth || 0
15
+ })
16
+ }
17
+
18
+ const trace = {
19
+ traceEvents,
20
+ displayTimeUnit: 'ms'
21
+ }
22
+
23
+ fs.writeFileSync(file, JSON.stringify(trace, null, 2))
24
+ // TODO: remove debug log before production
25
+ console.log('✅', 'file', file);
26
+ }
@@ -0,0 +1,101 @@
1
+ import chalk from 'chalk'
2
+
3
+ /* ================= utils ================= */
4
+
5
+ const formatTime = (ms) => {
6
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
7
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
8
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
9
+ return `${(ms / 60000).toFixed(2)} min`
10
+ }
11
+
12
+ const makeBar = (ratio, width = 32) => {
13
+ const len = Math.max(1, Math.round(ratio * width))
14
+ return '█'.repeat(len)
15
+ }
16
+
17
+ /* ================= profiler ================= */
18
+
19
+ export const createProfiler = ({
20
+ enabled = false,
21
+ verbose = false,
22
+ flame = false,
23
+ slowThreshold = 500, // ms
24
+ hotThreshold = 0.8 // ratio (0~1)
25
+ } = {}) => {
26
+ const start = process.hrtime.bigint()
27
+ let last = start
28
+ const events = []
29
+
30
+ const toMs = (a, b) => Number(b - a) / 1e6
31
+
32
+ /* -------- step -------- */
33
+ const step = (name, meta = {}) => {
34
+ const now = process.hrtime.bigint()
35
+ const duration = toMs(last, now)
36
+ last = now
37
+
38
+ events.push({
39
+ name,
40
+ duration,
41
+ sinceStart: toMs(start, now),
42
+ meta
43
+ })
44
+
45
+ if (!enabled && !verbose) return
46
+
47
+ const isSlow = duration >= slowThreshold
48
+
49
+ console.log(
50
+ chalk.gray(' ├─'),
51
+ isSlow ? chalk.red.bold(name) : chalk.white(name),
52
+ chalk.yellow(formatTime(duration))
53
+ )
54
+ }
55
+
56
+ /* -------- end -------- */
57
+ const end = (label = 'Total') => {
58
+ const endTime = process.hrtime.bigint()
59
+ const total = toMs(start, endTime)
60
+
61
+ // 计算 HOT
62
+ for (const e of events) {
63
+ e.ratio = total > 0 ? e.duration / total : 0
64
+ e.hot = e.ratio >= hotThreshold
65
+ }
66
+
67
+ if (enabled || verbose) {
68
+ console.log(
69
+ chalk.cyan('⏱'),
70
+ chalk.bold(label),
71
+ chalk.yellow(formatTime(total))
72
+ )
73
+
74
+ if (flame) {
75
+ for (const e of events) {
76
+ const bar = makeBar(e.ratio)
77
+ const hotMark = e.hot ? chalk.red.bold(' 🔥 HOT') : ''
78
+ const slowMark =
79
+ !e.hot && e.duration >= slowThreshold
80
+ ? chalk.yellow(' ⚠ SLOW')
81
+ : ''
82
+
83
+ console.log(
84
+ chalk.gray(' ├─'),
85
+ chalk.white(e.name.padEnd(24)),
86
+ chalk.yellow(formatTime(e.duration)),
87
+ chalk.gray(bar),
88
+ hotMark || slowMark
89
+ )
90
+ }
91
+ }
92
+ }
93
+
94
+ return {
95
+ total,
96
+ events
97
+ }
98
+ }
99
+
100
+ return { step, end }
101
+ }
@@ -0,0 +1,37 @@
1
+ // utils/scopeTimer.js
2
+ import chalk from 'chalk'
3
+
4
+ const formatTime = (ms) => {
5
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
6
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
7
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
8
+ return `${(ms / 60000).toFixed(2)} min`
9
+ }
10
+
11
+ export const createScopeTimer = (scope) => {
12
+ const start = process.hrtime.bigint()
13
+ let last = start
14
+
15
+ const step = (label) => {
16
+ const now = process.hrtime.bigint()
17
+ const ms = Number(now - last) / 1e6
18
+ last = now
19
+
20
+ console.log(
21
+ chalk.gray(' ├─'),
22
+ chalk.white(label),
23
+ chalk.yellow(formatTime(ms))
24
+ )
25
+ }
26
+
27
+ const end = () => {
28
+ const total = Number(process.hrtime.bigint() - start) / 1e6
29
+ console.log(
30
+ chalk.cyan('⏱'),
31
+ chalk.bold(scope),
32
+ chalk.yellow(formatTime(total))
33
+ )
34
+ }
35
+
36
+ return { step, end }
37
+ }
@@ -0,0 +1,33 @@
1
+ // utils/timer.js
2
+ import chalk from 'chalk'
3
+
4
+ const ENABLED = process.env.CLI_TIMER !== 'false'
5
+
6
+ const formatTime = (ms) => {
7
+ if (ms < 1) return `${(ms * 1000).toFixed(2)} μs`
8
+ if (ms < 1000) return `${ms.toFixed(2)} ms`
9
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)} s`
10
+ return `${(ms / 60000).toFixed(2)} min`
11
+ }
12
+
13
+ export const createTimer = (label, { silent = false } = {}) => {
14
+ const start = process.hrtime.bigint()
15
+
16
+ const stop = (suffix) => {
17
+ if (!ENABLED || silent) return
18
+
19
+ const end = process.hrtime.bigint()
20
+ const ms = Number(end - start) / 1e6
21
+
22
+ console.log(
23
+ chalk.cyan('⏱'),
24
+ chalk.white(label),
25
+ suffix ? chalk.gray(`(${suffix})`) : '',
26
+ chalk.bold.yellow(formatTime(ms))
27
+ )
28
+
29
+ return ms
30
+ }
31
+
32
+ return { stop }
33
+ }
@@ -0,0 +1,11 @@
1
+ // utils/withTimer.js
2
+ import { createTimer } from './timer.mjs'
3
+
4
+ export const withTimer = async (label, fn, opts) => {
5
+ const timer = createTimer(label, opts)
6
+ try {
7
+ return await fn()
8
+ } finally {
9
+ timer.stop()
10
+ }
11
+ }
package/web/app.js CHANGED
@@ -3,6 +3,22 @@
3
3
  /* global echarts */
4
4
  const formatDate = (d) => new Date(d).toLocaleString()
5
5
 
6
+ function filterByDate(commits) {
7
+ const start = document.getElementById('startDate')?.value
8
+ const end = document.getElementById('endDate')?.value
9
+
10
+ if (!start && !end) return commits
11
+
12
+ const startTime = start ? new Date(`${start}T00:00:00`).getTime() : -Infinity
13
+
14
+ const endTime = end ? new Date(`${end}T23:59:59`).getTime() : Infinity
15
+
16
+ return commits.filter((c) => {
17
+ const t = new Date(c.date).getTime()
18
+ return t >= startTime && t <= endTime
19
+ })
20
+ }
21
+
6
22
  // ISO 周 key:YYYY-Www
7
23
  function getIsoWeekKey(dStr) {
8
24
  const d = new Date(dStr)
@@ -100,7 +116,8 @@ function renderCommitsTablePage() {
100
116
  tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${c.email}</td><td>${formatDate(c.date)}</td><td>${c.message}</td><td>${c.changed}</td>`
101
117
  tbody.appendChild(tr)
102
118
  })
103
- document.getElementById('commitsTotal').textContent = `共${filtered.length}条记录`
119
+ document.getElementById('commitsTotal').textContent =
120
+ `共${filtered.length}条记录`
104
121
  }
105
122
 
106
123
  function updatePager() {
@@ -115,16 +132,25 @@ function updatePager() {
115
132
  function applySearch() {
116
133
  const q = document.getElementById('searchInput').value.trim().toLowerCase()
117
134
 
135
+ // ① 先做日期过滤
136
+ const base = filterByDate(commitsAll)
137
+
118
138
  if (!q) {
119
- filtered = commitsAll.slice()
139
+ filtered = base.slice()
120
140
  } else {
121
- filtered = commitsAll.filter((c) => {
141
+ filtered = base.filter((c) => {
122
142
  const h = c.hash.toLowerCase()
123
143
  const a = String(c.author || '').toLowerCase()
124
144
  const e = String(c.email || '').toLowerCase()
125
145
  const m = String(c.message || '').toLowerCase()
126
146
  const d = formatDate(c.date).toLowerCase()
127
- return h.includes(q) || a.includes(q)|| e.includes(q) || m.includes(q) || d.includes(q)
147
+ return (
148
+ h.includes(q) ||
149
+ a.includes(q) ||
150
+ e.includes(q) ||
151
+ m.includes(q) ||
152
+ d.includes(q)
153
+ )
128
154
  })
129
155
  }
130
156
  page = 1
@@ -134,6 +160,13 @@ function applySearch() {
134
160
 
135
161
  function initTableControls() {
136
162
  document.getElementById('searchInput').addEventListener('input', applySearch)
163
+ document.getElementById('startDate')?.addEventListener('change', applySearch)
164
+ document.getElementById('endDate')?.addEventListener('change', applySearch)
165
+ document.getElementById('clearDate')?.addEventListener('click', () => {
166
+ document.getElementById('startDate').value = ''
167
+ document.getElementById('endDate').value = ''
168
+ applySearch()
169
+ })
137
170
  document.getElementById('pageSize').addEventListener('change', (e) => {
138
171
  pageSize = parseInt(e.target.value, 10) || 10
139
172
  page = 1
package/web/index.html CHANGED
@@ -109,6 +109,10 @@
109
109
  type="search"
110
110
  placeholder="搜索作者/信息/Hash/Date"
111
111
  />
112
+ <input type="date" id="startDate" />
113
+ <span>~</span>
114
+ <input type="date" id="endDate" />
115
+ <button id="clearDate">清除</button>
112
116
  <label for="pageSize">每页显示</label>
113
117
  <select id="pageSize">
114
118
  <option value="10">10</option>
@@ -139,6 +139,45 @@ td {
139
139
  box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
140
140
  outline: none;
141
141
  }
142
+ #tableControls input[type='date'] {
143
+ padding: 9px 12px;
144
+ border-radius: 8px;
145
+ border: 1px solid #ccc;
146
+ background: #fafafa;
147
+ font-size: 13px;
148
+ color: #111827;
149
+ transition:
150
+ border-color 0.2s,
151
+ box-shadow 0.2s;
152
+ }
153
+
154
+ #tableControls input[type='date']:focus {
155
+ border-color: #1976d2;
156
+ box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
157
+ outline: none;
158
+ }
159
+ /* 清除日期按钮 ------------------------------------------ */
160
+ #clearDate {
161
+ padding: 8px 10px;
162
+ border-radius: 8px;
163
+ border: none;
164
+ background: transparent;
165
+ color: #64748b; /* slate-500 */
166
+ font-size: 13px;
167
+ cursor: pointer;
168
+ transition:
169
+ color 0.2s ease,
170
+ background 0.2s ease;
171
+ }
172
+
173
+ #clearDate:hover {
174
+ color: #ef4444; /* red-500 */
175
+ background: rgba(239, 68, 68, 0.08);
176
+ }
177
+
178
+ #clearDate:active {
179
+ background: rgba(239, 68, 68, 0.16);
180
+ }
142
181
 
143
182
  /* 按钮 — MUI Button 风格 */
144
183
  .pager button {