Merge branch 'stable-2.12'
* stable-2.12: Add tests for reviewing and submitting refs/meta/config changes Update 2.11.8 release notes Prevent NPE in the SshLog Clear the input box after cancelling add reviewer action ReviewCommand: Don't add message twice on abandon or restore Correct schema migration instructions for MySQL in 2.12.1 release notes Documentation: remove submitted status from user search and review RebaseChangeOp: adding not null check for PatchSet groups Set version to 2.11.8 Release notes for Gerrit 2.11.8 Fix keyboard shortcuts for non-US keyboards Update commons-collections to 3.2.2 Update commons-collections to 3.2.2 Update 2.12.1 release notes Fix various formatting glitches in the 2.12.1 release notes Set version to 2.12.1 Update 2.12.1 release notes Move the logic out of SuggestReviewers and make super class Submit: Point at problematic other commits in tooltip Change-Id: Icf745ad1c95e7410ae638cfa9e5f0094541de094
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
// Copyright (C) 2016 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;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.common.data.GroupReference;
|
||||
import com.google.gerrit.common.errors.NoSuchGroupException;
|
||||
import com.google.gerrit.extensions.common.AccountInfo;
|
||||
import com.google.gerrit.extensions.common.GroupBaseInfo;
|
||||
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
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.account.AccountCache;
|
||||
import com.google.gerrit.server.account.AccountControl;
|
||||
import com.google.gerrit.server.account.AccountLoader;
|
||||
import com.google.gerrit.server.account.GroupBackend;
|
||||
import com.google.gerrit.server.account.GroupMembers;
|
||||
import com.google.gerrit.server.change.PostReviewers;
|
||||
import com.google.gerrit.server.change.ReviewerSuggestionCache;
|
||||
import com.google.gerrit.server.change.SuggestReviewers;
|
||||
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 java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ReviewersUtil {
|
||||
private static final String MAX_SUFFIX = "\u9fa5";
|
||||
private static final Ordering<SuggestedReviewerInfo> ORDERING =
|
||||
Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
|
||||
if (suggestedReviewerInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return suggestedReviewerInfo.account != null
|
||||
? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
|
||||
Strings.nullToEmpty(suggestedReviewerInfo.account.name))
|
||||
: Strings.nullToEmpty(suggestedReviewerInfo.group.name);
|
||||
}
|
||||
});
|
||||
private final AccountLoader accountLoader;
|
||||
private final AccountCache accountCache;
|
||||
private final ReviewerSuggestionCache reviewerSuggestionCache;
|
||||
private final AccountControl accountControl;
|
||||
private final Provider<ReviewDb> dbProvider;
|
||||
private final GroupBackend groupBackend;
|
||||
private final GroupMembers.Factory groupMembersFactory;
|
||||
private final Provider<CurrentUser> currentUser;
|
||||
|
||||
@Inject
|
||||
ReviewersUtil(AccountLoader.Factory accountLoaderFactory,
|
||||
AccountCache accountCache,
|
||||
ReviewerSuggestionCache reviewerSuggestionCache,
|
||||
AccountControl.Factory accountControlFactory,
|
||||
Provider<ReviewDb> dbProvider,
|
||||
GroupBackend groupBackend,
|
||||
GroupMembers.Factory groupMembersFactory,
|
||||
Provider<CurrentUser> currentUser) {
|
||||
this.accountLoader = accountLoaderFactory.create(true);
|
||||
this.accountCache = accountCache;
|
||||
this.reviewerSuggestionCache = reviewerSuggestionCache;
|
||||
this.accountControl = accountControlFactory.get();
|
||||
this.dbProvider = dbProvider;
|
||||
this.groupBackend = groupBackend;
|
||||
this.groupMembersFactory = groupMembersFactory;
|
||||
this.currentUser = currentUser;
|
||||
}
|
||||
|
||||
public interface VisibilityControl {
|
||||
boolean isVisibleTo(Account.Id account) throws OrmException;
|
||||
}
|
||||
|
||||
public List<SuggestedReviewerInfo> suggestReviewers(
|
||||
SuggestReviewers suggestReviewers, ProjectControl projectControl,
|
||||
VisibilityControl visibilityControl)
|
||||
throws IOException, OrmException, BadRequestException {
|
||||
String query = suggestReviewers.getQuery();
|
||||
boolean suggestAccounts = suggestReviewers.getSuggestAccounts();
|
||||
int suggestFrom = suggestReviewers.getSuggestFrom();
|
||||
boolean useFullTextSearch = suggestReviewers.getUseFullTextSearch();
|
||||
int limit = suggestReviewers.getLimit();
|
||||
|
||||
if (Strings.isNullOrEmpty(query)) {
|
||||
throw new BadRequestException("missing query field");
|
||||
}
|
||||
|
||||
if (!suggestAccounts || query.length() < suggestFrom) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<AccountInfo> suggestedAccounts;
|
||||
if (useFullTextSearch) {
|
||||
suggestedAccounts = suggestAccountFullTextSearch(suggestReviewers, visibilityControl);
|
||||
} else {
|
||||
suggestedAccounts = suggestAccount(suggestReviewers, visibilityControl);
|
||||
}
|
||||
|
||||
List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
|
||||
for (AccountInfo a : suggestedAccounts) {
|
||||
SuggestedReviewerInfo info = new SuggestedReviewerInfo();
|
||||
info.account = a;
|
||||
reviewer.add(info);
|
||||
}
|
||||
|
||||
for (GroupReference g : suggestAccountGroup(suggestReviewers, projectControl)) {
|
||||
if (suggestGroupAsReviewer(suggestReviewers, projectControl.getProject(),
|
||||
g, visibilityControl)) {
|
||||
GroupBaseInfo info = new GroupBaseInfo();
|
||||
info.id = Url.encode(g.getUUID().get());
|
||||
info.name = g.getName();
|
||||
SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
|
||||
suggestedReviewerInfo.group = info;
|
||||
reviewer.add(suggestedReviewerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
reviewer = ORDERING.immutableSortedCopy(reviewer);
|
||||
if (reviewer.size() <= limit) {
|
||||
return reviewer;
|
||||
} else {
|
||||
return reviewer.subList(0, limit);
|
||||
}
|
||||
}
|
||||
|
||||
private List<AccountInfo> suggestAccountFullTextSearch(
|
||||
SuggestReviewers suggestReviewers, VisibilityControl visibilityControl)
|
||||
throws IOException, OrmException {
|
||||
List<AccountInfo> results = reviewerSuggestionCache.search(
|
||||
suggestReviewers.getQuery(), suggestReviewers.getFullTextMaxMatches());
|
||||
|
||||
Iterator<AccountInfo> it = results.iterator();
|
||||
while (it.hasNext()) {
|
||||
Account.Id accountId = new Account.Id(it.next()._accountId);
|
||||
if (!(visibilityControl.isVisibleTo(accountId)
|
||||
&& accountControl.canSee(accountId))) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<AccountInfo> suggestAccount(SuggestReviewers suggestReviewers,
|
||||
VisibilityControl visibilityControl)
|
||||
throws OrmException {
|
||||
String query = suggestReviewers.getQuery();
|
||||
int limit = suggestReviewers.getLimit();
|
||||
|
||||
String a = query;
|
||||
String b = a + MAX_SUFFIX;
|
||||
|
||||
Map<Account.Id, AccountInfo> r = new LinkedHashMap<>();
|
||||
Map<Account.Id, String> queryEmail = new HashMap<>();
|
||||
|
||||
for (Account p : dbProvider.get().accounts()
|
||||
.suggestByFullName(a, b, limit)) {
|
||||
if (p.isActive()) {
|
||||
addSuggestion(r, p.getId(), visibilityControl);
|
||||
}
|
||||
}
|
||||
|
||||
if (r.size() < limit) {
|
||||
for (Account p : dbProvider.get().accounts()
|
||||
.suggestByPreferredEmail(a, b, limit - r.size())) {
|
||||
if (p.isActive()) {
|
||||
addSuggestion(r, 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();
|
||||
if (p.isActive()) {
|
||||
if (addSuggestion(r, p.getId(), visibilityControl)) {
|
||||
queryEmail.put(e.getAccountId(), e.getEmailAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accountLoader.fill();
|
||||
for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
|
||||
AccountInfo info = r.get(p.getKey());
|
||||
if (info != null) {
|
||||
info.email = p.getValue();
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(r.values());
|
||||
}
|
||||
|
||||
private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
|
||||
Account.Id account, VisibilityControl visibilityControl)
|
||||
throws OrmException {
|
||||
if (!map.containsKey(account)
|
||||
// Can the suggestion see the change?
|
||||
&& visibilityControl.isVisibleTo(account)
|
||||
// Can the account see the current user?
|
||||
&& accountControl.canSee(account)) {
|
||||
map.put(account, accountLoader.get(account));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<GroupReference> suggestAccountGroup(
|
||||
SuggestReviewers suggestReviewers, ProjectControl ctl) {
|
||||
return Lists.newArrayList(
|
||||
Iterables.limit(groupBackend.suggest(suggestReviewers.getQuery(), ctl),
|
||||
suggestReviewers.getLimit()));
|
||||
}
|
||||
|
||||
private boolean suggestGroupAsReviewer(SuggestReviewers suggestReviewers,
|
||||
Project project, GroupReference group,
|
||||
VisibilityControl visibilityControl) throws OrmException, IOException {
|
||||
int maxAllowed = suggestReviewers.getMaxAllowed();
|
||||
|
||||
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.getId())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (NoSuchGroupException e) {
|
||||
return false;
|
||||
} catch (NoSuchProjectException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ import com.google.gerrit.server.change.Revert;
|
||||
import com.google.gerrit.server.change.Reviewers;
|
||||
import com.google.gerrit.server.change.Revisions;
|
||||
import com.google.gerrit.server.change.SubmittedTogether;
|
||||
import com.google.gerrit.server.change.SuggestReviewers;
|
||||
import com.google.gerrit.server.change.SuggestChangeReviewers;
|
||||
import com.google.gerrit.server.git.UpdateException;
|
||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
@@ -77,7 +77,7 @@ class ChangeApiImpl implements ChangeApi {
|
||||
private final Revisions revisions;
|
||||
private final ReviewerApiImpl.Factory reviewerApi;
|
||||
private final RevisionApiImpl.Factory revisionApi;
|
||||
private final Provider<SuggestReviewers> suggestReviewers;
|
||||
private final Provider<SuggestChangeReviewers> suggestReviewers;
|
||||
private final ChangeResource change;
|
||||
private final Abandon abandon;
|
||||
private final Revert revert;
|
||||
@@ -104,7 +104,7 @@ class ChangeApiImpl implements ChangeApi {
|
||||
Revisions revisions,
|
||||
ReviewerApiImpl.Factory reviewerApi,
|
||||
RevisionApiImpl.Factory revisionApi,
|
||||
Provider<SuggestReviewers> suggestReviewers,
|
||||
Provider<SuggestChangeReviewers> suggestReviewers,
|
||||
Abandon abandon,
|
||||
Revert revert,
|
||||
Restore restore,
|
||||
@@ -304,7 +304,7 @@ class ChangeApiImpl implements ChangeApi {
|
||||
private List<SuggestedReviewerInfo> suggestReviewers(SuggestedReviewersRequest r)
|
||||
throws RestApiException {
|
||||
try {
|
||||
SuggestReviewers mySuggestReviewers = suggestReviewers.get();
|
||||
SuggestChangeReviewers mySuggestReviewers = suggestReviewers.get();
|
||||
mySuggestReviewers.setQuery(r.getQuery());
|
||||
mySuggestReviewers.setLimit(r.getLimit());
|
||||
return mySuggestReviewers.apply(change);
|
||||
|
||||
@@ -26,6 +26,7 @@ import static com.google.gerrit.server.change.VoteResource.VOTE_KIND;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.extensions.restapi.RestApiModule;
|
||||
import com.google.gerrit.server.account.AccountLoader;
|
||||
import com.google.gerrit.server.change.SuggestChangeReviewers;
|
||||
import com.google.gerrit.server.change.Reviewed.DeleteReviewed;
|
||||
import com.google.gerrit.server.change.Reviewed.PutReviewed;
|
||||
|
||||
@@ -72,7 +73,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);
|
||||
get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
|
||||
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
|
||||
get(REVIEWER_KIND).to(GetReviewer.class);
|
||||
delete(REVIEWER_KIND).to(DeleteReviewer.class);
|
||||
|
||||
@@ -102,7 +102,7 @@ public class ReviewerSuggestionCache {
|
||||
});
|
||||
}
|
||||
|
||||
List<AccountInfo> search(String query, int n) throws IOException {
|
||||
public List<AccountInfo> search(String query, int n) throws IOException {
|
||||
IndexSearcher searcher = get();
|
||||
if (searcher == null) {
|
||||
return Collections.emptyList();
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
package com.google.gerrit.server.change;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.FluentIterable;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gerrit.common.data.ParameterizedString;
|
||||
@@ -71,6 +74,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -96,7 +100,7 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
|
||||
private static final String CLICK_FAILURE_TOOLTIP =
|
||||
"Clicking the button would fail";
|
||||
private static final String CHANGES_NOT_MERGEABLE =
|
||||
"See the \"Submitted Together\" tab for problems";
|
||||
"See the \"Submitted Together\" tab for problems, specially see: ";
|
||||
|
||||
public static class Output {
|
||||
transient Change change;
|
||||
@@ -263,11 +267,18 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
|
||||
MergeOp.checkSubmitRule(c);
|
||||
}
|
||||
|
||||
Boolean csIsMergeable = isPatchSetMergeable(cs);
|
||||
if (csIsMergeable == null) {
|
||||
Collection<ChangeData> unmergeable = unmergeableChanges(cs);
|
||||
if (unmergeable == null) {
|
||||
return CLICK_FAILURE_TOOLTIP;
|
||||
} else if (!csIsMergeable) {
|
||||
return CHANGES_NOT_MERGEABLE;
|
||||
} else if (!unmergeable.isEmpty()) {
|
||||
return CHANGES_NOT_MERGEABLE + Joiner.on(", ").join(
|
||||
Iterables.transform(unmergeable,
|
||||
new Function<ChangeData, String>() {
|
||||
@Override
|
||||
public String apply(ChangeData cd) {
|
||||
return String.valueOf(cd.getId().get());
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (ResourceConflictException e) {
|
||||
return BLOCKED_SUBMIT_TOOLTIP;
|
||||
@@ -407,11 +418,11 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
|
||||
return change != null ? change.getStatus().name().toLowerCase() : "deleted";
|
||||
}
|
||||
|
||||
public Boolean isPatchSetMergeable(ChangeSet cs)
|
||||
public Collection<ChangeData> unmergeableChanges(ChangeSet cs)
|
||||
throws OrmException, IOException {
|
||||
Map<ChangeData, Boolean> mergeabilityMap = new HashMap<>();
|
||||
Set<ChangeData> mergeabilityMap = new HashSet<>();
|
||||
for (ChangeData change : cs.changes()) {
|
||||
mergeabilityMap.put(change, false);
|
||||
mergeabilityMap.add(change);
|
||||
}
|
||||
|
||||
Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch();
|
||||
@@ -442,17 +453,19 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
|
||||
// Skip whole check, cannot determine if mergeable
|
||||
return null;
|
||||
}
|
||||
mergeabilityMap.put(change, mergeable);
|
||||
if (mergeable) {
|
||||
mergeabilityMap.remove(change);
|
||||
}
|
||||
|
||||
if (isLastInChain && isMergeCommit && mergeable) {
|
||||
for (ChangeData c : targetBranch) {
|
||||
mergeabilityMap.put(c, true);
|
||||
mergeabilityMap.remove(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return !mergeabilityMap.values().contains(Boolean.FALSE);
|
||||
return mergeabilityMap;
|
||||
}
|
||||
|
||||
private HashMap<Change.Id, RevCommit> findCommits(
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (C) 2016 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.common.SuggestedReviewerInfo;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.ReviewersUtil;
|
||||
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
|
||||
import com.google.gerrit.server.ReviewersUtil.VisibilityControl;
|
||||
import com.google.gerrit.server.account.AccountVisibility;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class SuggestChangeReviewers extends SuggestReviewers
|
||||
implements RestReadView<ChangeResource> {
|
||||
@Inject
|
||||
SuggestChangeReviewers(AccountVisibility av,
|
||||
GenericFactory identifiedUserFactory,
|
||||
Provider<ReviewDb> dbProvider,
|
||||
@GerritServerConfig Config cfg,
|
||||
ReviewersUtil reviewersUtil) {
|
||||
super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
|
||||
throws BadRequestException, OrmException, IOException {
|
||||
return reviewersUtil.suggestReviewers(this,
|
||||
rsrc.getControl().getProjectControl(), getVisibility(rsrc));
|
||||
}
|
||||
|
||||
private VisibilityControl getVisibility(final ChangeResource rsrc) {
|
||||
if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
|
||||
return new VisibilityControl() {
|
||||
@Override
|
||||
public boolean isVisibleTo(Account.Id account) throws OrmException {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return new VisibilityControl() {
|
||||
@Override
|
||||
public boolean isVisibleTo(Account.Id account) throws OrmException {
|
||||
IdentifiedUser who =
|
||||
identifiedUserFactory.create(dbProvider, account);
|
||||
// we can't use changeControl directly as it won't suggest reviewers
|
||||
// to drafts
|
||||
return rsrc.getControl().forUser(who).isRefVisible();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,89 +14,33 @@
|
||||
|
||||
package com.google.gerrit.server.change;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.common.data.GroupReference;
|
||||
import com.google.gerrit.common.errors.NoSuchGroupException;
|
||||
import com.google.gerrit.extensions.common.AccountInfo;
|
||||
import com.google.gerrit.extensions.common.GroupBaseInfo;
|
||||
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
|
||||
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.AccountLoader;
|
||||
import com.google.gerrit.server.ReviewersUtil;
|
||||
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.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.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class SuggestReviewers implements RestReadView<ChangeResource> {
|
||||
private static final String MAX_SUFFIX = "\u9fa5";
|
||||
public class SuggestReviewers {
|
||||
private static final int DEFAULT_MAX_SUGGESTED = 10;
|
||||
private static final int DEFAULT_MAX_MATCHES = 100;
|
||||
private static final Ordering<SuggestedReviewerInfo> ORDERING =
|
||||
Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
|
||||
if (suggestedReviewerInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return suggestedReviewerInfo.account != null
|
||||
? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
|
||||
Strings.nullToEmpty(suggestedReviewerInfo.account.name))
|
||||
: Strings.nullToEmpty(suggestedReviewerInfo.group.name);
|
||||
}
|
||||
});
|
||||
|
||||
private final AccountLoader accountLoader;
|
||||
private final AccountControl accountControl;
|
||||
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;
|
||||
protected final Provider<ReviewDb> dbProvider;
|
||||
protected final IdentifiedUser.GenericFactory identifiedUserFactory;
|
||||
protected final ReviewersUtil reviewersUtil;
|
||||
|
||||
private final boolean suggestAccounts;
|
||||
private final int suggestFrom;
|
||||
private final int maxAllowed;
|
||||
private int limit;
|
||||
private String query;
|
||||
protected int limit;
|
||||
protected String query;
|
||||
private boolean useFullTextSearch;
|
||||
private final int fullTextMaxMatches;
|
||||
private final int maxSuggestedReviewers;
|
||||
private final ReviewerSuggestionCache reviewerSuggestionCache;
|
||||
protected final int maxSuggestedReviewers;
|
||||
|
||||
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
|
||||
usage = "maximum number of reviewers to list")
|
||||
@@ -112,27 +56,43 @@ public class SuggestReviewers implements RestReadView<ChangeResource> {
|
||||
this.query = q;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public boolean getSuggestAccounts() {
|
||||
return suggestAccounts;
|
||||
}
|
||||
|
||||
public int getSuggestFrom() {
|
||||
return suggestFrom;
|
||||
}
|
||||
|
||||
public boolean getUseFullTextSearch() {
|
||||
return useFullTextSearch;
|
||||
}
|
||||
|
||||
public int getFullTextMaxMatches() {
|
||||
return fullTextMaxMatches;
|
||||
}
|
||||
|
||||
public int getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public int getMaxAllowed() {
|
||||
return maxAllowed;
|
||||
}
|
||||
|
||||
@Inject
|
||||
SuggestReviewers(AccountVisibility av,
|
||||
AccountLoader.Factory accountLoaderFactory,
|
||||
AccountControl.Factory accountControlFactory,
|
||||
AccountCache accountCache,
|
||||
GroupMembers.Factory groupMembersFactory,
|
||||
public SuggestReviewers(AccountVisibility av,
|
||||
IdentifiedUser.GenericFactory identifiedUserFactory,
|
||||
Provider<CurrentUser> currentUser,
|
||||
Provider<ReviewDb> dbProvider,
|
||||
@GerritServerConfig Config cfg,
|
||||
GroupBackend groupBackend,
|
||||
ReviewerSuggestionCache reviewerSuggestionCache) {
|
||||
this.accountLoader = accountLoaderFactory.create(true);
|
||||
this.accountControl = accountControlFactory.get();
|
||||
this.accountCache = accountCache;
|
||||
this.groupMembersFactory = groupMembersFactory;
|
||||
ReviewersUtil reviewersUtil) {
|
||||
this.dbProvider = dbProvider;
|
||||
this.identifiedUserFactory = identifiedUserFactory;
|
||||
this.currentUser = currentUser;
|
||||
this.groupBackend = groupBackend;
|
||||
this.reviewerSuggestionCache = reviewerSuggestionCache;
|
||||
this.reviewersUtil = reviewersUtil;
|
||||
this.maxSuggestedReviewers =
|
||||
cfg.getInt("suggest", "maxSuggestedReviewers", DEFAULT_MAX_SUGGESTED);
|
||||
this.limit = this.maxSuggestedReviewers;
|
||||
@@ -152,196 +112,4 @@ public class SuggestReviewers implements RestReadView<ChangeResource> {
|
||||
this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
|
||||
PostReviewers.DEFAULT_MAX_REVIEWERS);
|
||||
}
|
||||
|
||||
private interface VisibilityControl {
|
||||
boolean isVisibleTo(Account.Id 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;
|
||||
if (useFullTextSearch) {
|
||||
suggestedAccounts = suggestAccountFullTextSearch(visibilityControl);
|
||||
} else {
|
||||
suggestedAccounts = suggestAccount(visibilityControl);
|
||||
}
|
||||
|
||||
List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
|
||||
for (AccountInfo a : suggestedAccounts) {
|
||||
SuggestedReviewerInfo info = new SuggestedReviewerInfo();
|
||||
info.account = a;
|
||||
reviewer.add(info);
|
||||
}
|
||||
|
||||
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();
|
||||
SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
|
||||
suggestedReviewerInfo.group = info;
|
||||
reviewer.add(suggestedReviewerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
reviewer = ORDERING.immutableSortedCopy(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.Id account) throws OrmException {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return new VisibilityControl() {
|
||||
@Override
|
||||
public boolean isVisibleTo(Account.Id account) throws OrmException {
|
||||
IdentifiedUser who =
|
||||
identifiedUserFactory.create(dbProvider, account);
|
||||
// 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;
|
||||
|
||||
Map<Account.Id, AccountInfo> r = new LinkedHashMap<>();
|
||||
Map<Account.Id, String> queryEmail = new HashMap<>();
|
||||
|
||||
for (Account p : dbProvider.get().accounts()
|
||||
.suggestByFullName(a, b, limit)) {
|
||||
if (p.isActive()) {
|
||||
addSuggestion(r, p.getId(), visibilityControl);
|
||||
}
|
||||
}
|
||||
|
||||
if (r.size() < limit) {
|
||||
for (Account p : dbProvider.get().accounts()
|
||||
.suggestByPreferredEmail(a, b, limit - r.size())) {
|
||||
if (p.isActive()) {
|
||||
addSuggestion(r, 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();
|
||||
if (p.isActive()) {
|
||||
if (addSuggestion(r, p.getId(), visibilityControl)) {
|
||||
queryEmail.put(e.getAccountId(), e.getEmailAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accountLoader.fill();
|
||||
for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
|
||||
AccountInfo info = r.get(p.getKey());
|
||||
if (info != null) {
|
||||
info.email = p.getValue();
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(r.values());
|
||||
}
|
||||
|
||||
private List<AccountInfo> suggestAccountFullTextSearch(
|
||||
VisibilityControl visibilityControl) throws IOException, OrmException {
|
||||
List<AccountInfo> results = reviewerSuggestionCache.search(
|
||||
query, fullTextMaxMatches);
|
||||
|
||||
Iterator<AccountInfo> it = results.iterator();
|
||||
while (it.hasNext()) {
|
||||
Account.Id accountId = new Account.Id(it.next()._accountId);
|
||||
if (!(visibilityControl.isVisibleTo(accountId)
|
||||
&& accountControl.canSee(accountId))) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
|
||||
Account.Id account, VisibilityControl visibilityControl)
|
||||
throws OrmException {
|
||||
if (!map.containsKey(account)
|
||||
// Can the suggestion see the change?
|
||||
&& visibilityControl.isVisibleTo(account)
|
||||
// Can the account see the current user?
|
||||
&& accountControl.canSee(account)) {
|
||||
map.put(account, accountLoader.get(account));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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.getId())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (NoSuchGroupException e) {
|
||||
return false;
|
||||
} catch (NoSuchProjectException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user