-
Notifications
You must be signed in to change notification settings - Fork 408
CNTRLPLANE-1615: Add repo metrics tool for tracking AI-assisted commits #6983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# Repository Metrics | ||
|
||
Tools for analyzing repository metrics and statistics. | ||
|
||
## Setup | ||
|
||
This project uses [uv](https://github.com/astral-sh/uv) for dependency management. | ||
|
||
```bash | ||
# Install uv if you don't have it | ||
curl -LsSf https://astral.sh/uv/install.sh | sh | ||
|
||
# Install dependencies (to be run inside the directory where pyproject.toml is) | ||
uv sync --dev | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should note, this should be ran in the same directory as the toml file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point |
||
``` | ||
|
||
## Tools | ||
|
||
### AI-Assisted Commits Analyzer | ||
|
||
Analyzes git commits to identify those assisted by AI tools (Claude, GPT, etc.). | ||
|
||
**Usage:** | ||
|
||
```bash | ||
# Run with default (last 2 weeks) | ||
uv run python ai_assisted_commits.py | ||
|
||
# Analyze last N commits | ||
uv run python ai_assisted_commits.py -n 100 | ||
|
||
# Analyze since relative date | ||
uv run python ai_assisted_commits.py --since "1 month ago" | ||
|
||
# Analyze since specific date | ||
uv run python ai_assisted_commits.py --since "2025-09-01" | ||
``` | ||
|
||
**Note:** You cannot specify both `--since` and `-n/--max-count` at the same time. | ||
|
||
**Output:** | ||
|
||
```text | ||
=== AI-Assisted Commits Report (2 weeks ago) === | ||
|
||
Absolute Numbers: | ||
Total commits: 48 | ||
- Merge commits: 25 | ||
- Non-merge commits: 23 | ||
AI-assisted commits: 13 | ||
- AI-assisted non-merge: 11 | ||
- AI-assisted merge: 2 | ||
|
||
Percentages: | ||
Overall AI-assisted: 27.1% (13/48) | ||
Non-merge AI-assisted: 47.8% (11/23) | ||
``` | ||
|
||
## Testing | ||
|
||
```bash | ||
# Run all tests | ||
uv run pytest | ||
|
||
# Run with verbose output | ||
uv run pytest -v | ||
|
||
# Run specific test file | ||
uv run pytest test_ai_assisted_commits.py -v | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
#!/usr/bin/env python3 | ||
""" | ||
Generate a report of AI-assisted commits in the repository. | ||
|
||
This script analyzes git commit messages to identify commits that were | ||
assisted by AI tools (Claude, GPT, etc.) and generates statistics. | ||
""" | ||
|
||
import re | ||
import subprocess | ||
import sys | ||
from dataclasses import dataclass | ||
from datetime import datetime, timedelta | ||
from typing import List, Set, Tuple | ||
|
||
|
||
@dataclass | ||
class CommitStats: | ||
"""Statistics about commits in the repository.""" | ||
|
||
total_commits: int | ||
merge_commits: int | ||
non_merge_commits: int | ||
ai_assisted_commits: int | ||
ai_assisted_non_merge: int | ||
ai_assisted_merge: int | ||
ai_commits: List[str] | ||
|
||
|
||
AI_PATTERNS = [ | ||
re.compile(r'^\s*Assisted-by:', re.MULTILINE | re.IGNORECASE), | ||
re.compile(r'^\s*Co-authored-by:\s+Claude', re.MULTILINE | re.IGNORECASE), | ||
re.compile(r'🤖\s*Generated', re.IGNORECASE), | ||
re.compile(r'^\s*Commit-Message-Assisted-by:', re.MULTILINE | re.IGNORECASE), | ||
] | ||
|
||
|
||
class GitCommandRunner: | ||
"""Interface for running git commands.""" | ||
|
||
def run(self, args: List[str]) -> str: | ||
"""Run a git command and return its output.""" | ||
raise NotImplementedError | ||
|
||
|
||
class RealGitCommandRunner(GitCommandRunner): | ||
"""Real implementation that executes git commands.""" | ||
|
||
def run(self, args: List[str]) -> str: | ||
"""Run a git command and return its output.""" | ||
try: | ||
result = subprocess.run( | ||
['git'] + args, | ||
capture_output=True, | ||
text=True, | ||
check=True | ||
) | ||
return result.stdout.strip() | ||
except subprocess.CalledProcessError as e: | ||
print(f"Error running git command: {e}", file=sys.stderr) | ||
sys.exit(1) | ||
|
||
|
||
def get_commit_hashes(git_runner: GitCommandRunner, since: str = '', max_count: int = 0) -> List[str]: | ||
"""Get all commit hashes since a given date or up to max_count commits.""" | ||
args = ['log'] | ||
if max_count > 0: | ||
args.extend([f'-{max_count}']) | ||
elif since: | ||
args.extend([f'--since={since}']) | ||
args.append('--pretty=format:%H') | ||
output = git_runner.run(args) | ||
return output.split('\n') if output else [] | ||
|
||
|
||
def get_commit_body(git_runner: GitCommandRunner, commit_hash: str) -> str: | ||
"""Get the full commit message body for a given commit hash.""" | ||
return git_runner.run(['show', commit_hash, '--quiet', '--format=%B']) | ||
|
||
|
||
def is_merge_commit(git_runner: GitCommandRunner, commit_hash: str) -> bool: | ||
"""Check if a commit is a merge commit.""" | ||
output = git_runner.run(['rev-list', '--parents', '-n', '1', commit_hash]) | ||
parents = output.split() | ||
return len(parents) > 2 | ||
|
||
|
||
def is_ai_assisted(commit_body: str) -> bool: | ||
"""Check if a commit body contains AI assistance markers.""" | ||
return any(pattern.search(commit_body) for pattern in AI_PATTERNS) | ||
|
||
|
||
def get_commit_oneline(git_runner: GitCommandRunner, commit_hash: str) -> str: | ||
"""Get the one-line summary of a commit.""" | ||
return git_runner.run(['log', '--oneline', '-1', commit_hash]) | ||
|
||
|
||
def analyze_commits(git_runner: GitCommandRunner, since: str = '2 weeks ago', max_count: int = 0) -> CommitStats: | ||
"""Analyze commits and return statistics.""" | ||
commit_hashes = get_commit_hashes(git_runner, since=since, max_count=max_count) | ||
total_commits = len(commit_hashes) | ||
|
||
merge_commits = 0 | ||
non_merge_commits = 0 | ||
ai_assisted_hashes: Set[str] = set() | ||
ai_assisted_merge = 0 | ||
ai_assisted_non_merge = 0 | ||
ai_commit_details: List[Tuple[str, str]] = [] | ||
|
||
for commit_hash in commit_hashes: | ||
body = get_commit_body(git_runner, commit_hash) | ||
is_merge = is_merge_commit(git_runner, commit_hash) | ||
is_ai = is_ai_assisted(body) | ||
|
||
if is_merge: | ||
merge_commits += 1 | ||
if is_ai: | ||
ai_assisted_merge += 1 | ||
else: | ||
non_merge_commits += 1 | ||
if is_ai: | ||
ai_assisted_non_merge += 1 | ||
|
||
if is_ai: | ||
ai_assisted_hashes.add(commit_hash) | ||
oneline = get_commit_oneline(git_runner, commit_hash) | ||
ai_commit_details.append((commit_hash, oneline)) | ||
|
||
return CommitStats( | ||
total_commits=total_commits, | ||
merge_commits=merge_commits, | ||
non_merge_commits=non_merge_commits, | ||
ai_assisted_commits=len(ai_assisted_hashes), | ||
ai_assisted_non_merge=ai_assisted_non_merge, | ||
ai_assisted_merge=ai_assisted_merge, | ||
ai_commits=[detail[1] for detail in ai_commit_details] | ||
) | ||
|
||
|
||
def print_report(stats: CommitStats, period: str = "last 2 weeks") -> None: | ||
"""Print the statistics report.""" | ||
print(f"=== AI-Assisted Commits Report ({period}) ===\n") | ||
|
||
print("Absolute Numbers:") | ||
print(f" Total commits: {stats.total_commits}") | ||
print(f" - Merge commits: {stats.merge_commits}") | ||
print(f" - Non-merge commits: {stats.non_merge_commits}") | ||
print(f" AI-assisted commits: {stats.ai_assisted_commits}") | ||
print(f" - AI-assisted non-merge: {stats.ai_assisted_non_merge}") | ||
print(f" - AI-assisted merge: {stats.ai_assisted_merge}") | ||
|
||
print("\nPercentages:") | ||
if stats.total_commits > 0: | ||
overall_pct = (stats.ai_assisted_commits / stats.total_commits) * 100 | ||
print(f" Overall AI-assisted: {overall_pct:.1f}% ({stats.ai_assisted_commits}/{stats.total_commits})") | ||
|
||
if stats.non_merge_commits > 0: | ||
non_merge_pct = (stats.ai_assisted_non_merge / stats.non_merge_commits) * 100 | ||
print(f" Non-merge AI-assisted: {non_merge_pct:.1f}% ({stats.ai_assisted_non_merge}/{stats.non_merge_commits})") | ||
|
||
if stats.ai_commits: | ||
print("\nAI-Assisted Commits:") | ||
for commit in stats.ai_commits: | ||
print(f" {commit}") | ||
|
||
|
||
def main() -> None: | ||
"""Main entry point.""" | ||
import argparse | ||
|
||
parser = argparse.ArgumentParser( | ||
description='Analyze AI-assisted commits in the repository' | ||
) | ||
parser.add_argument( | ||
'--since', | ||
help='Analyze commits since this date (e.g., "2 weeks ago", "2025-01-01")' | ||
) | ||
parser.add_argument( | ||
'-n', '--max-count', | ||
type=int, | ||
help='Analyze last N commits' | ||
) | ||
|
||
args = parser.parse_args() | ||
|
||
# Validate arguments | ||
if args.since and args.max_count: | ||
parser.error("Cannot specify both --since and --max-count") | ||
|
||
if not args.since and not args.max_count: | ||
args.since = '2 weeks ago' | ||
|
||
git_runner = RealGitCommandRunner() | ||
stats = analyze_commits(git_runner, since=args.since or '', max_count=args.max_count or 0) | ||
|
||
if args.max_count: | ||
period = f"last {args.max_count} commits" | ||
else: | ||
period = args.since | ||
print_report(stats, period=period) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[project] | ||
name = "repo-metrics" | ||
version = "0.1.0" | ||
description = "Repository metrics and analysis tools" | ||
requires-python = ">=3.9" | ||
dependencies = [] | ||
|
||
[project.optional-dependencies] | ||
dev = [ | ||
"pytest>=7.0.0", | ||
] | ||
|
||
[build-system] | ||
requires = ["setuptools>=61.0"] | ||
build-backend = "setuptools.build_meta" | ||
|
||
[tool.setuptools] | ||
py-modules = ["ai_assisted_commits"] | ||
|
||
[tool.pytest.ini_options] | ||
testpaths = ["."] | ||
python_files = ["test_*.py"] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.