wukong-gitlog-cli 1.0.39 → 1.0.40
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/.eslintrc +1 -0
- package/.prettierrc +2 -1
- package/CHANGELOG.md +95 -0
- package/README.md +93 -173
- package/README.zh-CN.md +85 -137
- package/bin/wukong-gitlog-cli +0 -0
- package/doc//347/233/256/345/275/225/347/273/223/346/236/204.md +2871 -0
- package/package.json +32 -29
- package/rc/.wukonggitlogrc +53 -0
- package/scripts/compareHourlyCounts.mjs +42 -0
- package/scripts/compareLatest.mjs +106 -0
- package/src/app/analyzeAction.mjs +120 -0
- package/src/app/exportAction.mjs +215 -0
- package/src/app/exportActionProgress.mjs +37 -0
- package/src/app/helpers.mjs +292 -0
- package/src/app/initAction.mjs +110 -0
- package/src/app/initActionWithTemp.mjs +192 -0
- package/src/app/journalAction.mjs +117 -0
- package/src/app/overtimeAction.mjs +100 -0
- package/src/app/runProfileEnd.mjs +0 -0
- package/src/app/serveAction.mjs +73 -0
- package/src/app/versionAction.mjs +7 -0
- package/src/cli/defineOptions.mjs +209 -0
- package/src/cli/index.mjs +0 -0
- package/src/cli/parseOptions.mjs +126 -8
- package/src/constants/index.mjs +16 -2
- package/src/domain/author/analyze.mjs +6 -0
- package/src/domain/author/map.mjs +0 -0
- package/src/domain/export/exportAuthor.mjs +28 -0
- package/src/domain/export/exportAuthorChanges.mjs +27 -0
- package/src/domain/export/exportAuthorChangesJson.mjs +31 -0
- package/src/domain/export/exportByMonth.mjs +157 -0
- package/src/domain/export/exportByWeek.mjs +121 -0
- package/src/domain/export/exportCommits.mjs +26 -0
- package/src/domain/export/exportCommitsExcel.mjs +45 -0
- package/src/domain/export/exportCommitsJson.mjs +31 -0
- package/src/domain/export/index.mjs +91 -0
- package/src/domain/git/ensureGitAvailable.mjs +66 -0
- package/src/domain/git/ensureGitRepo.mjs +41 -0
- package/src/domain/git/getGitFeatures.mjs +59 -0
- package/src/domain/git/getGitLogs.mjs +326 -0
- package/src/domain/git/getGitUser.mjs +44 -0
- package/src/domain/git/getRepoRoot.mjs +32 -0
- package/src/domain/git/gitCapability.mjs +119 -0
- package/src/domain/git/index.mjs +96 -0
- package/src/domain/git/resolveGerrit.mjs +102 -0
- package/src/domain/overtime/analyze.mjs +48 -0
- package/src/domain/overtime/index.mjs +3 -0
- package/src/domain/overtime/perPeriod.mjs +15 -0
- package/src/domain/overtime/render.mjs +15 -0
- package/src/i18n/index.mjs +38 -0
- package/src/i18n/resources.mjs +252 -0
- package/src/index.mjs +132 -649
- package/src/infra/cache.mjs +0 -0
- package/src/infra/configStore.mjs +128 -0
- package/src/infra/fs.mjs +0 -0
- package/src/infra/path.mjs +0 -0
- package/src/output/csv/overtime.mjs +12 -0
- package/src/output/csv.mjs +0 -0
- package/src/output/data/readData.mjs +54 -0
- package/src/output/data/writeData.mjs +145 -0
- package/src/output/excel/commits.mjs +9 -0
- package/src/output/excel/outputExcelDayReport.mjs +92 -0
- package/src/output/excel/perPeriod.mjs +24 -0
- package/src/{excel.mjs → output/excel.mjs} +3 -2
- package/src/output/index.mjs +79 -0
- package/src/output/json/overtime.mjs +9 -0
- package/src/output/tab/overtime.mjs +12 -0
- package/src/output/tab.mjs +0 -0
- package/src/output/text/commits.mjs +9 -0
- package/src/output/text/index.mjs +3 -0
- package/src/output/text/outputTxtDayReport.mjs +74 -0
- package/src/output/text/overtime.mjs +18 -0
- package/src/output/utils/getEsmJs.mjs +10 -0
- package/src/output/utils/index.mjs +14 -0
- package/src/output/utils/outputPath.mjs +19 -0
- package/src/output/utils/writeFile.mjs +10 -0
- package/src/serve/index.mjs +0 -0
- package/src/{server.mjs → serve/startServer.mjs} +21 -3
- package/src/serve/writeData.mjs +0 -0
- package/src/utils/authorNormalizer.mjs +28 -2
- package/src/utils/buildAuthorChangeStats.mjs +44 -0
- package/src/utils/deepMerge.mjs +13 -0
- package/src/utils/getPackage.mjs +11 -0
- package/src/utils/getProfileDirFile.mjs +12 -0
- package/src/utils/{file.mjs → groupRecords.mjs} +8 -9
- package/src/utils/index.mjs +5 -2
- package/src/utils/logger.mjs +28 -17
- package/src/utils/profiler.mjs +0 -101
- package/src/utils/resolve.mjs +11 -0
- package/src/utils/showVersionInfo.mjs +6 -2
- package/src/utils/time.mjs +0 -0
- package/src/utils/wait.mjs +2 -0
- package/web/app.js +3197 -257
- package/web/index.html +171 -22
- package/web/revoke/alpha1/app.js +4324 -0
- package/web/revoke/alpha1/index.html +266 -0
- package/web/revoke/app.before.js +3139 -0
- package/web/revoke/index-before.html +181 -0
- package/web/static/style.css +116 -9
- package/src/git.mjs +0 -256
- package/src/handlers/handleServe.mjs +0 -203
- package/src/lib/configStore.mjs +0 -11
- package/src/lib/memoize.mjs +0 -14
- package/src/utils/analyzeOvertimeCached.mjs +0 -7
- package/src/utils/checkUpdate.mjs +0 -130
- package/src/utils/exitWithTime.mjs +0 -17
- package/src/utils/handleSuccess.mjs +0 -9
- package/src/utils/logDev.mjs +0 -19
- package/src/utils/output.mjs +0 -26
- package/src/utils/profiler/diff.mjs +0 -26
- package/src/utils/profiler/format.mjs +0 -11
- package/src/utils/profiler/index.mjs +0 -144
- package/src/utils/profiler/trace.mjs +0 -26
- package/src/utils/time/scopeTimer.mjs +0 -37
- package/src/utils/time/timer.mjs +0 -33
- package/src/utils/time/withTimer.mjs +0 -11
- package/src/utils/timer.mjs +0 -35
- /package/src/{overtime → domain/overtime}/createOvertimeStats.mjs +0 -0
- /package/src/{overtime → domain/overtime}/overtime.mjs +0 -0
- /package/src/{json.mjs → output/json.mjs} +0 -0
- /package/src/{renderAuthorMapText.mjs → output/renderAuthorMapText.mjs} +0 -0
- /package/src/{stats-text.mjs → output/stats-text.mjs} +0 -0
- /package/src/{stats.mjs → output/stats.mjs} +0 -0
- /package/src/{text.mjs → output/text.mjs} +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { getWorkOvertimeStats } from '../domain/overtime/analyze.mjs'
|
|
5
|
+
import { getWeekRange } from '../utils/getWeekRange.mjs'
|
|
6
|
+
import { groupRecords } from '../utils/groupRecords.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 处理单条 commit message
|
|
10
|
+
* 如果冒号前是 feat-* 或 fix-*,则去掉前缀
|
|
11
|
+
*/
|
|
12
|
+
function normalizeCommitMsg(message) {
|
|
13
|
+
// 匹配:feat-xxx: 或 fix-xxx:
|
|
14
|
+
const match = message.match(/^(feat|fix)-[^:]+:(.+)$/i)
|
|
15
|
+
|
|
16
|
+
if (match) {
|
|
17
|
+
return match[2].trim()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return message
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export const getWorkTimeConfig = (opts) => {
|
|
25
|
+
// startHour = 9, endHour = 18, lunchStart = 12, lunchEnd = 14, country = 'CN'
|
|
26
|
+
return {
|
|
27
|
+
startHour: opts.worktime.start,
|
|
28
|
+
endHour: opts.worktime.end,
|
|
29
|
+
lunchStart: opts.worktime.lunch.start,
|
|
30
|
+
lunchEnd: opts.worktime.lunch.end,
|
|
31
|
+
country: opts.worktime.country,
|
|
32
|
+
overnightCutoff: opts.worktime.overnightCutoff
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const getOvertimeByWeek = (commits) => {
|
|
37
|
+
// 新增:每周趋势数据(用于前端图表)
|
|
38
|
+
const weekGroups = groupRecords(commits, 'week')
|
|
39
|
+
const weekKeys = Object.keys(weekGroups).sort()
|
|
40
|
+
const weeklySeries = weekKeys.map((k) => {
|
|
41
|
+
const s = getWorkOvertimeStats(weekGroups[k])
|
|
42
|
+
return {
|
|
43
|
+
period: k,
|
|
44
|
+
range: getWeekRange(k),
|
|
45
|
+
total: s.total,
|
|
46
|
+
outsideWorkCount: s.outsideWorkCount,
|
|
47
|
+
outsideWorkRate: s.outsideWorkRate,
|
|
48
|
+
nonWorkdayCount: s.nonWorkdayCount,
|
|
49
|
+
nonWorkdayRate: s.nonWorkdayRate
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
return weeklySeries
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const getOvertimeByMonth = (commits) => {
|
|
56
|
+
// 新增:每月趋势数据(用于前端图表)
|
|
57
|
+
const monthGroups = groupRecords(commits, 'month')
|
|
58
|
+
const monthKeys = Object.keys(monthGroups).sort()
|
|
59
|
+
const monthlySeries = monthKeys.map((k) => {
|
|
60
|
+
const s = getWorkOvertimeStats(monthGroups[k])
|
|
61
|
+
return {
|
|
62
|
+
period: k,
|
|
63
|
+
total: s.total,
|
|
64
|
+
outsideWorkCount: s.outsideWorkCount,
|
|
65
|
+
outsideWorkRate: s.outsideWorkRate,
|
|
66
|
+
nonWorkdayCount: s.nonWorkdayCount,
|
|
67
|
+
nonWorkdayRate: s.nonWorkdayRate
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
return monthlySeries
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 每日最晚提交小时(用于显著展示加班严重程度)
|
|
74
|
+
export const getLatestCommitByDay = ({ commits, opts }) => {
|
|
75
|
+
// 新增:每日最晚提交小时(用于显著展示加班严重程度)
|
|
76
|
+
const dayGroups2 = groupRecords(commits, 'day')
|
|
77
|
+
const dayKeys2 = Object.keys(dayGroups2).sort()
|
|
78
|
+
|
|
79
|
+
// 次日凌晨归并窗口(默认 6 点前仍算前一天的加班)
|
|
80
|
+
const overnightCutoff = Number.isFinite(opts.overnightCutoff)
|
|
81
|
+
? opts.overnightCutoff
|
|
82
|
+
: 6
|
|
83
|
+
// 次日上班时间(默认按 workStart,若未指定则 9 点)
|
|
84
|
+
const workStartHour =
|
|
85
|
+
opts.workStart || opts.workStart === 0 ? opts.workStart : 9
|
|
86
|
+
const workEndHour = opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18
|
|
87
|
+
|
|
88
|
+
// 有些日期「本身没有 commit」,但第二天凌晨有提交要归并到这一天,
|
|
89
|
+
// 需要补出这些“虚拟日期”,否则 latestByDay 会漏掉这天。
|
|
90
|
+
const virtualPrevDays = new Set()
|
|
91
|
+
commits.forEach((r) => {
|
|
92
|
+
const d = new Date(r.date)
|
|
93
|
+
if (Number.isNaN(d.valueOf())) return
|
|
94
|
+
const h = d.getHours()
|
|
95
|
+
if (h < 0 || h >= overnightCutoff || h >= workStartHour) return
|
|
96
|
+
const curDay = dayjs(d).format('YYYY-MM-DD')
|
|
97
|
+
const prevDay = dayjs(curDay).subtract(1, 'day').format('YYYY-MM-DD')
|
|
98
|
+
if (!dayGroups2[prevDay]) {
|
|
99
|
+
virtualPrevDays.add(prevDay)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const allDayKeys = Array.from(
|
|
104
|
+
new Set([...dayKeys2, ...virtualPrevDays])
|
|
105
|
+
).sort()
|
|
106
|
+
|
|
107
|
+
const latestByDay = allDayKeys.map((k) => {
|
|
108
|
+
const list = dayGroups2[k] || []
|
|
109
|
+
|
|
110
|
+
// 1) 当天「下班后」的提交:只统计 >= workEndHour 的小时
|
|
111
|
+
const sameDayHours = list
|
|
112
|
+
.map((r) => new Date(r.date))
|
|
113
|
+
.filter((d) => !Number.isNaN(d.valueOf()))
|
|
114
|
+
.map((d) => d.getHours())
|
|
115
|
+
.filter((h) => h >= workEndHour && h < 24)
|
|
116
|
+
|
|
117
|
+
// 2) 次日凌晨、但仍算前一日加班的提交
|
|
118
|
+
const nextKey = dayjs(k).add(1, 'day').format('YYYY-MM-DD')
|
|
119
|
+
const early = dayGroups2[nextKey] || []
|
|
120
|
+
const earlyHours = early
|
|
121
|
+
.map((r) => new Date(r.date))
|
|
122
|
+
.filter((d) => !Number.isNaN(d.valueOf()))
|
|
123
|
+
.map((d) => d.getHours())
|
|
124
|
+
// 只看 [0, overnightCutoff) 之间的小时,
|
|
125
|
+
// 并且默认认为 < workStartHour 属于「次日上班前」
|
|
126
|
+
.filter(
|
|
127
|
+
(h) =>
|
|
128
|
+
h >= 0 &&
|
|
129
|
+
h < overnightCutoff &&
|
|
130
|
+
// 保护性判断:若有人把 overnightCutoff 设得大于上班时间,
|
|
131
|
+
// 我们仍然只统计到上班时间为止
|
|
132
|
+
h < workStartHour
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// 3) 计算「逻辑上的最晚加班时间」
|
|
136
|
+
// - 当天晚上的用原始小时(如 22 点)
|
|
137
|
+
// - 次日凌晨的用 24 + 小时(如 1 点 → 25)
|
|
138
|
+
const overtimeValues = [
|
|
139
|
+
...sameDayHours.map((h) => h),
|
|
140
|
+
...earlyHours.map((h) => 24 + h)
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
if (overtimeValues.length === 0) {
|
|
144
|
+
// 这一天没有任何「下班后到次日上班前」的提交
|
|
145
|
+
return {
|
|
146
|
+
date: k,
|
|
147
|
+
latestHour: null,
|
|
148
|
+
latestHourNormalized: null
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const latestHourNormalized = Math.max(...overtimeValues)
|
|
153
|
+
|
|
154
|
+
// latestHour 保留「当天自然日内」的最晚提交通常小时数,供前端需要时参考
|
|
155
|
+
const sameDayMax =
|
|
156
|
+
sameDayHours.length > 0 ? Math.max(...sameDayHours) : null
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
date: k,
|
|
160
|
+
latestHour: sameDayMax,
|
|
161
|
+
latestHourNormalized
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
return latestByDay
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @function getGitLogsDayReport
|
|
169
|
+
* @description 返回数据包含 git commit 的日期day (YYYY-MM-DD),msg(当天提交的所有合并到一个msg),author
|
|
170
|
+
* 用于 git log --all 的日报统计(Gerrit 友好)
|
|
171
|
+
*
|
|
172
|
+
* - 按 Change-Id 去重(同一 Change 只统计一次,取最新提交)
|
|
173
|
+
* - 按 day + author 聚合
|
|
174
|
+
* - msg:normalize 后 + 去重 + 合并
|
|
175
|
+
* - originalMsg:当天所有原始 commit message(不去重,便于排查)
|
|
176
|
+
* 返回数据包含:
|
|
177
|
+
* - day: YYYY-MM-DD
|
|
178
|
+
* - author
|
|
179
|
+
* - originalMsg: 当天所有原始 commit message
|
|
180
|
+
* - msg: 处理后的 message(去掉 feat-/fix- 冒号前缀)去重合并后的结果
|
|
181
|
+
* @param {Array} records
|
|
182
|
+
* @param {Object} opts
|
|
183
|
+
* @returns {Array<{ day: string, msg: string, author: string }>}
|
|
184
|
+
*/
|
|
185
|
+
export const getGitLogsDayReport = async (records = [], opts = {}) => {
|
|
186
|
+
if (!Array.isArray(records) || records.length === 0) {
|
|
187
|
+
return []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const authorFilter = opts?.author
|
|
191
|
+
function toList(v) {
|
|
192
|
+
if (!v) return null
|
|
193
|
+
if (Array.isArray(v)) return v.map((s) => String(s).trim()).filter(Boolean)
|
|
194
|
+
return String(v)
|
|
195
|
+
.split(',')
|
|
196
|
+
.map((s) => s.trim())
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
}
|
|
199
|
+
const include = authorFilter && typeof authorFilter === 'object' ? toList(authorFilter.include) : (typeof authorFilter === 'string' ? toList(authorFilter) : null)
|
|
200
|
+
const exclude = authorFilter && typeof authorFilter === 'object' ? toList(authorFilter.exclude) : null
|
|
201
|
+
|
|
202
|
+
/* ---------------- 1️⃣ Change-Id 级别去重(取最新) ---------------- */
|
|
203
|
+
|
|
204
|
+
const changeMap = new Map()
|
|
205
|
+
|
|
206
|
+
for (const item of records) {
|
|
207
|
+
if (!item?.date || !item?.message) continue
|
|
208
|
+
|
|
209
|
+
const key = item.changeId || item.hash
|
|
210
|
+
const time = dayjs(item.date).valueOf()
|
|
211
|
+
|
|
212
|
+
const prev = changeMap.get(key)
|
|
213
|
+
if (!prev || time > dayjs(prev.date).valueOf()) {
|
|
214
|
+
changeMap.set(key, item)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const dedupedRecords = Array.from(changeMap.values())
|
|
219
|
+
|
|
220
|
+
/* ---------------- 2️⃣ Day + Author 聚合 ---------------- */
|
|
221
|
+
|
|
222
|
+
const map = {}
|
|
223
|
+
|
|
224
|
+
for (const item of dedupedRecords) {
|
|
225
|
+
const author =
|
|
226
|
+
item.author ||
|
|
227
|
+
item.originalAuthor ||
|
|
228
|
+
item.email ||
|
|
229
|
+
'unknown'
|
|
230
|
+
|
|
231
|
+
// --author 过滤:支持字符串/数组 或 { include: [], exclude: [] }
|
|
232
|
+
if ((include && include.length) || (exclude && exclude.length)) {
|
|
233
|
+
const name = (item.author || item.originalAuthor || '').trim().toLowerCase()
|
|
234
|
+
const mail = (item.email || '').trim().toLowerCase()
|
|
235
|
+
const matches = (list) =>
|
|
236
|
+
list.some((it) => {
|
|
237
|
+
const v = String(it).trim().toLowerCase()
|
|
238
|
+
if (v.includes('@')) return v === mail
|
|
239
|
+
return v === name
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
if (include && include.length) {
|
|
243
|
+
if (!matches(include)) continue
|
|
244
|
+
}
|
|
245
|
+
if (exclude && exclude.length) {
|
|
246
|
+
if (matches(exclude)) continue
|
|
247
|
+
}
|
|
248
|
+
} else if (authorFilter && typeof authorFilter === 'string') {
|
|
249
|
+
// legacy: simple string match
|
|
250
|
+
if (author !== authorFilter) continue
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const day = dayjs(item.date).format('YYYY-MM-DD')
|
|
254
|
+
const key = `${day}__${author}`
|
|
255
|
+
|
|
256
|
+
if (!map[key]) {
|
|
257
|
+
map[key] = {
|
|
258
|
+
day,
|
|
259
|
+
author,
|
|
260
|
+
originalMsgs: [],
|
|
261
|
+
msgSet: new Set()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const originalMsg = item.message.trim()
|
|
266
|
+
const handledMsg = normalizeCommitMsg(originalMsg)
|
|
267
|
+
|
|
268
|
+
map[key].originalMsgs.push(originalMsg)
|
|
269
|
+
map[key].msgSet.add(handledMsg)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/*
|
|
273
|
+
wukong-gitlog-cli journal --since 2026-02-01 可以得到2026-02-02日数据
|
|
274
|
+
wukong-gitlog-cli journal --since 2026-02-02 不能得到2026-02-02日数据
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
/* ---------------- 3️⃣ 输出 + 稳定排序 ---------------- */
|
|
278
|
+
|
|
279
|
+
return Object.values(map)
|
|
280
|
+
.map((item) => ({
|
|
281
|
+
day: item.day,
|
|
282
|
+
author: item.author,
|
|
283
|
+
originalMsg: item.originalMsgs.join('\n'),
|
|
284
|
+
msg: Array.from(item.msgSet).join('\n')
|
|
285
|
+
}))
|
|
286
|
+
.sort((a, b) => {
|
|
287
|
+
const dayDiff =
|
|
288
|
+
dayjs(b.day).valueOf() - dayjs(a.day).valueOf()
|
|
289
|
+
if (dayDiff !== 0) return dayDiff
|
|
290
|
+
return a.author.localeCompare(b.author, 'zh')
|
|
291
|
+
})
|
|
292
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file: initAction.mjs
|
|
3
|
+
* @description: 使用 @inquirer/prompts 初始化配置文件并维护 .gitignore
|
|
4
|
+
* @author: King Monkey
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import yaml from 'yaml';
|
|
9
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
10
|
+
import { DEFAULT_CONFIG, RC_NAMES } from '../infra/configStore.mjs';
|
|
11
|
+
import { WUKONG_GITLOG_RC } from '#src/constants/index.mjs';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 自动将输出目录添加到 .gitignore
|
|
16
|
+
*/
|
|
17
|
+
async function manageGitignore(outputDir) {
|
|
18
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(gitignorePath)) return;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
24
|
+
|
|
25
|
+
// 同时检查输出目录与常见配置文件名(从 configStore 导出),若全部存在则跳过
|
|
26
|
+
const configFiles = Array.isArray(RC_NAMES) ? RC_NAMES : [];
|
|
27
|
+
|
|
28
|
+
const hasOutput = content.includes(outputDir);
|
|
29
|
+
const hasAllConfigs = configFiles.length && configFiles.every((f) => content.includes(f));
|
|
30
|
+
if (hasOutput && hasAllConfigs) return;
|
|
31
|
+
|
|
32
|
+
const shouldAdd = await confirm({
|
|
33
|
+
message: `是否自动将报告目录 "${outputDir}/" 以及配置文件名添加到 .gitignore?`,
|
|
34
|
+
default: true
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (shouldAdd) {
|
|
38
|
+
const prefix = content.endsWith('\n') ? '' : '\n';
|
|
39
|
+
let entry = `${prefix}\n# Wukong GitLog Reports\n`;
|
|
40
|
+
if (!hasOutput) entry += `${outputDir}/\n`;
|
|
41
|
+
|
|
42
|
+
const missingConfigs = configFiles.filter((f) => !content.includes(f));
|
|
43
|
+
if (missingConfigs.length) {
|
|
44
|
+
entry += `\n# Wukong GitLog Config\n${ missingConfigs.map((f) => `${f}\n`).join('')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fs.appendFileSync(gitignorePath, entry, 'utf8');
|
|
48
|
+
console.log(`✅ 已更新 .gitignore`);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (err.name !== 'ExitPromptError') {
|
|
52
|
+
console.warn(`⚠️ 无法更新 .gitignore: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
export async function initAction(options) {
|
|
59
|
+
console.log(`\n🚀 ${'Wukong GitLog'} 配置文件初始化\n`);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const format = await select({
|
|
63
|
+
message: '请选择要生成的配置文件格式:',
|
|
64
|
+
choices: [
|
|
65
|
+
{ name: 'ES Module (.mjs)', value: 'mjs' },
|
|
66
|
+
{ name: 'JavaScript (.js)', value: 'js' },
|
|
67
|
+
{ name: 'YAML (.yml)', value: 'yml' },
|
|
68
|
+
{ name: 'JSON (.json)', value: 'json' },
|
|
69
|
+
{ name: 'YAML 无后缀 (.wukonggitlogrc)', value: 'plain' }
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const fileName = format === 'plain' ? WUKONG_GITLOG_RC : `${WUKONG_GITLOG_RC}.${format}`;
|
|
74
|
+
const targetPath = path.join(process.cwd(), fileName);
|
|
75
|
+
|
|
76
|
+
if (fs.existsSync(targetPath) && !options.force) {
|
|
77
|
+
console.error(`\n❌ 错误: 当前目录已存在 ${fileName}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let content = '';
|
|
82
|
+
const headerComment = `// Wukong GitLog Config\n// Generated at ${new Date().toLocaleString()}\n\n`;
|
|
83
|
+
|
|
84
|
+
switch (format) {
|
|
85
|
+
case 'mjs':
|
|
86
|
+
case 'js':
|
|
87
|
+
content = `${headerComment}export default ${JSON.stringify(DEFAULT_CONFIG, null, 2)};`;
|
|
88
|
+
break;
|
|
89
|
+
case 'yml':
|
|
90
|
+
case 'plain':
|
|
91
|
+
content = `# Wukong GitLog Config\n${yaml.stringify(DEFAULT_CONFIG)}`;
|
|
92
|
+
break;
|
|
93
|
+
case 'json':
|
|
94
|
+
content = JSON.stringify(DEFAULT_CONFIG, null, 2);
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
throw new Error('Unsupported format selected.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
101
|
+
console.log(`✅ 成功生成配置: ${fileName}`);
|
|
102
|
+
|
|
103
|
+
await manageGitignore(DEFAULT_CONFIG.output.dir);
|
|
104
|
+
console.log(`\n✨ 初始化完成!\n`);
|
|
105
|
+
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err.name === 'ExitPromptError') console.log('\n👋 已取消');
|
|
108
|
+
else console.error(`\n❌ 失败: ${err.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file: initActionWithTemp.mjs
|
|
3
|
+
* @description: 使用 @inquirer/prompts 初始化带详细注释的配置文件
|
|
4
|
+
* @author: King Monkey
|
|
5
|
+
*/
|
|
6
|
+
import { confirm, select } from '@inquirer/prompts'
|
|
7
|
+
import chalk from 'chalk'
|
|
8
|
+
import fs from 'fs'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
|
|
11
|
+
import { t } from '../i18n/index.mjs'
|
|
12
|
+
import { DEFAULT_CONFIG, RC_NAMES } from '../infra/configStore.mjs'
|
|
13
|
+
|
|
14
|
+
// 动态生成 YAML 模板
|
|
15
|
+
const getYamlTemplate =
|
|
16
|
+
() => `# ---------------------------------------------------------
|
|
17
|
+
# Wukong GitLog Config (.wukonggitlogrc.yml)
|
|
18
|
+
# ${t('template.generated_at')}: ${new Date().toLocaleString()}
|
|
19
|
+
# ---------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
# ${t('template.author_config')}
|
|
22
|
+
author:
|
|
23
|
+
include: [] # ${t('template.author_include')}
|
|
24
|
+
exclude: [] # ${t('template.author_exclude')}
|
|
25
|
+
|
|
26
|
+
# ${t('template.git_config')}
|
|
27
|
+
git:
|
|
28
|
+
merges: true # ${t('template.git_merges')}
|
|
29
|
+
limit: 5000 # ${t('template.git_limit')}
|
|
30
|
+
|
|
31
|
+
# ${t('template.period_config')}
|
|
32
|
+
period:
|
|
33
|
+
groupBy: month # ${t('template.period_group')}
|
|
34
|
+
since: "" # ${t('template.period_since')}
|
|
35
|
+
until: "" # ${t('template.period_until')}
|
|
36
|
+
|
|
37
|
+
# ${t('template.gerrit_config')}
|
|
38
|
+
gerrit:
|
|
39
|
+
prefix: "" # Example: https://gerrit.xxx.com/c/{{changeNumber}}
|
|
40
|
+
api: "" # Gerrit API URL
|
|
41
|
+
auth: "" # "user:pass" or "TOKEN"
|
|
42
|
+
|
|
43
|
+
# ${t('template.worktime_config')}
|
|
44
|
+
worktime:
|
|
45
|
+
country: CN # ${t('template.worktime_country')}
|
|
46
|
+
start: 9 # ${t('template.worktime_start')} (0-23)
|
|
47
|
+
end: 18 # ${t('template.worktime_end')} (0-23)
|
|
48
|
+
lunch:
|
|
49
|
+
start: 12 # ${t('template.worktime_lunch')} start
|
|
50
|
+
end: 14 # ${t('template.worktime_lunch')} end
|
|
51
|
+
overnightCutoff: 6 # ${t('template.worktime_cutoff')}
|
|
52
|
+
|
|
53
|
+
# ${t('template.output_config')}
|
|
54
|
+
output:
|
|
55
|
+
dir: "output-wukong" # ${t('template.output_dir')}
|
|
56
|
+
formats: ["text", "excel"] # ${t('template.output_formats')}
|
|
57
|
+
perPeriod:
|
|
58
|
+
enabled: true # ${t('template.output_per_period')}
|
|
59
|
+
excelMode: "sheets" # sheets | files
|
|
60
|
+
|
|
61
|
+
# ${t('template.author_aliases')}
|
|
62
|
+
authorAliases: {}
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
// 动态生成 JS 模板
|
|
66
|
+
const getJsTemplate = () => `/**
|
|
67
|
+
* Wukong GitLog Config (.wukonggitlogrc.js)
|
|
68
|
+
* ${t('template.generated_at')}: ${new Date().toLocaleString()}
|
|
69
|
+
*/
|
|
70
|
+
export default {
|
|
71
|
+
// ${t('template.author_config')}
|
|
72
|
+
author: {
|
|
73
|
+
include: [], // ${t('template.author_include')}
|
|
74
|
+
exclude: [] // ${t('template.author_exclude')}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// ${t('template.git_config')}
|
|
78
|
+
git: {
|
|
79
|
+
merges: true,
|
|
80
|
+
limit: 5000
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// ${t('template.worktime_config')}
|
|
84
|
+
worktime: {
|
|
85
|
+
country: 'CN',
|
|
86
|
+
start: 9,
|
|
87
|
+
end: 18,
|
|
88
|
+
lunch: { start: 12, end: 14 },
|
|
89
|
+
overnightCutoff: 6
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// ${t('template.author_aliases')}
|
|
93
|
+
authorAliases: {},
|
|
94
|
+
|
|
95
|
+
// ${t('template.output_config')}
|
|
96
|
+
output: {
|
|
97
|
+
dir: 'output-wukong',
|
|
98
|
+
formats: ['text', 'excel'],
|
|
99
|
+
perPeriod: { enabled: true, excelMode: 'sheets' }
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
`
|
|
103
|
+
async function manageGitignore(outputDir) {
|
|
104
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore')
|
|
105
|
+
if (!fs.existsSync(gitignorePath)) return
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const content = fs.readFileSync(gitignorePath, 'utf8')
|
|
109
|
+
|
|
110
|
+
// 使用从 configStore 导出的 RC_NAMES
|
|
111
|
+
const configFiles = Array.isArray(RC_NAMES) ? RC_NAMES : []
|
|
112
|
+
|
|
113
|
+
const hasOutput = content.includes(outputDir)
|
|
114
|
+
const hasAllConfigs =
|
|
115
|
+
configFiles.length && configFiles.every((f) => content.includes(f))
|
|
116
|
+
if (hasOutput && hasAllConfigs) return
|
|
117
|
+
|
|
118
|
+
const shouldAdd = await confirm({
|
|
119
|
+
message: t('init.gitignore_ask'), // `是否自动将报告目录 "${outputDir}/" 以及配置文件名添加到 .gitignore?`,
|
|
120
|
+
default: true
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (shouldAdd) {
|
|
124
|
+
const prefix = content.endsWith('\n') ? '' : '\n'
|
|
125
|
+
let entry = `${prefix}\n# Wukong GitLog Reports\n`
|
|
126
|
+
if (!hasOutput) entry += `${outputDir}/\n`
|
|
127
|
+
|
|
128
|
+
const missingConfigs = configFiles.filter((f) => !content.includes(f))
|
|
129
|
+
if (missingConfigs.length) {
|
|
130
|
+
entry += `\n# Wukong GitLog Config\n${missingConfigs.map((f) => `${f}\n`).join('')}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fs.appendFileSync(gitignorePath, entry, 'utf8')
|
|
134
|
+
console.log(`✅ ${t('init.gitignore_updated')}`)
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err.name !== 'ExitPromptError') {
|
|
138
|
+
console.warn(`⚠️ ${t('init.gitignore_warn')} ${err.message}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function initActionWithTemp(options) {
|
|
144
|
+
console.log(`\n🚀 Wukong GitLog ${t('init.title')}\n`)
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const format = await select({
|
|
148
|
+
message: t('init.select_format'),
|
|
149
|
+
choices: [
|
|
150
|
+
{ name: t('init.formats.mjs'), value: 'mjs' },
|
|
151
|
+
{ name: t('init.formats.js'), value: 'js' },
|
|
152
|
+
{ name: t('init.formats.yaml'), value: 'yaml' },
|
|
153
|
+
{ name: t('init.formats.json'), value: 'json' },
|
|
154
|
+
{ name: t('init.formats.plain'), value: 'plain' }
|
|
155
|
+
]
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const fileNameMap = {
|
|
159
|
+
mjs: '.wukonggitlogrc.mjs',
|
|
160
|
+
js: '.wukonggitlogrc.js',
|
|
161
|
+
yaml: '.wukonggitlogrc.yml',
|
|
162
|
+
json: '.wukonggitlogrc.json',
|
|
163
|
+
plain: '.wukonggitlogrc'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fileName = fileNameMap[format]
|
|
167
|
+
const targetPath = path.join(process.cwd(), fileName)
|
|
168
|
+
|
|
169
|
+
if (fs.existsSync(targetPath) && !options.force) {
|
|
170
|
+
console.error(`\n❌ ${t('init.error_exists')} (${fileName})`)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let content = ''
|
|
175
|
+
if (format === 'js' || format === 'mjs') content = getJsTemplate()
|
|
176
|
+
else if (format === 'yaml' || format === 'plain')
|
|
177
|
+
content = getYamlTemplate()
|
|
178
|
+
else content = JSON.stringify(DEFAULT_CONFIG, null, 2)
|
|
179
|
+
|
|
180
|
+
fs.writeFileSync(targetPath, content, 'utf8')
|
|
181
|
+
console.log(`✅ ${t('init.success_created')} ${chalk.green(fileName)}`)
|
|
182
|
+
|
|
183
|
+
await manageGitignore(DEFAULT_CONFIG.output.dir)
|
|
184
|
+
console.log(`\n✨ ${t('init.complete')}\n`)
|
|
185
|
+
} catch (err) {
|
|
186
|
+
if (err.name === 'ExitPromptError') {
|
|
187
|
+
console.log(`\n👋 ${t('init.cancel')}`)
|
|
188
|
+
} else {
|
|
189
|
+
console.error(`\n❌ ${t('init.fail')} ${err.message}`)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|