LCOV - code coverage report
Current view: top level - server/change - ChangeKindCacheImpl.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 140 160 87.5 %
Date: 2022-11-19 15:00:39 Functions: 25 29 86.2 %

          Line data    Source code
       1             : // Copyright (C) 2013 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.change;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : 
      19             : import com.google.auto.value.AutoValue;
      20             : import com.google.common.annotations.VisibleForTesting;
      21             : import com.google.common.cache.Cache;
      22             : import com.google.common.cache.Weigher;
      23             : import com.google.common.collect.FluentIterable;
      24             : import com.google.common.flogger.FluentLogger;
      25             : import com.google.gerrit.common.Nullable;
      26             : import com.google.gerrit.entities.Change;
      27             : import com.google.gerrit.entities.PatchSet;
      28             : import com.google.gerrit.entities.Project;
      29             : import com.google.gerrit.exceptions.StorageException;
      30             : import com.google.gerrit.extensions.client.ChangeKind;
      31             : import com.google.gerrit.proto.Protos;
      32             : import com.google.gerrit.server.cache.CacheModule;
      33             : import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
      34             : import com.google.gerrit.server.cache.serialize.CacheSerializer;
      35             : import com.google.gerrit.server.cache.serialize.EnumCacheSerializer;
      36             : import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
      37             : import com.google.gerrit.server.config.GerritServerConfig;
      38             : import com.google.gerrit.server.git.GitRepositoryManager;
      39             : import com.google.gerrit.server.git.InMemoryInserter;
      40             : import com.google.gerrit.server.git.MergeUtil;
      41             : import com.google.gerrit.server.query.change.ChangeData;
      42             : import com.google.inject.Inject;
      43             : import com.google.inject.Module;
      44             : import com.google.inject.name.Named;
      45             : import java.io.IOException;
      46             : import java.util.Arrays;
      47             : import java.util.Collection;
      48             : import java.util.Objects;
      49             : import java.util.Set;
      50             : import java.util.concurrent.Callable;
      51             : import java.util.concurrent.ExecutionException;
      52             : import org.eclipse.jgit.errors.LargeObjectException;
      53             : import org.eclipse.jgit.lib.AnyObjectId;
      54             : import org.eclipse.jgit.lib.Config;
      55             : import org.eclipse.jgit.lib.ObjectId;
      56             : import org.eclipse.jgit.lib.ObjectInserter;
      57             : import org.eclipse.jgit.lib.Repository;
      58             : import org.eclipse.jgit.merge.ThreeWayMerger;
      59             : import org.eclipse.jgit.revwalk.RevCommit;
      60             : import org.eclipse.jgit.revwalk.RevWalk;
      61             : 
      62             : public class ChangeKindCacheImpl implements ChangeKindCache {
      63         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      64             : 
      65             :   private static final String ID_CACHE = "change_kind";
      66             : 
      67             :   public static Module module() {
      68         152 :     return new CacheModule() {
      69             :       @Override
      70             :       protected void configure() {
      71         152 :         bind(ChangeKindCache.class).to(ChangeKindCacheImpl.class);
      72         152 :         persist(ID_CACHE, Key.class, ChangeKind.class)
      73         152 :             .maximumWeight(2 << 20)
      74         152 :             .weigher(ChangeKindWeigher.class)
      75         152 :             .version(1)
      76         152 :             .keySerializer(new Key.Serializer())
      77         152 :             .valueSerializer(new EnumCacheSerializer<>(ChangeKind.class));
      78         152 :       }
      79             :     };
      80             :   }
      81             : 
      82             :   public static class NoCache implements ChangeKindCache {
      83             :     private final boolean useRecursiveMerge;
      84             :     private final ChangeData.Factory changeDataFactory;
      85             :     private final GitRepositoryManager repoManager;
      86             : 
      87             :     @Inject
      88             :     NoCache(
      89             :         @GerritServerConfig Config serverConfig,
      90             :         ChangeData.Factory changeDataFactory,
      91           0 :         GitRepositoryManager repoManager) {
      92           0 :       this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
      93           0 :       this.changeDataFactory = changeDataFactory;
      94           0 :       this.repoManager = repoManager;
      95           0 :     }
      96             : 
      97             :     @Override
      98             :     public ChangeKind getChangeKind(
      99             :         Project.NameKey project,
     100             :         @Nullable RevWalk rw,
     101             :         @Nullable Config repoConfig,
     102             :         ObjectId prior,
     103             :         ObjectId next) {
     104             :       try {
     105           0 :         Key key = Key.create(prior, next, useRecursiveMerge);
     106           0 :         return new Loader(key, repoManager, project, rw, repoConfig).call();
     107           0 :       } catch (IOException e) {
     108           0 :         logger.atWarning().withCause(e).log(
     109           0 :             "Cannot check trivial rebase of new patch set %s in %s", next.name(), project);
     110           0 :         return ChangeKind.REWORK;
     111             :       }
     112             :     }
     113             : 
     114             :     @Override
     115             :     public ChangeKind getChangeKind(Change change, PatchSet patch) {
     116           0 :       return getChangeKindInternal(this, change, patch, changeDataFactory, repoManager);
     117             :     }
     118             : 
     119             :     @Override
     120             :     public ChangeKind getChangeKind(
     121             :         @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) {
     122           0 :       return getChangeKindInternal(this, rw, repoConfig, cd, patch);
     123             :     }
     124             :   }
     125             : 
     126             :   @AutoValue
     127          54 :   public abstract static class Key {
     128             :     public static Key create(AnyObjectId prior, AnyObjectId next, String strategyName) {
     129          54 :       return new AutoValue_ChangeKindCacheImpl_Key(prior.copy(), next.copy(), strategyName);
     130             :     }
     131             : 
     132             :     private static Key create(AnyObjectId prior, AnyObjectId next, boolean useRecursiveMerge) {
     133          53 :       return create(prior, next, MergeUtil.mergeStrategyName(true, useRecursiveMerge));
     134             :     }
     135             : 
     136             :     public abstract ObjectId prior();
     137             : 
     138             :     public abstract ObjectId next();
     139             : 
     140             :     public abstract String strategyName();
     141             : 
     142             :     @VisibleForTesting
     143         152 :     static class Serializer implements CacheSerializer<Key> {
     144             :       @Override
     145             :       public byte[] serialize(Key object) {
     146           2 :         ObjectIdConverter idConverter = ObjectIdConverter.create();
     147           2 :         return Protos.toByteArray(
     148           2 :             ChangeKindKeyProto.newBuilder()
     149           2 :                 .setPrior(idConverter.toByteString(object.prior()))
     150           2 :                 .setNext(idConverter.toByteString(object.next()))
     151           2 :                 .setStrategyName(object.strategyName())
     152           2 :                 .build());
     153             :       }
     154             : 
     155             :       @Override
     156             :       public Key deserialize(byte[] in) {
     157           1 :         ChangeKindKeyProto proto = Protos.parseUnchecked(ChangeKindKeyProto.parser(), in);
     158           1 :         ObjectIdConverter idConverter = ObjectIdConverter.create();
     159           1 :         return create(
     160           1 :             idConverter.fromByteString(proto.getPrior()),
     161           1 :             idConverter.fromByteString(proto.getNext()),
     162           1 :             proto.getStrategyName());
     163             :       }
     164             :     }
     165             :   }
     166             : 
     167             :   private static class Loader implements Callable<ChangeKind> {
     168             :     private final Key key;
     169             :     private final GitRepositoryManager repoManager;
     170             :     private final Project.NameKey projectName;
     171             :     private final RevWalk alreadyOpenRw;
     172             :     private final Config repoConfig;
     173             : 
     174             :     private Loader(
     175             :         Key key,
     176             :         GitRepositoryManager repoManager,
     177             :         Project.NameKey projectName,
     178             :         @Nullable RevWalk rw,
     179          53 :         @Nullable Config repoConfig) {
     180          53 :       checkArgument(
     181             :           (rw == null && repoConfig == null) || (rw != null && repoConfig != null),
     182             :           "must either provide both revwalk/config, or neither; got %s/%s",
     183             :           rw,
     184             :           repoConfig);
     185          53 :       this.key = key;
     186          53 :       this.repoManager = repoManager;
     187          53 :       this.projectName = projectName;
     188          53 :       this.alreadyOpenRw = rw;
     189          53 :       this.repoConfig = repoConfig;
     190          53 :     }
     191             : 
     192             :     @SuppressWarnings("resource") // Resources are manually managed.
     193             :     @Override
     194             :     public ChangeKind call() throws IOException {
     195          53 :       if (Objects.equals(key.prior(), key.next())) {
     196           2 :         return ChangeKind.NO_CODE_CHANGE;
     197             :       }
     198             : 
     199          53 :       RevWalk rw = alreadyOpenRw;
     200          53 :       Config config = repoConfig;
     201          53 :       Repository repo = null;
     202          53 :       if (alreadyOpenRw == null) {
     203           3 :         repo = repoManager.openRepository(projectName);
     204           3 :         rw = new RevWalk(repo);
     205           3 :         config = repo.getConfig();
     206             :       }
     207             :       try {
     208          53 :         RevCommit prior = rw.parseCommit(key.prior());
     209          53 :         rw.parseBody(prior);
     210          53 :         RevCommit next = rw.parseCommit(key.next());
     211          53 :         rw.parseBody(next);
     212             : 
     213          53 :         if (!next.getFullMessage().equals(prior.getFullMessage())) {
     214          37 :           if (isSameDeltaAndTree(rw, prior, next)) {
     215          24 :             return ChangeKind.NO_CODE_CHANGE;
     216             :           }
     217          32 :           return ChangeKind.REWORK;
     218             :         }
     219             : 
     220          45 :         if (isSameDeltaAndTree(rw, prior, next)) {
     221          15 :           return ChangeKind.NO_CHANGE;
     222             :         }
     223             : 
     224          44 :         if (prior.getParentCount() == 0 || next.getParentCount() == 0) {
     225             :           // At this point we have considered all the kinds that could be applicable to root
     226             :           // commits; the remainder of the checks in this method all assume that both commits have
     227             :           // at least one parent.
     228           9 :           return ChangeKind.REWORK;
     229             :         }
     230             : 
     231          43 :         if ((prior.getParentCount() > 1 || next.getParentCount() > 1)
     232          11 :             && !onlyFirstParentChanged(prior, next)) {
     233             :           // Trivial rebases done by machine only work well on 1 parent.
     234          11 :           return ChangeKind.REWORK;
     235             :         }
     236             : 
     237             :         // A trivial rebase can be detected by looking for the next commit
     238             :         // having the same tree as would exist when the prior commit is
     239             :         // cherry-picked onto the next commit's new first parent.
     240          41 :         try (ObjectInserter ins = new InMemoryInserter(rw.getObjectReader())) {
     241          41 :           ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName());
     242          41 :           merger.setBase(prior.getParent(0));
     243          41 :           if (merger.merge(next.getParent(0), prior)
     244          41 :               && merger.getResultTreeId().equals(next.getTree())) {
     245          15 :             if (prior.getParentCount() == 1) {
     246          15 :               return ChangeKind.TRIVIAL_REBASE;
     247             :             }
     248           1 :             return ChangeKind.MERGE_FIRST_PARENT_UPDATE;
     249             :           }
     250          15 :         } catch (LargeObjectException e) {
     251             :           // Some object is too large for the merge attempt to succeed. Assume
     252             :           // it was a rework.
     253          37 :         }
     254          37 :         return ChangeKind.REWORK;
     255             :       } finally {
     256          53 :         if (repo != null) {
     257           3 :           rw.close();
     258           3 :           repo.close();
     259             :         }
     260             :       }
     261             :     }
     262             : 
     263             :     public static boolean onlyFirstParentChanged(RevCommit prior, RevCommit next) {
     264          11 :       return !sameFirstParents(prior, next) && sameRestOfParents(prior, next);
     265             :     }
     266             : 
     267             :     private static boolean sameFirstParents(RevCommit prior, RevCommit next) {
     268          11 :       if (prior.getParentCount() == 0) {
     269           0 :         return next.getParentCount() == 0;
     270             :       }
     271          11 :       return prior.getParent(0).equals(next.getParent(0));
     272             :     }
     273             : 
     274             :     private static boolean sameRestOfParents(RevCommit prior, RevCommit next) {
     275           3 :       Set<RevCommit> priorRestParents = allExceptFirstParent(prior.getParents());
     276           3 :       Set<RevCommit> nextRestParents = allExceptFirstParent(next.getParents());
     277           3 :       return priorRestParents.equals(nextRestParents);
     278             :     }
     279             : 
     280             :     private static Set<RevCommit> allExceptFirstParent(RevCommit[] parents) {
     281           3 :       return FluentIterable.from(Arrays.asList(parents)).skip(1).toSet();
     282             :     }
     283             : 
     284             :     private static boolean isSameDeltaAndTree(RevWalk rw, RevCommit prior, RevCommit next)
     285             :         throws IOException {
     286          53 :       if (!Objects.equals(next.getTree(), prior.getTree())) {
     287          52 :         return false;
     288             :       }
     289             : 
     290          28 :       if (prior.getParentCount() != next.getParentCount()) {
     291           8 :         return false;
     292          27 :       } else if (prior.getParentCount() == 0) {
     293           1 :         return true;
     294             :       }
     295             : 
     296             :       // Make sure that the prior/next delta is the same - not just the tree.
     297             :       // This is done by making sure that the parent trees are equal.
     298          26 :       for (int i = 0; i < prior.getParentCount(); i++) {
     299             :         // Parse parent commits so that their trees are available.
     300          26 :         rw.parseCommit(prior.getParent(i));
     301          26 :         rw.parseCommit(next.getParent(i));
     302             : 
     303          26 :         if (!Objects.equals(next.getParent(i).getTree(), prior.getParent(i).getTree())) {
     304           7 :           return false;
     305             :         }
     306             :       }
     307          25 :       return true;
     308             :     }
     309             :   }
     310             : 
     311         152 :   public static class ChangeKindWeigher implements Weigher<Key, ChangeKind> {
     312             :     @Override
     313             :     public int weigh(Key key, ChangeKind changeKind) {
     314          53 :       return 16
     315             :           + 2 * 36
     316          53 :           + 2 * key.strategyName().length() // Size of Key, 64 bit JVM
     317          53 :           + 2 * changeKind.name().length(); // Size of ChangeKind, 64 bit JVM
     318             :     }
     319             :   }
     320             : 
     321             :   private final Cache<Key, ChangeKind> cache;
     322             :   private final boolean useRecursiveMerge;
     323             :   private final ChangeData.Factory changeDataFactory;
     324             :   private final GitRepositoryManager repoManager;
     325             : 
     326             :   @Inject
     327             :   ChangeKindCacheImpl(
     328             :       @GerritServerConfig Config serverConfig,
     329             :       @Named(ID_CACHE) Cache<Key, ChangeKind> cache,
     330             :       ChangeData.Factory changeDataFactory,
     331         146 :       GitRepositoryManager repoManager) {
     332         146 :     this.cache = cache;
     333         146 :     this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
     334         146 :     this.changeDataFactory = changeDataFactory;
     335         146 :     this.repoManager = repoManager;
     336         146 :   }
     337             : 
     338             :   @Override
     339             :   public ChangeKind getChangeKind(
     340             :       Project.NameKey project,
     341             :       @Nullable RevWalk rw,
     342             :       @Nullable Config repoConfig,
     343             :       ObjectId prior,
     344             :       ObjectId next) {
     345             :     try {
     346          53 :       Key key = Key.create(prior, next, useRecursiveMerge);
     347          53 :       ChangeKind kind = cache.get(key, new Loader(key, repoManager, project, rw, repoConfig));
     348          53 :       logger.atFine().log("Change kind of new patch set %s in %s: %s", next.name(), project, kind);
     349          53 :       return kind;
     350           1 :     } catch (ExecutionException e) {
     351           1 :       logger.atWarning().withCause(e).log(
     352           1 :           "Cannot check change kind of new patch set %s in %s", next.name(), project);
     353           1 :       return ChangeKind.REWORK;
     354             :     }
     355             :   }
     356             : 
     357             :   @Override
     358             :   public ChangeKind getChangeKind(Change change, PatchSet patch) {
     359           2 :     return getChangeKindInternal(this, change, patch, changeDataFactory, repoManager);
     360             :   }
     361             : 
     362             :   @Override
     363             :   public ChangeKind getChangeKind(
     364             :       @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) {
     365         103 :     return getChangeKindInternal(this, rw, repoConfig, cd, patch);
     366             :   }
     367             : 
     368             :   private static ChangeKind getChangeKindInternal(
     369             :       ChangeKindCache cache,
     370             :       @Nullable RevWalk rw,
     371             :       @Nullable Config repoConfig,
     372             :       ChangeData change,
     373             :       PatchSet patch) {
     374         103 :     ChangeKind kind = ChangeKind.REWORK;
     375             :     // Trivial case: if we're on the first patch, we don't need to use
     376             :     // the repository.
     377         103 :     if (patch.id().get() > 1) {
     378             :       try {
     379          52 :         Collection<PatchSet> patchSetCollection = change.patchSets();
     380          52 :         PatchSet priorPs = patch;
     381          52 :         for (PatchSet ps : patchSetCollection) {
     382          52 :           if (ps.id().get() < patch.id().get()
     383          52 :               && (ps.id().get() > priorPs.id().get() || priorPs == patch)) {
     384             :             // We only want the previous patch set, so walk until the last one
     385          52 :             priorPs = ps;
     386             :           }
     387          52 :         }
     388             : 
     389             :         // If we still think the previous patch is the current patch,
     390             :         // we only have one patch set.  Return the default.
     391             :         // This can happen if a user creates a draft, uploads a second patch,
     392             :         // and deletes the draft.
     393          52 :         if (priorPs != patch) {
     394          52 :           kind =
     395          52 :               cache.getChangeKind(
     396          52 :                   change.project(), rw, repoConfig, priorPs.commitId(), patch.commitId());
     397             :         }
     398           0 :       } catch (StorageException e) {
     399             :         // Do nothing; assume we have a complex change
     400           0 :         logger.atWarning().withCause(e).log(
     401             :             "Unable to get change kind for patchSet %s of change %s",
     402           0 :             patch.number(), change.getId());
     403          52 :       }
     404             :     }
     405         103 :     logger.atFine().log(
     406         103 :         "Change kind for patchSet %s of change %s: %s", patch.number(), change.getId(), kind);
     407         103 :     return kind;
     408             :   }
     409             : 
     410             :   private static ChangeKind getChangeKindInternal(
     411             :       ChangeKindCache cache,
     412             :       Change change,
     413             :       PatchSet patch,
     414             :       ChangeData.Factory changeDataFactory,
     415             :       GitRepositoryManager repoManager) {
     416             :     // TODO - dborowitz: add NEW_CHANGE type for default.
     417           2 :     ChangeKind kind = ChangeKind.REWORK;
     418             :     // Trivial case: if we're on the first patch, we don't need to open
     419             :     // the repository.
     420           2 :     if (patch.id().get() > 1) {
     421           2 :       try (Repository repo = repoManager.openRepository(change.getProject());
     422           2 :           RevWalk rw = new RevWalk(repo)) {
     423           2 :         kind =
     424           2 :             getChangeKindInternal(
     425           2 :                 cache, rw, repo.getConfig(), changeDataFactory.create(change), patch);
     426           0 :       } catch (IOException e) {
     427             :         // Do nothing; assume we have a complex change
     428           0 :         logger.atWarning().withCause(e).log(
     429             :             "Unable to get change kind for patchSet %s of change %s",
     430           0 :             patch.number(), change.getChangeId());
     431           2 :       }
     432             :     }
     433           2 :     logger.atFine().log(
     434           2 :         "Change kind for patchSet %s of change %s: %s", patch.number(), change.getChangeId(), kind);
     435           2 :     return kind;
     436             :   }
     437             : }

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