UNPKG

difflytic

Version:

AI-powered code review for GitLab/GitHub MRs using OpenAI and more.

170 lines (169 loc) 6.61 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = reviewCommand; const GitLabProvider_1 = require("../../providers/GitLabProvider"); const OpenAI_1 = require("../../ai/OpenAI"); function exitWithError(message, code = 1) { console.error(`Error: ${message}`); process.exit(code); } function parseArgs(args) { let mode = 'file'; let provider = args[0] || 'gitlab'; let projectId = args[1]; let mergeRequestIid = args[2]; for (const arg of args) { if (arg.startsWith('--mode=')) { const val = arg.split('=')[1]; if (val === 'block' || val === 'function' || val === 'file') mode = val; } } return { provider, projectId, mergeRequestIid, mode }; } function getFirstValidNewLine(diff) { // Find the first @@ ... +start,count @@ hunk header const hunkMatch = diff.match(/^@@.*\+(\d+)(?:,(\d+))?/m); if (!hunkMatch) return null; const startLine = parseInt(hunkMatch[1], 10); // Find the first line in the hunk that starts with '+' (but not '+++') const lines = diff.split('\n'); let currentLine = startLine; let inHunk = false; for (const line of lines) { if (line.startsWith('@@')) { inHunk = true; continue; } if (inHunk) { if (line.startsWith('+') && !line.startsWith('+++')) { return currentLine; } if (!line.startsWith('-')) { currentLine++; } } } return startLine; // fallback to hunk start } async function reviewCommand() { const args = process.argv.slice(3); // skip 'review' command const { provider, projectId, mergeRequestIid, mode } = parseArgs(args); console.log('Parsed arguments:', parseArgs(args)); if (!projectId || !mergeRequestIid) { exitWithError('Usage: review gitlab <projectId> <mergeRequestIid> [--mode=file|block|function]'); } if (provider !== 'gitlab') { exitWithError('Only gitlab provider is supported in this demo.'); } let providerInstance; let ai; try { providerInstance = new GitLabProvider_1.GitLabProvider(); } catch (err) { exitWithError(`GitLabProvider error: ${err.message || err}`); return; } try { ai = new OpenAI_1.OpenAI(); } catch (err) { exitWithError(`OpenAI initialization error: ${err.message || err}`); return; } try { const mrData = await providerInstance.fetchMergeRequestData(projectId, mergeRequestIid); const changes = mrData.changes || []; const baseSha = mrData.diff_refs?.base_sha; const startSha = mrData.diff_refs?.start_sha; const headSha = mrData.diff_refs?.head_sha; if (changes.length === 0) { console.log('No file changes found in this merge request.'); return; } let needsAdjustment = false; for (const file of changes) { const diff = file.diff || ''; if (!diff) continue; // Only send added lines (lines starting with a single '+', not '+++') to the AI const addedLines = diff .split('\n') .filter((line) => line.startsWith('+') && !line.startsWith('+++')) .map((line) => line.slice(1)); const addedCode = addedLines.join('\n'); console.log('\n========================================'); console.log(`Review for file: ${file.new_path}`); console.log('----------------------------------------'); let review = ''; try { review = await ai.analyzeCode(addedCode, mode); } catch (aiErr) { console.error('OpenAI API error for this file:', aiErr.message || aiErr); review = ''; } if (review && review.trim()) { needsAdjustment = true; console.log(review.trim()); // Find a valid line to comment on const validLine = getFirstValidNewLine(diff); if (validLine !== null) { const position = { base_sha: baseSha, start_sha: startSha, head_sha: headSha, position_type: 'text', new_path: file.new_path, new_line: validLine }; try { await providerInstance.createDiscussion(projectId, mergeRequestIid, review.trim(), position); console.log('AI review comment posted to MR.'); } catch (err) { if (err.response && err.response.text) { const errorBody = await err.response.text(); console.error('Failed to post comment to MR:', err.message || err, errorBody); } else { console.error('Failed to post comment to MR:', err.message || err); } } } else { console.log('No valid added line found in diff to post comment.'); } } else { console.log('No suggestions or feedback from AI.'); } console.log('========================================\n'); } // Add label based on review result const label = needsAdjustment ? 'Needs Adjustment' : 'Review Passed'; try { await providerInstance.updateMergeRequestLabels(projectId, mergeRequestIid, [label]); console.log(`Label '${label}' applied to MR.`); } catch (err) { console.error('Failed to update MR label:', err.message || err); } } catch (err) { if (err.name === 'FetchError' || err.code === 'ENOTFOUND') { exitWithError('Network error: Could not reach GitLab or OpenAI API.'); } else if (err.message && err.message.includes('Failed to fetch MR data')) { exitWithError('Could not fetch merge request data. Please check your projectId and mergeRequestIid.'); } else { exitWithError(`Unexpected error during review: ${err.message || err}`); } } } if (require.main === module) { reviewCommand(); }