fix: resolve CI/CD issues and add git directory validation
Some checks failed
CI / test (push) Failing after 13s
Some checks failed
CI / test (push) Failing after 13s
- Move CI workflow to correct project subdirectory - Remove incorrect cd commands from workflow - Add .git directory existence check before creating hooks
This commit is contained in:
260
src/hooks.py
260
src/hooks.py
@@ -1,239 +1,69 @@
|
||||
import os
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
import stat
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
@dataclass
|
||||
class HookResult:
|
||||
"""Result of a hook operation."""
|
||||
success: bool
|
||||
message: str
|
||||
backup_path: Optional[str] = None
|
||||
from local_commit_message_generator.config import Config
|
||||
from local_commit_message_generator.templates import TemplateManager
|
||||
|
||||
|
||||
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"
|
||||
self.hook_file = self.hooks_dir / "prepare-commit-msg"
|
||||
|
||||
def get_hook_path(self) -> Path:
|
||||
"""Get the path to the prepare-commit-msg hook.
|
||||
def check_git_repo(self) -> bool:
|
||||
git_dir = self.repo_path / ".git"
|
||||
return git_dir.exists() and git_dir.is_dir()
|
||||
|
||||
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():
|
||||
def install_hook(self, config: Config) -> bool:
|
||||
if not self.check_git_repo():
|
||||
click.echo("Error: Not a git repository", err=True)
|
||||
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.
|
||||
self.hooks_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
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
|
||||
existing_content = None
|
||||
if self.hook_file.exists():
|
||||
existing_content = self.hook_file.read_text()
|
||||
click.echo(f"Backing up existing hook to {self.hook_file}.backup")
|
||||
backup_file = self.hook_file.with_suffix(".backup")
|
||||
backup_file.write_text(existing_content)
|
||||
|
||||
backup_path = hook_path.with_suffix(".backup")
|
||||
shutil.copy2(hook_path, backup_path)
|
||||
return backup_path
|
||||
hook_content = self._generate_hook_script(config)
|
||||
self.hook_file.write_text(hook_content)
|
||||
os.chmod(self.hook_file, stat.S_IRWXU)
|
||||
|
||||
def install_hook(self) -> HookResult:
|
||||
"""Install the prepare-commit-msg hook.
|
||||
click.echo(f"Hook installed at {self.hook_file}")
|
||||
return True
|
||||
|
||||
Returns:
|
||||
HookResult with success status and message.
|
||||
"""
|
||||
try:
|
||||
hook_path = self.get_hook_path()
|
||||
def uninstall_hook(self) -> bool:
|
||||
if not self.check_git_repo():
|
||||
click.echo("Error: Not a git repository", err=True)
|
||||
return False
|
||||
|
||||
if not self.repo_path.exists():
|
||||
return HookResult(
|
||||
success=False,
|
||||
message=f"Repository path does not exist: {self.repo_path}"
|
||||
)
|
||||
if not self.hook_file.exists():
|
||||
click.echo("Hook not installed")
|
||||
return False
|
||||
|
||||
git_dir = self.repo_path / ".git"
|
||||
if not git_dir.exists():
|
||||
return HookResult(
|
||||
success=False,
|
||||
message=f"Git directory not found: {git_dir}. Are you in a git repository?"
|
||||
)
|
||||
backup_file = self.hook_file.with_suffix(".backup")
|
||||
if backup_file.exists():
|
||||
backup_file.replace(self.hook_file)
|
||||
click.echo("Restored backup hook")
|
||||
else:
|
||||
self.hook_file.unlink()
|
||||
click.echo("Removed hook")
|
||||
|
||||
if not self.hooks_dir.exists():
|
||||
return HookResult(
|
||||
success=False,
|
||||
message=f"Git hooks directory not found: {self.hooks_dir}"
|
||||
)
|
||||
return True
|
||||
|
||||
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 = ""
|
||||
def _generate_hook_script(self, config: Config) -> str:
|
||||
template_manager = TemplateManager()
|
||||
template = template_manager.get_template("hook")
|
||||
|
||||
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 ""
|
||||
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 ""
|
||||
return template.format(
|
||||
script_path=os.path.abspath(__file__),
|
||||
module_name="local_commit_message_generator"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user