Skip to content

Commit ced4223

Browse files
committed
feat: add production-ready error handling and validation
- Add comprehensive input validation for all tools - Add timeout protection (30s) for git commands - Add buffer limits (10MB) for large repos - Add pagination limits to prevent memory issues - Validate date formats (YYYY-MM-DD) - Validate git repository existence - Improve error messages with context
1 parent 713709c commit ced4223

File tree

1 file changed

+64
-8
lines changed

1 file changed

+64
-8
lines changed

src/git-metrics.ts

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,36 @@ function runGitCommand(repoPath: string, command: string): string {
2626
if (!existsSync(fullPath)) {
2727
throw new Error(`Repository path does not exist: ${fullPath}`);
2828
}
29-
return execSync(command, { cwd: fullPath, encoding: "utf-8" });
29+
try {
30+
return execSync(command, {
31+
cwd: fullPath,
32+
encoding: "utf-8",
33+
timeout: 30000,
34+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large repos
35+
});
36+
} catch (error: any) {
37+
throw new Error(`Git command failed: ${error.message}`);
38+
}
39+
}
40+
41+
function validateDate(date: string, fieldName: string): void {
42+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
43+
throw new Error(`Invalid ${fieldName} format. Use YYYY-MM-DD (e.g., 2025-11-21)`);
44+
}
45+
}
46+
47+
function validateRepoPath(repoPath: string): void {
48+
if (!repoPath || typeof repoPath !== 'string') {
49+
throw new Error('repo_path is required and must be a string');
50+
}
51+
const fullPath = resolve(repoPath);
52+
if (!existsSync(fullPath)) {
53+
throw new Error(`Repository path does not exist: ${fullPath}`);
54+
}
55+
const gitPath = resolve(fullPath, '.git');
56+
if (!existsSync(gitPath)) {
57+
throw new Error(`Not a git repository: ${fullPath}`);
58+
}
3059
}
3160

3261
function parseCommitData(output: string) {
@@ -190,13 +219,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
190219

191220
if (request.params.name === "get_commit_stats") {
192221
const { repo_path, since, until, author } = args;
222+
223+
validateRepoPath(repo_path);
224+
validateDate(since, "since");
225+
if (until) validateDate(until, "until");
226+
193227
let cmd = `git log --since="${since}"`;
194228
if (until) cmd += ` --until="${until}"`;
195229
if (author) cmd += ` --author="${author}"`;
196230
cmd += ` --pretty=format:"%H|%an|%ae|%ad|%s" --date=short --numstat`;
197231

198232
const output = runGitCommand(repo_path, cmd);
199-
const lines = output.trim().split("\n");
233+
const lines = output.trim().split("\n").slice(0, 10000); // Limit to 10k lines
200234

201235
let commits = 0, additions = 0, deletions = 0, filesChanged = 0;
202236

@@ -226,12 +260,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
226260

227261
if (request.params.name === "get_author_metrics") {
228262
const { repo_path, since, until } = args;
263+
264+
validateRepoPath(repo_path);
265+
validateDate(since, "since");
266+
if (until) validateDate(until, "until");
267+
229268
let cmd = `git log --since="${since}"`;
230269
if (until) cmd += ` --until="${until}"`;
231270
cmd += ` --pretty=format:"%an|%ae" --numstat`;
232271

233272
const output = runGitCommand(repo_path, cmd);
234-
const lines = output.trim().split("\n");
273+
const lines = output.trim().split("\n").slice(0, 10000);
235274

236275
const authorStats: Record<string, { commits: number; additions: number; deletions: number; files: number }> = {};
237276
let currentAuthor = "";
@@ -259,10 +298,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
259298

260299
if (request.params.name === "get_file_churn") {
261300
const { repo_path, since, limit = 10 } = args;
301+
302+
validateRepoPath(repo_path);
303+
validateDate(since, "since");
304+
262305
const cmd = `git log --since="${since}" --name-only --pretty=format:`;
263306

264307
const output = runGitCommand(repo_path, cmd);
265-
const files = output.trim().split("\n").filter(f => f);
308+
const files = output.trim().split("\n").filter(f => f).slice(0, 10000);
266309

267310
const fileCount: Record<string, number> = {};
268311
for (const file of files) {
@@ -271,7 +314,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
271314

272315
const sorted = Object.entries(fileCount)
273316
.sort(([, a], [, b]) => b - a)
274-
.slice(0, limit)
317+
.slice(0, Math.min(limit, 100)) // Max 100 files
275318
.map(([file, changes]) => ({ file, changes }));
276319

277320
return {
@@ -282,12 +325,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
282325
if (request.params.name === "get_team_summary") {
283326
const { repo_path, since, until } = args;
284327

328+
validateRepoPath(repo_path);
329+
validateDate(since, "since");
330+
if (until) validateDate(until, "until");
331+
285332
let statsCmd = `git log --since="${since}"`;
286333
if (until) statsCmd += ` --until="${until}"`;
287334
statsCmd += ` --pretty=format:"%an|%ae" --numstat`;
288335

289336
const output = runGitCommand(repo_path, statsCmd);
290-
const lines = output.trim().split("\n");
337+
const lines = output.trim().split("\n").slice(0, 10000);
291338

292339
const authorStats: Record<string, any> = {};
293340
let currentAuthor = "";
@@ -331,12 +378,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
331378

332379
if (request.params.name === "get_commit_patterns") {
333380
const { repo_path, since, until } = args;
381+
382+
validateRepoPath(repo_path);
383+
validateDate(since, "since");
384+
if (until) validateDate(until, "until");
385+
334386
let cmd = `git log --since="${since}"`;
335387
if (until) cmd += ` --until="${until}"`;
336388
cmd += ` --pretty=format:"%ad" --date=format:"%u|%H"`;
337389

338390
const output = runGitCommand(repo_path, cmd);
339-
const lines = output.trim().split("\n");
391+
const lines = output.trim().split("\n").slice(0, 10000);
340392

341393
const byDay: Record<string, number> = { "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0, "7": 0 };
342394
const byHour: Record<string, number> = {};
@@ -374,10 +426,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
374426

375427
if (request.params.name === "get_code_ownership") {
376428
const { repo_path, since } = args;
429+
430+
validateRepoPath(repo_path);
431+
validateDate(since, "since");
432+
377433
const cmd = `git log --since="${since}" --pretty=format:"%an|%ae" --name-only`;
378434

379435
const output = runGitCommand(repo_path, cmd);
380-
const lines = output.trim().split("\n");
436+
const lines = output.trim().split("\n").slice(0, 10000);
381437

382438
const fileAuthors: Record<string, Set<string>> = {};
383439
let currentAuthor = "";

0 commit comments

Comments
 (0)