409 lines
14 KiB
Python
Executable File
409 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""
|
|
============================================================================
|
|
SCRIPT: convention-recommender.py
|
|
PURPOSE: Generate project-specific commit convention recommendations
|
|
VERSION: 1.0.0
|
|
USAGE: ./convention-recommender.py --count N --branch BRANCH [--priority LEVEL]
|
|
RETURNS: JSON format with prioritized recommendations
|
|
EXIT CODES:
|
|
0 - Success
|
|
1 - Not a git repository
|
|
2 - No commit history
|
|
3 - Analysis failed
|
|
DEPENDENCIES: git, python3, style-analyzer.sh, pattern-detector.py, scope-extractor.sh
|
|
============================================================================
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
import json
|
|
import argparse
|
|
import os
|
|
from typing import Dict, List, Tuple
|
|
|
|
|
|
def run_script(script_path: str, args: List[str]) -> Tuple[int, str]:
|
|
"""Execute a script and return exit code and output."""
|
|
try:
|
|
cmd = [script_path] + args
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
cwd=os.path.dirname(script_path)
|
|
)
|
|
return result.returncode, result.stdout.strip()
|
|
except Exception as e:
|
|
return 1, json.dumps({'error': str(e)})
|
|
|
|
|
|
def gather_analysis_data(count: int, branch: str, scripts_dir: str) -> Dict:
|
|
"""Gather all analysis data from other scripts."""
|
|
data = {}
|
|
|
|
# Run style-analyzer.sh
|
|
style_script = os.path.join(scripts_dir, 'style-analyzer.sh')
|
|
code, output = run_script(style_script, [str(count), branch])
|
|
if code == 0:
|
|
try:
|
|
data['style'] = json.loads(output)
|
|
except json.JSONDecodeError:
|
|
data['style'] = {}
|
|
|
|
# Run pattern-detector.py
|
|
pattern_script = os.path.join(scripts_dir, 'pattern-detector.py')
|
|
code, output = run_script(pattern_script, [
|
|
'--count', str(count),
|
|
'--branch', branch
|
|
])
|
|
if code == 0:
|
|
try:
|
|
data['patterns'] = json.loads(output)
|
|
except json.JSONDecodeError:
|
|
data['patterns'] = {}
|
|
|
|
# Run scope-extractor.sh
|
|
scope_script = os.path.join(scripts_dir, 'scope-extractor.sh')
|
|
code, output = run_script(scope_script, [
|
|
'--count', str(count),
|
|
'--branch', branch,
|
|
'--min-frequency', '2'
|
|
])
|
|
if code == 0:
|
|
try:
|
|
data['scopes'] = json.loads(output)
|
|
except json.JSONDecodeError:
|
|
data['scopes'] = {}
|
|
|
|
return data
|
|
|
|
|
|
def generate_recommendations(data: Dict, priority_filter: str) -> Dict:
|
|
"""Generate prioritized recommendations based on analysis data."""
|
|
recommendations = {
|
|
'high_priority': [],
|
|
'medium_priority': [],
|
|
'low_priority': []
|
|
}
|
|
|
|
style = data.get('style', {}).get('project_style', {})
|
|
patterns = data.get('patterns', {})
|
|
scopes = data.get('scopes', {})
|
|
|
|
# HIGH PRIORITY RECOMMENDATIONS
|
|
|
|
# 1. Conventional commits adoption
|
|
conv_pct = style.get('conventional_commits_percentage', 0)
|
|
if conv_pct < 50:
|
|
recommendations['high_priority'].append({
|
|
'id': 1,
|
|
'title': 'Adopt Conventional Commits Format',
|
|
'status': 'needs_improvement',
|
|
'current_usage': conv_pct,
|
|
'target_usage': 80,
|
|
'action': 'Migrate to conventional commits format: <type>(<scope>): <subject>',
|
|
'benefit': 'Enables automated changelog, semantic versioning, and better git history',
|
|
'priority': 'high',
|
|
'examples': [
|
|
'feat(auth): implement OAuth2 authentication',
|
|
'fix(api): handle null pointer in user endpoint',
|
|
'docs: update API documentation'
|
|
]
|
|
})
|
|
elif conv_pct < 80:
|
|
recommendations['medium_priority'].append({
|
|
'id': 1,
|
|
'title': 'Increase Conventional Commits Usage',
|
|
'status': 'moderate',
|
|
'current_usage': conv_pct,
|
|
'target_usage': 90,
|
|
'action': 'Encourage team to use conventional commits consistently',
|
|
'benefit': 'Better consistency and tooling support',
|
|
'priority': 'medium'
|
|
})
|
|
else:
|
|
recommendations['high_priority'].append({
|
|
'id': 1,
|
|
'title': 'Continue Using Conventional Commits',
|
|
'status': 'good',
|
|
'current_usage': conv_pct,
|
|
'target_usage': 90,
|
|
'action': 'Maintain current practice',
|
|
'benefit': 'Already well-adopted, enables automation',
|
|
'priority': 'high'
|
|
})
|
|
|
|
# 2. Subject line length
|
|
avg_length = style.get('average_subject_length', 0)
|
|
if avg_length > 60:
|
|
recommendations['high_priority'].append({
|
|
'id': 2,
|
|
'title': 'Reduce Subject Line Length',
|
|
'status': 'needs_improvement',
|
|
'current_value': avg_length,
|
|
'target_value': 50,
|
|
'action': 'Keep subject lines under 50 characters',
|
|
'benefit': 'Better readability in git log, GitHub UI, and terminal',
|
|
'priority': 'high'
|
|
})
|
|
elif avg_length > 50:
|
|
recommendations['medium_priority'].append({
|
|
'id': 2,
|
|
'title': 'Optimize Subject Line Length',
|
|
'status': 'moderate',
|
|
'current_value': avg_length,
|
|
'target_value': 50,
|
|
'action': 'Aim for concise subject lines (under 50 chars)',
|
|
'priority': 'medium'
|
|
})
|
|
|
|
# 3. Imperative mood
|
|
imperative_pct = style.get('imperative_mood_percentage', 0)
|
|
if imperative_pct < 80:
|
|
recommendations['high_priority'].append({
|
|
'id': 3,
|
|
'title': 'Use Imperative Mood Consistently',
|
|
'status': 'needs_improvement',
|
|
'current_usage': imperative_pct,
|
|
'target_usage': 90,
|
|
'action': 'Use imperative mood: "add" not "added", "fix" not "fixed"',
|
|
'benefit': 'Clearer, more professional commit messages',
|
|
'priority': 'high',
|
|
'examples': [
|
|
'✓ add user authentication',
|
|
'✗ added user authentication',
|
|
'✓ fix null pointer exception',
|
|
'✗ fixed null pointer exception'
|
|
]
|
|
})
|
|
|
|
# MEDIUM PRIORITY RECOMMENDATIONS
|
|
|
|
# 4. Body usage
|
|
body_pct = style.get('has_body_percentage', 0)
|
|
if body_pct < 50:
|
|
recommendations['medium_priority'].append({
|
|
'id': 4,
|
|
'title': 'Increase Body Usage for Complex Changes',
|
|
'status': 'low',
|
|
'current_usage': body_pct,
|
|
'target_usage': 50,
|
|
'action': 'Add commit body for non-trivial changes (>3 files, complex logic)',
|
|
'benefit': 'Better context for code review and future reference',
|
|
'priority': 'medium',
|
|
'when_to_use': [
|
|
'Multiple files changed (>3)',
|
|
'Complex logic modifications',
|
|
'Breaking changes',
|
|
'Security-related changes'
|
|
]
|
|
})
|
|
|
|
# 5. Issue references
|
|
issue_pct = style.get('references_issues_percentage', 0)
|
|
if issue_pct > 50:
|
|
recommendations['medium_priority'].append({
|
|
'id': 5,
|
|
'title': 'Continue Issue Referencing Practice',
|
|
'status': 'good',
|
|
'current_usage': issue_pct,
|
|
'action': 'Maintain consistent issue references',
|
|
'benefit': 'Excellent traceability between commits and issues',
|
|
'priority': 'medium'
|
|
})
|
|
elif issue_pct > 25:
|
|
recommendations['medium_priority'].append({
|
|
'id': 5,
|
|
'title': 'Increase Issue References',
|
|
'status': 'moderate',
|
|
'current_usage': issue_pct,
|
|
'target_usage': 60,
|
|
'action': 'Reference related issues: "Closes #123", "Fixes #456", "Refs #789"',
|
|
'benefit': 'Better traceability',
|
|
'priority': 'medium'
|
|
})
|
|
|
|
# LOW PRIORITY RECOMMENDATIONS
|
|
|
|
# 6. Scope standardization
|
|
scope_count = scopes.get('total_scopes', 0)
|
|
if scope_count > 0:
|
|
top_scopes = scopes.get('scopes', [])[:5]
|
|
scope_names = [s['name'] for s in top_scopes]
|
|
recommendations['medium_priority'].append({
|
|
'id': 6,
|
|
'title': 'Use Standard Project Scopes',
|
|
'status': 'good',
|
|
'action': f'Use these common scopes: {", ".join(scope_names)}',
|
|
'benefit': 'Consistent scope usage across team',
|
|
'priority': 'medium',
|
|
'scopes': scope_names
|
|
})
|
|
|
|
# 7. Co-author attribution
|
|
recommendations['low_priority'].append({
|
|
'id': 7,
|
|
'title': 'Consider Co-Author Attribution',
|
|
'status': 'optional',
|
|
'action': 'Add co-authors for pair programming: Co-authored-by: Name <email>',
|
|
'benefit': 'Team recognition and contribution tracking',
|
|
'priority': 'low',
|
|
'example': 'Co-authored-by: Jane Doe <jane@example.com>'
|
|
})
|
|
|
|
# 8. Breaking change documentation
|
|
recommendations['low_priority'].append({
|
|
'id': 8,
|
|
'title': 'Document Breaking Changes',
|
|
'status': 'important',
|
|
'action': 'Use BREAKING CHANGE: footer when applicable',
|
|
'benefit': 'Clear communication of breaking changes for semantic versioning',
|
|
'priority': 'low',
|
|
'example': 'BREAKING CHANGE: API now requires OAuth tokens instead of API keys'
|
|
})
|
|
|
|
# Filter by priority if specified
|
|
if priority_filter and priority_filter != 'all':
|
|
priority_key = f'{priority_filter}_priority'
|
|
filtered = {priority_key: recommendations.get(priority_key, [])}
|
|
return filtered
|
|
|
|
return recommendations
|
|
|
|
|
|
def generate_style_guide(data: Dict) -> Dict:
|
|
"""Generate project-specific style guide."""
|
|
style = data.get('style', {}).get('project_style', {})
|
|
scopes_data = data.get('scopes', {})
|
|
|
|
# Extract common types
|
|
common_types = style.get('common_types', [])
|
|
types_sorted = sorted(common_types, key=lambda x: x.get('count', 0), reverse=True)
|
|
|
|
# Extract common scopes
|
|
scopes = scopes_data.get('scopes', [])[:10]
|
|
|
|
# Build style guide
|
|
return {
|
|
'format': '<type>(<scope>): <subject>',
|
|
'max_subject_length': 50,
|
|
'body_wrap': 72,
|
|
'types': [
|
|
{
|
|
'name': t.get('type', 'unknown'),
|
|
'percentage': t.get('percentage', 0),
|
|
'description': get_type_description(t.get('type', 'unknown'))
|
|
}
|
|
for t in types_sorted
|
|
],
|
|
'scopes': [
|
|
{
|
|
'name': s.get('name', 'unknown'),
|
|
'percentage': s.get('percentage', 0),
|
|
'description': s.get('description', 'Unknown'),
|
|
'category': s.get('category', 'other')
|
|
}
|
|
for s in scopes
|
|
],
|
|
'rules': [
|
|
'Use imperative mood ("add" not "added")',
|
|
'Capitalize first letter of subject',
|
|
'No period at end of subject line',
|
|
'Use lowercase for scopes',
|
|
'Wrap body at 72 characters',
|
|
'Separate body and footer with blank line',
|
|
'Use bullet points in body',
|
|
'Reference issues when applicable'
|
|
]
|
|
}
|
|
|
|
|
|
def get_type_description(type_name: str) -> str:
|
|
"""Get description for commit type."""
|
|
descriptions = {
|
|
'feat': 'New features',
|
|
'fix': 'Bug fixes',
|
|
'docs': 'Documentation changes',
|
|
'style': 'Formatting changes (no code change)',
|
|
'refactor': 'Code restructuring',
|
|
'perf': 'Performance improvements',
|
|
'test': 'Test additions/updates',
|
|
'build': 'Build system changes',
|
|
'ci': 'CI/CD changes',
|
|
'chore': 'Maintenance tasks',
|
|
'revert': 'Revert previous commit'
|
|
}
|
|
return descriptions.get(type_name, 'Other changes')
|
|
|
|
|
|
def calculate_confidence(data: Dict) -> str:
|
|
"""Calculate confidence level of recommendations."""
|
|
commits_analyzed = data.get('style', {}).get('project_style', {}).get('commits_analyzed', 0)
|
|
|
|
if commits_analyzed >= 100:
|
|
return 'high'
|
|
elif commits_analyzed >= 50:
|
|
return 'medium'
|
|
elif commits_analyzed >= 20:
|
|
return 'low'
|
|
else:
|
|
return 'very_low'
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Generate convention recommendations')
|
|
parser.add_argument('--count', type=int, default=50, help='Number of commits to analyze')
|
|
parser.add_argument('--branch', default='HEAD', help='Branch to analyze')
|
|
parser.add_argument('--priority', choices=['high', 'medium', 'low', 'all'], default='all',
|
|
help='Filter by priority level')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Get scripts directory
|
|
scripts_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Gather analysis data
|
|
data = gather_analysis_data(args.count, args.branch, scripts_dir)
|
|
|
|
if not data:
|
|
print(json.dumps({'error': 'Failed to gather analysis data'}), file=sys.stderr)
|
|
sys.exit(3)
|
|
|
|
# Generate recommendations
|
|
recommendations = generate_recommendations(data, args.priority)
|
|
|
|
# Generate style guide
|
|
style_guide = generate_style_guide(data)
|
|
|
|
# Calculate confidence
|
|
confidence = calculate_confidence(data)
|
|
|
|
# Calculate consistency score
|
|
consistency_score = data.get('patterns', {}).get('consistency_score', 0)
|
|
|
|
# Build output
|
|
output = {
|
|
'commits_analyzed': data.get('style', {}).get('project_style', {}).get('commits_analyzed', 0),
|
|
'branch': args.branch,
|
|
'consistency_score': consistency_score,
|
|
'confidence': confidence,
|
|
'recommendations': recommendations,
|
|
'style_guide': style_guide,
|
|
'automation': {
|
|
'commitlint': True,
|
|
'changelog_generator': 'standard-version',
|
|
'semantic_release': True
|
|
}
|
|
}
|
|
|
|
# Output JSON
|
|
print(json.dumps(output, indent=2))
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|