UNPKG

@aaswe/codebase-ai

Version:

AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs

527 lines 23.1 kB
"use strict"; /** * Git Service * Handles Git repository operations, monitoring, and change detection */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GitService = void 0; const simple_git_1 = require("simple-git"); const events_1 = require("events"); const fs_1 = require("fs"); const path = __importStar(require("path")); const logger_1 = __importDefault(require("../../../utils/logger")); class GitService extends events_1.EventEmitter { repositories = new Map(); monitoringIntervals = new Map(); constructor() { super(); } /** * Initialize Git service and load existing repositories */ async initialize() { logger_1.default.info('Initializing Git service'); try { // Load repositories from configuration or database await this.loadRepositories(); // Start monitoring active repositories for (const repo of this.repositories.values()) { if (repo.status === 'active') { await this.startMonitoring(repo.id); } } logger_1.default.info(`Git service initialized with ${this.repositories.size} repositories`); } catch (error) { logger_1.default.error('Failed to initialize Git service:', error); throw error; } } /** * Add a new repository for monitoring */ async addRepository(config) { const id = this.generateRepositoryId(config.name, config.path); logger_1.default.info(`Adding repository: ${config.name} at ${config.path}`); try { // Validate repository path if (!(0, fs_1.existsSync)(config.path)) { throw new Error(`Repository path does not exist: ${config.path}`); } // Initialize Git instance for this repository const repoGit = (0, simple_git_1.simpleGit)(config.path); // Check if it's a valid Git repository const isRepo = await repoGit.checkIsRepo(); if (!isRepo) { throw new Error(`Path is not a Git repository: ${config.path}`); } // Get current branch and commit const status = await repoGit.status(); const currentBranch = config.branch || status.current || 'main'; const log = await repoGit.log(['-1']); const lastCommitHash = log.latest?.hash; // Create repository object const repository = { id, name: config.name, path: config.path, branch: currentBranch, status: 'active', config: { includePatterns: ['**/*.ts', '**/*.js', '**/*.py', '**/*.java', '**/*.go', '**/*.rs', '**/*.cpp'], excludePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**', '**/*.test.*'], languages: ['typescript', 'javascript', 'python', 'java', 'go', 'rust', 'cpp'], enableWebhooks: false, enableFileWatcher: true, batchSize: 100, analysisDepth: 10, ...config.config } }; if (config.url !== undefined) { repository.url = config.url; } if (lastCommitHash !== undefined) { repository.lastCommitHash = lastCommitHash; } this.repositories.set(id, repository); // Save to persistent storage await this.saveRepositories(); // Start monitoring if active if (repository.status === 'active') { await this.startMonitoring(id); } this.emit('repositoryAdded', repository); logger_1.default.info(`Repository added successfully: ${repository.name} (${id})`); return repository; } catch (error) { logger_1.default.error(`Failed to add repository ${config.name}:`, error); throw error; } } /** * Remove a repository from monitoring */ async removeRepository(repositoryId) { logger_1.default.info(`Removing repository: ${repositoryId}`); const repository = this.repositories.get(repositoryId); if (!repository) { throw new Error(`Repository not found: ${repositoryId}`); } // Stop monitoring await this.stopMonitoring(repositoryId); // Remove from collection this.repositories.delete(repositoryId); // Save to persistent storage await this.saveRepositories(); this.emit('repositoryRemoved', repository); logger_1.default.info(`Repository removed: ${repository.name}`); } /** * Start monitoring a repository for changes */ async startMonitoring(repositoryId) { const repository = this.repositories.get(repositoryId); if (!repository) { throw new Error(`Repository not found: ${repositoryId}`); } // Stop existing monitoring if any await this.stopMonitoring(repositoryId); logger_1.default.info(`Starting Git monitoring for repository: ${repository.name}`); // Set up periodic polling for changes const interval = setInterval(async () => { try { await this.checkForChanges(repositoryId); } catch (error) { logger_1.default.error(`Error checking changes for repository ${repository.name}:`, error); } }, 30000); // Check every 30 seconds this.monitoringIntervals.set(repositoryId, interval); // Update repository status repository.status = 'active'; this.repositories.set(repositoryId, repository); } /** * Stop monitoring a repository */ async stopMonitoring(repositoryId) { const interval = this.monitoringIntervals.get(repositoryId); if (interval) { clearInterval(interval); this.monitoringIntervals.delete(repositoryId); } const repository = this.repositories.get(repositoryId); if (repository) { repository.status = 'inactive'; this.repositories.set(repositoryId, repository); logger_1.default.info(`Stopped monitoring repository: ${repository.name}`); } } /** * Check for changes in a repository */ async checkForChanges(repositoryId) { const repository = this.repositories.get(repositoryId); if (!repository) { return; } try { const repoGit = (0, simple_git_1.simpleGit)(repository.path); // Skip remote operations to prevent SSH authentication prompts during analysis logger_1.default.info('ℹ️ Skipping Git remote operations to prevent SSH authentication prompts'); logger_1.default.info('💡 Local Git operations only - remote changes will not be detected automatically'); // Get current commit (local only) const log = await repoGit.log(['-1']); const currentCommitHash = log.latest?.hash; // Check if there are new commits (local only) if (currentCommitHash && currentCommitHash !== repository.lastCommitHash) { logger_1.default.info(`New commits detected in repository: ${repository.name}`); logger_1.default.info(`Commit hash changed from ${repository.lastCommitHash} to ${currentCommitHash}`); // Get commits since last known commit (local only) const commits = await this.getCommitsSince(repository, repository.lastCommitHash); // Update repository repository.lastCommitHash = currentCommitHash; this.repositories.set(repositoryId, repository); // Emit change event this.emit('commitsDetected', { repository, commits }); } } catch (error) { logger_1.default.error(`Error checking changes for repository ${repository.name}:`, error); } } /** * Get commits since a specific commit hash */ async getCommitsSince(repository, sinceCommit) { const repoGit = (0, simple_git_1.simpleGit)(repository.path); const commits = []; try { const logOptions = sinceCommit ? [`${sinceCommit}..HEAD`] : ['-10']; // Last 10 commits if no since commit const log = await repoGit.log(logOptions); for (const commit of log.all) { // Get file changes for this commit const diffSummary = await repoGit.diffSummary([`${commit.hash}^`, commit.hash]); const files = diffSummary.files.map(file => ({ path: file.file, status: this.mapGitStatus(file), additions: 'insertions' in file ? file.insertions : 0, deletions: 'deletions' in file ? file.deletions : 0 })); commits.push({ hash: commit.hash, author: commit.author_name, email: commit.author_email, message: commit.message, timestamp: new Date(commit.date), files }); } return commits; } catch (error) { logger_1.default.error(`Error getting commits for repository ${repository.name}:`, error); return []; } } /** * Get file changes between two commits */ async getFileChanges(repositoryId, fromCommit, toCommit) { const repository = this.repositories.get(repositoryId); if (!repository) { throw new Error(`Repository not found: ${repositoryId}`); } try { const repoGit = (0, simple_git_1.simpleGit)(repository.path); const diffSummary = await repoGit.diffSummary([fromCommit, toCommit]); return diffSummary.files.map(file => ({ path: file.file, status: this.mapGitStatus(file), additions: 'insertions' in file ? file.insertions : 0, deletions: 'deletions' in file ? file.deletions : 0 })); } catch (error) { logger_1.default.error(`Error getting file changes for repository ${repository.name}:`, error); throw error; } } /** * Get repository status */ async getRepositoryStatus(repositoryId) { const repository = this.repositories.get(repositoryId); if (!repository) { throw new Error(`Repository not found: ${repositoryId}`); } try { const repoGit = (0, simple_git_1.simpleGit)(repository.path); return await repoGit.status(); } catch (error) { logger_1.default.error(`Error getting status for repository ${repository.name}:`, error); throw error; } } /** * Get all repositories */ getRepositories() { return Array.from(this.repositories.values()); } /** * Get repository by ID */ getRepository(repositoryId) { return this.repositories.get(repositoryId); } /** * Update repository configuration */ async updateRepositoryConfig(repositoryId, config) { const repository = this.repositories.get(repositoryId); if (!repository) { throw new Error(`Repository not found: ${repositoryId}`); } repository.config = { ...repository.config, ...config }; this.repositories.set(repositoryId, repository); // Save to persistent storage await this.saveRepositories(); this.emit('repositoryUpdated', repository); logger_1.default.info(`Repository configuration updated: ${repository.name}`); return repository; } /** * Cleanup resources */ async cleanup() { logger_1.default.info('Cleaning up Git service'); // Stop all monitoring for (const repositoryId of this.repositories.keys()) { await this.stopMonitoring(repositoryId); } this.repositories.clear(); this.removeAllListeners(); } // Private helper methods async loadRepositories() { try { const { readFile } = await Promise.resolve().then(() => __importStar(require('fs/promises'))); const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); const configPath = '.aaswe/repositories.json'; if (!existsSync(configPath)) { logger_1.default.debug('No repository configuration file found, auto-detecting current project'); await this.autoDetectCurrentProject(); return; } const configData = await readFile(configPath, 'utf-8'); const repositoriesData = JSON.parse(configData); // Only load repositories that still exist on disk for (const repoData of repositoriesData.repositories || []) { // Validate repository data and check if path exists if (!repoData.id || !repoData.name || !repoData.path) { logger_1.default.debug(`Invalid repository data found in config: ${JSON.stringify(repoData)}`); continue; } // Check if repository path still exists if (!existsSync(repoData.path)) { logger_1.default.debug(`Repository path no longer exists, skipping: ${repoData.path}`); continue; } // Only load if it's the current project directory or a subdirectory const currentDir = process.cwd(); if (!repoData.path.startsWith(currentDir) && !currentDir.startsWith(repoData.path)) { logger_1.default.debug(`Repository not related to current project, skipping: ${repoData.path}`); continue; } // Reconstruct repository object const repository = { id: repoData.id, name: repoData.name, path: repoData.path, branch: repoData.branch, status: repoData.status || 'inactive', config: { includePatterns: repoData.config?.includePatterns || ['**/*.ts', '**/*.js', '**/*.py', '**/*.java', '**/*.go', '**/*.rs', '**/*.cpp'], excludePatterns: repoData.config?.excludePatterns || ['node_modules/**', '.git/**', 'dist/**', 'build/**', '**/*.test.*'], languages: repoData.config?.languages || ['typescript', 'javascript', 'python', 'java', 'go', 'rust', 'cpp'], enableWebhooks: repoData.config?.enableWebhooks || false, enableFileWatcher: repoData.config?.enableFileWatcher !== false, batchSize: repoData.config?.batchSize || 100, analysisDepth: repoData.config?.analysisDepth || 10 } }; // Add optional fields if they exist if (repoData.url) repository.url = repoData.url; if (repoData.lastCommitHash) repository.lastCommitHash = repoData.lastCommitHash; if (repoData.lastAnalyzed) repository.lastAnalyzed = new Date(repoData.lastAnalyzed); this.repositories.set(repository.id, repository); } logger_1.default.info(`Loaded ${this.repositories.size} valid repositories from configuration`); // If no valid repositories found, auto-detect current project if (this.repositories.size === 0) { await this.autoDetectCurrentProject(); } } catch (error) { logger_1.default.error('Failed to load repositories from storage:', error); // Fallback to auto-detection await this.autoDetectCurrentProject(); } } async saveRepositories() { try { const { writeFile, mkdir } = await Promise.resolve().then(() => __importStar(require('fs/promises'))); const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); const configDir = '.aaswe'; const configPath = `${configDir}/repositories.json`; // Only save if we have repositories and they're valid if (this.repositories.size === 0) { logger_1.default.debug('No repositories to save'); return; } // Ensure config directory exists if (!existsSync(configDir)) { await mkdir(configDir, { recursive: true }); } // Only save repositories that are related to the current project const currentDir = process.cwd(); const validRepos = Array.from(this.repositories.values()).filter(repo => repo.path.startsWith(currentDir) || currentDir.startsWith(repo.path)); const repositoriesData = { version: '1.0.0', lastUpdated: new Date().toISOString(), projectRoot: currentDir, repositories: validRepos.map(repo => ({ id: repo.id, name: repo.name, path: repo.path, url: repo.url, branch: repo.branch, lastCommitHash: repo.lastCommitHash, lastAnalyzed: repo.lastAnalyzed?.toISOString(), status: repo.status, config: repo.config })) }; await writeFile(configPath, JSON.stringify(repositoriesData, null, 2), 'utf-8'); logger_1.default.debug(`Saved ${validRepos.length} project repositories to configuration`); } catch (error) { logger_1.default.error('Failed to save repositories to storage:', error); } } /** * Auto-detect and add the current project as a repository */ async autoDetectCurrentProject() { try { const currentDir = process.cwd(); const repoGit = (0, simple_git_1.simpleGit)(currentDir); // Check if current directory is a Git repository const isRepo = await repoGit.checkIsRepo(); if (!isRepo) { logger_1.default.debug('Current directory is not a Git repository, skipping auto-detection'); return; } // Get repository information (local operations only to avoid SSH prompts) const status = await repoGit.status(); const log = await repoGit.log(['-1']); // Skip remote operations to prevent SSH authentication prompts logger_1.default.info('ℹ️ Skipping Git remote detection to prevent SSH authentication prompts'); const projectName = path.basename(currentDir); const repository = { id: `${projectName}-${Date.now()}`, name: projectName, path: currentDir, branch: status.current || 'main', status: 'active', config: { includePatterns: ['**/*.ts', '**/*.js', '**/*.py', '**/*.java', '**/*.go', '**/*.rs', '**/*.cpp'], excludePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*'], languages: ['typescript', 'javascript', 'python', 'java', 'go', 'rust', 'cpp'], enableWebhooks: false, enableFileWatcher: true, batchSize: 100, analysisDepth: 10 } }; // Add optional fields if they exist (local only) if (log.latest?.hash) { repository.lastCommitHash = log.latest.hash; } this.repositories.set(repository.id, repository); await this.saveRepositories(); logger_1.default.info('Auto-detected current project as repository', { name: repository.name, path: repository.path, branch: repository.branch, note: 'Remote URL detection skipped to prevent SSH prompts' }); } catch (error) { logger_1.default.debug('Failed to auto-detect current project', { error }); } } generateRepositoryId(name, _path) { const { v4: uuidv4 } = require('uuid'); return `${name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${uuidv4().substring(0, 8)}`; } mapGitStatus(file) { // Map Git file status to our enum if (file.binary) return 'modified'; if (file.insertions > 0 && file.deletions === 0) return 'added'; if (file.insertions === 0 && file.deletions > 0) return 'deleted'; return 'modified'; } } exports.GitService = GitService; exports.default = GitService; //# sourceMappingURL=GitService.js.map