Skip to content

Commit b2c5fe9

Browse files
corebontsGabor Garancsi
and
Gabor Garancsi
authored
Improve Gerrit connector usability (#230)
* Allow usage of Gerrit HTTP password tokens (#220) This makes it possible to use http password token that is defined in Settings -> HTTP Password page instead of the regular site password. https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication * URL encode changeId when creating GerritPatchset (#221) When a cherry-pick is done and there are more commits with the same changeId, an exended id can be used to distinguish between them. It has a tilde separated format: project/subproject~branch/subbranch~changeId If this is not encoded but just put in the URL, an error message comes from the server like: Request not successful. Message: Not Found. Status-Code: 404. Content: Not found: project * Support gerrit configuration to omit duplicate comments (#108) As it's a Gerrit feature and not a client side deduplication, it should not have a large overhead, therefore it's enabled by default. * Improve test coverage of Gerrit connector * Use Gerrit API's URL encoding in GerritFacadeBuilder This actually does the same as the old 'safeUrlEncode(String)'. Also a reference is now added for the ID encoding method. * Clean up Gerrit connector tests to better fit the project style Co-authored-by: Gabor Garancsi <gabor.garancsi@casrd.net>
1 parent aa51743 commit b2c5fe9

File tree

9 files changed

+262
-20
lines changed

9 files changed

+262
-20
lines changed

src/main/java/pl/touk/sputnik/configuration/GeneralOption.java

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ public enum GeneralOption implements ConfigurationOption {
8282

8383
GITHUB_API_KEY("github.api.key", "Personal access tokens for Github", ""),
8484

85+
GERRIT_USE_HTTP_PASSWORD("gerrit.useHttpPassword", "Use Gerrit's internal password token.", "false"),
86+
GERRIT_OMIT_DUPLICATE_COMMENTS("gerrit.omitDuplicateComments", "Avoid publishing same comments for the same patchset.", "true"),
87+
8588
JAVA_SRC_DIR("java.src.dir", "Java root source directory", Paths.SRC_MAIN),
8689
JAVA_TEST_DIR("java.test.dir", "Java root test directory", Paths.SRC_TEST),
8790

src/main/java/pl/touk/sputnik/connector/gerrit/GerritFacade.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package pl.touk.sputnik.connector.gerrit;
22

3+
import com.google.common.annotations.VisibleForTesting;
34
import com.google.gerrit.extensions.api.GerritApi;
45
import com.google.gerrit.extensions.api.changes.ReviewInput;
56
import com.google.gerrit.extensions.api.changes.RevisionApi;
@@ -26,7 +27,10 @@ public class GerritFacade implements ConnectorFacade, ReviewPublisher {
2627
private static final String COMMIT_MSG = "/COMMIT_MSG";
2728

2829
private final GerritApi gerritApi;
29-
private final GerritPatchset gerritPatchset;
30+
@VisibleForTesting
31+
final GerritPatchset gerritPatchset;
32+
@VisibleForTesting
33+
final GerritOptions options;
3034

3135
@NotNull
3236
@Override
@@ -63,6 +67,8 @@ public void publish(@NotNull Review review) {
6367
try {
6468
log.debug("Set review in Gerrit: {}", review);
6569
ReviewInput reviewInput = new ReviewInputBuilder().toReviewInput(review, gerritPatchset.getTag());
70+
reviewInput.omitDuplicateComments = options.isOmitDuplicateComments();
71+
6672
gerritApi.changes()
6773
.id(gerritPatchset.getChangeId())
6874
.revision(gerritPatchset.getRevisionId())

src/main/java/pl/touk/sputnik/connector/gerrit/GerritFacadeBuilder.java

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package pl.touk.sputnik.connector.gerrit;
22

3+
import com.google.common.base.Splitter;
34
import com.google.common.base.Strings;
45
import com.google.gerrit.extensions.api.GerritApi;
6+
import com.google.gerrit.extensions.restapi.Url;
57
import com.urswolfer.gerrit.client.rest.GerritAuthData;
68
import com.urswolfer.gerrit.client.rest.GerritRestApiFactory;
79
import com.urswolfer.gerrit.client.rest.http.HttpClientBuilderExtension;
10+
811
import lombok.extern.slf4j.Slf4j;
912
import org.apache.http.impl.client.HttpClientBuilder;
1013
import org.jetbrains.annotations.NotNull;
@@ -16,10 +19,13 @@
1619

1720
import static org.apache.commons.lang3.Validate.notBlank;
1821

22+
import java.util.stream.Collectors;
23+
import java.util.stream.StreamSupport;
24+
1925
@Slf4j
2026
public class GerritFacadeBuilder {
2127

22-
private HttpHelper httpHelper = new HttpHelper();
28+
private final HttpHelper httpHelper = new HttpHelper();
2329

2430
@NotNull
2531
public GerritFacade build(Configuration configuration) {
@@ -32,9 +38,12 @@ public GerritFacade build(Configuration configuration) {
3238
hostUri += connectorDetails.getPath();
3339
}
3440

41+
GerritOptions gerritOptions = GerritOptions.from(configuration);
42+
3543
log.info("Using Gerrit URL: {}", hostUri);
3644
GerritAuthData.Basic authData = new GerritAuthData.Basic(hostUri,
37-
connectorDetails.getUsername(), connectorDetails.getPassword());
45+
connectorDetails.getUsername(), connectorDetails.getPassword(),
46+
gerritOptions.isUseHttpPassword());
3847
GerritApi gerritApi = gerritRestApiFactory.create(authData, new HttpClientBuilderExtension() {
3948
@Override
4049
public HttpClientBuilder extend(HttpClientBuilder httpClientBuilder, GerritAuthData authData) {
@@ -44,7 +53,7 @@ public HttpClientBuilder extend(HttpClientBuilder httpClientBuilder, GerritAuthD
4453
}
4554
});
4655

47-
return new GerritFacade(gerritApi, gerritPatchset);
56+
return new GerritFacade(gerritApi, gerritPatchset, gerritOptions);
4857
}
4958

5059
@NotNull
@@ -56,6 +65,17 @@ private GerritPatchset buildGerritPatchset(Configuration configuration) {
5665
notBlank(changeId, "You must provide non blank Gerrit change Id");
5766
notBlank(revisionId, "You must provide non blank Gerrit revision Id");
5867

59-
return new GerritPatchset(changeId, revisionId, tag);
68+
return new GerritPatchset(urlEncodeChangeId(changeId), revisionId, tag);
69+
}
70+
71+
public static String urlEncodeChangeId(String changeId) {
72+
if (changeId.indexOf('%') >= 0) {
73+
// ChangeID is already encoded (otherwise why it would have a '%' character?)
74+
return changeId;
75+
}
76+
// To keep the changeId readable, we don't encode '~' (it is not needed according to RFC2396)
77+
// See also: ChangesRestClient.id(String, String) and ChangesRestClient.id(String, String, String)
78+
return StreamSupport.stream(Splitter.on('~').split(changeId).spliterator(), false)
79+
.map(Url::encode).collect(Collectors.joining("~"));
6080
}
6181
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package pl.touk.sputnik.connector.gerrit;
2+
3+
import com.google.common.annotations.VisibleForTesting;
4+
5+
import lombok.AccessLevel;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Data;
8+
import pl.touk.sputnik.configuration.Configuration;
9+
import pl.touk.sputnik.configuration.GeneralOption;
10+
11+
@Data
12+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
13+
public class GerritOptions {
14+
/**
15+
* Indicates whether to use Gerrit's internal password token.
16+
*/
17+
private final boolean useHttpPassword;
18+
/**
19+
* Indicates whether to avoid publishing the same comment again when the review is retriggered
20+
* for the same revision.
21+
*/
22+
private final boolean omitDuplicateComments;
23+
24+
static GerritOptions from(Configuration configuration) {
25+
return new GerritOptions(
26+
Boolean.parseBoolean(configuration.getProperty(GeneralOption.GERRIT_USE_HTTP_PASSWORD)),
27+
Boolean.parseBoolean(configuration.getProperty(GeneralOption.GERRIT_OMIT_DUPLICATE_COMMENTS)));
28+
}
29+
30+
@VisibleForTesting
31+
static GerritOptions empty() {
32+
return new GerritOptions(false, false);
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package pl.touk.sputnik.connector.gerrit;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.when;
6+
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.ExtendWith;
10+
import org.mockito.Mock;
11+
import org.mockito.junit.jupiter.MockitoExtension;
12+
13+
import pl.touk.sputnik.configuration.CliOption;
14+
import pl.touk.sputnik.configuration.Configuration;
15+
import pl.touk.sputnik.configuration.ConfigurationOption;
16+
import pl.touk.sputnik.configuration.GeneralOption;
17+
18+
@ExtendWith(MockitoExtension.class)
19+
class GerritFacadeBuilderTest {
20+
private static final String CHANGE_ID_WITH_SLASH = "project/subproject~branch/subbranch~changeId";
21+
private static final String REVISION_ID = "changeId";
22+
23+
@Mock
24+
private Configuration configuration;
25+
26+
private GerritFacadeBuilder gerritFacadeBuilder;
27+
28+
@BeforeEach
29+
void setup() {
30+
when(configuration.getProperty(any()))
31+
.then(invocation -> ((ConfigurationOption)invocation.getArgument(0)).getDefaultValue());
32+
configure(CliOption.CHANGE_ID, CHANGE_ID_WITH_SLASH);
33+
configure(CliOption.REVISION_ID, REVISION_ID);
34+
35+
gerritFacadeBuilder = new GerritFacadeBuilder();
36+
}
37+
38+
@Test
39+
void shouldEscapeChangeIdWithSlash() {
40+
GerritFacade connector = gerritFacadeBuilder.build(configuration);
41+
42+
assertThat(connector.gerritPatchset.getChangeId())
43+
.isEqualTo("project%2Fsubproject~branch%2Fsubbranch~changeId");
44+
assertThat(connector.gerritPatchset.getRevisionId())
45+
.isEqualTo(REVISION_ID);
46+
}
47+
48+
@Test
49+
void shouldBuildWithCorrectOptions() {
50+
configure(GeneralOption.GERRIT_USE_HTTP_PASSWORD, "true");
51+
configure(GeneralOption.GERRIT_OMIT_DUPLICATE_COMMENTS, "false");
52+
53+
GerritFacade connector = gerritFacadeBuilder.build(configuration);
54+
55+
assertThat(connector.options.isUseHttpPassword()).isTrue();
56+
assertThat(connector.options.isOmitDuplicateComments()).isFalse();
57+
}
58+
59+
private void configure(ConfigurationOption option, String value) {
60+
when(configuration.getProperty(option)).thenReturn(value);
61+
}
62+
}

src/test/java/pl/touk/sputnik/connector/gerrit/GerritFacadeExceptionTest.java

+33-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.google.gerrit.extensions.api.GerritApi;
44
import com.google.gerrit.extensions.api.changes.Changes;
5+
import com.google.gerrit.extensions.restapi.RestApiException;
6+
57
import org.junit.jupiter.api.Test;
68
import org.junit.jupiter.api.extension.ExtendWith;
79
import org.mockito.Mock;
@@ -11,6 +13,10 @@
1113
import static org.assertj.core.api.Assertions.catchThrowable;
1214
import static org.mockito.Mockito.when;
1315

16+
import java.util.ArrayList;
17+
18+
import pl.touk.sputnik.review.Review;
19+
1420
@ExtendWith(MockitoExtension.class)
1521
class GerritFacadeExceptionTest {
1622

@@ -25,10 +31,10 @@ class GerritFacadeExceptionTest {
2531
private Changes changes;
2632

2733
@Test
28-
void shouldWrapConnectorException() throws Exception {
34+
void listFiles_shouldWrapConnectorException() throws Exception {
2935
when(gerritApi.changes()).thenReturn(changes);
3036
when(changes.id(CHANGE_ID)).thenThrow(new RuntimeException("Connection refused"));
31-
GerritFacade gerritFacade = new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG));
37+
GerritFacade gerritFacade = new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG), GerritOptions.empty());
3238

3339
Throwable thrown = catchThrowable(gerritFacade::listFiles);
3440

@@ -37,4 +43,29 @@ void shouldWrapConnectorException() throws Exception {
3743
.hasMessageContaining("Error when listing files");
3844
}
3945

46+
@Test
47+
void publish_shouldWrapConnectorException() throws Exception {
48+
when(gerritApi.changes()).thenReturn(changes);
49+
when(changes.id(CHANGE_ID)).thenThrow(new RuntimeException("Connection refused"));
50+
GerritFacade gerritFacade = new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG), GerritOptions.empty());
51+
52+
Throwable thrown = catchThrowable(() -> gerritFacade.publish(new Review(new ArrayList<>(), null)));
53+
54+
assertThat(thrown)
55+
.isInstanceOf(GerritException.class)
56+
.hasMessageContaining("Error when setting review");
57+
}
58+
59+
@Test
60+
void getRevision_shouldWrapRestApiException() throws Exception {
61+
when(gerritApi.changes()).thenReturn(changes);
62+
when(changes.id(CHANGE_ID)).thenThrow(RestApiException.wrap("Connection refused", new RuntimeException("Something bad happened")));
63+
GerritFacade gerritFacade = new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG), GerritOptions.empty());
64+
65+
Throwable thrown = catchThrowable(gerritFacade::getRevision);
66+
67+
assertThat(thrown)
68+
.isInstanceOf(GerritException.class)
69+
.hasMessageContaining("Error when retrieve modified lines");
70+
}
4071
}

src/test/java/pl/touk/sputnik/connector/gerrit/GerritFacadeTest.java

+48-12
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,36 @@
55
import com.google.gerrit.extensions.api.GerritApi;
66
import com.google.gerrit.extensions.api.changes.ChangeApi;
77
import com.google.gerrit.extensions.api.changes.Changes;
8+
import com.google.gerrit.extensions.api.changes.ReviewInput;
89
import com.google.gerrit.extensions.api.changes.RevisionApi;
910
import com.google.gerrit.extensions.common.FileInfo;
1011
import com.google.gerrit.extensions.restapi.RestApiException;
1112
import com.google.gson.Gson;
1213
import com.google.gson.JsonElement;
1314
import com.google.gson.JsonParser;
1415
import com.urswolfer.gerrit.client.rest.http.changes.FileInfoParser;
15-
import org.junit.jupiter.api.BeforeEach;
1616
import org.junit.jupiter.api.Test;
1717
import org.junit.jupiter.api.extension.ExtendWith;
18+
import org.mockito.ArgumentCaptor;
1819
import org.mockito.Mock;
1920
import org.mockito.junit.jupiter.MockitoExtension;
21+
22+
import pl.touk.sputnik.configuration.Configuration;
23+
import pl.touk.sputnik.review.Review;
2024
import pl.touk.sputnik.review.ReviewFile;
25+
import pl.touk.sputnik.review.ReviewFormatter;
2126

2227
import java.io.IOException;
28+
import java.util.ArrayList;
2329
import java.util.List;
2430
import java.util.Map;
2531

2632
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.mockito.ArgumentMatchers.any;
2734
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.verify;
2836
import static org.mockito.Mockito.when;
37+
import static org.mockito.Mockito.withSettings;
2938

3039
@ExtendWith(MockitoExtension.class)
3140
class GerritFacadeTest {
@@ -36,13 +45,10 @@ class GerritFacadeTest {
3645

3746
@Mock
3847
private GerritApi gerritApi;
39-
40-
private GerritFacade gerritFacade;
41-
42-
@BeforeEach
43-
void setUp() {
44-
gerritFacade = new GerritFacade(gerritApi, null);
45-
}
48+
@Mock
49+
private GerritOptions gerritOptions;
50+
@Mock
51+
private Configuration configuration;
4652

4753
@Test
4854
void shouldParseListFilesResponse() throws IOException, RestApiException {
@@ -56,7 +62,38 @@ void shouldNotListDeletedFiles() throws IOException, RestApiException {
5662
assertThat(reviewFiles).hasSize(1);
5763
}
5864

65+
@Test
66+
void shouldCallGerritApiOnPublish() throws IOException, RestApiException {
67+
when(gerritOptions.isOmitDuplicateComments()).thenReturn(true);
68+
Review review = new Review(new ArrayList<>(), new ReviewFormatter(configuration));
69+
ArgumentCaptor<ReviewInput> reviewInputCaptor = ArgumentCaptor.forClass(ReviewInput.class);
70+
71+
createGerritFacade().publish(review);
72+
73+
verify(gerritApi.changes().id(CHANGE_ID).revision(REVISION_ID)).review(reviewInputCaptor.capture());
74+
assertThat(reviewInputCaptor.getValue().omitDuplicateComments).isTrue();
75+
assertThat(reviewInputCaptor.getValue().tag).isEqualTo(TAG);
76+
}
77+
78+
@Test
79+
void shouldRevisionApiBeConfigured() throws IOException, RestApiException {
80+
GerritFacade gerritFacade = createGerritFacade();
81+
82+
assertThat(gerritFacade.getRevision())
83+
.isEqualTo(gerritApi.changes().id(CHANGE_ID).revision(REVISION_ID));
84+
}
85+
86+
@Test
87+
void shouldReviewDelegateToPublish() throws IOException, RestApiException {
88+
Review review = new Review(new ArrayList<>(), new ReviewFormatter(configuration));
89+
90+
createGerritFacade().setReview(review);
91+
92+
verify(gerritApi.changes().id(CHANGE_ID).revision(REVISION_ID)).review(any());
93+
}
94+
5995
private GerritFacade createGerritFacade() throws IOException, RestApiException {
96+
@SuppressWarnings("UnstableApiUsage")
6097
String listFilesJson = Resources.toString(Resources.getResource("json/gerrit-listfiles.json"), Charsets.UTF_8);
6198
JsonElement jsonElement = new JsonParser().parse(listFilesJson);
6299
Map<String, FileInfo> fileInfoMap = new FileInfoParser(new Gson()).parseFileInfos(jsonElement);
@@ -65,10 +102,9 @@ private GerritFacade createGerritFacade() throws IOException, RestApiException {
65102
when(gerritApi.changes()).thenReturn(changes);
66103
ChangeApi changeApi = mock(ChangeApi.class);
67104
when(changes.id(CHANGE_ID)).thenReturn(changeApi);
68-
RevisionApi revisionApi = mock(RevisionApi.class);
105+
RevisionApi revisionApi = mock(RevisionApi.class, withSettings().lenient());
69106
when(changeApi.revision(REVISION_ID)).thenReturn(revisionApi);
70107
when(revisionApi.files()).thenReturn(fileInfoMap);
71-
return new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG));
108+
return new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG), gerritOptions);
72109
}
73-
74-
}
110+
}

0 commit comments

Comments
 (0)