LCOV - code coverage report
Current view: top level - server/change - RelatedChangesSorter.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 105 117 89.7 %
Date: 2022-11-19 15:00:39 Functions: 13 17 76.5 %

          Line data    Source code
       1             : // Copyright (C) 2015 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             : package com.google.gerrit.server.change;
      15             : 
      16             : import static com.google.common.base.Preconditions.checkArgument;
      17             : import static java.util.Objects.requireNonNull;
      18             : import static java.util.stream.Collectors.toMap;
      19             : 
      20             : import com.google.auto.value.AutoValue;
      21             : import com.google.auto.value.extension.memoized.Memoized;
      22             : import com.google.common.annotations.VisibleForTesting;
      23             : import com.google.common.collect.ImmutableList;
      24             : import com.google.common.collect.ListMultimap;
      25             : import com.google.common.collect.Lists;
      26             : import com.google.common.collect.Maps;
      27             : import com.google.common.collect.MultimapBuilder;
      28             : import com.google.gerrit.entities.Change;
      29             : import com.google.gerrit.entities.PatchSet;
      30             : import com.google.gerrit.entities.Project;
      31             : import com.google.gerrit.server.git.GitRepositoryManager;
      32             : import com.google.gerrit.server.permissions.ChangePermission;
      33             : import com.google.gerrit.server.permissions.PermissionBackend;
      34             : import com.google.gerrit.server.permissions.PermissionBackendException;
      35             : import com.google.gerrit.server.project.ProjectCache;
      36             : import com.google.gerrit.server.project.ProjectState;
      37             : import com.google.gerrit.server.query.change.ChangeData;
      38             : import com.google.inject.Inject;
      39             : import com.google.inject.Singleton;
      40             : import java.io.IOException;
      41             : import java.util.ArrayDeque;
      42             : import java.util.ArrayList;
      43             : import java.util.Collection;
      44             : import java.util.Deque;
      45             : import java.util.HashMap;
      46             : import java.util.HashSet;
      47             : import java.util.LinkedHashSet;
      48             : import java.util.List;
      49             : import java.util.Map;
      50             : import java.util.Objects;
      51             : import java.util.Set;
      52             : import org.eclipse.jgit.lib.ObjectId;
      53             : import org.eclipse.jgit.lib.Repository;
      54             : import org.eclipse.jgit.revwalk.RevCommit;
      55             : import org.eclipse.jgit.revwalk.RevWalk;
      56             : 
      57             : @Singleton
      58             : public class RelatedChangesSorter {
      59             :   private final GitRepositoryManager repoManager;
      60             :   private final PermissionBackend permissionBackend;
      61             :   private final ProjectCache projectCache;
      62             : 
      63             :   @Inject
      64             :   RelatedChangesSorter(
      65             :       GitRepositoryManager repoManager,
      66             :       PermissionBackend permissionBackend,
      67         145 :       ProjectCache projectCache) {
      68         145 :     this.repoManager = repoManager;
      69         145 :     this.permissionBackend = permissionBackend;
      70         145 :     this.projectCache = projectCache;
      71         145 :   }
      72             : 
      73             :   public List<PatchSetData> sort(List<ChangeData> in, PatchSet startPs)
      74             :       throws IOException, PermissionBackendException {
      75           5 :     checkArgument(!in.isEmpty(), "Input may not be empty");
      76             :     // Map of all patch sets, keyed by commit SHA-1.
      77           5 :     Map<ObjectId, PatchSetData> byId = collectById(in);
      78           5 :     PatchSetData start = byId.get(startPs.commitId());
      79           5 :     requireNonNull(
      80             :         start,
      81             :         () ->
      82           0 :             String.format(
      83             :                 "commit %s of patch set %s not found in %s",
      84           0 :                 startPs.commitId().name(),
      85           0 :                 startPs.id(),
      86           0 :                 byId.entrySet().stream()
      87           0 :                     .collect(toMap(e -> e.getKey().name(), e -> e.getValue().patchSet().id()))));
      88             : 
      89             :     // Map of patch set -> immediate parent.
      90           5 :     ListMultimap<PatchSetData, PatchSetData> parents =
      91           5 :         MultimapBuilder.hashKeys(in.size()).arrayListValues(3).build();
      92             :     // Map of patch set -> immediate children.
      93           5 :     ListMultimap<PatchSetData, PatchSetData> children =
      94           5 :         MultimapBuilder.hashKeys(in.size()).arrayListValues(3).build();
      95             :     // All other patch sets of the same change as startPs.
      96           5 :     List<PatchSetData> otherPatchSetsOfStart = new ArrayList<>();
      97             : 
      98           5 :     for (ChangeData cd : in) {
      99           5 :       for (PatchSet ps : cd.patchSets()) {
     100           5 :         PatchSetData thisPsd = requireNonNull(byId.get(ps.commitId()));
     101           5 :         if (cd.getId().equals(start.id()) && !ps.id().equals(start.psId())) {
     102           2 :           otherPatchSetsOfStart.add(thisPsd);
     103             :         }
     104           5 :         for (RevCommit p : thisPsd.commit().getParents()) {
     105           5 :           PatchSetData parentPsd = byId.get(p);
     106           5 :           if (parentPsd != null) {
     107           5 :             parents.put(thisPsd, parentPsd);
     108           5 :             children.put(parentPsd, thisPsd);
     109             :           }
     110             :         }
     111           5 :       }
     112           5 :     }
     113             : 
     114           5 :     Collection<PatchSetData> ancestors = walkAncestors(parents, start);
     115           5 :     List<PatchSetData> descendants =
     116           5 :         walkDescendants(children, start, otherPatchSetsOfStart, ancestors);
     117           5 :     List<PatchSetData> result = new ArrayList<>(ancestors.size() + descendants.size() - 1);
     118           5 :     result.addAll(Lists.reverse(descendants));
     119           5 :     result.addAll(ancestors);
     120           5 :     return result;
     121             :   }
     122             : 
     123             :   private Map<ObjectId, PatchSetData> collectById(List<ChangeData> in) throws IOException {
     124           5 :     Project.NameKey project = in.get(0).change().getProject();
     125           5 :     Map<ObjectId, PatchSetData> result = Maps.newHashMapWithExpectedSize(in.size() * 3);
     126           5 :     try (Repository repo = repoManager.openRepository(project);
     127           5 :         RevWalk rw = new RevWalk(repo)) {
     128           5 :       rw.setRetainBody(true);
     129           5 :       for (ChangeData cd : in) {
     130           5 :         checkArgument(
     131           5 :             cd.change().getProject().equals(project),
     132             :             "Expected change %s in project %s, found %s",
     133           5 :             cd.getId(),
     134             :             project,
     135           5 :             cd.change().getProject());
     136           5 :         for (PatchSet ps : cd.patchSets()) {
     137           5 :           RevCommit c = rw.parseCommit(ps.commitId());
     138           5 :           PatchSetData psd = PatchSetData.create(cd, ps, c);
     139           5 :           result.put(ps.commitId(), psd);
     140           5 :         }
     141           5 :       }
     142             :     }
     143           5 :     return result;
     144             :   }
     145             : 
     146             :   private Collection<PatchSetData> walkAncestors(
     147             :       ListMultimap<PatchSetData, PatchSetData> parents, PatchSetData start)
     148             :       throws PermissionBackendException {
     149           5 :     LinkedHashSet<PatchSetData> result = new LinkedHashSet<>();
     150           5 :     Deque<PatchSetData> pending = new ArrayDeque<>();
     151           5 :     pending.add(start);
     152           5 :     while (!pending.isEmpty()) {
     153           5 :       PatchSetData psd = pending.remove();
     154           5 :       if (result.contains(psd) || !isVisible(psd)) {
     155           0 :         continue;
     156             :       }
     157           5 :       result.add(psd);
     158           5 :       pending.addAll(Lists.reverse(parents.get(psd)));
     159           5 :     }
     160           5 :     return result;
     161             :   }
     162             : 
     163             :   private List<PatchSetData> walkDescendants(
     164             :       ListMultimap<PatchSetData, PatchSetData> children,
     165             :       PatchSetData start,
     166             :       List<PatchSetData> otherPatchSetsOfStart,
     167             :       Iterable<PatchSetData> ancestors)
     168             :       throws PermissionBackendException {
     169           5 :     Set<Change.Id> alreadyEmittedChanges = new HashSet<>();
     170           5 :     addAllChangeIds(alreadyEmittedChanges, ancestors);
     171             : 
     172             :     // Prefer descendants found by following the original patch set passed in.
     173           5 :     List<PatchSetData> result =
     174           5 :         walkDescendentsImpl(alreadyEmittedChanges, children, ImmutableList.of(start));
     175           5 :     addAllChangeIds(alreadyEmittedChanges, result);
     176             : 
     177             :     // Then, go back and add new indirect descendants found by following any
     178             :     // other patch sets of start. These show up after all direct descendants,
     179             :     // because we wouldn't know where in the walk to insert them.
     180           5 :     result.addAll(walkDescendentsImpl(alreadyEmittedChanges, children, otherPatchSetsOfStart));
     181           5 :     return result;
     182             :   }
     183             : 
     184             :   private static void addAllChangeIds(
     185             :       Collection<Change.Id> changeIds, Iterable<PatchSetData> psds) {
     186           5 :     for (PatchSetData psd : psds) {
     187           5 :       changeIds.add(psd.id());
     188           5 :     }
     189           5 :   }
     190             : 
     191             :   private List<PatchSetData> walkDescendentsImpl(
     192             :       Set<Change.Id> alreadyEmittedChanges,
     193             :       ListMultimap<PatchSetData, PatchSetData> children,
     194             :       List<PatchSetData> start)
     195             :       throws PermissionBackendException {
     196           5 :     if (start.isEmpty()) {
     197           4 :       return new ArrayList<>();
     198             :     }
     199           5 :     Map<Change.Id, PatchSet.Id> maxPatchSetIds = new HashMap<>();
     200           5 :     Set<PatchSetData> seen = new HashSet<>();
     201           5 :     List<PatchSetData> allPatchSets = new ArrayList<>();
     202           5 :     Deque<PatchSetData> pending = new ArrayDeque<>();
     203           5 :     pending.addAll(start);
     204           5 :     while (!pending.isEmpty()) {
     205           5 :       PatchSetData psd = pending.remove();
     206           5 :       if (seen.contains(psd) || !isVisible(psd)) {
     207           0 :         continue;
     208             :       }
     209           5 :       seen.add(psd);
     210           5 :       if (!alreadyEmittedChanges.contains(psd.id())) {
     211             :         // Don't emit anything for changes that were previously emitted, even
     212             :         // though different patch sets might show up later. However, do
     213             :         // continue walking through them for the purposes of finding indirect
     214             :         // descendants.
     215           4 :         PatchSet.Id oldMax = maxPatchSetIds.get(psd.id());
     216           4 :         if (oldMax == null || psd.psId().get() > oldMax.get()) {
     217           4 :           maxPatchSetIds.put(psd.id(), psd.psId());
     218             :         }
     219           4 :         allPatchSets.add(psd);
     220             :       }
     221             :       // Depth-first search with newest children first.
     222           5 :       for (PatchSetData child : children.get(psd)) {
     223           4 :         pending.addFirst(child);
     224           4 :       }
     225           5 :     }
     226             : 
     227             :     // If we saw the same change multiple times, prefer the latest patch set.
     228           5 :     List<PatchSetData> result = new ArrayList<>(allPatchSets.size());
     229           5 :     for (PatchSetData psd : allPatchSets) {
     230           4 :       if (requireNonNull(maxPatchSetIds.get(psd.id())).equals(psd.psId())) {
     231           4 :         result.add(psd);
     232             :       }
     233           4 :     }
     234           5 :     return result;
     235             :   }
     236             : 
     237             :   private boolean isVisible(PatchSetData psd) throws PermissionBackendException {
     238           5 :     PermissionBackend.WithUser perm = permissionBackend.currentUser();
     239           5 :     return perm.change(psd.data()).test(ChangePermission.READ)
     240           5 :         && projectCache.get(psd.data().project()).map(ProjectState::statePermitsRead).orElse(false);
     241             :   }
     242             : 
     243             :   @AutoValue
     244           5 :   public abstract static class PatchSetData {
     245             :     @VisibleForTesting
     246             :     static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
     247           5 :       return new AutoValue_RelatedChangesSorter_PatchSetData(cd, ps, commit);
     248             :     }
     249             : 
     250             :     public abstract ChangeData data();
     251             : 
     252             :     public abstract PatchSet patchSet();
     253             : 
     254             :     public abstract RevCommit commit();
     255             : 
     256             :     public PatchSet.Id psId() {
     257           5 :       return patchSet().id();
     258             :     }
     259             : 
     260             :     public Change.Id id() {
     261           5 :       return psId().changeId();
     262             :     }
     263             : 
     264             :     @Memoized
     265             :     @Override
     266             :     public int hashCode() {
     267           5 :       return Objects.hash(patchSet().id(), commit());
     268             :     }
     269             : 
     270             :     @Override
     271             :     public final boolean equals(Object obj) {
     272           0 :       if (!(obj instanceof PatchSetData)) {
     273           0 :         return false;
     274             :       }
     275           0 :       PatchSetData o = (PatchSetData) obj;
     276           0 :       return Objects.equals(patchSet().id(), o.patchSet().id())
     277           0 :           && Objects.equals(commit(), o.commit());
     278             :     }
     279             :   }
     280             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750