blob: fb13920a2e4dd02fef542651436f2f7274ef0fe3 [file] [log] [blame]
// 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();
}
}