diff --git a/git_commit_ai/cli/cli.py b/git_commit_ai/cli/cli.py index cf070fc..f6d8da7 100644 --- a/git_commit_ai/cli/cli.py +++ b/git_commit_ai/cli/cli.py @@ -1,343 +1,44 @@ -"""CLI interface for Git Commit AI.""" - -import sys - import click - -from git_commit_ai.core.cache import CacheManager, get_cache_manager -from git_commit_ai.core.config import Config, get_config -from git_commit_ai.core.conventional import ( - ConventionalCommitParser, - ConventionalCommitFixer, - validate_commit_message, -) -from git_commit_ai.core.git_handler import GitError, GitHandler, get_git_handler -from git_commit_ai.core.ollama_client import OllamaClient, OllamaError, get_client - +from git_commit_ai.core.git_handler import get_staged_changes, get_commit_history +from git_commit_ai.core.ollama_client import generate_commit_message +from git_commit_ai.core.prompt_builder import build_prompt +from git_commit_ai.core.conventional import validate_conventional, fix_conventional +from git_commit_ai.core.config import load_config @click.group() -@click.option( - "--config", - type=click.Path(exists=True, dir_okay=False), - help="Path to config.yaml file", -) -@click.pass_context -def main(ctx: click.Context, config: str) -> None: - """Git Commit AI - Generate intelligent commit messages with local LLM.""" - ctx.ensure_object(dict) - cfg = get_config(config) if config else get_config() - ctx.obj["config"] = cfg +def cli(): + """AI-powered Git commit message generator.""" + pass - -@main.command() -@click.option( - "--conventional/--no-conventional", - default=None, - help="Generate conventional commit format messages", -) -@click.option( - "--model", - default=None, - help="Ollama model to use", -) -@click.option( - "--base-url", - default=None, - help="Ollama API base URL", -) -@click.option( - "--interactive/--no-interactive", - default=None, - help="Interactive mode for selecting messages", -) -@click.option( - "--show-diff", - is_flag=True, - default=None, - help="Show the diff being analyzed", -) -@click.option( - "--auto-fix", - is_flag=True, - default=False, - help="Auto-fix conventional commit format issues", -) -@click.pass_obj -def generate( - ctx: dict, - conventional: bool | None, - model: str | None, - base_url: str | None, - interactive: bool | None, - show_diff: bool, - auto_fix: bool, -) -> None: - """Generate commit message suggestions for staged changes.""" - config: Config = ctx.get("config", get_config()) - - if conventional is None: - conventional = config.conventional_by_default - if interactive is None: - interactive = config.interactive - if show_diff is None: - show_diff = config.show_diff - - git_handler = get_git_handler() - - if not git_handler.is_repository(): - click.echo(click.style("Error: Not in a git repository", fg="red"), err=True) - click.echo("Please run this command from within a git repository.", err=True) - sys.exit(1) - - if not git_handler.is_staged(): - click.echo(click.style("No staged changes found.", fg="yellow")) - click.echo("Please stage your changes first with 'git add '", err=True) - sys.exit(1) - - diff = git_handler.get_staged_changes() - if show_diff: - click.echo("\nStaged diff:") - click.echo("-" * 50) - click.echo(diff[:2000] + "..." if len(diff) > 2000 else diff) - click.echo("-" * 50) - - cache_manager = get_cache_manager(config) - - cached = cache_manager.get(diff, conventional=conventional, model=model or config.ollama_model) - if cached: - messages = cached - click.echo(click.style("Using cached suggestions", fg="cyan")) - else: - ollama_client = get_client(config) - if model: - ollama_client.model = model - if base_url: - ollama_client.base_url = base_url - - if not ollama_client.is_available(): - click.echo(click.style("Error: Ollama server is not available", fg="red"), err=True) - click.echo(f"Please ensure Ollama is running at {ollama_client.base_url}", err=True) - sys.exit(1) - - if not ollama_client.check_model_exists(): - click.echo(click.style(f"Model '{ollama_client.model}' not found", fg="yellow"), err=True) - if click.confirm("Would you like to pull this model?"): - if ollama_client.pull_model(): - click.echo(click.style("Model pulled successfully", fg="green")) - else: - click.echo(click.style("Failed to pull model", fg="red"), err=True) - sys.exit(1) - else: - available = ollama_client.list_models() - if available: - click.echo("Available models:", err=True) - for m in available[:10]: - click.echo(f" - {m.get('name', 'unknown')}", err=True) - sys.exit(1) - - try: - commit_history = git_handler.get_commit_history(max_commits=3) - context = "\n".join(f"- {c['hash']}: {c['message']}" for c in commit_history) - - response = ollama_client.generate_commit_message( - diff=diff, context=context if context else None, conventional=conventional, model=model - ) - - messages = [m.strip() for m in response.split("\n") if m.strip() and not m.strip().lower().startswith("suggestion")] - - if len(messages) == 1: - single = messages[0].split("1.", "2.", "3.") - if len(single) > 1: - messages = [s.strip() for s in single if s.strip()] - - messages = messages[:config.num_suggestions] - - cache_manager.set(diff, messages, conventional=conventional, model=model or config.ollama_model) - - except OllamaError as e: - click.echo(click.style(f"Error generating commit message: {e}", fg="red"), err=True) - sys.exit(1) - - if not messages: - click.echo(click.style("No suggestions generated", fg="yellow"), err=True) - sys.exit(1) - - if conventional and auto_fix: - fixed_messages = [] - for msg in messages: - is_valid, errors = validate_commit_message(msg) +@cli.command() +@click.option('--conventional', is_flag=True, help='Generate conventional commit format') +@click.option('--model', default=None, help='Ollama model to use') +@click.option('--base-url', default=None, help='Ollama API base URL') +def generate(conventional, model, base_url): + """Generate a commit message for staged changes.""" + try: + config = load_config() + model = model or config.get('model', 'qwen2.5-coder:3b') + base_url = base_url or config.get('base_url', 'http://localhost:11434') + + staged = get_staged_changes() + if not staged: + click.echo("No staged changes found. Stage your changes first.") + return + + history = get_commit_history() + prompt = build_prompt(staged, conventional=conventional, history=history) + + message = generate_commit_message(prompt, model=model, base_url=base_url) + + if conventional: + is_valid, suggestion = validate_conventional(message) if not is_valid: - fixed = ConventionalCommitFixer.fix(msg, diff) - fixed_messages.append(fixed) - else: - fixed_messages.append(msg) - messages = fixed_messages - - click.echo("\n" + click.style("Suggested commit messages:", fg="green")) - for i, msg in enumerate(messages, 1): - click.echo(f" {i}. {msg}") - - if conventional: - click.echo() - for i, msg in enumerate(messages, 1): - is_valid, errors = validate_commit_message(msg) - if is_valid: - click.echo(click.style(f" {i}. [Valid conventional format]", fg="green")) - else: - click.echo(click.style(f" {i}. [Format issues: {', '.join(errors)}]", fg="yellow")) - - if interactive: - choice = click.prompt("\nSelect a message (number) or press Enter to see all:", type=int, default=0, show_default=False) - if 1 <= choice <= len(messages): - selected = messages[choice - 1] - click.echo(f"\nSelected: {selected}") - click.echo(f"\nTo commit, run:") - click.echo(f' git commit -m "{selected}"') - else: - click.echo(f"\nTo use the first suggestion, run:") - click.echo(click.style(f' git commit -m "{messages[0]}"', fg="cyan")) - - -@main.command() -@click.option("--model", help="Ollama model to check") -@click.pass_obj -def status(ctx: dict, model: str | None) -> None: - """Check Ollama and repository status.""" - config: Config = ctx.get("config", get_config()) - - click.echo("Git Commit AI Status") - click.echo("=" * 40) - - git_handler = get_git_handler() - click.echo(f"\nGit Repository: {'Yes' if git_handler.is_repository() else 'No'}") - - if git_handler.is_repository(): - click.echo(f"Staged Changes: {'Yes' if git_handler.is_staged() else 'No'}") - - ollama_client = get_client(config) - if model: - ollama_client.model = model - - click.echo(f"\nOllama:") - click.echo(f" Base URL: {ollama_client.base_url}") - click.echo(f" Model: {ollama_client.model}") - - if ollama_client.is_available(): - click.echo(f" Status: {click.style('Running', fg='green')}") - - if ollama_client.check_model_exists(): - click.echo(f" Model: {click.style('Available', fg='green')}") - else: - click.echo(f" Model: {click.style('Not found', fg='yellow')}") - available = ollama_client.list_models() - if available: - click.echo(" Available models:") - for m in available[:5]: - click.echo(f" - {m.get('name', 'unknown')}") - else: - click.echo(f" Status: {click.style('Not running', fg='red')}") - click.echo(f" Start Ollama with: {click.style('ollama serve', fg='cyan')}") - - cache_manager = get_cache_manager(config) - stats = cache_manager.get_stats() - click.echo(f"\nCache:") - click.echo(f" Enabled: {'Yes' if stats['enabled'] else 'No'}") - click.echo(f" Entries: {stats['entries']}") - if stats['entries'] > 0: - click.echo(f" Size: {stats['size_bytes'] // 1024} KB") - - -@main.command() -@click.pass_obj -def models(ctx: dict) -> None: - """List available Ollama models.""" - config: Config = ctx.get("config", get_config()) - ollama_client = get_client(config) - - if not ollama_client.is_available(): - click.echo(click.style("Error: Ollama server is not available", fg="red"), err=True) - sys.exit(1) - - models = ollama_client.list_models() - if models: - click.echo("Available models:") - for m in models: - name = m.get("name", "unknown") - size = m.get("size", 0) - size_mb = size / (1024 * 1024) if size else 0 - click.echo(f" {name} ({size_mb:.1f} MB)") - else: - click.echo("No models found.") - - -@main.command() -@click.option("--model", help="Model to pull") -@click.pass_obj -def pull(ctx: dict, model: str | None) -> None: - """Pull an Ollama model.""" - config: Config = ctx.get("config", get_config()) - ollama_client = get_client(config) - - model = model or config.ollama_model - - if not ollama_client.is_available(): - click.echo(click.style("Error: Ollama server is not available", fg="red"), err=True) - sys.exit(1) - - with click.progressbar(length=100, label=f"Pulling {model}", show_percent=True, show_pos=True) as progress: - success = ollama_client.pull_model(model) - if success: - progress.update(100) - click.echo(click.style(f"\nModel {model} pulled successfully", fg="green")) - else: - click.echo(click.style(f"\nFailed to pull model {model}", fg="red"), err=True) - sys.exit(1) - - -@main.command() -@click.option("--force", is_flag=True, help="Force cleanup without confirmation") -@click.pass_obj -def cache(ctx: dict, force: bool) -> None: - """Manage cache.""" - config: Config = ctx.get("config", get_config()) - cache_manager = get_cache_manager(config) - - stats = cache_manager.get_stats() - - click.echo("Cache Status:") - click.echo(f" Enabled: {'Yes' if stats['enabled'] else 'No'}") - click.echo(f" Entries: {stats['entries']}") - click.echo(f" Expired: {stats['expired']}") - click.echo(f" Size: {stats['size_bytes'] // 1024} KB") - - if stats['entries'] > 0: - if force or click.confirm("\nClear all cache entries?"): - cleared = cache_manager.clear() - click.echo(f"Cleared {cleared} entries") - - -@main.command() -@click.argument("message") -@click.option("--auto-fix", is_flag=True, help="Attempt to auto-fix format issues") -def validate(message: str, auto_fix: bool) -> None: - """Validate a commit message format.""" - is_valid, errors = validate_commit_message(message) - - if is_valid: - click.echo(click.style("Valid commit message", fg="green")) - else: - click.echo(click.style("Invalid commit message:", fg="red")) - for error in errors: - click.echo(f" - {error}") - - if auto_fix: - fixed = ConventionalCommitFixer.fix(message, "") - if fixed != message: - click.echo() - click.echo(click.style(f"Suggested fix: {fixed}", fg="cyan")) - - sys.exit(0 if is_valid else 1) - - -if __name__ == "__main__": - main() + fixed = fix_conventional(message, staged) + if fixed: + message = fixed + + click.echo(f"\nSuggested commit message:\n{message}") + + except Exception as e: + click.echo(f"Error: {e}")