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.
@@ -1,136 +1,235 @@
1
- import { simpleGit } from 'simple-git';
2
- import logger from '../utils/logger.js';
1
+ import simpleGit from 'simple-git';
2
+ import path from 'path';
3
3
 
4
- const git = simpleGit();
4
+ export class GitService {
5
+ constructor(basePath = process.cwd()) {
6
+ this.git = simpleGit(basePath);
7
+ this.basePath = basePath;
8
+ }
5
9
 
6
- export async function isGitRepo() {
7
- try {
8
- await git.revparse(['--git-dir']);
9
- return true;
10
- } catch {
11
- return false;
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
- export async function getLastCommitInfo() {
16
- try {
17
- const log = await git.log({ maxCount: 1 });
18
- if (log.latest) {
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 null;
27
- } catch (error) {
28
- logger.error(`获取 commit 信息失败: ${error.message}`);
29
- return null;
24
+ return {
25
+ sha: log.latest.hash,
26
+ message: log.latest.message
27
+ };
30
28
  }
31
- }
32
29
 
33
- export async function getCommitInfo(sha) {
34
- try {
35
- const log = await git.log({ from: sha, to: sha, maxCount: 1 });
36
- if (log.latest) {
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: log.latest.hash,
39
- message: log.latest.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
- export async function getDiff(options = {}) {
52
- try {
53
- const { staged, commit, from, to } = options;
47
+ async getLastCommitDiff() {
48
+ const diff = await this.git.diff(['HEAD~1', 'HEAD']);
49
+ return diff;
50
+ }
54
51
 
55
- if (staged) {
56
- // 暂存区 diff
57
- return await git.diff(['--cached']);
58
- }
52
+ async getCommitDiff(sha) {
53
+ const diff = await this.git.diff([`${sha}~1`, sha]);
54
+ return diff;
55
+ }
59
56
 
60
- if (commit) {
61
- // 指定 commit diff
62
- return await git.diff([`${commit}^`, commit]);
63
- }
57
+ async getStagedDiff() {
58
+ const diff = await this.git.diff(['--cached']);
59
+ return diff;
60
+ }
64
61
 
65
- if (from && to) {
66
- // commit 范围
67
- return await git.diff([from, to]);
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
- // 默认: 最近一次 commit 的 diff
71
- const log = await git.log({ maxCount: 1 });
72
- if (log.latest) {
73
- return await git.diff([`${log.latest.hash}^`, log.latest.hash]);
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
- return '';
77
- } catch (error) {
78
- // 如果是首次 commit,没有父 commit
79
- if (error.message.includes('unknown revision')) {
80
- try {
81
- // 尝试获取首次 commit 的全部内容
82
- return await git.diff(['--cached', 'HEAD']);
83
- } catch {
84
- return await git.diff(['HEAD']);
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
- export async function getChangedFiles(options = {}) {
93
- try {
94
- const { staged, commit, from, to } = options;
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
- if (staged) {
97
- const status = await git.status();
98
- return status.staged;
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
- if (commit) {
102
- const result = await git.diff(['--name-only', `${commit}^`, commit]);
103
- return result.trim().split('\n').filter(Boolean);
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
- if (from && to) {
107
- const result = await git.diff(['--name-only', from, to]);
108
- return result.trim().split('\n').filter(Boolean);
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
- // 默认: 最近一次 commit
112
- const log = await git.log({ maxCount: 1 });
113
- if (log.latest) {
114
- const result = await git.diff(['--name-only', `${log.latest.hash}^`, log.latest.hash]);
115
- return result.trim().split('\n').filter(Boolean);
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
- return [];
119
- } catch (error) {
120
- logger.error(`获取变更文件列表失败: ${error.message}`);
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
- export async function getStagedDiff() {
126
- return getDiff({ staged: true });
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;