pr-vibe
Version:
AI-powered PR review responder that vibes with CodeRabbit, DeepSource, and other bots to automate repetitive feedback
205 lines (180 loc) • 7.32 kB
JavaScript
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,
bodyPreview: r.body ? r.body.substring(0, 100) + '...' : '',
state: r.state,
commentsCount: r.comments?.length || 0,
commentPreviews: r.comments?.slice(0, 2).map(c => ({
body: c.body?.substring(0, 50) + '...',
path: c.path,
line: c.line
}))
}))
};
if (options.debug) {
console.log('Debug: Found comments/reviews:', JSON.stringify(debugInfo, null, 2));
}
// Combine all comments - normalize the structure
const allComments = [
...pr.comments.map(c => ({
...c,
type: 'issue',
user: c.user || c.author || (c.author ? { login: c.author } : null),
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: c.id || `review-comment-${Date.now()}-${Math.random()}`,
user: { login: r.author },
body: c.body || '', // Ensure body is never undefined
type: 'pr_review_comment',
review_id: r.id,
created_at: c.created_at || r.submitted_at // Fallback to review timestamp
})))
];
// Separate bot and human comments using the bot detector
const botComments = [];
const humanComments = [];
const skippedComments = [];
if (options.debug) {
console.log(`Debug: Processing ${allComments.length} total comments`);
}
allComments.forEach(comment => {
const username = comment.user?.login || comment.author?.login || '';
if (options.debug && (comment.type === 'pr_review_comment' || username.toLowerCase().includes('rabbit'))) {
console.log(`Debug: Processing comment from ${username} (${comment.type})`);
console.log(` - Has body: ${!!comment.body}`);
console.log(` - Body preview: ${comment.body ? comment.body.substring(0, 80) + '...' : 'NO BODY'}`);
console.log(` - ID: ${comment.id}`);
if (comment.review_id) {
console.log(` - From review: ${comment.review_id}`);
}
}
// 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 (options.debug && username.includes('coderabbit')) {
console.log(`Debug: Processing ${username} (${comment.type}):`, shouldProcess);
}
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,
body: comment.body?.substring(0, 50) + '...'
});
}
});
// Group into threads
const botThreads = groupIntoThreads(botComments);
const humanThreads = groupIntoThreads(humanComments);
if (options.debug) {
console.log('\nDebug Summary:');
console.log(` - Total comments processed: ${allComments.length}`);
console.log(` - Bot comments found: ${botComments.length}`);
console.log(` - Human comments found: ${humanComments.length}`);
console.log(` - Skipped comments: ${skippedComments.length}`);
if (botComments.length > 0) {
console.log('\nBot comments details:');
botComments.forEach((c, i) => {
console.log(` ${i + 1}. ${c.botType} (${c.type}): ${c.body?.substring(0, 60)}...`);
});
}
if (skippedComments.length > 0) {
console.log('\nSkipped comments:');
skippedComments.forEach((s, i) => {
console.log(` ${i + 1}. ${s.username}: ${s.reason} (confidence: ${s.confidence})`);
});
}
}
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;
}