| // 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. | |
| * | |
| * <p>The method returns instance of {@link DiffCalculatorResult} with the following data: | |
| * | |
| * <ul> | |
| * <li>All changed lines | |
| * <li>Additional lines to be displayed above and below the changed lines | |
| * <li>All changed and unchanged lines with comments | |
| * <li>Additional lines to be displayed above and below lines with commentsEdits with special | |
| * "fake" edits for unchanged lines with comments | |
| * </ul> | |
| * | |
| * <p>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<Edit> 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<Edit> sortedEdits = correctForDifferencesInNewlineAtEnd(srcA, srcB, edits); | |
| DiffContent diffContent = | |
| packContent(srcA, srcB, diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE, sortedEdits); | |
| return new DiffCalculatorResult(diffContent, sortedEdits); | |
| } | |
| private ImmutableList<Edit> correctForDifferencesInNewlineAtEnd( | |
| TextSource a, TextSource b, ImmutableList<Edit> 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<Edit> lastEdit = getLast(edits); | |
| if (isNewlineAtEndDeleted(a, b)) { | |
| Optional<Edit> 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<Edit> newEditsBuilder = | |
| ImmutableList.builderWithExpectedSize(edits.size()); | |
| return newEditsBuilder | |
| .addAll(edits.subList(0, edits.size() - 1)) | |
| .add(updatedLastLineEdit) | |
| .build(); | |
| } | |
| ImmutableList.Builder<Edit> 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<Edit> 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<Edit> newEditsBuilder = | |
| ImmutableList.builderWithExpectedSize(edits.size()); | |
| return newEditsBuilder | |
| .addAll(edits.subList(0, edits.size() - 1)) | |
| .add(updatedLastLineEdit) | |
| .build(); | |
| } | |
| ImmutableList.Builder<Edit> 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 <T> Optional<T> getLast(List<T> 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<Edit> 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<Edit> edits; | |
| DiffCalculatorResult(DiffContent diffContent, ImmutableList<Edit> 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); | |
| } | |
| } | |
| } |