wukong-gitlog-cli 0.0.5 → 0.0.6
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 +10 -0
- package/README.md +45 -0
- package/package.json +17 -6
- package/src/cli.mjs +45 -0
- package/src/overtime.mjs +233 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
### [0.0.6](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.5...v0.0.6) (2025-11-27)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 统计非工作时间提交量 ([7b8b0e7](https://github.com/tomatobybike/wukong-gitlog-cli/commit/7b8b0e7f2f85f2cd9e597d9bfd48cb4445937a09))
|
|
11
|
+
* 🎸 lock package.json ([ae94dc7](https://github.com/tomatobybike/wukong-gitlog-cli/commit/ae94dc7c4445347ef0f06141176c9cb7f429ecf0))
|
|
12
|
+
* 🎸 Statistical overtime culture ([c6841bb](https://github.com/tomatobybike/wukong-gitlog-cli/commit/c6841bbff1656e72a5a0129c1cc08f1ee457c768))
|
|
13
|
+
* 🎸 Statistical overtime culture 统计加班文化 ([65ac7ca](https://github.com/tomatobybike/wukong-gitlog-cli/commit/65ac7ca3772ad018dc57623796859ad1f87df861))
|
|
14
|
+
|
|
5
15
|
### [0.0.5](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.4...v0.0.5) (2025-11-27)
|
|
6
16
|
|
|
7
17
|
### [0.0.4](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.3...v0.0.4) (2025-11-27)
|
package/README.md
CHANGED
|
@@ -76,6 +76,12 @@ Command-line options:
|
|
|
76
76
|
- `--json` Output JSON
|
|
77
77
|
- `--format <type>` Output format: `text` | `excel` | `json` (default: `text`)
|
|
78
78
|
- `--group-by <type>` Group commits by date: `day` | `month`
|
|
79
|
+
- `--overtime` Analyze overtime culture: output counts/percentages for commits outside work hours and on non-workdays (per-person breakdown)
|
|
80
|
+
- `--country <code>` Country/region for holidays (CN|US). Default: `CN`.
|
|
81
|
+
- `--work-start <hour>` Workday start hour. Default: `9`.
|
|
82
|
+
- `--work-end <hour>` Workday end hour. Default: `18`.
|
|
83
|
+
- `--lunch-start <hour>` Lunch break start hour. Default: `12`.
|
|
84
|
+
- `--lunch-end <hour>` Lunch break end hour. Default: `14`.
|
|
79
85
|
- `--stats` Include a `Stats` sheet in the Excel export
|
|
80
86
|
- `--gerrit-api <url>` Optional: Gerrit REST API base URL for resolving `{{changeNumber}}` (e.g. `https://gerrit.example.com/gerrit`)
|
|
81
87
|
- `--gerrit-auth <token>` Optional: Authorization for Gerrit REST API (either `user:pass` for Basic or token string for Bearer)
|
|
@@ -212,6 +218,45 @@ ea82531 | tom | 2025-11-25 | feat: 🎸 init
|
|
|
212
218
|
741de50 | tom | 2025-11-25 | first commit
|
|
213
219
|
```
|
|
214
220
|
|
|
221
|
+
You can also analyze overtime culture with the `--overtime` flag to get overall and per-person overtime submission rates (default work window is 09:00-18:00). Example:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
wukong-gitlog-cli --overtime --limit 500
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Overtime demo scripts (npm)
|
|
228
|
+
|
|
229
|
+
Below are helpful npm scripts added for quickly running the overtime analysis with commonly used configurations. They are already present in `package.json` and can be run from the project root.
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Run a US-focused overtime text report using 10:00-19:00 work hours and a 12:00-13:00 lunch break
|
|
233
|
+
npm run cli:overtime-text-us
|
|
234
|
+
|
|
235
|
+
# Run a US-focused overtime text report and write the outputs into the project parent's output folder
|
|
236
|
+
npm run cli:overtime-text-us-parent
|
|
237
|
+
|
|
238
|
+
# Run a US-focused overtime text report and write the output into ../output (explicit --out-dir)
|
|
239
|
+
npm run cli:overtime-text-us-outdir
|
|
240
|
+
|
|
241
|
+
# Run a CN-focused overtime Excel report (default 9:00-18:00 work hours and 12:00-14:00 lunch)
|
|
242
|
+
npm run cli:overtime-excel-cn
|
|
243
|
+
|
|
244
|
+
# Run a CN-focused overtime Excel report and write outputs to parent output folder
|
|
245
|
+
npm run cli:overtime-excel-cn-parent
|
|
246
|
+
|
|
247
|
+
# Run a CN-focused overtime Excel report and write outputs to ../output via --out-dir
|
|
248
|
+
npm run cli:overtime-excel-cn-outdir
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Notes:
|
|
252
|
+
|
|
253
|
+
- Output files are written into `output/` by default. Use `--out-dir` or `--out-parent` to change output location.
|
|
254
|
+
- If you prefer different working hours or country codes, either modify the script in `package.json` or run the CLI manually with flags (e.g. `--work-start`, `--work-end`, `--lunch-start`, `--lunch-end`, `--country`).
|
|
255
|
+
|
|
256
|
+
Formatting note:
|
|
257
|
+
|
|
258
|
+
- The text report is formatted to align columns correctly even when commit messages or author names contain mixed Chinese and English characters (uses `string-width` for display-aware padding).
|
|
259
|
+
|
|
215
260
|
Example JSON output (from `npm run cli:json-demo`):
|
|
216
261
|
|
|
217
262
|
```json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-gitlog-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -45,6 +45,15 @@
|
|
|
45
45
|
"cli:json-demo": "node ./src/cli.mjs --json --limit 5 --out commits.json",
|
|
46
46
|
"cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
|
|
47
47
|
"cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
|
|
48
|
+
"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
|
+
"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
|
+
"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
|
+
"cli:overtime-excel-cn-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
48
57
|
"format": "prettier --write \"src/**/*.{js,mjs}\"",
|
|
49
58
|
"lint": "eslint src --ext .js,.mjs src",
|
|
50
59
|
"lint:fix": "eslint src --ext .js,.mjs --fix",
|
|
@@ -75,11 +84,13 @@
|
|
|
75
84
|
]
|
|
76
85
|
},
|
|
77
86
|
"dependencies": {
|
|
78
|
-
"chalk": "
|
|
79
|
-
"commander": "
|
|
80
|
-
"dayjs": "
|
|
81
|
-
"
|
|
82
|
-
"
|
|
87
|
+
"chalk": "5.6.0",
|
|
88
|
+
"commander": "12.1.0",
|
|
89
|
+
"dayjs": "1.11.19",
|
|
90
|
+
"date-holidays": "2.1.1",
|
|
91
|
+
"string-width": "5.1.2",
|
|
92
|
+
"exceljs": "4.4.0",
|
|
93
|
+
"zx": "7.2.4"
|
|
83
94
|
},
|
|
84
95
|
"devDependencies": {
|
|
85
96
|
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
package/src/cli.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { getGitLogs } from './git.mjs';
|
|
5
5
|
import { renderText } from './text.mjs';
|
|
6
|
+
import { analyzeOvertime, renderOvertimeText, renderOvertimeTab, renderOvertimeCsv } from './overtime.mjs';
|
|
6
7
|
import { exportExcel } from './excel.mjs';
|
|
7
8
|
import { groupRecords, writeJSON, writeTextFile, outputFilePath } from './utils/index.mjs';
|
|
8
9
|
|
|
@@ -24,6 +25,12 @@ program
|
|
|
24
25
|
.option('--gerrit <prefix>', '显示 Gerrit 地址,支持在 prefix 中使用 {{hash}} 占位符')
|
|
25
26
|
.option('--gerrit-api <url>', '可选:Gerrit REST API 基础地址,用于解析 changeNumber,例如 `https://gerrit.example.com`')
|
|
26
27
|
.option('--gerrit-auth <tokenOrUserPass>', '可选:Gerrit API 授权,格式为 `user:pass` 或 `TOKEN`(表示 Bearer token)')
|
|
28
|
+
.option('--overtime', '分析公司加班文化(输出下班时间与非工作日提交占比)')
|
|
29
|
+
.option('--country <code>', '节假日国家:CN 或 US,默认为 CN', 'CN')
|
|
30
|
+
.option('--work-start <hour>', '上班开始小时,默认 9', (v) => parseInt(v, 10), 9)
|
|
31
|
+
.option('--work-end <hour>', '下班小时,默认 18', (v) => parseInt(v, 10), 18)
|
|
32
|
+
.option('--lunch-start <hour>', '午休开始小时,默认 12', (v) => parseInt(v, 10), 12)
|
|
33
|
+
.option('--lunch-end <hour>', '午休结束小时,默认 14', (v) => parseInt(v, 10), 14)
|
|
27
34
|
.option('--out <file>', '输出文件名(不含路径)')
|
|
28
35
|
.option('--out-dir <dir>', '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output`')
|
|
29
36
|
.option('--out-parent', '将输出目录放到当前工程的父目录的 `output/`(等同于 `--out-dir ../output`)')
|
|
@@ -133,6 +140,44 @@ const opts = program.opts();
|
|
|
133
140
|
// --- 分组 ---
|
|
134
141
|
const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null;
|
|
135
142
|
|
|
143
|
+
// --- Overtime analysis ---
|
|
144
|
+
if (opts.overtime) {
|
|
145
|
+
const stats = analyzeOvertime(records, {
|
|
146
|
+
startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
147
|
+
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
148
|
+
lunchStart: opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
149
|
+
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
150
|
+
country: opts.country || 'CN',
|
|
151
|
+
});
|
|
152
|
+
// Output to console
|
|
153
|
+
console.log('\n--- Overtime analysis ---\n');
|
|
154
|
+
console.log(renderOvertimeText(stats));
|
|
155
|
+
// if user requested json format, write stats to file
|
|
156
|
+
if (opts.json || opts.format === 'json') {
|
|
157
|
+
const file = opts.out || 'overtime.json';
|
|
158
|
+
const filepath = outputFilePath(file, outDir);
|
|
159
|
+
writeJSON(filepath, stats);
|
|
160
|
+
console.log(chalk.green(`overtime JSON 已导出: ${filepath}`));
|
|
161
|
+
}
|
|
162
|
+
// Always write human readable overtime text to file (default: overtime.txt)
|
|
163
|
+
const outBase = opts.out ? path.basename(opts.out, path.extname(opts.out)) : 'commits';
|
|
164
|
+
const overtimeFileName = `overtime_${outBase}.txt`;
|
|
165
|
+
const overtimeFile = outputFilePath(overtimeFileName, outDir);
|
|
166
|
+
writeTextFile(overtimeFile, renderOvertimeText(stats));
|
|
167
|
+
// write tab-separated text file for better alignment in editors that use proportional fonts
|
|
168
|
+
const overtimeTabFileName = `overtime_${outBase}.tab.txt`;
|
|
169
|
+
const overtimeTabFile = outputFilePath(overtimeTabFileName, outDir);
|
|
170
|
+
writeTextFile(overtimeTabFile, renderOvertimeTab(stats));
|
|
171
|
+
// write CSV for structured data consumption
|
|
172
|
+
const overtimeCsvFileName = `overtime_${outBase}.csv`;
|
|
173
|
+
const overtimeCsvFile = outputFilePath(overtimeCsvFileName, outDir);
|
|
174
|
+
writeTextFile(overtimeCsvFile, renderOvertimeCsv(stats));
|
|
175
|
+
console.log(chalk.green(`Overtime text 已导出: ${overtimeFile}`));
|
|
176
|
+
console.log(chalk.green(`Overtime table (tabs) 已导出: ${overtimeTabFile}`));
|
|
177
|
+
console.log(chalk.green(`Overtime CSV 已导出: ${overtimeCsvFile}`));
|
|
178
|
+
// don't return — allow other outputs to proceed
|
|
179
|
+
}
|
|
180
|
+
|
|
136
181
|
// --- JSON ---
|
|
137
182
|
if (opts.json || opts.format === 'json') {
|
|
138
183
|
const file = opts.out || 'commits.json';
|
package/src/overtime.mjs
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import DateHolidays from 'date-holidays';
|
|
3
|
+
import stringWidth from 'string-width';
|
|
4
|
+
|
|
5
|
+
export function parseCommitDate(d) {
|
|
6
|
+
// d examples: '2025-11-14 23:53:04 +0800' or ISO format
|
|
7
|
+
// Try dayjs parsing; fallback to slicing timezone
|
|
8
|
+
let dt = dayjs(d);
|
|
9
|
+
if (!dt.isValid()) {
|
|
10
|
+
// strip timezone offset and try again
|
|
11
|
+
const m = String(d).match(/^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})/);
|
|
12
|
+
if (m) dt = dayjs(m[1]);
|
|
13
|
+
}
|
|
14
|
+
return dt;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isWeekend(dt) {
|
|
18
|
+
const day = dt.day();
|
|
19
|
+
return day === 0 || day === 6; // Sunday=0, Saturday=6
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isOutsideWorkHours(dt, startHour = 9, endHour = 18, lunchStart = 12, lunchEnd = 14) {
|
|
23
|
+
const hour = dt.hour();
|
|
24
|
+
// Working hours are startHour <= hour < endHour, excluding lunchtime lunchStart <= hour < lunchEnd
|
|
25
|
+
const inWorkHours = hour >= startHour && hour < endHour && !(hour >= lunchStart && hour < lunchEnd);
|
|
26
|
+
return !inWorkHours;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function analyzeOvertime(records, opts = {}) {
|
|
30
|
+
const { startHour = 9, endHour = 18, lunchStart = 12, lunchEnd = 14, country = 'CN' } = opts;
|
|
31
|
+
const total = records.length;
|
|
32
|
+
|
|
33
|
+
let outsideWorkCount = 0;
|
|
34
|
+
let nonWorkdayCount = 0;
|
|
35
|
+
let holidayCount = 0;
|
|
36
|
+
|
|
37
|
+
const byAuthor = new Map();
|
|
38
|
+
|
|
39
|
+
let startCommit = null;
|
|
40
|
+
let endCommit = null;
|
|
41
|
+
let latestCommit = null;
|
|
42
|
+
|
|
43
|
+
// init holiday checker for country
|
|
44
|
+
const hd = new DateHolidays();
|
|
45
|
+
try {
|
|
46
|
+
hd.init(String(country).toUpperCase());
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// fallback to CN
|
|
49
|
+
hd.init('CN');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
records.forEach((r) => {
|
|
53
|
+
const dt = parseCommitDate(r.date);
|
|
54
|
+
if (!dt || !dt.isValid()) return; // skip
|
|
55
|
+
|
|
56
|
+
const outside = isOutsideWorkHours(dt, startHour, endHour, lunchStart, lunchEnd);
|
|
57
|
+
const isHoliday = !!hd.isHoliday(dt.toDate());
|
|
58
|
+
const isNonWork = isWeekend(dt) || isHoliday;
|
|
59
|
+
|
|
60
|
+
if (outside) outsideWorkCount++;
|
|
61
|
+
if (isNonWork) nonWorkdayCount++;
|
|
62
|
+
if (isHoliday) holidayCount++;
|
|
63
|
+
|
|
64
|
+
const authorName = r.author || r.email || 'unknown';
|
|
65
|
+
const key = `${authorName} <${r.email || ''}>`;
|
|
66
|
+
const info = byAuthor.get(key) || {
|
|
67
|
+
name: authorName,
|
|
68
|
+
email: r.email || '',
|
|
69
|
+
total: 0,
|
|
70
|
+
outsideWorkCount: 0,
|
|
71
|
+
nonWorkdayCount: 0,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
info.total++;
|
|
75
|
+
if (outside) info.outsideWorkCount++;
|
|
76
|
+
if (isNonWork) info.nonWorkdayCount++;
|
|
77
|
+
if (isHoliday) info.holidayCount = (info.holidayCount || 0) + 1;
|
|
78
|
+
byAuthor.set(key, info);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const perAuthor = [];
|
|
82
|
+
for (const [, v] of byAuthor) {
|
|
83
|
+
perAuthor.push({
|
|
84
|
+
name: v.name,
|
|
85
|
+
email: v.email,
|
|
86
|
+
total: v.total,
|
|
87
|
+
outsideWorkCount: v.outsideWorkCount,
|
|
88
|
+
nonWorkdayCount: v.nonWorkdayCount,
|
|
89
|
+
holidayCount: v.holidayCount || 0,
|
|
90
|
+
outsideWorkRate: v.total ? +(v.outsideWorkCount / v.total).toFixed(3) : 0,
|
|
91
|
+
nonWorkdayRate: v.total ? +(v.nonWorkdayCount / v.total).toFixed(3) : 0,
|
|
92
|
+
holidayRate: v.total ? +((v.holidayCount || 0) / v.total).toFixed(3) : 0,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// compute start/end/latest commit
|
|
97
|
+
// convert to dayjs and find min/max
|
|
98
|
+
const validRecords = records
|
|
99
|
+
.map((r) => ({ ...r, _dt: parseCommitDate(r.date) }))
|
|
100
|
+
.filter((r) => r._dt && r._dt.isValid());
|
|
101
|
+
if (validRecords.length > 0) {
|
|
102
|
+
validRecords.sort((a, b) => a._dt.valueOf() - b._dt.valueOf());
|
|
103
|
+
[startCommit] = validRecords;
|
|
104
|
+
endCommit = validRecords[validRecords.length - 1];
|
|
105
|
+
latestCommit = endCommit;
|
|
106
|
+
// cleanup temp _dt
|
|
107
|
+
for (let i = 0; i < validRecords.length; i++) {
|
|
108
|
+
// copy the object without _dt for safety
|
|
109
|
+
delete validRecords[i]._dt;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// sort perAuthor by outsideWorkRate desc
|
|
114
|
+
perAuthor.sort((a, b) => b.outsideWorkRate - a.outsideWorkRate || b.total - a.total);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
total,
|
|
118
|
+
outsideWorkCount,
|
|
119
|
+
nonWorkdayCount,
|
|
120
|
+
outsideWorkRate: total ? +(outsideWorkCount / total).toFixed(3) : 0,
|
|
121
|
+
nonWorkdayRate: total ? +(nonWorkdayCount / total).toFixed(3) : 0,
|
|
122
|
+
perAuthor,
|
|
123
|
+
/// 提示:计算 min/max 日期 & latest commit
|
|
124
|
+
startCommit: startCommit || null,
|
|
125
|
+
endCommit: endCommit || null,
|
|
126
|
+
latestCommit: latestCommit || null,
|
|
127
|
+
startHour,
|
|
128
|
+
endHour,
|
|
129
|
+
lunchStart,
|
|
130
|
+
lunchEnd,
|
|
131
|
+
country,
|
|
132
|
+
holidayCount,
|
|
133
|
+
holidayRate: total ? +(holidayCount / total).toFixed(3) : 0,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function renderOvertimeText(stats) {
|
|
138
|
+
const { total, outsideWorkCount, nonWorkdayCount, holidayCount, outsideWorkRate, nonWorkdayRate, holidayRate, perAuthor, startHour, endHour, lunchStart, lunchEnd, country } = stats;
|
|
139
|
+
const { startCommit, endCommit, latestCommit } = stats;
|
|
140
|
+
const lines = [];
|
|
141
|
+
|
|
142
|
+
const formatPercent = (v) => `${(v * 100).toFixed(1)}%`;
|
|
143
|
+
const padDisplayEnd = (s, width) => {
|
|
144
|
+
const t = String(s ?? '');
|
|
145
|
+
const w = stringWidth(t);
|
|
146
|
+
return t + ' '.repeat(Math.max(0, width - w));
|
|
147
|
+
};
|
|
148
|
+
const padDisplayStart = (s, width) => {
|
|
149
|
+
const t = String(s ?? '');
|
|
150
|
+
const w = stringWidth(t);
|
|
151
|
+
return ' '.repeat(Math.max(0, width - w)) + t;
|
|
152
|
+
};
|
|
153
|
+
const cols = {
|
|
154
|
+
name: 22,
|
|
155
|
+
total: 6,
|
|
156
|
+
outside: 9,
|
|
157
|
+
outsideRate: 10,
|
|
158
|
+
nonWork: 10,
|
|
159
|
+
nonWorkRate: 12,
|
|
160
|
+
holiday: 8,
|
|
161
|
+
holidayRate: 10,
|
|
162
|
+
};
|
|
163
|
+
lines.push(`总提交数:${total}`);
|
|
164
|
+
if (startCommit && endCommit) {
|
|
165
|
+
lines.push(`统计区间:${startCommit.date} — ${endCommit.date}`);
|
|
166
|
+
}
|
|
167
|
+
if (latestCommit) {
|
|
168
|
+
lines.push('最晚一次提交:');
|
|
169
|
+
lines.push(` Hash : ${latestCommit.hash}`);
|
|
170
|
+
lines.push(` Author : ${latestCommit.author}`);
|
|
171
|
+
lines.push(` Date : ${latestCommit.date}`);
|
|
172
|
+
lines.push(` Message: ${latestCommit.message}`);
|
|
173
|
+
}
|
|
174
|
+
// country: holiday region, lunchStart/lunchEnd define midday break
|
|
175
|
+
lines.push(`下班时间定义:${startHour}:00 - ${endHour}:00 (午休 ${lunchStart}:00 - ${lunchEnd}:00)`);
|
|
176
|
+
lines.push(`国家假期参考:${String(country).toUpperCase()},节假日提交数:${holidayCount},占比:${(holidayRate * 100).toFixed(1)}%`);
|
|
177
|
+
lines.push(`下班时间(工作时间外)提交数:${outsideWorkCount},占比:${(outsideWorkRate * 100).toFixed(1)}%`);
|
|
178
|
+
lines.push(`非工作日(周末)提交数:${nonWorkdayCount},占比:${(nonWorkdayRate * 100).toFixed(1)}%`);
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push('按人员统计:');
|
|
181
|
+
// header
|
|
182
|
+
const header = ` ${padDisplayEnd('Name', cols.name)} | ${padDisplayStart('总数', cols.total)} | ${padDisplayStart('下班外数', cols.outside)} | ${padDisplayStart('下班外占比', cols.outsideRate)} | ${padDisplayStart('非工作日数', cols.nonWork)} | ${padDisplayStart('非工作日占比', cols.nonWorkRate)} | ${padDisplayStart('假日数', cols.holiday)} | ${padDisplayStart('假日占比', cols.holidayRate)}`;
|
|
183
|
+
lines.push(header);
|
|
184
|
+
const totalWidth = cols.name + cols.total + cols.outside + cols.outsideRate + cols.nonWork + cols.nonWorkRate + cols.holiday + cols.holidayRate + 3 * 7; // approximate separator widths
|
|
185
|
+
lines.push('-'.repeat(Math.max(80, totalWidth)));
|
|
186
|
+
|
|
187
|
+
perAuthor.forEach((p) => {
|
|
188
|
+
const name = (p.name || '-').toString();
|
|
189
|
+
const row = ` ${padDisplayEnd(name, cols.name)} | ${padDisplayStart(String(p.total), cols.total)} | ${padDisplayStart(String(p.outsideWorkCount), cols.outside)} | ${padDisplayStart(formatPercent(p.outsideWorkRate), cols.outsideRate)} | ${padDisplayStart(String(p.nonWorkdayCount), cols.nonWork)} | ${padDisplayStart(formatPercent(p.nonWorkdayRate), cols.nonWorkRate)} | ${padDisplayStart(String(p.holidayCount), cols.holiday)} | ${padDisplayStart(formatPercent(p.holidayRate), cols.holidayRate)}`;
|
|
190
|
+
lines.push(row);
|
|
191
|
+
});
|
|
192
|
+
lines.push('');
|
|
193
|
+
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function renderOvertimeTab(stats) {
|
|
198
|
+
const { total, outsideWorkCount, nonWorkdayCount, holidayCount, outsideWorkRate, nonWorkdayRate, holidayRate, perAuthor, startHour, endHour, lunchStart, lunchEnd, country } = stats;
|
|
199
|
+
const { startCommit, endCommit, latestCommit } = stats;
|
|
200
|
+
const rows = [];
|
|
201
|
+
rows.push(`总提交数:\t${total}`);
|
|
202
|
+
if (startCommit && endCommit) rows.push(`统计区间:\t${startCommit.date} — ${endCommit.date}`);
|
|
203
|
+
if (latestCommit) rows.push(`最晚一次提交:\t${latestCommit.hash}\t${latestCommit.author}\t${latestCommit.date}\t${latestCommit.message}`);
|
|
204
|
+
rows.push(`下班时间定义:\t${startHour}:00 - ${endHour}:00 (午休 ${lunchStart}:00 - ${lunchEnd}:00)`);
|
|
205
|
+
rows.push(`国家假期参考:\t${String(country).toUpperCase()}\t节假日提交数:\t${holidayCount}\t节假日占比:\t${(holidayRate * 100).toFixed(1)}%`);
|
|
206
|
+
rows.push(`下班时间(工作时间外)提交数:\t${outsideWorkCount}\t占比:\t${(outsideWorkRate * 100).toFixed(1)}%`);
|
|
207
|
+
rows.push(`非工作日(周末)提交数:\t${nonWorkdayCount}\t占比:\t${(nonWorkdayRate * 100).toFixed(1)}%`);
|
|
208
|
+
rows.push('');
|
|
209
|
+
rows.push(['Name', '总数', '下班外数', '下班外占比', '非工作日数', '非工作日占比', '假日数', '假日占比'].join('\t'));
|
|
210
|
+
perAuthor.forEach((p) => {
|
|
211
|
+
rows.push([p.name || '-', p.total, p.outsideWorkCount, `${(p.outsideWorkRate * 100).toFixed(1)}%`, p.nonWorkdayCount, `${(p.nonWorkdayRate * 100).toFixed(1)}%`, p.holidayCount || 0, `${((p.holidayCount || 0) / p.total * 100).toFixed(1)}%`].join('\t'));
|
|
212
|
+
});
|
|
213
|
+
return rows.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function escapeCsv(v) {
|
|
217
|
+
const s = String(v ?? '');
|
|
218
|
+
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
|
219
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
220
|
+
}
|
|
221
|
+
return s;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function renderOvertimeCsv(stats) {
|
|
225
|
+
const { perAuthor } = stats;
|
|
226
|
+
const rows = [];
|
|
227
|
+
rows.push('Name,Total,OutsideCount,OutsideRate,NonWorkdayCount,NonWorkdayRate,HolidayCount,HolidayRate');
|
|
228
|
+
perAuthor.forEach((p) => {
|
|
229
|
+
rows.push(`${escapeCsv(p.name)},${p.total},${p.outsideWorkCount},${(p.outsideWorkRate * 100).toFixed(1)}%,${p.nonWorkdayCount},${(p.nonWorkdayRate * 100).toFixed(1)}%,${p.holidayCount || 0},${((p.holidayCount || 0) / p.total * 100).toFixed(1)}%`);
|
|
230
|
+
});
|
|
231
|
+
return rows.join('\n');
|
|
232
|
+
}
|
|
233
|
+
|