Skip to content

Commit 35acc92

Browse files
feat: Add get_commits method to pull_request_read (#2608)
* feat: Add get_commits method to pull_request_read * Add nil check and additional test case --------- Co-authored-by: Sam Morrow <info@sam-morrow.com>
1 parent 1654d32 commit 35acc92

6 files changed

Lines changed: 270 additions & 10 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,10 +1122,11 @@ The following sets of tools are available:
11221122
2. get_diff - Get the diff of a pull request.
11231123
3. get_status - Get combined commit status of a head commit in a pull request.
11241124
4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.
1125-
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.
1126-
6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.
1127-
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.
1128-
8. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.
1125+
5. get_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.
1126+
6. 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.
1127+
7. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.
1128+
8. 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.
1129+
9. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.
11291130
(string, required)
11301131
- `owner`: Repository owner (string, required)
11311132
- `page`: Page number for pagination (min 1) (number, optional)

pkg/github/__toolsnaps__/pull_request_read.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
"type": "string"
1212
},
1313
"method": {
14-
"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 combined commit status of a head commit in a pull request.\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. Use with pagination parameters to control the number of results returned.\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 8. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.\n",
14+
"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 combined commit status of a head commit in a pull request.\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_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.\n 6. 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 7. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.\n 8. 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 9. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.\n",
1515
"enum": [
1616
"get",
1717
"get_diff",
1818
"get_status",
1919
"get_files",
20+
"get_commits",
2021
"get_review_comments",
2122
"get_reviews",
2223
"get_comments",

pkg/github/helper_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const (
6969
// Pull request endpoints
7070
GetReposPullsByOwnerByRepo = "GET /repos/{owner}/{repo}/pulls"
7171
GetReposPullsByOwnerByRepoByPullNumber = "GET /repos/{owner}/{repo}/pulls/{pull_number}"
72+
GetReposPullsCommitsByOwnerByRepoByPullNumber = "GET /repos/{owner}/{repo}/pulls/{pull_number}/commits"
7273
GetReposPullsFilesByOwnerByRepoByPullNumber = "GET /repos/{owner}/{repo}/pulls/{pull_number}/files"
7374
GetReposPullsReviewsByOwnerByRepoByPullNumber = "GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews"
7475
PostReposPullsByOwnerByRepo = "POST /repos/{owner}/{repo}/pulls"

pkg/github/minimal_types.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ type MinimalPRFile struct {
123123
PreviousFilename string `json:"previous_filename,omitempty"`
124124
}
125125

126+
// MinimalPullRequestCommit is the trimmed output type for commits listed on a pull request.
127+
type MinimalPullRequestCommit struct {
128+
SHA string `json:"sha"`
129+
HTMLURL string `json:"html_url,omitempty"`
130+
Message string `json:"message,omitempty"`
131+
Author *MinimalCommitAuthor `json:"author,omitempty"`
132+
}
133+
126134
// MinimalCommit is the trimmed output type for commit objects.
127135
type MinimalCommit struct {
128136
SHA string `json:"sha"`
@@ -1609,6 +1617,44 @@ func convertToMinimalPRFiles(files []*github.CommitFile) []MinimalPRFile {
16091617
return result
16101618
}
16111619

1620+
func convertToMinimalPullRequestCommits(commits []*github.RepositoryCommit) []MinimalPullRequestCommit {
1621+
result := make([]MinimalPullRequestCommit, 0, len(commits))
1622+
for _, commit := range commits {
1623+
if commit == nil {
1624+
continue
1625+
}
1626+
1627+
minimalCommit := MinimalPullRequestCommit{
1628+
SHA: commit.GetSHA(),
1629+
HTMLURL: commit.GetHTMLURL(),
1630+
}
1631+
1632+
if commit.Commit != nil {
1633+
minimalCommit.Message = commit.Commit.GetMessage()
1634+
minimalCommit.Author = convertToMinimalCommitAuthor(commit.Commit.Author)
1635+
}
1636+
1637+
result = append(result, minimalCommit)
1638+
}
1639+
return result
1640+
}
1641+
1642+
func convertToMinimalCommitAuthor(author *github.CommitAuthor) *MinimalCommitAuthor {
1643+
if author == nil {
1644+
return nil
1645+
}
1646+
1647+
minimalAuthor := &MinimalCommitAuthor{
1648+
Name: author.GetName(),
1649+
Email: author.GetEmail(),
1650+
}
1651+
if author.Date != nil {
1652+
minimalAuthor.Date = author.Date.Format(time.RFC3339)
1653+
}
1654+
1655+
return minimalAuthor
1656+
}
1657+
16121658
// convertToMinimalBranch converts a GitHub API Branch to MinimalBranch
16131659
func convertToMinimalBranch(branch *github.Branch) MinimalBranch {
16141660
return MinimalBranch{

pkg/github/pullrequests.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ Possible options:
3636
2. get_diff - Get the diff of a pull request.
3737
3. get_status - Get combined commit status of a head commit in a pull request.
3838
4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.
39-
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.
40-
6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.
41-
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.
42-
8. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.
39+
5. get_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.
40+
6. 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.
41+
7. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned.
42+
8. 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.
43+
9. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.
4344
`,
44-
Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"},
45+
Enum: []any{"get", "get_diff", "get_status", "get_files", "get_commits", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"},
4546
},
4647
"owner": {
4748
Type: "string",
@@ -130,6 +131,9 @@ Possible options:
130131
case "get_files":
131132
result, err := GetPullRequestFiles(ctx, client, owner, repo, pullNumber, pagination)
132133
return attachIFC(result), nil, err
134+
case "get_commits":
135+
result, err := GetPullRequestCommits(ctx, client, owner, repo, pullNumber, pagination)
136+
return attachIFC(result), nil, err
133137
case "get_review_comments":
134138
gqlClient, err := deps.GetGQLClient(ctx)
135139
if err != nil {
@@ -382,6 +386,34 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo
382386
return MarshalledTextResult(minimalFiles), nil
383387
}
384388

389+
func GetPullRequestCommits(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
390+
opts := &github.ListOptions{
391+
PerPage: pagination.PerPage,
392+
Page: pagination.Page,
393+
}
394+
commits, resp, err := client.PullRequests.ListCommits(ctx, owner, repo, pullNumber, opts)
395+
if err != nil {
396+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
397+
"failed to get pull request commits",
398+
resp,
399+
err,
400+
), nil
401+
}
402+
defer func() { _ = resp.Body.Close() }()
403+
404+
if resp.StatusCode != http.StatusOK {
405+
body, err := io.ReadAll(resp.Body)
406+
if err != nil {
407+
return nil, fmt.Errorf("failed to read response body: %w", err)
408+
}
409+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request commits", resp, body), nil
410+
}
411+
412+
minimalCommits := convertToMinimalPullRequestCommits(commits)
413+
414+
return MarshalledTextResult(minimalCommits), nil
415+
}
416+
385417
// GraphQL types for review threads query
386418
type reviewThreadsQuery struct {
387419
Repository struct {

pkg/github/pullrequests_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,185 @@ func Test_GetPullRequestFiles(t *testing.T) {
12601260
}
12611261
}
12621262

1263+
func Test_GetPullRequestCommits(t *testing.T) {
1264+
// Verify tool definition once
1265+
serverTool := PullRequestRead(translations.NullTranslationHelper)
1266+
tool := serverTool.Tool
1267+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
1268+
1269+
assert.Equal(t, "pull_request_read", tool.Name)
1270+
assert.NotEmpty(t, tool.Description)
1271+
schema := tool.InputSchema.(*jsonschema.Schema)
1272+
assert.Contains(t, schema.Properties, "method")
1273+
assert.Contains(t, schema.Properties, "owner")
1274+
assert.Contains(t, schema.Properties, "repo")
1275+
assert.Contains(t, schema.Properties, "pullNumber")
1276+
assert.Contains(t, schema.Properties, "page")
1277+
assert.Contains(t, schema.Properties, "perPage")
1278+
assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"})
1279+
1280+
authorDate := time.Date(2026, 4, 1, 12, 0, 0, 0, time.UTC)
1281+
mockCommits := []*github.RepositoryCommit{
1282+
{
1283+
SHA: github.Ptr("abc123def456"),
1284+
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"),
1285+
Commit: &github.Commit{
1286+
Message: github.Ptr("feat: add commit listing"),
1287+
Author: &github.CommitAuthor{
1288+
Name: github.Ptr("Test User"),
1289+
Email: github.Ptr("test@example.com"),
1290+
Date: &github.Timestamp{Time: authorDate},
1291+
},
1292+
Committer: &github.CommitAuthor{
1293+
Name: github.Ptr("Merge Bot"),
1294+
Email: github.Ptr("merge@example.com"),
1295+
Date: &github.Timestamp{Time: authorDate.Add(30 * time.Minute)},
1296+
},
1297+
},
1298+
Author: &github.User{
1299+
Login: github.Ptr("test-user"),
1300+
ID: github.Ptr(int64(12345)),
1301+
HTMLURL: github.Ptr("https://github.com/test-user"),
1302+
AvatarURL: github.Ptr("https://github.com/test-user.png"),
1303+
},
1304+
Committer: &github.User{
1305+
Login: github.Ptr("merge-bot"),
1306+
ID: github.Ptr(int64(67890)),
1307+
HTMLURL: github.Ptr("https://github.com/merge-bot"),
1308+
AvatarURL: github.Ptr("https://github.com/merge-bot.png"),
1309+
},
1310+
},
1311+
{
1312+
SHA: github.Ptr("def456abc789"),
1313+
HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456abc789"),
1314+
Commit: &github.Commit{
1315+
Message: github.Ptr("fix: handle pagination"),
1316+
},
1317+
},
1318+
}
1319+
1320+
tests := []struct {
1321+
name string
1322+
mockedClient *http.Client
1323+
requestArgs map[string]any
1324+
expectError bool
1325+
expectedCommits []*github.RepositoryCommit
1326+
expectedErrMsg string
1327+
}{
1328+
{
1329+
name: "successful commits fetch",
1330+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1331+
GetReposPullsCommitsByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{
1332+
"page": "1",
1333+
"per_page": "30",
1334+
}).andThen(
1335+
mockResponse(t, http.StatusOK, mockCommits),
1336+
),
1337+
}),
1338+
requestArgs: map[string]any{
1339+
"method": "get_commits",
1340+
"owner": "owner",
1341+
"repo": "repo",
1342+
"pullNumber": float64(42),
1343+
},
1344+
expectError: false,
1345+
expectedCommits: mockCommits,
1346+
},
1347+
{
1348+
name: "successful commits fetch with pagination",
1349+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1350+
GetReposPullsCommitsByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{
1351+
"page": "2",
1352+
"per_page": "10",
1353+
}).andThen(
1354+
mockResponse(t, http.StatusOK, mockCommits),
1355+
),
1356+
}),
1357+
requestArgs: map[string]any{
1358+
"method": "get_commits",
1359+
"owner": "owner",
1360+
"repo": "repo",
1361+
"pullNumber": float64(42),
1362+
"page": float64(2),
1363+
"perPage": float64(10),
1364+
},
1365+
expectError: false,
1366+
expectedCommits: mockCommits,
1367+
},
1368+
{
1369+
name: "commits fetch fails",
1370+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1371+
GetReposPullsCommitsByOwnerByRepoByPullNumber: expectQueryParams(t, map[string]string{
1372+
"page": "1",
1373+
"per_page": "30",
1374+
}).andThen(
1375+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1376+
w.WriteHeader(http.StatusNotFound)
1377+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
1378+
}),
1379+
),
1380+
}),
1381+
requestArgs: map[string]any{
1382+
"method": "get_commits",
1383+
"owner": "owner",
1384+
"repo": "repo",
1385+
"pullNumber": float64(999),
1386+
},
1387+
expectError: true,
1388+
expectedErrMsg: "failed to get pull request commits",
1389+
},
1390+
}
1391+
1392+
for _, tc := range tests {
1393+
t.Run(tc.name, func(t *testing.T) {
1394+
client := mustNewGHClient(t, tc.mockedClient)
1395+
serverTool := PullRequestRead(translations.NullTranslationHelper)
1396+
deps := BaseDeps{
1397+
Client: client,
1398+
RepoAccessCache: stubRepoAccessCache(nil, 5*time.Minute),
1399+
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
1400+
}
1401+
handler := serverTool.Handler(deps)
1402+
request := createMCPRequest(tc.requestArgs)
1403+
1404+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1405+
1406+
if tc.expectError {
1407+
require.NoError(t, err)
1408+
require.True(t, result.IsError)
1409+
errorContent := getErrorResult(t, result)
1410+
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
1411+
return
1412+
}
1413+
1414+
require.NoError(t, err)
1415+
require.False(t, result.IsError)
1416+
1417+
textContent := getTextResult(t, result)
1418+
assert.NotContains(t, textContent.Text, `"committer"`)
1419+
assert.NotContains(t, textContent.Text, `"profile_url"`)
1420+
1421+
var returnedCommits []MinimalPullRequestCommit
1422+
err = json.Unmarshal([]byte(textContent.Text), &returnedCommits)
1423+
require.NoError(t, err)
1424+
assert.Len(t, returnedCommits, len(tc.expectedCommits))
1425+
for i, commit := range returnedCommits {
1426+
assert.Equal(t, tc.expectedCommits[i].GetSHA(), commit.SHA)
1427+
assert.Equal(t, tc.expectedCommits[i].GetHTMLURL(), commit.HTMLURL)
1428+
assert.Equal(t, tc.expectedCommits[i].GetCommit().GetMessage(), commit.Message)
1429+
}
1430+
1431+
assert.Equal(t, authorDate.Format(time.RFC3339), returnedCommits[0].Author.Date)
1432+
})
1433+
}
1434+
}
1435+
1436+
func Test_ConvertToMinimalPullRequestCommitsSkipsNilCommit(t *testing.T) {
1437+
commits := convertToMinimalPullRequestCommits([]*github.RepositoryCommit{nil})
1438+
1439+
require.Empty(t, commits)
1440+
}
1441+
12631442
func Test_GetPullRequestStatus(t *testing.T) {
12641443
// Verify tool definition once
12651444
serverTool := PullRequestRead(translations.NullTranslationHelper)

0 commit comments

Comments
 (0)