wukong-gitlog-cli 1.0.16 → 1.0.18
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 +17 -0
- package/package.json +9 -8
- package/src/cli.mjs +31 -2
- package/src/excel.mjs +116 -46
- package/src/git.mjs +33 -7
- package/src/json.mjs +5 -0
- package/src/stats-text.mjs +51 -0
- package/src/stats.mjs +44 -0
- package/src/text.mjs +37 -42
- package/web/app.js +528 -73
- package/web/index.html +42 -6
- package/web/static/style.css +76 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.0.18](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.17...v1.0.18) (2025-12-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 buildAuthorLatestDataset ([e607836](https://github.com/tomatobybike/wukong-gitlog-cli/commit/e6078360d29e18d9a9f6f6346547821ef13c1e7d))
|
|
11
|
+
* 🎸 css ([f2c4a71](https://github.com/tomatobybike/wukong-gitlog-cli/commit/f2c4a715b594f37e3ff2fd34f641c57aac33c50a))
|
|
12
|
+
* 🎸 risk ([1f6a55e](https://github.com/tomatobybike/wukong-gitlog-cli/commit/1f6a55e68ad28cc9523019294eaa69bde0036d86))
|
|
13
|
+
|
|
14
|
+
### [1.0.17](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.16...v1.0.17) (2025-12-05)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* 🎸 change line total ([bf01655](https://github.com/tomatobybike/wukong-gitlog-cli/commit/bf01655f3721b87d4bb36e74a7ce6dadf5e7ee94))
|
|
20
|
+
* 🎸 develop Changes totals ([fb64986](https://github.com/tomatobybike/wukong-gitlog-cli/commit/fb64986cfde215fc58ce7b1b66cbc8bdbf280627))
|
|
21
|
+
|
|
5
22
|
### [1.0.16](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.15...v1.0.16) (2025-12-03)
|
|
6
23
|
|
|
7
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wukong-gitlog-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -90,20 +90,19 @@
|
|
|
90
90
|
]
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
+
"boxen": "8.0.1",
|
|
94
|
+
"chalk": "5.6.2",
|
|
93
95
|
"commander": "12.1.0",
|
|
94
|
-
"dayjs": "1.11.19",
|
|
95
96
|
"date-holidays": "2.1.1",
|
|
96
|
-
"
|
|
97
|
+
"dayjs": "1.11.19",
|
|
97
98
|
"exceljs": "4.4.0",
|
|
98
|
-
"zx": "7.2.4",
|
|
99
99
|
"is-online": "12.0.2",
|
|
100
|
-
"
|
|
101
|
-
"
|
|
100
|
+
"ora": "9.0.0",
|
|
101
|
+
"string-width": "5.1.2",
|
|
102
|
+
"zx": "7.2.4"
|
|
102
103
|
},
|
|
103
104
|
"devDependencies": {
|
|
104
105
|
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
|
105
|
-
"husky": "^9.1.7",
|
|
106
|
-
"lint-staged": "^16.2.7",
|
|
107
106
|
"eslint": "8.57.1",
|
|
108
107
|
"eslint-config-airbnb-base": "15.0.0",
|
|
109
108
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -111,6 +110,8 @@
|
|
|
111
110
|
"eslint-plugin-import": "2.32.0",
|
|
112
111
|
"eslint-plugin-prettier": "5.5.4",
|
|
113
112
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
|
113
|
+
"husky": "^9.1.7",
|
|
114
|
+
"lint-staged": "^16.2.7",
|
|
114
115
|
"prettier": "3.6.2",
|
|
115
116
|
"prettier-plugin-packagejson": "2.5.19",
|
|
116
117
|
"sort-package-json": "3.4.0",
|
package/src/cli.mjs
CHANGED
|
@@ -3,12 +3,18 @@ import { Command } from 'commander'
|
|
|
3
3
|
import dayjs from 'dayjs'
|
|
4
4
|
import isoWeek from 'dayjs/plugin/isoWeek.js'
|
|
5
5
|
import fs from 'fs'
|
|
6
|
+
import ora from 'ora'
|
|
6
7
|
import path from 'path'
|
|
7
8
|
import { fileURLToPath } from 'url'
|
|
8
9
|
|
|
9
10
|
import { CLI_NAME } from './constants/index.mjs'
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
exportExcel,
|
|
13
|
+
exportExcelAuthorChangeStats,
|
|
14
|
+
exportExcelPerPeriodSheets
|
|
15
|
+
} from './excel.mjs'
|
|
11
16
|
import { getGitLogs } from './git.mjs'
|
|
17
|
+
import { renderAuthorChangesJson } from './json.mjs'
|
|
12
18
|
import {
|
|
13
19
|
analyzeOvertime,
|
|
14
20
|
renderOvertimeCsv,
|
|
@@ -16,7 +22,7 @@ import {
|
|
|
16
22
|
renderOvertimeText
|
|
17
23
|
} from './overtime.mjs'
|
|
18
24
|
import { startServer } from './server.mjs'
|
|
19
|
-
import { renderText } from './text.mjs'
|
|
25
|
+
import { renderChangedLinesText, renderText } from './text.mjs'
|
|
20
26
|
import { checkUpdateWithPatch } from './utils/checkUpdate.mjs'
|
|
21
27
|
import {
|
|
22
28
|
groupRecords,
|
|
@@ -194,6 +200,8 @@ const main = async () => {
|
|
|
194
200
|
return
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
const spinner = ora('Loading...').start()
|
|
204
|
+
|
|
197
205
|
let records = await getGitLogs(opts)
|
|
198
206
|
|
|
199
207
|
// compute output directory root if user provided one or wants parent
|
|
@@ -342,6 +350,13 @@ const main = async () => {
|
|
|
342
350
|
const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
|
|
343
351
|
const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
|
|
344
352
|
writeTextFile(dataCommitsFile, commitsModule)
|
|
353
|
+
|
|
354
|
+
const dataCommitsChangedFile = outputFilePath('data/author-changes.mjs', outDir)
|
|
355
|
+
const jsonText = renderAuthorChangesJson(records)
|
|
356
|
+
|
|
357
|
+
const commitsChangedModule = `export default ${JSON.stringify(jsonText, null, 2)};\n`
|
|
358
|
+
writeTextFile(dataCommitsChangedFile, commitsChangedModule)
|
|
359
|
+
|
|
345
360
|
const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
|
|
346
361
|
const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`
|
|
347
362
|
writeTextFile(dataStatsFile, statsModule)
|
|
@@ -770,7 +785,10 @@ const main = async () => {
|
|
|
770
785
|
const file = opts.out || 'commits.json'
|
|
771
786
|
const filepath = outputFilePath(file, outDir)
|
|
772
787
|
writeJSON(filepath, groups || records)
|
|
788
|
+
const jsonText = renderAuthorChangesJson(records)
|
|
789
|
+
writeJSON(outputFilePath('author-changes.json', outDir), jsonText)
|
|
773
790
|
console.log(chalk.green(`JSON 已导出: ${filepath}`))
|
|
791
|
+
spinner.succeed('Done')
|
|
774
792
|
return
|
|
775
793
|
}
|
|
776
794
|
|
|
@@ -779,8 +797,13 @@ const main = async () => {
|
|
|
779
797
|
const filepath = outputFilePath(file, outDir)
|
|
780
798
|
const text = renderText(records, groups, { showGerrit: !!opts.gerrit })
|
|
781
799
|
writeTextFile(filepath, text)
|
|
800
|
+
writeTextFile(
|
|
801
|
+
outputFilePath('author-changes.text', outDir),
|
|
802
|
+
renderChangedLinesText(records)
|
|
803
|
+
)
|
|
782
804
|
console.log(text)
|
|
783
805
|
console.log(chalk.green(`文本已导出: ${filepath}`))
|
|
806
|
+
spinner.succeed('Done')
|
|
784
807
|
return
|
|
785
808
|
}
|
|
786
809
|
|
|
@@ -794,11 +817,17 @@ const main = async () => {
|
|
|
794
817
|
stats: opts.stats,
|
|
795
818
|
gerrit: opts.gerrit
|
|
796
819
|
})
|
|
820
|
+
await exportExcelAuthorChangeStats(
|
|
821
|
+
records,
|
|
822
|
+
outputFilePath('author_stats.xlsx', outDir)
|
|
823
|
+
)
|
|
797
824
|
const text = renderText(records, groups)
|
|
798
825
|
writeTextFile(txtPath, text)
|
|
799
826
|
console.log(chalk.green(`Excel 已导出: ${excelPath}`))
|
|
800
827
|
console.log(chalk.green(`文本已自动导出: ${txtPath}`))
|
|
828
|
+
spinner.succeed('Done')
|
|
801
829
|
}
|
|
830
|
+
|
|
802
831
|
await autoCheckUpdate()
|
|
803
832
|
}
|
|
804
833
|
|
package/src/excel.mjs
CHANGED
|
@@ -1,97 +1,167 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import ExcelJS from 'exceljs'
|
|
3
|
+
import { buildAuthorChangeStats } from './stats.mjs'
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
export async function exportExcel(records, groups, options = {}) {
|
|
5
|
-
const { file, stats, gerrit } = options
|
|
7
|
+
const { file, stats, gerrit } = options
|
|
6
8
|
|
|
7
|
-
const wb = new ExcelJS.Workbook()
|
|
8
|
-
const ws = wb.addWorksheet('Commits')
|
|
9
|
+
const wb = new ExcelJS.Workbook()
|
|
10
|
+
const ws = wb.addWorksheet('Commits')
|
|
9
11
|
|
|
10
12
|
const cols = [
|
|
11
13
|
{ header: 'Hash', key: 'hash', width: 12 },
|
|
12
14
|
{ header: 'Author', key: 'author', width: 20 },
|
|
13
15
|
{ header: 'Email', key: 'email', width: 30 },
|
|
14
16
|
{ header: 'Date', key: 'date', width: 20 },
|
|
15
|
-
{ header: 'Message', key: 'message', width: 80 }
|
|
16
|
-
|
|
17
|
+
{ header: 'Message', key: 'message', width: 80 },
|
|
18
|
+
{ header: 'Changed', key: 'changed', width: 12 } // ★ 新增 changed 列
|
|
19
|
+
]
|
|
17
20
|
|
|
18
21
|
if (gerrit) {
|
|
19
|
-
cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 })
|
|
22
|
+
cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 })
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
ws.columns = cols
|
|
25
|
+
ws.columns = cols
|
|
23
26
|
|
|
24
|
-
(groups ? Object.values(groups).flat() : records).forEach(r =>
|
|
27
|
+
;(groups ? Object.values(groups).flat() : records).forEach((r) =>
|
|
25
28
|
ws.addRow(r)
|
|
26
|
-
)
|
|
29
|
+
)
|
|
27
30
|
|
|
28
|
-
ws.autoFilter = {
|
|
31
|
+
ws.autoFilter = {
|
|
32
|
+
from: { row: 1, column: 1 },
|
|
33
|
+
to: { row: 1, column: cols.length }
|
|
34
|
+
}
|
|
29
35
|
|
|
30
36
|
// --- stats sheet ---
|
|
31
37
|
if (stats) {
|
|
32
|
-
const statWs = wb.addWorksheet('Stats')
|
|
38
|
+
const statWs = wb.addWorksheet('Stats')
|
|
33
39
|
|
|
34
|
-
const map = {}
|
|
40
|
+
const map = {}
|
|
35
41
|
|
|
36
|
-
records.forEach(r => {
|
|
37
|
-
const d = dayjs(r.date).format('YYYY-MM-DD')
|
|
38
|
-
map[d] = (map[d] || 0) + 1
|
|
39
|
-
})
|
|
42
|
+
records.forEach((r) => {
|
|
43
|
+
const d = dayjs(r.date).format('YYYY-MM-DD')
|
|
44
|
+
map[d] = (map[d] || 0) + 1
|
|
45
|
+
})
|
|
40
46
|
|
|
41
47
|
statWs.columns = [
|
|
42
48
|
{ header: 'Date', key: 'date', width: 15 },
|
|
43
49
|
{ header: 'Commits', key: 'count', width: 15 }
|
|
44
|
-
]
|
|
50
|
+
]
|
|
45
51
|
|
|
46
52
|
Object.entries(map).forEach(([d, cnt]) =>
|
|
47
53
|
statWs.addRow({ date: d, count: cnt })
|
|
48
|
-
)
|
|
54
|
+
)
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
await wb.xlsx.writeFile(file)
|
|
57
|
+
await wb.xlsx.writeFile(file)
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
export async function exportExcelPerPeriodSheets(groups, file, options = {}) {
|
|
55
|
-
|
|
56
|
-
const { stats, gerrit } = options;
|
|
61
|
+
const { stats, gerrit } = options
|
|
57
62
|
|
|
58
|
-
const wb = new ExcelJS.Workbook()
|
|
63
|
+
const wb = new ExcelJS.Workbook()
|
|
59
64
|
|
|
60
65
|
const cols = [
|
|
61
66
|
{ header: 'Hash', key: 'hash', width: 12 },
|
|
62
67
|
{ header: 'Author', key: 'author', width: 20 },
|
|
63
68
|
{ header: 'Email', key: 'email', width: 30 },
|
|
64
69
|
{ header: 'Date', key: 'date', width: 20 },
|
|
65
|
-
{ header: 'Message', key: 'message', width: 80 }
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
{ header: 'Message', key: 'message', width: 80 },
|
|
71
|
+
{ header: 'Changed', key: 'changed', width: 12 } // ★ 新增 changed 列
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
if (gerrit) cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 })
|
|
75
|
+
|
|
76
|
+
const keys = Object.keys(groups).sort()
|
|
68
77
|
|
|
69
|
-
const keys = Object.keys(groups).sort();
|
|
70
78
|
keys.forEach((k) => {
|
|
71
|
-
const ws = wb.addWorksheet(String(k).slice(0, 31))
|
|
72
|
-
ws.columns = cols
|
|
73
|
-
groups[k].forEach(r => ws.addRow(r))
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const ws = wb.addWorksheet(String(k).slice(0, 31))
|
|
80
|
+
ws.columns = cols
|
|
81
|
+
groups[k].forEach((r) => ws.addRow(r))
|
|
82
|
+
|
|
83
|
+
ws.autoFilter = {
|
|
84
|
+
from: { row: 1, column: 1 },
|
|
85
|
+
to: { row: 1, column: cols.length }
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// summary sheet
|
|
90
|
+
const summary = wb.addWorksheet('Summary')
|
|
79
91
|
summary.columns = [
|
|
80
92
|
{ header: 'Period', key: 'period', width: 20 },
|
|
81
93
|
{ header: 'Commits', key: 'count', width: 12 }
|
|
82
|
-
]
|
|
83
|
-
keys.forEach(k => summary.addRow({ period: k, count: groups[k].length }))
|
|
94
|
+
]
|
|
95
|
+
keys.forEach((k) => summary.addRow({ period: k, count: groups[k].length }))
|
|
84
96
|
|
|
85
|
-
await wb.xlsx.writeFile(file)
|
|
97
|
+
await wb.xlsx.writeFile(file)
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
export async function exportExcelPerPeriodFiles(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
export async function exportExcelPerPeriodFiles(
|
|
101
|
+
groups,
|
|
102
|
+
dir,
|
|
103
|
+
filePrefix,
|
|
104
|
+
options = {}
|
|
105
|
+
) {
|
|
106
|
+
const keys = Object.keys(groups).sort()
|
|
93
107
|
for (const k of keys) {
|
|
94
|
-
const perFile = `${dir}/overtime_${filePrefix}_${k}.xlsx
|
|
95
|
-
await exportExcel(groups[k], null, {
|
|
108
|
+
const perFile = `${dir}/overtime_${filePrefix}_${k}.xlsx`
|
|
109
|
+
await exportExcel(groups[k], null, {
|
|
110
|
+
file: perFile,
|
|
111
|
+
stats: options.stats,
|
|
112
|
+
gerrit: options.gerrit
|
|
113
|
+
})
|
|
96
114
|
}
|
|
97
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 导出作者的日/周/月 changed 统计
|
|
119
|
+
* @param {*} records 原始 commits(包含 author/date/changed)
|
|
120
|
+
* @param {*} file 输出文件
|
|
121
|
+
*/
|
|
122
|
+
export async function exportExcelAuthorChangeStats(records, file) {
|
|
123
|
+
const stats = buildAuthorChangeStats(records)
|
|
124
|
+
|
|
125
|
+
const wb = new ExcelJS.Workbook()
|
|
126
|
+
|
|
127
|
+
//-------------------------------------------
|
|
128
|
+
// 工具方法:创建一个统计 sheet
|
|
129
|
+
//-------------------------------------------
|
|
130
|
+
function addStatSheet(title, dataMap) {
|
|
131
|
+
const ws = wb.addWorksheet(title)
|
|
132
|
+
|
|
133
|
+
ws.columns = [
|
|
134
|
+
{ header: 'Author', key: 'author', width: 20 },
|
|
135
|
+
{ header: 'Period', key: 'period', width: 20 },
|
|
136
|
+
{ header: 'Changed', key: 'changed', width: 12 }
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for (const [author, periodMap] of Object.entries(dataMap)) {
|
|
140
|
+
const periods = Object.keys(periodMap).sort()
|
|
141
|
+
for (const p of periods) {
|
|
142
|
+
ws.addRow({
|
|
143
|
+
author,
|
|
144
|
+
period: p,
|
|
145
|
+
changed: periodMap[p]
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ws.autoFilter = {
|
|
151
|
+
from: { row: 1, column: 1 },
|
|
152
|
+
to: { row: 1, column: 3 }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//-------------------------------------------
|
|
157
|
+
// 创建 3 个 Sheet
|
|
158
|
+
//-------------------------------------------
|
|
159
|
+
addStatSheet('Author Daily', stats.daily)
|
|
160
|
+
addStatSheet('Author Weekly', stats.weekly)
|
|
161
|
+
addStatSheet('Author Monthly', stats.monthly)
|
|
162
|
+
|
|
163
|
+
//-------------------------------------------
|
|
164
|
+
// 保存文件
|
|
165
|
+
//-------------------------------------------
|
|
166
|
+
await wb.xlsx.writeFile(file)
|
|
167
|
+
}
|
package/src/git.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { $ } from 'zx'
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
export async function getGitLogs(opts) {
|
|
5
4
|
const { author, email, since, until, limit, merges } = opts
|
|
6
5
|
|
|
7
|
-
// include subject and full body so we can extract Change-Id from commit message
|
|
8
6
|
const pretty = '%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%B%x1e'
|
|
9
|
-
|
|
10
7
|
const args = ['log', `--pretty=format:${pretty}`, '--date=iso']
|
|
11
8
|
|
|
12
9
|
if (author) args.push(`--author=${author}`)
|
|
@@ -16,15 +13,13 @@ export async function getGitLogs(opts) {
|
|
|
16
13
|
if (merges === false) args.push(`--no-merges`)
|
|
17
14
|
if (limit) args.push(`-n`, `${limit}`)
|
|
18
15
|
|
|
19
|
-
// 使用 spread 形式传参,ZX 才会正确处理
|
|
20
16
|
const { stdout } = await $`git ${args}`.quiet()
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
const commits = stdout
|
|
23
19
|
.split('\x1e')
|
|
24
20
|
.filter(Boolean)
|
|
25
21
|
.map((r) => {
|
|
26
22
|
const f = r.split('\x1f').map((s) => (s || '').trim())
|
|
27
|
-
|
|
28
23
|
const hash = f[0]
|
|
29
24
|
const authorName = f[1]
|
|
30
25
|
const emailAddr = f[2]
|
|
@@ -32,7 +27,6 @@ export async function getGitLogs(opts) {
|
|
|
32
27
|
const subject = f[4]
|
|
33
28
|
const body = f[5] || ''
|
|
34
29
|
|
|
35
|
-
// extract Change-Id from commit body (line like "Change-Id: Iabc123...")
|
|
36
30
|
const [, changeId] = body.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
|
|
37
31
|
|
|
38
32
|
return {
|
|
@@ -45,4 +39,36 @@ export async function getGitLogs(opts) {
|
|
|
45
39
|
changeId
|
|
46
40
|
}
|
|
47
41
|
})
|
|
42
|
+
|
|
43
|
+
// === 新增:为每个 commit 计算代码增量 ===
|
|
44
|
+
for (const c of commits) {
|
|
45
|
+
try {
|
|
46
|
+
const { stdout: diffOut } = await $`git show --numstat --format= ${c.hash}`.quiet()
|
|
47
|
+
// numstat 格式: "12 5 path/file.js"
|
|
48
|
+
const lines = diffOut
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map((l) => l.trim())
|
|
51
|
+
.filter((l) => l && /^\d+\s+\d+/.test(l))
|
|
52
|
+
|
|
53
|
+
let added = 0
|
|
54
|
+
let deleted = 0
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const [a, d] = line.split(/\s+/)
|
|
58
|
+
added += parseInt(a, 10) || 0
|
|
59
|
+
deleted += parseInt(d, 10) || 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
c.added = added
|
|
63
|
+
c.deleted = deleted
|
|
64
|
+
c.changed = added + deleted
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// 避免阻塞,异常时改动量设置为0
|
|
67
|
+
c.added = 0
|
|
68
|
+
c.deleted = 0
|
|
69
|
+
c.changed = 0
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return commits
|
|
48
74
|
}
|
package/src/json.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function renderAuthorChangeStatsText(stats, opts = {}) {
|
|
2
|
+
const {
|
|
3
|
+
section = 'all', // 'daily' | 'weekly' | 'monthly' | 'all'
|
|
4
|
+
} = opts;
|
|
5
|
+
|
|
6
|
+
const pad = (s, n) =>
|
|
7
|
+
s.length >= n ? `${s.slice(0, n - 1)}…` : s + ' '.repeat(n - s.length);
|
|
8
|
+
|
|
9
|
+
const buildSection = (title, obj, unitLabel) => {
|
|
10
|
+
if (!obj || !Object.keys(obj).length) return '';
|
|
11
|
+
|
|
12
|
+
const lines = [];
|
|
13
|
+
|
|
14
|
+
const header =
|
|
15
|
+
`${pad('Author', 18)} | ${pad(unitLabel, 12)} | ${pad('Changed', 10)}`;
|
|
16
|
+
const line = '-'.repeat(header.length);
|
|
17
|
+
|
|
18
|
+
lines.push(`\n=== ${title} ===`);
|
|
19
|
+
lines.push(header);
|
|
20
|
+
lines.push(line);
|
|
21
|
+
|
|
22
|
+
for (const [author, periodMap] of Object.entries(obj)) {
|
|
23
|
+
const keys = Object.keys(periodMap).sort();
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
lines.push(
|
|
26
|
+
[
|
|
27
|
+
pad(author, 18),
|
|
28
|
+
pad(key, 12),
|
|
29
|
+
pad(String(periodMap[key]), 10),
|
|
30
|
+
].join(' | ')
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let output = '';
|
|
39
|
+
|
|
40
|
+
if (section === 'all' || section === 'daily') {
|
|
41
|
+
output += buildSection('Daily Changed', stats.daily, 'Date');
|
|
42
|
+
}
|
|
43
|
+
if (section === 'all' || section === 'weekly') {
|
|
44
|
+
output += buildSection('Weekly Changed', stats.weekly, 'Week');
|
|
45
|
+
}
|
|
46
|
+
if (section === 'all' || section === 'monthly') {
|
|
47
|
+
output += buildSection('Monthly Changed', stats.monthly, 'Month');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `${output.trim() }\n`;
|
|
51
|
+
}
|
package/src/stats.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import isoWeek from 'dayjs/plugin/isoWeek.js'
|
|
3
|
+
|
|
4
|
+
dayjs.extend(isoWeek)
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 自动统计每个作者的日/周/月 changed
|
|
8
|
+
*
|
|
9
|
+
* records: [{
|
|
10
|
+
* author: 'Tom',
|
|
11
|
+
* date: '2025-01-01T12:00:00Z',
|
|
12
|
+
* changed: 123
|
|
13
|
+
* }]
|
|
14
|
+
*/
|
|
15
|
+
export function buildAuthorChangeStats(records) {
|
|
16
|
+
const result = {
|
|
17
|
+
daily: {}, // { author: { 'YYYY-MM-DD': x } }
|
|
18
|
+
weekly: {}, // { author: { 'YYYY-WW': x } }
|
|
19
|
+
monthly: {} // { author: { 'YYYY-MM': x } }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const r of records) {
|
|
23
|
+
const author = r.author || 'Unknown'
|
|
24
|
+
const changed = Number(r.changed || 0)
|
|
25
|
+
|
|
26
|
+
const d = dayjs(r.date)
|
|
27
|
+
|
|
28
|
+
const dayKey = d.format('YYYY-MM-DD')
|
|
29
|
+
const weekKey = `${d.format('GGGG')}-W${d.isoWeek().toString().padStart(2, '0')}`
|
|
30
|
+
const monthKey = d.format('YYYY-MM')
|
|
31
|
+
|
|
32
|
+
if (!result.daily[author]) result.daily[author] = {}
|
|
33
|
+
if (!result.weekly[author]) result.weekly[author] = {}
|
|
34
|
+
if (!result.monthly[author]) result.monthly[author] = {}
|
|
35
|
+
|
|
36
|
+
result.daily[author][dayKey] = (result.daily[author][dayKey] || 0) + changed
|
|
37
|
+
result.weekly[author][weekKey] =
|
|
38
|
+
(result.weekly[author][weekKey] || 0) + changed
|
|
39
|
+
result.monthly[author][monthKey] =
|
|
40
|
+
(result.monthly[author][monthKey] || 0) + changed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
}
|
package/src/text.mjs
CHANGED
|
@@ -1,56 +1,51 @@
|
|
|
1
|
+
import { renderAuthorChangeStatsText } from './stats-text.mjs'
|
|
2
|
+
import { buildAuthorChangeStats } from './stats.mjs'
|
|
3
|
+
|
|
1
4
|
export function renderText(records, groups = null, opts = {}) {
|
|
2
|
-
const { showGerrit = false } = opts
|
|
5
|
+
const { showGerrit = false } = opts
|
|
3
6
|
const pad = (s, n) =>
|
|
4
|
-
s.length >= n ? `${s.slice(0, n - 1)
|
|
7
|
+
s.length >= n ? `${s.slice(0, n - 1)}…` : s + ' '.repeat(n - s.length)
|
|
8
|
+
|
|
9
|
+
const baseHeader = `${pad('Hash', 10)} | ${pad('Author', 18)} | ${pad(
|
|
10
|
+
'Date',
|
|
11
|
+
20
|
|
12
|
+
)} | ${pad('Message', 40)} | ${pad('Changed', 8)}` // ★ 新增 changed 列
|
|
5
13
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
} | ${
|
|
9
|
-
pad('Author', 18)
|
|
10
|
-
} | ${
|
|
11
|
-
pad('Date', 20)
|
|
12
|
-
} | ${
|
|
13
|
-
pad('Message', 60)}`;
|
|
14
|
+
const gerritHeader = showGerrit ? ` | ${pad('Gerrit', 50)}` : ''
|
|
15
|
+
const header = baseHeader + gerritHeader
|
|
14
16
|
|
|
15
|
-
const
|
|
16
|
-
const header = baseHeader + gerritHeader;
|
|
17
|
+
const line = '-'.repeat(header.length)
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
+
const rows = []
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
+
const buildRow = (r) => {
|
|
22
|
+
return (
|
|
23
|
+
[
|
|
24
|
+
pad(r.hash.slice(0, 8), 10),
|
|
25
|
+
pad(r.author, 18),
|
|
26
|
+
pad(r.date.replace(/ .+/, ''), 20),
|
|
27
|
+
pad(r.message, 40),
|
|
28
|
+
pad(String(r.changed ?? 0), 8) // ★ 输出 changed 数量
|
|
29
|
+
].join(' | ') + (showGerrit ? ` | ${pad(r.gerrit || '', 50)}` : '')
|
|
30
|
+
)
|
|
31
|
+
}
|
|
21
32
|
|
|
22
33
|
if (groups) {
|
|
23
34
|
for (const [g, list] of Object.entries(groups)) {
|
|
24
|
-
rows.push(`\n=== ${g} ===\n`)
|
|
25
|
-
list.forEach(r =>
|
|
26
|
-
rows.push(
|
|
27
|
-
(
|
|
28
|
-
[
|
|
29
|
-
pad(r.hash.slice(0, 8), 10),
|
|
30
|
-
pad(r.author, 18),
|
|
31
|
-
pad(r.date.replace(/ .+/, ''), 20),
|
|
32
|
-
pad(r.message, 60)
|
|
33
|
-
].join(' | ') +
|
|
34
|
-
(showGerrit ? ` | ${ pad(r.gerrit || '', 50)}` : '')
|
|
35
|
-
)
|
|
36
|
-
);
|
|
37
|
-
});
|
|
35
|
+
rows.push(`\n=== ${g} ===\n`)
|
|
36
|
+
list.forEach((r) => rows.push(buildRow(r)))
|
|
38
37
|
}
|
|
39
38
|
} else {
|
|
40
|
-
records.forEach(r =>
|
|
41
|
-
rows.push(
|
|
42
|
-
(
|
|
43
|
-
[
|
|
44
|
-
pad(r.hash.slice(0, 8), 10),
|
|
45
|
-
pad(r.author, 18),
|
|
46
|
-
pad(r.date.replace(/ .+/, ''), 20),
|
|
47
|
-
pad(r.message, 60)
|
|
48
|
-
].join(' | ') +
|
|
49
|
-
(showGerrit ? ` | ${ pad(r.gerrit || '', 50)}` : '')
|
|
50
|
-
)
|
|
51
|
-
);
|
|
52
|
-
});
|
|
39
|
+
records.forEach((r) => rows.push(buildRow(r)))
|
|
53
40
|
}
|
|
54
41
|
|
|
55
|
-
return [header, line, ...rows].join('\n')
|
|
42
|
+
return [header, line, ...rows].join('\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderChangedLinesText(records, opts = {}) {
|
|
46
|
+
const stats = buildAuthorChangeStats(records)
|
|
47
|
+
|
|
48
|
+
const result = renderAuthorChangeStatsText(stats, opts)
|
|
49
|
+
|
|
50
|
+
return result
|
|
56
51
|
}
|