| // Copyright (C) 2009 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.query.change; | |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; | |
| import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN; | |
| import static com.google.gerrit.server.account.AccountResolver.isSelf; | |
| import static com.google.gerrit.server.query.change.ChangeData.asChanges; | |
| import static java.util.stream.Collectors.toList; | |
| import static java.util.stream.Collectors.toSet; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.base.Enums; | |
| import com.google.common.base.Splitter; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.collect.Sets; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.common.primitives.Ints; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.entities.AccountGroup; | |
| import com.google.gerrit.entities.Address; | |
| import com.google.gerrit.entities.BranchNameKey; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.GroupDescription; | |
| import com.google.gerrit.entities.GroupReference; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.entities.RefNames; | |
| import com.google.gerrit.entities.SubmitRecord; | |
| import com.google.gerrit.exceptions.NotSignedInException; | |
| import com.google.gerrit.exceptions.StorageException; | |
| import com.google.gerrit.extensions.registration.DynamicMap; | |
| import com.google.gerrit.index.IndexConfig; | |
| import com.google.gerrit.index.Schema; | |
| import com.google.gerrit.index.SchemaFieldDefs.SchemaField; | |
| import com.google.gerrit.index.SchemaUtil; | |
| import com.google.gerrit.index.query.LimitPredicate; | |
| import com.google.gerrit.index.query.Predicate; | |
| import com.google.gerrit.index.query.QueryBuilder; | |
| import com.google.gerrit.index.query.QueryParseException; | |
| import com.google.gerrit.index.query.QueryRequiresAuthException; | |
| import com.google.gerrit.server.CommentsUtil; | |
| import com.google.gerrit.server.CurrentUser; | |
| import com.google.gerrit.server.DraftCommentsReader; | |
| import com.google.gerrit.server.IdentifiedUser; | |
| import com.google.gerrit.server.StarredChangesUtil; | |
| import com.google.gerrit.server.account.AccountCache; | |
| import com.google.gerrit.server.account.AccountResolver; | |
| import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException; | |
| import com.google.gerrit.server.account.AccountState; | |
| import com.google.gerrit.server.account.DestinationList; | |
| import com.google.gerrit.server.account.GroupBackend; | |
| import com.google.gerrit.server.account.GroupBackends; | |
| import com.google.gerrit.server.account.GroupMembers; | |
| import com.google.gerrit.server.account.QueryList; | |
| import com.google.gerrit.server.account.VersionedAccountDestinations; | |
| import com.google.gerrit.server.account.VersionedAccountQueries; | |
| import com.google.gerrit.server.change.ChangeTriplet; | |
| import com.google.gerrit.server.change.MergeabilityComputationBehavior; | |
| import com.google.gerrit.server.config.AllProjectsName; | |
| import com.google.gerrit.server.config.AllUsersName; | |
| import com.google.gerrit.server.config.GerritServerConfig; | |
| import com.google.gerrit.server.config.HasOperandAliasConfig; | |
| import com.google.gerrit.server.config.OperatorAliasConfig; | |
| import com.google.gerrit.server.experiments.ExperimentFeatures; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.index.change.ChangeField; | |
| import com.google.gerrit.server.index.change.ChangeIndex; | |
| import com.google.gerrit.server.index.change.ChangeIndexCollection; | |
| import com.google.gerrit.server.index.change.ChangeIndexRewriter; | |
| import com.google.gerrit.server.notedb.ReviewerStateInternal; | |
| import com.google.gerrit.server.patch.PatchListCache; | |
| import com.google.gerrit.server.permissions.PermissionBackend; | |
| import com.google.gerrit.server.plugincontext.PluginSetContext; | |
| import com.google.gerrit.server.project.ChildProjects; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.gerrit.server.query.change.ChangePredicates.EditByPredicateProvider; | |
| import com.google.gerrit.server.query.change.PredicateArgs.ValOp; | |
| import com.google.gerrit.server.rules.SubmitRule; | |
| import com.google.gerrit.server.submit.SubmitDryRun; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| import com.google.inject.ProvisionException; | |
| import com.google.inject.util.Providers; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.util.Set; | |
| import java.util.function.Function; | |
| import java.util.regex.Pattern; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; | |
| import org.eclipse.jgit.lib.Config; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| /** Parses a query string meant to be applied to change objects. */ | |
| public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuilder> { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| public interface ChangeOperatorFactory extends OperatorFactory<ChangeData, ChangeQueryBuilder> {} | |
| /** | |
| * Converts a operand (operator value) passed to an operator into a {@link Predicate}. | |
| * | |
| * <p>Register a ChangeOperandFactory in a config Module like this (note, for an example we are | |
| * using the has predicate, when other predicate plugin operands are created they can be | |
| * registered in a similar manner): | |
| * | |
| * <p>bind(ChangeHasOperandFactory.class) .annotatedWith(Exports.named("your has operand")) | |
| * .to(YourClass.class); | |
| */ | |
| public interface ChangeOperandFactory { | |
| Predicate<ChangeData> create(ChangeQueryBuilder builder) throws QueryParseException; | |
| } | |
| public interface ChangeHasOperandFactory extends ChangeOperandFactory {} | |
| public interface ChangeIsOperandFactory extends ChangeOperandFactory {} | |
| private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$"); | |
| private static final Pattern PAT_CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN); | |
| private static final Pattern DEF_CHANGE = | |
| Pattern.compile("^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$"); | |
| static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10; | |
| // NOTE: As new search operations are added, please keep the suggestions in | |
| // gr-search-bar.ts up to date. | |
| public static final String FIELD_ADDED = "added"; | |
| public static final String FIELD_AGE = "age"; | |
| public static final String FIELD_ATTENTION_SET_USERS = "attentionusers"; | |
| public static final String FIELD_ATTENTION_SET_USERS_COUNT = "attentionuserscount"; | |
| public static final String FIELD_ATTENTION_SET_FULL = "attentionfull"; | |
| @Deprecated public static final String FIELD_ASSIGNEE = "assignee"; | |
| public static final String FIELD_AUTHOR = "author"; | |
| public static final String FIELD_EXACTAUTHOR = "exactauthor"; | |
| public static final String FIELD_CHANGE = "change"; | |
| public static final String FIELD_CHANGE_ID = "change_id"; | |
| public static final String FIELD_COMMENT = "comment"; | |
| public static final String FIELD_COMMENTBY = "commentby"; | |
| public static final String FIELD_COMMIT = "commit"; | |
| public static final String FIELD_COMMITTER = "committer"; | |
| public static final String FIELD_CUSTOM_KEYED_VALUES = "custom_keyed_values"; | |
| public static final String FIELD_DIRECTORY = "directory"; | |
| public static final String FIELD_EXACTCOMMITTER = "exactcommitter"; | |
| public static final String FIELD_EXTENSION = "extension"; | |
| public static final String FIELD_ONLY_EXTENSIONS = "onlyextensions"; | |
| public static final String FIELD_FOOTER = "footer"; | |
| public static final String FIELD_FOOTER_NAME = "footernames"; | |
| public static final String FIELD_CONFLICTS = "conflicts"; | |
| public static final String FIELD_DELETED = "deleted"; | |
| public static final String FIELD_DELTA = "delta"; | |
| public static final String FIELD_DESTINATION = "destination"; | |
| public static final String FIELD_DRAFTBY = "draftby"; | |
| public static final String FIELD_EDITBY = "editby"; | |
| public static final String FIELD_EXACTCOMMIT = "exactcommit"; | |
| public static final String FIELD_FILE = "file"; | |
| public static final String FIELD_FILEPART = "filepart"; | |
| public static final String FIELD_GROUP = "group"; | |
| public static final String FIELD_HASHTAG = "hashtag"; | |
| public static final String FIELD_LABEL = "label"; | |
| public static final String FIELD_LIMIT = "limit"; | |
| public static final String FIELD_MERGE = "merge"; | |
| public static final String FIELD_MERGEABLE = "mergeable2"; | |
| public static final String FIELD_MERGED_ON = "mergedon"; | |
| public static final String FIELD_MESSAGE = "message"; | |
| public static final String FIELD_SUBJECT = "subject"; | |
| public static final String FIELD_PREFIX_SUBJECT = "prefixsubject"; | |
| public static final String FIELD_MESSAGE_EXACT = "messageexact"; | |
| public static final String FIELD_OWNER = "owner"; | |
| public static final String FIELD_OWNERIN = "ownerin"; | |
| public static final String FIELD_PARENTOF = "parentof"; | |
| public static final String FIELD_PARENTPROJECT = "parentproject"; | |
| public static final String FIELD_PENDING_REVIEWER = "pendingreviewer"; | |
| public static final String FIELD_PENDING_REVIEWER_BY_EMAIL = "pendingreviewerbyemail"; | |
| public static final String FIELD_PRIVATE = "private"; | |
| public static final String FIELD_PROJECT = "project"; | |
| public static final String FIELD_PROJECTS = "projects"; | |
| public static final String FIELD_REF = "ref"; | |
| public static final String FIELD_REVIEWEDBY = "reviewedby"; | |
| public static final String FIELD_REVIEWERIN = "reviewerin"; | |
| public static final String FIELD_STAR = "star"; | |
| public static final String FIELD_STARBY = "starby"; | |
| public static final String FIELD_STARTED = "started"; | |
| public static final String FIELD_STATUS = "status"; | |
| public static final String FIELD_SUBMISSIONID = "submissionid"; | |
| public static final String FIELD_TR = "tr"; | |
| public static final String FIELD_UNRESOLVED_COMMENT_COUNT = "unresolved"; | |
| public static final String FIELD_UPLOADER = "uploader"; | |
| public static final String FIELD_UPLOADERIN = "uploaderin"; | |
| public static final String FIELD_VISIBLETO = "visibleto"; | |
| public static final String FIELD_WATCHEDBY = "watchedby"; | |
| public static final String FIELD_WIP = "wip"; | |
| public static final String FIELD_REVERTOF = "revertof"; | |
| public static final String FIELD_PURE_REVERT = "ispurerevert"; | |
| public static final String FIELD_CHERRYPICK = "cherrypick"; | |
| public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange"; | |
| public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset"; | |
| public static final String FIELD_IS_SUBMITTABLE = "issubmittable"; | |
| public static final String ARG_ID_NAME = "name"; | |
| public static final String ARG_ID_USER = "user"; | |
| public static final String ARG_ID_GROUP = "group"; | |
| public static final String ARG_ID_OWNER = "owner"; | |
| public static final String ARG_ID_NON_UPLOADER = "non_uploader"; | |
| public static final String ARG_ID_NON_CONTRIBUTOR = "non_contributor"; | |
| public static final String ARG_COUNT = "count"; | |
| public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0); | |
| public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1); | |
| public static final Account.Id NON_CONTRIBUTOR_ACCOUNT_ID = Account.id(-2); | |
| public static final Account.Id NON_EXISTING_ACCOUNT_ID = Account.id(-1000); | |
| public static final String OPERATOR_MERGED_BEFORE = "mergedbefore"; | |
| public static final String OPERATOR_MERGED_AFTER = "mergedafter"; | |
| // Operators to match on the last time the change was updated. Naming for legacy reasons. | |
| public static final String OPERATOR_BEFORE = "before"; | |
| public static final String OPERATOR_AFTER = "after"; | |
| private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef = | |
| new QueryBuilder.Definition<>(ChangeQueryBuilder.class); | |
| @VisibleForTesting | |
| public static class Arguments { | |
| final AccountCache accountCache; | |
| final AccountResolver accountResolver; | |
| final AllProjectsName allProjectsName; | |
| final AllUsersName allUsersName; | |
| final PermissionBackend permissionBackend; | |
| final ChangeData.Factory changeDataFactory; | |
| final ChangeIndex index; | |
| final ChangeIndexRewriter rewriter; | |
| final CommentsUtil commentsUtil; | |
| final DraftCommentsReader draftCommentsReader; | |
| final ConflictsCache conflictsCache; | |
| final DynamicMap<ChangeHasOperandFactory> hasOperands; | |
| final DynamicMap<ChangeIsOperandFactory> isOperands; | |
| final DynamicMap<ChangeOperatorFactory> opFactories; | |
| final GitRepositoryManager repoManager; | |
| final GroupBackend groupBackend; | |
| final IdentifiedUser.GenericFactory userFactory; | |
| final IndexConfig indexConfig; | |
| final PatchListCache patchListCache; | |
| final ProjectCache projectCache; | |
| final Provider<InternalChangeQuery> queryProvider; | |
| final ChildProjects childProjects; | |
| final StarredChangesUtil starredChangesUtil; | |
| final SubmitDryRun submitDryRun; | |
| final GroupMembers groupMembers; | |
| final ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory; | |
| final OperatorAliasConfig operatorAliasConfig; | |
| final boolean indexMergeable; | |
| final boolean conflictsPredicateEnabled; | |
| final ExperimentFeatures experimentFeatures; | |
| final HasOperandAliasConfig hasOperandAliasConfig; | |
| final PluginSetContext<SubmitRule> submitRules; | |
| private final Provider<CurrentUser> self; | |
| private final EditByPredicateProvider editByPredicateProvider; | |
| @Inject | |
| @VisibleForTesting | |
| public Arguments( | |
| Provider<InternalChangeQuery> queryProvider, | |
| ChangeIndexRewriter rewriter, | |
| DynamicMap<ChangeOperatorFactory> opFactories, | |
| DynamicMap<ChangeHasOperandFactory> hasOperands, | |
| DynamicMap<ChangeIsOperandFactory> isOperands, | |
| IdentifiedUser.GenericFactory userFactory, | |
| Provider<CurrentUser> self, | |
| PermissionBackend permissionBackend, | |
| ChangeData.Factory changeDataFactory, | |
| CommentsUtil commentsUtil, | |
| DraftCommentsReader draftCommentsReader, | |
| AccountResolver accountResolver, | |
| GroupBackend groupBackend, | |
| AllProjectsName allProjectsName, | |
| AllUsersName allUsersName, | |
| PatchListCache patchListCache, | |
| GitRepositoryManager repoManager, | |
| ProjectCache projectCache, | |
| ChildProjects childProjects, | |
| ChangeIndexCollection indexes, | |
| SubmitDryRun submitDryRun, | |
| ConflictsCache conflictsCache, | |
| IndexConfig indexConfig, | |
| StarredChangesUtil starredChangesUtil, | |
| AccountCache accountCache, | |
| GroupMembers groupMembers, | |
| OperatorAliasConfig operatorAliasConfig, | |
| @GerritServerConfig Config gerritConfig, | |
| ExperimentFeatures experimentFeatures, | |
| HasOperandAliasConfig hasOperandAliasConfig, | |
| ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory, | |
| PluginSetContext<SubmitRule> submitRules, | |
| EditByPredicateProvider editByPredicateProvider) { | |
| this( | |
| queryProvider, | |
| rewriter, | |
| opFactories, | |
| hasOperands, | |
| isOperands, | |
| userFactory, | |
| self, | |
| permissionBackend, | |
| changeDataFactory, | |
| commentsUtil, | |
| draftCommentsReader, | |
| accountResolver, | |
| groupBackend, | |
| allProjectsName, | |
| allUsersName, | |
| patchListCache, | |
| repoManager, | |
| projectCache, | |
| childProjects, | |
| submitDryRun, | |
| conflictsCache, | |
| indexes != null ? indexes.getSearchIndex() : null, | |
| indexConfig, | |
| starredChangesUtil, | |
| accountCache, | |
| groupMembers, | |
| operatorAliasConfig, | |
| MergeabilityComputationBehavior.fromConfig(gerritConfig).includeInIndex(), | |
| gerritConfig.getBoolean("change", null, "conflictsPredicateEnabled", true), | |
| experimentFeatures, | |
| hasOperandAliasConfig, | |
| changeIsVisbleToPredicateFactory, | |
| submitRules, | |
| editByPredicateProvider); | |
| } | |
| private Arguments( | |
| Provider<InternalChangeQuery> queryProvider, | |
| ChangeIndexRewriter rewriter, | |
| DynamicMap<ChangeOperatorFactory> opFactories, | |
| DynamicMap<ChangeHasOperandFactory> hasOperands, | |
| DynamicMap<ChangeIsOperandFactory> isOperands, | |
| IdentifiedUser.GenericFactory userFactory, | |
| Provider<CurrentUser> self, | |
| PermissionBackend permissionBackend, | |
| ChangeData.Factory changeDataFactory, | |
| CommentsUtil commentsUtil, | |
| DraftCommentsReader draftCommentsReader, | |
| AccountResolver accountResolver, | |
| GroupBackend groupBackend, | |
| AllProjectsName allProjectsName, | |
| AllUsersName allUsersName, | |
| PatchListCache patchListCache, | |
| GitRepositoryManager repoManager, | |
| ProjectCache projectCache, | |
| ChildProjects childProjects, | |
| SubmitDryRun submitDryRun, | |
| ConflictsCache conflictsCache, | |
| ChangeIndex index, | |
| IndexConfig indexConfig, | |
| StarredChangesUtil starredChangesUtil, | |
| AccountCache accountCache, | |
| GroupMembers groupMembers, | |
| OperatorAliasConfig operatorAliasConfig, | |
| boolean indexMergeable, | |
| boolean conflictsPredicateEnabled, | |
| ExperimentFeatures experimentFeatures, | |
| HasOperandAliasConfig hasOperandAliasConfig, | |
| ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory, | |
| PluginSetContext<SubmitRule> submitRules, | |
| EditByPredicateProvider editByPredicateProvider) { | |
| this.queryProvider = queryProvider; | |
| this.rewriter = rewriter; | |
| this.opFactories = opFactories; | |
| this.userFactory = userFactory; | |
| this.self = self; | |
| this.permissionBackend = permissionBackend; | |
| this.changeDataFactory = changeDataFactory; | |
| this.commentsUtil = commentsUtil; | |
| this.draftCommentsReader = draftCommentsReader; | |
| this.accountResolver = accountResolver; | |
| this.groupBackend = groupBackend; | |
| this.allProjectsName = allProjectsName; | |
| this.allUsersName = allUsersName; | |
| this.patchListCache = patchListCache; | |
| this.repoManager = repoManager; | |
| this.projectCache = projectCache; | |
| this.childProjects = childProjects; | |
| this.submitDryRun = submitDryRun; | |
| this.conflictsCache = conflictsCache; | |
| this.index = index; | |
| this.indexConfig = indexConfig; | |
| this.starredChangesUtil = starredChangesUtil; | |
| this.accountCache = accountCache; | |
| this.hasOperands = hasOperands; | |
| this.isOperands = isOperands; | |
| this.groupMembers = groupMembers; | |
| this.changeIsVisbleToPredicateFactory = changeIsVisbleToPredicateFactory; | |
| this.operatorAliasConfig = operatorAliasConfig; | |
| this.indexMergeable = indexMergeable; | |
| this.conflictsPredicateEnabled = conflictsPredicateEnabled; | |
| this.experimentFeatures = experimentFeatures; | |
| this.hasOperandAliasConfig = hasOperandAliasConfig; | |
| this.submitRules = submitRules; | |
| this.editByPredicateProvider = editByPredicateProvider; | |
| } | |
| public Arguments asUser(CurrentUser otherUser) { | |
| return new Arguments( | |
| queryProvider, | |
| rewriter, | |
| opFactories, | |
| hasOperands, | |
| isOperands, | |
| userFactory, | |
| Providers.of(otherUser), | |
| permissionBackend, | |
| changeDataFactory, | |
| commentsUtil, | |
| draftCommentsReader, | |
| accountResolver, | |
| groupBackend, | |
| allProjectsName, | |
| allUsersName, | |
| patchListCache, | |
| repoManager, | |
| projectCache, | |
| childProjects, | |
| submitDryRun, | |
| conflictsCache, | |
| index, | |
| indexConfig, | |
| starredChangesUtil, | |
| accountCache, | |
| groupMembers, | |
| operatorAliasConfig, | |
| indexMergeable, | |
| conflictsPredicateEnabled, | |
| experimentFeatures, | |
| hasOperandAliasConfig, | |
| changeIsVisbleToPredicateFactory, | |
| submitRules, | |
| editByPredicateProvider); | |
| } | |
| Arguments asUser(Account.Id otherId) { | |
| try { | |
| CurrentUser u = self.get(); | |
| if (u.isIdentifiedUser() && otherId.equals(u.getAccountId())) { | |
| return this; | |
| } | |
| } catch (ProvisionException e) { | |
| // Doesn't match current user, continue. | |
| } | |
| return asUser(userFactory.create(otherId)); | |
| } | |
| IdentifiedUser getIdentifiedUser() throws QueryRequiresAuthException { | |
| try { | |
| CurrentUser u = getUser(); | |
| if (u.isIdentifiedUser()) { | |
| return u.asIdentifiedUser(); | |
| } | |
| throw new QueryRequiresAuthException(NotSignedInException.MESSAGE); | |
| } catch (ProvisionException e) { | |
| throw new QueryRequiresAuthException(NotSignedInException.MESSAGE, e); | |
| } | |
| } | |
| CurrentUser getUser() throws QueryRequiresAuthException { | |
| try { | |
| return self.get(); | |
| } catch (ProvisionException e) { | |
| throw new QueryRequiresAuthException(NotSignedInException.MESSAGE, e); | |
| } | |
| } | |
| @Nullable | |
| Schema<ChangeData> getSchema() { | |
| return index != null ? index.getSchema() : null; | |
| } | |
| } | |
| protected final Arguments args; | |
| protected Map<String, String> hasOperandAliases = Collections.emptyMap(); | |
| private final Map<BranchNameKey, DestinationList> destinationListByBranch = new HashMap<>(); | |
| private final Map<BranchNameKey, QueryList> queryListByBranch = new HashMap<>(); | |
| private static final Splitter RULE_SPLITTER = Splitter.on("="); | |
| private static final Splitter PLUGIN_SPLITTER = Splitter.on("_"); | |
| protected static final Splitter LABEL_SPLITTER = Splitter.on(","); | |
| @Inject | |
| protected ChangeQueryBuilder(Arguments args) { | |
| this(mydef, args); | |
| setupAliases(); | |
| } | |
| @VisibleForTesting | |
| protected ChangeQueryBuilder(Definition<ChangeData, ChangeQueryBuilder> def, Arguments args) { | |
| super(def, args.opFactories); | |
| this.args = args; | |
| } | |
| private void setupAliases() { | |
| setOperatorAliases(args.operatorAliasConfig.getChangeQueryOperatorAliases()); | |
| hasOperandAliases = args.hasOperandAliasConfig.getChangeQueryHasOperandAliases(); | |
| } | |
| public ChangeQueryBuilder asUser(CurrentUser user) { | |
| return new ChangeQueryBuilder(builderDef, args.asUser(user)); | |
| } | |
| public Arguments getArgs() { | |
| return args; | |
| } | |
| @Operator | |
| public Predicate<ChangeData> age(String value) { | |
| return new AgePredicate(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> before(String value) throws QueryParseException { | |
| return new BeforePredicate(ChangeField.UPDATED_SPEC, ChangeQueryBuilder.OPERATOR_BEFORE, value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> until(String value) throws QueryParseException { | |
| return before(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> after(String value) throws QueryParseException { | |
| return new AfterPredicate(ChangeField.UPDATED_SPEC, ChangeQueryBuilder.OPERATOR_AFTER, value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> since(String value) throws QueryParseException { | |
| return after(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException { | |
| checkOperatorAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_BEFORE); | |
| return new BeforePredicate( | |
| ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException { | |
| checkOperatorAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_AFTER); | |
| return new AfterPredicate( | |
| ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> change(String query) throws QueryParseException { | |
| Optional<ChangeTriplet> triplet = ChangeTriplet.parse(query); | |
| if (triplet.isPresent()) { | |
| return Predicate.and( | |
| project(triplet.get().project().get()), | |
| branch(triplet.get().branch().branch()), | |
| ChangePredicates.idPrefix(parseChangeId(triplet.get().id().get()))); | |
| } | |
| if (PAT_LEGACY_ID.matcher(query).matches()) { | |
| Integer id = Ints.tryParse(query); | |
| if (id != null) { | |
| return ChangePredicates.idStr(Change.id(id)); | |
| } | |
| } else if (PAT_CHANGE_ID.matcher(query).matches()) { | |
| return ChangePredicates.idPrefix(parseChangeId(query)); | |
| } | |
| throw new QueryParseException("Invalid change format"); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> comment(String value) { | |
| return ChangePredicates.comment(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> status(String statusName) throws QueryParseException { | |
| if ("reviewed".equalsIgnoreCase(statusName)) { | |
| return ChangePredicates.unreviewed(); | |
| } | |
| return ChangeStatusPredicate.parse(statusName); | |
| } | |
| public Predicate<ChangeData> statusOpen() { | |
| return ChangeStatusPredicate.open(); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> rule(String value) throws QueryParseException { | |
| String ruleNameArg = value; | |
| String statusArg = null; | |
| List<String> queryArgs = RULE_SPLITTER.splitToList(value); | |
| if (queryArgs.size() > 2) { | |
| throw new QueryParseException( | |
| "Invalid query arguments. Correct format is 'rule:<rule_name>=<status>' " | |
| + "with <rule_name> in the form of <plugin>~<rule>. For Gerrit core rules, " | |
| + "rule name should be specified as gerrit~<rule>."); | |
| } | |
| if (queryArgs.size() == 2) { | |
| ruleNameArg = queryArgs.get(0); | |
| statusArg = queryArgs.get(1); | |
| } | |
| return statusArg == null | |
| ? Predicate.or( | |
| Arrays.asList( | |
| ChangePredicates.submitRuleStatus(ruleNameArg + "=" + SubmitRecord.Status.OK), | |
| ChangePredicates.submitRuleStatus(ruleNameArg + "=" + SubmitRecord.Status.FORCED))) | |
| : ChangePredicates.submitRuleStatus(ruleNameArg + "=" + statusArg); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> has(String value) throws QueryParseException { | |
| value = hasOperandAliases.getOrDefault(value, value); | |
| if ("star".equalsIgnoreCase(value)) { | |
| return starredBySelf(); | |
| } | |
| if ("draft".equalsIgnoreCase(value)) { | |
| return draftBySelf(); | |
| } | |
| if ("edit".equalsIgnoreCase(value)) { | |
| return this.args.editByPredicateProvider.editBy(self()); | |
| } | |
| if ("attention".equalsIgnoreCase(value)) { | |
| checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention"); | |
| return new IsAttentionPredicate(); | |
| } | |
| if ("unresolved".equalsIgnoreCase(value)) { | |
| return new IsUnresolvedPredicate(); | |
| } | |
| // for plugins the value will be operandName_pluginName | |
| List<String> names = PLUGIN_SPLITTER.splitToList(value); | |
| if (names.size() == 2) { | |
| ChangeHasOperandFactory op = args.hasOperands.get(names.get(1), names.get(0)); | |
| if (op != null) { | |
| return op.create(this); | |
| } | |
| } | |
| throw new IllegalArgumentException(); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> is(String value) throws QueryParseException { | |
| if ("starred".equalsIgnoreCase(value)) { | |
| return starredBySelf(); | |
| } | |
| if ("watched".equalsIgnoreCase(value)) { | |
| return new IsWatchedByPredicate(args); | |
| } | |
| if ("visible".equalsIgnoreCase(value)) { | |
| return isVisible(); | |
| } | |
| if ("reviewed".equalsIgnoreCase(value)) { | |
| return ChangePredicates.unreviewed(); | |
| } | |
| if ("owner".equalsIgnoreCase(value)) { | |
| return ChangePredicates.owner(self()); | |
| } | |
| if ("uploader".equalsIgnoreCase(value)) { | |
| checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "is:uploader"); | |
| return ChangePredicates.uploader(self()); | |
| } | |
| if ("reviewer".equalsIgnoreCase(value)) { | |
| return Predicate.and( | |
| Predicate.not(new BooleanPredicate(ChangeField.WIP_SPEC)), | |
| ReviewerPredicate.reviewer(self())); | |
| } | |
| if ("cc".equalsIgnoreCase(value)) { | |
| return ReviewerPredicate.cc(self()); | |
| } | |
| if ("mergeable".equalsIgnoreCase(value)) { | |
| if (!args.indexMergeable) { | |
| throw new QueryParseException( | |
| "'is:mergeable' operator is not supported on this gerrit host"); | |
| } | |
| return new BooleanPredicate(ChangeField.MERGEABLE_SPEC); | |
| } | |
| if ("merge".equalsIgnoreCase(value)) { | |
| checkOperatorAvailable(ChangeField.MERGE_SPEC, "is:merge"); | |
| return new BooleanPredicate(ChangeField.MERGE_SPEC); | |
| } | |
| if ("private".equalsIgnoreCase(value)) { | |
| return new BooleanPredicate(ChangeField.PRIVATE_SPEC); | |
| } | |
| if ("attention".equalsIgnoreCase(value)) { | |
| checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention"); | |
| return new IsAttentionPredicate(); | |
| } | |
| if ("pure-revert".equalsIgnoreCase(value)) { | |
| checkOperatorAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert"); | |
| return ChangePredicates.pureRevert("1"); | |
| } | |
| if ("submittable".equalsIgnoreCase(value)) { | |
| if (!args.index.getSchema().hasField(ChangeField.IS_SUBMITTABLE_SPEC)) { | |
| // SubmittablePredicate will match if *any* of the submit records are OK, | |
| // but we need to check that they're *all* OK, so check that none of the | |
| // submit records match any of the negative cases. To avoid checking yet | |
| // more negative cases for CLOSED and FORCED, instead make sure at least | |
| // one submit record is OK. | |
| return Predicate.and( | |
| new SubmittablePredicate(SubmitRecord.Status.OK), | |
| Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)), | |
| Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR))); | |
| } | |
| checkOperatorAvailable(ChangeField.IS_SUBMITTABLE_SPEC, "is:submittable"); | |
| return new IsSubmittablePredicate(); | |
| } | |
| if ("started".equalsIgnoreCase(value)) { | |
| checkOperatorAvailable(ChangeField.STARTED_SPEC, "is:started"); | |
| return new BooleanPredicate(ChangeField.STARTED_SPEC); | |
| } | |
| if ("wip".equalsIgnoreCase(value)) { | |
| return new BooleanPredicate(ChangeField.WIP_SPEC); | |
| } | |
| if ("cherrypick".equalsIgnoreCase(value)) { | |
| checkOperatorAvailable(ChangeField.CHERRY_PICK_SPEC, "is:cherrypick"); | |
| return new BooleanPredicate(ChangeField.CHERRY_PICK_SPEC); | |
| } | |
| // for plugins the value will be operandName_pluginName | |
| List<String> names = PLUGIN_SPLITTER.splitToList(value); | |
| if (names.size() == 2) { | |
| ChangeIsOperandFactory op = args.isOperands.get(names.get(1), names.get(0)); | |
| if (op != null) { | |
| return op.create(this); | |
| } | |
| } | |
| return status(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> commit(String id) { | |
| return ChangePredicates.commitPrefix(id); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> conflicts(String value) throws QueryParseException { | |
| if (!args.conflictsPredicateEnabled) { | |
| throw new QueryParseException("'conflicts:' operator is not supported on this gerrit host"); | |
| } | |
| List<Change> changes = parseChange(value); | |
| List<Predicate<ChangeData>> or = new ArrayList<>(changes.size()); | |
| for (Change c : changes) { | |
| or.add(ConflictsPredicate.create(args, value, c)); | |
| } | |
| return Predicate.or(or); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> p(String name) { | |
| return project(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> project(String name) { | |
| if (name.startsWith("^")) { | |
| return new RegexProjectPredicate(name); | |
| } | |
| return ChangePredicates.project(Project.nameKey(name)); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> projects(String name) { | |
| return ChangePredicates.projectPrefix(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> parentof(String value) throws QueryParseException { | |
| List<ChangeData> changes = parseChangeData(value); | |
| List<Predicate<ChangeData>> or = new ArrayList<>(changes.size()); | |
| for (ChangeData c : changes) { | |
| for (RevCommit revCommit : getParents(c)) { | |
| or.add(ChangePredicates.commitPrefix(revCommit.getId().getName())); | |
| } | |
| } | |
| return Predicate.or(or); | |
| } | |
| private Set<RevCommit> getParents(ChangeData change) { | |
| PatchSet ps = change.currentPatchSet(); | |
| try (Repository repo = args.repoManager.openRepository(change.project()); | |
| RevWalk walk = new RevWalk(repo)) { | |
| RevCommit c = walk.parseCommit(ps.commitId()); | |
| return Sets.newHashSet(c.getParents()); | |
| } catch (IOException e) { | |
| throw new StorageException( | |
| String.format( | |
| "Loading commit %s for ps %d of change %d failed.", | |
| ps.commitId(), ps.id().get(), ps.id().changeId().get()), | |
| e); | |
| } | |
| } | |
| @Operator | |
| public Predicate<ChangeData> parentproject(String name) { | |
| return new ParentProjectPredicate(args.projectCache, args.childProjects, name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> repository(String name) { | |
| return project(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> repositories(String name) { | |
| return projects(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> parentrepository(String name) { | |
| return parentproject(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> repo(String name) { | |
| return project(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> repos(String name) { | |
| return projects(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> parentrepo(String name) { | |
| return parentproject(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> branch(String name) throws QueryParseException { | |
| if (name.startsWith("^")) { | |
| return ref("^" + RefNames.fullName(name.substring(1))); | |
| } | |
| return ref(RefNames.fullName(name)); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> hashtag(String hashtag) { | |
| return ChangePredicates.hashtag(hashtag); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> inhashtag(String hashtag) throws QueryParseException { | |
| if (hashtag.startsWith("^")) { | |
| return new RegexHashtagPredicate(hashtag); | |
| } | |
| if (hashtag.isEmpty()) { | |
| return ChangePredicates.hashtag(hashtag); | |
| } | |
| checkOperatorAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag"); | |
| return ChangePredicates.fuzzyHashtag(hashtag); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> prefixhashtag(String hashtag) throws QueryParseException { | |
| if (hashtag.isEmpty()) { | |
| return ChangePredicates.hashtag(hashtag); | |
| } | |
| checkOperatorAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag"); | |
| return ChangePredicates.prefixHashtag(hashtag); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> topic(String name) { | |
| return ChangePredicates.exactTopic(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> intopic(String name) { | |
| if (name.startsWith("^")) { | |
| return new RegexTopicPredicate(name); | |
| } | |
| if (name.isEmpty()) { | |
| return ChangePredicates.exactTopic(name); | |
| } | |
| return ChangePredicates.fuzzyTopic(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> prefixtopic(String name) throws QueryParseException { | |
| if (name.isEmpty()) { | |
| return ChangePredicates.exactTopic(name); | |
| } | |
| checkOperatorAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic"); | |
| return ChangePredicates.prefixTopic(name); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> ref(String ref) throws QueryParseException { | |
| if (ref.startsWith("^")) { | |
| return new RegexRefPredicate(ref); | |
| } | |
| return ChangePredicates.ref(ref); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> f(String file) throws QueryParseException { | |
| return file(file); | |
| } | |
| /** | |
| * Creates a predicate to match changes by file. | |
| * | |
| * @param file the value of the {@code file} query operator | |
| * @throws QueryParseException thrown if parsing the value fails (may be thrown by subclasses) | |
| */ | |
| @Operator | |
| public Predicate<ChangeData> file(String file) throws QueryParseException { | |
| if (file.startsWith("^")) { | |
| return new RegexPathPredicate(file); | |
| } | |
| return ChangePredicates.file(args, file); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> path(String path) { | |
| if (path.startsWith("^")) { | |
| return new RegexPathPredicate(path); | |
| } | |
| return ChangePredicates.path(path); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> ext(String ext) { | |
| return extension(ext); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> extension(String ext) { | |
| return new FileExtensionPredicate(ext); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> onlyexts(String extList) { | |
| return onlyextensions(extList); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> onlyextensions(String extList) { | |
| return new FileExtensionListPredicate(extList); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> footer(String footer) { | |
| return ChangePredicates.footer(footer); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> hasfooter(String footerName) throws QueryParseException { | |
| checkOperatorAvailable(ChangeField.FOOTER_NAME, "hasfooter"); | |
| return ChangePredicates.hasFooter(footerName); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> dir(String directory) { | |
| return directory(directory); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> directory(String directory) { | |
| if (directory.startsWith("^")) { | |
| return new RegexDirectoryPredicate(directory); | |
| } | |
| return ChangePredicates.directory(directory); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> label(String name) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| Set<Account.Id> accounts = null; | |
| AccountGroup.UUID group = null; | |
| Integer count = null; | |
| PredicateArgs.Operator countOp = null; | |
| // Parse for: | |
| // label:Code-Review=1,user=jsmith or | |
| // label:Code-Review=1,jsmith or | |
| // label:Code-Review=1,group=android_approvers or | |
| // label:Code-Review=1,android_approvers | |
| // user/groups without a label will first attempt to match user | |
| // Special case: votes by owners can be tracked with ",owner": | |
| // label:Code-Review+2,owner | |
| // label:Code-Review+2,user=owner | |
| // label:Code-Review+1,count=2 | |
| List<String> splitReviewer = LABEL_SPLITTER.limit(2).splitToList(name); | |
| name = splitReviewer.get(0); // remove all but the vote piece, e.g.'CodeReview=1' | |
| if (splitReviewer.size() == 2) { | |
| // process the user/group piece | |
| PredicateArgs lblArgs = new PredicateArgs(splitReviewer.get(1)); | |
| // Disallow using the "count=" arg in conjunction with the "user=" or "group=" args. to avoid | |
| // unnecessary complexity. | |
| assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_USER); | |
| assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_GROUP); | |
| for (Map.Entry<String, ValOp> pair : lblArgs.keyValue.entrySet()) { | |
| String key = pair.getKey(); | |
| String value = pair.getValue().value(); | |
| PredicateArgs.Operator operator = pair.getValue().operator(); | |
| if (key.equalsIgnoreCase(ARG_ID_USER)) { | |
| if (value.equals(ARG_ID_OWNER)) { | |
| accounts = Collections.singleton(OWNER_ACCOUNT_ID); | |
| } else if (value.equals(ARG_ID_NON_UPLOADER)) { | |
| accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID); | |
| } else if (value.equals(ARG_ID_NON_CONTRIBUTOR)) { | |
| accounts = Collections.singleton(NON_CONTRIBUTOR_ACCOUNT_ID); | |
| } else { | |
| accounts = parseAccountIgnoreVisibility(value); | |
| } | |
| } else if (key.equalsIgnoreCase(ARG_ID_GROUP)) { | |
| group = parseGroup(value).getUUID(); | |
| } else if (key.equalsIgnoreCase(ARG_COUNT)) { | |
| if (!isInt(value)) { | |
| throw new QueryParseException("Invalid count argument. Value should be an integer"); | |
| } | |
| count = Integer.parseInt(value); | |
| countOp = operator; | |
| if (count == 0) { | |
| throw new QueryParseException("Argument count=0 is not allowed."); | |
| } | |
| if (count > LabelPredicate.MAX_COUNT) { | |
| throw new QueryParseException( | |
| String.format( | |
| "count=%d is not allowed. Maximum allowed value for count is %d.", | |
| count, LabelPredicate.MAX_COUNT)); | |
| } | |
| } else { | |
| throw new QueryParseException("Invalid argument identifier '" + pair.getKey() + "'"); | |
| } | |
| } | |
| for (String value : lblArgs.positional) { | |
| if (accounts != null || group != null) { | |
| throw new QueryParseException("more than one user/group specified (" + value + ")"); | |
| } | |
| if (value.equals(ARG_ID_OWNER)) { | |
| accounts = Collections.singleton(OWNER_ACCOUNT_ID); | |
| } else if (value.equals(ARG_ID_NON_UPLOADER)) { | |
| accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID); | |
| } else if (value.equals(ARG_ID_NON_CONTRIBUTOR)) { | |
| accounts = Collections.singleton(NON_CONTRIBUTOR_ACCOUNT_ID); | |
| } else { | |
| accounts = parseAccountIgnoreVisibility(value); | |
| } | |
| if (accounts.contains(NON_EXISTING_ACCOUNT_ID)) { | |
| // If it doesn't match an account, see if it matches a group | |
| // (accounts get precedence) | |
| try { | |
| group = parseGroup(value).getUUID(); | |
| } catch (QueryParseException e) { | |
| throw error("Neither user nor group " + value + " found", e); | |
| } | |
| } | |
| } | |
| } | |
| if (group != null) { | |
| accounts = getMembers(group); | |
| } | |
| // If the vote piece looks like Code-Review=NEED with a valid non-numeric | |
| // submit record status, interpret as a submit record query. | |
| int eq = name.indexOf('='); | |
| if (eq > 0) { | |
| String statusName = name.substring(eq + 1).toUpperCase(Locale.US); | |
| if (!isInt(statusName) && !MagicLabelValue.tryParse(statusName).isPresent()) { | |
| SubmitRecord.Label.Status status = | |
| Enums.getIfPresent(SubmitRecord.Label.Status.class, statusName).orNull(); | |
| if (status == null) { | |
| throw error("Invalid label status " + statusName + " in " + name); | |
| } | |
| return SubmitRecordPredicate.create(name.substring(0, eq), status, accounts); | |
| } | |
| } | |
| validateLabelArgs(accounts); | |
| return new LabelPredicate(args, name, accounts, group, count, countOp); | |
| } | |
| protected void validateLabelArgs(Set<Account.Id> accounts) throws QueryParseException { | |
| if (accounts != null && accounts.contains(NON_CONTRIBUTOR_ACCOUNT_ID)) { | |
| throw new QueryParseException("non_contributor arg is not allowed in change queries"); | |
| } | |
| } | |
| /** Assert that keys {@code k1} and {@code k2} do not exist in {@code labelArgs} together. */ | |
| private void assertDisjunctive(PredicateArgs labelArgs, String k1, String k2) | |
| throws QueryParseException { | |
| Map<String, ValOp> keyValArgs = labelArgs.keyValue; | |
| if (keyValArgs.containsKey(k1) && keyValArgs.containsKey(k2)) { | |
| throw new QueryParseException( | |
| String.format( | |
| "Cannot use the '%s' argument in conjunction with the '%s' argument", k1, k2)); | |
| } | |
| } | |
| private static boolean isInt(String s) { | |
| if (s == null) { | |
| return false; | |
| } | |
| if (s.startsWith("+")) { | |
| s = s.substring(1); | |
| } | |
| return Ints.tryParse(s) != null; | |
| } | |
| @Operator | |
| public Predicate<ChangeData> message(String text) throws QueryParseException { | |
| if (text.startsWith("^")) { | |
| checkFieldAvailable( | |
| ChangeField.COMMIT_MESSAGE_EXACT, | |
| "'message' operator with regular expression is not supported on this gerrit host"); | |
| return new RegexMessagePredicate(text); | |
| } | |
| return ChangePredicates.message(text); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> subject(String value) throws QueryParseException { | |
| checkOperatorAvailable(ChangeField.SUBJECT_SPEC, ChangeQueryBuilder.FIELD_SUBJECT); | |
| return ChangePredicates.subject(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> prefixsubject(String value) throws QueryParseException { | |
| checkOperatorAvailable( | |
| ChangeField.PREFIX_SUBJECT_SPEC, ChangeQueryBuilder.FIELD_PREFIX_SUBJECT); | |
| return ChangePredicates.prefixSubject(value); | |
| } | |
| private Predicate<ChangeData> starredBySelf() throws QueryParseException { | |
| return ChangePredicates.starBy(args.starredChangesUtil, self()); | |
| } | |
| private Predicate<ChangeData> draftBySelf() throws QueryParseException { | |
| return ChangePredicates.draftBy(args.draftCommentsReader, self()); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> visibleto(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| if (isSelf(who)) { | |
| return isVisible(); | |
| } | |
| Set<Account.Id> accounts = null; | |
| try { | |
| accounts = parseAccount(who); | |
| } catch (QueryParseException e) { | |
| if (e instanceof QueryRequiresAuthException) { | |
| throw e; | |
| } | |
| } | |
| if (accounts != null) { | |
| if (accounts.size() == 1) { | |
| return visibleto(args.userFactory.create(Iterables.getOnlyElement(accounts))); | |
| } else if (accounts.size() > 1) { | |
| throw error(String.format("\"%s\" resolves to multiple accounts", who)); | |
| } | |
| } | |
| // If its not an account, maybe its a group? | |
| Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null); | |
| if (!suggestions.isEmpty()) { | |
| HashSet<AccountGroup.UUID> ids = new HashSet<>(); | |
| for (GroupReference ref : suggestions) { | |
| ids.add(ref.getUUID()); | |
| } | |
| return visibleto(new GroupBackedUser(ids)); | |
| } | |
| throw error("No user or group matches \"" + who + "\"."); | |
| } | |
| public Predicate<ChangeData> visibleto(CurrentUser user) { | |
| return args.changeIsVisbleToPredicateFactory.forUser(user); | |
| } | |
| public Predicate<ChangeData> isVisible() throws QueryParseException { | |
| return visibleto(args.getUser()); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> o(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| return owner(who); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> owner(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| return owner(parseAccountIgnoreVisibility(who, (AccountState s) -> true)); | |
| } | |
| private Predicate<ChangeData> owner(Set<Account.Id> who) { | |
| List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size()); | |
| for (Account.Id id : who) { | |
| p.add(ChangePredicates.owner(id)); | |
| } | |
| return Predicate.or(p); | |
| } | |
| private Predicate<ChangeData> ownerDefaultField(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| Set<Account.Id> accounts = parseAccountIgnoreVisibility(who); | |
| if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) { | |
| return Predicate.any(); | |
| } | |
| return owner(accounts); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> uploader(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "uploader"); | |
| return uploader(parseAccountIgnoreVisibility(who, (AccountState s) -> true)); | |
| } | |
| private Predicate<ChangeData> uploader(Set<Account.Id> who) { | |
| List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size()); | |
| for (Account.Id id : who) { | |
| p.add(ChangePredicates.uploader(id)); | |
| } | |
| return Predicate.or(p); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> attention(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "attention"); | |
| return attention(parseAccountIgnoreVisibility(who, (AccountState s) -> true)); | |
| } | |
| private Predicate<ChangeData> attention(Set<Account.Id> who) { | |
| return Predicate.or(who.stream().map(ChangePredicates::attentionSet).collect(toImmutableSet())); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> ownerin(String group) throws QueryParseException, IOException { | |
| GroupReference g = parseGroup(group); | |
| AccountGroup.UUID groupId = g.getUUID(); | |
| if (args.groupBackend.isOrContainsExternalGroup(groupId)) { | |
| return new OwnerinPredicate(args.userFactory, groupId); | |
| } | |
| Set<Account.Id> accounts = getMembers(groupId); | |
| List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(accounts.size()); | |
| for (Account.Id id : accounts) { | |
| p.add(ChangePredicates.owner(id)); | |
| } | |
| return Predicate.or(p); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException { | |
| checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "uploaderin"); | |
| GroupReference g = parseGroup(group); | |
| AccountGroup.UUID groupId = g.getUUID(); | |
| if (args.groupBackend.isOrContainsExternalGroup(groupId)) { | |
| return new UploaderinPredicate(args.userFactory, groupId); | |
| } | |
| Set<Account.Id> accounts = getMembers(groupId); | |
| List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(accounts.size()); | |
| for (Account.Id id : accounts) { | |
| p.add(ChangePredicates.uploader(id)); | |
| } | |
| return Predicate.or(p); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> r(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| return reviewer(who); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> reviewer(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| return reviewer(who, false); | |
| } | |
| private Predicate<ChangeData> reviewerDefaultField(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| return reviewer(who, true); | |
| } | |
| private Predicate<ChangeData> reviewer(String who, boolean forDefaultField) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| Predicate<ChangeData> byState = | |
| reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField); | |
| if (Objects.equals(byState, Predicate.<ChangeData>any())) { | |
| return Predicate.any(); | |
| } | |
| return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP_SPEC)), byState); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> cc(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| return reviewerByState(who, ReviewerStateInternal.CC, false); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> reviewerin(String group) throws QueryParseException { | |
| GroupReference g = parseGroup(group); | |
| return new ReviewerinPredicate(args.userFactory, g.getUUID()); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> tr(String trackingId) { | |
| return ChangePredicates.trackingId(trackingId); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> bug(String trackingId) { | |
| return tr(trackingId); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> limit(String query) throws QueryParseException { | |
| Integer limit = Ints.tryParse(query); | |
| if (limit == null) { | |
| throw error("Invalid limit: " + query); | |
| } | |
| return new LimitPredicate<>(FIELD_LIMIT, limit); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> added(String value) throws QueryParseException { | |
| return new AddedPredicate(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> deleted(String value) throws QueryParseException { | |
| return new DeletedPredicate(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> size(String value) throws QueryParseException { | |
| return delta(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> delta(String value) throws QueryParseException { | |
| return new DeltaPredicate(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> commentby(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| return commentby(parseAccountIgnoreVisibility(who)); | |
| } | |
| private Predicate<ChangeData> commentby(Set<Account.Id> who) { | |
| List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size()); | |
| for (Account.Id id : who) { | |
| p.add(ChangePredicates.commentBy(id)); | |
| } | |
| return Predicate.or(p); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> from(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| Set<Account.Id> ownerIds = parseAccountIgnoreVisibility(who); | |
| return Predicate.or(owner(ownerIds), commentby(ownerIds)); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> query(String value) throws QueryParseException { | |
| // [name=]NAME[,user=USER|,group=GROUP] | |
| PredicateArgs inputArgs = new PredicateArgs(value); | |
| String name = null; | |
| Account.Id account = null; | |
| GroupDescription.Internal group = null; | |
| if (inputArgs.keyValue.containsKey(ARG_ID_USER) | |
| && inputArgs.keyValue.containsKey(ARG_ID_GROUP)) { | |
| throw new QueryParseException("User and group arguments are mutually exclusive"); | |
| } | |
| // [name=]<name> | |
| if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) { | |
| name = inputArgs.keyValue.get(ARG_ID_NAME).value(); | |
| } else if (inputArgs.positional.size() == 1) { | |
| name = Iterables.getOnlyElement(inputArgs.positional); | |
| } else if (inputArgs.positional.size() > 1) { | |
| throw new QueryParseException("Error parsing named query: " + value); | |
| } | |
| try { | |
| // [,user=<user>] | |
| if (inputArgs.keyValue.containsKey(ARG_ID_USER)) { | |
| Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value()); | |
| if (accounts != null && accounts.size() > 1) { | |
| throw error( | |
| String.format( | |
| "\"%s\" resolves to multiple accounts", inputArgs.keyValue.get(ARG_ID_USER))); | |
| } | |
| account = (accounts == null ? self() : Iterables.getOnlyElement(accounts)); | |
| } else { | |
| account = self(); | |
| } | |
| // [,group=<group>] | |
| if (inputArgs.keyValue.containsKey(ARG_ID_GROUP)) { | |
| AccountGroup.UUID groupId = | |
| parseGroup(inputArgs.keyValue.get(ARG_ID_GROUP).value()).getUUID(); | |
| GroupDescription.Basic backendGroup = args.groupBackend.get(groupId); | |
| if (!(backendGroup instanceof GroupDescription.Internal)) { | |
| throw error(backendGroup.getName() + " is not an Internal group"); | |
| } | |
| group = (GroupDescription.Internal) backendGroup; | |
| } | |
| BranchNameKey branch = BranchNameKey.create(args.allUsersName, RefNames.refsUsers(account)); | |
| if (group != null) { | |
| branch = BranchNameKey.create(args.allUsersName, RefNames.refsGroups(group.getGroupUUID())); | |
| } | |
| String query = getQueryList(branch).getQuery(name); | |
| if (query != null) { | |
| return parse(query); | |
| } | |
| } catch (RepositoryNotFoundException e) { | |
| throw new QueryParseException( | |
| "Unknown named query (no " + args.allUsersName + " repo): " + name, e); | |
| } catch (IOException | ConfigInvalidException e) { | |
| throw new QueryParseException("Error parsing named query: " + value, e); | |
| } | |
| throw new QueryParseException("Unknown named query: " + name); | |
| } | |
| protected QueryList getQueryList(BranchNameKey branch) | |
| throws ConfigInvalidException, IOException { | |
| QueryList ql = queryListByBranch.get(branch); | |
| if (ql == null) { | |
| ql = loadQueryList(branch); | |
| queryListByBranch.put(branch, ql); | |
| } | |
| return ql; | |
| } | |
| protected QueryList loadQueryList(BranchNameKey branch) | |
| throws ConfigInvalidException, IOException { | |
| VersionedAccountQueries q = VersionedAccountQueries.forBranch(branch); | |
| try (Repository git = args.repoManager.openRepository(args.allUsersName)) { | |
| q.load(args.allUsersName, git); | |
| } | |
| return q.getQueryList(); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> reviewedby(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| return ChangePredicates.reviewedBy(parseAccountIgnoreVisibility(who)); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> destination(String value) throws QueryParseException { | |
| // [name=]<name>[,user=<user>|,group=<group>] || [group=<group>,|user=<user>,][name=]<name> | |
| PredicateArgs inputArgs = new PredicateArgs(value); | |
| String name = null; | |
| Account.Id account = null; | |
| GroupDescription.Internal group = null; | |
| if (inputArgs.keyValue.containsKey(ARG_ID_USER) | |
| && inputArgs.keyValue.containsKey(ARG_ID_GROUP)) { | |
| throw new QueryParseException("User and group arguments are mutually exclusive"); | |
| } | |
| // [name=]<name> | |
| if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) { | |
| name = inputArgs.keyValue.get(ARG_ID_NAME).value(); | |
| } else if (inputArgs.positional.size() == 1) { | |
| name = Iterables.getOnlyElement(inputArgs.positional); | |
| } else if (inputArgs.positional.size() > 1) { | |
| throw new QueryParseException("Error parsing named destination: " + value); | |
| } | |
| try { | |
| // [,user=<user>] | |
| if (inputArgs.keyValue.containsKey(ARG_ID_USER)) { | |
| Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value()); | |
| if (accounts != null && accounts.size() > 1) { | |
| throw error( | |
| String.format( | |
| "\"%s\" resolves to multiple accounts", inputArgs.keyValue.get(ARG_ID_USER))); | |
| } | |
| account = (accounts == null ? self() : Iterables.getOnlyElement(accounts)); | |
| } else { | |
| account = self(); | |
| } | |
| // [,group=<group>] | |
| if (inputArgs.keyValue.containsKey(ARG_ID_GROUP)) { | |
| AccountGroup.UUID groupId = | |
| parseGroup(inputArgs.keyValue.get(ARG_ID_GROUP).value()).getUUID(); | |
| GroupDescription.Basic backendGroup = args.groupBackend.get(groupId); | |
| if (!(backendGroup instanceof GroupDescription.Internal)) { | |
| throw error(backendGroup.getName() + " is not an Internal group"); | |
| } | |
| group = (GroupDescription.Internal) backendGroup; | |
| } | |
| BranchNameKey branch = BranchNameKey.create(args.allUsersName, RefNames.refsUsers(account)); | |
| if (group != null) { | |
| branch = BranchNameKey.create(args.allUsersName, RefNames.refsGroups(group.getGroupUUID())); | |
| } | |
| Set<BranchNameKey> destinations = getDestinationList(branch).getDestinations(name); | |
| if (destinations != null && !destinations.isEmpty()) { | |
| return new BranchSetIndexPredicate(FIELD_DESTINATION + ":" + value, destinations); | |
| } | |
| } catch (RepositoryNotFoundException e) { | |
| throw new QueryParseException( | |
| "Unknown named destination (no " + args.allUsersName + " repo): " + name, e); | |
| } catch (IOException | ConfigInvalidException e) { | |
| throw new QueryParseException("Error parsing named destination: " + value, e); | |
| } | |
| throw new QueryParseException("Unknown named destination: " + name); | |
| } | |
| protected DestinationList getDestinationList(BranchNameKey branch) | |
| throws ConfigInvalidException, RepositoryNotFoundException, IOException { | |
| DestinationList dl = destinationListByBranch.get(branch); | |
| if (dl == null) { | |
| dl = loadDestinationList(branch); | |
| destinationListByBranch.put(branch, dl); | |
| } | |
| return dl; | |
| } | |
| protected DestinationList loadDestinationList(BranchNameKey branch) | |
| throws ConfigInvalidException, RepositoryNotFoundException, IOException { | |
| VersionedAccountDestinations d = VersionedAccountDestinations.forBranch(branch); | |
| try (Repository git = args.repoManager.openRepository(args.allUsersName)) { | |
| d.load(args.allUsersName, git); | |
| } | |
| return d.getDestinationList(); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> a(String who) throws QueryParseException { | |
| return author(who); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> author(String who) throws QueryParseException { | |
| return getAuthorOrCommitterPredicate( | |
| who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> committer(String who) throws QueryParseException { | |
| return getAuthorOrCommitterPredicate( | |
| who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> unresolved(String value) throws QueryParseException { | |
| return new IsUnresolvedPredicate(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> revertof(String value) throws QueryParseException { | |
| if (value == null || Ints.tryParse(value) == null) { | |
| throw new QueryParseException("'revertof' must be an integer"); | |
| } | |
| return ChangePredicates.revertOf(Change.id(Ints.tryParse(value))); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> submissionId(String value) { | |
| return ChangePredicates.submissionId(value); | |
| } | |
| @Operator | |
| public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException { | |
| checkOperatorAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf"); | |
| checkOperatorAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf"); | |
| if (Ints.tryParse(value) != null) { | |
| return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value))); | |
| } | |
| try { | |
| PatchSet.Id patchSetId = PatchSet.Id.parse(value); | |
| return ChangePredicates.cherryPickOf(patchSetId); | |
| } catch (IllegalArgumentException e) { | |
| throw new QueryParseException( | |
| "'" | |
| + value | |
| + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.", | |
| e); | |
| } | |
| } | |
| @Override | |
| protected Predicate<ChangeData> defaultField(String query) throws QueryParseException { | |
| if (query.startsWith("refs/")) { | |
| return ref(query); | |
| } else if (DEF_CHANGE.matcher(query).matches()) { | |
| List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(2); | |
| try { | |
| predicates.add(change(query)); | |
| } catch (QueryParseException e) { | |
| // Skip. | |
| } | |
| // For PAT_LEGACY_ID, it may also be the prefix of some commits. | |
| if (query.length() >= 6 && PAT_LEGACY_ID.matcher(query).matches()) { | |
| predicates.add(commit(query)); | |
| } | |
| return Predicate.or(predicates); | |
| } | |
| // Adapt the capacity of this list when adding more default predicates. | |
| List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11); | |
| try { | |
| Predicate<ChangeData> p = ownerDefaultField(query); | |
| if (!Objects.equals(p, Predicate.<ChangeData>any())) { | |
| predicates.add(p); | |
| } | |
| } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) { | |
| // Skip. | |
| } | |
| try { | |
| Predicate<ChangeData> p = reviewerDefaultField(query); | |
| if (!Objects.equals(p, Predicate.<ChangeData>any())) { | |
| predicates.add(p); | |
| } | |
| } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) { | |
| // Skip. | |
| } | |
| predicates.add(file(query)); | |
| try { | |
| predicates.add(label(query)); | |
| } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) { | |
| // Skip. | |
| } | |
| predicates.add(commit(query)); | |
| predicates.add(message(query)); | |
| predicates.add(comment(query)); | |
| predicates.add(projects(query)); | |
| predicates.add(ref(query)); | |
| predicates.add(branch(query)); | |
| predicates.add(topic(query)); | |
| // Adapt the capacity of the "predicates" list when adding more default | |
| // predicates. | |
| return Predicate.or(predicates); | |
| } | |
| private void checkOperatorAvailable(SchemaField<ChangeData, ?> field, String operator) | |
| throws QueryParseException { | |
| checkFieldAvailable( | |
| field, String.format("'%s' operator is not supported on this gerrit host", operator)); | |
| } | |
| protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String errorMessage) | |
| throws QueryParseException { | |
| if (!args.index.getSchema().hasField(field)) { | |
| throw new QueryParseException(errorMessage); | |
| } | |
| } | |
| private Predicate<ChangeData> getAuthorOrCommitterPredicate( | |
| String who, | |
| Function<String, Predicate<ChangeData>> exactPredicateFunc, | |
| Function<String, Predicate<ChangeData>> fullPredicateFunc) | |
| throws QueryParseException { | |
| if (Address.tryParse(who) != null) { | |
| return exactPredicateFunc.apply(who); | |
| } | |
| return getAuthorOrCommitterFullTextPredicate(who, fullPredicateFunc); | |
| } | |
| private Predicate<ChangeData> getAuthorOrCommitterFullTextPredicate( | |
| String who, Function<String, Predicate<ChangeData>> fullPredicateFunc) | |
| throws QueryParseException { | |
| if (isSelf(who)) { | |
| IdentifiedUser me = args.getIdentifiedUser(); | |
| List<Predicate<ChangeData>> predicates = | |
| me.getEmailAddresses().stream().map(fullPredicateFunc).collect(toList()); | |
| return Predicate.or(predicates); | |
| } | |
| Set<String> parts = SchemaUtil.getNameParts(who); | |
| if (parts.isEmpty()) { | |
| throw error("invalid value"); | |
| } | |
| List<Predicate<ChangeData>> predicates = | |
| parts.stream().map(fullPredicateFunc).collect(toList()); | |
| return Predicate.and(predicates); | |
| } | |
| private Set<Account.Id> getMembers(AccountGroup.UUID g) throws IOException { | |
| Set<Account.Id> accounts; | |
| Set<Account.Id> allMembers = | |
| args.groupMembers.listAccounts(g).stream().map(Account::id).collect(toSet()); | |
| int maxTerms = args.indexConfig.maxTerms(); | |
| if (allMembers.size() > maxTerms) { | |
| // limit the number of query terms otherwise Gerrit will barf | |
| accounts = allMembers.stream().limit(maxTerms).collect(toSet()); | |
| } else { | |
| accounts = allMembers; | |
| } | |
| return accounts; | |
| } | |
| private Set<Account.Id> parseAccount(String who) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| try { | |
| return args.accountResolver.resolveAsUser(args.getUser(), who).asNonEmptyIdSet(); | |
| } catch (UnresolvableAccountException e) { | |
| if (e.isSelf()) { | |
| throw new QueryRequiresAuthException(e.getMessage(), e); | |
| } | |
| throw new QueryParseException(e.getMessage(), e); | |
| } | |
| } | |
| private Set<Account.Id> parseAccountIgnoreVisibility(String who) | |
| throws QueryRequiresAuthException, IOException, ConfigInvalidException { | |
| try { | |
| return args.accountResolver | |
| .resolveAsUserIgnoreVisibility(args.getUser(), who) | |
| .asNonEmptyIdSet(); | |
| } catch (UnresolvableAccountException e) { | |
| if (e.isSelf()) { | |
| throw new QueryRequiresAuthException(e.getMessage(), e); | |
| } | |
| return ImmutableSet.of(NON_EXISTING_ACCOUNT_ID); | |
| } | |
| } | |
| private Set<Account.Id> parseAccountIgnoreVisibility( | |
| String who, java.util.function.Predicate<AccountState> activityFilter) | |
| throws QueryRequiresAuthException, IOException, ConfigInvalidException { | |
| try { | |
| return args.accountResolver | |
| .resolveAsUserIgnoreVisibility(args.getUser(), who, activityFilter) | |
| .asNonEmptyIdSet(); | |
| } catch (UnresolvableAccountException e) { | |
| // Thrown if no account was found. | |
| // Users can always see their own account. This means if self was being resolved and there was | |
| // no match the user wasn't logged in and the request was done anonymously. | |
| if (e.isSelf()) { | |
| throw new QueryRequiresAuthException(e.getMessage(), e); | |
| } | |
| // If no account is found, we don't want to fail with an error as this would allow users to | |
| // probe the existence of accounts (error -> account doesn't exist, empty result -> account | |
| // exists but didn't take part in any visible changes). Hence, we return a special account ID | |
| // (NON_EXISTING_ACCOUNT_ID) that doesn't match any account so the query can be normally | |
| // executed | |
| return ImmutableSet.of(NON_EXISTING_ACCOUNT_ID); | |
| } | |
| } | |
| private GroupReference parseGroup(String group) throws QueryParseException { | |
| GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group); | |
| if (g == null) { | |
| throw error("Group " + group + " not found"); | |
| } | |
| return g; | |
| } | |
| private List<Change> parseChange(String value) throws QueryParseException { | |
| return asChanges(parseChangeData(value)); | |
| } | |
| private List<ChangeData> parseChangeData(String value) throws QueryParseException { | |
| if (PAT_LEGACY_ID.matcher(value).matches()) { | |
| Optional<Change.Id> id = Change.Id.tryParse(value); | |
| if (!id.isPresent()) { | |
| throw error("Invalid change id " + value); | |
| } | |
| return args.queryProvider.get().byLegacyChangeId(id.get()); | |
| } else if (PAT_CHANGE_ID.matcher(value).matches()) { | |
| List<ChangeData> changes = args.queryProvider.get().byKeyPrefix(parseChangeId(value)); | |
| if (changes.isEmpty()) { | |
| throw error("Change " + value + " not found"); | |
| } | |
| return changes; | |
| } | |
| throw error("Change " + value + " not found"); | |
| } | |
| private static String parseChangeId(String value) { | |
| if (value.charAt(0) == 'i') { | |
| value = "I" + value.substring(1); | |
| } | |
| return value; | |
| } | |
| /** Returns {@link com.google.gerrit.entities.Account.Id} of the identified calling user. */ | |
| private Account.Id self() throws QueryParseException { | |
| return args.getIdentifiedUser().getAccountId(); | |
| } | |
| public Predicate<ChangeData> reviewerByState( | |
| String who, ReviewerStateInternal state, boolean forDefaultField) | |
| throws QueryParseException, IOException, ConfigInvalidException { | |
| Predicate<ChangeData> reviewerByEmailPredicate = null; | |
| Address address = Address.tryParse(who); | |
| if (address != null) { | |
| reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state); | |
| } | |
| Predicate<ChangeData> reviewerPredicate = null; | |
| try { | |
| Set<Account.Id> accounts = parseAccountIgnoreVisibility(who); | |
| if (!forDefaultField || accounts.size() <= MAX_ACCOUNTS_PER_DEFAULT_FIELD) { | |
| reviewerPredicate = | |
| Predicate.or( | |
| accounts.stream() | |
| .map(id -> ReviewerPredicate.forState(id, state)) | |
| .collect(toList())); | |
| } else { | |
| logger.atFine().log( | |
| "Skipping reviewer predicate for %s in default field query" | |
| + " because the number of matched accounts (%d) exceeds the limit of %d", | |
| who, accounts.size(), MAX_ACCOUNTS_PER_DEFAULT_FIELD); | |
| } | |
| } catch (QueryParseException e) { | |
| logger.atFine().log("Parsing %s as account failed: %s", who, e.getMessage()); | |
| // Propagate this exception only if we can't use 'who' to query by email | |
| if (reviewerByEmailPredicate == null) { | |
| throw e; | |
| } | |
| } | |
| if (reviewerPredicate != null && reviewerByEmailPredicate != null) { | |
| return Predicate.or(reviewerPredicate, reviewerByEmailPredicate); | |
| } else if (reviewerPredicate != null) { | |
| return reviewerPredicate; | |
| } else if (reviewerByEmailPredicate != null) { | |
| return reviewerByEmailPredicate; | |
| } else { | |
| return Predicate.any(); | |
| } | |
| } | |
| } |