diff --git a/product-team/roadmap-communicator/scripts/changelog_generator.py b/product-team/roadmap-communicator/scripts/changelog_generator.py index 6e50535..aa60675 100755 --- a/product-team/roadmap-communicator/scripts/changelog_generator.py +++ b/product-team/roadmap-communicator/scripts/changelog_generator.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 -"""Generate changelog sections from git log using conventional commit prefixes.""" +"""Generate changelog sections from git log or piped commit messages using conventional commit prefixes.""" import argparse +import shutil import subprocess +import sys from collections import defaultdict @@ -20,31 +22,71 @@ SECTIONS = { "revert": "Reverts", } +DEMO_COMMITS = [ + "feat: add user dashboard with analytics widgets", + "feat: implement dark mode toggle", + "fix: resolve crash on empty CSV import", + "fix: correct timezone offset in calendar view", + "docs: update API reference for v2 endpoints", + "refactor: extract shared validation into utils module", + "chore: bump dependencies to latest patch versions", + "perf: optimize database queries for user listing", +] + def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Generate changelog from git commits.") - parser.add_argument("--from", dest="from_ref", default="HEAD~50") - parser.add_argument("--to", dest="to_ref", default="HEAD") - parser.add_argument("--format", choices=["markdown", "text"], default="markdown") + parser = argparse.ArgumentParser( + description="Generate changelog from git commits or piped input.", + epilog="Examples:\n" + " %(prog)s --from v1.0.0 --to HEAD\n" + " git log --pretty=format:%%s v1.0..HEAD | %(prog)s --stdin\n" + " %(prog)s --demo\n", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("--from", dest="from_ref", default="HEAD~50", + help="Start ref for git log (default: HEAD~50)") + parser.add_argument("--to", dest="to_ref", default="HEAD", + help="End ref for git log (default: HEAD)") + parser.add_argument("--format", choices=["markdown", "text"], default="markdown", + help="Output format (default: markdown)") + parser.add_argument("--stdin", action="store_true", + help="Read commit subjects from stdin instead of git log") + parser.add_argument("--demo", action="store_true", + help="Run with sample data (no git required)") return parser.parse_args() def get_git_log(from_ref: str, to_ref: str) -> list[str]: + """Get commit subjects from git log. Requires git on PATH and a git repo.""" + if not shutil.which("git"): + print("Error: git not found on PATH. Use --stdin or --demo instead.", file=sys.stderr) + sys.exit(1) commit_range = f"{from_ref}..{to_ref}" cmd = ["git", "log", "--pretty=format:%s", commit_range] - result = subprocess.run(cmd, check=True, capture_output=True, text=True) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + except subprocess.TimeoutExpired: + print("Error: git log timed out.", file=sys.stderr) + sys.exit(1) + if result.returncode != 0: + print(f"Error: git log failed: {result.stderr.strip()}", file=sys.stderr) + sys.exit(1) lines = [line.strip() for line in result.stdout.splitlines() if line.strip()] return lines +def read_stdin() -> list[str]: + """Read commit subjects from stdin, one per line.""" + return [line.strip() for line in sys.stdin if line.strip()] + + def group_commits(subjects: list[str]) -> dict[str, list[str]]: - grouped = defaultdict(list) - grouped["other"] = [] + grouped: dict[str, list[str]] = defaultdict(list) for subject in subjects: commit_type = "other" for prefix in SECTIONS: - if subject.startswith(f"{prefix}:"): + if subject.startswith(f"{prefix}:") or subject.startswith(f"{prefix}("): commit_type = prefix break grouped[commit_type].append(subject) @@ -68,7 +110,7 @@ def render_markdown(grouped: dict[str, list[str]]) -> str: def render_text(grouped: dict[str, list[str]]) -> str: - out = [] + out: list[str] = [] ordered_types = list(SECTIONS.keys()) + ["other"] for commit_type in ordered_types: commits = grouped.get(commit_type, []) @@ -84,7 +126,18 @@ def render_text(grouped: dict[str, list[str]]) -> str: def main() -> int: args = parse_args() - subjects = get_git_log(args.from_ref, args.to_ref) + + if args.demo: + subjects = DEMO_COMMITS + elif args.stdin: + subjects = read_stdin() + else: + subjects = get_git_log(args.from_ref, args.to_ref) + + if not subjects: + print("No commits found.", file=sys.stderr) + return 0 + grouped = group_commits(subjects) if args.format == "markdown":