Add REST endpoint to suggest reviewers
Change-Id: I28eedd3dbde5750dffa1482f19da5cd7cb3310f1
This commit is contained in:
@@ -1020,6 +1020,48 @@ As result a list of link:#reviewer-info[ReviewerInfo] entries is returned.
|
||||
]
|
||||
----
|
||||
|
||||
[[suggest-reviewers]]
|
||||
Suggest Reviewers
|
||||
~~~~~~~~~~~~~~~~~
|
||||
[verse]
|
||||
'GET /changes/link:#change-id[\{change-id\}]/suggest_reviewers?q=J&n=5'
|
||||
|
||||
Suggest the reviewers for a given query `q` and result limit `n`. If result
|
||||
limit is not passed, then the default 10 is used.
|
||||
|
||||
As result a list of link:#suggested-reviewer-info[SuggestedReviewerInfo] entries is returned.
|
||||
|
||||
.Request
|
||||
----
|
||||
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/suggest_reviewers?q=J HTTP/1.0
|
||||
----
|
||||
|
||||
.Response
|
||||
----
|
||||
HTTP/1.1 200 OK
|
||||
Content-Disposition: attachment
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
|
||||
)]}'
|
||||
[
|
||||
{
|
||||
"kind": "gerritcodereview#suggestedreviewer",
|
||||
"account": {
|
||||
"_account_id": 1000097,
|
||||
"name": "Jane Roe",
|
||||
"email": "jane.roe@example.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "gerritcodereview#suggestedreviewer",
|
||||
"group": {
|
||||
"id": "4fd581c0657268f2bdcc26699fbf9ddb76e3a279",
|
||||
"name": "Joiner"
|
||||
}
|
||||
}
|
||||
]
|
||||
----
|
||||
|
||||
[[get-reviewer]]
|
||||
Get Reviewer
|
||||
~~~~~~~~~~~~
|
||||
@@ -2540,6 +2582,18 @@ permitted to vote on that label.
|
||||
The time and date describing when the approval was made.
|
||||
|===========================
|
||||
|
||||
[[group-base-info]]
|
||||
GroupBaseInfo
|
||||
~~~~~~~~~~~~~
|
||||
The `GroupBaseInfo` entity contains base information about the group.
|
||||
|
||||
[options="header",width="50%",cols="1,6"]
|
||||
|==========================
|
||||
|Field Name |Description
|
||||
|`id` |The id of the group.
|
||||
|`name` |The name of the group.
|
||||
|==========================
|
||||
|
||||
[[change-info]]
|
||||
ChangeInfo
|
||||
~~~~~~~~~~
|
||||
@@ -3118,6 +3172,17 @@ If `SKIP` the parent filters are not called, allowing the test
|
||||
to return results from the input rule.
|
||||
|===========================
|
||||
|
||||
[[suggested-reviewer-info]]
|
||||
SuggestedReviewerInfo
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
The `SuggestedReviewerInfo` entity contains information about a reviewer
|
||||
that can be added to a change (an account or a group).
|
||||
|
||||
`SuggestedReviewerInfo` has either the `account` field that contains
|
||||
the link:rest-api-accounts.html#account-info[AccountInfo] entity, or
|
||||
the `group` field that contains the
|
||||
link:rest-api-changes.html#group-base-info[GroupBaseInfo] entity.
|
||||
|
||||
[[submit-info]]
|
||||
SubmitInfo
|
||||
~~~~~~~~~~
|
||||
|
@@ -0,0 +1,31 @@
|
||||
// Copyright (C) 2013 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.client.groups;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||
import com.google.gwt.core.client.JavaScriptObject;
|
||||
import com.google.gwt.http.client.URL;
|
||||
|
||||
public class GroupBaseInfo extends JavaScriptObject {
|
||||
public final AccountGroup.UUID getGroupUUID() {
|
||||
return new AccountGroup.UUID(URL.decodeQueryString(id()));
|
||||
}
|
||||
|
||||
public final native String id() /*-{ return this.id; }-*/;
|
||||
public final native String name() /*-{ return this.name; }-*/;
|
||||
|
||||
protected GroupBaseInfo() {
|
||||
}
|
||||
}
|
@@ -20,17 +20,11 @@ import com.google.gwt.core.client.JavaScriptObject;
|
||||
import com.google.gwt.core.client.JsArray;
|
||||
import com.google.gwt.http.client.URL;
|
||||
|
||||
public class GroupInfo extends JavaScriptObject {
|
||||
public class GroupInfo extends GroupBaseInfo {
|
||||
public final AccountGroup.Id getGroupId() {
|
||||
return new AccountGroup.Id(group_id());
|
||||
}
|
||||
|
||||
public final AccountGroup.UUID getGroupUUID() {
|
||||
return new AccountGroup.UUID(URL.decodeQueryString(id()));
|
||||
}
|
||||
|
||||
public final native String id() /*-{ return this.id; }-*/;
|
||||
public final native String name() /*-{ return this.name; }-*/;
|
||||
public final native GroupOptionsInfo options() /*-{ return this.options; }-*/;
|
||||
public final native String description() /*-{ return this.description; }-*/;
|
||||
public final native String url() /*-{ return this.url; }-*/;
|
||||
|
@@ -59,6 +59,7 @@ public class Module extends RestApiModule {
|
||||
post(CHANGE_KIND, "index").to(Index.class);
|
||||
|
||||
post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
|
||||
get(CHANGE_KIND, "suggest_reviewers").to(SuggestReviewers.class);
|
||||
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
|
||||
get(REVIEWER_KIND).to(GetReviewer.class);
|
||||
delete(REVIEWER_KIND).to(DeleteReviewer.class);
|
||||
|
@@ -0,0 +1,296 @@
|
||||
// Copyright (C) 2013 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.base.Objects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.gerrit.common.data.GroupReference;
|
||||
import com.google.gerrit.common.errors.NoSuchGroupException;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||
import com.google.gerrit.extensions.restapi.Url;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.account.AccountControl;
|
||||
import com.google.gerrit.server.account.AccountInfo;
|
||||
import com.google.gerrit.server.account.AccountVisibility;
|
||||
import com.google.gerrit.server.account.GroupBackend;
|
||||
import com.google.gerrit.server.account.GroupMembers;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.group.GroupJson.GroupBaseInfo;
|
||||
import com.google.gerrit.server.project.NoSuchProjectException;
|
||||
import com.google.gerrit.server.project.ProjectControl;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.kohsuke.args4j.Option;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
class SuggestReviewers implements RestReadView<ChangeResource> {
|
||||
|
||||
private static final String MAX_SUFFIX = "\u9fa5";
|
||||
private static final int MAX = 10;
|
||||
|
||||
private final AccountInfo.Loader.Factory accountLoaderFactory;
|
||||
private final AccountControl.Factory accountControlFactory;
|
||||
private final GroupMembers.Factory groupMembersFactory;
|
||||
private final AccountCache accountCache;
|
||||
private final Provider<ReviewDb> dbProvider;
|
||||
private final Provider<CurrentUser> currentUser;
|
||||
private final IdentifiedUser.GenericFactory identifiedUserFactory;
|
||||
private final GroupBackend groupBackend;
|
||||
private final boolean suggestAccounts;
|
||||
private final int suggestFrom;
|
||||
private final int maxAllowed;
|
||||
private int limit;
|
||||
private String query;
|
||||
|
||||
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
|
||||
usage = "maximum number of reviewers to list")
|
||||
public void setLimit(int l) {
|
||||
this.limit = l <= 0 ? MAX : Math.min(l, MAX);
|
||||
}
|
||||
|
||||
@Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY",
|
||||
usage = "match reviewers query")
|
||||
public void setQuery(String q) {
|
||||
this.query = q;
|
||||
}
|
||||
|
||||
@Inject
|
||||
SuggestReviewers(AccountVisibility av,
|
||||
AccountInfo.Loader.Factory accountLoaderFactory,
|
||||
AccountControl.Factory accountControlFactory,
|
||||
AccountCache accountCache,
|
||||
GroupMembers.Factory groupMembersFactory,
|
||||
IdentifiedUser.GenericFactory identifiedUserFactory,
|
||||
Provider<CurrentUser> currentUser,
|
||||
Provider<ReviewDb> dbProvider,
|
||||
@GerritServerConfig Config cfg,
|
||||
GroupBackend groupBackend) {
|
||||
this.accountLoaderFactory = accountLoaderFactory;
|
||||
this.accountControlFactory = accountControlFactory;
|
||||
this.accountCache = accountCache;
|
||||
this.groupMembersFactory = groupMembersFactory;
|
||||
this.dbProvider = dbProvider;
|
||||
this.identifiedUserFactory = identifiedUserFactory;
|
||||
this.currentUser = currentUser;
|
||||
this.groupBackend = groupBackend;
|
||||
|
||||
String suggest = cfg.getString("suggest", null, "accounts");
|
||||
if ("OFF".equalsIgnoreCase(suggest)
|
||||
|| "false".equalsIgnoreCase(suggest)) {
|
||||
this.suggestAccounts = false;
|
||||
} else {
|
||||
this.suggestAccounts = (av != AccountVisibility.NONE);
|
||||
}
|
||||
|
||||
this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
|
||||
this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
|
||||
PostReviewers.DEFAULT_MAX_REVIEWERS);
|
||||
}
|
||||
|
||||
private interface VisibilityControl {
|
||||
boolean isVisibleTo(Account account) throws OrmException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
|
||||
throws BadRequestException, OrmException, IOException {
|
||||
if (Strings.isNullOrEmpty(query)) {
|
||||
throw new BadRequestException("missing query field");
|
||||
}
|
||||
|
||||
if (!suggestAccounts || query.length() < suggestFrom) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
VisibilityControl visibilityControl = getVisibility(rsrc);
|
||||
List<AccountInfo> suggestedAccounts = suggestAccount(visibilityControl);
|
||||
accountLoaderFactory.create(true).fill(suggestedAccounts);
|
||||
|
||||
List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
|
||||
for (AccountInfo a : suggestedAccounts) {
|
||||
reviewer.add(new SuggestedReviewerInfo(a));
|
||||
}
|
||||
|
||||
Project p = rsrc.getControl().getProject();
|
||||
for (GroupReference g : suggestAccountGroup(
|
||||
rsrc.getControl().getProjectControl())) {
|
||||
if (suggestGroupAsReviewer(p, g, visibilityControl)) {
|
||||
GroupBaseInfo info = new GroupBaseInfo();
|
||||
info.id = Url.encode(g.getUUID().get());
|
||||
info.name = g.getName();
|
||||
reviewer.add(new SuggestedReviewerInfo(info));
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(reviewer);
|
||||
if (reviewer.size() <= limit) {
|
||||
return reviewer;
|
||||
} else {
|
||||
return reviewer.subList(0, limit);
|
||||
}
|
||||
}
|
||||
|
||||
private VisibilityControl getVisibility(final ChangeResource rsrc) {
|
||||
if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
|
||||
return new VisibilityControl() {
|
||||
@Override
|
||||
public boolean isVisibleTo(Account account) throws OrmException {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return new VisibilityControl() {
|
||||
@Override
|
||||
public boolean isVisibleTo(Account account) throws OrmException {
|
||||
IdentifiedUser who =
|
||||
identifiedUserFactory.create(dbProvider, account.getId());
|
||||
// we can't use changeControl directly as it won't suggest reviewers
|
||||
// to drafts
|
||||
return rsrc.getControl().forUser(who).isRefVisible();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private List<GroupReference> suggestAccountGroup(ProjectControl ctl) {
|
||||
return Lists.newArrayList(
|
||||
Iterables.limit(groupBackend.suggest(query, ctl), limit));
|
||||
}
|
||||
|
||||
private List<AccountInfo> suggestAccount(VisibilityControl visibilityControl)
|
||||
throws OrmException {
|
||||
String a = query;
|
||||
String b = a + MAX_SUFFIX;
|
||||
|
||||
LinkedHashMap<Account.Id, AccountInfo> r = Maps.newLinkedHashMap();
|
||||
for (Account p : dbProvider.get().accounts()
|
||||
.suggestByFullName(a, b, limit)) {
|
||||
addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
|
||||
}
|
||||
|
||||
if (r.size() < limit) {
|
||||
for (Account p : dbProvider.get().accounts()
|
||||
.suggestByPreferredEmail(a, b, limit - r.size())) {
|
||||
addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
|
||||
}
|
||||
}
|
||||
|
||||
if (r.size() < limit) {
|
||||
for (AccountExternalId e : dbProvider.get().accountExternalIds()
|
||||
.suggestByEmailAddress(a, b, limit - r.size())) {
|
||||
if (!r.containsKey(e.getAccountId())) {
|
||||
Account p = accountCache.get(e.getAccountId()).getAccount();
|
||||
AccountInfo info = new AccountInfo(p.getId());
|
||||
addSuggestion(r, p, info, visibilityControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Lists.newArrayList(r.values());
|
||||
}
|
||||
|
||||
private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account,
|
||||
AccountInfo info, VisibilityControl visibilityControl)
|
||||
throws OrmException {
|
||||
if (!map.containsKey(account.getId())
|
||||
&& account.isActive()
|
||||
// Can the suggestion see the change?
|
||||
&& visibilityControl.isVisibleTo(account)
|
||||
// Can the account see the current user?
|
||||
&& accountControlFactory.get().canSee(account)) {
|
||||
map.put(account.getId(), info);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean suggestGroupAsReviewer(Project project,
|
||||
GroupReference group, VisibilityControl visibilityControl)
|
||||
throws OrmException, IOException {
|
||||
if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Set<Account> members = groupMembersFactory
|
||||
.create(currentUser.get())
|
||||
.listAccounts(group.getUUID(), project.getNameKey());
|
||||
|
||||
if (members.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxAllowed > 0 && members.size() > maxAllowed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// require that at least one member in the group can see the change
|
||||
for (Account account : members) {
|
||||
if (visibilityControl.isVisibleTo(account)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (NoSuchGroupException e) {
|
||||
return false;
|
||||
} catch (NoSuchProjectException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static class SuggestedReviewerInfo implements Comparable<SuggestedReviewerInfo> {
|
||||
String kind = "gerritcodereview#suggestedreviewer";
|
||||
AccountInfo account;
|
||||
GroupBaseInfo group;
|
||||
|
||||
SuggestedReviewerInfo(AccountInfo a) {
|
||||
this.account = a;
|
||||
}
|
||||
|
||||
SuggestedReviewerInfo(GroupBaseInfo g) {
|
||||
this.group = g;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(SuggestedReviewerInfo o) {
|
||||
return getSortValue().compareTo(o.getSortValue());
|
||||
}
|
||||
|
||||
private String getSortValue() {
|
||||
return account != null
|
||||
? Objects.firstNonNull(account.email,
|
||||
Strings.nullToEmpty(account.name))
|
||||
: Strings.nullToEmpty(group.name);
|
||||
}
|
||||
}
|
||||
}
|
@@ -126,10 +126,8 @@ public class GroupJson {
|
||||
}
|
||||
}
|
||||
|
||||
public static class GroupInfo {
|
||||
public static class GroupInfo extends GroupBaseInfo {
|
||||
final String kind = "gerritcodereview#group";
|
||||
public String id;
|
||||
public String name;
|
||||
public String url;
|
||||
public GroupOptionsInfo options;
|
||||
|
||||
@@ -143,4 +141,9 @@ public class GroupJson {
|
||||
public List<AccountInfo> members;
|
||||
public List<GroupInfo> includes;
|
||||
}
|
||||
|
||||
public static class GroupBaseInfo {
|
||||
public String id;
|
||||
public String name;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user