commit-report 1.0.1 → 1.0.2
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/README.md +2 -2
- package/dist/index.js +1794 -463
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/templates/report-scripts/00-advanced-derived.html +462 -0
- package/templates/report-scripts/00-filter-state.html +374 -0
- package/templates/report-scripts/00-report-controls.html +272 -0
- package/templates/report-scripts/01-core.html +255 -0
- package/templates/report-scripts/02-commit-details.html +275 -0
- package/templates/report-scripts/03-basic-charts.html +378 -0
- package/templates/report-scripts/04-trend-charts.html +309 -0
- package/templates/report-scripts/05-tables-team-stability.html +372 -0
- package/templates/report-scripts/06-pressure-churn.html +339 -0
- package/templates/report-scripts/07-collab-debt-ai.html +534 -0
- package/templates/report-scripts/08-engineering.html +200 -0
- package/templates/report-scripts/09-extensions.html +313 -0
- package/templates/report-scripts/10-runtime.html +54 -0
- package/templates/report-sections/01-overview.html +342 -0
- package/templates/report-sections/02-advanced.html +406 -0
- package/templates/report.html +40 -1998
package/dist/index.js
CHANGED
|
@@ -94,31 +94,34 @@ import ora2 from "ora";
|
|
|
94
94
|
import chalk from "chalk";
|
|
95
95
|
|
|
96
96
|
// src/analyzer/git-log-parser.ts
|
|
97
|
-
import {
|
|
97
|
+
import { execFileSync } from "child_process";
|
|
98
98
|
import { readFile } from "fs/promises";
|
|
99
99
|
import { join as join2 } from "path";
|
|
100
100
|
import ig from "ignore";
|
|
101
101
|
var COMMIT_SEPARATOR = "---COMMITX_SEP---";
|
|
102
|
+
var COMMIT_END = "---COMMITX_END---";
|
|
102
103
|
var FIELD_SEPARATOR = "|";
|
|
103
|
-
var FORMAT = `${COMMIT_SEPARATOR}%H${FIELD_SEPARATOR}%an${FIELD_SEPARATOR}%ae${FIELD_SEPARATOR}%aI${FIELD_SEPARATOR}%s`;
|
|
104
|
+
var FORMAT = `${COMMIT_SEPARATOR}%H${FIELD_SEPARATOR}%P${FIELD_SEPARATOR}%an${FIELD_SEPARATOR}%ae${FIELD_SEPARATOR}%aI${FIELD_SEPARATOR}%s%n%B${COMMIT_END}`;
|
|
104
105
|
async function parseGitLog(repoPath, timeRange, author) {
|
|
105
106
|
const ignoreFilter = await loadGitignore(repoPath);
|
|
106
107
|
const args = [
|
|
107
|
-
"git",
|
|
108
108
|
"log",
|
|
109
|
-
`--format
|
|
110
|
-
"--
|
|
109
|
+
`--format=${FORMAT}`,
|
|
110
|
+
"--raw",
|
|
111
|
+
"--numstat",
|
|
112
|
+
"--find-renames",
|
|
113
|
+
"--find-copies"
|
|
111
114
|
];
|
|
112
115
|
if (timeRange) {
|
|
113
|
-
args.push(`--since
|
|
114
|
-
args.push(`--until
|
|
116
|
+
args.push(`--since=${timeRange.from.toISOString()}`);
|
|
117
|
+
args.push(`--until=${timeRange.to.toISOString()}`);
|
|
115
118
|
}
|
|
116
119
|
if (author) {
|
|
117
|
-
args.push(`--author
|
|
120
|
+
args.push(`--author=${author}`);
|
|
118
121
|
}
|
|
119
122
|
let output;
|
|
120
123
|
try {
|
|
121
|
-
output =
|
|
124
|
+
output = execFileSync("git", args, {
|
|
122
125
|
cwd: repoPath,
|
|
123
126
|
encoding: "utf-8",
|
|
124
127
|
maxBuffer: 100 * 1024 * 1024,
|
|
@@ -137,16 +140,23 @@ function parseOutput(output, ignoreFilter) {
|
|
|
137
140
|
const commits = [];
|
|
138
141
|
const blocks = output.split(COMMIT_SEPARATOR).filter((b) => b.trim());
|
|
139
142
|
for (const block of blocks) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
+
const endIndex = block.indexOf(COMMIT_END);
|
|
144
|
+
const metadataText = endIndex >= 0 ? block.slice(0, endIndex) : block;
|
|
145
|
+
const changesText = endIndex >= 0 ? block.slice(endIndex + COMMIT_END.length) : "";
|
|
146
|
+
const metadataLines = metadataText.trim().split("\n");
|
|
147
|
+
if (metadataLines.length === 0) continue;
|
|
148
|
+
const headerLine = metadataLines[0].replace(/^"|"$/g, "");
|
|
143
149
|
const parts = headerLine.split(FIELD_SEPARATOR);
|
|
144
|
-
if (parts.length <
|
|
145
|
-
const [hash, authorName, email, dateStr, ...messageParts] = parts;
|
|
150
|
+
if (parts.length < 6) continue;
|
|
151
|
+
const [hash, parentHashText, authorName, email, dateStr, ...messageParts] = parts;
|
|
146
152
|
const message = messageParts.join(FIELD_SEPARATOR);
|
|
153
|
+
const body = metadataLines.slice(1).join("\n").trim();
|
|
154
|
+
const parentHashes = parentHashText.trim() ? parentHashText.trim().split(/\s+/) : [];
|
|
155
|
+
const changeLines = changesText.trim().split("\n");
|
|
156
|
+
const statusByPath = parseFileStatuses(changeLines);
|
|
147
157
|
const files = [];
|
|
148
|
-
for (
|
|
149
|
-
const line =
|
|
158
|
+
for (const rawLine of changeLines) {
|
|
159
|
+
const line = rawLine.trim();
|
|
150
160
|
if (!line) continue;
|
|
151
161
|
const tabParts = line.split(" ");
|
|
152
162
|
if (tabParts.length !== 3) continue;
|
|
@@ -154,7 +164,13 @@ function parseOutput(output, ignoreFilter) {
|
|
|
154
164
|
const added = addedStr === "-" ? 0 : parseInt(addedStr, 10) || 0;
|
|
155
165
|
const deleted = deletedStr === "-" ? 0 : parseInt(deletedStr, 10) || 0;
|
|
156
166
|
if (ignoreFilter.ignores(filePath)) continue;
|
|
157
|
-
|
|
167
|
+
const status = statusByPath.get(filePath);
|
|
168
|
+
files.push({
|
|
169
|
+
added,
|
|
170
|
+
deleted,
|
|
171
|
+
path: filePath,
|
|
172
|
+
...status && { status }
|
|
173
|
+
});
|
|
158
174
|
}
|
|
159
175
|
commits.push({
|
|
160
176
|
hash,
|
|
@@ -162,11 +178,38 @@ function parseOutput(output, ignoreFilter) {
|
|
|
162
178
|
email,
|
|
163
179
|
date: new Date(dateStr),
|
|
164
180
|
message,
|
|
181
|
+
body,
|
|
182
|
+
parentHashes,
|
|
165
183
|
files
|
|
166
184
|
});
|
|
167
185
|
}
|
|
168
186
|
return commits;
|
|
169
187
|
}
|
|
188
|
+
function parseFileStatuses(lines) {
|
|
189
|
+
const statusByPath = /* @__PURE__ */ new Map();
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
if (!line.startsWith(":")) continue;
|
|
192
|
+
const parts = line.trim().split(" ");
|
|
193
|
+
if (parts.length < 2) continue;
|
|
194
|
+
const metadata = parts[0].split(/\s+/);
|
|
195
|
+
const statusToken = metadata[4] || "";
|
|
196
|
+
const status = normalizeStatus(statusToken);
|
|
197
|
+
const path = status === "renamed" || status === "copied" ? parts[2] || parts[1] : parts[1];
|
|
198
|
+
if (path) {
|
|
199
|
+
statusByPath.set(path, status);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return statusByPath;
|
|
203
|
+
}
|
|
204
|
+
function normalizeStatus(statusToken) {
|
|
205
|
+
const type = statusToken[0];
|
|
206
|
+
if (type === "A") return "added";
|
|
207
|
+
if (type === "M") return "modified";
|
|
208
|
+
if (type === "D") return "deleted";
|
|
209
|
+
if (type === "R") return "renamed";
|
|
210
|
+
if (type === "C") return "copied";
|
|
211
|
+
return "unknown";
|
|
212
|
+
}
|
|
170
213
|
async function loadGitignore(repoPath) {
|
|
171
214
|
const ignoreInstance = ig();
|
|
172
215
|
try {
|
|
@@ -183,377 +226,524 @@ async function loadGitignore(repoPath) {
|
|
|
183
226
|
}
|
|
184
227
|
|
|
185
228
|
// src/analyzer/stats-calculator.ts
|
|
186
|
-
import { extname } from "path";
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
229
|
+
import { extname as extname2 } from "path";
|
|
230
|
+
|
|
231
|
+
// src/analyzer/tech-debt/ai-detector.ts
|
|
232
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
233
|
+
import { join as join3 } from "path";
|
|
234
|
+
import { existsSync } from "fs";
|
|
235
|
+
function calculateAIScore(commit) {
|
|
236
|
+
return evaluateAIScore(commit).score;
|
|
237
|
+
}
|
|
238
|
+
function evaluateAIScore(commit) {
|
|
239
|
+
let score = 0;
|
|
240
|
+
const reasons = [];
|
|
241
|
+
const totalLines = commit.files.reduce((sum, f) => sum + f.added, 0);
|
|
242
|
+
const addedFiles = commit.files.filter((file) => file.status === "added").length;
|
|
243
|
+
if (mentionsAITool(commit.message) || mentionsAITool(commit.body || "")) {
|
|
244
|
+
score += 55;
|
|
245
|
+
reasons.push("AI \u5DE5\u5177\u5173\u952E\u8BCD");
|
|
246
|
+
}
|
|
247
|
+
if (mentionsGeneratedOutput(commit.message) || mentionsGeneratedOutput(commit.body || "")) {
|
|
248
|
+
score += 15;
|
|
249
|
+
reasons.push("\u751F\u6210\u4EA7\u7269\u5173\u952E\u8BCD");
|
|
250
|
+
}
|
|
251
|
+
if (totalLines > 100 && commit.files.length >= 3 && addedFiles / commit.files.length > 0.6) {
|
|
252
|
+
score += 20;
|
|
253
|
+
reasons.push("\u5927\u6279\u91CF\u65B0\u589E\u6587\u4EF6");
|
|
254
|
+
}
|
|
255
|
+
if (totalLines > 80 && commit.files.reduce((sum, f) => sum + f.deleted, 0) <= totalLines * 0.05) {
|
|
256
|
+
score += 15;
|
|
257
|
+
reasons.push("\u4F4E\u5220\u9664\u7387\u5927\u65B0\u589E");
|
|
258
|
+
}
|
|
259
|
+
if (totalLines > 1e3 && commit.files.length > 10 && isGenericMessage(commit.message)) {
|
|
260
|
+
score += 40;
|
|
261
|
+
reasons.push("\u5927\u578B\u901A\u7528\u63D0\u4EA4");
|
|
262
|
+
} else if (totalLines > 500) {
|
|
263
|
+
score += 20;
|
|
264
|
+
reasons.push("\u5927\u578B\u63D0\u4EA4");
|
|
265
|
+
}
|
|
266
|
+
const anomalousFiles = commit.files.filter((f) => hasAnomalousNaming(f.path));
|
|
267
|
+
if (anomalousFiles.length > 0) {
|
|
268
|
+
score += Math.min(30, anomalousFiles.length / commit.files.length * 30);
|
|
269
|
+
reasons.push("\u5F02\u5E38\u547D\u540D");
|
|
270
|
+
}
|
|
271
|
+
return { score: Math.min(100, score), reasons };
|
|
272
|
+
}
|
|
273
|
+
async function detectAICode(commits, repoPath) {
|
|
274
|
+
const suspiciousFiles = [];
|
|
275
|
+
for (const commit of commits) {
|
|
276
|
+
const totalLines = commit.files.reduce((sum, f) => sum + f.added, 0);
|
|
277
|
+
if (totalLines > 1e3 && commit.files.length > 10) {
|
|
278
|
+
if (isGenericMessage(commit.message)) {
|
|
279
|
+
suspiciousFiles.push({
|
|
280
|
+
commit: commit.hash,
|
|
281
|
+
reason: "large-commit-generic-message",
|
|
282
|
+
score: 80,
|
|
283
|
+
description: `\u5927\u578B\u63D0\u4EA4\uFF08${totalLines}\u884C\uFF09\u914D\u5408\u901A\u7528\u63D0\u4EA4\u4FE1\u606F`
|
|
284
|
+
});
|
|
285
|
+
}
|
|
228
286
|
}
|
|
229
|
-
authorStat.commits++;
|
|
230
|
-
authorStat.lastActiveDate = commit.date;
|
|
231
287
|
for (const file of commit.files) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!ftStat) {
|
|
240
|
-
ftStat = { extension: ext, added: 0, deleted: 0, fileCount: 0 };
|
|
241
|
-
fileTypeMap.set(ext, ftStat);
|
|
242
|
-
}
|
|
243
|
-
ftStat.added += file.added;
|
|
244
|
-
ftStat.deleted += file.deleted;
|
|
245
|
-
const topDir = getTopDirectory(file.path);
|
|
246
|
-
let dirStat = directoryMap.get(topDir);
|
|
247
|
-
if (!dirStat) {
|
|
248
|
-
dirStat = { path: topDir, commits: 0, linesChanged: 0 };
|
|
249
|
-
directoryMap.set(topDir, dirStat);
|
|
250
|
-
directoryCommitSet.set(topDir, /* @__PURE__ */ new Set());
|
|
288
|
+
if (hasAnomalousNaming(file.path)) {
|
|
289
|
+
suspiciousFiles.push({
|
|
290
|
+
file: file.path,
|
|
291
|
+
reason: "anomalous-naming",
|
|
292
|
+
score: 60,
|
|
293
|
+
description: "\u547D\u540D\u6A21\u5F0F\u5F02\u5E38\uFF08\u5982 function1, temp1\uFF09"
|
|
294
|
+
});
|
|
251
295
|
}
|
|
252
|
-
dirStat.linesChanged += file.added + file.deleted;
|
|
253
|
-
directoryCommitSet.get(topDir).add(commit.hash);
|
|
254
296
|
}
|
|
255
297
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
298
|
+
const uniqueFiles = new Set(commits.flatMap((c) => c.files.map((f) => f.path)));
|
|
299
|
+
const filesToCheck = Array.from(uniqueFiles).slice(0, 50);
|
|
300
|
+
for (const filePath of filesToCheck) {
|
|
301
|
+
const fullPath = join3(repoPath, filePath);
|
|
302
|
+
if (!existsSync(fullPath)) continue;
|
|
303
|
+
try {
|
|
304
|
+
const content = await readFile2(fullPath, "utf-8");
|
|
305
|
+
const commentDensity = calculateCommentDensity(content);
|
|
306
|
+
if (commentDensity > 0.3) {
|
|
307
|
+
suspiciousFiles.push({
|
|
308
|
+
file: filePath,
|
|
309
|
+
reason: "excessive-comments",
|
|
310
|
+
score: 70,
|
|
311
|
+
description: `\u6CE8\u91CA\u5BC6\u5EA6\u8FC7\u9AD8\uFF08${(commentDensity * 100).toFixed(1)}%\uFF09`
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
260
315
|
}
|
|
261
316
|
}
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
317
|
+
const uniqueSuspicious = Array.from(
|
|
318
|
+
new Map(suspiciousFiles.map((f) => [f.file || f.commit, f])).values()
|
|
319
|
+
);
|
|
320
|
+
return {
|
|
321
|
+
suspiciousFiles: uniqueSuspicious.sort((a, b) => b.score - a.score).slice(0, 20),
|
|
322
|
+
totalSuspicious: uniqueSuspicious.length
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function isGenericMessage(message) {
|
|
326
|
+
const genericPatterns = [
|
|
327
|
+
/^fix$/i,
|
|
328
|
+
/^update$/i,
|
|
329
|
+
/^refactor$/i,
|
|
330
|
+
/^fix bug$/i,
|
|
331
|
+
/^update code$/i,
|
|
332
|
+
/^bug fix$/i,
|
|
333
|
+
/^minor fix$/i,
|
|
334
|
+
/^wip$/i,
|
|
335
|
+
/^temp$/i,
|
|
336
|
+
/^test$/i
|
|
337
|
+
];
|
|
338
|
+
const normalized = message.trim().toLowerCase();
|
|
339
|
+
return genericPatterns.some((pattern) => pattern.test(normalized));
|
|
340
|
+
}
|
|
341
|
+
function mentionsAITool(text) {
|
|
342
|
+
const aiPatterns = [
|
|
343
|
+
/\bai\b/i,
|
|
344
|
+
/\bcopilot\b/i,
|
|
345
|
+
/\bclaude\b/i,
|
|
346
|
+
/\bcursor\b/i,
|
|
347
|
+
/\bchatgpt\b/i,
|
|
348
|
+
/\bgpt[-\s]?\d*\b/i
|
|
349
|
+
];
|
|
350
|
+
return aiPatterns.some((pattern) => pattern.test(text));
|
|
351
|
+
}
|
|
352
|
+
function mentionsGeneratedOutput(text) {
|
|
353
|
+
return /\bgenerated\b|\bcodegen\b|\bauto[-\s]?generated\b/i.test(text);
|
|
354
|
+
}
|
|
355
|
+
function hasAnomalousNaming(path) {
|
|
356
|
+
const anomalousPatterns = [
|
|
357
|
+
/function\d+/i,
|
|
358
|
+
/temp\d+/i,
|
|
359
|
+
/test\d+/i,
|
|
360
|
+
/file\d+/i,
|
|
361
|
+
/component\d+/i,
|
|
362
|
+
/generated/i,
|
|
363
|
+
/auto[-_]?generated/i,
|
|
364
|
+
/untitled/i,
|
|
365
|
+
/copy\d*/i
|
|
366
|
+
];
|
|
367
|
+
return anomalousPatterns.some((pattern) => pattern.test(path));
|
|
368
|
+
}
|
|
369
|
+
function calculateCommentDensity(content) {
|
|
370
|
+
const lines = content.split("\n");
|
|
371
|
+
let commentLines = 0;
|
|
372
|
+
let codeLines = 0;
|
|
373
|
+
for (const line of lines) {
|
|
374
|
+
const trimmed = line.trim();
|
|
375
|
+
if (!trimmed) continue;
|
|
376
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#")) {
|
|
377
|
+
commentLines++;
|
|
378
|
+
} else {
|
|
379
|
+
codeLines++;
|
|
267
380
|
}
|
|
268
|
-
fileCountByExt.get(ext).add(filePath);
|
|
269
381
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
382
|
+
const totalLines = commentLines + codeLines;
|
|
383
|
+
return totalLines > 0 ? commentLines / totalLines : 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/analyzer/ai-stats-calculator.ts
|
|
387
|
+
function calculateAIMetrics(commits) {
|
|
388
|
+
if (commits.length === 0) {
|
|
389
|
+
return {
|
|
390
|
+
aiMetrics: { totalAILines: 0, totalLines: 0, aiPercentage: 0, suspiciousCommits: 0, highAICommits: [] },
|
|
391
|
+
authorAIStats: [],
|
|
392
|
+
directoryAIStats: [],
|
|
393
|
+
aiTrends: []
|
|
394
|
+
};
|
|
275
395
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
396
|
+
const aiCommits = [];
|
|
397
|
+
const authorAIMap = /* @__PURE__ */ new Map();
|
|
398
|
+
const directoryAIMap = /* @__PURE__ */ new Map();
|
|
399
|
+
const weeklyAIMap = /* @__PURE__ */ new Map();
|
|
400
|
+
let totalAILines = 0;
|
|
401
|
+
let totalLines = 0;
|
|
402
|
+
let suspiciousCommits = 0;
|
|
403
|
+
for (const commit of commits) {
|
|
404
|
+
const aiEvaluation = evaluateAIScore(commit);
|
|
405
|
+
const aiScore = aiEvaluation.score;
|
|
406
|
+
const commitLines = commit.files.reduce((sum, f) => sum + f.added, 0);
|
|
407
|
+
const estimatedAILines = Math.round(commitLines * aiScore / 100);
|
|
408
|
+
totalLines += commitLines;
|
|
409
|
+
totalAILines += estimatedAILines;
|
|
410
|
+
if (aiScore > 50) {
|
|
411
|
+
suspiciousCommits++;
|
|
412
|
+
aiCommits.push({
|
|
413
|
+
hash: commit.hash,
|
|
414
|
+
author: commit.author,
|
|
415
|
+
date: commit.date,
|
|
416
|
+
aiScore,
|
|
417
|
+
estimatedAILines,
|
|
418
|
+
linesAdded: commitLines,
|
|
419
|
+
filesCount: commit.files.length,
|
|
420
|
+
message: commit.message,
|
|
421
|
+
reasons: aiEvaluation.reasons
|
|
422
|
+
});
|
|
280
423
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
424
|
+
const authorKey = commit.email.toLowerCase();
|
|
425
|
+
const authorData = authorAIMap.get(authorKey) || { aiLines: 0, totalLines: 0 };
|
|
426
|
+
authorData.aiLines += estimatedAILines;
|
|
427
|
+
authorData.totalLines += commitLines;
|
|
428
|
+
authorAIMap.set(authorKey, authorData);
|
|
429
|
+
for (const file of commit.files) {
|
|
430
|
+
const dir = getTopDirectory(file.path);
|
|
431
|
+
const fileAILines = Math.round(file.added * aiScore / 100);
|
|
432
|
+
const dirData = directoryAIMap.get(dir) || {
|
|
433
|
+
aiLines: 0,
|
|
434
|
+
totalLines: 0,
|
|
435
|
+
commits: /* @__PURE__ */ new Set(),
|
|
436
|
+
lastModified: commit.date
|
|
437
|
+
};
|
|
438
|
+
dirData.aiLines += fileAILines;
|
|
439
|
+
dirData.totalLines += file.added;
|
|
440
|
+
dirData.commits.add(commit.hash);
|
|
441
|
+
if (commit.date > dirData.lastModified) {
|
|
442
|
+
dirData.lastModified = commit.date;
|
|
443
|
+
}
|
|
444
|
+
directoryAIMap.set(dir, dirData);
|
|
445
|
+
}
|
|
446
|
+
const week = getWeekKey(commit.date);
|
|
447
|
+
const weekData = weeklyAIMap.get(week) || { aiLines: 0, totalLines: 0 };
|
|
448
|
+
weekData.aiLines += estimatedAILines;
|
|
449
|
+
weekData.totalLines += commitLines;
|
|
450
|
+
weeklyAIMap.set(week, weekData);
|
|
451
|
+
}
|
|
452
|
+
const aiMetrics = {
|
|
453
|
+
totalAILines,
|
|
454
|
+
totalLines,
|
|
455
|
+
aiPercentage: totalLines > 0 ? totalAILines / totalLines * 100 : 0,
|
|
456
|
+
suspiciousCommits,
|
|
457
|
+
highAICommits: aiCommits.sort((a, b) => b.aiScore - a.aiScore).slice(0, 20)
|
|
458
|
+
};
|
|
459
|
+
const authorAIStats = [];
|
|
460
|
+
const authorMap = /* @__PURE__ */ new Map();
|
|
461
|
+
for (const commit of commits) {
|
|
462
|
+
authorMap.set(commit.email.toLowerCase(), { name: commit.author, email: commit.email });
|
|
463
|
+
}
|
|
464
|
+
for (const [authorKey, data] of authorAIMap) {
|
|
465
|
+
const author = authorMap.get(authorKey);
|
|
466
|
+
if (author) {
|
|
467
|
+
authorAIStats.push({
|
|
468
|
+
author: author.name,
|
|
469
|
+
email: author.email,
|
|
470
|
+
aiLines: data.aiLines,
|
|
471
|
+
totalLines: data.totalLines,
|
|
472
|
+
aiPercentage: data.totalLines > 0 ? data.aiLines / data.totalLines * 100 : 0
|
|
295
473
|
});
|
|
296
474
|
}
|
|
475
|
+
}
|
|
476
|
+
const directoryAIStats = [];
|
|
477
|
+
for (const [path, data] of directoryAIMap) {
|
|
478
|
+
const aiPercentage = data.totalLines > 0 ? data.aiLines / data.totalLines * 100 : 0;
|
|
479
|
+
const isHighRisk = data.commits.size > 50 && aiPercentage > 60;
|
|
480
|
+
directoryAIStats.push({
|
|
481
|
+
path,
|
|
482
|
+
displayPath: path,
|
|
483
|
+
commits: data.commits.size,
|
|
484
|
+
aiLines: data.aiLines,
|
|
485
|
+
totalLines: data.totalLines,
|
|
486
|
+
aiPercentage,
|
|
487
|
+
lastModified: data.lastModified,
|
|
488
|
+
isHighRisk
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const aiTrends = Array.from(weeklyAIMap.entries()).map(([week, data]) => ({
|
|
492
|
+
week,
|
|
493
|
+
aiLines: data.aiLines,
|
|
494
|
+
totalLines: data.totalLines,
|
|
495
|
+
aiPercentage: data.totalLines > 0 ? data.aiLines / data.totalLines * 100 : 0
|
|
496
|
+
})).sort((a, b) => a.week.localeCompare(b.week));
|
|
497
|
+
return {
|
|
498
|
+
aiMetrics,
|
|
499
|
+
authorAIStats: authorAIStats.sort((a, b) => b.aiPercentage - a.aiPercentage),
|
|
500
|
+
directoryAIStats: directoryAIStats.sort((a, b) => b.aiPercentage - a.aiPercentage),
|
|
501
|
+
aiTrends
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function getTopDirectory(filePath) {
|
|
505
|
+
const parts = filePath.split("/");
|
|
506
|
+
return parts.length > 1 ? parts[0] : "(\u6839\u76EE\u5F55)";
|
|
507
|
+
}
|
|
508
|
+
function getWeekKey(date) {
|
|
509
|
+
const year = date.getFullYear();
|
|
510
|
+
const startOfYear = new Date(year, 0, 1);
|
|
511
|
+
const days = Math.floor((date.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1e3));
|
|
512
|
+
const week = Math.ceil((days + startOfYear.getDay() + 1) / 7);
|
|
513
|
+
return `${year}-W${week.toString().padStart(2, "0")}`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/analyzer/stats-utils.ts
|
|
517
|
+
function getTopDirectory2(filePath) {
|
|
518
|
+
const parts = filePath.split("/");
|
|
519
|
+
return parts.length > 1 ? parts[0] : "(\u6839\u76EE\u5F55)";
|
|
520
|
+
}
|
|
521
|
+
function formatDateKey(date) {
|
|
522
|
+
const y = date.getFullYear();
|
|
523
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
524
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
525
|
+
return `${y}-${m}-${d}`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/analyzer/extended-stats.ts
|
|
529
|
+
var MAX_FILES_PER_COMMIT_FOR_PAIRS = 80;
|
|
530
|
+
var MAX_DIRS_FOR_MATRIX = 12;
|
|
531
|
+
function calculateChangeSizeDistribution(commits) {
|
|
532
|
+
if (commits.length === 0) {
|
|
297
533
|
return {
|
|
298
|
-
|
|
299
|
-
|
|
534
|
+
buckets: emptyBuckets(),
|
|
535
|
+
avgChangeSize: 0,
|
|
536
|
+
medianChangeSize: 0,
|
|
537
|
+
p95ChangeSize: 0,
|
|
538
|
+
largeCommits: []
|
|
300
539
|
};
|
|
301
|
-
}
|
|
540
|
+
}
|
|
541
|
+
const sizes = [];
|
|
542
|
+
const commitInfo = [];
|
|
543
|
+
for (const commit of commits) {
|
|
544
|
+
let total2 = 0;
|
|
545
|
+
for (const f of commit.files) total2 += f.added + f.deleted;
|
|
546
|
+
sizes.push(total2);
|
|
547
|
+
commitInfo.push({
|
|
548
|
+
hash: commit.hash,
|
|
549
|
+
author: commit.author,
|
|
550
|
+
date: commit.date,
|
|
551
|
+
message: commit.message,
|
|
552
|
+
totalLines: total2,
|
|
553
|
+
filesCount: commit.files.length
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const counts = { XS: 0, S: 0, M: 0, L: 0, XL: 0 };
|
|
557
|
+
for (const size of sizes) {
|
|
558
|
+
if (size < 10) counts.XS++;
|
|
559
|
+
else if (size < 50) counts.S++;
|
|
560
|
+
else if (size < 200) counts.M++;
|
|
561
|
+
else if (size < 1e3) counts.L++;
|
|
562
|
+
else counts.XL++;
|
|
563
|
+
}
|
|
564
|
+
const total = sizes.length;
|
|
565
|
+
const buckets = [
|
|
566
|
+
{ label: "XS", range: "<10", count: counts.XS, percentage: pct(counts.XS, total) },
|
|
567
|
+
{ label: "S", range: "10-49", count: counts.S, percentage: pct(counts.S, total) },
|
|
568
|
+
{ label: "M", range: "50-199", count: counts.M, percentage: pct(counts.M, total) },
|
|
569
|
+
{ label: "L", range: "200-999", count: counts.L, percentage: pct(counts.L, total) },
|
|
570
|
+
{ label: "XL", range: "\u22651000", count: counts.XL, percentage: pct(counts.XL, total) }
|
|
571
|
+
];
|
|
572
|
+
const sorted = [...sizes].sort((a, b) => a - b);
|
|
573
|
+
const avgChangeSize = sizes.reduce((a, b) => a + b, 0) / sizes.length;
|
|
574
|
+
const medianChangeSize = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
575
|
+
const p95ChangeSize = sorted[Math.floor(sorted.length * 0.95)] ?? 0;
|
|
576
|
+
const largeCommits = commitInfo.sort((a, b) => b.totalLines - a.totalLines).slice(0, 20);
|
|
302
577
|
return {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
lastCommitDate: sorted[sorted.length - 1].date,
|
|
309
|
-
busiestDay,
|
|
310
|
-
authors,
|
|
311
|
-
fileTypes,
|
|
312
|
-
directories,
|
|
313
|
-
hourlyDistribution,
|
|
314
|
-
dailyHeatmap,
|
|
315
|
-
hourlyByAuthor: hourlyByAuthorArray,
|
|
316
|
-
quality: calculateQualityMetrics(sorted),
|
|
317
|
-
timePatterns: calculateTimePatterns(sorted),
|
|
318
|
-
trends: calculateTrends(sorted),
|
|
319
|
-
collaboration: calculateCollaboration(sorted),
|
|
320
|
-
messageStats: calculateMessageStats(sorted),
|
|
321
|
-
authorFileTypeContributions: calculateAuthorFileTypeContributions(sorted)
|
|
578
|
+
buckets,
|
|
579
|
+
avgChangeSize,
|
|
580
|
+
medianChangeSize,
|
|
581
|
+
p95ChangeSize,
|
|
582
|
+
largeCommits
|
|
322
583
|
};
|
|
323
584
|
}
|
|
324
|
-
function
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
merged.lastCommitDate = stats.lastCommitDate;
|
|
338
|
-
}
|
|
339
|
-
for (let i = 0; i < 24; i++) {
|
|
340
|
-
merged.hourlyDistribution[i] += stats.hourlyDistribution[i];
|
|
341
|
-
}
|
|
342
|
-
for (const [date, count] of Object.entries(stats.dailyHeatmap)) {
|
|
343
|
-
merged.dailyHeatmap[date] = (merged.dailyHeatmap[date] || 0) + count;
|
|
585
|
+
function calculateDirectoryCoupling(commits) {
|
|
586
|
+
if (commits.length === 0) {
|
|
587
|
+
return { pairs: [], matrix: [], directories: [] };
|
|
588
|
+
}
|
|
589
|
+
const dirCommitCount = /* @__PURE__ */ new Map();
|
|
590
|
+
const pairMap = /* @__PURE__ */ new Map();
|
|
591
|
+
for (const commit of commits) {
|
|
592
|
+
const files = commit.files.slice(0, MAX_FILES_PER_COMMIT_FOR_PAIRS);
|
|
593
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
594
|
+
for (const f of files) dirs.add(getTopDirectory2(f.path));
|
|
595
|
+
const dirArr = Array.from(dirs).sort();
|
|
596
|
+
for (const d of dirArr) {
|
|
597
|
+
dirCommitCount.set(d, (dirCommitCount.get(d) || 0) + 1);
|
|
344
598
|
}
|
|
345
|
-
for (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
existing.linesAdded += author.linesAdded;
|
|
352
|
-
existing.linesDeleted += author.linesDeleted;
|
|
353
|
-
if (author.lastActiveDate > existing.lastActiveDate) {
|
|
354
|
-
existing.lastActiveDate = author.lastActiveDate;
|
|
355
|
-
}
|
|
356
|
-
} else {
|
|
357
|
-
merged.authors.push({ ...author });
|
|
599
|
+
for (let i = 0; i < dirArr.length; i++) {
|
|
600
|
+
for (let j = i + 1; j < dirArr.length; j++) {
|
|
601
|
+
const key = `${dirArr[i]}|||${dirArr[j]}`;
|
|
602
|
+
const existing = pairMap.get(key) || { dir1: dirArr[i], dir2: dirArr[j], count: 0 };
|
|
603
|
+
existing.count++;
|
|
604
|
+
pairMap.set(key, existing);
|
|
358
605
|
}
|
|
359
606
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
607
|
+
}
|
|
608
|
+
const pairs = Array.from(pairMap.values()).map(({ dir1, dir2, count }) => {
|
|
609
|
+
const c1 = dirCommitCount.get(dir1) || 1;
|
|
610
|
+
const c2 = dirCommitCount.get(dir2) || 1;
|
|
611
|
+
const coupling = count / Math.min(c1, c2);
|
|
612
|
+
return { dir1, dir2, coOccurrence: count, coupling };
|
|
613
|
+
}).filter((p) => p.coOccurrence >= 2).sort((a, b) => b.coupling - a.coupling).slice(0, 30);
|
|
614
|
+
const topDirs = Array.from(dirCommitCount.entries()).sort((a, b) => b[1] - a[1]).slice(0, MAX_DIRS_FOR_MATRIX).map(([d]) => d);
|
|
615
|
+
const matrix = [];
|
|
616
|
+
for (const d1 of topDirs) {
|
|
617
|
+
for (const d2 of topDirs) {
|
|
618
|
+
if (d1 === d2) {
|
|
619
|
+
matrix.push({ dir1: d1, dir2: d2, value: dirCommitCount.get(d1) || 0 });
|
|
368
620
|
} else {
|
|
369
|
-
|
|
621
|
+
const key = d1 < d2 ? `${d1}|||${d2}` : `${d2}|||${d1}`;
|
|
622
|
+
matrix.push({ dir1: d1, dir2: d2, value: pairMap.get(key)?.count || 0 });
|
|
370
623
|
}
|
|
371
624
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
pairs,
|
|
628
|
+
matrix,
|
|
629
|
+
directories: topDirs
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function calculateAIQualityRisk(commits) {
|
|
633
|
+
if (commits.length === 0) {
|
|
634
|
+
return {
|
|
635
|
+
files: [],
|
|
636
|
+
scatter: [],
|
|
637
|
+
summary: { highAIHighChurn: 0, highAILowChurn: 0, lowAIHighChurn: 0, lowAILowChurn: 0 }
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
641
|
+
for (const commit of commits) {
|
|
642
|
+
const aiScore = calculateAIScore(commit);
|
|
643
|
+
for (const file of commit.files) {
|
|
644
|
+
const lines = file.added + file.deleted;
|
|
645
|
+
const entry = fileMap.get(file.path) || {
|
|
646
|
+
added: 0,
|
|
647
|
+
deleted: 0,
|
|
648
|
+
modifyCount: 0,
|
|
649
|
+
weightedAI: 0,
|
|
650
|
+
weight: 0
|
|
651
|
+
};
|
|
652
|
+
entry.added += file.added;
|
|
653
|
+
entry.deleted += file.deleted;
|
|
654
|
+
entry.modifyCount++;
|
|
655
|
+
const w = Math.max(lines, 1);
|
|
656
|
+
entry.weightedAI += aiScore * w;
|
|
657
|
+
entry.weight += w;
|
|
658
|
+
fileMap.set(file.path, entry);
|
|
380
659
|
}
|
|
381
|
-
merged.quality.avgFilesPerCommit += stats.quality.avgFilesPerCommit;
|
|
382
|
-
merged.quality.avgLinesPerCommit += stats.quality.avgLinesPerCommit;
|
|
383
|
-
merged.quality.churnRate += stats.quality.churnRate;
|
|
384
|
-
for (const hf of stats.quality.hotFiles) {
|
|
385
|
-
const existing = merged.quality.hotFiles.find((h) => h.path === hf.path);
|
|
386
|
-
if (existing) {
|
|
387
|
-
existing.modifyCount += hf.modifyCount;
|
|
388
|
-
for (const author of hf.authors) {
|
|
389
|
-
if (!existing.authors.includes(author)) {
|
|
390
|
-
existing.authors.push(author);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
} else {
|
|
394
|
-
merged.quality.hotFiles.push({ ...hf, authors: [...hf.authors] });
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
for (let i = 0; i < 7; i++) {
|
|
398
|
-
merged.timePatterns.weekdayDistribution[i] += stats.timePatterns.weekdayDistribution[i];
|
|
399
|
-
}
|
|
400
|
-
for (const wp of stats.trends.weeklyTrend) {
|
|
401
|
-
const existing = merged.trends.weeklyTrend.find((w) => w.week === wp.week);
|
|
402
|
-
if (existing) {
|
|
403
|
-
existing.commits += wp.commits;
|
|
404
|
-
existing.linesAdded += wp.linesAdded;
|
|
405
|
-
existing.linesDeleted += wp.linesDeleted;
|
|
406
|
-
} else {
|
|
407
|
-
merged.trends.weeklyTrend.push({ ...wp });
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
for (const cp of stats.trends.cumulativeLines) {
|
|
411
|
-
const existing = merged.trends.cumulativeLines.find(
|
|
412
|
-
(c) => c.date === cp.date
|
|
413
|
-
);
|
|
414
|
-
if (existing) {
|
|
415
|
-
existing.netLines += cp.netLines;
|
|
416
|
-
} else {
|
|
417
|
-
merged.trends.cumulativeLines.push({ ...cp });
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
for (const sf of stats.collaboration.soloFiles) {
|
|
421
|
-
const existing = merged.collaboration.soloFiles.find(
|
|
422
|
-
(s) => s.path === sf.path
|
|
423
|
-
);
|
|
424
|
-
if (existing) {
|
|
425
|
-
existing.commits += sf.commits;
|
|
426
|
-
} else {
|
|
427
|
-
merged.collaboration.soloFiles.push({ ...sf });
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
for (const ch of stats.collaboration.collaborationHotspots) {
|
|
431
|
-
const existing = merged.collaboration.collaborationHotspots.find(
|
|
432
|
-
(c) => c.path === ch.path
|
|
433
|
-
);
|
|
434
|
-
if (existing) {
|
|
435
|
-
existing.totalCommits += ch.totalCommits;
|
|
436
|
-
existing.authorCount = Math.max(existing.authorCount, ch.authorCount);
|
|
437
|
-
} else {
|
|
438
|
-
merged.collaboration.collaborationHotspots.push({ ...ch });
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
for (const [type, count] of Object.entries(stats.messageStats.typeDistribution)) {
|
|
442
|
-
merged.messageStats.typeDistribution[type] = (merged.messageStats.typeDistribution[type] || 0) + count;
|
|
443
|
-
}
|
|
444
|
-
merged.messageStats.avgMessageLength += stats.messageStats.avgMessageLength;
|
|
445
|
-
}
|
|
446
|
-
let busiestDay = { date: "", count: 0 };
|
|
447
|
-
for (const [date, count] of Object.entries(merged.dailyHeatmap)) {
|
|
448
|
-
if (count > busiestDay.count) {
|
|
449
|
-
busiestDay = { date, count };
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
merged.busiestDay = busiestDay;
|
|
453
|
-
merged.authors.sort((a, b) => b.commits - a.commits);
|
|
454
|
-
merged.fileTypes.sort(
|
|
455
|
-
(a, b) => b.added + b.deleted - (a.added + a.deleted)
|
|
456
|
-
);
|
|
457
|
-
merged.directories.sort((a, b) => b.linesChanged - a.linesChanged);
|
|
458
|
-
merged.directories = merged.directories.slice(0, 10);
|
|
459
|
-
const repoCount = statsList.length;
|
|
460
|
-
merged.quality.avgFilesPerCommit /= repoCount;
|
|
461
|
-
merged.quality.avgLinesPerCommit /= repoCount;
|
|
462
|
-
merged.quality.churnRate /= repoCount;
|
|
463
|
-
merged.quality.hotFiles.sort((a, b) => b.modifyCount - a.modifyCount);
|
|
464
|
-
merged.quality.hotFiles = merged.quality.hotFiles.slice(0, 10);
|
|
465
|
-
const totalWeekdayCommits = merged.timePatterns.weekdayDistribution.reduce(
|
|
466
|
-
(a, b) => a + b,
|
|
467
|
-
0
|
|
468
|
-
);
|
|
469
|
-
if (totalWeekdayCommits > 0) {
|
|
470
|
-
merged.timePatterns.weekendCommits = (merged.timePatterns.weekdayDistribution[5] + merged.timePatterns.weekdayDistribution[6]) / totalWeekdayCommits;
|
|
471
|
-
}
|
|
472
|
-
merged.trends.weeklyTrend.sort((a, b) => a.week.localeCompare(b.week));
|
|
473
|
-
merged.trends.cumulativeLines.sort((a, b) => a.date.localeCompare(b.date));
|
|
474
|
-
let cumulative = 0;
|
|
475
|
-
for (const point of merged.trends.cumulativeLines) {
|
|
476
|
-
cumulative += point.netLines;
|
|
477
|
-
point.netLines = cumulative;
|
|
478
660
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}).slice(0, 20);
|
|
506
|
-
return merged;
|
|
507
|
-
}
|
|
508
|
-
function emptyStats() {
|
|
661
|
+
const files = [];
|
|
662
|
+
const scatter = [];
|
|
663
|
+
let highAIHighChurn = 0, highAILowChurn = 0, lowAIHighChurn = 0, lowAILowChurn = 0;
|
|
664
|
+
for (const [path, data] of fileMap) {
|
|
665
|
+
const avgAI = data.weight > 0 ? data.weightedAI / data.weight : 0;
|
|
666
|
+
const churnRate = data.added > 0 ? data.deleted / data.added : 0;
|
|
667
|
+
const totalLines = data.added + data.deleted;
|
|
668
|
+
if (data.modifyCount < 2 && totalLines < 50) continue;
|
|
669
|
+
const riskScore = avgAI / 100 * Math.min(churnRate, 2) * Math.log2(data.modifyCount + 1) * 50;
|
|
670
|
+
files.push({
|
|
671
|
+
path,
|
|
672
|
+
aiScore: avgAI,
|
|
673
|
+
churnRate,
|
|
674
|
+
modifyCount: data.modifyCount,
|
|
675
|
+
totalLines,
|
|
676
|
+
riskScore
|
|
677
|
+
});
|
|
678
|
+
scatter.push({ path, aiScore: avgAI, churnRate, modifyCount: data.modifyCount });
|
|
679
|
+
const highAI = avgAI > 50;
|
|
680
|
+
const highChurn = churnRate > 0.5;
|
|
681
|
+
if (highAI && highChurn) highAIHighChurn++;
|
|
682
|
+
else if (highAI) highAILowChurn++;
|
|
683
|
+
else if (highChurn) lowAIHighChurn++;
|
|
684
|
+
else lowAILowChurn++;
|
|
685
|
+
}
|
|
686
|
+
files.sort((a, b) => b.riskScore - a.riskScore);
|
|
509
687
|
return {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
firstCommitDate: /* @__PURE__ */ new Date(),
|
|
515
|
-
lastCommitDate: /* @__PURE__ */ new Date(),
|
|
516
|
-
busiestDay: { date: "", count: 0 },
|
|
517
|
-
authors: [],
|
|
518
|
-
fileTypes: [],
|
|
519
|
-
directories: [],
|
|
520
|
-
hourlyDistribution: new Array(24).fill(0),
|
|
521
|
-
dailyHeatmap: {},
|
|
522
|
-
quality: emptyQualityMetrics(),
|
|
523
|
-
timePatterns: emptyTimePatterns(),
|
|
524
|
-
trends: emptyTrendData(),
|
|
525
|
-
collaboration: emptyCollaborationMetrics(),
|
|
526
|
-
messageStats: emptyMessageStats(),
|
|
527
|
-
authorFileTypeContributions: []
|
|
688
|
+
files: files.slice(0, 20),
|
|
689
|
+
scatter: scatter.slice(0, 500),
|
|
690
|
+
// 限制散点数
|
|
691
|
+
summary: { highAIHighChurn, highAILowChurn, lowAIHighChurn, lowAILowChurn }
|
|
528
692
|
};
|
|
529
693
|
}
|
|
530
|
-
function
|
|
531
|
-
|
|
532
|
-
|
|
694
|
+
function emptyBuckets() {
|
|
695
|
+
return [
|
|
696
|
+
{ label: "XS", range: "<10", count: 0, percentage: 0 },
|
|
697
|
+
{ label: "S", range: "10-49", count: 0, percentage: 0 },
|
|
698
|
+
{ label: "M", range: "50-199", count: 0, percentage: 0 },
|
|
699
|
+
{ label: "L", range: "200-999", count: 0, percentage: 0 },
|
|
700
|
+
{ label: "XL", range: "\u22651000", count: 0, percentage: 0 }
|
|
701
|
+
];
|
|
533
702
|
}
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
703
|
+
function pct(part, total) {
|
|
704
|
+
return total > 0 ? part / total * 100 : 0;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/analyzer/stats-metrics.ts
|
|
708
|
+
import { extname } from "path";
|
|
709
|
+
function calculateCommitDetails(commits) {
|
|
710
|
+
return commits.map((commit) => {
|
|
711
|
+
let linesAdded = 0;
|
|
712
|
+
let linesDeleted = 0;
|
|
713
|
+
for (const file of commit.files) {
|
|
714
|
+
linesAdded += file.added;
|
|
715
|
+
linesDeleted += file.deleted;
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
hash: commit.hash,
|
|
719
|
+
author: commit.author,
|
|
720
|
+
email: commit.email,
|
|
721
|
+
repoName: "",
|
|
722
|
+
date: commit.date,
|
|
723
|
+
message: commit.message,
|
|
724
|
+
linesAdded,
|
|
725
|
+
linesDeleted,
|
|
726
|
+
files: commit.files.map((file) => ({ ...file }))
|
|
727
|
+
};
|
|
728
|
+
});
|
|
539
729
|
}
|
|
540
730
|
function calculateQualityMetrics(commits) {
|
|
541
731
|
if (commits.length === 0) {
|
|
542
732
|
return emptyQualityMetrics();
|
|
543
733
|
}
|
|
544
|
-
const totalFiles = commits.reduce((sum,
|
|
734
|
+
const totalFiles = commits.reduce((sum, commit) => sum + commit.files.length, 0);
|
|
545
735
|
const avgFilesPerCommit = totalFiles / commits.length;
|
|
546
736
|
const totalLines = commits.reduce(
|
|
547
|
-
(sum,
|
|
737
|
+
(sum, commit) => sum + commit.files.reduce((s, file) => s + file.added + file.deleted, 0),
|
|
548
738
|
0
|
|
549
739
|
);
|
|
550
740
|
const avgLinesPerCommit = totalLines / commits.length;
|
|
551
741
|
const totalAdded = commits.reduce(
|
|
552
|
-
(sum,
|
|
742
|
+
(sum, commit) => sum + commit.files.reduce((s, file) => s + file.added, 0),
|
|
553
743
|
0
|
|
554
744
|
);
|
|
555
745
|
const totalDeleted = commits.reduce(
|
|
556
|
-
(sum,
|
|
746
|
+
(sum, commit) => sum + commit.files.reduce((s, file) => s + file.deleted, 0),
|
|
557
747
|
0
|
|
558
748
|
);
|
|
559
749
|
const churnRate = totalAdded > 0 ? totalDeleted / totalAdded : 0;
|
|
@@ -621,77 +811,13 @@ function calculateTimePatterns(commits) {
|
|
|
621
811
|
weekdayByAuthor: weekdayByAuthorArray
|
|
622
812
|
};
|
|
623
813
|
}
|
|
624
|
-
function calculateStreaks(sortedCommits) {
|
|
625
|
-
if (sortedCommits.length === 0) {
|
|
626
|
-
return { longestStreak: 0, currentStreak: 0 };
|
|
627
|
-
}
|
|
628
|
-
const uniqueDates = /* @__PURE__ */ new Set();
|
|
629
|
-
for (const commit of sortedCommits) {
|
|
630
|
-
uniqueDates.add(formatDateKey(commit.date));
|
|
631
|
-
}
|
|
632
|
-
const sortedDates = Array.from(uniqueDates).sort();
|
|
633
|
-
if (sortedDates.length === 0) {
|
|
634
|
-
return { longestStreak: 0, currentStreak: 0 };
|
|
635
|
-
}
|
|
636
|
-
let longestStreak = 1;
|
|
637
|
-
let currentStreakCount = 1;
|
|
638
|
-
let tempStreak = 1;
|
|
639
|
-
for (let i = 1; i < sortedDates.length; i++) {
|
|
640
|
-
const prevDate = new Date(sortedDates[i - 1]);
|
|
641
|
-
const currDate = new Date(sortedDates[i]);
|
|
642
|
-
const diffDays = Math.round(
|
|
643
|
-
(currDate.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
644
|
-
);
|
|
645
|
-
if (diffDays === 1) {
|
|
646
|
-
tempStreak++;
|
|
647
|
-
} else {
|
|
648
|
-
tempStreak = 1;
|
|
649
|
-
}
|
|
650
|
-
longestStreak = Math.max(longestStreak, tempStreak);
|
|
651
|
-
}
|
|
652
|
-
const today = formatDateKey(/* @__PURE__ */ new Date());
|
|
653
|
-
const lastCommitDate = sortedDates[sortedDates.length - 1];
|
|
654
|
-
const lastDate = new Date(lastCommitDate);
|
|
655
|
-
const todayDate = new Date(today);
|
|
656
|
-
const daysSinceLastCommit = Math.round(
|
|
657
|
-
(todayDate.getTime() - lastDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
658
|
-
);
|
|
659
|
-
if (daysSinceLastCommit <= 1) {
|
|
660
|
-
currentStreakCount = 1;
|
|
661
|
-
for (let i = sortedDates.length - 2; i >= 0; i--) {
|
|
662
|
-
const currDate = new Date(sortedDates[i + 1]);
|
|
663
|
-
const prevDate = new Date(sortedDates[i]);
|
|
664
|
-
const diffDays = Math.round(
|
|
665
|
-
(currDate.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
666
|
-
);
|
|
667
|
-
if (diffDays === 1) {
|
|
668
|
-
currentStreakCount++;
|
|
669
|
-
} else {
|
|
670
|
-
break;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
} else {
|
|
674
|
-
currentStreakCount = 0;
|
|
675
|
-
}
|
|
676
|
-
return { longestStreak, currentStreak: currentStreakCount };
|
|
677
|
-
}
|
|
678
|
-
function getWeekKey(date) {
|
|
679
|
-
const d = new Date(date);
|
|
680
|
-
d.setHours(0, 0, 0, 0);
|
|
681
|
-
d.setDate(d.getDate() + 4 - (d.getDay() || 7));
|
|
682
|
-
const yearStart = new Date(d.getFullYear(), 0, 1);
|
|
683
|
-
const weekNo = Math.ceil(
|
|
684
|
-
((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7
|
|
685
|
-
);
|
|
686
|
-
return `${d.getFullYear()}-W${String(weekNo).padStart(2, "0")}`;
|
|
687
|
-
}
|
|
688
814
|
function calculateTrends(commits) {
|
|
689
815
|
if (commits.length === 0) {
|
|
690
816
|
return emptyTrendData();
|
|
691
817
|
}
|
|
692
818
|
const weekMap = /* @__PURE__ */ new Map();
|
|
693
819
|
for (const commit of commits) {
|
|
694
|
-
const week =
|
|
820
|
+
const week = getWeekKey2(commit.date);
|
|
695
821
|
const entry = weekMap.get(week) || {
|
|
696
822
|
week,
|
|
697
823
|
commits: 0,
|
|
@@ -711,7 +837,7 @@ function calculateTrends(commits) {
|
|
|
711
837
|
const dailyNet = /* @__PURE__ */ new Map();
|
|
712
838
|
for (const commit of commits) {
|
|
713
839
|
const dateKey = formatDateKey(commit.date);
|
|
714
|
-
const net = commit.files.reduce((sum,
|
|
840
|
+
const net = commit.files.reduce((sum, file) => sum + file.added - file.deleted, 0);
|
|
715
841
|
dailyNet.set(dateKey, (dailyNet.get(dateKey) || 0) + net);
|
|
716
842
|
}
|
|
717
843
|
let cumulative = 0;
|
|
@@ -817,53 +943,545 @@ function calculateAuthorFileTypeContributions(commits) {
|
|
|
817
943
|
if (!commitCountMap.has(key)) {
|
|
818
944
|
commitCountMap.set(key, /* @__PURE__ */ new Set());
|
|
819
945
|
}
|
|
820
|
-
commitCountMap.get(key).add(commit.hash);
|
|
946
|
+
commitCountMap.get(key).add(commit.hash);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
for (const [key, contribution] of contributionMap) {
|
|
950
|
+
contribution.commits = commitCountMap.get(key)?.size || 0;
|
|
951
|
+
contribution.fileCount = uniqueFilesMap.get(key)?.size || 0;
|
|
952
|
+
}
|
|
953
|
+
return Array.from(contributionMap.values()).sort((a, b) => {
|
|
954
|
+
const totalA = a.linesAdded + a.linesDeleted;
|
|
955
|
+
const totalB = b.linesAdded + b.linesDeleted;
|
|
956
|
+
return totalB - totalA;
|
|
957
|
+
}).slice(0, 20);
|
|
958
|
+
}
|
|
959
|
+
function emptyQualityMetrics() {
|
|
960
|
+
return {
|
|
961
|
+
avgFilesPerCommit: 0,
|
|
962
|
+
avgLinesPerCommit: 0,
|
|
963
|
+
churnRate: 0,
|
|
964
|
+
hotFiles: []
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function emptyTimePatterns() {
|
|
968
|
+
return {
|
|
969
|
+
weekdayDistribution: new Array(7).fill(0),
|
|
970
|
+
weekendCommits: 0,
|
|
971
|
+
avgCommitInterval: 0,
|
|
972
|
+
longestStreak: 0,
|
|
973
|
+
currentStreak: 0
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function emptyTrendData() {
|
|
977
|
+
return {
|
|
978
|
+
weeklyTrend: [],
|
|
979
|
+
cumulativeLines: []
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function emptyCollaborationMetrics() {
|
|
983
|
+
return {
|
|
984
|
+
soloFiles: [],
|
|
985
|
+
collaborationHotspots: []
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function emptyMessageStats() {
|
|
989
|
+
return {
|
|
990
|
+
typeDistribution: {},
|
|
991
|
+
avgMessageLength: 0
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function calculateStreaks(sortedCommits) {
|
|
995
|
+
if (sortedCommits.length === 0) {
|
|
996
|
+
return { longestStreak: 0, currentStreak: 0 };
|
|
997
|
+
}
|
|
998
|
+
const uniqueDates = /* @__PURE__ */ new Set();
|
|
999
|
+
for (const commit of sortedCommits) {
|
|
1000
|
+
uniqueDates.add(formatDateKey(commit.date));
|
|
1001
|
+
}
|
|
1002
|
+
const sortedDates = Array.from(uniqueDates).sort();
|
|
1003
|
+
if (sortedDates.length === 0) {
|
|
1004
|
+
return { longestStreak: 0, currentStreak: 0 };
|
|
1005
|
+
}
|
|
1006
|
+
let longestStreak = 1;
|
|
1007
|
+
let currentStreakCount = 1;
|
|
1008
|
+
let tempStreak = 1;
|
|
1009
|
+
for (let i = 1; i < sortedDates.length; i++) {
|
|
1010
|
+
const prevDate = new Date(sortedDates[i - 1]);
|
|
1011
|
+
const currDate = new Date(sortedDates[i]);
|
|
1012
|
+
const diffDays = Math.round(
|
|
1013
|
+
(currDate.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
1014
|
+
);
|
|
1015
|
+
if (diffDays === 1) {
|
|
1016
|
+
tempStreak++;
|
|
1017
|
+
} else {
|
|
1018
|
+
tempStreak = 1;
|
|
1019
|
+
}
|
|
1020
|
+
longestStreak = Math.max(longestStreak, tempStreak);
|
|
1021
|
+
}
|
|
1022
|
+
const today = formatDateKey(/* @__PURE__ */ new Date());
|
|
1023
|
+
const lastCommitDate = sortedDates[sortedDates.length - 1];
|
|
1024
|
+
const lastDate = new Date(lastCommitDate);
|
|
1025
|
+
const todayDate = new Date(today);
|
|
1026
|
+
const daysSinceLastCommit = Math.round(
|
|
1027
|
+
(todayDate.getTime() - lastDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
1028
|
+
);
|
|
1029
|
+
if (daysSinceLastCommit <= 1) {
|
|
1030
|
+
currentStreakCount = 1;
|
|
1031
|
+
for (let i = sortedDates.length - 2; i >= 0; i--) {
|
|
1032
|
+
const currDate = new Date(sortedDates[i + 1]);
|
|
1033
|
+
const prevDate = new Date(sortedDates[i]);
|
|
1034
|
+
const diffDays = Math.round(
|
|
1035
|
+
(currDate.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
1036
|
+
);
|
|
1037
|
+
if (diffDays === 1) {
|
|
1038
|
+
currentStreakCount++;
|
|
1039
|
+
} else {
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
currentStreakCount = 0;
|
|
1045
|
+
}
|
|
1046
|
+
return { longestStreak, currentStreak: currentStreakCount };
|
|
1047
|
+
}
|
|
1048
|
+
function getWeekKey2(date) {
|
|
1049
|
+
const d = new Date(date);
|
|
1050
|
+
d.setHours(0, 0, 0, 0);
|
|
1051
|
+
d.setDate(d.getDate() + 4 - (d.getDay() || 7));
|
|
1052
|
+
const yearStart = new Date(d.getFullYear(), 0, 1);
|
|
1053
|
+
const weekNo = Math.ceil(
|
|
1054
|
+
((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7
|
|
1055
|
+
);
|
|
1056
|
+
return `${d.getFullYear()}-W${String(weekNo).padStart(2, "0")}`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/analyzer/stats-empty.ts
|
|
1060
|
+
function emptyStats() {
|
|
1061
|
+
return {
|
|
1062
|
+
totalCommits: 0,
|
|
1063
|
+
linesAdded: 0,
|
|
1064
|
+
linesDeleted: 0,
|
|
1065
|
+
filesChanged: 0,
|
|
1066
|
+
firstCommitDate: /* @__PURE__ */ new Date(),
|
|
1067
|
+
lastCommitDate: /* @__PURE__ */ new Date(),
|
|
1068
|
+
busiestDay: { date: "", count: 0 },
|
|
1069
|
+
authors: [],
|
|
1070
|
+
fileTypes: [],
|
|
1071
|
+
directories: [],
|
|
1072
|
+
hourlyDistribution: new Array(24).fill(0),
|
|
1073
|
+
dailyHeatmap: {},
|
|
1074
|
+
quality: emptyQualityMetrics(),
|
|
1075
|
+
timePatterns: emptyTimePatterns(),
|
|
1076
|
+
trends: emptyTrendData(),
|
|
1077
|
+
collaboration: emptyCollaborationMetrics(),
|
|
1078
|
+
messageStats: emptyMessageStats(),
|
|
1079
|
+
authorFileTypeContributions: [],
|
|
1080
|
+
commitDetails: []
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/analyzer/stats-calculator.ts
|
|
1085
|
+
function calculateStats(commits) {
|
|
1086
|
+
if (commits.length === 0) {
|
|
1087
|
+
return emptyStats();
|
|
1088
|
+
}
|
|
1089
|
+
const sorted = [...commits].sort(
|
|
1090
|
+
(a, b) => a.date.getTime() - b.date.getTime()
|
|
1091
|
+
);
|
|
1092
|
+
let totalLinesAdded = 0;
|
|
1093
|
+
let totalLinesDeleted = 0;
|
|
1094
|
+
const allFilePaths = /* @__PURE__ */ new Set();
|
|
1095
|
+
const authorMap = /* @__PURE__ */ new Map();
|
|
1096
|
+
const fileTypeMap = /* @__PURE__ */ new Map();
|
|
1097
|
+
const directoryMap = /* @__PURE__ */ new Map();
|
|
1098
|
+
const directoryCommitSet = /* @__PURE__ */ new Map();
|
|
1099
|
+
const hourlyDistribution = new Array(24).fill(0);
|
|
1100
|
+
const dailyHeatmap = {};
|
|
1101
|
+
const hourlyByAuthor = /* @__PURE__ */ new Map();
|
|
1102
|
+
const dailyCounts = /* @__PURE__ */ new Map();
|
|
1103
|
+
for (const commit of sorted) {
|
|
1104
|
+
const hour = commit.date.getHours();
|
|
1105
|
+
hourlyDistribution[hour]++;
|
|
1106
|
+
if (!hourlyByAuthor.has(hour)) {
|
|
1107
|
+
hourlyByAuthor.set(hour, /* @__PURE__ */ new Map());
|
|
1108
|
+
}
|
|
1109
|
+
const hourAuthors = hourlyByAuthor.get(hour);
|
|
1110
|
+
hourAuthors.set(commit.author, (hourAuthors.get(commit.author) || 0) + 1);
|
|
1111
|
+
const dateKey = formatDateKey(commit.date);
|
|
1112
|
+
dailyHeatmap[dateKey] = (dailyHeatmap[dateKey] || 0) + 1;
|
|
1113
|
+
dailyCounts.set(dateKey, (dailyCounts.get(dateKey) || 0) + 1);
|
|
1114
|
+
const authorKey = commit.email.toLowerCase();
|
|
1115
|
+
let authorStat = authorMap.get(authorKey);
|
|
1116
|
+
if (!authorStat) {
|
|
1117
|
+
authorStat = {
|
|
1118
|
+
name: commit.author,
|
|
1119
|
+
email: commit.email,
|
|
1120
|
+
commits: 0,
|
|
1121
|
+
linesAdded: 0,
|
|
1122
|
+
linesDeleted: 0,
|
|
1123
|
+
lastActiveDate: commit.date
|
|
1124
|
+
};
|
|
1125
|
+
authorMap.set(authorKey, authorStat);
|
|
1126
|
+
}
|
|
1127
|
+
authorStat.commits++;
|
|
1128
|
+
authorStat.lastActiveDate = commit.date;
|
|
1129
|
+
for (const file of commit.files) {
|
|
1130
|
+
totalLinesAdded += file.added;
|
|
1131
|
+
totalLinesDeleted += file.deleted;
|
|
1132
|
+
allFilePaths.add(file.path);
|
|
1133
|
+
authorStat.linesAdded += file.added;
|
|
1134
|
+
authorStat.linesDeleted += file.deleted;
|
|
1135
|
+
const ext = extname2(file.path).toLowerCase() || "(\u65E0\u6269\u5C55\u540D)";
|
|
1136
|
+
let ftStat = fileTypeMap.get(ext);
|
|
1137
|
+
if (!ftStat) {
|
|
1138
|
+
ftStat = { extension: ext, added: 0, deleted: 0, fileCount: 0 };
|
|
1139
|
+
fileTypeMap.set(ext, ftStat);
|
|
1140
|
+
}
|
|
1141
|
+
ftStat.added += file.added;
|
|
1142
|
+
ftStat.deleted += file.deleted;
|
|
1143
|
+
const topDir = getTopDirectory2(file.path);
|
|
1144
|
+
let dirStat = directoryMap.get(topDir);
|
|
1145
|
+
if (!dirStat) {
|
|
1146
|
+
dirStat = { path: topDir, commits: 0, linesChanged: 0 };
|
|
1147
|
+
directoryMap.set(topDir, dirStat);
|
|
1148
|
+
directoryCommitSet.set(topDir, /* @__PURE__ */ new Set());
|
|
1149
|
+
}
|
|
1150
|
+
dirStat.linesChanged += file.added + file.deleted;
|
|
1151
|
+
directoryCommitSet.get(topDir).add(commit.hash);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
for (const [dir, commitSet] of directoryCommitSet) {
|
|
1155
|
+
const dirStat = directoryMap.get(dir);
|
|
1156
|
+
if (dirStat) {
|
|
1157
|
+
dirStat.commits = commitSet.size;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const fileCountByExt = /* @__PURE__ */ new Map();
|
|
1161
|
+
for (const filePath of allFilePaths) {
|
|
1162
|
+
const ext = extname2(filePath).toLowerCase() || "(\u65E0\u6269\u5C55\u540D)";
|
|
1163
|
+
if (!fileCountByExt.has(ext)) {
|
|
1164
|
+
fileCountByExt.set(ext, /* @__PURE__ */ new Set());
|
|
1165
|
+
}
|
|
1166
|
+
fileCountByExt.get(ext).add(filePath);
|
|
1167
|
+
}
|
|
1168
|
+
for (const [ext, files] of fileCountByExt) {
|
|
1169
|
+
const ftStat = fileTypeMap.get(ext);
|
|
1170
|
+
if (ftStat) {
|
|
1171
|
+
ftStat.fileCount = files.size;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
let busiestDay = { date: "", count: 0 };
|
|
1175
|
+
for (const [date, count] of dailyCounts) {
|
|
1176
|
+
if (count > busiestDay.count) {
|
|
1177
|
+
busiestDay = { date, count };
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const authors = Array.from(authorMap.values()).sort(
|
|
1181
|
+
(a, b) => b.commits - a.commits
|
|
1182
|
+
);
|
|
1183
|
+
const fileTypes = Array.from(fileTypeMap.values()).sort(
|
|
1184
|
+
(a, b) => b.added + b.deleted - (a.added + a.deleted)
|
|
1185
|
+
);
|
|
1186
|
+
const directories = Array.from(directoryMap.values()).sort((a, b) => b.linesChanged - a.linesChanged).slice(0, 10);
|
|
1187
|
+
const hourlyByAuthorArray = Array.from({ length: 24 }, (_, hour) => {
|
|
1188
|
+
const authorMap2 = hourlyByAuthor.get(hour);
|
|
1189
|
+
const authors2 = {};
|
|
1190
|
+
if (authorMap2) {
|
|
1191
|
+
authorMap2.forEach((count, author) => {
|
|
1192
|
+
authors2[author] = count;
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
return {
|
|
1196
|
+
count: hourlyDistribution[hour],
|
|
1197
|
+
authors: authors2
|
|
1198
|
+
};
|
|
1199
|
+
});
|
|
1200
|
+
const aiStats = calculateAIMetrics(sorted);
|
|
1201
|
+
return {
|
|
1202
|
+
totalCommits: sorted.length,
|
|
1203
|
+
linesAdded: totalLinesAdded,
|
|
1204
|
+
linesDeleted: totalLinesDeleted,
|
|
1205
|
+
filesChanged: allFilePaths.size,
|
|
1206
|
+
firstCommitDate: sorted[0].date,
|
|
1207
|
+
lastCommitDate: sorted[sorted.length - 1].date,
|
|
1208
|
+
busiestDay,
|
|
1209
|
+
authors,
|
|
1210
|
+
fileTypes,
|
|
1211
|
+
directories,
|
|
1212
|
+
hourlyDistribution,
|
|
1213
|
+
dailyHeatmap,
|
|
1214
|
+
hourlyByAuthor: hourlyByAuthorArray,
|
|
1215
|
+
quality: calculateQualityMetrics(sorted),
|
|
1216
|
+
timePatterns: calculateTimePatterns(sorted),
|
|
1217
|
+
trends: calculateTrends(sorted),
|
|
1218
|
+
collaboration: calculateCollaboration(sorted),
|
|
1219
|
+
messageStats: calculateMessageStats(sorted),
|
|
1220
|
+
authorFileTypeContributions: calculateAuthorFileTypeContributions(sorted),
|
|
1221
|
+
commitDetails: calculateCommitDetails(sorted),
|
|
1222
|
+
aiMetrics: aiStats.aiMetrics,
|
|
1223
|
+
authorAIStats: aiStats.authorAIStats,
|
|
1224
|
+
directoryAIStats: aiStats.directoryAIStats,
|
|
1225
|
+
aiTrends: aiStats.aiTrends,
|
|
1226
|
+
changeSizeDistribution: calculateChangeSizeDistribution(sorted),
|
|
1227
|
+
directoryCoupling: calculateDirectoryCoupling(sorted),
|
|
1228
|
+
aiQualityRisk: calculateAIQualityRisk(sorted)
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function mergeStats(statsList) {
|
|
1232
|
+
if (statsList.length === 0) return emptyStats();
|
|
1233
|
+
if (statsList.length === 1) return statsList[0];
|
|
1234
|
+
const merged = emptyStats();
|
|
1235
|
+
for (const stats of statsList) {
|
|
1236
|
+
merged.totalCommits += stats.totalCommits;
|
|
1237
|
+
merged.linesAdded += stats.linesAdded;
|
|
1238
|
+
merged.linesDeleted += stats.linesDeleted;
|
|
1239
|
+
merged.filesChanged += stats.filesChanged;
|
|
1240
|
+
if (!merged.firstCommitDate || stats.firstCommitDate < merged.firstCommitDate) {
|
|
1241
|
+
merged.firstCommitDate = stats.firstCommitDate;
|
|
1242
|
+
}
|
|
1243
|
+
if (!merged.lastCommitDate || stats.lastCommitDate > merged.lastCommitDate) {
|
|
1244
|
+
merged.lastCommitDate = stats.lastCommitDate;
|
|
1245
|
+
}
|
|
1246
|
+
for (let i = 0; i < 24; i++) {
|
|
1247
|
+
merged.hourlyDistribution[i] += stats.hourlyDistribution[i];
|
|
1248
|
+
}
|
|
1249
|
+
for (const [date, count] of Object.entries(stats.dailyHeatmap)) {
|
|
1250
|
+
merged.dailyHeatmap[date] = (merged.dailyHeatmap[date] || 0) + count;
|
|
1251
|
+
}
|
|
1252
|
+
for (const author of stats.authors) {
|
|
1253
|
+
const existing = merged.authors.find(
|
|
1254
|
+
(a) => a.email.toLowerCase() === author.email.toLowerCase()
|
|
1255
|
+
);
|
|
1256
|
+
if (existing) {
|
|
1257
|
+
existing.commits += author.commits;
|
|
1258
|
+
existing.linesAdded += author.linesAdded;
|
|
1259
|
+
existing.linesDeleted += author.linesDeleted;
|
|
1260
|
+
if (author.lastActiveDate > existing.lastActiveDate) {
|
|
1261
|
+
existing.lastActiveDate = author.lastActiveDate;
|
|
1262
|
+
}
|
|
1263
|
+
} else {
|
|
1264
|
+
merged.authors.push({ ...author });
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
for (const ft of stats.fileTypes) {
|
|
1268
|
+
const existing = merged.fileTypes.find(
|
|
1269
|
+
(f) => f.extension === ft.extension
|
|
1270
|
+
);
|
|
1271
|
+
if (existing) {
|
|
1272
|
+
existing.added += ft.added;
|
|
1273
|
+
existing.deleted += ft.deleted;
|
|
1274
|
+
existing.fileCount += ft.fileCount;
|
|
1275
|
+
} else {
|
|
1276
|
+
merged.fileTypes.push({ ...ft });
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
for (const dir of stats.directories) {
|
|
1280
|
+
const existing = merged.directories.find((d) => d.path === dir.path);
|
|
1281
|
+
if (existing) {
|
|
1282
|
+
existing.commits += dir.commits;
|
|
1283
|
+
existing.linesChanged += dir.linesChanged;
|
|
1284
|
+
} else {
|
|
1285
|
+
merged.directories.push({ ...dir });
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
merged.quality.avgFilesPerCommit += stats.quality.avgFilesPerCommit;
|
|
1289
|
+
merged.quality.avgLinesPerCommit += stats.quality.avgLinesPerCommit;
|
|
1290
|
+
merged.quality.churnRate += stats.quality.churnRate;
|
|
1291
|
+
for (const hf of stats.quality.hotFiles) {
|
|
1292
|
+
const existing = merged.quality.hotFiles.find((h) => h.path === hf.path);
|
|
1293
|
+
if (existing) {
|
|
1294
|
+
existing.modifyCount += hf.modifyCount;
|
|
1295
|
+
for (const author of hf.authors) {
|
|
1296
|
+
if (!existing.authors.includes(author)) {
|
|
1297
|
+
existing.authors.push(author);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
} else {
|
|
1301
|
+
merged.quality.hotFiles.push({ ...hf, authors: [...hf.authors] });
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
for (let i = 0; i < 7; i++) {
|
|
1305
|
+
merged.timePatterns.weekdayDistribution[i] += stats.timePatterns.weekdayDistribution[i];
|
|
1306
|
+
}
|
|
1307
|
+
for (const wp of stats.trends.weeklyTrend) {
|
|
1308
|
+
const existing = merged.trends.weeklyTrend.find((w) => w.week === wp.week);
|
|
1309
|
+
if (existing) {
|
|
1310
|
+
existing.commits += wp.commits;
|
|
1311
|
+
existing.linesAdded += wp.linesAdded;
|
|
1312
|
+
existing.linesDeleted += wp.linesDeleted;
|
|
1313
|
+
} else {
|
|
1314
|
+
merged.trends.weeklyTrend.push({ ...wp });
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
for (const cp of stats.trends.cumulativeLines) {
|
|
1318
|
+
const existing = merged.trends.cumulativeLines.find(
|
|
1319
|
+
(c) => c.date === cp.date
|
|
1320
|
+
);
|
|
1321
|
+
if (existing) {
|
|
1322
|
+
existing.netLines += cp.netLines;
|
|
1323
|
+
} else {
|
|
1324
|
+
merged.trends.cumulativeLines.push({ ...cp });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
for (const sf of stats.collaboration.soloFiles) {
|
|
1328
|
+
const existing = merged.collaboration.soloFiles.find(
|
|
1329
|
+
(s) => s.path === sf.path
|
|
1330
|
+
);
|
|
1331
|
+
if (existing) {
|
|
1332
|
+
existing.commits += sf.commits;
|
|
1333
|
+
} else {
|
|
1334
|
+
merged.collaboration.soloFiles.push({ ...sf });
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
for (const ch of stats.collaboration.collaborationHotspots) {
|
|
1338
|
+
const existing = merged.collaboration.collaborationHotspots.find(
|
|
1339
|
+
(c) => c.path === ch.path
|
|
1340
|
+
);
|
|
1341
|
+
if (existing) {
|
|
1342
|
+
existing.totalCommits += ch.totalCommits;
|
|
1343
|
+
existing.authorCount = Math.max(existing.authorCount, ch.authorCount);
|
|
1344
|
+
} else {
|
|
1345
|
+
merged.collaboration.collaborationHotspots.push({ ...ch });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
for (const [type, count] of Object.entries(stats.messageStats.typeDistribution)) {
|
|
1349
|
+
merged.messageStats.typeDistribution[type] = (merged.messageStats.typeDistribution[type] || 0) + count;
|
|
1350
|
+
}
|
|
1351
|
+
merged.messageStats.avgMessageLength += stats.messageStats.avgMessageLength;
|
|
1352
|
+
merged.commitDetails.push(...stats.commitDetails);
|
|
1353
|
+
}
|
|
1354
|
+
let busiestDay = { date: "", count: 0 };
|
|
1355
|
+
for (const [date, count] of Object.entries(merged.dailyHeatmap)) {
|
|
1356
|
+
if (count > busiestDay.count) {
|
|
1357
|
+
busiestDay = { date, count };
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
merged.busiestDay = busiestDay;
|
|
1361
|
+
merged.authors.sort((a, b) => b.commits - a.commits);
|
|
1362
|
+
merged.fileTypes.sort(
|
|
1363
|
+
(a, b) => b.added + b.deleted - (a.added + a.deleted)
|
|
1364
|
+
);
|
|
1365
|
+
merged.directories.sort((a, b) => b.linesChanged - a.linesChanged);
|
|
1366
|
+
merged.directories = merged.directories.slice(0, 10);
|
|
1367
|
+
const repoCount = statsList.length;
|
|
1368
|
+
merged.quality.avgFilesPerCommit /= repoCount;
|
|
1369
|
+
merged.quality.avgLinesPerCommit /= repoCount;
|
|
1370
|
+
merged.quality.churnRate /= repoCount;
|
|
1371
|
+
merged.quality.hotFiles.sort((a, b) => b.modifyCount - a.modifyCount);
|
|
1372
|
+
merged.quality.hotFiles = merged.quality.hotFiles.slice(0, 10);
|
|
1373
|
+
const totalWeekdayCommits = merged.timePatterns.weekdayDistribution.reduce(
|
|
1374
|
+
(a, b) => a + b,
|
|
1375
|
+
0
|
|
1376
|
+
);
|
|
1377
|
+
if (totalWeekdayCommits > 0) {
|
|
1378
|
+
merged.timePatterns.weekendCommits = (merged.timePatterns.weekdayDistribution[5] + merged.timePatterns.weekdayDistribution[6]) / totalWeekdayCommits;
|
|
1379
|
+
}
|
|
1380
|
+
merged.trends.weeklyTrend.sort((a, b) => a.week.localeCompare(b.week));
|
|
1381
|
+
merged.trends.cumulativeLines.sort((a, b) => a.date.localeCompare(b.date));
|
|
1382
|
+
let cumulative = 0;
|
|
1383
|
+
for (const point of merged.trends.cumulativeLines) {
|
|
1384
|
+
cumulative += point.netLines;
|
|
1385
|
+
point.netLines = cumulative;
|
|
1386
|
+
}
|
|
1387
|
+
merged.collaboration.soloFiles.sort((a, b) => b.commits - a.commits);
|
|
1388
|
+
merged.collaboration.soloFiles = merged.collaboration.soloFiles.slice(0, 10);
|
|
1389
|
+
merged.collaboration.collaborationHotspots.sort(
|
|
1390
|
+
(a, b) => b.totalCommits - a.totalCommits
|
|
1391
|
+
);
|
|
1392
|
+
merged.collaboration.collaborationHotspots = merged.collaboration.collaborationHotspots.slice(0, 10);
|
|
1393
|
+
merged.messageStats.avgMessageLength /= repoCount;
|
|
1394
|
+
merged.commitDetails.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
1395
|
+
mergeAIStats(merged, statsList);
|
|
1396
|
+
const contributionMap = /* @__PURE__ */ new Map();
|
|
1397
|
+
for (const stats of statsList) {
|
|
1398
|
+
for (const contrib of stats.authorFileTypeContributions) {
|
|
1399
|
+
const key = `${contrib.email.toLowerCase()}|||${contrib.extension}`;
|
|
1400
|
+
const existing = contributionMap.get(key);
|
|
1401
|
+
if (existing) {
|
|
1402
|
+
existing.linesAdded += contrib.linesAdded;
|
|
1403
|
+
existing.linesDeleted += contrib.linesDeleted;
|
|
1404
|
+
existing.commits += contrib.commits;
|
|
1405
|
+
existing.fileCount += contrib.fileCount;
|
|
1406
|
+
} else {
|
|
1407
|
+
contributionMap.set(key, { ...contrib });
|
|
1408
|
+
}
|
|
821
1409
|
}
|
|
822
1410
|
}
|
|
823
|
-
|
|
824
|
-
contribution.commits = commitCountMap.get(key)?.size || 0;
|
|
825
|
-
contribution.fileCount = uniqueFilesMap.get(key)?.size || 0;
|
|
826
|
-
}
|
|
827
|
-
return Array.from(contributionMap.values()).sort((a, b) => {
|
|
1411
|
+
merged.authorFileTypeContributions = Array.from(contributionMap.values()).sort((a, b) => {
|
|
828
1412
|
const totalA = a.linesAdded + a.linesDeleted;
|
|
829
1413
|
const totalB = b.linesAdded + b.linesDeleted;
|
|
830
1414
|
return totalB - totalA;
|
|
831
1415
|
}).slice(0, 20);
|
|
1416
|
+
return merged;
|
|
832
1417
|
}
|
|
833
|
-
function
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1418
|
+
function mergeAIStats(merged, statsList) {
|
|
1419
|
+
const aiStatsList = statsList.filter((stats) => stats.aiMetrics);
|
|
1420
|
+
if (aiStatsList.length === 0) return;
|
|
1421
|
+
const highAICommits = [];
|
|
1422
|
+
const authorMap = /* @__PURE__ */ new Map();
|
|
1423
|
+
const directoryMap = /* @__PURE__ */ new Map();
|
|
1424
|
+
const trendMap = /* @__PURE__ */ new Map();
|
|
1425
|
+
let totalAILines = 0;
|
|
1426
|
+
let totalLines = 0;
|
|
1427
|
+
let suspiciousCommits = 0;
|
|
1428
|
+
for (const stats of aiStatsList) {
|
|
1429
|
+
const metrics = stats.aiMetrics;
|
|
1430
|
+
totalAILines += metrics.totalAILines;
|
|
1431
|
+
totalLines += metrics.totalLines;
|
|
1432
|
+
suspiciousCommits += metrics.suspiciousCommits;
|
|
1433
|
+
highAICommits.push(...metrics.highAICommits);
|
|
1434
|
+
for (const author of stats.authorAIStats || []) {
|
|
1435
|
+
const key = author.email.toLowerCase();
|
|
1436
|
+
const existing = authorMap.get(key);
|
|
1437
|
+
if (existing) {
|
|
1438
|
+
existing.aiLines += author.aiLines;
|
|
1439
|
+
existing.totalLines += author.totalLines;
|
|
1440
|
+
existing.aiPercentage = calculatePercentage(existing.aiLines, existing.totalLines);
|
|
1441
|
+
} else {
|
|
1442
|
+
authorMap.set(key, { ...author });
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
for (const directory of stats.directoryAIStats || []) {
|
|
1446
|
+
const key = directory.repoName ? `${directory.repoName}|||${directory.path}` : directory.path;
|
|
1447
|
+
const displayPath = directory.repoName ? `${directory.repoName} / ${directory.path}` : directory.displayPath || directory.path;
|
|
1448
|
+
const existing = directoryMap.get(key);
|
|
1449
|
+
if (existing) {
|
|
1450
|
+
existing.commits += directory.commits;
|
|
1451
|
+
existing.aiLines += directory.aiLines;
|
|
1452
|
+
existing.totalLines += directory.totalLines;
|
|
1453
|
+
existing.aiPercentage = calculatePercentage(existing.aiLines, existing.totalLines);
|
|
1454
|
+
existing.displayPath = displayPath;
|
|
1455
|
+
existing.lastModified = new Date(directory.lastModified) > new Date(existing.lastModified) ? directory.lastModified : existing.lastModified;
|
|
1456
|
+
existing.isHighRisk = existing.commits > 50 && existing.aiPercentage > 60;
|
|
1457
|
+
} else {
|
|
1458
|
+
directoryMap.set(key, { ...directory, displayPath });
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
for (const trend of stats.aiTrends || []) {
|
|
1462
|
+
const existing = trendMap.get(trend.week);
|
|
1463
|
+
if (existing) {
|
|
1464
|
+
existing.aiLines += trend.aiLines;
|
|
1465
|
+
existing.totalLines += trend.totalLines;
|
|
1466
|
+
existing.aiPercentage = calculatePercentage(existing.aiLines, existing.totalLines);
|
|
1467
|
+
} else {
|
|
1468
|
+
trendMap.set(trend.week, { ...trend });
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
merged.aiMetrics = {
|
|
1473
|
+
totalAILines,
|
|
1474
|
+
totalLines,
|
|
1475
|
+
aiPercentage: calculatePercentage(totalAILines, totalLines),
|
|
1476
|
+
suspiciousCommits,
|
|
1477
|
+
highAICommits: highAICommits.sort((a, b) => b.aiScore - a.aiScore).slice(0, 20)
|
|
860
1478
|
};
|
|
1479
|
+
merged.authorAIStats = Array.from(authorMap.values()).sort((a, b) => b.aiPercentage - a.aiPercentage);
|
|
1480
|
+
merged.directoryAIStats = Array.from(directoryMap.values()).sort((a, b) => b.aiPercentage - a.aiPercentage);
|
|
1481
|
+
merged.aiTrends = Array.from(trendMap.values()).sort((a, b) => a.week.localeCompare(b.week));
|
|
861
1482
|
}
|
|
862
|
-
function
|
|
863
|
-
return
|
|
864
|
-
typeDistribution: {},
|
|
865
|
-
avgMessageLength: 0
|
|
866
|
-
};
|
|
1483
|
+
function calculatePercentage(part, total) {
|
|
1484
|
+
return total > 0 ? part / total * 100 : 0;
|
|
867
1485
|
}
|
|
868
1486
|
|
|
869
1487
|
// src/analyzer/advanced/team-health.ts
|
|
@@ -992,7 +1610,7 @@ function calculateStability(commits) {
|
|
|
992
1610
|
const dirStats = /* @__PURE__ */ new Map();
|
|
993
1611
|
for (const commit of commits) {
|
|
994
1612
|
for (const file of commit.files) {
|
|
995
|
-
const dir =
|
|
1613
|
+
const dir = getTopDirectory3(file.path);
|
|
996
1614
|
const stat2 = dirStats.get(dir) || { added: 0, deleted: 0, files: /* @__PURE__ */ new Set() };
|
|
997
1615
|
stat2.added += file.added;
|
|
998
1616
|
stat2.deleted += file.deleted;
|
|
@@ -1023,7 +1641,7 @@ function calculateStability(commits) {
|
|
|
1023
1641
|
stabilityScore
|
|
1024
1642
|
};
|
|
1025
1643
|
}
|
|
1026
|
-
function
|
|
1644
|
+
function getTopDirectory3(filePath) {
|
|
1027
1645
|
const parts = filePath.split("/");
|
|
1028
1646
|
return parts.length > 1 ? parts[0] : "(\u6839\u76EE\u5F55)";
|
|
1029
1647
|
}
|
|
@@ -1350,6 +1968,658 @@ function calculateAdvancedStats(commits) {
|
|
|
1350
1968
|
};
|
|
1351
1969
|
}
|
|
1352
1970
|
|
|
1971
|
+
// src/analyzer/tech-debt/risk-scorer.ts
|
|
1972
|
+
function calculateRiskScores(commits) {
|
|
1973
|
+
if (commits.length === 0) return [];
|
|
1974
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
1975
|
+
for (const commit of commits) {
|
|
1976
|
+
for (const file of commit.files) {
|
|
1977
|
+
if (!fileMap.has(file.path)) {
|
|
1978
|
+
fileMap.set(file.path, {
|
|
1979
|
+
path: file.path,
|
|
1980
|
+
commits: [],
|
|
1981
|
+
authors: /* @__PURE__ */ new Set(),
|
|
1982
|
+
totalAdded: 0,
|
|
1983
|
+
totalDeleted: 0,
|
|
1984
|
+
lastModified: commit.date
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
const data = fileMap.get(file.path);
|
|
1988
|
+
data.commits.push(commit);
|
|
1989
|
+
data.authors.add(commit.author);
|
|
1990
|
+
data.totalAdded += file.added;
|
|
1991
|
+
data.totalDeleted += file.deleted;
|
|
1992
|
+
if (commit.date > data.lastModified) {
|
|
1993
|
+
data.lastModified = commit.date;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const riskFiles = [];
|
|
1998
|
+
for (const [path, data] of fileMap) {
|
|
1999
|
+
const complexity = calculateComplexity(data);
|
|
2000
|
+
const churnRate = calculateChurnRate(data, commits.length);
|
|
2001
|
+
const testCoverage = estimateTestCoverage(path, fileMap);
|
|
2002
|
+
const knowledgeRisk = calculateKnowledgeRisk(data);
|
|
2003
|
+
const riskScore = complexity * 0.3 + churnRate * 100 * 0.25 + (100 - testCoverage) * 0.2 + knowledgeRisk * 0.15 + (data.totalAdded > 500 ? 10 : 0);
|
|
2004
|
+
riskFiles.push({
|
|
2005
|
+
path,
|
|
2006
|
+
riskScore: Math.min(riskScore, 100),
|
|
2007
|
+
complexity,
|
|
2008
|
+
churnRate,
|
|
2009
|
+
testCoverage,
|
|
2010
|
+
knowledgeRisk,
|
|
2011
|
+
primaryAuthor: getMostActiveAuthor(data),
|
|
2012
|
+
lastModified: data.lastModified
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
return riskFiles.sort((a, b) => b.riskScore - a.riskScore);
|
|
2016
|
+
}
|
|
2017
|
+
function calculateComplexity(data) {
|
|
2018
|
+
const linesOfCode = data.totalAdded;
|
|
2019
|
+
if (linesOfCode > 1e3) return 90;
|
|
2020
|
+
if (linesOfCode > 500) return 70;
|
|
2021
|
+
if (linesOfCode > 300) return 50;
|
|
2022
|
+
if (linesOfCode > 100) return 30;
|
|
2023
|
+
return 10;
|
|
2024
|
+
}
|
|
2025
|
+
function calculateChurnRate(data, totalCommits) {
|
|
2026
|
+
return data.commits.length / totalCommits;
|
|
2027
|
+
}
|
|
2028
|
+
function estimateTestCoverage(path, fileMap) {
|
|
2029
|
+
const testPatterns = [".test.", ".spec.", "__tests__", "/test/", "/tests/"];
|
|
2030
|
+
const isTestFile = testPatterns.some((pattern) => path.includes(pattern));
|
|
2031
|
+
if (isTestFile) return 100;
|
|
2032
|
+
const baseName = path.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
2033
|
+
const possibleTestFiles = [
|
|
2034
|
+
`${baseName}.test.ts`,
|
|
2035
|
+
`${baseName}.test.js`,
|
|
2036
|
+
`${baseName}.spec.ts`,
|
|
2037
|
+
`${baseName}.spec.js`
|
|
2038
|
+
];
|
|
2039
|
+
for (const testFile of possibleTestFiles) {
|
|
2040
|
+
if (fileMap.has(testFile)) {
|
|
2041
|
+
return 80;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const dir = path.split("/").slice(0, -1).join("/");
|
|
2045
|
+
for (const [filePath] of fileMap) {
|
|
2046
|
+
if (filePath.startsWith(dir) && testPatterns.some((p) => filePath.includes(p))) {
|
|
2047
|
+
return 40;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
return 0;
|
|
2051
|
+
}
|
|
2052
|
+
function calculateKnowledgeRisk(data) {
|
|
2053
|
+
const authorCount = data.authors.size;
|
|
2054
|
+
if (authorCount === 1) return 100;
|
|
2055
|
+
if (authorCount === 2) return 60;
|
|
2056
|
+
if (authorCount === 3) return 30;
|
|
2057
|
+
return 10;
|
|
2058
|
+
}
|
|
2059
|
+
function getMostActiveAuthor(data) {
|
|
2060
|
+
const authorCommits = /* @__PURE__ */ new Map();
|
|
2061
|
+
for (const commit of data.commits) {
|
|
2062
|
+
authorCommits.set(commit.author, (authorCommits.get(commit.author) || 0) + 1);
|
|
2063
|
+
}
|
|
2064
|
+
let maxAuthor = "";
|
|
2065
|
+
let maxCommits = 0;
|
|
2066
|
+
for (const [author, count] of authorCommits) {
|
|
2067
|
+
if (count > maxCommits) {
|
|
2068
|
+
maxCommits = count;
|
|
2069
|
+
maxAuthor = author;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return maxAuthor;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/analyzer/tech-debt/duplication.ts
|
|
2076
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2077
|
+
import { join as join4 } from "path";
|
|
2078
|
+
import { readdirSync, statSync } from "fs";
|
|
2079
|
+
import { createHash } from "crypto";
|
|
2080
|
+
async function detectDuplication(repoPath) {
|
|
2081
|
+
const codeFiles = findCodeFiles(repoPath);
|
|
2082
|
+
const fileHashes = /* @__PURE__ */ new Map();
|
|
2083
|
+
const fileSizes = /* @__PURE__ */ new Map();
|
|
2084
|
+
for (const file of codeFiles.slice(0, 100)) {
|
|
2085
|
+
try {
|
|
2086
|
+
const content = await readFile3(file, "utf-8");
|
|
2087
|
+
const normalized = normalizeCode(content);
|
|
2088
|
+
const hash = simpleHash(normalized);
|
|
2089
|
+
if (!fileHashes.has(hash)) {
|
|
2090
|
+
fileHashes.set(hash, []);
|
|
2091
|
+
}
|
|
2092
|
+
fileHashes.get(hash).push(file.replace(repoPath, "").replace(/^[/\\]/, ""));
|
|
2093
|
+
fileSizes.set(file, content.split("\n").length);
|
|
2094
|
+
} catch {
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
const clusters = [];
|
|
2098
|
+
for (const [hash, files] of fileHashes) {
|
|
2099
|
+
if (files.length > 1) {
|
|
2100
|
+
const lines = fileSizes.get(join4(repoPath, files[0])) || 0;
|
|
2101
|
+
clusters.push({
|
|
2102
|
+
files,
|
|
2103
|
+
similarity: 100,
|
|
2104
|
+
lines
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
const fileScores = /* @__PURE__ */ new Map();
|
|
2109
|
+
for (const cluster of clusters) {
|
|
2110
|
+
for (const file of cluster.files) {
|
|
2111
|
+
fileScores.set(file, (fileScores.get(file) || 0) + cluster.lines);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
const sortedScores = Array.from(fileScores.entries()).map(([file, score]) => ({ file, score })).sort((a, b) => b.score - a.score);
|
|
2115
|
+
return {
|
|
2116
|
+
clusters: clusters.sort((a, b) => b.lines - a.lines).slice(0, 10),
|
|
2117
|
+
fileScores: sortedScores.slice(0, 20)
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
function findCodeFiles(dir, maxDepth = 3, currentDepth = 0) {
|
|
2121
|
+
if (currentDepth > maxDepth) return [];
|
|
2122
|
+
const files = [];
|
|
2123
|
+
const ignorePatterns = ["node_modules", ".git", "dist", "build", "coverage", ".next"];
|
|
2124
|
+
try {
|
|
2125
|
+
const entries = readdirSync(dir);
|
|
2126
|
+
for (const entry of entries) {
|
|
2127
|
+
if (ignorePatterns.includes(entry)) continue;
|
|
2128
|
+
const fullPath = join4(dir, entry);
|
|
2129
|
+
try {
|
|
2130
|
+
const stat2 = statSync(fullPath);
|
|
2131
|
+
if (stat2.isDirectory()) {
|
|
2132
|
+
files.push(...findCodeFiles(fullPath, maxDepth, currentDepth + 1));
|
|
2133
|
+
} else if (stat2.isFile() && isCodeFile(entry)) {
|
|
2134
|
+
files.push(fullPath);
|
|
2135
|
+
}
|
|
2136
|
+
} catch {
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
} catch {
|
|
2140
|
+
}
|
|
2141
|
+
return files;
|
|
2142
|
+
}
|
|
2143
|
+
function isCodeFile(filename) {
|
|
2144
|
+
const codeExtensions = [".ts", ".js", ".tsx", ".jsx", ".py", ".java", ".go", ".rs", ".cpp", ".c"];
|
|
2145
|
+
return codeExtensions.some((ext) => filename.endsWith(ext));
|
|
2146
|
+
}
|
|
2147
|
+
function normalizeCode(content) {
|
|
2148
|
+
return content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
2149
|
+
}
|
|
2150
|
+
function simpleHash(content) {
|
|
2151
|
+
return createHash("md5").update(content).digest("hex");
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// src/analyzer/tech-debt/prioritizer.ts
|
|
2155
|
+
function prioritizeActions(riskFiles) {
|
|
2156
|
+
const actionItems = riskFiles.map((file) => {
|
|
2157
|
+
const impact = calculateImpact(file);
|
|
2158
|
+
const effort = estimateEffort(file);
|
|
2159
|
+
const priority = file.riskScore * impact / Math.max(effort, 1);
|
|
2160
|
+
return {
|
|
2161
|
+
file: file.path,
|
|
2162
|
+
riskLevel: getRiskLevel(file.riskScore),
|
|
2163
|
+
impact,
|
|
2164
|
+
effort,
|
|
2165
|
+
priority,
|
|
2166
|
+
suggestedAction: generateSuggestion(file),
|
|
2167
|
+
owner: file.primaryAuthor
|
|
2168
|
+
};
|
|
2169
|
+
});
|
|
2170
|
+
return actionItems.sort((a, b) => b.priority - a.priority).slice(0, 10);
|
|
2171
|
+
}
|
|
2172
|
+
function calculateImpact(file) {
|
|
2173
|
+
let impact = 50;
|
|
2174
|
+
if (file.churnRate > 0.5) impact += 20;
|
|
2175
|
+
if (file.knowledgeRisk > 70) impact += 15;
|
|
2176
|
+
if (file.testCoverage < 30) impact += 15;
|
|
2177
|
+
return Math.min(impact, 100);
|
|
2178
|
+
}
|
|
2179
|
+
function estimateEffort(file) {
|
|
2180
|
+
let effort = 1;
|
|
2181
|
+
if (file.complexity > 70) effort += 2;
|
|
2182
|
+
if (file.complexity > 50) effort += 1;
|
|
2183
|
+
if (file.knowledgeRisk > 70) effort += 1;
|
|
2184
|
+
return effort;
|
|
2185
|
+
}
|
|
2186
|
+
function getRiskLevel(score) {
|
|
2187
|
+
if (score >= 80) return "critical";
|
|
2188
|
+
if (score >= 60) return "high";
|
|
2189
|
+
if (score >= 40) return "medium";
|
|
2190
|
+
return "low";
|
|
2191
|
+
}
|
|
2192
|
+
function generateSuggestion(file) {
|
|
2193
|
+
const suggestions = [];
|
|
2194
|
+
if (file.complexity > 70) {
|
|
2195
|
+
suggestions.push("\u62C6\u5206\u4E3A\u591A\u4E2A\u5C0F\u6587\u4EF6");
|
|
2196
|
+
}
|
|
2197
|
+
if (file.testCoverage < 30) {
|
|
2198
|
+
suggestions.push("\u589E\u52A0\u5355\u5143\u6D4B\u8BD5\u8986\u76D6");
|
|
2199
|
+
}
|
|
2200
|
+
if (file.knowledgeRisk > 70) {
|
|
2201
|
+
suggestions.push("\u8FDB\u884C\u77E5\u8BC6\u5206\u4EAB\u548C\u4EE3\u7801\u5BA1\u67E5");
|
|
2202
|
+
}
|
|
2203
|
+
if (file.churnRate > 0.5) {
|
|
2204
|
+
suggestions.push("\u91CD\u6784\u4EE5\u63D0\u9AD8\u7A33\u5B9A\u6027");
|
|
2205
|
+
}
|
|
2206
|
+
if (suggestions.length === 0) {
|
|
2207
|
+
suggestions.push("\u4EE3\u7801\u5BA1\u67E5\u548C\u6587\u6863\u5B8C\u5584");
|
|
2208
|
+
}
|
|
2209
|
+
return suggestions.join("\uFF1B");
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// src/analyzer/tech-debt/index.ts
|
|
2213
|
+
async function calculateTechDebt(commits, repoPath) {
|
|
2214
|
+
if (commits.length === 0) {
|
|
2215
|
+
return emptyTechDebt();
|
|
2216
|
+
}
|
|
2217
|
+
const riskFiles = calculateRiskScores(commits);
|
|
2218
|
+
const aiDetection = await detectAICode(commits, repoPath);
|
|
2219
|
+
const duplication = await detectDuplication(repoPath);
|
|
2220
|
+
const actionItems = prioritizeActions(riskFiles);
|
|
2221
|
+
const radar = calculateRadarDimensions(riskFiles, aiDetection, duplication);
|
|
2222
|
+
const trends = calculateTrends2(commits);
|
|
2223
|
+
return {
|
|
2224
|
+
radar,
|
|
2225
|
+
highRiskFiles: riskFiles.slice(0, 10),
|
|
2226
|
+
aiDetection,
|
|
2227
|
+
duplication,
|
|
2228
|
+
trends,
|
|
2229
|
+
actionItems
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
function calculateRadarDimensions(riskFiles, aiDetection, duplication) {
|
|
2233
|
+
const avgComplexity = riskFiles.length > 0 ? riskFiles.reduce((sum, f) => sum + f.complexity, 0) / riskFiles.length : 0;
|
|
2234
|
+
const avgDuplication = duplication.fileScores.length > 0 ? duplication.fileScores.reduce((sum, file) => sum + file.score, 0) / duplication.fileScores.length : 0;
|
|
2235
|
+
const avgTestCoverage = riskFiles.length > 0 ? riskFiles.reduce((sum, f) => sum + f.testCoverage, 0) / riskFiles.length : 100;
|
|
2236
|
+
const avgKnowledgeRisk = riskFiles.length > 0 ? riskFiles.reduce((sum, f) => sum + f.knowledgeRisk, 0) / riskFiles.length : 0;
|
|
2237
|
+
const avgChurnRate = riskFiles.length > 0 ? riskFiles.reduce((sum, f) => sum + f.churnRate, 0) / riskFiles.length : 0;
|
|
2238
|
+
return [
|
|
2239
|
+
{
|
|
2240
|
+
dimension: "Complexity",
|
|
2241
|
+
score: Math.min(avgComplexity, 100),
|
|
2242
|
+
riskLevel: avgComplexity > 70 ? "high" : avgComplexity > 40 ? "medium" : "low",
|
|
2243
|
+
description: "\u4EE3\u7801\u590D\u6742\u5EA6",
|
|
2244
|
+
affectedFiles: riskFiles.filter((f) => f.complexity > 70).length
|
|
2245
|
+
},
|
|
2246
|
+
{
|
|
2247
|
+
dimension: "Duplication",
|
|
2248
|
+
score: Math.min(avgDuplication / 10, 100),
|
|
2249
|
+
riskLevel: avgDuplication > 700 ? "high" : avgDuplication > 400 ? "medium" : "low",
|
|
2250
|
+
description: "\u4EE3\u7801\u91CD\u590D\u5EA6",
|
|
2251
|
+
affectedFiles: duplication.fileScores.filter((file) => file.score > 100).length
|
|
2252
|
+
},
|
|
2253
|
+
{
|
|
2254
|
+
dimension: "Test Coverage",
|
|
2255
|
+
score: 100 - avgTestCoverage,
|
|
2256
|
+
riskLevel: avgTestCoverage < 30 ? "high" : avgTestCoverage < 60 ? "medium" : "low",
|
|
2257
|
+
description: "\u6D4B\u8BD5\u8986\u76D6\u7387",
|
|
2258
|
+
affectedFiles: riskFiles.filter((f) => f.testCoverage < 30).length
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
dimension: "Documentation",
|
|
2262
|
+
score: aiDetection.suspiciousFiles.filter((file) => file.reason === "excessive-comments").length * 10,
|
|
2263
|
+
riskLevel: aiDetection.suspiciousFiles.length > 10 ? "high" : aiDetection.suspiciousFiles.length > 5 ? "medium" : "low",
|
|
2264
|
+
description: "\u6587\u6863\u5B8C\u6574\u6027",
|
|
2265
|
+
affectedFiles: aiDetection.suspiciousFiles.length
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
dimension: "Stability",
|
|
2269
|
+
score: avgChurnRate * 100,
|
|
2270
|
+
riskLevel: avgChurnRate > 0.7 ? "high" : avgChurnRate > 0.4 ? "medium" : "low",
|
|
2271
|
+
description: "\u4EE3\u7801\u7A33\u5B9A\u6027",
|
|
2272
|
+
affectedFiles: riskFiles.filter((f) => f.churnRate > 0.7).length
|
|
2273
|
+
},
|
|
2274
|
+
{
|
|
2275
|
+
dimension: "Knowledge Risk",
|
|
2276
|
+
score: avgKnowledgeRisk,
|
|
2277
|
+
riskLevel: avgKnowledgeRisk > 70 ? "high" : avgKnowledgeRisk > 40 ? "medium" : "low",
|
|
2278
|
+
description: "\u77E5\u8BC6\u96C6\u4E2D\u5EA6",
|
|
2279
|
+
affectedFiles: riskFiles.filter((f) => f.knowledgeRisk > 70).length
|
|
2280
|
+
}
|
|
2281
|
+
];
|
|
2282
|
+
}
|
|
2283
|
+
function calculateTrends2(commits) {
|
|
2284
|
+
const sorted = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
2285
|
+
const trends = [];
|
|
2286
|
+
const interval = Math.max(1, Math.floor(sorted.length / 20));
|
|
2287
|
+
for (let i = 0; i < sorted.length; i += interval) {
|
|
2288
|
+
const chunk = sorted.slice(Math.max(0, i - interval), i + 1);
|
|
2289
|
+
const debt = chunk.reduce((sum, c) => {
|
|
2290
|
+
const largeFiles = c.files.filter((f) => f.added > 100).length;
|
|
2291
|
+
return sum + largeFiles;
|
|
2292
|
+
}, 0);
|
|
2293
|
+
trends.push({
|
|
2294
|
+
date: sorted[i].date.toISOString().split("T")[0],
|
|
2295
|
+
debt
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
return trends;
|
|
2299
|
+
}
|
|
2300
|
+
function emptyTechDebt() {
|
|
2301
|
+
return {
|
|
2302
|
+
radar: [],
|
|
2303
|
+
highRiskFiles: [],
|
|
2304
|
+
aiDetection: { suspiciousFiles: [], totalSuspicious: 0 },
|
|
2305
|
+
duplication: { clusters: [], fileScores: [] },
|
|
2306
|
+
trends: [],
|
|
2307
|
+
actionItems: []
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// src/analyzer/engineering-metrics.ts
|
|
2312
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2313
|
+
var CONVENTIONAL_RE = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\([^)]+\))?!?:\s+\S/i;
|
|
2314
|
+
var SCOPED_CONVENTIONAL_RE = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)\([^)]+\)!?:\s+\S/i;
|
|
2315
|
+
var FIX_RE = /^fix(\([^)]+\))?!?:\s+\S/i;
|
|
2316
|
+
var REVIEW_TRAILER_RE = /^(Co-authored-by|Signed-off-by):\s*(.*?)\s*<([^<>]+)>$/gim;
|
|
2317
|
+
var DEFAULT_OWNERSHIP_FILE_LIMIT = 200;
|
|
2318
|
+
var OWNERSHIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2319
|
+
".c",
|
|
2320
|
+
".cpp",
|
|
2321
|
+
".css",
|
|
2322
|
+
".go",
|
|
2323
|
+
".h",
|
|
2324
|
+
".html",
|
|
2325
|
+
".java",
|
|
2326
|
+
".js",
|
|
2327
|
+
".jsx",
|
|
2328
|
+
".md",
|
|
2329
|
+
".py",
|
|
2330
|
+
".rs",
|
|
2331
|
+
".scss",
|
|
2332
|
+
".ts",
|
|
2333
|
+
".tsx",
|
|
2334
|
+
".vue"
|
|
2335
|
+
]);
|
|
2336
|
+
function calculateEngineeringMetrics(commits, repoPath) {
|
|
2337
|
+
return {
|
|
2338
|
+
...repoPath && {
|
|
2339
|
+
codeOwnership: calculateCodeOwnership(repoPath, {
|
|
2340
|
+
candidateFiles: selectOwnershipCandidates(commits)
|
|
2341
|
+
})
|
|
2342
|
+
},
|
|
2343
|
+
bugFixHotFiles: calculateBugFixHotFiles(commits),
|
|
2344
|
+
reviewQuality: calculateReviewQuality(commits),
|
|
2345
|
+
commitQuality: calculateCommitQuality(commits),
|
|
2346
|
+
changeMix: calculateChangeMix(commits)
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
function calculateCodeOwnership(repoPath, options = {}) {
|
|
2350
|
+
const trackedFiles = listTrackedFiles(repoPath);
|
|
2351
|
+
const trackedSet = new Set(trackedFiles);
|
|
2352
|
+
const maxFiles = options.maxFiles ?? DEFAULT_OWNERSHIP_FILE_LIMIT;
|
|
2353
|
+
const candidates = options.candidateFiles ? options.candidateFiles.filter((file) => trackedSet.has(file)) : trackedFiles;
|
|
2354
|
+
const filesToBlame = candidates.filter(isOwnershipTargetFile).slice(0, maxFiles);
|
|
2355
|
+
const files = [];
|
|
2356
|
+
for (const filePath of filesToBlame) {
|
|
2357
|
+
const ownership = calculateFileOwnership(repoPath, filePath);
|
|
2358
|
+
if (ownership) {
|
|
2359
|
+
files.push(ownership);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
files.sort((a, b) => {
|
|
2363
|
+
if (b.ownerLines !== a.ownerLines) return b.ownerLines - a.ownerLines;
|
|
2364
|
+
return b.ownershipRatio - a.ownershipRatio;
|
|
2365
|
+
});
|
|
2366
|
+
return {
|
|
2367
|
+
totalFiles: files.length,
|
|
2368
|
+
files
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
function calculateBugFixHotFiles(commits) {
|
|
2372
|
+
const hotFileMap = /* @__PURE__ */ new Map();
|
|
2373
|
+
let fixCommitCount = 0;
|
|
2374
|
+
for (const commit of commits) {
|
|
2375
|
+
if (!FIX_RE.test(commit.message)) continue;
|
|
2376
|
+
fixCommitCount++;
|
|
2377
|
+
for (const file of commit.files) {
|
|
2378
|
+
const entry = hotFileMap.get(file.path) || {
|
|
2379
|
+
fixCount: 0,
|
|
2380
|
+
lastFixDate: commit.date,
|
|
2381
|
+
fixAuthors: /* @__PURE__ */ new Set()
|
|
2382
|
+
};
|
|
2383
|
+
entry.fixCount++;
|
|
2384
|
+
entry.fixAuthors.add(commit.author);
|
|
2385
|
+
if (commit.date > entry.lastFixDate) {
|
|
2386
|
+
entry.lastFixDate = commit.date;
|
|
2387
|
+
}
|
|
2388
|
+
hotFileMap.set(file.path, entry);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
const hotFiles = Array.from(hotFileMap.entries()).map(([path, data]) => ({
|
|
2392
|
+
path,
|
|
2393
|
+
fixCount: data.fixCount,
|
|
2394
|
+
lastFixDate: data.lastFixDate,
|
|
2395
|
+
fixAuthors: Array.from(data.fixAuthors)
|
|
2396
|
+
})).sort((a, b) => {
|
|
2397
|
+
if (b.fixCount !== a.fixCount) return b.fixCount - a.fixCount;
|
|
2398
|
+
return b.lastFixDate.getTime() - a.lastFixDate.getTime();
|
|
2399
|
+
}).slice(0, 20);
|
|
2400
|
+
return { fixCommitCount, hotFiles };
|
|
2401
|
+
}
|
|
2402
|
+
function calculateReviewQuality(commits) {
|
|
2403
|
+
const reviewerMap = /* @__PURE__ */ new Map();
|
|
2404
|
+
let mergeCommitCount = 0;
|
|
2405
|
+
let reviewedMergeCount = 0;
|
|
2406
|
+
let order = 0;
|
|
2407
|
+
for (const commit of commits) {
|
|
2408
|
+
if (!isMergeCommit(commit)) continue;
|
|
2409
|
+
mergeCommitCount++;
|
|
2410
|
+
const reviewers2 = parseReviewers(commit.body || commit.message);
|
|
2411
|
+
if (reviewers2.length > 0) {
|
|
2412
|
+
reviewedMergeCount++;
|
|
2413
|
+
}
|
|
2414
|
+
for (const reviewer of reviewers2) {
|
|
2415
|
+
const key = reviewer.email.toLowerCase();
|
|
2416
|
+
const existing = reviewerMap.get(key);
|
|
2417
|
+
if (existing) {
|
|
2418
|
+
existing.commits++;
|
|
2419
|
+
} else {
|
|
2420
|
+
reviewerMap.set(key, {
|
|
2421
|
+
...reviewer,
|
|
2422
|
+
commits: 1,
|
|
2423
|
+
firstSeenIndex: order
|
|
2424
|
+
});
|
|
2425
|
+
order++;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
const reviewers = Array.from(reviewerMap.values()).sort((a, b) => {
|
|
2430
|
+
if (b.commits !== a.commits) return b.commits - a.commits;
|
|
2431
|
+
return a.firstSeenIndex - b.firstSeenIndex;
|
|
2432
|
+
}).map(({ firstSeenIndex, ...reviewer }) => reviewer);
|
|
2433
|
+
return {
|
|
2434
|
+
mergeCommitCount,
|
|
2435
|
+
reviewedMergeCount,
|
|
2436
|
+
reviewParticipationRate: mergeCommitCount > 0 ? roundRatio(reviewedMergeCount / mergeCommitCount) : 0,
|
|
2437
|
+
reviewers
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
function calculateCommitQuality(commits) {
|
|
2441
|
+
if (commits.length === 0) {
|
|
2442
|
+
return {
|
|
2443
|
+
score: 0,
|
|
2444
|
+
conventionalRate: 0,
|
|
2445
|
+
scopeCoverageRate: 0,
|
|
2446
|
+
averageMessageLength: 0,
|
|
2447
|
+
typeDistribution: {}
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
let conventionalCount = 0;
|
|
2451
|
+
let scopedCount = 0;
|
|
2452
|
+
let totalLength = 0;
|
|
2453
|
+
const typeDistribution = {};
|
|
2454
|
+
for (const commit of commits) {
|
|
2455
|
+
totalLength += commit.message.length;
|
|
2456
|
+
const conventionalMatch = commit.message.match(CONVENTIONAL_RE);
|
|
2457
|
+
if (conventionalMatch) {
|
|
2458
|
+
conventionalCount++;
|
|
2459
|
+
const type = conventionalMatch[1].toLowerCase();
|
|
2460
|
+
typeDistribution[type] = (typeDistribution[type] || 0) + 1;
|
|
2461
|
+
} else {
|
|
2462
|
+
typeDistribution.other = (typeDistribution.other || 0) + 1;
|
|
2463
|
+
}
|
|
2464
|
+
if (SCOPED_CONVENTIONAL_RE.test(commit.message)) {
|
|
2465
|
+
scopedCount++;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
const averageMessageLength = roundTo(totalLength / commits.length, 1);
|
|
2469
|
+
const conventionalRate = roundRatio(conventionalCount / commits.length);
|
|
2470
|
+
const scopeCoverageRate = roundRatio(scopedCount / commits.length);
|
|
2471
|
+
const messageLengthScore = Math.min(averageMessageLength / 24, 1);
|
|
2472
|
+
const score = Math.round(
|
|
2473
|
+
conventionalRate * 60 + scopeCoverageRate * 25 + messageLengthScore * 15
|
|
2474
|
+
);
|
|
2475
|
+
return {
|
|
2476
|
+
score,
|
|
2477
|
+
conventionalRate,
|
|
2478
|
+
scopeCoverageRate,
|
|
2479
|
+
averageMessageLength,
|
|
2480
|
+
typeDistribution
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
function calculateChangeMix(commits) {
|
|
2484
|
+
let createdFiles = 0;
|
|
2485
|
+
let deletedFiles = 0;
|
|
2486
|
+
let modifiedFiles = 0;
|
|
2487
|
+
for (const commit of commits) {
|
|
2488
|
+
for (const file of commit.files) {
|
|
2489
|
+
if (file.status === "added" || file.status === "copied") {
|
|
2490
|
+
createdFiles++;
|
|
2491
|
+
} else if (file.status === "deleted") {
|
|
2492
|
+
deletedFiles++;
|
|
2493
|
+
} else {
|
|
2494
|
+
modifiedFiles++;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
const total = createdFiles + deletedFiles + modifiedFiles;
|
|
2499
|
+
return {
|
|
2500
|
+
createdFiles,
|
|
2501
|
+
deletedFiles,
|
|
2502
|
+
modifiedFiles,
|
|
2503
|
+
featureRatio: total > 0 ? roundRatio((createdFiles + deletedFiles) / total) : 0,
|
|
2504
|
+
refactorRatio: total > 0 ? roundRatio(modifiedFiles / total) : 0
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
function selectOwnershipCandidates(commits) {
|
|
2508
|
+
const fileCounts = /* @__PURE__ */ new Map();
|
|
2509
|
+
for (const commit of commits) {
|
|
2510
|
+
for (const file of commit.files) {
|
|
2511
|
+
fileCounts.set(file.path, (fileCounts.get(file.path) || 0) + 1);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return Array.from(fileCounts.entries()).sort((a, b) => {
|
|
2515
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
2516
|
+
return a[0].localeCompare(b[0]);
|
|
2517
|
+
}).map(([file]) => file);
|
|
2518
|
+
}
|
|
2519
|
+
function listTrackedFiles(repoPath) {
|
|
2520
|
+
let output = "";
|
|
2521
|
+
try {
|
|
2522
|
+
output = execFileSync2("git", ["ls-files", "-z"], {
|
|
2523
|
+
cwd: repoPath,
|
|
2524
|
+
encoding: "utf-8",
|
|
2525
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
2526
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
2527
|
+
});
|
|
2528
|
+
} catch {
|
|
2529
|
+
return [];
|
|
2530
|
+
}
|
|
2531
|
+
return output.split("\0").filter((path) => path && !isIgnoredOwnershipFile(path));
|
|
2532
|
+
}
|
|
2533
|
+
function calculateFileOwnership(repoPath, filePath) {
|
|
2534
|
+
let output = "";
|
|
2535
|
+
try {
|
|
2536
|
+
output = execFileSync2("git", ["blame", "HEAD", "--line-porcelain", "--", filePath], {
|
|
2537
|
+
cwd: repoPath,
|
|
2538
|
+
encoding: "utf-8",
|
|
2539
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
2540
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
2541
|
+
});
|
|
2542
|
+
} catch {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
const contributors = parseBlameContributors(output);
|
|
2546
|
+
const totalLines = Array.from(contributors.values()).reduce(
|
|
2547
|
+
(sum, contributor) => sum + contributor.lines,
|
|
2548
|
+
0
|
|
2549
|
+
);
|
|
2550
|
+
if (totalLines === 0) return null;
|
|
2551
|
+
const sortedContributors = Array.from(
|
|
2552
|
+
contributors.values()
|
|
2553
|
+
).map((contributor) => ({
|
|
2554
|
+
...contributor,
|
|
2555
|
+
ratio: contributor.lines / totalLines
|
|
2556
|
+
})).sort((a, b) => b.lines - a.lines);
|
|
2557
|
+
const owner = sortedContributors[0];
|
|
2558
|
+
return {
|
|
2559
|
+
path: filePath,
|
|
2560
|
+
ownerName: owner.name,
|
|
2561
|
+
ownerEmail: owner.email,
|
|
2562
|
+
ownerLines: owner.lines,
|
|
2563
|
+
totalLines,
|
|
2564
|
+
ownershipRatio: owner.ratio,
|
|
2565
|
+
contributors: sortedContributors
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
function parseBlameContributors(output) {
|
|
2569
|
+
const contributors = /* @__PURE__ */ new Map();
|
|
2570
|
+
let currentName = "";
|
|
2571
|
+
for (const line of output.split("\n")) {
|
|
2572
|
+
if (line.startsWith("author ")) {
|
|
2573
|
+
currentName = line.slice("author ".length);
|
|
2574
|
+
} else if (line.startsWith("author-mail ")) {
|
|
2575
|
+
const email = line.slice("author-mail ".length).replace(/^<|>$/g, "").toLowerCase();
|
|
2576
|
+
const key = email || currentName;
|
|
2577
|
+
const existing = contributors.get(key) || {
|
|
2578
|
+
name: currentName || email,
|
|
2579
|
+
email,
|
|
2580
|
+
lines: 0
|
|
2581
|
+
};
|
|
2582
|
+
existing.lines++;
|
|
2583
|
+
contributors.set(key, existing);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
return contributors;
|
|
2587
|
+
}
|
|
2588
|
+
function parseReviewers(body) {
|
|
2589
|
+
const reviewers = [];
|
|
2590
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2591
|
+
REVIEW_TRAILER_RE.lastIndex = 0;
|
|
2592
|
+
let match = REVIEW_TRAILER_RE.exec(body);
|
|
2593
|
+
while (match) {
|
|
2594
|
+
const name = match[2].trim();
|
|
2595
|
+
const email = match[3].trim().toLowerCase();
|
|
2596
|
+
if (!seen.has(email)) {
|
|
2597
|
+
reviewers.push({ name, email, commits: 0 });
|
|
2598
|
+
seen.add(email);
|
|
2599
|
+
}
|
|
2600
|
+
match = REVIEW_TRAILER_RE.exec(body);
|
|
2601
|
+
}
|
|
2602
|
+
return reviewers;
|
|
2603
|
+
}
|
|
2604
|
+
function isMergeCommit(commit) {
|
|
2605
|
+
return (commit.parentHashes?.length || 0) > 1 || /^Merge\b/i.test(commit.message);
|
|
2606
|
+
}
|
|
2607
|
+
function isIgnoredOwnershipFile(filePath) {
|
|
2608
|
+
return /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$/.test(filePath);
|
|
2609
|
+
}
|
|
2610
|
+
function isOwnershipTargetFile(filePath) {
|
|
2611
|
+
if (isIgnoredOwnershipFile(filePath)) return false;
|
|
2612
|
+
const extension = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")).toLowerCase() : "";
|
|
2613
|
+
return OWNERSHIP_EXTENSIONS.has(extension);
|
|
2614
|
+
}
|
|
2615
|
+
function roundRatio(value) {
|
|
2616
|
+
return roundTo(value, 2);
|
|
2617
|
+
}
|
|
2618
|
+
function roundTo(value, digits) {
|
|
2619
|
+
const factor = 10 ** digits;
|
|
2620
|
+
return Math.round(value * factor) / factor;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
1353
2623
|
// src/analyzer/index.ts
|
|
1354
2624
|
async function analyzeRepos(options) {
|
|
1355
2625
|
const { repos, timeRange, author } = options;
|
|
@@ -1368,10 +2638,30 @@ async function analyzeRepos(options) {
|
|
|
1368
2638
|
}
|
|
1369
2639
|
const stats = calculateStats(commits);
|
|
1370
2640
|
const advancedStats = calculateAdvancedStats(commits);
|
|
2641
|
+
let techDebt;
|
|
2642
|
+
let engineering;
|
|
2643
|
+
if (repos.length === 1) {
|
|
2644
|
+
spinner.text = `\u5206\u6790\u6280\u672F\u503A - ${repo.name}`;
|
|
2645
|
+
techDebt = await calculateTechDebt(commits, repo.path);
|
|
2646
|
+
spinner.text = `\u5206\u6790\u5DE5\u7A0B\u8D28\u91CF - ${repo.name}`;
|
|
2647
|
+
engineering = calculateEngineeringMetrics(commits, repo.path);
|
|
2648
|
+
}
|
|
1371
2649
|
const fullStats = {
|
|
1372
2650
|
...stats,
|
|
1373
|
-
...advancedStats
|
|
2651
|
+
...advancedStats,
|
|
2652
|
+
...techDebt && { techDebt },
|
|
2653
|
+
...engineering && { engineering }
|
|
1374
2654
|
};
|
|
2655
|
+
fullStats.commitDetails.forEach((commit) => {
|
|
2656
|
+
commit.repoName = repo.name;
|
|
2657
|
+
});
|
|
2658
|
+
fullStats.aiMetrics?.highAICommits.forEach((commit) => {
|
|
2659
|
+
commit.repoName = repo.name;
|
|
2660
|
+
});
|
|
2661
|
+
fullStats.directoryAIStats?.forEach((directory) => {
|
|
2662
|
+
directory.repoName = repo.name;
|
|
2663
|
+
directory.displayPath = repos.length > 1 ? `${repo.name} / ${directory.path}` : directory.path;
|
|
2664
|
+
});
|
|
1375
2665
|
allStats.push(fullStats);
|
|
1376
2666
|
} catch (error) {
|
|
1377
2667
|
spinner.warn(
|
|
@@ -1397,11 +2687,32 @@ import ora3 from "ora";
|
|
|
1397
2687
|
import open from "open";
|
|
1398
2688
|
|
|
1399
2689
|
// src/reporter/html-builder.ts
|
|
1400
|
-
import { readFile as
|
|
2690
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1401
2691
|
import { resolve, dirname } from "path";
|
|
1402
2692
|
import { fileURLToPath } from "url";
|
|
2693
|
+
var REPORT_SCRIPT_FILES = [
|
|
2694
|
+
"report-scripts/01-core.html",
|
|
2695
|
+
"report-scripts/00-filter-state.html",
|
|
2696
|
+
"report-scripts/00-advanced-derived.html",
|
|
2697
|
+
"report-scripts/00-report-controls.html",
|
|
2698
|
+
"report-scripts/02-commit-details.html",
|
|
2699
|
+
"report-scripts/03-basic-charts.html",
|
|
2700
|
+
"report-scripts/04-trend-charts.html",
|
|
2701
|
+
"report-scripts/05-tables-team-stability.html",
|
|
2702
|
+
"report-scripts/06-pressure-churn.html",
|
|
2703
|
+
"report-scripts/08-engineering.html",
|
|
2704
|
+
"report-scripts/09-extensions.html",
|
|
2705
|
+
"report-scripts/07-collab-debt-ai.html",
|
|
2706
|
+
"report-scripts/10-runtime.html"
|
|
2707
|
+
];
|
|
2708
|
+
var REPORT_SECTION_FILES = [
|
|
2709
|
+
"report-sections/01-overview.html",
|
|
2710
|
+
"report-sections/02-advanced.html"
|
|
2711
|
+
];
|
|
1403
2712
|
async function buildHtml(stats, options) {
|
|
1404
|
-
const template = await
|
|
2713
|
+
const template = await loadTemplateFile("report.html");
|
|
2714
|
+
const reportSections = await loadTemplateParts(REPORT_SECTION_FILES);
|
|
2715
|
+
const reportScript = await loadReportScript();
|
|
1405
2716
|
const reportData = {
|
|
1406
2717
|
stats: serializeStats(stats),
|
|
1407
2718
|
generatedAt: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN"),
|
|
@@ -1412,7 +2723,7 @@ async function buildHtml(stats, options) {
|
|
|
1412
2723
|
repos: options.repoNames
|
|
1413
2724
|
};
|
|
1414
2725
|
const jsonData = JSON.stringify(reportData).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
1415
|
-
return template.replace("__REPORT_DATA__", jsonData);
|
|
2726
|
+
return template.replace("__REPORT_DATA__", jsonData).replace("__REPORT_SECTIONS__", reportSections).replace("__REPORT_SCRIPT__", reportScript);
|
|
1416
2727
|
}
|
|
1417
2728
|
function serializeStats(stats) {
|
|
1418
2729
|
const serializeAuthorDetail = (author) => ({
|
|
@@ -1427,6 +2738,10 @@ function serializeStats(stats) {
|
|
|
1427
2738
|
...a,
|
|
1428
2739
|
lastActiveDate: a.lastActiveDate.toISOString()
|
|
1429
2740
|
})),
|
|
2741
|
+
commitDetails: stats.commitDetails.map((commit) => ({
|
|
2742
|
+
...commit,
|
|
2743
|
+
date: commit.date.toISOString()
|
|
2744
|
+
})),
|
|
1430
2745
|
contributorChurn: stats.contributorChurn ? {
|
|
1431
2746
|
...stats.contributorChurn,
|
|
1432
2747
|
active: stats.contributorChurn.active.map(serializeAuthorDetail),
|
|
@@ -1434,23 +2749,39 @@ function serializeStats(stats) {
|
|
|
1434
2749
|
dormant: stats.contributorChurn.dormant.map(serializeAuthorDetail),
|
|
1435
2750
|
lost: stats.contributorChurn.lost.map(serializeAuthorDetail),
|
|
1436
2751
|
newJoiners: stats.contributorChurn.newJoiners.map(serializeAuthorDetail)
|
|
2752
|
+
} : void 0,
|
|
2753
|
+
changeSizeDistribution: stats.changeSizeDistribution ? {
|
|
2754
|
+
...stats.changeSizeDistribution,
|
|
2755
|
+
largeCommits: stats.changeSizeDistribution.largeCommits.map((c) => ({
|
|
2756
|
+
...c,
|
|
2757
|
+
date: c.date.toISOString()
|
|
2758
|
+
}))
|
|
1437
2759
|
} : void 0
|
|
1438
2760
|
};
|
|
1439
2761
|
}
|
|
1440
|
-
async function
|
|
2762
|
+
async function loadReportScript() {
|
|
2763
|
+
return loadTemplateParts(REPORT_SCRIPT_FILES);
|
|
2764
|
+
}
|
|
2765
|
+
async function loadTemplateParts(fileNames) {
|
|
2766
|
+
const parts = await Promise.all(
|
|
2767
|
+
fileNames.map((fileName) => loadTemplateFile(fileName))
|
|
2768
|
+
);
|
|
2769
|
+
return parts.join("\n");
|
|
2770
|
+
}
|
|
2771
|
+
async function loadTemplateFile(fileName) {
|
|
1441
2772
|
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
1442
2773
|
const possiblePaths = [
|
|
1443
|
-
resolve(currentDir, "../templates
|
|
1444
|
-
resolve(currentDir, "../../templates
|
|
1445
|
-
resolve(currentDir, "../../../templates
|
|
2774
|
+
resolve(currentDir, "../templates", fileName),
|
|
2775
|
+
resolve(currentDir, "../../templates", fileName),
|
|
2776
|
+
resolve(currentDir, "../../../templates", fileName)
|
|
1446
2777
|
];
|
|
1447
2778
|
for (const templatePath of possiblePaths) {
|
|
1448
2779
|
try {
|
|
1449
|
-
return await
|
|
2780
|
+
return await readFile4(templatePath, "utf-8");
|
|
1450
2781
|
} catch {
|
|
1451
2782
|
}
|
|
1452
2783
|
}
|
|
1453
|
-
throw new Error(
|
|
2784
|
+
throw new Error(`\u65E0\u6CD5\u627E\u5230 HTML \u6A21\u677F\u6587\u4EF6: ${fileName}`);
|
|
1454
2785
|
}
|
|
1455
2786
|
|
|
1456
2787
|
// src/reporter/index.ts
|
|
@@ -1523,7 +2854,7 @@ function resolveTimeRange(opts) {
|
|
|
1523
2854
|
|
|
1524
2855
|
// src/cli/index.ts
|
|
1525
2856
|
var program = new Command();
|
|
1526
|
-
program.name("commit-report").description("Git \u63D0\u4EA4\u7EDF\u8BA1\u5DE5\u5177\uFF0C\u751F\u6210\u53EF\u89C6\u5316 HTML \u62A5\u544A").version("1.0.
|
|
2857
|
+
program.name("commit-report").description("Git \u63D0\u4EA4\u7EDF\u8BA1\u5DE5\u5177\uFF0C\u751F\u6210\u53EF\u89C6\u5316 HTML \u62A5\u544A").version("1.0.1").argument("[directory]", "\u8981\u626B\u63CF\u7684\u76EE\u5F55\u8DEF\u5F84", process.cwd()).option("-p, --period <period>", "\u65F6\u95F4\u9884\u8BBE (7d/1m/3m/6m/1y/all)", "all").option("-f, --from <date>", "\u8D77\u59CB\u65E5\u671F (YYYY-MM-DD)").option("-t, --to <date>", "\u7ED3\u675F\u65E5\u671F (YYYY-MM-DD)").option("-a, --author <name>", "\u8FC7\u6EE4\u4F5C\u8005").option("-o, --output <file>", "\u8F93\u51FA\u6587\u4EF6\u540D", "commit-report.html").option("--no-open", "\u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668").option("-d, --depth <number>", "\u6700\u5927\u626B\u63CF\u6DF1\u5EA6", "20").action(async (directory, opts) => {
|
|
1527
2858
|
try {
|
|
1528
2859
|
await run(directory, opts);
|
|
1529
2860
|
} catch (error) {
|
|
@@ -1593,9 +2924,9 @@ async function run(directory, opts) {
|
|
|
1593
2924
|
});
|
|
1594
2925
|
}
|
|
1595
2926
|
async function checkGitInstalled() {
|
|
1596
|
-
const { execSync:
|
|
2927
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
1597
2928
|
try {
|
|
1598
|
-
|
|
2929
|
+
execSync2("git --version", { stdio: "ignore" });
|
|
1599
2930
|
} catch {
|
|
1600
2931
|
console.error(chalk3.red("\u8BF7\u5148\u5B89\u88C5 Git"));
|
|
1601
2932
|
process.exit(1);
|