difflytic
Version:
AI-powered code review for GitLab/GitHub MRs using OpenAI and more.
170 lines (169 loc) • 6.61 kB
JavaScript
;
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();
}