All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 static java.util.Comparator.comparing;

import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.data.CommentDetail;
import com.google.gerrit.entities.Comment;
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.EditList;
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.prettify.common.SparseFileContentBuilder;
import java.util.Comparator;
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 static final int MAX_CONTEXT = 5000000;

  private static final Comparator EDIT_SORT = comparing(Edit::getBeginA);

  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 * @param comments Existing comments for srcA and srcB * @return an instance of {@link DiffCalculatorResult}. */ DiffCalculatorResult calculateDiffContent( TextSource srcA, TextSource srcB, ImmutableList edits, CommentDetail comments) { int context = getContext(); if (srcA.src == srcB.src && srcA.size() <= context && 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.Builder builder = ImmutableList.builder(); builder.addAll(correctForDifferencesInNewlineAtEnd(srcA, srcB, edits)); boolean nonsortedEdits = false; if (comments != null) { ImmutableList commentEdits = ensureCommentsVisible(comments, edits); builder.addAll(commentEdits); nonsortedEdits = !commentEdits.isEmpty(); } ImmutableList sortedEdits = builder.build(); if (nonsortedEdits) { sortedEdits = ImmutableList.sortedCopyOf(EDIT_SORT, sortedEdits); } // In order to expand the skipped common lines or syntax highlight the // file properly we need to give the client the complete file contents. // So force our context temporarily to the complete file size. // DiffContent diffContent = packContent( srcA, srcB, diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE, sortedEdits, MAX_CONTEXT); return new DiffCalculatorResult(diffContent, sortedEdits); } private int getContext() { if (diffPrefs.context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) { return MAX_CONTEXT; } return Math.min(diffPrefs.context, MAX_CONTEXT); } 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 ImmutableList ensureCommentsVisible( CommentDetail comments, ImmutableList edits) { if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) { // No comments, no additional dummy edits are required. // return ImmutableList.of(); } // Construct empty Edit blocks around each location where a comment is. // This will force the later packContent method to include the regions // containing comments, potentially combining those regions together if // they have overlapping contexts. UI renders will also be able to make // correct hunks from this, but because the Edit is empty they will not // style it specially. // final ImmutableList.Builder commmentEdits = ImmutableList.builder(); int lastLine; lastLine = -1; for (Comment c : comments.getCommentsA()) { final int a = c.lineNbr; if (lastLine != a) { final int b = mapA2B(a - 1, edits); if (0 <= b) { getNewEditForComment(edits, new Edit(a - 1, b)).ifPresent(commmentEdits::add); } lastLine = a; } } lastLine = -1; for (Comment c : comments.getCommentsB()) { int b = c.lineNbr; if (lastLine != b) { final int a = mapB2A(b - 1, edits); if (0 <= a) { getNewEditForComment(edits, new Edit(a, b - 1)).ifPresent(commmentEdits::add); } lastLine = b; } } return commmentEdits.build(); } private Optional getNewEditForComment(ImmutableList edits, Edit toAdd) { final int a = toAdd.getBeginA(); final int b = toAdd.getBeginB(); for (Edit e : edits) { if (e.getBeginA() <= a && a <= e.getEndA()) { return Optional.empty(); } if (e.getBeginB() <= b && b <= e.getEndB()) { return Optional.empty(); } } return Optional.of(toAdd); } private int mapA2B(int a, ImmutableList edits) { if (edits.isEmpty()) { // Magic special case of an unmodified file. // return a; } for (int i = 0; i < edits.size(); i++) { final Edit e = edits.get(i); if (a < e.getBeginA()) { if (i == 0) { // Special case of context at start of file. // return a; } return e.getBeginB() - (e.getBeginA() - a); } if (e.getBeginA() <= a && a <= e.getEndA()) { return -1; } } final Edit last = edits.get(edits.size() - 1); return last.getEndB() + (a - last.getEndA()); } private int mapB2A(int b, ImmutableList edits) { if (edits.isEmpty()) { // Magic special case of an unmodified file. // return b; } for (int i = 0; i < edits.size(); i++) { final Edit e = edits.get(i); if (b < e.getBeginB()) { if (i == 0) { // Special case of context at start of file. // return b; } return e.getBeginA() - (e.getBeginB() - b); } if (e.getBeginB() <= b && b <= e.getEndB()) { return -1; } } final Edit last = edits.get(edits.size() - 1); return last.getEndA() + (b - last.getEndB()); } private DiffContent packContent( TextSource a, TextSource b, boolean ignoredWhitespace, ImmutableList edits, int context) { SparseFileContentBuilder diffA = new SparseFileContentBuilder(a.size()); SparseFileContentBuilder diffB = new SparseFileContentBuilder(b.size()); EditList list = new EditList(edits, context, a.size(), b.size()); for (EditList.Hunk hunk : list.getHunks()) { while (hunk.next()) { if (hunk.isContextLine()) { 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); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy