Skip to content

Commit d80b527

Browse files
committed
feat: add conventional commits analysis tool
- Analyze commit types (feat, fix, docs, etc.) and their frequency - Track scopes usage across commits - Count breaking changes - List recent releases with dates - Calculate release frequency - Show conventional commit adoption percentage
1 parent 6491f93 commit d80b527

File tree

3 files changed

+95
-0
lines changed

3 files changed

+95
-0
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This server provides tools to extract meaningful metrics from git repositories,
1818
- **Collaboration Metrics**: Team interaction patterns
1919
- **Quality Metrics**: Commit size, reverts, and fix rates
2020
- **Technical Debt**: Stale files and complexity hotspots
21+
- **Conventional Commits**: Analyze commit types, scopes, and release frequency
2122

2223
### Production Features
2324

@@ -379,6 +380,35 @@ Identify technical debt.
379380
}
380381
```
381382

383+
### get_conventional_commits
384+
385+
Analyze conventional commit usage and release patterns.
386+
387+
**Parameters:**
388+
- `repo_path` (required): Path to git repository
389+
- `since` (required): Start date (YYYY-MM-DD)
390+
391+
**Returns:**
392+
```json
393+
{
394+
"totalCommits": 25,
395+
"conventionalCommits": 25,
396+
"conventionalPercentage": "100.0%",
397+
"commitTypes": [
398+
{ "type": "feat", "count": 5 },
399+
{ "type": "fix", "count": 5 }
400+
],
401+
"topScopes": [
402+
{ "scope": "main", "count": 8 }
403+
],
404+
"breakingChanges": 0,
405+
"recentReleases": [
406+
{ "tag": "v2.0.1", "date": "2025-11-24" }
407+
],
408+
"releaseFrequency": "8 releases since 2025-11-01"
409+
}
410+
```
411+
382412
## Use Cases
383413

384414
### Sprint Retrospectives

src/git-metrics.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
211211
required: ["repo_path"],
212212
},
213213
},
214+
{
215+
name: "get_conventional_commits",
216+
description: "Analyze conventional commit usage and release patterns",
217+
inputSchema: {
218+
type: "object",
219+
properties: {
220+
repo_path: { type: "string", description: "Path to git repository" },
221+
since: { type: "string", description: "Start date (YYYY-MM-DD)" },
222+
},
223+
required: ["repo_path", "since"],
224+
},
225+
},
214226
],
215227
}));
216228

@@ -244,6 +256,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
244256
result = handlers.handleGetQualityMetrics(args);
245257
} else if (request.params.name === "get_technical_debt") {
246258
result = handlers.handleGetTechnicalDebt(args);
259+
} else if (request.params.name === "get_conventional_commits") {
260+
result = handlers.handleGetConventionalCommits(args);
247261
} else {
248262
log('ERROR', 'Unknown tool', { tool: toolName });
249263
throw new Error(`Unknown tool: ${request.params.name}`);

src/handlers.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,54 @@ export function handleGetTechnicalDebt(args: any) {
349349
complexityHotspots: largeFiles.sort((a, b) => b.churn - a.churn).slice(0, 10),
350350
};
351351
}
352+
353+
export function handleGetConventionalCommits(args: any) {
354+
const { repo_path, since } = args;
355+
356+
validateRepoPath(repo_path);
357+
validateDate(since, "since");
358+
359+
const cmd = `git log --since="${since}" --pretty=format:"%H|%s|%ad" --date=short`;
360+
const output = runGitCommand(repo_path, cmd);
361+
const lines = output.trim().split("\n").filter(l => l);
362+
363+
const types: Record<string, number> = {};
364+
const scopes: Record<string, number> = {};
365+
let breaking = 0;
366+
let conventional = 0;
367+
368+
const conventionalRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(([^)]+)\))?(!)?:/;
369+
370+
for (const line of lines) {
371+
const [hash, message, date] = line.split("|");
372+
const match = message.match(conventionalRegex);
373+
374+
if (match) {
375+
conventional++;
376+
const [, type, , scope, isBreaking] = match;
377+
types[type] = (types[type] || 0) + 1;
378+
if (scope) scopes[scope] = (scopes[scope] || 0) + 1;
379+
if (isBreaking || message.includes("BREAKING CHANGE")) breaking++;
380+
}
381+
}
382+
383+
const tagsCmd = `git tag --sort=-creatordate --format="%(refname:short)|%(creatordate:short)"`;
384+
const tagsOutput = runGitCommand(repo_path, tagsCmd);
385+
const tags = tagsOutput.trim().split("\n").filter(t => t).slice(0, 20);
386+
387+
const releases = tags.map(t => {
388+
const [tag, date] = t.split("|");
389+
return { tag, date };
390+
});
391+
392+
return {
393+
totalCommits: lines.length,
394+
conventionalCommits: conventional,
395+
conventionalPercentage: `${((conventional / lines.length) * 100).toFixed(1)}%`,
396+
commitTypes: Object.entries(types).sort(([,a], [,b]) => b - a).map(([type, count]) => ({ type, count })),
397+
topScopes: Object.entries(scopes).sort(([,a], [,b]) => b - a).slice(0, 10).map(([scope, count]) => ({ scope, count })),
398+
breakingChanges: breaking,
399+
recentReleases: releases,
400+
releaseFrequency: releases.length > 1 ? `${releases.length} releases since ${since}` : "No releases found"
401+
};
402+
}

0 commit comments

Comments
 (0)