wukong-gitlog-cli 0.0.7 → 0.0.8

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 CHANGED
@@ -2,6 +2,19 @@
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.8](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.7...v0.0.8) (2025-11-28)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 --overtime ([636633b](https://github.com/tomatobybike/wukong-gitlog-cli/commit/636633b424f8e440f53eeaabee1c9ffdbbbdcd04))
11
+ * 🎸 cn datetime format ([7773b08](https://github.com/tomatobybike/wukong-gitlog-cli/commit/7773b084dadae415edc12a735bc450353ec127bf))
12
+ * 🎸 export excel ([2e87900](https://github.com/tomatobybike/wukong-gitlog-cli/commit/2e87900af98883a1440871b2db6b581b158a3b1f))
13
+ * 🎸 last work ([c31edff](https://github.com/tomatobybike/wukong-gitlog-cli/commit/c31edff080d7e8b1d6b1c2c9bdee2da57abaa3a0))
14
+ * 🎸 last work ([64fcf3f](https://github.com/tomatobybike/wukong-gitlog-cli/commit/64fcf3f9fa0f4c7055b2aa39368f3553b41093b3))
15
+ * 🎸 weekly monthly ([a4f76dd](https://github.com/tomatobybike/wukong-gitlog-cli/commit/a4f76dd31192ee740f73f160729f6e4a8d32542f))
16
+ * **overtime:** add monthly and weekly overtime summary files ([024f3d4](https://github.com/tomatobybike/wukong-gitlog-cli/commit/024f3d497439106fabf8718bfd8076767404f7e8))
17
+
5
18
  ### [0.0.7](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.6...v0.0.7) (2025-11-27)
6
19
 
7
20
  ### [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
+ node ./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
+ node ./src/cli.mjs --overtime --limit 200 --format text --out commits.txt --per-period-formats csv,tab,xlsx --per-period-excel-mode sheets
102
+ node ./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
+ node ./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
@@ -240,6 +261,14 @@ npm run cli:overtime-excel-cn-parent
240
261
 
241
262
  # Run a CN-focused overtime Excel report and write outputs to ../output via --out-dir
242
263
  npm run cli:overtime-excel-cn-outdir
264
+ # Per-period CSV/Tab export: write per-period files to output/month/ and output/week/
265
+ npm run cli:overtime-per-period-csv-tab
266
+ # Per-period Excel export with sheet-per-period workbook
267
+ npm run cli:overtime-per-period-xlsx-sheets
268
+ # Per-period Excel export with one file per period
269
+ npm run cli:overtime-per-period-xlsx-files
270
+ # Per-period only (no consolidated monthly/weekly files)
271
+ npm run cli:overtime-per-period-only
243
272
  ```
244
273
 
245
274
  Notes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
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) outsideWorkCount++;
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)}%`);
@@ -1,28 +1,40 @@
1
- import fs from 'fs';
2
- import dayjs from 'dayjs';
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
- const key =
19
- mode === 'day'
20
- ? date.format('YYYY-MM-DD')
21
- : date.format('YYYY-MM');
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
  }
@@ -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'
@@ -17,5 +17,10 @@ export function ensureOutputDir(customDir) {
17
17
 
18
18
  export function outputFilePath(filename, customDir) {
19
19
  const dir = ensureOutputDir(customDir);
20
- return path.join(dir, filename);
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
  }