Add reviewers by email to REST API and Email sender
This change adds support for adding reviewers by email in PostReviewer and ChangeInfo. It also adds support for deleting reviewers using the ReviewerResource by adding Address to it. It further adds support for sending emails to reviewers that were added by email. This support includes emails for adding and removing a reviewer as well as for general change notifications. This change also adds tests and updates the documentation. Bug: Issue 4134 Change-Id: I901d711956ee69c25a05b455e068ec109821d79d
This commit is contained in:
@@ -2805,6 +2805,41 @@ To confirm the addition of the reviewers, resend the request with the
|
|||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
|
If link:config-project-config.html#reviewer.enableByEmail[reviewer.enableByEmail] is set
|
||||||
|
for the project, reviewers and CCs are not required to have a Gerrit account. If you POST
|
||||||
|
an email address of a reviewer or CC then, they will be added to the change even if they
|
||||||
|
don't have a Gerrit account.
|
||||||
|
|
||||||
|
If this option is disabled, the request would fail with `400 Bad Request` if the email
|
||||||
|
address can't be resolved to an active Gerrit account.
|
||||||
|
|
||||||
|
Note that the name is optional so both "un.registered@reviewer.com" and
|
||||||
|
"John Doe <un.registered@reviewer.com>" are valid inputs.
|
||||||
|
|
||||||
|
Reviewers without Gerrit accounts can only be added on changes visible to anonymous users.
|
||||||
|
|
||||||
|
.Request
|
||||||
|
----
|
||||||
|
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
|
||||||
|
Content-Type: application/json; charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"reviewer": "John Doe <un.registered@reviewer.com>"
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
.Response
|
||||||
|
----
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Disposition: attachment
|
||||||
|
Content-Type: application/json; charset=UTF-8
|
||||||
|
|
||||||
|
)]}'
|
||||||
|
{
|
||||||
|
"input": "John Doe <un.registered@reviewer.com>"
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
[[delete-reviewer]]
|
[[delete-reviewer]]
|
||||||
=== Delete Reviewer
|
=== Delete Reviewer
|
||||||
--
|
--
|
||||||
@@ -6183,6 +6218,10 @@ In addition `ReviewerInfo` has the following fields:
|
|||||||
|`approvals` |
|
|`approvals` |
|
||||||
The approvals of the reviewer as a map that maps the label names to the
|
The approvals of the reviewer as a map that maps the label names to the
|
||||||
approval values ("`-2`", "`-1`", "`0`", "`+1`", "`+2`").
|
approval values ("`-2`", "`-1`", "`0`", "`+1`", "`+2`").
|
||||||
|
|`_account_id` |
|
||||||
|
This field is inherited from `AccountInfo` but is optional here if an
|
||||||
|
unregistered reviewer was added by email. See
|
||||||
|
link:rest-api-changes.html#add-reviewer[add-reviewer] for details.
|
||||||
|==========================
|
|==========================
|
||||||
|
|
||||||
[[reviewer-input]]
|
[[reviewer-input]]
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import com.google.gerrit.server.group.SystemGroupBackend;
|
|||||||
import com.google.gerrit.server.index.change.ChangeIndex;
|
import com.google.gerrit.server.index.change.ChangeIndex;
|
||||||
import com.google.gerrit.server.index.change.ChangeIndexCollection;
|
import com.google.gerrit.server.index.change.ChangeIndexCollection;
|
||||||
import com.google.gerrit.server.index.change.ChangeIndexer;
|
import com.google.gerrit.server.index.change.ChangeIndexer;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gerrit.server.mail.send.EmailHeader;
|
import com.google.gerrit.server.mail.send.EmailHeader;
|
||||||
import com.google.gerrit.server.notedb.ChangeNoteUtil;
|
import com.google.gerrit.server.notedb.ChangeNoteUtil;
|
||||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||||
@@ -1200,21 +1201,29 @@ public abstract class AbstractDaemonTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void assertNotifyTo(TestAccount expected) {
|
protected void assertNotifyTo(TestAccount expected) {
|
||||||
|
assertNotifyTo(expected.emailAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertNotifyTo(Address expected) {
|
||||||
assertThat(sender.getMessages()).hasSize(1);
|
assertThat(sender.getMessages()).hasSize(1);
|
||||||
Message m = sender.getMessages().get(0);
|
Message m = sender.getMessages().get(0);
|
||||||
assertThat(m.rcpt()).containsExactly(expected.emailAddress);
|
assertThat(m.rcpt()).containsExactly(expected);
|
||||||
assertThat(((EmailHeader.AddressList) m.headers().get("To")).getAddressList())
|
assertThat(((EmailHeader.AddressList) m.headers().get("To")).getAddressList())
|
||||||
.containsExactly(expected.emailAddress);
|
.containsExactly(expected);
|
||||||
assertThat(m.headers().get("CC").isEmpty()).isTrue();
|
assertThat(m.headers().get("CC").isEmpty()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void assertNotifyCc(TestAccount expected) {
|
protected void assertNotifyCc(TestAccount expected) {
|
||||||
|
assertNotifyCc(expected.emailAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertNotifyCc(Address expected) {
|
||||||
assertThat(sender.getMessages()).hasSize(1);
|
assertThat(sender.getMessages()).hasSize(1);
|
||||||
Message m = sender.getMessages().get(0);
|
Message m = sender.getMessages().get(0);
|
||||||
assertThat(m.rcpt()).containsExactly(expected.emailAddress);
|
assertThat(m.rcpt()).containsExactly(expected);
|
||||||
assertThat(m.headers().get("To").isEmpty()).isTrue();
|
assertThat(m.headers().get("To").isEmpty()).isTrue();
|
||||||
assertThat(((EmailHeader.AddressList) m.headers().get("CC")).getAddressList())
|
assertThat(((EmailHeader.AddressList) m.headers().get("CC")).getAddressList())
|
||||||
.containsExactly(expected.emailAddress);
|
.containsExactly(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void assertNotifyBcc(TestAccount expected) {
|
protected void assertNotifyBcc(TestAccount expected) {
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
// Copyright (C) 2017 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.acceptance.rest.change;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static com.google.common.truth.TruthJUnit.assume;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||||
|
import com.google.gerrit.acceptance.NoHttpd;
|
||||||
|
import com.google.gerrit.acceptance.PushOneCommit;
|
||||||
|
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
|
||||||
|
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
||||||
|
import com.google.gerrit.extensions.client.ListChangesOption;
|
||||||
|
import com.google.gerrit.extensions.client.ReviewerState;
|
||||||
|
import com.google.gerrit.extensions.common.AccountInfo;
|
||||||
|
import com.google.gerrit.extensions.common.ChangeInfo;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
||||||
|
import com.google.gerrit.server.git.ProjectConfig;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
|
import com.google.gerrit.testutil.FakeEmailSender.Message;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@NoHttpd
|
||||||
|
public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
|
||||||
|
cfg.setEnableReviewerByEmail(true);
|
||||||
|
saveProjectConfig(project, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addByEmail() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
|
||||||
|
|
||||||
|
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
AddReviewerInput input = new AddReviewerInput();
|
||||||
|
input.reviewer = toRfcAddressString(acc);
|
||||||
|
input.state = state;
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer(input);
|
||||||
|
|
||||||
|
ChangeInfo info =
|
||||||
|
gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
|
||||||
|
assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc)));
|
||||||
|
// All reviewers added by email should be removable
|
||||||
|
assertThat(info.removableReviewers).isEqualTo(ImmutableList.of(acc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void removeByEmail() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
|
||||||
|
|
||||||
|
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
AddReviewerInput addInput = new AddReviewerInput();
|
||||||
|
addInput.reviewer = toRfcAddressString(acc);
|
||||||
|
addInput.state = state;
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer(addInput);
|
||||||
|
|
||||||
|
gApi.changes().id(r.getChangeId()).reviewer(acc.email).remove();
|
||||||
|
|
||||||
|
ChangeInfo info =
|
||||||
|
gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
|
||||||
|
assertThat(info.reviewers).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertFromCCToReviewer() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
|
||||||
|
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
AddReviewerInput addInput = new AddReviewerInput();
|
||||||
|
addInput.reviewer = toRfcAddressString(acc);
|
||||||
|
addInput.state = ReviewerState.CC;
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer(addInput);
|
||||||
|
|
||||||
|
AddReviewerInput modifyInput = new AddReviewerInput();
|
||||||
|
modifyInput.reviewer = addInput.reviewer;
|
||||||
|
modifyInput.state = ReviewerState.REVIEWER;
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer(modifyInput);
|
||||||
|
|
||||||
|
ChangeInfo info =
|
||||||
|
gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
|
||||||
|
assertThat(info.reviewers)
|
||||||
|
.isEqualTo(ImmutableMap.of(ReviewerState.REVIEWER, ImmutableList.of(acc)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addedReviewersGetNotified() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
|
||||||
|
|
||||||
|
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
AddReviewerInput input = new AddReviewerInput();
|
||||||
|
input.reviewer = toRfcAddressString(acc);
|
||||||
|
input.state = state;
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer(input);
|
||||||
|
|
||||||
|
List<Message> messages = sender.getMessages();
|
||||||
|
assertThat(messages).hasSize(1);
|
||||||
|
assertThat(messages.get(0).rcpt()).containsExactly(Address.parse(input.reviewer));
|
||||||
|
sender.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void removingReviewerTriggersNotification() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
|
||||||
|
|
||||||
|
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
AddReviewerInput addInput = new AddReviewerInput();
|
||||||
|
addInput.reviewer = toRfcAddressString(acc);
|
||||||
|
addInput.state = state;
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer(addInput);
|
||||||
|
|
||||||
|
// Review change as user
|
||||||
|
ReviewInput reviewInput = new ReviewInput();
|
||||||
|
reviewInput.message = "I have a comment";
|
||||||
|
setApiUser(user);
|
||||||
|
revision(r).review(reviewInput);
|
||||||
|
setApiUser(admin);
|
||||||
|
|
||||||
|
sender.clear();
|
||||||
|
|
||||||
|
// Delete as admin
|
||||||
|
gApi.changes().id(r.getChangeId()).reviewer(addInput.reviewer).remove();
|
||||||
|
|
||||||
|
List<Message> messages = sender.getMessages();
|
||||||
|
assertThat(messages).hasSize(1);
|
||||||
|
assertThat(messages.get(0).rcpt())
|
||||||
|
.containsExactly(Address.parse(addInput.reviewer), user.emailAddress);
|
||||||
|
sender.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reviewerAndCCReceiveRegularNotification() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
|
||||||
|
|
||||||
|
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
AddReviewerInput input = new AddReviewerInput();
|
||||||
|
input.reviewer = toRfcAddressString(acc);
|
||||||
|
input.state = state;
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer(input);
|
||||||
|
sender.clear();
|
||||||
|
|
||||||
|
gApi.changes()
|
||||||
|
.id(r.getChangeId())
|
||||||
|
.revision(r.getCommit().name())
|
||||||
|
.review(ReviewInput.approve());
|
||||||
|
|
||||||
|
if (state == ReviewerState.CC) {
|
||||||
|
assertNotifyCc(Address.parse(input.reviewer));
|
||||||
|
} else {
|
||||||
|
assertNotifyTo(Address.parse(input.reviewer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectMissingEmail() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
exception.expect(UnprocessableEntityException.class);
|
||||||
|
exception.expectMessage("email invalid");
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectMalformedEmail() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
exception.expect(UnprocessableEntityException.class);
|
||||||
|
exception.expectMessage("email invalid");
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectOnNonPublicChange() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
PushOneCommit.Result r = createDraftChange();
|
||||||
|
|
||||||
|
exception.expect(BadRequestException.class);
|
||||||
|
exception.expectMessage("change is not publicly visible");
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectWhenFeatureIsDisabled() throws Exception {
|
||||||
|
assume().that(notesMigration.readChanges()).isTrue();
|
||||||
|
|
||||||
|
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
|
||||||
|
cfg.setEnableReviewerByEmail(false);
|
||||||
|
saveProjectConfig(project, cfg);
|
||||||
|
|
||||||
|
PushOneCommit.Result r = createChange();
|
||||||
|
|
||||||
|
exception.expect(UnprocessableEntityException.class);
|
||||||
|
exception.expectMessage(
|
||||||
|
"Foo Bar <foo.bar@gerritcodereview.com> does not identify a registered user or group");
|
||||||
|
gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toRfcAddressString(AccountInfo info) {
|
||||||
|
return (new Address(info.name, info.email)).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,13 @@ public class ReviewerInfo extends AccountInfo {
|
|||||||
*/
|
*/
|
||||||
@Nullable public Map<String, String> approvals;
|
@Nullable public Map<String, String> approvals;
|
||||||
|
|
||||||
|
public static ReviewerInfo byEmail(@Nullable String name, String email) {
|
||||||
|
ReviewerInfo info = new ReviewerInfo();
|
||||||
|
info.name = name;
|
||||||
|
info.email = email;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
public ReviewerInfo(Integer id) {
|
public ReviewerInfo(Integer id) {
|
||||||
super(id);
|
super(id);
|
||||||
}
|
}
|
||||||
@@ -33,4 +40,6 @@ public class ReviewerInfo extends AccountInfo {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ReviewerInfo() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package com.google.gerrit.extensions.common;
|
package com.google.gerrit.extensions.common;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class AccountInfo {
|
public class AccountInfo {
|
||||||
public Integer _accountId;
|
public Integer _accountId;
|
||||||
@@ -29,4 +30,34 @@ public class AccountInfo {
|
|||||||
public AccountInfo(Integer id) {
|
public AccountInfo(Integer id) {
|
||||||
this._accountId = id;
|
this._accountId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** To be used ONLY in connection with unregistered reviewers and CCs. */
|
||||||
|
public AccountInfo(String name, String email) {
|
||||||
|
this.name = name;
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o instanceof AccountInfo) {
|
||||||
|
AccountInfo accountInfo = (AccountInfo) o;
|
||||||
|
return Objects.equals(_accountId, accountInfo._accountId)
|
||||||
|
&& Objects.equals(name, accountInfo.name)
|
||||||
|
&& Objects.equals(email, accountInfo.email)
|
||||||
|
&& Objects.equals(secondaryEmails, accountInfo.secondaryEmails)
|
||||||
|
&& Objects.equals(username, accountInfo.username)
|
||||||
|
&& Objects.equals(avatars, accountInfo.avatars)
|
||||||
|
&& Objects.equals(_moreAccounts, accountInfo._moreAccounts)
|
||||||
|
&& Objects.equals(status, accountInfo.status);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(
|
||||||
|
_accountId, name, email, secondaryEmails, username, avatars, _moreAccounts, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AccountInfo() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ import com.google.gerrit.server.git.LabelNormalizer;
|
|||||||
import com.google.gerrit.server.git.MergeUtil;
|
import com.google.gerrit.server.git.MergeUtil;
|
||||||
import com.google.gerrit.server.index.change.ChangeField;
|
import com.google.gerrit.server.index.change.ChangeField;
|
||||||
import com.google.gerrit.server.index.change.ChangeIndexCollection;
|
import com.google.gerrit.server.index.change.ChangeIndexCollection;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||||
import com.google.gerrit.server.notedb.ReviewerStateInternal;
|
import com.google.gerrit.server.notedb.ReviewerStateInternal;
|
||||||
import com.google.gerrit.server.patch.PatchListNotAvailableException;
|
import com.google.gerrit.server.patch.PatchListNotAvailableException;
|
||||||
@@ -531,6 +532,12 @@ public class ChangeJson {
|
|||||||
cd.reviewers().asTable().rowMap().entrySet()) {
|
cd.reviewers().asTable().rowMap().entrySet()) {
|
||||||
out.reviewers.put(e.getKey().asReviewerState(), toAccountInfo(e.getValue().keySet()));
|
out.reviewers.put(e.getKey().asReviewerState(), toAccountInfo(e.getValue().keySet()));
|
||||||
}
|
}
|
||||||
|
// TODO(hiesel) Load from ChangeData instead after the data was added there
|
||||||
|
for (Map.Entry<ReviewerStateInternal, Map<Address, Timestamp>> e :
|
||||||
|
cd.notes().getReviewersByEmail().asTable().rowMap().entrySet()) {
|
||||||
|
out.reviewers.put(
|
||||||
|
e.getKey().asReviewerState(), toAccountInfoByEmail(e.getValue().keySet()));
|
||||||
|
}
|
||||||
|
|
||||||
out.removableReviewers = removableReviewers(ctl, out);
|
out.removableReviewers = removableReviewers(ctl, out);
|
||||||
}
|
}
|
||||||
@@ -1075,9 +1082,11 @@ public class ChangeJson {
|
|||||||
Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC);
|
Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC);
|
||||||
if (ccs != null) {
|
if (ccs != null) {
|
||||||
for (AccountInfo ai : ccs) {
|
for (AccountInfo ai : ccs) {
|
||||||
Account.Id id = new Account.Id(ai._accountId);
|
if (ai._accountId != null) {
|
||||||
if (ctl.canRemoveReviewer(id, 0)) {
|
Account.Id id = new Account.Id(ai._accountId);
|
||||||
removable.add(id);
|
if (ctl.canRemoveReviewer(id, 0)) {
|
||||||
|
removable.add(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1091,6 +1100,14 @@ public class ChangeJson {
|
|||||||
for (Account.Id id : removable) {
|
for (Account.Id id : removable) {
|
||||||
result.add(accountLoader.get(id));
|
result.add(accountLoader.get(id));
|
||||||
}
|
}
|
||||||
|
// Reviewers added by email are always removable
|
||||||
|
for (Collection<AccountInfo> infos : out.reviewers.values()) {
|
||||||
|
for (AccountInfo info : infos) {
|
||||||
|
if (info._accountId == null) {
|
||||||
|
result.add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1102,6 +1119,14 @@ public class ChangeJson {
|
|||||||
.collect(toList());
|
.collect(toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) {
|
||||||
|
return addresses
|
||||||
|
.stream()
|
||||||
|
.map(a -> new AccountInfo(a.getName(), a.getEmail()))
|
||||||
|
.sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
|
||||||
|
.collect(toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException {
|
private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException {
|
||||||
if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
|
if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
|
||||||
|
|||||||
@@ -14,92 +14,38 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.change;
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
import com.google.gerrit.common.data.LabelType;
|
|
||||||
import com.google.gerrit.common.data.LabelTypes;
|
|
||||||
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
|
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
|
||||||
import com.google.gerrit.extensions.api.changes.NotifyHandling;
|
import com.google.gerrit.extensions.api.changes.NotifyHandling;
|
||||||
import com.google.gerrit.extensions.restapi.AuthException;
|
|
||||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
|
||||||
import com.google.gerrit.extensions.restapi.Response;
|
import com.google.gerrit.extensions.restapi.Response;
|
||||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
|
|
||||||
import com.google.gerrit.server.ApprovalsUtil;
|
|
||||||
import com.google.gerrit.server.ChangeMessagesUtil;
|
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
|
||||||
import com.google.gerrit.server.PatchSetUtil;
|
|
||||||
import com.google.gerrit.server.extensions.events.ReviewerDeleted;
|
|
||||||
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
|
|
||||||
import com.google.gerrit.server.notedb.NotesMigration;
|
|
||||||
import com.google.gerrit.server.update.BatchUpdate;
|
import com.google.gerrit.server.update.BatchUpdate;
|
||||||
import com.google.gerrit.server.update.BatchUpdateOp;
|
import com.google.gerrit.server.update.BatchUpdateOp;
|
||||||
import com.google.gerrit.server.update.BatchUpdateReviewDb;
|
|
||||||
import com.google.gerrit.server.update.ChangeContext;
|
|
||||||
import com.google.gerrit.server.update.Context;
|
|
||||||
import com.google.gerrit.server.update.UpdateException;
|
import com.google.gerrit.server.update.UpdateException;
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class DeleteReviewer implements RestModifyView<ReviewerResource, DeleteReviewerInput> {
|
public class DeleteReviewer implements RestModifyView<ReviewerResource, DeleteReviewerInput> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class);
|
|
||||||
|
|
||||||
private final Provider<ReviewDb> dbProvider;
|
private final Provider<ReviewDb> dbProvider;
|
||||||
private final ApprovalsUtil approvalsUtil;
|
|
||||||
private final PatchSetUtil psUtil;
|
|
||||||
private final ChangeMessagesUtil cmUtil;
|
|
||||||
private final BatchUpdate.Factory batchUpdateFactory;
|
private final BatchUpdate.Factory batchUpdateFactory;
|
||||||
private final IdentifiedUser.GenericFactory userFactory;
|
private final DeleteReviewerOp.Factory deleteReviewerOpFactory;
|
||||||
private final ReviewerDeleted reviewerDeleted;
|
private final DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory;
|
||||||
private final Provider<IdentifiedUser> user;
|
|
||||||
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
|
|
||||||
private final NotesMigration migration;
|
|
||||||
private final NotifyUtil notifyUtil;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
DeleteReviewer(
|
DeleteReviewer(
|
||||||
Provider<ReviewDb> dbProvider,
|
Provider<ReviewDb> dbProvider,
|
||||||
ApprovalsUtil approvalsUtil,
|
|
||||||
PatchSetUtil psUtil,
|
|
||||||
ChangeMessagesUtil cmUtil,
|
|
||||||
BatchUpdate.Factory batchUpdateFactory,
|
BatchUpdate.Factory batchUpdateFactory,
|
||||||
IdentifiedUser.GenericFactory userFactory,
|
DeleteReviewerOp.Factory deleteReviewerOpFactory,
|
||||||
ReviewerDeleted reviewerDeleted,
|
DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory) {
|
||||||
Provider<IdentifiedUser> user,
|
|
||||||
DeleteReviewerSender.Factory deleteReviewerSenderFactory,
|
|
||||||
NotesMigration migration,
|
|
||||||
NotifyUtil notifyUtil) {
|
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
this.approvalsUtil = approvalsUtil;
|
|
||||||
this.psUtil = psUtil;
|
|
||||||
this.cmUtil = cmUtil;
|
|
||||||
this.batchUpdateFactory = batchUpdateFactory;
|
this.batchUpdateFactory = batchUpdateFactory;
|
||||||
this.userFactory = userFactory;
|
this.deleteReviewerOpFactory = deleteReviewerOpFactory;
|
||||||
this.reviewerDeleted = reviewerDeleted;
|
this.deleteReviewerByEmailOpFactory = deleteReviewerByEmailOpFactory;
|
||||||
this.user = user;
|
|
||||||
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
|
|
||||||
this.migration = migration;
|
|
||||||
this.notifyUtil = notifyUtil;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -118,151 +64,15 @@ public class DeleteReviewer implements RestModifyView<ReviewerResource, DeleteRe
|
|||||||
rsrc.getChangeResource().getProject(),
|
rsrc.getChangeResource().getProject(),
|
||||||
rsrc.getChangeResource().getUser(),
|
rsrc.getChangeResource().getUser(),
|
||||||
TimeUtil.nowTs())) {
|
TimeUtil.nowTs())) {
|
||||||
Op op = new Op(rsrc.getReviewerUser().getAccount(), input);
|
BatchUpdateOp op;
|
||||||
|
if (rsrc.isByEmail()) {
|
||||||
|
op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail(), input);
|
||||||
|
} else {
|
||||||
|
op = deleteReviewerOpFactory.create(rsrc.getReviewerUser().getAccount(), input);
|
||||||
|
}
|
||||||
bu.addOp(rsrc.getChange().getId(), op);
|
bu.addOp(rsrc.getChange().getId(), op);
|
||||||
bu.execute();
|
bu.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.none();
|
return Response.none();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Op implements BatchUpdateOp {
|
|
||||||
private final Account reviewer;
|
|
||||||
private final DeleteReviewerInput input;
|
|
||||||
ChangeMessage changeMessage;
|
|
||||||
Change currChange;
|
|
||||||
PatchSet currPs;
|
|
||||||
Map<String, Short> newApprovals = new HashMap<>();
|
|
||||||
Map<String, Short> oldApprovals = new HashMap<>();
|
|
||||||
|
|
||||||
Op(Account reviewerAccount, DeleteReviewerInput input) {
|
|
||||||
this.reviewer = reviewerAccount;
|
|
||||||
this.input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean updateChange(ChangeContext ctx)
|
|
||||||
throws AuthException, ResourceNotFoundException, OrmException {
|
|
||||||
Account.Id reviewerId = reviewer.getId();
|
|
||||||
if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all().contains(reviewerId)) {
|
|
||||||
throw new ResourceNotFoundException();
|
|
||||||
}
|
|
||||||
currChange = ctx.getChange();
|
|
||||||
currPs = psUtil.current(ctx.getDb(), ctx.getNotes());
|
|
||||||
|
|
||||||
LabelTypes labelTypes = ctx.getControl().getLabelTypes();
|
|
||||||
// removing a reviewer will remove all her votes
|
|
||||||
for (LabelType lt : labelTypes.getLabelTypes()) {
|
|
||||||
newApprovals.put(lt.getName(), (short) 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder msg = new StringBuilder();
|
|
||||||
msg.append("Removed reviewer " + reviewer.getFullName());
|
|
||||||
StringBuilder removedVotesMsg = new StringBuilder();
|
|
||||||
removedVotesMsg.append(" with the following votes:\n\n");
|
|
||||||
List<PatchSetApproval> del = new ArrayList<>();
|
|
||||||
boolean votesRemoved = false;
|
|
||||||
for (PatchSetApproval a : approvals(ctx, reviewerId)) {
|
|
||||||
if (ctx.getControl().canRemoveReviewer(a)) {
|
|
||||||
del.add(a);
|
|
||||||
if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
|
|
||||||
oldApprovals.put(a.getLabel(), a.getValue());
|
|
||||||
removedVotesMsg
|
|
||||||
.append("* ")
|
|
||||||
.append(a.getLabel())
|
|
||||||
.append(formatLabelValue(a.getValue()))
|
|
||||||
.append(" by ")
|
|
||||||
.append(userFactory.create(a.getAccountId()).getNameEmail())
|
|
||||||
.append("\n");
|
|
||||||
votesRemoved = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new AuthException("delete reviewer not permitted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (votesRemoved) {
|
|
||||||
msg.append(removedVotesMsg);
|
|
||||||
} else {
|
|
||||||
msg.append(".");
|
|
||||||
}
|
|
||||||
ctx.getDb().patchSetApprovals().delete(del);
|
|
||||||
ChangeUpdate update = ctx.getUpdate(currPs.getId());
|
|
||||||
update.removeReviewer(reviewerId);
|
|
||||||
|
|
||||||
changeMessage =
|
|
||||||
ChangeMessagesUtil.newMessage(
|
|
||||||
ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_REVIEWER);
|
|
||||||
cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postUpdate(Context ctx) {
|
|
||||||
if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
|
|
||||||
emailReviewers(ctx.getProject(), currChange, changeMessage);
|
|
||||||
}
|
|
||||||
reviewerDeleted.fire(
|
|
||||||
currChange,
|
|
||||||
currPs,
|
|
||||||
reviewer,
|
|
||||||
ctx.getAccount(),
|
|
||||||
changeMessage.getMessage(),
|
|
||||||
newApprovals,
|
|
||||||
oldApprovals,
|
|
||||||
input.notify,
|
|
||||||
ctx.getWhen());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId)
|
|
||||||
throws OrmException {
|
|
||||||
Change.Id changeId = ctx.getNotes().getChangeId();
|
|
||||||
Iterable<PatchSetApproval> approvals;
|
|
||||||
PrimaryStorage r = PrimaryStorage.of(ctx.getChange());
|
|
||||||
|
|
||||||
if (migration.readChanges() && r == PrimaryStorage.REVIEW_DB) {
|
|
||||||
// Because NoteDb and ReviewDb have different semantics for zero-value
|
|
||||||
// approvals, we must fall back to ReviewDb as the source of truth here.
|
|
||||||
ReviewDb db = ctx.getDb();
|
|
||||||
|
|
||||||
if (db instanceof BatchUpdateReviewDb) {
|
|
||||||
db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
|
|
||||||
}
|
|
||||||
db = ReviewDbUtil.unwrapDb(db);
|
|
||||||
approvals = db.patchSetApprovals().byChange(changeId);
|
|
||||||
} else {
|
|
||||||
approvals = approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatLabelValue(short value) {
|
|
||||||
if (value > 0) {
|
|
||||||
return "+" + value;
|
|
||||||
}
|
|
||||||
return Short.toString(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void emailReviewers(
|
|
||||||
Project.NameKey projectName, Change change, ChangeMessage changeMessage) {
|
|
||||||
Account.Id userId = user.get().getAccountId();
|
|
||||||
if (userId.equals(reviewer.getId())) {
|
|
||||||
// The user knows they removed themselves, don't bother emailing them.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
|
|
||||||
cm.setFrom(userId);
|
|
||||||
cm.addReviewers(Collections.singleton(reviewer.getId()));
|
|
||||||
cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
|
|
||||||
cm.setNotify(input.notify);
|
|
||||||
cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
|
|
||||||
cm.send();
|
|
||||||
} catch (Exception err) {
|
|
||||||
log.error("Cannot email update for change " + change.getId(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (C) 2017 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.change;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
|
||||||
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
|
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||||
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||||
|
import com.google.gerrit.server.ChangeUtil;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
|
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
|
||||||
|
import com.google.gerrit.server.update.BatchUpdateOp;
|
||||||
|
import com.google.gerrit.server.update.ChangeContext;
|
||||||
|
import com.google.gerrit.server.update.Context;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.assistedinject.Assisted;
|
||||||
|
import java.util.Collections;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class DeleteReviewerByEmailOp implements BatchUpdateOp {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class);
|
||||||
|
|
||||||
|
public interface Factory {
|
||||||
|
DeleteReviewerByEmailOp create(Address reviewer, DeleteReviewerInput input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
|
||||||
|
private final NotifyUtil notifyUtil;
|
||||||
|
private final Address reviewer;
|
||||||
|
private final DeleteReviewerInput input;
|
||||||
|
|
||||||
|
private ChangeMessage changeMessage;
|
||||||
|
private Change.Id changeId;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DeleteReviewerByEmailOp(
|
||||||
|
DeleteReviewerSender.Factory deleteReviewerSenderFactory,
|
||||||
|
NotifyUtil notifyUtil,
|
||||||
|
@Assisted Address reviewer,
|
||||||
|
@Assisted DeleteReviewerInput input) {
|
||||||
|
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
|
||||||
|
this.notifyUtil = notifyUtil;
|
||||||
|
this.reviewer = reviewer;
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateChange(ChangeContext ctx) throws OrmException {
|
||||||
|
changeId = ctx.getChange().getId();
|
||||||
|
PatchSet.Id psId = ctx.getChange().currentPatchSetId();
|
||||||
|
String msg = "Removed reviewer " + reviewer;
|
||||||
|
changeMessage =
|
||||||
|
new ChangeMessage(
|
||||||
|
new ChangeMessage.Key(changeId, ChangeUtil.messageUuid()),
|
||||||
|
ctx.getAccountId(),
|
||||||
|
ctx.getWhen(),
|
||||||
|
psId);
|
||||||
|
changeMessage.setMessage(msg);
|
||||||
|
|
||||||
|
ctx.getUpdate(psId).setChangeMessage(msg);
|
||||||
|
ctx.getUpdate(psId).removeReviewerByEmail(reviewer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postUpdate(Context ctx) {
|
||||||
|
if (!NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DeleteReviewerSender cm = deleteReviewerSenderFactory.create(ctx.getProject(), changeId);
|
||||||
|
cm.setFrom(ctx.getAccountId());
|
||||||
|
cm.addReviewersByEmail(Collections.singleton(reviewer));
|
||||||
|
cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
|
||||||
|
cm.setNotify(input.notify);
|
||||||
|
cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
|
||||||
|
cm.send();
|
||||||
|
} catch (Exception err) {
|
||||||
|
log.error("Cannot email update for change " + changeId, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
// Copyright (C) 2017 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.change;
|
||||||
|
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.gerrit.common.data.LabelType;
|
||||||
|
import com.google.gerrit.common.data.LabelTypes;
|
||||||
|
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
|
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||||
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||||
|
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
|
||||||
|
import com.google.gerrit.server.ApprovalsUtil;
|
||||||
|
import com.google.gerrit.server.ChangeMessagesUtil;
|
||||||
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.gerrit.server.PatchSetUtil;
|
||||||
|
import com.google.gerrit.server.extensions.events.ReviewerDeleted;
|
||||||
|
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
|
||||||
|
import com.google.gerrit.server.notedb.ChangeUpdate;
|
||||||
|
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
|
||||||
|
import com.google.gerrit.server.notedb.NotesMigration;
|
||||||
|
import com.google.gerrit.server.update.BatchUpdateOp;
|
||||||
|
import com.google.gerrit.server.update.BatchUpdateReviewDb;
|
||||||
|
import com.google.gerrit.server.update.ChangeContext;
|
||||||
|
import com.google.gerrit.server.update.Context;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
import com.google.inject.assistedinject.Assisted;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class DeleteReviewerOp implements BatchUpdateOp {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class);
|
||||||
|
|
||||||
|
public interface Factory {
|
||||||
|
DeleteReviewerOp create(Account reviewerAccount, DeleteReviewerInput input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ApprovalsUtil approvalsUtil;
|
||||||
|
private final PatchSetUtil psUtil;
|
||||||
|
private final ChangeMessagesUtil cmUtil;
|
||||||
|
private final IdentifiedUser.GenericFactory userFactory;
|
||||||
|
private final ReviewerDeleted reviewerDeleted;
|
||||||
|
private final Provider<IdentifiedUser> user;
|
||||||
|
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
|
||||||
|
private final NotesMigration migration;
|
||||||
|
private final NotifyUtil notifyUtil;
|
||||||
|
|
||||||
|
private final Account reviewer;
|
||||||
|
private final DeleteReviewerInput input;
|
||||||
|
|
||||||
|
ChangeMessage changeMessage;
|
||||||
|
Change currChange;
|
||||||
|
PatchSet currPs;
|
||||||
|
Map<String, Short> newApprovals = new HashMap<>();
|
||||||
|
Map<String, Short> oldApprovals = new HashMap<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DeleteReviewerOp(
|
||||||
|
ApprovalsUtil approvalsUtil,
|
||||||
|
PatchSetUtil psUtil,
|
||||||
|
ChangeMessagesUtil cmUtil,
|
||||||
|
IdentifiedUser.GenericFactory userFactory,
|
||||||
|
ReviewerDeleted reviewerDeleted,
|
||||||
|
Provider<IdentifiedUser> user,
|
||||||
|
DeleteReviewerSender.Factory deleteReviewerSenderFactory,
|
||||||
|
NotesMigration migration,
|
||||||
|
NotifyUtil notifyUtil,
|
||||||
|
@Assisted Account reviewerAccount,
|
||||||
|
@Assisted DeleteReviewerInput input) {
|
||||||
|
this.approvalsUtil = approvalsUtil;
|
||||||
|
this.psUtil = psUtil;
|
||||||
|
this.cmUtil = cmUtil;
|
||||||
|
this.userFactory = userFactory;
|
||||||
|
this.reviewerDeleted = reviewerDeleted;
|
||||||
|
this.user = user;
|
||||||
|
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
|
||||||
|
this.migration = migration;
|
||||||
|
this.notifyUtil = notifyUtil;
|
||||||
|
|
||||||
|
this.reviewer = reviewerAccount;
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateChange(ChangeContext ctx)
|
||||||
|
throws AuthException, ResourceNotFoundException, OrmException {
|
||||||
|
Account.Id reviewerId = reviewer.getId();
|
||||||
|
if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all().contains(reviewerId)) {
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
currChange = ctx.getChange();
|
||||||
|
currPs = psUtil.current(ctx.getDb(), ctx.getNotes());
|
||||||
|
|
||||||
|
LabelTypes labelTypes = ctx.getControl().getLabelTypes();
|
||||||
|
// removing a reviewer will remove all her votes
|
||||||
|
for (LabelType lt : labelTypes.getLabelTypes()) {
|
||||||
|
newApprovals.put(lt.getName(), (short) 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder msg = new StringBuilder();
|
||||||
|
msg.append("Removed reviewer " + reviewer.getFullName());
|
||||||
|
StringBuilder removedVotesMsg = new StringBuilder();
|
||||||
|
removedVotesMsg.append(" with the following votes:\n\n");
|
||||||
|
List<PatchSetApproval> del = new ArrayList<>();
|
||||||
|
boolean votesRemoved = false;
|
||||||
|
for (PatchSetApproval a : approvals(ctx, reviewerId)) {
|
||||||
|
if (ctx.getControl().canRemoveReviewer(a)) {
|
||||||
|
del.add(a);
|
||||||
|
if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
|
||||||
|
oldApprovals.put(a.getLabel(), a.getValue());
|
||||||
|
removedVotesMsg
|
||||||
|
.append("* ")
|
||||||
|
.append(a.getLabel())
|
||||||
|
.append(formatLabelValue(a.getValue()))
|
||||||
|
.append(" by ")
|
||||||
|
.append(userFactory.create(a.getAccountId()).getNameEmail())
|
||||||
|
.append("\n");
|
||||||
|
votesRemoved = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new AuthException("delete reviewer not permitted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (votesRemoved) {
|
||||||
|
msg.append(removedVotesMsg);
|
||||||
|
} else {
|
||||||
|
msg.append(".");
|
||||||
|
}
|
||||||
|
ctx.getDb().patchSetApprovals().delete(del);
|
||||||
|
ChangeUpdate update = ctx.getUpdate(currPs.getId());
|
||||||
|
update.removeReviewer(reviewerId);
|
||||||
|
|
||||||
|
changeMessage =
|
||||||
|
ChangeMessagesUtil.newMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_REVIEWER);
|
||||||
|
cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postUpdate(Context ctx) {
|
||||||
|
if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
|
||||||
|
emailReviewers(ctx.getProject(), currChange, changeMessage);
|
||||||
|
}
|
||||||
|
reviewerDeleted.fire(
|
||||||
|
currChange,
|
||||||
|
currPs,
|
||||||
|
reviewer,
|
||||||
|
ctx.getAccount(),
|
||||||
|
changeMessage.getMessage(),
|
||||||
|
newApprovals,
|
||||||
|
oldApprovals,
|
||||||
|
input.notify,
|
||||||
|
ctx.getWhen());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId)
|
||||||
|
throws OrmException {
|
||||||
|
Change.Id changeId = ctx.getNotes().getChangeId();
|
||||||
|
Iterable<PatchSetApproval> approvals;
|
||||||
|
PrimaryStorage r = PrimaryStorage.of(ctx.getChange());
|
||||||
|
|
||||||
|
if (migration.readChanges() && r == PrimaryStorage.REVIEW_DB) {
|
||||||
|
// Because NoteDb and ReviewDb have different semantics for zero-value
|
||||||
|
// approvals, we must fall back to ReviewDb as the source of truth here.
|
||||||
|
ReviewDb db = ctx.getDb();
|
||||||
|
|
||||||
|
if (db instanceof BatchUpdateReviewDb) {
|
||||||
|
db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
|
||||||
|
}
|
||||||
|
db = ReviewDbUtil.unwrapDb(db);
|
||||||
|
approvals = db.patchSetApprovals().byChange(changeId);
|
||||||
|
} else {
|
||||||
|
approvals = approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatLabelValue(short value) {
|
||||||
|
if (value > 0) {
|
||||||
|
return "+" + value;
|
||||||
|
}
|
||||||
|
return Short.toString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emailReviewers(
|
||||||
|
Project.NameKey projectName, Change change, ChangeMessage changeMessage) {
|
||||||
|
Account.Id userId = user.get().getAccountId();
|
||||||
|
if (userId.equals(reviewer.getId())) {
|
||||||
|
// The user knows they removed themselves, don't bother emailing them.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
|
||||||
|
cm.setFrom(userId);
|
||||||
|
cm.addReviewers(Collections.singleton(reviewer.getId()));
|
||||||
|
cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
|
||||||
|
cm.setNotify(input.notify);
|
||||||
|
cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
|
||||||
|
cm.send();
|
||||||
|
} catch (Exception err) {
|
||||||
|
log.error("Cannot email update for change " + change.getId(), err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import com.google.gerrit.extensions.restapi.RestReadView;
|
|||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.ApprovalsUtil;
|
import com.google.gerrit.server.ApprovalsUtil;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
@@ -48,11 +49,16 @@ class ListReviewers implements RestReadView<ChangeResource> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ReviewerInfo> apply(ChangeResource rsrc) throws OrmException {
|
public List<ReviewerInfo> apply(ChangeResource rsrc) throws OrmException {
|
||||||
Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>();
|
Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
|
||||||
ReviewDb db = dbProvider.get();
|
ReviewDb db = dbProvider.get();
|
||||||
for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
|
for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
|
||||||
if (!reviewers.containsKey(accountId)) {
|
if (!reviewers.containsKey(accountId.toString())) {
|
||||||
reviewers.put(accountId, resourceFactory.create(rsrc, accountId));
|
reviewers.put(accountId.toString(), resourceFactory.create(rsrc, accountId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Address adr : rsrc.getNotes().getReviewersByEmail().all()) {
|
||||||
|
if (!reviewers.containsKey(adr.toString())) {
|
||||||
|
reviewers.put(adr.toString(), new ReviewerResource(rsrc, adr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json.format(reviewers.values());
|
return json.format(reviewers.values());
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.google.gerrit.extensions.restapi.RestReadView;
|
|||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.ApprovalsUtil;
|
import com.google.gerrit.server.ApprovalsUtil;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
@@ -54,11 +55,16 @@ class ListRevisionReviewers implements RestReadView<RevisionResource> {
|
|||||||
throw new MethodNotAllowedException("Cannot list reviewers on non-current patch set");
|
throw new MethodNotAllowedException("Cannot list reviewers on non-current patch set");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>();
|
Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
|
||||||
ReviewDb db = dbProvider.get();
|
ReviewDb db = dbProvider.get();
|
||||||
for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
|
for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
|
||||||
if (!reviewers.containsKey(accountId)) {
|
if (!reviewers.containsKey(accountId.toString())) {
|
||||||
reviewers.put(accountId, resourceFactory.create(rsrc, accountId));
|
reviewers.put(accountId.toString(), resourceFactory.create(rsrc, accountId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Address address : rsrc.getNotes().getReviewersByEmail().all()) {
|
||||||
|
if (!reviewers.containsKey(address.toString())) {
|
||||||
|
reviewers.put(address.toString(), new ReviewerResource(rsrc, address));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json.format(reviewers.values());
|
return json.format(reviewers.values());
|
||||||
|
|||||||
@@ -161,5 +161,7 @@ public class Module extends RestApiModule {
|
|||||||
factory(SetAssigneeOp.Factory.class);
|
factory(SetAssigneeOp.Factory.class);
|
||||||
factory(SetHashtagsOp.Factory.class);
|
factory(SetHashtagsOp.Factory.class);
|
||||||
factory(ChangeResource.Factory.class);
|
factory(ChangeResource.Factory.class);
|
||||||
|
factory(DeleteReviewerOp.Factory.class);
|
||||||
|
factory(DeleteReviewerByEmailOp.Factory.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import com.google.gerrit.server.PatchSetUtil;
|
|||||||
import com.google.gerrit.server.ReviewerSet;
|
import com.google.gerrit.server.ReviewerSet;
|
||||||
import com.google.gerrit.server.account.AccountsCollection;
|
import com.google.gerrit.server.account.AccountsCollection;
|
||||||
import com.google.gerrit.server.extensions.events.CommentAdded;
|
import com.google.gerrit.server.extensions.events.CommentAdded;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
import com.google.gerrit.server.notedb.ChangeUpdate;
|
||||||
import com.google.gerrit.server.notedb.NotesMigration;
|
import com.google.gerrit.server.notedb.NotesMigration;
|
||||||
@@ -313,14 +314,18 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
|
|||||||
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
|
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
|
||||||
List<Account.Id> to = new ArrayList<>();
|
List<Account.Id> to = new ArrayList<>();
|
||||||
List<Account.Id> cc = new ArrayList<>();
|
List<Account.Id> cc = new ArrayList<>();
|
||||||
|
List<Address> toByEmail = new ArrayList<>();
|
||||||
|
List<Address> ccByEmail = new ArrayList<>();
|
||||||
for (PostReviewers.Addition addition : reviewerAdditions) {
|
for (PostReviewers.Addition addition : reviewerAdditions) {
|
||||||
if (addition.op.state == ReviewerState.REVIEWER) {
|
if (addition.op.state == ReviewerState.REVIEWER) {
|
||||||
to.addAll(addition.op.reviewers.keySet());
|
to.addAll(addition.op.reviewers.keySet());
|
||||||
|
toByEmail.addAll(addition.op.reviewersByEmail);
|
||||||
} else if (addition.op.state == ReviewerState.CC) {
|
} else if (addition.op.state == ReviewerState.CC) {
|
||||||
cc.addAll(addition.op.reviewers.keySet());
|
cc.addAll(addition.op.reviewers.keySet());
|
||||||
|
ccByEmail.addAll(addition.op.reviewersByEmail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
postReviewers.emailReviewers(change, to, cc, notify, accountsToNotify);
|
postReviewers.emailReviewers(change, to, cc, toByEmail, ccByEmail, notify, accountsToNotify);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in)
|
private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableListMultimap;
|
|||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ListMultimap;
|
import com.google.common.collect.ListMultimap;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.gerrit.common.Nullable;
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
import com.google.gerrit.common.data.GroupDescription;
|
import com.google.gerrit.common.data.GroupDescription;
|
||||||
import com.google.gerrit.common.errors.NoSuchGroupException;
|
import com.google.gerrit.common.errors.NoSuchGroupException;
|
||||||
@@ -32,6 +33,7 @@ import com.google.gerrit.extensions.api.changes.NotifyHandling;
|
|||||||
import com.google.gerrit.extensions.api.changes.RecipientType;
|
import com.google.gerrit.extensions.api.changes.RecipientType;
|
||||||
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
|
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
|
||||||
import com.google.gerrit.extensions.client.ReviewerState;
|
import com.google.gerrit.extensions.client.ReviewerState;
|
||||||
|
import com.google.gerrit.extensions.common.AccountInfo;
|
||||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
@@ -42,6 +44,7 @@ import com.google.gerrit.reviewdb.client.Change;
|
|||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.AnonymousUser;
|
||||||
import com.google.gerrit.server.ApprovalsUtil;
|
import com.google.gerrit.server.ApprovalsUtil;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
@@ -52,12 +55,17 @@ import com.google.gerrit.server.account.AccountsCollection;
|
|||||||
import com.google.gerrit.server.account.GroupMembers;
|
import com.google.gerrit.server.account.GroupMembers;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.gerrit.server.extensions.events.ReviewerAdded;
|
import com.google.gerrit.server.extensions.events.ReviewerAdded;
|
||||||
|
import com.google.gerrit.server.git.ProjectConfig;
|
||||||
import com.google.gerrit.server.group.GroupsCollection;
|
import com.google.gerrit.server.group.GroupsCollection;
|
||||||
import com.google.gerrit.server.group.SystemGroupBackend;
|
import com.google.gerrit.server.group.SystemGroupBackend;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gerrit.server.mail.send.AddReviewerSender;
|
import com.google.gerrit.server.mail.send.AddReviewerSender;
|
||||||
|
import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
|
||||||
import com.google.gerrit.server.notedb.NotesMigration;
|
import com.google.gerrit.server.notedb.NotesMigration;
|
||||||
|
import com.google.gerrit.server.notedb.ReviewerStateInternal;
|
||||||
import com.google.gerrit.server.project.ChangeControl;
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
import com.google.gerrit.server.project.NoSuchProjectException;
|
import com.google.gerrit.server.project.NoSuchProjectException;
|
||||||
|
import com.google.gerrit.server.project.ProjectCache;
|
||||||
import com.google.gerrit.server.update.BatchUpdate;
|
import com.google.gerrit.server.update.BatchUpdate;
|
||||||
import com.google.gerrit.server.update.BatchUpdateOp;
|
import com.google.gerrit.server.update.BatchUpdateOp;
|
||||||
import com.google.gerrit.server.update.ChangeContext;
|
import com.google.gerrit.server.update.ChangeContext;
|
||||||
@@ -104,6 +112,8 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
private final NotesMigration migration;
|
private final NotesMigration migration;
|
||||||
private final AccountCache accountCache;
|
private final AccountCache accountCache;
|
||||||
private final NotifyUtil notifyUtil;
|
private final NotifyUtil notifyUtil;
|
||||||
|
private final ProjectCache projectCache;
|
||||||
|
private final Provider<AnonymousUser> anonymousProvider;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
PostReviewers(
|
PostReviewers(
|
||||||
@@ -124,7 +134,9 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
ReviewerAdded reviewerAdded,
|
ReviewerAdded reviewerAdded,
|
||||||
NotesMigration migration,
|
NotesMigration migration,
|
||||||
AccountCache accountCache,
|
AccountCache accountCache,
|
||||||
NotifyUtil notifyUtil) {
|
NotifyUtil notifyUtil,
|
||||||
|
ProjectCache projectCache,
|
||||||
|
Provider<AnonymousUser> anonymousProvider) {
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.reviewerFactory = reviewerFactory;
|
this.reviewerFactory = reviewerFactory;
|
||||||
this.approvalsUtil = approvalsUtil;
|
this.approvalsUtil = approvalsUtil;
|
||||||
@@ -143,6 +155,8 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
this.migration = migration;
|
this.migration = migration;
|
||||||
this.accountCache = accountCache;
|
this.accountCache = accountCache;
|
||||||
this.notifyUtil = notifyUtil;
|
this.notifyUtil = notifyUtil;
|
||||||
|
this.projectCache = projectCache;
|
||||||
|
this.anonymousProvider = anonymousProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -167,6 +181,7 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
return addition.result;
|
return addition.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(hiesel) Refactor this as it starts to become unreadable
|
||||||
public Addition prepareApplication(
|
public Addition prepareApplication(
|
||||||
ChangeResource rsrc, AddReviewerInput input, boolean allowGroup)
|
ChangeResource rsrc, AddReviewerInput input, boolean allowGroup)
|
||||||
throws OrmException, RestApiException, IOException {
|
throws OrmException, RestApiException, IOException {
|
||||||
@@ -174,17 +189,28 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
try {
|
try {
|
||||||
accountId = accounts.parse(input.reviewer).getAccountId();
|
accountId = accounts.parse(input.reviewer).getAccountId();
|
||||||
} catch (UnprocessableEntityException e) {
|
} catch (UnprocessableEntityException e) {
|
||||||
|
ProjectConfig projectConfig = projectCache.checkedGet(rsrc.getProject()).getConfig();
|
||||||
if (allowGroup) {
|
if (allowGroup) {
|
||||||
try {
|
try {
|
||||||
return putGroup(rsrc, input);
|
return putGroup(rsrc, input);
|
||||||
} catch (UnprocessableEntityException e2) {
|
} catch (UnprocessableEntityException e2) {
|
||||||
throw new UnprocessableEntityException(
|
if (!projectConfig.getEnableReviewerByEmail()) {
|
||||||
MessageFormat.format(
|
throw new UnprocessableEntityException(
|
||||||
ChangeMessages.get().reviewerNotFoundUserOrGroup, input.reviewer));
|
MessageFormat.format(
|
||||||
|
ChangeMessages.get().reviewerNotFoundUserOrGroup, input.reviewer));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new UnprocessableEntityException(
|
if (!projectConfig.getEnableReviewerByEmail()) {
|
||||||
MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
|
throw new UnprocessableEntityException(
|
||||||
|
MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
|
||||||
|
}
|
||||||
|
return putAccountByEmail(
|
||||||
|
input.reviewer,
|
||||||
|
rsrc,
|
||||||
|
input.state(),
|
||||||
|
input.notify,
|
||||||
|
notifyUtil.resolveAccounts(input.notifyDetails));
|
||||||
}
|
}
|
||||||
return putAccount(
|
return putAccount(
|
||||||
input.reviewer,
|
input.reviewer,
|
||||||
@@ -199,6 +225,7 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
user.getUserName(),
|
user.getUserName(),
|
||||||
revision.getChangeResource(),
|
revision.getChangeResource(),
|
||||||
ImmutableMap.of(user.getAccountId(), revision.getControl()),
|
ImmutableMap.of(user.getAccountId(), revision.getControl()),
|
||||||
|
null,
|
||||||
CC,
|
CC,
|
||||||
NotifyHandling.NONE,
|
NotifyHandling.NONE,
|
||||||
ImmutableListMultimap.of());
|
ImmutableListMultimap.of());
|
||||||
@@ -218,6 +245,7 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
reviewer,
|
reviewer,
|
||||||
rsrc.getChangeResource(),
|
rsrc.getChangeResource(),
|
||||||
ImmutableMap.of(member.getId(), control),
|
ImmutableMap.of(member.getId(), control),
|
||||||
|
null,
|
||||||
state,
|
state,
|
||||||
notify,
|
notify,
|
||||||
accountsToNotify);
|
accountsToNotify);
|
||||||
@@ -228,6 +256,32 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
throw new UnprocessableEntityException(String.format("Account of %s is inactive.", reviewer));
|
throw new UnprocessableEntityException(String.format("Account of %s is inactive.", reviewer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Addition putAccountByEmail(
|
||||||
|
String reviewer,
|
||||||
|
ChangeResource rsrc,
|
||||||
|
ReviewerState state,
|
||||||
|
NotifyHandling notify,
|
||||||
|
ListMultimap<RecipientType, Account.Id> accountsToNotify)
|
||||||
|
throws UnprocessableEntityException, OrmException, BadRequestException {
|
||||||
|
if (!rsrc.getControl().forUser(anonymousProvider.get()).isVisible(dbProvider.get())) {
|
||||||
|
throw new BadRequestException("change is not publicly visible");
|
||||||
|
}
|
||||||
|
if (!migration.readChanges()) {
|
||||||
|
throw new BadRequestException("feature only supported in NoteDb");
|
||||||
|
}
|
||||||
|
Address adr;
|
||||||
|
try {
|
||||||
|
adr = Address.parse(reviewer);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new UnprocessableEntityException(String.format("email invalid %s", reviewer));
|
||||||
|
}
|
||||||
|
if (!OutgoingEmailValidator.isValid(adr.getEmail())) {
|
||||||
|
throw new UnprocessableEntityException(String.format("email invalid %s", reviewer));
|
||||||
|
}
|
||||||
|
return new Addition(
|
||||||
|
reviewer, rsrc, null, ImmutableList.of(adr), state, notify, accountsToNotify);
|
||||||
|
}
|
||||||
|
|
||||||
private Addition putGroup(ChangeResource rsrc, AddReviewerInput input)
|
private Addition putGroup(ChangeResource rsrc, AddReviewerInput input)
|
||||||
throws RestApiException, OrmException, IOException {
|
throws RestApiException, OrmException, IOException {
|
||||||
GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer);
|
GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer);
|
||||||
@@ -283,6 +337,7 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
input.reviewer,
|
input.reviewer,
|
||||||
rsrc,
|
rsrc,
|
||||||
reviewers,
|
reviewers,
|
||||||
|
null,
|
||||||
input.state(),
|
input.state(),
|
||||||
input.notify,
|
input.notify,
|
||||||
notifyUtil.resolveAccounts(input.notifyDetails));
|
notifyUtil.resolveAccounts(input.notifyDetails));
|
||||||
@@ -314,26 +369,28 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
final Op op;
|
final Op op;
|
||||||
|
|
||||||
private final Map<Account.Id, ChangeControl> reviewers;
|
private final Map<Account.Id, ChangeControl> reviewers;
|
||||||
|
private final Collection<Address> reviewersByEmail;
|
||||||
|
|
||||||
protected Addition(String reviewer) {
|
protected Addition(String reviewer) {
|
||||||
this(reviewer, null, null, REVIEWER, null, ImmutableListMultimap.of());
|
this(reviewer, null, null, null, REVIEWER, null, ImmutableListMultimap.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Addition(
|
protected Addition(
|
||||||
String reviewer,
|
String reviewer,
|
||||||
ChangeResource rsrc,
|
ChangeResource rsrc,
|
||||||
Map<Account.Id, ChangeControl> reviewers,
|
@Nullable Map<Account.Id, ChangeControl> reviewers,
|
||||||
|
@Nullable Collection<Address> reviewersByEmail,
|
||||||
ReviewerState state,
|
ReviewerState state,
|
||||||
NotifyHandling notify,
|
NotifyHandling notify,
|
||||||
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
|
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
|
||||||
result = new AddReviewerResult(reviewer);
|
result = new AddReviewerResult(reviewer);
|
||||||
if (reviewers == null) {
|
this.reviewers = reviewers == null ? ImmutableMap.of() : reviewers;
|
||||||
this.reviewers = ImmutableMap.of();
|
this.reviewersByEmail = reviewersByEmail == null ? ImmutableList.of() : reviewersByEmail;
|
||||||
|
if (reviewers == null && reviewersByEmail == null) {
|
||||||
op = null;
|
op = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.reviewers = reviewers;
|
op = new Op(rsrc, this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify);
|
||||||
op = new Op(rsrc, reviewers, state, notify, accountsToNotify);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void gatherResults() throws OrmException {
|
void gatherResults() throws OrmException {
|
||||||
@@ -345,6 +402,9 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
result.ccs.add(json.format(new ReviewerInfo(accountId.get()), reviewers.get(accountId)));
|
result.ccs.add(json.format(new ReviewerInfo(accountId.get()), reviewers.get(accountId)));
|
||||||
}
|
}
|
||||||
accountLoaderFactory.create(true).fill(result.ccs);
|
accountLoaderFactory.create(true).fill(result.ccs);
|
||||||
|
for (Address a : reviewersByEmail) {
|
||||||
|
result.ccs.add(new AccountInfo(a.getName(), a.getEmail()));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result.reviewers = Lists.newArrayListWithCapacity(op.addedReviewers.size());
|
result.reviewers = Lists.newArrayListWithCapacity(op.addedReviewers.size());
|
||||||
for (PatchSetApproval psa : op.addedReviewers) {
|
for (PatchSetApproval psa : op.addedReviewers) {
|
||||||
@@ -356,17 +416,22 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
ImmutableList.of(psa)));
|
ImmutableList.of(psa)));
|
||||||
}
|
}
|
||||||
accountLoaderFactory.create(true).fill(result.reviewers);
|
accountLoaderFactory.create(true).fill(result.reviewers);
|
||||||
|
for (Address a : reviewersByEmail) {
|
||||||
|
result.reviewers.add(ReviewerInfo.byEmail(a.getName(), a.getEmail()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Op implements BatchUpdateOp {
|
public class Op implements BatchUpdateOp {
|
||||||
final Map<Account.Id, ChangeControl> reviewers;
|
final Map<Account.Id, ChangeControl> reviewers;
|
||||||
|
final Collection<Address> reviewersByEmail;
|
||||||
final ReviewerState state;
|
final ReviewerState state;
|
||||||
final NotifyHandling notify;
|
final NotifyHandling notify;
|
||||||
final ListMultimap<RecipientType, Account.Id> accountsToNotify;
|
final ListMultimap<RecipientType, Account.Id> accountsToNotify;
|
||||||
List<PatchSetApproval> addedReviewers;
|
List<PatchSetApproval> addedReviewers = new ArrayList<>();
|
||||||
Collection<Account.Id> addedCCs;
|
Collection<Account.Id> addedCCs = new ArrayList<>();
|
||||||
|
Collection<Address> addedCCsByEmail = new ArrayList<>();
|
||||||
|
|
||||||
private final ChangeResource rsrc;
|
private final ChangeResource rsrc;
|
||||||
private PatchSet patchSet;
|
private PatchSet patchSet;
|
||||||
@@ -374,11 +439,13 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
Op(
|
Op(
|
||||||
ChangeResource rsrc,
|
ChangeResource rsrc,
|
||||||
Map<Account.Id, ChangeControl> reviewers,
|
Map<Account.Id, ChangeControl> reviewers,
|
||||||
|
Collection<Address> reviewersByEmail,
|
||||||
ReviewerState state,
|
ReviewerState state,
|
||||||
NotifyHandling notify,
|
NotifyHandling notify,
|
||||||
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
|
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
|
||||||
this.rsrc = rsrc;
|
this.rsrc = rsrc;
|
||||||
this.reviewers = reviewers;
|
this.reviewers = reviewers;
|
||||||
|
this.reviewersByEmail = reviewersByEmail;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.notify = notify;
|
this.notify = notify;
|
||||||
this.accountsToNotify = checkNotNull(accountsToNotify);
|
this.accountsToNotify = checkNotNull(accountsToNotify);
|
||||||
@@ -387,55 +454,55 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
@Override
|
@Override
|
||||||
public boolean updateChange(ChangeContext ctx)
|
public boolean updateChange(ChangeContext ctx)
|
||||||
throws RestApiException, OrmException, IOException {
|
throws RestApiException, OrmException, IOException {
|
||||||
if (migration.readChanges() && state == CC) {
|
if (!reviewers.isEmpty()) {
|
||||||
addedCCs =
|
if (migration.readChanges() && state == CC) {
|
||||||
approvalsUtil.addCcs(
|
addedCCs =
|
||||||
ctx.getNotes(),
|
approvalsUtil.addCcs(
|
||||||
ctx.getUpdate(ctx.getChange().currentPatchSetId()),
|
ctx.getNotes(),
|
||||||
reviewers.keySet());
|
ctx.getUpdate(ctx.getChange().currentPatchSetId()),
|
||||||
if (addedCCs.isEmpty()) {
|
reviewers.keySet());
|
||||||
return false;
|
if (addedCCs.isEmpty()) {
|
||||||
}
|
return false;
|
||||||
} else {
|
}
|
||||||
addedReviewers =
|
} else {
|
||||||
approvalsUtil.addReviewers(
|
addedReviewers =
|
||||||
ctx.getDb(),
|
approvalsUtil.addReviewers(
|
||||||
ctx.getNotes(),
|
ctx.getDb(),
|
||||||
ctx.getUpdate(ctx.getChange().currentPatchSetId()),
|
ctx.getNotes(),
|
||||||
rsrc.getControl().getLabelTypes(),
|
ctx.getUpdate(ctx.getChange().currentPatchSetId()),
|
||||||
rsrc.getChange(),
|
rsrc.getControl().getLabelTypes(),
|
||||||
reviewers.keySet());
|
rsrc.getChange(),
|
||||||
if (addedReviewers.isEmpty()) {
|
reviewers.keySet());
|
||||||
return false;
|
if (addedReviewers.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (Address a : reviewersByEmail) {
|
||||||
|
ctx.getUpdate(ctx.getChange().currentPatchSetId())
|
||||||
|
.putReviewerByEmail(a, ReviewerStateInternal.fromReviewerState(state));
|
||||||
|
}
|
||||||
|
|
||||||
patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
|
patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postUpdate(Context ctx) throws Exception {
|
public void postUpdate(Context ctx) throws Exception {
|
||||||
if (addedReviewers != null || addedCCs != null) {
|
emailReviewers(
|
||||||
if (addedReviewers == null) {
|
rsrc.getChange(),
|
||||||
addedReviewers = new ArrayList<>();
|
Lists.transform(addedReviewers, r -> r.getAccountId()),
|
||||||
}
|
addedCCs == null ? ImmutableList.of() : addedCCs,
|
||||||
if (addedCCs == null) {
|
reviewersByEmail,
|
||||||
addedCCs = new ArrayList<>();
|
addedCCsByEmail,
|
||||||
}
|
notify,
|
||||||
emailReviewers(
|
accountsToNotify);
|
||||||
rsrc.getChange(),
|
if (!addedReviewers.isEmpty()) {
|
||||||
Lists.transform(addedReviewers, r -> r.getAccountId()),
|
List<Account> reviewers =
|
||||||
addedCCs,
|
Lists.transform(
|
||||||
notify,
|
addedReviewers, psa -> accountCache.get(psa.getAccountId()).getAccount());
|
||||||
accountsToNotify);
|
reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
|
||||||
if (!addedReviewers.isEmpty()) {
|
|
||||||
List<Account> reviewers =
|
|
||||||
Lists.transform(
|
|
||||||
addedReviewers, psa -> accountCache.get(psa.getAccountId()).getAccount());
|
|
||||||
reviewerAdded.fire(
|
|
||||||
rsrc.getChange(), patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -444,9 +511,11 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
Change change,
|
Change change,
|
||||||
Collection<Account.Id> added,
|
Collection<Account.Id> added,
|
||||||
Collection<Account.Id> copied,
|
Collection<Account.Id> copied,
|
||||||
|
Collection<Address> addedByEmail,
|
||||||
|
Collection<Address> copiedByEmail,
|
||||||
NotifyHandling notify,
|
NotifyHandling notify,
|
||||||
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
|
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
|
||||||
if (added.isEmpty() && copied.isEmpty()) {
|
if (added.isEmpty() && copied.isEmpty() && addedByEmail.isEmpty() && copiedByEmail.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +535,7 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
toCopy.add(id);
|
toCopy.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toMail.isEmpty() && toCopy.isEmpty()) {
|
if (toMail.isEmpty() && toCopy.isEmpty() && addedByEmail.isEmpty() && copiedByEmail.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +547,9 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
|
|||||||
cm.setAccountsToNotify(accountsToNotify);
|
cm.setAccountsToNotify(accountsToNotify);
|
||||||
cm.setFrom(userId);
|
cm.setFrom(userId);
|
||||||
cm.addReviewers(toMail);
|
cm.addReviewers(toMail);
|
||||||
|
cm.addReviewersByEmail(addedByEmail);
|
||||||
cm.addExtraCC(toCopy);
|
cm.addExtraCC(toCopy);
|
||||||
|
cm.addExtraCCByEmail(copiedByEmail);
|
||||||
cm.send();
|
cm.send();
|
||||||
} catch (Exception err) {
|
} catch (Exception err) {
|
||||||
log.error("Cannot send email to new reviewers of change " + change.getId(), err);
|
log.error("Cannot send email to new reviewers of change " + change.getId(), err);
|
||||||
|
|||||||
@@ -14,11 +14,15 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.change;
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
import com.google.gerrit.common.Nullable;
|
||||||
import com.google.gerrit.extensions.restapi.RestResource;
|
import com.google.gerrit.extensions.restapi.RestResource;
|
||||||
import com.google.gerrit.extensions.restapi.RestView;
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gerrit.server.project.ChangeControl;
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
import com.google.inject.TypeLiteral;
|
import com.google.inject.TypeLiteral;
|
||||||
import com.google.inject.assistedinject.Assisted;
|
import com.google.inject.assistedinject.Assisted;
|
||||||
@@ -36,7 +40,8 @@ public class ReviewerResource implements RestResource {
|
|||||||
|
|
||||||
private final ChangeResource change;
|
private final ChangeResource change;
|
||||||
private final RevisionResource revision;
|
private final RevisionResource revision;
|
||||||
private final IdentifiedUser user;
|
@Nullable private final IdentifiedUser user;
|
||||||
|
@Nullable private final Address address;
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
ReviewerResource(
|
ReviewerResource(
|
||||||
@@ -44,8 +49,9 @@ public class ReviewerResource implements RestResource {
|
|||||||
@Assisted ChangeResource change,
|
@Assisted ChangeResource change,
|
||||||
@Assisted Account.Id id) {
|
@Assisted Account.Id id) {
|
||||||
this.change = change;
|
this.change = change;
|
||||||
this.revision = null;
|
|
||||||
this.user = userFactory.create(id);
|
this.user = userFactory.create(id);
|
||||||
|
this.revision = null;
|
||||||
|
this.address = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
@@ -56,6 +62,21 @@ public class ReviewerResource implements RestResource {
|
|||||||
this.revision = revision;
|
this.revision = revision;
|
||||||
this.change = revision.getChangeResource();
|
this.change = revision.getChangeResource();
|
||||||
this.user = userFactory.create(id);
|
this.user = userFactory.create(id);
|
||||||
|
this.address = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReviewerResource(ChangeResource change, Address address) {
|
||||||
|
this.change = change;
|
||||||
|
this.address = address;
|
||||||
|
this.revision = null;
|
||||||
|
this.user = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReviewerResource(RevisionResource revision, Address address) {
|
||||||
|
this.revision = revision;
|
||||||
|
this.change = revision.getChangeResource();
|
||||||
|
this.address = address;
|
||||||
|
this.user = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChangeResource getChangeResource() {
|
public ChangeResource getChangeResource() {
|
||||||
@@ -75,10 +96,28 @@ public class ReviewerResource implements RestResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IdentifiedUser getReviewerUser() {
|
public IdentifiedUser getReviewerUser() {
|
||||||
|
checkArgument(user != null, "no user provided");
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Address getReviewerByEmail() {
|
||||||
|
checkArgument(address != null, "no address provided");
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Check if this resource was constructed by email or by {@code Account.Id}.
|
||||||
|
*
|
||||||
|
* @return true if the resource was constructed by providing an {@code Address}; false if the
|
||||||
|
* resource was constructed by providing an {@code Account.Id}.
|
||||||
|
*/
|
||||||
|
public boolean isByEmail() {
|
||||||
|
return user == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the control for the caller's user.
|
||||||
|
*
|
||||||
* @return the control for the caller's user (as opposed to the reviewer's user as returned by
|
* @return the control for the caller's user (as opposed to the reviewer's user as returned by
|
||||||
* {@link #getReviewerControl()}).
|
* {@link #getReviewerControl()}).
|
||||||
*/
|
*/
|
||||||
@@ -87,10 +126,13 @@ public class ReviewerResource implements RestResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Get the control for the reviewer's user.
|
||||||
|
*
|
||||||
* @return the control for the reviewer's user (as opposed to the caller's user as returned by
|
* @return the control for the reviewer's user (as opposed to the caller's user as returned by
|
||||||
* {@link #getControl()}).
|
* {@link #getControl()}).
|
||||||
*/
|
*/
|
||||||
public ChangeControl getReviewerControl() {
|
public ChangeControl getReviewerControl() {
|
||||||
|
checkArgument(user != null, "no user provided");
|
||||||
return change.getControl().forUser(user);
|
return change.getControl().forUser(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.google.gerrit.reviewdb.client.Account;
|
|||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.ApprovalsUtil;
|
import com.google.gerrit.server.ApprovalsUtil;
|
||||||
import com.google.gerrit.server.account.AccountsCollection;
|
import com.google.gerrit.server.account.AccountsCollection;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
@@ -69,12 +70,26 @@ public class Reviewers implements ChildCollection<ChangeResource, ReviewerResour
|
|||||||
@Override
|
@Override
|
||||||
public ReviewerResource parse(ChangeResource rsrc, IdString id)
|
public ReviewerResource parse(ChangeResource rsrc, IdString id)
|
||||||
throws OrmException, ResourceNotFoundException, AuthException {
|
throws OrmException, ResourceNotFoundException, AuthException {
|
||||||
Account.Id accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
|
Address address = Address.tryParse(id.get());
|
||||||
|
|
||||||
|
Account.Id accountId = null;
|
||||||
|
try {
|
||||||
|
accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
|
||||||
|
} catch (ResourceNotFoundException e) {
|
||||||
|
if (address == null) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
// See if the id exists as a reviewer for this change
|
// See if the id exists as a reviewer for this change
|
||||||
if (fetchAccountIds(rsrc).contains(accountId)) {
|
if (accountId != null && fetchAccountIds(rsrc).contains(accountId)) {
|
||||||
return resourceFactory.create(rsrc, accountId);
|
return resourceFactory.create(rsrc, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See if the address exists as a reviewer on the change
|
||||||
|
if (address != null && rsrc.getNotes().getReviewersByEmail().all().contains(address)) {
|
||||||
|
return new ReviewerResource(rsrc, address);
|
||||||
|
}
|
||||||
|
|
||||||
throw new ResourceNotFoundException(id);
|
throw new ResourceNotFoundException(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import com.google.gerrit.reviewdb.client.Account;
|
|||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.ApprovalsUtil;
|
import com.google.gerrit.server.ApprovalsUtil;
|
||||||
import com.google.gerrit.server.account.AccountsCollection;
|
import com.google.gerrit.server.account.AccountsCollection;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
@@ -73,14 +74,28 @@ public class RevisionReviewers implements ChildCollection<RevisionResource, Revi
|
|||||||
if (!rsrc.isCurrent()) {
|
if (!rsrc.isCurrent()) {
|
||||||
throw new MethodNotAllowedException("Cannot access on non-current patch set");
|
throw new MethodNotAllowedException("Cannot access on non-current patch set");
|
||||||
}
|
}
|
||||||
|
Address address = Address.tryParse(id.get());
|
||||||
|
|
||||||
Account.Id accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
|
Account.Id accountId = null;
|
||||||
|
try {
|
||||||
|
accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
|
||||||
|
} catch (ResourceNotFoundException e) {
|
||||||
|
if (address == null) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
Collection<Account.Id> reviewers =
|
Collection<Account.Id> reviewers =
|
||||||
approvalsUtil.getReviewers(dbProvider.get(), rsrc.getNotes()).all();
|
approvalsUtil.getReviewers(dbProvider.get(), rsrc.getNotes()).all();
|
||||||
|
// See if the id exists as a reviewer for this change
|
||||||
if (reviewers.contains(accountId)) {
|
if (reviewers.contains(accountId)) {
|
||||||
return resourceFactory.create(rsrc, accountId);
|
return resourceFactory.create(rsrc, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See if the address exists as a reviewer on the change
|
||||||
|
if (address != null && rsrc.getNotes().getReviewersByEmail().all().contains(address)) {
|
||||||
|
return new ReviewerResource(rsrc, address);
|
||||||
|
}
|
||||||
|
|
||||||
throw new ResourceNotFoundException(id);
|
throw new ResourceNotFoundException(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ public class Address {
|
|||||||
throw new IllegalArgumentException("Invalid email address: " + in);
|
throw new IllegalArgumentException("Invalid email address: " + in);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Address tryParse(String in) {
|
||||||
|
try {
|
||||||
|
return parse(in);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final String email;
|
final String email;
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,20 @@ public abstract class ChangeEmail extends NotificationEmail {
|
|||||||
setHeader("X-Gerrit-Change-Number", "" + change.getChangeId());
|
setHeader("X-Gerrit-Change-Number", "" + change.getChangeId());
|
||||||
setChangeUrlHeader();
|
setChangeUrlHeader();
|
||||||
setCommitIdHeader();
|
setCommitIdHeader();
|
||||||
|
|
||||||
|
if (notify.ordinal() >= NotifyHandling.OWNER_REVIEWERS.ordinal()) {
|
||||||
|
try {
|
||||||
|
// TODO(hiesel) Load from index instead
|
||||||
|
addByEmail(
|
||||||
|
RecipientType.CC,
|
||||||
|
changeData.notes().getReviewersByEmail().byState(ReviewerStateInternal.CC));
|
||||||
|
addByEmail(
|
||||||
|
RecipientType.TO,
|
||||||
|
changeData.notes().getReviewersByEmail().byState(ReviewerStateInternal.REVIEWER));
|
||||||
|
} catch (OrmException e) {
|
||||||
|
throw new EmailException("Failed to add unregistered CCs " + change.getChangeId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setChangeUrlHeader() {
|
private void setChangeUrlHeader() {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.google.gerrit.reviewdb.client.Account;
|
|||||||
import com.google.gerrit.reviewdb.client.Change;
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
import com.google.gerrit.server.account.WatchConfig.NotifyType;
|
import com.google.gerrit.server.account.WatchConfig.NotifyType;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.assistedinject.Assisted;
|
import com.google.inject.assistedinject.Assisted;
|
||||||
@@ -32,6 +33,7 @@ import java.util.Set;
|
|||||||
/** Let users know that a reviewer and possibly her review have been removed. */
|
/** Let users know that a reviewer and possibly her review have been removed. */
|
||||||
public class DeleteReviewerSender extends ReplyToChangeSender {
|
public class DeleteReviewerSender extends ReplyToChangeSender {
|
||||||
private final Set<Account.Id> reviewers = new HashSet<>();
|
private final Set<Account.Id> reviewers = new HashSet<>();
|
||||||
|
private final Set<Address> reviewersByEmail = new HashSet<>();
|
||||||
|
|
||||||
public interface Factory extends ReplyToChangeSender.Factory<DeleteReviewerSender> {
|
public interface Factory extends ReplyToChangeSender.Factory<DeleteReviewerSender> {
|
||||||
@Override
|
@Override
|
||||||
@@ -49,6 +51,10 @@ public class DeleteReviewerSender extends ReplyToChangeSender {
|
|||||||
reviewers.addAll(cc);
|
reviewers.addAll(cc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addReviewersByEmail(Collection<Address> cc) {
|
||||||
|
reviewersByEmail.addAll(cc);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void init() throws EmailException {
|
protected void init() throws EmailException {
|
||||||
super.init();
|
super.init();
|
||||||
@@ -58,6 +64,7 @@ public class DeleteReviewerSender extends ReplyToChangeSender {
|
|||||||
ccExistingReviewers();
|
ccExistingReviewers();
|
||||||
includeWatchers(NotifyType.ALL_COMMENTS);
|
includeWatchers(NotifyType.ALL_COMMENTS);
|
||||||
add(RecipientType.TO, reviewers);
|
add(RecipientType.TO, reviewers);
|
||||||
|
addByEmail(RecipientType.TO, reviewersByEmail);
|
||||||
removeUsersThatIgnoredTheChange();
|
removeUsersThatIgnoredTheChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +77,16 @@ public class DeleteReviewerSender extends ReplyToChangeSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getReviewerNames() {
|
public List<String> getReviewerNames() {
|
||||||
if (reviewers.isEmpty()) {
|
if (reviewers.isEmpty() && reviewersByEmail.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
List<String> names = new ArrayList<>();
|
List<String> names = new ArrayList<>();
|
||||||
for (Account.Id id : reviewers) {
|
for (Account.Id id : reviewers) {
|
||||||
names.add(getNameFor(id));
|
names.add(getNameFor(id));
|
||||||
}
|
}
|
||||||
|
for (Address a : reviewersByEmail) {
|
||||||
|
names.add(a.toString());
|
||||||
|
}
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package com.google.gerrit.server.mail.send;
|
|||||||
import com.google.gerrit.common.errors.EmailException;
|
import com.google.gerrit.common.errors.EmailException;
|
||||||
import com.google.gerrit.extensions.api.changes.RecipientType;
|
import com.google.gerrit.extensions.api.changes.RecipientType;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
|
import com.google.gerrit.server.mail.Address;
|
||||||
import com.google.gerrit.server.query.change.ChangeData;
|
import com.google.gerrit.server.query.change.ChangeData;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -28,7 +29,9 @@ import java.util.Set;
|
|||||||
/** Sends an email alerting a user to a new change for them to review. */
|
/** Sends an email alerting a user to a new change for them to review. */
|
||||||
public abstract class NewChangeSender extends ChangeEmail {
|
public abstract class NewChangeSender extends ChangeEmail {
|
||||||
private final Set<Account.Id> reviewers = new HashSet<>();
|
private final Set<Account.Id> reviewers = new HashSet<>();
|
||||||
|
private final Set<Address> reviewersByEmail = new HashSet<>();
|
||||||
private final Set<Account.Id> extraCC = new HashSet<>();
|
private final Set<Account.Id> extraCC = new HashSet<>();
|
||||||
|
private final Set<Address> extraCCByEmail = new HashSet<>();
|
||||||
|
|
||||||
protected NewChangeSender(EmailArguments ea, ChangeData cd) throws OrmException {
|
protected NewChangeSender(EmailArguments ea, ChangeData cd) throws OrmException {
|
||||||
super(ea, "newchange", cd);
|
super(ea, "newchange", cd);
|
||||||
@@ -38,10 +41,18 @@ public abstract class NewChangeSender extends ChangeEmail {
|
|||||||
reviewers.addAll(cc);
|
reviewers.addAll(cc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addReviewersByEmail(final Collection<Address> cc) {
|
||||||
|
reviewersByEmail.addAll(cc);
|
||||||
|
}
|
||||||
|
|
||||||
public void addExtraCC(final Collection<Account.Id> cc) {
|
public void addExtraCC(final Collection<Account.Id> cc) {
|
||||||
extraCC.addAll(cc);
|
extraCC.addAll(cc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addExtraCCByEmail(final Collection<Address> cc) {
|
||||||
|
extraCCByEmail.addAll(cc);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void init() throws EmailException {
|
protected void init() throws EmailException {
|
||||||
super.init();
|
super.init();
|
||||||
@@ -55,9 +66,11 @@ public abstract class NewChangeSender extends ChangeEmail {
|
|||||||
case ALL:
|
case ALL:
|
||||||
default:
|
default:
|
||||||
add(RecipientType.CC, extraCC);
|
add(RecipientType.CC, extraCC);
|
||||||
|
extraCCByEmail.stream().forEach(cc -> add(RecipientType.CC, cc));
|
||||||
//$FALL-THROUGH$
|
//$FALL-THROUGH$
|
||||||
case OWNER_REVIEWERS:
|
case OWNER_REVIEWERS:
|
||||||
add(RecipientType.TO, reviewers);
|
add(RecipientType.TO, reviewers);
|
||||||
|
addByEmail(RecipientType.TO, reviewersByEmail);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -441,6 +441,13 @@ public abstract class OutgoingEmail {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Schedule this message for delivery to the listed address. */
|
||||||
|
protected void addByEmail(final RecipientType rt, final Collection<Address> list) {
|
||||||
|
for (final Address id : list) {
|
||||||
|
add(rt, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void add(final RecipientType rt, final UserIdentity who) {
|
protected void add(final RecipientType rt, final UserIdentity who) {
|
||||||
if (who != null && who.getAccount() != null) {
|
if (who != null && who.getAccount() != null) {
|
||||||
add(rt, who.getAccount());
|
add(rt, who.getAccount());
|
||||||
|
|||||||
@@ -29,13 +29,17 @@ public enum ReviewerStateInternal {
|
|||||||
/** The user was previously a reviewer on the change, but was removed. */
|
/** The user was previously a reviewer on the change, but was removed. */
|
||||||
REMOVED(new FooterKey("Removed"), ReviewerState.REMOVED);
|
REMOVED(new FooterKey("Removed"), ReviewerState.REMOVED);
|
||||||
|
|
||||||
|
public static ReviewerStateInternal fromReviewerState(ReviewerState state) {
|
||||||
|
return ReviewerStateInternal.values()[state.ordinal()];
|
||||||
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
boolean ok = true;
|
boolean ok = true;
|
||||||
if (ReviewerStateInternal.values().length != ReviewerState.values().length) {
|
if (ReviewerStateInternal.values().length != ReviewerState.values().length) {
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
for (ReviewerStateInternal s : ReviewerStateInternal.values()) {
|
for (int i = 0; i < ReviewerStateInternal.values().length; i++) {
|
||||||
ok &= s.name().equals(s.state.name());
|
ok &= ReviewerState.values()[i].equals(ReviewerStateInternal.values()[i].state);
|
||||||
}
|
}
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
|
|||||||
Reference in New Issue
Block a user