UNPKG

sfcoe-ailabs

Version:

AI-powered code review tool with static analysis integration for comprehensive code quality assessment.

111 lines (110 loc) 4.47 kB
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)}`); } } }