gitset
Version:
Enhanced git init with user configuration management
239 lines • 7.37 kB
JavaScript
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