UNPKG

ds-sfcoe-ailabs

Version:

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

160 lines (159 loc) 6.91 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 * * @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 a map of file paths to arrays of changed line ranges * @throws Error if Git diff operation fails * @example * ```typescript * const changedLines = await GitHelper.getChangedLineRanges('/path/to/repo', 'main', 'feature-branch'); * ``` */ 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; } /** * Gets the status of files changed between two commits * Returns a map of file paths to their status ('A'=added, 'M'=modified, 'D'=deleted, 'R'=renamed) * * @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 a map of file paths to their Git status * @throws Error if Git diff operation fails * @example * ```typescript * const statuses = await GitHelper.getFileStatuses('/path/to/repo', 'main', 'feature-branch'); * console.log(statuses.get('src/file.ts')); // 'M' for modified, 'A' for added, etc. * ``` */ static async getFileStatuses(repoDir, from, to) { const result = await execa('git', ['diff', '--name-status', `${from}...${to}`], { cwd: repoDir }); const fileStatuses = new Map(); for (const line of result.stdout.split('\n')) { if (line.trim()) { const parts = line.trim().split('\t'); if (parts.length >= 2) { const status = parts[0].charAt(0); // Get first character (A, M, D, R, etc.) const filename = parts[parts.length - 1]; // Get the last part (filename) fileStatuses.set(filename, status); } } } return fileStatuses; } /** * Finds the git root directory by traversing up the directory tree * * @param startDir - Optional starting directory to search from (defaults to current working directory) * @returns Promise resolving to the absolute path of the Git repository root * @throws Error if no Git repository is found or Git operation fails * @example * ```typescript * const gitRoot = await GitHelper.findGitRoot('/path/to/subdirectory'); * console.log(gitRoot); // '/path/to/repo' * ``` */ 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)}`); } } }