"""Template management for commit message formatting.""" import re from dataclasses import dataclass from typing import Any, Dict, List, Optional @dataclass class TemplateVariable: """Represents a template variable with its value.""" name: str value: str display_name: Optional[str] = None class TemplateManager: """Manages commit message templates.""" DEFAULT_TEMPLATES = { "simple": "{type}{scope}: {description}", "detailed": "{type}({scope}): {description}\n\n{body}", "conventional": "{type}{scope}: {description}\n\n{files}", "verbose": "{type}{scope}: {description}\n\n{files}\n\n{body}", } def __init__(self, template: Optional[str] = None): """Initialize template manager. Args: template: Template string to use. If not provided, uses simple template. """ self.template = template or self.DEFAULT_TEMPLATES["simple"] def render( self, type: str, scope: str, description: str, body: str = "", files: Optional[List[str]] = None, **kwargs: Any ) -> str: """Render a commit message from template. Args: type: Commit type (feat, fix, docs, etc.) scope: Commit scope. description: Commit description. body: Optional extended body text. files: Optional list of changed files. **kwargs: Additional template variables. Returns: Rendered commit message string. """ files_part = "" if files: files_str = "\n".join(f" - {f}" for f in files[:10]) if len(files) > 10: files_str += f"\n ... and {len(files) - 10} more" files_part = f"\n\nFiles changed:\n{files_str}" variables = { "type": type, "scope": scope, "description": description, "body": body, "files": files_part, **kwargs } result = self.template for key, value in variables.items(): placeholder = f"{{{key}}}" result = result.replace(placeholder, str(value)) return result.strip() @classmethod def get_default_templates(cls) -> Dict[str, str]: """Get all default templates. Returns: Dictionary of template name to template string. """ return cls.DEFAULT_TEMPLATES.copy() @classmethod def validate_template(cls, template: str) -> tuple[bool, Optional[str]]: """Validate a template string. Args: template: Template string to validate. Returns: Tuple of (is_valid, error_message). """ try: placeholders = re.findall(r"\{(\w+)\}", template) valid_placeholders = {"type", "scope", "description", "body", "files"} invalid = set(placeholders) - valid_placeholders if invalid: return False, f"Invalid placeholders: {', '.join(sorted(invalid))}" return True, None except re.error as e: return False, f"Invalid regex in template: {e}" def generate_suggestions(self, changes: List[str]) -> List[str]: """Generate description suggestions based on changed files. Args: changes: List of changed file paths. Returns: List of suggested descriptions. """ suggestions = [] for path in changes[:5]: filename = path.split("/")[-1] base_name = ".".join(filename.split(".")[:-1]) if base_name: suggestions.append(f"update {base_name}") else: suggestions.append(f"update {filename}") return suggestions def format_message( type: str, scope: str, description: str, template: Optional[str] = None, files: Optional[List[str]] = None ) -> str: """Format a commit message. Args: type: Commit type. scope: Commit scope. description: Commit description. template: Optional custom template. files: Optional list of changed files. Returns: Formatted commit message. """ manager = TemplateManager(template) return manager.render(type, scope, description, files=files)