review-copilot
Version:
ReviewCopilot - AI-powered code review assistant with customizable prompts
353 lines (352 loc) ⢠14.9 kB
JavaScript
;
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));
}
}