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.gitdiff; 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.ImmutableMap; 23 : import com.google.gerrit.entities.Patch; 24 : import com.google.gerrit.proto.Protos; 25 : import com.google.gerrit.server.cache.CacheModule; 26 : import com.google.gerrit.server.cache.proto.Cache.ModifiedFilesProto; 27 : import com.google.gerrit.server.cache.serialize.CacheSerializer; 28 : import com.google.gerrit.server.git.GitRepositoryManager; 29 : import com.google.gerrit.server.patch.DiffNotAvailableException; 30 : import com.google.inject.Inject; 31 : import com.google.inject.Module; 32 : import com.google.inject.Singleton; 33 : import com.google.inject.TypeLiteral; 34 : import com.google.inject.name.Named; 35 : import java.io.IOException; 36 : import java.util.List; 37 : import java.util.Optional; 38 : import java.util.concurrent.ExecutionException; 39 : import org.eclipse.jgit.diff.DiffEntry; 40 : import org.eclipse.jgit.diff.DiffEntry.ChangeType; 41 : import org.eclipse.jgit.diff.DiffFormatter; 42 : import org.eclipse.jgit.lib.ObjectId; 43 : import org.eclipse.jgit.lib.ObjectReader; 44 : import org.eclipse.jgit.lib.Repository; 45 : import org.eclipse.jgit.util.io.DisabledOutputStream; 46 : 47 : /** Implementation of the {@link GitModifiedFilesCache} */ 48 : @Singleton 49 : public class GitModifiedFilesCacheImpl implements GitModifiedFilesCache { 50 : private static final String GIT_MODIFIED_FILES = "git_modified_files"; 51 152 : private static final ImmutableMap<ChangeType, Patch.ChangeType> changeTypeMap = 52 152 : ImmutableMap.of( 53 : DiffEntry.ChangeType.ADD, 54 : Patch.ChangeType.ADDED, 55 : DiffEntry.ChangeType.MODIFY, 56 : Patch.ChangeType.MODIFIED, 57 : DiffEntry.ChangeType.DELETE, 58 : Patch.ChangeType.DELETED, 59 : DiffEntry.ChangeType.RENAME, 60 : Patch.ChangeType.RENAMED, 61 : DiffEntry.ChangeType.COPY, 62 : Patch.ChangeType.COPIED); 63 : 64 : private LoadingCache<GitModifiedFilesCacheKey, ImmutableList<ModifiedFile>> cache; 65 : 66 : public static Module module() { 67 152 : return new CacheModule() { 68 : @Override 69 : protected void configure() { 70 152 : bind(GitModifiedFilesCache.class).to(GitModifiedFilesCacheImpl.class); 71 : 72 152 : persist( 73 : GIT_MODIFIED_FILES, 74 : GitModifiedFilesCacheKey.class, 75 152 : new TypeLiteral<ImmutableList<ModifiedFile>>() {}) 76 152 : .keySerializer(GitModifiedFilesCacheKey.Serializer.INSTANCE) 77 152 : .valueSerializer(ValueSerializer.INSTANCE) 78 : // The documentation has some defaults and recommendations for setting the cache 79 : // attributes: 80 : // https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#cache. 81 152 : .maximumWeight(10 << 20) 82 152 : .weigher(GitModifiedFilesWeigher.class) 83 : // The cache is using the default disk limit as per section cache.<name>.diskLimit 84 : // in the cache documentation link. 85 152 : .version(2) 86 152 : .loader(GitModifiedFilesCacheImpl.Loader.class); 87 152 : } 88 : }; 89 : } 90 : 91 : @Inject 92 : public GitModifiedFilesCacheImpl( 93 : @Named(GIT_MODIFIED_FILES) 94 152 : LoadingCache<GitModifiedFilesCacheKey, ImmutableList<ModifiedFile>> cache) { 95 152 : this.cache = cache; 96 152 : } 97 : 98 : @Override 99 : public ImmutableList<ModifiedFile> get(GitModifiedFilesCacheKey key) 100 : throws DiffNotAvailableException { 101 : try { 102 104 : return cache.get(key); 103 0 : } catch (ExecutionException e) { 104 0 : throw new DiffNotAvailableException(e); 105 : } 106 : } 107 : 108 : static class Loader extends CacheLoader<GitModifiedFilesCacheKey, ImmutableList<ModifiedFile>> { 109 : private final GitRepositoryManager repoManager; 110 : 111 : @Inject 112 152 : Loader(GitRepositoryManager repoManager) { 113 152 : this.repoManager = repoManager; 114 152 : } 115 : 116 : @Override 117 : public ImmutableList<ModifiedFile> load(GitModifiedFilesCacheKey key) throws IOException { 118 104 : try (Repository repo = repoManager.openRepository(key.project()); 119 104 : ObjectReader reader = repo.newObjectReader()) { 120 104 : List<DiffEntry> entries = getGitTreeDiff(repo, reader, key); 121 : 122 104 : return entries.stream().map(Loader::toModifiedFile).collect(toImmutableList()); 123 : } 124 : } 125 : 126 : private List<DiffEntry> getGitTreeDiff( 127 : Repository repo, ObjectReader reader, GitModifiedFilesCacheKey key) throws IOException { 128 104 : try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) { 129 104 : df.setReader(reader, repo.getConfig()); 130 104 : if (key.renameDetection()) { 131 104 : df.setDetectRenames(true); 132 104 : df.getRenameDetector().setRenameScore(key.renameScore()); 133 : } 134 : // Skip detecting content renames for binary files. 135 104 : df.getRenameDetector().setSkipContentRenamesForBinaryFiles(true); 136 : // The scan method only returns the file paths that are different. Callers may choose to 137 : // format these paths themselves. 138 104 : return df.scan(key.aTree().equals(ObjectId.zeroId()) ? null : key.aTree(), key.bTree()); 139 : } 140 : } 141 : 142 : private static ModifiedFile toModifiedFile(DiffEntry entry) { 143 93 : String oldPath = entry.getOldPath(); 144 93 : String newPath = entry.getNewPath(); 145 93 : return ModifiedFile.builder() 146 93 : .changeType(toChangeType(entry.getChangeType())) 147 93 : .oldPath(oldPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(oldPath)) 148 93 : .newPath(newPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(newPath)) 149 93 : .build(); 150 : } 151 : 152 : private static Patch.ChangeType toChangeType(DiffEntry.ChangeType changeType) { 153 93 : if (!changeTypeMap.containsKey(changeType)) { 154 0 : throw new IllegalArgumentException("Unsupported type " + changeType); 155 : } 156 93 : return changeTypeMap.get(changeType); 157 : } 158 : } 159 : 160 153 : public enum ValueSerializer implements CacheSerializer<ImmutableList<ModifiedFile>> { 161 153 : INSTANCE; 162 : 163 : @Override 164 : public byte[] serialize(ImmutableList<ModifiedFile> modifiedFiles) { 165 7 : ModifiedFilesProto.Builder builder = ModifiedFilesProto.newBuilder(); 166 7 : modifiedFiles.forEach( 167 5 : f -> builder.addModifiedFile(ModifiedFile.Serializer.INSTANCE.toProto(f))); 168 7 : return Protos.toByteArray(builder.build()); 169 : } 170 : 171 : @Override 172 : public ImmutableList<ModifiedFile> deserialize(byte[] in) { 173 2 : ImmutableList.Builder<ModifiedFile> modifiedFiles = ImmutableList.builder(); 174 : ModifiedFilesProto modifiedFilesProto = 175 2 : Protos.parseUnchecked(ModifiedFilesProto.parser(), in); 176 2 : modifiedFilesProto 177 2 : .getModifiedFileList() 178 2 : .forEach(f -> modifiedFiles.add(ModifiedFile.Serializer.INSTANCE.fromProto(f))); 179 2 : return modifiedFiles.build(); 180 : } 181 : } 182 : }