Skip to content

Commit ca08150

Browse files
authored
add rest endpoint for pr comment reply (#237)
* add rest endpoint for pr comment reply * add pr review id to prs.comment * verify more props parsed on pr review comment event
1 parent 643d047 commit ca08150

File tree

6 files changed

+238
-33
lines changed

6 files changed

+238
-33
lines changed

src/main/java/com/spotify/github/v3/clients/PullRequestClient.java

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,41 @@
2020

2121
package com.spotify.github.v3.clients;
2222

23-
import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER;
24-
import static com.spotify.github.v3.clients.GitHubClient.LIST_COMMIT_TYPE_REFERENCE;
25-
import static com.spotify.github.v3.clients.GitHubClient.LIST_PR_TYPE_REFERENCE;
26-
import static com.spotify.github.v3.clients.GitHubClient.LIST_REVIEW_REQUEST_TYPE_REFERENCE;
27-
import static com.spotify.github.v3.clients.GitHubClient.LIST_REVIEW_TYPE_REFERENCE;
28-
import static java.util.Objects.isNull;
29-
30-
import com.google.common.base.Strings;
31-
import com.google.common.collect.ImmutableMap;
32-
import com.spotify.github.async.AsyncPage;
33-
import com.spotify.github.v3.prs.*;
34-
import com.spotify.github.v3.prs.requests.PullRequestCreate;
35-
import com.spotify.github.v3.prs.requests.PullRequestParameters;
36-
import com.spotify.github.v3.prs.requests.PullRequestUpdate;
37-
import com.spotify.github.v3.repos.CommitItem;
38-
3923
import java.io.InputStreamReader;
4024
import java.io.Reader;
4125
import java.lang.invoke.MethodHandles;
4226
import java.util.Iterator;
4327
import java.util.List;
4428
import java.util.Map;
29+
import static java.util.Objects.isNull;
4530
import java.util.concurrent.CompletableFuture;
31+
4632
import javax.ws.rs.core.HttpHeaders;
33+
4734
import org.slf4j.Logger;
4835
import org.slf4j.LoggerFactory;
4936

37+
import com.google.common.base.Strings;
38+
import com.google.common.collect.ImmutableMap;
39+
import com.spotify.github.async.AsyncPage;
40+
import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER;
41+
import static com.spotify.github.v3.clients.GitHubClient.LIST_COMMIT_TYPE_REFERENCE;
42+
import static com.spotify.github.v3.clients.GitHubClient.LIST_PR_TYPE_REFERENCE;
43+
import static com.spotify.github.v3.clients.GitHubClient.LIST_REVIEW_REQUEST_TYPE_REFERENCE;
44+
import static com.spotify.github.v3.clients.GitHubClient.LIST_REVIEW_TYPE_REFERENCE;
45+
import com.spotify.github.v3.prs.Comment;
46+
import com.spotify.github.v3.prs.MergeParameters;
47+
import com.spotify.github.v3.prs.PullRequest;
48+
import com.spotify.github.v3.prs.PullRequestItem;
49+
import com.spotify.github.v3.prs.RequestReviewParameters;
50+
import com.spotify.github.v3.prs.Review;
51+
import com.spotify.github.v3.prs.ReviewParameters;
52+
import com.spotify.github.v3.prs.ReviewRequests;
53+
import com.spotify.github.v3.prs.requests.PullRequestCreate;
54+
import com.spotify.github.v3.prs.requests.PullRequestParameters;
55+
import com.spotify.github.v3.prs.requests.PullRequestUpdate;
56+
import com.spotify.github.v3.repos.CommitItem;
57+
5058
/** Pull call API client */
5159
public class PullRequestClient {
5260

@@ -57,6 +65,8 @@ public class PullRequestClient {
5765
private static final String PR_REVIEWS_TEMPLATE = "/repos/%s/%s/pulls/%s/reviews";
5866
private static final String PR_REVIEW_REQUESTS_TEMPLATE =
5967
"/repos/%s/%s/pulls/%s/requested_reviewers";
68+
private static final String PR_COMMENT_REPLIES_TEMPLATE =
69+
"/repos/%s/%s/pulls/%s/comments/%s/replies";
6070

6171
private final GitHubClient github;
6272
private final String owner;
@@ -450,4 +460,23 @@ private CompletableFuture<List<PullRequestItem>> list(final String parameterPath
450460
log.debug("Fetching pull requests from " + path);
451461
return github.request(path, LIST_PR_TYPE_REFERENCE);
452462
}
463+
464+
/**
465+
* Creates a reply to a pull request review comment.
466+
*
467+
* @param prNumber pull request number
468+
* @param commentId the ID of the comment to reply to
469+
* @param body the reply message
470+
* @return the created comment
471+
* @see "https://docs.github.com/en/rest/pulls/comments#create-a-reply-for-a-review-comment"
472+
*/
473+
public CompletableFuture<Comment> createCommentReply(
474+
final long prNumber, final long commentId, final String body) {
475+
final String path =
476+
String.format(PR_COMMENT_REPLIES_TEMPLATE, owner, repo, prNumber, commentId);
477+
final Map<String, String> payload = ImmutableMap.of("body", body);
478+
final String jsonPayload = github.json().toJsonUnchecked(payload);
479+
log.debug("Creating reply to PR comment: " + path);
480+
return github.post(path, jsonPayload, Comment.class);
481+
}
453482
}

src/main/java/com/spotify/github/v3/prs/Comment.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ public interface Comment extends UpdateTracking {
7373
/** Base commit sha. */
7474
@Nullable
7575
String originalCommitId();
76-
7776
/** Comment author. */
7877
@Nullable
7978
User user();
@@ -82,6 +81,38 @@ public interface Comment extends UpdateTracking {
8281
@Nullable
8382
String body();
8483

84+
/** The ID of the comment to reply to. */
85+
@Nullable
86+
Long inReplyToId();
87+
88+
/** The author association of the comment. */
89+
@Nullable
90+
String authorAssociation();
91+
92+
/** The starting line number in the diff. */
93+
@Nullable
94+
Integer startLine();
95+
96+
/** The starting line number in the original file. */
97+
@Nullable
98+
Integer originalStartLine();
99+
100+
/** The side of the diff where the starting line is from. */
101+
@Nullable
102+
String startSide();
103+
104+
/** The line number in the diff. */
105+
@Nullable
106+
Integer line();
107+
108+
/** The line number in the original file. */
109+
@Nullable
110+
Integer originalLine();
111+
112+
/** The side of the diff where the line is from. */
113+
@Nullable
114+
String side();
115+
85116
/** Comment URL. */
86117
@Nullable
87118
URI htmlUrl();
@@ -98,4 +129,8 @@ public interface Comment extends UpdateTracking {
98129
/** Node ID */
99130
@Nullable
100131
String nodeId();
132+
133+
/** Pull request review ID. */
134+
@Nullable
135+
Long pullRequestReviewId();
101136
}

src/test/java/com/spotify/github/v3/activity/events/PullRequestReviewCommentEventTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,16 @@ public void testDeserialization() throws IOException {
4545
assertThat(event.comment().nodeId(), is("abc234"));
4646
assertThat(event.pullRequest().nodeId(), is("abc123"));
4747
assertThat(event.comment().body(), is("Maybe you should use more emojji on this line."));
48+
assertThat(event.comment().originalCommitId(), is("0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c"));
49+
assertThat(event.comment().originalLine(), is(1));
50+
assertThat(event.comment().originalPosition(), is(1));
51+
assertThat(event.comment().originalStartLine(), is(1));
52+
assertThat(event.comment().line(), is(1));
53+
assertThat(event.comment().side(), is("RIGHT"));
54+
assertThat(event.comment().startLine(), is(1));
55+
assertThat(event.comment().startSide(), is("RIGHT"));
56+
assertThat(event.comment().authorAssociation(), is("NONE"));
57+
assertThat(event.comment().pullRequestReviewId(), is(42L));
58+
assertThat(event.comment().inReplyToId(), is(426899381L));
4859
}
4960
}

src/test/java/com/spotify/github/v3/clients/PullRequestClientTest.java

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
99
* You may obtain a copy of the License at
10-
*
10+
*
1111
* http://www.apache.org/licenses/LICENSE-2.0
12-
*
12+
*
1313
* Unless required by applicable law or agreed to in writing, software
1414
* distributed under the License is distributed on an "AS IS" BASIS,
1515
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,34 +20,47 @@
2020

2121
package com.spotify.github.v3.clients;
2222

23-
import static com.google.common.io.Resources.getResource;
23+
import java.io.IOException;
24+
import java.io.Reader;
25+
import java.net.URI;
2426
import static java.nio.charset.Charset.defaultCharset;
27+
import java.util.concurrent.CompletableFuture;
28+
import java.util.concurrent.ExecutionException;
29+
30+
import org.apache.commons.io.IOUtils;
2531
import static org.hamcrest.MatcherAssert.assertThat;
2632
import static org.hamcrest.core.Is.is;
27-
import static org.junit.jupiter.api.Assertions.*;
33+
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
import static org.junit.jupiter.api.Assertions.assertThrows;
35+
import org.junit.jupiter.api.BeforeEach;
36+
import org.junit.jupiter.api.Test;
37+
import org.mockito.ArgumentCaptor;
2838
import static org.mockito.ArgumentMatchers.any;
29-
import static org.mockito.Mockito.*;
39+
import static org.mockito.Mockito.doNothing;
40+
import static org.mockito.Mockito.mock;
41+
import static org.mockito.Mockito.when;
3042

3143
import com.google.common.collect.ImmutableList;
3244
import com.google.common.io.Resources;
45+
import static com.google.common.io.Resources.getResource;
3346
import com.spotify.github.v3.exceptions.RequestNotOkException;
47+
import com.spotify.github.v3.prs.Comment;
3448
import com.spotify.github.v3.prs.ImmutableRequestReviewParameters;
3549
import com.spotify.github.v3.prs.PullRequest;
3650
import com.spotify.github.v3.prs.ReviewRequests;
3751
import com.spotify.github.v3.prs.requests.ImmutablePullRequestCreate;
3852
import com.spotify.github.v3.prs.requests.ImmutablePullRequestUpdate;
3953
import com.spotify.github.v3.prs.requests.PullRequestCreate;
4054
import com.spotify.github.v3.prs.requests.PullRequestUpdate;
41-
import java.io.IOException;
42-
import java.io.Reader;
43-
import java.net.URI;
44-
import java.util.concurrent.CompletableFuture;
45-
import java.util.concurrent.ExecutionException;
46-
import okhttp3.*;
47-
import org.apache.commons.io.IOUtils;
48-
import org.junit.jupiter.api.BeforeEach;
49-
import org.junit.jupiter.api.Test;
50-
import org.mockito.ArgumentCaptor;
55+
56+
import okhttp3.Call;
57+
import okhttp3.Callback;
58+
import okhttp3.MediaType;
59+
import okhttp3.OkHttpClient;
60+
import okhttp3.Protocol;
61+
import okhttp3.Request;
62+
import okhttp3.Response;
63+
import okhttp3.ResponseBody;
5164

5265
public class PullRequestClientTest {
5366

@@ -309,4 +322,55 @@ public void testGetDiff() throws Throwable {
309322

310323
assertEquals(getFixture("diff.txt"), IOUtils.toString(diffReader));
311324
}
325+
326+
@Test
327+
public void testCreateCommentReply() throws Throwable {
328+
final Call call = mock(Call.class);
329+
final ArgumentCaptor<Callback> capture = ArgumentCaptor.forClass(Callback.class);
330+
doNothing().when(call).enqueue(capture.capture());
331+
332+
final Response response =
333+
new Response.Builder()
334+
.code(201)
335+
.protocol(Protocol.HTTP_1_1)
336+
.message("Created")
337+
.body(
338+
ResponseBody.create(
339+
MediaType.get("application/json"),
340+
getFixture("pull_request_review_comment_reply.json")))
341+
.request(new Request.Builder().url("http://localhost/").build())
342+
.build();
343+
344+
when(client.newCall(any())).thenReturn(call);
345+
346+
final PullRequestClient pullRequestClient =
347+
PullRequestClient.create(github, "owner", "repo");
348+
349+
final String replyBody = "Thanks for the feedback!";
350+
final CompletableFuture<Comment> result =
351+
pullRequestClient.createCommentReply(1L, 123L, replyBody);
352+
353+
capture.getValue().onResponse(call, response);
354+
355+
Comment comment = result.get();
356+
357+
assertThat(comment.body(), is("Great stuff!"));
358+
assertThat(comment.id(), is(10L));
359+
assertThat(comment.diffHunk(), is("@@ -16,33 +16,40 @@ public class Connection : IConnection..."));
360+
assertThat(comment.path(), is("file1.txt"));
361+
assertThat(comment.position(), is(1));
362+
assertThat(comment.originalPosition(), is(4));
363+
assertThat(comment.commitId(), is("6dcb09b5b57875f334f61aebed695e2e4193db5e"));
364+
assertThat(comment.originalCommitId(), is("9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840"));
365+
assertThat(comment.inReplyToId(), is(426899381L));
366+
assertThat(comment.authorAssociation(), is("NONE"));
367+
assertThat(comment.user().login(), is("octocat"));
368+
assertThat(comment.startLine(), is(1));
369+
assertThat(comment.originalStartLine(), is(1));
370+
assertThat(comment.startSide(), is("RIGHT"));
371+
assertThat(comment.line(), is(2));
372+
assertThat(comment.originalLine(), is(2));
373+
assertThat(comment.side(), is("RIGHT"));
374+
assertThat(comment.pullRequestReviewId(), is(42L));
375+
}
312376
}

src/test/resources/com/spotify/github/v3/activity/events/fixtures/pull_request_review_comment_event.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,19 @@
77
"diff_hunk": "@@ -1 +1 @@\n-# public-repo",
88
"path": "README.md",
99
"position": 1,
10-
"original_position": 1,
1110
"commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
1211
"original_commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
12+
"original_line": 1,
13+
"original_position": 1,
14+
"original_start_line": 1,
15+
"line": 1,
16+
"side": "RIGHT",
17+
"start_line": 1,
18+
"start_side": "RIGHT",
19+
"author_association": "NONE",
20+
"pull_request_review_id": 42,
21+
"in_reply_to_id": 426899381,
22+
"subject_type": "line",
1323
"user": {
1424
"login": "baxterthehacker",
1525
"id": 6752317,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1",
3+
"pull_request_review_id": 42,
4+
"id": 10,
5+
"node_id": "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDEw",
6+
"diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...",
7+
"path": "file1.txt",
8+
"position": 1,
9+
"original_position": 4,
10+
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
11+
"original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840",
12+
"in_reply_to_id": 426899381,
13+
"user": {
14+
"login": "octocat",
15+
"id": 1,
16+
"node_id": "MDQ6VXNlcjE=",
17+
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
18+
"gravatar_id": "",
19+
"url": "https://api.github.com/users/octocat",
20+
"html_url": "https://github.com/octocat",
21+
"followers_url": "https://api.github.com/users/octocat/followers",
22+
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
23+
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
24+
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
25+
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
26+
"organizations_url": "https://api.github.com/users/octocat/orgs",
27+
"repos_url": "https://api.github.com/users/octocat/repos",
28+
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
29+
"received_events_url": "https://api.github.com/users/octocat/received_events",
30+
"type": "User",
31+
"site_admin": false
32+
},
33+
"body": "Great stuff!",
34+
"created_at": "2011-04-14T16:00:49Z",
35+
"updated_at": "2011-04-14T16:00:49Z",
36+
"html_url": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1",
37+
"pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1",
38+
"author_association": "NONE",
39+
"_links": {
40+
"self": {
41+
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1"
42+
},
43+
"html": {
44+
"href": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1"
45+
},
46+
"pull_request": {
47+
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/1"
48+
}
49+
},
50+
"start_line": 1,
51+
"original_start_line": 1,
52+
"start_side": "RIGHT",
53+
"line": 2,
54+
"original_line": 2,
55+
"side": "RIGHT"
56+
}

0 commit comments

Comments
 (0)