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.filediff; 16 : 17 : import static com.google.gerrit.server.patch.DiffUtil.stringSize; 18 : 19 : import com.google.auto.value.AutoValue; 20 : import com.google.common.base.Converter; 21 : import com.google.common.base.Enums; 22 : import com.google.common.collect.ImmutableList; 23 : import com.google.gerrit.entities.Patch; 24 : import com.google.gerrit.entities.Patch.ChangeType; 25 : import com.google.gerrit.entities.Patch.FileMode; 26 : import com.google.gerrit.entities.Patch.PatchType; 27 : import com.google.gerrit.proto.Protos; 28 : import com.google.gerrit.server.cache.proto.Cache.FileDiffOutputProto; 29 : import com.google.gerrit.server.cache.serialize.CacheSerializer; 30 : import com.google.gerrit.server.cache.serialize.ObjectIdConverter; 31 : import com.google.gerrit.server.patch.ComparisonType; 32 : import com.google.protobuf.Descriptors.FieldDescriptor; 33 : import java.io.Serializable; 34 : import java.util.Optional; 35 : import java.util.stream.Collectors; 36 : import org.eclipse.jgit.lib.ObjectId; 37 : 38 : /** File diff for a single file path. Produced as output of the {@link FileDiffCache}. */ 39 : @AutoValue 40 105 : public abstract class FileDiffOutput implements Serializable { 41 : private static final long serialVersionUID = 1L; 42 : 43 : /** 44 : * The 20 bytes SHA-1 object ID of the old git commit used in the diff, or {@link 45 : * ObjectId#zeroId()} if {@link #newCommitId()} was a root commit. 46 : */ 47 : public abstract ObjectId oldCommitId(); 48 : 49 : /** The 20 bytes SHA-1 object ID of the new git commit used in the diff. */ 50 : public abstract ObjectId newCommitId(); 51 : 52 : /** Comparison type of old and new commits: against another patchset, parent or auto-merge. */ 53 : public abstract ComparisonType comparisonType(); 54 : 55 : /** 56 : * The file path at the old commit. Returns an empty Optional if {@link #changeType()} is equal to 57 : * {@link ChangeType#ADDED}. 58 : */ 59 : public abstract Optional<String> oldPath(); 60 : 61 : /** 62 : * The file path at the new commit. Returns an empty optional if {@link #changeType()} is equal to 63 : * {@link ChangeType#DELETED}. 64 : */ 65 : public abstract Optional<String> newPath(); 66 : 67 : /** 68 : * The file mode of the old file at the old git tree diff identified by {@link #oldCommitId()} 69 : * ()}. 70 : */ 71 : public abstract Optional<Patch.FileMode> oldMode(); 72 : 73 : /** 74 : * The file mode of the new file at the new git tree diff identified by {@link #newCommitId()} 75 : * ()}. 76 : */ 77 : public abstract Optional<Patch.FileMode> newMode(); 78 : 79 : /** The change type of the underlying file, e.g. added, deleted, renamed, etc... */ 80 : public abstract Patch.ChangeType changeType(); 81 : 82 : /** The patch type of the underlying file, e.g. unified, binary , etc... */ 83 : public abstract Optional<Patch.PatchType> patchType(); 84 : 85 : /** 86 : * A list of strings representation of the header lines of the {@link 87 : * org.eclipse.jgit.patch.FileHeader} that is produced as output of the diff. 88 : */ 89 : public abstract ImmutableList<String> headerLines(); 90 : 91 : /** The list of edits resulting from the diff hunks of the file. */ 92 : public abstract ImmutableList<TaggedEdit> edits(); 93 : 94 : /** The file size at the new commit. */ 95 : public abstract long size(); 96 : 97 : /** Difference in file size between the old and new commits. */ 98 : public abstract long sizeDelta(); 99 : 100 : /** 101 : * Returns {@code true} if the diff computation was not able to compute a diff, i.e. for diffs 102 : * taking a very long time to compute. We cache negative result in this case. 103 : */ 104 : public abstract Optional<Boolean> negative(); 105 : 106 : public abstract Builder toBuilder(); 107 : 108 : /** A boolean indicating if all underlying edits of the file diff are due to rebase. */ 109 : public boolean allEditsDueToRebase() { 110 104 : return !edits().isEmpty() && edits().stream().allMatch(TaggedEdit::dueToRebase); 111 : } 112 : 113 : /** Returns the number of inserted lines for the file diff. */ 114 : public int insertions() { 115 103 : int ins = 0; 116 103 : for (TaggedEdit e : edits()) { 117 103 : if (!e.dueToRebase()) { 118 103 : ins += e.edit().endB() - e.edit().beginB(); 119 : } 120 103 : } 121 103 : return ins; 122 : } 123 : 124 : /** Returns the number of deleted lines for the file diff. */ 125 : public int deletions() { 126 103 : int del = 0; 127 103 : for (TaggedEdit e : edits()) { 128 103 : if (!e.dueToRebase()) { 129 103 : del += e.edit().endA() - e.edit().beginA(); 130 : } 131 103 : } 132 103 : return del; 133 : } 134 : 135 : /** Returns an entity representing an unchanged file between two commits. */ 136 : public static FileDiffOutput empty(String filePath, ObjectId oldCommitId, ObjectId newCommitId) { 137 25 : return builder() 138 25 : .oldCommitId(oldCommitId) 139 25 : .newCommitId(newCommitId) 140 25 : .comparisonType(ComparisonType.againstOtherPatchSet()) // not important 141 25 : .oldPath(Optional.empty()) 142 25 : .newPath(Optional.of(filePath)) 143 25 : .changeType(ChangeType.MODIFIED) 144 25 : .headerLines(ImmutableList.of()) 145 25 : .edits(ImmutableList.of()) 146 25 : .size(0) 147 25 : .sizeDelta(0) 148 25 : .build(); 149 : } 150 : 151 : /** 152 : * Create a negative file diff. We use this to cache negative diffs for entries that result in 153 : * timeouts. 154 : */ 155 : public static FileDiffOutput createNegative( 156 : String filePath, ObjectId oldCommitId, ObjectId newCommitId) { 157 0 : return empty(filePath, oldCommitId, newCommitId) 158 0 : .toBuilder() 159 0 : .negative(Optional.of(true)) 160 0 : .build(); 161 : } 162 : 163 : /** Returns true if this entity represents an unchanged file between two commits. */ 164 : public boolean isEmpty() { 165 104 : return headerLines().isEmpty() && edits().isEmpty(); 166 : } 167 : 168 : /** 169 : * Returns {@code true} if the diff computation was not able to compute a diff. We cache negative 170 : * result in this case. 171 : */ 172 : public boolean isNegative() { 173 104 : return negative().isPresent() && negative().get(); 174 : } 175 : 176 : public static Builder builder() { 177 105 : return new AutoValue_FileDiffOutput.Builder(); 178 : } 179 : 180 : public int weight() { 181 104 : int result = 0; 182 104 : if (oldPath().isPresent()) { 183 54 : result += stringSize(oldPath().get()); 184 : } 185 104 : if (newPath().isPresent()) { 186 104 : result += stringSize(newPath().get()); 187 : } 188 104 : result += 20 + 20; // old and new commit IDs 189 104 : result += 4; // comparison type 190 104 : result += 4; // changeType 191 104 : if (patchType().isPresent()) { 192 104 : result += 4; 193 : } 194 104 : result += 4 + 4; // insertions and deletions 195 104 : result += 4 + 4; // size and size delta 196 104 : result += 20 * edits().size(); // each edit is 4 Integers + boolean = 4 * 4 + 4 = 20 197 104 : for (String s : headerLines()) { 198 104 : s += stringSize(s); 199 104 : } 200 104 : if (negative().isPresent()) { 201 0 : result += 1; 202 : } 203 104 : return result; 204 : } 205 : 206 : @AutoValue.Builder 207 105 : public abstract static class Builder { 208 : 209 : public abstract Builder oldCommitId(ObjectId value); 210 : 211 : public abstract Builder newCommitId(ObjectId value); 212 : 213 : public abstract Builder comparisonType(ComparisonType value); 214 : 215 : public abstract Builder oldPath(Optional<String> value); 216 : 217 : public abstract Builder newPath(Optional<String> value); 218 : 219 : public abstract Builder oldMode(Optional<Patch.FileMode> oldMode); 220 : 221 : public abstract Builder newMode(Optional<Patch.FileMode> newMode); 222 : 223 : public abstract Builder changeType(ChangeType value); 224 : 225 : public abstract Builder patchType(Optional<PatchType> value); 226 : 227 : public abstract Builder headerLines(ImmutableList<String> value); 228 : 229 : public abstract Builder edits(ImmutableList<TaggedEdit> value); 230 : 231 : public abstract Builder size(long value); 232 : 233 : public abstract Builder sizeDelta(long value); 234 : 235 : public abstract Builder negative(Optional<Boolean> value); 236 : 237 : public abstract FileDiffOutput build(); 238 : } 239 : 240 153 : public enum Serializer implements CacheSerializer<FileDiffOutput> { 241 153 : INSTANCE; 242 : 243 153 : private static final Converter<String, FileMode> FILE_MODE_CONVERTER = 244 153 : Enums.stringConverter(Patch.FileMode.class); 245 : 246 : private static final FieldDescriptor OLD_PATH_DESCRIPTOR = 247 153 : FileDiffOutputProto.getDescriptor().findFieldByNumber(1); 248 : 249 : private static final FieldDescriptor NEW_PATH_DESCRIPTOR = 250 153 : FileDiffOutputProto.getDescriptor().findFieldByNumber(2); 251 : 252 : private static final FieldDescriptor PATCH_TYPE_DESCRIPTOR = 253 153 : FileDiffOutputProto.getDescriptor().findFieldByNumber(4); 254 : 255 : private static final FieldDescriptor NEGATIVE_DESCRIPTOR = 256 153 : FileDiffOutputProto.getDescriptor().findFieldByNumber(12); 257 : 258 : private static final FieldDescriptor OLD_MODE_DESCRIPTOR = 259 153 : FileDiffOutputProto.getDescriptor().findFieldByNumber(13); 260 : 261 153 : private static final FieldDescriptor NEW_MODE_DESCRIPTOR = 262 153 : FileDiffOutputProto.getDescriptor().findFieldByNumber(14); 263 : 264 : @Override 265 : public byte[] serialize(FileDiffOutput fileDiff) { 266 7 : ObjectIdConverter idConverter = ObjectIdConverter.create(); 267 : FileDiffOutputProto.Builder builder = 268 7 : FileDiffOutputProto.newBuilder() 269 7 : .setOldCommit(idConverter.toByteString(fileDiff.oldCommitId().toObjectId())) 270 7 : .setNewCommit(idConverter.toByteString(fileDiff.newCommitId().toObjectId())) 271 7 : .setComparisonType(fileDiff.comparisonType().toProto()) 272 7 : .setSize(fileDiff.size()) 273 7 : .setSizeDelta(fileDiff.sizeDelta()) 274 7 : .addAllHeaderLines(fileDiff.headerLines()) 275 7 : .setChangeType(fileDiff.changeType().name()) 276 7 : .addAllEdits( 277 7 : fileDiff.edits().stream() 278 7 : .map( 279 : e -> 280 7 : FileDiffOutputProto.TaggedEdit.newBuilder() 281 7 : .setEdit( 282 7 : FileDiffOutputProto.Edit.newBuilder() 283 7 : .setBeginA(e.edit().beginA()) 284 7 : .setEndA(e.edit().endA()) 285 7 : .setBeginB(e.edit().beginB()) 286 7 : .setEndB(e.edit().endB()) 287 7 : .build()) 288 7 : .setDueToRebase(e.dueToRebase()) 289 7 : .build()) 290 7 : .collect(Collectors.toList())); 291 : 292 7 : if (fileDiff.oldPath().isPresent()) { 293 1 : builder.setOldPath(fileDiff.oldPath().get()); 294 : } 295 : 296 7 : if (fileDiff.newPath().isPresent()) { 297 6 : builder.setNewPath(fileDiff.newPath().get()); 298 : } 299 : 300 7 : if (fileDiff.patchType().isPresent()) { 301 7 : builder.setPatchType(fileDiff.patchType().get().name()); 302 : } 303 : 304 7 : if (fileDiff.negative().isPresent()) { 305 1 : builder.setNegative(fileDiff.negative().get()); 306 : } 307 : 308 7 : if (fileDiff.oldMode().isPresent()) { 309 5 : builder.setOldMode(FILE_MODE_CONVERTER.reverse().convert(fileDiff.oldMode().get())); 310 : } 311 7 : if (fileDiff.newMode().isPresent()) { 312 5 : builder.setNewMode(FILE_MODE_CONVERTER.reverse().convert(fileDiff.newMode().get())); 313 : } 314 : 315 7 : return Protos.toByteArray(builder.build()); 316 : } 317 : 318 : @Override 319 : public FileDiffOutput deserialize(byte[] in) { 320 2 : ObjectIdConverter idConverter = ObjectIdConverter.create(); 321 2 : FileDiffOutputProto proto = Protos.parseUnchecked(FileDiffOutputProto.parser(), in); 322 2 : FileDiffOutput.Builder builder = FileDiffOutput.builder(); 323 2 : builder 324 2 : .oldCommitId(idConverter.fromByteString(proto.getOldCommit())) 325 2 : .newCommitId(idConverter.fromByteString(proto.getNewCommit())) 326 2 : .comparisonType(ComparisonType.fromProto(proto.getComparisonType())) 327 2 : .size(proto.getSize()) 328 2 : .sizeDelta(proto.getSizeDelta()) 329 2 : .headerLines(proto.getHeaderLinesList().stream().collect(ImmutableList.toImmutableList())) 330 2 : .changeType(ChangeType.valueOf(proto.getChangeType())) 331 2 : .edits( 332 2 : proto.getEditsList().stream() 333 2 : .map( 334 : e -> 335 2 : TaggedEdit.create( 336 2 : Edit.create( 337 2 : e.getEdit().getBeginA(), 338 2 : e.getEdit().getEndA(), 339 2 : e.getEdit().getBeginB(), 340 2 : e.getEdit().getEndB()), 341 2 : e.getDueToRebase())) 342 2 : .collect(ImmutableList.toImmutableList())); 343 : 344 2 : if (proto.hasField(OLD_PATH_DESCRIPTOR)) { 345 1 : builder.oldPath(Optional.of(proto.getOldPath())); 346 : } 347 2 : if (proto.hasField(NEW_PATH_DESCRIPTOR)) { 348 1 : builder.newPath(Optional.of(proto.getNewPath())); 349 : } 350 2 : if (proto.hasField(PATCH_TYPE_DESCRIPTOR)) { 351 2 : builder.patchType(Optional.of(Patch.PatchType.valueOf(proto.getPatchType()))); 352 : } 353 2 : if (proto.hasField(NEGATIVE_DESCRIPTOR)) { 354 1 : builder.negative(Optional.of(proto.getNegative())); 355 : } 356 2 : if (proto.hasField(OLD_MODE_DESCRIPTOR)) { 357 1 : builder.oldMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getOldMode()))); 358 : } 359 2 : if (proto.hasField(NEW_MODE_DESCRIPTOR)) { 360 1 : builder.newMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getNewMode()))); 361 : } 362 2 : return builder.build(); 363 : } 364 : } 365 : }