🔧
This commit is contained in:
3 files changed
+140
-80
No files matched your search
@@ -1,50 +1,68 @@
|
||||
"""
|
||||
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
|
||||
import re
|
||||
|
||||
|
||||
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 _extract_pr_reference(self, message: str) -> Optional[str]:
|
||||
"""Extract PR reference from commit message if it exists."""
|
||||
# Look for patterns like (#123) or (#123, #456)
|
||||
pr_pattern = r"\(#(\d+)(?:,\s*#\d+)*\)"
|
||||
match = re.search(pr_pattern, message)
|
||||
if match:
|
||||
# Return the full match including parentheses
|
||||
return match.group(0)
|
||||
return None
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -62,25 +80,30 @@ class ChangelogGenerator(AIService):
|
||||
- 📝 Documentation
|
||||
- 🔧 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}"
|
||||
|
||||
- 🔒 Security
|
||||
6. Only include PR references if they exist in the original commit message
|
||||
7. Keep the changelog clean and professional"""
|
||||
|
||||
# Format changes for the prompt, preserving PR references
|
||||
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
|
||||
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:
|
||||
|
||||
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'
|
||||
@@ -88,41 +111,55 @@ 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"
|
||||
|
||||
# Create new version entry
|
||||
new_version_entry = f"## {version}\n\n{changelog_content}\n\n"
|
||||
|
||||
if existing_content:
|
||||
new_content += existing_content
|
||||
|
||||
# Check if version already exists
|
||||
version_pattern = f"## {version}\n"
|
||||
if version_pattern in existing_content:
|
||||
# Replace existing version content
|
||||
pattern = f"{version_pattern}.*?(?=## |$)"
|
||||
new_content = re.sub(
|
||||
pattern, new_version_entry, existing_content, flags=re.DOTALL
|
||||
)
|
||||
else:
|
||||
# Add new version at the top
|
||||
new_content = new_version_entry + existing_content
|
||||
else:
|
||||
# Create new changelog
|
||||
new_content = f"# Changelog\n\n{new_version_entry}"
|
||||
|
||||
# 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",
|
||||
@@ -132,32 +169,33 @@ 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
|
||||
@@ -20,12 +20,12 @@ class CommitGenerator(AIService):
|
||||
"""Generate a commit message from a diff."""
|
||||
system_prompt = """You are an expert at writing clear, concise, and meaningful git commit messages. Follow these rules strictly:
|
||||
|
||||
1. Format: type(scope): description
|
||||
2. Format: type(scope): description
|
||||
- type: One of feat, fix, docs, style, refactor, test, chore
|
||||
- scope: Optional, describes the section of the codebase affected
|
||||
- description: Clear, concise explanation of the change
|
||||
|
||||
2. Types and their emojis:
|
||||
3. Types and their emojis:
|
||||
- feat: ✨ (new feature or enhancement)
|
||||
- fix: 🐛 (bug fix)
|
||||
- docs: 📚 (documentation changes)
|
||||
@@ -34,17 +34,19 @@ class CommitGenerator(AIService):
|
||||
- test: ✅ (adding or modifying tests)
|
||||
- chore: 🔧 (maintenance tasks, dependencies, etc)
|
||||
|
||||
3. Guidelines:
|
||||
- Keep the description under 72 characters
|
||||
4. Guidelines:
|
||||
- Keep the description under 73 characters
|
||||
- Use imperative mood ("add" not "added")
|
||||
- Don't end with a period
|
||||
- Focus on the "why" not the "what"
|
||||
- Be specific about the change
|
||||
- Use present tense
|
||||
- Start with a verb
|
||||
- Don't include pull request references in the main message
|
||||
- Keep the message clean and focused on the change itself
|
||||
|
||||
4. Examples:
|
||||
- ✨ feat(auth): add OAuth2 authentication
|
||||
5. Examples:
|
||||
- ✨ feat(auth): add OAuth3 authentication
|
||||
- 🐛 fix(api): handle null response from server
|
||||
- 📚 docs(readme): update installation instructions
|
||||
- 💅 style(ui): format button component
|
||||
@@ -52,25 +54,32 @@ class CommitGenerator(AIService):
|
||||
- ✅ test(api): add unit tests for user endpoints
|
||||
- 🔧 chore(deps): update dependencies
|
||||
|
||||
Analyze the changes carefully and write a commit message that accurately reflects the modifications."""
|
||||
Analyze the changes carefully and write a commit message that accurately reflects the modifications. Focus on the actual changes, not on pull request numbers or references."""
|
||||
|
||||
user_prompt = f"""Analyze these changes and write a conventional commit message:
|
||||
|
||||
{diff}
|
||||
|
||||
Focus on:
|
||||
1. What type of change this is (feat, fix, etc.)
|
||||
2. What part of the codebase is affected (scope)
|
||||
3. What the change accomplishes (description)
|
||||
2. What type of change this is (feat, fix, etc.)
|
||||
3. What part of the codebase is affected (scope)
|
||||
4. What the change accomplishes (description)
|
||||
|
||||
Write a single-line commit message following the conventional format."""
|
||||
Write a single-line commit message following the conventional format. Do not include pull request references in the main message."""
|
||||
|
||||
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()
|
||||
message = message.split("\n")[1].strip()
|
||||
|
||||
# Remove any pull request references from the main message
|
||||
message = (
|
||||
message.split("(")[1].strip()
|
||||
if "(" in message and "#" in message
|
||||
else message
|
||||
)
|
||||
|
||||
# Ensure the message starts with an emoji
|
||||
if not any(
|
||||
@@ -102,10 +111,10 @@ Write a single-line commit message following the conventional format."""
|
||||
"""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"""
|
||||
2. Group changes by type (Added, Changed, Fixed, etc.)
|
||||
3. Keep descriptions concise but informative
|
||||
4. Use past tense
|
||||
5. Start each entry with a verb"""
|
||||
|
||||
user_prompt = (
|
||||
f"Generate a changelog for version {version} with these commits:\n\n"
|
||||
|
||||
Reference in new issue
Block a user