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
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
// 获取配置目录
|
|
7
|
+
function getConfigDir() {
|
|
8
|
+
const configDir = path.join(os.homedir(), '.config', 'goodiffer-nodejs');
|
|
9
|
+
if (!fs.existsSync(configDir)) {
|
|
10
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
return configDir;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class GoodifferDatabase {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.dbPath = path.join(getConfigDir(), 'goodiffer.db');
|
|
18
|
+
this.db = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
init() {
|
|
22
|
+
this.db = new Database(this.dbPath);
|
|
23
|
+
this.db.pragma('journal_mode = WAL');
|
|
24
|
+
this.createTables();
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
createTables() {
|
|
29
|
+
// 项目表
|
|
30
|
+
this.db.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
32
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
name TEXT NOT NULL UNIQUE,
|
|
34
|
+
path TEXT NOT NULL,
|
|
35
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
37
|
+
)
|
|
38
|
+
`);
|
|
39
|
+
|
|
40
|
+
// 开发者表
|
|
41
|
+
this.db.exec(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS developers (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
git_email TEXT NOT NULL UNIQUE,
|
|
45
|
+
git_name TEXT NOT NULL,
|
|
46
|
+
display_name TEXT,
|
|
47
|
+
team TEXT,
|
|
48
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
49
|
+
)
|
|
50
|
+
`);
|
|
51
|
+
|
|
52
|
+
// 开发者别名映射表
|
|
53
|
+
this.db.exec(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS developer_aliases (
|
|
55
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
email_pattern TEXT NOT NULL,
|
|
57
|
+
developer_id INTEGER NOT NULL,
|
|
58
|
+
FOREIGN KEY (developer_id) REFERENCES developers(id)
|
|
59
|
+
)
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
// Code Review 记录表
|
|
63
|
+
this.db.exec(`
|
|
64
|
+
CREATE TABLE IF NOT EXISTS reviews (
|
|
65
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
66
|
+
project_id INTEGER NOT NULL,
|
|
67
|
+
developer_id INTEGER NOT NULL,
|
|
68
|
+
commit_sha TEXT NOT NULL,
|
|
69
|
+
commit_message TEXT NOT NULL,
|
|
70
|
+
commit_date DATETIME NOT NULL,
|
|
71
|
+
branch TEXT,
|
|
72
|
+
review_type TEXT NOT NULL,
|
|
73
|
+
from_sha TEXT,
|
|
74
|
+
to_sha TEXT,
|
|
75
|
+
files_changed INTEGER DEFAULT 0,
|
|
76
|
+
insertions INTEGER DEFAULT 0,
|
|
77
|
+
deletions INTEGER DEFAULT 0,
|
|
78
|
+
diff_content TEXT,
|
|
79
|
+
ai_response TEXT NOT NULL,
|
|
80
|
+
summary TEXT,
|
|
81
|
+
commit_match INTEGER,
|
|
82
|
+
commit_match_reason TEXT,
|
|
83
|
+
error_count INTEGER DEFAULT 0,
|
|
84
|
+
warning_count INTEGER DEFAULT 0,
|
|
85
|
+
info_count INTEGER DEFAULT 0,
|
|
86
|
+
risk_count INTEGER DEFAULT 0,
|
|
87
|
+
model_used TEXT,
|
|
88
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
89
|
+
FOREIGN KEY (project_id) REFERENCES projects(id),
|
|
90
|
+
FOREIGN KEY (developer_id) REFERENCES developers(id)
|
|
91
|
+
)
|
|
92
|
+
`);
|
|
93
|
+
|
|
94
|
+
// Issues 详细表
|
|
95
|
+
this.db.exec(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS issues (
|
|
97
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
review_id INTEGER NOT NULL,
|
|
99
|
+
level TEXT NOT NULL,
|
|
100
|
+
type TEXT NOT NULL,
|
|
101
|
+
file TEXT NOT NULL,
|
|
102
|
+
line TEXT,
|
|
103
|
+
code TEXT,
|
|
104
|
+
description TEXT NOT NULL,
|
|
105
|
+
suggestion TEXT,
|
|
106
|
+
fix_prompt TEXT,
|
|
107
|
+
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
|
|
108
|
+
)
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
// 关联风险表
|
|
112
|
+
this.db.exec(`
|
|
113
|
+
CREATE TABLE IF NOT EXISTS association_risks (
|
|
114
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
|
+
review_id INTEGER NOT NULL,
|
|
116
|
+
changed_file TEXT NOT NULL,
|
|
117
|
+
related_files TEXT,
|
|
118
|
+
risk TEXT NOT NULL,
|
|
119
|
+
check_prompt TEXT,
|
|
120
|
+
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
|
|
121
|
+
)
|
|
122
|
+
`);
|
|
123
|
+
|
|
124
|
+
// 创建索引
|
|
125
|
+
this.db.exec(`
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_project ON reviews(project_id);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_developer ON reviews(developer_id);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_date ON reviews(commit_date);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_commit ON reviews(commit_sha);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_issues_review ON issues(review_id);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_issues_level ON issues(level);
|
|
132
|
+
`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============ 项目操作 ============
|
|
136
|
+
|
|
137
|
+
getOrCreateProject(name, projectPath) {
|
|
138
|
+
let project = this.db.prepare('SELECT * FROM projects WHERE name = ?').get(name);
|
|
139
|
+
if (!project) {
|
|
140
|
+
const result = this.db.prepare(
|
|
141
|
+
'INSERT INTO projects (name, path) VALUES (?, ?)'
|
|
142
|
+
).run(name, projectPath);
|
|
143
|
+
project = { id: result.lastInsertRowid, name, path: projectPath };
|
|
144
|
+
} else {
|
|
145
|
+
// 更新路径和时间
|
|
146
|
+
this.db.prepare(
|
|
147
|
+
'UPDATE projects SET path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
|
|
148
|
+
).run(projectPath, project.id);
|
|
149
|
+
}
|
|
150
|
+
return project;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getProject(name) {
|
|
154
|
+
return this.db.prepare('SELECT * FROM projects WHERE name = ?').get(name);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
listProjects() {
|
|
158
|
+
return this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============ 开发者操作 ============
|
|
162
|
+
|
|
163
|
+
getOrCreateDeveloper(gitEmail, gitName) {
|
|
164
|
+
let developer = this.db.prepare('SELECT * FROM developers WHERE git_email = ?').get(gitEmail);
|
|
165
|
+
if (!developer) {
|
|
166
|
+
const result = this.db.prepare(
|
|
167
|
+
'INSERT INTO developers (git_email, git_name, display_name) VALUES (?, ?, ?)'
|
|
168
|
+
).run(gitEmail, gitName, gitName);
|
|
169
|
+
developer = { id: result.lastInsertRowid, git_email: gitEmail, git_name: gitName, display_name: gitName };
|
|
170
|
+
}
|
|
171
|
+
return developer;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getDeveloper(email) {
|
|
175
|
+
return this.db.prepare('SELECT * FROM developers WHERE git_email = ?').get(email);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getDeveloperById(id) {
|
|
179
|
+
return this.db.prepare('SELECT * FROM developers WHERE id = ?').get(id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
listDevelopers() {
|
|
183
|
+
return this.db.prepare('SELECT * FROM developers ORDER BY git_name').all();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
updateDeveloper(email, updates) {
|
|
187
|
+
const { displayName, team } = updates;
|
|
188
|
+
if (displayName !== undefined) {
|
|
189
|
+
this.db.prepare('UPDATE developers SET display_name = ? WHERE git_email = ?').run(displayName, email);
|
|
190
|
+
}
|
|
191
|
+
if (team !== undefined) {
|
|
192
|
+
this.db.prepare('UPDATE developers SET team = ? WHERE git_email = ?').run(team, email);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 设置开发者别名
|
|
197
|
+
setDeveloperAlias(emailPattern, targetEmail) {
|
|
198
|
+
const target = this.getDeveloper(targetEmail);
|
|
199
|
+
if (!target) {
|
|
200
|
+
throw new Error(`开发者 ${targetEmail} 不存在`);
|
|
201
|
+
}
|
|
202
|
+
// 删除旧的映射
|
|
203
|
+
this.db.prepare('DELETE FROM developer_aliases WHERE email_pattern = ?').run(emailPattern);
|
|
204
|
+
// 添加新映射
|
|
205
|
+
this.db.prepare(
|
|
206
|
+
'INSERT INTO developer_aliases (email_pattern, developer_id) VALUES (?, ?)'
|
|
207
|
+
).run(emailPattern, target.id);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 解析开发者(考虑别名)
|
|
211
|
+
resolveDeveloper(email) {
|
|
212
|
+
// 先检查别名映射
|
|
213
|
+
const alias = this.db.prepare(`
|
|
214
|
+
SELECT developer_id FROM developer_aliases
|
|
215
|
+
WHERE ? LIKE email_pattern
|
|
216
|
+
ORDER BY LENGTH(email_pattern) DESC
|
|
217
|
+
LIMIT 1
|
|
218
|
+
`).get(email);
|
|
219
|
+
|
|
220
|
+
if (alias) {
|
|
221
|
+
return this.getDeveloperById(alias.developer_id);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 直接匹配
|
|
225
|
+
return this.getDeveloper(email);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============ Review 操作 ============
|
|
229
|
+
|
|
230
|
+
saveReview(reviewData) {
|
|
231
|
+
const {
|
|
232
|
+
projectId, developerId, commitSha, commitMessage, commitDate, branch,
|
|
233
|
+
reviewType, fromSha, toSha, filesChanged, insertions, deletions,
|
|
234
|
+
diffContent, aiResponse, summary, commitMatch, commitMatchReason,
|
|
235
|
+
errorCount, warningCount, infoCount, riskCount, modelUsed,
|
|
236
|
+
issues, associationRisks
|
|
237
|
+
} = reviewData;
|
|
238
|
+
|
|
239
|
+
// 插入 review 记录
|
|
240
|
+
const result = this.db.prepare(`
|
|
241
|
+
INSERT INTO reviews (
|
|
242
|
+
project_id, developer_id, commit_sha, commit_message, commit_date, branch,
|
|
243
|
+
review_type, from_sha, to_sha, files_changed, insertions, deletions,
|
|
244
|
+
diff_content, ai_response, summary, commit_match, commit_match_reason,
|
|
245
|
+
error_count, warning_count, info_count, risk_count, model_used
|
|
246
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
247
|
+
`).run(
|
|
248
|
+
projectId, developerId, commitSha, commitMessage, commitDate, branch,
|
|
249
|
+
reviewType, fromSha, toSha, filesChanged, insertions, deletions,
|
|
250
|
+
diffContent, aiResponse, summary, commitMatch ? 1 : 0, commitMatchReason,
|
|
251
|
+
errorCount, warningCount, infoCount, riskCount, modelUsed
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const reviewId = result.lastInsertRowid;
|
|
255
|
+
|
|
256
|
+
// 插入 issues
|
|
257
|
+
if (issues && issues.length > 0) {
|
|
258
|
+
const insertIssue = this.db.prepare(`
|
|
259
|
+
INSERT INTO issues (review_id, level, type, file, line, code, description, suggestion, fix_prompt)
|
|
260
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
261
|
+
`);
|
|
262
|
+
|
|
263
|
+
for (const issue of issues) {
|
|
264
|
+
insertIssue.run(
|
|
265
|
+
reviewId, issue.level, issue.type || '', issue.file || '',
|
|
266
|
+
issue.line || '', issue.code || '', issue.description || '',
|
|
267
|
+
issue.suggestion || '', issue.fixPrompt || ''
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 插入关联风险
|
|
273
|
+
if (associationRisks && associationRisks.length > 0) {
|
|
274
|
+
const insertRisk = this.db.prepare(`
|
|
275
|
+
INSERT INTO association_risks (review_id, changed_file, related_files, risk, check_prompt)
|
|
276
|
+
VALUES (?, ?, ?, ?, ?)
|
|
277
|
+
`);
|
|
278
|
+
|
|
279
|
+
for (const risk of associationRisks) {
|
|
280
|
+
insertRisk.run(
|
|
281
|
+
reviewId, risk.changedFile || '',
|
|
282
|
+
JSON.stringify(risk.relatedFiles || []),
|
|
283
|
+
risk.risk || '', risk.checkPrompt || ''
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return reviewId;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
getReview(id) {
|
|
292
|
+
const review = this.db.prepare(`
|
|
293
|
+
SELECT r.*, p.name as project_name, d.display_name as developer_name, d.git_email as developer_email
|
|
294
|
+
FROM reviews r
|
|
295
|
+
JOIN projects p ON r.project_id = p.id
|
|
296
|
+
JOIN developers d ON r.developer_id = d.id
|
|
297
|
+
WHERE r.id = ?
|
|
298
|
+
`).get(id);
|
|
299
|
+
|
|
300
|
+
if (review) {
|
|
301
|
+
review.issues = this.db.prepare('SELECT * FROM issues WHERE review_id = ?').all(id);
|
|
302
|
+
review.associationRisks = this.db.prepare('SELECT * FROM association_risks WHERE review_id = ?').all(id);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return review;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
queryReviews(filters = {}) {
|
|
309
|
+
const { projectId, developerId, since, until, limit = 50, offset = 0 } = filters;
|
|
310
|
+
|
|
311
|
+
let sql = `
|
|
312
|
+
SELECT r.*, p.name as project_name, d.display_name as developer_name, d.git_email as developer_email
|
|
313
|
+
FROM reviews r
|
|
314
|
+
JOIN projects p ON r.project_id = p.id
|
|
315
|
+
JOIN developers d ON r.developer_id = d.id
|
|
316
|
+
WHERE 1=1
|
|
317
|
+
`;
|
|
318
|
+
const params = [];
|
|
319
|
+
|
|
320
|
+
if (projectId) {
|
|
321
|
+
sql += ' AND r.project_id = ?';
|
|
322
|
+
params.push(projectId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (developerId) {
|
|
326
|
+
sql += ' AND r.developer_id = ?';
|
|
327
|
+
params.push(developerId);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (since) {
|
|
331
|
+
sql += ' AND r.commit_date >= ?';
|
|
332
|
+
params.push(since);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (until) {
|
|
336
|
+
sql += ' AND r.commit_date <= ?';
|
|
337
|
+
params.push(until);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
sql += ' ORDER BY r.commit_date DESC LIMIT ? OFFSET ?';
|
|
341
|
+
params.push(limit, offset);
|
|
342
|
+
|
|
343
|
+
return this.db.prepare(sql).all(...params);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ============ 统计查询 ============
|
|
347
|
+
|
|
348
|
+
getProjectStats(projectId, dateRange = {}) {
|
|
349
|
+
let sql = `
|
|
350
|
+
SELECT
|
|
351
|
+
COUNT(*) as total_reviews,
|
|
352
|
+
SUM(error_count) as total_errors,
|
|
353
|
+
SUM(warning_count) as total_warnings,
|
|
354
|
+
SUM(info_count) as total_infos,
|
|
355
|
+
SUM(risk_count) as total_risks,
|
|
356
|
+
SUM(insertions) as total_insertions,
|
|
357
|
+
SUM(deletions) as total_deletions,
|
|
358
|
+
SUM(files_changed) as total_files_changed,
|
|
359
|
+
COUNT(DISTINCT developer_id) as developer_count
|
|
360
|
+
FROM reviews
|
|
361
|
+
WHERE project_id = ?
|
|
362
|
+
`;
|
|
363
|
+
const params = [projectId];
|
|
364
|
+
|
|
365
|
+
if (dateRange.since) {
|
|
366
|
+
sql += ' AND commit_date >= ?';
|
|
367
|
+
params.push(dateRange.since);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (dateRange.until) {
|
|
371
|
+
sql += ' AND commit_date <= ?';
|
|
372
|
+
params.push(dateRange.until);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return this.db.prepare(sql).get(...params);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getDeveloperStats(developerId, dateRange = {}) {
|
|
379
|
+
let sql = `
|
|
380
|
+
SELECT
|
|
381
|
+
COUNT(*) as total_reviews,
|
|
382
|
+
SUM(error_count) as total_errors,
|
|
383
|
+
SUM(warning_count) as total_warnings,
|
|
384
|
+
SUM(info_count) as total_infos,
|
|
385
|
+
SUM(risk_count) as total_risks,
|
|
386
|
+
SUM(insertions) as total_insertions,
|
|
387
|
+
SUM(deletions) as total_deletions,
|
|
388
|
+
SUM(files_changed) as total_files_changed,
|
|
389
|
+
COUNT(DISTINCT project_id) as project_count,
|
|
390
|
+
SUM(CASE WHEN commit_match = 1 THEN 1 ELSE 0 END) as commit_match_count
|
|
391
|
+
FROM reviews
|
|
392
|
+
WHERE developer_id = ?
|
|
393
|
+
`;
|
|
394
|
+
const params = [developerId];
|
|
395
|
+
|
|
396
|
+
if (dateRange.since) {
|
|
397
|
+
sql += ' AND commit_date >= ?';
|
|
398
|
+
params.push(dateRange.since);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (dateRange.until) {
|
|
402
|
+
sql += ' AND commit_date <= ?';
|
|
403
|
+
params.push(dateRange.until);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return this.db.prepare(sql).get(...params);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
getDeveloperStatsByProject(projectId, dateRange = {}) {
|
|
410
|
+
let sql = `
|
|
411
|
+
SELECT
|
|
412
|
+
d.id, d.display_name, d.git_email, d.team,
|
|
413
|
+
COUNT(*) as total_reviews,
|
|
414
|
+
SUM(r.error_count) as total_errors,
|
|
415
|
+
SUM(r.warning_count) as total_warnings,
|
|
416
|
+
SUM(r.info_count) as total_infos,
|
|
417
|
+
SUM(r.risk_count) as total_risks,
|
|
418
|
+
SUM(r.insertions) as total_insertions,
|
|
419
|
+
SUM(r.deletions) as total_deletions,
|
|
420
|
+
SUM(CASE WHEN r.commit_match = 1 THEN 1 ELSE 0 END) as commit_match_count
|
|
421
|
+
FROM reviews r
|
|
422
|
+
JOIN developers d ON r.developer_id = d.id
|
|
423
|
+
WHERE r.project_id = ?
|
|
424
|
+
`;
|
|
425
|
+
const params = [projectId];
|
|
426
|
+
|
|
427
|
+
if (dateRange.since) {
|
|
428
|
+
sql += ' AND r.commit_date >= ?';
|
|
429
|
+
params.push(dateRange.since);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (dateRange.until) {
|
|
433
|
+
sql += ' AND r.commit_date <= ?';
|
|
434
|
+
params.push(dateRange.until);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
sql += ' GROUP BY d.id ORDER BY total_reviews DESC';
|
|
438
|
+
|
|
439
|
+
return this.db.prepare(sql).all(...params);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
getIssueTypeDistribution(filters = {}) {
|
|
443
|
+
const { projectId, developerId, since, until } = filters;
|
|
444
|
+
|
|
445
|
+
let sql = `
|
|
446
|
+
SELECT i.type, i.level, COUNT(*) as count
|
|
447
|
+
FROM issues i
|
|
448
|
+
JOIN reviews r ON i.review_id = r.id
|
|
449
|
+
WHERE 1=1
|
|
450
|
+
`;
|
|
451
|
+
const params = [];
|
|
452
|
+
|
|
453
|
+
if (projectId) {
|
|
454
|
+
sql += ' AND r.project_id = ?';
|
|
455
|
+
params.push(projectId);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (developerId) {
|
|
459
|
+
sql += ' AND r.developer_id = ?';
|
|
460
|
+
params.push(developerId);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (since) {
|
|
464
|
+
sql += ' AND r.commit_date >= ?';
|
|
465
|
+
params.push(since);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (until) {
|
|
469
|
+
sql += ' AND r.commit_date <= ?';
|
|
470
|
+
params.push(until);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
sql += ' GROUP BY i.type, i.level ORDER BY count DESC';
|
|
474
|
+
|
|
475
|
+
return this.db.prepare(sql).all(...params);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 获取开发者的典型问题
|
|
479
|
+
getDeveloperTopIssues(developerId, limit = 5) {
|
|
480
|
+
return this.db.prepare(`
|
|
481
|
+
SELECT i.*, r.commit_sha, r.commit_message, p.name as project_name
|
|
482
|
+
FROM issues i
|
|
483
|
+
JOIN reviews r ON i.review_id = r.id
|
|
484
|
+
JOIN projects p ON r.project_id = p.id
|
|
485
|
+
WHERE r.developer_id = ?
|
|
486
|
+
ORDER BY
|
|
487
|
+
CASE i.level WHEN 'error' THEN 1 WHEN 'warning' THEN 2 ELSE 3 END,
|
|
488
|
+
r.commit_date DESC
|
|
489
|
+
LIMIT ?
|
|
490
|
+
`).all(developerId, limit);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 获取所有 reviews 的详细数据(用于报告生成)
|
|
494
|
+
getReviewsWithDetails(filters = {}) {
|
|
495
|
+
const reviews = this.queryReviews(filters);
|
|
496
|
+
|
|
497
|
+
for (const review of reviews) {
|
|
498
|
+
review.issues = this.db.prepare('SELECT * FROM issues WHERE review_id = ?').all(review.id);
|
|
499
|
+
review.associationRisks = this.db.prepare('SELECT * FROM association_risks WHERE review_id = ?').all(review.id);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return reviews;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
close() {
|
|
506
|
+
if (this.db) {
|
|
507
|
+
this.db.close();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 单例实例
|
|
513
|
+
let instance = null;
|
|
514
|
+
|
|
515
|
+
export function getDatabase() {
|
|
516
|
+
if (!instance) {
|
|
517
|
+
instance = new GoodifferDatabase();
|
|
518
|
+
instance.init();
|
|
519
|
+
}
|
|
520
|
+
return instance;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export { GoodifferDatabase, getConfigDir };
|
|
524
|
+
export default GoodifferDatabase;
|