wukong-gitlog-cli 0.0.7 → 0.0.9
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 +15 -0
- package/README.md +21 -112
- package/doc/help.md +194 -0
- package/package.json +6 -2
- package/src/cli.mjs +168 -2
- package/src/excel.mjs +45 -0
- package/src/overtime.mjs +42 -8
- package/src/utils/file.mjs +27 -15
- package/src/utils/index.mjs +2 -2
- package/src/utils/output.mjs +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.9](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.8...v0.0.9) (2025-11-28)
|
|
6
|
+
|
|
7
|
+
### [0.0.8](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.7...v0.0.8) (2025-11-28)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* 🎸 --overtime ([636633b](https://github.com/tomatobybike/wukong-gitlog-cli/commit/636633b424f8e440f53eeaabee1c9ffdbbbdcd04))
|
|
13
|
+
* 🎸 cn datetime format ([7773b08](https://github.com/tomatobybike/wukong-gitlog-cli/commit/7773b084dadae415edc12a735bc450353ec127bf))
|
|
14
|
+
* 🎸 export excel ([2e87900](https://github.com/tomatobybike/wukong-gitlog-cli/commit/2e87900af98883a1440871b2db6b581b158a3b1f))
|
|
15
|
+
* 🎸 last work ([c31edff](https://github.com/tomatobybike/wukong-gitlog-cli/commit/c31edff080d7e8b1d6b1c2c9bdee2da57abaa3a0))
|
|
16
|
+
* 🎸 last work ([64fcf3f](https://github.com/tomatobybike/wukong-gitlog-cli/commit/64fcf3f9fa0f4c7055b2aa39368f3553b41093b3))
|
|
17
|
+
* 🎸 weekly monthly ([a4f76dd](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a4f76dd31192ee740f73f160729f6e4a8d32542f))
|
|
18
|
+
* **overtime:** add monthly and weekly overtime summary files ([024f3d4](https://github.com/tomatobybike/wukong-gitlog-cli/commit/024f3d497439106fabf8718bfd8076767404f7e8))
|
|
19
|
+
|
|
5
20
|
### [0.0.7](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.6...v0.0.7) (2025-11-27)
|
|
6
21
|
|
|
7
22
|
### [0.0.6](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.5...v0.0.6) (2025-11-27)
|
package/README.md
CHANGED
|
@@ -88,6 +88,27 @@ Command-line options:
|
|
|
88
88
|
>
|
|
89
89
|
> Tip: Use `--out-parent` or `--out-dir ../output` to write outputs into the parent folder's `output/` to avoid accidentally committing generated files to your repository.
|
|
90
90
|
|
|
91
|
+
### Per-period outputs
|
|
92
|
+
You can generate per-month and per-week outputs under `output/month/` and `output/week/` using the `--per-period-formats` option. Example:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
wukong-gitlog-cli ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Want per-period Excel outputs? Use `xlsx` along with `--per-period-excel-mode` for `sheets` or `files`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
wukong-gitlog-cli ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets
|
|
102
|
+
wukong-gitlog-cli ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If you'd like only per-period outputs and not the combined monthly/weekly summary files, add `--per-period-only`:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
wukong-gitlog-cli ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-only
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
|
|
91
112
|
---
|
|
92
113
|
|
|
93
114
|
## Gerrit support
|
|
@@ -167,124 +188,12 @@ wukong-gitlog-cli --out-dir ../output --format text --limit 5 --out custom1.txt
|
|
|
167
188
|
|
|
168
189
|
---
|
|
169
190
|
|
|
170
|
-
## Quick demo (npm scripts)
|
|
171
|
-
|
|
172
|
-
We provided a few convenient npm scripts to quickly run common scenarios. Run them from the project root:
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
# show help
|
|
176
|
-
npm run cli:help
|
|
177
|
-
|
|
178
|
-
# simple text export (commits.txt in ./output)
|
|
179
|
-
npm run cli:text-demo
|
|
180
|
-
|
|
181
|
-
# Excel export with stats (commits.xlsx + commits.txt in ./output)
|
|
182
|
-
npm run cli:excel-demo
|
|
183
|
-
|
|
184
|
-
# JSON export (commits.json in ./output)
|
|
185
|
-
npm run cli:json-demo
|
|
186
|
-
|
|
187
|
-
# Gerrit text export demo
|
|
188
|
-
npm run cli:gerrit-demo
|
|
189
|
-
# Gerrit Change-Id demo (use commit Change-Id to build Gerrit URLs when present)
|
|
190
|
-
npm run cli:gerrit-changeid-demo
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
If you prefer to write output outside the project (e.g., a parent `output/` folder), we also provide `npm` scripts that run with `--out-parent`:
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
# text export to parent `output/`
|
|
197
|
-
npm run cli:text-demo-parent
|
|
198
|
-
|
|
199
|
-
# excel export to parent `output/`
|
|
200
|
-
npm run cli:excel-demo-parent
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Example text output (from `npm run cli:text-demo`):
|
|
204
|
-
|
|
205
|
-
```text
|
|
206
|
-
Hash | Author | Date | Message
|
|
207
|
-
---------------------------------------------------------------------------------------------------------------------
|
|
208
|
-
c5bdf9d4 | tom | 2025-11-25 | feat: 🎸 增加output目录
|
|
209
191
|
|
|
210
|
-
ea82531 | tom | 2025-11-25 | feat: 🎸 init
|
|
211
|
-
|
|
212
|
-
741de50 | tom | 2025-11-25 | first commit
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
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:
|
|
216
192
|
|
|
217
193
|
```bash
|
|
218
194
|
wukong-gitlog-cli --overtime --limit 500
|
|
219
195
|
```
|
|
220
196
|
|
|
221
|
-
## Overtime demo scripts (npm)
|
|
222
|
-
|
|
223
|
-
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.
|
|
224
|
-
|
|
225
|
-
```bash
|
|
226
|
-
# Run a US-focused overtime text report using 10:00-19:00 work hours and a 12:00-13:00 lunch break
|
|
227
|
-
npm run cli:overtime-text-us
|
|
228
|
-
|
|
229
|
-
# Run a US-focused overtime text report and write the outputs into the project parent's output folder
|
|
230
|
-
npm run cli:overtime-text-us-parent
|
|
231
|
-
|
|
232
|
-
# Run a US-focused overtime text report and write the output into ../output (explicit --out-dir)
|
|
233
|
-
npm run cli:overtime-text-us-outdir
|
|
234
|
-
|
|
235
|
-
# Run a CN-focused overtime Excel report (default 9:00-18:00 work hours and 12:00-14:00 lunch)
|
|
236
|
-
npm run cli:overtime-excel-cn
|
|
237
|
-
|
|
238
|
-
# Run a CN-focused overtime Excel report and write outputs to parent output folder
|
|
239
|
-
npm run cli:overtime-excel-cn-parent
|
|
240
|
-
|
|
241
|
-
# Run a CN-focused overtime Excel report and write outputs to ../output via --out-dir
|
|
242
|
-
npm run cli:overtime-excel-cn-outdir
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Notes:
|
|
246
|
-
|
|
247
|
-
- Output files are written into `output/` by default. Use `--out-dir` or `--out-parent` to change output location.
|
|
248
|
-
- 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`).
|
|
249
|
-
|
|
250
|
-
Formatting note:
|
|
251
|
-
|
|
252
|
-
- 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).
|
|
253
|
-
|
|
254
|
-
Example JSON output (from `npm run cli:json-demo`):
|
|
255
|
-
|
|
256
|
-
```json
|
|
257
|
-
[
|
|
258
|
-
{
|
|
259
|
-
"hash": "c5bdf9d4f52f39bd7d580318bafc8ba4b6c129bc",
|
|
260
|
-
"author": "tom",
|
|
261
|
-
"email": "",
|
|
262
|
-
"date": "2025-11-25 17:24:32 +0800",
|
|
263
|
-
"message": "feat: 🎸 增加output目录"
|
|
264
|
-
}
|
|
265
|
-
/* truncated... */
|
|
266
|
-
]
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
Example JSON output including `changeId`/`gerrit` when `--gerrit` uses `{{changeId}}` (if present in commit):
|
|
270
|
-
|
|
271
|
-
```json
|
|
272
|
-
[
|
|
273
|
-
{
|
|
274
|
-
"hash": "Iabc...",
|
|
275
|
-
"author": "tom",
|
|
276
|
-
"email": "",
|
|
277
|
-
"date": "2025-11-25 17:24:32 +0800",
|
|
278
|
-
"message": "feat: add feature",
|
|
279
|
-
"body": "feat: add feature\n\nChange-Id: Iabcd123456789",
|
|
280
|
-
"changeId": "Iabcd123456789",
|
|
281
|
-
"gerrit": "https://gerrit.example.com/c/project/+/Iabcd123456789"
|
|
282
|
-
}
|
|
283
|
-
]
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
---
|
|
287
|
-
|
|
288
197
|
## Notes & Developer Info
|
|
289
198
|
|
|
290
199
|
- The CLI prints helpful messages after exporting files and writes outputs to the `output/` folder in the repo root.
|
package/doc/help.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
### Per-period outputs
|
|
2
|
+
|
|
3
|
+
You can generate per-month and per-week outputs under `output/month/` and `output/week/` using the `--per-period-formats` option. Example:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Want per-period Excel outputs? Use `xlsx` along with `--per-period-excel-mode` for `sheets` or `files`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets
|
|
13
|
+
node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats xlsx --per-period-excel-mode files
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If you'd like only per-period outputs and not the combined monthly/weekly summary files, add `--per-period-only`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-only
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Gerrit support
|
|
25
|
+
|
|
26
|
+
Use the `--gerrit` option to include a Gerrit link for each commit. You can provide a template containing `{{hash}}` to place the full commit hash into the URL, for example:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
wukong-gitlog-cli --gerrit "https://gerrit.example.com/c/project/+/{{hash}}" --limit 5 --format text
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If `{{hash}}` is not present, the CLI will append the commit hash to the prefix with a `/` separator.
|
|
33
|
+
|
|
34
|
+
You can also use `{{changeId}}` in the template to reference Gerrit change id. The tool will try to extract a `Change-Id: I...` value from the commit body and replace `{{changeId}}` with it. If it can't find a `Change-Id`, the CLI will fall back to using the commit `hash`.
|
|
35
|
+
|
|
36
|
+
The Gerrit link will show up in:
|
|
37
|
+
|
|
38
|
+
- The text output if `--format text` (as a new `Gerrit` column)
|
|
39
|
+
- The Excel export as a `Gerrit` column if `--format excel`
|
|
40
|
+
- JSON output will include a `gerrit` field for each record when `--gerrit` is used
|
|
41
|
+
- JSON output will include a `gerrit` field for each record when `--gerrit` is used
|
|
42
|
+
- When `--gerrit` uses `{{changeId}}`, the CLI will try to extract `Change-Id:` from the commit body and include `changeId` and `body` in the JSON record. If no `Change-Id` is present, the CLI falls back to `hash` when forming the Gerrit URL.
|
|
43
|
+
|
|
44
|
+
Note: `--out <file>` is the filename only and the directory used to store that file depends on:
|
|
45
|
+
|
|
46
|
+
- The default directory `./output/` in the current working directory
|
|
47
|
+
- `--out-dir <dir>` to override the target folder (relative or absolute)
|
|
48
|
+
- `--out-parent` to write to the parent repository folder `../output/` (same as `--out-dir ../output`)
|
|
49
|
+
|
|
50
|
+
For example:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# using globally installed CLI
|
|
54
|
+
wukong-gitlog-cli --out parent.json --out-parent
|
|
55
|
+
wukong-gitlog-cli --out demo.txt --out-dir ../temp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
Export as text, grouped by month, with Gerrit links:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
wukong-gitlog-cli --format text --group-by month --gerrit "https://gerrit.example.com/c/project/+/{{hash}}"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Quick demo (npm scripts)
|
|
71
|
+
|
|
72
|
+
We provided a few convenient npm scripts to quickly run common scenarios. Run them from the project root:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# show help
|
|
76
|
+
npm run cli:help
|
|
77
|
+
|
|
78
|
+
# simple text export (commits.txt in ./output)
|
|
79
|
+
npm run cli:text-demo
|
|
80
|
+
|
|
81
|
+
# Excel export with stats (commits.xlsx + commits.txt in ./output)
|
|
82
|
+
npm run cli:excel-demo
|
|
83
|
+
|
|
84
|
+
# JSON export (commits.json in ./output)
|
|
85
|
+
npm run cli:json-demo
|
|
86
|
+
|
|
87
|
+
# Gerrit text export demo
|
|
88
|
+
npm run cli:gerrit-demo
|
|
89
|
+
# Gerrit Change-Id demo (use commit Change-Id to build Gerrit URLs when present)
|
|
90
|
+
npm run cli:gerrit-changeid-demo
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If you prefer to write output outside the project (e.g., a parent `output/` folder), we also provide `npm` scripts that run with `--out-parent`:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# text export to parent `output/`
|
|
97
|
+
npm run cli:text-demo-parent
|
|
98
|
+
|
|
99
|
+
# excel export to parent `output/`
|
|
100
|
+
npm run cli:excel-demo-parent
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Example text output (from `npm run cli:text-demo`):
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
Hash | Author | Date | Message
|
|
107
|
+
---------------------------------------------------------------------------------------------------------------------
|
|
108
|
+
c5bdf9d4 | tom | 2025-11-25 | feat: 🎸 增加output目录
|
|
109
|
+
|
|
110
|
+
ea82531 | tom | 2025-11-25 | feat: 🎸 init
|
|
111
|
+
|
|
112
|
+
741de50 | tom | 2025-11-25 | first commit
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
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:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
wukong-gitlog-cli --overtime --limit 500
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Overtime demo scripts (npm)
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Run a US-focused overtime text report using 10:00-19:00 work hours and a 12:00-13:00 lunch break
|
|
127
|
+
npm run cli:overtime-text-us
|
|
128
|
+
|
|
129
|
+
# Run a US-focused overtime text report and write the outputs into the project parent's output folder
|
|
130
|
+
npm run cli:overtime-text-us-parent
|
|
131
|
+
|
|
132
|
+
# Run a US-focused overtime text report and write the output into ../output (explicit --out-dir)
|
|
133
|
+
npm run cli:overtime-text-us-outdir
|
|
134
|
+
|
|
135
|
+
# Run a CN-focused overtime Excel report (default 9:00-18:00 work hours and 12:00-14:00 lunch)
|
|
136
|
+
npm run cli:overtime-excel-cn
|
|
137
|
+
|
|
138
|
+
# Run a CN-focused overtime Excel report and write outputs to parent output folder
|
|
139
|
+
npm run cli:overtime-excel-cn-parent
|
|
140
|
+
|
|
141
|
+
# Run a CN-focused overtime Excel report and write outputs to ../output via --out-dir
|
|
142
|
+
npm run cli:overtime-excel-cn-outdir
|
|
143
|
+
# Per-period CSV/Tab export: write per-period files to output/month/ and output/week/
|
|
144
|
+
npm run cli:overtime-per-period-csv-tab
|
|
145
|
+
# Per-period Excel export with sheet-per-period workbook
|
|
146
|
+
npm run cli:overtime-per-period-xlsx-sheets
|
|
147
|
+
# Per-period Excel export with one file per period
|
|
148
|
+
npm run cli:overtime-per-period-xlsx-files
|
|
149
|
+
# Per-period only (no consolidated monthly/weekly files)
|
|
150
|
+
npm run cli:overtime-per-period-only
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Notes:
|
|
154
|
+
|
|
155
|
+
- Output files are written into `output/` by default. Use `--out-dir` or `--out-parent` to change output location.
|
|
156
|
+
- 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`).
|
|
157
|
+
|
|
158
|
+
Formatting note:
|
|
159
|
+
|
|
160
|
+
- 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).
|
|
161
|
+
|
|
162
|
+
Example JSON output (from `npm run cli:json-demo`):
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
[
|
|
166
|
+
{
|
|
167
|
+
"hash": "c5bdf9d4f52f39bd7d580318bafc8ba4b6c129bc",
|
|
168
|
+
"author": "tom",
|
|
169
|
+
"email": "",
|
|
170
|
+
"date": "2025-11-25 17:24:32 +0800",
|
|
171
|
+
"message": "feat: 🎸 增加output目录"
|
|
172
|
+
}
|
|
173
|
+
/* truncated... */
|
|
174
|
+
]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Example JSON output including `changeId`/`gerrit` when `--gerrit` uses `{{changeId}}` (if present in commit):
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
[
|
|
181
|
+
{
|
|
182
|
+
"hash": "Iabc...",
|
|
183
|
+
"author": "tom",
|
|
184
|
+
"email": "",
|
|
185
|
+
"date": "2025-11-25 17:24:32 +0800",
|
|
186
|
+
"message": "feat: add feature",
|
|
187
|
+
"body": "feat: add feature\n\nChange-Id: Iabcd123456789",
|
|
188
|
+
"changeId": "Iabcd123456789",
|
|
189
|
+
"gerrit": "https://gerrit.example.com/c/project/+/Iabcd123456789"
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-gitlog-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"wukong-gitlog-cli": "./bin/wukong-gitlog-cli"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"cli:excel-demo": "node ./src/cli.mjs --format excel --stats --limit 5 --out commits.xlsx",
|
|
40
|
+
"cli:excel-demo": "node ./src/cli.mjs --overtime --format excel --stats --limit 5 --out commits.xlsx",
|
|
41
41
|
"cli:excel-demo-parent": "node ./src/cli.mjs --out-parent --format excel --stats --limit 5 --out commits-parent.xlsx",
|
|
42
42
|
"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
43
|
"cli:gerrit-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{hash}}\" --limit 5 --out commits-gerrit.txt",
|
|
@@ -54,6 +54,10 @@
|
|
|
54
54
|
"cli:overtime-excel-cn": "node ./src/cli.mjs --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
55
55
|
"cli:overtime-excel-cn-parent": "node ./src/cli.mjs --out-parent --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
56
56
|
"cli:overtime-excel-cn-outdir": "node ./src/cli.mjs --out-dir ../output --overtime --limit 50 --format excel --out commits.xlsx --country CN",
|
|
57
|
+
"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
|
+
"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",
|
|
57
61
|
"format": "prettier --write \"src/**/*.{js,mjs}\"",
|
|
58
62
|
"lint": "eslint src --ext .js,.mjs src",
|
|
59
63
|
"lint:fix": "eslint src --ext .js,.mjs --fix",
|
package/src/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { getGitLogs } from './git.mjs';
|
|
5
5
|
import { renderText } from './text.mjs';
|
|
6
6
|
import { analyzeOvertime, renderOvertimeText, renderOvertimeTab, renderOvertimeCsv } from './overtime.mjs';
|
|
7
|
-
import { exportExcel } from './excel.mjs';
|
|
7
|
+
import { exportExcel, exportExcelPerPeriodSheets } from './excel.mjs';
|
|
8
8
|
import { groupRecords, writeJSON, writeTextFile, outputFilePath } from './utils/index.mjs';
|
|
9
9
|
|
|
10
10
|
const program = new Command();
|
|
@@ -20,7 +20,7 @@ program
|
|
|
20
20
|
.option('--no-merges', '不包含 merge commit')
|
|
21
21
|
.option('--json', '输出 JSON')
|
|
22
22
|
.option('--format <type>', '输出格式: text | excel | json', 'text')
|
|
23
|
-
.option('--group-by <type>', '按日期分组: day | month')
|
|
23
|
+
.option('--group-by <type>', '按日期分组: day | month | week')
|
|
24
24
|
.option('--stats', '输出每日统计数据')
|
|
25
25
|
.option('--gerrit <prefix>', '显示 Gerrit 地址,支持在 prefix 中使用 {{hash}} 占位符')
|
|
26
26
|
.option('--gerrit-api <url>', '可选:Gerrit REST API 基础地址,用于解析 changeNumber,例如 `https://gerrit.example.com`')
|
|
@@ -34,6 +34,9 @@ program
|
|
|
34
34
|
.option('--out <file>', '输出文件名(不含路径)')
|
|
35
35
|
.option('--out-dir <dir>', '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output`')
|
|
36
36
|
.option('--out-parent', '将输出目录放到当前工程的父目录的 `output/`(等同于 `--out-dir ../output`)')
|
|
37
|
+
.option('--per-period-formats <formats>', '每个周期单独输出的格式,逗号分隔:text,csv,tab,xlsx。默认为空(不输出 CSV/Tab/XLSX)', '')
|
|
38
|
+
.option('--per-period-excel-mode <mode>', 'per-period Excel 模式:sheets|files(默认:sheets)', 'sheets')
|
|
39
|
+
.option('--per-period-only', '仅输出 per-period(month/week)文件,不输出合并的 monthly/weekly 汇总文件')
|
|
37
40
|
.parse();
|
|
38
41
|
|
|
39
42
|
const opts = program.opts();
|
|
@@ -175,6 +178,169 @@ const opts = program.opts();
|
|
|
175
178
|
console.log(chalk.green(`Overtime text 已导出: ${overtimeFile}`));
|
|
176
179
|
console.log(chalk.green(`Overtime table (tabs) 已导出: ${overtimeTabFile}`));
|
|
177
180
|
console.log(chalk.green(`Overtime CSV 已导出: ${overtimeCsvFile}`));
|
|
181
|
+
// 按月输出每个月的加班统计(合并文件 + individual files in month/)
|
|
182
|
+
const perPeriodFormats = String(opts.perPeriodFormats || '').split(',').map(s => String(s || '').trim().toLowerCase()).filter(Boolean);
|
|
183
|
+
try {
|
|
184
|
+
const monthGroups = groupRecords(records, 'month');
|
|
185
|
+
const monthlyFileName = `overtime_${outBase}_monthly.txt`;
|
|
186
|
+
const monthlyFile = outputFilePath(monthlyFileName, outDir);
|
|
187
|
+
let monthlyContent = '';
|
|
188
|
+
const monthKeys = Object.keys(monthGroups).sort();
|
|
189
|
+
monthKeys.forEach((k) => {
|
|
190
|
+
const groupRecs = monthGroups[k];
|
|
191
|
+
const s = analyzeOvertime(groupRecs, {
|
|
192
|
+
startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
193
|
+
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
194
|
+
lunchStart: opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
195
|
+
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
196
|
+
country: opts.country || 'CN',
|
|
197
|
+
});
|
|
198
|
+
monthlyContent += `===== ${k} =====\n`;
|
|
199
|
+
monthlyContent += `${renderOvertimeText(s)}\n\n`;
|
|
200
|
+
// Also write a single file per month under 'month/' folder
|
|
201
|
+
try {
|
|
202
|
+
const perMonthFileName = `month/overtime_${outBase}_${k}.txt`;
|
|
203
|
+
const perMonthFile = outputFilePath(perMonthFileName, outDir);
|
|
204
|
+
writeTextFile(perMonthFile, renderOvertimeText(s));
|
|
205
|
+
console.log(chalk.green(`Overtime 月度(${k}) 已导出: ${perMonthFile}`));
|
|
206
|
+
// per-period CSV / Tab format (按需生成)
|
|
207
|
+
if (perPeriodFormats.includes('csv')) {
|
|
208
|
+
try {
|
|
209
|
+
const perMonthCsvName = `month/overtime_${outBase}_${k}.csv`;
|
|
210
|
+
writeTextFile(outputFilePath(perMonthCsvName, outDir), renderOvertimeCsv(s));
|
|
211
|
+
console.log(chalk.green(`Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`));
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.warn(`Write monthly CSV for ${k} failed:`, err && err.message ? err.message : err);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (perPeriodFormats.includes('tab')) {
|
|
217
|
+
try {
|
|
218
|
+
const perMonthTabName = `month/overtime_${outBase}_${k}.tab.txt`;
|
|
219
|
+
writeTextFile(outputFilePath(perMonthTabName, outDir), renderOvertimeTab(s));
|
|
220
|
+
console.log(chalk.green(`Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`));
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.warn(`Write monthly Tab for ${k} failed:`, err && err.message ? err.message : err);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.warn(`Write monthly file for ${k} failed:`, err && err.message ? err.message : err);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
if (!opts.perPeriodOnly) {
|
|
230
|
+
writeTextFile(monthlyFile, monthlyContent);
|
|
231
|
+
console.log(chalk.green(`Overtime 月度汇总 已导出: ${monthlyFile}`));
|
|
232
|
+
}
|
|
233
|
+
// per-period Excel (sheets or files)
|
|
234
|
+
if (perPeriodFormats.includes('xlsx')) {
|
|
235
|
+
const perPeriodExcelMode = String(opts.perPeriodExcelMode || 'sheets');
|
|
236
|
+
if (perPeriodExcelMode === 'sheets') {
|
|
237
|
+
try {
|
|
238
|
+
const monthXlsxName = `month/overtime_${outBase}_monthly.xlsx`;
|
|
239
|
+
const monthXlsxFile = outputFilePath(monthXlsxName, outDir);
|
|
240
|
+
await exportExcelPerPeriodSheets(monthGroups, monthXlsxFile, { stats: opts.stats, gerrit: opts.gerrit });
|
|
241
|
+
console.log(chalk.green(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`));
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.warn('Export month XLSX (sheets) failed:', err && err.message ? err.message : err);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
try {
|
|
247
|
+
const monthKeys2 = Object.keys(monthGroups).sort();
|
|
248
|
+
const tasks = monthKeys2.map(k2 => {
|
|
249
|
+
const perMonthXlsxName = `month/overtime_${outBase}_${k2}.xlsx`;
|
|
250
|
+
const perMonthXlsxFile = outputFilePath(perMonthXlsxName, outDir);
|
|
251
|
+
return exportExcel(monthGroups[k2], null, { file: perMonthXlsxFile, stats: opts.stats, gerrit: opts.gerrit })
|
|
252
|
+
.then(() => console.log(chalk.green(`Overtime 月度(XLSX)(${k2}) 已导出: ${perMonthXlsxFile}`)));
|
|
253
|
+
});
|
|
254
|
+
await Promise.all(tasks);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.warn('Export monthly XLSX files failed:', err && err.message ? err.message : err);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.warn('Generate monthly overtime failed:', err && err.message ? err.message : err);
|
|
262
|
+
}
|
|
263
|
+
// 按周输出每周的加班统计(合并文件 + individual files in week/)
|
|
264
|
+
try {
|
|
265
|
+
const weekGroups = groupRecords(records, 'week');
|
|
266
|
+
const weeklyFileName = `overtime_${outBase}_weekly.txt`;
|
|
267
|
+
const weeklyFile = outputFilePath(weeklyFileName, outDir);
|
|
268
|
+
let weeklyContent = '';
|
|
269
|
+
const weekKeys = Object.keys(weekGroups).sort();
|
|
270
|
+
weekKeys.forEach((k) => {
|
|
271
|
+
const groupRecs = weekGroups[k];
|
|
272
|
+
const s = analyzeOvertime(groupRecs, {
|
|
273
|
+
startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
|
|
274
|
+
endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
|
|
275
|
+
lunchStart: opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
|
|
276
|
+
lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
|
|
277
|
+
country: opts.country || 'CN',
|
|
278
|
+
});
|
|
279
|
+
weeklyContent += `===== ${k} =====\n`;
|
|
280
|
+
weeklyContent += `${renderOvertimeText(s)}\n\n`;
|
|
281
|
+
// Also write a single file per week under 'week/' folder
|
|
282
|
+
try {
|
|
283
|
+
const perWeekFileName = `week/overtime_${outBase}_${k}.txt`;
|
|
284
|
+
const perWeekFile = outputFilePath(perWeekFileName, outDir);
|
|
285
|
+
writeTextFile(perWeekFile, renderOvertimeText(s));
|
|
286
|
+
console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`));
|
|
287
|
+
// per-period CSV / Tab format (按需生成)
|
|
288
|
+
if (perPeriodFormats.includes('csv')) {
|
|
289
|
+
try {
|
|
290
|
+
const perWeekCsvName = `week/overtime_${outBase}_${k}.csv`;
|
|
291
|
+
writeTextFile(outputFilePath(perWeekCsvName, outDir), renderOvertimeCsv(s));
|
|
292
|
+
console.log(chalk.green(`Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`));
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.warn(`Write weekly CSV for ${k} failed:`, err && err.message ? err.message : err);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (perPeriodFormats.includes('tab')) {
|
|
298
|
+
try {
|
|
299
|
+
const perWeekTabName = `week/overtime_${outBase}_${k}.tab.txt`;
|
|
300
|
+
writeTextFile(outputFilePath(perWeekTabName, outDir), renderOvertimeTab(s));
|
|
301
|
+
console.log(chalk.green(`Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`));
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.warn(`Write weekly Tab for ${k} failed:`, err && err.message ? err.message : err);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.warn(`Write weekly file for ${k} failed:`, err && err.message ? err.message : err);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
if (!opts.perPeriodOnly) {
|
|
311
|
+
writeTextFile(weeklyFile, weeklyContent);
|
|
312
|
+
console.log(chalk.green(`Overtime 周度汇总 已导出: ${weeklyFile}`));
|
|
313
|
+
}
|
|
314
|
+
// per-period Excel (sheets or files)
|
|
315
|
+
if (perPeriodFormats.includes('xlsx')) {
|
|
316
|
+
const perPeriodExcelMode = String(opts.perPeriodExcelMode || 'sheets');
|
|
317
|
+
if (perPeriodExcelMode === 'sheets') {
|
|
318
|
+
try {
|
|
319
|
+
const weekXlsxName = `week/overtime_${outBase}_weekly.xlsx`;
|
|
320
|
+
const weekXlsxFile = outputFilePath(weekXlsxName, outDir);
|
|
321
|
+
await exportExcelPerPeriodSheets(weekGroups, weekXlsxFile, { stats: opts.stats, gerrit: opts.gerrit });
|
|
322
|
+
console.log(chalk.green(`Overtime 周度(XLSX) 已导出: ${weekXlsxFile}`));
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.warn('Export week XLSX (sheets) failed:', err && err.message ? err.message : err);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
try {
|
|
328
|
+
const weekKeys2 = Object.keys(weekGroups).sort();
|
|
329
|
+
const tasks2 = weekKeys2.map(k2 => {
|
|
330
|
+
const perWeekXlsxName = `week/overtime_${outBase}_${k2}.xlsx`;
|
|
331
|
+
const perWeekXlsxFile = outputFilePath(perWeekXlsxName, outDir);
|
|
332
|
+
return exportExcel(weekGroups[k2], null, { file: perWeekXlsxFile, stats: opts.stats, gerrit: opts.gerrit })
|
|
333
|
+
.then(() => console.log(chalk.green(`Overtime 周度(XLSX)(${k2}) 已导出: ${perWeekXlsxFile}`)));
|
|
334
|
+
});
|
|
335
|
+
await Promise.all(tasks2);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.warn('Export weekly XLSX files failed:', err && err.message ? err.message : err);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.warn('Generate weekly overtime failed:', err && err.message ? err.message : err);
|
|
343
|
+
}
|
|
178
344
|
// don't return — allow other outputs to proceed
|
|
179
345
|
}
|
|
180
346
|
|
package/src/excel.mjs
CHANGED
|
@@ -50,3 +50,48 @@ export async function exportExcel(records, groups, options = {}) {
|
|
|
50
50
|
|
|
51
51
|
await wb.xlsx.writeFile(file);
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
export async function exportExcelPerPeriodSheets(groups, file, options = {}) {
|
|
55
|
+
// groups: { periodKey: [records] }
|
|
56
|
+
const { stats, gerrit } = options;
|
|
57
|
+
|
|
58
|
+
const wb = new ExcelJS.Workbook();
|
|
59
|
+
|
|
60
|
+
const cols = [
|
|
61
|
+
{ header: 'Hash', key: 'hash', width: 12 },
|
|
62
|
+
{ header: 'Author', key: 'author', width: 20 },
|
|
63
|
+
{ header: 'Email', key: 'email', width: 30 },
|
|
64
|
+
{ header: 'Date', key: 'date', width: 20 },
|
|
65
|
+
{ header: 'Message', key: 'message', width: 80 }
|
|
66
|
+
];
|
|
67
|
+
if (gerrit) cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 });
|
|
68
|
+
|
|
69
|
+
const keys = Object.keys(groups).sort();
|
|
70
|
+
keys.forEach((k) => {
|
|
71
|
+
const ws = wb.addWorksheet(String(k).slice(0, 31)); // Excel sheet name limit 31
|
|
72
|
+
ws.columns = cols;
|
|
73
|
+
groups[k].forEach(r => ws.addRow(r));
|
|
74
|
+
ws.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: cols.length } };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// summary sheet with counts per period
|
|
78
|
+
const summary = wb.addWorksheet('Summary');
|
|
79
|
+
summary.columns = [
|
|
80
|
+
{ header: 'Period', key: 'period', width: 20 },
|
|
81
|
+
{ header: 'Commits', key: 'count', width: 12 }
|
|
82
|
+
];
|
|
83
|
+
keys.forEach(k => summary.addRow({ period: k, count: groups[k].length }));
|
|
84
|
+
|
|
85
|
+
await wb.xlsx.writeFile(file);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function exportExcelPerPeriodFiles(groups, dir, filePrefix, options = {}) {
|
|
89
|
+
// groups: { periodKey: [records] }
|
|
90
|
+
// dir: output directory path, filePrefix: base name without extension
|
|
91
|
+
// For each key, call exportExcel with that key's records
|
|
92
|
+
const keys = Object.keys(groups).sort();
|
|
93
|
+
for (const k of keys) {
|
|
94
|
+
const perFile = `${dir}/overtime_${filePrefix}_${k}.xlsx`;
|
|
95
|
+
await exportExcel(groups[k], null, { file: perFile, stats: options.stats, gerrit: options.gerrit });
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/overtime.mjs
CHANGED
|
@@ -14,6 +14,20 @@ export function parseCommitDate(d) {
|
|
|
14
14
|
return dt;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function formatDateForCountry(dateStr, country) {
|
|
18
|
+
try {
|
|
19
|
+
const dt = parseCommitDate(dateStr);
|
|
20
|
+
if (!dt || !dt.isValid()) return dateStr;
|
|
21
|
+
if (String(country).toUpperCase() === 'CN') {
|
|
22
|
+
// Force display in +08:00 timezone
|
|
23
|
+
return dt.utcOffset(8 * 60).format('YYYY-MM-DD HH:mm:ss ZZ');
|
|
24
|
+
}
|
|
25
|
+
return dt.format('YYYY-MM-DD HH:mm:ss ZZ');
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return dateStr;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
function isWeekend(dt) {
|
|
18
32
|
const day = dt.day();
|
|
19
33
|
return day === 0 || day === 6; // Sunday=0, Saturday=6
|
|
@@ -39,6 +53,7 @@ export function analyzeOvertime(records, opts = {}) {
|
|
|
39
53
|
let startCommit = null;
|
|
40
54
|
let endCommit = null;
|
|
41
55
|
let latestCommit = null;
|
|
56
|
+
let latestOutsideCommit = null;
|
|
42
57
|
|
|
43
58
|
// init holiday checker for country
|
|
44
59
|
const hd = new DateHolidays();
|
|
@@ -57,7 +72,13 @@ export function analyzeOvertime(records, opts = {}) {
|
|
|
57
72
|
const isHoliday = !!hd.isHoliday(dt.toDate());
|
|
58
73
|
const isNonWork = isWeekend(dt) || isHoliday;
|
|
59
74
|
|
|
60
|
-
if (outside)
|
|
75
|
+
if (outside) {
|
|
76
|
+
outsideWorkCount++;
|
|
77
|
+
// 记录最新的加班提交
|
|
78
|
+
if (!latestOutsideCommit || dt.isAfter(parseCommitDate(latestOutsideCommit.date))) {
|
|
79
|
+
latestOutsideCommit = r;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
61
82
|
if (isNonWork) nonWorkdayCount++;
|
|
62
83
|
if (isHoliday) holidayCount++;
|
|
63
84
|
|
|
@@ -124,6 +145,7 @@ export function analyzeOvertime(records, opts = {}) {
|
|
|
124
145
|
startCommit: startCommit || null,
|
|
125
146
|
endCommit: endCommit || null,
|
|
126
147
|
latestCommit: latestCommit || null,
|
|
148
|
+
latestOutsideCommit: latestOutsideCommit || null,
|
|
127
149
|
startHour,
|
|
128
150
|
endHour,
|
|
129
151
|
lunchStart,
|
|
@@ -136,7 +158,7 @@ export function analyzeOvertime(records, opts = {}) {
|
|
|
136
158
|
|
|
137
159
|
export function renderOvertimeText(stats) {
|
|
138
160
|
const { total, outsideWorkCount, nonWorkdayCount, holidayCount, outsideWorkRate, nonWorkdayRate, holidayRate, perAuthor, startHour, endHour, lunchStart, lunchEnd, country } = stats;
|
|
139
|
-
const { startCommit, endCommit, latestCommit } = stats;
|
|
161
|
+
const { startCommit, endCommit, latestCommit, latestOutsideCommit } = stats;
|
|
140
162
|
const lines = [];
|
|
141
163
|
|
|
142
164
|
const formatPercent = (v) => `${(v * 100).toFixed(1)}%`;
|
|
@@ -162,15 +184,22 @@ export function renderOvertimeText(stats) {
|
|
|
162
184
|
};
|
|
163
185
|
lines.push(`总提交数:${total}`);
|
|
164
186
|
if (startCommit && endCommit) {
|
|
165
|
-
lines.push(`统计区间:${startCommit.date} — ${endCommit.date}`);
|
|
187
|
+
lines.push(`统计区间:${formatDateForCountry(startCommit.date, country)} — ${formatDateForCountry(endCommit.date, country)}`);
|
|
166
188
|
}
|
|
167
189
|
if (latestCommit) {
|
|
168
190
|
lines.push('最晚一次提交:');
|
|
169
191
|
lines.push(` Hash : ${latestCommit.hash}`);
|
|
170
192
|
lines.push(` Author : ${latestCommit.author}`);
|
|
171
|
-
lines.push(` Date : ${latestCommit.date}`);
|
|
193
|
+
lines.push(` Date : ${formatDateForCountry(latestCommit.date, country)}`);
|
|
172
194
|
lines.push(` Message: ${latestCommit.message}`);
|
|
173
195
|
}
|
|
196
|
+
if (latestOutsideCommit) {
|
|
197
|
+
lines.push('加班最晚的一次提交:');
|
|
198
|
+
lines.push(` Hash : ${latestOutsideCommit.hash}`);
|
|
199
|
+
lines.push(` Author : ${latestOutsideCommit.author}`);
|
|
200
|
+
lines.push(` Date : ${formatDateForCountry(latestOutsideCommit.date, country)}`);
|
|
201
|
+
lines.push(` Message: ${latestOutsideCommit.message}`);
|
|
202
|
+
}
|
|
174
203
|
// country: holiday region, lunchStart/lunchEnd define midday break
|
|
175
204
|
lines.push(`下班时间定义:${startHour}:00 - ${endHour}:00 (午休 ${lunchStart}:00 - ${lunchEnd}:00)`);
|
|
176
205
|
lines.push(`国家假期参考:${String(country).toUpperCase()},节假日提交数:${holidayCount},占比:${(holidayRate * 100).toFixed(1)}%`);
|
|
@@ -196,11 +225,12 @@ export function renderOvertimeText(stats) {
|
|
|
196
225
|
|
|
197
226
|
export function renderOvertimeTab(stats) {
|
|
198
227
|
const { total, outsideWorkCount, nonWorkdayCount, holidayCount, outsideWorkRate, nonWorkdayRate, holidayRate, perAuthor, startHour, endHour, lunchStart, lunchEnd, country } = stats;
|
|
199
|
-
const { startCommit, endCommit, latestCommit } = stats;
|
|
228
|
+
const { startCommit, endCommit, latestCommit, latestOutsideCommit } = stats;
|
|
200
229
|
const rows = [];
|
|
201
230
|
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}`);
|
|
231
|
+
if (startCommit && endCommit) rows.push(`统计区间:\t${formatDateForCountry(startCommit.date, country)} — ${formatDateForCountry(endCommit.date, country)}`);
|
|
232
|
+
if (latestCommit) rows.push(`最晚一次提交:\t${latestCommit.hash}\t${latestCommit.author}\t${formatDateForCountry(latestCommit.date, country)}\t${latestCommit.message}`);
|
|
233
|
+
if (latestOutsideCommit) rows.push(`加班最晚的一次提交:\t${latestOutsideCommit.hash}\t${latestOutsideCommit.author}\t${formatDateForCountry(latestOutsideCommit.date, country)}\t${latestOutsideCommit.message}`);
|
|
204
234
|
rows.push(`下班时间定义:\t${startHour}:00 - ${endHour}:00 (午休 ${lunchStart}:00 - ${lunchEnd}:00)`);
|
|
205
235
|
rows.push(`国家假期参考:\t${String(country).toUpperCase()}\t节假日提交数:\t${holidayCount}\t节假日占比:\t${(holidayRate * 100).toFixed(1)}%`);
|
|
206
236
|
rows.push(`下班时间(工作时间外)提交数:\t${outsideWorkCount}\t占比:\t${(outsideWorkRate * 100).toFixed(1)}%`);
|
|
@@ -222,8 +252,12 @@ function escapeCsv(v) {
|
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
export function renderOvertimeCsv(stats) {
|
|
225
|
-
const { perAuthor } = stats;
|
|
255
|
+
const { perAuthor, latestOutsideCommit, country } = stats;
|
|
226
256
|
const rows = [];
|
|
257
|
+
if (latestOutsideCommit) {
|
|
258
|
+
rows.push(`# 加班最晚的一次提交,Hash,Author,Date,Message`);
|
|
259
|
+
rows.push(`# ,${escapeCsv(latestOutsideCommit.hash)},${escapeCsv(latestOutsideCommit.author)},${escapeCsv(formatDateForCountry(latestOutsideCommit.date, country))},${escapeCsv(latestOutsideCommit.message)}`);
|
|
260
|
+
}
|
|
227
261
|
rows.push('Name,Total,OutsideCount,OutsideRate,NonWorkdayCount,NonWorkdayRate,HolidayCount,HolidayRate');
|
|
228
262
|
perAuthor.forEach((p) => {
|
|
229
263
|
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)}%`);
|
package/src/utils/file.mjs
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import isoWeek from 'dayjs/plugin/isoWeek.js'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
|
|
5
|
+
// add ISO week plugin to dayjs once when module loaded
|
|
6
|
+
dayjs.extend(isoWeek)
|
|
3
7
|
|
|
4
8
|
export function writeJSON(file, data) {
|
|
5
|
-
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8')
|
|
9
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8')
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
export function writeTextFile(file, text) {
|
|
9
|
-
fs.writeFileSync(file, text, 'utf8')
|
|
13
|
+
fs.writeFileSync(file, text, 'utf8')
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export function groupRecords(records, mode) {
|
|
13
|
-
const group = {}
|
|
17
|
+
const group = {}
|
|
14
18
|
|
|
15
|
-
records.forEach(r => {
|
|
16
|
-
const date = dayjs(r.date)
|
|
19
|
+
records.forEach((r) => {
|
|
20
|
+
const date = dayjs(r.date)
|
|
21
|
+
let key
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
if (mode === 'day') {
|
|
24
|
+
key = date.format('YYYY-MM-DD')
|
|
25
|
+
} else if (mode === 'week') {
|
|
26
|
+
// use dayjs isoWeek / isoWeekYear plugins for accurate ISO week computation (Monday-based)
|
|
27
|
+
const week = date.isoWeek()
|
|
28
|
+
const year = date.isoWeekYear()
|
|
29
|
+
key = `${year}-W${String(week).padStart(2, '0')}`
|
|
30
|
+
} else {
|
|
31
|
+
// default to month grouping
|
|
32
|
+
key = date.format('YYYY-MM')
|
|
33
|
+
}
|
|
22
34
|
|
|
23
|
-
if (!group[key]) group[key] = []
|
|
24
|
-
group[key].push(r)
|
|
25
|
-
})
|
|
35
|
+
if (!group[key]) group[key] = []
|
|
36
|
+
group[key].push(r)
|
|
37
|
+
})
|
|
26
38
|
|
|
27
|
-
return group
|
|
39
|
+
return group
|
|
28
40
|
}
|
package/src/utils/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { groupRecords, writeJSON, writeTextFile } from './file.mjs'
|
|
2
|
-
export * from './output.mjs'
|
|
1
|
+
export { groupRecords, writeJSON, writeTextFile } from './file.mjs'
|
|
2
|
+
export * from './output.mjs'
|
package/src/utils/output.mjs
CHANGED
|
@@ -17,5 +17,10 @@ export function ensureOutputDir(customDir) {
|
|
|
17
17
|
|
|
18
18
|
export function outputFilePath(filename, customDir) {
|
|
19
19
|
const dir = ensureOutputDir(customDir);
|
|
20
|
-
|
|
20
|
+
const fullpath = path.join(dir, filename);
|
|
21
|
+
const parent = path.dirname(fullpath);
|
|
22
|
+
if (!fs.existsSync(parent)) {
|
|
23
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
return fullpath;
|
|
21
26
|
}
|