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.filediff; 16 : 17 : import com.google.common.collect.ImmutableMap; 18 : import com.google.common.flogger.FluentLogger; 19 : import com.google.gerrit.server.patch.DiffNotAvailableException; 20 : import com.google.gerrit.server.patch.DiffUtil; 21 : import com.google.gerrit.server.patch.gitfilediff.GitFileDiff; 22 : import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCache; 23 : import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheKey; 24 : import com.google.inject.Inject; 25 : import com.google.inject.assistedinject.Assisted; 26 : import java.io.IOException; 27 : import java.util.HashMap; 28 : import java.util.List; 29 : import java.util.Map; 30 : import java.util.Optional; 31 : import java.util.function.Function; 32 : import java.util.stream.Collectors; 33 : import org.eclipse.jgit.lib.ObjectId; 34 : import org.eclipse.jgit.revwalk.RevWalk; 35 : 36 : /** 37 : * A helper class that computes the four {@link GitFileDiff}s for a list of {@link 38 : * FileDiffCacheKey}s: 39 : * 40 : * <ul> 41 : * <li>old commit vs. new commit 42 : * <li>old parent vs. old commit 43 : * <li>new parent vs. new commit 44 : * <li>old parent vs. new parent 45 : * </ul> 46 : * 47 : * The four {@link GitFileDiff} are stored in the entity class {@link AllFileGitDiffs}. We use these 48 : * diffs to identify the edits due to rebase using the {@link EditTransformer} class. 49 : */ 50 : class AllDiffsEvaluator { 51 104 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 52 : 53 : private final RevWalk rw; 54 : private final GitFileDiffCache gitCache; 55 : 56 : interface Factory { 57 : AllDiffsEvaluator create(RevWalk rw); 58 : } 59 : 60 : @Inject 61 104 : private AllDiffsEvaluator(GitFileDiffCache gitCache, @Assisted RevWalk rw) { 62 104 : this.gitCache = gitCache; 63 104 : this.rw = rw; 64 104 : } 65 : 66 : Map<AugmentedFileDiffCacheKey, AllFileGitDiffs> execute( 67 : List<AugmentedFileDiffCacheKey> augmentedKeys) throws DiffNotAvailableException { 68 104 : ImmutableMap.Builder<AugmentedFileDiffCacheKey, AllFileGitDiffs> keyToAllDiffs = 69 104 : ImmutableMap.builderWithExpectedSize(augmentedKeys.size()); 70 : 71 104 : List<AugmentedFileDiffCacheKey> keysWithRebaseEdits = 72 104 : augmentedKeys.stream().filter(k -> !k.ignoreRebase()).collect(Collectors.toList()); 73 : 74 : // TODO(ghareeb): as an enhancement, you can batch these calls as follows. 75 : // First batch: "old commit vs. new commit" and "new parent vs. new commit" 76 : // Second batch: "old parent vs. old commit" and "old parent vs. new parent" 77 : 78 104 : Map<FileDiffCacheKey, GitDiffEntity> mainDiffs = 79 104 : computeGitFileDiffs( 80 104 : createGitKeys( 81 : augmentedKeys, 82 93 : k -> k.key().oldCommit(), 83 93 : k -> k.key().newCommit(), 84 93 : k -> k.key().newFilePath())); 85 : 86 104 : Map<FileDiffCacheKey, GitDiffEntity> oldVsParentDiffs = 87 104 : computeGitFileDiffs( 88 104 : createGitKeys( 89 : keysWithRebaseEdits, 90 4 : k -> k.oldParentId().get(), // oldParent is set for keysWithRebaseEdits 91 4 : k -> k.key().oldCommit(), 92 4 : k -> mainDiffs.get(k.key()).gitDiff().oldPath().orElse(null))); 93 : 94 104 : Map<FileDiffCacheKey, GitDiffEntity> newVsParentDiffs = 95 104 : computeGitFileDiffs( 96 104 : createGitKeys( 97 : keysWithRebaseEdits, 98 4 : k -> k.newParentId().get(), // newParent is set for keysWithRebaseEdits 99 4 : k -> k.key().newCommit(), 100 4 : k -> k.key().newFilePath())); 101 : 102 104 : Map<FileDiffCacheKey, GitDiffEntity> parentsDiffs = 103 104 : computeGitFileDiffs( 104 104 : createGitKeys( 105 : keysWithRebaseEdits, 106 4 : k -> k.oldParentId().get(), 107 4 : k -> k.newParentId().get(), 108 : k -> { 109 4 : GitFileDiff newVsParDiff = newVsParentDiffs.get(k.key()).gitDiff(); 110 : // TODO(ghareeb): Follow up on replacing key.newFilePath as a fallback. 111 : // If the file was added between newParent and newCommit, we actually wouldn't 112 : // need to have to determine the oldParent vs. newParent diff as nothing in 113 : // that file could be an edit due to rebase anymore. Only if the returned diff 114 : // is empty, the oldParent vs. newParent diff becomes relevant again (e.g. to 115 : // identify a file deletion which was due to rebase. Check if the structure 116 : // can be improved to make this clearer. Can we maybe even skip the diff in 117 : // the first situation described? 118 4 : return newVsParDiff.oldPath().orElse(k.key().newFilePath()); 119 : })); 120 : 121 104 : for (AugmentedFileDiffCacheKey augmentedKey : augmentedKeys) { 122 93 : FileDiffCacheKey key = augmentedKey.key(); 123 : AllFileGitDiffs.Builder builder = 124 93 : AllFileGitDiffs.builder().augmentedKey(augmentedKey).mainDiff(mainDiffs.get(key)); 125 : 126 93 : if (augmentedKey.ignoreRebase()) { 127 93 : keyToAllDiffs.put(augmentedKey, builder.build()); 128 93 : continue; 129 : } 130 : 131 4 : if (oldVsParentDiffs.containsKey(key) && !oldVsParentDiffs.get(key).gitDiff().isEmpty()) { 132 4 : builder.oldVsParentDiff(Optional.of(oldVsParentDiffs.get(key))); 133 : } 134 : 135 4 : if (newVsParentDiffs.containsKey(key) && !newVsParentDiffs.get(key).gitDiff().isEmpty()) { 136 4 : builder.newVsParentDiff(Optional.of(newVsParentDiffs.get(key))); 137 : } 138 : 139 4 : if (parentsDiffs.containsKey(key) && !parentsDiffs.get(key).gitDiff().isEmpty()) { 140 3 : builder.parentVsParentDiff(Optional.of(parentsDiffs.get(key))); 141 : } 142 : 143 4 : keyToAllDiffs.put(augmentedKey, builder.build()); 144 4 : } 145 104 : return keyToAllDiffs.build(); 146 : } 147 : 148 : /** 149 : * Computes the git diff for the git keys of the input map {@code keys} parameter. The computation 150 : * uses the underlying {@link GitFileDiffCache}. 151 : */ 152 : private Map<FileDiffCacheKey, GitDiffEntity> computeGitFileDiffs( 153 : Map<FileDiffCacheKey, GitFileDiffCacheKey> keys) throws DiffNotAvailableException { 154 104 : ImmutableMap.Builder<FileDiffCacheKey, GitDiffEntity> result = 155 104 : ImmutableMap.builderWithExpectedSize(keys.size()); 156 104 : ImmutableMap<GitFileDiffCacheKey, GitFileDiff> gitDiffs = gitCache.getAll(keys.values()); 157 104 : for (FileDiffCacheKey key : keys.keySet()) { 158 93 : GitFileDiffCacheKey gitKey = keys.get(key); 159 93 : GitFileDiff gitFileDiff = gitDiffs.get(gitKey); 160 93 : result.put(key, GitDiffEntity.create(gitKey, gitFileDiff)); 161 93 : } 162 104 : return result.build(); 163 : } 164 : 165 : /** 166 : * Convert a list of {@link AugmentedFileDiffCacheKey} to their corresponding {@link 167 : * GitFileDiffCacheKey} which can be used to call the underlying {@link GitFileDiffCache}. 168 : * 169 : * @param keys a list of input {@link AugmentedFileDiffCacheKey}s. 170 : * @param aCommitFn a function to compute the aCommit that will be used in the git diff. 171 : * @param bCommitFn a function to compute the bCommit that will be used in the git diff. 172 : * @param newPathFn a function to compute the new path of the git key. 173 : * @return a map of the input {@link FileDiffCacheKey} to the {@link GitFileDiffCacheKey}. 174 : */ 175 : private Map<FileDiffCacheKey, GitFileDiffCacheKey> createGitKeys( 176 : List<AugmentedFileDiffCacheKey> keys, 177 : Function<AugmentedFileDiffCacheKey, ObjectId> aCommitFn, 178 : Function<AugmentedFileDiffCacheKey, ObjectId> bCommitFn, 179 : Function<AugmentedFileDiffCacheKey, String> newPathFn) { 180 104 : Map<FileDiffCacheKey, GitFileDiffCacheKey> result = new HashMap<>(); 181 104 : for (AugmentedFileDiffCacheKey key : keys) { 182 : try { 183 93 : String path = newPathFn.apply(key); 184 93 : if (path != null) { 185 93 : result.put( 186 93 : key.key(), 187 93 : createGitKey(key.key(), aCommitFn.apply(key), bCommitFn.apply(key), path, rw)); 188 : } 189 0 : } catch (IOException e) { 190 : // TODO(ghareeb): This implies that the output keys may not have the same size as the input. 191 : // Check the caller's code path about the correctness of the computation in this case. If 192 : // errors are rare, it may be better to throw an exception and fail the whole computation. 193 0 : logger.atWarning().log("Failed to compute the git key for key %s: %s", key, e.getMessage()); 194 93 : } 195 93 : } 196 104 : return result; 197 : } 198 : 199 : /** Returns the {@link GitFileDiffCacheKey} for the {@code key} input parameter. */ 200 : private GitFileDiffCacheKey createGitKey( 201 : FileDiffCacheKey key, ObjectId aCommit, ObjectId bCommit, String pathNew, RevWalk rw) 202 : throws IOException { 203 : ObjectId oldTreeId = 204 93 : aCommit.equals(ObjectId.zeroId()) ? ObjectId.zeroId() : DiffUtil.getTreeId(rw, aCommit); 205 93 : ObjectId newTreeId = DiffUtil.getTreeId(rw, bCommit); 206 93 : return GitFileDiffCacheKey.builder() 207 93 : .project(key.project()) 208 93 : .oldTree(oldTreeId) 209 93 : .newTree(newTreeId) 210 93 : .newFilePath(pathNew == null ? key.newFilePath() : pathNew) 211 93 : .renameScore(key.renameScore()) 212 93 : .diffAlgorithm(key.diffAlgorithm()) 213 93 : .whitespace(key.whitespace()) 214 93 : .useTimeout(key.useTimeout()) 215 93 : .build(); 216 : } 217 : }