Skip to content

Commit 26d2c40

Browse files
Add line-specific comments functionality
1 parent de87c72 commit 26d2c40

10 files changed

+423
-55
lines changed

config.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"promptStrategy":"modified-files"}
1+
{"promptStrategy":"line-comments"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { type Context } from 'probot'
2+
3+
// Marker to identify our AI analysis comments
4+
const COMMENT_MARKER = '<!-- REVU-AI-ANALYSIS -->'
5+
6+
/**
7+
* Find existing AI analysis comment by looking for the unique marker
8+
*/
9+
async function findExistingAnalysisComment(context: Context, prNumber: number) {
10+
const repo = context.repo()
11+
12+
// Get all comments on the PR
13+
const { data: comments } = await context.octokit.issues.listComments({
14+
...repo,
15+
issue_number: prNumber
16+
})
17+
18+
// Find the comment with our marker
19+
return comments.find((comment) => comment.body.includes(COMMENT_MARKER))
20+
}
21+
22+
/**
23+
* Handles the creation or update of a global comment containing the analysis.
24+
* This is the original behavior of the application.
25+
*/
26+
export async function globalCommentHandler(
27+
context: Context,
28+
prNumber: number,
29+
analysis: string
30+
) {
31+
const repo = context.repo()
32+
33+
// Format the analysis with our marker
34+
const formattedAnalysis = `${COMMENT_MARKER}\n\n${analysis}`
35+
36+
// Check if we already have an analysis comment
37+
const existingComment = await findExistingAnalysisComment(context, prNumber)
38+
39+
if (existingComment) {
40+
// Update the existing comment
41+
await context.octokit.issues.updateComment({
42+
...repo,
43+
comment_id: existingComment.id,
44+
body: formattedAnalysis
45+
})
46+
return `Updated existing analysis comment on PR #${prNumber}`
47+
} else {
48+
// Post a new comment
49+
await context.octokit.issues.createComment({
50+
...repo,
51+
issue_number: prNumber,
52+
body: formattedAnalysis
53+
})
54+
return `Created new analysis comment on PR #${prNumber}`
55+
}
56+
}

src/comment-handlers/index.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { type Context } from 'probot'
2+
import { globalCommentHandler } from './global-comment-handler.ts'
3+
import { lineCommentsHandler } from './line-comments-handler.ts'
4+
5+
/**
6+
* Callback type for comment handlers
7+
*/
8+
export type CommentHandler = (
9+
context: Context,
10+
prNumber: number,
11+
analysis: string
12+
) => Promise<string>
13+
14+
/**
15+
* Gets the appropriate comment handler based on the strategy name.
16+
* This allows for different comment handling strategies based on the prompt strategy.
17+
*
18+
* @param strategyName - The name of the prompt strategy used
19+
* @returns The appropriate comment handler function
20+
*/
21+
export function getCommentHandler(strategyName: string): CommentHandler {
22+
switch (strategyName.toLowerCase()) {
23+
case 'line-comments':
24+
return lineCommentsHandler
25+
case 'default':
26+
case 'modified-files':
27+
default:
28+
return globalCommentHandler
29+
}
30+
}
31+
32+
export { globalCommentHandler, lineCommentsHandler }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { type Context } from 'probot'
2+
import { globalCommentHandler } from './global-comment-handler.ts'
3+
4+
// Marker to identify our AI review comments
5+
const REVIEW_MARKER = '<!-- REVU-AI-REVIEW -->'
6+
7+
/**
8+
* Find existing AI review by looking for the unique marker
9+
*/
10+
async function findExistingReview(context: Context, prNumber: number) {
11+
const repo = context.repo()
12+
13+
// Get all reviews on the PR
14+
const { data: reviews } = await context.octokit.pulls.listReviews({
15+
...repo,
16+
pull_number: prNumber
17+
})
18+
19+
// Find the review with our marker
20+
return reviews.find(
21+
(review) => review.body && review.body.includes(REVIEW_MARKER)
22+
)
23+
}
24+
25+
/**
26+
* Handles the creation of a review with line-specific comments.
27+
* This expects the analysis to be a JSON string with the following structure:
28+
* {
29+
* "summary": "Overall PR summary",
30+
* "comments": [
31+
* {
32+
* "path": "file/path.ts",
33+
* "line": 42,
34+
* "body": "Comment text",
35+
* "suggestion": "Optional suggested code"
36+
* }
37+
* ]
38+
* }
39+
*/
40+
export async function lineCommentsHandler(
41+
context: Context,
42+
prNumber: number,
43+
analysis: string
44+
) {
45+
const repo = context.repo()
46+
47+
try {
48+
// Parse the JSON response
49+
const parsedAnalysis = JSON.parse(analysis)
50+
51+
// Format the summary with our marker
52+
const formattedSummary = `${REVIEW_MARKER}\n\n${parsedAnalysis.summary}`
53+
54+
// Prepare comments for the review
55+
const reviewComments = parsedAnalysis.comments.map((comment) => {
56+
let commentBody = comment.body
57+
58+
// Add suggested code if available
59+
if (comment.suggestion) {
60+
commentBody += '\n\n```suggestion\n' + comment.suggestion + '\n```'
61+
}
62+
63+
return {
64+
path: comment.path,
65+
line: comment.line,
66+
body: commentBody
67+
}
68+
})
69+
70+
// Check if we already have a review for this PR
71+
const existingReview = await findExistingReview(context, prNumber)
72+
73+
if (existingReview) {
74+
// For now, we can't update existing review comments directly with the GitHub API
75+
// So we'll create a new review and mention it's an update
76+
await context.octokit.pulls.createReview({
77+
...repo,
78+
pull_number: prNumber,
79+
event: 'COMMENT',
80+
body: `${REVIEW_MARKER}\n\n**Updated Review:** ${parsedAnalysis.summary}`,
81+
comments: reviewComments
82+
})
83+
84+
return `Updated review with ${reviewComments.length} line comments on PR #${prNumber}`
85+
} else {
86+
// Create a new review with the summary and comments
87+
await context.octokit.pulls.createReview({
88+
...repo,
89+
pull_number: prNumber,
90+
event: 'COMMENT',
91+
body: formattedSummary,
92+
comments: reviewComments
93+
})
94+
}
95+
96+
return `Created review with ${reviewComments.length} line comments on PR #${prNumber}`
97+
} catch (error) {
98+
// In case of error, fall back to the global comment handler
99+
console.error(
100+
'Error parsing or creating line comments, falling back to global comment:',
101+
error
102+
)
103+
return globalCommentHandler(context, prNumber, analysis)
104+
}
105+
}

src/index.ts

+29-46
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,19 @@
11
import { config } from 'dotenv'
2-
import { Probot, type Context } from 'probot'
2+
import * as fs from 'fs/promises'
3+
import * as path from 'path'
4+
import { Probot } from 'probot'
5+
import { getCommentHandler } from './comment-handlers/index.ts'
36
import { sendToAnthropic } from './send-to-anthropic.ts'
47

58
// Load environment variables
69
config()
710

8-
// Marker to identify our AI analysis comments
9-
const COMMENT_MARKER = '<!-- REVU-AI-ANALYSIS -->'
10-
1111
export default async (app: Probot, { getRouter }) => {
1212
app.log.info('Revu GitHub App started!')
1313

1414
// Container health check route
1515
getRouter('/healthz').get('/', (req, res) => res.end('OK'))
1616

17-
/**
18-
* Find existing AI analysis comment by looking for the unique marker
19-
*/
20-
async function findExistingAnalysisComment(context: Context, prNumber) {
21-
const repo = context.repo()
22-
23-
// Get all comments on the PR
24-
const { data: comments } = await context.octokit.issues.listComments({
25-
...repo,
26-
issue_number: prNumber
27-
})
28-
29-
// Find the comment with our marker
30-
return comments.find((comment) => comment.body.includes(COMMENT_MARKER))
31-
}
32-
3317
// Listen for PR opens and updates
3418
app.on(
3519
['pull_request.opened', 'pull_request.synchronize'],
@@ -52,44 +36,43 @@ export default async (app: Probot, { getRouter }) => {
5236
})
5337
.then((response) => response.data.token)
5438

39+
// Get the current strategy from configuration
40+
const strategyName = await getStrategyNameFromConfig()
41+
5542
// Get the analysis from Anthropic
5643
const analysis = await sendToAnthropic({
5744
repositoryUrl,
5845
branch,
59-
token: installationAccessToken
46+
token: installationAccessToken,
47+
strategyName
6048
})
6149

62-
// Format the analysis with our marker
63-
const formattedAnalysis = `${COMMENT_MARKER}\n\n${analysis}`
50+
// Get the appropriate comment handler based on the strategy
51+
const commentHandler = getCommentHandler(strategyName)
6452

65-
// Check if we already have an analysis comment
66-
const existingComment = await findExistingAnalysisComment(
67-
context,
68-
pr.number
69-
)
70-
71-
if (existingComment) {
72-
// Update the existing comment
73-
await context.octokit.issues.updateComment({
74-
...repo,
75-
comment_id: existingComment.id,
76-
body: formattedAnalysis
77-
})
78-
app.log.info(`Updated existing analysis comment on PR #${pr.number}`)
79-
} else {
80-
// Post a new comment
81-
await context.octokit.issues.createComment({
82-
...repo,
83-
issue_number: pr.number,
84-
body: formattedAnalysis
85-
})
86-
app.log.info(`Created new analysis comment on PR #${pr.number}`)
87-
}
53+
// Handle the analysis with the appropriate handler
54+
const result = await commentHandler(context, pr.number, analysis)
8855

56+
app.log.info(result)
8957
app.log.info(`Successfully analyzed PR #${pr.number}`)
9058
} catch (error) {
9159
app.log.error(`Error processing PR #${pr.number}: ${error}`)
9260
}
9361
}
9462
)
63+
64+
/**
65+
* Gets the strategy name from the configuration file
66+
*/
67+
async function getStrategyNameFromConfig() {
68+
try {
69+
const configPath = path.join(process.cwd(), 'config.json')
70+
const configContent = await fs.readFile(configPath, 'utf-8')
71+
const config = JSON.parse(configContent)
72+
return config.promptStrategy || 'default'
73+
} catch (error) {
74+
console.error('Error reading configuration:', error)
75+
return 'default'
76+
}
77+
}
9578
}

src/populate-template.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { getStrategyFromConfig } from './prompt-strategies/index.ts'
1+
import {
2+
getStrategyByName,
3+
getStrategyFromConfig
4+
} from './prompt-strategies/index.ts'
25

36
interface PopulateTemplateOptions {
47
repositoryUrl: string
58
branch: string
69
templatePath?: string
710
token?: string
11+
strategyName?: string
812
}
913

1014
/**
@@ -29,10 +33,13 @@ export async function populateTemplate({
2933
repositoryUrl,
3034
branch,
3135
templatePath,
32-
token
36+
token,
37+
strategyName
3338
}: PopulateTemplateOptions): Promise<string> {
34-
// Get the appropriate strategy based on configuration
35-
const strategy = await getStrategyFromConfig()
39+
// Get the appropriate strategy based on provided strategy name or configuration
40+
const strategy = strategyName
41+
? getStrategyByName(strategyName)
42+
: await getStrategyFromConfig()
3643

3744
// Use the strategy to generate the prompt
3845
return strategy(repositoryUrl, branch, templatePath, token)

src/prompt-strategies/get-strategy.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as fs from 'fs/promises'
22
import * as path from 'path'
33
import { defaultPromptStrategy } from './default-strategy.ts'
44
import { modifiedFilesPromptStrategy } from './modified-files-strategy.ts'
5+
import { lineCommentsPromptStrategy } from './line-comments-strategy.ts'
56
import type { PromptStrategy } from './prompt-strategy.ts'
67

78
/**
@@ -11,11 +12,11 @@ import type { PromptStrategy } from './prompt-strategy.ts'
1112
* @returns The strategy implementation function
1213
*/
1314
export function getStrategyByName(strategyName: string): PromptStrategy {
14-
// Currently only the default strategy is implemented
15-
// Additional strategies can be added here in the future
1615
switch (strategyName.toLowerCase()) {
1716
case 'modified-files':
1817
return modifiedFilesPromptStrategy
18+
case 'line-comments':
19+
return lineCommentsPromptStrategy
1920
case 'default':
2021
default:
2122
return defaultPromptStrategy

0 commit comments

Comments
 (0)