| // 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 static com.google.gerrit.server.patch.DiffUtil.stringSize; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.collect.ImmutableList; |
| import com.google.gerrit.entities.Patch; |
| import com.google.gerrit.entities.Patch.ChangeType; |
| import com.google.gerrit.entities.Patch.PatchType; |
| import com.google.gerrit.proto.Protos; |
| import com.google.gerrit.server.cache.proto.Cache.FileDiffOutputProto; |
| import com.google.gerrit.server.cache.serialize.CacheSerializer; |
| import com.google.gerrit.server.cache.serialize.ObjectIdConverter; |
| import com.google.gerrit.server.patch.ComparisonType; |
| import com.google.protobuf.Descriptors.FieldDescriptor; |
| import java.io.Serializable; |
| import java.util.Optional; |
| import java.util.stream.Collectors; |
| import org.eclipse.jgit.lib.ObjectId; |
| |
| /** File diff for a single file path. Produced as output of the {@link FileDiffCache}. */ |
| @AutoValue |
| public abstract class FileDiffOutput implements Serializable { |
| private static final long serialVersionUID = 1L; |
| |
| /** |
| * The 20 bytes SHA-1 object ID of the old git commit used in the diff, or {@link |
| * ObjectId#zeroId()} if {@link #newCommitId()} was a root commit. |
| */ |
| public abstract ObjectId oldCommitId(); |
| |
| /** The 20 bytes SHA-1 object ID of the new git commit used in the diff. */ |
| public abstract ObjectId newCommitId(); |
| |
| /** Comparison type of old and new commits: against another patchset, parent or auto-merge. */ |
| public abstract ComparisonType comparisonType(); |
| |
| /** |
| * The file path at the old commit. Returns an empty Optional if {@link #changeType()} is equal to |
| * {@link ChangeType#ADDED}. |
| */ |
| public abstract Optional<String> oldPath(); |
| |
| /** |
| * The file path at the new commit. Returns an empty optional if {@link #changeType()} is equal to |
| * {@link ChangeType#DELETED}. |
| */ |
| public abstract Optional<String> newPath(); |
| |
| /** The change type of the underlying file, e.g. added, deleted, renamed, etc... */ |
| public abstract Patch.ChangeType changeType(); |
| |
| /** The patch type of the underlying file, e.g. unified, binary , etc... */ |
| public abstract Optional<Patch.PatchType> patchType(); |
| |
| /** |
| * A list of strings representation of the header lines of the {@link |
| * org.eclipse.jgit.patch.FileHeader} that is produced as output of the diff. |
| */ |
| public abstract ImmutableList<String> headerLines(); |
| |
| /** The list of edits resulting from the diff hunks of the file. */ |
| public abstract ImmutableList<TaggedEdit> edits(); |
| |
| /** The file size at the new commit. */ |
| public abstract long size(); |
| |
| /** Difference in file size between the old and new commits. */ |
| public abstract long sizeDelta(); |
| |
| /** |
| * Returns {@code true} if the diff computation was not able to compute a diff, i.e. for diffs |
| * taking a very long time to compute. We cache negative result in this case. |
| */ |
| public abstract Optional<Boolean> negative(); |
| |
| public abstract Builder toBuilder(); |
| |
| /** A boolean indicating if all underlying edits of the file diff are due to rebase. */ |
| public boolean allEditsDueToRebase() { |
| return !edits().isEmpty() && edits().stream().allMatch(TaggedEdit::dueToRebase); |
| } |
| |
| /** Returns the number of inserted lines for the file diff. */ |
| public int insertions() { |
| int ins = 0; |
| for (TaggedEdit e : edits()) { |
| if (!e.dueToRebase()) { |
| ins += e.edit().endB() - e.edit().beginB(); |
| } |
| } |
| return ins; |
| } |
| |
| /** Returns the number of deleted lines for the file diff. */ |
| public int deletions() { |
| int del = 0; |
| for (TaggedEdit e : edits()) { |
| if (!e.dueToRebase()) { |
| del += e.edit().endA() - e.edit().beginA(); |
| } |
| } |
| return del; |
| } |
| |
| /** Returns an entity representing an unchanged file between two commits. */ |
| public static FileDiffOutput empty(String filePath, ObjectId oldCommitId, ObjectId newCommitId) { |
| return builder() |
| .oldCommitId(oldCommitId) |
| .newCommitId(newCommitId) |
| .comparisonType(ComparisonType.againstOtherPatchSet()) // not important |
| .oldPath(Optional.empty()) |
| .newPath(Optional.of(filePath)) |
| .changeType(ChangeType.MODIFIED) |
| .headerLines(ImmutableList.of()) |
| .edits(ImmutableList.of()) |
| .size(0) |
| .sizeDelta(0) |
| .build(); |
| } |
| |
| /** |
| * Create a negative file diff. We use this to cache negative diffs for entries that result in |
| * timeouts. |
| */ |
| public static FileDiffOutput createNegative( |
| String filePath, ObjectId oldCommitId, ObjectId newCommitId) { |
| return empty(filePath, oldCommitId, newCommitId) |
| .toBuilder() |
| .negative(Optional.of(true)) |
| .build(); |
| } |
| |
| /** Returns true if this entity represents an unchanged file between two commits. */ |
| public boolean isEmpty() { |
| return headerLines().isEmpty() && edits().isEmpty(); |
| } |
| |
| /** |
| * Returns {@code true} if the diff computation was not able to compute a diff. We cache negative |
| * result in this case. |
| */ |
| public boolean isNegative() { |
| return negative().isPresent() && negative().get(); |
| } |
| |
| public static Builder builder() { |
| return new AutoValue_FileDiffOutput.Builder(); |
| } |
| |
| public int weight() { |
| int result = 0; |
| if (oldPath().isPresent()) { |
| result += stringSize(oldPath().get()); |
| } |
| if (newPath().isPresent()) { |
| result += stringSize(newPath().get()); |
| } |
| result += 20 + 20; // old and new commit IDs |
| result += 4; // comparison type |
| result += 4; // changeType |
| if (patchType().isPresent()) { |
| result += 4; |
| } |
| result += 4 + 4; // insertions and deletions |
| result += 4 + 4; // size and size delta |
| result += 20 * edits().size(); // each edit is 4 Integers + boolean = 4 * 4 + 4 = 20 |
| for (String s : headerLines()) { |
| s += stringSize(s); |
| } |
| if (negative().isPresent()) { |
| result += 1; |
| } |
| return result; |
| } |
| |
| @AutoValue.Builder |
| public abstract static class Builder { |
| |
| public abstract Builder oldCommitId(ObjectId value); |
| |
| public abstract Builder newCommitId(ObjectId value); |
| |
| public abstract Builder comparisonType(ComparisonType value); |
| |
| public abstract Builder oldPath(Optional<String> value); |
| |
| public abstract Builder newPath(Optional<String> value); |
| |
| public abstract Builder changeType(ChangeType value); |
| |
| public abstract Builder patchType(Optional<PatchType> value); |
| |
| public abstract Builder headerLines(ImmutableList<String> value); |
| |
| public abstract Builder edits(ImmutableList<TaggedEdit> value); |
| |
| public abstract Builder size(long value); |
| |
| public abstract Builder sizeDelta(long value); |
| |
| public abstract Builder negative(Optional<Boolean> value); |
| |
| public abstract FileDiffOutput build(); |
| } |
| |
| public enum Serializer implements CacheSerializer<FileDiffOutput> { |
| INSTANCE; |
| |
| private static final FieldDescriptor OLD_PATH_DESCRIPTOR = |
| FileDiffOutputProto.getDescriptor().findFieldByNumber(1); |
| |
| private static final FieldDescriptor NEW_PATH_DESCRIPTOR = |
| FileDiffOutputProto.getDescriptor().findFieldByNumber(2); |
| |
| private static final FieldDescriptor PATCH_TYPE_DESCRIPTOR = |
| FileDiffOutputProto.getDescriptor().findFieldByNumber(4); |
| |
| private static final FieldDescriptor NEGATIVE_DESCRIPTOR = |
| FileDiffOutputProto.getDescriptor().findFieldByNumber(12); |
| |
| @Override |
| public byte[] serialize(FileDiffOutput fileDiff) { |
| ObjectIdConverter idConverter = ObjectIdConverter.create(); |
| FileDiffOutputProto.Builder builder = |
| FileDiffOutputProto.newBuilder() |
| .setOldCommit(idConverter.toByteString(fileDiff.oldCommitId().toObjectId())) |
| .setNewCommit(idConverter.toByteString(fileDiff.newCommitId().toObjectId())) |
| .setComparisonType(fileDiff.comparisonType().toProto()) |
| .setSize(fileDiff.size()) |
| .setSizeDelta(fileDiff.sizeDelta()) |
| .addAllHeaderLines(fileDiff.headerLines()) |
| .setChangeType(fileDiff.changeType().name()) |
| .addAllEdits( |
| fileDiff.edits().stream() |
| .map( |
| e -> |
| FileDiffOutputProto.TaggedEdit.newBuilder() |
| .setEdit( |
| FileDiffOutputProto.Edit.newBuilder() |
| .setBeginA(e.edit().beginA()) |
| .setEndA(e.edit().endA()) |
| .setBeginB(e.edit().beginB()) |
| .setEndB(e.edit().endB()) |
| .build()) |
| .setDueToRebase(e.dueToRebase()) |
| .build()) |
| .collect(Collectors.toList())); |
| |
| if (fileDiff.oldPath().isPresent()) { |
| builder.setOldPath(fileDiff.oldPath().get()); |
| } |
| |
| if (fileDiff.newPath().isPresent()) { |
| builder.setNewPath(fileDiff.newPath().get()); |
| } |
| |
| if (fileDiff.patchType().isPresent()) { |
| builder.setPatchType(fileDiff.patchType().get().name()); |
| } |
| |
| if (fileDiff.negative().isPresent()) { |
| builder.setNegative(fileDiff.negative().get()); |
| } |
| |
| return Protos.toByteArray(builder.build()); |
| } |
| |
| @Override |
| public FileDiffOutput deserialize(byte[] in) { |
| ObjectIdConverter idConverter = ObjectIdConverter.create(); |
| FileDiffOutputProto proto = Protos.parseUnchecked(FileDiffOutputProto.parser(), in); |
| FileDiffOutput.Builder builder = FileDiffOutput.builder(); |
| builder |
| .oldCommitId(idConverter.fromByteString(proto.getOldCommit())) |
| .newCommitId(idConverter.fromByteString(proto.getNewCommit())) |
| .comparisonType(ComparisonType.fromProto(proto.getComparisonType())) |
| .size(proto.getSize()) |
| .sizeDelta(proto.getSizeDelta()) |
| .headerLines(proto.getHeaderLinesList().stream().collect(ImmutableList.toImmutableList())) |
| .changeType(ChangeType.valueOf(proto.getChangeType())) |
| .edits( |
| proto.getEditsList().stream() |
| .map( |
| e -> |
| TaggedEdit.create( |
| Edit.create( |
| e.getEdit().getBeginA(), |
| e.getEdit().getEndA(), |
| e.getEdit().getBeginB(), |
| e.getEdit().getEndB()), |
| e.getDueToRebase())) |
| .collect(ImmutableList.toImmutableList())); |
| |
| if (proto.hasField(OLD_PATH_DESCRIPTOR)) { |
| builder.oldPath(Optional.of(proto.getOldPath())); |
| } |
| if (proto.hasField(NEW_PATH_DESCRIPTOR)) { |
| builder.newPath(Optional.of(proto.getNewPath())); |
| } |
| if (proto.hasField(PATCH_TYPE_DESCRIPTOR)) { |
| builder.patchType(Optional.of(Patch.PatchType.valueOf(proto.getPatchType()))); |
| } |
| if (proto.hasField(NEGATIVE_DESCRIPTOR)) { |
| builder.negative(Optional.of(proto.getNegative())); |
| } |
| return builder.build(); |
| } |
| } |
| } |