deployment
This commit is contained in:
296
build/generate_changelog.py
Executable file
296
build/generate_changelog.py
Executable file
@@ -0,0 +1,296 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user