blob: a387da6d2e8f3b5ccbad6e5e3d82f908abab7c66 [file] [log] [blame]
// 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);
}
}
}