This commit is contained in:
170
app/src/git_commit_generator/changelog_generator.py
Normal file
170
app/src/git_commit_generator/changelog_generator.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
"""Changelog generation from git history."""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from git_commit_generator.config import Config, get_config
|
||||||
|
from git_commit_generator.git_utils import GitUtils, get_git_utils
|
||||||
|
from git_commit_generator.ollama_client import OllamaClient, get_ollama_client
|
||||||
|
|
||||||
|
|
||||||
|
class ChangelogGenerator:
|
||||||
|
"""Generates CHANGELOG.md from git history."""
|
||||||
|
|
||||||
|
CHANGE_TYPES = {
|
||||||
|
"feat": ("Features", "green"),
|
||||||
|
"fix": ("Bug Fixes", "red"),
|
||||||
|
"docs": ("Documentation", "blue"),
|
||||||
|
"style": ("Code Style", "gray"),
|
||||||
|
"refactor": ("Refactoring", "magenta"),
|
||||||
|
"perf": ("Performance", "yellow"),
|
||||||
|
"test": ("Tests", "cyan"),
|
||||||
|
"build": ("Build System", "white"),
|
||||||
|
"ci": ("CI/CD", "white"),
|
||||||
|
"chore": ("Maintenance", "gray"),
|
||||||
|
"revert": ("Reverts", "red"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: Optional[Config] = None,
|
||||||
|
ollama_client: Optional[OllamaClient] = None,
|
||||||
|
repo_path: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""Initialize changelog generator."""
|
||||||
|
self.config = config or get_config()
|
||||||
|
self.ollama_client = ollama_client or OllamaClient(
|
||||||
|
host=self.config.ollama_host,
|
||||||
|
model=self.config.ollama_model
|
||||||
|
)
|
||||||
|
self.git_utils = GitUtils(repo_path)
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
since: Optional[str] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
output_path: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a changelog."""
|
||||||
|
commits = self.git_utils.get_commit_history(since=since, limit=limit)
|
||||||
|
|
||||||
|
if not commits:
|
||||||
|
raise ValueError("No commits found in repository.")
|
||||||
|
|
||||||
|
prompt = self.config.read_prompt("changelog.txt")
|
||||||
|
if not prompt:
|
||||||
|
prompt = self._get_default_changelog_prompt()
|
||||||
|
|
||||||
|
commits_text = self._format_commits_for_prompt(commits)
|
||||||
|
changelog = self.ollama_client.generate_changelog(
|
||||||
|
commits=commits_text,
|
||||||
|
prompt=prompt,
|
||||||
|
model=self.config.ollama_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
changelog = self._clean_changelog(changelog)
|
||||||
|
|
||||||
|
if output_path:
|
||||||
|
self._write_changelog(output_path, changelog)
|
||||||
|
|
||||||
|
return changelog
|
||||||
|
|
||||||
|
def _get_default_changelog_prompt(self) -> str:
|
||||||
|
"""Get default changelog prompt."""
|
||||||
|
return """Generate a changelog in markdown format from the following commits.
|
||||||
|
Group by type (feat, fix, docs, etc.) and format properly."""
|
||||||
|
|
||||||
|
def _format_commits_for_prompt(self, commits: list[dict]) -> str:
|
||||||
|
"""Format commits for the LLM prompt."""
|
||||||
|
lines = []
|
||||||
|
for commit in commits:
|
||||||
|
lines.append(
|
||||||
|
f"[{commit['hash']}] {commit['message']} ({commit['author']}, {commit['date']})"
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _clean_changelog(self, changelog: str) -> str:
|
||||||
|
"""Clean and normalize changelog."""
|
||||||
|
lines = changelog.strip().split("\n")
|
||||||
|
|
||||||
|
cleaned_lines = []
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("```"):
|
||||||
|
cleaned_lines.append(line)
|
||||||
|
|
||||||
|
return "\n".join(cleaned_lines)
|
||||||
|
|
||||||
|
def _write_changelog(self, path: str, changelog: str) -> None:
|
||||||
|
"""Write changelog to file."""
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(f"# Changelog\n\nGenerated on {datetime.now().isoformat()}\n\n")
|
||||||
|
f.write(changelog)
|
||||||
|
|
||||||
|
def generate_simple(
|
||||||
|
self,
|
||||||
|
since: Optional[str] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
output_path: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a simple changelog without LLM."""
|
||||||
|
commits = self.git_utils.get_conventional_commits(since=since, limit=limit)
|
||||||
|
|
||||||
|
if not commits:
|
||||||
|
raise ValueError("No conventional commits found in repository.")
|
||||||
|
|
||||||
|
grouped = self._group_commits_by_type(commits)
|
||||||
|
|
||||||
|
changelog = self._format_simple_changelog(grouped)
|
||||||
|
|
||||||
|
if output_path:
|
||||||
|
self._write_changelog(output_path, changelog)
|
||||||
|
|
||||||
|
return changelog
|
||||||
|
|
||||||
|
def _group_commits_by_type(self, commits: list[dict]) -> dict:
|
||||||
|
"""Group commits by their type."""
|
||||||
|
grouped = {}
|
||||||
|
for commit in commits:
|
||||||
|
commit_type = commit.get("type", "chore")
|
||||||
|
if commit_type not in grouped:
|
||||||
|
grouped[commit_type] = []
|
||||||
|
grouped[commit_type].append(commit)
|
||||||
|
return grouped
|
||||||
|
|
||||||
|
def _format_simple_changelog(self, grouped: dict) -> str:
|
||||||
|
"""Format grouped commits into markdown changelog."""
|
||||||
|
lines = ["# Changelog\n"]
|
||||||
|
|
||||||
|
type_order = [
|
||||||
|
"feat", "fix", "perf", "refactor", "docs",
|
||||||
|
"style", "test", "build", "ci", "chore", "revert"
|
||||||
|
]
|
||||||
|
|
||||||
|
for commit_type in type_order:
|
||||||
|
if commit_type in grouped:
|
||||||
|
type_name, _ = self.CHANGE_TYPES.get(
|
||||||
|
commit_type, (commit_type.title(), "white")
|
||||||
|
)
|
||||||
|
lines.append(f"\n## {type_name}\n")
|
||||||
|
for commit in grouped[commit_type]:
|
||||||
|
scope = commit.get("scope", "")
|
||||||
|
description = commit.get("description", "")
|
||||||
|
if scope:
|
||||||
|
lines.append(f"- **{commit_type}({scope}):** {description}")
|
||||||
|
else:
|
||||||
|
lines.append(f"- **{commit_type}:** {description}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def get_changelog_generator(
|
||||||
|
config: Optional[Config] = None,
|
||||||
|
ollama_client: Optional[OllamaClient] = None,
|
||||||
|
repo_path: Optional[str] = None,
|
||||||
|
) -> ChangelogGenerator:
|
||||||
|
"""Get ChangelogGenerator instance."""
|
||||||
|
return ChangelogGenerator(
|
||||||
|
config=config,
|
||||||
|
ollama_client=ollama_client,
|
||||||
|
repo_path=repo_path,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user