Files
local-commit-message-generator/app/local_commit_message_generator/src/hooks.py

229 lines
6.4 KiB
Python

"""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 ""