Files
Text-Texture-Generator-for-…/build/generate_changelog.py
2025-09-11 14:07:55 +03:00

296 lines
9.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Changelog generation script for Text Texture Generator.
Generates changelog from git commits between tags.
"""
import os
import sys
import json
import subprocess
import argparse
from pathlib import Path
from typing import List, Dict, Optional, Tuple
from datetime import datetime
import re
def get_project_root() -> Path:
"""Get the project root directory."""
return Path(__file__).parent.parent
def load_config() -> Dict:
"""Load build configuration."""
config_path = get_project_root() / "build" / "config.json"
with open(config_path, 'r') as f:
return json.load(f)
def run_git_command(command: List[str]) -> Optional[str]:
"""Run a git command and return output."""
try:
result = subprocess.run(
['git'] + command,
capture_output=True,
text=True,
check=True,
cwd=get_project_root()
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Git command failed: {' '.join(['git'] + command)}")
print(f"Error: {e.stderr}")
return None
def get_git_tags() -> List[str]:
"""Get all git tags sorted by version."""
output = run_git_command(['tag', '-l', '--sort=-version:refname'])
if output:
return [tag for tag in output.split('\n') if tag.strip()]
return []
def get_commits_between(from_ref: Optional[str], to_ref: str) -> List[Dict]:
"""Get commits between two references."""
if from_ref:
commit_range = f"{from_ref}..{to_ref}"
else:
commit_range = to_ref
# Get commit info in a structured format
format_str = "%H|%an|%ae|%ad|%s"
output = run_git_command([
'log',
'--pretty=format:' + format_str,
'--date=iso',
commit_range
])
if not output:
return []
commits = []
for line in output.split('\n'):
if '|' in line:
parts = line.split('|', 4)
if len(parts) == 5:
commits.append({
'hash': parts[0],
'author': parts[1],
'email': parts[2],
'date': parts[3],
'message': parts[4]
})
return commits
def categorize_commit(commit_message: str) -> Tuple[str, str]:
"""Categorize a commit based on its message."""
message_lower = commit_message.lower()
# Define categories and their patterns
categories = {
'Features': [
r'^feat(\(.+\))?:', r'^add:', r'^implement:',
r'\bfeature\b', r'\badd\b.*\bfeature\b', r'\bnew\b.*\bfeature\b'
],
'Bug Fixes': [
r'^fix(\(.+\))?:', r'^bug:', r'^hotfix:',
r'\bfix\b', r'\bbug\b.*\bfix\b', r'\bresolve\b.*\bissue\b'
],
'Improvements': [
r'^improve(\(.+\))?:', r'^enhance(\(.+\))?:', r'^refactor(\(.+\))?:',
r'\bimprove\b', r'\benhance\b', r'\boptimize\b', r'\brefactor\b'
],
'Documentation': [
r'^docs?(\(.+\))?:', r'^documentation:',
r'\bdocs?\b', r'\bdocumentation\b', r'\breadme\b'
],
'Build System': [
r'^build(\(.+\))?:', r'^ci(\(.+\))?:', r'^chore(\(.+\))?:',
r'\bbuild\b', r'\bci\b', r'\bchore\b', r'\bscript\b'
],
'Tests': [
r'^test(\(.+\))?:', r'\btest\b', r'\btesting\b'
]
}
# Check each category
for category, patterns in categories.items():
for pattern in patterns:
if re.search(pattern, message_lower):
# Clean up the message
cleaned_message = commit_message
# Remove conventional commit prefixes
cleaned_message = re.sub(r'^(feat|fix|docs?|style|refactor|test|chore|build|ci|perf|improve|enhance|add|implement|bug|hotfix)(\(.+\))?:\s*', '', cleaned_message, flags=re.IGNORECASE)
return category, cleaned_message
# Default category
return 'Other Changes', commit_message
def generate_changelog_content(version: str, commits: List[Dict], previous_version: Optional[str] = None) -> str:
"""Generate changelog content for a version."""
lines = []
# Header
lines.append(f"## [{version}] - {datetime.now().strftime('%Y-%m-%d')}")
lines.append("")
if not commits:
lines.append("No changes recorded.")
lines.append("")
return "\n".join(lines)
# Categorize commits
categorized = {}
for commit in commits:
category, message = categorize_commit(commit['message'])
if category not in categorized:
categorized[category] = []
categorized[category].append({
'message': message,
'hash': commit['hash'][:8],
'author': commit['author']
})
# Order categories by importance
category_order = [
'Features',
'Bug Fixes',
'Improvements',
'Documentation',
'Build System',
'Tests',
'Other Changes'
]
# Generate sections
for category in category_order:
if category in categorized:
lines.append(f"### {category}")
lines.append("")
for item in categorized[category]:
# Format: - Message (hash)
lines.append(f"- {item['message']} ({item['hash']})")
lines.append("")
# Add comparison link if we have a previous version
if previous_version:
repo_url = "https://github.com/marcmintel/text-texture-generator" # Adjust as needed
lines.append(f"**Full Changelog**: {repo_url}/compare/{previous_version}...{version}")
lines.append("")
return "\n".join(lines)
def generate_full_changelog() -> str:
"""Generate complete changelog from all tags."""
lines = []
# Header
lines.append("# Changelog")
lines.append("")
lines.append("All notable changes to this project will be documented in this file.")
lines.append("")
lines.append("The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),")
lines.append("and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).")
lines.append("")
# Get tags
tags = get_git_tags()
if not tags:
lines.append("## [Unreleased]")
lines.append("")
# Get all commits
commits = get_commits_between(None, "HEAD")
version_content = generate_changelog_content("Unreleased", commits)
lines.append(version_content)
else:
# Process each version
for i, tag in enumerate(tags):
# Clean version (remove 'v' prefix if present)
clean_version = tag.lstrip('v')
# Get previous tag
previous_tag = tags[i + 1] if i + 1 < len(tags) else None
# Get commits for this version
commits = get_commits_between(previous_tag, tag)
# Generate content
version_content = generate_changelog_content(clean_version, commits, previous_tag)
lines.append(version_content)
return "\n".join(lines)
def generate_changelog_for_version(version: str, output_file: Optional[Path] = None) -> str:
"""Generate changelog for a specific version."""
tags = get_git_tags()
# Find the version tag
version_tag = None
previous_tag = None
for i, tag in enumerate(tags):
if tag == version or tag == f"v{version}":
version_tag = tag
previous_tag = tags[i + 1] if i + 1 < len(tags) else None
break
if not version_tag:
print(f"Warning: Version tag '{version}' not found. Using current HEAD.")
version_tag = "HEAD"
previous_tag = tags[0] if tags else None
# Get commits
commits = get_commits_between(previous_tag, version_tag)
# Generate content
content = generate_changelog_content(version, commits, previous_tag)
# Write to file if specified
if output_file:
with open(output_file, 'w') as f:
f.write(content)
print(f"Changelog written to: {output_file}")
return content
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Generate changelog from git commits")
parser.add_argument("--version", help="Generate changelog for specific version")
parser.add_argument("--output", type=Path, help="Output file path")
parser.add_argument("--full", action="store_true", help="Generate full changelog")
args = parser.parse_args()
try:
if args.full:
content = generate_full_changelog()
output_file = args.output or get_project_root() / "CHANGELOG.md"
elif args.version:
content = generate_changelog_for_version(args.version, args.output)
output_file = args.output
else:
# Default: generate for latest version
tags = get_git_tags()
if tags:
latest_version = tags[0].lstrip('v')
content = generate_changelog_for_version(latest_version, args.output)
output_file = args.output
else:
print("No git tags found. Use --full to generate unreleased changelog.")
sys.exit(1)
# Write to file or print
if output_file and not args.version:
with open(output_file, 'w') as f:
f.write(content)
print(f"Changelog written to: {output_file}")
elif not args.version:
print(content)
except Exception as e:
print(f"❌ Changelog generation failed: {e}")
sys.exit(1)
if __name__ == "__main__":
main()