UNPKG

gitset

Version:

Enhanced git init with user configuration management

239 lines 7.37 kB
import { spawn } from 'cross-spawn'; import { GIT_CONFIG, PLATFORM_CONFIG } from '../config/constants.js'; /** * Optimized Git command executor with caching and retry logic */ export class GitCommandExecutor { static cache = new Map(); static DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes static MAX_RETRIES = 3; static RETRY_DELAY = 1000; // 1 second /** * Execute a Git command with caching support */ static async execute(args, options = {}) { const { cwd, timeout = GIT_CONFIG.timeout, cacheable = false, ttl = this.DEFAULT_TTL, retries = this.MAX_RETRIES } = options; const cacheKey = this.generateCacheKey(args, cwd); // Check cache first if cacheable if (cacheable) { const cached = this.getFromCache(cacheKey); if (cached) { return cached; } } // Execute command with retry logic const result = await this.executeWithRetry(args, { cwd, timeout }, retries); // Cache successful results if cacheable if (cacheable && result.success) { this.setCache(cacheKey, result, ttl); } return result; } /** * Execute command with retry logic */ static async executeWithRetry(args, options, maxRetries) { let lastError = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await this.executeSingle(args, options); } catch (error) { lastError = error; // Don't retry on certain types of errors if (this.isNonRetryableError(error)) { break; } // Wait before retry (exponential backoff) if (attempt < maxRetries) { await this.delay(this.RETRY_DELAY * Math.pow(2, attempt)); } } } // All retries failed return { success: false, stdout: '', stderr: lastError?.message || 'Command execution failed', exitCode: -1 }; } /** * Execute a single Git command */ static async executeSingle(args, options) { return new Promise((resolve, reject) => { const child = spawn('git', args, { cwd: options.cwd, stdio: 'pipe' }); let stdout = ''; let stderr = ''; let timedOut = false; // Set up timeout const timer = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); }, options.timeout); // Collect stdout child.stdout?.on('data', (data) => { stdout += data.toString(); }); // Collect stderr child.stderr?.on('data', (data) => { stderr += data.toString(); }); // Handle completion child.on('close', (code) => { clearTimeout(timer); if (timedOut) { reject(new Error(`Git command timed out after ${options.timeout}ms`)); return; } resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code || 0 }); }); // Handle errors child.on('error', (error) => { clearTimeout(timer); reject(error); }); }); } /** * Generate cache key for command and options */ static generateCacheKey(args, cwd) { const argsStr = args.join(' '); const cwdStr = cwd || 'global'; return `${argsStr}:${cwdStr}`; } /** * Get result from cache if valid */ static getFromCache(key) { const entry = this.cache.get(key); if (!entry) { return null; } // Check if cache entry is still valid if (Date.now() - entry.timestamp > entry.ttl) { this.cache.delete(key); return null; } return entry.result; } /** * Store result in cache */ static setCache(key, result, ttl) { this.cache.set(key, { result, timestamp: Date.now(), ttl }); // Clean up old entries periodically if (this.cache.size > 100) { this.cleanupCache(); } } /** * Clean up expired cache entries */ static cleanupCache() { const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp > entry.ttl) { this.cache.delete(key); } } } /** * Check if error should not be retried */ static isNonRetryableError(error) { const message = error.message.toLowerCase(); return PLATFORM_CONFIG.git.nonRetryableErrors.some((msg) => message.includes(msg)); } /** * Delay execution for specified milliseconds */ static delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Clear all cached results */ static clearCache() { this.cache.clear(); } /** * Get cache statistics */ static getCacheStats() { return { size: this.cache.size, keys: Array.from(this.cache.keys()) }; } /** * Common Git commands with pre-configured caching */ static commands = { /** * Get Git version (cacheable for 1 hour) */ getVersion: () => this.execute(['--version'], { cacheable: true, ttl: 60 * 60 * 1000 }), /** * Get global user name (cacheable for 5 minutes) */ getGlobalUserName: () => this.execute(['config', 'user.name'], { cacheable: true, ttl: 5 * 60 * 1000 }), /** * Get global user email (cacheable for 5 minutes) */ getGlobalUserEmail: () => this.execute(['config', 'user.email'], { cacheable: true, ttl: 5 * 60 * 1000 }), /** * Initialize repository (not cacheable) */ init: (directory) => this.execute(['init'], { cwd: directory, cacheable: false }), /** * Set local user name (not cacheable) */ setLocalUserName: (directory, name) => this.execute(['config', 'user.name', name], { cwd: directory, cacheable: false }), /** * Set local user email (not cacheable) */ setLocalUserEmail: (directory, email) => this.execute(['config', 'user.email', email], { cwd: directory, cacheable: false }), /** * Check if directory is a Git repository (cacheable for 1 minute) */ isRepository: (directory) => this.execute(['rev-parse', '--git-dir'], { cwd: directory, cacheable: true, ttl: 60 * 1000 }) }; } //# sourceMappingURL=git-command-executor.js.map