Files
gh-netresearch-claude-code-…/scripts/create-commit-message.py
2025-11-30 08:43:24 +08:00

187 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
TYPO3 Core Contribution Commit Message Generator
Creates properly formatted commit messages following TYPO3 standards
"""
import argparse
import sys
import re
from typing import Optional
COMMIT_TYPES = {
'BUGFIX': 'Bug fixes',
'FEATURE': 'New features (main branch only)',
'TASK': 'Refactoring, cleanup, miscellaneous',
'DOCS': 'Documentation changes',
'SECURITY': 'Security vulnerability fixes'
}
BREAKING_CHANGE_PREFIX = '[!!!]'
def validate_subject(subject: str, has_breaking: bool) -> tuple[bool, Optional[str]]:
"""Validate subject line against TYPO3 rules"""
max_length = 52 if not has_breaking else 47 # Account for [!!!] prefix
if len(subject) > 72:
return False, "Subject line exceeds 72 characters (absolute limit)"
if len(subject) > max_length:
return False, f"Subject line exceeds {max_length} characters (recommended limit)"
if not subject[0].isupper():
return False, "Subject must start with uppercase letter"
if subject.endswith('.'):
return False, "Subject should not end with a period"
# Check for imperative mood (simple heuristic)
past_tense_endings = ['ed', 'ing']
first_word = subject.split()[0].lower()
if any(first_word.endswith(end) for end in past_tense_endings):
return False, f"Use imperative mood ('{first_word}' appears to be past/present continuous tense)"
return True, None
def wrap_text(text: str, width: int = 72) -> str:
"""Wrap text at specified width"""
words = text.split()
lines = []
current_line = []
current_length = 0
for word in words:
word_length = len(word)
if current_length + word_length + len(current_line) > width:
if current_line:
lines.append(' '.join(current_line))
current_line = [word]
current_length = word_length
else:
current_line.append(word)
current_length += word_length
if current_line:
lines.append(' '.join(current_line))
return '\n'.join(lines)
def parse_releases(releases_str: str) -> list[str]:
"""Parse comma-separated release versions"""
releases = [r.strip() for r in releases_str.split(',')]
# Validate format
valid_releases = []
for release in releases:
if release == 'main' or re.match(r'^\d+\.\d+$', release):
valid_releases.append(release)
else:
print(f"Warning: Invalid release format '{release}', skipping")
return valid_releases
def main():
parser = argparse.ArgumentParser(
description='Generate TYPO3-compliant commit messages',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
%(prog)s --issue 105737 --type BUGFIX
%(prog)s --issue 105737 --type FEATURE --breaking
%(prog)s --type TASK --related 12345,12346
'''
)
parser.add_argument('--issue', type=int, help='Forge issue number')
parser.add_argument('--related', help='Related issue numbers (comma-separated)')
parser.add_argument('--type', choices=COMMIT_TYPES.keys(), required=True,
help='Commit type')
parser.add_argument('--breaking', action='store_true',
help='Mark as breaking change (adds [!!!] prefix)')
parser.add_argument('--releases', default='main',
help='Target releases (comma-separated, e.g., "main, 13.4, 12.4")')
parser.add_argument('--output', help='Output file (default: print to stdout)')
args = parser.parse_args()
# Interactive mode
print("=== TYPO3 Commit Message Generator ===\n")
# Get subject line
print(f"Commit Type: [{args.type}]")
if args.breaking:
print(f"Breaking Change: Yes (will add {BREAKING_CHANGE_PREFIX} prefix)")
print()
subject = input("Enter subject line (max 52 chars, imperative mood): ").strip()
# Validate subject
valid, error = validate_subject(subject, args.breaking)
if not valid:
print(f"\n❌ Error: {error}")
sys.exit(1)
# Get description
print("\nEnter description (explain how and why, not what).")
print("Press Ctrl+D (Linux/Mac) or Ctrl+Z (Windows) when done:")
description_lines = []
try:
while True:
line = input()
description_lines.append(line)
except EOFError:
pass
description = '\n'.join(description_lines).strip()
if description:
description = wrap_text(description)
# Build commit message
type_prefix = f"{BREAKING_CHANGE_PREFIX}{args.type}" if args.breaking else args.type
message = f"[{type_prefix}] {subject}\n\n"
if description:
message += f"{description}\n\n"
# Add footer
if args.issue:
message += f"Resolves: #{args.issue}\n"
if args.related:
related_issues = [f"#{num.strip()}" for num in args.related.split(',')]
for issue in related_issues:
message += f"Related: {issue}\n"
releases = parse_releases(args.releases)
if releases:
message += f"Releases: {', '.join(releases)}\n"
# Output
print("\n" + "="*60)
print("Generated Commit Message:")
print("="*60)
print(message)
print("="*60)
print("\nNote: Change-Id will be added automatically by git hook")
print("="*60)
if args.output:
with open(args.output, 'w') as f:
f.write(message)
print(f"\n✓ Commit message saved to: {args.output}")
print(f" Use: git commit -F {args.output}")
else:
print("\nTo use this message:")
print(" 1. Copy the message above")
print(" 2. Run: git commit")
print(" 3. Paste into your editor")
return 0
if __name__ == '__main__':
sys.exit(main())