Suggested commit message
This commit is contained in:
@@ -500,6 +500,21 @@ export function registerWorktreeHandlers(
|
||||
|
||||
debug('Merge result. isStageOnly:', isStageOnly, 'newStatus:', newStatus, 'staged:', staged);
|
||||
|
||||
// Read suggested commit message if staging succeeded
|
||||
let suggestedCommitMessage: string | undefined;
|
||||
if (staged) {
|
||||
const commitMsgPath = path.join(specDir, 'suggested_commit_message.txt');
|
||||
try {
|
||||
if (existsSync(commitMsgPath)) {
|
||||
const { readFileSync } = require('fs');
|
||||
suggestedCommitMessage = readFileSync(commitMsgPath, 'utf-8').trim();
|
||||
debug('Read suggested commit message:', suggestedCommitMessage?.substring(0, 100));
|
||||
}
|
||||
} catch (e) {
|
||||
debug('Failed to read suggested commit message:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the status change to implementation_plan.json
|
||||
const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
|
||||
try {
|
||||
@@ -531,7 +546,8 @@ export function registerWorktreeHandlers(
|
||||
success: true,
|
||||
message,
|
||||
staged,
|
||||
projectPath: staged ? project.path : undefined
|
||||
projectPath: staged ? project.path : undefined,
|
||||
suggestedCommitMessage
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -120,6 +120,7 @@ function TaskDetailModalContent({ open, task, onOpenChange }: { open: boolean; t
|
||||
state.setWorkspaceError(null);
|
||||
state.setStagedSuccess(result.data.message || 'Changes staged in main project');
|
||||
state.setStagedProjectPath(result.data.projectPath);
|
||||
state.setSuggestedCommitMessage(result.data.suggestedCommitMessage);
|
||||
} else {
|
||||
onOpenChange(false);
|
||||
}
|
||||
@@ -393,6 +394,7 @@ function TaskDetailModalContent({ open, task, onOpenChange }: { open: boolean; t
|
||||
stageOnly={state.stageOnly}
|
||||
stagedSuccess={state.stagedSuccess}
|
||||
stagedProjectPath={state.stagedProjectPath}
|
||||
suggestedCommitMessage={state.suggestedCommitMessage}
|
||||
mergePreview={state.mergePreview}
|
||||
isLoadingPreview={state.isLoadingPreview}
|
||||
showConflictDialog={state.showConflictDialog}
|
||||
|
||||
@@ -84,6 +84,7 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
|
||||
state.setWorkspaceError(null);
|
||||
state.setStagedSuccess(result.data.message || 'Changes staged in main project');
|
||||
state.setStagedProjectPath(result.data.projectPath);
|
||||
state.setSuggestedCommitMessage(result.data.suggestedCommitMessage);
|
||||
} else {
|
||||
console.warn('[TaskDetailPanel] Full merge success, closing panel');
|
||||
onClose();
|
||||
@@ -196,6 +197,7 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
|
||||
stageOnly={state.stageOnly}
|
||||
stagedSuccess={state.stagedSuccess}
|
||||
stagedProjectPath={state.stagedProjectPath}
|
||||
suggestedCommitMessage={state.suggestedCommitMessage}
|
||||
mergePreview={state.mergePreview}
|
||||
isLoadingPreview={state.isLoadingPreview}
|
||||
showConflictDialog={state.showConflictDialog}
|
||||
|
||||
@@ -26,6 +26,7 @@ interface TaskReviewProps {
|
||||
stageOnly: boolean;
|
||||
stagedSuccess: string | null;
|
||||
stagedProjectPath: string | undefined;
|
||||
suggestedCommitMessage: string | undefined;
|
||||
mergePreview: { files: string[]; conflicts: MergeConflict[]; summary: MergeStats; gitConflicts?: GitConflictInfo; uncommittedChanges?: { hasChanges: boolean; files: string[]; count: number } | null } | null;
|
||||
isLoadingPreview: boolean;
|
||||
showConflictDialog: boolean;
|
||||
@@ -64,6 +65,7 @@ export function TaskReview({
|
||||
stageOnly,
|
||||
stagedSuccess,
|
||||
stagedProjectPath,
|
||||
suggestedCommitMessage,
|
||||
mergePreview,
|
||||
isLoadingPreview,
|
||||
showConflictDialog,
|
||||
@@ -88,6 +90,7 @@ export function TaskReview({
|
||||
stagedSuccess={stagedSuccess}
|
||||
stagedProjectPath={stagedProjectPath}
|
||||
task={task}
|
||||
suggestedCommitMessage={suggestedCommitMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) {
|
||||
const [stageOnly, setStageOnly] = useState(task.status === 'human_review');
|
||||
const [stagedSuccess, setStagedSuccess] = useState<string | null>(null);
|
||||
const [stagedProjectPath, setStagedProjectPath] = useState<string | undefined>(undefined);
|
||||
const [suggestedCommitMessage, setSuggestedCommitMessage] = useState<string | undefined>(undefined);
|
||||
const [phaseLogs, setPhaseLogs] = useState<TaskLogs | null>(null);
|
||||
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
|
||||
const [expandedPhases, setExpandedPhases] = useState<Set<TaskLogPhase>>(new Set());
|
||||
@@ -279,6 +280,7 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) {
|
||||
stageOnly,
|
||||
stagedSuccess,
|
||||
stagedProjectPath,
|
||||
suggestedCommitMessage,
|
||||
phaseLogs,
|
||||
isLoadingLogs,
|
||||
expandedPhases,
|
||||
@@ -318,6 +320,7 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) {
|
||||
setStageOnly,
|
||||
setStagedSuccess,
|
||||
setStagedProjectPath,
|
||||
setSuggestedCommitMessage,
|
||||
setPhaseLogs,
|
||||
setIsLoadingLogs,
|
||||
setExpandedPhases,
|
||||
|
||||
+61
-2
@@ -1,11 +1,14 @@
|
||||
import { GitMerge, ExternalLink } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { GitMerge, ExternalLink, Copy, Check, Sparkles } from 'lucide-react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Textarea } from '../../ui/textarea';
|
||||
import type { Task } from '../../../../shared/types';
|
||||
|
||||
interface StagedSuccessMessageProps {
|
||||
stagedSuccess: string;
|
||||
stagedProjectPath: string | undefined;
|
||||
task: Task;
|
||||
suggestedCommitMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,8 +17,23 @@ interface StagedSuccessMessageProps {
|
||||
export function StagedSuccessMessage({
|
||||
stagedSuccess,
|
||||
stagedProjectPath,
|
||||
task
|
||||
task,
|
||||
suggestedCommitMessage
|
||||
}: StagedSuccessMessageProps) {
|
||||
const [commitMessage, setCommitMessage] = useState(suggestedCommitMessage || '');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!commitMessage) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(commitMessage);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-success/30 bg-success/10 p-4">
|
||||
<h3 className="font-medium text-sm text-foreground mb-2 flex items-center gap-2">
|
||||
@@ -25,6 +43,47 @@ export function StagedSuccessMessage({
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{stagedSuccess}
|
||||
</p>
|
||||
|
||||
{/* Commit Message Section */}
|
||||
{suggestedCommitMessage && (
|
||||
<div className="bg-background/50 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1.5">
|
||||
<Sparkles className="h-3 w-3 text-purple-400" />
|
||||
AI-generated commit message
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="h-6 px-2 text-xs"
|
||||
disabled={!commitMessage}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-3 w-3 mr-1 text-success" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Textarea
|
||||
value={commitMessage}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
className="font-mono text-xs min-h-[100px] bg-background/80 resize-y"
|
||||
placeholder="Commit message..."
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground mt-1.5">
|
||||
Edit as needed, then copy and use with <code className="bg-background px-1 rounded">git commit -m "..."</code>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-background/50 rounded-lg p-3 mb-3">
|
||||
<p className="text-xs text-muted-foreground mb-2">Next steps:</p>
|
||||
<ol className="text-xs text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
|
||||
@@ -356,6 +356,8 @@ export interface WorktreeMergeResult {
|
||||
staged?: boolean;
|
||||
alreadyStaged?: boolean;
|
||||
projectPath?: string;
|
||||
// AI-generated commit message suggestion (for stage-only mode)
|
||||
suggestedCommitMessage?: string;
|
||||
// New conflict info from smart merge
|
||||
conflicts?: MergeConflict[];
|
||||
stats?: MergeStats;
|
||||
|
||||
@@ -189,7 +189,83 @@ def handle_merge_command(
|
||||
Returns:
|
||||
True if merge succeeded, False otherwise
|
||||
"""
|
||||
return merge_existing_build(project_dir, spec_name, no_commit=no_commit)
|
||||
success = merge_existing_build(project_dir, spec_name, no_commit=no_commit)
|
||||
|
||||
# Generate commit message suggestion if staging succeeded (no_commit mode)
|
||||
if success and no_commit:
|
||||
_generate_and_save_commit_message(project_dir, spec_name)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def _generate_and_save_commit_message(project_dir: Path, spec_name: str) -> None:
|
||||
"""
|
||||
Generate a commit message suggestion and save it for the UI.
|
||||
|
||||
Args:
|
||||
project_dir: Project root directory
|
||||
spec_name: Name of the spec
|
||||
"""
|
||||
try:
|
||||
from commit_message import generate_commit_message_sync
|
||||
|
||||
# Get diff summary for context
|
||||
diff_summary = ""
|
||||
files_changed = []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--staged", "--stat"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
diff_summary = result.stdout.strip()
|
||||
|
||||
# Get list of changed files
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--staged", "--name-only"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
files_changed = [
|
||||
f.strip() for f in result.stdout.strip().split("\n") if f.strip()
|
||||
]
|
||||
except Exception as e:
|
||||
debug_warning(MODULE, f"Could not get diff summary: {e}")
|
||||
|
||||
# Generate commit message
|
||||
debug(MODULE, "Generating commit message suggestion...")
|
||||
commit_message = generate_commit_message_sync(
|
||||
project_dir=project_dir,
|
||||
spec_name=spec_name,
|
||||
diff_summary=diff_summary,
|
||||
files_changed=files_changed,
|
||||
)
|
||||
|
||||
if commit_message:
|
||||
# Save to spec directory for UI to read
|
||||
spec_dir = project_dir / ".auto-claude" / "specs" / spec_name
|
||||
if not spec_dir.exists():
|
||||
spec_dir = project_dir / "auto-claude" / "specs" / spec_name
|
||||
|
||||
if spec_dir.exists():
|
||||
commit_msg_file = spec_dir / "suggested_commit_message.txt"
|
||||
commit_msg_file.write_text(commit_message, encoding="utf-8")
|
||||
debug_success(
|
||||
MODULE, f"Saved commit message suggestion to {commit_msg_file}"
|
||||
)
|
||||
else:
|
||||
debug_warning(MODULE, f"Spec directory not found: {spec_dir}")
|
||||
else:
|
||||
debug_warning(MODULE, "No commit message generated")
|
||||
|
||||
except ImportError:
|
||||
debug_warning(MODULE, "commit_message module not available")
|
||||
except Exception as e:
|
||||
debug_warning(MODULE, f"Failed to generate commit message: {e}")
|
||||
|
||||
|
||||
def handle_review_command(project_dir: Path, spec_name: str) -> None:
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
"""
|
||||
Commit Message Generator
|
||||
========================
|
||||
|
||||
Generates high-quality commit messages using Claude Haiku.
|
||||
|
||||
Features:
|
||||
- Conventional commits format (feat/fix/refactor/etc)
|
||||
- GitHub issue references (Fixes #123)
|
||||
- Context-aware descriptions from spec metadata
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Map task categories to conventional commit types
|
||||
CATEGORY_TO_COMMIT_TYPE = {
|
||||
"feature": "feat",
|
||||
"bug_fix": "fix",
|
||||
"bug": "fix",
|
||||
"refactoring": "refactor",
|
||||
"refactor": "refactor",
|
||||
"documentation": "docs",
|
||||
"docs": "docs",
|
||||
"testing": "test",
|
||||
"test": "test",
|
||||
"performance": "perf",
|
||||
"perf": "perf",
|
||||
"security": "security",
|
||||
"chore": "chore",
|
||||
"style": "style",
|
||||
"ci": "ci",
|
||||
"build": "build",
|
||||
}
|
||||
|
||||
SYSTEM_PROMPT = """You are a Git expert who writes clear, concise commit messages following conventional commits format.
|
||||
|
||||
Rules:
|
||||
1. First line: type(scope): description (max 72 chars total)
|
||||
2. Leave blank line after first line
|
||||
3. Body: 1-3 sentences explaining WHAT changed and WHY
|
||||
4. If GitHub issue number provided, end with "Fixes #N" on its own line
|
||||
5. Be specific about the changes, not generic
|
||||
6. Use imperative mood ("Add feature" not "Added feature")
|
||||
|
||||
Types: feat, fix, refactor, docs, test, perf, chore, style, ci, build
|
||||
|
||||
Example output:
|
||||
feat(auth): add OAuth2 login flow
|
||||
|
||||
Implement OAuth2 authentication with Google and GitHub providers.
|
||||
Add token refresh logic and secure storage.
|
||||
|
||||
Fixes #42"""
|
||||
|
||||
|
||||
def _get_spec_context(spec_dir: Path) -> dict:
|
||||
"""
|
||||
Extract context from spec files for commit message generation.
|
||||
|
||||
Returns dict with:
|
||||
- title: Feature/task title
|
||||
- category: Task category (feature, bug_fix, etc)
|
||||
- description: Brief description
|
||||
- github_issue: GitHub issue number if linked
|
||||
"""
|
||||
context = {
|
||||
"title": "",
|
||||
"category": "chore",
|
||||
"description": "",
|
||||
"github_issue": None,
|
||||
}
|
||||
|
||||
# Try to read spec.md for title
|
||||
spec_file = spec_dir / "spec.md"
|
||||
if spec_file.exists():
|
||||
try:
|
||||
content = spec_file.read_text(encoding="utf-8")
|
||||
# Extract title from first H1 or H2
|
||||
title_match = re.search(r"^#+ (.+)$", content, re.MULTILINE)
|
||||
if title_match:
|
||||
context["title"] = title_match.group(1).strip()
|
||||
|
||||
# Look for overview/description section
|
||||
overview_match = re.search(
|
||||
r"## Overview\s*\n(.+?)(?=\n##|\Z)", content, re.DOTALL
|
||||
)
|
||||
if overview_match:
|
||||
context["description"] = overview_match.group(1).strip()[:200]
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not read spec.md: {e}")
|
||||
|
||||
# Try to read requirements.json for metadata
|
||||
req_file = spec_dir / "requirements.json"
|
||||
if req_file.exists():
|
||||
try:
|
||||
req_data = json.loads(req_file.read_text(encoding="utf-8"))
|
||||
if not context["title"] and req_data.get("feature"):
|
||||
context["title"] = req_data["feature"]
|
||||
if req_data.get("workflow_type"):
|
||||
context["category"] = req_data["workflow_type"]
|
||||
if req_data.get("task_description") and not context["description"]:
|
||||
context["description"] = req_data["task_description"][:200]
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not read requirements.json: {e}")
|
||||
|
||||
# Try to read implementation_plan.json for GitHub issue
|
||||
plan_file = spec_dir / "implementation_plan.json"
|
||||
if plan_file.exists():
|
||||
try:
|
||||
plan_data = json.loads(plan_file.read_text(encoding="utf-8"))
|
||||
# Check for GitHub metadata
|
||||
metadata = plan_data.get("metadata", {})
|
||||
if metadata.get("githubIssueNumber"):
|
||||
context["github_issue"] = metadata["githubIssueNumber"]
|
||||
# Fallback title
|
||||
if not context["title"]:
|
||||
context["title"] = plan_data.get("feature") or plan_data.get("title", "")
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not read implementation_plan.json: {e}")
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def _build_prompt(
|
||||
spec_context: dict,
|
||||
diff_summary: str,
|
||||
files_changed: list[str],
|
||||
) -> str:
|
||||
"""Build the prompt for Claude."""
|
||||
commit_type = CATEGORY_TO_COMMIT_TYPE.get(
|
||||
spec_context.get("category", "").lower(), "chore"
|
||||
)
|
||||
|
||||
github_ref = ""
|
||||
if spec_context.get("github_issue"):
|
||||
github_ref = f"\nGitHub Issue: #{spec_context['github_issue']} (include 'Fixes #{spec_context['github_issue']}' at the end)"
|
||||
|
||||
# Truncate file list if too long
|
||||
if len(files_changed) > 20:
|
||||
files_display = "\n".join(files_changed[:20]) + f"\n... and {len(files_changed) - 20} more files"
|
||||
else:
|
||||
files_display = "\n".join(files_changed) if files_changed else "(no files listed)"
|
||||
|
||||
prompt = f"""Generate a commit message for this change.
|
||||
|
||||
Task: {spec_context.get('title', 'Unknown task')}
|
||||
Type: {commit_type}
|
||||
Files changed: {len(files_changed)}
|
||||
{github_ref}
|
||||
|
||||
Description: {spec_context.get('description', 'No description available')}
|
||||
|
||||
Changed files:
|
||||
{files_display}
|
||||
|
||||
Diff summary:
|
||||
{diff_summary[:2000] if diff_summary else '(no diff available)'}
|
||||
|
||||
Generate ONLY the commit message, nothing else. Follow the format exactly:
|
||||
type(scope): short description
|
||||
|
||||
Body explaining changes.
|
||||
|
||||
Fixes #N (if applicable)"""
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
async def _call_claude_haiku(prompt: str) -> str:
|
||||
"""Call Claude Haiku with low thinking for fast commit message generation."""
|
||||
from core.auth import ensure_claude_code_oauth_token, get_auth_token
|
||||
|
||||
if not get_auth_token():
|
||||
logger.warning("No authentication token found")
|
||||
return ""
|
||||
|
||||
ensure_claude_code_oauth_token()
|
||||
|
||||
try:
|
||||
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
||||
except ImportError:
|
||||
logger.warning("claude_agent_sdk not installed")
|
||||
return ""
|
||||
|
||||
client = ClaudeSDKClient(
|
||||
options=ClaudeAgentOptions(
|
||||
model="claude-haiku-4-5-20251001",
|
||||
system_prompt=SYSTEM_PROMPT,
|
||||
allowed_tools=[],
|
||||
max_turns=1,
|
||||
max_thinking_tokens=1024, # Low thinking for speed
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
async with client:
|
||||
await client.query(prompt)
|
||||
|
||||
response_text = ""
|
||||
async for msg in client.receive_response():
|
||||
msg_type = type(msg).__name__
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
if hasattr(block, "text"):
|
||||
response_text += block.text
|
||||
|
||||
logger.info(f"Generated commit message: {len(response_text)} chars")
|
||||
return response_text.strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Claude SDK call failed: {e}")
|
||||
print(f" [WARN] Commit message generation failed: {e}", file=sys.stderr)
|
||||
return ""
|
||||
|
||||
|
||||
def generate_commit_message_sync(
|
||||
project_dir: Path,
|
||||
spec_name: str,
|
||||
diff_summary: str = "",
|
||||
files_changed: list[str] | None = None,
|
||||
github_issue: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a commit message synchronously.
|
||||
|
||||
Args:
|
||||
project_dir: Project root directory
|
||||
spec_name: Spec identifier (e.g., "001-add-feature")
|
||||
diff_summary: Git diff stat or summary
|
||||
files_changed: List of changed file paths
|
||||
github_issue: GitHub issue number if linked (overrides spec metadata)
|
||||
|
||||
Returns:
|
||||
Generated commit message or fallback message
|
||||
"""
|
||||
# Find spec directory
|
||||
spec_dir = project_dir / ".auto-claude" / "specs" / spec_name
|
||||
if not spec_dir.exists():
|
||||
# Try alternative location
|
||||
spec_dir = project_dir / "auto-claude" / "specs" / spec_name
|
||||
|
||||
# Get context from spec files
|
||||
spec_context = _get_spec_context(spec_dir) if spec_dir.exists() else {}
|
||||
|
||||
# Override with provided github_issue
|
||||
if github_issue:
|
||||
spec_context["github_issue"] = github_issue
|
||||
|
||||
# Build prompt
|
||||
prompt = _build_prompt(
|
||||
spec_context,
|
||||
diff_summary,
|
||||
files_changed or [],
|
||||
)
|
||||
|
||||
# Call Claude
|
||||
try:
|
||||
result = asyncio.run(_call_claude_haiku(prompt))
|
||||
if result:
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate commit message: {e}")
|
||||
|
||||
# Fallback message
|
||||
commit_type = CATEGORY_TO_COMMIT_TYPE.get(
|
||||
spec_context.get("category", "").lower(), "chore"
|
||||
)
|
||||
title = spec_context.get("title", spec_name)
|
||||
fallback = f"{commit_type}: {title}"
|
||||
|
||||
if github_issue or spec_context.get("github_issue"):
|
||||
issue_num = github_issue or spec_context.get("github_issue")
|
||||
fallback += f"\n\nFixes #{issue_num}"
|
||||
|
||||
return fallback
|
||||
|
||||
|
||||
async def generate_commit_message(
|
||||
project_dir: Path,
|
||||
spec_name: str,
|
||||
diff_summary: str = "",
|
||||
files_changed: list[str] | None = None,
|
||||
github_issue: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a commit message asynchronously.
|
||||
|
||||
Args:
|
||||
project_dir: Project root directory
|
||||
spec_name: Spec identifier (e.g., "001-add-feature")
|
||||
diff_summary: Git diff stat or summary
|
||||
files_changed: List of changed file paths
|
||||
github_issue: GitHub issue number if linked (overrides spec metadata)
|
||||
|
||||
Returns:
|
||||
Generated commit message or fallback message
|
||||
"""
|
||||
# Find spec directory
|
||||
spec_dir = project_dir / ".auto-claude" / "specs" / spec_name
|
||||
if not spec_dir.exists():
|
||||
spec_dir = project_dir / "auto-claude" / "specs" / spec_name
|
||||
|
||||
# Get context from spec files
|
||||
spec_context = _get_spec_context(spec_dir) if spec_dir.exists() else {}
|
||||
|
||||
# Override with provided github_issue
|
||||
if github_issue:
|
||||
spec_context["github_issue"] = github_issue
|
||||
|
||||
# Build prompt
|
||||
prompt = _build_prompt(
|
||||
spec_context,
|
||||
diff_summary,
|
||||
files_changed or [],
|
||||
)
|
||||
|
||||
# Call Claude
|
||||
try:
|
||||
result = await _call_claude_haiku(prompt)
|
||||
if result:
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate commit message: {e}")
|
||||
|
||||
# Fallback message
|
||||
commit_type = CATEGORY_TO_COMMIT_TYPE.get(
|
||||
spec_context.get("category", "").lower(), "chore"
|
||||
)
|
||||
title = spec_context.get("title", spec_name)
|
||||
fallback = f"{commit_type}: {title}"
|
||||
|
||||
if github_issue or spec_context.get("github_issue"):
|
||||
issue_num = github_issue or spec_context.get("github_issue")
|
||||
fallback += f"\n\nFixes #{issue_num}"
|
||||
|
||||
return fallback
|
||||
Reference in New Issue
Block a user