Merge "Add an extension point to extend the change message on post review"
This commit is contained in:
@@ -1453,6 +1453,19 @@ this can be specified by setting `scope = CapabilityScope.CORE`:
|
||||
[...]
|
||||
----
|
||||
|
||||
[post_review_extensions]
|
||||
== Post Review Extensions
|
||||
|
||||
By implementing the `com.google.gerrit.server.restapi.change.OnPostReview`
|
||||
interface plugins can extend the change message that is being posted when the
|
||||
[post review](rest-api-changes.html#set-review) REST endpoint is invoked.
|
||||
|
||||
This is useful if certain approvals have a special meaning (e.g. custom logic
|
||||
that is implemented in Prolog submit rules, signal for triggering an action
|
||||
like running CI etc.), as it allows the plugin to tell users about this meaning
|
||||
in the change message. This makes the effect of a given approval more
|
||||
transparent to the user.
|
||||
|
||||
[[ui_extension]]
|
||||
== UI Extension
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.google.gerrit.server.git.validators.CommitValidationListener;
|
||||
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
|
||||
import com.google.gerrit.server.git.validators.RefOperationValidationListener;
|
||||
import com.google.gerrit.server.logging.PerformanceLogger;
|
||||
import com.google.gerrit.server.restapi.change.OnPostReview;
|
||||
import com.google.gerrit.server.rules.SubmitRule;
|
||||
import com.google.gerrit.server.validators.AccountActivationValidationListener;
|
||||
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
|
||||
@@ -83,6 +84,7 @@ public class ExtensionRegistry {
|
||||
private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
|
||||
private final DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions;
|
||||
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
|
||||
private final DynamicSet<OnPostReview> onPostReviews;
|
||||
|
||||
@Inject
|
||||
ExtensionRegistry(
|
||||
@@ -113,7 +115,8 @@ public class ExtensionRegistry {
|
||||
DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners,
|
||||
DynamicMap<CapabilityDefinition> capabilityDefinitions,
|
||||
DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions,
|
||||
DynamicMap<ProjectConfigEntry> pluginConfigEntries) {
|
||||
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
|
||||
DynamicSet<OnPostReview> onPostReviews) {
|
||||
this.accountIndexedListeners = accountIndexedListeners;
|
||||
this.changeIndexedListeners = changeIndexedListeners;
|
||||
this.groupIndexedListeners = groupIndexedListeners;
|
||||
@@ -142,6 +145,7 @@ public class ExtensionRegistry {
|
||||
this.capabilityDefinitions = capabilityDefinitions;
|
||||
this.pluginProjectPermissionDefinitions = pluginProjectPermissionDefinitions;
|
||||
this.pluginConfigEntries = pluginConfigEntries;
|
||||
this.onPostReviews = onPostReviews;
|
||||
}
|
||||
|
||||
public Registration newRegistration() {
|
||||
@@ -270,6 +274,10 @@ public class ExtensionRegistry {
|
||||
return add(pluginConfigEntries, pluginConfigEntry, exportName);
|
||||
}
|
||||
|
||||
public Registration add(OnPostReview onPostReview) {
|
||||
return add(onPostReviews, onPostReview);
|
||||
}
|
||||
|
||||
private <T> Registration add(DynamicSet<T> dynamicSet, T extension) {
|
||||
return add(dynamicSet, extension, "gerrit");
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
|
||||
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
|
||||
import com.google.gerrit.server.query.change.ConflictsCacheImpl;
|
||||
import com.google.gerrit.server.quota.QuotaEnforcer;
|
||||
import com.google.gerrit.server.restapi.change.OnPostReview;
|
||||
import com.google.gerrit.server.restapi.change.SuggestReviewers;
|
||||
import com.google.gerrit.server.restapi.group.GroupModule;
|
||||
import com.google.gerrit.server.rules.DefaultSubmitRule;
|
||||
@@ -413,6 +414,7 @@ public class GerritGlobalModule extends FactoryModule {
|
||||
DynamicSet.setOf(binder(), ExceptionHook.class);
|
||||
DynamicSet.bind(binder(), ExceptionHook.class).to(ExceptionHookImpl.class);
|
||||
DynamicSet.setOf(binder(), MailSoyTemplateProvider.class);
|
||||
DynamicSet.setOf(binder(), OnPostReview.class);
|
||||
|
||||
DynamicMap.mapOf(binder(), MailFilter.class);
|
||||
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2020 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.restapi.change;
|
||||
|
||||
import com.google.gerrit.entities.PatchSet;
|
||||
import com.google.gerrit.extensions.annotations.ExtensionPoint;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Extension point that is invoked on post review. */
|
||||
@ExtensionPoint
|
||||
public interface OnPostReview {
|
||||
/**
|
||||
* Allows implementors to return a message that should be included into the change message that is
|
||||
* posted on post review.
|
||||
*
|
||||
* @param user the user that posts the review
|
||||
* @param changeNotes the change on which post review is performed
|
||||
* @param patchSet the patch set on which post review is performed
|
||||
* @param oldApprovals old approvals that changed as result of post review
|
||||
* @param approvals all current approvals
|
||||
* @return message that should be included into the change message that is posted on post review,
|
||||
* {@link Optional#empty()} if the change message should not be extended
|
||||
*/
|
||||
default Optional<String> getChangeMessageAddOn(
|
||||
IdentifiedUser user,
|
||||
ChangeNotes changeNotes,
|
||||
PatchSet patchSet,
|
||||
Map<String, Short> oldApprovals,
|
||||
Map<String, Short> approvals) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import static java.util.stream.Collectors.toSet;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
@@ -177,6 +178,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
private final ProjectCache projectCache;
|
||||
private final PermissionBackend permissionBackend;
|
||||
private final PluginSetContext<CommentValidator> commentValidators;
|
||||
private final PluginSetContext<OnPostReview> onPostReviews;
|
||||
private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
|
||||
private final boolean strictLabels;
|
||||
|
||||
@@ -202,6 +204,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
ProjectCache projectCache,
|
||||
PermissionBackend permissionBackend,
|
||||
PluginSetContext<CommentValidator> commentValidators,
|
||||
PluginSetContext<OnPostReview> onPostReviews,
|
||||
ReplyAttentionSetUpdates replyAttentionSetUpdates) {
|
||||
this.updateFactory = updateFactory;
|
||||
this.changeResourceFactory = changeResourceFactory;
|
||||
@@ -222,6 +225,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
this.projectCache = projectCache;
|
||||
this.permissionBackend = permissionBackend;
|
||||
this.commentValidators = commentValidators;
|
||||
this.onPostReviews = onPostReviews;
|
||||
this.replyAttentionSetUpdates = replyAttentionSetUpdates;
|
||||
this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
|
||||
}
|
||||
@@ -1413,6 +1417,23 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
||||
} else if (in.ready) {
|
||||
buf.append("\n\n" + START_REVIEW_MESSAGE);
|
||||
}
|
||||
|
||||
List<String> pluginMessages = new ArrayList<>();
|
||||
onPostReviews.runEach(
|
||||
onPostReview ->
|
||||
onPostReview
|
||||
.getChangeMessageAddOn(user, ctx.getNotes(), ps, oldApprovals, approvals)
|
||||
.ifPresent(
|
||||
pluginMessage ->
|
||||
pluginMessages.add(
|
||||
!pluginMessage.endsWith("\n")
|
||||
? pluginMessage + "\n"
|
||||
: pluginMessage)));
|
||||
if (!pluginMessages.isEmpty()) {
|
||||
buf.append("\n\n");
|
||||
buf.append(Joiner.on("\n").join(pluginMessages));
|
||||
}
|
||||
|
||||
if (buf.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -29,9 +29,16 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.truth.Correspondence;
|
||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||
import com.google.gerrit.acceptance.ExtensionRegistry;
|
||||
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
|
||||
import com.google.gerrit.acceptance.PushOneCommit;
|
||||
import com.google.gerrit.acceptance.TestAccount;
|
||||
import com.google.gerrit.acceptance.config.GerritConfig;
|
||||
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.entities.Account;
|
||||
import com.google.gerrit.entities.Change;
|
||||
import com.google.gerrit.entities.PatchSet;
|
||||
import com.google.gerrit.extensions.annotations.Exports;
|
||||
import com.google.gerrit.extensions.api.changes.DraftInput;
|
||||
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
||||
@@ -49,6 +56,9 @@ import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
import com.google.gerrit.extensions.validators.CommentForValidation;
|
||||
import com.google.gerrit.extensions.validators.CommentValidationContext;
|
||||
import com.google.gerrit.extensions.validators.CommentValidator;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
import com.google.gerrit.server.restapi.change.OnPostReview;
|
||||
import com.google.gerrit.server.restapi.change.PostReview;
|
||||
import com.google.gerrit.server.update.CommentsRejectedException;
|
||||
import com.google.gerrit.testing.TestCommentHelper;
|
||||
@@ -58,6 +68,7 @@ import java.sql.Timestamp;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -69,6 +80,7 @@ public class PostReviewIT extends AbstractDaemonTest {
|
||||
@Inject private CommentValidator mockCommentValidator;
|
||||
@Inject private TestCommentHelper testCommentHelper;
|
||||
@Inject private RequestScopeOperations requestScopeOperations;
|
||||
@Inject private ExtensionRegistry extensionRegistry;
|
||||
|
||||
private static final String COMMENT_TEXT = "The comment text";
|
||||
private static final CommentForValidation FILE_COMMENT_FOR_VALIDATION =
|
||||
@@ -474,6 +486,124 @@ public class PostReviewIT extends AbstractDaemonTest {
|
||||
assertThat(reviewer._accountId).isEqualTo(user.id().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extendChangeMessageFromPlugin() throws Exception {
|
||||
PushOneCommit.Result r = createChange();
|
||||
|
||||
String testMessage = "hello from plugin";
|
||||
TestOnPostReview testOnPostReview = new TestOnPostReview(testMessage);
|
||||
try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) {
|
||||
ReviewInput input = new ReviewInput().label("Code-Review", 1);
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
Collection<ChangeMessageInfo> messages = gApi.changes().id(r.getChangeId()).get().messages;
|
||||
assertThat(Iterables.getLast(messages).message)
|
||||
.isEqualTo(String.format("Patch Set 1: Code-Review+1\n\n%s\n", testMessage));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extendChangeMessageFromMultiplePlugins() throws Exception {
|
||||
PushOneCommit.Result r = createChange();
|
||||
|
||||
String testMessage1 = "hello from plugin 1";
|
||||
String testMessage2 = "message from plugin 2";
|
||||
TestOnPostReview testOnPostReview1 = new TestOnPostReview(testMessage1);
|
||||
TestOnPostReview testOnPostReview2 = new TestOnPostReview(testMessage2);
|
||||
try (Registration registration =
|
||||
extensionRegistry.newRegistration().add(testOnPostReview1).add(testOnPostReview2)) {
|
||||
ReviewInput input = new ReviewInput().label("Code-Review", 1);
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
Collection<ChangeMessageInfo> messages = gApi.changes().id(r.getChangeId()).get().messages;
|
||||
assertThat(Iterables.getLast(messages).message)
|
||||
.isEqualTo(
|
||||
String.format(
|
||||
"Patch Set 1: Code-Review+1\n\n%s\n\n%s\n", testMessage1, testMessage2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onPostReviewExtensionThatDoesntExtendTheChangeMessage() throws Exception {
|
||||
PushOneCommit.Result r = createChange();
|
||||
|
||||
TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null);
|
||||
try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) {
|
||||
ReviewInput input = new ReviewInput().label("Code-Review", 1);
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
Collection<ChangeMessageInfo> messages = gApi.changes().id(r.getChangeId()).get().messages;
|
||||
assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Code-Review+1");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onPostReviewCallbackGetsCorrectChangeAndPatchSet() throws Exception {
|
||||
PushOneCommit.Result r = createChange();
|
||||
amendChange(r.getChangeId());
|
||||
|
||||
TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null);
|
||||
try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) {
|
||||
ReviewInput input = new ReviewInput().label("Code-Review", 1);
|
||||
|
||||
// Vote on current patch set.
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
testOnPostReview.assertChangeAndPatchSet(r.getChange().getId(), 2);
|
||||
|
||||
// Vote on old patch set.
|
||||
gApi.changes().id(r.getChangeId()).revision(1).review(input);
|
||||
testOnPostReview.assertChangeAndPatchSet(r.getChange().getId(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onPostReviewCallbackGetsCorrectUser() throws Exception {
|
||||
PushOneCommit.Result r = createChange();
|
||||
|
||||
TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null);
|
||||
try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) {
|
||||
ReviewInput input = new ReviewInput().label("Code-Review", 1);
|
||||
|
||||
// Vote from admin.
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
testOnPostReview.assertUser(admin);
|
||||
|
||||
// Vote from user.
|
||||
requestScopeOperations.setApiUser(user.id());
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
testOnPostReview.assertUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onPostReviewCallbackGetsCorrectApprovals() throws Exception {
|
||||
PushOneCommit.Result r = createChange();
|
||||
|
||||
TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null);
|
||||
try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) {
|
||||
// Add a new vote.
|
||||
ReviewInput input = new ReviewInput().label("Code-Review", 1);
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
testOnPostReview.assertApproval(
|
||||
"Code-Review", /* expectedOldValue= */ 0, /* expectedNewValue= */ 1);
|
||||
|
||||
// Update an existing vote.
|
||||
input = new ReviewInput().label("Code-Review", 2);
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
testOnPostReview.assertApproval(
|
||||
"Code-Review", /* expectedOldValue= */ 1, /* expectedNewValue= */ 2);
|
||||
|
||||
// Post without changing the vote.
|
||||
input = new ReviewInput().label("Code-Review", 2);
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
testOnPostReview.assertApproval(
|
||||
"Code-Review", /* expectedOldValue= */ null, /* expectedNewValue= */ 2);
|
||||
|
||||
// Delete the vote.
|
||||
input = new ReviewInput().label("Code-Review", 0);
|
||||
gApi.changes().id(r.getChangeId()).current().review(input);
|
||||
testOnPostReview.assertApproval(
|
||||
"Code-Review", /* expectedOldValue= */ 2, /* expectedNewValue= */ 0);
|
||||
}
|
||||
}
|
||||
|
||||
private List<RobotCommentInfo> getRobotComments(String changeId) throws RestApiException {
|
||||
return gApi.changes().id(changeId).robotComments().values().stream()
|
||||
.flatMap(Collection::stream)
|
||||
@@ -495,4 +625,50 @@ public class PostReviewIT extends AbstractDaemonTest {
|
||||
.comparingElementsUsing(COMMENT_CORRESPONDENCE)
|
||||
.containsExactly(commentsForValidation);
|
||||
}
|
||||
|
||||
private static class TestOnPostReview implements OnPostReview {
|
||||
private final Optional<String> message;
|
||||
|
||||
private Change.Id changeId;
|
||||
private PatchSet.Id patchSetId;
|
||||
private Account.Id accountId;
|
||||
private Map<String, Short> oldApprovals;
|
||||
private Map<String, Short> approvals;
|
||||
|
||||
TestOnPostReview(@Nullable String message) {
|
||||
this.message = Optional.ofNullable(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getChangeMessageAddOn(
|
||||
IdentifiedUser user,
|
||||
ChangeNotes changeNotes,
|
||||
PatchSet patchSet,
|
||||
Map<String, Short> oldApprovals,
|
||||
Map<String, Short> approvals) {
|
||||
this.changeId = changeNotes.getChangeId();
|
||||
this.patchSetId = patchSet.id();
|
||||
this.accountId = user.getAccountId();
|
||||
this.oldApprovals = oldApprovals;
|
||||
this.approvals = approvals;
|
||||
return message;
|
||||
}
|
||||
|
||||
public void assertChangeAndPatchSet(Change.Id expectedChangeId, int expectedPatchSetNum) {
|
||||
assertThat(changeId).isEqualTo(expectedChangeId);
|
||||
assertThat(patchSetId.get()).isEqualTo(expectedPatchSetNum);
|
||||
}
|
||||
|
||||
public void assertUser(TestAccount expectedUser) {
|
||||
assertThat(accountId).isEqualTo(expectedUser.id());
|
||||
}
|
||||
|
||||
public void assertApproval(
|
||||
String labelName, @Nullable Integer expectedOldValue, int expectedNewValue) {
|
||||
assertThat(oldApprovals)
|
||||
.containsExactly(
|
||||
labelName, expectedOldValue != null ? expectedOldValue.shortValue() : null);
|
||||
assertThat(approvals).containsExactly(labelName, (short) expectedNewValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user