Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb35578055 | |||
| 853b5d757e | |||
| bb42985efb | |||
| f723146e51 | |||
| df72b5e819 | |||
| 75e8ed4bb3 | |||
| d25a697ce9 | |||
| bae3d509c3 | |||
| f6e938c2de | |||
| 2b3b3e8a2e | |||
| fef0ba7d4c | |||
| 9f604d31f7 | |||
| 6dfa82e092 | |||
| 1b7705cf1a | |||
| e1c18d0451 | |||
| 7e5b37eb71 | |||
| ecb169e003 | |||
| 02e93f863a | |||
| 180cacec79 | |||
| 39e8277504 | |||
| 853b738b4b | |||
| aabb84cdcf | |||
| e5447c8296 | |||
| 52041d975e | |||
| b74ff8a624 | |||
| 3de0498d5e | |||
| edd15de989 | |||
| 1ebc45808e | |||
| cf6a1a838b | |||
| 1b14e0f6cd | |||
| 68698fa359 | |||
| b903f80399 | |||
| 3d1c1d11ac | |||
| 99ca60c7b4 | |||
| b86f66a574 | |||
| 4a8be31b24 | |||
| 49f8c47c69 | |||
| 2f9148bbbe | |||
| eeb016d39d | |||
| 7d4a7ab61c | |||
| 53dfd70fc8 |
@@ -14,11 +14,11 @@ jobs:
|
|||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
- name: Install dependencies
|
- name: Run tests and linting
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
cd app
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
- name: Run tests
|
python -m pytest tests/ -v --cov=src --cov-report=term-missing
|
||||||
run: pytest tests/ -v --cov=src --cov-report=term-missing
|
pip install ruff
|
||||||
- name: Check linting
|
ruff check src/
|
||||||
run: pip install ruff && ruff check .
|
|
||||||
|
|||||||
6
app/ruff.toml
Normal file
6
app/ruff.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[lint]
|
||||||
|
extend-ignore = ["F821"]
|
||||||
|
exclude = [
|
||||||
|
"__pycache__",
|
||||||
|
"*.egg-info",
|
||||||
|
]
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
"""Changelog generation from git history."""
|
"""Changelog generation from git history."""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from git_commit_generator.config import Config, get_config
|
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
|
||||||
from git_commit_generator.ollama_client import OllamaClient, get_ollama_client
|
from git_commit_generator.git_utils import GitUtils
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ChangelogGenerator:
|
class ChangelogGenerator:
|
||||||
@@ -28,6 +31,7 @@ class ChangelogGenerator:
|
|||||||
self,
|
self,
|
||||||
config: Optional[Config] = None,
|
config: Optional[Config] = None,
|
||||||
ollama_client: Optional[OllamaClient] = None,
|
ollama_client: Optional[OllamaClient] = None,
|
||||||
|
git_utils: Optional[GitUtils] = None,
|
||||||
repo_path: Optional[str] = None,
|
repo_path: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Initialize changelog generator."""
|
"""Initialize changelog generator."""
|
||||||
@@ -36,7 +40,7 @@ class ChangelogGenerator:
|
|||||||
host=self.config.ollama_host,
|
host=self.config.ollama_host,
|
||||||
model=self.config.ollama_model
|
model=self.config.ollama_model
|
||||||
)
|
)
|
||||||
self.git_utils = GitUtils(repo_path)
|
self.git_utils = git_utils or GitUtils(repo_path)
|
||||||
|
|
||||||
def generate(
|
def generate(
|
||||||
self,
|
self,
|
||||||
@@ -160,11 +164,13 @@ Group by type (feat, fix, docs, etc.) and format properly."""
|
|||||||
def get_changelog_generator(
|
def get_changelog_generator(
|
||||||
config: Optional[Config] = None,
|
config: Optional[Config] = None,
|
||||||
ollama_client: Optional[OllamaClient] = None,
|
ollama_client: Optional[OllamaClient] = None,
|
||||||
|
git_utils: Optional[GitUtils] = None,
|
||||||
repo_path: Optional[str] = None,
|
repo_path: Optional[str] = None,
|
||||||
) -> ChangelogGenerator:
|
) -> ChangelogGenerator:
|
||||||
"""Get ChangelogGenerator instance."""
|
"""Get ChangelogGenerator instance."""
|
||||||
return ChangelogGenerator(
|
return ChangelogGenerator(
|
||||||
config=config,
|
config=config,
|
||||||
ollama_client=ollama_client,
|
ollama_client=ollama_client,
|
||||||
|
git_utils=git_utils,
|
||||||
repo_path=repo_path,
|
repo_path=repo_path,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ from typing import Optional
|
|||||||
import click
|
import click
|
||||||
from rich import print as rprint
|
from rich import print as rprint
|
||||||
|
|
||||||
from git_commit_generator.changelog_generator import ChangelogGenerator, get_changelog_generator
|
from git_commit_generator.changelog_generator import get_changelog_generator
|
||||||
from git_commit_generator.config import Config, get_config
|
from git_commit_generator.config import Config, get_config
|
||||||
from git_commit_generator.git_utils import GitUtils, get_git_utils
|
from git_commit_generator.git_utils import get_git_utils
|
||||||
from git_commit_generator.interactive import Action, InteractiveMode, get_interactive_mode
|
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.message_generator import MessageGenerator, get_message_generator
|
||||||
from git_commit_generator.ollama_client import OllamaClient, get_ollama_client
|
from git_commit_generator.ollama_client import get_ollama_client
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@@ -97,7 +97,7 @@ def generate(
|
|||||||
unstaged=not staged,
|
unstaged=not staged,
|
||||||
model=model,
|
model=model,
|
||||||
)
|
)
|
||||||
rprint(f"\n[bold green]Generated commit message:[/bold green]")
|
rprint("\n[bold green]Generated commit message:[/bold green]")
|
||||||
rprint(f"[cyan]{message}[/cyan]")
|
rprint(f"[cyan]{message}[/cyan]")
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
@@ -146,7 +146,7 @@ def _run_interactive_mode(
|
|||||||
elif action == Action.ACCEPT:
|
elif action == Action.ACCEPT:
|
||||||
final_message = edited_message or message
|
final_message = edited_message or message
|
||||||
if interactive_mode.confirm_commit(final_message):
|
if interactive_mode.confirm_commit(final_message):
|
||||||
rprint(f"\n[bold]Commit message:[/bold]")
|
rprint("\n[bold]Commit message:[/bold]")
|
||||||
rprint(f"[green]{final_message}[/green]")
|
rprint(f"[green]{final_message}[/green]")
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
@@ -308,7 +308,7 @@ def status(ctx: click.Context) -> None:
|
|||||||
|
|
||||||
rprint("[bold]Git Commit Message Generator Status[/bold]\n")
|
rprint("[bold]Git Commit Message Generator Status[/bold]\n")
|
||||||
|
|
||||||
rprint(f"[bold]Configuration:[/bold]")
|
rprint("[bold]Configuration:[/bold]")
|
||||||
rprint(f" Ollama Host: {host}")
|
rprint(f" Ollama Host: {host}")
|
||||||
rprint(f" Default Model: {model}")
|
rprint(f" Default Model: {model}")
|
||||||
rprint(f" Prompt Directory: {config.prompt_dir}")
|
rprint(f" Prompt Directory: {config.prompt_dir}")
|
||||||
@@ -320,7 +320,7 @@ def status(ctx: click.Context) -> None:
|
|||||||
|
|
||||||
if connected:
|
if connected:
|
||||||
models = ollama_client.list_models()
|
models = ollama_client.list_models()
|
||||||
rprint(f"\n[bold]Available Models:[/bold]")
|
rprint("\n[bold]Available Models:[/bold]")
|
||||||
if models:
|
if models:
|
||||||
for m in models[:5]:
|
for m in models[:5]:
|
||||||
rprint(f" - {m.get('name', 'unknown')}")
|
rprint(f" - {m.get('name', 'unknown')}")
|
||||||
@@ -335,7 +335,7 @@ def status(ctx: click.Context) -> None:
|
|||||||
try:
|
try:
|
||||||
git_utils = get_git_utils()
|
git_utils = get_git_utils()
|
||||||
is_repo = git_utils.is_repo()
|
is_repo = git_utils.is_repo()
|
||||||
rprint(f"\n[bold]Git Repository:[/bold]")
|
rprint("\n[bold]Git Repository:[/bold]")
|
||||||
rprint(f" Repository Detected: {'Yes' if is_repo else 'No'}")
|
rprint(f" Repository Detected: {'Yes' if is_repo else 'No'}")
|
||||||
except Exception:
|
except Exception:
|
||||||
rprint("\n[bold]Git Repository:[/bold]")
|
rprint("\n[bold]Git Repository:[/bold]")
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class InteractiveMode:
|
|||||||
|
|
||||||
def confirm_commit(self, message: str) -> bool:
|
def confirm_commit(self, message: str) -> bool:
|
||||||
"""Confirm the commit message before proceeding."""
|
"""Confirm the commit message before proceeding."""
|
||||||
self.console.print(f"\n[bold]Final commit message:[/bold]")
|
self.console.print("\n[bold]Final commit message:[/bold]")
|
||||||
self.console.print(f"[green]{message}[/green]")
|
self.console.print(f"[green]{message}[/green]")
|
||||||
|
|
||||||
confirm = Prompt.ask(
|
confirm = Prompt.ask(
|
||||||
@@ -85,7 +85,7 @@ class InteractiveMode:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Show Ollama connection status."""
|
"""Show Ollama connection status."""
|
||||||
if connected:
|
if connected:
|
||||||
status = f"[green]Connected to Ollama[/green]"
|
status = "[green]Connected to Ollama[/green]"
|
||||||
if model:
|
if model:
|
||||||
status += f" (model: {model})"
|
status += f" (model: {model})"
|
||||||
self.console.print(status)
|
self.console.print(status)
|
||||||
|
|||||||
208
app/src/git_commit_generator/message_generator.py
Normal file
208
app/src/git_commit_generator/message_generator.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""Message generator for commit messages."""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
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 MessageGenerator:
|
||||||
|
"""Generates conventional commit messages using LLM."""
|
||||||
|
|
||||||
|
CHANGE_TYPE_INDICATORS = {
|
||||||
|
"test": ["+def test_", "+class Test", "test_", "assert ", "pytest", "+ assert "],
|
||||||
|
"feat": ["+def ", "+class ", "+async def ", "new feature", "add "],
|
||||||
|
"fix": ["-def ", "bug", "fix", "resolve", "error", "issue"],
|
||||||
|
"docs": ["#", "doc", "readme", "documentation"],
|
||||||
|
"refactor": ["refactor", "rename", "reorganize"],
|
||||||
|
"chore": ["chore", "dependency", "config"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, config: Config, ollama_client: OllamaClient, git_utils: GitUtils | None = None):
|
||||||
|
"""Initialize message generator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration object.
|
||||||
|
ollama_client: Ollama client instance.
|
||||||
|
git_utils: Git utils instance (optional).
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
self.ollama_client = ollama_client
|
||||||
|
self.git_utils = git_utils or get_git_utils()
|
||||||
|
|
||||||
|
def detect_change_type(self, diff: str) -> str:
|
||||||
|
"""Detect the type of change from diff.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diff: Git diff content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Change type (feat, fix, docs, test, refactor, chore).
|
||||||
|
"""
|
||||||
|
diff_lower = diff.lower()
|
||||||
|
|
||||||
|
for change_type, indicators in self.CHANGE_TYPE_INDICATORS.items():
|
||||||
|
for indicator in indicators:
|
||||||
|
if indicator.lower() in diff_lower:
|
||||||
|
return change_type
|
||||||
|
|
||||||
|
return "feat"
|
||||||
|
|
||||||
|
def detect_scope(self, files: list[str]) -> str:
|
||||||
|
"""Detect the scope from file paths.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: List of file paths.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Scope name (directory or 'core' for root files).
|
||||||
|
"""
|
||||||
|
if not files:
|
||||||
|
return "core"
|
||||||
|
|
||||||
|
scopes: dict[str, int] = {}
|
||||||
|
|
||||||
|
for file_path in files:
|
||||||
|
parts = file_path.split("/")
|
||||||
|
if len(parts) > 1:
|
||||||
|
scope = parts[0]
|
||||||
|
scopes[scope] = scopes.get(scope, 0) + 1
|
||||||
|
else:
|
||||||
|
scopes["core"] = scopes.get("core", 0) + 1
|
||||||
|
|
||||||
|
if not scopes:
|
||||||
|
return "core"
|
||||||
|
|
||||||
|
return max(scopes.items(), key=lambda x: x[1])[0]
|
||||||
|
|
||||||
|
def parse_conventional_message(self, message: str) -> dict[str, Any]:
|
||||||
|
"""Parse a conventional commit message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Commit message to parse.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with type, scope, description, and full_message.
|
||||||
|
"""
|
||||||
|
message = self._clean_message(message)
|
||||||
|
|
||||||
|
if ": " in message:
|
||||||
|
parts = message.split(": ", 1)
|
||||||
|
type_scope = parts[0]
|
||||||
|
description = parts[1] if len(parts) > 1 else ""
|
||||||
|
elif ": " not in message and " " in message:
|
||||||
|
type_scope = message.split(" ")[0]
|
||||||
|
description = message[len(type_scope) + 1:] if len(message) > len(type_scope) + 1 else ""
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"type": "feat",
|
||||||
|
"scope": "",
|
||||||
|
"description": message,
|
||||||
|
"full_message": message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "(" in type_scope and ")" in type_scope:
|
||||||
|
scope_start = type_scope.find("(")
|
||||||
|
scope_end = type_scope.find(")")
|
||||||
|
change_type = type_scope[:scope_start]
|
||||||
|
scope = type_scope[scope_start + 1:scope_end]
|
||||||
|
else:
|
||||||
|
change_type = type_scope
|
||||||
|
scope = ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": change_type,
|
||||||
|
"scope": scope,
|
||||||
|
"description": description,
|
||||||
|
"full_message": message,
|
||||||
|
}
|
||||||
|
|
||||||
|
def format_conventional_message(self, message_type: str, scope: str, description: str) -> str:
|
||||||
|
"""Format a conventional commit message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_type: Type of change (feat, fix, etc.).
|
||||||
|
scope: Scope of the change.
|
||||||
|
description: Description of the change.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted commit message.
|
||||||
|
"""
|
||||||
|
if scope:
|
||||||
|
return f"{message_type}({scope}): {description}"
|
||||||
|
return f"{message_type}: {description}"
|
||||||
|
|
||||||
|
def _clean_message(self, message: str) -> str:
|
||||||
|
"""Clean a commit message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Raw message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned message.
|
||||||
|
"""
|
||||||
|
message = message.strip()
|
||||||
|
if (message.startswith('"') and message.endswith('"')) or (
|
||||||
|
message.startswith("'") and message.endswith("'")
|
||||||
|
):
|
||||||
|
message = message[1:-1]
|
||||||
|
return message.strip()
|
||||||
|
|
||||||
|
def generate(self) -> str:
|
||||||
|
"""Generate a commit message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated commit message.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no changes are detected.
|
||||||
|
"""
|
||||||
|
diff = self.git_utils.get_all_changes()
|
||||||
|
|
||||||
|
if not diff:
|
||||||
|
raise ValueError("No changes detected. Stage some changes first.")
|
||||||
|
|
||||||
|
change_type = self.detect_change_type(diff)
|
||||||
|
files = self.git_utils.get_staged_files()
|
||||||
|
scope = self.detect_scope(files) if files else "core"
|
||||||
|
|
||||||
|
prompt = self.config.read_prompt("commit_message.txt")
|
||||||
|
|
||||||
|
raw_message = self.ollama_client.generate_commit_message(diff, prompt)
|
||||||
|
message = self._clean_message(raw_message)
|
||||||
|
|
||||||
|
parsed = self.parse_conventional_message(message)
|
||||||
|
parsed["type"] = change_type
|
||||||
|
parsed["scope"] = scope
|
||||||
|
|
||||||
|
formatted = self.format_conventional_message(
|
||||||
|
message_type=parsed["type"],
|
||||||
|
scope=parsed["scope"],
|
||||||
|
description=parsed["description"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
|
||||||
|
def get_message_generator(
|
||||||
|
config: Config | None = None,
|
||||||
|
ollama_client: OllamaClient | None = None,
|
||||||
|
git_utils: GitUtils | None = None,
|
||||||
|
) -> MessageGenerator:
|
||||||
|
"""Get MessageGenerator instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration object (optional).
|
||||||
|
ollama_client: Ollama client (optional).
|
||||||
|
git_utils: Git utils (optional).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MessageGenerator instance.
|
||||||
|
"""
|
||||||
|
config = config or get_config()
|
||||||
|
ollama_client = ollama_client or get_ollama_client()
|
||||||
|
return MessageGenerator(
|
||||||
|
config=config,
|
||||||
|
ollama_client=ollama_client,
|
||||||
|
git_utils=git_utils,
|
||||||
|
)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Ollama client wrapper for LLM interactions."""
|
"""Ollama client wrapper for LLM interactions."""
|
||||||
import ollama as ollama_lib
|
import ollama as ollama_lib
|
||||||
from ollama import ChatResponse, ListResponse
|
|
||||||
|
|
||||||
|
|
||||||
class OllamaClient:
|
class OllamaClient:
|
||||||
@@ -24,7 +23,7 @@ class OllamaClient:
|
|||||||
True if connection successful, False otherwise.
|
True if connection successful, False otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response: ListResponse = ollama_lib.list()
|
ollama_lib.list()
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
@@ -39,9 +38,10 @@ class OllamaClient:
|
|||||||
True if model is available, False otherwise.
|
True if model is available, False otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response: ListResponse = ollama_lib.list()
|
list_result = ollama_lib.list()
|
||||||
|
models = list_result.get("models", [])
|
||||||
return any(m["name"] == model or m["name"].startswith(model)
|
return any(m["name"] == model or m["name"].startswith(model)
|
||||||
for m in response.get("models", []))
|
for m in models)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class OllamaClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Generated commit message.
|
Generated commit message.
|
||||||
"""
|
"""
|
||||||
response: ChatResponse = ollama_lib.chat(
|
response = ollama_lib.chat(
|
||||||
model=model or self.model,
|
model=model or self.model,
|
||||||
messages=[
|
messages=[
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@ class OllamaClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Generated changelog.
|
Generated changelog.
|
||||||
"""
|
"""
|
||||||
response: ChatResponse = ollama_lib.chat(
|
response = ollama_lib.chat(
|
||||||
model=model or self.model,
|
model=model or self.model,
|
||||||
messages=[
|
messages=[
|
||||||
{
|
{
|
||||||
@@ -110,8 +110,8 @@ class OllamaClient:
|
|||||||
List of available models with their details.
|
List of available models with their details.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response: ListResponse = ollama_lib.list()
|
list_result = ollama_lib.list()
|
||||||
return response.get("models", [])
|
return list_result.get("models", [])
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ class TestChangelogGenerator:
|
|||||||
def changelog_generator(self):
|
def changelog_generator(self):
|
||||||
"""Create a ChangelogGenerator instance with mocked dependencies."""
|
"""Create a ChangelogGenerator instance with mocked dependencies."""
|
||||||
with patch("git_commit_generator.changelog_generator.get_config") as mock_config:
|
with patch("git_commit_generator.changelog_generator.get_config") as mock_config:
|
||||||
with patch("git_commit_generator.changelog_generator.get_ollama_client") as mock_ollama:
|
|
||||||
config = MagicMock()
|
config = MagicMock()
|
||||||
config.ollama_host = "http://localhost:11434"
|
config.ollama_host = "http://localhost:11434"
|
||||||
config.ollama_model = "llama3"
|
config.ollama_model = "llama3"
|
||||||
@@ -22,18 +21,15 @@ class TestChangelogGenerator:
|
|||||||
|
|
||||||
ollama_client = MagicMock()
|
ollama_client = MagicMock()
|
||||||
ollama_client.generate_changelog.return_value = "# Changelog\n\n## Features\n- New feature"
|
ollama_client.generate_changelog.return_value = "# Changelog\n\n## Features\n- New feature"
|
||||||
mock_ollama.return_value = ollama_client
|
|
||||||
|
|
||||||
with patch("git_commit_generator.changelog_generator.get_git_utils") as mock_git:
|
|
||||||
git_utils = MagicMock()
|
git_utils = MagicMock()
|
||||||
git_utils.get_commit_history.return_value = []
|
git_utils.get_commit_history.return_value = []
|
||||||
mock_git.return_value = git_utils
|
|
||||||
|
|
||||||
generator = ChangelogGenerator(
|
generator = ChangelogGenerator(
|
||||||
config=config,
|
config=config,
|
||||||
ollama_client=ollama_client,
|
ollama_client=ollama_client,
|
||||||
|
git_utils=git_utils,
|
||||||
)
|
)
|
||||||
generator.git_utils = git_utils
|
|
||||||
yield generator
|
yield generator
|
||||||
|
|
||||||
def test_group_commits_by_type(self, changelog_generator):
|
def test_group_commits_by_type(self, changelog_generator):
|
||||||
@@ -52,10 +48,11 @@ class TestChangelogGenerator:
|
|||||||
assert len(result["fix"]) == 1
|
assert len(result["fix"]) == 1
|
||||||
|
|
||||||
def test_format_simple_changelog(self, changelog_generator):
|
def test_format_simple_changelog(self, changelog_generator):
|
||||||
"""Test formatting simple changelog."""
|
"""Test simple changelog formatting."""
|
||||||
grouped = {
|
grouped = {
|
||||||
"feat": [
|
"feat": [
|
||||||
{"type": "feat", "scope": "api", "description": "add endpoint"},
|
{"type": "feat", "scope": "api", "description": "add endpoint"},
|
||||||
|
{"type": "feat", "scope": "ui", "description": "add button"},
|
||||||
],
|
],
|
||||||
"fix": [
|
"fix": [
|
||||||
{"type": "fix", "scope": "db", "description": "fix bug"},
|
{"type": "fix", "scope": "db", "description": "fix bug"},
|
||||||
@@ -68,48 +65,50 @@ class TestChangelogGenerator:
|
|||||||
assert "## Features" in result
|
assert "## Features" in result
|
||||||
assert "## Bug Fixes" in result
|
assert "## Bug Fixes" in result
|
||||||
assert "**feat(api):** add endpoint" in result
|
assert "**feat(api):** add endpoint" in result
|
||||||
|
assert "**feat(ui):** add button" in result
|
||||||
assert "**fix(db):** fix bug" in result
|
assert "**fix(db):** fix bug" in result
|
||||||
|
|
||||||
def test_generate_simple_no_commits_raises_error(self, changelog_generator):
|
def test_generate_simple_no_commits_raises_error(self, changelog_generator):
|
||||||
"""Test that simple generation raises error with no commits."""
|
"""Test that generate_simple raises error when no commits."""
|
||||||
changelog_generator.git_utils.get_conventional_commits.return_value = []
|
changelog_generator.git_utils.get_conventional_commits.return_value = []
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="No conventional commits found"):
|
with pytest.raises(ValueError, match="No conventional commits"):
|
||||||
changelog_generator.generate_simple()
|
changelog_generator.generate_simple()
|
||||||
|
|
||||||
def test_format_commits_for_prompt(self, changelog_generator):
|
def test_format_commits_for_prompt(self, changelog_generator):
|
||||||
"""Test formatting commits for LLM prompt."""
|
"""Test commit formatting for LLM prompt."""
|
||||||
commits = [
|
commits = [
|
||||||
{
|
{"hash": "abc123", "message": "feat: add feature", "author": "John", "date": "2024-01-01"},
|
||||||
"hash": "abc1234",
|
{"hash": "def456", "message": "fix: fix bug", "author": "Jane", "date": "2024-01-02"},
|
||||||
"message": "feat: add feature",
|
|
||||||
"author": "Test User",
|
|
||||||
"date": "2024-01-15T10:30:00",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
result = changelog_generator._format_commits_for_prompt(commits)
|
result = changelog_generator._format_commits_for_prompt(commits)
|
||||||
|
|
||||||
assert "[abc1234] feat: add feature" in result
|
assert "[abc123] feat: add feature" in result
|
||||||
assert "Test User" in result
|
assert "[def456] fix: fix bug" in result
|
||||||
|
assert "John" in result
|
||||||
|
assert "2024-01-01" in result
|
||||||
|
|
||||||
def test_clean_changelog(self, changelog_generator):
|
def test_clean_changelog(self, changelog_generator):
|
||||||
"""Test cleaning changelog output."""
|
"""Test changelog cleaning."""
|
||||||
raw_changelog = """```
|
raw = """
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Feature 1
|
- New feature
|
||||||
```"""
|
|
||||||
|
|
||||||
result = changelog_generator._clean_changelog(raw_changelog)
|
```python
|
||||||
|
print("hello")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
result = changelog_generator._clean_changelog(raw)
|
||||||
|
|
||||||
assert "```" not in result
|
assert "```python" not in result
|
||||||
assert "# Changelog" in result
|
assert "# Changelog" in result
|
||||||
assert "## Features" in result
|
assert "## Features" in result
|
||||||
|
|
||||||
def test_generate_no_commits_raises_error(self, changelog_generator):
|
def test_generate_no_commits_raises_error(self, changelog_generator):
|
||||||
"""Test that generation raises error with no commits."""
|
"""Test that generate raises error when no commits."""
|
||||||
changelog_generator.git_utils.get_commit_history.return_value = []
|
changelog_generator.git_utils.get_commit_history.return_value = []
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="No commits found"):
|
with pytest.raises(ValueError, match="No commits found"):
|
||||||
|
|||||||
Reference in New Issue
Block a user