LCOV - code coverage report
Current view: top level - server/patch - PatchScriptBuilder.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 169 180 93.9 %
Date: 2022-11-19 15:00:39 Functions: 30 31 96.8 %

          Line data    Source code
       1             : // Copyright (C) 2009 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;
      16             : 
      17             : import static com.google.common.collect.ImmutableList.toImmutableList;
      18             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      19             : 
      20             : import com.google.common.collect.ImmutableList;
      21             : import com.google.common.collect.ImmutableSet;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.common.data.PatchScript;
      24             : import com.google.gerrit.common.data.PatchScript.DisplayMethod;
      25             : import com.google.gerrit.entities.FixReplacement;
      26             : import com.google.gerrit.entities.Patch;
      27             : import com.google.gerrit.entities.Patch.ChangeType;
      28             : import com.google.gerrit.entities.Patch.PatchType;
      29             : import com.google.gerrit.extensions.client.DiffPreferencesInfo;
      30             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      31             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      32             : import com.google.gerrit.server.fixes.FixCalculator;
      33             : import com.google.gerrit.server.mime.FileTypeRegistry;
      34             : import com.google.gerrit.server.patch.DiffContentCalculator.DiffCalculatorResult;
      35             : import com.google.gerrit.server.patch.DiffContentCalculator.TextSource;
      36             : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
      37             : import com.google.gerrit.server.patch.filediff.TaggedEdit;
      38             : import com.google.inject.Inject;
      39             : import eu.medsea.mimeutil.MimeType;
      40             : import eu.medsea.mimeutil.MimeUtil2;
      41             : import java.io.IOException;
      42             : import java.util.List;
      43             : import java.util.Objects;
      44             : import java.util.Optional;
      45             : import java.util.Set;
      46             : import org.eclipse.jgit.diff.Edit;
      47             : import org.eclipse.jgit.lib.Constants;
      48             : import org.eclipse.jgit.lib.FileMode;
      49             : import org.eclipse.jgit.lib.ObjectId;
      50             : import org.eclipse.jgit.lib.ObjectReader;
      51             : import org.eclipse.jgit.lib.Repository;
      52             : import org.eclipse.jgit.revwalk.RevTree;
      53             : import org.eclipse.jgit.revwalk.RevWalk;
      54             : import org.eclipse.jgit.treewalk.TreeWalk;
      55             : 
      56             : class PatchScriptBuilder {
      57             : 
      58             :   private DiffPreferencesInfo diffPrefs;
      59             :   private final FileTypeRegistry registry;
      60             :   private IntraLineDiffCalculator intralineDiffCalculator;
      61             : 
      62             :   @Inject
      63          14 :   PatchScriptBuilder(FileTypeRegistry ftr) {
      64          14 :     registry = ftr;
      65          14 :   }
      66             : 
      67             :   void setDiffPrefs(DiffPreferencesInfo dp) {
      68          14 :     diffPrefs = dp;
      69          14 :   }
      70             : 
      71             :   void setIntraLineDiffCalculator(IntraLineDiffCalculator calculator) {
      72           6 :     intralineDiffCalculator = calculator;
      73           6 :   }
      74             : 
      75             :   /** Convert into {@link PatchScript} using the new diff cache output. */
      76             :   PatchScript toPatchScript(Repository git, FileDiffOutput content) throws IOException {
      77          12 :     PatchFileChange change =
      78             :         new PatchFileChange(
      79          12 :             content.edits().stream().map(TaggedEdit::jgitEdit).collect(toImmutableList()),
      80          12 :             content.edits().stream()
      81          12 :                 .filter(TaggedEdit::dueToRebase)
      82          12 :                 .map(TaggedEdit::jgitEdit)
      83          12 :                 .collect(toImmutableSet()),
      84          12 :             content.headerLines(),
      85          12 :             FilePathAdapter.getOldPath(content.oldPath(), content.changeType()),
      86          12 :             FilePathAdapter.getNewPath(content.oldPath(), content.newPath(), content.changeType()),
      87          12 :             content.changeType(),
      88          12 :             content.patchType().orElse(null));
      89          12 :     SidesResolver sidesResolver = new SidesResolver(git, content.comparisonType());
      90          12 :     ResolvedSides sides =
      91          12 :         resolveSides(
      92             :             git,
      93             :             sidesResolver,
      94          12 :             oldName(change),
      95          12 :             newName(change),
      96          12 :             content.oldCommitId(),
      97          12 :             content.newCommitId());
      98          12 :     return build(sides.a, sides.b, change);
      99             :   }
     100             : 
     101             :   private ResolvedSides resolveSides(
     102             :       Repository git,
     103             :       SidesResolver sidesResolver,
     104             :       String oldName,
     105             :       String newName,
     106             :       ObjectId aId,
     107             :       ObjectId bId)
     108             :       throws IOException {
     109          12 :     try (ObjectReader reader = git.newObjectReader()) {
     110          12 :       PatchSide a = sidesResolver.resolve(registry, reader, oldName, null, aId, true);
     111          12 :       PatchSide b =
     112          12 :           sidesResolver.resolve(registry, reader, newName, a, bId, Objects.equals(aId, bId));
     113          12 :       return new ResolvedSides(a, b);
     114             :     }
     115             :   }
     116             : 
     117             :   PatchScript toPatchScript(
     118             :       Repository git, ObjectId baseId, String fileName, List<FixReplacement> fixReplacements)
     119             :       throws IOException, ResourceConflictException, ResourceNotFoundException {
     120           2 :     SidesResolver sidesResolver = new SidesResolver(git, ComparisonType.againstOtherPatchSet());
     121           2 :     PatchSide a = resolveSideA(git, sidesResolver, fileName, baseId);
     122           2 :     if (a.mode == FileMode.MISSING) {
     123           2 :       throw new ResourceNotFoundException(String.format("File %s not found", fileName));
     124             :     }
     125           2 :     FixCalculator.FixResult fixResult = FixCalculator.calculateFix(a.src, fixReplacements);
     126           2 :     PatchSide b =
     127             :         new PatchSide(
     128             :             null,
     129             :             fileName,
     130           2 :             ObjectId.zeroId(),
     131             :             a.mode,
     132           2 :             fixResult.text.getContent(),
     133             :             fixResult.text,
     134             :             a.mimeType,
     135             :             a.displayMethod,
     136             :             a.fileMode);
     137             : 
     138           2 :     PatchFileChange change =
     139             :         new PatchFileChange(
     140             :             fixResult.edits,
     141           2 :             ImmutableSet.of(),
     142           2 :             ImmutableList.of(),
     143             :             fileName,
     144             :             fileName,
     145             :             ChangeType.MODIFIED,
     146             :             PatchType.UNIFIED);
     147             : 
     148           2 :     return build(a, b, change);
     149             :   }
     150             : 
     151             :   private PatchSide resolveSideA(
     152             :       Repository git, SidesResolver sidesResolver, String path, ObjectId baseId)
     153             :       throws IOException {
     154           2 :     try (ObjectReader reader = git.newObjectReader()) {
     155           2 :       return sidesResolver.resolve(registry, reader, path, null, baseId, true);
     156             :     }
     157             :   }
     158             : 
     159             :   private PatchScript build(PatchSide a, PatchSide b, PatchFileChange content) {
     160          14 :     ImmutableList<Edit> contentEdits = content.getEdits();
     161          14 :     ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase();
     162             : 
     163          14 :     IntraLineDiffCalculatorResult intralineResult = IntraLineDiffCalculatorResult.NO_RESULT;
     164             : 
     165          14 :     if (isModify(content) && intralineDiffCalculator != null && isIntralineModeAllowed(b)) {
     166           5 :       intralineResult =
     167           5 :           intralineDiffCalculator.calculateIntraLineDiff(
     168             :               contentEdits, editsDueToRebase, a.id, b.id, a.src, b.src, b.treeId, b.path);
     169             :     }
     170          14 :     ImmutableList<Edit> finalEdits = intralineResult.edits.orElse(contentEdits);
     171          14 :     DiffContentCalculator calculator = new DiffContentCalculator(diffPrefs);
     172          14 :     DiffCalculatorResult diffCalculatorResult =
     173          14 :         calculator.calculateDiffContent(new TextSource(a.src), new TextSource(b.src), finalEdits);
     174             : 
     175          14 :     return new PatchScript(
     176          14 :         content.getChangeType(),
     177          14 :         content.getOldName(),
     178          14 :         content.getNewName(),
     179             :         a.fileMode,
     180             :         b.fileMode,
     181          14 :         content.getHeaderLines(),
     182             :         diffPrefs,
     183             :         diffCalculatorResult.diffContent.a,
     184             :         diffCalculatorResult.diffContent.b,
     185             :         diffCalculatorResult.edits,
     186             :         editsDueToRebase,
     187             :         a.displayMethod,
     188             :         b.displayMethod,
     189             :         a.mimeType,
     190             :         b.mimeType,
     191             :         intralineResult.failure,
     192             :         intralineResult.timeout,
     193          14 :         content.getPatchType() == Patch.PatchType.BINARY,
     194          14 :         a.treeId == null ? null : a.treeId.getName(),
     195          14 :         b.treeId == null ? null : b.treeId.getName());
     196             :   }
     197             : 
     198             :   private static boolean isModify(PatchFileChange content) {
     199          14 :     switch (content.getChangeType()) {
     200             :       case MODIFIED:
     201             :       case COPIED:
     202             :       case RENAMED:
     203             :       case REWRITE:
     204          10 :         return true;
     205             : 
     206             :       case ADDED:
     207             :       case DELETED:
     208             :       default:
     209           8 :         return false;
     210             :     }
     211             :   }
     212             : 
     213             :   @Nullable
     214             :   private static String oldName(PatchFileChange entry) {
     215          12 :     switch (entry.getChangeType()) {
     216             :       case ADDED:
     217           8 :         return null;
     218             :       case DELETED:
     219             :       case MODIFIED:
     220           7 :         return entry.getNewName();
     221             :       case COPIED:
     222             :       case RENAMED:
     223             :       case REWRITE:
     224             :       default:
     225           4 :         return entry.getOldName();
     226             :     }
     227             :   }
     228             : 
     229             :   @Nullable
     230             :   private static String newName(PatchFileChange entry) {
     231          12 :     switch (entry.getChangeType()) {
     232             :       case DELETED:
     233           4 :         return null;
     234             :       case ADDED:
     235             :       case MODIFIED:
     236             :       case COPIED:
     237             :       case RENAMED:
     238             :       case REWRITE:
     239             :       default:
     240          12 :         return entry.getNewName();
     241             :     }
     242             :   }
     243             : 
     244             :   private static boolean isIntralineModeAllowed(PatchSide side) {
     245             :     // The intraline diff cache keys are the same for these cases. It's better to not show
     246             :     // intraline results than showing completely wrong diffs or to run into a server error.
     247           5 :     return !Patch.isMagic(side.path) && !isSubmoduleCommit(side.mode);
     248             :   }
     249             : 
     250             :   private static boolean isSubmoduleCommit(FileMode mode) {
     251           5 :     return mode.getObjectType() == Constants.OBJ_COMMIT;
     252             :   }
     253             : 
     254             :   private static class PatchSide {
     255             :     final ObjectId treeId;
     256             :     final String path;
     257             :     final ObjectId id;
     258             :     final FileMode mode;
     259             :     final byte[] srcContent;
     260             :     final Text src;
     261             :     final String mimeType;
     262             :     final DisplayMethod displayMethod;
     263             :     final PatchScript.FileMode fileMode;
     264             : 
     265             :     private PatchSide(
     266             :         ObjectId treeId,
     267             :         String path,
     268             :         ObjectId id,
     269             :         FileMode mode,
     270             :         byte[] srcContent,
     271             :         Text src,
     272             :         String mimeType,
     273             :         DisplayMethod displayMethod,
     274          14 :         PatchScript.FileMode fileMode) {
     275          14 :       this.treeId = treeId;
     276          14 :       this.path = path;
     277          14 :       this.id = id;
     278          14 :       this.mode = mode;
     279          14 :       this.srcContent = srcContent;
     280          14 :       this.src = src;
     281          14 :       this.mimeType = mimeType;
     282          14 :       this.displayMethod = displayMethod;
     283          14 :       this.fileMode = fileMode;
     284          14 :     }
     285             :   }
     286             : 
     287             :   private static class ResolvedSides {
     288             :     // Not an @AutoValue because PatchSide can't be AutoValue
     289             :     public final PatchSide a;
     290             :     public final PatchSide b;
     291             : 
     292          12 :     ResolvedSides(PatchSide a, PatchSide b) {
     293          12 :       this.a = a;
     294          12 :       this.b = b;
     295          12 :     }
     296             :   }
     297             : 
     298             :   static class SidesResolver {
     299             : 
     300             :     private final Repository db;
     301             :     private final ComparisonType comparisonType;
     302             : 
     303          14 :     SidesResolver(Repository db, ComparisonType comparisonType) {
     304          14 :       this.db = db;
     305          14 :       this.comparisonType = comparisonType;
     306          14 :     }
     307             : 
     308             :     PatchSide resolve(
     309             :         final FileTypeRegistry registry,
     310             :         final ObjectReader reader,
     311             :         final String path,
     312             :         final PatchSide other,
     313             :         final ObjectId within,
     314             :         final boolean isWithinEqualsA)
     315             :         throws IOException {
     316             :       try {
     317          14 :         boolean isCommitMsg = Patch.COMMIT_MSG.equals(path);
     318          14 :         boolean isMergeList = Patch.MERGE_LIST.equals(path);
     319          14 :         if (isCommitMsg || isMergeList) {
     320           5 :           if (comparisonType.isAgainstParentOrAutoMerge() && isWithinEqualsA) {
     321           0 :             return createSide(
     322             :                 within,
     323             :                 path,
     324           0 :                 ObjectId.zeroId(),
     325             :                 FileMode.MISSING,
     326             :                 Text.NO_BYTES,
     327             :                 Text.EMPTY,
     328           0 :                 MimeUtil2.UNKNOWN_MIME_TYPE.toString(),
     329             :                 DisplayMethod.NONE,
     330             :                 false);
     331             :           }
     332             :           Text src =
     333           5 :               isCommitMsg
     334           4 :                   ? Text.forCommit(reader, within)
     335           5 :                   : Text.forMergeList(comparisonType, reader, within);
     336           5 :           byte[] srcContent = src.getContent();
     337             :           DisplayMethod displayMethod;
     338             :           FileMode mode;
     339           5 :           if (src == Text.EMPTY) {
     340           0 :             mode = FileMode.MISSING;
     341           0 :             displayMethod = DisplayMethod.NONE;
     342             :           } else {
     343           5 :             mode = FileMode.REGULAR_FILE;
     344           5 :             displayMethod = DisplayMethod.DIFF;
     345             :           }
     346           5 :           return createSide(
     347             :               within,
     348             :               path,
     349             :               within,
     350             :               mode,
     351             :               srcContent,
     352             :               src,
     353           5 :               MimeUtil2.UNKNOWN_MIME_TYPE.toString(),
     354             :               displayMethod,
     355             :               false);
     356             :         }
     357          14 :         final TreeWalk tw = find(reader, path, within);
     358          14 :         ObjectId id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
     359          14 :         FileMode mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
     360          14 :         boolean reuse =
     361             :             other != null
     362          11 :                 && other.id.equals(id)
     363          14 :                 && (other.mode == mode || isBothFile(other.mode, mode));
     364          14 :         Text src = null;
     365             :         byte[] srcContent;
     366          14 :         if (reuse) {
     367           4 :           srcContent = other.srcContent;
     368             :         } else {
     369          14 :           srcContent = SrcContentResolver.getSourceContent(db, id, mode);
     370             :         }
     371          14 :         String mimeType = MimeUtil2.UNKNOWN_MIME_TYPE.toString();
     372          14 :         DisplayMethod displayMethod = DisplayMethod.DIFF;
     373          14 :         if (reuse) {
     374           4 :           mimeType = other.mimeType;
     375           4 :           displayMethod = other.displayMethod;
     376           4 :           src = other.src;
     377             : 
     378          14 :         } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) {
     379          13 :           MimeType registryMimeType = registry.getMimeType(path, srcContent);
     380          13 :           if ("image".equals(registryMimeType.getMediaType())
     381           0 :               && registry.isSafeInline(registryMimeType)) {
     382           0 :             displayMethod = DisplayMethod.IMG;
     383             :           }
     384          13 :           mimeType = registryMimeType.toString();
     385             :         }
     386          14 :         return createSide(within, path, id, mode, srcContent, src, mimeType, displayMethod, reuse);
     387             : 
     388           0 :       } catch (IOException err) {
     389           0 :         throw new IOException("Cannot read " + within.name() + ":" + path, err);
     390             :       }
     391             :     }
     392             : 
     393             :     private PatchSide createSide(
     394             :         ObjectId treeId,
     395             :         String path,
     396             :         ObjectId id,
     397             :         FileMode mode,
     398             :         byte[] srcContent,
     399             :         Text src,
     400             :         String mimeType,
     401             :         DisplayMethod displayMethod,
     402             :         boolean reuse) {
     403          14 :       if (!reuse) {
     404          14 :         if (srcContent == Text.NO_BYTES) {
     405          10 :           src = Text.EMPTY;
     406             :         } else {
     407          14 :           src = new Text(srcContent);
     408             :         }
     409             :       }
     410          14 :       if (mode == FileMode.MISSING) {
     411          10 :         displayMethod = DisplayMethod.NONE;
     412             :       }
     413          14 :       PatchScript.FileMode fileMode = PatchScript.FileMode.fromJgitFileMode(mode);
     414          14 :       return new PatchSide(
     415             :           treeId, path, id, mode, srcContent, src, mimeType, displayMethod, fileMode);
     416             :     }
     417             : 
     418             :     @Nullable
     419             :     private TreeWalk find(ObjectReader reader, String path, ObjectId within) throws IOException {
     420          14 :       if (path == null || within == null) {
     421           8 :         return null;
     422             :       }
     423          13 :       try (RevWalk rw = new RevWalk(reader)) {
     424          13 :         final RevTree tree = rw.parseTree(within);
     425          13 :         return TreeWalk.forPath(reader, path, tree);
     426             :       }
     427             :     }
     428             :   }
     429             : 
     430             :   private static boolean isBothFile(FileMode a, FileMode b) {
     431           0 :     return (a.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE
     432           0 :         && (b.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE;
     433             :   }
     434             : 
     435             :   static class IntraLineDiffCalculatorResult {
     436             :     // Not an @AutoValue because Edit is mutable
     437             :     final boolean failure;
     438             :     final boolean timeout;
     439             :     private final Optional<ImmutableList<Edit>> edits;
     440             : 
     441             :     private IntraLineDiffCalculatorResult(
     442          14 :         Optional<ImmutableList<Edit>> edits, boolean failure, boolean timeout) {
     443          14 :       this.failure = failure;
     444          14 :       this.timeout = timeout;
     445          14 :       this.edits = edits;
     446          14 :     }
     447             : 
     448          14 :     static final IntraLineDiffCalculatorResult NO_RESULT =
     449          14 :         new IntraLineDiffCalculatorResult(Optional.empty(), false, false);
     450          14 :     static final IntraLineDiffCalculatorResult FAILURE =
     451          14 :         new IntraLineDiffCalculatorResult(Optional.empty(), true, false);
     452          14 :     static final IntraLineDiffCalculatorResult TIMEOUT =
     453          14 :         new IntraLineDiffCalculatorResult(Optional.empty(), false, true);
     454             : 
     455             :     static IntraLineDiffCalculatorResult success(ImmutableList<Edit> edits) {
     456           5 :       return new IntraLineDiffCalculatorResult(Optional.of(edits), false, false);
     457             :     }
     458             :   }
     459             : 
     460             :   interface IntraLineDiffCalculator {
     461             : 
     462             :     IntraLineDiffCalculatorResult calculateIntraLineDiff(
     463             :         ImmutableList<Edit> edits,
     464             :         Set<Edit> editsDueToRebase,
     465             :         ObjectId aId,
     466             :         ObjectId bId,
     467             :         Text aSrc,
     468             :         Text bSrc,
     469             :         ObjectId bTreeId,
     470             :         String bPath);
     471             :   }
     472             : 
     473             :   static class PatchFileChange {
     474             :     private final ImmutableList<Edit> edits;
     475             :     private final ImmutableSet<Edit> editsDueToRebase;
     476             :     private final ImmutableList<String> headerLines;
     477             :     private final String oldName;
     478             :     private final String newName;
     479             :     private final ChangeType changeType;
     480             :     private final Patch.PatchType patchType;
     481             : 
     482             :     public PatchFileChange(
     483             :         ImmutableList<Edit> edits,
     484             :         ImmutableSet<Edit> editsDueToRebase,
     485             :         ImmutableList<String> headerLines,
     486             :         String oldName,
     487             :         String newName,
     488             :         ChangeType changeType,
     489          14 :         Patch.PatchType patchType) {
     490          14 :       this.edits = edits;
     491          14 :       this.editsDueToRebase = editsDueToRebase;
     492          14 :       this.headerLines = headerLines;
     493          14 :       this.oldName = oldName;
     494          14 :       this.newName = newName;
     495          14 :       this.changeType = changeType;
     496          14 :       this.patchType = patchType;
     497          14 :     }
     498             : 
     499             :     ImmutableList<Edit> getEdits() {
     500          14 :       return edits;
     501             :     }
     502             : 
     503             :     ImmutableSet<Edit> getEditsDueToRebase() {
     504          14 :       return editsDueToRebase;
     505             :     }
     506             : 
     507             :     ImmutableList<String> getHeaderLines() {
     508          14 :       return headerLines;
     509             :     }
     510             : 
     511             :     String getNewName() {
     512          14 :       return newName;
     513             :     }
     514             : 
     515             :     String getOldName() {
     516          14 :       return oldName;
     517             :     }
     518             : 
     519             :     ChangeType getChangeType() {
     520          14 :       return changeType;
     521             :     }
     522             : 
     523             :     Patch.PatchType getPatchType() {
     524          14 :       return patchType;
     525             :     }
     526             :   }
     527             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750