sfcoe-ailabs
Version:
AI-powered code review tool with static analysis integration for comprehensive code quality assessment.
111 lines (110 loc) • 4.47 kB
JavaScript
import * as path from 'node:path';
import * as process from 'node:process';
import { simpleGit } from 'simple-git';
import fse from 'fs-extra';
// SimpleGit options
const options = {
binary: 'git',
maxConcurrentProcesses: 6,
trimmed: false,
};
const { execa } = await import('execa');
export default class GitHelper {
/**
* Create a temporary directory containing only the changed files between two Git references
*
* @param repoDir - The path to the Git repository directory
* @param from - The source commit/branch reference for diff comparison
* @param to - The target commit/branch reference for diff comparison
* @returns Promise resolving to the path of the temporary directory containing changed files
* @throws Error if Git operations fail or file copying encounters issues
*/
static async createDirectoryWithDiff(repoDir, from, to) {
options.baseDir = path.resolve(repoDir);
const tmpDir = path.join(options.baseDir, 'tmp-diff');
const git = simpleGit(options);
// Get changed files
const diff = await git.diff([`${from}..${to}`, '--name-only']);
const changedFiles = diff
.split('\n')
.filter((file) => file && !file.includes('staticresource'));
// Ensure temp directory is clean
fse.removeSync(tmpDir);
fse.ensureDirSync(tmpDir);
// Copy changed files to tmp preserving structure
for (const file of changedFiles) {
const src = path.join(repoDir, file);
const dest = path.join(tmpDir, file);
// Skip if source file doesn't exist (deleted files)
if (!fse.existsSync(src)) {
// Skip deleted files
continue;
}
fse.ensureDirSync(path.dirname(dest));
fse.copySync(src, dest);
}
return tmpDir;
}
/**
* Verify and resolve a commit SHA to its full form
*
* @param repoDir - The path to the Git repository directory
* @param commitSha - The commit SHA or reference to verify (can be short SHA, HEAD, branch name, etc.)
* @returns Promise resolving to the full commit SHA
* @throws Error if the commit SHA cannot be verified or resolved
*/
static async verifyCommitSha(repoDir, commitSha) {
options.baseDir = path.resolve(repoDir);
const git = simpleGit(options);
const commit = await git.revparse(['--verify', commitSha]);
return commit;
}
/**
* Gets changed line ranges for each file between two commits
*/
static async getChangedLineRanges(repoDir, from, to) {
const result = await execa('git', ['diff', '--unified=0', `${from}...${to}`], { cwd: repoDir });
const changedLines = new Map();
let currentFile = '';
for (const line of result.stdout.split('\n')) {
if (line.startsWith('+++')) {
// Extract filename from +++ b/filename
const match = line.match(/^\+\+\+ b\/(.+)$/);
if (match) {
currentFile = match[1];
}
}
else if (line.startsWith('@@')) {
// Parse hunk header: @@ -old_start,old_count +new_start,new_count @@
const match = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
if (match && currentFile) {
const startLine = parseInt(match[1], 10);
const lineCount = match[2] ? parseInt(match[2], 10) : 1;
const endLine = startLine + lineCount - 1;
if (!changedLines.has(currentFile)) {
changedLines.set(currentFile, []);
}
changedLines
.get(currentFile)
.push({ start: startLine, end: endLine });
}
}
}
return changedLines;
}
/**
* Finds the git root directory by traversing up the directory tree
*/
static async findGitRoot(startDir) {
const cwd = startDir ?? process.cwd();
try {
const result = await execa('git', ['rev-parse', '--show-toplevel'], {
cwd,
});
return result.stdout.trim();
}
catch (error) {
throw new Error(`Failed to find git root from ${cwd}: ${String(error)}`);
}
}
}