"""Git hook integration for prepare-commit-msg.""" import os import shutil from dataclasses import dataclass from pathlib import Path from typing import Optional @dataclass class HookResult: """Result of a hook operation.""" success: bool message: str backup_path: Optional[str] = None class HookManager: """Manages git prepare-commit-msg hook installation.""" HOOK_FILENAME = "prepare-commit-msg" HOOK_CONTENT = '''#!/bin/bash # prepare-commit-msg hook installed by local-commit-message-generator # Generated commit message - edit as needed exec commit-gen hook "$@" ''' def __init__(self, repo_path: Optional[str] = None): """Initialize hook manager. Args: repo_path: Optional path to git repository. """ self.repo_path = Path(repo_path) if repo_path else Path.cwd() self.hooks_dir = self.repo_path / ".git" / "hooks" def get_hook_path(self) -> Path: """Get the path to the prepare-commit-msg hook. Returns: Path to the hook file. """ return self.hooks_dir / self.HOOK_FILENAME def hook_exists(self) -> bool: """Check if a prepare-commit-msg hook already exists. Returns: True if hook exists, False otherwise. """ hook_path = self.get_hook_path() return hook_path.exists() and hook_path.stat().st_size > 0 def has_our_hook(self) -> bool: """Check if our hook is installed. Returns: True if our hook is installed, False otherwise. """ hook_path = self.get_hook_path() if not hook_path.exists(): return False content = hook_path.read_text() return "commit-gen hook" in content def backup_existing_hook(self) -> Optional[Path]: """Backup existing hook if it exists. Returns: Path to backup file or None if no backup was needed. """ hook_path = self.get_hook_path() if not hook_path.exists(): return None backup_path = hook_path.with_suffix(".backup") shutil.copy2(hook_path, backup_path) return backup_path def install_hook(self) -> HookResult: """Install the prepare-commit-msg hook. Returns: HookResult with success status and message. """ try: hook_path = self.get_hook_path() if not self.hooks_dir.exists(): return HookResult( success=False, message=f"Git hooks directory not found: {self.hooks_dir}" ) backup_path = None if self.hook_exists() and not self.has_our_hook(): backup_path = self.backup_existing_hook() msg = f"Backed up existing hook to {backup_path}" else: msg = "" hook_path.write_text(self.HOOK_CONTENT) os.chmod(hook_path, 0o755) if msg: msg = f"Hook installed. {msg}" else: msg = "Hook installed successfully." return HookResult( success=True, message=msg, backup_path=str(backup_path) if backup_path else None ) except PermissionError: return HookResult( success=False, message="Permission denied. Check write permissions on .git/hooks/" ) except OSError as e: return HookResult( success=False, message=f"Failed to install hook: {e}" ) def uninstall_hook(self) -> HookResult: """Uninstall the prepare-commit-msg hook. Returns: HookResult with success status and message. """ try: hook_path = self.get_hook_path() if not hook_path.exists(): return HookResult( success=False, message="No hook file found to uninstall." ) if self.has_our_hook(): backup_path = hook_path.with_suffix(".our_backup") os.rename(hook_path, backup_path) return HookResult( success=True, message=f"Hook uninstalled. Backup saved to {backup_path}" ) else: return HookResult( success=False, message="Our hook was not installed. Not modifying." ) except OSError as e: return HookResult( success=False, message=f"Failed to uninstall hook: {e}" ) def restore_hook(self) -> HookResult: """Restore a backed-up hook. Returns: HookResult with success status and message. """ hook_path = self.get_hook_path() backup_path = hook_path.with_suffix(".backup") if not backup_path.exists(): return HookResult( success=False, message="No backup file found to restore." ) try: os.rename(backup_path, hook_path) return HookResult( success=True, message="Restored original hook from backup." ) except OSError as e: return HookResult( success=False, message=f"Failed to restore hook: {e}" ) def is_hook_mode(args: list) -> bool: """Check if running in hook mode. Args: args: Command line arguments. Returns: True if running in hook mode. """ return len(args) >= 1 and args[0] == "hook" def handle_hook_invocation(args: list, repo_path: Optional[str] = None) -> str: """Handle prepare-commit-msg hook invocation. Args: args: Command line arguments from hook. repo_path: Optional repository path. Returns: Generated commit message or empty string. """ if len(args) < 1: return "" commit_msg_file = args[0] commit_source = args[1] if len(args) > 1 else "" sha1 = args[2] if len(args) > 2 else "" try: from .generator import generate_commit_message message = generate_commit_message(repo_path) if message and commit_source != "merge" and commit_source != "squash": Path(commit_msg_file).write_text(message) return message or "" except Exception: return ""