🔧 Based on the provided code snippets, the commit message for the changes in the README.md file should be:

This commit is contained in:
sankalp committed 2025-06-04 13:18:51 +05:45
1 parent 89af435abc
commit 13d0353fa9
11 files changed
+1033 -513

No files matched your search

+266 -72
View File
@@ -1,12 +1,13 @@
"""
Changelog generation CLI.
"""
import os
import click
from rich.console import Console
from rich.prompt import Prompt, Confirm
from rich.progress import Progress, SpinnerColumn, TextColumn
from .generator import ChangelogGenerator
from ..commitgen.config import CommitGenConfig
from ..shared.config import Config
console = Console()
@@ -16,96 +17,289 @@ def cli():
pass
@cli.command()
@click.option("--version", "-v", required=True, help="Version number")
@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", default="CHANGELOG.md", help="Output file path")
@click.option("--temperature", "-t", type=float, default=0.7, help="AI temperature")
def generate(version: str, output: str, temperature: float):
"""Generate a changelog interactively."""
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console
) as progress:
# Initialize services
progress.add_task("Initializing...", total=None)
config = CommitGenConfig()
generator = ChangelogGenerator(config)
@click.option("--temperature", type=float, default=0.7, help="AI temperature (0.0-1.0)")
def generate(version: str, from_tag: str, days: int, commits: int, output: str, temperature: float):
"""Generate a changelog from git history."""
try:
# Get configuration
config = Config()
# Collect changes
console.print("\n[bold]Enter your changes (one per line). Start each line with an emoji to indicate the type:[/]")
console.print("✨ Added")
console.print("🔄 Changed")
console.print("🐛 Fixed")
console.print("🚀 Performance")
console.print("📝 Documentation")
console.print("🔧 Maintenance")
console.print("🗑️ Removed")
console.print("🔒 Security")
console.print("\nPress Ctrl+D (Unix) or Ctrl+Z (Windows) when done.\n")
changes_text = []
while True:
try:
line = Prompt.ask("")
if line:
changes_text.append(line)
except EOFError:
break
if not changes_text:
console.print("[yellow]No changes provided. Exiting.")
# Validate version format
if not version.startswith("v"):
version = f"v{version}"
# Get git history
if from_tag:
# Get commits since tag
cmd = f"git log {from_tag}..HEAD --pretty=format:%s"
elif days:
# Get commits from last N days
cmd = f"git log --since='{days} days ago' --pretty=format:%s"
elif commits:
# Get last N commits
cmd = f"git log -n {commits} --pretty=format:%s"
else:
# Get all commits
cmd = "git log --pretty=format:%s"
# Execute git command
result = os.popen(cmd).read().strip()
if not result:
console.print("[yellow]No commits found in the specified range")
return
# Parse changes
progress.add_task("Parsing changes...", total=None)
changes = generator.parse_changes_from_text("\n".join(changes_text))
# Split into individual commits
commit_messages = [msg.strip() for msg in result.split("\n") if msg.strip()]
# Group commits by type
groups = {
"✨ Added": [],
"🔄 Changed": [],
"🐛 Fixed": [],
"🚀 Performance": [],
"📝 Documentation": [],
"🔧 Maintenance": [],
"🗑️ Removed": [],
"🔒 Security": []
}
# Map commit types to groups
type_map = {
"feat": "✨ Added",
"add": "✨ Added",
"new": "✨ Added",
"change": "🔄 Changed",
"update": "🔄 Changed",
"fix": "🐛 Fixed",
"bug": "🐛 Fixed",
"perf": "🚀 Performance",
"docs": "📝 Documentation",
"chore": "🔧 Maintenance",
"refactor": "🔧 Maintenance",
"remove": "🗑️ Removed",
"delete": "🗑️ Removed",
"security": "🔒 Security"
}
# Categorize commits
for msg in commit_messages:
msg_lower = msg.lower()
categorized = False
# Check for type prefixes
for type_key, group in type_map.items():
if msg_lower.startswith(type_key):
groups[group].append(msg)
categorized = True
break
# If not categorized, check for keywords
if not categorized:
for type_key, group in type_map.items():
if type_key in msg_lower:
groups[group].append(msg)
break
else:
# Default to maintenance if no match
groups["🔧 Maintenance"].append(msg)
# Generate changelog
progress.add_task("Generating changelog...", total=None)
generator.update_changelog_file(version, changes, output, temperature)
changelog = f"# Changelog\n\n## {version}\n\n"
console.print(f"\n[green]Generated changelog at {output}")
# Add each group
for group, messages in groups.items():
if messages:
changelog += f"### {group}\n\n"
for msg in messages:
changelog += f"- {msg}\n"
changelog += "\n"
# Write to file
with open(output, "w") as f:
f.write(changelog)
console.print(f"[green]✓[/green] Changelog generated successfully at: {output}")
except Exception as e:
console.print(f"[red]Error:[/red] {str(e)}")
@cli.command()
@click.option("--version", "-v", required=True, help="Version number")
@click.option("--version", "-v", required=True, help="Version number for the changelog entry")
@click.option("--output", "-o", default="CHANGELOG.md", help="Output file path")
@click.option("--temperature", type=float, default=0.7, help="AI temperature (0.0-1.0)")
def generate_interactive(version: str, output: str, temperature: float):
"""Generate a changelog interactively."""
try:
# Get configuration
config = Config()
# Validate version format
if not version.startswith("v"):
version = f"v{version}"
# Initialize groups
groups = {
"✨ Added": [],
"🔄 Changed": [],
"🐛 Fixed": [],
"🚀 Performance": [],
"📝 Documentation": [],
"🔧 Maintenance": [],
"🗑️ Removed": [],
"🔒 Security": []
}
console.print("\n[bold cyan]Enter your changes (press Enter twice to finish):[/bold cyan]")
console.print("[dim]Start each line with an emoji to categorize it:[/dim]")
console.print("✨ - Added")
console.print("🔄 - Changed")
console.print("🐛 - Fixed")
console.print("🚀 - Performance")
console.print("📝 - Documentation")
console.print("🔧 - Maintenance")
console.print("🗑️ - Removed")
console.print("🔒 - Security")
console.print("\n[dim]Example: ✨ Added new feature X[/dim]\n")
while True:
line = click.prompt("", default="", show_default=False)
if not line:
break
# Categorize based on emoji
for group in groups:
if line.startswith(group.split()[0]):
groups[group].append(line)
break
else:
# Default to maintenance if no emoji
groups["🔧 Maintenance"].append(line)
# Generate changelog
changelog = f"# Changelog\n\n## {version}\n\n"
# Add each group
for group, messages in groups.items():
if messages:
changelog += f"### {group}\n\n"
for msg in messages:
changelog += f"- {msg}\n"
changelog += "\n"
# Write to file
with open(output, "w") as f:
f.write(changelog)
console.print(f"[green]✓[/green] Changelog generated successfully at: {output}")
except Exception as e:
console.print(f"[red]Error:[/red] {str(e)}")
@cli.command()
@click.option("--version", "-v", required=True, help="Version number for the changelog entry")
@click.option("--input", "-i", required=True, help="Input file with changes")
@click.option("--output", "-o", default="CHANGELOG.md", help="Output file path")
@click.option("--temperature", "-t", type=float, default=0.7, help="AI temperature")
@click.option("--temperature", type=float, default=0.7, help="AI temperature (0.0-1.0)")
def generate_from_file(version: str, input: str, output: str, temperature: float):
"""Generate a changelog from a file."""
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console
) as progress:
# Initialize services
progress.add_task("Initializing...", total=None)
config = CommitGenConfig()
generator = ChangelogGenerator(config)
try:
# Get configuration
config = Config()
# Read changes from file
progress.add_task("Reading changes...", total=None)
try:
with open(input, "r") as f:
changes_text = f.read()
except FileNotFoundError:
console.print(f"[red]Input file not found: {input}")
return
# Validate version format
if not version.startswith("v"):
version = f"v{version}"
# Read input file
with open(input, "r") as f:
changes = f.read().strip()
# Parse changes
progress.add_task("Parsing changes...", total=None)
changes = generator.parse_changes_from_text(changes_text)
if not changes:
console.print("[yellow]No changes found in input file.")
console.print("[yellow]No changes found in input file")
return
# Generate changelog
progress.add_task("Generating changelog...", total=None)
generator.update_changelog_file(version, changes, output, temperature)
# Split into lines
lines = [line.strip() for line in changes.split("\n") if line.strip()]
console.print(f"\n[green]Generated changelog at {output}")
# Initialize groups
groups = {
"✨ Added": [],
"🔄 Changed": [],
"🐛 Fixed": [],
"🚀 Performance": [],
"📝 Documentation": [],
"🔧 Maintenance": [],
"🗑️ Removed": [],
"🔒 Security": []
}
# Categorize changes
for line in lines:
line_lower = line.lower()
categorized = False
# Check for emoji prefixes
for group in groups:
if line.startswith(group.split()[0]):
groups[group].append(line)
categorized = True
break
# If not categorized, check for keywords
if not categorized:
type_map = {
"add": "✨ Added",
"new": "✨ Added",
"change": "🔄 Changed",
"update": "🔄 Changed",
"fix": "🐛 Fixed",
"bug": "🐛 Fixed",
"perf": "🚀 Performance",
"docs": "📝 Documentation",
"chore": "🔧 Maintenance",
"refactor": "🔧 Maintenance",
"remove": "🗑️ Removed",
"delete": "🗑️ Removed",
"security": "🔒 Security"
}
for type_key, group in type_map.items():
if type_key in line_lower:
groups[group].append(line)
break
else:
# Default to maintenance if no match
groups["🔧 Maintenance"].append(line)
# Generate changelog
changelog = f"# Changelog\n\n## {version}\n\n"
# Add each group
for group, messages in groups.items():
if messages:
changelog += f"### {group}\n\n"
for msg in messages:
changelog += f"- {msg}\n"
changelog += "\n"
# Write to file
with open(output, "w") as f:
f.write(changelog)
console.print(f"[green]✓[/green] Changelog generated successfully at: {output}")
except Exception as e:
console.print(f"[red]Error:[/red] {str(e)}")
# Add command aliases
cli.add_command(generate, name="g")
cli.add_command(generate_from_file, name="f")
cli.add_command(generate_interactive, name="i")
cli.add_command(generate_from_file, name="f")
if __name__ == "__main__":
cli()
+47 -58
View File
@@ -1,57 +1,50 @@
"""
Changelog generation using AI.
"""
from typing import List, Dict, Optional
from ..shared.ai import AIService
from ..shared.config import Config
from ..shared.git import GitService
class ChangelogGenerator(AIService):
"""AI-powered changelog generator."""
def __init__(self, config: Config, git_service: Optional[GitService] = None):
"""Initialize changelog generator.
Args:
config: Configuration object
git_service: Optional GitService instance
"""
super().__init__(config)
self.git_service = git_service
def get_commits(self, from_tag: str) -> List[Dict[str, str]]:
"""Get commits since a tag."""
if not self.git_service:
raise ValueError("GitService not provided")
return self.git_service.get_commits_since_tag(from_tag)
def get_commits_since_date(self, days: int) -> List[Dict[str, str]]:
"""Get commits since a date."""
if not self.git_service:
raise ValueError("GitService not provided")
return self.git_service.get_commits_since_date(days)
def get_last_n_commits(self, n: int) -> List[Dict[str, str]]:
"""Get last N commits."""
if not self.git_service:
raise ValueError("GitService not provided")
return self.git_service.get_commit_history(limit=n)
def generate_changelog(
self,
version: str,
changes: List[Dict[str, str]],
temperature: Optional[float] = None,
) -> str:
def generate_changelog(self, version: str, changes: List[Dict[str, str]], temperature: Optional[float] = None) -> str:
"""Generate a changelog from a list of changes.
Args:
version: Version number for the changelog
changes: List of changes, each with 'message' and 'hash'
temperature: AI temperature for generation
Returns:
Generated changelog content
"""
@@ -70,27 +63,24 @@ class ChangelogGenerator(AIService):
- 🔧 Maintenance
- 🗑️ Removed
- 🔒 Security"""
# Format changes for the prompt
changes_text = "\n".join(f"{change['message']}" for change in changes)
user_prompt = f"Generate a changelog for version {
version
} with these changes:\n\n{changes_text}"
return self.generate_completion(
system_prompt, user_prompt, temperature=temperature
changes_text = "\n".join(
f"{change['message']}"
for change in changes
)
def update_changelog_file(
self,
version: str,
changes: List[Dict[str, str]],
output_file: str,
temperature: Optional[float] = None,
) -> None:
user_prompt = f"Generate a changelog for version {version} with these changes:\n\n{changes_text}"
return self.generate_completion(
system_prompt,
user_prompt,
temperature=temperature
)
def update_changelog_file(self, version: str, changes: List[Dict[str, str]], output_file: str, temperature: Optional[float] = None) -> None:
"""Generate and update a changelog file.
Args:
version: Version number for the changelog
changes: List of changes, each with 'message' and 'hash'
@@ -98,41 +88,41 @@ class ChangelogGenerator(AIService):
temperature: AI temperature for generation
"""
changelog_content = self.generate_changelog(version, changes, temperature)
# Read existing changelog if it exists
try:
with open(output_file, "r") as f:
existing_content = f.read()
except FileNotFoundError:
existing_content = ""
# Add new changelog entry at the top
new_content = f"## {version}\n\n{changelog_content}\n\n"
if existing_content:
new_content += existing_content
# Write updated changelog
with open(output_file, "w") as f:
f.write(new_content)
def parse_changes_from_text(self, text: str) -> List[Dict[str, str]]:
"""Parse changes from text input.
Args:
text: Text containing changes
Returns:
List of changes with type and description
"""
changes = []
current_type = None
current_description = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
# Check if line starts with a type marker
type_markers = {
"✨": "Added",
@@ -142,33 +132,32 @@ class ChangelogGenerator(AIService):
"📝": "Documentation",
"🔧": "Maintenance",
"🗑️": "Removed",
"🔒": "Security",
"🔒": "Security"
}
for marker, type_ in type_markers.items():
if line.startswith(marker):
# Save previous change if exists
if current_type and current_description:
changes.append(
{
"type": current_type,
"description": " ".join(current_description),
}
)
changes.append({
"type": current_type,
"description": " ".join(current_description)
})
# Start new change
current_type = type_
current_description = [line[len(marker) :].strip()]
current_description = [line[len(marker):].strip()]
break
else:
# Continue current change
if current_type:
current_description.append(line)
# Add last change if exists
if current_type and current_description:
changes.append(
{"type": current_type, "description": " ".join(current_description)}
)
return changes
changes.append({
"type": current_type,
"description": " ".join(current_description)
})
return changes