LCOV - code coverage report
Current view: top level - server/git - PureRevertCache.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 62 69 89.9 %
Date: 2022-11-19 15:00:39 Functions: 9 9 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2019 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.git;
      16             : 
      17             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      18             : 
      19             : import com.google.common.annotations.VisibleForTesting;
      20             : import com.google.common.base.Throwables;
      21             : import com.google.common.cache.CacheLoader;
      22             : import com.google.common.cache.LoadingCache;
      23             : import com.google.gerrit.entities.Change;
      24             : import com.google.gerrit.entities.Project;
      25             : import com.google.gerrit.extensions.restapi.BadRequestException;
      26             : import com.google.gerrit.server.cache.CacheModule;
      27             : import com.google.gerrit.server.cache.proto.Cache;
      28             : import com.google.gerrit.server.cache.proto.Cache.PureRevertKeyProto;
      29             : import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
      30             : import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
      31             : import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
      32             : import com.google.gerrit.server.logging.Metadata;
      33             : import com.google.gerrit.server.logging.TraceContext;
      34             : import com.google.gerrit.server.notedb.ChangeNotes;
      35             : import com.google.gerrit.server.project.ProjectCache;
      36             : import com.google.inject.Inject;
      37             : import com.google.inject.Module;
      38             : import com.google.inject.Singleton;
      39             : import com.google.inject.name.Named;
      40             : import com.google.protobuf.ByteString;
      41             : import java.io.IOException;
      42             : import java.util.List;
      43             : import java.util.concurrent.ExecutionException;
      44             : import org.eclipse.jgit.diff.DiffEntry;
      45             : import org.eclipse.jgit.diff.DiffFormatter;
      46             : import org.eclipse.jgit.errors.InvalidObjectIdException;
      47             : import org.eclipse.jgit.errors.MissingObjectException;
      48             : import org.eclipse.jgit.lib.ObjectId;
      49             : import org.eclipse.jgit.lib.ObjectInserter;
      50             : import org.eclipse.jgit.lib.Repository;
      51             : import org.eclipse.jgit.merge.ThreeWayMerger;
      52             : import org.eclipse.jgit.revwalk.RevCommit;
      53             : import org.eclipse.jgit.revwalk.RevWalk;
      54             : import org.eclipse.jgit.util.io.DisabledOutputStream;
      55             : 
      56             : /** Computes and caches if a change is a pure revert of another change. */
      57             : @Singleton
      58             : public class PureRevertCache {
      59             :   private static final String ID_CACHE = "pure_revert";
      60             : 
      61             :   public static Module module() {
      62         152 :     return new CacheModule() {
      63             :       @Override
      64             :       protected void configure() {
      65         152 :         persist(ID_CACHE, Cache.PureRevertKeyProto.class, Boolean.class)
      66         152 :             .maximumWeight(100)
      67         152 :             .loader(Loader.class)
      68         152 :             .version(1)
      69         152 :             .keySerializer(new ProtobufSerializer<>(Cache.PureRevertKeyProto.parser()))
      70         152 :             .valueSerializer(BooleanCacheSerializer.INSTANCE);
      71         152 :       }
      72             :     };
      73             :   }
      74             : 
      75             :   private final LoadingCache<PureRevertKeyProto, Boolean> cache;
      76             :   private final ChangeNotes.Factory notesFactory;
      77             : 
      78             :   @Inject
      79             :   PureRevertCache(
      80             :       @Named(ID_CACHE) LoadingCache<PureRevertKeyProto, Boolean> cache,
      81         146 :       ChangeNotes.Factory notesFactory) {
      82         146 :     this.cache = cache;
      83         146 :     this.notesFactory = notesFactory;
      84         146 :   }
      85             : 
      86             :   /**
      87             :    * Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of the change that is
      88             :    * referenced in {@link Change#getRevertOf()}.
      89             :    *
      90             :    * @return {@code true} if {@code claimedRevert} is a pure (clean) revert.
      91             :    * @throws IOException if there was a problem with the storage layer
      92             :    * @throws BadRequestException if there is a problem with the provided {@link ChangeNotes}
      93             :    */
      94             :   public boolean isPureRevert(ChangeNotes claimedRevert) throws IOException, BadRequestException {
      95          15 :     if (claimedRevert.getChange().getRevertOf() == null) {
      96           2 :       throw new BadRequestException("revertOf not set");
      97             :     }
      98          14 :     ChangeNotes claimedOriginal =
      99          14 :         notesFactory.createChecked(
     100          14 :             claimedRevert.getProjectName(), claimedRevert.getChange().getRevertOf());
     101          14 :     return isPureRevert(
     102          14 :         claimedRevert.getProjectName(),
     103          14 :         claimedRevert.getCurrentPatchSet().commitId(),
     104          14 :         claimedOriginal.getCurrentPatchSet().commitId());
     105             :   }
     106             : 
     107             :   /**
     108             :    * Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
     109             :    * claimedOriginal}.
     110             :    *
     111             :    * @return {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
     112             :    *     claimedOriginal}.
     113             :    * @throws IOException if there was a problem with the storage layer
     114             :    * @throws BadRequestException if there is a problem with the provided {@link ObjectId}s
     115             :    */
     116             :   public boolean isPureRevert(
     117             :       Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal)
     118             :       throws IOException, BadRequestException {
     119             :     try {
     120          14 :       return cache.get(key(project, claimedRevert, claimedOriginal));
     121           0 :     } catch (ExecutionException e) {
     122           0 :       Throwables.throwIfInstanceOf(e.getCause(), BadRequestException.class);
     123           0 :       throw new IOException(e);
     124             :     }
     125             :   }
     126             : 
     127             :   @VisibleForTesting
     128             :   static PureRevertKeyProto key(
     129             :       Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal) {
     130          15 :     ByteString original = ObjectIdConverter.create().toByteString(claimedOriginal);
     131          15 :     ByteString revert = ObjectIdConverter.create().toByteString(claimedRevert);
     132          15 :     return PureRevertKeyProto.newBuilder()
     133          15 :         .setProject(project.get())
     134          15 :         .setClaimedOriginal(original)
     135          15 :         .setClaimedRevert(revert)
     136          15 :         .build();
     137             :   }
     138             : 
     139             :   static class Loader extends CacheLoader<PureRevertKeyProto, Boolean> {
     140             :     private final GitRepositoryManager repoManager;
     141             :     private final MergeUtilFactory mergeUtilFactory;
     142             :     private final ProjectCache projectCache;
     143             : 
     144             :     @Inject
     145             :     Loader(
     146             :         GitRepositoryManager repoManager,
     147             :         MergeUtilFactory mergeUtilFactory,
     148         152 :         ProjectCache projectCache) {
     149         152 :       this.repoManager = repoManager;
     150         152 :       this.mergeUtilFactory = mergeUtilFactory;
     151         152 :       this.projectCache = projectCache;
     152         152 :     }
     153             : 
     154             :     @Override
     155             :     public Boolean load(PureRevertKeyProto key) throws BadRequestException, IOException {
     156          14 :       try (TraceContext.TraceTimer ignored =
     157          14 :           TraceContext.newTimer(
     158             :               "Loading pure revert",
     159          14 :               Metadata.builder().cacheKey(key.toString()).projectName(key.getProject()).build())) {
     160          14 :         ObjectId original = ObjectIdConverter.create().fromByteString(key.getClaimedOriginal());
     161          14 :         ObjectId revert = ObjectIdConverter.create().fromByteString(key.getClaimedRevert());
     162          14 :         Project.NameKey project = Project.nameKey(key.getProject());
     163             : 
     164          14 :         try (Repository repo = repoManager.openRepository(project);
     165          14 :             ObjectInserter oi = repo.newObjectInserter();
     166          14 :             RevWalk rw = new RevWalk(repo)) {
     167             :           RevCommit claimedOriginalCommit;
     168             :           try {
     169          14 :             claimedOriginalCommit = rw.parseCommit(original);
     170           0 :           } catch (InvalidObjectIdException | MissingObjectException e) {
     171           0 :             throw new BadRequestException("invalid object ID", e);
     172          14 :           }
     173          14 :           if (claimedOriginalCommit.getParentCount() == 0) {
     174           0 :             throw new BadRequestException("can't check against initial commit");
     175             :           }
     176          14 :           RevCommit claimedRevertCommit = rw.parseCommit(revert);
     177          14 :           if (claimedRevertCommit.getParentCount() == 0) {
     178           0 :             return false;
     179             :           }
     180             :           // Rebase claimed revert onto claimed original
     181          14 :           ThreeWayMerger merger =
     182             :               mergeUtilFactory
     183          14 :                   .create(projectCache.get(project).orElseThrow(illegalState(project)))
     184          14 :                   .newThreeWayMerger(oi, repo.getConfig());
     185          14 :           merger.setBase(claimedRevertCommit.getParent(0));
     186          14 :           boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
     187          14 :           if (!success || merger.getResultTreeId() == null) {
     188             :             // Merge conflict during rebase
     189           1 :             return false;
     190             :           }
     191             : 
     192             :           // Any differences between claimed original's parent and the rebase result indicate that
     193             :           // the claimedRevert is not a pure revert but made content changes
     194          14 :           try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
     195          14 :             df.setReader(oi.newReader(), repo.getConfig());
     196          14 :             List<DiffEntry> entries =
     197          14 :                 df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
     198          14 :             return entries.isEmpty();
     199             :           }
     200           1 :         }
     201           1 :       }
     202             :     }
     203             :   }
     204             : }

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