| // Copyright (C) 2020 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.patch.filediff; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.server.patch.DiffNotAvailableException; |
| import com.google.gerrit.server.patch.DiffUtil; |
| import com.google.gerrit.server.patch.gitfilediff.GitFileDiff; |
| import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCache; |
| import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheKey; |
| import com.google.inject.Inject; |
| import com.google.inject.assistedinject.Assisted; |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.function.Function; |
| import java.util.stream.Collectors; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| /** |
| * A helper class that computes the four {@link GitFileDiff}s for a list of {@link |
| * FileDiffCacheKey}s: |
| * |
| * <ul> |
| * <li>old commit vs. new commit |
| * <li>old parent vs. old commit |
| * <li>new parent vs. new commit |
| * <li>old parent vs. new parent |
| * </ul> |
| * |
| * The four {@link GitFileDiff} are stored in the entity class {@link AllFileGitDiffs}. We use these |
| * diffs to identify the edits due to rebase using the {@link EditTransformer} class. |
| */ |
| class AllDiffsEvaluator { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final RevWalk rw; |
| private final GitFileDiffCache gitCache; |
| |
| interface Factory { |
| AllDiffsEvaluator create(RevWalk rw); |
| } |
| |
| @Inject |
| private AllDiffsEvaluator(GitFileDiffCache gitCache, @Assisted RevWalk rw) { |
| this.gitCache = gitCache; |
| this.rw = rw; |
| } |
| |
| Map<AugmentedFileDiffCacheKey, AllFileGitDiffs> execute( |
| List<AugmentedFileDiffCacheKey> augmentedKeys) throws DiffNotAvailableException { |
| ImmutableMap.Builder<AugmentedFileDiffCacheKey, AllFileGitDiffs> keyToAllDiffs = |
| ImmutableMap.builderWithExpectedSize(augmentedKeys.size()); |
| |
| List<AugmentedFileDiffCacheKey> keysWithRebaseEdits = |
| augmentedKeys.stream().filter(k -> !k.ignoreRebase()).collect(Collectors.toList()); |
| |
| // TODO(ghareeb): as an enhancement, you can batch these calls as follows. |
| // First batch: "old commit vs. new commit" and "new parent vs. new commit" |
| // Second batch: "old parent vs. old commit" and "old parent vs. new parent" |
| |
| ImmutableMap<FileDiffCacheKey, GitDiffEntity> mainDiffs = |
| computeGitFileDiffs( |
| createGitKeys( |
| augmentedKeys, |
| k -> k.key().oldCommit(), |
| k -> k.key().newCommit(), |
| k -> k.key().newFilePath())); |
| |
| ImmutableMap<FileDiffCacheKey, GitDiffEntity> oldVsParentDiffs = |
| computeGitFileDiffs( |
| createGitKeys( |
| keysWithRebaseEdits, |
| k -> k.oldParentId().get(), // oldParent is set for keysWithRebaseEdits |
| k -> k.key().oldCommit(), |
| k -> mainDiffs.get(k.key()).gitDiff().oldPath().orElse(null))); |
| |
| ImmutableMap<FileDiffCacheKey, GitDiffEntity> newVsParentDiffs = |
| computeGitFileDiffs( |
| createGitKeys( |
| keysWithRebaseEdits, |
| k -> k.newParentId().get(), // newParent is set for keysWithRebaseEdits |
| k -> k.key().newCommit(), |
| k -> k.key().newFilePath())); |
| |
| ImmutableMap<FileDiffCacheKey, GitDiffEntity> parentsDiffs = |
| computeGitFileDiffs( |
| createGitKeys( |
| keysWithRebaseEdits, |
| k -> k.oldParentId().get(), |
| k -> k.newParentId().get(), |
| k -> { |
| GitFileDiff newVsParDiff = newVsParentDiffs.get(k.key()).gitDiff(); |
| // TODO(ghareeb): Follow up on replacing key.newFilePath as a fallback. |
| // If the file was added between newParent and newCommit, we actually wouldn't |
| // need to have to determine the oldParent vs. newParent diff as nothing in |
| // that file could be an edit due to rebase anymore. Only if the returned diff |
| // is empty, the oldParent vs. newParent diff becomes relevant again (e.g. to |
| // identify a file deletion which was due to rebase. Check if the structure |
| // can be improved to make this clearer. Can we maybe even skip the diff in |
| // the first situation described? |
| return newVsParDiff.oldPath().orElse(k.key().newFilePath()); |
| })); |
| |
| for (AugmentedFileDiffCacheKey augmentedKey : augmentedKeys) { |
| FileDiffCacheKey key = augmentedKey.key(); |
| AllFileGitDiffs.Builder builder = |
| AllFileGitDiffs.builder().augmentedKey(augmentedKey).mainDiff(mainDiffs.get(key)); |
| |
| if (augmentedKey.ignoreRebase()) { |
| keyToAllDiffs.put(augmentedKey, builder.build()); |
| continue; |
| } |
| |
| if (oldVsParentDiffs.containsKey(key) && !oldVsParentDiffs.get(key).gitDiff().isEmpty()) { |
| builder.oldVsParentDiff(Optional.of(oldVsParentDiffs.get(key))); |
| } |
| |
| if (newVsParentDiffs.containsKey(key) && !newVsParentDiffs.get(key).gitDiff().isEmpty()) { |
| builder.newVsParentDiff(Optional.of(newVsParentDiffs.get(key))); |
| } |
| |
| if (parentsDiffs.containsKey(key) && !parentsDiffs.get(key).gitDiff().isEmpty()) { |
| builder.parentVsParentDiff(Optional.of(parentsDiffs.get(key))); |
| } |
| |
| keyToAllDiffs.put(augmentedKey, builder.build()); |
| } |
| return keyToAllDiffs.build(); |
| } |
| |
| /** |
| * Computes the git diff for the git keys of the input map {@code keys} parameter. The computation |
| * uses the underlying {@link GitFileDiffCache}. |
| */ |
| private ImmutableMap<FileDiffCacheKey, GitDiffEntity> computeGitFileDiffs( |
| Map<FileDiffCacheKey, GitFileDiffCacheKey> keys) throws DiffNotAvailableException { |
| ImmutableMap.Builder<FileDiffCacheKey, GitDiffEntity> result = |
| ImmutableMap.builderWithExpectedSize(keys.size()); |
| ImmutableMap<GitFileDiffCacheKey, GitFileDiff> gitDiffs = gitCache.getAll(keys.values()); |
| for (FileDiffCacheKey key : keys.keySet()) { |
| GitFileDiffCacheKey gitKey = keys.get(key); |
| GitFileDiff gitFileDiff = gitDiffs.get(gitKey); |
| result.put(key, GitDiffEntity.create(gitKey, gitFileDiff)); |
| } |
| return result.build(); |
| } |
| |
| /** |
| * Convert a list of {@link AugmentedFileDiffCacheKey} to their corresponding {@link |
| * GitFileDiffCacheKey} which can be used to call the underlying {@link GitFileDiffCache}. |
| * |
| * @param keys a list of input {@link AugmentedFileDiffCacheKey}s. |
| * @param aCommitFn a function to compute the aCommit that will be used in the git diff. |
| * @param bCommitFn a function to compute the bCommit that will be used in the git diff. |
| * @param newPathFn a function to compute the new path of the git key. |
| * @return a map of the input {@link FileDiffCacheKey} to the {@link GitFileDiffCacheKey}. |
| */ |
| private Map<FileDiffCacheKey, GitFileDiffCacheKey> createGitKeys( |
| List<AugmentedFileDiffCacheKey> keys, |
| Function<AugmentedFileDiffCacheKey, ObjectId> aCommitFn, |
| Function<AugmentedFileDiffCacheKey, ObjectId> bCommitFn, |
| Function<AugmentedFileDiffCacheKey, String> newPathFn) { |
| Map<FileDiffCacheKey, GitFileDiffCacheKey> result = new HashMap<>(); |
| for (AugmentedFileDiffCacheKey key : keys) { |
| try { |
| String path = newPathFn.apply(key); |
| if (path != null) { |
| result.put( |
| key.key(), |
| createGitKey(key.key(), aCommitFn.apply(key), bCommitFn.apply(key), path, rw)); |
| } |
| } catch (IOException e) { |
| // TODO(ghareeb): This implies that the output keys may not have the same size as the input. |
| // Check the caller's code path about the correctness of the computation in this case. If |
| // errors are rare, it may be better to throw an exception and fail the whole computation. |
| logger.atWarning().log("Failed to compute the git key for key %s: %s", key, e.getMessage()); |
| } |
| } |
| return result; |
| } |
| |
| /** Returns the {@link GitFileDiffCacheKey} for the {@code key} input parameter. */ |
| private GitFileDiffCacheKey createGitKey( |
| FileDiffCacheKey key, ObjectId aCommit, ObjectId bCommit, String pathNew, RevWalk rw) |
| throws IOException { |
| ObjectId oldTreeId = |
| aCommit.equals(ObjectId.zeroId()) ? ObjectId.zeroId() : DiffUtil.getTreeId(rw, aCommit); |
| ObjectId newTreeId = DiffUtil.getTreeId(rw, bCommit); |
| return GitFileDiffCacheKey.builder() |
| .project(key.project()) |
| .oldTree(oldTreeId) |
| .newTree(newTreeId) |
| .newFilePath(pathNew == null ? key.newFilePath() : pathNew) |
| .renameScore(key.renameScore()) |
| .diffAlgorithm(key.diffAlgorithm()) |
| .whitespace(key.whitespace()) |
| .useTimeout(key.useTimeout()) |
| .build(); |
| } |
| } |