| // Copyright (C) 2021 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.change; | |
| import static com.google.common.base.Preconditions.checkArgument; | |
| import static java.util.stream.Collectors.toSet; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.index.IndexConfig; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.query.change.ChangeData; | |
| import com.google.gerrit.server.query.change.InternalChangeQuery; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| import com.google.inject.Singleton; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Set; | |
| /** Utility class that gets the ancestor changes and the descendent changes of a specific change. */ | |
| @Singleton | |
| public class GetRelatedChangesUtil { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| private final Provider<InternalChangeQuery> queryProvider; | |
| private final RelatedChangesSorter sorter; | |
| private final IndexConfig indexConfig; | |
| private final ChangeData.Factory changeDataFactory; | |
| @Inject | |
| GetRelatedChangesUtil( | |
| Provider<InternalChangeQuery> queryProvider, | |
| RelatedChangesSorter sorter, | |
| IndexConfig indexConfig, | |
| ChangeData.Factory changeDataFactory) { | |
| this.queryProvider = queryProvider; | |
| this.sorter = sorter; | |
| this.indexConfig = indexConfig; | |
| this.changeDataFactory = changeDataFactory; | |
| } | |
| /** | |
| * Gets related changes of a specific change revision. | |
| * | |
| * @param changeData the change of the inputted revision. | |
| * @param basePs the revision that the method checks for related changes. | |
| * @return list of related changes, sorted via {@link RelatedChangesSorter} | |
| */ | |
| public List<RelatedChangesSorter.PatchSetData> getRelated(ChangeData changeData, PatchSet basePs) | |
| throws IOException, PermissionBackendException { | |
| List<ChangeData> cds = getUnsortedRelated(changeData, basePs, false); | |
| if (cds.isEmpty()) { | |
| return Collections.emptyList(); | |
| } | |
| return sorter.sort(cds, basePs); | |
| } | |
| /** | |
| * Gets ancestor changes of a specific change revision. | |
| * | |
| * @param changeData the change of the inputted revision. | |
| * @param basePs the revision that the method checks for related changes. | |
| * @param alwaysIncludeOriginalChange whether to return the given change when no ancestors found. | |
| * @return list of ancestor changes, sorted via {@link RelatedChangesSorter} | |
| */ | |
| public List<RelatedChangesSorter.PatchSetData> getAncestors( | |
| ChangeData changeData, PatchSet basePs, boolean alwaysIncludeOriginalChange) | |
| throws IOException, PermissionBackendException { | |
| List<ChangeData> cds = getUnsortedRelated(changeData, basePs, alwaysIncludeOriginalChange); | |
| if (cds.isEmpty()) { | |
| return Collections.emptyList(); | |
| } | |
| return sorter.sortAncestors(cds, basePs); | |
| } | |
| private List<ChangeData> getUnsortedRelated( | |
| ChangeData changeData, PatchSet basePs, boolean alwaysIncludeOriginalChange) { | |
| Set<String> groups = getAllGroups(changeData.patchSets()); | |
| logger.atFine().log("groups = %s", groups); | |
| if (groups.isEmpty()) { | |
| return Collections.emptyList(); | |
| } | |
| List<ChangeData> cds = | |
| InternalChangeQuery.byProjectGroups( | |
| queryProvider, indexConfig, changeData.project(), groups); | |
| if (cds.isEmpty()) { | |
| return Collections.emptyList(); | |
| } | |
| if (cds.size() == 1 && cds.get(0).getId().equals(changeData.getId())) { | |
| return alwaysIncludeOriginalChange ? cds : Collections.emptyList(); | |
| } | |
| return reloadChangeIfStale(cds, changeData, basePs); | |
| } | |
| private List<ChangeData> reloadChangeIfStale( | |
| List<ChangeData> changeDatasFromIndex, ChangeData wantedChange, PatchSet wantedPs) { | |
| checkArgument( | |
| wantedChange.getId().equals(wantedPs.id().changeId()), | |
| "change of wantedPs (%s) doesn't match wantedChange (%s)", | |
| wantedPs.id().changeId(), | |
| wantedChange.getId()); | |
| List<ChangeData> changeDatas = new ArrayList<>(changeDatasFromIndex.size() + 1); | |
| changeDatas.addAll(changeDatasFromIndex); | |
| // Reload the change in case the patch set is absent. | |
| changeDatas.stream() | |
| .filter( | |
| cd -> cd.getId().equals(wantedPs.id().changeId()) && cd.patchSet(wantedPs.id()) == null) | |
| .forEach(ChangeData::reloadChange); | |
| if (changeDatas.stream().noneMatch(cd -> cd.getId().equals(wantedPs.id().changeId()))) { | |
| // The change of the wanted patch set is missing in the result from the index. | |
| // Load it from NoteDb and add it to the result. | |
| changeDatas.add(changeDataFactory.create(wantedChange.change())); | |
| } | |
| return changeDatas; | |
| } | |
| @VisibleForTesting | |
| public static Set<String> getAllGroups(Collection<PatchSet> patchSets) { | |
| return patchSets.stream().flatMap(ps -> ps.groups().stream()).collect(toSet()); | |
| } | |
| } |