Line data Source code
1 : // Copyright (C) 2020 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.patch.gitfilediff; 16 : 17 : import static com.google.common.collect.ImmutableList.toImmutableList; 18 : import static com.google.gerrit.server.patch.DiffUtil.stringSize; 19 : 20 : import com.google.auto.value.AutoValue; 21 : import com.google.common.base.Converter; 22 : import com.google.common.base.Enums; 23 : import com.google.common.collect.ImmutableList; 24 : import com.google.common.collect.ImmutableMap; 25 : import com.google.gerrit.entities.Patch; 26 : import com.google.gerrit.entities.Patch.ChangeType; 27 : import com.google.gerrit.entities.Patch.PatchType; 28 : import com.google.gerrit.proto.Protos; 29 : import com.google.gerrit.server.cache.proto.Cache.GitFileDiffProto; 30 : import com.google.gerrit.server.cache.serialize.CacheSerializer; 31 : import com.google.gerrit.server.cache.serialize.ObjectIdConverter; 32 : import com.google.gerrit.server.patch.filediff.Edit; 33 : import com.google.protobuf.Descriptors.FieldDescriptor; 34 : import java.util.Map; 35 : import java.util.Optional; 36 : import org.eclipse.jgit.diff.DiffEntry; 37 : import org.eclipse.jgit.lib.AbbreviatedObjectId; 38 : import org.eclipse.jgit.lib.FileMode; 39 : import org.eclipse.jgit.lib.ObjectId; 40 : import org.eclipse.jgit.patch.FileHeader; 41 : 42 : /** 43 : * Entity representing a modified file (added, deleted, modified, renamed, etc...) between two 44 : * different git commits. 45 : */ 46 : @AutoValue 47 94 : public abstract class GitFileDiff { 48 94 : private static final Map<FileMode, Patch.FileMode> fileModeMap = 49 94 : ImmutableMap.<FileMode, Patch.FileMode>builder() 50 94 : .put(FileMode.TREE, Patch.FileMode.TREE) 51 94 : .put(FileMode.SYMLINK, Patch.FileMode.SYMLINK) 52 94 : .put(FileMode.GITLINK, Patch.FileMode.GITLINK) 53 94 : .put(FileMode.REGULAR_FILE, Patch.FileMode.REGULAR_FILE) 54 94 : .put(FileMode.EXECUTABLE_FILE, Patch.FileMode.EXECUTABLE_FILE) 55 94 : .put(FileMode.MISSING, Patch.FileMode.MISSING) 56 94 : .build(); 57 : 58 : private static Patch.FileMode mapFileMode(FileMode jgitFileMode) { 59 93 : if (!fileModeMap.containsKey(jgitFileMode)) { 60 0 : throw new IllegalArgumentException("Unsupported type " + jgitFileMode); 61 : } 62 93 : return fileModeMap.get(jgitFileMode); 63 : } 64 : 65 : /** 66 : * Creates a {@link GitFileDiff} using the {@code diffEntry} and the {@code diffFormatter} 67 : * parameters. 68 : */ 69 : static GitFileDiff create(DiffEntry diffEntry, FileHeader fileHeader) { 70 93 : ImmutableList<Edit> edits = 71 93 : fileHeader.toEditList().stream().map(Edit::fromJGitEdit).collect(toImmutableList()); 72 : 73 93 : return builder() 74 93 : .edits(edits) 75 93 : .oldId(diffEntry.getOldId()) 76 93 : .newId(diffEntry.getNewId()) 77 93 : .fileHeader(FileHeaderUtil.toString(fileHeader)) 78 93 : .oldPath(FileHeaderUtil.getOldPath(fileHeader)) 79 93 : .newPath(FileHeaderUtil.getNewPath(fileHeader)) 80 93 : .changeType(FileHeaderUtil.getChangeType(fileHeader)) 81 93 : .patchType(Optional.of(FileHeaderUtil.getPatchType(fileHeader))) 82 93 : .oldMode(Optional.of(mapFileMode(diffEntry.getOldMode()))) 83 93 : .newMode(Optional.of(mapFileMode(diffEntry.getNewMode()))) 84 93 : .build(); 85 : } 86 : 87 : /** 88 : * Represents an empty file diff, which means that the file was not modified between the two git 89 : * trees identified by {@link #oldId()} and {@link #newId()}. 90 : * 91 : * @param newFilePath the file name at the {@link #newId()} git tree. 92 : */ 93 : static GitFileDiff empty( 94 : AbbreviatedObjectId oldId, AbbreviatedObjectId newId, String newFilePath) { 95 3 : return builder() 96 3 : .oldId(oldId) 97 3 : .newId(newId) 98 3 : .newPath(Optional.of(newFilePath)) 99 3 : .changeType(ChangeType.MODIFIED) 100 3 : .edits(ImmutableList.of()) 101 3 : .fileHeader("") 102 3 : .build(); 103 : } 104 : 105 : /** 106 : * Create a negative result to be cached, i.e. if the diff computation did not finish in a 107 : * reasonable amount of time. 108 : */ 109 : static GitFileDiff createNegative( 110 : AbbreviatedObjectId oldId, AbbreviatedObjectId newId, String newFilePath) { 111 0 : return empty(oldId, newId, newFilePath).toBuilder().negative(Optional.of(true)).build(); 112 : } 113 : 114 : /** An {@link ImmutableList} of the modified regions in the file. */ 115 : public abstract ImmutableList<Edit> edits(); 116 : 117 : /** A string representation of the {@link org.eclipse.jgit.patch.FileHeader}. */ 118 : public abstract String fileHeader(); 119 : 120 : /** The file name at the old git tree identified by {@link #oldId()} */ 121 : public abstract Optional<String> oldPath(); 122 : 123 : /** The file name at the new git tree identified by {@link #newId()} */ 124 : public abstract Optional<String> newPath(); 125 : 126 : /** 127 : * The 20 bytes SHA-1 object ID of the old git tree of the diff, or {@link ObjectId#zeroId()} if 128 : * {@link #newId()} was a root git tree (i.e. has no parents). 129 : */ 130 : public abstract AbbreviatedObjectId oldId(); 131 : 132 : /** The 20 bytes SHA-1 object ID of the new git tree of the diff. */ 133 : public abstract AbbreviatedObjectId newId(); 134 : 135 : /** The file mode of the old file at the old git tree diff identified by {@link #oldId()}. */ 136 : public abstract Optional<Patch.FileMode> oldMode(); 137 : 138 : /** The file mode of the new file at the new git tree diff identified by {@link #newId()}. */ 139 : public abstract Optional<Patch.FileMode> newMode(); 140 : 141 : /** The change type associated with the file. */ 142 : public abstract ChangeType changeType(); 143 : 144 : /** The patch type associated with the file. */ 145 : public abstract Optional<PatchType> patchType(); 146 : 147 : /** 148 : * Returns {@code true} if the diff computation was not able to compute a diff. We cache negative 149 : * result in this case. 150 : */ 151 : public abstract Optional<Boolean> negative(); 152 : 153 : /** 154 : * Returns true if the object was created using the {@link #empty(AbbreviatedObjectId, 155 : * AbbreviatedObjectId, String)} method. 156 : */ 157 : public boolean isEmpty() { 158 4 : return edits().isEmpty(); 159 : } 160 : 161 : /** 162 : * Returns {@code true} if the diff computation was not able to compute a diff. We cache negative 163 : * result in this case. 164 : */ 165 : public boolean isNegative() { 166 93 : return negative().isPresent() && negative().get(); 167 : } 168 : 169 : /** Returns the size of the object in bytes. */ 170 : public int weight() { 171 93 : int result = 20 * 2; // oldId and newId 172 93 : result += 16 * edits().size(); // each edit contains 4 integers (hence 16 bytes) 173 93 : result += stringSize(fileHeader()); 174 93 : if (oldPath().isPresent()) { 175 54 : result += stringSize(oldPath().get()); 176 : } 177 93 : if (newPath().isPresent()) { 178 93 : result += stringSize(newPath().get()); 179 : } 180 93 : result += 4; 181 93 : if (patchType().isPresent()) { 182 93 : result += 4; 183 : } 184 93 : if (oldMode().isPresent()) { 185 93 : result += 4; 186 : } 187 93 : if (newMode().isPresent()) { 188 93 : result += 4; 189 : } 190 93 : if (negative().isPresent()) { 191 0 : result += 1; 192 : } 193 93 : return result; 194 : } 195 : 196 : public String getDefaultPath() { 197 0 : return oldPath().isPresent() ? oldPath().get() : newPath().get(); 198 : } 199 : 200 : public static Builder builder() { 201 94 : return new AutoValue_GitFileDiff.Builder(); 202 : } 203 : 204 : public abstract Builder toBuilder(); 205 : 206 : @AutoValue.Builder 207 94 : public abstract static class Builder { 208 : 209 : public abstract Builder edits(ImmutableList<Edit> value); 210 : 211 : public abstract Builder fileHeader(String value); 212 : 213 : public abstract Builder oldPath(Optional<String> value); 214 : 215 : public abstract Builder newPath(Optional<String> value); 216 : 217 : public abstract Builder oldId(AbbreviatedObjectId value); 218 : 219 : public abstract Builder newId(AbbreviatedObjectId value); 220 : 221 : public abstract Builder oldMode(Optional<Patch.FileMode> value); 222 : 223 : public abstract Builder newMode(Optional<Patch.FileMode> value); 224 : 225 : public abstract Builder changeType(ChangeType value); 226 : 227 : public abstract Builder patchType(Optional<PatchType> value); 228 : 229 : public abstract Builder negative(Optional<Boolean> value); 230 : 231 : public abstract GitFileDiff build(); 232 : } 233 : 234 153 : public enum Serializer implements CacheSerializer<GitFileDiff> { 235 153 : INSTANCE; 236 : 237 153 : private static final Converter<String, Patch.FileMode> FILE_MODE_CONVERTER = 238 153 : Enums.stringConverter(Patch.FileMode.class); 239 : 240 153 : private static final Converter<String, Patch.ChangeType> CHANGE_TYPE_CONVERTER = 241 153 : Enums.stringConverter(Patch.ChangeType.class); 242 : 243 153 : private static final Converter<String, Patch.PatchType> PATCH_TYPE_CONVERTER = 244 153 : Enums.stringConverter(Patch.PatchType.class); 245 : 246 : private static final FieldDescriptor OLD_PATH_DESCRIPTOR = 247 153 : GitFileDiffProto.getDescriptor().findFieldByNumber(3); 248 : 249 : private static final FieldDescriptor NEW_PATH_DESCRIPTOR = 250 153 : GitFileDiffProto.getDescriptor().findFieldByNumber(4); 251 : 252 : private static final FieldDescriptor OLD_MODE_DESCRIPTOR = 253 153 : GitFileDiffProto.getDescriptor().findFieldByNumber(7); 254 : 255 : private static final FieldDescriptor NEW_MODE_DESCRIPTOR = 256 153 : GitFileDiffProto.getDescriptor().findFieldByNumber(8); 257 : 258 : private static final FieldDescriptor PATCH_TYPE_DESCRIPTOR = 259 153 : GitFileDiffProto.getDescriptor().findFieldByNumber(10); 260 : 261 153 : private static final FieldDescriptor NEGATIVE_DESCRIPTOR = 262 153 : GitFileDiffProto.getDescriptor().findFieldByNumber(11); 263 : 264 : @Override 265 : public byte[] serialize(GitFileDiff gitFileDiff) { 266 5 : ObjectIdConverter idConverter = ObjectIdConverter.create(); 267 : GitFileDiffProto.Builder builder = 268 5 : GitFileDiffProto.newBuilder() 269 5 : .setFileHeader(gitFileDiff.fileHeader()) 270 5 : .setOldId(idConverter.toByteString(gitFileDiff.oldId().toObjectId())) 271 5 : .setNewId(idConverter.toByteString(gitFileDiff.newId().toObjectId())) 272 5 : .setChangeType(CHANGE_TYPE_CONVERTER.reverse().convert(gitFileDiff.changeType())); 273 5 : gitFileDiff 274 5 : .edits() 275 5 : .forEach( 276 : e -> 277 5 : builder.addEdits( 278 5 : GitFileDiffProto.Edit.newBuilder() 279 5 : .setBeginA(e.beginA()) 280 5 : .setEndA(e.endA()) 281 5 : .setBeginB(e.beginB()) 282 5 : .setEndB(e.endB()))); 283 5 : if (gitFileDiff.oldPath().isPresent()) { 284 1 : builder.setOldPath(gitFileDiff.oldPath().get()); 285 : } 286 5 : if (gitFileDiff.newPath().isPresent()) { 287 4 : builder.setNewPath(gitFileDiff.newPath().get()); 288 : } 289 5 : if (gitFileDiff.oldMode().isPresent()) { 290 5 : builder.setOldMode(FILE_MODE_CONVERTER.reverse().convert(gitFileDiff.oldMode().get())); 291 : } 292 5 : if (gitFileDiff.newMode().isPresent()) { 293 5 : builder.setNewMode(FILE_MODE_CONVERTER.reverse().convert(gitFileDiff.newMode().get())); 294 : } 295 5 : if (gitFileDiff.patchType().isPresent()) { 296 5 : builder.setPatchType(PATCH_TYPE_CONVERTER.reverse().convert(gitFileDiff.patchType().get())); 297 : } 298 5 : if (gitFileDiff.negative().isPresent()) { 299 1 : builder.setNegative(gitFileDiff.negative().get()); 300 : } 301 5 : return Protos.toByteArray(builder.build()); 302 : } 303 : 304 : @Override 305 : public GitFileDiff deserialize(byte[] in) { 306 1 : ObjectIdConverter idConverter = ObjectIdConverter.create(); 307 1 : GitFileDiffProto proto = Protos.parseUnchecked(GitFileDiffProto.parser(), in); 308 1 : GitFileDiff.Builder builder = GitFileDiff.builder(); 309 1 : builder 310 1 : .edits( 311 1 : proto.getEditsList().stream() 312 1 : .map(e -> Edit.create(e.getBeginA(), e.getEndA(), e.getBeginB(), e.getEndB())) 313 1 : .collect(toImmutableList())) 314 1 : .fileHeader(proto.getFileHeader()) 315 1 : .oldId(AbbreviatedObjectId.fromObjectId(idConverter.fromByteString(proto.getOldId()))) 316 1 : .newId(AbbreviatedObjectId.fromObjectId(idConverter.fromByteString(proto.getNewId()))) 317 1 : .changeType(CHANGE_TYPE_CONVERTER.convert(proto.getChangeType())); 318 : 319 1 : if (proto.hasField(OLD_PATH_DESCRIPTOR)) { 320 1 : builder.oldPath(Optional.of(proto.getOldPath())); 321 : } 322 1 : if (proto.hasField(NEW_PATH_DESCRIPTOR)) { 323 0 : builder.newPath(Optional.of(proto.getNewPath())); 324 : } 325 1 : if (proto.hasField(OLD_MODE_DESCRIPTOR)) { 326 1 : builder.oldMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getOldMode()))); 327 : } 328 1 : if (proto.hasField(NEW_MODE_DESCRIPTOR)) { 329 1 : builder.newMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getNewMode()))); 330 : } 331 1 : if (proto.hasField(PATCH_TYPE_DESCRIPTOR)) { 332 1 : builder.patchType(Optional.of(PATCH_TYPE_CONVERTER.convert(proto.getPatchType()))); 333 : } 334 1 : if (proto.hasField(NEGATIVE_DESCRIPTOR)) { 335 1 : builder.negative(Optional.of(proto.getNegative())); 336 : } 337 1 : return builder.build(); 338 : } 339 : } 340 : }