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 : }