@spaceflow/review-summary 0.24.0 → 0.26.0
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 +12 -0
- package/dist/index.js +821 -6
- package/package.json +1 -1
- package/src/index.ts +77 -5
- package/src/review-summary.service.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review-summary@0.24.0...@spaceflow/review-summary@0.25.0) (2026-03-02)
|
|
4
|
+
|
|
5
|
+
### 新特性
|
|
6
|
+
|
|
7
|
+
* **review-summary:** 实现 review-summary 命令核心功能 ([5964eec](https://github.com/Lydanne/spaceflow/commit/5964eec0e2ee4ac46f74808466429b0edc0cbaa1))
|
|
8
|
+
|
|
9
|
+
## [0.24.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review-summary@0.23.0...@spaceflow/review-summary@0.24.0) (2026-03-02)
|
|
10
|
+
|
|
11
|
+
### 新特性
|
|
12
|
+
|
|
13
|
+
* **review-summary:** 新增文件过滤功能并优化缺陷率计算 ([9655f82](https://github.com/Lydanne/spaceflow/commit/9655f828cec77c4a7db29ae1cda59fd3ea10ceab))
|
|
14
|
+
|
|
3
15
|
## [0.23.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review-summary@0.22.0...@spaceflow/review-summary@0.23.0) (2026-03-02)
|
|
4
16
|
|
|
5
17
|
### 新特性
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { addLocaleResources, defineExtension, t } from "@spaceflow/core";
|
|
1
|
+
import { addLocaleResources, defineExtension, normalizeVerbose, parseVerbose, shouldLog, t } from "@spaceflow/core";
|
|
2
|
+
import { MarkdownFormatter } from "@spaceflow/review";
|
|
3
|
+
import micromatch from "micromatch";
|
|
4
|
+
import { writeFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
2
6
|
// The require scope
|
|
3
7
|
var __webpack_require__ = {};
|
|
4
8
|
|
|
@@ -40,10 +44,756 @@ var en_review_summary_namespaceObject = JSON.parse('{"description":"Period summa
|
|
|
40
44
|
};
|
|
41
45
|
addLocaleResources("review-summary", reviewSummaryLocales);
|
|
42
46
|
|
|
47
|
+
;// CONCATENATED MODULE: external "@spaceflow/review"
|
|
48
|
+
|
|
49
|
+
;// CONCATENATED MODULE: external "micromatch"
|
|
50
|
+
|
|
51
|
+
;// CONCATENATED MODULE: external "fs"
|
|
52
|
+
|
|
53
|
+
;// CONCATENATED MODULE: external "path"
|
|
54
|
+
|
|
55
|
+
;// CONCATENATED MODULE: ./src/review-summary.service.ts
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/** 加权模式默认权重 */ const DEFAULT_WEIGHTED_WEIGHTS = {
|
|
62
|
+
prBase: 10,
|
|
63
|
+
additionsPer100: 2,
|
|
64
|
+
deletionsPer100: 1,
|
|
65
|
+
changedFile: 0.5,
|
|
66
|
+
issueDeduction: 3,
|
|
67
|
+
fixedBonus: 1
|
|
68
|
+
};
|
|
69
|
+
/** 分数累计模式默认权重 */ const DEFAULT_COMMIT_BASED_WEIGHTS = {
|
|
70
|
+
validCommit: 5,
|
|
71
|
+
errorDeduction: 2,
|
|
72
|
+
warnDeduction: 1,
|
|
73
|
+
errorFixedBonus: 1,
|
|
74
|
+
warnFixedBonus: 0.5,
|
|
75
|
+
minCommitLines: 5
|
|
76
|
+
};
|
|
77
|
+
/** issue-based 模式默认权重 */ const DEFAULT_ISSUE_BASED_WEIGHTS = {
|
|
78
|
+
minBase: 60,
|
|
79
|
+
maxBase: 100,
|
|
80
|
+
capLines: 1000,
|
|
81
|
+
errorDeduction: 8,
|
|
82
|
+
warnDeduction: 3,
|
|
83
|
+
errorFixedBonus: 5,
|
|
84
|
+
warnFixedBonus: 2
|
|
85
|
+
};
|
|
86
|
+
/** defect-rate 模式默认权重 */ const DEFAULT_DEFECT_RATE_WEIGHTS = {
|
|
87
|
+
errorPenalty: 0.3,
|
|
88
|
+
warnPenalty: 0.1,
|
|
89
|
+
fixedDiscount: 0.05
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* 周期统计服务
|
|
93
|
+
*/ class PeriodSummaryService {
|
|
94
|
+
gitProvider;
|
|
95
|
+
config;
|
|
96
|
+
constructor(gitProvider, config){
|
|
97
|
+
this.gitProvider = gitProvider;
|
|
98
|
+
this.config = config;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 从配置和选项获取执行上下文
|
|
102
|
+
*/ getContextFromOptions(options) {
|
|
103
|
+
let owner;
|
|
104
|
+
let repo;
|
|
105
|
+
if (options.repository) {
|
|
106
|
+
const parts = options.repository.split("/");
|
|
107
|
+
if (parts.length !== 2) {
|
|
108
|
+
throw new Error(`仓库格式不正确,期望 "owner/repo",实际: "${options.repository}"`);
|
|
109
|
+
}
|
|
110
|
+
owner = parts[0];
|
|
111
|
+
repo = parts[1];
|
|
112
|
+
} else {
|
|
113
|
+
const ciConf = this.config.get("ci");
|
|
114
|
+
const repository = ciConf?.repository;
|
|
115
|
+
if (!repository) {
|
|
116
|
+
throw new Error("缺少仓库配置,请通过 --repository 参数或环境变量 GITHUB_REPOSITORY / GITEA_REPOSITORY 指定");
|
|
117
|
+
}
|
|
118
|
+
const parts = repository.split("/");
|
|
119
|
+
owner = parts[0];
|
|
120
|
+
repo = parts[1];
|
|
121
|
+
}
|
|
122
|
+
if (options.ci) {
|
|
123
|
+
this.gitProvider.validateConfig();
|
|
124
|
+
}
|
|
125
|
+
const { since, until } = this.resolveDateRange(options);
|
|
126
|
+
if (since > until) {
|
|
127
|
+
throw new Error("开始日期不能晚于结束日期");
|
|
128
|
+
}
|
|
129
|
+
const output = options.output ?? "console";
|
|
130
|
+
if (output === "issue" && !options.ci) {
|
|
131
|
+
this.gitProvider.validateConfig();
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
owner,
|
|
135
|
+
repo,
|
|
136
|
+
since,
|
|
137
|
+
until,
|
|
138
|
+
format: options.format ?? (output === "console" ? "table" : "markdown"),
|
|
139
|
+
output,
|
|
140
|
+
outputFile: options.outputFile,
|
|
141
|
+
verbose: normalizeVerbose(options.verbose)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 执行周期统计
|
|
146
|
+
*/ async execute(context) {
|
|
147
|
+
const { owner, repo, since, until, verbose } = context;
|
|
148
|
+
if (shouldLog(verbose, 1)) {
|
|
149
|
+
console.log(`📊 开始统计 ${owner}/${repo} 的 PR 数据...`);
|
|
150
|
+
console.log(`📅 时间范围: ${this.formatDate(since)} ~ ${this.formatDate(until)}`);
|
|
151
|
+
}
|
|
152
|
+
const allPrs = await this.gitProvider.listAllPullRequests(owner, repo, {
|
|
153
|
+
state: "closed"
|
|
154
|
+
});
|
|
155
|
+
const mergedPrs = allPrs.filter((pr)=>{
|
|
156
|
+
if (!pr.merged_at) return false;
|
|
157
|
+
const mergedAt = new Date(pr.merged_at);
|
|
158
|
+
return mergedAt >= since && mergedAt <= until;
|
|
159
|
+
});
|
|
160
|
+
if (shouldLog(verbose, 1)) {
|
|
161
|
+
console.log(`📝 找到 ${mergedPrs.length} 个已合并的 PR`);
|
|
162
|
+
}
|
|
163
|
+
const prStatsList = [];
|
|
164
|
+
for (const pr of mergedPrs){
|
|
165
|
+
if (shouldLog(verbose, 1)) {
|
|
166
|
+
console.log(` 处理 PR #${pr.number}: ${pr.title}`);
|
|
167
|
+
}
|
|
168
|
+
const stats = await this.collectPrStats(owner, repo, pr);
|
|
169
|
+
prStatsList.push(stats);
|
|
170
|
+
}
|
|
171
|
+
const userStatsMap = this.aggregateByUser(prStatsList);
|
|
172
|
+
const sortedUserStats = this.sortUserStats(userStatsMap);
|
|
173
|
+
return {
|
|
174
|
+
period: {
|
|
175
|
+
since: this.formatDate(since),
|
|
176
|
+
until: this.formatDate(until)
|
|
177
|
+
},
|
|
178
|
+
repository: `${owner}/${repo}`,
|
|
179
|
+
totalPrs: mergedPrs.length,
|
|
180
|
+
userStats: sortedUserStats
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 收集单个 PR 的统计数据
|
|
185
|
+
*/ async collectPrStats(owner, repo, pr) {
|
|
186
|
+
let additions = 0;
|
|
187
|
+
let deletions = 0;
|
|
188
|
+
let changedFiles = 0;
|
|
189
|
+
const includes = this.resolveIncludes();
|
|
190
|
+
try {
|
|
191
|
+
const allFiles = await this.gitProvider.getPullRequestFiles(owner, repo, pr.number);
|
|
192
|
+
const files = includes.length > 0 ? allFiles.filter((f)=>micromatch.isMatch(f.filename ?? "", includes, {
|
|
193
|
+
matchBase: true
|
|
194
|
+
})) : allFiles;
|
|
195
|
+
changedFiles = files.length;
|
|
196
|
+
for (const file of files){
|
|
197
|
+
additions += file.additions ?? 0;
|
|
198
|
+
deletions += file.deletions ?? 0;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// 如果获取文件失败,使用默认值
|
|
202
|
+
}
|
|
203
|
+
const issueStats = await this.extractIssueStats(owner, repo, pr.number);
|
|
204
|
+
const validCommitCount = await this.countValidCommits(owner, repo, pr.number);
|
|
205
|
+
return {
|
|
206
|
+
number: pr.number,
|
|
207
|
+
title: pr.title ?? "",
|
|
208
|
+
author: pr.user?.login ?? "unknown",
|
|
209
|
+
mergedAt: pr.merged_at ?? "",
|
|
210
|
+
additions,
|
|
211
|
+
deletions,
|
|
212
|
+
changedFiles,
|
|
213
|
+
...issueStats,
|
|
214
|
+
validCommitCount,
|
|
215
|
+
description: this.extractDescription(pr)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 从 PR 评论中提取问题统计
|
|
220
|
+
*/ async extractIssueStats(owner, repo, prNumber) {
|
|
221
|
+
const empty = {
|
|
222
|
+
issueCount: 0,
|
|
223
|
+
fixedCount: 0,
|
|
224
|
+
errorCount: 0,
|
|
225
|
+
warnCount: 0,
|
|
226
|
+
fixedErrors: 0,
|
|
227
|
+
fixedWarns: 0
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
|
|
231
|
+
// 优先从 review 模块嵌入的结构化数据中精确提取
|
|
232
|
+
const formatter = new MarkdownFormatter();
|
|
233
|
+
for (const comment of comments){
|
|
234
|
+
const body = comment.body ?? "";
|
|
235
|
+
const parsed = formatter.parse(body);
|
|
236
|
+
if (parsed?.result?.issues) {
|
|
237
|
+
return this.computeIssueStatsFromReviewIssues(parsed.result.issues);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// 回退:没有结构化数据时,从评论文本正则提取(兼容旧数据)
|
|
241
|
+
return this.extractIssueStatsFromText(comments);
|
|
242
|
+
} catch {
|
|
243
|
+
return empty;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 从 ReviewIssue 列表中精确计算各类问题统计
|
|
248
|
+
*/ computeIssueStatsFromReviewIssues(issues) {
|
|
249
|
+
const errorCount = issues.filter((i)=>i.severity === "error").length;
|
|
250
|
+
const warnCount = issues.filter((i)=>i.severity === "warn").length;
|
|
251
|
+
const fixedErrors = issues.filter((i)=>i.severity === "error" && i.fixed).length;
|
|
252
|
+
const fixedWarns = issues.filter((i)=>i.severity === "warn" && i.fixed).length;
|
|
253
|
+
return {
|
|
254
|
+
issueCount: issues.length,
|
|
255
|
+
fixedCount: fixedErrors + fixedWarns,
|
|
256
|
+
errorCount,
|
|
257
|
+
warnCount,
|
|
258
|
+
fixedErrors,
|
|
259
|
+
fixedWarns
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 回退:从评论文本正则提取问题统计(兼容无结构化数据的旧评论)
|
|
264
|
+
*/ extractIssueStatsFromText(comments) {
|
|
265
|
+
let issueCount = 0;
|
|
266
|
+
let fixedCount = 0;
|
|
267
|
+
let errorCount = 0;
|
|
268
|
+
let warnCount = 0;
|
|
269
|
+
for (const comment of comments){
|
|
270
|
+
const body = comment.body ?? "";
|
|
271
|
+
const issueMatch = body.match(/发现\s*(\d+)\s*个问题/);
|
|
272
|
+
if (issueMatch) {
|
|
273
|
+
issueCount = Math.max(issueCount, parseInt(issueMatch[1], 10));
|
|
274
|
+
}
|
|
275
|
+
const fixedMatch = body.match(/已修复[::]\s*(\d+)/);
|
|
276
|
+
if (fixedMatch) {
|
|
277
|
+
fixedCount = Math.max(fixedCount, parseInt(fixedMatch[1], 10));
|
|
278
|
+
}
|
|
279
|
+
const statsMatch = body.match(/🔴\s*(\d+).*🟡\s*(\d+)/);
|
|
280
|
+
if (statsMatch) {
|
|
281
|
+
errorCount = Math.max(errorCount, parseInt(statsMatch[1], 10));
|
|
282
|
+
warnCount = Math.max(warnCount, parseInt(statsMatch[2], 10));
|
|
283
|
+
issueCount = Math.max(issueCount, errorCount + warnCount);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// 文本模式无法区分修复类型,统一设为 0
|
|
287
|
+
return {
|
|
288
|
+
issueCount,
|
|
289
|
+
fixedCount,
|
|
290
|
+
errorCount,
|
|
291
|
+
warnCount,
|
|
292
|
+
fixedErrors: 0,
|
|
293
|
+
fixedWarns: 0
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 统计 PR 中有效 commit 数量(逐 commit 获取行数判断)
|
|
298
|
+
*/ async countValidCommits(owner, repo, prNumber) {
|
|
299
|
+
const config = this.getStrategyConfig();
|
|
300
|
+
const minLines = config.commitBasedWeights?.minCommitLines ?? DEFAULT_COMMIT_BASED_WEIGHTS.minCommitLines;
|
|
301
|
+
try {
|
|
302
|
+
const commits = await this.gitProvider.getPullRequestCommits(owner, repo, prNumber);
|
|
303
|
+
let validCount = 0;
|
|
304
|
+
for (const commit of commits){
|
|
305
|
+
if (!commit.sha) continue;
|
|
306
|
+
// 跳过 merge commit(commit message 以 "Merge" 开头)
|
|
307
|
+
if (commit.commit?.message?.startsWith("Merge")) continue;
|
|
308
|
+
try {
|
|
309
|
+
const commitInfo = await this.gitProvider.getCommit(owner, repo, commit.sha);
|
|
310
|
+
const totalLines = (commitInfo.files ?? []).reduce((sum, file)=>sum + (file.additions ?? 0) + (file.deletions ?? 0), 0);
|
|
311
|
+
if (totalLines >= minLines) {
|
|
312
|
+
validCount++;
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
// 获取单个 commit 失败,跳过
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return validCount;
|
|
319
|
+
} catch {
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* 从 PR 提取功能描述
|
|
325
|
+
*/ extractDescription(pr) {
|
|
326
|
+
if (pr.title) {
|
|
327
|
+
return pr.title.replace(/^\[.*?\]\s*/, "").trim();
|
|
328
|
+
}
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 按用户聚合统计数据
|
|
333
|
+
*/ aggregateByUser(prStatsList) {
|
|
334
|
+
const userMap = new Map();
|
|
335
|
+
for (const pr of prStatsList){
|
|
336
|
+
let userStats = userMap.get(pr.author);
|
|
337
|
+
if (!userStats) {
|
|
338
|
+
userStats = {
|
|
339
|
+
username: pr.author,
|
|
340
|
+
prCount: 0,
|
|
341
|
+
totalAdditions: 0,
|
|
342
|
+
totalDeletions: 0,
|
|
343
|
+
totalChangedFiles: 0,
|
|
344
|
+
totalIssues: 0,
|
|
345
|
+
totalFixed: 0,
|
|
346
|
+
totalErrors: 0,
|
|
347
|
+
totalWarns: 0,
|
|
348
|
+
totalFixedErrors: 0,
|
|
349
|
+
totalFixedWarns: 0,
|
|
350
|
+
totalValidCommits: 0,
|
|
351
|
+
score: 0,
|
|
352
|
+
features: [],
|
|
353
|
+
prs: []
|
|
354
|
+
};
|
|
355
|
+
userMap.set(pr.author, userStats);
|
|
356
|
+
}
|
|
357
|
+
userStats.prCount++;
|
|
358
|
+
userStats.totalAdditions += pr.additions;
|
|
359
|
+
userStats.totalDeletions += pr.deletions;
|
|
360
|
+
userStats.totalChangedFiles += pr.changedFiles;
|
|
361
|
+
userStats.totalIssues += pr.issueCount;
|
|
362
|
+
userStats.totalFixed += pr.fixedCount;
|
|
363
|
+
userStats.totalErrors += pr.errorCount;
|
|
364
|
+
userStats.totalWarns += pr.warnCount;
|
|
365
|
+
userStats.totalFixedErrors += pr.fixedErrors;
|
|
366
|
+
userStats.totalFixedWarns += pr.fixedWarns;
|
|
367
|
+
userStats.totalValidCommits += pr.validCommitCount;
|
|
368
|
+
if (pr.description) {
|
|
369
|
+
userStats.features.push(pr.description);
|
|
370
|
+
}
|
|
371
|
+
userStats.prs.push(pr);
|
|
372
|
+
}
|
|
373
|
+
this.applyScoreStrategy(userMap);
|
|
374
|
+
return userMap;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* 解析文件过滤 glob 模式:优先使用 review-summary.includes,fallback 到 review.includes
|
|
378
|
+
*/ resolveIncludes() {
|
|
379
|
+
const config = this.getStrategyConfig();
|
|
380
|
+
if (config.includes && config.includes.length > 0) {
|
|
381
|
+
return config.includes;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const reviewConfig = this.config.get("review");
|
|
385
|
+
return reviewConfig?.includes ?? [];
|
|
386
|
+
} catch {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 获取当前评分策略配置
|
|
392
|
+
*/ getStrategyConfig() {
|
|
393
|
+
try {
|
|
394
|
+
return this.config.get("review-summary") ?? {};
|
|
395
|
+
} catch {
|
|
396
|
+
return {};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* 根据配置的策略计算所有用户的分数
|
|
401
|
+
*/ applyScoreStrategy(userMap) {
|
|
402
|
+
const config = this.getStrategyConfig();
|
|
403
|
+
const strategy = config.strategy ?? "weighted";
|
|
404
|
+
for (const userStats of userMap.values()){
|
|
405
|
+
switch(strategy){
|
|
406
|
+
case "defect-rate":
|
|
407
|
+
{
|
|
408
|
+
const rate = this.calculateDefectRate(userStats, config);
|
|
409
|
+
userStats.defectRate = rate;
|
|
410
|
+
userStats.score = Math.round((100 - rate) * 10) / 10;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case "issue-based":
|
|
414
|
+
userStats.score = this.calculateIssueBasedScore(userStats, config);
|
|
415
|
+
break;
|
|
416
|
+
case "commit-based":
|
|
417
|
+
userStats.score = this.calculateCommitBasedScore(userStats, config);
|
|
418
|
+
break;
|
|
419
|
+
case "weighted":
|
|
420
|
+
default:
|
|
421
|
+
userStats.score = this.calculateWeightedScore(userStats, config);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* 加权模式:计算用户综合分数
|
|
428
|
+
*/ calculateWeightedScore(stats, config) {
|
|
429
|
+
const weights = {
|
|
430
|
+
...DEFAULT_WEIGHTED_WEIGHTS,
|
|
431
|
+
...config.scoreWeights
|
|
432
|
+
};
|
|
433
|
+
const prScore = stats.prCount * weights.prBase;
|
|
434
|
+
const additionsScore = stats.totalAdditions / 100 * weights.additionsPer100;
|
|
435
|
+
const deletionsScore = stats.totalDeletions / 100 * weights.deletionsPer100;
|
|
436
|
+
const filesScore = stats.totalChangedFiles * weights.changedFile;
|
|
437
|
+
const unfixedIssues = stats.totalIssues - stats.totalFixed;
|
|
438
|
+
const issueDeduction = unfixedIssues * weights.issueDeduction;
|
|
439
|
+
const fixedBonus = stats.totalFixed * weights.fixedBonus;
|
|
440
|
+
const totalScore = prScore + additionsScore + deletionsScore + filesScore - issueDeduction + fixedBonus;
|
|
441
|
+
return Math.max(0, Math.round(totalScore * 10) / 10);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* issue-based 模式:逐 PR 计算基础分(对数缩放) + 问题扣分/修复加分
|
|
445
|
+
*/ calculateIssueBasedScore(stats, config) {
|
|
446
|
+
const weights = {
|
|
447
|
+
...DEFAULT_ISSUE_BASED_WEIGHTS,
|
|
448
|
+
...config.issueBasedWeights
|
|
449
|
+
};
|
|
450
|
+
let totalScore = 0;
|
|
451
|
+
for (const pr of stats.prs){
|
|
452
|
+
const baseScore = this.calculatePrBaseScore(pr.additions + pr.deletions, weights.minBase, weights.maxBase, weights.capLines);
|
|
453
|
+
const deduction = pr.errorCount * weights.errorDeduction + pr.warnCount * weights.warnDeduction;
|
|
454
|
+
const bonus = pr.fixedErrors * weights.errorFixedBonus + pr.fixedWarns * weights.warnFixedBonus;
|
|
455
|
+
totalScore += Math.max(0, baseScore - deduction + bonus);
|
|
456
|
+
}
|
|
457
|
+
return Math.round(totalScore * 10) / 10;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 计算单个 PR 的基础分(对数缩放,映射到 [minBase, maxBase] 区间)
|
|
461
|
+
*/ calculatePrBaseScore(totalLines, minBase, maxBase, capLines) {
|
|
462
|
+
if (totalLines <= 0) return minBase;
|
|
463
|
+
const ratio = Math.min(1, Math.log2(1 + totalLines) / Math.log2(1 + capLines));
|
|
464
|
+
return minBase + (maxBase - minBase) * ratio;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* defect-rate 模式:基于问题密度(每百行代码)计算缺陷率
|
|
468
|
+
*/ calculateDefectRate(stats, config) {
|
|
469
|
+
const weights = {
|
|
470
|
+
...DEFAULT_DEFECT_RATE_WEIGHTS,
|
|
471
|
+
...config.defectRateWeights
|
|
472
|
+
};
|
|
473
|
+
if (stats.prs.length === 0) return 0;
|
|
474
|
+
let complianceSum = 0;
|
|
475
|
+
let counted = 0;
|
|
476
|
+
for (const pr of stats.prs){
|
|
477
|
+
// 跳过 merge PR(标题以 "Merge" 开头),合并操作不纳入缺陷率统计
|
|
478
|
+
if (pr.title.startsWith("Merge")) continue;
|
|
479
|
+
const totalLines = pr.additions + pr.deletions;
|
|
480
|
+
const per100 = Math.max(1, totalLines / 100);
|
|
481
|
+
const penalty = (pr.errorCount * weights.errorPenalty + pr.warnCount * weights.warnPenalty) / per100;
|
|
482
|
+
const recovery = (pr.fixedErrors + pr.fixedWarns) * weights.fixedDiscount;
|
|
483
|
+
const compliance = Math.min(1, Math.max(0, 1 - penalty + recovery));
|
|
484
|
+
complianceSum += compliance;
|
|
485
|
+
counted++;
|
|
486
|
+
}
|
|
487
|
+
if (counted === 0) return 0;
|
|
488
|
+
const avgCompliance = complianceSum / counted;
|
|
489
|
+
return Math.round((1 - avgCompliance) * 1000) / 10;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* 分数累计模式:按有效 commit 加分,按 error/warn 扣分
|
|
493
|
+
*/ calculateCommitBasedScore(stats, config) {
|
|
494
|
+
const weights = {
|
|
495
|
+
...DEFAULT_COMMIT_BASED_WEIGHTS,
|
|
496
|
+
...config.commitBasedWeights
|
|
497
|
+
};
|
|
498
|
+
const commitScore = stats.totalValidCommits * weights.validCommit;
|
|
499
|
+
const errorDeduction = stats.totalErrors * weights.errorDeduction;
|
|
500
|
+
const warnDeduction = stats.totalWarns * weights.warnDeduction;
|
|
501
|
+
const fixedBonus = stats.totalFixedErrors * weights.errorFixedBonus + stats.totalFixedWarns * weights.warnFixedBonus;
|
|
502
|
+
const totalScore = commitScore - errorDeduction - warnDeduction + fixedBonus;
|
|
503
|
+
return Math.max(0, Math.round(totalScore * 10) / 10);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* 按分数排序用户统计
|
|
507
|
+
*/ sortUserStats(userMap) {
|
|
508
|
+
const config = this.getStrategyConfig();
|
|
509
|
+
if (config.strategy === "defect-rate") {
|
|
510
|
+
return Array.from(userMap.values()).sort((a, b)=>(a.defectRate ?? 0) - (b.defectRate ?? 0));
|
|
511
|
+
}
|
|
512
|
+
return Array.from(userMap.values()).sort((a, b)=>b.score - a.score);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 解析日期字符串
|
|
516
|
+
*/ parseDate(dateStr, fieldName) {
|
|
517
|
+
const date = new Date(dateStr);
|
|
518
|
+
if (isNaN(date.getTime())) {
|
|
519
|
+
throw new Error(`${fieldName}格式不正确: "${dateStr}",请使用 YYYY-MM-DD 格式`);
|
|
520
|
+
}
|
|
521
|
+
return date;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* 根据预设或手动输入解析日期范围
|
|
525
|
+
*/ resolveDateRange(options) {
|
|
526
|
+
if (options.preset) {
|
|
527
|
+
return this.resolvePresetDateRange(options.preset);
|
|
528
|
+
}
|
|
529
|
+
if (!options.since) {
|
|
530
|
+
throw new Error("请指定 --since 参数或使用 --preset 预设时间范围");
|
|
531
|
+
}
|
|
532
|
+
const since = this.parseDate(options.since, "开始日期");
|
|
533
|
+
const until = options.until ? this.parseDate(options.until, "结束日期") : new Date();
|
|
534
|
+
until.setHours(23, 59, 59, 999);
|
|
535
|
+
return {
|
|
536
|
+
since,
|
|
537
|
+
until
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* 根据预设解析日期范围
|
|
542
|
+
*/ resolvePresetDateRange(preset) {
|
|
543
|
+
const now = new Date();
|
|
544
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
545
|
+
let since;
|
|
546
|
+
let until;
|
|
547
|
+
switch(preset){
|
|
548
|
+
case "this-week":
|
|
549
|
+
{
|
|
550
|
+
const dayOfWeek = today.getDay();
|
|
551
|
+
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
|
552
|
+
since = new Date(today);
|
|
553
|
+
since.setDate(today.getDate() + mondayOffset);
|
|
554
|
+
until = new Date(today);
|
|
555
|
+
until.setHours(23, 59, 59, 999);
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
case "last-week":
|
|
559
|
+
{
|
|
560
|
+
const dayOfWeek = today.getDay();
|
|
561
|
+
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
|
562
|
+
until = new Date(today);
|
|
563
|
+
until.setDate(today.getDate() + mondayOffset - 1);
|
|
564
|
+
until.setHours(23, 59, 59, 999);
|
|
565
|
+
since = new Date(until);
|
|
566
|
+
since.setDate(until.getDate() - 6);
|
|
567
|
+
since.setHours(0, 0, 0, 0);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case "this-month":
|
|
571
|
+
{
|
|
572
|
+
since = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
573
|
+
until = new Date(today);
|
|
574
|
+
until.setHours(23, 59, 59, 999);
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
case "last-month":
|
|
578
|
+
{
|
|
579
|
+
since = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
580
|
+
until = new Date(today.getFullYear(), today.getMonth(), 0);
|
|
581
|
+
until.setHours(23, 59, 59, 999);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
case "last-7-days":
|
|
585
|
+
{
|
|
586
|
+
since = new Date(today);
|
|
587
|
+
since.setDate(today.getDate() - 6);
|
|
588
|
+
until = new Date(today);
|
|
589
|
+
until.setHours(23, 59, 59, 999);
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case "last-15-days":
|
|
593
|
+
{
|
|
594
|
+
since = new Date(today);
|
|
595
|
+
since.setDate(today.getDate() - 14);
|
|
596
|
+
until = new Date(today);
|
|
597
|
+
until.setHours(23, 59, 59, 999);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
case "last-30-days":
|
|
601
|
+
{
|
|
602
|
+
since = new Date(today);
|
|
603
|
+
since.setDate(today.getDate() - 29);
|
|
604
|
+
until = new Date(today);
|
|
605
|
+
until.setHours(23, 59, 59, 999);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
default:
|
|
609
|
+
throw new Error(`未知的时间预设: ${preset}`);
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
since,
|
|
613
|
+
until
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* 格式化日期为 YYYY-MM-DD
|
|
618
|
+
*/ formatDate(date) {
|
|
619
|
+
return date.toISOString().split("T")[0];
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* 格式化输出结果
|
|
623
|
+
*/ formatOutput(result, format) {
|
|
624
|
+
switch(format){
|
|
625
|
+
case "json":
|
|
626
|
+
return JSON.stringify(result, null, 2);
|
|
627
|
+
case "markdown":
|
|
628
|
+
return this.formatMarkdown(result);
|
|
629
|
+
case "table":
|
|
630
|
+
default:
|
|
631
|
+
return this.formatTable(result);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* 格式化为表格输出
|
|
636
|
+
*/ formatTable(result) {
|
|
637
|
+
const lines = [];
|
|
638
|
+
lines.push("");
|
|
639
|
+
lines.push(`📊 周期统计报告`);
|
|
640
|
+
lines.push(`${"─".repeat(60)}`);
|
|
641
|
+
lines.push(`📦 仓库: ${result.repository}`);
|
|
642
|
+
lines.push(`📅 周期: ${result.period.since} ~ ${result.period.until}`);
|
|
643
|
+
lines.push(`📝 合并 PR 数: ${result.totalPrs}`);
|
|
644
|
+
lines.push("");
|
|
645
|
+
lines.push(`🏆 贡献者排名`);
|
|
646
|
+
lines.push(`${"─".repeat(60)}`);
|
|
647
|
+
const isDefectRate = result.userStats.some((u)=>u.defectRate !== undefined);
|
|
648
|
+
const header = isDefectRate ? [
|
|
649
|
+
"排名".padEnd(4),
|
|
650
|
+
"用户".padEnd(15),
|
|
651
|
+
"PR数".padStart(5),
|
|
652
|
+
"新增".padStart(8),
|
|
653
|
+
"删除".padStart(8),
|
|
654
|
+
"问题".padStart(5),
|
|
655
|
+
"缺陷率".padStart(8)
|
|
656
|
+
].join(" │ ") : [
|
|
657
|
+
"排名".padEnd(4),
|
|
658
|
+
"用户".padEnd(15),
|
|
659
|
+
"PR数".padStart(5),
|
|
660
|
+
"新增".padStart(8),
|
|
661
|
+
"删除".padStart(8),
|
|
662
|
+
"问题".padStart(5),
|
|
663
|
+
"分数".padStart(8)
|
|
664
|
+
].join(" │ ");
|
|
665
|
+
lines.push(header);
|
|
666
|
+
lines.push("─".repeat(60));
|
|
667
|
+
result.userStats.forEach((user, index)=>{
|
|
668
|
+
const lastCol = isDefectRate ? `${(user.defectRate ?? 0).toFixed(1)}%`.padStart(8) : user.score.toFixed(1).padStart(8);
|
|
669
|
+
const row = [
|
|
670
|
+
`#${index + 1}`.padEnd(4),
|
|
671
|
+
user.username.slice(0, 15).padEnd(15),
|
|
672
|
+
String(user.prCount).padStart(5),
|
|
673
|
+
`+${user.totalAdditions}`.padStart(8),
|
|
674
|
+
`-${user.totalDeletions}`.padStart(8),
|
|
675
|
+
String(user.totalIssues).padStart(5),
|
|
676
|
+
lastCol
|
|
677
|
+
].join(" │ ");
|
|
678
|
+
lines.push(row);
|
|
679
|
+
});
|
|
680
|
+
lines.push("─".repeat(60));
|
|
681
|
+
lines.push("");
|
|
682
|
+
lines.push(`📋 功能摘要`);
|
|
683
|
+
lines.push(`${"─".repeat(60)}`);
|
|
684
|
+
for (const user of result.userStats){
|
|
685
|
+
if (user.features.length > 0) {
|
|
686
|
+
lines.push(`\n👤 ${user.username}:`);
|
|
687
|
+
for (const feature of user.features){
|
|
688
|
+
lines.push(` • ${feature}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
lines.push("");
|
|
693
|
+
return lines.join("\n");
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* 格式化为 Markdown 输出
|
|
697
|
+
*/ formatMarkdown(result) {
|
|
698
|
+
const lines = [];
|
|
699
|
+
lines.push(`# 📊 周期统计报告`);
|
|
700
|
+
lines.push("");
|
|
701
|
+
lines.push(`- **仓库**: ${result.repository}`);
|
|
702
|
+
lines.push(`- **周期**: ${result.period.since} ~ ${result.period.until}`);
|
|
703
|
+
lines.push(`- **合并 PR 数**: ${result.totalPrs}`);
|
|
704
|
+
lines.push("");
|
|
705
|
+
lines.push(`## 🏆 贡献者排名`);
|
|
706
|
+
lines.push("");
|
|
707
|
+
const isDefectRate = result.userStats.some((u)=>u.defectRate !== undefined);
|
|
708
|
+
if (isDefectRate) {
|
|
709
|
+
lines.push(`| 排名 | 用户 | PR数 | 新增 | 删除 | 问题 | 缺陷率 |`);
|
|
710
|
+
lines.push(`|------|------|------|------|------|------|--------|`);
|
|
711
|
+
result.userStats.forEach((user, index)=>{
|
|
712
|
+
lines.push(`| #${index + 1} | ${user.username} | ${user.prCount} | +${user.totalAdditions} | -${user.totalDeletions} | ${user.totalIssues} | ${(user.defectRate ?? 0).toFixed(1)}% |`);
|
|
713
|
+
});
|
|
714
|
+
} else {
|
|
715
|
+
lines.push(`| 排名 | 用户 | PR数 | 新增 | 删除 | 问题 | 分数 |`);
|
|
716
|
+
lines.push(`|------|------|------|------|------|------|------|`);
|
|
717
|
+
result.userStats.forEach((user, index)=>{
|
|
718
|
+
lines.push(`| #${index + 1} | ${user.username} | ${user.prCount} | +${user.totalAdditions} | -${user.totalDeletions} | ${user.totalIssues} | ${user.score.toFixed(1)} |`);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
lines.push("");
|
|
722
|
+
lines.push(`## 📋 功能摘要`);
|
|
723
|
+
lines.push("");
|
|
724
|
+
for (const user of result.userStats){
|
|
725
|
+
if (user.features.length > 0) {
|
|
726
|
+
lines.push(`### 👤 ${user.username}`);
|
|
727
|
+
lines.push("");
|
|
728
|
+
for (const feature of user.features){
|
|
729
|
+
lines.push(`- ${feature}`);
|
|
730
|
+
}
|
|
731
|
+
lines.push("");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return lines.join("\n");
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* 输出报告到指定目标
|
|
738
|
+
*/ async outputReport(context, result) {
|
|
739
|
+
const content = this.formatOutput(result, context.format);
|
|
740
|
+
switch(context.output){
|
|
741
|
+
case "issue":
|
|
742
|
+
return this.outputToIssue(context, result, content);
|
|
743
|
+
case "file":
|
|
744
|
+
return this.outputToFile(context, result, content);
|
|
745
|
+
case "console":
|
|
746
|
+
default:
|
|
747
|
+
console.log(content);
|
|
748
|
+
return {
|
|
749
|
+
type: "console"
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* 输出报告到 GitHub Issue
|
|
755
|
+
*/ async outputToIssue(context, result, content) {
|
|
756
|
+
const title = `📊 周期统计报告: ${result.period.since} ~ ${result.period.until}`;
|
|
757
|
+
const config = this.getStrategyConfig();
|
|
758
|
+
const labelName = config.issueLabel ?? "report";
|
|
759
|
+
const issue = await this.gitProvider.createIssue(context.owner, context.repo, {
|
|
760
|
+
title,
|
|
761
|
+
body: content,
|
|
762
|
+
labels: [
|
|
763
|
+
labelName
|
|
764
|
+
]
|
|
765
|
+
});
|
|
766
|
+
const location = issue.html_url ?? `#${issue.number}`;
|
|
767
|
+
if (shouldLog(context.verbose, 1)) {
|
|
768
|
+
console.log(`✅ 已创建 Issue: ${location}`);
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
type: "issue",
|
|
772
|
+
location
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* 输出报告到 Markdown 文件
|
|
777
|
+
*/ outputToFile(context, result, content) {
|
|
778
|
+
const filename = context.outputFile ?? `period-summary-${result.period.since}-${result.period.until}.md`;
|
|
779
|
+
const filepath = join(process.cwd(), filename);
|
|
780
|
+
writeFileSync(filepath, content, "utf-8");
|
|
781
|
+
if (shouldLog(context.verbose, 1)) {
|
|
782
|
+
console.log(`✅ 已保存到文件: ${filepath}`);
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
type: "file",
|
|
786
|
+
location: filepath
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
43
791
|
;// CONCATENATED MODULE: ./src/index.ts
|
|
44
792
|
|
|
45
793
|
|
|
46
794
|
|
|
795
|
+
|
|
796
|
+
|
|
47
797
|
const extension = defineExtension({
|
|
48
798
|
name: "review-summary",
|
|
49
799
|
version: "1.0.0",
|
|
@@ -55,13 +805,78 @@ const extension = defineExtension({
|
|
|
55
805
|
description: t("review-summary:description"),
|
|
56
806
|
options: [
|
|
57
807
|
{
|
|
58
|
-
flags: "-
|
|
59
|
-
description: "
|
|
808
|
+
flags: "-p, --preset <preset>",
|
|
809
|
+
description: t("review-summary:options.preset")
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
flags: "-s, --since <date>",
|
|
813
|
+
description: t("review-summary:options.since")
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
flags: "-u, --until <date>",
|
|
817
|
+
description: t("review-summary:options.until")
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
flags: "-r, --repository <repo>",
|
|
821
|
+
description: t("review-summary:options.repository")
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
flags: "-f, --format <format>",
|
|
825
|
+
description: t("review-summary:options.format")
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
flags: "-o, --output <target>",
|
|
829
|
+
description: t("review-summary:options.output")
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
flags: "--output-file <path>",
|
|
833
|
+
description: t("review-summary:options.outputFile")
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
flags: "-c, --ci",
|
|
837
|
+
description: t("common.options.ci")
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
flags: "-v, --verbose",
|
|
841
|
+
description: t("review-summary:options.verbose"),
|
|
842
|
+
isCount: true
|
|
60
843
|
}
|
|
61
844
|
],
|
|
62
|
-
run: async (
|
|
63
|
-
ctx.
|
|
64
|
-
|
|
845
|
+
run: async (_args, options, ctx)=>{
|
|
846
|
+
if (!ctx.hasService("gitProvider")) {
|
|
847
|
+
ctx.output.error("review-summary 命令需要配置 Git Provider,请在 spaceflow.json 中配置 gitProvider 字段");
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
const gitProvider = ctx.getService("gitProvider");
|
|
851
|
+
const service = new PeriodSummaryService(gitProvider, ctx.config);
|
|
852
|
+
const summaryOptions = {
|
|
853
|
+
preset: options?.preset,
|
|
854
|
+
since: options?.since,
|
|
855
|
+
until: options?.until,
|
|
856
|
+
repository: options?.repository,
|
|
857
|
+
format: options?.format,
|
|
858
|
+
output: options?.output,
|
|
859
|
+
outputFile: options?.outputFile,
|
|
860
|
+
ci: !!options?.ci,
|
|
861
|
+
verbose: parseVerbose(options?.verbose)
|
|
862
|
+
};
|
|
863
|
+
try {
|
|
864
|
+
const context = service.getContextFromOptions(summaryOptions);
|
|
865
|
+
const result = await service.execute(context);
|
|
866
|
+
const report = await service.outputReport(context, result);
|
|
867
|
+
if (report.location) {
|
|
868
|
+
ctx.output.success(t("review-summary:reportOutput", {
|
|
869
|
+
location: report.location
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (error instanceof Error) {
|
|
874
|
+
ctx.output.error(error.message);
|
|
875
|
+
} else {
|
|
876
|
+
ctx.output.error(String(error));
|
|
877
|
+
}
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
65
880
|
}
|
|
66
881
|
}
|
|
67
882
|
]
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import "./locales";
|
|
2
2
|
import { defineExtension } from "@spaceflow/core";
|
|
3
3
|
import { t } from "@spaceflow/core";
|
|
4
|
+
import type { GitProviderService } from "@spaceflow/core";
|
|
5
|
+
import { parseVerbose } from "@spaceflow/core";
|
|
6
|
+
import { PeriodSummaryService } from "./review-summary.service";
|
|
7
|
+
import type { PeriodSummaryOptions, TimePreset, OutputTarget } from "./types";
|
|
4
8
|
|
|
5
9
|
export const extension = defineExtension({
|
|
6
10
|
name: "review-summary",
|
|
@@ -13,13 +17,81 @@ export const extension = defineExtension({
|
|
|
13
17
|
description: t("review-summary:description"),
|
|
14
18
|
options: [
|
|
15
19
|
{
|
|
16
|
-
flags: "-
|
|
17
|
-
description: "
|
|
20
|
+
flags: "-p, --preset <preset>",
|
|
21
|
+
description: t("review-summary:options.preset"),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
flags: "-s, --since <date>",
|
|
25
|
+
description: t("review-summary:options.since"),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
flags: "-u, --until <date>",
|
|
29
|
+
description: t("review-summary:options.until"),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
flags: "-r, --repository <repo>",
|
|
33
|
+
description: t("review-summary:options.repository"),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
flags: "-f, --format <format>",
|
|
37
|
+
description: t("review-summary:options.format"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
flags: "-o, --output <target>",
|
|
41
|
+
description: t("review-summary:options.output"),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
flags: "--output-file <path>",
|
|
45
|
+
description: t("review-summary:options.outputFile"),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
flags: "-c, --ci",
|
|
49
|
+
description: t("common.options.ci"),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
flags: "-v, --verbose",
|
|
53
|
+
description: t("review-summary:options.verbose"),
|
|
54
|
+
isCount: true,
|
|
18
55
|
},
|
|
19
56
|
],
|
|
20
|
-
run: async (
|
|
21
|
-
ctx.
|
|
22
|
-
|
|
57
|
+
run: async (_args, options, ctx) => {
|
|
58
|
+
if (!ctx.hasService("gitProvider")) {
|
|
59
|
+
ctx.output.error(
|
|
60
|
+
"review-summary 命令需要配置 Git Provider,请在 spaceflow.json 中配置 gitProvider 字段",
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const gitProvider = ctx.getService<GitProviderService>("gitProvider");
|
|
66
|
+
const service = new PeriodSummaryService(gitProvider, ctx.config);
|
|
67
|
+
|
|
68
|
+
const summaryOptions: PeriodSummaryOptions = {
|
|
69
|
+
preset: options?.preset as TimePreset,
|
|
70
|
+
since: options?.since as string,
|
|
71
|
+
until: options?.until as string,
|
|
72
|
+
repository: options?.repository as string,
|
|
73
|
+
format: options?.format as "table" | "json" | "markdown",
|
|
74
|
+
output: options?.output as OutputTarget,
|
|
75
|
+
outputFile: options?.outputFile as string,
|
|
76
|
+
ci: !!options?.ci,
|
|
77
|
+
verbose: parseVerbose(options?.verbose as string | boolean | undefined),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const context = service.getContextFromOptions(summaryOptions);
|
|
82
|
+
const result = await service.execute(context);
|
|
83
|
+
const report = await service.outputReport(context, result);
|
|
84
|
+
if (report.location) {
|
|
85
|
+
ctx.output.success(t("review-summary:reportOutput", { location: report.location }));
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof Error) {
|
|
89
|
+
ctx.output.error(error.message);
|
|
90
|
+
} else {
|
|
91
|
+
ctx.output.error(String(error));
|
|
92
|
+
}
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
23
95
|
},
|
|
24
96
|
},
|
|
25
97
|
],
|
|
@@ -71,7 +71,7 @@ export class PeriodSummaryService {
|
|
|
71
71
|
/**
|
|
72
72
|
* 从配置和选项获取执行上下文
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
getContextFromOptions(options: PeriodSummaryOptions): PeriodSummaryContext {
|
|
75
75
|
let owner: string;
|
|
76
76
|
let repo: string;
|
|
77
77
|
if (options.repository) {
|