Line data Source code
1 : // Copyright (C) 2020 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.patch.diff; 16 : 17 : import static com.google.common.collect.ImmutableList.toImmutableList; 18 : 19 : import com.google.common.cache.CacheLoader; 20 : import com.google.common.cache.LoadingCache; 21 : import com.google.common.collect.ImmutableList; 22 : import com.google.common.collect.ImmutableSet; 23 : import com.google.common.collect.Sets; 24 : import com.google.common.collect.Streams; 25 : import com.google.common.flogger.FluentLogger; 26 : import com.google.gerrit.server.cache.CacheModule; 27 : import com.google.gerrit.server.git.GitRepositoryManager; 28 : import com.google.gerrit.server.patch.DiffNotAvailableException; 29 : import com.google.gerrit.server.patch.DiffUtil; 30 : import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache; 31 : import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCacheImpl; 32 : import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCacheKey; 33 : import com.google.gerrit.server.patch.gitdiff.ModifiedFile; 34 : import com.google.inject.Inject; 35 : import com.google.inject.Module; 36 : import com.google.inject.Singleton; 37 : import com.google.inject.TypeLiteral; 38 : import com.google.inject.name.Named; 39 : import java.io.IOException; 40 : import java.util.List; 41 : import java.util.Set; 42 : import java.util.stream.Stream; 43 : import org.eclipse.jgit.lib.ObjectId; 44 : import org.eclipse.jgit.lib.Repository; 45 : import org.eclipse.jgit.revwalk.RevCommit; 46 : import org.eclipse.jgit.revwalk.RevWalk; 47 : 48 : /** 49 : * A cache for the list of Git modified files between 2 commits (patchsets) with extra Gerrit logic. 50 : * 51 : * <p>The loader of this cache wraps a {@link GitModifiedFilesCache} to retrieve the git modified 52 : * files. 53 : * 54 : * <p>If the {@link ModifiedFilesCacheKey#aCommit()} is equal to {@link ObjectId#zeroId()}, the diff 55 : * will be evaluated against the empty tree, and the result will be exactly the same as the caller 56 : * can get from {@link GitModifiedFilesCache#get(GitModifiedFilesCacheKey)} 57 : */ 58 : @Singleton 59 : public class ModifiedFilesCacheImpl implements ModifiedFilesCache { 60 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 61 : 62 : private static final String MODIFIED_FILES = "modified_files"; 63 : 64 : private final LoadingCache<ModifiedFilesCacheKey, ImmutableList<ModifiedFile>> cache; 65 : 66 : public static Module module() { 67 152 : return new CacheModule() { 68 : @Override 69 : protected void configure() { 70 152 : bind(ModifiedFilesCache.class).to(ModifiedFilesCacheImpl.class); 71 : 72 : // The documentation has some defaults and recommendations for setting the cache 73 : // attributes: 74 : // https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#cache. 75 : // The cache is using the default disk limit as per section cache.<name>.diskLimit 76 : // in the cache documentation link. 77 152 : persist( 78 : ModifiedFilesCacheImpl.MODIFIED_FILES, 79 : ModifiedFilesCacheKey.class, 80 152 : new TypeLiteral<ImmutableList<ModifiedFile>>() {}) 81 152 : .keySerializer(ModifiedFilesCacheKey.Serializer.INSTANCE) 82 152 : .valueSerializer(GitModifiedFilesCacheImpl.ValueSerializer.INSTANCE) 83 152 : .maximumWeight(10 << 20) 84 152 : .weigher(ModifiedFilesWeigher.class) 85 152 : .version(4) 86 152 : .loader(ModifiedFilesLoader.class); 87 152 : } 88 : }; 89 : } 90 : 91 : @Inject 92 : public ModifiedFilesCacheImpl( 93 : @Named(ModifiedFilesCacheImpl.MODIFIED_FILES) 94 152 : LoadingCache<ModifiedFilesCacheKey, ImmutableList<ModifiedFile>> cache) { 95 152 : this.cache = cache; 96 152 : } 97 : 98 : @Override 99 : public ImmutableList<ModifiedFile> get(ModifiedFilesCacheKey key) 100 : throws DiffNotAvailableException { 101 : try { 102 104 : return cache.get(key); 103 0 : } catch (Exception e) { 104 0 : throw new DiffNotAvailableException(e); 105 : } 106 : } 107 : 108 : static class ModifiedFilesLoader 109 : extends CacheLoader<ModifiedFilesCacheKey, ImmutableList<ModifiedFile>> { 110 : private final GitModifiedFilesCache gitCache; 111 : private final GitRepositoryManager repoManager; 112 : 113 : @Inject 114 152 : ModifiedFilesLoader(GitModifiedFilesCache gitCache, GitRepositoryManager repoManager) { 115 152 : this.gitCache = gitCache; 116 152 : this.repoManager = repoManager; 117 152 : } 118 : 119 : @Override 120 : public ImmutableList<ModifiedFile> load(ModifiedFilesCacheKey key) 121 : throws IOException, DiffNotAvailableException { 122 104 : try (Repository repo = repoManager.openRepository(key.project()); 123 104 : RevWalk rw = new RevWalk(repo.newObjectReader())) { 124 104 : return loadModifiedFiles(key, rw); 125 : } 126 : } 127 : 128 : private ImmutableList<ModifiedFile> loadModifiedFiles(ModifiedFilesCacheKey key, RevWalk rw) 129 : throws IOException, DiffNotAvailableException { 130 : ObjectId aTree = 131 104 : key.aCommit().equals(ObjectId.zeroId()) 132 32 : ? key.aCommit() 133 104 : : DiffUtil.getTreeId(rw, key.aCommit()); 134 104 : ObjectId bTree = DiffUtil.getTreeId(rw, key.bCommit()); 135 : GitModifiedFilesCacheKey gitKey = 136 104 : GitModifiedFilesCacheKey.builder() 137 104 : .project(key.project()) 138 104 : .aTree(aTree) 139 104 : .bTree(bTree) 140 104 : .renameScore(key.renameScore()) 141 104 : .build(); 142 104 : ImmutableList<ModifiedFile> modifiedFiles = 143 104 : DiffUtil.mergeRewrittenModifiedFiles(gitCache.get(gitKey)); 144 104 : if (key.aCommit().equals(ObjectId.zeroId())) { 145 32 : return modifiedFiles; 146 : } 147 100 : RevCommit revCommitA = DiffUtil.getRevCommit(rw, key.aCommit()); 148 100 : RevCommit revCommitB = DiffUtil.getRevCommit(rw, key.bCommit()); 149 100 : if (DiffUtil.areRelated(revCommitA, revCommitB)) { 150 100 : return modifiedFiles; 151 : } 152 4 : Set<String> touchedFiles = 153 4 : getTouchedFilesWithParents( 154 4 : key, revCommitA.getParent(0).getId(), revCommitB.getParent(0).getId(), rw); 155 4 : return modifiedFiles.stream() 156 4 : .filter(f -> isTouched(touchedFiles, f)) 157 4 : .collect(toImmutableList()); 158 : } 159 : 160 : /** 161 : * Returns the paths of files that were modified between the old and new commits versus their 162 : * parents (i.e. old commit vs. its parent, and new commit vs. its parent). 163 : * 164 : * @param key the {@link ModifiedFilesCacheKey} representing the commits we are diffing 165 : * @param rw a {@link RevWalk} for the repository 166 : * @return The list of modified files between the old/new commits and their parents 167 : */ 168 : private Set<String> getTouchedFilesWithParents( 169 : ModifiedFilesCacheKey key, ObjectId parentOfA, ObjectId parentOfB, RevWalk rw) 170 : throws IOException { 171 : try { 172 : // TODO(ghareeb): as an enhancement: the 3 calls of the underlying git cache can be combined 173 4 : GitModifiedFilesCacheKey oldVsBaseKey = 174 4 : GitModifiedFilesCacheKey.create( 175 4 : key.project(), parentOfA, key.aCommit(), key.renameScore(), rw); 176 4 : List<ModifiedFile> oldVsBase = gitCache.get(oldVsBaseKey); 177 : 178 4 : GitModifiedFilesCacheKey newVsBaseKey = 179 4 : GitModifiedFilesCacheKey.create( 180 4 : key.project(), parentOfB, key.bCommit(), key.renameScore(), rw); 181 4 : List<ModifiedFile> newVsBase = gitCache.get(newVsBaseKey); 182 : 183 4 : return Sets.union(getOldAndNewPaths(oldVsBase), getOldAndNewPaths(newVsBase)); 184 0 : } catch (DiffNotAvailableException e) { 185 0 : logger.atWarning().log( 186 : "Failed to retrieve the touched files' commits (%s, %s) and parents (%s, %s): %s", 187 0 : key.aCommit(), key.bCommit(), parentOfA, parentOfB, e.getMessage()); 188 0 : return ImmutableSet.of(); 189 : } 190 : } 191 : 192 : private ImmutableSet<String> getOldAndNewPaths(List<ModifiedFile> files) { 193 4 : return files.stream() 194 4 : .flatMap( 195 4 : file -> Stream.concat(Streams.stream(file.oldPath()), Streams.stream(file.newPath()))) 196 4 : .collect(ImmutableSet.toImmutableSet()); 197 : } 198 : 199 : private static boolean isTouched(Set<String> touchedFilePaths, ModifiedFile modifiedFile) { 200 4 : String oldFilePath = modifiedFile.oldPath().orElse(null); 201 4 : String newFilePath = modifiedFile.newPath().orElse(null); 202 : // One of the above file paths could be /dev/null but we need not explicitly check for this 203 : // value as the set of file paths shouldn't contain it. 204 4 : return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath); 205 : } 206 : } 207 : }