Skip to content

Commit def68a0

Browse files
committed
feat(contrib): add repo metrics tool for tracking AI-assisted commits
Add a Python tool under contrib/repo_metrics to analyze git commits and identify which ones were AI-assisted based on commit message co-author patterns. Signed-off-by: Antoni Segura Puimedon <[email protected]> Assisted-by: Claude Sonnet 4 (via Claude Code)
1 parent 4d2eef3 commit def68a0

File tree

7 files changed

+1209
-1
lines changed

7 files changed

+1209
-1
lines changed

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ tools/bin
4343
*_mock.go
4444

4545
# dev
46-
dev/
46+
dev/
47+
48+
# Python
49+
__pycache__/
50+
*.py[cod]
51+
*$py.class
52+
*.egg-info/
53+
.pytest_cache/
54+
.venv/
55+
venv/
56+
*.egg

contrib/repo_metrics/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Repository Metrics
2+
3+
Tools for analyzing repository metrics and statistics.
4+
5+
## Setup
6+
7+
This project uses [uv](https://github.com/astral-sh/uv) for dependency management.
8+
9+
```bash
10+
# Install uv if you don't have it
11+
curl -LsSf https://astral.sh/uv/install.sh | sh
12+
13+
# Install dependencies (to be run inside the directory where pyproject.toml is)
14+
uv sync --dev
15+
```
16+
17+
## Tools
18+
19+
### AI-Assisted Commits Analyzer
20+
21+
Analyzes git commits to identify those assisted by AI tools (Claude, GPT, etc.).
22+
23+
**Usage:**
24+
25+
```bash
26+
# Run with default (last 2 weeks)
27+
uv run python ai_assisted_commits.py
28+
29+
# Analyze last N commits
30+
uv run python ai_assisted_commits.py -n 100
31+
32+
# Analyze since relative date
33+
uv run python ai_assisted_commits.py --since "1 month ago"
34+
35+
# Analyze since specific date
36+
uv run python ai_assisted_commits.py --since "2025-09-01"
37+
```
38+
39+
**Note:** You cannot specify both `--since` and `-n/--max-count` at the same time.
40+
41+
**Output:**
42+
43+
```text
44+
=== AI-Assisted Commits Report (2 weeks ago) ===
45+
46+
Absolute Numbers:
47+
Total commits: 48
48+
- Merge commits: 25
49+
- Non-merge commits: 23
50+
AI-assisted commits: 13
51+
- AI-assisted non-merge: 11
52+
- AI-assisted merge: 2
53+
54+
Percentages:
55+
Overall AI-assisted: 27.1% (13/48)
56+
Non-merge AI-assisted: 47.8% (11/23)
57+
```
58+
59+
## Testing
60+
61+
```bash
62+
# Run all tests
63+
uv run pytest
64+
65+
# Run with verbose output
66+
uv run pytest -v
67+
68+
# Run specific test file
69+
uv run pytest test_ai_assisted_commits.py -v
70+
```
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate a report of AI-assisted commits in the repository.
4+
5+
This script analyzes git commit messages to identify commits that were
6+
assisted by AI tools (Claude, GPT, etc.) and generates statistics.
7+
"""
8+
9+
import re
10+
import subprocess
11+
import sys
12+
from dataclasses import dataclass
13+
from datetime import datetime, timedelta
14+
from typing import List, Set, Tuple
15+
16+
17+
@dataclass
18+
class CommitStats:
19+
"""Statistics about commits in the repository."""
20+
21+
total_commits: int
22+
merge_commits: int
23+
non_merge_commits: int
24+
ai_assisted_commits: int
25+
ai_assisted_non_merge: int
26+
ai_assisted_merge: int
27+
ai_commits: List[str]
28+
29+
30+
AI_PATTERNS = [
31+
re.compile(r'^\s*Assisted-by:', re.MULTILINE | re.IGNORECASE),
32+
re.compile(r'^\s*Co-authored-by:\s+Claude', re.MULTILINE | re.IGNORECASE),
33+
re.compile(r'🤖\s*Generated', re.IGNORECASE),
34+
re.compile(r'^\s*Commit-Message-Assisted-by:', re.MULTILINE | re.IGNORECASE),
35+
]
36+
37+
38+
class GitCommandRunner:
39+
"""Interface for running git commands."""
40+
41+
def run(self, args: List[str]) -> str:
42+
"""Run a git command and return its output."""
43+
raise NotImplementedError
44+
45+
46+
class RealGitCommandRunner(GitCommandRunner):
47+
"""Real implementation that executes git commands."""
48+
49+
def run(self, args: List[str]) -> str:
50+
"""Run a git command and return its output."""
51+
try:
52+
result = subprocess.run(
53+
['git'] + args,
54+
capture_output=True,
55+
text=True,
56+
check=True
57+
)
58+
return result.stdout.strip()
59+
except subprocess.CalledProcessError as e:
60+
print(f"Error running git command: {e}", file=sys.stderr)
61+
sys.exit(1)
62+
63+
64+
def get_commit_hashes(git_runner: GitCommandRunner, since: str = '', max_count: int = 0) -> List[str]:
65+
"""Get all commit hashes since a given date or up to max_count commits."""
66+
args = ['log']
67+
if max_count > 0:
68+
args.extend([f'-{max_count}'])
69+
elif since:
70+
args.extend([f'--since={since}'])
71+
args.append('--pretty=format:%H')
72+
output = git_runner.run(args)
73+
return output.split('\n') if output else []
74+
75+
76+
def get_commit_body(git_runner: GitCommandRunner, commit_hash: str) -> str:
77+
"""Get the full commit message body for a given commit hash."""
78+
return git_runner.run(['show', commit_hash, '--quiet', '--format=%B'])
79+
80+
81+
def is_merge_commit(git_runner: GitCommandRunner, commit_hash: str) -> bool:
82+
"""Check if a commit is a merge commit."""
83+
output = git_runner.run(['rev-list', '--parents', '-n', '1', commit_hash])
84+
parents = output.split()
85+
return len(parents) > 2
86+
87+
88+
def is_ai_assisted(commit_body: str) -> bool:
89+
"""Check if a commit body contains AI assistance markers."""
90+
return any(pattern.search(commit_body) for pattern in AI_PATTERNS)
91+
92+
93+
def get_commit_oneline(git_runner: GitCommandRunner, commit_hash: str) -> str:
94+
"""Get the one-line summary of a commit."""
95+
return git_runner.run(['log', '--oneline', '-1', commit_hash])
96+
97+
98+
def analyze_commits(git_runner: GitCommandRunner, since: str = '2 weeks ago', max_count: int = 0) -> CommitStats:
99+
"""Analyze commits and return statistics."""
100+
commit_hashes = get_commit_hashes(git_runner, since=since, max_count=max_count)
101+
total_commits = len(commit_hashes)
102+
103+
merge_commits = 0
104+
non_merge_commits = 0
105+
ai_assisted_hashes: Set[str] = set()
106+
ai_assisted_merge = 0
107+
ai_assisted_non_merge = 0
108+
ai_commit_details: List[Tuple[str, str]] = []
109+
110+
for commit_hash in commit_hashes:
111+
body = get_commit_body(git_runner, commit_hash)
112+
is_merge = is_merge_commit(git_runner, commit_hash)
113+
is_ai = is_ai_assisted(body)
114+
115+
if is_merge:
116+
merge_commits += 1
117+
if is_ai:
118+
ai_assisted_merge += 1
119+
else:
120+
non_merge_commits += 1
121+
if is_ai:
122+
ai_assisted_non_merge += 1
123+
124+
if is_ai:
125+
ai_assisted_hashes.add(commit_hash)
126+
oneline = get_commit_oneline(git_runner, commit_hash)
127+
ai_commit_details.append((commit_hash, oneline))
128+
129+
return CommitStats(
130+
total_commits=total_commits,
131+
merge_commits=merge_commits,
132+
non_merge_commits=non_merge_commits,
133+
ai_assisted_commits=len(ai_assisted_hashes),
134+
ai_assisted_non_merge=ai_assisted_non_merge,
135+
ai_assisted_merge=ai_assisted_merge,
136+
ai_commits=[detail[1] for detail in ai_commit_details]
137+
)
138+
139+
140+
def print_report(stats: CommitStats, period: str = "last 2 weeks") -> None:
141+
"""Print the statistics report."""
142+
print(f"=== AI-Assisted Commits Report ({period}) ===\n")
143+
144+
print("Absolute Numbers:")
145+
print(f" Total commits: {stats.total_commits}")
146+
print(f" - Merge commits: {stats.merge_commits}")
147+
print(f" - Non-merge commits: {stats.non_merge_commits}")
148+
print(f" AI-assisted commits: {stats.ai_assisted_commits}")
149+
print(f" - AI-assisted non-merge: {stats.ai_assisted_non_merge}")
150+
print(f" - AI-assisted merge: {stats.ai_assisted_merge}")
151+
152+
print("\nPercentages:")
153+
if stats.total_commits > 0:
154+
overall_pct = (stats.ai_assisted_commits / stats.total_commits) * 100
155+
print(f" Overall AI-assisted: {overall_pct:.1f}% ({stats.ai_assisted_commits}/{stats.total_commits})")
156+
157+
if stats.non_merge_commits > 0:
158+
non_merge_pct = (stats.ai_assisted_non_merge / stats.non_merge_commits) * 100
159+
print(f" Non-merge AI-assisted: {non_merge_pct:.1f}% ({stats.ai_assisted_non_merge}/{stats.non_merge_commits})")
160+
161+
if stats.ai_commits:
162+
print("\nAI-Assisted Commits:")
163+
for commit in stats.ai_commits:
164+
print(f" {commit}")
165+
166+
167+
def main() -> None:
168+
"""Main entry point."""
169+
import argparse
170+
171+
parser = argparse.ArgumentParser(
172+
description='Analyze AI-assisted commits in the repository'
173+
)
174+
parser.add_argument(
175+
'--since',
176+
help='Analyze commits since this date (e.g., "2 weeks ago", "2025-01-01")'
177+
)
178+
parser.add_argument(
179+
'-n', '--max-count',
180+
type=int,
181+
help='Analyze last N commits'
182+
)
183+
184+
args = parser.parse_args()
185+
186+
# Validate arguments
187+
if args.since and args.max_count:
188+
parser.error("Cannot specify both --since and --max-count")
189+
190+
if not args.since and not args.max_count:
191+
args.since = '2 weeks ago'
192+
193+
git_runner = RealGitCommandRunner()
194+
stats = analyze_commits(git_runner, since=args.since or '', max_count=args.max_count or 0)
195+
196+
if args.max_count:
197+
period = f"last {args.max_count} commits"
198+
else:
199+
period = args.since
200+
print_report(stats, period=period)
201+
202+
203+
if __name__ == '__main__':
204+
main()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[project]
2+
name = "repo-metrics"
3+
version = "0.1.0"
4+
description = "Repository metrics and analysis tools"
5+
requires-python = ">=3.9"
6+
dependencies = []
7+
8+
[project.optional-dependencies]
9+
dev = [
10+
"pytest>=7.0.0",
11+
]
12+
13+
[build-system]
14+
requires = ["setuptools>=61.0"]
15+
build-backend = "setuptools.build_meta"
16+
17+
[tool.setuptools]
18+
py-modules = ["ai_assisted_commits"]
19+
20+
[tool.pytest.ini_options]
21+
testpaths = ["."]
22+
python_files = ["test_*.py"]

0 commit comments

Comments
 (0)