| // 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.gitfilediff; |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.gerrit.server.patch.DiffUtil.stringSize; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.base.Converter; |
| import com.google.common.base.Enums; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| 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.GitFileDiffProto; |
| import com.google.gerrit.server.cache.serialize.CacheSerializer; |
| import com.google.gerrit.server.cache.serialize.ObjectIdConverter; |
| import com.google.gerrit.server.patch.filediff.Edit; |
| import com.google.protobuf.Descriptors.FieldDescriptor; |
| import java.util.Optional; |
| import org.eclipse.jgit.diff.DiffEntry; |
| import org.eclipse.jgit.lib.AbbreviatedObjectId; |
| import org.eclipse.jgit.lib.FileMode; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.patch.FileHeader; |
| |
| /** |
| * Entity representing a modified file (added, deleted, modified, renamed, etc...) between two |
| * different git commits. |
| */ |
| @AutoValue |
| public abstract class GitFileDiff { |
| private static final ImmutableMap<FileMode, Patch.FileMode> fileModeMap = |
| ImmutableMap.<FileMode, Patch.FileMode>builder() |
| .put(FileMode.TREE, Patch.FileMode.TREE) |
| .put(FileMode.SYMLINK, Patch.FileMode.SYMLINK) |
| .put(FileMode.GITLINK, Patch.FileMode.GITLINK) |
| .put(FileMode.REGULAR_FILE, Patch.FileMode.REGULAR_FILE) |
| .put(FileMode.EXECUTABLE_FILE, Patch.FileMode.EXECUTABLE_FILE) |
| .put(FileMode.MISSING, Patch.FileMode.MISSING) |
| .build(); |
| |
| private static Patch.FileMode mapFileMode(FileMode jgitFileMode) { |
| if (!fileModeMap.containsKey(jgitFileMode)) { |
| throw new IllegalArgumentException("Unsupported type " + jgitFileMode); |
| } |
| return fileModeMap.get(jgitFileMode); |
| } |
| |
| /** |
| * Creates a {@link GitFileDiff} using the {@code diffEntry} and the {@code diffFormatter} |
| * parameters. |
| */ |
| static GitFileDiff create(DiffEntry diffEntry, FileHeader fileHeader) { |
| ImmutableList<Edit> edits = |
| fileHeader.toEditList().stream().map(Edit::fromJGitEdit).collect(toImmutableList()); |
| |
| return builder() |
| .edits(edits) |
| .oldId(diffEntry.getOldId()) |
| .newId(diffEntry.getNewId()) |
| .fileHeader(FileHeaderUtil.toString(fileHeader)) |
| .oldPath(FileHeaderUtil.getOldPath(fileHeader)) |
| .newPath(FileHeaderUtil.getNewPath(fileHeader)) |
| .changeType(FileHeaderUtil.getChangeType(fileHeader)) |
| .patchType(Optional.of(FileHeaderUtil.getPatchType(fileHeader))) |
| .oldMode(Optional.of(mapFileMode(diffEntry.getOldMode()))) |
| .newMode(Optional.of(mapFileMode(diffEntry.getNewMode()))) |
| .build(); |
| } |
| |
| /** |
| * Represents an empty file diff, which means that the file was not modified between the two git |
| * trees identified by {@link #oldId()} and {@link #newId()}. |
| * |
| * @param newFilePath the file name at the {@link #newId()} git tree. |
| */ |
| static GitFileDiff empty( |
| AbbreviatedObjectId oldId, AbbreviatedObjectId newId, String newFilePath) { |
| return builder() |
| .oldId(oldId) |
| .newId(newId) |
| .newPath(Optional.of(newFilePath)) |
| .changeType(ChangeType.MODIFIED) |
| .edits(ImmutableList.of()) |
| .fileHeader("") |
| .build(); |
| } |
| |
| /** |
| * Create a negative result to be cached, i.e. if the diff computation did not finish in a |
| * reasonable amount of time. |
| */ |
| static GitFileDiff createNegative( |
| AbbreviatedObjectId oldId, AbbreviatedObjectId newId, String newFilePath) { |
| return empty(oldId, newId, newFilePath).toBuilder().negative(Optional.of(true)).build(); |
| } |
| |
| /** An {@link ImmutableList} of the modified regions in the file. */ |
| public abstract ImmutableList<Edit> edits(); |
| |
| /** A string representation of the {@link org.eclipse.jgit.patch.FileHeader}. */ |
| public abstract String fileHeader(); |
| |
| /** The file name at the old git tree identified by {@link #oldId()} */ |
| public abstract Optional<String> oldPath(); |
| |
| /** The file name at the new git tree identified by {@link #newId()} */ |
| public abstract Optional<String> newPath(); |
| |
| /** |
| * The 20 bytes SHA-1 object ID of the old git tree of the diff, or {@link ObjectId#zeroId()} if |
| * {@link #newId()} was a root git tree (i.e. has no parents). |
| */ |
| public abstract AbbreviatedObjectId oldId(); |
| |
| /** The 20 bytes SHA-1 object ID of the new git tree of the diff. */ |
| public abstract AbbreviatedObjectId newId(); |
| |
| /** The file mode of the old file at the old git tree diff identified by {@link #oldId()}. */ |
| public abstract Optional<Patch.FileMode> oldMode(); |
| |
| /** The file mode of the new file at the new git tree diff identified by {@link #newId()}. */ |
| public abstract Optional<Patch.FileMode> newMode(); |
| |
| /** The change type associated with the file. */ |
| public abstract ChangeType changeType(); |
| |
| /** The patch type associated with the file. */ |
| public abstract Optional<PatchType> patchType(); |
| |
| /** |
| * Returns {@code true} if the diff computation was not able to compute a diff. We cache negative |
| * result in this case. |
| */ |
| public abstract Optional<Boolean> negative(); |
| |
| /** |
| * Returns true if the object was created using the {@link #empty(AbbreviatedObjectId, |
| * AbbreviatedObjectId, String)} method. |
| */ |
| public boolean isEmpty() { |
| return 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(); |
| } |
| |
| /** Returns the size of the object in bytes. */ |
| public int weight() { |
| int result = 20 * 2; // oldId and newId |
| result += 16 * edits().size(); // each edit contains 4 integers (hence 16 bytes) |
| result += stringSize(fileHeader()); |
| if (oldPath().isPresent()) { |
| result += stringSize(oldPath().get()); |
| } |
| if (newPath().isPresent()) { |
| result += stringSize(newPath().get()); |
| } |
| result += 4; |
| if (patchType().isPresent()) { |
| result += 4; |
| } |
| if (oldMode().isPresent()) { |
| result += 4; |
| } |
| if (newMode().isPresent()) { |
| result += 4; |
| } |
| if (negative().isPresent()) { |
| result += 1; |
| } |
| return result; |
| } |
| |
| public String getDefaultPath() { |
| return oldPath().isPresent() ? oldPath().get() : newPath().get(); |
| } |
| |
| public static Builder builder() { |
| return new AutoValue_GitFileDiff.Builder(); |
| } |
| |
| public abstract Builder toBuilder(); |
| |
| @AutoValue.Builder |
| public abstract static class Builder { |
| |
| public abstract Builder edits(ImmutableList<Edit> value); |
| |
| public abstract Builder fileHeader(String value); |
| |
| public abstract Builder oldPath(Optional<String> value); |
| |
| public abstract Builder newPath(Optional<String> value); |
| |
| public abstract Builder oldId(AbbreviatedObjectId value); |
| |
| public abstract Builder newId(AbbreviatedObjectId value); |
| |
| public abstract Builder oldMode(Optional<Patch.FileMode> value); |
| |
| public abstract Builder newMode(Optional<Patch.FileMode> value); |
| |
| public abstract Builder changeType(ChangeType value); |
| |
| public abstract Builder patchType(Optional<PatchType> value); |
| |
| public abstract Builder negative(Optional<Boolean> value); |
| |
| public abstract GitFileDiff build(); |
| } |
| |
| public enum Serializer implements CacheSerializer<GitFileDiff> { |
| INSTANCE; |
| |
| private static final Converter<String, Patch.FileMode> FILE_MODE_CONVERTER = |
| Enums.stringConverter(Patch.FileMode.class); |
| |
| private static final Converter<String, Patch.ChangeType> CHANGE_TYPE_CONVERTER = |
| Enums.stringConverter(Patch.ChangeType.class); |
| |
| private static final Converter<String, Patch.PatchType> PATCH_TYPE_CONVERTER = |
| Enums.stringConverter(Patch.PatchType.class); |
| |
| private static final FieldDescriptor OLD_PATH_DESCRIPTOR = |
| GitFileDiffProto.getDescriptor().findFieldByNumber(3); |
| |
| private static final FieldDescriptor NEW_PATH_DESCRIPTOR = |
| GitFileDiffProto.getDescriptor().findFieldByNumber(4); |
| |
| private static final FieldDescriptor OLD_MODE_DESCRIPTOR = |
| GitFileDiffProto.getDescriptor().findFieldByNumber(7); |
| |
| private static final FieldDescriptor NEW_MODE_DESCRIPTOR = |
| GitFileDiffProto.getDescriptor().findFieldByNumber(8); |
| |
| private static final FieldDescriptor PATCH_TYPE_DESCRIPTOR = |
| GitFileDiffProto.getDescriptor().findFieldByNumber(10); |
| |
| private static final FieldDescriptor NEGATIVE_DESCRIPTOR = |
| GitFileDiffProto.getDescriptor().findFieldByNumber(11); |
| |
| @Override |
| public byte[] serialize(GitFileDiff gitFileDiff) { |
| ObjectIdConverter idConverter = ObjectIdConverter.create(); |
| GitFileDiffProto.Builder builder = |
| GitFileDiffProto.newBuilder() |
| .setFileHeader(gitFileDiff.fileHeader()) |
| .setOldId(idConverter.toByteString(gitFileDiff.oldId().toObjectId())) |
| .setNewId(idConverter.toByteString(gitFileDiff.newId().toObjectId())) |
| .setChangeType(CHANGE_TYPE_CONVERTER.reverse().convert(gitFileDiff.changeType())); |
| gitFileDiff |
| .edits() |
| .forEach( |
| e -> |
| builder.addEdits( |
| GitFileDiffProto.Edit.newBuilder() |
| .setBeginA(e.beginA()) |
| .setEndA(e.endA()) |
| .setBeginB(e.beginB()) |
| .setEndB(e.endB()))); |
| if (gitFileDiff.oldPath().isPresent()) { |
| builder.setOldPath(gitFileDiff.oldPath().get()); |
| } |
| if (gitFileDiff.newPath().isPresent()) { |
| builder.setNewPath(gitFileDiff.newPath().get()); |
| } |
| if (gitFileDiff.oldMode().isPresent()) { |
| builder.setOldMode(FILE_MODE_CONVERTER.reverse().convert(gitFileDiff.oldMode().get())); |
| } |
| if (gitFileDiff.newMode().isPresent()) { |
| builder.setNewMode(FILE_MODE_CONVERTER.reverse().convert(gitFileDiff.newMode().get())); |
| } |
| if (gitFileDiff.patchType().isPresent()) { |
| builder.setPatchType(PATCH_TYPE_CONVERTER.reverse().convert(gitFileDiff.patchType().get())); |
| } |
| if (gitFileDiff.negative().isPresent()) { |
| builder.setNegative(gitFileDiff.negative().get()); |
| } |
| return Protos.toByteArray(builder.build()); |
| } |
| |
| @Override |
| public GitFileDiff deserialize(byte[] in) { |
| ObjectIdConverter idConverter = ObjectIdConverter.create(); |
| GitFileDiffProto proto = Protos.parseUnchecked(GitFileDiffProto.parser(), in); |
| GitFileDiff.Builder builder = GitFileDiff.builder(); |
| builder |
| .edits( |
| proto.getEditsList().stream() |
| .map(e -> Edit.create(e.getBeginA(), e.getEndA(), e.getBeginB(), e.getEndB())) |
| .collect(toImmutableList())) |
| .fileHeader(proto.getFileHeader()) |
| .oldId(AbbreviatedObjectId.fromObjectId(idConverter.fromByteString(proto.getOldId()))) |
| .newId(AbbreviatedObjectId.fromObjectId(idConverter.fromByteString(proto.getNewId()))) |
| .changeType(CHANGE_TYPE_CONVERTER.convert(proto.getChangeType())); |
| |
| 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(OLD_MODE_DESCRIPTOR)) { |
| builder.oldMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getOldMode()))); |
| } |
| if (proto.hasField(NEW_MODE_DESCRIPTOR)) { |
| builder.newMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getNewMode()))); |
| } |
| if (proto.hasField(PATCH_TYPE_DESCRIPTOR)) { |
| builder.patchType(Optional.of(PATCH_TYPE_CONVERTER.convert(proto.getPatchType()))); |
| } |
| if (proto.hasField(NEGATIVE_DESCRIPTOR)) { |
| builder.negative(Optional.of(proto.getNegative())); |
| } |
| return builder.build(); |
| } |
| } |
| } |