Extract DiffInfoCreator from GetDiff class

Extract DiffInfoCreator for reuse in robot's preview fix.

Change-Id: I32e6f41007a38e0d758b209046cbd45c8cf6b49d
This commit is contained in:
Dmitrii Filippov 2019-10-02 16:41:00 +02:00
parent bd41629222
commit 0a0ff68a03
12 changed files with 957 additions and 415 deletions

View File

@ -7,5 +7,7 @@ java_library(
deps = [
"//lib:guava",
"//lib:jgit",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
],
)

View File

@ -14,160 +14,175 @@
package com.google.gerrit.prettify.common;
import java.util.ArrayList;
import java.util.List;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
public class SparseFileContent {
protected List<Range> ranges;
protected int size;
/**
* A class to store subset of a file's lines in a memory efficient way. Internally, it stores lines
* as a list of ranges. Each range represents continuous set of lines and has information about line
* numbers in original file (zero-based).
*
* <p>{@link SparseFileContent.Accessor} must be used to work with the stored content.
*/
@AutoValue
public abstract class SparseFileContent {
abstract ImmutableList<Range> getRanges();
private transient int currentRangeIdx;
public abstract int getSize();
public SparseFileContent() {
ranges = new ArrayList<>();
public static SparseFileContent create(ImmutableList<Range> ranges, int size) {
return new AutoValue_SparseFileContent(ranges, size);
}
public int size() {
return size;
@VisibleForTesting
public int getRangesCount() {
return getRanges().size();
}
public void setSize(int s) {
size = s;
public Accessor createAccessor() {
return new Accessor(this);
}
public String get(int idx) {
final String line = getLine(idx);
if (line == null) {
throw new ArrayIndexOutOfBoundsException(idx);
}
return line;
}
/**
* Provide a methods to work with the content of a {@link SparseFileContent}.
*
* <p>The class hides internal representation of a {@link SparseFileContent} and provides
* convenient way for accessing a content.
*/
public static class Accessor {
private final SparseFileContent content;
private int currentRangeIdx;
public boolean contains(int idx) {
return getLine(idx) != null;
}
public int first() {
return ranges.isEmpty() ? size() : ranges.get(0).base;
}
public int next(int idx) {
// Most requests are sequential in nature, fetching the next
// line from the current range, or the immediate next range.
//
int high = ranges.size();
if (currentRangeIdx < high) {
Range cur = ranges.get(currentRangeIdx);
if (cur.contains(idx + 1)) {
return idx + 1;
}
if (++currentRangeIdx < high) {
// Its not plus one, its the base of the next range.
//
return ranges.get(currentRangeIdx).base;
}
private Accessor(SparseFileContent content) {
this.content = content;
}
// Binary search for the current value, since we know its a sorted list.
//
int low = 0;
do {
final int mid = (low + high) / 2;
final Range cur = ranges.get(mid);
public String get(int idx) {
final String line = getLine(idx);
if (line == null) {
throw new ArrayIndexOutOfBoundsException(idx);
}
return line;
}
if (cur.contains(idx)) {
public int getSize() {
return content.getSize();
}
public boolean contains(int idx) {
return getLine(idx) != null;
}
public int first() {
return content.getRanges().isEmpty() ? getSize() : content.getRanges().get(0).getBase();
}
public int next(int idx) {
// Most requests are sequential in nature, fetching the next
// line from the current range, or the immediate next range.
//
ImmutableList<Range> ranges = content.getRanges();
int high = ranges.size();
if (currentRangeIdx < high) {
Range cur = ranges.get(currentRangeIdx);
if (cur.contains(idx + 1)) {
// Trivial plus one case above failed due to wrong currentRangeIdx.
// Reset the cache so we don't miss in the future.
//
currentRangeIdx = mid;
return idx + 1;
}
if (mid + 1 < ranges.size()) {
// Its the base of the next range.
currentRangeIdx = mid + 1;
return ranges.get(currentRangeIdx).base;
}
// No more lines in the file.
//
return size();
}
if (idx < cur.base) {
high = mid;
} else {
low = mid + 1;
}
} while (low < high);
return size();
}
private String getLine(int idx) {
// Most requests are sequential in nature, fetching the next
// line from the current range, or the next range.
//
int high = ranges.size();
if (currentRangeIdx < high) {
Range cur = ranges.get(currentRangeIdx);
if (cur.contains(idx)) {
return cur.get(idx);
}
if (++currentRangeIdx < high) {
final Range next = ranges.get(currentRangeIdx);
if (next.contains(idx)) {
return next.get(idx);
if (++currentRangeIdx < high) {
// Its not plus one, its the base of the next range.
//
return ranges.get(currentRangeIdx).getBase();
}
}
// Binary search for the current value, since we know its a sorted list.
//
int low = 0;
do {
final int mid = (low + high) / 2;
final Range cur = ranges.get(mid);
if (cur.contains(idx)) {
if (cur.contains(idx + 1)) {
// Trivial plus one case above failed due to wrong currentRangeIdx.
// Reset the cache so we don't miss in the future.
//
currentRangeIdx = mid;
return idx + 1;
}
if (mid + 1 < ranges.size()) {
// Its the base of the next range.
currentRangeIdx = mid + 1;
return ranges.get(currentRangeIdx).getBase();
}
// No more lines in the file.
//
return getSize();
}
if (idx < cur.getBase()) {
high = mid;
} else {
low = mid + 1;
}
} while (low < high);
return getSize();
}
// Binary search for the range, since we know its a sorted list.
//
if (ranges.isEmpty()) {
private String getLine(int idx) {
// Most requests are sequential in nature, fetching the next
// line from the current range, or the next range.
//
ImmutableList<Range> ranges = content.getRanges();
int high = ranges.size();
if (currentRangeIdx < high) {
Range cur = ranges.get(currentRangeIdx);
if (cur.contains(idx)) {
return cur.get(idx);
}
if (++currentRangeIdx < high) {
final Range next = ranges.get(currentRangeIdx);
if (next.contains(idx)) {
return next.get(idx);
}
}
}
// Binary search for the range, since we know its a sorted list.
//
if (ranges.isEmpty()) {
return null;
}
int low = 0;
do {
final int mid = (low + high) / 2;
final Range cur = ranges.get(mid);
if (cur.contains(idx)) {
currentRangeIdx = mid;
return cur.get(idx);
}
if (idx < cur.getBase()) {
high = mid;
} else {
low = mid + 1;
}
} while (low < high);
return null;
}
int low = 0;
do {
final int mid = (low + high) / 2;
final Range cur = ranges.get(mid);
if (cur.contains(idx)) {
currentRangeIdx = mid;
return cur.get(idx);
}
if (idx < cur.base) {
high = mid;
} else {
low = mid + 1;
}
} while (low < high);
return null;
}
public void addLine(int i, String content) {
final Range r;
if (!ranges.isEmpty() && i == last().end()) {
r = last();
} else {
r = new Range(i);
ranges.add(r);
}
r.lines.add(content);
}
private Range last() {
return ranges.get(ranges.size() - 1);
}
@Override
public String toString() {
public final String toString() {
final StringBuilder b = new StringBuilder();
b.append("SparseFileContent[\n");
for (Range r : ranges) {
for (Range r : getRanges()) {
b.append(" ");
b.append(r.toString());
b.append('\n');
@ -176,33 +191,32 @@ public class SparseFileContent {
return b.toString();
}
static class Range {
protected int base;
protected List<String> lines;
private Range(int b) {
base = b;
lines = new ArrayList<>();
@AutoValue
abstract static class Range {
static Range create(int base, ImmutableList<String> lines) {
return new AutoValue_SparseFileContent_Range(base, lines);
}
protected Range() {}
abstract int getBase();
abstract ImmutableList<String> getLines();
private String get(int i) {
return lines.get(i - base);
return getLines().get(i - getBase());
}
private int end() {
return base + lines.size();
return getBase() + getLines().size();
}
private boolean contains(int i) {
return base <= i && i < end();
return getBase() <= i && i < end();
}
@Override
public String toString() {
public final String toString() {
// Usage of [ and ) is intentional to denote inclusive/exclusive range
return "Range[" + base + "," + end() + ")";
return "Range[" + getBase() + "," + end() + ")";
}
}
}

View File

@ -0,0 +1,90 @@
// 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.prettify.common;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.prettify.common.SparseFileContent.Range;
/**
* A builder for creating immutable {@link SparseFileContent}. Lines can be only be added in
* sequential (increased) order
*/
public class SparseFileContentBuilder {
private final ImmutableList.Builder<Range> ranges;
private final int size;
private int lastRangeBase;
private int lastRangeEnd;
private ImmutableList.Builder<String> lastRangeLines;
public SparseFileContentBuilder(int size) {
ranges = new ImmutableList.Builder<>();
startNextRange(0);
this.size = size;
}
public void addLine(int lineNumber, String content) {
if (lineNumber < 0) {
throw new IllegalArgumentException("Line number must be non-negative");
}
// if (lineNumber >= size) {
// The following 4 tests are failed if you uncomment this condition:
//
//
// diffOfFileWithMultilineRebaseHunkRemovingNewlineAtEndOfFileAndWithCommentReturnsFileContents
//
// diffOfFileWithMultilineRebaseHunkAddingNewlineAtEndOfFileAndWithCommentReturnsFileContents
//
//
// diffOfFileWithMultilineRebaseHunkRemovingNewlineAtEndOfFileAndWithCommentReturnsFileContents
//
// diffOfFileWithMultilineRebaseHunkAddingNewlineAtEndOfFileAndWithCommentReturnsFileContents
// Tests are failed because there are some bug with diff calculation.
// The condition must be uncommented after all these bugs are fixed.
// Also don't forget to remove ignore from for SparseFileContentBuilder
// throw new IllegalArgumentException(String.format("The zero-based line number %d is after
// the end of file. The file size is %d line(s).", lineNumber, size));
// }
if (lineNumber < lastRangeEnd) {
throw new IllegalArgumentException(
String.format(
"Invalid line number %d. You are trying to add a line before an already added line"
+ " %d",
lineNumber, lastRangeEnd));
}
if (lineNumber > lastRangeEnd) {
finishLastRange();
startNextRange(lineNumber);
}
lastRangeLines.add(content);
lastRangeEnd++;
}
private void startNextRange(int base) {
lastRangeLines = new ImmutableList.Builder<>();
lastRangeBase = lastRangeEnd = base;
}
private void finishLastRange() {
if (lastRangeEnd > lastRangeBase) {
ranges.add(Range.create(lastRangeBase, lastRangeLines.build()));
lastRangeLines = null;
}
}
public SparseFileContent build() {
finishLastRange();
return SparseFileContent.create(ranges.build(), size);
}
}

View File

@ -0,0 +1,14 @@
load("@rules_java//java:defs.bzl", "java_library")
package(default_testonly = True)
java_library(
name = "testing",
srcs = glob(["*.java"]),
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/prettify:server",
"//lib:guava",
"//lib/truth",
],
)

View File

@ -0,0 +1,65 @@
// 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.prettify.common.testing;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.IntegerSubject;
import com.google.common.truth.MapSubject;
import com.google.common.truth.Subject;
import com.google.gerrit.prettify.common.SparseFileContent;
import java.util.HashMap;
import java.util.Map;
public class SparseFileContentSubject extends Subject {
public static SparseFileContentSubject assertThat(SparseFileContent sparseFileContent) {
return assertAbout(sparseFileContent()).that(sparseFileContent);
}
private final SparseFileContent sparseFileContent;
private SparseFileContentSubject(FailureMetadata metadata, SparseFileContent actual) {
super(metadata, actual);
this.sparseFileContent = actual;
}
private static Subject.Factory<SparseFileContentSubject, SparseFileContent> sparseFileContent() {
return SparseFileContentSubject::new;
}
public IntegerSubject getSize() {
isNotNull();
return check("size()").that(sparseFileContent.getSize());
}
public IntegerSubject getRangesCount() {
isNotNull();
return check("rangesCount()").that(sparseFileContent.getRangesCount());
}
public MapSubject lines() {
isNotNull();
Map<Integer, String> lines = new HashMap<>();
SparseFileContent.Accessor accessor = sparseFileContent.createAccessor();
int size = accessor.getSize();
int idx = accessor.first();
while (idx < size) {
lines.put(idx, accessor.get(idx));
idx = accessor.next(idx);
}
return check("lines()").that(lines);
}
}

View File

@ -0,0 +1,299 @@
// 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.diff;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.common.data.PatchScript.PatchScriptFileInfo;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus;
import com.google.gerrit.extensions.common.DiffWebLinkInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
import com.google.gerrit.jgit.diff.ReplaceEdit;
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.server.change.FileContentUtil;
import com.google.gerrit.server.project.ProjectState;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.diff.Edit;
/** Creates and fills a new {@link DiffInfo} object based on diff between files. */
public class DiffInfoCreator {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
Maps.immutableEnumMap(
new ImmutableMap.Builder<Patch.ChangeType, ChangeType>()
.put(Patch.ChangeType.ADDED, ChangeType.ADDED)
.put(Patch.ChangeType.MODIFIED, ChangeType.MODIFIED)
.put(Patch.ChangeType.DELETED, ChangeType.DELETED)
.put(Patch.ChangeType.RENAMED, ChangeType.RENAMED)
.put(Patch.ChangeType.COPIED, ChangeType.COPIED)
.put(Patch.ChangeType.REWRITE, ChangeType.REWRITE)
.build());
private final DiffWebLinksProvider webLinksProvider;
private final boolean intraline;
private final ProjectState state;
public DiffInfoCreator(
ProjectState state, DiffWebLinksProvider webLinksProvider, boolean intraline) {
this.webLinksProvider = webLinksProvider;
this.state = state;
this.intraline = intraline;
}
/* Returns the {@link DiffInfo} to display for end-users */
public DiffInfo create(PatchScript ps, DiffSide sideA, DiffSide sideB) {
DiffInfo result = new DiffInfo();
ImmutableList<DiffWebLinkInfo> links = webLinksProvider.getDiffLinks();
result.webLinks = links.isEmpty() ? null : links;
if (ps.isBinary()) {
result.binary = true;
}
result.metaA = createFileMeta(sideA).orElse(null);
result.metaB = createFileMeta(sideB).orElse(null);
if (intraline) {
if (ps.hasIntralineTimeout()) {
result.intralineStatus = IntraLineStatus.TIMEOUT;
} else if (ps.hasIntralineFailure()) {
result.intralineStatus = IntraLineStatus.FAILURE;
} else {
result.intralineStatus = IntraLineStatus.OK;
}
logger.atFine().log("intralineStatus = %s", result.intralineStatus);
}
result.changeType = CHANGE_TYPE.get(ps.getChangeType());
logger.atFine().log("changeType = %s", result.changeType);
if (result.changeType == null) {
throw new IllegalStateException("unknown change type: " + ps.getChangeType());
}
if (ps.getPatchHeader().size() > 0) {
result.diffHeader = ps.getPatchHeader();
}
result.content = calculateDiffContentEntries(ps);
return result;
}
private static List<ContentEntry> calculateDiffContentEntries(PatchScript ps) {
ContentCollector contentCollector = new ContentCollector(ps);
Set<Edit> editsDueToRebase = ps.getEditsDueToRebase();
for (Edit edit : ps.getEdits()) {
logger.atFine().log("next edit = %s", edit);
if (edit.getType() == Edit.Type.EMPTY) {
logger.atFine().log("skip empty edit");
continue;
}
contentCollector.addCommon(edit.getBeginA());
checkState(
contentCollector.nextA == edit.getBeginA(),
"nextA = %s; want %s",
contentCollector.nextA,
edit.getBeginA());
checkState(
contentCollector.nextB == edit.getBeginB(),
"nextB = %s; want %s",
contentCollector.nextB,
edit.getBeginB());
switch (edit.getType()) {
case DELETE:
case INSERT:
case REPLACE:
List<Edit> internalEdit =
edit instanceof ReplaceEdit ? ((ReplaceEdit) edit).getInternalEdits() : null;
boolean dueToRebase = editsDueToRebase.contains(edit);
contentCollector.addDiff(edit.getEndA(), edit.getEndB(), internalEdit, dueToRebase);
break;
case EMPTY:
default:
throw new IllegalStateException();
}
}
contentCollector.addCommon(ps.getA().getSize());
return contentCollector.lines;
}
private Optional<FileMeta> createFileMeta(DiffSide side) {
PatchScriptFileInfo fileInfo = side.fileInfo();
if (fileInfo.displayMethod == DisplayMethod.NONE) {
return Optional.empty();
}
FileMeta result = new FileMeta();
result.name = side.fileName();
result.contentType =
FileContentUtil.resolveContentType(
state, side.fileName(), fileInfo.mode, fileInfo.mimeType);
result.lines = fileInfo.content.getSize();
ImmutableList<WebLinkInfo> links = webLinksProvider.getFileWebLinks(side.type());
result.webLinks = links.isEmpty() ? null : links;
result.commitId = fileInfo.commitId;
return Optional.of(result);
}
private static class ContentCollector {
private final List<ContentEntry> lines;
private final SparseFileContent.Accessor fileA;
private final SparseFileContent.Accessor fileB;
private final boolean ignoreWS;
private int nextA;
private int nextB;
ContentCollector(PatchScript ps) {
lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2);
fileA = ps.getA().createAccessor();
fileB = ps.getB().createAccessor();
ignoreWS = ps.isIgnoreWhitespace();
}
void addCommon(int end) {
logger.atFine().log("addCommon: end = %d", end);
end = Math.min(end, fileA.getSize());
logger.atFine().log("end = %d", end);
if (nextA >= end) {
logger.atFine().log("nextA >= end: nextA = %d, end = %d", nextA, end);
return;
}
while (nextA < end) {
logger.atFine().log("nextA < end: nextA = %d, end = %d", nextA, end);
if (!fileA.contains(nextA)) {
logger.atFine().log("fileA does not contain nextA: nextA = %d", nextA);
int endRegion = Math.min(end, nextA == 0 ? fileA.first() : fileA.next(nextA - 1));
int len = endRegion - nextA;
entry().skip = len;
nextA = endRegion;
nextB += len;
logger.atFine().log("setting: nextA = %d, nextB = %d", nextA, nextB);
continue;
}
ContentEntry e = null;
for (int i = nextA; i == nextA && i < end; i = fileA.next(i), nextA++, nextB++) {
if (ignoreWS && fileB.contains(nextB)) {
if (e == null || e.common == null) {
logger.atFine().log("create new common entry: nextA = %d, nextB = %d", nextA, nextB);
e = entry();
e.a = Lists.newArrayListWithCapacity(end - nextA);
e.b = Lists.newArrayListWithCapacity(end - nextA);
e.common = true;
}
e.a.add(fileA.get(nextA));
e.b.add(fileB.get(nextB));
} else {
if (e == null || e.common != null) {
logger.atFine().log(
"create new non-common entry: nextA = %d, nextB = %d", nextA, nextB);
e = entry();
e.ab = Lists.newArrayListWithCapacity(end - nextA);
}
e.ab.add(fileA.get(nextA));
}
}
}
}
void addDiff(int endA, int endB, List<Edit> internalEdit, boolean dueToRebase) {
logger.atFine().log(
"addDiff: endA = %d, endB = %d, numberOfInternalEdits = %d, dueToRebase = %s",
endA, endB, internalEdit != null ? internalEdit.size() : 0, dueToRebase);
int lenA = endA - nextA;
int lenB = endB - nextB;
logger.atFine().log("lenA = %d, lenB = %d", lenA, lenB);
checkState(lenA > 0 || lenB > 0);
logger.atFine().log("create non-common entry");
ContentEntry e = entry();
if (lenA > 0) {
logger.atFine().log("lenA > 0: lenA = %d", lenA);
e.a = Lists.newArrayListWithCapacity(lenA);
for (; nextA < endA; nextA++) {
e.a.add(fileA.get(nextA));
}
}
if (lenB > 0) {
logger.atFine().log("lenB > 0: lenB = %d", lenB);
e.b = Lists.newArrayListWithCapacity(lenB);
for (; nextB < endB; nextB++) {
e.b.add(fileB.get(nextB));
}
}
if (internalEdit != null && !internalEdit.isEmpty()) {
logger.atFine().log("processing internal edits");
e.editA = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
e.editB = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
int lastA = 0;
int lastB = 0;
for (Edit edit : internalEdit) {
logger.atFine().log("internal edit = %s", edit);
if (edit.getBeginA() != edit.getEndA()) {
logger.atFine().log(
"edit.getBeginA() != edit.getEndA(): edit.getBeginA() = %d, edit.getEndA() = %d",
edit.getBeginA(), edit.getEndA());
e.editA.add(
ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
lastA = edit.getEndA();
logger.atFine().log("lastA = %d", lastA);
}
if (edit.getBeginB() != edit.getEndB()) {
logger.atFine().log(
"edit.getBeginB() != edit.getEndB(): edit.getBeginB() = %d, edit.getEndB() = %d",
edit.getBeginB(), edit.getEndB());
e.editB.add(
ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
lastB = edit.getEndB();
logger.atFine().log("lastB = %d", lastB);
}
}
}
e.dueToRebase = dueToRebase ? true : null;
}
private ContentEntry entry() {
ContentEntry e = new ContentEntry();
lines.add(e);
return e;
}
}
}

View File

@ -0,0 +1,37 @@
// 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.diff;
import com.google.auto.value.AutoValue;
import com.google.gerrit.common.data.PatchScript.PatchScriptFileInfo;
/** Contains settings for one of two sides in diff view. Each diff view has exactly 2 sides. */
@AutoValue
public abstract class DiffSide {
public enum Type {
SIDE_A,
SIDE_B
}
public static DiffSide create(PatchScriptFileInfo fileInfo, String fileName, Type type) {
return new AutoValue_DiffSide(fileInfo, fileName, type);
}
public abstract PatchScriptFileInfo fileInfo();
public abstract String fileName();
public abstract Type type();
}

View File

@ -0,0 +1,29 @@
// 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.diff;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.common.DiffWebLinkInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
/** Provider for different types of links which can be displayed in a diff view. */
public interface DiffWebLinksProvider {
/** Returns links associated with the diff view */
ImmutableList<DiffWebLinkInfo> getDiffLinks();
/** Returns links associated with the diff side */
ImmutableList<WebLinkInfo> getFileWebLinks(DiffSide.Type fileInfoType);
}

View File

@ -28,7 +28,7 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.prettify.common.EditList;
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.prettify.common.SparseFileContentBuilder;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.inject.Inject;
import eu.medsea.mimeutil.MimeType;
@ -198,8 +198,8 @@ class PatchScriptBuilder {
b.fileMode,
content.getHeaderLines(),
diffPrefs,
a.dst,
b.dst,
a.dst.build(),
b.dst.build(),
edits,
editsDueToRebase,
a.displayMethod,
@ -478,9 +478,9 @@ class PatchScriptBuilder {
final MimeType mimeType;
final DisplayMethod displayMethod;
final PatchScript.FileMode fileMode;
final SparseFileContent dst;
final SparseFileContentBuilder dst;
public Side(
private Side(
String path,
ObjectId id,
FileMode mode,
@ -497,8 +497,7 @@ class PatchScriptBuilder {
this.mimeType = mimeType;
this.displayMethod = displayMethod;
this.fileMode = fileMode;
dst = new SparseFileContent();
dst.setSize(size());
dst = new SparseFileContentBuilder(size());
}
int size() {

View File

@ -14,27 +14,18 @@
package com.google.gerrit.server.restapi.change;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.util.cli.Localizable.localizable;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus;
import com.google.gerrit.extensions.common.DiffWebLinkInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
import com.google.gerrit.extensions.restapi.AuthException;
@ -44,12 +35,12 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.jgit.diff.ReplaceEdit;
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.change.FileContentUtil;
import com.google.gerrit.server.change.FileResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.diff.DiffInfoCreator;
import com.google.gerrit.server.diff.DiffSide;
import com.google.gerrit.server.diff.DiffWebLinksProvider;
import com.google.gerrit.server.git.LargeObjectException;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.PatchScriptFactory;
@ -60,10 +51,7 @@ import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.diff.Edit;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.NamedOptionDef;
@ -76,17 +64,6 @@ import org.kohsuke.args4j.spi.Setter;
public class GetDiff implements RestReadView<FileResource> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
Maps.immutableEnumMap(
new ImmutableMap.Builder<Patch.ChangeType, ChangeType>()
.put(Patch.ChangeType.ADDED, ChangeType.ADDED)
.put(Patch.ChangeType.MODIFIED, ChangeType.MODIFIED)
.put(Patch.ChangeType.DELETED, ChangeType.DELETED)
.put(Patch.ChangeType.RENAMED, ChangeType.RENAMED)
.put(Patch.ChangeType.COPIED, ChangeType.COPIED)
.put(Patch.ChangeType.REWRITE, ChangeType.REWRITE)
.build());
private final ProjectCache projectCache;
private final PatchScriptFactory.Factory patchScriptFactoryFactory;
private final Revisions revisions;
@ -164,111 +141,18 @@ public class GetDiff implements RestReadView<FileResource> {
psf.setLoadHistory(false);
psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT);
PatchScript ps = psf.call();
ContentCollector contentCollector = new ContentCollector(ps);
Set<Edit> editsDueToRebase = ps.getEditsDueToRebase();
for (Edit edit : ps.getEdits()) {
logger.atFine().log("next edit = %s", edit);
if (edit.getType() == Edit.Type.EMPTY) {
logger.atFine().log("skip empty edit");
continue;
}
contentCollector.addCommon(edit.getBeginA());
checkState(
contentCollector.nextA == edit.getBeginA(),
"nextA = %s; want %s",
contentCollector.nextA,
edit.getBeginA());
checkState(
contentCollector.nextB == edit.getBeginB(),
"nextB = %s; want %s",
contentCollector.nextB,
edit.getBeginB());
switch (edit.getType()) {
case DELETE:
case INSERT:
case REPLACE:
List<Edit> internalEdit =
edit instanceof ReplaceEdit ? ((ReplaceEdit) edit).getInternalEdits() : null;
boolean dueToRebase = editsDueToRebase.contains(edit);
contentCollector.addDiff(edit.getEndA(), edit.getEndB(), internalEdit, dueToRebase);
break;
case EMPTY:
default:
throw new IllegalStateException();
}
}
contentCollector.addCommon(ps.getA().size());
ProjectState state = projectCache.get(resource.getRevision().getChange().getProject());
DiffInfo result = new DiffInfo();
String revA = basePatchSet != null ? basePatchSet.refName() : ps.getFileInfoA().commitId;
String revB =
resource.getRevision().getEdit().isPresent()
? resource.getRevision().getEdit().get().getRefName()
: resource.getRevision().getPatchSet().refName();
logger.atFine().log("revA = %s, revB = %s", revA, revB);
ImmutableList<DiffWebLinkInfo> links =
webLinks.getDiffLinks(
state.getName(),
resource.getPatchKey().patchSetId().changeId().get(),
basePatchSet != null ? basePatchSet.id().get() : null,
revA,
Project.NameKey projectName = resource.getRevision().getChange().getProject();
ProjectState state = projectCache.get(projectName);
DiffSide sideA =
DiffSide.create(
ps.getFileInfoA(),
MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()),
resource.getPatchKey().patchSetId().get(),
revB,
ps.getNewName());
result.webLinks = links.isEmpty() ? null : links;
if (ps.isBinary()) {
result.binary = true;
}
if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
result.metaA = new FileMeta();
result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName());
result.metaA.contentType =
FileContentUtil.resolveContentType(
state, result.metaA.name, ps.getFileModeA(), ps.getMimeTypeA());
result.metaA.lines = ps.getA().size();
result.metaA.webLinks = getFileWebLinks(state.getProject(), revA, result.metaA.name);
result.metaA.commitId = ps.getFileInfoA().commitId;
}
if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
result.metaB = new FileMeta();
result.metaB.name = ps.getNewName();
result.metaB.contentType =
FileContentUtil.resolveContentType(
state, result.metaB.name, ps.getFileModeB(), ps.getMimeTypeB());
result.metaB.lines = ps.getB().size();
result.metaB.webLinks = getFileWebLinks(state.getProject(), revB, result.metaB.name);
result.metaB.commitId = ps.getFileInfoB().commitId;
}
if (intraline) {
if (ps.hasIntralineTimeout()) {
result.intralineStatus = IntraLineStatus.TIMEOUT;
} else if (ps.hasIntralineFailure()) {
result.intralineStatus = IntraLineStatus.FAILURE;
} else {
result.intralineStatus = IntraLineStatus.OK;
}
logger.atFine().log("intralineStatus = %s", result.intralineStatus);
}
result.changeType = CHANGE_TYPE.get(ps.getChangeType());
logger.atFine().log("changeType = %s", result.changeType);
if (result.changeType == null) {
throw new IllegalStateException("unknown change type: " + ps.getChangeType());
}
if (ps.getPatchHeader().size() > 0) {
result.diffHeader = ps.getPatchHeader();
}
result.content = contentCollector.lines;
DiffSide.Type.SIDE_A);
DiffSide sideB = DiffSide.create(ps.getFileInfoB(), ps.getNewName(), DiffSide.Type.SIDE_B);
DiffWebLinksProvider webLinksProvider =
new DiffWebLinksProviderImpl(sideA, sideB, projectName, basePatchSet, webLinks, resource);
DiffInfoCreator diffInfoCreator = new DiffInfoCreator(state, webLinksProvider, intraline);
DiffInfo result = diffInfoCreator.create(ps, sideA, sideB);
Response<DiffInfo> r = Response.ok(result);
if (resource.isCacheable()) {
@ -282,9 +166,69 @@ public class GetDiff implements RestReadView<FileResource> {
}
}
private List<WebLinkInfo> getFileWebLinks(Project project, String rev, String file) {
ImmutableList<WebLinkInfo> links = webLinks.getFileLinks(project.getName(), rev, file);
return links.isEmpty() ? null : links;
private static class DiffWebLinksProviderImpl implements DiffWebLinksProvider {
private final WebLinks webLinks;
private final Project.NameKey projectName;
private final DiffSide sideA;
private final DiffSide sideB;
private final String revA;
private final String revB;
private final FileResource resource;
@Nullable private final PatchSet basePatchSet;
DiffWebLinksProviderImpl(
DiffSide sideA,
DiffSide sideB,
Project.NameKey projectName,
@Nullable PatchSet basePatchSet,
WebLinks webLinks,
FileResource resource) {
this.projectName = projectName;
this.webLinks = webLinks;
this.basePatchSet = basePatchSet;
this.resource = resource;
this.sideA = sideA;
this.sideB = sideB;
revA = basePatchSet != null ? basePatchSet.refName() : sideA.fileInfo().commitId;
RevisionResource revision = resource.getRevision();
revB =
revision
.getEdit()
.map(edit -> edit.getRefName())
.orElseGet(() -> revision.getPatchSet().refName());
logger.atFine().log("revA = %s, revB = %s", revA, revB);
}
@Override
public ImmutableList<DiffWebLinkInfo> getDiffLinks() {
return webLinks.getDiffLinks(
projectName.get(),
resource.getPatchKey().patchSetId().changeId().get(),
basePatchSet != null ? basePatchSet.id().get() : null,
revA,
sideA.fileName(),
resource.getPatchKey().patchSetId().get(),
revB,
sideB.fileName());
}
@Override
public ImmutableList<WebLinkInfo> getFileWebLinks(DiffSide.Type type) {
String rev;
DiffSide side;
if (type == DiffSide.Type.SIDE_A) {
rev = revA;
side = sideA;
} else {
rev = revB;
side = sideB;
}
return webLinks.getFileLinks(projectName.get(), rev, side.fileName());
}
}
public GetDiff setBase(String base) {
@ -312,141 +256,6 @@ public class GetDiff implements RestReadView<FileResource> {
return this;
}
private static class ContentCollector {
final List<ContentEntry> lines;
final SparseFileContent fileA;
final SparseFileContent fileB;
final boolean ignoreWS;
int nextA;
int nextB;
ContentCollector(PatchScript ps) {
lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2);
fileA = ps.getA();
fileB = ps.getB();
ignoreWS = ps.isIgnoreWhitespace();
}
void addCommon(int end) {
logger.atFine().log("addCommon: end = %d", end);
end = Math.min(end, fileA.size());
logger.atFine().log("end = %d", end);
if (nextA >= end) {
logger.atFine().log("nextA >= end: nextA = %d, end = %d", nextA, end);
return;
}
while (nextA < end) {
logger.atFine().log("nextA < end: nextA = %d, end = %d", nextA, end);
if (!fileA.contains(nextA)) {
logger.atFine().log("fileA does not contain nextA: nextA = %d", nextA);
int endRegion = Math.min(end, nextA == 0 ? fileA.first() : fileA.next(nextA - 1));
int len = endRegion - nextA;
entry().skip = len;
nextA = endRegion;
nextB += len;
logger.atFine().log("setting: nextA = %d, nextB = %d", nextA, nextB);
continue;
}
ContentEntry e = null;
for (int i = nextA; i == nextA && i < end; i = fileA.next(i), nextA++, nextB++) {
if (ignoreWS && fileB.contains(nextB)) {
if (e == null || e.common == null) {
logger.atFine().log("create new common entry: nextA = %d, nextB = %d", nextA, nextB);
e = entry();
e.a = Lists.newArrayListWithCapacity(end - nextA);
e.b = Lists.newArrayListWithCapacity(end - nextA);
e.common = true;
}
e.a.add(fileA.get(nextA));
e.b.add(fileB.get(nextB));
} else {
if (e == null || e.common != null) {
logger.atFine().log(
"create new non-common entry: nextA = %d, nextB = %d", nextA, nextB);
e = entry();
e.ab = Lists.newArrayListWithCapacity(end - nextA);
}
e.ab.add(fileA.get(nextA));
}
}
logger.atFine().log("nextA = %d, nextB = %d", nextA, nextB);
}
}
void addDiff(int endA, int endB, List<Edit> internalEdit, boolean dueToRebase) {
logger.atFine().log(
"addDiff: endA = %d, endB = %d, numberOfInternalEdits = %d, dueToRebase = %s",
endA, endB, internalEdit != null ? internalEdit.size() : 0, dueToRebase);
int lenA = endA - nextA;
int lenB = endB - nextB;
logger.atFine().log("lenA = %d, lenB = %d", lenA, lenB);
checkState(lenA > 0 || lenB > 0);
logger.atFine().log("create non-common entry");
ContentEntry e = entry();
if (lenA > 0) {
logger.atFine().log("lenA > 0: lenA = %d", lenA);
e.a = Lists.newArrayListWithCapacity(lenA);
for (; nextA < endA; nextA++) {
e.a.add(fileA.get(nextA));
}
}
if (lenB > 0) {
logger.atFine().log("lenB > 0: lenB = %d", lenB);
e.b = Lists.newArrayListWithCapacity(lenB);
for (; nextB < endB; nextB++) {
e.b.add(fileB.get(nextB));
}
}
if (internalEdit != null && !internalEdit.isEmpty()) {
logger.atFine().log("processing internal edits");
e.editA = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
e.editB = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
int lastA = 0;
int lastB = 0;
for (Edit edit : internalEdit) {
logger.atFine().log("internal edit = %s", edit);
if (edit.getBeginA() != edit.getEndA()) {
logger.atFine().log(
"edit.getBeginA() != edit.getEndA(): edit.getBeginA() = %d, edit.getEndA() = %d",
edit.getBeginA(), edit.getEndA());
e.editA.add(
ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
lastA = edit.getEndA();
logger.atFine().log("lastA = %d", lastA);
}
if (edit.getBeginB() != edit.getEndB()) {
logger.atFine().log(
"edit.getBeginB() != edit.getEndB(): edit.getBeginB() = %d, edit.getEndB() = %d",
edit.getBeginB(), edit.getEndB());
e.editB.add(
ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
lastB = edit.getEndB();
logger.atFine().log("lastB = %d", lastB);
}
}
}
e.dueToRebase = dueToRebase ? true : null;
}
private ContentEntry entry() {
ContentEntry e = new ContentEntry();
lines.add(e);
return e;
}
}
@Deprecated
enum IgnoreWhitespace {
NONE(DiffPreferencesInfo.Whitespace.IGNORE_NONE),
@ -462,6 +271,7 @@ public class GetDiff implements RestReadView<FileResource> {
}
public static class ContextOptionHandler extends OptionHandler<Short> {
public ContextOptionHandler(CmdLineParser parser, OptionDef option, Setter<Short> setter) {
super(parser, option, setter);
}

View File

@ -0,0 +1,13 @@
load("//tools/bzl:junit.bzl", "junit_tests")
junit_tests(
name = "prettify_tests",
srcs = glob(["**/*.java"]),
deps = [
"//java/com/google/gerrit/prettify:server",
"//java/com/google/gerrit/prettify/common/testing",
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:guava",
"//lib/truth",
],
)

View File

@ -0,0 +1,170 @@
// 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.prettify.common;
import static com.google.gerrit.prettify.common.testing.SparseFileContentSubject.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableMap;
import org.junit.Ignore;
import org.junit.Test;
public class SparseFileContentBuilderTest {
@Test
public void addLineWithNegativeNumber() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(10);
assertThrows(IllegalArgumentException.class, () -> builder.addLine(-1, "First line"));
assertThrows(IllegalArgumentException.class, () -> builder.addLine(-5, "First line"));
}
@Test
@Ignore
public void addLineNumberZeroFileSize() {
// Temporary ignore - see comments in SparseFileContentBuilder.build() method
SparseFileContentBuilder builder = new SparseFileContentBuilder(0);
assertThrows(IllegalArgumentException.class, () -> builder.addLine(0, "First line"));
}
@Test
@Ignore
public void addLineNumberNonZeroFileSize() {
// Temporary ignore - see comments in SparseFileContentBuilder.build() method
SparseFileContentBuilder builder = new SparseFileContentBuilder(5);
assertThrows(IllegalArgumentException.class, () -> builder.addLine(5, "First line"));
assertThrows(IllegalArgumentException.class, () -> builder.addLine(6, "First line"));
assertThrows(IllegalArgumentException.class, () -> builder.addLine(7, "First line"));
}
@Test
public void addLineIncorrectOrder() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(5);
builder.addLine(0, "First line");
builder.addLine(1, "Second line");
builder.addLine(3, "Third line");
builder.addLine(4, "Fourth line");
assertThrows(IllegalArgumentException.class, () -> builder.addLine(4, "Other Line"));
assertThrows(IllegalArgumentException.class, () -> builder.addLine(2, "Other Line"));
}
@Test
public void emptyContentZeroSize() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(0);
SparseFileContent content = builder.build();
assertThat(content).getSize().isEqualTo(0);
assertThat(content).getRangesCount().isEqualTo(0);
assertThat(content).lines().isEmpty();
}
@Test
public void emptyContentNonZeroSize() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(4);
SparseFileContent content = builder.build();
assertThat(content).getSize().isEqualTo(4);
assertThat(content).getRangesCount().isEqualTo(0);
assertThat(content).lines().isEmpty();
}
@Test
public void oneLineContentLineNumberZero() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(1);
builder.addLine(0, "First line");
SparseFileContent content = builder.build();
assertThat(content).getSize().isEqualTo(1);
assertThat(content).getRangesCount().isEqualTo(1);
assertThat(content).lines().containsExactlyEntriesIn(ImmutableMap.of(0, "First line"));
}
@Test
public void oneLineContentLineNumberNotZero() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(6);
builder.addLine(5, "First line");
SparseFileContent content = builder.build();
assertThat(content).getSize().isEqualTo(6);
assertThat(content).getRangesCount().isEqualTo(1);
assertThat(content).lines().containsExactlyEntriesIn(ImmutableMap.of(5, "First line"));
}
@Test
public void multiLineContinuousContentStartingFromZero() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(5);
builder.addLine(0, "First line");
builder.addLine(1, "Second line");
builder.addLine(2, "Third line");
SparseFileContent content = builder.build();
assertThat(content).getSize().isEqualTo(5);
assertThat(content).getRangesCount().isEqualTo(1);
assertThat(content)
.lines()
.containsExactlyEntriesIn(
ImmutableMap.of(
0, "First line",
1, "Second line",
2, "Third line"));
}
@Test
public void multiLineContentStartingFromNonZeroLine() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(8);
builder.addLine(5, "First line");
builder.addLine(6, "Second line");
builder.addLine(7, "Third line");
SparseFileContent content = builder.build();
assertThat(content).getSize().isEqualTo(8);
assertThat(content).getRangesCount().isEqualTo(1);
assertThat(content)
.lines()
.containsExactlyEntriesIn(
ImmutableMap.of(
5, "First line",
6, "Second line",
7, "Third line"));
}
@Test
public void multiLineContentWithGaps() {
SparseFileContentBuilder builder = new SparseFileContentBuilder(10000);
builder.addLine(0, "First line");
builder.addLine(1, "Second line");
builder.addLine(3, "Third line");
builder.addLine(4, "Fourth line");
builder.addLine(5, "Fifth line");
builder.addLine(6, "Sixth line");
builder.addLine(10, "Seventh line");
SparseFileContent content = builder.build();
assertThat(content).getSize().isEqualTo(10000);
assertThat(content).getRangesCount().isEqualTo(3);
assertThat(content)
.lines()
.containsExactlyEntriesIn(
ImmutableMap.builder()
.put(0, "First line")
.put(1, "Second line")
.put(3, "Third line")
.put(4, "Fourth line")
.put(5, "Fifth line")
.put(6, "Sixth line")
.put(10, "Seventh line")
.build());
}
}