Suggest Reviewers REST endpoint: Add option to suggest CCs

The reviewer suggestion excludes existing reviewers. This means if it is
used to suggest CCs, it doesn't show existing reviewers and hence users
cannot easily move existing reviewers to CC. Add an option to suggest
accounts for CC that includes existing reviewers, but excludes existing
CCs.

Bug: Issue 11354
Change-Id: Ic8960452d1eb5fe1b4559a27d6d7104428b9be85
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin
2019-08-27 09:43:31 +02:00
parent 2639eaad7f
commit 0ca3f727bb
7 changed files with 71 additions and 4 deletions

View File

@@ -3035,6 +3035,14 @@ As result a list of link:#suggested-reviewer-info[SuggestedReviewerInfo] entries
] ]
---- ----
To suggest CCs `reviewer-state=CC` can be specified as additional URL
parameter. This includes existing reviewers in the result, but excludes
existing CCs.
--
'GET /changes/link:#change-id[\{change-id\}]/suggest_reviewers?q=J&reviewer-state=CC'
--
[[get-reviewer]] [[get-reviewer]]
=== Get Reviewer === Get Reviewer
-- --

View File

@@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.ListChangesOption; 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.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -216,6 +217,10 @@ public interface ChangeApi {
return suggestReviewers().withQuery(query); return suggestReviewers().withQuery(query);
} }
default SuggestedReviewersRequest suggestCcs(String query) throws RestApiException {
return suggestReviewers().forCc().withQuery(query);
}
/** /**
* Retrieve reviewers ({@code ReviewerState.REVIEWER} and {@code ReviewerState.CC}) on the change. * Retrieve reviewers ({@code ReviewerState.REVIEWER} and {@code ReviewerState.CC}) on the change.
*/ */
@@ -382,6 +387,7 @@ public interface ChangeApi {
private String query; private String query;
private int limit; private int limit;
private boolean excludeGroups; private boolean excludeGroups;
private ReviewerState reviewerState = ReviewerState.REVIEWER;
public abstract List<SuggestedReviewerInfo> get() throws RestApiException; public abstract List<SuggestedReviewerInfo> get() throws RestApiException;
@@ -400,6 +406,11 @@ public interface ChangeApi {
return this; return this;
} }
public SuggestedReviewersRequest forCc() {
this.reviewerState = ReviewerState.CC;
return this;
}
public String getQuery() { public String getQuery() {
return query; return query;
} }
@@ -411,6 +422,10 @@ public interface ChangeApi {
public boolean getExcludeGroups() { public boolean getExcludeGroups() {
return excludeGroups; return excludeGroups;
} }
public ReviewerState getReviewerState() {
return reviewerState;
}
} }
/** /**

View File

@@ -449,6 +449,7 @@ class ChangeApiImpl implements ChangeApi {
suggestReviewers.setQuery(r.getQuery()); suggestReviewers.setQuery(r.getQuery());
suggestReviewers.setLimit(r.getLimit()); suggestReviewers.setLimit(r.getLimit());
suggestReviewers.setExcludeGroups(r.getExcludeGroups()); suggestReviewers.setExcludeGroups(r.getExcludeGroups());
suggestReviewers.setReviewerState(r.getReviewerState());
return suggestReviewers.apply(change).value(); return suggestReviewers.apply(change).value();
} catch (Exception e) { } catch (Exception e) {
throw asRestApiException("Cannot retrieve suggested reviewers", e); throw asRestApiException("Cannot retrieve suggested reviewers", e);

View File

@@ -14,7 +14,6 @@
package com.google.gerrit.server.restapi.change; package com.google.gerrit.server.restapi.change;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
import com.google.common.base.Strings; import com.google.common.base.Strings;
@@ -23,6 +22,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.index.query.Predicate; import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException; import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
@@ -34,6 +34,7 @@ import com.google.gerrit.server.change.SuggestedReviewer;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.index.change.ChangeField;
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.plugincontext.PluginMapContext; import com.google.gerrit.server.plugincontext.PluginMapContext;
import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData;
@@ -96,6 +97,7 @@ public class ReviewerRecommender {
} }
public List<Account.Id> suggestReviewers( public List<Account.Id> suggestReviewers(
ReviewerState reviewerState,
@Nullable ChangeNotes changeNotes, @Nullable ChangeNotes changeNotes,
SuggestReviewers suggestReviewers, SuggestReviewers suggestReviewers,
ProjectState projectState, ProjectState projectState,
@@ -178,7 +180,7 @@ public class ReviewerRecommender {
// Remove existing reviewers // Remove existing reviewers
approvalsUtil approvalsUtil
.getReviewers(changeNotes) .getReviewers(changeNotes)
.byState(REVIEWER) .byState(ReviewerStateInternal.fromReviewerState(reviewerState))
.forEach( .forEach(
r -> { r -> {
if (reviewerScores.remove(r) != null) { if (reviewerScores.remove(r) != null) {

View File

@@ -24,6 +24,7 @@ import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.GroupBaseInfo; import com.google.gerrit.extensions.common.GroupBaseInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo; import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.extensions.restapi.Url;
@@ -157,6 +158,7 @@ public class ReviewersUtil {
} }
public List<SuggestedReviewerInfo> suggestReviewers( public List<SuggestedReviewerInfo> suggestReviewers(
ReviewerState reviewerState,
@Nullable ChangeNotes changeNotes, @Nullable ChangeNotes changeNotes,
SuggestReviewers suggestReviewers, SuggestReviewers suggestReviewers,
ProjectState projectState, ProjectState projectState,
@@ -190,7 +192,8 @@ public class ReviewersUtil {
} }
List<Account.Id> sortedRecommendations = List<Account.Id> sortedRecommendations =
recommendAccounts(changeNotes, suggestReviewers, projectState, candidateList); recommendAccounts(
reviewerState, changeNotes, suggestReviewers, projectState, candidateList);
logger.atFine().log("Sorted recommendations: %s", sortedRecommendations); logger.atFine().log("Sorted recommendations: %s", sortedRecommendations);
// Filter accounts by visibility and enforce limit // Filter accounts by visibility and enforce limit
@@ -286,6 +289,7 @@ public class ReviewersUtil {
} }
private List<Account.Id> recommendAccounts( private List<Account.Id> recommendAccounts(
ReviewerState reviewerState,
@Nullable ChangeNotes changeNotes, @Nullable ChangeNotes changeNotes,
SuggestReviewers suggestReviewers, SuggestReviewers suggestReviewers,
ProjectState projectState, ProjectState projectState,
@@ -293,7 +297,7 @@ public class ReviewersUtil {
throws IOException, ConfigInvalidException { throws IOException, ConfigInvalidException {
try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) { try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) {
return reviewerRecommender.suggestReviewers( return reviewerRecommender.suggestReviewers(
changeNotes, suggestReviewers, projectState, candidateList); reviewerState, changeNotes, suggestReviewers, projectState, candidateList);
} }
} }

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change; package com.google.gerrit.server.restapi.change;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountVisibility; import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo; import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.AuthException;
@@ -44,6 +45,7 @@ public class SuggestChangeReviewers extends SuggestReviewers
private final ProjectCache projectCache; private final ProjectCache projectCache;
private boolean excludeGroups; private boolean excludeGroups;
private ReviewerState reviewerState = ReviewerState.REVIEWER;
@Option( @Option(
name = "--exclude-groups", name = "--exclude-groups",
@@ -54,6 +56,16 @@ public class SuggestChangeReviewers extends SuggestReviewers
return this; return this;
} }
@Option(
name = "--reviewer-state",
usage =
"The type of reviewers that should be suggested"
+ " (can be 'REVIEWER' or 'CC', default is 'REVIEWER')")
public SuggestChangeReviewers setReviewerState(ReviewerState reviewerState) {
this.reviewerState = reviewerState;
return this;
}
@Inject @Inject
SuggestChangeReviewers( SuggestChangeReviewers(
AccountVisibility av, AccountVisibility av,
@@ -75,8 +87,13 @@ public class SuggestChangeReviewers extends SuggestReviewers
if (!self.get().isIdentifiedUser()) { if (!self.get().isIdentifiedUser()) {
throw new AuthException("Authentication required"); throw new AuthException("Authentication required");
} }
if (reviewerState.equals(ReviewerState.REMOVED)) {
throw new BadRequestException(
String.format("Unsupported reviewer state: %s", ReviewerState.REMOVED));
}
return Response.ok( return Response.ok(
reviewersUtil.suggestReviewers( reviewersUtil.suggestReviewers(
reviewerState,
rsrc.getNotes(), rsrc.getNotes(),
this, this,
projectCache.checkedGet(rsrc.getProject()), projectCache.checkedGet(rsrc.getProject()),

View File

@@ -547,6 +547,22 @@ public class SuggestReviewersIT extends AbstractDaemonTest {
suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of()); suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
} }
@Test
public void suggestReviewerAsCc() throws Exception {
String name = name("foo");
TestAccount foo1 = accountCreator.create(name + "-1");
TestAccount foo2 = accountCreator.create(name + "-2");
String changeId = createChange().getChangeId();
assertReviewers(suggestCcs(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
AddReviewerInput reviewerInput = new AddReviewerInput();
reviewerInput.reviewer = foo2.id().toString();
reviewerInput.state = ReviewerState.REVIEWER;
gApi.changes().id(changeId).addReviewer(reviewerInput);
assertReviewers(suggestCcs(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
}
@Test @Test
public void suggestBySecondaryEmailWithModifyAccount() throws Exception { public void suggestBySecondaryEmailWithModifyAccount() throws Exception {
String secondaryEmail = "foo.secondary@example.com"; String secondaryEmail = "foo.secondary@example.com";
@@ -611,6 +627,10 @@ public class SuggestReviewersIT extends AbstractDaemonTest {
return gApi.changes().id(changeId).suggestReviewers(query).withLimit(n).get(); return gApi.changes().id(changeId).suggestReviewers(query).withLimit(n).get();
} }
private List<SuggestedReviewerInfo> suggestCcs(String changeId, String query) throws Exception {
return gApi.changes().id(changeId).suggestCcs(query).get();
}
private AccountGroup.UUID createGroupWithArbitraryMembers(int numMembers) { private AccountGroup.UUID createGroupWithArbitraryMembers(int numMembers) {
Set<Account.Id> members = Set<Account.Id> members =
IntStream.rangeClosed(1, numMembers) IntStream.rangeClosed(1, numMembers)