| // Copyright (C) 2014 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.base.Preconditions.checkArgument; |
| import static com.google.gerrit.index.query.Predicate.and; |
| import static com.google.gerrit.index.query.Predicate.not; |
| import static com.google.gerrit.index.query.Predicate.or; |
| import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.index.IndexConfig; |
| import com.google.gerrit.index.query.InternalQuery; |
| import com.google.gerrit.index.query.Predicate; |
| import com.google.gerrit.server.index.change.ChangeIndexCollection; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.Supplier; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| |
| /** |
| * Query wrapper for the change index. |
| * |
| * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than |
| * holding on to a single instance. |
| */ |
| public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChangeQuery> { |
| @FunctionalInterface |
| static interface ChangeIdPredicateFactory { |
| Predicate<ChangeData> create(Change.Id id); |
| } |
| |
| private static Predicate<ChangeData> ref(BranchNameKey branch) { |
| return new RefPredicate(branch.branch()); |
| } |
| |
| private static Predicate<ChangeData> change(Change.Key key) { |
| return new ChangeIdPredicate(key.get()); |
| } |
| |
| private static Predicate<ChangeData> project(Project.NameKey project) { |
| return new ProjectPredicate(project.get()); |
| } |
| |
| private static Predicate<ChangeData> status(Change.Status status) { |
| return ChangeStatusPredicate.forStatus(status); |
| } |
| |
| private static Predicate<ChangeData> commit(String id) { |
| return new CommitPredicate(id); |
| } |
| |
| private final ChangeData.Factory changeDataFactory; |
| private final ChangeNotes.Factory notesFactory; |
| |
| // TODO(davido): Remove the below fields when support for legacy numeric fields is removed. |
| private final ChangeIdPredicateFactory predicateFactory; |
| |
| @Inject |
| InternalChangeQuery( |
| ChangeQueryProcessor queryProcessor, |
| ChangeIndexCollection indexes, |
| IndexConfig indexConfig, |
| ChangeData.Factory changeDataFactory, |
| ChangeNotes.Factory notesFactory) { |
| super(queryProcessor, indexes, indexConfig); |
| this.changeDataFactory = changeDataFactory; |
| this.notesFactory = notesFactory; |
| predicateFactory = |
| (id) -> |
| schema().useLegacyNumericFields() |
| ? new LegacyChangeIdPredicate(id) |
| : new LegacyChangeIdStrPredicate(id); |
| } |
| |
| public List<ChangeData> byKey(Change.Key key) { |
| return byKeyPrefix(key.get()); |
| } |
| |
| public List<ChangeData> byKeyPrefix(String prefix) { |
| return query(new ChangeIdPredicate(prefix)); |
| } |
| |
| public List<ChangeData> byLegacyChangeId(Change.Id id) { |
| return query(predicateFactory.create(id)); |
| } |
| |
| public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) { |
| List<Predicate<ChangeData>> preds = new ArrayList<>(ids.size()); |
| for (Change.Id id : ids) { |
| preds.add(predicateFactory.create(id)); |
| } |
| return query(or(preds)); |
| } |
| |
| public List<ChangeData> byBranchKey(BranchNameKey branch, Change.Key key) { |
| return query(byBranchKeyPred(branch, key)); |
| } |
| |
| public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key) { |
| return query(and(byBranchKeyPred(BranchNameKey.create(project, branch), key), open())); |
| } |
| |
| public static Predicate<ChangeData> byBranchKeyOpenPred( |
| Project.NameKey project, String branch, Change.Key key) { |
| return and(byBranchKeyPred(BranchNameKey.create(project, branch), key), open()); |
| } |
| |
| private static Predicate<ChangeData> byBranchKeyPred(BranchNameKey branch, Change.Key key) { |
| return and(ref(branch), project(branch.project()), change(key)); |
| } |
| |
| public List<ChangeData> byProject(Project.NameKey project) { |
| return query(project(project)); |
| } |
| |
| public List<ChangeData> byBranchOpen(BranchNameKey branch) { |
| return query(and(ref(branch), project(branch.project()), open())); |
| } |
| |
| public List<ChangeData> byBranchNew(BranchNameKey branch) { |
| return query(and(ref(branch), project(branch.project()), status(Change.Status.NEW))); |
| } |
| |
| public Iterable<ChangeData> byCommitsOnBranchNotMerged( |
| Repository repo, BranchNameKey branch, Collection<String> hashes) throws IOException { |
| return byCommitsOnBranchNotMerged( |
| repo, |
| branch, |
| hashes, |
| // Account for all commit predicates plus ref, project, status. |
| indexConfig.maxTerms() - 3); |
| } |
| |
| @VisibleForTesting |
| Iterable<ChangeData> byCommitsOnBranchNotMerged( |
| Repository repo, BranchNameKey branch, Collection<String> hashes, int indexLimit) |
| throws IOException { |
| if (hashes.size() > indexLimit) { |
| return byCommitsOnBranchNotMergedFromDatabase(repo, branch, hashes); |
| } |
| return byCommitsOnBranchNotMergedFromIndex(branch, hashes); |
| } |
| |
| private Iterable<ChangeData> byCommitsOnBranchNotMergedFromDatabase( |
| Repository repo, BranchNameKey branch, Collection<String> hashes) throws IOException { |
| Set<Change.Id> changeIds = Sets.newHashSetWithExpectedSize(hashes.size()); |
| String lastPrefix = null; |
| for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) { |
| String r = ref.getName(); |
| if ((lastPrefix != null && r.startsWith(lastPrefix)) |
| || !hashes.contains(ref.getObjectId().name())) { |
| continue; |
| } |
| Change.Id id = Change.Id.fromRef(r); |
| if (id == null) { |
| continue; |
| } |
| if (changeIds.add(id)) { |
| lastPrefix = r.substring(0, r.lastIndexOf('/')); |
| } |
| } |
| |
| List<ChangeNotes> notes = |
| notesFactory.create( |
| branch.project(), |
| changeIds, |
| cn -> { |
| Change c = cn.getChange(); |
| return c.getDest().equals(branch) && !c.isMerged(); |
| }); |
| return Lists.transform(notes, n -> changeDataFactory.create(n)); |
| } |
| |
| private Iterable<ChangeData> byCommitsOnBranchNotMergedFromIndex( |
| BranchNameKey branch, Collection<String> hashes) { |
| return query( |
| and( |
| ref(branch), |
| project(branch.project()), |
| not(status(Change.Status.MERGED)), |
| or(commits(hashes)))); |
| } |
| |
| private static List<Predicate<ChangeData>> commits(Collection<String> hashes) { |
| List<Predicate<ChangeData>> commits = new ArrayList<>(hashes.size()); |
| for (String s : hashes) { |
| commits.add(commit(s)); |
| } |
| return commits; |
| } |
| |
| public List<ChangeData> byProjectOpen(Project.NameKey project) { |
| return query(and(project(project), open())); |
| } |
| |
| public List<ChangeData> byTopicOpen(String topic) { |
| return query(and(new ExactTopicPredicate(topic), open())); |
| } |
| |
| public List<ChangeData> byCommit(ObjectId id) { |
| return byCommit(id.name()); |
| } |
| |
| public List<ChangeData> byCommit(String hash) { |
| return query(commit(hash)); |
| } |
| |
| public List<ChangeData> byProjectCommit(Project.NameKey project, ObjectId id) { |
| return byProjectCommit(project, id.name()); |
| } |
| |
| public List<ChangeData> byProjectCommit(Project.NameKey project, String hash) { |
| return query(and(project(project), commit(hash))); |
| } |
| |
| public List<ChangeData> byProjectCommits(Project.NameKey project, List<String> hashes) { |
| int n = indexConfig.maxTerms() - 1; |
| checkArgument(hashes.size() <= n, "cannot exceed %s commits", n); |
| return query(and(project(project), or(commits(hashes)))); |
| } |
| |
| public List<ChangeData> byBranchCommit(String project, String branch, String hash) { |
| return query(byBranchCommitPred(project, branch, hash)); |
| } |
| |
| public List<ChangeData> byBranchCommit(BranchNameKey branch, String hash) { |
| return byBranchCommit(branch.project().get(), branch.branch(), hash); |
| } |
| |
| public List<ChangeData> byBranchCommitOpen(String project, String branch, String hash) { |
| return query(and(byBranchCommitPred(project, branch, hash), open())); |
| } |
| |
| public static Predicate<ChangeData> byBranchCommitOpenPred( |
| Project.NameKey project, String branch, String hash) { |
| return and(byBranchCommitPred(project.get(), branch, hash), open()); |
| } |
| |
| private static Predicate<ChangeData> byBranchCommitPred( |
| String project, String branch, String hash) { |
| return and(new ProjectPredicate(project), new RefPredicate(branch), commit(hash)); |
| } |
| |
| public List<ChangeData> bySubmissionId(String cs) { |
| if (Strings.isNullOrEmpty(cs)) { |
| return Collections.emptyList(); |
| } |
| return query(new SubmissionIdPredicate(cs)); |
| } |
| |
| private static Predicate<ChangeData> byProjectGroupsPredicate( |
| IndexConfig indexConfig, Project.NameKey project, Collection<String> groups) { |
| int n = indexConfig.maxTerms() - 1; |
| checkArgument(groups.size() <= n, "cannot exceed %s groups", n); |
| List<GroupPredicate> groupPredicates = new ArrayList<>(groups.size()); |
| for (String g : groups) { |
| groupPredicates.add(new GroupPredicate(g)); |
| } |
| return and(project(project), or(groupPredicates)); |
| } |
| |
| public static List<ChangeData> byProjectGroups( |
| Provider<InternalChangeQuery> queryProvider, |
| IndexConfig indexConfig, |
| Project.NameKey project, |
| Collection<String> groups) { |
| // These queries may be complex along multiple dimensions: |
| // * Many groups per change, if there are very many patch sets. This requires partitioning the |
| // list of predicates and combining results. |
| // * Many changes with the same set of groups, if the relation chain is very long. This |
| // requires querying exhaustively with pagination. |
| // For both cases, we need to invoke the queryProvider multiple times, since each |
| // InternalChangeQuery is single-use. |
| |
| Supplier<InternalChangeQuery> querySupplier = () -> queryProvider.get().enforceVisibility(true); |
| int batchSize = indexConfig.maxTerms() - 1; |
| if (groups.size() <= batchSize) { |
| return queryExhaustively( |
| querySupplier, byProjectGroupsPredicate(indexConfig, project, groups)); |
| } |
| Set<Change.Id> seen = new HashSet<>(); |
| List<ChangeData> result = new ArrayList<>(); |
| for (List<String> part : Iterables.partition(groups, batchSize)) { |
| for (ChangeData cd : |
| queryExhaustively(querySupplier, byProjectGroupsPredicate(indexConfig, project, part))) { |
| if (!seen.add(cd.getId())) { |
| result.add(cd); |
| } |
| } |
| } |
| return result; |
| } |
| } |