diff --git a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java index 9bfae602..563dbc17 100644 --- a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java @@ -40,6 +40,7 @@ import com.spotify.github.v3.comment.CommentReaction; import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException; import com.spotify.github.v3.exceptions.RequestNotOkException; +import com.spotify.github.v3.git.FileItem; import com.spotify.github.v3.git.Reference; import com.spotify.github.v3.orgs.TeamInvitation; import com.spotify.github.v3.prs.PullRequestItem; @@ -108,6 +109,9 @@ public class GitHubClient { static final TypeReference> LIST_PENDING_TEAM_INVITATIONS = new TypeReference<>() {}; + static final TypeReference> LIST_FILE_ITEMS = + new TypeReference<>() {}; + private static final String GET_ACCESS_TOKEN_URL = "app/installations/%s/access_tokens"; private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/src/main/java/com/spotify/github/v3/clients/PullRequestClient.java b/src/main/java/com/spotify/github/v3/clients/PullRequestClient.java index feefc801..6a8ef8e3 100644 --- a/src/main/java/com/spotify/github/v3/clients/PullRequestClient.java +++ b/src/main/java/com/spotify/github/v3/clients/PullRequestClient.java @@ -20,28 +20,18 @@ package com.spotify.github.v3.clients; -import java.io.InputStreamReader; -import java.io.Reader; -import java.lang.invoke.MethodHandles; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import static java.util.Objects.isNull; -import java.util.concurrent.CompletableFuture; - -import javax.ws.rs.core.HttpHeaders; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; -import com.spotify.github.async.AsyncPage; import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER; import static com.spotify.github.v3.clients.GitHubClient.LIST_COMMIT_TYPE_REFERENCE; +import static com.spotify.github.v3.clients.GitHubClient.LIST_FILE_ITEMS; import static com.spotify.github.v3.clients.GitHubClient.LIST_PR_TYPE_REFERENCE; import static com.spotify.github.v3.clients.GitHubClient.LIST_REVIEW_REQUEST_TYPE_REFERENCE; import static com.spotify.github.v3.clients.GitHubClient.LIST_REVIEW_TYPE_REFERENCE; +import static java.util.Objects.isNull; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.spotify.github.async.AsyncPage; +import com.spotify.github.v3.git.FileItem; import com.spotify.github.v3.prs.Comment; import com.spotify.github.v3.prs.MergeParameters; import com.spotify.github.v3.prs.PullRequest; @@ -54,6 +44,16 @@ import com.spotify.github.v3.prs.requests.PullRequestParameters; import com.spotify.github.v3.prs.requests.PullRequestUpdate; import com.spotify.github.v3.repos.CommitItem; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.invoke.MethodHandles; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.core.HttpHeaders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Pull call API client */ public class PullRequestClient { @@ -63,6 +63,7 @@ public class PullRequestClient { private static final String PR_NUMBER_TEMPLATE = "/repos/%s/%s/pulls/%s"; private static final String PR_COMMITS_TEMPLATE = "/repos/%s/%s/pulls/%s/commits"; private static final String PR_REVIEWS_TEMPLATE = "/repos/%s/%s/pulls/%s/reviews"; + private static final String PR_CHANGED_FILES_TEMPLATE = "/repos/%s/%s/pulls/%s/files"; private static final String PR_REVIEW_REQUESTS_TEMPLATE = "/repos/%s/%s/pulls/%s/requested_reviewers"; private static final String PR_COMMENT_REPLIES_TEMPLATE = @@ -449,6 +450,11 @@ public CompletableFuture diff(final long prNumber) { }); } + public Iterator> changedFiles(final long prNumber) { + final String path = String.format(PR_CHANGED_FILES_TEMPLATE, owner, repo, prNumber); + return new GithubPageIterator<>(new GithubPage<>(github, path, LIST_FILE_ITEMS)); + } + /** * List pull requests using given parameters. * diff --git a/src/test/java/com/spotify/github/v3/clients/PullRequestClientTest.java b/src/test/java/com/spotify/github/v3/clients/PullRequestClientTest.java index 2d590036..10a36dc0 100644 --- a/src/test/java/com/spotify/github/v3/clients/PullRequestClientTest.java +++ b/src/test/java/com/spotify/github/v3/clients/PullRequestClientTest.java @@ -20,21 +20,16 @@ package com.spotify.github.v3.clients; -import java.io.IOException; -import java.io.Reader; -import java.net.URI; +import static com.google.common.io.Resources.getResource; +import static com.spotify.github.MockHelper.createMockResponse; +import static java.lang.String.format; import static java.nio.charset.Charset.defaultCharset; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -import org.apache.commons.io.IOUtils; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; @@ -42,8 +37,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; -import static com.google.common.io.Resources.getResource; +import com.spotify.github.async.Async; +import com.spotify.github.async.AsyncPage; +import com.spotify.github.http.HttpResponse; import com.spotify.github.v3.exceptions.RequestNotOkException; +import com.spotify.github.v3.git.FileItem; +import com.spotify.github.v3.git.ImmutableFileItem; import com.spotify.github.v3.prs.Comment; import com.spotify.github.v3.prs.ImmutableRequestReviewParameters; import com.spotify.github.v3.prs.PullRequest; @@ -52,7 +51,12 @@ import com.spotify.github.v3.prs.requests.ImmutablePullRequestUpdate; import com.spotify.github.v3.prs.requests.PullRequestCreate; import com.spotify.github.v3.prs.requests.PullRequestUpdate; - +import java.io.IOException; +import java.io.Reader; +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; @@ -61,10 +65,16 @@ import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; public class PullRequestClientTest { + private static final String PR_CHANGED_FILES_TEMPLATE = "/repos/%s/%s/pulls/%s/files"; private GitHubClient github; + private GitHubClient mockGithub; private OkHttpClient client; private static String getFixture(String resource) throws IOException { @@ -74,7 +84,10 @@ private static String getFixture(String resource) throws IOException { @BeforeEach public void setUp() { client = mock(OkHttpClient.class); - github = GitHubClient.create(client, URI.create("http://bogus"), URI.create("https://bogus/graphql"), "token"); + github = + GitHubClient.create( + client, URI.create("http://bogus"), URI.create("https://bogus/graphql"), "token"); + mockGithub = mock(GitHubClient.class); } @Test @@ -95,18 +108,16 @@ public void createPullRequest() throws Exception { .message("Created") .body( ResponseBody.create( - MediaType.get("application/json"), - getFixture("pull_request.json"))) + MediaType.get("application/json"), getFixture("pull_request.json"))) .request(new Request.Builder().url("http://localhost/").build()) .build(); when(client.newCall(any())).thenReturn(call); - final PullRequestClient pullRequestClient = - PullRequestClient.create(github, "owner", "repo"); + final PullRequestClient pullRequestClient = PullRequestClient.create(github, "owner", "repo"); - final PullRequestCreate request = ImmutablePullRequestCreate.builder().title(title).body(body) - .head(head).base(base).build(); + final PullRequestCreate request = + ImmutablePullRequestCreate.builder().title(title).body(body).head(head).base(base).build(); final CompletableFuture result = pullRequestClient.create(request); @@ -136,18 +147,16 @@ public void updatePullRequest() throws Exception { .message("OK") .body( ResponseBody.create( - MediaType.get("application/json"), - getFixture("pull_request.json"))) + MediaType.get("application/json"), getFixture("pull_request.json"))) .request(new Request.Builder().url("http://localhost/").build()) .build(); when(client.newCall(any())).thenReturn(call); - final PullRequestClient pullRequestClient = - PullRequestClient.create(github, "owner", "repo"); + final PullRequestClient pullRequestClient = PullRequestClient.create(github, "owner", "repo"); - final PullRequestUpdate request = ImmutablePullRequestUpdate.builder().title(title).body(body) - .build(); + final PullRequestUpdate request = + ImmutablePullRequestUpdate.builder().title(title).body(body).build(); final CompletableFuture result = pullRequestClient.update(1L, request); @@ -172,18 +181,15 @@ public void testListReviewRequests() throws Throwable { .message("OK") .body( ResponseBody.create( - MediaType.get("application/json"), - getFixture("requestedReviews.json"))) + MediaType.get("application/json"), getFixture("requestedReviews.json"))) .request(new Request.Builder().url("http://localhost/").build()) .build(); when(client.newCall(any())).thenReturn(call); - final PullRequestClient pullRequestClient = - PullRequestClient.create(github, "owner", "repo"); + final PullRequestClient pullRequestClient = PullRequestClient.create(github, "owner", "repo"); - final CompletableFuture result = - pullRequestClient.listReviewRequests(1L); + final CompletableFuture result = pullRequestClient.listReviewRequests(1L); capture.getValue().onResponse(call, response); @@ -212,13 +218,14 @@ public void testRemoveRequestedReview() throws Throwable { when(client.newCall(any())).thenReturn(call); - PullRequestClient pullRequestClient = - PullRequestClient.create(github, "owner", "repo"); + PullRequestClient pullRequestClient = PullRequestClient.create(github, "owner", "repo"); CompletableFuture result = - pullRequestClient.removeRequestedReview(1L, ImmutableRequestReviewParameters.builder() - .reviewers(ImmutableList.of("user1", "user2")) - .build()); + pullRequestClient.removeRequestedReview( + 1L, + ImmutableRequestReviewParameters.builder() + .reviewers(ImmutableList.of("user1", "user2")) + .build()); capture.getValue().onResponse(call, response); @@ -243,13 +250,14 @@ public void testRemoveRequestedReview_failure() throws Throwable { when(client.newCall(any())).thenReturn(call); - PullRequestClient pullRequestClient = - PullRequestClient.create(github, "owner", "repo"); + PullRequestClient pullRequestClient = PullRequestClient.create(github, "owner", "repo"); CompletableFuture result = - pullRequestClient.removeRequestedReview(1L, ImmutableRequestReviewParameters.builder() - .reviewers(ImmutableList.of("user1", "user2")) - .build()); + pullRequestClient.removeRequestedReview( + 1L, + ImmutableRequestReviewParameters.builder() + .reviewers(ImmutableList.of("user1", "user2")) + .build()); capture.getValue().onResponse(call, response); @@ -270,18 +278,15 @@ public void testGetPatch() throws Throwable { .message("OK") .body( ResponseBody.create( - MediaType.get("application/vnd.github.patch"), - getFixture("patch.txt"))) + MediaType.get("application/vnd.github.patch"), getFixture("patch.txt"))) .request(new Request.Builder().url("http://localhost/").build()) .build(); when(client.newCall(any())).thenReturn(call); - final PullRequestClient pullRequestClient = - PullRequestClient.create(github, "owner", "repo"); + final PullRequestClient pullRequestClient = PullRequestClient.create(github, "owner", "repo"); - final CompletableFuture result = - pullRequestClient.patch(1L); + final CompletableFuture result = pullRequestClient.patch(1L); capture.getValue().onResponse(call, response); @@ -303,18 +308,15 @@ public void testGetDiff() throws Throwable { .message("OK") .body( ResponseBody.create( - MediaType.get("application/vnd.github.diff"), - getFixture("diff.txt"))) + MediaType.get("application/vnd.github.diff"), getFixture("diff.txt"))) .request(new Request.Builder().url("http://localhost/").build()) .build(); when(client.newCall(any())).thenReturn(call); - final PullRequestClient pullRequestClient = - PullRequestClient.create(github, "owner", "repo"); + final PullRequestClient pullRequestClient = PullRequestClient.create(github, "owner", "repo"); - final CompletableFuture result = - pullRequestClient.diff(1L); + final CompletableFuture result = pullRequestClient.diff(1L); capture.getValue().onResponse(call, response); @@ -343,8 +345,7 @@ public void testCreateCommentReply() throws Throwable { when(client.newCall(any())).thenReturn(call); - final PullRequestClient pullRequestClient = - PullRequestClient.create(github, "owner", "repo"); + final PullRequestClient pullRequestClient = PullRequestClient.create(github, "owner", "repo"); final String replyBody = "Thanks for the feedback!"; final CompletableFuture result = @@ -356,7 +357,8 @@ public void testCreateCommentReply() throws Throwable { assertThat(comment.body(), is("Great stuff!")); assertThat(comment.id(), is(10L)); - assertThat(comment.diffHunk(), is("@@ -16,33 +16,40 @@ public class Connection : IConnection...")); + assertThat( + comment.diffHunk(), is("@@ -16,33 +16,40 @@ public class Connection : IConnection...")); assertThat(comment.path(), is("file1.txt")); assertThat(comment.position(), is(1)); assertThat(comment.originalPosition(), is(4)); @@ -373,4 +375,67 @@ public void testCreateCommentReply() throws Throwable { assertThat(comment.side(), is("RIGHT")); assertThat(comment.pullRequestReviewId(), is(42L)); } + + @Test + void testChangedFiles() throws IOException { + FileItem file1 = + ImmutableFileItem.builder() + .filename("file1.txt") + .status("added") + .additions(103) + .deletions(21) + .changes(124) + .rawUrl( + URI.create( + "https://github.com/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt")) + .blobUrl( + URI.create( + "https://github.com/octocat/Hello-World/blob/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt")) + .patch("@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test") + .contentsUrl( + URI.create( + "https://api.github.com/repos/octocat/Hello-World/contents/file1.txt?ref=6dcb09b5b57875f334f61aebed695e2e4193db5e")) + .sha("bbcd538c8e72b8c175046e27cc8f907076331401") + .build(); + FileItem file2 = + ImmutableFileItem.builder() + .filename("file2.txt") + .status("modified") + .additions(103) + .deletions(21) + .changes(124) + .rawUrl( + URI.create( + "https://github.com/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/file2.txt")) + .blobUrl( + URI.create( + "https://github.com/octocat/Hello-World/blob/6dcb09b5b57875f334f61aebed695e2e4193db5e/file2.txt")) + .patch("@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test") + .contentsUrl( + URI.create( + "https://api.github.com/repos/octocat/Hello-World/contents/file2.txt?ref=6dcb09b5b57875f334f61aebed695e2e4193db5e")) + .sha("bbcd538c8e72b8c175046e27cc8f907076331401") + .build(); + List expectedFiles = List.of(file1, file2); + final String expectedBody = github.json().toJsonUnchecked(expectedFiles); + + final String pageLink = + "; rel=\"first\""; + + final HttpResponse firstPageResponse = createMockResponse(pageLink, expectedBody); + + when(mockGithub.request(format(PR_CHANGED_FILES_TEMPLATE, "owner", "repo", "1"))) + .thenReturn(completedFuture(firstPageResponse)); + + when(mockGithub.json()).thenReturn(github.json()); + + final PullRequestClient pullRequestClient = + PullRequestClient.create(mockGithub, "owner", "repo"); + + final Iterable> pageIterator = () -> pullRequestClient.changedFiles(1L); + List actualFiles = Async.streamFromPaginatingIterable(pageIterator).collect(toList()); + assertEquals(actualFiles.size(), expectedFiles.size()); + assertEquals(actualFiles.get(0).filename(), expectedFiles.get(0).filename()); + assertEquals(actualFiles.get(1).filename(), expectedFiles.get(1).filename()); + } }