commit-report 1.0.0 → 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 +10 -3
- package/dist/index.js +1818 -447
- 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,357 +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
|
-
|
|
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
|
+
}
|
|
222
286
|
}
|
|
223
|
-
authorStat.commits++;
|
|
224
|
-
authorStat.lastActiveDate = commit.date;
|
|
225
287
|
for (const file of commit.files) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (!ftStat) {
|
|
234
|
-
ftStat = { extension: ext, added: 0, deleted: 0, fileCount: 0 };
|
|
235
|
-
fileTypeMap.set(ext, ftStat);
|
|
236
|
-
}
|
|
237
|
-
ftStat.added += file.added;
|
|
238
|
-
ftStat.deleted += file.deleted;
|
|
239
|
-
const topDir = getTopDirectory(file.path);
|
|
240
|
-
let dirStat = directoryMap.get(topDir);
|
|
241
|
-
if (!dirStat) {
|
|
242
|
-
dirStat = { path: topDir, commits: 0, linesChanged: 0 };
|
|
243
|
-
directoryMap.set(topDir, dirStat);
|
|
244
|
-
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
|
+
});
|
|
245
295
|
}
|
|
246
|
-
dirStat.linesChanged += file.added + file.deleted;
|
|
247
|
-
directoryCommitSet.get(topDir).add(commit.hash);
|
|
248
296
|
}
|
|
249
297
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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 {
|
|
254
315
|
}
|
|
255
316
|
}
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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++;
|
|
261
380
|
}
|
|
262
|
-
fileCountByExt.get(ext).add(filePath);
|
|
263
381
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
};
|
|
269
395
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
});
|
|
423
|
+
}
|
|
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
|
|
473
|
+
});
|
|
274
474
|
}
|
|
275
475
|
}
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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));
|
|
283
497
|
return {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
firstCommitDate: sorted[0].date,
|
|
289
|
-
lastCommitDate: sorted[sorted.length - 1].date,
|
|
290
|
-
busiestDay,
|
|
291
|
-
authors,
|
|
292
|
-
fileTypes,
|
|
293
|
-
directories,
|
|
294
|
-
hourlyDistribution,
|
|
295
|
-
dailyHeatmap,
|
|
296
|
-
quality: calculateQualityMetrics(sorted),
|
|
297
|
-
timePatterns: calculateTimePatterns(sorted),
|
|
298
|
-
trends: calculateTrends(sorted),
|
|
299
|
-
collaboration: calculateCollaboration(sorted),
|
|
300
|
-
messageStats: calculateMessageStats(sorted),
|
|
301
|
-
authorFileTypeContributions: calculateAuthorFileTypeContributions(sorted)
|
|
498
|
+
aiMetrics,
|
|
499
|
+
authorAIStats: authorAIStats.sort((a, b) => b.aiPercentage - a.aiPercentage),
|
|
500
|
+
directoryAIStats: directoryAIStats.sort((a, b) => b.aiPercentage - a.aiPercentage),
|
|
501
|
+
aiTrends
|
|
302
502
|
};
|
|
303
503
|
}
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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) {
|
|
533
|
+
return {
|
|
534
|
+
buckets: emptyBuckets(),
|
|
535
|
+
avgChangeSize: 0,
|
|
536
|
+
medianChangeSize: 0,
|
|
537
|
+
p95ChangeSize: 0,
|
|
538
|
+
largeCommits: []
|
|
539
|
+
};
|
|
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);
|
|
577
|
+
return {
|
|
578
|
+
buckets,
|
|
579
|
+
avgChangeSize,
|
|
580
|
+
medianChangeSize,
|
|
581
|
+
p95ChangeSize,
|
|
582
|
+
largeCommits
|
|
583
|
+
};
|
|
584
|
+
}
|
|
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);
|
|
324
598
|
}
|
|
325
|
-
for (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
existing.linesAdded += author.linesAdded;
|
|
332
|
-
existing.linesDeleted += author.linesDeleted;
|
|
333
|
-
if (author.lastActiveDate > existing.lastActiveDate) {
|
|
334
|
-
existing.lastActiveDate = author.lastActiveDate;
|
|
335
|
-
}
|
|
336
|
-
} else {
|
|
337
|
-
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);
|
|
338
605
|
}
|
|
339
606
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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 });
|
|
348
620
|
} else {
|
|
349
|
-
|
|
621
|
+
const key = d1 < d2 ? `${d1}|||${d2}` : `${d2}|||${d1}`;
|
|
622
|
+
matrix.push({ dir1: d1, dir2: d2, value: pairMap.get(key)?.count || 0 });
|
|
350
623
|
}
|
|
351
624
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
} else {
|
|
387
|
-
merged.trends.weeklyTrend.push({ ...wp });
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
for (const cp of stats.trends.cumulativeLines) {
|
|
391
|
-
const existing = merged.trends.cumulativeLines.find(
|
|
392
|
-
(c) => c.date === cp.date
|
|
393
|
-
);
|
|
394
|
-
if (existing) {
|
|
395
|
-
existing.netLines += cp.netLines;
|
|
396
|
-
} else {
|
|
397
|
-
merged.trends.cumulativeLines.push({ ...cp });
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
for (const sf of stats.collaboration.soloFiles) {
|
|
401
|
-
const existing = merged.collaboration.soloFiles.find(
|
|
402
|
-
(s) => s.path === sf.path
|
|
403
|
-
);
|
|
404
|
-
if (existing) {
|
|
405
|
-
existing.commits += sf.commits;
|
|
406
|
-
} else {
|
|
407
|
-
merged.collaboration.soloFiles.push({ ...sf });
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
for (const ch of stats.collaboration.collaborationHotspots) {
|
|
411
|
-
const existing = merged.collaboration.collaborationHotspots.find(
|
|
412
|
-
(c) => c.path === ch.path
|
|
413
|
-
);
|
|
414
|
-
if (existing) {
|
|
415
|
-
existing.totalCommits += ch.totalCommits;
|
|
416
|
-
existing.authorCount = Math.max(existing.authorCount, ch.authorCount);
|
|
417
|
-
} else {
|
|
418
|
-
merged.collaboration.collaborationHotspots.push({ ...ch });
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
for (const [type, count] of Object.entries(stats.messageStats.typeDistribution)) {
|
|
422
|
-
merged.messageStats.typeDistribution[type] = (merged.messageStats.typeDistribution[type] || 0) + count;
|
|
423
|
-
}
|
|
424
|
-
merged.messageStats.avgMessageLength += stats.messageStats.avgMessageLength;
|
|
425
|
-
}
|
|
426
|
-
let busiestDay = { date: "", count: 0 };
|
|
427
|
-
for (const [date, count] of Object.entries(merged.dailyHeatmap)) {
|
|
428
|
-
if (count > busiestDay.count) {
|
|
429
|
-
busiestDay = { date, count };
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
merged.busiestDay = busiestDay;
|
|
433
|
-
merged.authors.sort((a, b) => b.commits - a.commits);
|
|
434
|
-
merged.fileTypes.sort(
|
|
435
|
-
(a, b) => b.added + b.deleted - (a.added + a.deleted)
|
|
436
|
-
);
|
|
437
|
-
merged.directories.sort((a, b) => b.linesChanged - a.linesChanged);
|
|
438
|
-
merged.directories = merged.directories.slice(0, 10);
|
|
439
|
-
const repoCount = statsList.length;
|
|
440
|
-
merged.quality.avgFilesPerCommit /= repoCount;
|
|
441
|
-
merged.quality.avgLinesPerCommit /= repoCount;
|
|
442
|
-
merged.quality.churnRate /= repoCount;
|
|
443
|
-
merged.quality.hotFiles.sort((a, b) => b.modifyCount - a.modifyCount);
|
|
444
|
-
merged.quality.hotFiles = merged.quality.hotFiles.slice(0, 10);
|
|
445
|
-
const totalWeekdayCommits = merged.timePatterns.weekdayDistribution.reduce(
|
|
446
|
-
(a, b) => a + b,
|
|
447
|
-
0
|
|
448
|
-
);
|
|
449
|
-
if (totalWeekdayCommits > 0) {
|
|
450
|
-
merged.timePatterns.weekendCommits = (merged.timePatterns.weekdayDistribution[5] + merged.timePatterns.weekdayDistribution[6]) / totalWeekdayCommits;
|
|
451
|
-
}
|
|
452
|
-
merged.trends.weeklyTrend.sort((a, b) => a.week.localeCompare(b.week));
|
|
453
|
-
merged.trends.cumulativeLines.sort((a, b) => a.date.localeCompare(b.date));
|
|
454
|
-
let cumulative = 0;
|
|
455
|
-
for (const point of merged.trends.cumulativeLines) {
|
|
456
|
-
cumulative += point.netLines;
|
|
457
|
-
point.netLines = cumulative;
|
|
458
|
-
}
|
|
459
|
-
merged.collaboration.soloFiles.sort((a, b) => b.commits - a.commits);
|
|
460
|
-
merged.collaboration.soloFiles = merged.collaboration.soloFiles.slice(0, 10);
|
|
461
|
-
merged.collaboration.collaborationHotspots.sort(
|
|
462
|
-
(a, b) => b.totalCommits - a.totalCommits
|
|
463
|
-
);
|
|
464
|
-
merged.collaboration.collaborationHotspots = merged.collaboration.collaborationHotspots.slice(0, 10);
|
|
465
|
-
merged.messageStats.avgMessageLength /= repoCount;
|
|
466
|
-
const contributionMap = /* @__PURE__ */ new Map();
|
|
467
|
-
for (const stats of statsList) {
|
|
468
|
-
for (const contrib of stats.authorFileTypeContributions) {
|
|
469
|
-
const key = `${contrib.email.toLowerCase()}|||${contrib.extension}`;
|
|
470
|
-
const existing = contributionMap.get(key);
|
|
471
|
-
if (existing) {
|
|
472
|
-
existing.linesAdded += contrib.linesAdded;
|
|
473
|
-
existing.linesDeleted += contrib.linesDeleted;
|
|
474
|
-
existing.commits += contrib.commits;
|
|
475
|
-
existing.fileCount += contrib.fileCount;
|
|
476
|
-
} else {
|
|
477
|
-
contributionMap.set(key, { ...contrib });
|
|
478
|
-
}
|
|
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);
|
|
479
659
|
}
|
|
480
660
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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);
|
|
489
687
|
return {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
firstCommitDate: /* @__PURE__ */ new Date(),
|
|
495
|
-
lastCommitDate: /* @__PURE__ */ new Date(),
|
|
496
|
-
busiestDay: { date: "", count: 0 },
|
|
497
|
-
authors: [],
|
|
498
|
-
fileTypes: [],
|
|
499
|
-
directories: [],
|
|
500
|
-
hourlyDistribution: new Array(24).fill(0),
|
|
501
|
-
dailyHeatmap: {},
|
|
502
|
-
quality: emptyQualityMetrics(),
|
|
503
|
-
timePatterns: emptyTimePatterns(),
|
|
504
|
-
trends: emptyTrendData(),
|
|
505
|
-
collaboration: emptyCollaborationMetrics(),
|
|
506
|
-
messageStats: emptyMessageStats(),
|
|
507
|
-
authorFileTypeContributions: []
|
|
688
|
+
files: files.slice(0, 20),
|
|
689
|
+
scatter: scatter.slice(0, 500),
|
|
690
|
+
// 限制散点数
|
|
691
|
+
summary: { highAIHighChurn, highAILowChurn, lowAIHighChurn, lowAILowChurn }
|
|
508
692
|
};
|
|
509
693
|
}
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
];
|
|
513
702
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
+
});
|
|
519
729
|
}
|
|
520
730
|
function calculateQualityMetrics(commits) {
|
|
521
731
|
if (commits.length === 0) {
|
|
522
732
|
return emptyQualityMetrics();
|
|
523
733
|
}
|
|
524
|
-
const totalFiles = commits.reduce((sum,
|
|
734
|
+
const totalFiles = commits.reduce((sum, commit) => sum + commit.files.length, 0);
|
|
525
735
|
const avgFilesPerCommit = totalFiles / commits.length;
|
|
526
736
|
const totalLines = commits.reduce(
|
|
527
|
-
(sum,
|
|
737
|
+
(sum, commit) => sum + commit.files.reduce((s, file) => s + file.added + file.deleted, 0),
|
|
528
738
|
0
|
|
529
739
|
);
|
|
530
740
|
const avgLinesPerCommit = totalLines / commits.length;
|
|
531
741
|
const totalAdded = commits.reduce(
|
|
532
|
-
(sum,
|
|
742
|
+
(sum, commit) => sum + commit.files.reduce((s, file) => s + file.added, 0),
|
|
533
743
|
0
|
|
534
744
|
);
|
|
535
745
|
const totalDeleted = commits.reduce(
|
|
536
|
-
(sum,
|
|
746
|
+
(sum, commit) => sum + commit.files.reduce((s, file) => s + file.deleted, 0),
|
|
537
747
|
0
|
|
538
748
|
);
|
|
539
749
|
const churnRate = totalAdded > 0 ? totalDeleted / totalAdded : 0;
|
|
@@ -558,10 +768,16 @@ function calculateTimePatterns(commits) {
|
|
|
558
768
|
return emptyTimePatterns();
|
|
559
769
|
}
|
|
560
770
|
const weekdayDistribution = new Array(7).fill(0);
|
|
771
|
+
const weekdayByAuthor = /* @__PURE__ */ new Map();
|
|
561
772
|
for (const commit of commits) {
|
|
562
773
|
const day = commit.date.getDay();
|
|
563
774
|
const idx = day === 0 ? 6 : day - 1;
|
|
564
775
|
weekdayDistribution[idx]++;
|
|
776
|
+
if (!weekdayByAuthor.has(idx)) {
|
|
777
|
+
weekdayByAuthor.set(idx, /* @__PURE__ */ new Map());
|
|
778
|
+
}
|
|
779
|
+
const dayAuthors = weekdayByAuthor.get(idx);
|
|
780
|
+
dayAuthors.set(commit.author, (dayAuthors.get(commit.author) || 0) + 1);
|
|
565
781
|
}
|
|
566
782
|
const weekendCommits = (weekdayDistribution[5] + weekdayDistribution[6]) / commits.length;
|
|
567
783
|
const sorted = [...commits].sort(
|
|
@@ -573,85 +789,35 @@ function calculateTimePatterns(commits) {
|
|
|
573
789
|
}
|
|
574
790
|
const avgCommitInterval = sorted.length > 1 ? totalInterval / (sorted.length - 1) / 36e5 : 0;
|
|
575
791
|
const { longestStreak, currentStreak } = calculateStreaks(sorted);
|
|
792
|
+
const weekdayByAuthorArray = Array.from({ length: 7 }, (_, day) => {
|
|
793
|
+
const authorMap = weekdayByAuthor.get(day);
|
|
794
|
+
const authors = {};
|
|
795
|
+
if (authorMap) {
|
|
796
|
+
authorMap.forEach((count, author) => {
|
|
797
|
+
authors[author] = count;
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
count: weekdayDistribution[day],
|
|
802
|
+
authors
|
|
803
|
+
};
|
|
804
|
+
});
|
|
576
805
|
return {
|
|
577
806
|
weekdayDistribution,
|
|
578
807
|
weekendCommits,
|
|
579
808
|
avgCommitInterval,
|
|
580
809
|
longestStreak,
|
|
581
|
-
currentStreak
|
|
810
|
+
currentStreak,
|
|
811
|
+
weekdayByAuthor: weekdayByAuthorArray
|
|
582
812
|
};
|
|
583
813
|
}
|
|
584
|
-
function calculateStreaks(sortedCommits) {
|
|
585
|
-
if (sortedCommits.length === 0) {
|
|
586
|
-
return { longestStreak: 0, currentStreak: 0 };
|
|
587
|
-
}
|
|
588
|
-
const uniqueDates = /* @__PURE__ */ new Set();
|
|
589
|
-
for (const commit of sortedCommits) {
|
|
590
|
-
uniqueDates.add(formatDateKey(commit.date));
|
|
591
|
-
}
|
|
592
|
-
const sortedDates = Array.from(uniqueDates).sort();
|
|
593
|
-
if (sortedDates.length === 0) {
|
|
594
|
-
return { longestStreak: 0, currentStreak: 0 };
|
|
595
|
-
}
|
|
596
|
-
let longestStreak = 1;
|
|
597
|
-
let currentStreakCount = 1;
|
|
598
|
-
let tempStreak = 1;
|
|
599
|
-
for (let i = 1; i < sortedDates.length; i++) {
|
|
600
|
-
const prevDate = new Date(sortedDates[i - 1]);
|
|
601
|
-
const currDate = new Date(sortedDates[i]);
|
|
602
|
-
const diffDays = Math.round(
|
|
603
|
-
(currDate.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
604
|
-
);
|
|
605
|
-
if (diffDays === 1) {
|
|
606
|
-
tempStreak++;
|
|
607
|
-
} else {
|
|
608
|
-
tempStreak = 1;
|
|
609
|
-
}
|
|
610
|
-
longestStreak = Math.max(longestStreak, tempStreak);
|
|
611
|
-
}
|
|
612
|
-
const today = formatDateKey(/* @__PURE__ */ new Date());
|
|
613
|
-
const lastCommitDate = sortedDates[sortedDates.length - 1];
|
|
614
|
-
const lastDate = new Date(lastCommitDate);
|
|
615
|
-
const todayDate = new Date(today);
|
|
616
|
-
const daysSinceLastCommit = Math.round(
|
|
617
|
-
(todayDate.getTime() - lastDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
618
|
-
);
|
|
619
|
-
if (daysSinceLastCommit <= 1) {
|
|
620
|
-
currentStreakCount = 1;
|
|
621
|
-
for (let i = sortedDates.length - 2; i >= 0; i--) {
|
|
622
|
-
const currDate = new Date(sortedDates[i + 1]);
|
|
623
|
-
const prevDate = new Date(sortedDates[i]);
|
|
624
|
-
const diffDays = Math.round(
|
|
625
|
-
(currDate.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
626
|
-
);
|
|
627
|
-
if (diffDays === 1) {
|
|
628
|
-
currentStreakCount++;
|
|
629
|
-
} else {
|
|
630
|
-
break;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
} else {
|
|
634
|
-
currentStreakCount = 0;
|
|
635
|
-
}
|
|
636
|
-
return { longestStreak, currentStreak: currentStreakCount };
|
|
637
|
-
}
|
|
638
|
-
function getWeekKey(date) {
|
|
639
|
-
const d = new Date(date);
|
|
640
|
-
d.setHours(0, 0, 0, 0);
|
|
641
|
-
d.setDate(d.getDate() + 4 - (d.getDay() || 7));
|
|
642
|
-
const yearStart = new Date(d.getFullYear(), 0, 1);
|
|
643
|
-
const weekNo = Math.ceil(
|
|
644
|
-
((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7
|
|
645
|
-
);
|
|
646
|
-
return `${d.getFullYear()}-W${String(weekNo).padStart(2, "0")}`;
|
|
647
|
-
}
|
|
648
814
|
function calculateTrends(commits) {
|
|
649
815
|
if (commits.length === 0) {
|
|
650
816
|
return emptyTrendData();
|
|
651
817
|
}
|
|
652
818
|
const weekMap = /* @__PURE__ */ new Map();
|
|
653
819
|
for (const commit of commits) {
|
|
654
|
-
const week =
|
|
820
|
+
const week = getWeekKey2(commit.date);
|
|
655
821
|
const entry = weekMap.get(week) || {
|
|
656
822
|
week,
|
|
657
823
|
commits: 0,
|
|
@@ -671,7 +837,7 @@ function calculateTrends(commits) {
|
|
|
671
837
|
const dailyNet = /* @__PURE__ */ new Map();
|
|
672
838
|
for (const commit of commits) {
|
|
673
839
|
const dateKey = formatDateKey(commit.date);
|
|
674
|
-
const net = commit.files.reduce((sum,
|
|
840
|
+
const net = commit.files.reduce((sum, file) => sum + file.added - file.deleted, 0);
|
|
675
841
|
dailyNet.set(dateKey, (dailyNet.get(dateKey) || 0) + net);
|
|
676
842
|
}
|
|
677
843
|
let cumulative = 0;
|
|
@@ -777,53 +943,545 @@ function calculateAuthorFileTypeContributions(commits) {
|
|
|
777
943
|
if (!commitCountMap.has(key)) {
|
|
778
944
|
commitCountMap.set(key, /* @__PURE__ */ new Set());
|
|
779
945
|
}
|
|
780
|
-
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
|
+
}
|
|
781
1409
|
}
|
|
782
1410
|
}
|
|
783
|
-
|
|
784
|
-
contribution.commits = commitCountMap.get(key)?.size || 0;
|
|
785
|
-
contribution.fileCount = uniqueFilesMap.get(key)?.size || 0;
|
|
786
|
-
}
|
|
787
|
-
return Array.from(contributionMap.values()).sort((a, b) => {
|
|
1411
|
+
merged.authorFileTypeContributions = Array.from(contributionMap.values()).sort((a, b) => {
|
|
788
1412
|
const totalA = a.linesAdded + a.linesDeleted;
|
|
789
1413
|
const totalB = b.linesAdded + b.linesDeleted;
|
|
790
1414
|
return totalB - totalA;
|
|
791
1415
|
}).slice(0, 20);
|
|
1416
|
+
return merged;
|
|
792
1417
|
}
|
|
793
|
-
function
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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)
|
|
820
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));
|
|
821
1482
|
}
|
|
822
|
-
function
|
|
823
|
-
return
|
|
824
|
-
typeDistribution: {},
|
|
825
|
-
avgMessageLength: 0
|
|
826
|
-
};
|
|
1483
|
+
function calculatePercentage(part, total) {
|
|
1484
|
+
return total > 0 ? part / total * 100 : 0;
|
|
827
1485
|
}
|
|
828
1486
|
|
|
829
1487
|
// src/analyzer/advanced/team-health.ts
|
|
@@ -952,7 +1610,7 @@ function calculateStability(commits) {
|
|
|
952
1610
|
const dirStats = /* @__PURE__ */ new Map();
|
|
953
1611
|
for (const commit of commits) {
|
|
954
1612
|
for (const file of commit.files) {
|
|
955
|
-
const dir =
|
|
1613
|
+
const dir = getTopDirectory3(file.path);
|
|
956
1614
|
const stat2 = dirStats.get(dir) || { added: 0, deleted: 0, files: /* @__PURE__ */ new Set() };
|
|
957
1615
|
stat2.added += file.added;
|
|
958
1616
|
stat2.deleted += file.deleted;
|
|
@@ -983,7 +1641,7 @@ function calculateStability(commits) {
|
|
|
983
1641
|
stabilityScore
|
|
984
1642
|
};
|
|
985
1643
|
}
|
|
986
|
-
function
|
|
1644
|
+
function getTopDirectory3(filePath) {
|
|
987
1645
|
const parts = filePath.split("/");
|
|
988
1646
|
return parts.length > 1 ? parts[0] : "(\u6839\u76EE\u5F55)";
|
|
989
1647
|
}
|
|
@@ -1310,6 +1968,658 @@ function calculateAdvancedStats(commits) {
|
|
|
1310
1968
|
};
|
|
1311
1969
|
}
|
|
1312
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
|
+
|
|
1313
2623
|
// src/analyzer/index.ts
|
|
1314
2624
|
async function analyzeRepos(options) {
|
|
1315
2625
|
const { repos, timeRange, author } = options;
|
|
@@ -1328,10 +2638,30 @@ async function analyzeRepos(options) {
|
|
|
1328
2638
|
}
|
|
1329
2639
|
const stats = calculateStats(commits);
|
|
1330
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
|
+
}
|
|
1331
2649
|
const fullStats = {
|
|
1332
2650
|
...stats,
|
|
1333
|
-
...advancedStats
|
|
2651
|
+
...advancedStats,
|
|
2652
|
+
...techDebt && { techDebt },
|
|
2653
|
+
...engineering && { engineering }
|
|
1334
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
|
+
});
|
|
1335
2665
|
allStats.push(fullStats);
|
|
1336
2666
|
} catch (error) {
|
|
1337
2667
|
spinner.warn(
|
|
@@ -1357,11 +2687,32 @@ import ora3 from "ora";
|
|
|
1357
2687
|
import open from "open";
|
|
1358
2688
|
|
|
1359
2689
|
// src/reporter/html-builder.ts
|
|
1360
|
-
import { readFile as
|
|
2690
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1361
2691
|
import { resolve, dirname } from "path";
|
|
1362
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
|
+
];
|
|
1363
2712
|
async function buildHtml(stats, options) {
|
|
1364
|
-
const template = await
|
|
2713
|
+
const template = await loadTemplateFile("report.html");
|
|
2714
|
+
const reportSections = await loadTemplateParts(REPORT_SECTION_FILES);
|
|
2715
|
+
const reportScript = await loadReportScript();
|
|
1365
2716
|
const reportData = {
|
|
1366
2717
|
stats: serializeStats(stats),
|
|
1367
2718
|
generatedAt: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN"),
|
|
@@ -1372,7 +2723,7 @@ async function buildHtml(stats, options) {
|
|
|
1372
2723
|
repos: options.repoNames
|
|
1373
2724
|
};
|
|
1374
2725
|
const jsonData = JSON.stringify(reportData).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
1375
|
-
return template.replace("__REPORT_DATA__", jsonData);
|
|
2726
|
+
return template.replace("__REPORT_DATA__", jsonData).replace("__REPORT_SECTIONS__", reportSections).replace("__REPORT_SCRIPT__", reportScript);
|
|
1376
2727
|
}
|
|
1377
2728
|
function serializeStats(stats) {
|
|
1378
2729
|
const serializeAuthorDetail = (author) => ({
|
|
@@ -1387,6 +2738,10 @@ function serializeStats(stats) {
|
|
|
1387
2738
|
...a,
|
|
1388
2739
|
lastActiveDate: a.lastActiveDate.toISOString()
|
|
1389
2740
|
})),
|
|
2741
|
+
commitDetails: stats.commitDetails.map((commit) => ({
|
|
2742
|
+
...commit,
|
|
2743
|
+
date: commit.date.toISOString()
|
|
2744
|
+
})),
|
|
1390
2745
|
contributorChurn: stats.contributorChurn ? {
|
|
1391
2746
|
...stats.contributorChurn,
|
|
1392
2747
|
active: stats.contributorChurn.active.map(serializeAuthorDetail),
|
|
@@ -1394,23 +2749,39 @@ function serializeStats(stats) {
|
|
|
1394
2749
|
dormant: stats.contributorChurn.dormant.map(serializeAuthorDetail),
|
|
1395
2750
|
lost: stats.contributorChurn.lost.map(serializeAuthorDetail),
|
|
1396
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
|
+
}))
|
|
1397
2759
|
} : void 0
|
|
1398
2760
|
};
|
|
1399
2761
|
}
|
|
1400
|
-
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) {
|
|
1401
2772
|
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
1402
2773
|
const possiblePaths = [
|
|
1403
|
-
resolve(currentDir, "../templates
|
|
1404
|
-
resolve(currentDir, "../../templates
|
|
1405
|
-
resolve(currentDir, "../../../templates
|
|
2774
|
+
resolve(currentDir, "../templates", fileName),
|
|
2775
|
+
resolve(currentDir, "../../templates", fileName),
|
|
2776
|
+
resolve(currentDir, "../../../templates", fileName)
|
|
1406
2777
|
];
|
|
1407
2778
|
for (const templatePath of possiblePaths) {
|
|
1408
2779
|
try {
|
|
1409
|
-
return await
|
|
2780
|
+
return await readFile4(templatePath, "utf-8");
|
|
1410
2781
|
} catch {
|
|
1411
2782
|
}
|
|
1412
2783
|
}
|
|
1413
|
-
throw new Error(
|
|
2784
|
+
throw new Error(`\u65E0\u6CD5\u627E\u5230 HTML \u6A21\u677F\u6587\u4EF6: ${fileName}`);
|
|
1414
2785
|
}
|
|
1415
2786
|
|
|
1416
2787
|
// src/reporter/index.ts
|
|
@@ -1483,7 +2854,7 @@ function resolveTimeRange(opts) {
|
|
|
1483
2854
|
|
|
1484
2855
|
// src/cli/index.ts
|
|
1485
2856
|
var program = new Command();
|
|
1486
|
-
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) => {
|
|
1487
2858
|
try {
|
|
1488
2859
|
await run(directory, opts);
|
|
1489
2860
|
} catch (error) {
|
|
@@ -1553,9 +2924,9 @@ async function run(directory, opts) {
|
|
|
1553
2924
|
});
|
|
1554
2925
|
}
|
|
1555
2926
|
async function checkGitInstalled() {
|
|
1556
|
-
const { execSync:
|
|
2927
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
1557
2928
|
try {
|
|
1558
|
-
|
|
2929
|
+
execSync2("git --version", { stdio: "ignore" });
|
|
1559
2930
|
} catch {
|
|
1560
2931
|
console.error(chalk3.red("\u8BF7\u5148\u5B89\u88C5 Git"));
|
|
1561
2932
|
process.exit(1);
|