UNPKG

remcode

Version:

Turn your AI assistant into a codebase expert. Intelligent code analysis, semantic search, and software engineering guidance through MCP integration.

301 lines (300 loc) 11.4 kB
"use strict"; 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ChangeDetector = void 0; const logger_1 = require("../utils/logger"); const child_process_1 = require("child_process"); const path = __importStar(require("path")); const fs = __importStar(require("fs")); const logger = (0, logger_1.getLogger)('ChangeDetector'); class ChangeDetector { constructor(repoPath = process.cwd()) { this.repoPath = path.resolve(repoPath); logger.debug(`Initialized ChangeDetector with repo path: ${this.repoPath}`); // Verify this is a git repository if (!this.isGitRepository()) { throw new Error(`Directory is not a git repository: ${this.repoPath}`); } } /** * Check if the directory is a git repository */ isGitRepository() { try { (0, child_process_1.execSync)('git rev-parse --is-inside-work-tree', { cwd: this.repoPath, stdio: 'ignore' }); return true; } catch (error) { return false; } } /** * Get the current HEAD commit hash */ getCurrentCommit() { try { return (0, child_process_1.execSync)('git rev-parse HEAD', { cwd: this.repoPath }).toString().trim(); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(`Failed to get current commit: ${errorMsg}`); throw new Error(`Failed to get current commit: ${errorMsg}`); } } /** * Check if a commit exists in the repository */ commitExists(commit) { try { (0, child_process_1.execSync)(`git cat-file -e ${commit}^{commit}`, { cwd: this.repoPath, stdio: 'ignore' }); return true; } catch (error) { return false; } } /** * Get list of changed files between two commits */ async getChangedFiles(fromCommit, toCommit = 'HEAD') { logger.info(`Detecting changes from ${fromCommit} to ${toCommit}`); // Verify commits exist if (!this.commitExists(fromCommit)) { throw new Error(`From commit does not exist: ${fromCommit}`); } if (!this.commitExists(toCommit)) { throw new Error(`To commit does not exist: ${toCommit}`); } try { // Get diff with name status and renamed detection const diffCommand = `git diff --name-status -M ${fromCommit} ${toCommit}`; logger.debug(`Executing: ${diffCommand}`); const diffOutput = (0, child_process_1.execSync)(diffCommand, { cwd: this.repoPath }).toString().trim(); if (!diffOutput) { logger.info('No changes detected between commits'); return []; } // Parse git diff output const changes = []; const lines = diffOutput.split('\n'); for (const line of lines) { // Format is: STATUS\tFILE_PATH (or STATUS\tOLD_PATH\tNEW_PATH for renames) const parts = line.split('\t'); const status = parts[0].trim(); // Skip submodule changes if (status.startsWith('S')) continue; if (status.startsWith('A')) { // Added file changes.push(this.enrichFileInfo({ status: 'added', path: parts[1] })); } else if (status.startsWith('M') || status.startsWith('T')) { // Modified file (T = type changed but content still modified) changes.push(this.enrichFileInfo({ status: 'modified', path: parts[1] })); } else if (status.startsWith('D')) { // Deleted file changes.push({ status: 'deleted', path: parts[1], extension: path.extname(parts[1]) }); } else if (status.startsWith('R')) { // Renamed file changes.push(this.enrichFileInfo({ status: 'renamed', previousPath: parts[1], path: parts[2] })); } } logger.info(`Detected ${changes.length} changed files`); return changes; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(`Failed to get changed files: ${errorMsg}`); throw new Error(`Failed to get changed files: ${errorMsg}`); } } /** * Add additional file information */ enrichFileInfo(change) { // Skip enrichment for deleted files if (change.status === 'deleted') { return change; } const filePath = path.join(this.repoPath, change.path); change.extension = path.extname(change.path); try { if (fs.existsSync(filePath)) { const stats = fs.statSync(filePath); change.size = stats.size; } } catch (error) { logger.warn(`Couldn't get file stats for ${filePath}`); } return change; } /** * Check if there are any changes between two commits */ async hasChanges(fromCommit, toCommit = 'HEAD') { try { // Using --quiet option to just check if changes exist const result = (0, child_process_1.execSync)(`git diff --quiet ${fromCommit} ${toCommit} || echo "has_changes"`, { cwd: this.repoPath }).toString().trim(); return result === 'has_changes'; } catch (error) { // Git diff returns exit code 1 if there are changes return true; } } /** * Get a list of modified lines for a specific file */ async getModifiedLines(filePath, fromCommit, toCommit = 'HEAD') { try { // Use git diff to get line-by-line changes for the file const diffCommand = `git diff -U0 ${fromCommit} ${toCommit} -- "${filePath}"`; const diffOutput = (0, child_process_1.execSync)(diffCommand, { cwd: this.repoPath }).toString(); if (!diffOutput) { return []; } // Parse diff output to extract line numbers const modifiedLines = []; const lines = diffOutput.split('\n'); let currentLine = 0; for (const line of lines) { // Look for hunk headers @@ -a,b +c,d @@ if (line.startsWith('@@')) { const match = line.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/); if (match) { // New file content starts at line number match[3] currentLine = parseInt(match[3], 10); continue; } } // Added or modified lines start with + if (line.startsWith('+') && !line.startsWith('+++')) { modifiedLines.push(currentLine); } // Increment current line for added and context lines, not for removed lines if (line.startsWith('+') || !line.startsWith('-')) { currentLine++; } } return modifiedLines; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(`Failed to get modified lines for ${filePath}: ${errorMsg}`); return []; } } /** * Filter for only code files */ async filterCodeFiles(changes) { // Expanded list of code file extensions const codeExtensions = [ // TypeScript/JavaScript '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', // Python '.py', '.pyi', '.pyx', '.pxd', // Java and JVM '.java', '.kt', '.groovy', '.scala', // C-family '.c', '.cpp', '.h', '.hpp', '.cc', '.cxx', // C# '.cs', // Go '.go', // Rust '.rs', // Swift '.swift', // Web '.html', '.htm', '.css', '.scss', '.sass', '.less', // PHP '.php', // Ruby '.rb', // Shell '.sh', '.bash', '.zsh', // Misc '.hs', '.fs', '.fsx', '.pl', '.r' ]; logger.info(`Filtering ${changes.length} changes for code files`); const filteredChanges = changes.filter(change => { const extension = path.extname(change.path).toLowerCase(); return codeExtensions.includes(extension); }); logger.info(`Found ${filteredChanges.length} code file changes`); return filteredChanges; } /** * Get list of ignored files and directories from .gitignore */ async getIgnoredPaths() { const gitignorePath = path.join(this.repoPath, '.gitignore'); if (!fs.existsSync(gitignorePath)) { return []; } try { const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); return gitignoreContent .split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); } catch (error) { logger.warn(`Failed to read .gitignore: ${error instanceof Error ? error.message : String(error)}`); return []; } } } exports.ChangeDetector = ChangeDetector;