This commit is contained in:
sankalp committed 2025-06-04 09:39:28 +05:45
1 parent 61849e7645
commit 1814fdf298
23 files changed
+2117

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
"""
Commit message generator using AI.
"""
from ..shared.config import Config
from .generator import CommitGenerator
from .git import CommitGenGitService
from ..changelog import ChangelogGenerator
__all__ = [
"Config",
"CommitGenerator",
"CommitGenGitService",
"ChangelogGenerator"
]
+247
View File
@@ -0,0 +1,247 @@
"""
Command-line interface for commitgen tool.
"""
import click
from rich.console import Console
from rich.prompt import Prompt
from rich.panel import Panel
from rich.table import Table
from rich.syntax import Syntax
from rich.progress import Progress, SpinnerColumn, TextColumn
from ..shared.config import Config
from .git import CommitGenGitService
from .generator import CommitGenerator
from ..changelog import ChangelogGenerator
from ..shared.git import GitService
import os
console = Console()
@click.group()
def cli():
"""Generate commit messages and changelogs using AI."""
pass
@cli.command()
@click.option('--files', '-f', multiple=True, help='Specific files to generate commit messages for')
@click.option('--repo', '-r', help='Repository path')
@click.option('--commit', '-c', is_flag=True, help='Automatically commit changes')
@click.option('--push', '-p', is_flag=True, help='Push changes after commit')
@click.option('--conventional/--no-conventional', default=True, help='Use conventional commit format')
@click.option('--no-stage', is_flag=True, help='Skip automatic staging of changes')
@click.option('--sign', is_flag=True, help='Sign commits with GPG')
@click.option('--temperature', type=float, help='AI temperature (0.0-1.0)')
def generate(files: tuple, repo: str, commit: bool, push: bool, conventional: bool, no_stage: bool, sign: bool, temperature: float):
"""Generate commit messages for staged changes"""
try:
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console
) as progress:
# Initialize services
config = Config()
git_service = CommitGenGitService(config)
ai_service = CommitGenerator(config)
# Stage all changes if not disabled
if not no_stage:
task = progress.add_task("Staging changes...", total=None)
git_service.stage_all_changes()
progress.update(task, completed=True)
# Get staged changes
task = progress.add_task("Analyzing changes...", total=None)
staged_changes = git_service.get_staged_changes(files)
progress.update(task, completed=True)
if not staged_changes:
console.print("[yellow]No staged changes found.[/yellow]")
return
# Generate commit message
task = progress.add_task("Generating commit message...", total=None)
commit_message = ai_service.generate_commit_message(staged_changes, temperature)
progress.update(task, completed=True)
# Show preview and confirm
console.print("\n[bold]Generated commit message:[/bold]")
console.print(Panel(commit_message, title="Preview", border_style="blue"))
if commit:
if Prompt.ask("\nApply this commit message?", choices=["y", "n"], default="y") == "y":
task = progress.add_task("Committing changes...", total=None)
git_service.commit_changes(commit_message, sign=sign)
progress.update(task, completed=True)
if push:
task = progress.add_task("Pushing changes...", total=None)
git_service.push_changes()
progress.update(task, completed=True)
console.print("[green]Changes committed successfully![/green]")
else:
console.print("[yellow]Commit cancelled.[/yellow]")
else:
console.print("[yellow]Use --commit to apply the commit message.[/yellow]")
except Exception as e:
console.print(f"[red]Error: {str(e)}[/red]")
raise click.Abort()
@cli.group()
def changelog():
"""Generate and manage changelogs"""
pass
@changelog.command(name='generate')
@click.option('--version', '-v', required=True, help='Version number for the changelog entry')
@click.option('--from-tag', '-t', help='Generate changelog from this tag')
@click.option('--days', '-d', type=int, help='Generate changelog from the last N days')
@click.option('--commits', '-n', type=int, help='Generate changelog from the last N commits')
@click.option('--output', '-o', help='Output file path (default: CHANGELOG.md)')
@click.option('--temperature', type=float, help='AI temperature (0.0-1.0)')
def generate_log(version: str, from_tag: str, days: int, commits: int, output: str, temperature: float):
"""Generate a changelog from git history"""
try:
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console
) as progress:
# Initialize services
config = Config()
git_service = CommitGenGitService(config)
changelog_gen = ChangelogGenerator(config, git_service)
task = progress.add_task("Analyzing commit history...", total=None)
# Determine which method to use for getting commits
if from_tag:
entries = changelog_gen.get_commits(from_tag)
elif days:
entries = changelog_gen.get_commits_since_date(days)
elif commits:
entries = changelog_gen.get_last_n_commits(commits)
else:
# Default: get commits from the last 7 days
entries = changelog_gen.get_commits_since_date(7)
progress.update(task, completed=True)
if not entries:
console.print("[yellow]No conventional commits found in the specified range.[/yellow]")
return
task = progress.add_task("Generating changelog...", total=None)
output_path = output if output else "CHANGELOG.md"
changelog_gen.update_changelog_file(version, entries, output_path)
progress.update(task, completed=True)
console.print(f"[green]Changelog generated successfully at {output_path}![/green]")
except Exception as e:
console.print(f"[red]Error: {str(e)}[/red]")
raise click.Abort()
@cli.group()
def config():
"""Manage configuration"""
pass
@config.command()
@click.option('--key', '-k', required=True, help='Configuration key')
@click.option('--value', '-v', required=True, help='Configuration value')
def set(key: str, value: str):
"""Set configuration value"""
try:
config = Config()
config.set(key, value)
console.print(f"[green]Configuration updated: {key}={value}[/green]")
except Exception as e:
console.print(f"[red]Error: {str(e)}[/red]")
raise click.Abort()
@config.command()
def show():
"""Show current configuration"""
try:
config = Config()
console.print(Panel(
"\n".join(f"{k}: {v}" for k, v in config.get_all().items()),
title="[blue]Current Configuration[/blue]",
border_style="blue"
))
except Exception as e:
console.print(f"[red]Error: {str(e)}[/red]")
raise click.Abort()
@cli.command()
def status():
"""Show the current state of the repository."""
try:
# Initialize services
git = GitService(os.getcwd())
# Get repository info
repo_name = git.get_repo_name()
current_branch = git.get_current_branch()
# Get changes
staged_files = git.get_staged_files()
unstaged_files = git.get_unstaged_files()
# Create status table
table = Table(title=f"Repository Status: {repo_name} ({current_branch})")
table.add_column("Status", style="cyan")
table.add_column("Files", style="green")
# Add staged changes
if staged_files:
staged_content = "\n".join(f" • {file}" for file in staged_files)
table.add_row("Staged", staged_content)
else:
table.add_row("Staged", "No staged changes")
# Add unstaged changes
if unstaged_files:
unstaged_content = "\n".join(f" • {file}" for file in unstaged_files)
table.add_row("Unstaged", unstaged_content)
else:
table.add_row("Unstaged", "No unstaged changes")
# Show status
console.print("\n[bold]Repository Status:[/bold]")
console.print(table)
# Show diffs if there are changes
if staged_files or unstaged_files:
console.print("\n[bold]Changes:[/bold]")
# Show staged diffs
if staged_files:
console.print("\n[cyan]Staged Changes:[/cyan]")
for file in staged_files:
diff = git.get_diff(file, staged=True)
if diff:
console.print(Syntax(diff, "diff", theme="monokai"))
# Show unstaged diffs
if unstaged_files:
console.print("\n[yellow]Unstaged Changes:[/yellow]")
for file in unstaged_files:
diff = git.get_diff(file, staged=False)
if diff:
console.print(Syntax(diff, "diff", theme="monokai"))
except Exception as e:
console.print(f"[red]Error: {str(e)}[/red]")
raise click.Abort()
# Add command aliases
cli.add_command(generate, name='g')
cli.add_command(config, name='c')
cli.add_command(changelog, name='log')
if __name__ == "__main__":
cli()
+79
View File
@@ -0,0 +1,79 @@
"""
Commit message and changelog generation using AI.
"""
from typing import List, Dict, Optional
from ..shared.ai import AIService
from ..shared.config import Config
class CommitGenerator(AIService):
"""AI-powered commit message and changelog generator."""
def __init__(self, config: Config):
"""Initialize commit generator."""
super().__init__(config)
def generate_commit_message(self, diff: str, temperature: Optional[float] = None) -> str:
"""Generate a commit message from a diff."""
system_prompt = """You are a helpful AI that generates git commit messages. Keep responses concise and follow the conventional commit format.
Format: type(scope): description
Types and their emojis:
- feat: ✨ (new feature)
- fix: 🐛 (bug fix)
- docs: 📚 (documentation)
- style: 💅 (formatting, missing semi colons, etc)
- refactor: ♻️ (refactoring code)
- test: ✅ (adding tests)
- chore: 🔧 (maintenance)
The message should be a single line and follow the conventional commit format strictly. Include the appropriate emoji at the start of the message."""
user_prompt = f"Write a conventional commit message for these changes:\n\n{diff}"
message = self.generate_completion(
system_prompt,
user_prompt,
temperature=temperature
)
# Clean up the response to ensure it's a valid commit message
message = message.split('\n')[0].strip() # Take only the first line
# Ensure the message starts with an emoji
if not any(emoji in message for emoji in ['✨', '🐛', '📚', '💅', '♻️', '✅', '🔧']):
# Add appropriate emoji based on the commit type
if message.startswith('feat'):
message = '✨ ' + message
elif message.startswith('fix'):
message = '🐛 ' + message
elif message.startswith('docs'):
message = '📚 ' + message
elif message.startswith('style'):
message = '💅 ' + message
elif message.startswith('refactor'):
message = '♻️ ' + message
elif message.startswith('test'):
message = '✅ ' + message
elif message.startswith('chore'):
message = '🔧 ' + message
else:
message = '🔧 ' + message # Default to chore
return message
def generate_changelog(self, commits: List[str], version: str, temperature: Optional[float] = None) -> str:
"""Generate a changelog from a list of commits."""
system_prompt = """You are a helpful AI that generates changelogs.
Follow these rules:
1. Group changes by type (Added, Changed, Fixed, etc.)
2. Keep descriptions concise but informative
3. Use past tense
4. Start each entry with a verb"""
user_prompt = f"Generate a changelog for version {version} with these commits:\n\n" + "\n".join(commits)
return self.generate_completion(
system_prompt,
user_prompt,
temperature=temperature
)
+41
View File
@@ -0,0 +1,41 @@
"""
Git service for commit_gen tool.
"""
from typing import List, Optional
from ..shared.git import GitService
from ..shared.config import Config
import os
class CommitGenGitService(GitService):
"""Git service for commit_gen tool."""
def __init__(self, config: Config):
"""Initialize commit_gen git service."""
super().__init__(os.getcwd()) # Use current directory as repo path
self.config = config
def get_staged_changes(self, files: Optional[List[str]] = None) -> str:
"""Get staged changes as a diff.
Args:
files: Optional list of files to get changes for
"""
diffs = self.get_all_diffs(staged=True)
if files:
diffs = {file: diff for file, diff in diffs.items() if file in files}
return "\n\n".join(f"Changes in {file}:\n{diff}" for file, diff in diffs.items())
def get_unstaged_changes(self) -> str:
"""Get unstaged changes as a diff."""
diffs = self.get_all_diffs(staged=False)
return "\n\n".join(f"Changes in {file}:\n{diff}" for file, diff in diffs.items())
def commit_changes(self, message: str, sign: bool = False) -> None:
"""Commit changes with the given message."""
if not self.commit(message, sign=sign):
raise Exception("Failed to commit changes")
def get_recent_commits(self, limit: int = 10) -> List[str]:
"""Get recent commit messages."""
commits = self.get_commit_history(limit=limit)
return [commit["message"] for commit in commits]