From 8cf74984d91fd2d8f87cc459bb83656203b82510 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Wed, 4 Feb 2026 17:54:26 +0000 Subject: [PATCH] Initial upload with CI/CD workflow --- app/src/git_commit_generator/cli.py | 349 ++++++++++++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 app/src/git_commit_generator/cli.py diff --git a/app/src/git_commit_generator/cli.py b/app/src/git_commit_generator/cli.py new file mode 100644 index 0000000..bc35204 --- /dev/null +++ b/app/src/git_commit_generator/cli.py @@ -0,0 +1,349 @@ +"""CLI interface for git-commit-message-generator.""" +from pathlib import Path +from typing import Optional + +import click +from rich import print as rprint + +from git_commit_generator.changelog_generator import ChangelogGenerator, get_changelog_generator +from git_commit_generator.config import Config, get_config +from git_commit_generator.git_utils import GitUtils, get_git_utils +from git_commit_generator.interactive import Action, InteractiveMode, get_interactive_mode +from git_commit_generator.message_generator import MessageGenerator, get_message_generator +from git_commit_generator.ollama_client import OllamaClient, get_ollama_client + + +@click.group() +@click.option( + "--host", + default=None, + help="Ollama server URL", +) +@click.option( + "--model", + default=None, + help="Model to use for generation", +) +@click.pass_context +def main(ctx: click.Context, host: Optional[str], model: Optional[str]) -> None: + """Git Commit Message Generator CLI. + + A CLI tool that generates git commit messages using local LLMs. + """ + ctx.ensure_object(dict) + config = get_config() + ctx.obj["config"] = config + ctx.obj["host"] = host or config.ollama_host + ctx.obj["model"] = model or config.ollama_model + + +@main.command("generate") +@click.option( + "--staged/--unstaged", + default=True, + help="Include staged (default) or unstaged changes", +) +@click.option( + "--interactive", + "-i", + is_flag=True, + default=False, + help="Use interactive mode for message refinement", +) +@click.option( + "--output", + "-o", + type=click.Path(), + help="Write commit message to file", +) +@click.pass_context +def generate( + ctx: click.Context, + staged: bool, + interactive: bool, + output: Optional[str], +) -> None: + """Generate a commit message from staged/unstaged changes.""" + config: Config = ctx.obj["config"] + host: str = ctx.obj["host"] + model: str = ctx.obj["model"] + + interactive_mode = get_interactive_mode() + + try: + ollama_client = get_ollama_client(host=host, model=model) + + if not ollama_client.check_connection(): + interactive_mode.show_error( + f"Could not connect to Ollama at {host}. " + "Make sure Ollama is running." + ) + interactive_mode.show_info( + "Tip: Run 'ollama serve' to start Ollama server." + ) + return + + interactive_mode.show_connection_status(True, model) + interactive_mode.show_info("Generating commit message...") + + message_generator = get_message_generator( + config=config, + ollama_client=ollama_client, + ) + + if not interactive: + message = message_generator.generate( + staged=staged, + unstaged=not staged, + model=model, + ) + rprint(f"\n[bold green]Generated commit message:[/bold green]") + rprint(f"[cyan]{message}[/cyan]") + + if output: + Path(output).write_text(message) + interactive_mode.show_info(f"Message written to {output}") + + else: + _run_interactive_mode( + message_generator=message_generator, + interactive_mode=interactive_mode, + staged=staged, + unstaged=not staged, + output=output, + ) + + except ValueError as e: + interactive_mode.show_error(str(e)) + interactive_mode.show_no_changes_warning() + except Exception as e: + interactive_mode.show_error(f"An error occurred: {str(e)}") + + +def _run_interactive_mode( + message_generator: MessageGenerator, + interactive_mode: InteractiveMode, + staged: bool, + unstaged: bool, + output: Optional[str], +) -> None: + """Run interactive message generation loop.""" + while True: + try: + message = message_generator.generate( + staged=staged, + unstaged=unstaged, + ) + + action, edited_message = interactive_mode.prompt_for_action(message) + + if action == Action.QUIT: + interactive_mode.show_info("Cancelled.") + return + elif action == Action.REGENERATE: + interactive_mode.show_info("Regenerating...") + continue + elif action == Action.ACCEPT: + final_message = edited_message or message + if interactive_mode.confirm_commit(final_message): + rprint(f"\n[bold]Commit message:[/bold]") + rprint(f"[green]{final_message}[/green]") + + if output: + Path(output).write_text(final_message) + interactive_mode.show_info(f"Message written to {output}") + + return + + except ValueError as e: + interactive_mode.show_error(str(e)) + interactive_mode.show_no_changes_warning() + return + except Exception as e: + interactive_mode.show_error(f"An error occurred: {str(e)}") + return + + +@main.command("changelog") +@click.option( + "--since", + default=None, + help="Only include commits since this date/tag", +) +@click.option( + "--limit", + default=50, + type=int, + help="Maximum number of commits to include", +) +@click.option( + "--output", + "-o", + type=click.Path(), + help="Write changelog to file", +) +@click.option( + "--simple", + is_flag=True, + default=False, + help="Generate simple changelog without LLM", +) +@click.pass_context +def changelog( + ctx: click.Context, + since: Optional[str], + limit: int, + output: Optional[str], + simple: bool, +) -> None: + """Generate CHANGELOG.md from git history.""" + config: Config = ctx.obj["config"] + host: str = ctx.obj["host"] + model: str = ctx.obj["model"] + + interactive_mode = get_interactive_mode() + + try: + ollama_client = get_ollama_client(host=host, model=model) + + if not simple and not ollama_client.check_connection(): + interactive_mode.show_error( + f"Could not connect to Ollama at {host}. " + "Use --simple flag for LLM-free generation." + ) + return + + if not simple: + interactive_mode.show_connection_status(True, model) + interactive_mode.show_info("Generating changelog with LLM...") + + changelog_generator = get_changelog_generator( + config=config, + ollama_client=ollama_client, + ) + + changelog = changelog_generator.generate( + since=since, + limit=limit, + output_path=output, + ) + else: + interactive_mode.show_info("Generating simple changelog...") + changelog_generator = get_changelog_generator(config=config) + changelog = changelog_generator.generate_simple( + since=since, + limit=limit, + output_path=output, + ) + + rprint("\n[bold green]Generated Changelog:[/bold green]") + rprint(changelog) + + if output: + interactive_mode.show_info(f"Changelog written to {output}") + + except ValueError as e: + interactive_mode.show_error(str(e)) + except Exception as e: + interactive_mode.show_error(f"An error occurred: {str(e)}") + + +@main.command("config") +@click.option( + "--show", + is_flag=True, + default=False, + help="Show current configuration", +) +@click.option( + "--host", + default=None, + help="Set Ollama host URL", +) +@click.option( + "--model", + default=None, + help="Set default model", +) +@click.pass_context +def config_cmd( + ctx: click.Context, + show: bool, + host: Optional[str], + model: Optional[str], +) -> None: + """Configure git-commit-message-generator settings.""" + config: Config = ctx.obj["config"] + + if show: + rprint("[bold]Current Configuration:[/bold]") + rprint(f" Ollama Host: {config.ollama_host}") + rprint(f" Ollama Model: {config.ollama_model}") + rprint(f" Prompt Directory: {config.prompt_dir}") + return + + if host or model: + config_path = config.config_path or "./config.yaml" + import yaml + config_data = { + "ollama_host": host or config.ollama_host, + "ollama_model": model or config.ollama_model, + } + with open(config_path, "w") as f: + yaml.dump(config_data, f) + rprint(f"[green]Configuration saved to {config_path}[/green]") + else: + click.echo(main.get_help(ctx)) + + +@main.command("status") +@click.pass_context +def status(ctx: click.Context) -> None: + """Check system status and configuration.""" + config: Config = ctx.obj["config"] + host: str = ctx.obj["host"] + model: str = ctx.obj["model"] + + interactive_mode = get_interactive_mode() + + rprint("[bold]Git Commit Message Generator Status[/bold]\n") + + rprint(f"[bold]Configuration:[/bold]") + rprint(f" Ollama Host: {host}") + rprint(f" Default Model: {model}") + rprint(f" Prompt Directory: {config.prompt_dir}") + + try: + ollama_client = get_ollama_client(host=host, model=model) + connected = ollama_client.check_connection() + interactive_mode.show_connection_status(connected, model) + + if connected: + models = ollama_client.list_models() + rprint(f"\n[bold]Available Models:[/bold]") + if models: + for m in models[:5]: + rprint(f" - {m.get('name', 'unknown')}") + if len(models) > 5: + rprint(f" ... and {len(models) - 5} more") + else: + rprint(" No models found") + + except Exception as e: + interactive_mode.show_error(f"Could not connect to Ollama: {e}") + + try: + git_utils = get_git_utils() + is_repo = git_utils.is_repo() + rprint(f"\n[bold]Git Repository:[/bold]") + rprint(f" Repository Detected: {'Yes' if is_repo else 'No'}") + except Exception: + rprint("\n[bold]Git Repository:[/bold]") + rprint(" Not in a git repository") + + +@main.command("help") +def help_cmd() -> None: + """Show help information.""" + interactive_mode = get_interactive_mode() + interactive_mode.show_help()