You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/scripts/release-tools/generate_changelog.py

163 lines
5.7 KiB

#!/usr/bin/env python3
"""
Generate a changelog from git merge commits between two tags/branches.
This script analyzes merge commits (pull requests) and generates a categorized
changelog. It attempts to get commits within each PR by looking at the second
parent of merge commits.
Usage:
python generate_changelog.py --from-tag v3.0.3 --to-tag v3.0 --output changelog.md
"""
import subprocess
import re
import argparse
import sys
def run(cmd_list):
"""Run command and return output."""
return subprocess.check_output(cmd_list, text=True).strip()
def get_merge_commits(from_tag, to_tag):
"""Get merge commits between two tags."""
merge_log = run(["git", "log", f"{from_tag}..{to_tag}", "--merges", "--pretty=format:%H %s"])
lines = merge_log.split('\n')
prs = []
for line in lines:
if 'Merge pull request' in line:
hash_, subject = line.split(' ', 1)
# extract PR number
match = re.search(r'#(\d+)', subject)
if match:
pr_num = match.group(1)
prs.append((hash_, pr_num, subject))
# ignore branch merges
return prs
def get_pr_commits(prs):
"""For each PR merge commit, get commits in the PR branch (second parent)."""
pr_commits = {}
for hash_, pr_num, subject in prs:
try:
output = run(["git", "log", "--oneline", "--no-merges", f"{hash_}^2"])
except subprocess.CalledProcessError:
# merge commit may have only one parent, skip
continue
commits = output.split('\n') if output else []
pr_commits[pr_num] = (subject, commits)
return pr_commits
def categorize_pr(subject, commits):
"""Categorize a PR based on its subject and commit messages."""
subj_lower = subject.lower()
# keyword matching
if any(word in subj_lower for word in ['fix', 'bug', 'issue', 'crash', 'vulnerability']):
return 'Bug Fixes'
if any(word in subj_lower for word in ['add', 'new', 'support', 'implement', 'feature', 'introduce', 'enable']):
return 'New Features'
if any(word in subj_lower for word in ['improve', 'optimize', 'enhance', 'performance', 'better']):
return 'Improvements'
if any(word in subj_lower for word in ['doc', 'documentation', 'doxygen']):
return 'Documentation'
if any(word in subj_lower for word in ['test', 'tap', 'regression']):
return 'Testing'
if any(word in subj_lower for word in ['build', 'package', 'opensuse', 'docker']):
return 'Build/Packaging'
if any(word in subj_lower for word in ['refactor', 'cleanup', 'restructure']):
return 'Refactoring'
if any(word in subj_lower for word in ['security', 'injection', 'vulnerability']):
return 'Security'
if any(word in subj_lower for word in ['monitor', 'metric', 'log']):
return 'Monitoring'
if any(word in subj_lower for word in ['postgresql', 'pgsql', 'pg']):
return 'PostgreSQL'
if any(word in subj_lower for word in ['mysql']):
return 'MySQL'
return 'Other'
def generate_changelog(from_tag, to_tag, output_file, verbose=False):
"""Main function to generate changelog."""
if verbose:
print(f"Generating changelog from {from_tag} to {to_tag}...")
prs = get_merge_commits(from_tag, to_tag)
if verbose:
print(f"Found {len(prs)} PR merges")
pr_commits = get_pr_commits(prs)
categories = {
'Bug Fixes': [],
'New Features': [],
'Improvements': [],
'Documentation': [],
'Testing': [],
'Build/Packaging': [],
'Refactoring': [],
'Security': [],
'Monitoring': [],
'PostgreSQL': [],
'MySQL': [],
'Other': [],
}
for pr_num, (subject, commits) in pr_commits.items():
cat = categorize_pr(subject, commits)
categories[cat].append((pr_num, subject, commits))
# Build output
output_lines = []
output_lines.append(f'# Changelog from {from_tag} to {to_tag}\n')
output_lines.append('\n')
output_lines.append(f'This changelog summarizes all changes between {from_tag} and {to_tag}.\n')
output_lines.append('\n')
for cat in sorted(categories.keys()):
entries = categories[cat]
if not entries:
continue
output_lines.append(f'## {cat}\n')
for pr_num, subject, commits in entries:
output_lines.append(f'- **PR #{pr_num}**: {subject}\n')
if commits:
for commit in commits:
# commit format: hash message
msg = commit.split(' ', 1)[1] if ' ' in commit else commit
output_lines.append(f' - {msg}\n')
output_lines.append('\n')
with open(output_file, 'w') as f:
f.writelines(output_lines)
if verbose:
print(f"Changelog written to {output_file}")
def main():
parser = argparse.ArgumentParser(
description='Generate a changelog from git merge commits.',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --from-tag v3.0.3 --to-tag v3.0 --output changelog.md
%(prog)s --from-tag v3.0.3 --to-tag HEAD --output changes.md --verbose
"""
)
parser.add_argument('--from-tag', required=True, help='Starting tag/branch (e.g., v3.0.3)')
parser.add_argument('--to-tag', required=True, help='Ending tag/branch (e.g., v3.0 or HEAD)')
parser.add_argument('--output', '-o', default='changelog.md', help='Output file (default: changelog.md)')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
generate_changelog(args.from_tag, args.to_tag, args.output, args.verbose)
if __name__ == '__main__':
main()