Fix CI issues: indentation error in ollama_client.py, add missing message_generator.py, fix changelog_generator.py
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
- Fixed indentation error in ollama_client.py (extra space before docstring) - Created missing message_generator.py module - Fixed ChangelogGenerator to accept git_utils parameter - Updated message_generator change type detection to prioritize test indicator - Fixed test fixtures to properly pass mocked dependencies
This commit is contained in:
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,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user