@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
385 lines (316 loc) • 14.3 kB
JavaScript
import { GoogleGenAI, Type } from '@google/genai';
import { validateConfig } from './config.js';
import { rateLimiter } from './rateLimiter.js';
import logger from '../utils/logger.js';
// Configure the client
let ai = null;
function getAIClient() {
if (!ai) {
const { apiKey } = validateConfig();
ai = new GoogleGenAI({ apiKey });
}
return ai;
}
// Function declaration for intelligent commit decisions
const shouldCommitFunctionDeclaration = {
name: 'should_commit_changes',
description: 'Analyzes code changes and determines if they warrant a commit based on significance, completeness, and configurable thresholds.',
parameters: {
type: Type.OBJECT,
properties: {
shouldCommit: {
type: Type.BOOLEAN,
description: 'Whether the changes meet the threshold and completeness requirements for a commit'
},
reason: {
type: Type.STRING,
description: 'Detailed explanation for the commit decision, including threshold and completeness analysis'
},
commitMessage: {
type: Type.STRING,
description: 'Suggested commit message if shouldCommit is true'
},
significance: {
type: Type.STRING,
enum: ['trivial', 'minor', 'medium', 'major', 'critical'],
description: 'The significance level of the changes: trivial (formatting, comments), minor (small fixes), medium (features, refactoring), major (new functionality, breaking changes), critical (security, major features)'
},
completeness: {
type: Type.STRING,
enum: ['incomplete', 'partial', 'complete'],
description: 'Whether the changes represent a complete implementation: incomplete (work in progress), partial (functional but missing pieces), complete (ready for commit)'
},
changeType: {
type: Type.STRING,
enum: ['feature', 'bugfix', 'refactor', 'docs', 'style', 'test', 'chore', 'performance', 'security'],
description: 'The primary type of change being made'
},
riskLevel: {
type: Type.STRING,
enum: ['low', 'medium', 'high'],
description: 'Risk level of the changes: low (safe changes), medium (moderate impact), high (breaking changes, major refactoring)'
}
},
required: ['shouldCommit', 'reason', 'significance', 'completeness', 'changeType']
}
};
export async function generateCommitMessage(diffText) {
validateConfig();
if (!diffText || diffText.trim().length === 0) {
throw new Error('No diff text provided for commit message generation');
}
// Check rate limit
if (!rateLimiter.canMakeCall()) {
const waitTime = Math.ceil(rateLimiter.getTimeUntilNextCall() / 1000);
throw new Error(`Rate limit exceeded. Please wait ${waitTime} seconds before making another request.`);
}
logger.debug('Calling Gemini API for commit message generation...');
try {
const aiClient = getAIClient();
const response = await aiClient.models.generateContent({
model: 'gemini-2.0-flash',
contents: [{
role: 'user',
parts: [{
text: `Generate a conventional commit message for these changes. Use format: type(scope): description
Rules:
- Use conventional commit format: type(scope): description
- Types: feat, fix, docs, style, refactor, test, chore
- Keep messages under 50 characters for the subject line
- Be specific but concise
- Focus on what changed, not how
- Use imperative mood (e.g., "add" not "added")
Examples:
- feat(auth): add user login validation
- fix(api): resolve null pointer exception
- docs(readme): update installation steps
- refactor(utils): simplify date formatting
Changes to analyze:
${diffText}`
}]
}]
});
if (!response.text) {
throw new Error('Gemini returned an empty commit message');
}
// Clean up the response - remove any extra formatting or explanations
const commitMessage = response.text
.split('\n')[0] // Take only the first line
.replace(/^["']|["']$/g, '') // Remove surrounding quotes
.trim();
if (!commitMessage) {
throw new Error('Gemini returned an empty commit message');
}
// Record the API call for rate limiting
rateLimiter.recordCall();
logger.debug(`Generated commit message: "${commitMessage}"`);
return commitMessage;
} catch (error) {
if (error.message.includes('API') || error.message.includes('fetch')) {
throw new Error(`Failed to generate commit message: ${error.message}`);
}
throw error;
}
}
export async function analyzeChangesForCommit(diffText, commitThreshold = 'medium', requireCompleteness = true) {
validateConfig();
if (!diffText || diffText.trim().length === 0) {
throw new Error('No diff text provided for commit analysis');
}
// Check rate limit
if (!rateLimiter.canMakeCall()) {
const waitTime = Math.ceil(rateLimiter.getTimeUntilNextCall() / 1000);
throw new Error(`Rate limit exceeded. Please wait ${waitTime} seconds before making another request.`);
}
logger.debug(`Calling Gemini API for intelligent commit analysis with threshold: ${commitThreshold}, requireCompleteness: ${requireCompleteness}`);
try {
const aiClient = getAIClient();
// Build threshold-specific guidance
const thresholdGuidance = getThresholdGuidance(commitThreshold);
const completenessGuidance = requireCompleteness ?
'\n\n**CRITICAL: Only commit if changes are COMPLETE implementations. Do not commit work-in-progress, partial implementations, or incomplete features.**' :
'\n\n**Completeness is preferred but not required for commits.**';
const response = await aiClient.models.generateContent({
model: 'gemini-2.0-flash',
contents: [{
role: 'user',
parts: [{
text: `Analyze these code changes and determine if they warrant a commit based on the configured threshold and completeness requirements.
**COMMIT THRESHOLD: ${commitThreshold.toUpperCase()}**
${thresholdGuidance}${completenessGuidance}
**Analysis Framework:**
1. **Significance Assessment** (trivial → minor → medium → major → critical):
- **Trivial**: Whitespace, formatting, comments only
- **Minor**: Small bug fixes, typos, minor tweaks
- **Medium**: Feature additions, meaningful refactoring, substantial bug fixes
- **Major**: New functionality, breaking changes, architectural changes
- **Critical**: Security fixes, major features, system-wide changes
2. **Completeness Assessment** (incomplete → partial → complete):
- **Incomplete**: Work in progress, debugging code, temporary changes
- **Partial**: Functional but missing tests, documentation, or edge cases
- **Complete**: Fully implemented, tested, documented, ready for production
3. **Change Type Classification**:
- Identify the primary type: feature, bugfix, refactor, docs, style, test, chore, performance, security
4. **Risk Assessment**:
- **Low**: Safe changes with minimal impact
- **Medium**: Changes that could affect existing functionality
- **High**: Breaking changes, major refactoring, architectural modifications
**Decision Logic:**
- Must meet or exceed the significance threshold: ${commitThreshold}
- Must meet completeness requirement: ${requireCompleteness ? 'complete' : 'any'}
- Consider if changes form a logical, atomic unit
- Avoid committing broken, incomplete, or experimental code
- Prioritize clean, meaningful commits over frequent commits
**Files and Changes to Analyze:**
${diffText}`
}]
}],
config: {
tools: [{
functionDeclarations: [shouldCommitFunctionDeclaration]
}]
}
});
// Check for function calls in the response
if (response.functionCalls && response.functionCalls.length > 0) {
const functionCall = response.functionCalls[0];
if (functionCall.name === 'should_commit_changes') {
// Record the API call for rate limiting
rateLimiter.recordCall();
const args = functionCall.args;
logger.debug(`Commit analysis result: ${JSON.stringify(args)}`);
// Apply threshold filtering
const meetsThreshold = checkSignificanceThreshold(args.significance, commitThreshold);
const meetsCompleteness = !requireCompleteness || args.completeness === 'complete';
// Override shouldCommit based on threshold and completeness
const finalShouldCommit = args.shouldCommit && meetsThreshold && meetsCompleteness;
if (args.shouldCommit && !finalShouldCommit) {
const reasons = [];
if (!meetsThreshold) reasons.push(`significance '${args.significance}' below threshold '${commitThreshold}'`);
if (!meetsCompleteness) reasons.push(`completeness '${args.completeness}' does not meet requirement`);
args.reason += ` (Filtered out: ${reasons.join(', ')})`;
}
return {
shouldCommit: finalShouldCommit,
reason: args.reason,
commitMessage: args.commitMessage || null,
significance: args.significance,
completeness: args.completeness || 'unknown',
changeType: args.changeType || 'unknown',
riskLevel: args.riskLevel || 'medium'
};
}
}
// Fallback if no function call was made
throw new Error('Gemini did not provide a structured commit analysis');
} catch (error) {
if (error.message.includes('API') || error.message.includes('fetch')) {
throw new Error(`Failed to analyze changes for commit: ${error.message}`);
}
throw error;
}
}
function getThresholdGuidance(threshold) {
switch (threshold) {
case 'any':
return `
**THRESHOLD: ANY** - Commit all meaningful changes, including minor fixes and formatting.
- Commit: Any change that improves the codebase
- Skip: Only completely meaningless changes (empty commits, etc.)`;
case 'medium':
return `
**THRESHOLD: MEDIUM** - Commit substantial changes, skip trivial ones.
- Commit: Features, bug fixes, meaningful refactoring, documentation updates
- Skip: Formatting only, comment changes, trivial tweaks`;
case 'major':
return `
**THRESHOLD: MAJOR** - Only commit significant features and important changes.
- Commit: New features, major bug fixes, breaking changes, architectural improvements
- Skip: Minor fixes, small refactoring, documentation updates, formatting`;
default:
return `**THRESHOLD: ${threshold.toUpperCase()}** - Apply standard commit practices.`;
}
}
function checkSignificanceThreshold(significance, threshold) {
const significanceOrder = ['trivial', 'minor', 'medium', 'major', 'critical'];
const thresholdOrder = { 'any': 0, 'medium': 2, 'major': 3 };
const significanceLevel = significanceOrder.indexOf(significance);
const requiredLevel = thresholdOrder[threshold] || 2;
return significanceLevel >= requiredLevel;
}
export async function generateErrorSuggestion(errorText) {
validateConfig();
if (!errorText || errorText.trim().length === 0) {
throw new Error('No error text provided for suggestion generation');
}
// Sanitize error text to remove sensitive information
const sanitizedError = sanitizeErrorText(errorText);
try {
const aiClient = getAIClient();
const response = await aiClient.models.generateContent({
model: 'gemini-2.0-flash',
contents: `You are an expert Git and command-line troubleshooting assistant. Analyze this error and provide clear, actionable solutions.
**Rules:**
- Provide specific, step-by-step commands to resolve the issue
- Focus on the most common and effective solutions first
- Use standard commands and best practices
- Be concise but thorough
- Include explanations for why the solution works
- If multiple solutions exist, mention the safest option first
**Common error patterns and solutions:**
- Git errors: git status, resolve conflicts, git add, git commit
- Push rejected: git pull --rebase, resolve conflicts, git push
- Detached HEAD: git checkout main/master
- Uncommitted changes: git stash, git stash pop
- Authentication issues: check credentials, SSH keys
- Remote tracking: git push --set-upstream origin branch-name
- Command not found: check spelling, install package, check PATH
- Permission denied: check file permissions, use sudo if needed
**Format your response with:**
1. **What went wrong** (1-2 sentences)
2. **Quick fix** (exact command to run)
3. **Alternative solutions** (if applicable)
**Error to analyze:**
${sanitizedError}`,
config: {
maxOutputTokens: 500,
temperature: 0.1
}
});
const suggestion = response.text?.trim();
if (!suggestion) {
throw new Error('Gemini returned an empty suggestion');
}
logger.debug('Generated error suggestion');
return suggestion;
} catch (error) {
if (error.message.includes('API') || error.message.includes('fetch')) {
throw new Error(`Failed to generate error suggestion: ${error.message}`);
}
throw error;
}
}
function sanitizeErrorText(errorText) {
// Remove sensitive information from error text
let sanitized = errorText;
// Remove file paths that might contain usernames
sanitized = sanitized.replace(/\/Users\/[^\/\s]+/g, '/Users/[username]');
sanitized = sanitized.replace(/\/home\/[^\/\s]+/g, '/home/[username]');
sanitized = sanitized.replace(/C:\\Users\\[^\\s]+/g, 'C:\\Users\\[username]');
// Remove potential API keys or tokens
sanitized = sanitized.replace(/[a-zA-Z0-9]{32,}/g, '[TOKEN]');
// Remove email addresses
sanitized = sanitized.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[email]');
// Remove URLs with credentials
sanitized = sanitized.replace(/https?:\/\/[^@\s]+@[^\s]+/g, 'https://[credentials]@[url]');
// Keep only the essential error information
const lines = sanitized.split('\n');
const relevantLines = lines.filter(line => {
const trimmed = line.trim();
return trimmed &&
!trimmed.startsWith('#') &&
!trimmed.startsWith('On branch') &&
!trimmed.startsWith('Your branch');
});
return relevantLines.join('\n').trim();
}