fix(product): resolve merge conflicts and improve changelog generator

- Resolve plugin.json conflict: keep version 2.1.2, update to 12 skills
- Resolve CLAUDE.md conflict: merge detailed tool docs with new skills
- Improve changelog_generator.py: add --stdin, --demo modes, graceful
  error when git unavailable, support scoped prefixes (feat(scope):)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Reza Rezvani
2026-03-11 15:00:48 +01:00
parent be61f84e36
commit d18c63d2aa

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env python3 #!/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 argparse
import shutil
import subprocess import subprocess
import sys
from collections import defaultdict from collections import defaultdict
@@ -20,31 +22,71 @@ SECTIONS = {
"revert": "Reverts", "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: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate changelog from git commits.") parser = argparse.ArgumentParser(
parser.add_argument("--from", dest="from_ref", default="HEAD~50") description="Generate changelog from git commits or piped input.",
parser.add_argument("--to", dest="to_ref", default="HEAD") epilog="Examples:\n"
parser.add_argument("--format", choices=["markdown", "text"], default="markdown") " %(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() return parser.parse_args()
def get_git_log(from_ref: str, to_ref: str) -> list[str]: 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}" commit_range = f"{from_ref}..{to_ref}"
cmd = ["git", "log", "--pretty=format:%s", commit_range] 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()] lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
return lines 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]]: def group_commits(subjects: list[str]) -> dict[str, list[str]]:
grouped = defaultdict(list) grouped: dict[str, list[str]] = defaultdict(list)
grouped["other"] = []
for subject in subjects: for subject in subjects:
commit_type = "other" commit_type = "other"
for prefix in SECTIONS: for prefix in SECTIONS:
if subject.startswith(f"{prefix}:"): if subject.startswith(f"{prefix}:") or subject.startswith(f"{prefix}("):
commit_type = prefix commit_type = prefix
break break
grouped[commit_type].append(subject) 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: def render_text(grouped: dict[str, list[str]]) -> str:
out = [] out: list[str] = []
ordered_types = list(SECTIONS.keys()) + ["other"] ordered_types = list(SECTIONS.keys()) + ["other"]
for commit_type in ordered_types: for commit_type in ordered_types:
commits = grouped.get(commit_type, []) commits = grouped.get(commit_type, [])
@@ -84,7 +126,18 @@ def render_text(grouped: dict[str, list[str]]) -> str:
def main() -> int: def main() -> int:
args = parse_args() 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) grouped = group_commits(subjects)
if args.format == "markdown": if args.format == "markdown":