UNPKG

@sbeeredd04/auto-git

Version:

AI-powered Git automation with intelligent commit decisions using Gemini function calling, smart diff optimization, push control, and enhanced interactive terminal session with persistent command history

521 lines (438 loc) 16.1 kB
import * as vscode from 'vscode'; import { configManager } from './config'; import logger from './logger'; /** * AI utilities for GitCue extension v0.3.5 * Handles error analysis and suggestions using Google GenAI SDK */ // Global debug mode const DEBUG_MODE = process.env.NODE_ENV === 'development' || process.env.GITCUE_DEBUG === 'true'; function debugLog(message: string, data?: any) { if (DEBUG_MODE) { logger.debug('[GitCue v0.3.5] ' + message + (data ? ': ' + JSON.stringify(data) : '')); } } // Function declarations for AI function calling const getCommitDecisionFunctionDeclaration = async () => { const { Type } = await import('@google/genai'); return { name: 'make_commit_decision', description: 'Analyzes Git changes and decides whether to commit them based on code quality, completeness, and significance.', parameters: { type: Type.OBJECT, properties: { shouldCommit: { type: Type.BOOLEAN, description: 'Whether the changes should be committed now', }, reason: { type: Type.STRING, description: 'Clear explanation for the commit decision', }, significance: { type: Type.STRING, enum: ['LOW', 'MEDIUM', 'HIGH'], description: 'The significance level of the changes', }, suggestedMessage: { type: Type.STRING, description: 'Suggested commit message if shouldCommit is true', }, nextSteps: { type: Type.ARRAY, items: { type: Type.STRING }, description: 'Recommended next steps for the developer', } }, required: ['shouldCommit', 'reason', 'significance'], }, }; }; const getCommitMessageFunctionDeclaration = async () => { const { Type } = await import('@google/genai'); return { name: 'generate_commit_message', description: 'Generates a high-quality commit message following conventional commit standards.', parameters: { type: Type.OBJECT, properties: { type: { type: Type.STRING, enum: ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'ci', 'build'], description: 'The type of change according to conventional commits', }, scope: { type: Type.STRING, description: 'Optional scope of the change (e.g., component, module)', }, description: { type: Type.STRING, description: 'Brief description of the change (imperative mood)', }, body: { type: Type.STRING, description: 'Optional longer description explaining the change', }, breakingChange: { type: Type.BOOLEAN, description: 'Whether this is a breaking change', } }, required: ['type', 'description'], }, }; }; interface CommitDecision { shouldCommit: boolean; reason: string; significance: 'LOW' | 'MEDIUM' | 'HIGH'; suggestedMessage?: string; nextSteps?: string[]; } interface CommitMessage { type: string; scope?: string; description: string; body?: string; breakingChange?: boolean; } /** * Generate AI-powered error suggestion */ export async function generateErrorSuggestion(errorContext: string): Promise<string> { const config = configManager.getConfig(); debugLog('Starting AI error analysis', errorContext.substring(0, 100) + '...'); if (!config.geminiApiKey) { const error = 'Gemini API key not configured'; logger.error('AI analysis failed: ' + error); throw new Error(error); } if (!config.enableSuggestions) { const message = 'AI suggestions are disabled in settings.'; debugLog('AI suggestions disabled'); return message; } try { debugLog('Importing Google GenAI SDK...'); const { GoogleGenAI } = await import('@google/genai'); debugLog('Initializing AI with API key...'); const ai = new GoogleGenAI({ apiKey: config.geminiApiKey }); const prompt = `Analyze this command error and provide a concise solution: Error Context: ${errorContext} Provide a brief response with: 1. **What went wrong** (1-2 sentences) 2. **Most likely fix** (the exact command to run) 3. **Alternative solutions** (if applicable) Keep it under 200 words. Use markdown formatting. Focus on actionable solutions.`; debugLog('Sending request to Gemini 2.0 Flash...'); // Always use gemini-2.0-flash const response = await ai.models.generateContent({ model: 'gemini-2.0-flash', contents: prompt, config: { maxOutputTokens: 1000, temperature: 0.3, } }); debugLog('Received response from AI'); if (!response || !response.text) { logger.error('AI response is empty or invalid: ' + JSON.stringify(response)); throw new Error('AI response is empty or invalid'); } debugLog('AI analysis completed successfully'); return response.text; } catch (error) { logger.error('AI suggestion generation failed: ' + (error instanceof Error ? error.message : String(error))); // Add more detailed error logging if (error instanceof Error) { logger.error(`Error details - Name: ${error.name}, Message: ${error.message}, Stack: ${error.stack || 'No stack trace'}`); } // Add retry logic for transient errors if (error instanceof Error && (error.message.includes('API') || error.message.includes('network') || error.message.includes('timeout'))) { try { debugLog('Retrying AI request after 1 second...'); await new Promise(resolve => setTimeout(resolve, 1000)); return generateErrorSuggestion(errorContext); } catch (retryError) { logger.error('Retry failed: ' + (retryError instanceof Error ? retryError.message : String(retryError))); } } throw error; // Propagate the error to be handled by the caller } } /** * Test function to verify AI functionality */ export async function testAIConnection(): Promise<boolean> { try { debugLog('Testing AI connection...'); const testResponse = await generateErrorSuggestion('Test error: command not found'); debugLog('AI test successful', testResponse.substring(0, 100) + '...'); return true; } catch (error) { logger.error('AI test failed: ' + (error instanceof Error ? error.message : String(error))); return false; } } /** * Format AI suggestion with markdown styling */ export function formatAISuggestion(suggestion: string): string { // Add header let formatted = '🤖 **AI Assistant Suggestion**\n\n'; formatted += suggestion; formatted += '\n\n---\n'; formatted += '*Powered by Google Gemini AI*'; return formatted; } /** * Format Git command with syntax highlighting */ export function formatGitCommand(command: string): string { // Simple syntax highlighting for Git commands return command .replace(/^git\s+/, '**git** ') .replace(/\s(add|commit|push|pull|status|log|branch|checkout|merge|rebase|reset|diff|show)\s/, ' **$1** ') .replace(/\s--?([a-zA-Z-]+)/g, ' `--$1`') .replace(/\s-([a-zA-Z])/g, ' `-$1`'); } /** * Format markdown content for display */ export function formatMarkdown(content: string): string { // Basic markdown formatting for terminal display return content .replace(/\*\*(.*?)\*\*/g, '$1') // Bold .replace(/\*(.*?)\*/g, '$1') // Italic .replace(/`(.*?)`/g, '$1') // Code .replace(/^#{1,6}\s+(.*)$/gm, '$1') // Headers .replace(/^>\s+(.*)$/gm, ' $1') // Blockquotes .replace(/^-\s+(.*)$/gm, '• $1') // Lists .replace(/^\d+\.\s+(.*)$/gm, '$1') // Numbered lists .trim(); } /** * Create a boxed message for important information */ export function createBox(message: string, title?: string): string { const lines = message.split('\n'); const maxLength = Math.max(...lines.map(line => line.length), title ? title.length : 0); const width = Math.min(maxLength + 4, 80); let box = '┌' + '─'.repeat(width - 2) + '┐\n'; if (title) { const padding = Math.max(0, width - title.length - 4); const leftPad = Math.floor(padding / 2); const rightPad = padding - leftPad; box += '│ ' + ' '.repeat(leftPad) + title + ' '.repeat(rightPad) + ' │\n'; box += '├' + '─'.repeat(width - 2) + '┤\n'; } lines.forEach(line => { const padding = Math.max(0, width - line.length - 4); box += '│ ' + line + ' '.repeat(padding) + ' │\n'; }); box += '└' + '─'.repeat(width - 2) + '┘'; return box; } /** * Rate limiting for AI API calls */ class RateLimiter { private calls: number[] = []; private maxCalls: number; constructor(maxCallsPerMinute: number) { this.maxCalls = maxCallsPerMinute; } canMakeCall(): boolean { const now = Date.now(); const oneMinuteAgo = now - 60000; // Remove calls older than 1 minute this.calls = this.calls.filter(time => time > oneMinuteAgo); return this.calls.length < this.maxCalls; } recordCall(): void { this.calls.push(Date.now()); } getTimeUntilNextCall(): number { if (this.canMakeCall()) { return 0; } const oldestCall = Math.min(...this.calls); const timeUntilExpiry = (oldestCall + 60000) - Date.now(); return Math.max(0, timeUntilExpiry); } } // Global rate limiter instance let rateLimiter: RateLimiter; /** * Get or create rate limiter with current configuration */ function getRateLimiter(): RateLimiter { const config = configManager.getConfig(); if (!rateLimiter || rateLimiter['maxCalls'] !== config.maxCallsPerMinute) { rateLimiter = new RateLimiter(config.maxCallsPerMinute); } return rateLimiter; } /** * Generate error suggestion with rate limiting */ export async function generateErrorSuggestionWithRateLimit(errorContext: string): Promise<string> { const limiter = getRateLimiter(); if (!limiter.canMakeCall()) { const waitTime = Math.ceil(limiter.getTimeUntilNextCall() / 1000); return `Rate limit exceeded. Please wait ${waitTime} seconds before requesting another AI suggestion.`; } try { limiter.recordCall(); return await generateErrorSuggestion(errorContext); } catch (error) { logger.error('AI suggestion failed', error instanceof Error ? error.message : String(error)); return `Failed to generate AI suggestion: ${error instanceof Error ? error.message : 'Unknown error'}`; } } /** * Show AI suggestion in VS Code */ export async function showAISuggestionInVSCode(errorContext: string): Promise<void> { try { const suggestion = await generateErrorSuggestionWithRateLimit(errorContext); const formattedSuggestion = formatAISuggestion(suggestion); // Show in information message with action buttons const action = await vscode.window.showInformationMessage( 'AI Assistant has analyzed the error and generated suggestions.', 'View Suggestions', 'Copy to Clipboard', 'Dismiss' ); if (action === 'View Suggestions') { // Create a new document with the suggestion const doc = await vscode.workspace.openTextDocument({ content: formattedSuggestion, language: 'markdown' }); await vscode.window.showTextDocument(doc); } else if (action === 'Copy to Clipboard') { await vscode.env.clipboard.writeText(suggestion); vscode.window.showInformationMessage('AI suggestion copied to clipboard'); } // Also log to output channel logger.aiSuggestion('Generated suggestion for error: ' + errorContext.substring(0, 100) + '...'); } catch (error) { logger.error('Failed to show AI suggestion', error instanceof Error ? error.message : String(error)); vscode.window.showErrorMessage('Failed to generate AI suggestion: ' + (error instanceof Error ? error.message : 'Unknown error')); } } /** * Uses AI function calling to make intelligent commit decisions */ export async function makeCommitDecisionWithAI(gitDiff: string, gitStatus: string): Promise<CommitDecision> { const config = configManager.getConfig(); if (!config.geminiApiKey) { throw new Error('Gemini API key not configured'); } try { const { GoogleGenAI, Type } = await import('@google/genai'); const ai = new GoogleGenAI({ apiKey: config.geminiApiKey }); const prompt = `Analyze these Git changes and decide if they should be committed: Git Status: ${gitStatus} Git Diff (first 3000 chars): ${gitDiff.substring(0, 3000)} Consider: - Are these meaningful, complete changes? - Is this a good stopping point for a commit? - Are there any incomplete features or broken functionality? - Code quality and consistency - Whether changes form a logical unit Use the make_commit_decision function to provide your analysis.`; const response = await ai.models.generateContent({ model: 'gemini-2.0-flash', contents: prompt, config: { tools: [{ functionDeclarations: [await getCommitDecisionFunctionDeclaration()] }], maxOutputTokens: 1000, temperature: 0.3, } }); if (response.functionCalls && response.functionCalls.length > 0) { const functionCall = response.functionCalls[0]; if (functionCall.name === 'make_commit_decision') { return functionCall.args as unknown as CommitDecision; } } // Fallback if no function call return { shouldCommit: true, reason: 'AI analysis completed without function call, defaulting to commit', significance: 'MEDIUM' }; } catch (error) { logger.error('AI commit decision failed: ' + (error instanceof Error ? error.message : String(error))); throw error; } } /** * Uses AI function calling to generate commit messages */ export async function generateCommitMessageWithAI(gitDiff: string, gitStatus: string): Promise<string> { const config = configManager.getConfig(); if (!config.geminiApiKey) { throw new Error('Gemini API key not configured'); } try { const { GoogleGenAI, Type } = await import('@google/genai'); const ai = new GoogleGenAI({ apiKey: config.geminiApiKey }); const prompt = `Generate a high-quality commit message for these changes following conventional commit standards: Git Status: ${gitStatus} Git Diff (first 3000 chars): ${gitDiff.substring(0, 3000)} Requirements: - Use conventional commit format: type(scope): description - Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build - Keep description under 50 characters, imperative mood - Add body if changes are complex - Be specific about what changed, not just "update files" Use the generate_commit_message function to create a proper commit message.`; const response = await ai.models.generateContent({ model: 'gemini-2.0-flash', contents: prompt, config: { tools: [{ functionDeclarations: [await getCommitMessageFunctionDeclaration()] }], maxOutputTokens: 500, temperature: 0.3, } }); if (response.functionCalls && response.functionCalls.length > 0) { const functionCall = response.functionCalls[0]; if (functionCall.name === 'generate_commit_message') { const msg = functionCall.args as unknown as CommitMessage; let commitMessage = `${msg.type}`; if (msg.scope) { commitMessage += `(${msg.scope})`; } if (msg.breakingChange) { commitMessage += '!'; } commitMessage += `: ${msg.description}`; if (msg.body) { commitMessage += `\n\n${msg.body}`; } return commitMessage; } } // Fallback to regular generation return await generateErrorSuggestion(`Generate a commit message for: ${gitStatus}`); } catch (error) { logger.error('AI commit message generation failed: ' + (error instanceof Error ? error.message : String(error))); throw error; } }