From 083d3bff254da6abb8dc39e32a6ab137236ebe24 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 29 Jan 2026 12:00:31 +0000 Subject: [PATCH] Initial upload of auto-changelog-generator --- src/changeloggen/main.py | 404 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 src/changeloggen/main.py diff --git a/src/changeloggen/main.py b/src/changeloggen/main.py new file mode 100644 index 0000000..d88c3fd --- /dev/null +++ b/src/changeloggen/main.py @@ -0,0 +1,404 @@ +from pathlib import Path +from typing import Optional +import typer +from rich.console import Console +from rich.panel import Panel +from rich.text import Text + +from .git_client import ChangeSet, FileChange +from .llm_client import OllamaAPIClient, Config as LLMConfig +from .changelog_generator import ( + categorize_changes, + format_conventional_changelog, + format_json_output, + format_release_notes, +) +from .config import load_config +from .git_hooks import ( + install_prepare_hook, + install_commit_msg_hook, + remove_hook, + list_installed_hooks, +) + +app = typer.Typer( + name="changeloggen", + help="Auto Changelog Generator - Generate changelogs from git diffs using local AI", + add_completion=False +) + +console = Console() + + +@app.command() +def generate( + output: str = typer.Option( + "markdown", + "--output", "-o", + help="Output format: markdown, json, release" + ), + model: str = typer.Option( + "llama3.2", + "--model", "-m", + help="LLM model to use" + ), + all_changes: bool = typer.Option( + False, + "--all", "-a", + help="Include unstaged changes" + ), + version: str = typer.Option( + "1.0.0", + "--version", "-v", + help="Version string for changelog" + ), + output_file: Optional[Path] = typer.Option( + None, + "--output-file", "-f", + help="Write output to file" + ), +): + """Generate changelog from git diffs.""" + try: + from git import Repo + repo = Repo(Path.cwd()) + except Exception as e: + console.print(Panel( + Text(f"Error: Not a git repository or git not available.\n{str(e)}", style="red"), + title="Error" + )) + raise typer.Exit(1) + + llm_config = LLMConfig(model=model) + llm_client = OllamaAPIClient(llm_config) + + if not llm_client.is_available(): + console.print(Panel( + Text("Error: LLM API not available. Please ensure Ollama or LM Studio is running.", style="red"), + title="Error" + )) + raise typer.Exit(1) + + change_set = ChangeSet() + + try: + staged = repo.index.diff("HEAD") + for diff in staged: + file_change = FileChange( + file_path=diff.a_path or diff.b_path, + change_type="modified", + diff_content=diff.diff.decode() if isinstance(diff.diff, bytes) else str(diff.diff), + staged=True + ) + change_set.staged_changes.append(file_change) + + if all_changes: + unstaged = repo.index.diff(None) + for diff in unstaged: + file_change = FileChange( + file_path=diff.a_path or diff.b_path, + change_type="modified", + diff_content=diff.diff.decode() if isinstance(diff.diff, bytes) else str(diff.diff), + staged=False + ) + change_set.unstaged_changes.append(file_change) + + except Exception as e: + console.print(Panel( + Text(f"Error reading git diff: {str(e)}", style="red"), + title="Error" + )) + raise typer.Exit(1) + + if not change_set.all_changes: + console.print(Panel( + Text("No changes detected. Stage some files first.", style="yellow"), + title="Info" + )) + return + + console.print(f"Found {change_set.total_files_changed} changed files") + + categorized = categorize_changes(change_set, llm_client, model) + + if output == "markdown": + result = format_conventional_changelog(categorized, version) + elif output == "json": + result = format_json_output(categorized, version) + elif output == "release": + result = format_release_notes(categorized, version) + elif output == "commit-message": + if categorized.changes: + change = categorized.changes[0] + scope_part = f"({change.scope})" if change.scope else "" + result = f"{change.type}{scope_part}: {change.description}" + else: + result = categorized.summary + else: + result = format_conventional_changelog(categorized, version) + + if output_file: + output_file.write_text(result) + console.print(f"Changelog written to: {output_file}") + else: + console.print(result) + + +@app.command() +def release( + version: str = typer.Option( + "1.0.0", + "--version", "-v", + help="Version for release notes" + ), + output_file: Optional[Path] = typer.Option( + None, + "--output-file", "-f", + help="Write output to file" + ), + model: str = typer.Option( + "llama3.2", + "--model", "-m", + help="LLM model to use" + ), +): + """Generate release notes for GitHub/GitLab.""" + try: + from git import Repo + repo = Repo(Path.cwd()) + except Exception as e: + console.print(Panel( + Text(f"Error: Not a git repository.\n{str(e)}", style="red"), + title="Error" + )) + raise typer.Exit(1) + + llm_config = LLMConfig(model=model) + llm_client = OllamaAPIClient(llm_config) + + if not llm_client.is_available(): + console.print(Panel( + Text("Error: LLM API not available.", style="red"), + title="Error" + )) + raise typer.Exit(1) + + try: + commits = list(repo.iter_commits("HEAD~10..HEAD")) + + change_set = ChangeSet() + for commit in commits: + file_change = FileChange( + file_path=str(commit.hexsha), + change_type="modified", + diff_content=f"{commit.message}\nAuthor: {commit.author.name}" + ) + change_set.staged_changes.append(file_change) + + except Exception as e: + console.print(Panel( + Text(f"Error reading commit history: {str(e)}", style="red"), + title="Error" + )) + raise typer.Exit(1) + + categorized = categorize_changes(change_set, llm_client, model) + result = format_release_notes(categorized, version) + + if output_file: + output_file.write_text(result) + console.print(f"Release notes written to: {output_file}") + else: + console.print(result) + + +@app.command() +def install_hook( + hook_type: str = typer.Argument( + help="Hook type: prepare-commit-msg or commit-msg" + ), + model: str = typer.Option( + "llama3.2", + "--model", "-m", + help="LLM model to use" + ), + branches: Optional[str] = typer.Option( + None, + "--branches", "-b", + help="Comma-separated list of branches to run on (default: all)" + ), +): + """Install git hook for automatic changelog generation.""" + if hook_type not in ["prepare-commit-msg", "commit-msg"]: + console.print(Panel( + Text("Invalid hook type. Use 'prepare-commit-msg' or 'commit-msg'", style="red"), + title="Error" + )) + raise typer.Exit(1) + + try: + from git import Repo + repo = Repo(Path.cwd()) + except Exception: + console.print(Panel( + Text("Error: Not a git repository.", style="red"), + title="Error" + )) + raise typer.Exit(1) + + branch_list = None + if branches: + branch_list = [b.strip() for b in branches.split(",")] + + if hook_type == "prepare-commit-msg": + hook_path = install_prepare_hook(Path.cwd(), model, branch_list) + else: + hook_path = install_commit_msg_hook(Path.cwd(), model, branch_list) + + console.print(Panel( + Text(f"Hook installed at: {hook_path}", style="green"), + title="Success" + )) + + +@app.command() +def remove_hook_cmd( + hook_type: str = typer.Argument( + help="Hook type to remove" + ), +): + """Remove installed git hook.""" + if hook_type not in ["prepare-commit-msg", "commit-msg"]: + console.print(Panel( + Text("Invalid hook type.", style="red"), + title="Error" + )) + raise typer.Exit(1) + + removed = remove_hook(hook_type, Path.cwd()) + + if removed: + console.print(Panel( + Text(f"Hook '{hook_type}' removed.", style="green"), + title="Success" + )) + else: + console.print(Panel( + Text(f"Hook '{hook_type}' not found.", style="yellow"), + title="Info" + )) + + +@app.command() +def list_hooks(): + """List all installed changeloggen hooks.""" + hooks = list_installed_hooks(Path.cwd()) + + if hooks: + console.print("Installed hooks:") + for hook in hooks: + console.print(f" - {hook}") + else: + console.print("No changeloggen hooks installed.") + + +@app.command() +def config_show(): + """Show current configuration.""" + config = load_config() + + console.print(Panel( + f"Ollama URL: {config.ollama_url}\n" + f"Model: {config.model}\n" + f"Temperature: {config.temperature}\n" + f"Output Format: {config.output_format}\n" + f"Include Unstaged: {config.include_unstaged}", + title="Configuration" + )) + + +@app.command() +def config_set( + ollama_url: Optional[str] = typer.Option( + None, + "--ollama-url", + help="Set Ollama/LM Studio URL" + ), + model: Optional[str] = typer.Option( + None, + "--model", + help="Set LLM model" + ), + temperature: Optional[float] = typer.Option( + None, + "--temperature", + help="Set temperature (0.0-1.0)" + ), + output_format: Optional[str] = typer.Option( + None, + "--output-format", + help="Set default output format" + ), +): + """Update configuration.""" + config = load_config() + + if ollama_url: + config.ollama_url = ollama_url + if model: + config.model = model + if temperature is not None: + config.temperature = temperature + if output_format: + config.output_format = output_format + + config_path = Path(".changeloggen.yaml") + from .config import save_config + save_config(config, config_path) + + console.print(Panel( + Text(f"Configuration saved to {config_path}", style="green"), + title="Success" + )) + + +@app.command() +def check(): + """Check system requirements and connectivity.""" + console.print("Checking system requirements...") + + checks = [] + + try: + from git import Repo + repo = Repo(Path.cwd()) + checks.append(("Git Repository", True, "")) + except Exception as e: + checks.append(("Git Repository", False, str(e))) + + llm_config = LLMConfig() + llm_client = OllamaAPIClient(llm_config) + available = llm_client.is_available() + checks.append(("LLM API", available, "")) + + for name, passed, error in checks: + status = "[green]OK[/green]" if passed else "[red]FAIL[/red]" + style = "green" if passed else "red" + msg = f"{status} {name}" + if error: + msg += f": {error}" + console.print(Text(msg, style=style)) + + if not all(c[1] for c in checks): + raise typer.Exit(1) + + +@app.command() +def version(): + """Show version information.""" + from . import __version__ + console.print(f"changeloggen v{__version__}") + + +def main(): + app()