Merge "Support notify settings for posting checks"
diff --git a/java/com/google/gerrit/plugins/checks/ChecksUpdate.java b/java/com/google/gerrit/plugins/checks/ChecksUpdate.java
index f29d079..796bce3 100644
--- a/java/com/google/gerrit/plugins/checks/ChecksUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/ChecksUpdate.java
@@ -15,7 +15,12 @@
package com.google.gerrit.plugins.checks;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.plugins.checks.api.CombinedCheckState;
import com.google.gerrit.plugins.checks.email.CombinedCheckStateUpdatedSender;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -23,11 +28,14 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
+import java.util.Map;
import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
/**
* API to update checks.
@@ -50,6 +58,7 @@
private final CombinedCheckStateUpdatedSender.Factory combinedCheckStateUpdatedSenderFactory;
private final ChangeNotes.Factory notesFactory;
private final PatchSetUtil psUtil;
+ private final NotifyResolver notifyResolver;
private final Optional<IdentifiedUser> currentUser;
private final ChecksStorageUpdate checksStorageUpdate;
@@ -60,11 +69,13 @@
CombinedCheckStateUpdatedSender.Factory combinedCheckStateUpdatedSenderFactory,
ChangeNotes.Factory notesFactory,
PatchSetUtil psUtil,
+ NotifyResolver notifyResolver,
@Assisted IdentifiedUser currentUser) {
this.combinedCheckStateCache = combinedCheckStateCache;
this.combinedCheckStateUpdatedSenderFactory = combinedCheckStateUpdatedSenderFactory;
this.notesFactory = notesFactory;
this.psUtil = psUtil;
+ this.notifyResolver = notifyResolver;
this.currentUser = Optional.of(currentUser);
this.checksStorageUpdate = checksStorageUpdate;
}
@@ -75,17 +86,23 @@
CombinedCheckStateCache combinedCheckStateCache,
CombinedCheckStateUpdatedSender.Factory combinedCheckStateUpdatedSenderFactory,
ChangeNotes.Factory notesFactory,
- PatchSetUtil psUtil) {
+ PatchSetUtil psUtil,
+ NotifyResolver notifyResolver) {
this.combinedCheckStateCache = combinedCheckStateCache;
this.combinedCheckStateUpdatedSenderFactory = combinedCheckStateUpdatedSenderFactory;
this.notesFactory = notesFactory;
this.psUtil = psUtil;
+ this.notifyResolver = notifyResolver;
this.currentUser = Optional.empty();
this.checksStorageUpdate = checksStorageUpdate;
}
- public Check createCheck(CheckKey key, CheckUpdate checkUpdate)
- throws DuplicateKeyException, IOException {
+ public Check createCheck(
+ CheckKey key,
+ CheckUpdate checkUpdate,
+ @Nullable NotifyHandling notifyHandling,
+ @Nullable Map<RecipientType, NotifyInfo> notifyDetails)
+ throws DuplicateKeyException, BadRequestException, IOException, ConfigInvalidException {
CombinedCheckState oldCombinedCheckState =
combinedCheckStateCache.get(key.repository(), key.patchSet());
@@ -94,13 +111,18 @@
CombinedCheckState newCombinedCheckState =
combinedCheckStateCache.get(key.repository(), key.patchSet());
if (oldCombinedCheckState != newCombinedCheckState) {
- sendEmail(key, newCombinedCheckState);
+ sendEmail(notifyHandling, notifyDetails, key, newCombinedCheckState);
}
return check;
}
- public Check updateCheck(CheckKey key, CheckUpdate checkUpdate) throws IOException {
+ public Check updateCheck(
+ CheckKey key,
+ CheckUpdate checkUpdate,
+ @Nullable NotifyHandling notifyHandling,
+ @Nullable Map<RecipientType, NotifyInfo> notifyDetails)
+ throws BadRequestException, IOException, ConfigInvalidException {
CombinedCheckState oldCombinedCheckState =
combinedCheckStateCache.get(key.repository(), key.patchSet());
@@ -109,13 +131,21 @@
CombinedCheckState newCombinedCheckState =
combinedCheckStateCache.get(key.repository(), key.patchSet());
if (oldCombinedCheckState != newCombinedCheckState) {
- sendEmail(key, newCombinedCheckState);
+ sendEmail(notifyHandling, notifyDetails, key, newCombinedCheckState);
}
return check;
}
- private void sendEmail(CheckKey checkKey, CombinedCheckState combinedCheckState) {
+ private void sendEmail(
+ @Nullable NotifyHandling notifyHandling,
+ @Nullable Map<RecipientType, NotifyInfo> notifyDetails,
+ CheckKey checkKey,
+ CombinedCheckState combinedCheckState)
+ throws BadRequestException, IOException, ConfigInvalidException {
+ notifyHandling = notifyHandling != null ? notifyHandling : NotifyHandling.ALL;
+ NotifyResolver.Result notify = notifyResolver.resolve(notifyHandling, notifyDetails);
+
try {
CombinedCheckStateUpdatedSender sender =
combinedCheckStateUpdatedSenderFactory.create(
@@ -131,6 +161,7 @@
sender.setPatchSet(patchSet);
sender.setCombinedCheckState(combinedCheckState);
+ sender.setNotify(notify);
sender.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log(
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
index 310af7a..6e6de5b 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
@@ -19,6 +19,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.plugins.checks.Check;
import com.google.gerrit.plugins.checks.CheckJson;
import com.google.gerrit.plugins.checks.CheckKey;
@@ -66,7 +67,8 @@
@Override
public TestCheckUpdate.Builder newCheck(CheckKey key) {
return TestCheckUpdate.builder(key)
- .checkUpdater(u -> checksUpdate.get().createCheck(key, toCheckUpdate(u)));
+ .checkUpdater(
+ u -> checksUpdate.get().createCheck(key, toCheckUpdate(u), NotifyHandling.NONE, null));
}
final class PerCheckOperationsImpl implements PerCheckOperations {
@@ -117,7 +119,10 @@
public TestCheckUpdate.Builder forUpdate() {
return TestCheckUpdate.builder(key)
.checkUpdater(
- testUpdate -> checksUpdate.get().updateCheck(key, toCheckUpdate(testUpdate)));
+ testUpdate ->
+ checksUpdate
+ .get()
+ .updateCheck(key, toCheckUpdate(testUpdate), NotifyHandling.NONE, null));
}
}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckInput.java b/java/com/google/gerrit/plugins/checks/api/CheckInput.java
index 2162ce3..fd3409d 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckInput.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckInput.java
@@ -16,7 +16,11 @@
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
import java.sql.Timestamp;
+import java.util.Map;
import java.util.Objects;
/** Input to create or update a {@link com.google.gerrit.plugins.checks.Check}. */
@@ -33,6 +37,13 @@
@Nullable public Timestamp started;
/** Date/Time at which the checker finished processing this check. */
@Nullable public Timestamp finished;
+ /**
+ * Whom to send email notifications to when the combined check state changes due to posting this
+ * check.
+ */
+ @Nullable public NotifyHandling notify;
+ /** Additional information about whom to notify regardless of the {@link #notify} setting. */
+ @Nullable public Map<RecipientType, NotifyInfo> notifyDetails;
@Override
public boolean equals(Object o) {
@@ -45,12 +56,14 @@
&& Objects.equals(other.message, message)
&& Objects.equals(other.url, url)
&& Objects.equals(other.started, started)
- && Objects.equals(other.finished, finished);
+ && Objects.equals(other.finished, finished)
+ && Objects.equals(other.notify, notify)
+ && Objects.equals(other.notifyDetails, notifyDetails);
}
@Override
public int hashCode() {
- return Objects.hash(checkerUuid, state, message, url, started, finished);
+ return Objects.hash(checkerUuid, state, message, url, started, finished, notify, notifyDetails);
}
@Override
@@ -62,6 +75,8 @@
.add("url", url)
.add("started", started)
.add("finished", finished)
+ .add("notify", notify)
+ .add("notifyDetails", notifyDetails)
.toString();
}
}
diff --git a/java/com/google/gerrit/plugins/checks/api/PostCheck.java b/java/com/google/gerrit/plugins/checks/api/PostCheck.java
index 5531d9e..aaad490 100644
--- a/java/com/google/gerrit/plugins/checks/api/PostCheck.java
+++ b/java/com/google/gerrit/plugins/checks/api/PostCheck.java
@@ -109,9 +109,15 @@
() ->
new UnprocessableEntityException(
String.format("checker %s not found", checkerUuid)));
- updatedCheck = checksUpdate.get().createCheck(key, toCheckUpdate(input));
+ updatedCheck =
+ checksUpdate
+ .get()
+ .createCheck(key, toCheckUpdate(input), input.notify, input.notifyDetails);
} else {
- updatedCheck = checksUpdate.get().updateCheck(key, toCheckUpdate(input));
+ updatedCheck =
+ checksUpdate
+ .get()
+ .updateCheck(key, toCheckUpdate(input), input.notify, input.notifyDetails);
}
return Response.ok(checkJsonFactory.noOptions().format(updatedCheck));
}
diff --git a/java/com/google/gerrit/plugins/checks/api/RerunCheck.java b/java/com/google/gerrit/plugins/checks/api/RerunCheck.java
index 023e302..487742f 100644
--- a/java/com/google/gerrit/plugins/checks/api/RerunCheck.java
+++ b/java/com/google/gerrit/plugins/checks/api/RerunCheck.java
@@ -103,7 +103,7 @@
.unsetStarted()
.setMessage("")
.setUrl("");
- updatedCheck = checksUpdate.get().updateCheck(key, builder.build());
+ updatedCheck = checksUpdate.get().updateCheck(key, builder.build(), null, null);
}
return Response.ok(checkJsonFactory.noOptions().format(updatedCheck));
}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChecksEmailIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChecksEmailIT.java
index 8723614..ca1ce0c 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChecksEmailIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChecksEmailIT.java
@@ -20,10 +20,15 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.PluginDefinedInfo;
@@ -39,7 +44,9 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
import org.junit.Before;
import org.junit.Test;
@@ -227,6 +234,72 @@
assertThat(sender.getMessages()).isEmpty();
}
+ @Test
+ public void postCheckRespectsNotifySettings() throws Exception {
+ testNotifySettingsForPostCheck(NotifyHandling.ALL, owner, reviewer, starrer, watcher);
+ testNotifySettingsForPostCheck(NotifyHandling.OWNER, owner);
+ testNotifySettingsForPostCheck(NotifyHandling.OWNER_REVIEWERS, owner, reviewer);
+ testNotifySettingsForPostCheck(NotifyHandling.NONE);
+
+ testNotifySettingsForPostCheck(
+ ImmutableSet.of(user), NotifyHandling.ALL, user, owner, reviewer, starrer, watcher);
+ testNotifySettingsForPostCheck(ImmutableSet.of(user), NotifyHandling.OWNER, user, owner);
+ testNotifySettingsForPostCheck(
+ ImmutableSet.of(user), NotifyHandling.OWNER_REVIEWERS, user, owner, reviewer);
+ testNotifySettingsForPostCheck(ImmutableSet.of(user), NotifyHandling.NONE, user);
+ }
+
+ private void testNotifySettingsForPostCheck(
+ NotifyHandling notify, TestAccount... expectedRecipients) throws RestApiException {
+ testNotifySettingsForPostCheck(ImmutableSet.of(), notify, expectedRecipients);
+ }
+
+ private void testNotifySettingsForPostCheck(
+ Set<TestAccount> accountsToNotify, NotifyHandling notify, TestAccount... expectedRecipients)
+ throws RestApiException {
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.IN_PROGRESS);
+
+ sender.clear();
+
+ // Post a new check that changes the combined check state to FAILED.
+ requestScopeOperations.setApiUser(bot.id());
+ CheckInput input = new CheckInput();
+ if (!accountsToNotify.isEmpty()) {
+ input.notifyDetails =
+ ImmutableMap.of(
+ RecipientType.TO,
+ new NotifyInfo(
+ accountsToNotify.stream().map(TestAccount::username).collect(toImmutableList())));
+ }
+ input.notify = notify;
+ input.checkerUuid = checkerUuid1.get();
+ input.state = CheckState.FAILED;
+ checksApiFactory.revision(patchSetId).create(input).get();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.FAILED);
+
+ List<Message> messages = sender.getMessages();
+ if (expectedRecipients.length == 0) {
+ assertThat(messages).isEmpty();
+ } else {
+ assertThat(messages).hasSize(1);
+
+ Message message = messages.get(0);
+ assertThat(message.from().getName()).isEqualTo(bot.fullName() + " (Code Review)");
+ assertThat(message.body())
+ .contains("The combined check state has been updated to " + CombinedCheckState.FAILED);
+ assertThat(message.rcpt())
+ .containsExactlyElementsIn(
+ Arrays.stream(expectedRecipients)
+ .map(TestAccount::getEmailAddress)
+ .collect(toImmutableList()));
+ }
+
+ // reset combined check state
+ input.state = CheckState.SCHEDULED;
+ checksApiFactory.revision(patchSetId).create(input).get();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.IN_PROGRESS);
+ }
+
private CombinedCheckState getCombinedCheckState() throws RestApiException {
ChangeInfo changeInfo =
gApi.changes()
diff --git a/resources/Documentation/rest-api-checks.md b/resources/Documentation/rest-api-checks.md
index 1c64598..94346d5 100644
--- a/resources/Documentation/rest-api-checks.md
+++ b/resources/Documentation/rest-api-checks.md
@@ -202,6 +202,8 @@
| `url` | optional | A fully-qualified URL pointing to the result of the check on the checker's infrastructure.
| `started` | optional | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check started processing.
| `finished` | optional | The [timestamp](../../../Documentation/rest-api.html#timestamp) of when the check finished processing.
+| `notify` | optional | Notify handling that defines to whom email notifications should be sent when the combined check state changes due to posting this check. Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. If not set, the default is `ALL`.
+| `notifyDetails`| optional | Additional information about whom to notify when the combined check state changes due to posting this check as a map of recipient type to [NotifyInfo](../../../Documentation/rest-api-changes.html#notify-info) entity.
### <a id="check-state"> CheckState (enum)
The `CheckState` enum can have the following values: `NOT_STARTED`, `FAILED`,