mirror of https://github.com/sysown/proxysql
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.
163 lines
5.7 KiB
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() |