goodiffer 1.0.0 → 1.0.1
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/bin/goodiffer.js +92 -2
- package/package.json +4 -1
- package/src/commands/analyze.js +383 -97
- package/src/commands/developer.js +116 -0
- package/src/commands/history.js +120 -0
- package/src/commands/init.js +76 -86
- package/src/commands/report.js +138 -0
- package/src/commands/stats.js +139 -0
- package/src/index.js +9 -35
- package/src/prompts/report-prompt.js +187 -0
- package/src/prompts/review-prompt.js +26 -46
- package/src/services/database.js +524 -0
- package/src/services/git.js +200 -101
- package/src/services/report-generator.js +298 -0
- package/src/services/reporter.js +96 -97
- package/src/utils/config-store.js +6 -12
- package/src/utils/logger.js +33 -33
package/src/services/git.js
CHANGED
|
@@ -1,136 +1,235 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import simpleGit from 'simple-git';
|
|
2
|
+
import path from 'path';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
export class GitService {
|
|
5
|
+
constructor(basePath = process.cwd()) {
|
|
6
|
+
this.git = simpleGit(basePath);
|
|
7
|
+
this.basePath = basePath;
|
|
8
|
+
}
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
async isGitRepo() {
|
|
11
|
+
try {
|
|
12
|
+
await this.git.status();
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
12
17
|
}
|
|
13
|
-
}
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
sha: log.latest.hash,
|
|
21
|
-
message: log.latest.message,
|
|
22
|
-
author: log.latest.author_name,
|
|
23
|
-
date: log.latest.date
|
|
24
|
-
};
|
|
19
|
+
async getLastCommitInfo() {
|
|
20
|
+
const log = await this.git.log({ maxCount: 1 });
|
|
21
|
+
if (!log.latest) {
|
|
22
|
+
throw new Error('无法获取 commit 信息');
|
|
25
23
|
}
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
return {
|
|
25
|
+
sha: log.latest.hash,
|
|
26
|
+
message: log.latest.message
|
|
27
|
+
};
|
|
30
28
|
}
|
|
31
|
-
}
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
async getCommitInfo(sha) {
|
|
31
|
+
// 使用 raw 命令获取特定 commit 信息
|
|
32
|
+
try {
|
|
33
|
+
const result = await this.git.raw(['log', '-1', '--format=%H%n%s', sha]);
|
|
34
|
+
const lines = result.trim().split('\n');
|
|
35
|
+
if (lines.length < 2) {
|
|
36
|
+
throw new Error(`无法获取 commit ${sha} 的信息`);
|
|
37
|
+
}
|
|
37
38
|
return {
|
|
38
|
-
sha:
|
|
39
|
-
message:
|
|
40
|
-
author: log.latest.author_name,
|
|
41
|
-
date: log.latest.date
|
|
39
|
+
sha: lines[0],
|
|
40
|
+
message: lines.slice(1).join('\n')
|
|
42
41
|
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`无法获取 commit ${sha} 的信息`);
|
|
43
44
|
}
|
|
44
|
-
return null;
|
|
45
|
-
} catch (error) {
|
|
46
|
-
logger.error(`获取 commit ${sha} 信息失败: ${error.message}`);
|
|
47
|
-
return null;
|
|
48
45
|
}
|
|
49
|
-
}
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
async getLastCommitDiff() {
|
|
48
|
+
const diff = await this.git.diff(['HEAD~1', 'HEAD']);
|
|
49
|
+
return diff;
|
|
50
|
+
}
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
async getCommitDiff(sha) {
|
|
53
|
+
const diff = await this.git.diff([`${sha}~1`, sha]);
|
|
54
|
+
return diff;
|
|
55
|
+
}
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
async getStagedDiff() {
|
|
58
|
+
const diff = await this.git.diff(['--cached']);
|
|
59
|
+
return diff;
|
|
60
|
+
}
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
async getRangeDiff(from, to) {
|
|
63
|
+
const diff = await this.git.diff([from, to]);
|
|
64
|
+
return diff;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getChangedFiles() {
|
|
68
|
+
const status = await this.git.status();
|
|
69
|
+
return {
|
|
70
|
+
staged: status.staged,
|
|
71
|
+
modified: status.modified,
|
|
72
|
+
created: status.created,
|
|
73
|
+
deleted: status.deleted
|
|
74
|
+
};
|
|
75
|
+
}
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
77
|
+
// 获取 commit 的 author 信息
|
|
78
|
+
async getCommitAuthor(sha) {
|
|
79
|
+
if (!sha || sha === 'staged') {
|
|
80
|
+
// 对于暂存区,使用当前用户配置
|
|
81
|
+
const config = await this.git.raw(['config', 'user.name']);
|
|
82
|
+
const email = await this.git.raw(['config', 'user.email']);
|
|
83
|
+
return {
|
|
84
|
+
name: config.trim(),
|
|
85
|
+
email: email.trim(),
|
|
86
|
+
date: new Date().toISOString()
|
|
87
|
+
};
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
// 使用 raw 命令获取特定 commit 的作者信息
|
|
91
|
+
try {
|
|
92
|
+
const result = await this.git.raw([
|
|
93
|
+
'log', '-1',
|
|
94
|
+
'--format=%an%n%ae%n%aI',
|
|
95
|
+
sha
|
|
96
|
+
]);
|
|
97
|
+
const lines = result.trim().split('\n');
|
|
98
|
+
if (lines.length < 3) {
|
|
99
|
+
throw new Error(`无法获取 commit ${sha} 的作者信息`);
|
|
85
100
|
}
|
|
101
|
+
return {
|
|
102
|
+
name: lines[0],
|
|
103
|
+
email: lines[1],
|
|
104
|
+
date: lines[2]
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`无法获取 commit ${sha} 的作者信息`);
|
|
86
108
|
}
|
|
87
|
-
logger.error(`获取 diff 失败: ${error.message}`);
|
|
88
|
-
return '';
|
|
89
109
|
}
|
|
90
|
-
}
|
|
91
110
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
111
|
+
// 获取最近 commit 的 author 信息
|
|
112
|
+
async getLastCommitAuthor() {
|
|
113
|
+
const log = await this.git.log({ maxCount: 1 });
|
|
114
|
+
if (!log.latest) {
|
|
115
|
+
throw new Error('无法获取 commit 作者信息');
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
name: log.latest.author_name,
|
|
119
|
+
email: log.latest.author_email,
|
|
120
|
+
date: log.latest.date
|
|
121
|
+
};
|
|
122
|
+
}
|
|
95
123
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
124
|
+
// 获取项目名称 (从 remote 或目录名)
|
|
125
|
+
async getProjectName() {
|
|
126
|
+
try {
|
|
127
|
+
const remotes = await this.git.getRemotes(true);
|
|
128
|
+
const origin = remotes.find(r => r.name === 'origin');
|
|
129
|
+
if (origin && origin.refs && origin.refs.fetch) {
|
|
130
|
+
// 从 URL 提取项目名
|
|
131
|
+
const url = origin.refs.fetch;
|
|
132
|
+
const match = url.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
133
|
+
if (match) return match[1];
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// 忽略错误
|
|
99
137
|
}
|
|
138
|
+
// 回退到目录名
|
|
139
|
+
return path.basename(this.basePath);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 获取 diff 统计
|
|
143
|
+
async getDiffStats(from, to) {
|
|
144
|
+
try {
|
|
145
|
+
let args = [];
|
|
146
|
+
if (from && to) {
|
|
147
|
+
args = [from, to];
|
|
148
|
+
} else if (from) {
|
|
149
|
+
args = [from];
|
|
150
|
+
} else {
|
|
151
|
+
args = ['HEAD~1', 'HEAD'];
|
|
152
|
+
}
|
|
100
153
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
154
|
+
const summary = await this.git.diffSummary(args);
|
|
155
|
+
return {
|
|
156
|
+
filesChanged: summary.changed || 0,
|
|
157
|
+
insertions: summary.insertions || 0,
|
|
158
|
+
deletions: summary.deletions || 0
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
return {
|
|
162
|
+
filesChanged: 0,
|
|
163
|
+
insertions: 0,
|
|
164
|
+
deletions: 0
|
|
165
|
+
};
|
|
104
166
|
}
|
|
167
|
+
}
|
|
105
168
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
169
|
+
// 获取暂存区的 diff 统计
|
|
170
|
+
async getStagedDiffStats() {
|
|
171
|
+
try {
|
|
172
|
+
const summary = await this.git.diffSummary(['--cached']);
|
|
173
|
+
return {
|
|
174
|
+
filesChanged: summary.changed || 0,
|
|
175
|
+
insertions: summary.insertions || 0,
|
|
176
|
+
deletions: summary.deletions || 0
|
|
177
|
+
};
|
|
178
|
+
} catch {
|
|
179
|
+
return {
|
|
180
|
+
filesChanged: 0,
|
|
181
|
+
insertions: 0,
|
|
182
|
+
deletions: 0
|
|
183
|
+
};
|
|
109
184
|
}
|
|
185
|
+
}
|
|
110
186
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
return
|
|
187
|
+
// 获取当前分支
|
|
188
|
+
async getCurrentBranch() {
|
|
189
|
+
try {
|
|
190
|
+
const branch = await this.git.branch();
|
|
191
|
+
return branch.current;
|
|
192
|
+
} catch {
|
|
193
|
+
return 'unknown';
|
|
116
194
|
}
|
|
195
|
+
}
|
|
117
196
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
197
|
+
// 获取最近 n 条 commits (按时间从新到旧排序)
|
|
198
|
+
async getRecentCommits(count) {
|
|
199
|
+
const log = await this.git.log({ maxCount: count });
|
|
200
|
+
return log.all.map(commit => ({
|
|
201
|
+
sha: commit.hash,
|
|
202
|
+
message: commit.message,
|
|
203
|
+
author: {
|
|
204
|
+
name: commit.author_name,
|
|
205
|
+
email: commit.author_email,
|
|
206
|
+
date: commit.date
|
|
207
|
+
}
|
|
208
|
+
}));
|
|
122
209
|
}
|
|
123
|
-
}
|
|
124
210
|
|
|
125
|
-
|
|
126
|
-
|
|
211
|
+
// 获取指定范围的 commits (从第 start 条到第 end 条,1-based,按时间从新到旧)
|
|
212
|
+
async getCommitRange(start, end) {
|
|
213
|
+
// start 和 end 是 1-based 索引
|
|
214
|
+
// 例如 start=2, end=5 表示第 2、3、4、5 条 commit
|
|
215
|
+
const skip = start - 1;
|
|
216
|
+
const count = end - start + 1;
|
|
217
|
+
|
|
218
|
+
const log = await this.git.log({
|
|
219
|
+
maxCount: count,
|
|
220
|
+
'--skip': skip
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return log.all.map(commit => ({
|
|
224
|
+
sha: commit.hash,
|
|
225
|
+
message: commit.message,
|
|
226
|
+
author: {
|
|
227
|
+
name: commit.author_name,
|
|
228
|
+
email: commit.author_email,
|
|
229
|
+
date: commit.date
|
|
230
|
+
}
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
127
233
|
}
|
|
128
234
|
|
|
129
|
-
export default
|
|
130
|
-
isGitRepo,
|
|
131
|
-
getLastCommitInfo,
|
|
132
|
-
getCommitInfo,
|
|
133
|
-
getDiff,
|
|
134
|
-
getChangedFiles,
|
|
135
|
-
getStagedDiff
|
|
136
|
-
};
|
|
235
|
+
export default GitService;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import { getDatabase, getConfigDir } from './database.js';
|
|
5
|
+
import { AIClient } from './ai-client.js';
|
|
6
|
+
import { getConfig } from '../utils/config-store.js';
|
|
7
|
+
import { buildProjectReportPrompt, buildDeveloperReportPrompt } from '../prompts/report-prompt.js';
|
|
8
|
+
|
|
9
|
+
export class ReportGenerator {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.db = getDatabase();
|
|
12
|
+
this.config = getConfig();
|
|
13
|
+
this.aiClient = new AIClient(this.config);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 生成项目报告
|
|
17
|
+
async generateProjectReport(projectName, options = {}, onProgress) {
|
|
18
|
+
// 获取项目
|
|
19
|
+
const project = this.db.getProject(projectName);
|
|
20
|
+
if (!project) {
|
|
21
|
+
throw new Error(`项目 "${projectName}" 不存在`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 构建日期范围
|
|
25
|
+
const dateRange = this.buildDateRange(options);
|
|
26
|
+
|
|
27
|
+
// 收集数据
|
|
28
|
+
if (onProgress) onProgress('正在收集项目数据...');
|
|
29
|
+
const data = await this.gatherProjectData(project, dateRange);
|
|
30
|
+
|
|
31
|
+
// 构建提示词
|
|
32
|
+
if (onProgress) onProgress('正在生成报告...');
|
|
33
|
+
const prompt = buildProjectReportPrompt(data);
|
|
34
|
+
|
|
35
|
+
// 调用 AI 生成 HTML
|
|
36
|
+
const html = await this.aiClient.analyze(prompt);
|
|
37
|
+
|
|
38
|
+
// 保存报告
|
|
39
|
+
const outputPath = this.saveReport(html, projectName, options.output);
|
|
40
|
+
|
|
41
|
+
return outputPath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 生成开发者报告
|
|
45
|
+
async generateDeveloperReport(developerEmail, options = {}, onProgress) {
|
|
46
|
+
// 获取开发者
|
|
47
|
+
const developer = this.db.getDeveloper(developerEmail);
|
|
48
|
+
if (!developer) {
|
|
49
|
+
throw new Error(`开发者 "${developerEmail}" 不存在`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 构建日期范围
|
|
53
|
+
const dateRange = this.buildDateRange(options);
|
|
54
|
+
|
|
55
|
+
// 收集数据
|
|
56
|
+
if (onProgress) onProgress('正在收集开发者数据...');
|
|
57
|
+
const data = await this.gatherDeveloperData(developer, dateRange);
|
|
58
|
+
|
|
59
|
+
// 构建提示词
|
|
60
|
+
if (onProgress) onProgress('正在生成报告...');
|
|
61
|
+
const prompt = buildDeveloperReportPrompt(data);
|
|
62
|
+
|
|
63
|
+
// 调用 AI 生成 HTML
|
|
64
|
+
const html = await this.aiClient.analyze(prompt);
|
|
65
|
+
|
|
66
|
+
// 保存报告
|
|
67
|
+
const safeName = developerEmail.replace(/[@.]/g, '_');
|
|
68
|
+
const outputPath = this.saveReport(html, `developer_${safeName}`, options.output);
|
|
69
|
+
|
|
70
|
+
return outputPath;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 构建日期范围
|
|
74
|
+
buildDateRange(options) {
|
|
75
|
+
const dateRange = {
|
|
76
|
+
start: options.since ? dayjs(options.since).format('YYYY-MM-DD') : dayjs().subtract(30, 'day').format('YYYY-MM-DD'),
|
|
77
|
+
end: options.until ? dayjs(options.until).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD')
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...dateRange,
|
|
82
|
+
since: options.since ? dayjs(options.since).startOf('day').toISOString() : null,
|
|
83
|
+
until: options.until ? dayjs(options.until).endOf('day').toISOString() : null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 收集项目数据
|
|
88
|
+
async gatherProjectData(project, dateRange) {
|
|
89
|
+
const filters = {
|
|
90
|
+
projectId: project.id,
|
|
91
|
+
since: dateRange.since,
|
|
92
|
+
until: dateRange.until,
|
|
93
|
+
limit: 100
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// 获取统计数据
|
|
97
|
+
const stats = this.db.getProjectStats(project.id, {
|
|
98
|
+
since: dateRange.since,
|
|
99
|
+
until: dateRange.until
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// 获取开发者统计
|
|
103
|
+
const developerStatsRaw = this.db.getDeveloperStatsByProject(project.id, {
|
|
104
|
+
since: dateRange.since,
|
|
105
|
+
until: dateRange.until
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 获取最近的 reviews
|
|
109
|
+
const recentReviews = this.db.queryReviews({ ...filters, limit: 20 });
|
|
110
|
+
|
|
111
|
+
// 处理开发者统计
|
|
112
|
+
const developerStats = developerStatsRaw.map(dev => {
|
|
113
|
+
const topIssues = this.db.getDeveloperTopIssues(dev.id, 3);
|
|
114
|
+
const matchRate = dev.total_reviews > 0
|
|
115
|
+
? Math.round((dev.commit_match_count / dev.total_reviews) * 100)
|
|
116
|
+
: 0;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
displayName: dev.display_name,
|
|
120
|
+
email: dev.git_email,
|
|
121
|
+
team: dev.team,
|
|
122
|
+
totalReviews: dev.total_reviews,
|
|
123
|
+
insertions: dev.total_insertions || 0,
|
|
124
|
+
deletions: dev.total_deletions || 0,
|
|
125
|
+
errors: dev.total_errors || 0,
|
|
126
|
+
warnings: dev.total_warnings || 0,
|
|
127
|
+
commitMatchRate: matchRate,
|
|
128
|
+
topIssues: topIssues.map(i => ({
|
|
129
|
+
level: i.level,
|
|
130
|
+
file: i.file,
|
|
131
|
+
description: i.description
|
|
132
|
+
})),
|
|
133
|
+
topRisks: [] // 可以扩展添加风险统计
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
project: {
|
|
139
|
+
name: project.name,
|
|
140
|
+
path: project.path
|
|
141
|
+
},
|
|
142
|
+
dateRange: {
|
|
143
|
+
start: dateRange.start,
|
|
144
|
+
end: dateRange.end
|
|
145
|
+
},
|
|
146
|
+
stats: {
|
|
147
|
+
totalReviews: stats.total_reviews || 0,
|
|
148
|
+
totalInsertions: stats.total_insertions || 0,
|
|
149
|
+
totalDeletions: stats.total_deletions || 0,
|
|
150
|
+
totalErrors: stats.total_errors || 0,
|
|
151
|
+
totalWarnings: stats.total_warnings || 0,
|
|
152
|
+
totalInfos: stats.total_infos || 0,
|
|
153
|
+
totalRisks: stats.total_risks || 0
|
|
154
|
+
},
|
|
155
|
+
developers: developerStatsRaw.map(d => ({
|
|
156
|
+
displayName: d.display_name,
|
|
157
|
+
email: d.git_email
|
|
158
|
+
})),
|
|
159
|
+
developerStats,
|
|
160
|
+
recentReviews: recentReviews.map(r => ({
|
|
161
|
+
commitSha: r.commit_sha,
|
|
162
|
+
commitMessage: r.commit_message,
|
|
163
|
+
commitDate: dayjs(r.commit_date).format('YYYY-MM-DD HH:mm'),
|
|
164
|
+
developerName: r.developer_name,
|
|
165
|
+
summary: r.summary,
|
|
166
|
+
errorCount: r.error_count,
|
|
167
|
+
warningCount: r.warning_count
|
|
168
|
+
}))
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 收集开发者数据
|
|
173
|
+
async gatherDeveloperData(developer, dateRange) {
|
|
174
|
+
const filters = {
|
|
175
|
+
developerId: developer.id,
|
|
176
|
+
since: dateRange.since,
|
|
177
|
+
until: dateRange.until,
|
|
178
|
+
limit: 100
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// 获取统计数据
|
|
182
|
+
const stats = this.db.getDeveloperStats(developer.id, {
|
|
183
|
+
since: dateRange.since,
|
|
184
|
+
until: dateRange.until
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 获取 reviews
|
|
188
|
+
const reviews = this.db.queryReviews(filters);
|
|
189
|
+
|
|
190
|
+
// 获取问题分布
|
|
191
|
+
const issueDistribution = this.db.getIssueTypeDistribution({
|
|
192
|
+
developerId: developer.id,
|
|
193
|
+
since: dateRange.since,
|
|
194
|
+
until: dateRange.until
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 获取典型问题
|
|
198
|
+
const topIssues = this.db.getDeveloperTopIssues(developer.id, 5);
|
|
199
|
+
|
|
200
|
+
// 按项目分组统计
|
|
201
|
+
const projectStats = {};
|
|
202
|
+
reviews.forEach(r => {
|
|
203
|
+
if (!projectStats[r.project_name]) {
|
|
204
|
+
projectStats[r.project_name] = { name: r.project_name, commits: 0 };
|
|
205
|
+
}
|
|
206
|
+
projectStats[r.project_name].commits++;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 获取关联风险
|
|
210
|
+
const topRisks = [];
|
|
211
|
+
for (const review of reviews.slice(0, 10)) {
|
|
212
|
+
const risks = this.db.db.prepare('SELECT * FROM association_risks WHERE review_id = ? LIMIT 2').all(review.id);
|
|
213
|
+
risks.forEach(risk => {
|
|
214
|
+
topRisks.push({
|
|
215
|
+
changedFile: risk.changed_file,
|
|
216
|
+
relatedFiles: risk.related_files,
|
|
217
|
+
risk: risk.risk
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const matchRate = stats.total_reviews > 0
|
|
223
|
+
? Math.round((stats.commit_match_count / stats.total_reviews) * 100)
|
|
224
|
+
: 0;
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
developer: {
|
|
228
|
+
displayName: developer.display_name,
|
|
229
|
+
email: developer.git_email,
|
|
230
|
+
team: developer.team
|
|
231
|
+
},
|
|
232
|
+
dateRange: {
|
|
233
|
+
start: dateRange.start,
|
|
234
|
+
end: dateRange.end
|
|
235
|
+
},
|
|
236
|
+
projects: Object.values(projectStats),
|
|
237
|
+
stats: {
|
|
238
|
+
totalReviews: stats.total_reviews || 0,
|
|
239
|
+
totalInsertions: stats.total_insertions || 0,
|
|
240
|
+
totalDeletions: stats.total_deletions || 0,
|
|
241
|
+
totalErrors: stats.total_errors || 0,
|
|
242
|
+
totalWarnings: stats.total_warnings || 0,
|
|
243
|
+
commitMatchRate: matchRate
|
|
244
|
+
},
|
|
245
|
+
issueDistribution: issueDistribution.map(i => ({
|
|
246
|
+
type: i.type || 'unknown',
|
|
247
|
+
level: i.level,
|
|
248
|
+
count: i.count
|
|
249
|
+
})),
|
|
250
|
+
topIssues: topIssues.map(i => ({
|
|
251
|
+
level: i.level,
|
|
252
|
+
type: i.type,
|
|
253
|
+
file: i.file,
|
|
254
|
+
line: i.line,
|
|
255
|
+
description: i.description,
|
|
256
|
+
suggestion: i.suggestion
|
|
257
|
+
})),
|
|
258
|
+
topRisks: topRisks.slice(0, 5),
|
|
259
|
+
reviews: reviews.map(r => ({
|
|
260
|
+
projectName: r.project_name,
|
|
261
|
+
commitSha: r.commit_sha,
|
|
262
|
+
commitMessage: r.commit_message,
|
|
263
|
+
commitDate: dayjs(r.commit_date).format('YYYY-MM-DD HH:mm'),
|
|
264
|
+
summary: r.summary,
|
|
265
|
+
errorCount: r.error_count,
|
|
266
|
+
warningCount: r.warning_count,
|
|
267
|
+
infoCount: r.info_count
|
|
268
|
+
}))
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 保存报告
|
|
273
|
+
saveReport(html, name, customOutput) {
|
|
274
|
+
let outputPath;
|
|
275
|
+
|
|
276
|
+
if (customOutput) {
|
|
277
|
+
outputPath = path.resolve(customOutput);
|
|
278
|
+
} else {
|
|
279
|
+
const reportsDir = path.join(getConfigDir(), 'reports');
|
|
280
|
+
if (!fs.existsSync(reportsDir)) {
|
|
281
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
282
|
+
}
|
|
283
|
+
const filename = `${name}_${dayjs().format('YYYY-MM-DD_HH-mm')}.html`;
|
|
284
|
+
outputPath = path.join(reportsDir, filename);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 确保目录存在
|
|
288
|
+
const dir = path.dirname(outputPath);
|
|
289
|
+
if (!fs.existsSync(dir)) {
|
|
290
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fs.writeFileSync(outputPath, html, 'utf-8');
|
|
294
|
+
return outputPath;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export default ReportGenerator;
|