Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Gerrit connector usability #230

Merged
merged 6 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ public enum GeneralOption implements ConfigurationOption {

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

GERRIT_USE_HTTP_PASSWORD("gerrit.useHttpPassword", "Use Gerrit's internal password token.", "false"),
GERRIT_OMIT_DUPLICATE_COMMENTS("gerrit.omitDuplicateComments", "Avoid publishing same comments for the same patchset.", "true"),

JAVA_SRC_DIR("java.src.dir", "Java root source directory", Paths.SRC_MAIN),
JAVA_TEST_DIR("java.test.dir", "Java root test directory", Paths.SRC_TEST),

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pl.touk.sputnik.connector.gerrit;

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

private final GerritApi gerritApi;
private final GerritPatchset gerritPatchset;
@VisibleForTesting
final GerritPatchset gerritPatchset;
@VisibleForTesting
final GerritOptions options;

@NotNull
@Override
Expand Down Expand Up @@ -63,6 +67,8 @@ public void publish(@NotNull Review review) {
try {
log.debug("Set review in Gerrit: {}", review);
ReviewInput reviewInput = new ReviewInputBuilder().toReviewInput(review, gerritPatchset.getTag());
reviewInput.omitDuplicateComments = options.isOmitDuplicateComments();

gerritApi.changes()
.id(gerritPatchset.getChangeId())
.revision(gerritPatchset.getRevisionId())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pl.touk.sputnik.connector.gerrit;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.api.GerritApi;
import com.urswolfer.gerrit.client.rest.GerritAuthData;
Expand All @@ -16,10 +17,16 @@

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

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Slf4j
public class GerritFacadeBuilder {

private HttpHelper httpHelper = new HttpHelper();
private final HttpHelper httpHelper = new HttpHelper();

@NotNull
public GerritFacade build(Configuration configuration) {
Expand All @@ -32,9 +39,12 @@ public GerritFacade build(Configuration configuration) {
hostUri += connectorDetails.getPath();
}

GerritOptions gerritOptions = GerritOptions.from(configuration);

log.info("Using Gerrit URL: {}", hostUri);
GerritAuthData.Basic authData = new GerritAuthData.Basic(hostUri,
connectorDetails.getUsername(), connectorDetails.getPassword());
connectorDetails.getUsername(), connectorDetails.getPassword(),
gerritOptions.isUseHttpPassword());
GerritApi gerritApi = gerritRestApiFactory.create(authData, new HttpClientBuilderExtension() {
@Override
public HttpClientBuilder extend(HttpClientBuilder httpClientBuilder, GerritAuthData authData) {
Expand All @@ -44,7 +54,7 @@ public HttpClientBuilder extend(HttpClientBuilder httpClientBuilder, GerritAuthD
}
});

return new GerritFacade(gerritApi, gerritPatchset);
return new GerritFacade(gerritApi, gerritPatchset, gerritOptions);
}

@NotNull
Expand All @@ -56,6 +66,27 @@ private GerritPatchset buildGerritPatchset(Configuration configuration) {
notBlank(changeId, "You must provide non blank Gerrit change Id");
notBlank(revisionId, "You must provide non blank Gerrit revision Id");

return new GerritPatchset(changeId, revisionId, tag);
return new GerritPatchset(urlEncodeChangeId(changeId), revisionId, tag);
}

public static String urlEncodeChangeId(String changeId) {
if (changeId.indexOf('%') >= 0) {
// ChangeID is already encoded (otherwise why it would have a '%' character?)
return changeId;
}
// To keep the changeId readable, we don't encode '~' (it is not needed according to RFC2396)
return StreamSupport.stream(Splitter.on('~').split(changeId).spliterator(), false)
.map(GerritFacadeBuilder::safeUrlEncode).collect(Collectors.joining("~"));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just hope you're right and it works :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a comment with a reference to
ChangesRestClient.id where they do it in a very similar way with the 2 and 3 argument methods.

}

private static String safeUrlEncode(String stringToEncode) {
try {
return URLEncoder.encode(stringToEncode, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
// Should not happen
@SuppressWarnings("deprecation")
String fallbackValue = URLEncoder.encode(stringToEncode);
return fallbackValue;
}
}
}
34 changes: 34 additions & 0 deletions src/main/java/pl/touk/sputnik/connector/gerrit/GerritOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package pl.touk.sputnik.connector.gerrit;

import com.google.common.annotations.VisibleForTesting;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import pl.touk.sputnik.configuration.Configuration;
import pl.touk.sputnik.configuration.GeneralOption;

@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class GerritOptions {
/**
* Indicates whether to use Gerrit's internal password token.
*/
private final boolean useHttpPassword;
/**
* Indicates whether to avoid publishing the same comment again when the review is retriggered
* for the same revision.
*/
private final boolean omitDuplicateComments;

static GerritOptions from(Configuration configuration) {
return new GerritOptions(
Boolean.parseBoolean(configuration.getProperty(GeneralOption.GERRIT_USE_HTTP_PASSWORD)),
Boolean.parseBoolean(configuration.getProperty(GeneralOption.GERRIT_OMIT_DUPLICATE_COMMENTS)));
}

@VisibleForTesting
static GerritOptions empty() {
return new GerritOptions(false, false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package pl.touk.sputnik.connector.gerrit;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import pl.touk.sputnik.configuration.CliOption;
import pl.touk.sputnik.configuration.Configuration;
import pl.touk.sputnik.configuration.ConfigurationOption;
import pl.touk.sputnik.configuration.GeneralOption;

@ExtendWith(MockitoExtension.class)
class GerritFacadeBuilderTest {
private static final String CHANGE_ID_WITH_SLASH = "project/subproject~branch/subbranch~changeId";
private static final String REVISION_ID = "changeId";

@Mock
private Configuration configuration;

private GerritFacadeBuilder gerritFacadeBuilder;

@BeforeEach
void setup() {
when(configuration.getProperty(any()))
.then(invocation -> ((ConfigurationOption)invocation.getArgument(0)).getDefaultValue());
configure(CliOption.CHANGE_ID, CHANGE_ID_WITH_SLASH);
configure(CliOption.REVISION_ID, REVISION_ID);

gerritFacadeBuilder = new GerritFacadeBuilder();
}

@Test
void build_shouldEscapeChangeIdWithSlash() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to fit project style please skip build_ prefix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roger that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

GerritFacade connector = gerritFacadeBuilder.build(configuration);

assertEquals("project%2Fsubproject~branch%2Fsubbranch~changeId", connector.gerritPatchset.getChangeId());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use assertj assertions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roger that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

assertEquals(REVISION_ID, connector.gerritPatchset.getRevisionId());
}

@Test
void build_optionsPassed() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldBuildOptions maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roger that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

configure(GeneralOption.GERRIT_USE_HTTP_PASSWORD, "true");
configure(GeneralOption.GERRIT_OMIT_DUPLICATE_COMMENTS, "false");

GerritFacade connector = gerritFacadeBuilder.build(configuration);

assertTrue(connector.options.isUseHttpPassword());
assertFalse(connector.options.isOmitDuplicateComments());
}

public void configure(ConfigurationOption option, String value) {
when(configuration.getProperty(option)).thenReturn(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.Changes;
import com.google.gerrit.extensions.restapi.RestApiException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
Expand All @@ -11,6 +13,10 @@
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.Mockito.when;

import java.util.ArrayList;

import pl.touk.sputnik.review.Review;

@ExtendWith(MockitoExtension.class)
class GerritFacadeExceptionTest {

Expand All @@ -25,10 +31,10 @@ class GerritFacadeExceptionTest {
private Changes changes;

@Test
void shouldWrapConnectorException() throws Exception {
void listFiles_shouldWrapConnectorException() throws Exception {
when(gerritApi.changes()).thenReturn(changes);
when(changes.id(CHANGE_ID)).thenThrow(new RuntimeException("Connection refused"));
GerritFacade gerritFacade = new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG));
GerritFacade gerritFacade = new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG), GerritOptions.empty());

Throwable thrown = catchThrowable(gerritFacade::listFiles);

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

@Test
void publish_shouldWrapConnectorException() throws Exception {
when(gerritApi.changes()).thenReturn(changes);
when(changes.id(CHANGE_ID)).thenThrow(new RuntimeException("Connection refused"));
GerritFacade gerritFacade = new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG), GerritOptions.empty());

Throwable thrown = catchThrowable(() -> gerritFacade.publish(new Review(new ArrayList<>(), null)));

assertThat(thrown)
.isInstanceOf(GerritException.class)
.hasMessageContaining("Error when setting review");
}

@Test
void getRevision_shouldWrapRestApiException() throws Exception {
when(gerritApi.changes()).thenReturn(changes);
when(changes.id(CHANGE_ID)).thenThrow(RestApiException.wrap("Connection refused", new RuntimeException("Something bad happened")));
GerritFacade gerritFacade = new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG), GerritOptions.empty());

Throwable thrown = catchThrowable(gerritFacade::getRevision);

assertThat(thrown)
.isInstanceOf(GerritException.class)
.hasMessageContaining("Error when retrieve modified lines");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,41 @@
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.Changes;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.urswolfer.gerrit.client.rest.http.changes.FileInfoParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import pl.touk.sputnik.configuration.Configuration;
import pl.touk.sputnik.review.Review;
import pl.touk.sputnik.review.ReviewFile;
import pl.touk.sputnik.review.ReviewFormatter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

@ExtendWith(MockitoExtension.class)
class GerritFacadeTest {
Expand All @@ -36,13 +50,10 @@ class GerritFacadeTest {

@Mock
private GerritApi gerritApi;

private GerritFacade gerritFacade;

@BeforeEach
void setUp() {
gerritFacade = new GerritFacade(gerritApi, null);
}
@Mock
private GerritOptions gerritOptions;
@Mock
private Configuration configuration;

@Test
void shouldParseListFilesResponse() throws IOException, RestApiException {
Expand All @@ -56,7 +67,38 @@ void shouldNotListDeletedFiles() throws IOException, RestApiException {
assertThat(reviewFiles).hasSize(1);
}

@Test
void publish() throws IOException, RestApiException {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use should... naming style

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roger that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

when(gerritOptions.isOmitDuplicateComments()).thenReturn(true);
Review review = new Review(new ArrayList<>(), new ReviewFormatter(configuration));
ArgumentCaptor<ReviewInput> reviewInputCaptor = ArgumentCaptor.forClass(ReviewInput.class);

createGerritFacade().publish(review);

verify(gerritApi.changes().id(CHANGE_ID).revision(REVISION_ID)).review(reviewInputCaptor.capture());
ReviewInput reviewInput = reviewInputCaptor.getValue();
assertTrue(reviewInput.omitDuplicateComments);
assertEquals(TAG, reviewInput.tag);
}

@Test
void getRevision() throws IOException, RestApiException {
GerritFacade gerritFacade = createGerritFacade();

assertSame(gerritApi.changes().id(CHANGE_ID).revision(REVISION_ID), gerritFacade.getRevision());
}

@Test
void setReview_doesPublish() throws IOException, RestApiException {
Review review = new Review(new ArrayList<>(), new ReviewFormatter(configuration));

createGerritFacade().setReview(review);

verify(gerritApi.changes().id(CHANGE_ID).revision(REVISION_ID)).review(any());
}

private GerritFacade createGerritFacade() throws IOException, RestApiException {
@SuppressWarnings("UnstableApiUsage")
String listFilesJson = Resources.toString(Resources.getResource("json/gerrit-listfiles.json"), Charsets.UTF_8);
JsonElement jsonElement = new JsonParser().parse(listFilesJson);
Map<String, FileInfo> fileInfoMap = new FileInfoParser(new Gson()).parseFileInfos(jsonElement);
Expand All @@ -65,10 +107,9 @@ private GerritFacade createGerritFacade() throws IOException, RestApiException {
when(gerritApi.changes()).thenReturn(changes);
ChangeApi changeApi = mock(ChangeApi.class);
when(changes.id(CHANGE_ID)).thenReturn(changeApi);
RevisionApi revisionApi = mock(RevisionApi.class);
RevisionApi revisionApi = mock(RevisionApi.class, withSettings().lenient());
when(changeApi.revision(REVISION_ID)).thenReturn(revisionApi);
when(revisionApi.files()).thenReturn(fileInfoMap);
return new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG));
return new GerritFacade(gerritApi, new GerritPatchset(CHANGE_ID, REVISION_ID, TAG), gerritOptions);
}

}
}
Loading