| // 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.Map; | |
| 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 Map<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(); | |
| } | |
| } | |
| } |