Skip to content

Commit 5d85b0a

Browse files
committed
adding review comments grouped as threads
1 parent f197a9f commit 5d85b0a

File tree

5 files changed

+358
-144
lines changed

5 files changed

+358
-144
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ Possible options:
991991
2. get_diff - Get the diff of a pull request.
992992
3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.
993993
4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.
994-
5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.
994+
5. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.
995995
6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.
996996
7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.
997997
(string, required)

pkg/github/__toolsnaps__/pull_request_read.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"properties": {
1616
"method": {
1717
"type": "string",
18-
"description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n",
18+
"description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n",
1919
"enum": [
2020
"get",
2121
"get_diff",

pkg/github/pullrequests.go

Lines changed: 116 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
)
2222

2323
// PullRequestRead creates a tool to get details of a specific pull request.
24-
func PullRequestRead(getClient GetClientFn, cache *lockdown.RepoAccessCache, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
24+
func PullRequestRead(getClient GetClientFn, getGQLClient GetGQLClientFn, cache *lockdown.RepoAccessCache, t translations.TranslationHelperFunc, flags FeatureFlags) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
2525
schema := &jsonschema.Schema{
2626
Type: "object",
2727
Properties: map[string]*jsonschema.Schema{
@@ -33,7 +33,7 @@ Possible options:
3333
2. get_diff - Get the diff of a pull request.
3434
3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.
3535
4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.
36-
5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.
36+
5. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.
3737
6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.
3838
7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.
3939
`,
@@ -107,7 +107,11 @@ Possible options:
107107
result, err := GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination)
108108
return result, nil, err
109109
case "get_review_comments":
110-
result, err := GetPullRequestReviewComments(ctx, client, cache, owner, repo, pullNumber, pagination, flags)
110+
gqlClient, err := getGQLClient(ctx)
111+
if err != nil {
112+
return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil
113+
}
114+
result, err := GetPullRequestReviewComments(ctx, gqlClient, cache, owner, repo, pullNumber, pagination, flags)
111115
return result, nil, err
112116
case "get_reviews":
113117
result, err := GetPullRequestReviews(ctx, client, cache, owner, repo, pullNumber, flags)
@@ -282,54 +286,130 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo
282286
return utils.NewToolResultText(string(r)), nil
283287
}
284288

285-
func GetPullRequestReviewComments(ctx context.Context, client *github.Client, cache *lockdown.RepoAccessCache, owner, repo string, pullNumber int, pagination PaginationParams, ff FeatureFlags) (*mcp.CallToolResult, error) {
286-
opts := &github.PullRequestListCommentsOptions{
287-
ListOptions: github.ListOptions{
288-
PerPage: pagination.PerPage,
289-
Page: pagination.Page,
290-
},
289+
// GraphQL types for review threads query
290+
type reviewThreadsQuery struct {
291+
Repository struct {
292+
PullRequest struct {
293+
ReviewThreads struct {
294+
Nodes []reviewThreadNode
295+
PageInfo pageInfoFragment
296+
TotalCount githubv4.Int
297+
} `graphql:"reviewThreads(first: $first, after: $after)"`
298+
} `graphql:"pullRequest(number: $prNum)"`
299+
} `graphql:"repository(owner: $owner, name: $repo)"`
300+
}
301+
302+
type reviewThreadNode struct {
303+
ID githubv4.ID
304+
IsResolved githubv4.Boolean
305+
IsOutdated githubv4.Boolean
306+
IsCollapsed githubv4.Boolean
307+
Comments struct {
308+
Nodes []reviewCommentNode
309+
TotalCount githubv4.Int
310+
} `graphql:"comments(first: $commentsPerThread)"`
311+
}
312+
313+
type reviewCommentNode struct {
314+
ID githubv4.ID
315+
Body githubv4.String
316+
Path githubv4.String
317+
Line *githubv4.Int
318+
Author struct {
319+
Login githubv4.String
291320
}
321+
CreatedAt githubv4.DateTime
322+
UpdatedAt githubv4.DateTime
323+
URL githubv4.URI
324+
}
325+
326+
type pageInfoFragment struct {
327+
HasNextPage githubv4.Boolean
328+
HasPreviousPage githubv4.Boolean
329+
StartCursor githubv4.String
330+
EndCursor githubv4.String
331+
}
292332

293-
comments, resp, err := client.PullRequests.ListComments(ctx, owner, repo, pullNumber, opts)
333+
func GetPullRequestReviewComments(ctx context.Context, gqlClient *githubv4.Client, cache *lockdown.RepoAccessCache, owner, repo string, pullNumber int, pagination PaginationParams, ff FeatureFlags) (*mcp.CallToolResult, error) {
334+
// Convert pagination parameters to GraphQL format
335+
gqlParams, err := pagination.ToGraphQLParams()
294336
if err != nil {
295-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
296-
"failed to get pull request review comments",
297-
resp,
298-
err,
299-
), nil
337+
return utils.NewToolResultError(fmt.Sprintf("invalid pagination parameters: %v", err)), nil
300338
}
301-
defer func() { _ = resp.Body.Close() }()
302339

303-
if resp.StatusCode != http.StatusOK {
304-
body, err := io.ReadAll(resp.Body)
305-
if err != nil {
306-
return nil, fmt.Errorf("failed to read response body: %w", err)
307-
}
308-
return utils.NewToolResultError(fmt.Sprintf("failed to get pull request review comments: %s", string(body))), nil
340+
// Default to 100 threads if not specified, max is 100 for GraphQL
341+
perPage := int32(100)
342+
if gqlParams.First != nil && *gqlParams.First > 0 {
343+
perPage = *gqlParams.First
309344
}
310345

346+
// Build variables for GraphQL query
347+
vars := map[string]interface{}{
348+
"owner": githubv4.String(owner),
349+
"repo": githubv4.String(repo),
350+
"prNum": githubv4.Int(int32(pullNumber)), //nolint:gosec // pullNumber is controlled by user input validation
351+
"first": githubv4.Int(perPage),
352+
"commentsPerThread": githubv4.Int(50), // Max 50 comments per thread
353+
}
354+
355+
// Add cursor if provided
356+
if gqlParams.After != nil && *gqlParams.After != "" {
357+
vars["after"] = githubv4.String(*gqlParams.After)
358+
} else {
359+
vars["after"] = (*githubv4.String)(nil)
360+
}
361+
362+
// Execute GraphQL query
363+
var query reviewThreadsQuery
364+
if err := gqlClient.Query(ctx, &query, vars); err != nil {
365+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
366+
"failed to get pull request review threads",
367+
err,
368+
), nil
369+
}
370+
371+
// Lockdown mode filtering
311372
if ff.LockdownMode {
312373
if cache == nil {
313374
return nil, fmt.Errorf("lockdown cache is not configured")
314375
}
315-
filteredComments := make([]*github.PullRequestComment, 0, len(comments))
316-
for _, comment := range comments {
317-
user := comment.GetUser()
318-
if user == nil {
319-
continue
320-
}
321-
isSafeContent, err := cache.IsSafeContent(ctx, user.GetLogin(), owner, repo)
322-
if err != nil {
323-
return utils.NewToolResultError(fmt.Sprintf("failed to check lockdown mode: %v", err)), nil
324-
}
325-
if isSafeContent {
326-
filteredComments = append(filteredComments, comment)
376+
377+
// Iterate through threads and filter comments
378+
for i := range query.Repository.PullRequest.ReviewThreads.Nodes {
379+
thread := &query.Repository.PullRequest.ReviewThreads.Nodes[i]
380+
filteredComments := make([]reviewCommentNode, 0, len(thread.Comments.Nodes))
381+
382+
for _, comment := range thread.Comments.Nodes {
383+
login := string(comment.Author.Login)
384+
if login != "" {
385+
isSafeContent, err := cache.IsSafeContent(ctx, login, owner, repo)
386+
if err != nil {
387+
return nil, fmt.Errorf("failed to check lockdown mode: %w", err)
388+
}
389+
if isSafeContent {
390+
filteredComments = append(filteredComments, comment)
391+
}
392+
}
327393
}
394+
395+
thread.Comments.Nodes = filteredComments
396+
thread.Comments.TotalCount = githubv4.Int(int32(len(filteredComments))) //nolint:gosec // comment count is bounded by API limits
328397
}
329-
comments = filteredComments
330398
}
331399

332-
r, err := json.Marshal(comments)
400+
// Build response with review threads and pagination info
401+
response := map[string]interface{}{
402+
"reviewThreads": query.Repository.PullRequest.ReviewThreads.Nodes,
403+
"pageInfo": map[string]interface{}{
404+
"hasNextPage": query.Repository.PullRequest.ReviewThreads.PageInfo.HasNextPage,
405+
"hasPreviousPage": query.Repository.PullRequest.ReviewThreads.PageInfo.HasPreviousPage,
406+
"startCursor": string(query.Repository.PullRequest.ReviewThreads.PageInfo.StartCursor),
407+
"endCursor": string(query.Repository.PullRequest.ReviewThreads.PageInfo.EndCursor),
408+
},
409+
"totalCount": int(query.Repository.PullRequest.ReviewThreads.TotalCount),
410+
}
411+
412+
r, err := json.Marshal(response)
333413
if err != nil {
334414
return nil, fmt.Errorf("failed to marshal response: %w", err)
335415
}

0 commit comments

Comments
 (0)