LCOV - code coverage report
Current view: top level - server/patch - DiffContentCalculator.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 108 110 98.2 %
Date: 2022-11-19 15:00:39 Functions: 15 15 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2019 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 com.google.common.collect.ImmutableList;
      18             : import com.google.gerrit.extensions.client.DiffPreferencesInfo;
      19             : import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
      20             : import com.google.gerrit.jgit.diff.ReplaceEdit;
      21             : import com.google.gerrit.prettify.common.EditHunk;
      22             : import com.google.gerrit.prettify.common.SparseFileContent;
      23             : import com.google.gerrit.prettify.common.SparseFileContentBuilder;
      24             : import java.util.List;
      25             : import java.util.Optional;
      26             : import org.eclipse.jgit.diff.Edit;
      27             : 
      28             : /** Collects all lines and their content to be displayed in diff view. */
      29             : class DiffContentCalculator {
      30             :   private final DiffPreferencesInfo diffPrefs;
      31             : 
      32          14 :   DiffContentCalculator(DiffPreferencesInfo diffPrefs) {
      33          14 :     this.diffPrefs = diffPrefs;
      34          14 :   }
      35             : 
      36             :   /**
      37             :    * Gather information necessary to display line-by-line difference between 2 texts.
      38             :    *
      39             :    * <p>The method returns instance of {@link DiffCalculatorResult} with the following data:
      40             :    *
      41             :    * <ul>
      42             :    *   <li>All changed lines
      43             :    *   <li>Additional lines to be displayed above and below the changed lines
      44             :    *   <li>All changed and unchanged lines with comments
      45             :    *   <li>Additional lines to be displayed above and below lines with commentsEdits with special
      46             :    *       "fake" edits for unchanged lines with comments
      47             :    * </ul>
      48             :    *
      49             :    * <p>More details can be found in {@link DiffCalculatorResult}.
      50             :    *
      51             :    * @param srcA Original text content
      52             :    * @param srcB New text content
      53             :    * @param edits List of edits which was applied to srcA to produce srcB
      54             :    * @return an instance of {@link DiffCalculatorResult}.
      55             :    */
      56             :   DiffCalculatorResult calculateDiffContent(
      57             :       TextSource srcA, TextSource srcB, ImmutableList<Edit> edits) {
      58          14 :     if (srcA.src == srcB.src && edits.isEmpty()) {
      59             :       // Odd special case; the files are identical (100% rename or copy)
      60             :       // and the user has asked for context that is larger than the file.
      61             :       // Send them the entire file, with an empty edit after the last line.
      62             :       //
      63           4 :       SparseFileContentBuilder diffA = new SparseFileContentBuilder(srcA.size());
      64           4 :       for (int i = 0; i < srcA.size(); i++) {
      65           4 :         srcA.copyLineTo(diffA, i);
      66             :       }
      67           4 :       DiffContent diffContent =
      68           4 :           new DiffContent(diffA.build(), SparseFileContent.create(ImmutableList.of(), srcB.size()));
      69           4 :       Edit emptyEdit = new Edit(srcA.size(), srcA.size());
      70           4 :       return new DiffCalculatorResult(diffContent, ImmutableList.of(emptyEdit));
      71             :     }
      72          14 :     ImmutableList<Edit> sortedEdits = correctForDifferencesInNewlineAtEnd(srcA, srcB, edits);
      73             : 
      74          14 :     DiffContent diffContent =
      75          14 :         packContent(srcA, srcB, diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE, sortedEdits);
      76          14 :     return new DiffCalculatorResult(diffContent, sortedEdits);
      77             :   }
      78             : 
      79             :   private ImmutableList<Edit> correctForDifferencesInNewlineAtEnd(
      80             :       TextSource a, TextSource b, ImmutableList<Edit> edits) {
      81             :     // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it.
      82          14 :     int aSize = a.src.size();
      83          14 :     int bSize = b.src.size();
      84             : 
      85          14 :     if (edits.isEmpty() && (aSize == 0 || bSize == 0)) {
      86             :       // The diff was requested for a file which was either added or deleted but which JGit doesn't
      87             :       // consider a file addition/deletion (e.g. requesting a diff for the old file name of a
      88             :       // renamed file looks like a deletion).
      89           2 :       return edits;
      90             :     }
      91             : 
      92          14 :     if (edits.isEmpty() && (aSize != bSize)) {
      93             :       // Only edits due to rebase were present. If we now added the edits for the newlines, the
      94             :       // code which later assembles the file contents would fail.
      95           2 :       return edits;
      96             :     }
      97             : 
      98          14 :     Optional<Edit> lastEdit = getLast(edits);
      99          14 :     if (isNewlineAtEndDeleted(a, b)) {
     100           3 :       Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
     101             : 
     102           3 :       if (lastLineEdit.isPresent()) {
     103           3 :         Edit edit = lastLineEdit.get();
     104             :         Edit updatedLastLineEdit =
     105           3 :             edit instanceof ReplaceEdit
     106           1 :                 ? new ReplaceEdit(
     107           1 :                     edit.getBeginA(),
     108           1 :                     edit.getEndA() + 1,
     109           1 :                     edit.getBeginB(),
     110           1 :                     edit.getEndB(),
     111           1 :                     ((ReplaceEdit) edit).getInternalEdits())
     112           3 :                 : new Edit(edit.getBeginA(), edit.getEndA() + 1, edit.getBeginB(), edit.getEndB());
     113             : 
     114           3 :         ImmutableList.Builder<Edit> newEditsBuilder =
     115           3 :             ImmutableList.builderWithExpectedSize(edits.size());
     116           3 :         return newEditsBuilder
     117           3 :             .addAll(edits.subList(0, edits.size() - 1))
     118           3 :             .add(updatedLastLineEdit)
     119           3 :             .build();
     120             :       }
     121           2 :       ImmutableList.Builder<Edit> newEditsBuilder =
     122           2 :           ImmutableList.builderWithExpectedSize(edits.size() + 1);
     123           2 :       Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize);
     124           2 :       return newEditsBuilder.addAll(edits).add(newlineEdit).build();
     125             : 
     126          14 :     } else if (isNewlineAtEndAdded(a, b)) {
     127           7 :       Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize);
     128           7 :       if (lastLineEdit.isPresent()) {
     129           7 :         Edit edit = lastLineEdit.get();
     130             :         Edit updatedLastLineEdit =
     131           7 :             edit instanceof ReplaceEdit
     132           3 :                 ? new ReplaceEdit(
     133           3 :                     edit.getBeginA(),
     134           3 :                     edit.getEndA(),
     135           3 :                     edit.getBeginB(),
     136           3 :                     edit.getEndB() + 1,
     137           3 :                     ((ReplaceEdit) edit).getInternalEdits())
     138           7 :                 : new Edit(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB() + 1);
     139             : 
     140           7 :         ImmutableList.Builder<Edit> newEditsBuilder =
     141           7 :             ImmutableList.builderWithExpectedSize(edits.size());
     142           7 :         return newEditsBuilder
     143           7 :             .addAll(edits.subList(0, edits.size() - 1))
     144           7 :             .add(updatedLastLineEdit)
     145           7 :             .build();
     146             :       }
     147           2 :       ImmutableList.Builder<Edit> newEditsBuilder =
     148           2 :           ImmutableList.builderWithExpectedSize(edits.size() + 1);
     149           2 :       Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1);
     150           2 :       return newEditsBuilder.addAll(edits).add(newlineEdit).build();
     151             :     }
     152          12 :     return edits;
     153             :   }
     154             : 
     155             :   private static <T> Optional<T> getLast(List<T> list) {
     156          14 :     return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1));
     157             :   }
     158             : 
     159             :   private boolean isNewlineAtEndDeleted(TextSource a, TextSource b) {
     160          14 :     return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd();
     161             :   }
     162             : 
     163             :   private boolean isNewlineAtEndAdded(TextSource a, TextSource b) {
     164          14 :     return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd();
     165             :   }
     166             : 
     167             :   private DiffContent packContent(
     168             :       TextSource a, TextSource b, boolean ignoredWhitespace, ImmutableList<Edit> edits) {
     169          14 :     SparseFileContentBuilder diffA = new SparseFileContentBuilder(a.size());
     170          14 :     SparseFileContentBuilder diffB = new SparseFileContentBuilder(b.size());
     171          14 :     if (!edits.isEmpty()) {
     172          14 :       EditHunk hunk = new EditHunk(edits, a.size(), b.size());
     173          14 :       while (hunk.next()) {
     174          14 :         if (hunk.isUnmodifiedLine()) {
     175           6 :           String lineA = a.getSourceLine(hunk.getCurA());
     176           6 :           diffA.addLine(hunk.getCurA(), lineA);
     177             : 
     178           6 :           if (ignoredWhitespace) {
     179             :             // If we ignored whitespace in some form, also get the line
     180             :             // from b when it does not exactly match the line from a.
     181             :             //
     182           3 :             String lineB = b.getSourceLine(hunk.getCurB());
     183           3 :             if (!lineA.equals(lineB)) {
     184           0 :               diffB.addLine(hunk.getCurB(), lineB);
     185             :             }
     186             :           }
     187           6 :           hunk.incBoth();
     188           6 :           continue;
     189             :         }
     190             : 
     191          14 :         if (hunk.isDeletedA()) {
     192          10 :           a.copyLineTo(diffA, hunk.getCurA());
     193          10 :           hunk.incA();
     194             :         }
     195             : 
     196          14 :         if (hunk.isInsertedB()) {
     197          14 :           b.copyLineTo(diffB, hunk.getCurB());
     198          14 :           hunk.incB();
     199             :         }
     200             :       }
     201             :     }
     202          14 :     return new DiffContent(diffA.build(), diffB.build());
     203             :   }
     204             : 
     205             :   /** Contains information to be displayed in line-by-line diff view. */
     206             :   static class DiffCalculatorResult {
     207             :     // This class is not @AutoValue, because Edit is mutable
     208             : 
     209             :     /** Lines to be displayed */
     210             :     final DiffContent diffContent;
     211             :     /** List of edits including "fake" edits for unchanged lines with comments. */
     212             :     final ImmutableList<Edit> edits;
     213             : 
     214          14 :     DiffCalculatorResult(DiffContent diffContent, ImmutableList<Edit> edits) {
     215          14 :       this.diffContent = diffContent;
     216          14 :       this.edits = edits;
     217          14 :     }
     218             :   }
     219             : 
     220             :   /** Lines to be displayed in line-by-line diff view. */
     221             :   static class DiffContent {
     222             :     /* All lines from the original text (i.e. srcA) to be displayed. */
     223             :     final SparseFileContent a;
     224             :     /**
     225             :      * All lines from the new text (i.e. srcB) which are different than in original text. Lines are:
     226             :      * a) All changed lines (i.e. if the content of the line was replaced with the new line) b) All
     227             :      * inserted lines Note, that deleted lines are added to the a and are not added to b
     228             :      */
     229             :     final SparseFileContent b;
     230             : 
     231          14 :     DiffContent(SparseFileContent a, SparseFileContent b) {
     232          14 :       this.a = a;
     233          14 :       this.b = b;
     234          14 :     }
     235             :   }
     236             : 
     237             :   static class TextSource {
     238             :     final Text src;
     239             : 
     240          14 :     TextSource(Text src) {
     241          14 :       this.src = src;
     242          14 :     }
     243             : 
     244             :     int size() {
     245          14 :       if (src == null) {
     246           0 :         return 0;
     247             :       }
     248          14 :       if (src.isMissingNewlineAtEnd()) {
     249          14 :         return src.size();
     250             :       }
     251           8 :       return src.size() + 1;
     252             :     }
     253             : 
     254             :     void copyLineTo(SparseFileContentBuilder target, int lineNumber) {
     255          14 :       target.addLine(lineNumber, getSourceLine(lineNumber));
     256          14 :     }
     257             : 
     258             :     private String getSourceLine(int lineNumber) {
     259          14 :       return lineNumber >= src.size() ? "" : src.getString(lineNumber);
     260             :     }
     261             :   }
     262             : }

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