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/bin/goodiffer.js
CHANGED
|
@@ -1,5 +1,95 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { program } from '
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { initCommand } from '../src/commands/init.js';
|
|
5
|
+
import { analyzeCommand } from '../src/commands/analyze.js';
|
|
6
|
+
import { configCommand } from '../src/commands/config.js';
|
|
7
|
+
import { historyCommand } from '../src/commands/history.js';
|
|
8
|
+
import { statsCommand } from '../src/commands/stats.js';
|
|
9
|
+
import { developerCommand } from '../src/commands/developer.js';
|
|
10
|
+
import { reportCommand } from '../src/commands/report.js';
|
|
4
11
|
|
|
5
|
-
program
|
|
12
|
+
program
|
|
13
|
+
.name('goodiffer')
|
|
14
|
+
.description('AI-powered git diff analyzer for code review')
|
|
15
|
+
.version('1.0.1');
|
|
16
|
+
|
|
17
|
+
// 默认命令 - 分析
|
|
18
|
+
program
|
|
19
|
+
.option('-s, --staged', '分析暂存区的更改')
|
|
20
|
+
.option('-c, --commit <sha>', '分析指定的 commit')
|
|
21
|
+
.option('--from <sha>', '起始 commit (与 --to 配合使用)')
|
|
22
|
+
.option('--to <sha>', '结束 commit (与 --from 配合使用)')
|
|
23
|
+
.option('-n <number>', '分析最近 n 条 commit (n <= 10), 或与 -m 配合表示起始位置')
|
|
24
|
+
.option('-m <number>', '与 -n 配合使用,表示结束位置 (m-n <= 10)')
|
|
25
|
+
.option('--no-save', '不保存到数据库')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
await analyzeCommand(options);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// init 命令
|
|
31
|
+
program
|
|
32
|
+
.command('init')
|
|
33
|
+
.description('初始化配置')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
await initCommand();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// config 命令
|
|
39
|
+
program
|
|
40
|
+
.command('config <action> [key] [value]')
|
|
41
|
+
.description('配置管理 (list, get, set, clear)')
|
|
42
|
+
.action((action, key, value) => {
|
|
43
|
+
configCommand(action, key, value);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// history 命令
|
|
47
|
+
program
|
|
48
|
+
.command('history')
|
|
49
|
+
.description('查看 Code Review 历史记录')
|
|
50
|
+
.option('-p, --project [name]', '按项目筛选 (默认当前项目)')
|
|
51
|
+
.option('-d, --developer <name>', '按开发者筛选')
|
|
52
|
+
.option('--since <date>', '开始日期 (YYYY-MM-DD)')
|
|
53
|
+
.option('--until <date>', '结束日期 (YYYY-MM-DD)')
|
|
54
|
+
.option('-n, --limit <number>', '显示数量', '20')
|
|
55
|
+
.option('--json', '输出 JSON 格式')
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
await historyCommand(options);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// stats 命令
|
|
61
|
+
program
|
|
62
|
+
.command('stats')
|
|
63
|
+
.description('显示统计信息')
|
|
64
|
+
.option('-p, --project', '项目统计 (默认)')
|
|
65
|
+
.option('-d, --developer', '开发者统计')
|
|
66
|
+
.option('--since <date>', '开始日期 (YYYY-MM-DD)')
|
|
67
|
+
.option('--until <date>', '结束日期 (YYYY-MM-DD)')
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
await statsCommand(options);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// developer 命令
|
|
73
|
+
program
|
|
74
|
+
.command('developer <action> [args...]')
|
|
75
|
+
.description('开发者管理 (list, alias, rename, team)')
|
|
76
|
+
.action(async (action, args) => {
|
|
77
|
+
await developerCommand(action, args);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// report 命令
|
|
81
|
+
program
|
|
82
|
+
.command('report')
|
|
83
|
+
.description('生成 H5 分析报告')
|
|
84
|
+
.option('-p, --project [name]', '项目名称 (默认当前项目)')
|
|
85
|
+
.option('-d, --developer <email>', '生成个人报告')
|
|
86
|
+
.option('--all', '包含所有项目')
|
|
87
|
+
.option('--since <date>', '开始日期 (YYYY-MM-DD)')
|
|
88
|
+
.option('--until <date>', '结束日期 (YYYY-MM-DD)')
|
|
89
|
+
.option('-o, --output <path>', '输出文件路径')
|
|
90
|
+
.option('--open', '生成后自动打开')
|
|
91
|
+
.action(async (options) => {
|
|
92
|
+
await reportCommand(options);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goodiffer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "AI-powered git diff analyzer for code review - 智能代码审查工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,10 +41,13 @@
|
|
|
41
41
|
"node": ">=18.0.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"better-sqlite3": "^12.5.0",
|
|
44
45
|
"chalk": "^5.3.0",
|
|
45
46
|
"commander": "^12.1.0",
|
|
46
47
|
"conf": "^12.0.0",
|
|
48
|
+
"dayjs": "^1.11.19",
|
|
47
49
|
"inquirer": "^9.2.15",
|
|
50
|
+
"open": "^11.0.0",
|
|
48
51
|
"openai": "^4.70.0",
|
|
49
52
|
"ora": "^8.0.1",
|
|
50
53
|
"simple-git": "^3.22.0"
|
package/src/commands/analyze.js
CHANGED
|
@@ -1,137 +1,423 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { getConfig, isConfigured } from '../utils/config-store.js';
|
|
4
|
-
import
|
|
5
|
-
import { isGitRepo, getLastCommitInfo, getCommitInfo, getDiff, getChangedFiles } from '../services/git.js';
|
|
3
|
+
import { GitService } from '../services/git.js';
|
|
6
4
|
import { AIClient } from '../services/ai-client.js';
|
|
7
5
|
import { buildReviewPrompt } from '../prompts/review-prompt.js';
|
|
8
|
-
import {
|
|
6
|
+
import { generateReport } from '../services/reporter.js';
|
|
7
|
+
import { getDatabase } from '../services/database.js';
|
|
8
|
+
import logger from '../utils/logger.js';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// 解析 AI 响应
|
|
11
|
+
function parseAIResponse(response) {
|
|
12
|
+
try {
|
|
13
|
+
let jsonStr = response;
|
|
14
|
+
const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
15
|
+
if (jsonMatch) {
|
|
16
|
+
jsonStr = jsonMatch[1].trim();
|
|
17
|
+
}
|
|
18
|
+
return JSON.parse(jsonStr);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
15
21
|
}
|
|
22
|
+
}
|
|
16
23
|
|
|
24
|
+
export async function analyzeCommand(options) {
|
|
17
25
|
// 检查配置
|
|
18
26
|
if (!isConfigured()) {
|
|
19
|
-
logger.error('
|
|
27
|
+
logger.error('请先运行 goodiffer init 进行配置');
|
|
20
28
|
process.exit(1);
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
const config = getConfig();
|
|
32
|
+
const git = new GitService();
|
|
24
33
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
commitInfo = {
|
|
31
|
-
sha: 'staged',
|
|
32
|
-
message: '(暂存区变更)',
|
|
33
|
-
author: '',
|
|
34
|
-
date: new Date().toISOString()
|
|
35
|
-
};
|
|
36
|
-
} else {
|
|
37
|
-
commitInfo = await getLastCommitInfo();
|
|
34
|
+
// 检查是否在 git 仓库中
|
|
35
|
+
const isRepo = await git.isGitRepo();
|
|
36
|
+
if (!isRepo) {
|
|
37
|
+
logger.error('当前目录不是 git 仓库');
|
|
38
|
+
process.exit(1);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// 处理 -n 和 -m 参数的多 commit 模式
|
|
42
|
+
if (options.n || options.m) {
|
|
43
|
+
await analyzeMultipleCommits(options, config, git);
|
|
44
|
+
return;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
const spinner = ora('获取代码变更...').start();
|
|
47
|
+
let spinner = ora('获取 Git 信息...').start();
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
try {
|
|
50
|
+
// 获取 commit 信息和 diff
|
|
51
|
+
let commitInfo;
|
|
52
|
+
let diff;
|
|
53
|
+
let reviewType = 'commit';
|
|
54
|
+
let author;
|
|
55
|
+
let diffStats;
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (options.staged) {
|
|
58
|
+
// 分析暂存区
|
|
59
|
+
commitInfo = { message: '(暂存区更改)', sha: 'staged' };
|
|
60
|
+
diff = await git.getStagedDiff();
|
|
61
|
+
reviewType = 'staged';
|
|
62
|
+
author = await git.getCommitAuthor('staged');
|
|
63
|
+
diffStats = await git.getStagedDiffStats();
|
|
64
|
+
} else if (options.commit) {
|
|
65
|
+
// 分析指定 commit
|
|
66
|
+
commitInfo = await git.getCommitInfo(options.commit);
|
|
67
|
+
diff = await git.getCommitDiff(options.commit);
|
|
68
|
+
author = await git.getCommitAuthor(options.commit);
|
|
69
|
+
diffStats = await git.getDiffStats(`${options.commit}~1`, options.commit);
|
|
70
|
+
} else if (options.from && options.to) {
|
|
71
|
+
// 分析 commit 范围
|
|
72
|
+
commitInfo = { message: `${options.from}..${options.to}`, sha: 'range' };
|
|
73
|
+
diff = await git.getRangeDiff(options.from, options.to);
|
|
74
|
+
reviewType = 'range';
|
|
75
|
+
author = await git.getLastCommitAuthor();
|
|
76
|
+
diffStats = await git.getDiffStats(options.from, options.to);
|
|
77
|
+
} else {
|
|
78
|
+
// 默认: 分析最近一次 commit
|
|
79
|
+
commitInfo = await git.getLastCommitInfo();
|
|
80
|
+
diff = await git.getLastCommitDiff();
|
|
81
|
+
author = await git.getLastCommitAuthor();
|
|
82
|
+
diffStats = await git.getDiffStats('HEAD~1', 'HEAD');
|
|
83
|
+
}
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
to: options.to
|
|
65
|
-
});
|
|
85
|
+
if (!diff || diff.trim() === '') {
|
|
86
|
+
spinner.fail('没有找到代码变更');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
66
89
|
|
|
67
|
-
|
|
90
|
+
// 获取项目信息和分支
|
|
91
|
+
const projectName = await git.getProjectName();
|
|
92
|
+
const branch = await git.getCurrentBranch();
|
|
68
93
|
|
|
69
|
-
|
|
70
|
-
const diffLines = diff.split('\n').length;
|
|
71
|
-
if (diffLines > 2000) {
|
|
72
|
-
logger.warn(`diff 较大 (${diffLines} 行),分析可能需要较长时间`);
|
|
73
|
-
}
|
|
94
|
+
spinner.succeed('获取 Git 信息完成');
|
|
74
95
|
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
// 构建提示词
|
|
97
|
+
const prompt = buildReviewPrompt(commitInfo.message, diff);
|
|
77
98
|
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
// 调用 AI 分析
|
|
100
|
+
spinner = ora('AI 正在分析代码...').start();
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
console.log(chalk.gray('─'.repeat(60)));
|
|
102
|
+
const aiClient = new AIClient(config);
|
|
103
|
+
let response = '';
|
|
84
104
|
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
try {
|
|
106
|
+
response = await aiClient.analyzeStream(prompt, (chunk) => {
|
|
107
|
+
// 流式输出时更新 spinner
|
|
108
|
+
spinner.text = `AI 正在分析... (${response.length} 字符)`;
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
spinner.fail('AI 分析失败');
|
|
112
|
+
if (error.message.includes('401')) {
|
|
113
|
+
logger.error('API Key 无效或已过期');
|
|
114
|
+
} else if (error.message.includes('403')) {
|
|
115
|
+
logger.error('API 请求被拒绝 (403)');
|
|
116
|
+
logger.info('请检查:');
|
|
117
|
+
console.log(' 1. API Key 是否正确');
|
|
118
|
+
console.log(' 2. API Host 是否正确');
|
|
119
|
+
console.log(' 3. 模型名称是否正确');
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(` 当前配置:`);
|
|
122
|
+
console.log(` apiHost: ${config.apiHost}`);
|
|
123
|
+
console.log(` model: ${config.model}`);
|
|
124
|
+
} else if (error.message.includes('429')) {
|
|
125
|
+
logger.error('请求过于频繁,请稍后重试');
|
|
126
|
+
} else {
|
|
127
|
+
logger.error(`API 错误: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
87
131
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
spinner.succeed('AI 分析完成');
|
|
133
|
+
|
|
134
|
+
// 生成报告
|
|
135
|
+
generateReport(response, commitInfo);
|
|
136
|
+
|
|
137
|
+
// 保存到数据库 (除非指定 --no-save)
|
|
138
|
+
if (options.save !== false) {
|
|
139
|
+
try {
|
|
140
|
+
const db = getDatabase();
|
|
141
|
+
|
|
142
|
+
// 获取或创建项目
|
|
143
|
+
const project = db.getOrCreateProject(projectName, process.cwd());
|
|
144
|
+
|
|
145
|
+
// 获取或创建开发者
|
|
146
|
+
const developer = db.getOrCreateDeveloper(author.email, author.name);
|
|
147
|
+
|
|
148
|
+
// 解析 AI 响应
|
|
149
|
+
const parsed = parseAIResponse(response);
|
|
150
|
+
|
|
151
|
+
// 提取统计数据
|
|
152
|
+
let summary = '';
|
|
153
|
+
let commitMatch = false;
|
|
154
|
+
let commitMatchReason = '';
|
|
155
|
+
let errorCount = 0;
|
|
156
|
+
let warningCount = 0;
|
|
157
|
+
let infoCount = 0;
|
|
158
|
+
let riskCount = 0;
|
|
159
|
+
let issues = [];
|
|
160
|
+
let associationRisks = [];
|
|
161
|
+
|
|
162
|
+
if (parsed) {
|
|
163
|
+
summary = parsed.summary || '';
|
|
164
|
+
commitMatch = parsed.commitMatch || false;
|
|
165
|
+
commitMatchReason = parsed.commitMatchReason || '';
|
|
166
|
+
|
|
167
|
+
if (parsed.issues && Array.isArray(parsed.issues)) {
|
|
168
|
+
issues = parsed.issues;
|
|
169
|
+
errorCount = issues.filter(i => i.level === 'error').length;
|
|
170
|
+
warningCount = issues.filter(i => i.level === 'warning').length;
|
|
171
|
+
infoCount = issues.filter(i => i.level === 'info').length;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (parsed.associationRisks && Array.isArray(parsed.associationRisks)) {
|
|
175
|
+
associationRisks = parsed.associationRisks;
|
|
176
|
+
riskCount = associationRisks.length;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 保存 review 记录
|
|
181
|
+
const reviewId = db.saveReview({
|
|
182
|
+
projectId: project.id,
|
|
183
|
+
developerId: developer.id,
|
|
184
|
+
commitSha: commitInfo.sha,
|
|
185
|
+
commitMessage: commitInfo.message,
|
|
186
|
+
commitDate: author.date,
|
|
187
|
+
branch,
|
|
188
|
+
reviewType,
|
|
189
|
+
fromSha: options.from || null,
|
|
190
|
+
toSha: options.to || null,
|
|
191
|
+
filesChanged: diffStats.filesChanged,
|
|
192
|
+
insertions: diffStats.insertions,
|
|
193
|
+
deletions: diffStats.deletions,
|
|
194
|
+
diffContent: null, // 不存储 diff 内容以节省空间
|
|
195
|
+
aiResponse: response,
|
|
196
|
+
summary,
|
|
197
|
+
commitMatch,
|
|
198
|
+
commitMatchReason,
|
|
199
|
+
errorCount,
|
|
200
|
+
warningCount,
|
|
201
|
+
infoCount,
|
|
202
|
+
riskCount,
|
|
203
|
+
modelUsed: config.model,
|
|
204
|
+
issues,
|
|
205
|
+
associationRisks
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
logger.success(`Review #${reviewId} 已保存到数据库`);
|
|
209
|
+
} catch (dbError) {
|
|
210
|
+
logger.warning(`保存到数据库失败: ${dbError.message}`);
|
|
92
211
|
}
|
|
93
|
-
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
spinner.fail('分析失败');
|
|
216
|
+
logger.error(error.message);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default analyzeCommand;
|
|
222
|
+
|
|
223
|
+
// 分析多个 commits
|
|
224
|
+
async function analyzeMultipleCommits(options, config, git) {
|
|
225
|
+
const n = options.n ? parseInt(options.n, 10) : null;
|
|
226
|
+
const m = options.m ? parseInt(options.m, 10) : null;
|
|
227
|
+
|
|
228
|
+
// 验证参数
|
|
229
|
+
if (m !== null && n === null) {
|
|
230
|
+
logger.error('-m 参数必须与 -n 配合使用');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
94
233
|
|
|
95
|
-
|
|
234
|
+
if (n !== null && m === null) {
|
|
235
|
+
// 只有 -n,表示分析最近 n 条
|
|
236
|
+
if (n <= 0 || n > 10) {
|
|
237
|
+
logger.error('n 必须在 1-10 之间');
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
96
241
|
|
|
97
|
-
|
|
98
|
-
|
|
242
|
+
if (n !== null && m !== null) {
|
|
243
|
+
// 同时有 -n 和 -m,表示第 n 条到第 m 条
|
|
244
|
+
if (n <= 0 || m <= 0) {
|
|
245
|
+
logger.error('n 和 m 必须大于 0');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (m < n) {
|
|
249
|
+
logger.error('m 必须大于等于 n');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
if (m - n > 10) {
|
|
253
|
+
logger.error('m - n 不能超过 10');
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let spinner = ora('获取 Git 信息...').start();
|
|
99
259
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
260
|
+
try {
|
|
261
|
+
// 获取 commits
|
|
262
|
+
let commits;
|
|
263
|
+
if (m !== null) {
|
|
264
|
+
// 第 n 条到第 m 条
|
|
265
|
+
commits = await git.getCommitRange(n, m);
|
|
103
266
|
} else {
|
|
104
|
-
|
|
267
|
+
// 最近 n 条
|
|
268
|
+
commits = await git.getRecentCommits(n);
|
|
105
269
|
}
|
|
106
270
|
|
|
107
|
-
|
|
271
|
+
if (commits.length === 0) {
|
|
272
|
+
spinner.fail('没有找到 commits');
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const projectName = await git.getProjectName();
|
|
277
|
+
const branch = await git.getCurrentBranch();
|
|
278
|
+
|
|
279
|
+
spinner.succeed(`找到 ${commits.length} 个 commit`);
|
|
280
|
+
|
|
281
|
+
// 显示要分析的 commits
|
|
282
|
+
logger.info('\n要分析的 commits:');
|
|
283
|
+
commits.forEach((commit, index) => {
|
|
284
|
+
const shortSha = commit.sha.substring(0, 7);
|
|
285
|
+
const shortMsg = commit.message.split('\n')[0].substring(0, 50);
|
|
286
|
+
console.log(` ${index + 1}. ${shortSha} - ${shortMsg}`);
|
|
287
|
+
});
|
|
108
288
|
console.log('');
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
289
|
+
|
|
290
|
+
// 逐个分析
|
|
291
|
+
const aiClient = new AIClient(config);
|
|
292
|
+
const results = [];
|
|
293
|
+
|
|
294
|
+
for (let i = 0; i < commits.length; i++) {
|
|
295
|
+
const commit = commits[i];
|
|
296
|
+
const shortSha = commit.sha.substring(0, 7);
|
|
297
|
+
|
|
298
|
+
spinner = ora(`[${i + 1}/${commits.length}] 分析 commit ${shortSha}...`).start();
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// 获取 diff
|
|
302
|
+
const diff = await git.getCommitDiff(commit.sha);
|
|
303
|
+
if (!diff || diff.trim() === '') {
|
|
304
|
+
spinner.warn(`[${i + 1}/${commits.length}] commit ${shortSha} 没有代码变更,跳过`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const diffStats = await git.getDiffStats(`${commit.sha}~1`, commit.sha);
|
|
309
|
+
|
|
310
|
+
// 构建提示词并分析
|
|
311
|
+
const prompt = buildReviewPrompt(commit.message, diff);
|
|
312
|
+
let response = '';
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
response = await aiClient.analyzeStream(prompt, (chunk) => {
|
|
316
|
+
spinner.text = `[${i + 1}/${commits.length}] 分析 commit ${shortSha}... (${response.length} 字符)`;
|
|
317
|
+
});
|
|
318
|
+
} catch (error) {
|
|
319
|
+
spinner.fail(`[${i + 1}/${commits.length}] commit ${shortSha} 分析失败: ${error.message}`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
spinner.succeed(`[${i + 1}/${commits.length}] commit ${shortSha} 分析完成`);
|
|
324
|
+
|
|
325
|
+
// 生成报告
|
|
326
|
+
generateReport(response, { sha: commit.sha, message: commit.message });
|
|
327
|
+
|
|
328
|
+
// 保存到数据库
|
|
329
|
+
if (options.save !== false) {
|
|
330
|
+
try {
|
|
331
|
+
const db = getDatabase();
|
|
332
|
+
const project = db.getOrCreateProject(projectName, process.cwd());
|
|
333
|
+
const developer = db.getOrCreateDeveloper(commit.author.email, commit.author.name);
|
|
334
|
+
const parsed = parseAIResponse(response);
|
|
335
|
+
|
|
336
|
+
let summary = '';
|
|
337
|
+
let commitMatch = false;
|
|
338
|
+
let commitMatchReason = '';
|
|
339
|
+
let errorCount = 0;
|
|
340
|
+
let warningCount = 0;
|
|
341
|
+
let infoCount = 0;
|
|
342
|
+
let riskCount = 0;
|
|
343
|
+
let issues = [];
|
|
344
|
+
let associationRisks = [];
|
|
345
|
+
|
|
346
|
+
if (parsed) {
|
|
347
|
+
summary = parsed.summary || '';
|
|
348
|
+
commitMatch = parsed.commitMatch || false;
|
|
349
|
+
commitMatchReason = parsed.commitMatchReason || '';
|
|
350
|
+
|
|
351
|
+
if (parsed.issues && Array.isArray(parsed.issues)) {
|
|
352
|
+
issues = parsed.issues;
|
|
353
|
+
errorCount = issues.filter(i => i.level === 'error').length;
|
|
354
|
+
warningCount = issues.filter(i => i.level === 'warning').length;
|
|
355
|
+
infoCount = issues.filter(i => i.level === 'info').length;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (parsed.associationRisks && Array.isArray(parsed.associationRisks)) {
|
|
359
|
+
associationRisks = parsed.associationRisks;
|
|
360
|
+
riskCount = associationRisks.length;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const reviewId = db.saveReview({
|
|
365
|
+
projectId: project.id,
|
|
366
|
+
developerId: developer.id,
|
|
367
|
+
commitSha: commit.sha,
|
|
368
|
+
commitMessage: commit.message,
|
|
369
|
+
commitDate: commit.author.date,
|
|
370
|
+
branch,
|
|
371
|
+
reviewType: 'commit',
|
|
372
|
+
fromSha: null,
|
|
373
|
+
toSha: null,
|
|
374
|
+
filesChanged: diffStats.filesChanged,
|
|
375
|
+
insertions: diffStats.insertions,
|
|
376
|
+
deletions: diffStats.deletions,
|
|
377
|
+
diffContent: null,
|
|
378
|
+
aiResponse: response,
|
|
379
|
+
summary,
|
|
380
|
+
commitMatch,
|
|
381
|
+
commitMatchReason,
|
|
382
|
+
errorCount,
|
|
383
|
+
warningCount,
|
|
384
|
+
infoCount,
|
|
385
|
+
riskCount,
|
|
386
|
+
modelUsed: config.model,
|
|
387
|
+
issues,
|
|
388
|
+
associationRisks
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
results.push({ commit, reviewId, success: true });
|
|
392
|
+
} catch (dbError) {
|
|
393
|
+
logger.warning(`保存 commit ${shortSha} 到数据库失败: ${dbError.message}`);
|
|
394
|
+
results.push({ commit, success: false, error: dbError.message });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 在 commits 之间添加分隔线
|
|
399
|
+
if (i < commits.length - 1) {
|
|
400
|
+
console.log('\n' + '─'.repeat(60) + '\n');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
} catch (error) {
|
|
404
|
+
spinner.fail(`[${i + 1}/${commits.length}] commit ${shortSha} 处理失败: ${error.message}`);
|
|
405
|
+
results.push({ commit, success: false, error: error.message });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 显示汇总
|
|
410
|
+
console.log('\n' + '═'.repeat(60));
|
|
411
|
+
logger.success(`\n分析完成!共处理 ${commits.length} 个 commit`);
|
|
412
|
+
|
|
413
|
+
const successCount = results.filter(r => r.success).length;
|
|
414
|
+
if (successCount > 0) {
|
|
415
|
+
logger.info(`成功保存 ${successCount} 条记录到数据库`);
|
|
132
416
|
}
|
|
417
|
+
|
|
418
|
+
} catch (error) {
|
|
419
|
+
spinner.fail('分析失败');
|
|
420
|
+
logger.error(error.message);
|
|
133
421
|
process.exit(1);
|
|
134
422
|
}
|
|
135
423
|
}
|
|
136
|
-
|
|
137
|
-
export default analyzeCommand;
|