@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
527 lines • 23.1 kB
JavaScript
;
/**
* 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