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
JavaScript
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;
;