com.google.gerrit.server.patch.DiffContentCalculator Maven / Gradle / Ivy
// Copyright (C) 2019 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;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.jgit.diff.ReplaceEdit;
import com.google.gerrit.prettify.common.EditHunk;
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.prettify.common.SparseFileContentBuilder;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.diff.Edit;
/** Collects all lines and their content to be displayed in diff view. */
class DiffContentCalculator {
  private final DiffPreferencesInfo diffPrefs;
  DiffContentCalculator(DiffPreferencesInfo diffPrefs) {
    this.diffPrefs = diffPrefs;
  }
  /**
   * Gather information necessary to display line-by-line difference between 2 texts.
   *
   * The method returns instance of {@link DiffCalculatorResult} with the following data:
   *
   * 
   *   - All changed lines
   *   
 - Additional lines to be displayed above and below the changed lines
   *   
 - All changed and unchanged lines with comments
   *   
 - Additional lines to be displayed above and below lines with commentsEdits with special
   *       "fake" edits for unchanged lines with comments
   * 
 
   *
   * More details can be found in {@link DiffCalculatorResult}.
   *
   * @param srcA Original text content
   * @param srcB New text content
   * @param edits List of edits which was applied to srcA to produce srcB
   * @return an instance of {@link DiffCalculatorResult}.
   */
  DiffCalculatorResult calculateDiffContent(
      TextSource srcA, TextSource srcB, ImmutableList edits) {
    if (srcA.src == srcB.src && edits.isEmpty()) {
      // Odd special case; the files are identical (100% rename or copy)
      // and the user has asked for context that is larger than the file.
      // Send them the entire file, with an empty edit after the last line.
      //
      SparseFileContentBuilder diffA = new SparseFileContentBuilder(srcA.size());
      for (int i = 0; i < srcA.size(); i++) {
        srcA.copyLineTo(diffA, i);
      }
      DiffContent diffContent =
          new DiffContent(diffA.build(), SparseFileContent.create(ImmutableList.of(), srcB.size()));
      Edit emptyEdit = new Edit(srcA.size(), srcA.size());
      return new DiffCalculatorResult(diffContent, ImmutableList.of(emptyEdit));
    }
    ImmutableList sortedEdits = correctForDifferencesInNewlineAtEnd(srcA, srcB, edits);
    DiffContent diffContent =
        packContent(srcA, srcB, diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE, sortedEdits);
    return new DiffCalculatorResult(diffContent, sortedEdits);
  }
  private ImmutableList correctForDifferencesInNewlineAtEnd(
      TextSource a, TextSource b, ImmutableList edits) {
    // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it.
    int aSize = a.src.size();
    int bSize = b.src.size();
    if (edits.isEmpty() && (aSize == 0 || bSize == 0)) {
      // The diff was requested for a file which was either added or deleted but which JGit doesn't
      // consider a file addition/deletion (e.g. requesting a diff for the old file name of a
      // renamed file looks like a deletion).
      return edits;
    }
    if (edits.isEmpty() && (aSize != bSize)) {
      // Only edits due to rebase were present. If we now added the edits for the newlines, the
      // code which later assembles the file contents would fail.
      return edits;
    }
    Optional lastEdit = getLast(edits);
    if (isNewlineAtEndDeleted(a, b)) {
      Optional lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
      if (lastLineEdit.isPresent()) {
        Edit edit = lastLineEdit.get();
        Edit updatedLastLineEdit =
            edit instanceof ReplaceEdit
                ? new ReplaceEdit(
                    edit.getBeginA(),
                    edit.getEndA() + 1,
                    edit.getBeginB(),
                    edit.getEndB(),
                    ((ReplaceEdit) edit).getInternalEdits())
                : new Edit(edit.getBeginA(), edit.getEndA() + 1, edit.getBeginB(), edit.getEndB());
        ImmutableList.Builder newEditsBuilder =
            ImmutableList.builderWithExpectedSize(edits.size());
        return newEditsBuilder
            .addAll(edits.subList(0, edits.size() - 1))
            .add(updatedLastLineEdit)
            .build();
      }
      ImmutableList.Builder newEditsBuilder =
          ImmutableList.builderWithExpectedSize(edits.size() + 1);
      Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize);
      return newEditsBuilder.addAll(edits).add(newlineEdit).build();
    } else if (isNewlineAtEndAdded(a, b)) {
      Optional lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize);
      if (lastLineEdit.isPresent()) {
        Edit edit = lastLineEdit.get();
        Edit updatedLastLineEdit =
            edit instanceof ReplaceEdit
                ? new ReplaceEdit(
                    edit.getBeginA(),
                    edit.getEndA(),
                    edit.getBeginB(),
                    edit.getEndB() + 1,
                    ((ReplaceEdit) edit).getInternalEdits())
                : new Edit(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB() + 1);
        ImmutableList.Builder newEditsBuilder =
            ImmutableList.builderWithExpectedSize(edits.size());
        return newEditsBuilder
            .addAll(edits.subList(0, edits.size() - 1))
            .add(updatedLastLineEdit)
            .build();
      }
      ImmutableList.Builder newEditsBuilder =
          ImmutableList.builderWithExpectedSize(edits.size() + 1);
      Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1);
      return newEditsBuilder.addAll(edits).add(newlineEdit).build();
    }
    return edits;
  }
  private static  Optional getLast(List list) {
    return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1));
  }
  private boolean isNewlineAtEndDeleted(TextSource a, TextSource b) {
    return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd();
  }
  private boolean isNewlineAtEndAdded(TextSource a, TextSource b) {
    return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd();
  }
  private DiffContent packContent(
      TextSource a, TextSource b, boolean ignoredWhitespace, ImmutableList edits) {
    SparseFileContentBuilder diffA = new SparseFileContentBuilder(a.size());
    SparseFileContentBuilder diffB = new SparseFileContentBuilder(b.size());
    if (!edits.isEmpty()) {
      EditHunk hunk = new EditHunk(edits, a.size(), b.size());
      while (hunk.next()) {
        if (hunk.isUnmodifiedLine()) {
          String lineA = a.getSourceLine(hunk.getCurA());
          diffA.addLine(hunk.getCurA(), lineA);
          if (ignoredWhitespace) {
            // If we ignored whitespace in some form, also get the line
            // from b when it does not exactly match the line from a.
            //
            String lineB = b.getSourceLine(hunk.getCurB());
            if (!lineA.equals(lineB)) {
              diffB.addLine(hunk.getCurB(), lineB);
            }
          }
          hunk.incBoth();
          continue;
        }
        if (hunk.isDeletedA()) {
          a.copyLineTo(diffA, hunk.getCurA());
          hunk.incA();
        }
        if (hunk.isInsertedB()) {
          b.copyLineTo(diffB, hunk.getCurB());
          hunk.incB();
        }
      }
    }
    return new DiffContent(diffA.build(), diffB.build());
  }
  /** Contains information to be displayed in line-by-line diff view. */
  static class DiffCalculatorResult {
    // This class is not @AutoValue, because Edit is mutable
    /** Lines to be displayed */
    final DiffContent diffContent;
    /** List of edits including "fake" edits for unchanged lines with comments. */
    final ImmutableList edits;
    DiffCalculatorResult(DiffContent diffContent, ImmutableList edits) {
      this.diffContent = diffContent;
      this.edits = edits;
    }
  }
  /** Lines to be displayed in line-by-line diff view. */
  static class DiffContent {
    /* All lines from the original text (i.e. srcA) to be displayed. */
    final SparseFileContent a;
    /**
     * All lines from the new text (i.e. srcB) which are different than in original text. Lines are:
     * a) All changed lines (i.e. if the content of the line was replaced with the new line) b) All
     * inserted lines Note, that deleted lines are added to the a and are not added to b
     */
    final SparseFileContent b;
    DiffContent(SparseFileContent a, SparseFileContent b) {
      this.a = a;
      this.b = b;
    }
  }
  static class TextSource {
    final Text src;
    TextSource(Text src) {
      this.src = src;
    }
    int size() {
      if (src == null) {
        return 0;
      }
      if (src.isMissingNewlineAtEnd()) {
        return src.size();
      }
      return src.size() + 1;
    }
    void copyLineTo(SparseFileContentBuilder target, int lineNumber) {
      target.addLine(lineNumber, getSourceLine(lineNumber));
    }
    private String getSourceLine(int lineNumber) {
      return lineNumber >= src.size() ? "" : src.getString(lineNumber);
    }
  }
}