wukong-gitlog-cli 0.0.15 → 1.0.2
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 +20 -0
- package/bin/wukong-gitlog-cli +1 -1
- package/package.json +5 -3
- package/src/cli.mjs +460 -236
- package/src/constants/index.mjs +3 -0
- package/src/git.mjs +2 -2
- package/src/utils/checkUpdate.mjs +130 -0
- package/src/utils/colors.mjs +56 -0
- package/src/utils/showVersionInfo.mjs +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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.2](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.1...v1.0.2) (2025-11-28)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 await zx ([79343d6](https://github.com/tomatobybike/wukong-gitlog-cli/commit/79343d6722c19b8c6381211b1a9b35923443abb5))
|
|
11
|
+
|
|
12
|
+
### [1.0.1](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.15...v1.0.1) (2025-11-28)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* 🎸 show version ([4a81b72](https://github.com/tomatobybike/wukong-gitlog-cli/commit/4a81b7222100521dcacda4d1d8ff96db2d669a3c))
|
|
18
|
+
* 🎸 version ([8fa81d6](https://github.com/tomatobybike/wukong-gitlog-cli/commit/8fa81d64e1a6c44e4b4327fbb6a27559ed2b5372))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* 🐛 eslint ([17701b2](https://github.com/tomatobybike/wukong-gitlog-cli/commit/17701b248802532a63d5b5896cf331de29e07c07))
|
|
24
|
+
|
|
5
25
|
### [0.0.15](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.14...v0.0.15) (2025-11-28)
|
|
6
26
|
|
|
7
27
|
|
package/bin/wukong-gitlog-cli
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import('../src/cli.mjs')
|
|
2
|
+
import('../src/cli.mjs')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-gitlog-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -90,13 +90,15 @@
|
|
|
90
90
|
]
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"chalk": "5.6.0",
|
|
94
93
|
"commander": "12.1.0",
|
|
95
94
|
"dayjs": "1.11.19",
|
|
96
95
|
"date-holidays": "2.1.1",
|
|
97
96
|
"string-width": "5.1.2",
|
|
98
97
|
"exceljs": "4.4.0",
|
|
99
|
-
"zx": "7.2.4"
|
|
98
|
+
"zx": "7.2.4",
|
|
99
|
+
"is-online": "12.0.2",
|
|
100
|
+
"boxen": "8.0.1",
|
|
101
|
+
"chalk": "5.6.2"
|
|
100
102
|
},
|
|
101
103
|
"devDependencies": {
|
|
102
104
|
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
package/src/cli.mjs
CHANGED
|
@@ -1,405 +1,629 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { analyzeOvertime, renderOvertimeText, renderOvertimeTab, renderOvertimeCsv } from './overtime.mjs';
|
|
7
|
-
import { startServer } from './server.mjs';
|
|
8
|
-
import { exportExcel, exportExcelPerPeriodSheets } from './excel.mjs';
|
|
9
|
-
import { groupRecords, writeJSON, writeTextFile, outputFilePath } from './utils/index.mjs';
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
import { CLI_NAME } from './constants/index.mjs'
|
|
8
|
+
import { exportExcel, exportExcelPerPeriodSheets } from './excel.mjs'
|
|
9
|
+
import { getGitLogs } from './git.mjs'
|
|
10
|
+
import {
|
|
11
|
+
analyzeOvertime,
|
|
12
|
+
renderOvertimeCsv,
|
|
13
|
+
renderOvertimeTab,
|
|
14
|
+
renderOvertimeText
|
|
15
|
+
} from './overtime.mjs'
|
|
16
|
+
import { startServer } from './server.mjs'
|
|
17
|
+
import { renderText } from './text.mjs'
|
|
18
|
+
import { checkUpdateWithPatch } from './utils/checkUpdate.mjs'
|
|
19
|
+
import {
|
|
20
|
+
groupRecords,
|
|
21
|
+
outputFilePath,
|
|
22
|
+
writeJSON,
|
|
23
|
+
writeTextFile
|
|
24
|
+
} from './utils/index.mjs'
|
|
25
|
+
import { showVersionInfo } from './utils/showVersionInfo.mjs'
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
.name('git-commits')
|
|
15
|
-
.description('Advanced Git commit log exporter.')
|
|
16
|
-
.option('--author <name>', '指定 author 名')
|
|
17
|
-
.option('--email <email>', '指定 email')
|
|
18
|
-
.option('--since <date>', '起始日期')
|
|
19
|
-
.option('--until <date>', '结束日期')
|
|
20
|
-
.option('--limit <n>', '限制数量', parseInt)
|
|
21
|
-
.option('--no-merges', '不包含 merge commit')
|
|
22
|
-
.option('--json', '输出 JSON')
|
|
23
|
-
.option('--format <type>', '输出格式: text | excel | json', 'text')
|
|
24
|
-
.option('--group-by <type>', '按日期分组: day | month | week')
|
|
25
|
-
.option('--stats', '输出每日统计数据')
|
|
26
|
-
.option('--gerrit <prefix>', '显示 Gerrit 地址,支持在 prefix 中使用 {{hash}} 占位符')
|
|
27
|
-
.option('--gerrit-api <url>', '可选:Gerrit REST API 基础地址,用于解析 changeNumber,例如 `https://gerrit.example.com`')
|
|
28
|
-
.option('--gerrit-auth <tokenOrUserPass>', '可选:Gerrit API 授权,格式为 `user:pass` 或 `TOKEN`(表示 Bearer token)')
|
|
29
|
-
.option('--overtime', '分析公司加班文化(输出下班时间与非工作日提交占比)')
|
|
30
|
-
.option('--country <code>', '节假日国家:CN 或 US,默认为 CN', 'CN')
|
|
31
|
-
.option('--work-start <hour>', '上班开始小时,默认 9', (v) => parseInt(v, 10), 9)
|
|
32
|
-
.option('--work-end <hour>', '下班小时,默认 18', (v) => parseInt(v, 10), 18)
|
|
33
|
-
.option('--lunch-start <hour>', '午休开始小时,默认 12', (v) => parseInt(v, 10), 12)
|
|
34
|
-
.option('--lunch-end <hour>', '午休结束小时,默认 14', (v) => parseInt(v, 10), 14)
|
|
35
|
-
.option('--out <file>', '输出文件名(不含路径)')
|
|
36
|
-
.option('--out-dir <dir>', '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output`')
|
|
37
|
-
.option('--out-parent', '将输出目录放到当前工程的父目录的 `output/`(等同于 `--out-dir ../output`)')
|
|
38
|
-
.option('--per-period-formats <formats>', '每个周期单独输出的格式,逗号分隔:text,csv,tab,xlsx。默认为空(不输出 CSV/Tab/XLSX)', '')
|
|
39
|
-
.option('--per-period-excel-mode <mode>', 'per-period Excel 模式:sheets|files(默认:sheets)', 'sheets')
|
|
40
|
-
.option('--per-period-only', '仅输出 per-period(month/week)文件,不输出合并的 monthly/weekly 汇总文件')
|
|
41
|
-
.option('--serve', '启动本地 web 服务,查看提交统计(将在 output/data 下生成数据文件)')
|
|
42
|
-
.option('--port <n>', '本地 web 服务端口(默认 3000)', (v) => parseInt(v, 10), 3000)
|
|
43
|
-
.option('--serve-only', '仅启动 web 服务,不导出或分析数据(使用 output/data 中已有的数据)')
|
|
44
|
-
.parse();
|
|
27
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
45
28
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? path.resolve(process.cwd(), '..', 'output')
|
|
50
|
-
: opts.outDir || undefined;
|
|
29
|
+
const pkg = JSON.parse(
|
|
30
|
+
fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8')
|
|
31
|
+
)
|
|
51
32
|
|
|
52
|
-
|
|
33
|
+
const PKG_NAME = pkg.name
|
|
34
|
+
const VERSION = pkg.version
|
|
35
|
+
|
|
36
|
+
const autoCheckUpdate = async () => {
|
|
37
|
+
// === CLI 主逻辑完成后提示更新 ===
|
|
38
|
+
await checkUpdateWithPatch({
|
|
39
|
+
pkg: {
|
|
40
|
+
name: PKG_NAME,
|
|
41
|
+
version: VERSION
|
|
42
|
+
},
|
|
43
|
+
force: true
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const version = async () => {
|
|
48
|
+
showVersionInfo(VERSION)
|
|
49
|
+
await autoCheckUpdate()
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const main = async () => {
|
|
54
|
+
const program = new Command()
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.name('git-commits')
|
|
58
|
+
.description('Advanced Git commit log exporter.')
|
|
59
|
+
.option('--author <name>', '指定 author 名')
|
|
60
|
+
.option('--email <email>', '指定 email')
|
|
61
|
+
.option('--since <date>', '起始日期')
|
|
62
|
+
.option('--until <date>', '结束日期')
|
|
63
|
+
.option('--limit <n>', '限制数量', parseInt)
|
|
64
|
+
.option('--no-merges', '不包含 merge commit')
|
|
65
|
+
.option('--json', '输出 JSON')
|
|
66
|
+
.option('--format <type>', '输出格式: text | excel | json', 'text')
|
|
67
|
+
.option('--group-by <type>', '按日期分组: day | month | week')
|
|
68
|
+
.option('--stats', '输出每日统计数据')
|
|
69
|
+
.option(
|
|
70
|
+
'--gerrit <prefix>',
|
|
71
|
+
'显示 Gerrit 地址,支持在 prefix 中使用 {{hash}} 占位符'
|
|
72
|
+
)
|
|
73
|
+
.option(
|
|
74
|
+
'--gerrit-api <url>',
|
|
75
|
+
'可选:Gerrit REST API 基础地址,用于解析 changeNumber,例如 `https://gerrit.example.com`'
|
|
76
|
+
)
|
|
77
|
+
.option(
|
|
78
|
+
'--gerrit-auth <tokenOrUserPass>',
|
|
79
|
+
'可选:Gerrit API 授权,格式为 `user:pass` 或 `TOKEN`(表示 Bearer token)'
|
|
80
|
+
)
|
|
81
|
+
.option('--overtime', '分析公司加班文化(输出下班时间与非工作日提交占比)')
|
|
82
|
+
.option('--country <code>', '节假日国家:CN 或 US,默认为 CN', 'CN')
|
|
83
|
+
.option(
|
|
84
|
+
'--work-start <hour>',
|
|
85
|
+
'上班开始小时,默认 9',
|
|
86
|
+
(v) => parseInt(v, 10),
|
|
87
|
+
9
|
|
88
|
+
)
|
|
89
|
+
.option(
|
|
90
|
+
'--work-end <hour>',
|
|
91
|
+
'下班小时,默认 18',
|
|
92
|
+
(v) => parseInt(v, 10),
|
|
93
|
+
18
|
|
94
|
+
)
|
|
95
|
+
.option(
|
|
96
|
+
'--lunch-start <hour>',
|
|
97
|
+
'午休开始小时,默认 12',
|
|
98
|
+
(v) => parseInt(v, 10),
|
|
99
|
+
12
|
|
100
|
+
)
|
|
101
|
+
.option(
|
|
102
|
+
'--lunch-end <hour>',
|
|
103
|
+
'午休结束小时,默认 14',
|
|
104
|
+
(v) => parseInt(v, 10),
|
|
105
|
+
14
|
|
106
|
+
)
|
|
107
|
+
.option('--out <file>', '输出文件名(不含路径)')
|
|
108
|
+
.option(
|
|
109
|
+
'--out-dir <dir>',
|
|
110
|
+
'自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output`'
|
|
111
|
+
)
|
|
112
|
+
.option(
|
|
113
|
+
'--out-parent',
|
|
114
|
+
'将输出目录放到当前工程的父目录的 `output/`(等同于 `--out-dir ../output`)'
|
|
115
|
+
)
|
|
116
|
+
.option(
|
|
117
|
+
'--per-period-formats <formats>',
|
|
118
|
+
'每个周期单独输出的格式,逗号分隔:text,csv,tab,xlsx。默认为空(不输出 CSV/Tab/XLSX)',
|
|
119
|
+
''
|
|
120
|
+
)
|
|
121
|
+
.option(
|
|
122
|
+
'--per-period-excel-mode <mode>',
|
|
123
|
+
'per-period Excel 模式:sheets|files(默认:sheets)',
|
|
124
|
+
'sheets'
|
|
125
|
+
)
|
|
126
|
+
.option(
|
|
127
|
+
'--per-period-only',
|
|
128
|
+
'仅输出 per-period(month/week)文件,不输出合并的 monthly/weekly 汇总文件'
|
|
129
|
+
)
|
|
130
|
+
.option(
|
|
131
|
+
'--serve',
|
|
132
|
+
'启动本地 web 服务,查看提交统计(将在 output/data 下生成数据文件)'
|
|
133
|
+
)
|
|
134
|
+
.option(
|
|
135
|
+
'--port <n>',
|
|
136
|
+
'本地 web 服务端口(默认 3000)',
|
|
137
|
+
(v) => parseInt(v, 10),
|
|
138
|
+
3000
|
|
139
|
+
)
|
|
140
|
+
.option(
|
|
141
|
+
'--serve-only',
|
|
142
|
+
'仅启动 web 服务,不导出或分析数据(使用 output/data 中已有的数据)'
|
|
143
|
+
)
|
|
144
|
+
.option('--version', 'show version information')
|
|
145
|
+
.parse()
|
|
146
|
+
|
|
147
|
+
const opts = program.opts()
|
|
148
|
+
// compute output directory root early (so serve-only can use it)
|
|
149
|
+
const outDir = opts.outParent
|
|
150
|
+
? path.resolve(process.cwd(), '..', 'output')
|
|
151
|
+
: opts.outDir || undefined
|
|
152
|
+
|
|
153
|
+
if (opts.version) {
|
|
154
|
+
await version()
|
|
155
|
+
return
|
|
156
|
+
}
|
|
53
157
|
// if serve-only is requested, start server and exit
|
|
54
158
|
if (opts.serveOnly) {
|
|
55
159
|
try {
|
|
56
|
-
await startServer(opts.port || 3000, outDir)
|
|
160
|
+
await startServer(opts.port || 3000, outDir)
|
|
57
161
|
} catch (err) {
|
|
58
|
-
console.warn(
|
|
59
|
-
|
|
162
|
+
console.warn(
|
|
163
|
+
'Start server failed:',
|
|
164
|
+
err && err.message ? err.message : err
|
|
165
|
+
)
|
|
166
|
+
process.exit(1)
|
|
60
167
|
}
|
|
61
|
-
return
|
|
168
|
+
return
|
|
62
169
|
}
|
|
63
170
|
|
|
64
|
-
let records = await getGitLogs(opts)
|
|
171
|
+
let records = await getGitLogs(opts)
|
|
65
172
|
|
|
66
173
|
// compute output directory root if user provided one or wants parent
|
|
67
174
|
|
|
68
175
|
// --- Gerrit 地址处理(若提供) ---
|
|
69
176
|
if (opts.gerrit) {
|
|
70
|
-
const prefix = opts.gerrit
|
|
177
|
+
const prefix = opts.gerrit
|
|
71
178
|
// support optional changeNumber resolution via Gerrit REST API
|
|
72
|
-
const { gerritApi, gerritAuth } = opts
|
|
179
|
+
const { gerritApi, gerritAuth } = opts
|
|
73
180
|
// create new array to avoid mutating function parameters (eslint: no-param-reassign)
|
|
74
181
|
if (prefix.includes('{{changeNumber}}') && gerritApi) {
|
|
75
182
|
// async mapping to resolve changeNumber using Gerrit API
|
|
76
|
-
const cache = new Map()
|
|
77
|
-
const headers = {}
|
|
183
|
+
const cache = new Map()
|
|
184
|
+
const headers = {}
|
|
78
185
|
if (gerritAuth) {
|
|
79
186
|
if (gerritAuth.includes(':')) {
|
|
80
|
-
headers.Authorization = `Basic ${Buffer.from(gerritAuth).toString('base64')}
|
|
187
|
+
headers.Authorization = `Basic ${Buffer.from(gerritAuth).toString('base64')}`
|
|
81
188
|
} else {
|
|
82
|
-
headers.Authorization = `Bearer ${gerritAuth}
|
|
189
|
+
headers.Authorization = `Bearer ${gerritAuth}`
|
|
83
190
|
}
|
|
84
191
|
}
|
|
85
192
|
const fetchGerritJson = async (url) => {
|
|
86
193
|
try {
|
|
87
|
-
const res = await fetch(url, { headers })
|
|
88
|
-
const txt = await res.text()
|
|
194
|
+
const res = await fetch(url, { headers })
|
|
195
|
+
const txt = await res.text()
|
|
89
196
|
// Gerrit prepends )]}' to JSON responses — strip it
|
|
90
|
-
const jsonText = txt.replace(/^\)\]\}'\n/, '')
|
|
91
|
-
return JSON.parse(jsonText)
|
|
197
|
+
const jsonText = txt.replace(/^\)\]\}'\n/, '')
|
|
198
|
+
return JSON.parse(jsonText)
|
|
92
199
|
} catch (err) {
|
|
93
|
-
return null
|
|
200
|
+
return null
|
|
94
201
|
}
|
|
95
|
-
}
|
|
202
|
+
}
|
|
96
203
|
const resolveChangeNumber = async (r) => {
|
|
97
204
|
// try changeId first
|
|
98
205
|
if (r.changeId) {
|
|
99
|
-
if (cache.has(r.changeId)) return cache.get(r.changeId)
|
|
206
|
+
if (cache.has(r.changeId)) return cache.get(r.changeId)
|
|
100
207
|
// try `changes/{changeId}/detail`
|
|
101
|
-
const url = `${gerritApi.replace(/\/$/, '')}/changes/${encodeURIComponent(r.changeId)}/detail
|
|
102
|
-
let j = await fetchGerritJson(url)
|
|
208
|
+
const url = `${gerritApi.replace(/\/$/, '')}/changes/${encodeURIComponent(r.changeId)}/detail`
|
|
209
|
+
let j = await fetchGerritJson(url)
|
|
103
210
|
if (j && j._number) {
|
|
104
|
-
cache.set(r.changeId, j._number)
|
|
105
|
-
return j._number
|
|
211
|
+
cache.set(r.changeId, j._number)
|
|
212
|
+
return j._number
|
|
106
213
|
}
|
|
107
214
|
// fallback: query search
|
|
108
|
-
const url2 = `${gerritApi.replace(/\/$/, '')}/changes/?q=change:${encodeURIComponent(r.changeId)}
|
|
109
|
-
j = await fetchGerritJson(url2)
|
|
215
|
+
const url2 = `${gerritApi.replace(/\/$/, '')}/changes/?q=change:${encodeURIComponent(r.changeId)}`
|
|
216
|
+
j = await fetchGerritJson(url2)
|
|
110
217
|
if (Array.isArray(j) && j.length > 0 && j[0]._number) {
|
|
111
|
-
cache.set(r.changeId, j[0]._number)
|
|
112
|
-
return j[0]._number
|
|
218
|
+
cache.set(r.changeId, j[0]._number)
|
|
219
|
+
return j[0]._number
|
|
113
220
|
}
|
|
114
221
|
}
|
|
115
222
|
// try commit hash
|
|
116
223
|
if (r.hash) {
|
|
117
|
-
if (cache.has(r.hash)) return cache.get(r.hash)
|
|
118
|
-
const url3 = `${gerritApi.replace(/\/$/, '')}/changes/?q=commit:${encodeURIComponent(r.hash)}
|
|
119
|
-
const j = await fetchGerritJson(url3)
|
|
224
|
+
if (cache.has(r.hash)) return cache.get(r.hash)
|
|
225
|
+
const url3 = `${gerritApi.replace(/\/$/, '')}/changes/?q=commit:${encodeURIComponent(r.hash)}`
|
|
226
|
+
const j = await fetchGerritJson(url3)
|
|
120
227
|
if (Array.isArray(j) && j.length > 0 && j[0]._number) {
|
|
121
|
-
cache.set(r.hash, j[0]._number)
|
|
122
|
-
return j[0]._number
|
|
228
|
+
cache.set(r.hash, j[0]._number)
|
|
229
|
+
return j[0]._number
|
|
123
230
|
}
|
|
124
231
|
}
|
|
125
|
-
return null
|
|
126
|
-
}
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
127
234
|
records = await Promise.all(
|
|
128
235
|
records.map(async (r) => {
|
|
129
|
-
const changeNumber = await resolveChangeNumber(r)
|
|
130
|
-
const changeNumberOrFallback = changeNumber || r.changeId || r.hash
|
|
131
|
-
const gerritUrl = prefix.replace(
|
|
132
|
-
|
|
236
|
+
const changeNumber = await resolveChangeNumber(r)
|
|
237
|
+
const changeNumberOrFallback = changeNumber || r.changeId || r.hash
|
|
238
|
+
const gerritUrl = prefix.replace(
|
|
239
|
+
'{{changeNumber}}',
|
|
240
|
+
changeNumberOrFallback
|
|
241
|
+
)
|
|
242
|
+
return { ...r, gerrit: gerritUrl }
|
|
133
243
|
})
|
|
134
|
-
)
|
|
244
|
+
)
|
|
135
245
|
} else if (prefix.includes('{{changeNumber}}') && !gerritApi) {
|
|
136
|
-
console.warn(
|
|
137
|
-
|
|
246
|
+
console.warn(
|
|
247
|
+
'prefix contains {{changeNumber}} but no --gerrit-api provided — falling back to changeId/hash'
|
|
248
|
+
)
|
|
249
|
+
records = records.map((r) => ({
|
|
250
|
+
...r,
|
|
251
|
+
gerrit: prefix.replace('{{changeNumber}}', r.changeId || r.hash)
|
|
252
|
+
}))
|
|
138
253
|
} else {
|
|
139
254
|
records = records.map((r) => {
|
|
140
|
-
let gerritUrl
|
|
255
|
+
let gerritUrl
|
|
141
256
|
if (prefix.includes('{{changeId}}')) {
|
|
142
|
-
const changeId = r.changeId || r.hash
|
|
143
|
-
gerritUrl = prefix.replace('{{changeId}}', changeId)
|
|
257
|
+
const changeId = r.changeId || r.hash
|
|
258
|
+
gerritUrl = prefix.replace('{{changeId}}', changeId)
|
|
144
259
|
} else if (prefix.includes('{{hash}}')) {
|
|
145
|
-
gerritUrl = prefix.replace('{{hash}}', r.hash)
|
|
260
|
+
gerritUrl = prefix.replace('{{hash}}', r.hash)
|
|
146
261
|
} else {
|
|
147
|
-
gerritUrl = prefix.endsWith('/')
|
|
262
|
+
gerritUrl = prefix.endsWith('/')
|
|
263
|
+
? `${prefix}${r.hash}`
|
|
264
|
+
: `${prefix}/${r.hash}`
|
|
148
265
|
}
|
|
149
|
-
return { ...r, gerrit: gerritUrl }
|
|
150
|
-
})
|
|
266
|
+
return { ...r, gerrit: gerritUrl }
|
|
267
|
+
})
|
|
151
268
|
}
|
|
152
269
|
}
|
|
153
270
|
|
|
154
271
|
// --- 分组 ---
|
|
155
|
-
const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
|
|
272
|
+
const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
|
|
156
273
|
|
|
157
274
|
// --- Overtime analysis ---
|
|
158
275
|
if (opts.overtime) {
|
|
159
276
|
const stats = analyzeOvertime(records, {
|
|
160
277
|
startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
161
278
|
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
162
|
-
lunchStart:
|
|
279
|
+
lunchStart:
|
|
280
|
+
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
163
281
|
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
164
|
-
country: opts.country || 'CN'
|
|
165
|
-
})
|
|
282
|
+
country: opts.country || 'CN'
|
|
283
|
+
})
|
|
166
284
|
// Output to console
|
|
167
|
-
console.log('\n--- Overtime analysis ---\n')
|
|
168
|
-
console.log(renderOvertimeText(stats))
|
|
285
|
+
console.log('\n--- Overtime analysis ---\n')
|
|
286
|
+
console.log(renderOvertimeText(stats))
|
|
169
287
|
// if user requested json format, write stats to file
|
|
170
288
|
if (opts.json || opts.format === 'json') {
|
|
171
|
-
const file = opts.out || 'overtime.json'
|
|
172
|
-
const filepath = outputFilePath(file, outDir)
|
|
173
|
-
writeJSON(filepath, stats)
|
|
174
|
-
console.log(chalk.green(`overtime JSON 已导出: ${filepath}`))
|
|
289
|
+
const file = opts.out || 'overtime.json'
|
|
290
|
+
const filepath = outputFilePath(file, outDir)
|
|
291
|
+
writeJSON(filepath, stats)
|
|
292
|
+
console.log(chalk.green(`overtime JSON 已导出: ${filepath}`))
|
|
175
293
|
}
|
|
176
294
|
// Always write human readable overtime text to file (default: overtime.txt)
|
|
177
|
-
const outBase = opts.out
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
295
|
+
const outBase = opts.out
|
|
296
|
+
? path.basename(opts.out, path.extname(opts.out))
|
|
297
|
+
: 'commits'
|
|
298
|
+
const overtimeFileName = `overtime_${outBase}.txt`
|
|
299
|
+
const overtimeFile = outputFilePath(overtimeFileName, outDir)
|
|
300
|
+
writeTextFile(overtimeFile, renderOvertimeText(stats))
|
|
181
301
|
// write tab-separated text file for better alignment in editors that use proportional fonts
|
|
182
|
-
const overtimeTabFileName = `overtime_${outBase}.tab.txt
|
|
183
|
-
const overtimeTabFile = outputFilePath(overtimeTabFileName, outDir)
|
|
184
|
-
writeTextFile(overtimeTabFile, renderOvertimeTab(stats))
|
|
302
|
+
const overtimeTabFileName = `overtime_${outBase}.tab.txt`
|
|
303
|
+
const overtimeTabFile = outputFilePath(overtimeTabFileName, outDir)
|
|
304
|
+
writeTextFile(overtimeTabFile, renderOvertimeTab(stats))
|
|
185
305
|
// write CSV for structured data consumption
|
|
186
|
-
const overtimeCsvFileName = `overtime_${outBase}.csv
|
|
187
|
-
const overtimeCsvFile = outputFilePath(overtimeCsvFileName, outDir)
|
|
188
|
-
writeTextFile(overtimeCsvFile, renderOvertimeCsv(stats))
|
|
189
|
-
console.log(chalk.green(`Overtime text 已导出: ${overtimeFile}`))
|
|
190
|
-
console.log(chalk.green(`Overtime table (tabs) 已导出: ${overtimeTabFile}`))
|
|
191
|
-
console.log(chalk.green(`Overtime CSV 已导出: ${overtimeCsvFile}`))
|
|
306
|
+
const overtimeCsvFileName = `overtime_${outBase}.csv`
|
|
307
|
+
const overtimeCsvFile = outputFilePath(overtimeCsvFileName, outDir)
|
|
308
|
+
writeTextFile(overtimeCsvFile, renderOvertimeCsv(stats))
|
|
309
|
+
console.log(chalk.green(`Overtime text 已导出: ${overtimeFile}`))
|
|
310
|
+
console.log(chalk.green(`Overtime table (tabs) 已导出: ${overtimeTabFile}`))
|
|
311
|
+
console.log(chalk.green(`Overtime CSV 已导出: ${overtimeCsvFile}`))
|
|
192
312
|
|
|
193
313
|
// If serve mode is enabled, write data modules and launch the web server
|
|
194
314
|
if (opts.serve) {
|
|
195
315
|
try {
|
|
196
|
-
const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
|
|
197
|
-
const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n
|
|
198
|
-
writeTextFile(dataCommitsFile, commitsModule)
|
|
199
|
-
const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
|
|
200
|
-
const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n
|
|
201
|
-
writeTextFile(dataStatsFile, statsModule)
|
|
316
|
+
const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
|
|
317
|
+
const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
|
|
318
|
+
writeTextFile(dataCommitsFile, commitsModule)
|
|
319
|
+
const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
|
|
320
|
+
const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`
|
|
321
|
+
writeTextFile(dataStatsFile, statsModule)
|
|
202
322
|
|
|
203
323
|
// 新增:每周趋势数据(用于前端图表)
|
|
204
|
-
const weekGroups = groupRecords(records, 'week')
|
|
205
|
-
const weekKeys = Object.keys(weekGroups).sort()
|
|
324
|
+
const weekGroups = groupRecords(records, 'week')
|
|
325
|
+
const weekKeys = Object.keys(weekGroups).sort()
|
|
206
326
|
const weeklySeries = weekKeys.map((k) => {
|
|
207
327
|
const s = analyzeOvertime(weekGroups[k], {
|
|
208
|
-
startHour:
|
|
328
|
+
startHour:
|
|
329
|
+
opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
209
330
|
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
210
|
-
lunchStart:
|
|
331
|
+
lunchStart:
|
|
332
|
+
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
211
333
|
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
212
|
-
country: opts.country || 'CN'
|
|
213
|
-
})
|
|
334
|
+
country: opts.country || 'CN'
|
|
335
|
+
})
|
|
214
336
|
return {
|
|
215
337
|
period: k,
|
|
216
338
|
total: s.total,
|
|
217
339
|
outsideWorkCount: s.outsideWorkCount,
|
|
218
340
|
outsideWorkRate: s.outsideWorkRate,
|
|
219
341
|
nonWorkdayCount: s.nonWorkdayCount,
|
|
220
|
-
nonWorkdayRate: s.nonWorkdayRate
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
const dataWeeklyFile = outputFilePath(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
342
|
+
nonWorkdayRate: s.nonWorkdayRate
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
const dataWeeklyFile = outputFilePath(
|
|
346
|
+
'data/overtime-weekly.mjs',
|
|
347
|
+
outDir
|
|
348
|
+
)
|
|
349
|
+
const weeklyModule = `export default ${JSON.stringify(weeklySeries, null, 2)};\n`
|
|
350
|
+
writeTextFile(dataWeeklyFile, weeklyModule)
|
|
351
|
+
console.log(chalk.green(`Weekly series 已导出: ${dataWeeklyFile}`))
|
|
227
352
|
|
|
228
|
-
startServer(opts.port || 3000, outDir).catch(() => {})
|
|
353
|
+
startServer(opts.port || 3000, outDir).catch(() => {})
|
|
229
354
|
} catch (err) {
|
|
230
|
-
console.warn(
|
|
355
|
+
console.warn(
|
|
356
|
+
'Export data modules failed:',
|
|
357
|
+
err && err.message ? err.message : err
|
|
358
|
+
)
|
|
231
359
|
}
|
|
232
360
|
}
|
|
233
361
|
|
|
234
362
|
// 按月输出 ... 保持原逻辑
|
|
235
|
-
const perPeriodFormats = String(opts.perPeriodFormats || '')
|
|
363
|
+
const perPeriodFormats = String(opts.perPeriodFormats || '')
|
|
364
|
+
.split(',')
|
|
365
|
+
.map((s) =>
|
|
366
|
+
String(s || '')
|
|
367
|
+
.trim()
|
|
368
|
+
.toLowerCase()
|
|
369
|
+
)
|
|
370
|
+
.filter(Boolean)
|
|
236
371
|
try {
|
|
237
|
-
const monthGroups = groupRecords(records, 'month')
|
|
238
|
-
const monthlyFileName = `overtime_${outBase}_monthly.txt
|
|
239
|
-
const monthlyFile = outputFilePath(monthlyFileName, outDir)
|
|
240
|
-
let monthlyContent = ''
|
|
241
|
-
const monthKeys = Object.keys(monthGroups).sort()
|
|
372
|
+
const monthGroups = groupRecords(records, 'month')
|
|
373
|
+
const monthlyFileName = `overtime_${outBase}_monthly.txt`
|
|
374
|
+
const monthlyFile = outputFilePath(monthlyFileName, outDir)
|
|
375
|
+
let monthlyContent = ''
|
|
376
|
+
const monthKeys = Object.keys(monthGroups).sort()
|
|
242
377
|
monthKeys.forEach((k) => {
|
|
243
|
-
const groupRecs = monthGroups[k]
|
|
378
|
+
const groupRecs = monthGroups[k]
|
|
244
379
|
const s = analyzeOvertime(groupRecs, {
|
|
245
|
-
startHour:
|
|
380
|
+
startHour:
|
|
381
|
+
opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
246
382
|
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
247
|
-
lunchStart:
|
|
383
|
+
lunchStart:
|
|
384
|
+
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
248
385
|
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
249
|
-
country: opts.country || 'CN'
|
|
250
|
-
})
|
|
251
|
-
monthlyContent += `===== ${k} =====\n
|
|
252
|
-
monthlyContent += `${renderOvertimeText(s)}\n\n
|
|
386
|
+
country: opts.country || 'CN'
|
|
387
|
+
})
|
|
388
|
+
monthlyContent += `===== ${k} =====\n`
|
|
389
|
+
monthlyContent += `${renderOvertimeText(s)}\n\n`
|
|
253
390
|
// Also write a single file per month under 'month/' folder
|
|
254
391
|
try {
|
|
255
|
-
const perMonthFileName = `month/overtime_${outBase}_${k}.txt
|
|
256
|
-
const perMonthFile = outputFilePath(perMonthFileName, outDir)
|
|
257
|
-
writeTextFile(perMonthFile, renderOvertimeText(s))
|
|
258
|
-
console.log(
|
|
392
|
+
const perMonthFileName = `month/overtime_${outBase}_${k}.txt`
|
|
393
|
+
const perMonthFile = outputFilePath(perMonthFileName, outDir)
|
|
394
|
+
writeTextFile(perMonthFile, renderOvertimeText(s))
|
|
395
|
+
console.log(
|
|
396
|
+
chalk.green(`Overtime 月度(${k}) 已导出: ${perMonthFile}`)
|
|
397
|
+
)
|
|
259
398
|
// per-period CSV / Tab format (按需生成)
|
|
260
399
|
if (perPeriodFormats.includes('csv')) {
|
|
261
400
|
try {
|
|
262
|
-
const perMonthCsvName = `month/overtime_${outBase}_${k}.csv
|
|
263
|
-
writeTextFile(
|
|
264
|
-
|
|
401
|
+
const perMonthCsvName = `month/overtime_${outBase}_${k}.csv`
|
|
402
|
+
writeTextFile(
|
|
403
|
+
outputFilePath(perMonthCsvName, outDir),
|
|
404
|
+
renderOvertimeCsv(s)
|
|
405
|
+
)
|
|
406
|
+
console.log(
|
|
407
|
+
chalk.green(
|
|
408
|
+
`Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`
|
|
409
|
+
)
|
|
410
|
+
)
|
|
265
411
|
} catch (err) {
|
|
266
|
-
console.warn(
|
|
412
|
+
console.warn(
|
|
413
|
+
`Write monthly CSV for ${k} failed:`,
|
|
414
|
+
err && err.message ? err.message : err
|
|
415
|
+
)
|
|
267
416
|
}
|
|
268
417
|
}
|
|
269
418
|
if (perPeriodFormats.includes('tab')) {
|
|
270
419
|
try {
|
|
271
|
-
const perMonthTabName = `month/overtime_${outBase}_${k}.tab.txt
|
|
272
|
-
writeTextFile(
|
|
273
|
-
|
|
420
|
+
const perMonthTabName = `month/overtime_${outBase}_${k}.tab.txt`
|
|
421
|
+
writeTextFile(
|
|
422
|
+
outputFilePath(perMonthTabName, outDir),
|
|
423
|
+
renderOvertimeTab(s)
|
|
424
|
+
)
|
|
425
|
+
console.log(
|
|
426
|
+
chalk.green(
|
|
427
|
+
`Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`
|
|
428
|
+
)
|
|
429
|
+
)
|
|
274
430
|
} catch (err) {
|
|
275
|
-
console.warn(
|
|
431
|
+
console.warn(
|
|
432
|
+
`Write monthly Tab for ${k} failed:`,
|
|
433
|
+
err && err.message ? err.message : err
|
|
434
|
+
)
|
|
276
435
|
}
|
|
277
436
|
}
|
|
278
437
|
} catch (err) {
|
|
279
|
-
console.warn(
|
|
438
|
+
console.warn(
|
|
439
|
+
`Write monthly file for ${k} failed:`,
|
|
440
|
+
err && err.message ? err.message : err
|
|
441
|
+
)
|
|
280
442
|
}
|
|
281
|
-
})
|
|
443
|
+
})
|
|
282
444
|
if (!opts.perPeriodOnly) {
|
|
283
|
-
writeTextFile(monthlyFile, monthlyContent)
|
|
284
|
-
console.log(chalk.green(`Overtime 月度汇总 已导出: ${monthlyFile}`))
|
|
445
|
+
writeTextFile(monthlyFile, monthlyContent)
|
|
446
|
+
console.log(chalk.green(`Overtime 月度汇总 已导出: ${monthlyFile}`))
|
|
285
447
|
}
|
|
286
448
|
// per-period Excel (sheets or files)
|
|
287
449
|
if (perPeriodFormats.includes('xlsx')) {
|
|
288
|
-
const perPeriodExcelMode = String(opts.perPeriodExcelMode || 'sheets')
|
|
450
|
+
const perPeriodExcelMode = String(opts.perPeriodExcelMode || 'sheets')
|
|
289
451
|
if (perPeriodExcelMode === 'sheets') {
|
|
290
452
|
try {
|
|
291
|
-
const monthXlsxName = `month/overtime_${outBase}_monthly.xlsx
|
|
292
|
-
const monthXlsxFile = outputFilePath(monthXlsxName, outDir)
|
|
293
|
-
await exportExcelPerPeriodSheets(monthGroups, monthXlsxFile, {
|
|
294
|
-
|
|
453
|
+
const monthXlsxName = `month/overtime_${outBase}_monthly.xlsx`
|
|
454
|
+
const monthXlsxFile = outputFilePath(monthXlsxName, outDir)
|
|
455
|
+
await exportExcelPerPeriodSheets(monthGroups, monthXlsxFile, {
|
|
456
|
+
stats: opts.stats,
|
|
457
|
+
gerrit: opts.gerrit
|
|
458
|
+
})
|
|
459
|
+
console.log(
|
|
460
|
+
chalk.green(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`)
|
|
461
|
+
)
|
|
295
462
|
} catch (err) {
|
|
296
|
-
console.warn(
|
|
463
|
+
console.warn(
|
|
464
|
+
'Export month XLSX (sheets) failed:',
|
|
465
|
+
err && err.message ? err.message : err
|
|
466
|
+
)
|
|
297
467
|
}
|
|
298
468
|
} else {
|
|
299
469
|
try {
|
|
300
|
-
const monthKeys2 = Object.keys(monthGroups).sort()
|
|
301
|
-
const tasks = monthKeys2.map(k2 => {
|
|
302
|
-
const perMonthXlsxName = `month/overtime_${outBase}_${k2}.xlsx
|
|
303
|
-
const perMonthXlsxFile = outputFilePath(perMonthXlsxName, outDir)
|
|
304
|
-
return exportExcel(monthGroups[k2], null, {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
470
|
+
const monthKeys2 = Object.keys(monthGroups).sort()
|
|
471
|
+
const tasks = monthKeys2.map((k2) => {
|
|
472
|
+
const perMonthXlsxName = `month/overtime_${outBase}_${k2}.xlsx`
|
|
473
|
+
const perMonthXlsxFile = outputFilePath(perMonthXlsxName, outDir)
|
|
474
|
+
return exportExcel(monthGroups[k2], null, {
|
|
475
|
+
file: perMonthXlsxFile,
|
|
476
|
+
stats: opts.stats,
|
|
477
|
+
gerrit: opts.gerrit
|
|
478
|
+
}).then(() =>
|
|
479
|
+
console.log(
|
|
480
|
+
chalk.green(
|
|
481
|
+
`Overtime 月度(XLSX)(${k2}) 已导出: ${perMonthXlsxFile}`
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
})
|
|
486
|
+
await Promise.all(tasks)
|
|
308
487
|
} catch (err) {
|
|
309
|
-
console.warn(
|
|
488
|
+
console.warn(
|
|
489
|
+
'Export monthly XLSX files failed:',
|
|
490
|
+
err && err.message ? err.message : err
|
|
491
|
+
)
|
|
310
492
|
}
|
|
311
493
|
}
|
|
312
494
|
}
|
|
313
495
|
} catch (err) {
|
|
314
|
-
console.warn(
|
|
496
|
+
console.warn(
|
|
497
|
+
'Generate monthly overtime failed:',
|
|
498
|
+
err && err.message ? err.message : err
|
|
499
|
+
)
|
|
315
500
|
}
|
|
316
501
|
|
|
317
502
|
// 周度输出保持原逻辑(略)
|
|
318
503
|
try {
|
|
319
|
-
const weekGroups = groupRecords(records, 'week')
|
|
320
|
-
const weeklyFileName = `overtime_${outBase}_weekly.txt
|
|
321
|
-
const weeklyFile = outputFilePath(weeklyFileName, outDir)
|
|
322
|
-
let weeklyContent = ''
|
|
323
|
-
const weekKeys = Object.keys(weekGroups).sort()
|
|
504
|
+
const weekGroups = groupRecords(records, 'week')
|
|
505
|
+
const weeklyFileName = `overtime_${outBase}_weekly.txt`
|
|
506
|
+
const weeklyFile = outputFilePath(weeklyFileName, outDir)
|
|
507
|
+
let weeklyContent = ''
|
|
508
|
+
const weekKeys = Object.keys(weekGroups).sort()
|
|
324
509
|
weekKeys.forEach((k) => {
|
|
325
|
-
const groupRecs = weekGroups[k]
|
|
510
|
+
const groupRecs = weekGroups[k]
|
|
326
511
|
const s = analyzeOvertime(groupRecs, {
|
|
327
|
-
startHour:
|
|
512
|
+
startHour:
|
|
513
|
+
opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
328
514
|
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
329
|
-
lunchStart:
|
|
515
|
+
lunchStart:
|
|
516
|
+
opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
330
517
|
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
331
|
-
country: opts.country || 'CN'
|
|
332
|
-
})
|
|
333
|
-
weeklyContent += `===== ${k} =====\n
|
|
334
|
-
weeklyContent += `${renderOvertimeText(s)}\n\n
|
|
518
|
+
country: opts.country || 'CN'
|
|
519
|
+
})
|
|
520
|
+
weeklyContent += `===== ${k} =====\n`
|
|
521
|
+
weeklyContent += `${renderOvertimeText(s)}\n\n`
|
|
335
522
|
try {
|
|
336
|
-
const perWeekFileName = `week/overtime_${outBase}_${k}.txt
|
|
337
|
-
const perWeekFile = outputFilePath(perWeekFileName, outDir)
|
|
338
|
-
writeTextFile(perWeekFile, renderOvertimeText(s))
|
|
339
|
-
console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`))
|
|
523
|
+
const perWeekFileName = `week/overtime_${outBase}_${k}.txt`
|
|
524
|
+
const perWeekFile = outputFilePath(perWeekFileName, outDir)
|
|
525
|
+
writeTextFile(perWeekFile, renderOvertimeText(s))
|
|
526
|
+
console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`))
|
|
340
527
|
// eslint-disable-next-line no-shadow
|
|
341
|
-
const perPeriodFormats = String(opts.perPeriodFormats || '')
|
|
528
|
+
const perPeriodFormats = String(opts.perPeriodFormats || '')
|
|
529
|
+
.split(',')
|
|
530
|
+
// eslint-disable-next-line no-shadow
|
|
531
|
+
.map((s) =>
|
|
532
|
+
String(s || '')
|
|
533
|
+
.trim()
|
|
534
|
+
.toLowerCase()
|
|
535
|
+
)
|
|
536
|
+
.filter(Boolean)
|
|
342
537
|
if (perPeriodFormats.includes('csv')) {
|
|
343
538
|
try {
|
|
344
|
-
const perWeekCsvName = `week/overtime_${outBase}_${k}.csv
|
|
345
|
-
writeTextFile(
|
|
346
|
-
|
|
539
|
+
const perWeekCsvName = `week/overtime_${outBase}_${k}.csv`
|
|
540
|
+
writeTextFile(
|
|
541
|
+
outputFilePath(perWeekCsvName, outDir),
|
|
542
|
+
renderOvertimeCsv(s)
|
|
543
|
+
)
|
|
544
|
+
console.log(
|
|
545
|
+
chalk.green(
|
|
546
|
+
`Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`
|
|
547
|
+
)
|
|
548
|
+
)
|
|
347
549
|
} catch (err) {
|
|
348
|
-
console.warn(
|
|
550
|
+
console.warn(
|
|
551
|
+
`Write weekly CSV for ${k} failed:`,
|
|
552
|
+
err && err.message ? err.message : err
|
|
553
|
+
)
|
|
349
554
|
}
|
|
350
555
|
}
|
|
351
556
|
if (perPeriodFormats.includes('tab')) {
|
|
352
557
|
try {
|
|
353
|
-
const perWeekTabName = `week/overtime_${outBase}_${k}.tab.txt
|
|
354
|
-
writeTextFile(
|
|
355
|
-
|
|
558
|
+
const perWeekTabName = `week/overtime_${outBase}_${k}.tab.txt`
|
|
559
|
+
writeTextFile(
|
|
560
|
+
outputFilePath(perWeekTabName, outDir),
|
|
561
|
+
renderOvertimeTab(s)
|
|
562
|
+
)
|
|
563
|
+
console.log(
|
|
564
|
+
chalk.green(
|
|
565
|
+
`Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`
|
|
566
|
+
)
|
|
567
|
+
)
|
|
356
568
|
} catch (err) {
|
|
357
|
-
console.warn(
|
|
569
|
+
console.warn(
|
|
570
|
+
`Write weekly Tab for ${k} failed:`,
|
|
571
|
+
err && err.message ? err.message : err
|
|
572
|
+
)
|
|
358
573
|
}
|
|
359
574
|
}
|
|
360
575
|
} catch (err) {
|
|
361
|
-
console.warn(
|
|
576
|
+
console.warn(
|
|
577
|
+
`Write weekly file for ${k} failed:`,
|
|
578
|
+
err && err.message ? err.message : err
|
|
579
|
+
)
|
|
362
580
|
}
|
|
363
|
-
})
|
|
364
|
-
writeTextFile(weeklyFile, weeklyContent)
|
|
365
|
-
console.log(chalk.green(`Overtime 周度汇总 已导出: ${weeklyFile}`))
|
|
581
|
+
})
|
|
582
|
+
writeTextFile(weeklyFile, weeklyContent)
|
|
583
|
+
console.log(chalk.green(`Overtime 周度汇总 已导出: ${weeklyFile}`))
|
|
366
584
|
} catch (err) {
|
|
367
|
-
console.warn(
|
|
585
|
+
console.warn(
|
|
586
|
+
'Generate weekly overtime failed:',
|
|
587
|
+
err && err.message ? err.message : err
|
|
588
|
+
)
|
|
368
589
|
}
|
|
369
590
|
}
|
|
370
591
|
|
|
371
592
|
// --- JSON/TEXT/EXCEL(保持原逻辑) ---
|
|
372
593
|
if (opts.json || opts.format === 'json') {
|
|
373
|
-
const file = opts.out || 'commits.json'
|
|
374
|
-
const filepath = outputFilePath(file, outDir)
|
|
375
|
-
writeJSON(filepath, groups || records)
|
|
376
|
-
console.log(chalk.green(`JSON 已导出: ${filepath}`))
|
|
377
|
-
return
|
|
594
|
+
const file = opts.out || 'commits.json'
|
|
595
|
+
const filepath = outputFilePath(file, outDir)
|
|
596
|
+
writeJSON(filepath, groups || records)
|
|
597
|
+
console.log(chalk.green(`JSON 已导出: ${filepath}`))
|
|
598
|
+
return
|
|
378
599
|
}
|
|
379
600
|
|
|
380
601
|
if (opts.format === 'text') {
|
|
381
|
-
const file = opts.out || 'commits.txt'
|
|
382
|
-
const filepath = outputFilePath(file, outDir)
|
|
383
|
-
const text = renderText(records, groups, { showGerrit: !!opts.gerrit })
|
|
384
|
-
writeTextFile(filepath, text)
|
|
385
|
-
console.log(text)
|
|
386
|
-
console.log(chalk.green(`文本已导出: ${filepath}`))
|
|
387
|
-
return
|
|
602
|
+
const file = opts.out || 'commits.txt'
|
|
603
|
+
const filepath = outputFilePath(file, outDir)
|
|
604
|
+
const text = renderText(records, groups, { showGerrit: !!opts.gerrit })
|
|
605
|
+
writeTextFile(filepath, text)
|
|
606
|
+
console.log(text)
|
|
607
|
+
console.log(chalk.green(`文本已导出: ${filepath}`))
|
|
608
|
+
return
|
|
388
609
|
}
|
|
389
610
|
|
|
390
611
|
if (opts.format === 'excel') {
|
|
391
|
-
const excelFile = opts.out || 'commits.xlsx'
|
|
392
|
-
const excelPath = outputFilePath(excelFile, outDir)
|
|
393
|
-
const txtFile = excelFile.replace(/\.xlsx$/, '.txt')
|
|
394
|
-
const txtPath = outputFilePath(txtFile, outDir)
|
|
612
|
+
const excelFile = opts.out || 'commits.xlsx'
|
|
613
|
+
const excelPath = outputFilePath(excelFile, outDir)
|
|
614
|
+
const txtFile = excelFile.replace(/\.xlsx$/, '.txt')
|
|
615
|
+
const txtPath = outputFilePath(txtFile, outDir)
|
|
395
616
|
await exportExcel(records, groups, {
|
|
396
617
|
file: excelPath,
|
|
397
618
|
stats: opts.stats,
|
|
398
619
|
gerrit: opts.gerrit
|
|
399
|
-
})
|
|
400
|
-
const text = renderText(records, groups)
|
|
401
|
-
writeTextFile(txtPath, text)
|
|
402
|
-
console.log(chalk.green(`Excel 已导出: ${excelPath}`))
|
|
403
|
-
console.log(chalk.green(`文本已自动导出: ${txtPath}`))
|
|
620
|
+
})
|
|
621
|
+
const text = renderText(records, groups)
|
|
622
|
+
writeTextFile(txtPath, text)
|
|
623
|
+
console.log(chalk.green(`Excel 已导出: ${excelPath}`))
|
|
624
|
+
console.log(chalk.green(`文本已自动导出: ${txtPath}`))
|
|
404
625
|
}
|
|
405
|
-
|
|
626
|
+
await autoCheckUpdate()
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
main()
|
package/src/git.mjs
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import boxen from 'boxen'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import isOnline from 'is-online'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import semver from 'semver'
|
|
7
|
+
|
|
8
|
+
import { colors } from './colors.mjs'
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'configstore')
|
|
11
|
+
|
|
12
|
+
const getCacheFile = (pkg) =>
|
|
13
|
+
path.join(CONFIG_DIR, `update-notifier-${pkg.name}.json`)
|
|
14
|
+
|
|
15
|
+
async function fetchLatestVersion(pkgName, timeout = 1500) {
|
|
16
|
+
const url = `https://registry.npmjs.org/${pkgName}/latest`
|
|
17
|
+
const controller = new AbortController()
|
|
18
|
+
const timer = setTimeout(() => controller.abort(), timeout)
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(url, { signal: controller.signal })
|
|
22
|
+
clearTimeout(timer)
|
|
23
|
+
if (!res.ok) throw new Error('fetch fail')
|
|
24
|
+
const data = await res.json()
|
|
25
|
+
return data.version
|
|
26
|
+
} catch (e) {
|
|
27
|
+
clearTimeout(timer)
|
|
28
|
+
// 超时或其他错误都返回 null,防止程序卡死
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 格式化升级提示信息
|
|
35
|
+
*/
|
|
36
|
+
export function formatUpdateMessage(current, latest, name) {
|
|
37
|
+
const arrow = colors.dim('→')
|
|
38
|
+
const currentVer = colors.dim(current)
|
|
39
|
+
const latestVer = colors.green(latest)
|
|
40
|
+
|
|
41
|
+
const message =
|
|
42
|
+
`${colors.dim('Update available')}` +
|
|
43
|
+
` ${currentVer} ${arrow} ${latestVer}\n` +
|
|
44
|
+
`${colors.dim('Run')} ${colors.cyanish(`npm i -g ${name}`)} ${colors.dim(' to update')}`
|
|
45
|
+
|
|
46
|
+
return boxen(message, {
|
|
47
|
+
padding: 1,
|
|
48
|
+
margin: 1,
|
|
49
|
+
borderStyle: 'round',
|
|
50
|
+
borderColor: 'yellow'
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 检查更新(带缓存 + 支持 patch 更新)
|
|
56
|
+
*/
|
|
57
|
+
export async function checkUpdateWithPatch({
|
|
58
|
+
pkg,
|
|
59
|
+
interval = 24 * 60 * 60 * 1000,
|
|
60
|
+
// interval = 6 * 1000,
|
|
61
|
+
force = false
|
|
62
|
+
} = {}) {
|
|
63
|
+
const online = await isOnline()
|
|
64
|
+
// console.log('online', online)
|
|
65
|
+
if (!online) return null
|
|
66
|
+
const now = Date.now()
|
|
67
|
+
const CACHE_FILE = getCacheFile(pkg)
|
|
68
|
+
|
|
69
|
+
let cache = {}
|
|
70
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
71
|
+
try {
|
|
72
|
+
cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')) || {}
|
|
73
|
+
// eslint-disable-next-line no-empty
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// console.log('now - cache.lastCheck', now - cache.lastCheck, interval)
|
|
78
|
+
if (!force && cache.lastCheck && now - cache.lastCheck < interval) {
|
|
79
|
+
return cache.updateInfo || null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const latest = await fetchLatestVersion(pkg.name)
|
|
83
|
+
if (!latest) {
|
|
84
|
+
// console.log('无法获取最新版本')
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (semver.lt(pkg.version, latest)) {
|
|
89
|
+
// 构造 update 对象,兼容 update-notifier
|
|
90
|
+
const updateInfo = {
|
|
91
|
+
current: pkg.version,
|
|
92
|
+
latest,
|
|
93
|
+
type: semver.diff(pkg.version, latest) || 'patch',
|
|
94
|
+
name: pkg.name,
|
|
95
|
+
// 这里官方还会有 message 字段
|
|
96
|
+
// 用官方的默认消息格式生成,方便notify打印
|
|
97
|
+
message: `\nUpdate available ${pkg.version} → ${latest}\nRun npm i -g ${pkg.name} to update\n`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 缓存
|
|
101
|
+
try {
|
|
102
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
103
|
+
fs.writeFileSync(
|
|
104
|
+
CACHE_FILE,
|
|
105
|
+
JSON.stringify(
|
|
106
|
+
{
|
|
107
|
+
lastCheck: now,
|
|
108
|
+
updateInfo
|
|
109
|
+
},
|
|
110
|
+
null,
|
|
111
|
+
2
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error('缓存写入失败:', e)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { current } = updateInfo
|
|
119
|
+
console.log(formatUpdateMessage(current, latest, pkg.name))
|
|
120
|
+
return updateInfo
|
|
121
|
+
}
|
|
122
|
+
// 无更新,刷新缓存时间
|
|
123
|
+
try {
|
|
124
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
125
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ lastCheck: now }, null, 2))
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error('缓存写入失败:', e)
|
|
128
|
+
}
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Chalk } from 'chalk'
|
|
2
|
+
|
|
3
|
+
const chalk = new Chalk({ level: 3 }) // 强制开启 truecolor chalk v5
|
|
4
|
+
// 自定义灰色,适合透明和深色背景,避免看不清
|
|
5
|
+
const betterGray = chalk.hex('#BBBBBB')
|
|
6
|
+
|
|
7
|
+
// 统一颜色模块
|
|
8
|
+
export const colors = {
|
|
9
|
+
// 标题、版本号
|
|
10
|
+
title: chalk.bold.cyan,
|
|
11
|
+
|
|
12
|
+
// 命令名
|
|
13
|
+
command: chalk.green,
|
|
14
|
+
|
|
15
|
+
// 选项标记
|
|
16
|
+
option: chalk.magenta,
|
|
17
|
+
|
|
18
|
+
// 描述文字,替代默认灰色
|
|
19
|
+
desc: betterGray,
|
|
20
|
+
|
|
21
|
+
// Usage、Commands、Options、示例等小标题
|
|
22
|
+
header: chalk.yellow,
|
|
23
|
+
|
|
24
|
+
// 示例文字,普通白色
|
|
25
|
+
example: chalk.white,
|
|
26
|
+
|
|
27
|
+
// 警告红色
|
|
28
|
+
error: chalk.bold.red,
|
|
29
|
+
|
|
30
|
+
// 成功绿色
|
|
31
|
+
success: chalk.bold.green,
|
|
32
|
+
|
|
33
|
+
// 信息蓝色
|
|
34
|
+
info: chalk.blue,
|
|
35
|
+
|
|
36
|
+
// 可用更柔和的调暗白色,替代 dim gray
|
|
37
|
+
dim: chalk.white.dim,
|
|
38
|
+
bold: chalk.bold,
|
|
39
|
+
boldBlue: chalk.bold.blue,
|
|
40
|
+
gray: chalk.bold.gray,
|
|
41
|
+
green: chalk.green,
|
|
42
|
+
cyanish: chalk.hex('#57a097'),
|
|
43
|
+
white: chalk.white,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const c = {
|
|
47
|
+
title: chalk.bold.cyan,
|
|
48
|
+
label: chalk.yellow,
|
|
49
|
+
value: chalk.white,
|
|
50
|
+
dim: chalk.hex('#BBBBBB'),
|
|
51
|
+
success: chalk.green,
|
|
52
|
+
green: chalk.green,
|
|
53
|
+
error: chalk.red,
|
|
54
|
+
red: chalk.red,
|
|
55
|
+
link: chalk.blue.underline
|
|
56
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import os from 'os'
|
|
2
|
+
import process from 'process'
|
|
3
|
+
import { colors } from './colors.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function showVersionInfo(VERSION) {
|
|
7
|
+
console.log(colors.title(`\nwukong-deploy v${VERSION}\n`))
|
|
8
|
+
console.log(`${colors.bold('Node.js')}: ${process.version}`)
|
|
9
|
+
console.log(`${colors.bold('Platform')}: ${os.platform()} ${os.arch()}`)
|
|
10
|
+
}
|