""" Commit message and changelog generation using AI. """ from typing import List, Dict, Optional, Tuple from ..shared.ai import AIService from ..shared.config import Config from concurrent.futures import ThreadPoolExecutor class CommitGenerator(AIService): """AI-powered commit message and changelog generator.""" def __init__(self, config: Config): """Initialize commit generator.""" super().__init__(config) def generate_commit_message( self, diff: str, temperature: Optional[float] = None ) -> str: """Generate a commit message from a diff.""" system_prompt = """You are an expert Git assistant trained to write highly effective and conventional commit messages. Your task is to analyze the provided code diff and generate a commit message in the following format: Format: type(scope): description - type: One of feat, fix, docs, style, refactor, test, chore - scope: Optional but encouraged; represents the part of the codebase affected (e.g. auth, api, db, ui) - description: A short, clear explanation of the change Each type has an emoji prefix: - ✨ feat: New feature - 🐛 fix: Bug fix - 📚 docs: Documentation-only change - 💅 style: Code formatting (no logic change) - ♻️ refactor: Code changes without affecting behavior - ✅ test: Test additions/changes - 🔧 chore: Maintenance, build, or config tasks Strict rules: - Write the entire message in a single line - Use imperative mood (e.g. "add" not "added") - No file paths, no periods at the end - Avoid generic or vague descriptions - Don't mention pull requests, issues, or reviewers - Focus on the why and impact of the change, not just the what Examples: - ✨ feat(auth): add OAuth2 login support - 🐛 fix(api): return correct HTTP status for invalid input - ♻️ refactor(db): simplify user query joins Output ONLY the commit message. Do not include any comments or explanations.""" user_prompt = f"""Generate a single-line conventional commit message for the following code changes: {diff} Identify the most relevant type, a concise scope, and the purpose of the change. Output ONLY the commit message in the correct format (with emoji).""" message = self.generate_completion( system_prompt, user_prompt, temperature=temperature ) lines = [line.strip() for line in message.split("\n") if line.strip()] for line in lines: if any( skip in line.lower() for skip in [ "based on", "changes in", "can be written", "commit message", "following", "these changes", "the changes", ] ): continue if line.startswith("`") or line.startswith("/") or ":" in line: continue message = line break else: message = lines[0] if lines else "🔧 chore: update code" if not any( emoji in message for emoji in ["✨", "🐛", "📚", "💅", "♻️", "✅", "🔧"] ): if message.startswith("feat"): message = "✨ " + message elif message.startswith("fix"): message = "🐛 " + message elif message.startswith("docs"): message = "📚 " + message elif message.startswith("style"): message = "💅 " + message elif message.startswith("refactor"): message = "♻️ " + message elif message.startswith("test"): message = "✅ " + message elif message.startswith("chore"): message = "🔧 " + message else: message = "🔧 " + message # fallback return message def generate_batch_messages( self, diffs: Dict[str, str], temperature: Optional[float] = None ) -> Dict[str, str]: """Generate commit messages for multiple files with intelligent grouping. This method analyzes the diffs to determine if they should be grouped into a single commit message or kept separate based on their relationship and scope. Args: diffs: Dictionary mapping file paths to their diffs temperature: Optional temperature for generation Returns: Dictionary mapping file paths to their commit messages """ if not diffs: return {} if len(diffs) == 1: file_path, diff = next(iter(diffs.items())) return {file_path: self.generate_commit_message(diff, temperature)} # First, analyze the diffs to determine if they should be grouped system_prompt = """You are an expert at analyzing code changes and determining their relationships. Analyze the provided diffs and determine if they represent related changes that should be grouped together. Consider: 1. Are the changes part of the same feature or fix? 2. Do they share a common scope or purpose? 3. Would separating them make the commit history less meaningful? Respond with either: - "GROUP" if the changes should be combined into one commit - "SEPARATE" if the changes should have individual commit messages Include a brief explanation of your reasoning.""" # Prepare the diffs for analysis analysis_input = "\n\n".join( f"File: {file_path}\nChanges:\n{diff}" for file_path, diff in diffs.items() ) analysis = self.generate_completion( system_prompt, f"Analyze these changes:\n\n{analysis_input}", temperature=0.1, # Lower temperature for more consistent analysis ) should_group = "GROUP" in analysis.upper() if should_group: # Generate a single commit message for all changes try: # Create a structured diff that maintains file context structured_diff = "\n\n".join( f"=== Changes in {file_path} ===\n{diff}" for file_path, diff in diffs.items() ) message = self.generate_commit_message(structured_diff, temperature) return {file_path: message for file_path in diffs.keys()} except Exception as e: # Fallback to individual messages if combined generation fails print(f"Warning: Failed to generate combined message: {e}") should_group = False if not should_group: # Generate individual messages for each file results = {} for file_path, diff in diffs.items(): try: message = self.generate_commit_message(diff, temperature) results[file_path] = message except Exception as e: # Fallback to a basic message if generation fails print(f"Warning: Failed to generate message for {file_path}: {e}") results[file_path] = "🔧 chore: update code" return results def generate_changelog( self, commits: List[str], version: str, temperature: Optional[float] = None ) -> str: """Generate a changelog from a list of commits.""" system_prompt = """You are a professional release manager generating changelogs from commit messages. Follow these rules: - Group entries under clear sections: Added, Changed, Fixed, Removed, Refactored, Tests, Chore, etc. - Use past tense (e.g. "Added login flow", not "Add login flow") - Write concise but informative summaries - Each bullet point should start with a verb (e.g. "Fixed", "Added") - Skip commit types like "chore" unless they are user-relevant - Format sections like this: ## [version] ### Added - Added support for OAuth2 authentication ### Fixed - Fixed issue where empty input crashed the API Do NOT include raw commit messages. Use the commit messages as input and convert them into user-facing changelog entries.""" user_prompt = ( f"Generate a clean and structured changelog for version { version } using these commits:\n\n" + "\n".join(commits) ) return self.generate_completion( system_prompt, user_prompt, temperature=temperature )