| // 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.common.base.Strings; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ListMultimap; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Patch.ChangeType; |
| import com.google.gerrit.extensions.restapi.BinaryResult; |
| import com.google.gerrit.server.patch.diff.ModifiedFilesCache; |
| import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache; |
| import com.google.gerrit.server.patch.gitdiff.ModifiedFile; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Optional; |
| 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.RevCommit; |
| import org.eclipse.jgit.revwalk.RevTree; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.treewalk.filter.PathFilter; |
| |
| /** |
| * A utility class used by the diff cache interfaces {@link GitModifiedFilesCache} and {@link |
| * ModifiedFilesCache}. |
| */ |
| public class DiffUtil { |
| |
| /** |
| * Return the {@code modifiedFiles} input list while merging rewritten entries. |
| * |
| * <p>Background: In some cases, JGit returns two diff entries (ADDED/DELETED, RENAMED/DELETED, |
| * etc...) for the same file path. This happens e.g. when a file's mode is changed between |
| * patchsets, for example converting a symlink file to a regular file. We identify this case and |
| * return a single modified file with changeType = {@link ChangeType#REWRITE}. |
| */ |
| public static ImmutableList<ModifiedFile> mergeRewrittenModifiedFiles( |
| List<ModifiedFile> modifiedFiles) { |
| ImmutableList.Builder<ModifiedFile> result = ImmutableList.builder(); |
| ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create(); |
| modifiedFiles.stream() |
| .forEach( |
| f -> { |
| if (f.changeType() == ChangeType.DELETED) { |
| byPath.get(f.oldPath().get()).add(f); |
| } else { |
| byPath.get(f.newPath().get()).add(f); |
| } |
| }); |
| for (String path : byPath.keySet()) { |
| List<ModifiedFile> entries = byPath.get(path); |
| if (entries.size() == 1) { |
| result.add(entries.get(0)); |
| } else { |
| // More than one. Return a single REWRITE entry. |
| // Convert the first entry (prioritized according to change type enum order) to REWRITE |
| entries.sort(Comparator.comparingInt(o -> o.changeType().ordinal())); |
| result.add(entries.get(0).toBuilder().changeType(ChangeType.REWRITE).build()); |
| } |
| } |
| return result.build(); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| public static int stringSize(String str) { |
| if (str != null) { |
| // each character in the string occupies 2 bytes. Ignoring the fixed overhead for the string |
| // (length, offset and hash code) since they are negligible and do not affect the comparison |
| // of 2 strings. |
| return str.length() * 2; |
| } |
| return 0; |
| } |
| |
| /** |
| * Get formatted diff between the given commits, either for a single path if specified, or for the |
| * full trees. |
| * |
| * @param repo to get the diff from |
| * @param baseCommit to compare with |
| * @param childCommit to compare |
| * @param path to narrow the diff to |
| * @param out to append the diff to |
| * @throws IOException if the diff couldn't be written |
| */ |
| public static void getFormattedDiff( |
| Repository repo, |
| RevCommit baseCommit, |
| RevCommit childCommit, |
| @Nullable String path, |
| OutputStream out) |
| throws IOException { |
| getFormattedDiff(repo, null, baseCommit.getTree(), childCommit.getTree(), path, out); |
| } |
| |
| public static void getFormattedDiff( |
| Repository repo, |
| @Nullable ObjectReader reader, |
| RevTree baseTree, |
| RevTree childTree, |
| @Nullable String path, |
| OutputStream out) |
| throws IOException { |
| try (DiffFormatter fmt = new DiffFormatter(out)) { |
| fmt.setRepository(repo); |
| if (reader != null) { |
| fmt.setReader(reader, repo.getConfig()); |
| } |
| if (path != null) { |
| fmt.setPathFilter(PathFilter.create(path)); |
| } |
| fmt.format(baseTree, childTree); |
| fmt.flush(); |
| } |
| } |
| |
| public static String normalizePatchForComparison(final String patch) { |
| String res = removePatchHeader(patch); |
| return res |
| // Remove any lines which are not diff lines or file header lines - such index, |
| // hunk-headers, and context lines. |
| .replaceAll("(?m)^[^+-].*", "") |
| .replaceAll("(?m)^[+]{3} [ab]/", "+++ ") |
| .replaceAll("(?m)^-{3} [ab]/", "--- ") |
| // Remove empty lines |
| .replaceAll("\n+", "\n") |
| // Trim |
| .trim(); |
| } |
| |
| public static String removePatchHeader(final String patch) { |
| String res = patch.trim(); |
| if (!res.startsWith("diff --") && res.contains("\ndiff --")) { |
| return res.substring(res.indexOf("\ndiff --")); |
| } |
| return res; |
| } |
| |
| public static Optional<String> getPatchHeader(final String patch) { |
| String res = patch.trim(); |
| if (res.startsWith("diff ")) { |
| return Optional.empty(); |
| } |
| return Optional.ofNullable(Strings.emptyToNull(res.substring(0, res.indexOf("\ndiff ")))); |
| } |
| |
| public static String normalizePatchForComparison(BinaryResult bin) throws IOException { |
| return normalizePatchForComparison(bin.asString()); |
| } |
| |
| 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)); |
| } |
| } |