Skip to main content
Glama
git-repository-manager.ts10 kB
import { simpleGit, SimpleGit, GitError as SimpleGitError } from 'simple-git'; import tmp from 'tmp'; import * as fs from 'fs/promises'; import * as path from 'path'; import { ParsedGitUrl, RepositoryInfo, CloneOptions, GitError, GitAuthConfig, CloneProgress } from './types.js'; import { GitUrlParser } from './git-url-parser.js'; import { GitAuthManager } from './git-auth-manager.js'; import { GitCacheManager } from './git-cache-manager.js'; export class GitRepositoryManager { private git: SimpleGit; private authManager: GitAuthManager; private cacheManager: GitCacheManager; constructor(authConfig: GitAuthConfig = {}) { this.git = simpleGit(); this.authManager = new GitAuthManager(authConfig); this.cacheManager = new GitCacheManager(); } async cloneRepository(url: string, options: CloneOptions = {}): Promise<string> { const parsedUrl = GitUrlParser.parse(url); const branch = options.branch || 'main'; // Initialize cache manager if caching is enabled if (options.useCache) { await this.cacheManager.initialize(); } // Progress reporting helper const reportProgress = (progress: CloneProgress) => { if (options.progressCallback) { options.progressCallback(progress); } }; // Check cache first if enabled if (options.useCache && !options.cacheOptions?.forceRefresh) { reportProgress({ stage: 'validating', message: 'Checking cache for existing repository...' }); const cachedPath = await this.cacheManager.getCachedRepository(parsedUrl, branch); if (cachedPath) { console.log(`📦 Using cached repository: ${cachedPath}`); reportProgress({ stage: 'completed', message: 'Repository loaded from cache', percentage: 100 }); return cachedPath; } } reportProgress({ stage: 'validating', message: 'Loading authentication configuration...' }); // Load authentication configuration await this.authManager.loadAuthFromEnvironment(); await this.authManager.loadSSHConfig(); // Validate authentication const authValidation = this.authManager.validateAuthentication(parsedUrl); if (authValidation.warnings?.length) { authValidation.warnings.forEach(warning => console.warn(`⚠️ Authentication warning: ${warning}`) ); } reportProgress({ stage: 'cloning', message: 'Creating temporary directory...', percentage: 10 }); // Create temporary directory const tempDir = await this.createTempDirectory(options.tempDir); try { // Get authentication details const authDetails = this.authManager.getAuthForRepository(parsedUrl); // Use auth override if provided, otherwise use configured auth const cloneUrl = options.auth?.token ? this.buildAuthenticatedUrlWithToken(parsedUrl, options.auth.token) : authDetails.cloneUrl; // Prepare clone options const cloneArgs = this.buildCloneArgs(options); console.log(`🔐 Authentication method: ${authValidation.method}`); console.log(`📥 Cloning repository: ${parsedUrl.owner}/${parsedUrl.repo}`); console.log(`📁 Target directory: ${tempDir}`); reportProgress({ stage: 'cloning', message: `Cloning ${parsedUrl.owner}/${parsedUrl.repo}...`, percentage: 30 }); // Set environment variables if needed const originalEnv = process.env; if (authDetails.envVars) { Object.assign(process.env, authDetails.envVars); } try { // Clone the repository with progress tracking const git = simpleGit({ progress: ({ stage, progress }) => { const percentage = 30 + (progress * 0.6); // 30-90% for cloning reportProgress({ stage: 'cloning', message: `${stage}: ${Math.round(percentage)}%`, percentage }); } }); await git.clone(cloneUrl, tempDir, cloneArgs); console.log(`✅ Successfully cloned repository to: ${tempDir}`); reportProgress({ stage: 'cloning', message: 'Repository cloned successfully', percentage: 90 }); } finally { // Restore original environment if (authDetails.envVars) { process.env = originalEnv; } } // Add to cache if caching is enabled if (options.useCache) { reportProgress({ stage: 'caching', message: 'Adding repository to cache...', percentage: 95 }); try { const cachedPath = await this.cacheManager.addToCache(parsedUrl, branch, tempDir); console.log(`📦 Repository cached at: ${cachedPath}`); // Return cached path instead of temp path reportProgress({ stage: 'completed', message: 'Repository cached successfully', percentage: 100 }); return cachedPath; } catch (cacheError) { console.warn('Failed to cache repository, continuing with temporary directory:', cacheError); } } reportProgress({ stage: 'completed', message: 'Repository ready for scanning', percentage: 100 }); return tempDir; } catch (error) { // Clean up on failure await this.cleanup(tempDir); if (error instanceof SimpleGitError) { // Provide more helpful error messages let errorMessage = `Failed to clone repository: ${error.message}`; if (error.message.includes('Authentication failed')) { errorMessage += '\n 💡 Check your authentication tokens or SSH keys'; } else if (error.message.includes('not found')) { errorMessage += '\n 💡 Verify the repository URL and access permissions'; } throw new GitError(errorMessage, 'CLONE_FAILED', url); } throw new GitError( `Unexpected error cloning repository: ${error instanceof Error ? error.message : 'Unknown error'}`, 'CLONE_FAILED', url ); } } async validateRepository(url: string): Promise<RepositoryInfo> { const parsedUrl = GitUrlParser.parse(url); try { // Load authentication configuration await this.authManager.loadAuthFromEnvironment(); await this.authManager.loadSSHConfig(); // Get authentication details const authDetails = this.authManager.getAuthForRepository(parsedUrl); // Use git ls-remote to check if repository exists and is accessible const result = await this.git.listRemote([authDetails.cloneUrl]); if (!result) { throw new GitError( 'Repository not found or not accessible', 'REPOSITORY_NOT_FOUND', url ); } return { name: parsedUrl.repo, fullName: `${parsedUrl.owner}/${parsedUrl.repo}`, url: parsedUrl.originalUrl, branch: parsedUrl.branch || 'main' }; } catch (error) { if (error instanceof GitError) { throw error; } if (error instanceof SimpleGitError) { throw new GitError( `Failed to validate repository: ${error.message}`, 'VALIDATION_FAILED', url ); } throw new GitError( `Unexpected error validating repository: ${error instanceof Error ? error.message : 'Unknown error'}`, 'VALIDATION_FAILED', url ); } } async cleanup(tempPath: string): Promise<void> { try { console.log(`Cleaning up temporary directory: ${tempPath}`); await fs.rm(tempPath, { recursive: true, force: true }); console.log(`Successfully cleaned up: ${tempPath}`); } catch (error) { console.warn(`Failed to clean up temporary directory ${tempPath}:`, error); // Don't throw on cleanup failure - it's not critical } } parseGitUrl(url: string): ParsedGitUrl { return GitUrlParser.parse(url); } isGitUrl(url: string): boolean { return GitUrlParser.isGitUrl(url); } private async createTempDirectory(preferredPath?: string): Promise<string> { if (preferredPath) { await fs.mkdir(preferredPath, { recursive: true }); return preferredPath; } return new Promise((resolve, reject) => { tmp.dir({ prefix: 'coderag-git-', keep: false, // Will be cleaned up manually unsafeCleanup: true }, (err, path) => { if (err) { reject(new GitError('Failed to create temporary directory', 'TEMP_DIR_FAILED')); } else { resolve(path); } }); }); } private buildCloneArgs(options: CloneOptions): string[] { const args: string[] = []; if (options.branch) { args.push('--branch', options.branch); } if (options.depth) { args.push('--depth', options.depth.toString()); } if (options.singleBranch !== false) { args.push('--single-branch'); } return args; } updateAuthConfig(newConfig: GitAuthConfig): void { this.authManager.updateAuthConfig(newConfig); } getAuthConfig(): GitAuthConfig { return this.authManager.getAuthConfig(); } private buildAuthenticatedUrlWithToken(parsedUrl: ParsedGitUrl, token: string): string { return GitUrlParser.buildCloneUrl(parsedUrl, token); } async getCacheStats() { await this.cacheManager.initialize(); return this.cacheManager.getCacheStats(); } async clearCache(): Promise<void> { await this.cacheManager.clearCache(); } async getCacheManager(): Promise<GitCacheManager> { await this.cacheManager.initialize(); return this.cacheManager; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JonnoC/CodeRAG'

If you have feedback or need assistance with the MCP directory API, please join our Discord server