Line data Source code
1 : // Copyright (C) 2009 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.query.change;
16 :
17 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
18 : import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
19 : import static com.google.gerrit.server.account.AccountResolver.isSelf;
20 : import static com.google.gerrit.server.query.change.ChangeData.asChanges;
21 : import static java.util.stream.Collectors.toList;
22 : import static java.util.stream.Collectors.toSet;
23 :
24 : import com.google.common.annotations.VisibleForTesting;
25 : import com.google.common.base.Enums;
26 : import com.google.common.base.Splitter;
27 : import com.google.common.collect.Iterables;
28 : import com.google.common.collect.Lists;
29 : import com.google.common.collect.Sets;
30 : import com.google.common.flogger.FluentLogger;
31 : import com.google.common.primitives.Ints;
32 : import com.google.gerrit.common.Nullable;
33 : import com.google.gerrit.entities.Account;
34 : import com.google.gerrit.entities.AccountGroup;
35 : import com.google.gerrit.entities.Address;
36 : import com.google.gerrit.entities.BranchNameKey;
37 : import com.google.gerrit.entities.Change;
38 : import com.google.gerrit.entities.GroupDescription;
39 : import com.google.gerrit.entities.GroupReference;
40 : import com.google.gerrit.entities.PatchSet;
41 : import com.google.gerrit.entities.Project;
42 : import com.google.gerrit.entities.RefNames;
43 : import com.google.gerrit.entities.SubmitRecord;
44 : import com.google.gerrit.exceptions.NotSignedInException;
45 : import com.google.gerrit.exceptions.StorageException;
46 : import com.google.gerrit.extensions.registration.DynamicMap;
47 : import com.google.gerrit.index.IndexConfig;
48 : import com.google.gerrit.index.Schema;
49 : import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
50 : import com.google.gerrit.index.SchemaUtil;
51 : import com.google.gerrit.index.query.LimitPredicate;
52 : import com.google.gerrit.index.query.Predicate;
53 : import com.google.gerrit.index.query.QueryBuilder;
54 : import com.google.gerrit.index.query.QueryParseException;
55 : import com.google.gerrit.index.query.QueryRequiresAuthException;
56 : import com.google.gerrit.server.CommentsUtil;
57 : import com.google.gerrit.server.CurrentUser;
58 : import com.google.gerrit.server.IdentifiedUser;
59 : import com.google.gerrit.server.StarredChangesUtil;
60 : import com.google.gerrit.server.account.AccountCache;
61 : import com.google.gerrit.server.account.AccountResolver;
62 : import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
63 : import com.google.gerrit.server.account.AccountState;
64 : import com.google.gerrit.server.account.DestinationList;
65 : import com.google.gerrit.server.account.GroupBackend;
66 : import com.google.gerrit.server.account.GroupBackends;
67 : import com.google.gerrit.server.account.GroupMembers;
68 : import com.google.gerrit.server.account.VersionedAccountDestinations;
69 : import com.google.gerrit.server.account.VersionedAccountQueries;
70 : import com.google.gerrit.server.change.ChangeTriplet;
71 : import com.google.gerrit.server.change.MergeabilityComputationBehavior;
72 : import com.google.gerrit.server.config.AllProjectsName;
73 : import com.google.gerrit.server.config.AllUsersName;
74 : import com.google.gerrit.server.config.GerritServerConfig;
75 : import com.google.gerrit.server.config.HasOperandAliasConfig;
76 : import com.google.gerrit.server.config.OperatorAliasConfig;
77 : import com.google.gerrit.server.experiments.ExperimentFeatures;
78 : import com.google.gerrit.server.git.GitRepositoryManager;
79 : import com.google.gerrit.server.index.change.ChangeField;
80 : import com.google.gerrit.server.index.change.ChangeIndex;
81 : import com.google.gerrit.server.index.change.ChangeIndexCollection;
82 : import com.google.gerrit.server.index.change.ChangeIndexRewriter;
83 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
84 : import com.google.gerrit.server.patch.PatchListCache;
85 : import com.google.gerrit.server.permissions.PermissionBackend;
86 : import com.google.gerrit.server.plugincontext.PluginSetContext;
87 : import com.google.gerrit.server.project.ChildProjects;
88 : import com.google.gerrit.server.project.ProjectCache;
89 : import com.google.gerrit.server.query.change.PredicateArgs.ValOp;
90 : import com.google.gerrit.server.rules.SubmitRule;
91 : import com.google.gerrit.server.submit.SubmitDryRun;
92 : import com.google.inject.Inject;
93 : import com.google.inject.Provider;
94 : import com.google.inject.ProvisionException;
95 : import com.google.inject.util.Providers;
96 : import java.io.IOException;
97 : import java.util.ArrayList;
98 : import java.util.Arrays;
99 : import java.util.Collection;
100 : import java.util.Collections;
101 : import java.util.HashMap;
102 : import java.util.HashSet;
103 : import java.util.List;
104 : import java.util.Map;
105 : import java.util.Objects;
106 : import java.util.Optional;
107 : import java.util.Set;
108 : import java.util.function.Function;
109 : import java.util.regex.Pattern;
110 : import org.eclipse.jgit.errors.ConfigInvalidException;
111 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
112 : import org.eclipse.jgit.lib.Config;
113 : import org.eclipse.jgit.lib.Repository;
114 : import org.eclipse.jgit.revwalk.RevCommit;
115 : import org.eclipse.jgit.revwalk.RevWalk;
116 :
117 : /** Parses a query string meant to be applied to change objects. */
118 : public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuilder> {
119 150 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
120 :
121 : public interface ChangeOperatorFactory extends OperatorFactory<ChangeData, ChangeQueryBuilder> {}
122 :
123 : /**
124 : * Converts a operand (operator value) passed to an operator into a {@link Predicate}.
125 : *
126 : * <p>Register a ChangeOperandFactory in a config Module like this (note, for an example we are
127 : * using the has predicate, when other predicate plugin operands are created they can be
128 : * registered in a similar manner):
129 : *
130 : * <p>bind(ChangeHasOperandFactory.class) .annotatedWith(Exports.named("your has operand"))
131 : * .to(YourClass.class);
132 : */
133 : public interface ChangeOperandFactory {
134 : Predicate<ChangeData> create(ChangeQueryBuilder builder) throws QueryParseException;
135 : }
136 :
137 : public interface ChangeHasOperandFactory extends ChangeOperandFactory {}
138 :
139 : public interface ChangeIsOperandFactory extends ChangeOperandFactory {}
140 :
141 150 : private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
142 150 : private static final Pattern PAT_CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
143 150 : private static final Pattern DEF_CHANGE =
144 150 : Pattern.compile("^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$");
145 :
146 : static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10;
147 :
148 : // NOTE: As new search operations are added, please keep the suggestions in
149 : // gr-search-bar.ts up to date.
150 :
151 : public static final String FIELD_ADDED = "added";
152 : public static final String FIELD_AGE = "age";
153 : public static final String FIELD_ATTENTION_SET_USERS = "attentionusers";
154 : public static final String FIELD_ATTENTION_SET_USERS_COUNT = "attentionuserscount";
155 : public static final String FIELD_ATTENTION_SET_FULL = "attentionfull";
156 : public static final String FIELD_ASSIGNEE = "assignee";
157 : public static final String FIELD_AUTHOR = "author";
158 : public static final String FIELD_EXACTAUTHOR = "exactauthor";
159 :
160 : public static final String FIELD_CHANGE = "change";
161 : public static final String FIELD_CHANGE_ID = "change_id";
162 : public static final String FIELD_COMMENT = "comment";
163 : public static final String FIELD_COMMENTBY = "commentby";
164 : public static final String FIELD_COMMIT = "commit";
165 : public static final String FIELD_COMMITTER = "committer";
166 : public static final String FIELD_DIRECTORY = "directory";
167 : public static final String FIELD_EXACTCOMMITTER = "exactcommitter";
168 : public static final String FIELD_EXTENSION = "extension";
169 : public static final String FIELD_ONLY_EXTENSIONS = "onlyextensions";
170 : public static final String FIELD_FOOTER = "footer";
171 : public static final String FIELD_FOOTER_NAME = "footernames";
172 : public static final String FIELD_CONFLICTS = "conflicts";
173 : public static final String FIELD_DELETED = "deleted";
174 : public static final String FIELD_DELTA = "delta";
175 : public static final String FIELD_DESTINATION = "destination";
176 : public static final String FIELD_DRAFTBY = "draftby";
177 : public static final String FIELD_EDITBY = "editby";
178 : public static final String FIELD_EXACTCOMMIT = "exactcommit";
179 : public static final String FIELD_FILE = "file";
180 : public static final String FIELD_FILEPART = "filepart";
181 : public static final String FIELD_GROUP = "group";
182 : public static final String FIELD_HASHTAG = "hashtag";
183 : public static final String FIELD_LABEL = "label";
184 : public static final String FIELD_LIMIT = "limit";
185 : public static final String FIELD_MERGE = "merge";
186 : public static final String FIELD_MERGEABLE = "mergeable2";
187 : public static final String FIELD_MERGED_ON = "mergedon";
188 : public static final String FIELD_MESSAGE = "message";
189 : public static final String FIELD_MESSAGE_EXACT = "messageexact";
190 : public static final String FIELD_OWNER = "owner";
191 : public static final String FIELD_OWNERIN = "ownerin";
192 : public static final String FIELD_PARENTOF = "parentof";
193 : public static final String FIELD_PARENTPROJECT = "parentproject";
194 : public static final String FIELD_PENDING_REVIEWER = "pendingreviewer";
195 : public static final String FIELD_PENDING_REVIEWER_BY_EMAIL = "pendingreviewerbyemail";
196 : public static final String FIELD_PRIVATE = "private";
197 : public static final String FIELD_PROJECT = "project";
198 : public static final String FIELD_PROJECTS = "projects";
199 : public static final String FIELD_REF = "ref";
200 : public static final String FIELD_REVIEWEDBY = "reviewedby";
201 : public static final String FIELD_REVIEWERIN = "reviewerin";
202 : public static final String FIELD_STAR = "star";
203 : public static final String FIELD_STARBY = "starby";
204 : public static final String FIELD_STARTED = "started";
205 : public static final String FIELD_STATUS = "status";
206 : public static final String FIELD_SUBMISSIONID = "submissionid";
207 : public static final String FIELD_TR = "tr";
208 : public static final String FIELD_UNRESOLVED_COMMENT_COUNT = "unresolved";
209 : public static final String FIELD_UPLOADER = "uploader";
210 : public static final String FIELD_UPLOADERIN = "uploaderin";
211 : public static final String FIELD_VISIBLETO = "visibleto";
212 : public static final String FIELD_WATCHEDBY = "watchedby";
213 : public static final String FIELD_WIP = "wip";
214 : public static final String FIELD_REVERTOF = "revertof";
215 : public static final String FIELD_PURE_REVERT = "ispurerevert";
216 : public static final String FIELD_CHERRYPICK = "cherrypick";
217 : public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange";
218 : public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset";
219 : public static final String FIELD_IS_SUBMITTABLE = "issubmittable";
220 :
221 : public static final String ARG_ID_NAME = "name";
222 : public static final String ARG_ID_USER = "user";
223 : public static final String ARG_ID_GROUP = "group";
224 : public static final String ARG_ID_OWNER = "owner";
225 : public static final String ARG_ID_NON_UPLOADER = "non_uploader";
226 : public static final String ARG_COUNT = "count";
227 150 : public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
228 150 : public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
229 :
230 : public static final String OPERATOR_MERGED_BEFORE = "mergedbefore";
231 : public static final String OPERATOR_MERGED_AFTER = "mergedafter";
232 :
233 : // Operators to match on the last time the change was updated. Naming for legacy reasons.
234 : public static final String OPERATOR_BEFORE = "before";
235 : public static final String OPERATOR_AFTER = "after";
236 :
237 150 : private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
238 : new QueryBuilder.Definition<>(ChangeQueryBuilder.class);
239 :
240 : @VisibleForTesting
241 : public static class Arguments {
242 : final AccountCache accountCache;
243 : final AccountResolver accountResolver;
244 : final AllProjectsName allProjectsName;
245 : final AllUsersName allUsersName;
246 : final PermissionBackend permissionBackend;
247 : final ChangeData.Factory changeDataFactory;
248 : final ChangeIndex index;
249 : final ChangeIndexRewriter rewriter;
250 : final CommentsUtil commentsUtil;
251 : final ConflictsCache conflictsCache;
252 : final DynamicMap<ChangeHasOperandFactory> hasOperands;
253 : final DynamicMap<ChangeIsOperandFactory> isOperands;
254 : final DynamicMap<ChangeOperatorFactory> opFactories;
255 : final GitRepositoryManager repoManager;
256 : final GroupBackend groupBackend;
257 : final IdentifiedUser.GenericFactory userFactory;
258 : final IndexConfig indexConfig;
259 : final PatchListCache patchListCache;
260 : final ProjectCache projectCache;
261 : final Provider<InternalChangeQuery> queryProvider;
262 : final ChildProjects childProjects;
263 : final StarredChangesUtil starredChangesUtil;
264 : final SubmitDryRun submitDryRun;
265 : final GroupMembers groupMembers;
266 : final ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory;
267 : final OperatorAliasConfig operatorAliasConfig;
268 : final boolean indexMergeable;
269 : final boolean conflictsPredicateEnabled;
270 : final ExperimentFeatures experimentFeatures;
271 : final HasOperandAliasConfig hasOperandAliasConfig;
272 : final PluginSetContext<SubmitRule> submitRules;
273 :
274 : private final Provider<CurrentUser> self;
275 :
276 : @Inject
277 : @VisibleForTesting
278 : public Arguments(
279 : Provider<InternalChangeQuery> queryProvider,
280 : ChangeIndexRewriter rewriter,
281 : DynamicMap<ChangeOperatorFactory> opFactories,
282 : DynamicMap<ChangeHasOperandFactory> hasOperands,
283 : DynamicMap<ChangeIsOperandFactory> isOperands,
284 : IdentifiedUser.GenericFactory userFactory,
285 : Provider<CurrentUser> self,
286 : PermissionBackend permissionBackend,
287 : ChangeData.Factory changeDataFactory,
288 : CommentsUtil commentsUtil,
289 : AccountResolver accountResolver,
290 : GroupBackend groupBackend,
291 : AllProjectsName allProjectsName,
292 : AllUsersName allUsersName,
293 : PatchListCache patchListCache,
294 : GitRepositoryManager repoManager,
295 : ProjectCache projectCache,
296 : ChildProjects childProjects,
297 : ChangeIndexCollection indexes,
298 : SubmitDryRun submitDryRun,
299 : ConflictsCache conflictsCache,
300 : IndexConfig indexConfig,
301 : StarredChangesUtil starredChangesUtil,
302 : AccountCache accountCache,
303 : GroupMembers groupMembers,
304 : OperatorAliasConfig operatorAliasConfig,
305 : @GerritServerConfig Config gerritConfig,
306 : ExperimentFeatures experimentFeatures,
307 : HasOperandAliasConfig hasOperandAliasConfig,
308 : ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
309 : PluginSetContext<SubmitRule> submitRules) {
310 149 : this(
311 : queryProvider,
312 : rewriter,
313 : opFactories,
314 : hasOperands,
315 : isOperands,
316 : userFactory,
317 : self,
318 : permissionBackend,
319 : changeDataFactory,
320 : commentsUtil,
321 : accountResolver,
322 : groupBackend,
323 : allProjectsName,
324 : allUsersName,
325 : patchListCache,
326 : repoManager,
327 : projectCache,
328 : childProjects,
329 : submitDryRun,
330 : conflictsCache,
331 149 : indexes != null ? indexes.getSearchIndex() : null,
332 : indexConfig,
333 : starredChangesUtil,
334 : accountCache,
335 : groupMembers,
336 : operatorAliasConfig,
337 149 : MergeabilityComputationBehavior.fromConfig(gerritConfig).includeInIndex(),
338 149 : gerritConfig.getBoolean("change", null, "conflictsPredicateEnabled", true),
339 : experimentFeatures,
340 : hasOperandAliasConfig,
341 : changeIsVisbleToPredicateFactory,
342 : submitRules);
343 149 : }
344 :
345 : private Arguments(
346 : Provider<InternalChangeQuery> queryProvider,
347 : ChangeIndexRewriter rewriter,
348 : DynamicMap<ChangeOperatorFactory> opFactories,
349 : DynamicMap<ChangeHasOperandFactory> hasOperands,
350 : DynamicMap<ChangeIsOperandFactory> isOperands,
351 : IdentifiedUser.GenericFactory userFactory,
352 : Provider<CurrentUser> self,
353 : PermissionBackend permissionBackend,
354 : ChangeData.Factory changeDataFactory,
355 : CommentsUtil commentsUtil,
356 : AccountResolver accountResolver,
357 : GroupBackend groupBackend,
358 : AllProjectsName allProjectsName,
359 : AllUsersName allUsersName,
360 : PatchListCache patchListCache,
361 : GitRepositoryManager repoManager,
362 : ProjectCache projectCache,
363 : ChildProjects childProjects,
364 : SubmitDryRun submitDryRun,
365 : ConflictsCache conflictsCache,
366 : ChangeIndex index,
367 : IndexConfig indexConfig,
368 : StarredChangesUtil starredChangesUtil,
369 : AccountCache accountCache,
370 : GroupMembers groupMembers,
371 : OperatorAliasConfig operatorAliasConfig,
372 : boolean indexMergeable,
373 : boolean conflictsPredicateEnabled,
374 : ExperimentFeatures experimentFeatures,
375 : HasOperandAliasConfig hasOperandAliasConfig,
376 : ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
377 149 : PluginSetContext<SubmitRule> submitRules) {
378 149 : this.queryProvider = queryProvider;
379 149 : this.rewriter = rewriter;
380 149 : this.opFactories = opFactories;
381 149 : this.userFactory = userFactory;
382 149 : this.self = self;
383 149 : this.permissionBackend = permissionBackend;
384 149 : this.changeDataFactory = changeDataFactory;
385 149 : this.commentsUtil = commentsUtil;
386 149 : this.accountResolver = accountResolver;
387 149 : this.groupBackend = groupBackend;
388 149 : this.allProjectsName = allProjectsName;
389 149 : this.allUsersName = allUsersName;
390 149 : this.patchListCache = patchListCache;
391 149 : this.repoManager = repoManager;
392 149 : this.projectCache = projectCache;
393 149 : this.childProjects = childProjects;
394 149 : this.submitDryRun = submitDryRun;
395 149 : this.conflictsCache = conflictsCache;
396 149 : this.index = index;
397 149 : this.indexConfig = indexConfig;
398 149 : this.starredChangesUtil = starredChangesUtil;
399 149 : this.accountCache = accountCache;
400 149 : this.hasOperands = hasOperands;
401 149 : this.isOperands = isOperands;
402 149 : this.groupMembers = groupMembers;
403 149 : this.changeIsVisbleToPredicateFactory = changeIsVisbleToPredicateFactory;
404 149 : this.operatorAliasConfig = operatorAliasConfig;
405 149 : this.indexMergeable = indexMergeable;
406 149 : this.conflictsPredicateEnabled = conflictsPredicateEnabled;
407 149 : this.experimentFeatures = experimentFeatures;
408 149 : this.hasOperandAliasConfig = hasOperandAliasConfig;
409 149 : this.submitRules = submitRules;
410 149 : }
411 :
412 : Arguments asUser(CurrentUser otherUser) {
413 9 : return new Arguments(
414 : queryProvider,
415 : rewriter,
416 : opFactories,
417 : hasOperands,
418 : isOperands,
419 : userFactory,
420 9 : Providers.of(otherUser),
421 : permissionBackend,
422 : changeDataFactory,
423 : commentsUtil,
424 : accountResolver,
425 : groupBackend,
426 : allProjectsName,
427 : allUsersName,
428 : patchListCache,
429 : repoManager,
430 : projectCache,
431 : childProjects,
432 : submitDryRun,
433 : conflictsCache,
434 : index,
435 : indexConfig,
436 : starredChangesUtil,
437 : accountCache,
438 : groupMembers,
439 : operatorAliasConfig,
440 : indexMergeable,
441 : conflictsPredicateEnabled,
442 : experimentFeatures,
443 : hasOperandAliasConfig,
444 : changeIsVisbleToPredicateFactory,
445 : submitRules);
446 : }
447 :
448 : Arguments asUser(Account.Id otherId) {
449 : try {
450 0 : CurrentUser u = self.get();
451 0 : if (u.isIdentifiedUser() && otherId.equals(u.getAccountId())) {
452 0 : return this;
453 : }
454 0 : } catch (ProvisionException e) {
455 : // Doesn't match current user, continue.
456 0 : }
457 0 : return asUser(userFactory.create(otherId));
458 : }
459 :
460 : IdentifiedUser getIdentifiedUser() throws QueryRequiresAuthException {
461 : try {
462 10 : CurrentUser u = getUser();
463 10 : if (u.isIdentifiedUser()) {
464 10 : return u.asIdentifiedUser();
465 : }
466 4 : throw new QueryRequiresAuthException(NotSignedInException.MESSAGE);
467 0 : } catch (ProvisionException e) {
468 0 : throw new QueryRequiresAuthException(NotSignedInException.MESSAGE, e);
469 : }
470 : }
471 :
472 : CurrentUser getUser() throws QueryRequiresAuthException {
473 : try {
474 15 : return self.get();
475 0 : } catch (ProvisionException e) {
476 0 : throw new QueryRequiresAuthException(NotSignedInException.MESSAGE, e);
477 : }
478 : }
479 :
480 : @Nullable
481 : Schema<ChangeData> getSchema() {
482 11 : return index != null ? index.getSchema() : null;
483 : }
484 : }
485 :
486 : private final Arguments args;
487 149 : protected Map<String, String> hasOperandAliases = Collections.emptyMap();
488 149 : private Map<Account.Id, DestinationList> destinationListByAccount = new HashMap<>();
489 :
490 150 : private static final Splitter RULE_SPLITTER = Splitter.on("=");
491 150 : private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
492 150 : private static final Splitter LABEL_SPLITTER = Splitter.on(",");
493 :
494 : @Inject
495 : ChangeQueryBuilder(Arguments args) {
496 149 : this(mydef, args);
497 149 : setupAliases();
498 149 : }
499 :
500 : @VisibleForTesting
501 : protected ChangeQueryBuilder(Definition<ChangeData, ChangeQueryBuilder> def, Arguments args) {
502 149 : super(def, args.opFactories);
503 149 : this.args = args;
504 149 : }
505 :
506 : private void setupAliases() {
507 149 : setOperatorAliases(args.operatorAliasConfig.getChangeQueryOperatorAliases());
508 149 : hasOperandAliases = args.hasOperandAliasConfig.getChangeQueryHasOperandAliases();
509 149 : }
510 :
511 : public ChangeQueryBuilder asUser(CurrentUser user) {
512 9 : return new ChangeQueryBuilder(builderDef, args.asUser(user));
513 : }
514 :
515 : @Operator
516 : public Predicate<ChangeData> age(String value) {
517 5 : return new AgePredicate(value);
518 : }
519 :
520 : @Operator
521 : public Predicate<ChangeData> before(String value) throws QueryParseException {
522 4 : return new BeforePredicate(ChangeField.UPDATED, ChangeQueryBuilder.OPERATOR_BEFORE, value);
523 : }
524 :
525 : @Operator
526 : public Predicate<ChangeData> until(String value) throws QueryParseException {
527 4 : return before(value);
528 : }
529 :
530 : @Operator
531 : public Predicate<ChangeData> after(String value) throws QueryParseException {
532 4 : return new AfterPredicate(ChangeField.UPDATED, ChangeQueryBuilder.OPERATOR_AFTER, value);
533 : }
534 :
535 : @Operator
536 : public Predicate<ChangeData> since(String value) throws QueryParseException {
537 4 : return after(value);
538 : }
539 :
540 : @Operator
541 : public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
542 4 : checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_BEFORE);
543 4 : return new BeforePredicate(
544 : ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
545 : }
546 :
547 : @Operator
548 : public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
549 4 : checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_AFTER);
550 4 : return new AfterPredicate(
551 : ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
552 : }
553 :
554 : @Operator
555 : public Predicate<ChangeData> change(String query) throws QueryParseException {
556 19 : Optional<ChangeTriplet> triplet = ChangeTriplet.parse(query);
557 19 : if (triplet.isPresent()) {
558 4 : return Predicate.and(
559 4 : project(triplet.get().project().get()),
560 4 : branch(triplet.get().branch().branch()),
561 4 : ChangePredicates.idPrefix(parseChangeId(triplet.get().id().get())));
562 : }
563 19 : if (PAT_LEGACY_ID.matcher(query).matches()) {
564 9 : Integer id = Ints.tryParse(query);
565 9 : if (id != null) {
566 9 : return ChangePredicates.idStr(Change.id(id));
567 : }
568 16 : } else if (PAT_CHANGE_ID.matcher(query).matches()) {
569 16 : return ChangePredicates.idPrefix(parseChangeId(query));
570 : }
571 :
572 4 : throw new QueryParseException("Invalid change format");
573 : }
574 :
575 : @Operator
576 : public Predicate<ChangeData> comment(String value) {
577 10 : return ChangePredicates.comment(value);
578 : }
579 :
580 : @Operator
581 : public Predicate<ChangeData> status(String statusName) throws QueryParseException {
582 17 : if ("reviewed".equalsIgnoreCase(statusName)) {
583 4 : return ChangePredicates.unreviewed();
584 : }
585 17 : return ChangeStatusPredicate.parse(statusName);
586 : }
587 :
588 : public Predicate<ChangeData> statusOpen() {
589 0 : return ChangeStatusPredicate.open();
590 : }
591 :
592 : @Operator
593 : public Predicate<ChangeData> rule(String value) throws QueryParseException {
594 4 : String ruleNameArg = value;
595 4 : String statusArg = null;
596 4 : List<String> queryArgs = RULE_SPLITTER.splitToList(value);
597 4 : if (queryArgs.size() > 2) {
598 0 : throw new QueryParseException(
599 : "Invalid query arguments. Correct format is 'rule:<rule_name>=<status>' "
600 : + "with <rule_name> in the form of <plugin>~<rule>. For Gerrit core rules, "
601 : + "rule name should be specified as gerrit~<rule>.");
602 : }
603 4 : if (queryArgs.size() == 2) {
604 4 : ruleNameArg = queryArgs.get(0);
605 4 : statusArg = queryArgs.get(1);
606 : }
607 :
608 4 : return statusArg == null
609 4 : ? Predicate.or(
610 4 : Arrays.asList(
611 4 : ChangePredicates.submitRuleStatus(ruleNameArg + "=" + SubmitRecord.Status.OK),
612 4 : ChangePredicates.submitRuleStatus(ruleNameArg + "=" + SubmitRecord.Status.FORCED)))
613 4 : : ChangePredicates.submitRuleStatus(ruleNameArg + "=" + statusArg);
614 : }
615 :
616 : @Operator
617 : public Predicate<ChangeData> has(String value) throws QueryParseException {
618 12 : value = hasOperandAliases.getOrDefault(value, value);
619 12 : if ("star".equalsIgnoreCase(value)) {
620 4 : return starredBySelf();
621 : }
622 :
623 12 : if ("draft".equalsIgnoreCase(value)) {
624 6 : return draftBySelf();
625 : }
626 :
627 10 : if ("edit".equalsIgnoreCase(value)) {
628 8 : return ChangePredicates.editBy(self());
629 : }
630 :
631 6 : if ("attention".equalsIgnoreCase(value)) {
632 4 : checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
633 4 : return new IsAttentionPredicate();
634 : }
635 :
636 6 : if ("unresolved".equalsIgnoreCase(value)) {
637 6 : return new IsUnresolvedPredicate();
638 : }
639 :
640 : // for plugins the value will be operandName_pluginName
641 0 : List<String> names = PLUGIN_SPLITTER.splitToList(value);
642 0 : if (names.size() == 2) {
643 0 : ChangeHasOperandFactory op = args.hasOperands.get(names.get(1), names.get(0));
644 0 : if (op != null) {
645 0 : return op.create(this);
646 : }
647 : }
648 :
649 0 : throw new IllegalArgumentException();
650 : }
651 :
652 : @Operator
653 : public Predicate<ChangeData> is(String value) throws QueryParseException {
654 11 : if ("starred".equalsIgnoreCase(value)) {
655 4 : return starredBySelf();
656 : }
657 :
658 11 : if ("watched".equalsIgnoreCase(value)) {
659 4 : return new IsWatchedByPredicate(args);
660 : }
661 :
662 11 : if ("visible".equalsIgnoreCase(value)) {
663 4 : return isVisible();
664 : }
665 :
666 11 : if ("reviewed".equalsIgnoreCase(value)) {
667 4 : return ChangePredicates.unreviewed();
668 : }
669 :
670 11 : if ("owner".equalsIgnoreCase(value)) {
671 4 : return ChangePredicates.owner(self());
672 : }
673 :
674 11 : if ("uploader".equalsIgnoreCase(value)) {
675 4 : checkFieldAvailable(ChangeField.UPLOADER_SPEC, "is:uploader");
676 4 : return ChangePredicates.uploader(self());
677 : }
678 :
679 11 : if ("reviewer".equalsIgnoreCase(value)) {
680 4 : return Predicate.and(
681 4 : Predicate.not(new BooleanPredicate(ChangeField.WIP)), ReviewerPredicate.reviewer(self()));
682 : }
683 :
684 11 : if ("cc".equalsIgnoreCase(value)) {
685 4 : return ReviewerPredicate.cc(self());
686 : }
687 :
688 11 : if ("mergeable".equalsIgnoreCase(value)) {
689 6 : if (!args.indexMergeable) {
690 5 : throw new QueryParseException("'is:mergeable' operator is not supported by server");
691 : }
692 6 : return new BooleanPredicate(ChangeField.MERGEABLE);
693 : }
694 :
695 10 : if ("merge".equalsIgnoreCase(value)) {
696 4 : checkFieldAvailable(ChangeField.MERGE, "is:merge");
697 4 : return new BooleanPredicate(ChangeField.MERGE);
698 : }
699 :
700 10 : if ("private".equalsIgnoreCase(value)) {
701 4 : return new BooleanPredicate(ChangeField.PRIVATE);
702 : }
703 :
704 10 : if ("attention".equalsIgnoreCase(value)) {
705 4 : checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
706 4 : return new IsAttentionPredicate();
707 : }
708 :
709 10 : if ("assigned".equalsIgnoreCase(value)) {
710 4 : return Predicate.not(ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE)));
711 : }
712 :
713 10 : if ("unassigned".equalsIgnoreCase(value)) {
714 4 : return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
715 : }
716 :
717 10 : if ("pure-revert".equalsIgnoreCase(value)) {
718 5 : checkFieldAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert");
719 5 : return ChangePredicates.pureRevert("1");
720 : }
721 :
722 10 : if ("submittable".equalsIgnoreCase(value)) {
723 5 : if (!args.index.getSchema().hasField(ChangeField.IS_SUBMITTABLE_SPEC)) {
724 : // SubmittablePredicate will match if *any* of the submit records are OK,
725 : // but we need to check that they're *all* OK, so check that none of the
726 : // submit records match any of the negative cases. To avoid checking yet
727 : // more negative cases for CLOSED and FORCED, instead make sure at least
728 : // one submit record is OK.
729 0 : return Predicate.and(
730 : new SubmittablePredicate(SubmitRecord.Status.OK),
731 0 : Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
732 0 : Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
733 : }
734 5 : checkFieldAvailable(ChangeField.IS_SUBMITTABLE_SPEC, "is:submittable");
735 5 : return new IsSubmittablePredicate();
736 : }
737 :
738 9 : if ("started".equalsIgnoreCase(value)) {
739 4 : checkFieldAvailable(ChangeField.STARTED, "is:started");
740 4 : return new BooleanPredicate(ChangeField.STARTED);
741 : }
742 :
743 9 : if ("wip".equalsIgnoreCase(value)) {
744 5 : return new BooleanPredicate(ChangeField.WIP);
745 : }
746 :
747 9 : if ("cherrypick".equalsIgnoreCase(value)) {
748 4 : checkFieldAvailable(ChangeField.CHERRY_PICK, "is:cherrypick");
749 4 : return new BooleanPredicate(ChangeField.CHERRY_PICK);
750 : }
751 :
752 : // for plugins the value will be operandName_pluginName
753 9 : List<String> names = PLUGIN_SPLITTER.splitToList(value);
754 9 : if (names.size() == 2) {
755 2 : ChangeIsOperandFactory op = args.isOperands.get(names.get(1), names.get(0));
756 2 : if (op != null) {
757 2 : return op.create(this);
758 : }
759 : }
760 7 : return status(value);
761 : }
762 :
763 : @Operator
764 : public Predicate<ChangeData> commit(String id) {
765 11 : return ChangePredicates.commitPrefix(id);
766 : }
767 :
768 : @Operator
769 : public Predicate<ChangeData> conflicts(String value) throws QueryParseException {
770 4 : if (!args.conflictsPredicateEnabled) {
771 0 : throw new QueryParseException("'conflicts:' operator is not supported by server");
772 : }
773 4 : List<Change> changes = parseChange(value);
774 4 : List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
775 4 : for (Change c : changes) {
776 4 : or.add(ConflictsPredicate.create(args, value, c));
777 4 : }
778 4 : return Predicate.or(or);
779 : }
780 :
781 : @Operator
782 : public Predicate<ChangeData> p(String name) {
783 0 : return project(name);
784 : }
785 :
786 : @Operator
787 : public Predicate<ChangeData> project(String name) {
788 13 : if (name.startsWith("^")) {
789 0 : return new RegexProjectPredicate(name);
790 : }
791 13 : return ChangePredicates.project(Project.nameKey(name));
792 : }
793 :
794 : @Operator
795 : public Predicate<ChangeData> projects(String name) {
796 10 : return ChangePredicates.projectPrefix(name);
797 : }
798 :
799 : @Operator
800 : public Predicate<ChangeData> parentof(String value) throws QueryParseException {
801 4 : List<ChangeData> changes = parseChangeData(value);
802 4 : List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
803 4 : for (ChangeData c : changes) {
804 4 : for (RevCommit revCommit : getParents(c)) {
805 4 : or.add(ChangePredicates.commitPrefix(revCommit.getId().getName()));
806 4 : }
807 4 : }
808 4 : return Predicate.or(or);
809 : }
810 :
811 : private Set<RevCommit> getParents(ChangeData change) {
812 4 : PatchSet ps = change.currentPatchSet();
813 4 : try (Repository repo = args.repoManager.openRepository(change.project());
814 4 : RevWalk walk = new RevWalk(repo)) {
815 4 : RevCommit c = walk.parseCommit(ps.commitId());
816 4 : return Sets.newHashSet(c.getParents());
817 0 : } catch (IOException e) {
818 0 : throw new StorageException(
819 0 : String.format(
820 : "Loading commit %s for ps %d of change %d failed.",
821 0 : ps.commitId(), ps.id().get(), ps.id().changeId().get()),
822 : e);
823 : }
824 : }
825 :
826 : @Operator
827 : public Predicate<ChangeData> parentproject(String name) {
828 4 : return new ParentProjectPredicate(args.projectCache, args.childProjects, name);
829 : }
830 :
831 : @Operator
832 : public Predicate<ChangeData> repository(String name) {
833 4 : return project(name);
834 : }
835 :
836 : @Operator
837 : public Predicate<ChangeData> repositories(String name) {
838 4 : return projects(name);
839 : }
840 :
841 : @Operator
842 : public Predicate<ChangeData> parentrepository(String name) {
843 4 : return parentproject(name);
844 : }
845 :
846 : @Operator
847 : public Predicate<ChangeData> repo(String name) {
848 5 : return project(name);
849 : }
850 :
851 : @Operator
852 : public Predicate<ChangeData> repos(String name) {
853 4 : return projects(name);
854 : }
855 :
856 : @Operator
857 : public Predicate<ChangeData> parentrepo(String name) {
858 4 : return parentproject(name);
859 : }
860 :
861 : @Operator
862 : public Predicate<ChangeData> branch(String name) throws QueryParseException {
863 12 : if (name.startsWith("^")) {
864 0 : return ref("^" + RefNames.fullName(name.substring(1)));
865 : }
866 12 : return ref(RefNames.fullName(name));
867 : }
868 :
869 : @Operator
870 : public Predicate<ChangeData> hashtag(String hashtag) {
871 4 : return ChangePredicates.hashtag(hashtag);
872 : }
873 :
874 : @Operator
875 : public Predicate<ChangeData> inhashtag(String hashtag) throws QueryParseException {
876 4 : if (hashtag.startsWith("^")) {
877 4 : return new RegexHashtagPredicate(hashtag);
878 : }
879 4 : if (hashtag.isEmpty()) {
880 0 : return ChangePredicates.hashtag(hashtag);
881 : }
882 :
883 4 : checkFieldAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
884 4 : return ChangePredicates.fuzzyHashtag(hashtag);
885 : }
886 :
887 : @Operator
888 : public Predicate<ChangeData> prefixhashtag(String hashtag) throws QueryParseException {
889 4 : if (hashtag.isEmpty()) {
890 0 : return ChangePredicates.hashtag(hashtag);
891 : }
892 :
893 4 : checkFieldAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag");
894 4 : return ChangePredicates.prefixHashtag(hashtag);
895 : }
896 :
897 : @Operator
898 : public Predicate<ChangeData> topic(String name) {
899 12 : return ChangePredicates.exactTopic(name);
900 : }
901 :
902 : @Operator
903 : public Predicate<ChangeData> intopic(String name) {
904 4 : if (name.startsWith("^")) {
905 4 : return new RegexTopicPredicate(name);
906 : }
907 4 : if (name.isEmpty()) {
908 4 : return ChangePredicates.exactTopic(name);
909 : }
910 4 : return ChangePredicates.fuzzyTopic(name);
911 : }
912 :
913 : @Operator
914 : public Predicate<ChangeData> prefixtopic(String name) throws QueryParseException {
915 4 : if (name.isEmpty()) {
916 0 : return ChangePredicates.exactTopic(name);
917 : }
918 :
919 4 : checkFieldAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic");
920 4 : return ChangePredicates.prefixTopic(name);
921 : }
922 :
923 : @Operator
924 : public Predicate<ChangeData> ref(String ref) throws QueryParseException {
925 12 : if (ref.startsWith("^")) {
926 0 : return new RegexRefPredicate(ref);
927 : }
928 12 : return ChangePredicates.ref(ref);
929 : }
930 :
931 : @Operator
932 : public Predicate<ChangeData> f(String file) throws QueryParseException {
933 0 : return file(file);
934 : }
935 :
936 : /**
937 : * Creates a predicate to match changes by file.
938 : *
939 : * @param file the value of the {@code file} query operator
940 : * @throws QueryParseException thrown if parsing the value fails (may be thrown by subclasses)
941 : */
942 : @Operator
943 : public Predicate<ChangeData> file(String file) throws QueryParseException {
944 11 : if (file.startsWith("^")) {
945 5 : return new RegexPathPredicate(file);
946 : }
947 11 : return ChangePredicates.file(args, file);
948 : }
949 :
950 : @Operator
951 : public Predicate<ChangeData> path(String path) {
952 4 : if (path.startsWith("^")) {
953 4 : return new RegexPathPredicate(path);
954 : }
955 4 : return ChangePredicates.path(path);
956 : }
957 :
958 : @Operator
959 : public Predicate<ChangeData> ext(String ext) {
960 4 : return extension(ext);
961 : }
962 :
963 : @Operator
964 : public Predicate<ChangeData> extension(String ext) {
965 4 : return new FileExtensionPredicate(ext);
966 : }
967 :
968 : @Operator
969 : public Predicate<ChangeData> onlyexts(String extList) {
970 4 : return onlyextensions(extList);
971 : }
972 :
973 : @Operator
974 : public Predicate<ChangeData> onlyextensions(String extList) {
975 4 : return new FileExtensionListPredicate(extList);
976 : }
977 :
978 : @Operator
979 : public Predicate<ChangeData> footer(String footer) {
980 4 : return ChangePredicates.footer(footer);
981 : }
982 :
983 : @Operator
984 : public Predicate<ChangeData> hasfooter(String footerName) throws QueryParseException {
985 4 : checkFieldAvailable(ChangeField.FOOTER_NAME, "hasfooter");
986 4 : return ChangePredicates.hasFooter(footerName);
987 : }
988 :
989 : @Operator
990 : public Predicate<ChangeData> dir(String directory) {
991 5 : return directory(directory);
992 : }
993 :
994 : @Operator
995 : public Predicate<ChangeData> directory(String directory) {
996 5 : if (directory.startsWith("^")) {
997 4 : return new RegexDirectoryPredicate(directory);
998 : }
999 5 : return ChangePredicates.directory(directory);
1000 : }
1001 :
1002 : @Operator
1003 : public Predicate<ChangeData> label(String name)
1004 : throws QueryParseException, IOException, ConfigInvalidException {
1005 14 : Set<Account.Id> accounts = null;
1006 14 : AccountGroup.UUID group = null;
1007 14 : Integer count = null;
1008 14 : PredicateArgs.Operator countOp = null;
1009 :
1010 : // Parse for:
1011 : // label:Code-Review=1,user=jsmith or
1012 : // label:Code-Review=1,jsmith or
1013 : // label:Code-Review=1,group=android_approvers or
1014 : // label:Code-Review=1,android_approvers
1015 : // user/groups without a label will first attempt to match user
1016 : // Special case: votes by owners can be tracked with ",owner":
1017 : // label:Code-Review+2,owner
1018 : // label:Code-Review+2,user=owner
1019 : // label:Code-Review+1,count=2
1020 14 : List<String> splitReviewer = LABEL_SPLITTER.limit(2).splitToList(name);
1021 14 : name = splitReviewer.get(0); // remove all but the vote piece, e.g.'CodeReview=1'
1022 :
1023 14 : if (splitReviewer.size() == 2) {
1024 : // process the user/group piece
1025 5 : PredicateArgs lblArgs = new PredicateArgs(splitReviewer.get(1));
1026 :
1027 : // Disallow using the "count=" arg in conjunction with the "user=" or "group=" args. to avoid
1028 : // unnecessary complexity.
1029 5 : assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_USER);
1030 5 : assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_GROUP);
1031 :
1032 5 : for (Map.Entry<String, ValOp> pair : lblArgs.keyValue.entrySet()) {
1033 5 : String key = pair.getKey();
1034 5 : String value = pair.getValue().value();
1035 5 : PredicateArgs.Operator operator = pair.getValue().operator();
1036 5 : if (key.equalsIgnoreCase(ARG_ID_USER)) {
1037 5 : if (value.equals(ARG_ID_OWNER)) {
1038 4 : accounts = Collections.singleton(OWNER_ACCOUNT_ID);
1039 5 : } else if (value.equals(ARG_ID_NON_UPLOADER)) {
1040 5 : accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
1041 : } else {
1042 4 : accounts = parseAccount(value);
1043 : }
1044 4 : } else if (key.equalsIgnoreCase(ARG_ID_GROUP)) {
1045 4 : group = parseGroup(value).getUUID();
1046 4 : } else if (key.equalsIgnoreCase(ARG_COUNT)) {
1047 4 : if (!isInt(value)) {
1048 0 : throw new QueryParseException("Invalid count argument. Value should be an integer");
1049 : }
1050 4 : count = Integer.parseInt(value);
1051 4 : countOp = operator;
1052 4 : if (count == 0) {
1053 4 : throw new QueryParseException("Argument count=0 is not allowed.");
1054 : }
1055 4 : if (count > LabelPredicate.MAX_COUNT) {
1056 4 : throw new QueryParseException(
1057 4 : String.format(
1058 : "count=%d is not allowed. Maximum allowed value for count is %d.",
1059 4 : count, LabelPredicate.MAX_COUNT));
1060 : }
1061 : } else {
1062 0 : throw new QueryParseException("Invalid argument identifier '" + pair.getKey() + "'");
1063 : }
1064 5 : }
1065 :
1066 5 : for (String value : lblArgs.positional) {
1067 4 : if (accounts != null || group != null) {
1068 0 : throw new QueryParseException("more than one user/group specified (" + value + ")");
1069 : }
1070 : try {
1071 4 : if (value.equals(ARG_ID_OWNER)) {
1072 4 : accounts = Collections.singleton(OWNER_ACCOUNT_ID);
1073 4 : } else if (value.equals(ARG_ID_NON_UPLOADER)) {
1074 4 : accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
1075 : } else {
1076 4 : accounts = parseAccount(value);
1077 : }
1078 4 : } catch (QueryParseException qpex) {
1079 : // If it doesn't match an account, see if it matches a group
1080 : // (accounts get precedence)
1081 : try {
1082 4 : group = parseGroup(value).getUUID();
1083 0 : } catch (QueryParseException e) {
1084 0 : throw error("Neither user nor group " + value + " found", e);
1085 4 : }
1086 4 : }
1087 4 : }
1088 : }
1089 :
1090 14 : if (group != null) {
1091 4 : accounts = getMembers(group);
1092 : }
1093 :
1094 : // If the vote piece looks like Code-Review=NEED with a valid non-numeric
1095 : // submit record status, interpret as a submit record query.
1096 14 : int eq = name.indexOf('=');
1097 14 : if (eq > 0) {
1098 9 : String statusName = name.substring(eq + 1).toUpperCase();
1099 9 : if (!isInt(statusName) && !MagicLabelValue.tryParse(statusName).isPresent()) {
1100 5 : SubmitRecord.Label.Status status =
1101 5 : Enums.getIfPresent(SubmitRecord.Label.Status.class, statusName).orNull();
1102 5 : if (status == null) {
1103 0 : throw error("Invalid label status " + statusName + " in " + name);
1104 : }
1105 5 : return SubmitRecordPredicate.create(name.substring(0, eq), status, accounts);
1106 : }
1107 : }
1108 :
1109 14 : return new LabelPredicate(args, name, accounts, group, count, countOp);
1110 : }
1111 :
1112 : /** Assert that keys {@code k1} and {@code k2} do not exist in {@code labelArgs} together. */
1113 : private void assertDisjunctive(PredicateArgs labelArgs, String k1, String k2)
1114 : throws QueryParseException {
1115 5 : Map<String, ValOp> keyValArgs = labelArgs.keyValue;
1116 5 : if (keyValArgs.containsKey(k1) && keyValArgs.containsKey(k2)) {
1117 4 : throw new QueryParseException(
1118 4 : String.format(
1119 : "Cannot use the '%s' argument in conjunction with the '%s' argument", k1, k2));
1120 : }
1121 5 : }
1122 :
1123 : private static boolean isInt(String s) {
1124 9 : if (s == null) {
1125 0 : return false;
1126 : }
1127 9 : if (s.startsWith("+")) {
1128 7 : s = s.substring(1);
1129 : }
1130 9 : return Ints.tryParse(s) != null;
1131 : }
1132 :
1133 : @Operator
1134 : public Predicate<ChangeData> message(String text) throws QueryParseException {
1135 13 : if (text.startsWith("^")) {
1136 5 : checkFieldAvailable(ChangeField.COMMIT_MESSAGE_EXACT, "messageexact");
1137 4 : return new RegexMessagePredicate(text);
1138 : }
1139 13 : return ChangePredicates.message(text);
1140 : }
1141 :
1142 : private Predicate<ChangeData> starredBySelf() throws QueryParseException {
1143 4 : return ChangePredicates.starBy(
1144 4 : args.starredChangesUtil, self(), StarredChangesUtil.DEFAULT_LABEL);
1145 : }
1146 :
1147 : private Predicate<ChangeData> draftBySelf() throws QueryParseException {
1148 6 : return ChangePredicates.draftBy(args.commentsUtil, self());
1149 : }
1150 :
1151 : @Operator
1152 : public Predicate<ChangeData> visibleto(String who)
1153 : throws QueryParseException, IOException, ConfigInvalidException {
1154 4 : if (isSelf(who)) {
1155 4 : return isVisible();
1156 : }
1157 4 : Set<Account.Id> accounts = null;
1158 : try {
1159 4 : accounts = parseAccount(who);
1160 4 : } catch (QueryParseException e) {
1161 4 : if (e instanceof QueryRequiresAuthException) {
1162 0 : throw e;
1163 : }
1164 4 : }
1165 4 : if (accounts != null) {
1166 4 : if (accounts.size() == 1) {
1167 4 : return visibleto(args.userFactory.create(Iterables.getOnlyElement(accounts)));
1168 4 : } else if (accounts.size() > 1) {
1169 4 : throw error(String.format("\"%s\" resolves to multiple accounts", who));
1170 : }
1171 : }
1172 :
1173 : // If its not an account, maybe its a group?
1174 4 : Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
1175 4 : if (!suggestions.isEmpty()) {
1176 4 : HashSet<AccountGroup.UUID> ids = new HashSet<>();
1177 4 : for (GroupReference ref : suggestions) {
1178 4 : ids.add(ref.getUUID());
1179 4 : }
1180 4 : return visibleto(new GroupBackedUser(ids));
1181 : }
1182 :
1183 4 : throw error("No user or group matches \"" + who + "\".");
1184 : }
1185 :
1186 : public Predicate<ChangeData> visibleto(CurrentUser user) {
1187 12 : return args.changeIsVisbleToPredicateFactory.forUser(user);
1188 : }
1189 :
1190 : public Predicate<ChangeData> isVisible() throws QueryParseException {
1191 12 : return visibleto(args.getUser());
1192 : }
1193 :
1194 : @Operator
1195 : public Predicate<ChangeData> o(String who)
1196 : throws QueryParseException, IOException, ConfigInvalidException {
1197 0 : return owner(who);
1198 : }
1199 :
1200 : @Operator
1201 : public Predicate<ChangeData> owner(String who)
1202 : throws QueryParseException, IOException, ConfigInvalidException {
1203 10 : return owner(parseAccount(who, (AccountState s) -> true));
1204 : }
1205 :
1206 : private Predicate<ChangeData> owner(Set<Account.Id> who) {
1207 11 : List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
1208 11 : for (Account.Id id : who) {
1209 11 : p.add(ChangePredicates.owner(id));
1210 11 : }
1211 11 : return Predicate.or(p);
1212 : }
1213 :
1214 : private Predicate<ChangeData> ownerDefaultField(String who)
1215 : throws QueryParseException, IOException, ConfigInvalidException {
1216 5 : Set<Account.Id> accounts = parseAccount(who);
1217 5 : if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
1218 4 : return Predicate.any();
1219 : }
1220 5 : return owner(accounts);
1221 : }
1222 :
1223 : @Operator
1224 : public Predicate<ChangeData> uploader(String who)
1225 : throws QueryParseException, IOException, ConfigInvalidException {
1226 4 : checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploader");
1227 4 : return uploader(parseAccount(who, (AccountState s) -> true));
1228 : }
1229 :
1230 : private Predicate<ChangeData> uploader(Set<Account.Id> who) {
1231 4 : List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
1232 4 : for (Account.Id id : who) {
1233 4 : p.add(ChangePredicates.uploader(id));
1234 4 : }
1235 4 : return Predicate.or(p);
1236 : }
1237 :
1238 : @Operator
1239 : public Predicate<ChangeData> attention(String who)
1240 : throws QueryParseException, IOException, ConfigInvalidException {
1241 4 : checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
1242 4 : return attention(parseAccount(who, (AccountState s) -> true));
1243 : }
1244 :
1245 : private Predicate<ChangeData> attention(Set<Account.Id> who) {
1246 4 : return Predicate.or(who.stream().map(ChangePredicates::attentionSet).collect(toImmutableSet()));
1247 : }
1248 :
1249 : @Operator
1250 : public Predicate<ChangeData> assignee(String who)
1251 : throws QueryParseException, IOException, ConfigInvalidException {
1252 4 : return assignee(parseAccount(who, (AccountState s) -> true));
1253 : }
1254 :
1255 : private Predicate<ChangeData> assignee(Set<Account.Id> who) {
1256 4 : List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
1257 4 : for (Account.Id id : who) {
1258 4 : p.add(ChangePredicates.assignee(id));
1259 4 : }
1260 4 : return Predicate.or(p);
1261 : }
1262 :
1263 : @Operator
1264 : public Predicate<ChangeData> ownerin(String group) throws QueryParseException, IOException {
1265 5 : GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
1266 5 : if (g == null) {
1267 0 : throw error("Group " + group + " not found");
1268 : }
1269 :
1270 5 : AccountGroup.UUID groupId = g.getUUID();
1271 5 : GroupDescription.Basic groupDescription = args.groupBackend.get(groupId);
1272 5 : if (!(groupDescription instanceof GroupDescription.Internal)) {
1273 4 : return new OwnerinPredicate(args.userFactory, groupId);
1274 : }
1275 :
1276 5 : Set<Account.Id> accounts = getMembers(groupId);
1277 5 : List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(accounts.size());
1278 5 : for (Account.Id id : accounts) {
1279 4 : p.add(ChangePredicates.owner(id));
1280 4 : }
1281 5 : return Predicate.or(p);
1282 : }
1283 :
1284 : @Operator
1285 : public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
1286 4 : checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploaderin");
1287 :
1288 4 : GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
1289 4 : if (g == null) {
1290 0 : throw error("Group " + group + " not found");
1291 : }
1292 :
1293 4 : AccountGroup.UUID groupId = g.getUUID();
1294 4 : GroupDescription.Basic groupDescription = args.groupBackend.get(groupId);
1295 4 : if (!(groupDescription instanceof GroupDescription.Internal)) {
1296 0 : return new UploaderinPredicate(args.userFactory, groupId);
1297 : }
1298 :
1299 4 : Set<Account.Id> accounts = getMembers(groupId);
1300 4 : List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(accounts.size());
1301 4 : for (Account.Id id : accounts) {
1302 4 : p.add(ChangePredicates.uploader(id));
1303 4 : }
1304 4 : return Predicate.or(p);
1305 : }
1306 :
1307 : @Operator
1308 : public Predicate<ChangeData> r(String who)
1309 : throws QueryParseException, IOException, ConfigInvalidException {
1310 0 : return reviewer(who);
1311 : }
1312 :
1313 : @Operator
1314 : public Predicate<ChangeData> reviewer(String who)
1315 : throws QueryParseException, IOException, ConfigInvalidException {
1316 5 : return reviewer(who, false);
1317 : }
1318 :
1319 : private Predicate<ChangeData> reviewerDefaultField(String who)
1320 : throws QueryParseException, IOException, ConfigInvalidException {
1321 5 : return reviewer(who, true);
1322 : }
1323 :
1324 : private Predicate<ChangeData> reviewer(String who, boolean forDefaultField)
1325 : throws QueryParseException, IOException, ConfigInvalidException {
1326 10 : Predicate<ChangeData> byState =
1327 6 : reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
1328 6 : if (Objects.equals(byState, Predicate.<ChangeData>any())) {
1329 4 : return Predicate.any();
1330 : }
1331 6 : return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
1332 : }
1333 :
1334 : @Operator
1335 : public Predicate<ChangeData> cc(String who)
1336 : throws QueryParseException, IOException, ConfigInvalidException {
1337 4 : return reviewerByState(who, ReviewerStateInternal.CC, false);
1338 : }
1339 :
1340 : @Operator
1341 : public Predicate<ChangeData> reviewerin(String group) throws QueryParseException {
1342 4 : GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
1343 4 : if (g == null) {
1344 0 : throw error("Group " + group + " not found");
1345 : }
1346 4 : return new ReviewerinPredicate(args.userFactory, g.getUUID());
1347 : }
1348 :
1349 : @Operator
1350 : public Predicate<ChangeData> tr(String trackingId) {
1351 4 : return ChangePredicates.trackingId(trackingId);
1352 : }
1353 :
1354 : @Operator
1355 : public Predicate<ChangeData> bug(String trackingId) {
1356 4 : return tr(trackingId);
1357 : }
1358 :
1359 : @Operator
1360 : public Predicate<ChangeData> limit(String query) throws QueryParseException {
1361 6 : Integer limit = Ints.tryParse(query);
1362 6 : if (limit == null) {
1363 0 : throw error("Invalid limit: " + query);
1364 : }
1365 6 : return new LimitPredicate<>(FIELD_LIMIT, limit);
1366 : }
1367 :
1368 : @Operator
1369 : public Predicate<ChangeData> added(String value) throws QueryParseException {
1370 4 : return new AddedPredicate(value);
1371 : }
1372 :
1373 : @Operator
1374 : public Predicate<ChangeData> deleted(String value) throws QueryParseException {
1375 4 : return new DeletedPredicate(value);
1376 : }
1377 :
1378 : @Operator
1379 : public Predicate<ChangeData> size(String value) throws QueryParseException {
1380 4 : return delta(value);
1381 : }
1382 :
1383 : @Operator
1384 : public Predicate<ChangeData> delta(String value) throws QueryParseException {
1385 4 : return new DeltaPredicate(value);
1386 : }
1387 :
1388 : @Operator
1389 : public Predicate<ChangeData> commentby(String who)
1390 : throws QueryParseException, IOException, ConfigInvalidException {
1391 4 : return commentby(parseAccount(who));
1392 : }
1393 :
1394 : private Predicate<ChangeData> commentby(Set<Account.Id> who) {
1395 4 : List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
1396 4 : for (Account.Id id : who) {
1397 4 : p.add(ChangePredicates.commentBy(id));
1398 4 : }
1399 4 : return Predicate.or(p);
1400 : }
1401 :
1402 : @Operator
1403 : public Predicate<ChangeData> from(String who)
1404 : throws QueryParseException, IOException, ConfigInvalidException {
1405 4 : Set<Account.Id> ownerIds = parseAccount(who);
1406 4 : return Predicate.or(owner(ownerIds), commentby(ownerIds));
1407 : }
1408 :
1409 : @Operator
1410 : public Predicate<ChangeData> query(String value) throws QueryParseException {
1411 : // [name=]<name>[,user=<user>] || [user=<user>,][name=]<name>
1412 4 : PredicateArgs inputArgs = new PredicateArgs(value);
1413 4 : String name = null;
1414 4 : Account.Id account = null;
1415 :
1416 4 : try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
1417 : // [name=]<name>
1418 4 : if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
1419 4 : name = inputArgs.keyValue.get(ARG_ID_NAME).value();
1420 4 : } else if (inputArgs.positional.size() == 1) {
1421 4 : name = Iterables.getOnlyElement(inputArgs.positional);
1422 0 : } else if (inputArgs.positional.size() > 1) {
1423 0 : throw new QueryParseException("Error parsing named query: " + value);
1424 : }
1425 :
1426 : // [,user=<user>]
1427 4 : if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
1428 4 : Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
1429 4 : if (accounts != null && accounts.size() > 1) {
1430 0 : throw error(
1431 0 : String.format(
1432 0 : "\"%s\" resolves to multiple accounts", inputArgs.keyValue.get(ARG_ID_USER)));
1433 : }
1434 4 : account = (accounts == null ? self() : Iterables.getOnlyElement(accounts));
1435 4 : } else {
1436 4 : account = self();
1437 : }
1438 :
1439 4 : VersionedAccountQueries q = VersionedAccountQueries.forUser(account);
1440 4 : q.load(args.allUsersName, git);
1441 4 : String query = q.getQueryList().getQuery(name);
1442 4 : if (query != null) {
1443 4 : return parse(query);
1444 : }
1445 4 : } catch (RepositoryNotFoundException e) {
1446 0 : throw new QueryParseException(
1447 : "Unknown named query (no " + args.allUsersName + " repo): " + name, e);
1448 0 : } catch (IOException | ConfigInvalidException e) {
1449 0 : throw new QueryParseException("Error parsing named query: " + value, e);
1450 4 : }
1451 4 : throw new QueryParseException("Unknown named query: " + name);
1452 : }
1453 :
1454 : @Operator
1455 : public Predicate<ChangeData> reviewedby(String who)
1456 : throws QueryParseException, IOException, ConfigInvalidException {
1457 4 : return ChangePredicates.reviewedBy(parseAccount(who));
1458 : }
1459 :
1460 : @Operator
1461 : public Predicate<ChangeData> destination(String value) throws QueryParseException {
1462 : // [name=]<name>[,user=<user>] || [user=<user>,][name=]<name>
1463 4 : PredicateArgs inputArgs = new PredicateArgs(value);
1464 4 : String name = null;
1465 4 : Account.Id account = null;
1466 :
1467 4 : try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
1468 : // [name=]<name>
1469 4 : if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
1470 4 : name = inputArgs.keyValue.get(ARG_ID_NAME).value();
1471 4 : } else if (inputArgs.positional.size() == 1) {
1472 4 : name = Iterables.getOnlyElement(inputArgs.positional);
1473 0 : } else if (inputArgs.positional.size() > 1) {
1474 0 : throw new QueryParseException("Error parsing named destination: " + value);
1475 : }
1476 :
1477 : // [,user=<user>]
1478 4 : if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
1479 4 : Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
1480 4 : if (accounts != null && accounts.size() > 1) {
1481 0 : throw error(
1482 0 : String.format(
1483 0 : "\"%s\" resolves to multiple accounts", inputArgs.keyValue.get(ARG_ID_USER)));
1484 : }
1485 4 : account = (accounts == null ? self() : Iterables.getOnlyElement(accounts));
1486 4 : } else {
1487 4 : account = self();
1488 : }
1489 :
1490 4 : Set<BranchNameKey> destinations = getDestinationList(git, account).getDestinations(name);
1491 4 : if (destinations != null && !destinations.isEmpty()) {
1492 4 : return new DestinationPredicate(destinations, value);
1493 : }
1494 4 : } catch (RepositoryNotFoundException e) {
1495 0 : throw new QueryParseException(
1496 : "Unknown named destination (no " + args.allUsersName + " repo): " + name, e);
1497 0 : } catch (IOException | ConfigInvalidException e) {
1498 0 : throw new QueryParseException("Error parsing named destination: " + value, e);
1499 4 : }
1500 4 : throw new QueryParseException("Unknown named destination: " + name);
1501 : }
1502 :
1503 : protected DestinationList getDestinationList(Repository git, Account.Id account)
1504 : throws ConfigInvalidException, RepositoryNotFoundException, IOException {
1505 4 : DestinationList dl = destinationListByAccount.get(account);
1506 4 : if (dl == null) {
1507 4 : dl = loadDestinationList(git, account);
1508 4 : destinationListByAccount.put(account, dl);
1509 : }
1510 4 : return dl;
1511 : }
1512 :
1513 : protected DestinationList loadDestinationList(Repository git, Account.Id account)
1514 : throws ConfigInvalidException, RepositoryNotFoundException, IOException {
1515 4 : VersionedAccountDestinations d = VersionedAccountDestinations.forUser(account);
1516 4 : d.load(args.allUsersName, git);
1517 4 : return d.getDestinationList();
1518 : }
1519 :
1520 : @Operator
1521 : public Predicate<ChangeData> author(String who) throws QueryParseException {
1522 4 : return getAuthorOrCommitterPredicate(
1523 4 : who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
1524 : }
1525 :
1526 : @Operator
1527 : public Predicate<ChangeData> committer(String who) throws QueryParseException {
1528 4 : return getAuthorOrCommitterPredicate(
1529 4 : who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
1530 : }
1531 :
1532 : @Operator
1533 : public Predicate<ChangeData> unresolved(String value) throws QueryParseException {
1534 4 : return new IsUnresolvedPredicate(value);
1535 : }
1536 :
1537 : @Operator
1538 : public Predicate<ChangeData> revertof(String value) throws QueryParseException {
1539 4 : if (value == null || Ints.tryParse(value) == null) {
1540 0 : throw new QueryParseException("'revertof' must be an integer");
1541 : }
1542 4 : return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
1543 : }
1544 :
1545 : @Operator
1546 : public Predicate<ChangeData> submissionId(String value) {
1547 4 : return ChangePredicates.submissionId(value);
1548 : }
1549 :
1550 : @Operator
1551 : public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
1552 0 : checkFieldAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
1553 0 : checkFieldAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
1554 0 : if (Ints.tryParse(value) != null) {
1555 0 : return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
1556 : }
1557 : try {
1558 0 : PatchSet.Id patchSetId = PatchSet.Id.parse(value);
1559 0 : return ChangePredicates.cherryPickOf(patchSetId);
1560 0 : } catch (IllegalArgumentException e) {
1561 0 : throw new QueryParseException(
1562 : "'"
1563 : + value
1564 : + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
1565 : e);
1566 : }
1567 : }
1568 :
1569 : @Override
1570 : protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
1571 16 : if (query.startsWith("refs/")) {
1572 4 : return ref(query);
1573 16 : } else if (DEF_CHANGE.matcher(query).matches()) {
1574 14 : List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(2);
1575 : try {
1576 14 : predicates.add(change(query));
1577 0 : } catch (QueryParseException e) {
1578 : // Skip.
1579 14 : }
1580 :
1581 : // For PAT_LEGACY_ID, it may also be the prefix of some commits.
1582 14 : if (query.length() >= 6 && PAT_LEGACY_ID.matcher(query).matches()) {
1583 0 : predicates.add(commit(query));
1584 : }
1585 :
1586 14 : return Predicate.or(predicates);
1587 : }
1588 :
1589 : // Adapt the capacity of this list when adding more default predicates.
1590 10 : List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11);
1591 : try {
1592 5 : Predicate<ChangeData> p = ownerDefaultField(query);
1593 5 : if (!Objects.equals(p, Predicate.<ChangeData>any())) {
1594 5 : predicates.add(p);
1595 : }
1596 10 : } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
1597 : // Skip.
1598 5 : }
1599 : try {
1600 5 : Predicate<ChangeData> p = reviewerDefaultField(query);
1601 5 : if (!Objects.equals(p, Predicate.<ChangeData>any())) {
1602 5 : predicates.add(p);
1603 : }
1604 10 : } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
1605 : // Skip.
1606 5 : }
1607 10 : predicates.add(file(query));
1608 : try {
1609 10 : predicates.add(label(query));
1610 0 : } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
1611 : // Skip.
1612 10 : }
1613 10 : predicates.add(commit(query));
1614 10 : predicates.add(message(query));
1615 10 : predicates.add(comment(query));
1616 10 : predicates.add(projects(query));
1617 10 : predicates.add(ref(query));
1618 10 : predicates.add(branch(query));
1619 10 : predicates.add(topic(query));
1620 : // Adapt the capacity of the "predicates" list when adding more default
1621 : // predicates.
1622 10 : return Predicate.or(predicates);
1623 : }
1624 :
1625 : protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String operator)
1626 : throws QueryParseException {
1627 6 : if (!args.index.getSchema().hasField(field)) {
1628 0 : throw new QueryParseException(
1629 0 : String.format("'%s' operator is not supported by change index version", operator));
1630 : }
1631 6 : }
1632 :
1633 : private Predicate<ChangeData> getAuthorOrCommitterPredicate(
1634 : String who,
1635 : Function<String, Predicate<ChangeData>> exactPredicateFunc,
1636 : Function<String, Predicate<ChangeData>> fullPredicateFunc)
1637 : throws QueryParseException {
1638 4 : if (Address.tryParse(who) != null) {
1639 4 : return exactPredicateFunc.apply(who);
1640 : }
1641 4 : return getAuthorOrCommitterFullTextPredicate(who, fullPredicateFunc);
1642 : }
1643 :
1644 : private Predicate<ChangeData> getAuthorOrCommitterFullTextPredicate(
1645 : String who, Function<String, Predicate<ChangeData>> fullPredicateFunc)
1646 : throws QueryParseException {
1647 4 : if (isSelf(who)) {
1648 4 : IdentifiedUser me = args.getIdentifiedUser();
1649 4 : List<Predicate<ChangeData>> predicates =
1650 4 : me.getEmailAddresses().stream().map(fullPredicateFunc).collect(toList());
1651 4 : return Predicate.or(predicates);
1652 : }
1653 4 : Set<String> parts = SchemaUtil.getNameParts(who);
1654 4 : if (parts.isEmpty()) {
1655 4 : throw error("invalid value");
1656 : }
1657 :
1658 4 : List<Predicate<ChangeData>> predicates =
1659 4 : parts.stream().map(fullPredicateFunc).collect(toList());
1660 4 : return Predicate.and(predicates);
1661 : }
1662 :
1663 : private Set<Account.Id> getMembers(AccountGroup.UUID g) throws IOException {
1664 : Set<Account.Id> accounts;
1665 5 : Set<Account.Id> allMembers =
1666 5 : args.groupMembers.listAccounts(g).stream().map(Account::id).collect(toSet());
1667 5 : int maxTerms = args.indexConfig.maxTerms();
1668 5 : if (allMembers.size() > maxTerms) {
1669 : // limit the number of query terms otherwise Gerrit will barf
1670 0 : accounts = allMembers.stream().limit(maxTerms).collect(toSet());
1671 : } else {
1672 5 : accounts = allMembers;
1673 : }
1674 5 : return accounts;
1675 : }
1676 :
1677 : private Set<Account.Id> parseAccount(String who)
1678 : throws QueryParseException, IOException, ConfigInvalidException {
1679 : try {
1680 6 : return args.accountResolver.resolve(who).asNonEmptyIdSet();
1681 10 : } catch (UnresolvableAccountException e) {
1682 10 : if (e.isSelf()) {
1683 0 : throw new QueryRequiresAuthException(e.getMessage(), e);
1684 : }
1685 10 : throw new QueryParseException(e.getMessage(), e);
1686 : }
1687 : }
1688 :
1689 : private Set<Account.Id> parseAccount(
1690 : String who, java.util.function.Predicate<AccountState> activityFilter)
1691 : throws QueryParseException, IOException, ConfigInvalidException {
1692 : try {
1693 10 : return args.accountResolver.resolve(who, activityFilter).asNonEmptyIdSet();
1694 4 : } catch (UnresolvableAccountException e) {
1695 4 : if (e.isSelf()) {
1696 4 : throw new QueryRequiresAuthException(e.getMessage(), e);
1697 : }
1698 0 : throw new QueryParseException(e.getMessage(), e);
1699 : }
1700 : }
1701 :
1702 : private GroupReference parseGroup(String group) throws QueryParseException {
1703 4 : GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
1704 4 : if (g == null) {
1705 0 : throw error("Group " + group + " not found");
1706 : }
1707 4 : return g;
1708 : }
1709 :
1710 : private List<Change> parseChange(String value) throws QueryParseException {
1711 4 : return asChanges(parseChangeData(value));
1712 : }
1713 :
1714 : private List<ChangeData> parseChangeData(String value) throws QueryParseException {
1715 4 : if (PAT_LEGACY_ID.matcher(value).matches()) {
1716 4 : Optional<Change.Id> id = Change.Id.tryParse(value);
1717 4 : if (!id.isPresent()) {
1718 0 : throw error("Invalid change id " + value);
1719 : }
1720 4 : return args.queryProvider.get().byLegacyChangeId(id.get());
1721 4 : } else if (PAT_CHANGE_ID.matcher(value).matches()) {
1722 4 : List<ChangeData> changes = args.queryProvider.get().byKeyPrefix(parseChangeId(value));
1723 4 : if (changes.isEmpty()) {
1724 0 : throw error("Change " + value + " not found");
1725 : }
1726 4 : return changes;
1727 : }
1728 :
1729 0 : throw error("Change " + value + " not found");
1730 : }
1731 :
1732 : private static String parseChangeId(String value) {
1733 16 : if (value.charAt(0) == 'i') {
1734 0 : value = "I" + value.substring(1);
1735 : }
1736 16 : return value;
1737 : }
1738 :
1739 : private Account.Id self() throws QueryParseException {
1740 10 : return args.getIdentifiedUser().getAccountId();
1741 : }
1742 :
1743 : public Predicate<ChangeData> reviewerByState(
1744 : String who, ReviewerStateInternal state, boolean forDefaultField)
1745 : throws QueryParseException, IOException, ConfigInvalidException {
1746 10 : Predicate<ChangeData> reviewerByEmailPredicate = null;
1747 10 : Address address = Address.tryParse(who);
1748 10 : if (address != null) {
1749 6 : reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
1750 : }
1751 :
1752 10 : Predicate<ChangeData> reviewerPredicate = null;
1753 : try {
1754 6 : Set<Account.Id> accounts = parseAccount(who);
1755 6 : if (!forDefaultField || accounts.size() <= MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
1756 6 : reviewerPredicate =
1757 6 : Predicate.or(
1758 6 : accounts.stream()
1759 6 : .map(id -> ReviewerPredicate.forState(id, state))
1760 6 : .collect(toList()));
1761 : } else {
1762 4 : logger.atFine().log(
1763 : "Skipping reviewer predicate for %s in default field query"
1764 : + " because the number of matched accounts (%d) exceeds the limit of %d",
1765 4 : who, accounts.size(), MAX_ACCOUNTS_PER_DEFAULT_FIELD);
1766 : }
1767 10 : } catch (QueryParseException e) {
1768 10 : logger.atFine().log("Parsing %s as account failed: %s", who, e.getMessage());
1769 : // Propagate this exception only if we can't use 'who' to query by email
1770 10 : if (reviewerByEmailPredicate == null) {
1771 10 : throw e;
1772 : }
1773 6 : }
1774 :
1775 6 : if (reviewerPredicate != null && reviewerByEmailPredicate != null) {
1776 6 : return Predicate.or(reviewerPredicate, reviewerByEmailPredicate);
1777 4 : } else if (reviewerPredicate != null) {
1778 4 : return reviewerPredicate;
1779 4 : } else if (reviewerByEmailPredicate != null) {
1780 4 : return reviewerByEmailPredicate;
1781 : } else {
1782 4 : return Predicate.any();
1783 : }
1784 : }
1785 : }
|