"""CLI interface for local-commit-message-generator.""" import sys from pathlib import Path from typing import Optional import click from . import __version__ from .config import ( DEFAULT_CONFIG, ConfigError, ensure_config_exists, get_config_path, load_config, save_config, ) from .generator import GenerationError, generate_commit_message from .hooks import HookManager, handle_hook_invocation def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> None: """Print version information.""" if not value or ctx.resilient_parsing: return click.echo(f"local-commit-message-generator v{__version__}") ctx.exit() @click.group() @click.option( "--version", is_flag=True, callback=print_version, expose_value=False, help="Show version information." ) @click.option( "--repo", default=None, help="Path to git repository." ) @click.pass_context def main(ctx: click.Context, repo: Optional[str]) -> None: """Generate conventional commit messages from staged git changes.""" ctx.ensure_object(dict) ctx.obj["repo"] = repo @main.command() @click.pass_context def generate(ctx: click.Context) -> None: """Generate a commit message from staged changes.""" repo = ctx.obj.get("repo") try: message = generate_commit_message(repo_path=repo) click.echo(message) except GenerationError as e: click.echo(f"Error: {e}", err=True) click.echo("Make sure you have staged changes with 'git add'.", err=True) sys.exit(1) except Exception as e: click.echo(f"Unexpected error: {e}", err=True) sys.exit(1) @main.command() @click.pass_context def hook(ctx: click.Context) -> None: """Handle prepare-commit-msg hook invocation. This is called automatically by git when the hook is installed. Do not call this directly. """ repo = ctx.obj.get("repo") args = sys.argv[1:] if sys.argv else [] try: message = handle_hook_invocation(args, repo) if message: click.echo(message) except Exception: pass @main.command() @click.option( "--path", "config_path", default=None, help="Path to config file." ) @click.pass_context def install_hook(ctx: click.Context, config_path: Optional[str]) -> None: """Install prepare-commit-msg git hook.""" repo = ctx.obj.get("repo") repo_path = Path(repo) if repo else Path.cwd() manager = HookManager(repo_path) result = manager.install_hook() if result.success: click.secho(result.message, fg="green") click.echo("Hook is now active. Commit messages will be auto-generated.") else: click.secho(result.message, fg="red", err=True) sys.exit(1) @main.command() @click.pass_context def uninstall_hook(ctx: click.Context) -> None: """Uninstall prepare-commit-msg git hook.""" repo = ctx.obj.get("repo") repo_path = Path(repo) if repo else Path.cwd() manager = HookManager(repo_path) result = manager.uninstall_hook() if result.success: click.secho(result.message, fg="green") else: click.secho(result.message, fg="red", err=True) sys.exit(1) @main.command() @click.pass_context def status(ctx: click.Context) -> None: """Check git repository and staged changes status.""" repo = ctx.obj.get("repo") try: from .analyzer import ChangeAnalyzer analyzer = ChangeAnalyzer(repo) change_set = analyzer.get_staged_changes() if change_set.has_changes: click.secho(f"Staged changes: {change_set.total_count}", fg="green") for change in change_set.changes[:10]: emoji = { "added": "+", "deleted": "-", "modified": "~", "renamed": "R", }.get(change.change_type.value, "?") click.echo(f" {emoji} {change.path}") if change_set.total_count > 10: click.echo(f" ... and {change_set.total_count - 10} more") else: click.secho("No staged changes", fg="yellow") click.echo("Run 'git add' to stage changes.") except ValueError as e: click.secho(str(e), fg="red", err=True) sys.exit(1) @main.group() def config() -> None: """Manage configuration.""" pass @config.command("show") @click.pass_context def config_show(ctx: click.Context) -> None: """Show current configuration.""" try: ensure_config_exists() config = load_config() click.echo("Current Configuration:") click.echo("-" * 40) for key, value in config.items(): if key == "type_rules": click.echo("type_rules:") for type_, patterns in value.items(): click.echo(f" {type_}: {patterns}") else: click.echo(f"{key}: {value}") except ConfigError as e: click.secho(f"Error: {e}", fg="red", err=True) sys.exit(1) @config.command("set-template") @click.argument("template", nargs=-1) @click.pass_context def config_set_template(ctx: click.Context, template: tuple) -> None: """Set the commit message template.""" try: ensure_config_exists() config = load_config() template_str = " ".join(template) if template else "" config["template"] = template_str save_config(config) click.secho(f"Template updated to: {template_str}", fg="green") except ConfigError as e: click.secho(f"Error: {e}", fg="red", err=True) sys.exit(1) @config.command("reset") @click.pass_context def config_reset(ctx: click.Context) -> None: """Reset configuration to defaults.""" try: config_path = get_config_path() if config_path.exists(): config_path.unlink() save_config(DEFAULT_CONFIG.copy()) click.secho("Configuration reset to defaults.", fg="green") except ConfigError as e: click.secho(f"Error: {e}", fg="red", err=True) sys.exit(1) @main.command() @click.pass_context def preview(ctx: click.Context) -> None: """Preview the commit message without printing.""" repo = ctx.obj.get("repo") try: from .generator import get_commit_message_preview message, has_changes = get_commit_message_preview(repo) if has_changes: click.secho("Preview:", fg="cyan") click.echo("-" * 40) click.echo(message) click.echo("-" * 40) else: click.secho("No staged changes to preview.", fg="yellow") except Exception as e: click.secho(f"Error: {e}", fg="red", err=True) sys.exit(1) def cli_entrypoint() -> None: """Entry point for the CLI.""" main()