UNPKG

pr-vibe

Version:

AI-powered PR review responder that vibes with CodeRabbit, DeepSource, and other bots to automate repetitive feedback

156 lines (137 loc) 5.11 kB
import { execSync } from 'child_process'; import { GitHubProvider } from './providers/github-provider.js'; import { botDetector } from './bot-detector.js'; export async function analyzeGitHubPR(prNumber, repo, options = {}) { try { const provider = new GitHubProvider({ repo }); // Use gh CLI for now (simpler for prototype) const prJson = execSync(`gh pr view ${prNumber} --repo ${repo} --json title,body,state,comments,reviews`, { encoding: 'utf-8' }); const pr = JSON.parse(prJson); // Get review comments (inline) const reviewCommentsJson = execSync(`gh api repos/${repo}/pulls/${prNumber}/comments`, { encoding: 'utf-8' }); const reviewComments = JSON.parse(reviewCommentsJson); // Get PR reviews (this is where CodeRabbit posts!) const reviews = await provider.getReviews(prNumber); // Debug info for what we found const debugInfo = { issueComments: pr.comments.length, reviewComments: reviewComments.length, prReviews: reviews.length, reviewDetails: reviews.map(r => ({ author: r.author, hasBody: !!r.body, state: r.state, commentsCount: r.comments?.length || 0 })) }; if (options.debug) { console.log('Debug: Found comments/reviews:', debugInfo); } // Combine all comments - normalize the structure const allComments = [ ...pr.comments.map(c => ({ ...c, type: 'issue', user: c.user || c.author, created_at: c.created_at || c.createdAt })), ...reviewComments.map(c => ({ ...c, type: 'review_comment' })), // Add PR reviews as comments ...reviews.map(r => ({ id: `review-${r.id}`, user: { login: r.author }, body: r.body, created_at: r.submitted_at, type: 'pr_review', state: r.state, review_comments: r.comments })), // Also add individual review comments from PR reviews ...reviews.flatMap(r => r.comments.map(c => ({ ...c, id: `review-comment-${c.id}`, user: { login: r.author }, type: 'pr_review_comment', review_id: r.id }))) ]; // Separate bot and human comments using the bot detector const botComments = []; const humanComments = []; const skippedComments = []; allComments.forEach(comment => { const username = comment.user?.login || comment.author?.login || ''; // Skip Vercel bot if (username.toLowerCase() === 'vercel' || username.toLowerCase() === 'vercel[bot]') { skippedComments.push({ username, reason: 'vercel_bot' }); return; } // Use bot detector with nit filtering options const shouldProcess = botDetector.shouldProcessComment( username, comment.body || '', comment.type, { skipNits: options.skipNits, nitsOnly: options.nitsOnly } ); if (shouldProcess.process && shouldProcess.details?.isBot) { // It's a bot comment that should be processed botComments.push({ ...comment, isBot: true, botType: shouldProcess.details.botType, confidence: shouldProcess.confidence, isNit: shouldProcess.details.isNit, nitPattern: shouldProcess.details.nitPattern }); } else if (!shouldProcess.process && shouldProcess.reason === 'not_a_bot') { // It's a human comment if (username && !username.includes('github-actions')) { humanComments.push({ ...comment, isBot: false }); } } else { // Bot comment that should be skipped skippedComments.push({ username, reason: shouldProcess.reason, confidence: shouldProcess.confidence, isNit: shouldProcess.details?.isNit, nitPattern: shouldProcess.details?.nitPattern }); } }); // Group into threads const botThreads = groupIntoThreads(botComments); const humanThreads = groupIntoThreads(humanComments); return { pr, comments: botComments, // Maintain backward compatibility threads: botThreads, // Maintain backward compatibility botComments, humanComments, botThreads, humanThreads, allComments: [...botComments, ...humanComments], debugInfo: options.debug ? { ...debugInfo, skippedComments } : undefined }; } catch (error) { throw new Error(`Failed to fetch PR: ${error.message}`); } } function groupIntoThreads(comments) { const threads = {}; comments.forEach(comment => { // For review comments, use the discussion thread ID const threadId = comment.in_reply_to_id || comment.id; if (!threads[threadId]) { threads[threadId] = []; } threads[threadId].push(comment); }); // Sort threads by timestamp Object.values(threads).forEach(thread => { thread.sort((a, b) => new Date(a.created_at || a.createdAt) - new Date(b.created_at || b.createdAt)); }); return threads; }