Add the GitModifiedFilesCache and ModifiedFilesCache
The two new caches are part of the refactoring done to the Diff cache,
previously implemented in PatchListLoader.java.
The 2 new caches compute the set of modified files (added, deleted,
modified, etc...) between 2 specific commits.
1) The GitModifiedFilesCache computes the modified files between two
commits according to a Git (tree) diff. File contents are not available
with the cache values. The cache key uses the SHA-1s of the trees
instead of the SHA-1s of the commits. This allows the re-use of
computations when commits keep their trees but change in other ways
(e.g. for different commit messages).
2) The ModifiedFilesCache is a wrapper on top of the first cache and
adds extra Gerrit logic. This cache filters out the files that were not
touched between the 2 commits, previously implemented in
PatchListLoader. Similar to the first cache, the file contents and edits
are not available with the cache values.
For now, the magic file paths "/COMMIT_MSG", and "/MERGE_LIST" are not
returned from this cache. In a follow up change, the single file diff
caches will be added and the caller can query the magic file paths from
them. I excluded them from here because:
* Checking for the cases when we should compute the merge list (e.g.
determining if the diff is against base / patchsets) would require
adding some more logic.
* For commit messages between patchsets, to determine if there's a
change or not, we would need to parse the actual commit message text.
In the next follow up change, I will implement the protobuf
serialization for the cache entities as marked with TODOs in this
change.
Change-Id: I0c6fc4e015009624c4f93471a2e6335d180f6c82
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 605b493..512e0cb 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -49,6 +49,7 @@
* commons:compress
* commons:dbcp
* commons:lang
+* commons:lang3
* commons:net
* commons:pool
* commons:validator
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 069006b..b7a00dd 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -113,6 +113,7 @@
"//lib/commons:compress",
"//lib/commons:dbcp",
"//lib/commons:lang",
+ "//lib/commons:lang3",
"//lib/commons:net",
"//lib/commons:validator",
"//lib/errorprone:annotations",
diff --git a/java/com/google/gerrit/server/patch/DiffNotAvailableException.java b/java/com/google/gerrit/server/patch/DiffNotAvailableException.java
new file mode 100644
index 0000000..34e1577
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/DiffNotAvailableException.java
@@ -0,0 +1,28 @@
+// 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;
+
+import com.google.gerrit.server.patch.diff.ModifiedFilesCache;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
+
+/**
+ * Thrown by the diff caches - the {@link GitModifiedFilesCache} and the {@link ModifiedFilesCache},
+ * if the implementations failed to retrieve the modified files between the 2 commits.
+ */
+public class DiffNotAvailableException extends Exception {
+ public DiffNotAvailableException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/DiffUtil.java b/java/com/google/gerrit/server/patch/DiffUtil.java
new file mode 100644
index 0000000..9198666
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/DiffUtil.java
@@ -0,0 +1,79 @@
+// 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;
+
+import com.google.gerrit.server.patch.diff.ModifiedFilesCache;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
+import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * A utility class used by the diff cache interfaces {@link GitModifiedFilesCache} and {@link
+ * ModifiedFilesCache}.
+ */
+public class DiffUtil {
+
+ /**
+ * Returns the Git tree object ID pointed to by the commitId parameter.
+ *
+ * @param rw a {@link RevWalk} of an opened repository that is used to walk the commit graph.
+ * @param commitId 20 bytes commitId SHA-1 hash.
+ * @return Git tree object ID pointed to by the commitId.
+ */
+ public static ObjectId getTreeId(RevWalk rw, ObjectId commitId) throws IOException {
+ RevCommit current = rw.parseCommit(commitId);
+ return current.getTree().getId();
+ }
+
+ /**
+ * Returns the RevCommit object given the 20 bytes commitId SHA-1 hash.
+ *
+ * @param rw a {@link RevWalk} of an opened repository that is used to walk the commit graph.
+ * @param commitId 20 bytes commitId SHA-1 hash
+ * @return The RevCommit representing the commit in Git
+ * @throws IOException a pack file or loose object could not be read while parsing the commits.
+ */
+ public static RevCommit getRevCommit(RevWalk rw, ObjectId commitId) throws IOException {
+ return rw.parseCommit(commitId);
+ }
+
+ /**
+ * Returns true if the commitA and commitB parameters are parent/child, if they have a common
+ * parent, or if any of them is a root or merge commit.
+ */
+ public static boolean areRelated(RevCommit commitA, RevCommit commitB) {
+ return commitA == null
+ || isRootOrMergeCommit(commitA)
+ || isRootOrMergeCommit(commitB)
+ || areParentAndChild(commitA, commitB)
+ || haveCommonParent(commitA, commitB);
+ }
+
+ private static boolean isRootOrMergeCommit(RevCommit commit) {
+ return commit.getParentCount() != 1;
+ }
+
+ private static boolean areParentAndChild(RevCommit commitA, RevCommit commitB) {
+ return ObjectId.isEqual(commitA.getParent(0), commitB)
+ || ObjectId.isEqual(commitB.getParent(0), commitA);
+ }
+
+ private static boolean haveCommonParent(RevCommit commitA, RevCommit commitB) {
+ return ObjectId.isEqual(commitA.getParent(0), commitB.getParent(0));
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCache.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCache.java
new file mode 100644
index 0000000..9fc8150
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCache.java
@@ -0,0 +1,44 @@
+// 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.diff;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCacheImpl;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
+
+/**
+ * A cache for the list of Git modified files between 2 commits (patchsets) with extra Gerrit logic.
+ *
+ * <p>The loader uses the underlying {@link GitModifiedFilesCacheImpl} to retrieve the git modified
+ * files.
+ *
+ * <p>If the {@link ModifiedFilesCacheImpl.Key#aCommit()} is equal to {@link
+ * org.eclipse.jgit.lib.Constants#EMPTY_TREE_ID}, the diff will be evaluated against the empty tree,
+ * and the result will be exactly the same as the caller can get from {@link
+ * GitModifiedFilesCache#get(GitModifiedFilesCacheImpl.Key)}
+ */
+public interface ModifiedFilesCache {
+
+ /**
+ * @param key used to identify two git commits and contains other attributes to control the diff
+ * calculation.
+ * @return the list of {@link ModifiedFile}s between the 2 git commits identified by the key.
+ * @throws DiffNotAvailableException the supplied commits IDs of the key do no exist, are not IDs
+ * of a commit, or an exception occurred while reading a pack file.
+ */
+ ImmutableList<ModifiedFile> get(ModifiedFilesCacheImpl.Key key) throws DiffNotAvailableException;
+}
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
new file mode 100644
index 0000000..1e6b0f8
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
@@ -0,0 +1,264 @@
+// 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.diff;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffUtil;
+import com.google.gerrit.server.patch.diff.ModifiedFilesCacheImpl.Key.Serializer;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCacheImpl;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * A cache for the list of Git modified files between 2 commits (patchsets) with extra Gerrit logic.
+ *
+ * <p>The loader of this cache wraps a {@link GitModifiedFilesCache} to retrieve the git modified
+ * files.
+ *
+ * <p>If the {@link Key#aCommit()} is equal to {@link org.eclipse.jgit.lib.Constants#EMPTY_TREE_ID},
+ * the diff will be evaluated against the empty tree, and the result will be exactly the same as the
+ * caller can get from {@link GitModifiedFilesCache#get(GitModifiedFilesCacheImpl.Key)}
+ */
+public class ModifiedFilesCacheImpl implements ModifiedFilesCache {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final String MODIFIED_FILES = "modified_files";
+
+ private final LoadingCache<Key, ImmutableList<ModifiedFile>> cache;
+
+ public static Module module() {
+ return new CacheModule() {
+ @Override
+ protected void configure() {
+ bind(ModifiedFilesCache.class).to(ModifiedFilesCacheImpl.class);
+
+ // The documentation has some defaults and recommendations for setting the cache
+ // attributes:
+ // https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#cache.
+ // The cache is using the default disk limit as per section cache.<name>.diskLimit
+ // in the cache documentation link.
+ persist(
+ ModifiedFilesCacheImpl.MODIFIED_FILES,
+ Key.class,
+ new TypeLiteral<ImmutableList<ModifiedFile>>() {})
+ .keySerializer(Serializer.INSTANCE)
+ .valueSerializer(GitModifiedFilesCacheImpl.ValueSerializer.INSTANCE)
+ .maximumWeight(10 << 20)
+ .weigher(ModifiedFilesWeigher.class)
+ .version(1)
+ .loader(ModifiedFilesLoader.class);
+ }
+ };
+ }
+
+ @Inject
+ public ModifiedFilesCacheImpl(
+ @Named(ModifiedFilesCacheImpl.MODIFIED_FILES)
+ LoadingCache<Key, ImmutableList<ModifiedFile>> cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public ImmutableList<ModifiedFile> get(Key key) throws DiffNotAvailableException {
+ try {
+ return cache.get(key);
+ } catch (Exception e) {
+ throw new DiffNotAvailableException(e);
+ }
+ }
+
+ static class ModifiedFilesLoader extends CacheLoader<Key, ImmutableList<ModifiedFile>> {
+ private final GitModifiedFilesCache gitCache;
+ private final GitRepositoryManager repoManager;
+
+ @Inject
+ ModifiedFilesLoader(GitModifiedFilesCache gitCache, GitRepositoryManager repoManager) {
+ this.gitCache = gitCache;
+ this.repoManager = repoManager;
+ }
+
+ @Override
+ public ImmutableList<ModifiedFile> load(Key key) throws IOException, DiffNotAvailableException {
+ try (Repository repo = repoManager.openRepository(key.project());
+ RevWalk rw = new RevWalk(repo.newObjectReader())) {
+ return loadModifiedFiles(key, rw);
+ }
+ }
+
+ private ImmutableList<ModifiedFile> loadModifiedFiles(Key key, RevWalk rw)
+ throws IOException, DiffNotAvailableException {
+ ObjectId aTree =
+ key.aCommit().equals(EMPTY_TREE_ID)
+ ? key.aCommit()
+ : DiffUtil.getTreeId(rw, key.aCommit());
+ ObjectId bTree = DiffUtil.getTreeId(rw, key.bCommit());
+ GitModifiedFilesCacheImpl.Key gitKey =
+ GitModifiedFilesCacheImpl.Key.builder()
+ .project(key.project())
+ .aTree(aTree)
+ .bTree(bTree)
+ .renameScore(key.renameScore())
+ .build();
+ List<ModifiedFile> modifiedFiles = gitCache.get(gitKey);
+ if (key.aCommit().equals(EMPTY_TREE_ID)) {
+ return ImmutableList.copyOf(modifiedFiles);
+ }
+ RevCommit revCommitA = DiffUtil.getRevCommit(rw, key.aCommit());
+ RevCommit revCommitB = DiffUtil.getRevCommit(rw, key.bCommit());
+ if (DiffUtil.areRelated(revCommitA, revCommitB)) {
+ return ImmutableList.copyOf(modifiedFiles);
+ }
+ Set<String> touchedFiles =
+ getTouchedFilesWithParents(
+ key, revCommitA.getParent(0).getId(), revCommitB.getParent(0).getId(), rw);
+ return modifiedFiles.stream()
+ .filter(f -> isTouched(touchedFiles, f))
+ .collect(toImmutableList());
+ }
+
+ /**
+ * Returns the paths of files that were modified between the old and new commits versus their
+ * parents (i.e. old commit vs. its parent, and new commit vs. its parent).
+ *
+ * @param key the {@link Key} representing the commits we are diffing
+ * @param rw a {@link RevWalk} for the repository
+ * @return The list of modified files between the old/new commits and their parents
+ */
+ private Set<String> getTouchedFilesWithParents(
+ Key key, ObjectId parentOfA, ObjectId parentOfB, RevWalk rw) throws IOException {
+ try {
+ // TODO(ghareeb): as an enhancement: the 3 calls of the underlying git cache can be combined
+ GitModifiedFilesCacheImpl.Key oldVsBaseKey =
+ GitModifiedFilesCacheImpl.Key.create(
+ key.project(), parentOfA, key.aCommit(), key.renameScore(), rw);
+ List<ModifiedFile> oldVsBase = gitCache.get(oldVsBaseKey);
+
+ GitModifiedFilesCacheImpl.Key newVsBaseKey =
+ GitModifiedFilesCacheImpl.Key.create(
+ key.project(), parentOfB, key.bCommit(), key.renameScore(), rw);
+ List<ModifiedFile> newVsBase = gitCache.get(newVsBaseKey);
+
+ return Sets.union(getOldAndNewPaths(oldVsBase), getOldAndNewPaths(newVsBase));
+ } catch (DiffNotAvailableException e) {
+ logger.atWarning().log(
+ "Failed to retrieve the touched files' commits (%s, %s) and parents (%s, %s): %s",
+ key.aCommit(), key.bCommit(), parentOfA, parentOfB, e.getMessage());
+ return ImmutableSet.of();
+ }
+ }
+
+ private ImmutableSet<String> getOldAndNewPaths(List<ModifiedFile> files) {
+ return files.stream()
+ .flatMap(
+ file -> Stream.concat(Streams.stream(file.oldPath()), Streams.stream(file.newPath())))
+ .collect(ImmutableSet.toImmutableSet());
+ }
+
+ private static boolean isTouched(Set<String> touchedFilePaths, ModifiedFile modifiedFile) {
+ String oldFilePath = modifiedFile.oldPath().orElse(null);
+ String newFilePath = modifiedFile.newPath().orElse(null);
+ // One of the above file paths could be /dev/null but we need not explicitly check for this
+ // value as the set of file paths shouldn't contain it.
+ return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath);
+ }
+ }
+
+ @AutoValue
+ public abstract static class Key implements Serializable {
+ public abstract Project.NameKey project();
+
+ /** @return the old commit ID used in the git tree diff */
+ public abstract ObjectId aCommit();
+
+ /** @return the new commit ID used in the git tree diff */
+ public abstract ObjectId bCommit();
+
+ public abstract boolean renameDetectionFlag();
+
+ /**
+ * Percentage score used to identify a file as a "rename". A special value of -1 means that the
+ * computation will ignore renames and rename detection will be disabled.
+ */
+ public abstract int renameScore();
+
+ public int weight() {
+ return project().get().length() + 20 * 2 + 4;
+ }
+
+ public static Builder builder() {
+ return new AutoValue_ModifiedFilesCacheImpl_Key.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder project(NameKey value);
+
+ public abstract Builder aCommit(ObjectId value);
+
+ public abstract Builder bCommit(ObjectId value);
+
+ public abstract Builder renameDetectionFlag(boolean value);
+
+ public abstract Builder renameScore(int value);
+
+ public abstract Key build();
+ }
+
+ enum Serializer implements CacheSerializer<Key> {
+ INSTANCE;
+
+ @Override
+ public byte[] serialize(Key object) {
+ // TODO(ghareeb): implement protobuf serialization
+ return new byte[0];
+ }
+
+ @Override
+ public Key deserialize(byte[] in) {
+ return null;
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesWeigher.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesWeigher.java
new file mode 100644
index 0000000..0b2c69e
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesWeigher.java
@@ -0,0 +1,31 @@
+// 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.diff;
+
+import com.google.common.cache.Weigher;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
+
+public class ModifiedFilesWeigher
+ implements Weigher<ModifiedFilesCacheImpl.Key, ImmutableList<ModifiedFile>> {
+ @Override
+ public int weigh(ModifiedFilesCacheImpl.Key key, ImmutableList<ModifiedFile> modifiedFiles) {
+ int weight = key.weight();
+ for (ModifiedFile modifiedFile : modifiedFiles) {
+ weight += modifiedFile.weight();
+ }
+ return weight;
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCache.java b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCache.java
new file mode 100644
index 0000000..36af9fe
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCache.java
@@ -0,0 +1,41 @@
+// 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.gitdiff;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.diff.ModifiedFilesCache;
+
+/**
+ * A cache interface for identifying the list of Git modified files between 2 different git trees.
+ * This cache does not read the actual file contents, nor does it include the edits (modified
+ * regions) of the file.
+ *
+ * <p>The other {@link ModifiedFilesCache} is similar to this cache, and includes other extra Gerrit
+ * logic that we need to add with the list of modified files.
+ */
+public interface GitModifiedFilesCache {
+
+ /**
+ * Computes the list of of {@link ModifiedFile}s between the 2 git trees.
+ *
+ * @param key used to identify two git trees and contains other attributes to control the diff
+ * calculation.
+ * @return the list of {@link ModifiedFile}s between the 2 git trees identified by the key.
+ * @throws DiffNotAvailableException trees cannot be read or file contents cannot be read.
+ */
+ ImmutableList<ModifiedFile> get(GitModifiedFilesCacheImpl.Key key)
+ throws DiffNotAvailableException;
+}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java
new file mode 100644
index 0000000..bbdf814
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java
@@ -0,0 +1,258 @@
+// 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.gitdiff;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffUtil;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+
+/** Implementation of the {@link GitModifiedFilesCache} */
+public class GitModifiedFilesCacheImpl implements GitModifiedFilesCache {
+ private static final String GIT_MODIFIED_FILES = "git_modified_files";
+ private static final ImmutableMap<ChangeType, Patch.ChangeType> changeTypeMap =
+ ImmutableMap.of(
+ DiffEntry.ChangeType.ADD,
+ Patch.ChangeType.ADDED,
+ DiffEntry.ChangeType.MODIFY,
+ Patch.ChangeType.MODIFIED,
+ DiffEntry.ChangeType.DELETE,
+ Patch.ChangeType.DELETED,
+ DiffEntry.ChangeType.RENAME,
+ Patch.ChangeType.RENAMED,
+ DiffEntry.ChangeType.COPY,
+ Patch.ChangeType.COPIED);
+
+ private LoadingCache<Key, ImmutableList<ModifiedFile>> cache;
+
+ public static Module module() {
+ return new CacheModule() {
+ @Override
+ protected void configure() {
+ bind(GitModifiedFilesCache.class).to(GitModifiedFilesCacheImpl.class);
+
+ persist(GIT_MODIFIED_FILES, Key.class, new TypeLiteral<ImmutableList<ModifiedFile>>() {})
+ .keySerializer(Key.KeySerializer.INSTANCE)
+ .valueSerializer(ValueSerializer.INSTANCE)
+ // The documentation has some defaults and recommendations for setting the cache
+ // attributes:
+ // https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#cache.
+ .maximumWeight(10 << 20)
+ .weigher(GitModifiedFilesWeigher.class)
+ // The cache is using the default disk limit as per section cache.<name>.diskLimit
+ // in the cache documentation link.
+ .version(1)
+ .loader(GitModifiedFilesCacheImpl.Loader.class);
+ }
+ };
+ }
+
+ @Inject
+ public GitModifiedFilesCacheImpl(
+ @Named(GIT_MODIFIED_FILES) LoadingCache<Key, ImmutableList<ModifiedFile>> cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public ImmutableList<ModifiedFile> get(Key key) throws DiffNotAvailableException {
+ try {
+ return cache.get(key);
+ } catch (ExecutionException e) {
+ throw new DiffNotAvailableException(e);
+ }
+ }
+
+ static class Loader extends CacheLoader<Key, ImmutableList<ModifiedFile>> {
+ private final GitRepositoryManager repoManager;
+
+ @Inject
+ Loader(GitRepositoryManager repoManager) {
+ this.repoManager = repoManager;
+ }
+
+ @Override
+ public ImmutableList<ModifiedFile> load(Key key) throws IOException {
+ try (Repository repo = repoManager.openRepository(key.project());
+ ObjectReader reader = repo.newObjectReader()) {
+ List<DiffEntry> entries = getGitTreeDiff(repo, reader, key);
+
+ return entries.stream().map(Loader::toModifiedFile).collect(toImmutableList());
+ }
+ }
+
+ private List<DiffEntry> getGitTreeDiff(
+ Repository repo, ObjectReader reader, GitModifiedFilesCacheImpl.Key key)
+ throws IOException {
+ try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+ df.setReader(reader, repo.getConfig());
+ if (key.renameDetection()) {
+ df.setDetectRenames(true);
+ df.getRenameDetector().setRenameScore(key.renameScore());
+ }
+ // The scan method only returns the file paths that are different. Callers may choose to
+ // format these paths themselves.
+ return df.scan(key.aTree(), key.bTree());
+ }
+ }
+
+ private static ModifiedFile toModifiedFile(DiffEntry entry) {
+ String oldPath = entry.getOldPath();
+ String newPath = entry.getNewPath();
+ return ModifiedFile.builder()
+ .changeType(toChangeType(entry.getChangeType()))
+ .oldPath(oldPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(oldPath))
+ .newPath(newPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(newPath))
+ .build();
+ }
+
+ private static Patch.ChangeType toChangeType(DiffEntry.ChangeType changeType) {
+ if (!changeTypeMap.containsKey(changeType)) {
+ throw new IllegalArgumentException("Unsupported type " + changeType);
+ }
+ return changeTypeMap.get(changeType);
+ }
+ }
+
+ /**
+ * In this cache, we evaluate the diffs between two git trees (instead of git commits), hence the
+ * key contains the tree IDs.
+ */
+ @AutoValue
+ public abstract static class Key {
+
+ public abstract Project.NameKey project();
+
+ /**
+ * The git SHA-1 {@link ObjectId} of the first git tree object for which the diff should be
+ * computed.
+ */
+ public abstract ObjectId aTree();
+
+ /**
+ * The git SHA-1 {@link ObjectId} of the second git tree object for which the diff should be
+ * computed.
+ */
+ public abstract ObjectId bTree();
+
+ /**
+ * Percentage score used to identify a file as a rename. This value is only available if {@link
+ * #renameDetection()} is true. Otherwise, this method will return -1.
+ *
+ * <p>This value will be used to set the rename score of {@link
+ * DiffFormatter#getRenameDetector()}.
+ */
+ public abstract int renameScore();
+
+ /** Returns true if rename detection was set for this key. */
+ public boolean renameDetection() {
+ return renameScore() != -1;
+ }
+
+ public static Key create(
+ Project.NameKey project, ObjectId aCommit, ObjectId bCommit, int renameScore, RevWalk rw)
+ throws IOException {
+ ObjectId aTree = DiffUtil.getTreeId(rw, aCommit);
+ ObjectId bTree = DiffUtil.getTreeId(rw, bCommit);
+ return builder().project(project).aTree(aTree).bTree(bTree).renameScore(renameScore).build();
+ }
+
+ public static Builder builder() {
+ return new AutoValue_GitModifiedFilesCacheImpl_Key.Builder();
+ }
+
+ /** Returns the size of the object in bytes */
+ public int weight() {
+ return project().get().length()
+ + 20 * 2 // old and new tree IDs
+ + 4; // rename score
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder project(NameKey value);
+
+ public abstract Builder aTree(ObjectId value);
+
+ public abstract Builder bTree(ObjectId value);
+
+ public abstract Builder renameScore(int value);
+
+ public Builder disableRenameDetection() {
+ renameScore(-1);
+ return this;
+ }
+
+ public abstract Key build();
+ }
+
+ // TODO(ghareeb): Implement protobuf serialization
+ enum KeySerializer implements CacheSerializer<Key> {
+ INSTANCE;
+
+ @Override
+ public byte[] serialize(Key object) {
+ return null;
+ }
+
+ @Override
+ public Key deserialize(byte[] in) {
+ return null;
+ }
+ }
+ }
+
+ // TODO(ghareeb): Implement protobuf serialization
+ public enum ValueSerializer implements CacheSerializer<ImmutableList<ModifiedFile>> {
+ INSTANCE;
+
+ @Override
+ public byte[] serialize(ImmutableList<ModifiedFile> modifiedFiles) {
+ return new byte[0];
+ }
+
+ @Override
+ public ImmutableList<ModifiedFile> deserialize(byte[] in) {
+ return null;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesWeigher.java b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesWeigher.java
new file mode 100644
index 0000000..f096cef
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesWeigher.java
@@ -0,0 +1,26 @@
+// 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.gitdiff;
+
+import com.google.common.cache.Weigher;
+import com.google.common.collect.ImmutableList;
+
+public class GitModifiedFilesWeigher
+ implements Weigher<GitModifiedFilesCacheImpl.Key, ImmutableList<ModifiedFile>> {
+ @Override
+ public int weigh(GitModifiedFilesCacheImpl.Key key, ImmutableList<ModifiedFile> modifiedFiles) {
+ return key.weight() + modifiedFiles.stream().mapToInt(ModifiedFile::weight).sum();
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
new file mode 100644
index 0000000..1ca683f
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
@@ -0,0 +1,90 @@
+// 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.gitdiff;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import java.util.Optional;
+import org.apache.commons.lang.NotImplementedException;
+
+/**
+ * An entity representing a Modified file due to a diff between 2 git trees. This entity contains
+ * the change type and the old & new paths, but does not include any actual content diff of the
+ * file.
+ */
+@AutoValue
+public abstract class ModifiedFile {
+ /**
+ * Returns the change type (i.e. add, delete, modify, rename, etc...) associated with this
+ * modified file.
+ */
+ public abstract ChangeType changeType();
+
+ /**
+ * Returns the old name associated with this file. An empty optional is returned if {@link
+ * #changeType()} is equal to {@link ChangeType#ADDED}.
+ */
+ public abstract Optional<String> oldPath();
+
+ /**
+ * Returns the new name associated with this file. An empty optional is returned if {@link
+ * #changeType()} is equal to {@link ChangeType#DELETED}
+ */
+ public abstract Optional<String> newPath();
+
+ public static Builder builder() {
+ return new AutoValue_ModifiedFile.Builder();
+ }
+
+ /** Computes this object's weight, which is its size in bytes. */
+ public int weight() {
+ int weight = 1; // the changeType field
+ if (oldPath().isPresent()) {
+ weight += oldPath().get().length();
+ }
+ if (newPath().isPresent()) {
+ weight += newPath().get().length();
+ }
+ return weight;
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder changeType(ChangeType value);
+
+ public abstract Builder oldPath(Optional<String> value);
+
+ public abstract Builder newPath(Optional<String> value);
+
+ public abstract ModifiedFile build();
+ }
+
+ // TODO(ghareeb): Implement protobuf serialization
+ enum Serializer implements CacheSerializer<ModifiedFile> {
+ INSTANCE;
+
+ @Override
+ public byte[] serialize(ModifiedFile object) {
+ throw new NotImplementedException("This method is not yet implemented");
+ }
+
+ @Override
+ public ModifiedFile deserialize(byte[] in) {
+ throw new NotImplementedException("This method is not yet implemented");
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 82215b6..9fa562a 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -118,6 +118,12 @@
assertDiffForNewFile(result, COMMIT_MSG, result.getCommit().getFullMessage());
}
+ @Ignore
+ @Test
+ public void diffWithRootCommit() throws Exception {
+ // TODO(ghareeb): Implement this test
+ }
+
@Test
public void patchsetLevelFileDiffIsEmpty() throws Exception {
PushOneCommit.Result result = createChange();