UNPKG

review-copilot

Version:

ReviewCopilot - AI-powered code review assistant with customizable prompts

353 lines (352 loc) • 14.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.initializeReviewContext = initializeReviewContext; exports.reviewCommand = reviewCommand; exports.reviewBranchName = reviewBranchName; exports.reviewCommitMessages = reviewCommitMessages; exports.reviewCodeChanges = reviewCodeChanges; exports.filterAndProcessFiles = filterAndProcessFiles; exports.prepareCodeReviewFilesContent = prepareCodeReviewFilesContent; exports.handleReviewError = handleReviewError; const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); const config_manager_1 = require("../../config/config-manager"); const provider_factory_1 = require("../../providers/provider-factory"); const micromatch_1 = __importDefault(require("micromatch")); const git_platform_factory_1 = require("../../services/git-platform-factory"); const vcs_factory_1 = require("../../utils/vcs-factory"); const environment_helpers_1 = __importDefault(require("../../utils/environment-helpers")); const helpers_1 = require("./helpers"); const line_specific_review_service_1 = require("../../services/line-specific-review.service"); const logger_1 = require("../../utils/logger"); async function initializeReviewContext(options, spinner) { spinner.text = 'Loading configuration...'; const configManager = await config_manager_1.ConfigManager.getInstance(options.config); const factoryConfig = configManager.getConfig(); const enabledProvider = Object.entries(factoryConfig.providers).find(([_, p]) => p.enabled)?.[1]; if (!enabledProvider) { throw new Error('No enabled provider found in configuration'); } const config = { ai: { provider: (Object.entries(factoryConfig.providers).find(([_, p]) => p?.enabled)?.[0] || 'openai'), apiKey: enabledProvider.apiKey, model: enabledProvider.model, baseURL: enabledProvider.baseURL, }, triggers: factoryConfig.triggers, rules: factoryConfig.rules, customReviewPoints: factoryConfig.customReviewPoints, }; spinner.text = 'Initializing AI provider...'; const aiProvider = provider_factory_1.ProviderFactory.createProvider(factoryConfig); const vcs = (0, vcs_factory_1.getVcsProvider)(); let gitService; let prDetails; if (environment_helpers_1.default.isCI) { spinner.text = 'Getting PR details...'; gitService = git_platform_factory_1.GitPlatformFactory.createService(); prDetails = await gitService.getPRDetails(); } return { aiProvider, config, vcs, gitService, prDetails, spinner, }; } async function reviewCommand(options) { const spinner = (0, ora_1.default)('Starting code review...').start(); try { const context = await initializeReviewContext(options, spinner); if (context.config.rules.branchName.enabled) { const results = await reviewBranchName(context); await (0, helpers_1.outputReviewResults)(context, { branchName: [ { success: results.success, suggestions: results.suggestions, error: results.error, }, ], }, { branchName: results.branchName }); } if (context.config.rules.commitMessage.enabled) { const results = await reviewCommitMessages(context); await (0, helpers_1.outputReviewResults)(context, { commitMessages: [ { success: results.success, suggestions: results.suggestions, error: results.error, }, ], }, { commits: results.commits }); } if (context.config.rules.codeChanges.enabled) { const results = await reviewCodeChanges(context); await (0, helpers_1.outputReviewResults)(context, { codeChanges: [ { success: results.success, suggestions: results.suggestions, error: results.error, }, ], }); } spinner.succeed('Code review completed successfully'); return true; } catch (error) { await handleReviewError(spinner, error); return false; } finally { spinner.stop(); } } function buildSuccessResult(suggestions) { return { success: true, suggestions, error: undefined, }; } function buildErrorResult(error) { return { success: false, suggestions: [], error: { message: error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error', }, }; } async function withReviewErrorHandling(fn, fallbackMessage = 'Unknown error') { try { return await fn(); } catch (error) { return buildErrorResult(error); } } async function reviewBranchName(context) { return withReviewErrorHandling(async () => { context.spinner.text = 'Reviewing branch name...'; const branchName = await context.vcs.getCurrentBranchName(); if (!branchName) { return buildErrorResult('Could not get current branch name'); } console.log(chalk_1.default.blue(`\nReviewing branch: ${chalk_1.default.yellow(branchName)}`)); const result = await context.aiProvider.review(context.config.rules.branchName.prompt, branchName); return { ...buildSuccessResult([{ message: result, reviewType: 'general' }]), branchName, }; }, 'Unknown error in branch name review'); } async function reviewCommitMessages(context) { return withReviewErrorHandling(async () => { context.spinner.text = 'Getting commit messages...'; const commits = await context.vcs.getPullRequestCommits(); if (!Array.isArray(commits) || commits.length === 0) { logger_1.Logger.gray('\nNo commit messages to review.'); return { ...buildSuccessResult([]), commits: [] }; } logger_1.Logger.info(`\nFound ${commits.length} commit message(s) to review:`); const results = []; for (const [index, commit] of commits.entries()) { context.spinner.text = `Reviewing commit ${index + 1}/${commits.length}...`; logger_1.Logger.commitInfo(commit, index); try { const result = await context.aiProvider.review(context.config.rules.commitMessage.prompt, commit.message); results.push(buildSuccessResult([ { message: typeof result === 'string' ? result : JSON.stringify(result), reviewType: 'general', }, ])); } catch (error) { results.push(buildErrorResult(`Commit ${commit.hash.slice(0, 7)} - ${commit.author}`)); } } return { ...buildSuccessResult(results.flatMap((result) => result.suggestions)), commits, }; }, 'Failed to get commit messages'); } async function reviewCodeChanges(context) { if (!context.prDetails) { logger_1.Logger.warning('Code changes review is only available in PR/CI environment.'); return buildSuccessResult([ { message: 'Code changes review is only available in PR/CI environment.', reviewType: 'general', }, ]); } return withReviewErrorHandling(async () => { context.spinner.text = 'Getting code changes...'; const pullRequestReviewInfo = await context.vcs.getPullRequestFiles(); if (!pullRequestReviewInfo) { logger_1.Logger.gray('\nNo code changes to review.'); return buildSuccessResult([ { message: 'No code changes found', filename: '', line: 0, severity: 'info', reviewType: 'general', }, ]); } logger_1.Logger.prInfo(pullRequestReviewInfo); const { filteredFiles, skippedFiles } = await filterAndProcessFiles(context, pullRequestReviewInfo.files || []); if (filteredFiles.length === 0) { return buildSuccessResult([ { message: `No files matched the review patterns. ${skippedFiles} file(s) skipped.`, filename: '', line: 0, severity: 'info', reviewType: 'general', }, ]); } context.spinner.text = 'Analyzing code changes...'; console.log('filteredFiles========', filteredFiles); console.log('context.prDetails========', context.prDetails); console.log('context.gitService========', context.gitService); // Enhanced: Use line-specific review service if we have PR details and git service if (context.prDetails && context.gitService) { try { logger_1.Logger.info('\nšŸ” Performing line-specific code review...'); const lineSpecificService = new line_specific_review_service_1.LineSpecificReviewService(context.aiProvider, context.gitService); // Fix: Map filteredFiles to required format for performLineSpecificReview const filesForLineReview = filteredFiles.map((f) => ({ filename: f.filename, changes: f.changes, })); const reviewResult = await lineSpecificService.performLineSpecificReview(filesForLineReview, pullRequestReviewInfo.hash, context.prDetails.owner, context.prDetails.repo, context.prDetails.pullNumber, context.config.rules.codeChanges.prompt); logger_1.Logger.success(`āœ… Posted ${reviewResult.commentsPosted} line-specific comments`); logger_1.Logger.info(`šŸ“‹ Found ${reviewResult.generalSuggestions.length} general suggestions`); // Return combined results return buildSuccessResult([ ...reviewResult.generalSuggestions, ...reviewResult.lineSpecificSuggestions, ]); } catch (error) { logger_1.Logger.warning('āš ļø Line-specific review failed, falling back to general review'); logger_1.Logger.gray(error instanceof Error ? error.message : 'Unknown error'); } } // Fallback: Use traditional general review const reviewContent = prepareCodeReviewFilesContent(filteredFiles); try { const aiResult = await context.aiProvider.review(context.config.rules.codeChanges.prompt, reviewContent); return buildSuccessResult([ { message: aiResult, reviewType: 'general', }, ]); } catch (error) { return buildErrorResult(error); } }, 'Unknown error in code changes review'); } async function filterAndProcessFiles(context, files) { const patterns = context.config.rules.codeChanges.filePatterns || []; const normalizedFiles = files.map((f) => ({ ...f, changes: typeof f.changes === 'string' ? f.changes : '', })); const filteredFiles = normalizedFiles.filter((change) => { const isIgnored = patterns .filter((pattern) => pattern.startsWith('!')) .some((pattern) => { const cleanPattern = pattern.slice(1); return micromatch_1.default.isMatch(change.filename, cleanPattern, { dot: true }); }); if (isIgnored) { logger_1.Logger.gray(`ā­ļø Ignored: ${change.filename}`); return false; } const isIncluded = patterns .filter((pattern) => !pattern.startsWith('!')) .some((pattern) => micromatch_1.default.isMatch(change.filename, pattern, { dot: true })); if (!isIncluded) { logger_1.Logger.gray(`ā­ļø Skipped: ${change.filename} (not matching patterns)`); } else { const changeLength = change.changes.length; logger_1.Logger.success(`āœ… Included: ${change.filename} (${changeLength} bytes)`); } return isIncluded; }); return { filteredFiles, skippedFiles: normalizedFiles.length - filteredFiles.length, }; } function prepareCodeReviewFilesContent(files) { const MAX_FILE_SIZE = 50000; const MAX_TOTAL_SIZE = 100000; const truncatedFiles = files.map((change) => ({ ...change, changes: change.changes.length > MAX_FILE_SIZE ? change.changes.slice(0, MAX_FILE_SIZE) + '\n... (content truncated for size limit)' : change.changes, })); const combinedContent = truncatedFiles .map((change) => `File: ${change.filename}\n${change.changes}\n`) .join('\n---\n\n'); return combinedContent.length > MAX_TOTAL_SIZE ? combinedContent.slice(0, MAX_TOTAL_SIZE) + '\n... (content truncated for total size limit)' : combinedContent; } async function handleReviewError(spinner, error) { spinner.fail('Review failed'); if (error instanceof Error) { logger_1.Logger.error('\nāŒ Error Details:'); logger_1.Logger.warning('Message: ' + error.message); const message = error.message; const errorDetails = message.split('Details: ')[1]; if (errorDetails) { try { const errorObj = JSON.parse(errorDetails); if (errorObj.error) { logger_1.Logger.warning('\nšŸ¤– AI Provider Error:'); logger_1.Logger.gray('Type: ' + errorObj.error.type); logger_1.Logger.gray('Code: ' + errorObj.error.code); logger_1.Logger.gray('Message: ' + errorObj.error.message); } } catch { logger_1.Logger.gray('Raw error details: ' + errorDetails); } } if (error.stack) { logger_1.Logger.gray('\nšŸ“‹ Stack Trace:'); logger_1.Logger.gray(error.stack); } } else { logger_1.Logger.error('\nāŒ Unknown Error: ' + String(error)); } }