Files
gh-dev-gom-claude-code-mark…/skills/scripts/changelog_generator.py
2025-11-29 18:18:47 +08:00

206 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""
CHANGELOG generator from git commit history.
Supports Conventional Commits format.
"""
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Optional
class ChangelogGenerator:
def __init__(self, repo_path: str = "."):
self.repo_path = Path(repo_path).resolve()
# Conventional Commits types
self.commit_types = {
'feat': 'Added',
'fix': 'Fixed',
'docs': 'Documentation',
'style': 'Style',
'refactor': 'Changed',
'perf': 'Performance',
'test': 'Tests',
'chore': 'Chore',
'build': 'Build',
'ci': 'CI/CD',
}
def get_commits_since_tag(
self,
since_tag: Optional[str] = None) -> List[str]:
"""Get commit messages since last tag."""
try:
if since_tag:
cmd = [
'git',
'log',
f'{since_tag}..HEAD',
'--pretty=format:%s']
else:
# Get all commits if no tag specified
cmd = ['git', 'log', '--pretty=format:%s']
result = subprocess.run(
cmd,
cwd=self.repo_path,
capture_output=True,
text=True,
check=True,
encoding='utf-8'
)
return (
result.stdout.strip().split('\n')
if result.stdout.strip() else []
)
except subprocess.CalledProcessError as e:
print(f"Error getting git commits: {e.stderr}", file=sys.stderr)
return []
def parse_commit(self, commit_message: str) -> Dict[str, str]:
"""Parse conventional commit message."""
# Pattern: type(scope): description
pattern = r'^(\w+)(?:\(([^)]+)\))?: (.+)$'
match = re.match(pattern, commit_message)
if match:
commit_type, scope, description = match.groups()
return {
'type': commit_type,
'scope': scope or '',
'description': description,
'raw': commit_message
}
else:
return {
'type': 'other',
'scope': '',
'description': commit_message,
'raw': commit_message
}
def group_commits(self, commits: List[str]) -> Dict[str, List[str]]:
"""Group commits by type."""
grouped: Dict[str, List[str]] = {}
for commit_msg in commits:
parsed = self.parse_commit(commit_msg)
commit_type = parsed['type']
description = parsed['description']
category = self.commit_types.get(commit_type, 'Other')
if category not in grouped:
grouped[category] = []
grouped[category].append(description)
return grouped
def generate_changelog_entry(
self,
version: str,
since_tag: Optional[str] = None) -> str:
"""Generate changelog entry for new version."""
commits = self.get_commits_since_tag(since_tag)
if not commits:
print("No commits found", file=sys.stderr)
return ""
grouped = self.group_commits(commits)
# Build changelog entry
date = datetime.now().strftime('%Y-%m-%d')
lines = [
f"## [{version}] - {date}",
""
]
# Add sections in order
section_order = [
'Added', 'Fixed', 'Changed', 'Deprecated', 'Removed',
'Security', 'Performance', 'Documentation', 'Other'
]
for section in section_order:
if section in grouped:
lines.append(f"### {section}")
for item in grouped[section]:
lines.append(f"- {item}")
lines.append("")
return '\n'.join(lines)
def update_changelog_file(
self,
version: str,
since_tag: Optional[str] = None,
changelog_path: str = "CHANGELOG.md"
) -> bool:
"""Update CHANGELOG.md file with new entry."""
entry = self.generate_changelog_entry(version, since_tag)
if not entry:
return False
changelog_file = self.repo_path / changelog_path
if changelog_file.exists():
# Insert at top after header
content = changelog_file.read_text(encoding='utf-8')
# Find position to insert (after # Changelog header)
lines = content.split('\n')
insert_pos = 0
for i, line in enumerate(lines):
if line.startswith('# '):
insert_pos = i + 1
# Skip empty lines after header
while (insert_pos < len(lines) and
not lines[insert_pos].strip()):
insert_pos += 1
break
lines.insert(insert_pos, entry)
new_content = '\n'.join(lines)
else:
# Create new CHANGELOG
new_content = f"# Changelog\n\n{entry}"
changelog_file.write_text(new_content, encoding='utf-8')
print(f"✓ Updated {changelog_path}")
return True
def main():
if len(sys.argv) < 2:
print(
"Usage: python changelog_generator.py "
"<version> [since_tag] [changelog_path]",
file=sys.stderr
)
sys.exit(1)
version = sys.argv[1]
since_tag = sys.argv[2] if len(sys.argv) > 2 else None
changelog_path = sys.argv[3] if len(sys.argv) > 3 else "CHANGELOG.md"
generator = ChangelogGenerator()
if not generator.update_changelog_file(version, since_tag, changelog_path):
sys.exit(1)
if __name__ == "__main__":
main()