Fix an exception in the diff code for a corner case

This regression was introduced with I1e15767fc1. It resulted in an
internal server error when the diff was requested for the old file
name of a renamed file. It's actually not reasonable to request that
diff but due to an issue in PolyGerrit, users now easily run into that
case.

The fix reverts the behavior for this case back to the previous one:
The backend returns an empty diff result with only skipped lines,
oblivious of the passed context and the presence of comments.

Whether that behavior is *correct* is difficult to decide as it's hard
to define what should be returned in that case. Actually, requesting
the diff for the old file name (given that Gerrit identified a rename)
is not a use case which makes sense.

This change additionally adds two further tests which weren't covered
previously.

Users also reported a similar issue with PDF files. When trying to
reproduce it locally, it didn't occur. However, the same PDF files were
locally marked as binary whereas on a Gerrit server, on which the issue
occurred, their content was shown as text on PolyGerrit. Given this
and the similar exception, it's very likely that the fix of this change
will also help for the PDF issue.

Bug: Issue 9498
Bug: Issue 9557
Change-Id: I101d262c1566aa89a91a82394f51dab7c6e6311e
This commit is contained in:
Alice Kober-Sotzek
2018-08-16 17:38:35 +02:00
parent 944cd404ac
commit 3be9dc9c6f
3 changed files with 137 additions and 0 deletions

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.extensions.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.IterableSubject;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
@@ -81,4 +82,10 @@ public class ContentEntrySubject extends Subject<ContentEntrySubject, ContentEnt
ContentEntry contentEntry = actual();
return Truth.assertThat(contentEntry.editB).named("intraline edits of 'b'");
}
public IntegerSubject numberOfSkippedLines() {
isNotNull();
ContentEntry contentEntry = actual();
return Truth.assertThat(contentEntry.skip).named("number of skipped lines");
}
}

View File

@@ -285,6 +285,13 @@ class PatchScriptBuilder {
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;
}
Optional<Edit> lastEdit = getLast(edits);
if (isNewlineAtEndDeleted()) {
Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);

View File

@@ -32,6 +32,9 @@ import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.extensions.api.changes.FileApi;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
@@ -2343,6 +2346,126 @@ public class RevisionDiffIT extends AbstractDaemonTest {
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
}
@Test
public void diffOfUnmodifiedFileWithWholeFileContextReturnsFileContents() throws Exception {
addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
addModifiedPatchSet(
changeId, FILE_NAME2, content -> content.replace("2nd line\n", "Second line\n"));
DiffInfo diffInfo =
getDiffRequest(changeId, CURRENT, FILE_NAME)
.withBase(previousPatchSetId)
.withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
.get();
// We don't list the full file contents here as that is not the focus of this test.
assertThat(diffInfo)
.content()
.element(0)
.commonLines()
.containsAllOf("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
.inOrder();
}
@Test
public void diffOfUnmodifiedFileWithCommentAndWholeFileContextReturnsFileContents()
throws Exception {
addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
ReviewInput reviewInput = new ReviewInput();
reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment));
gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput);
addModifiedPatchSet(
changeId, FILE_NAME2, content -> content.replace("2nd line\n", "Second line\n"));
DiffInfo diffInfo =
getDiffRequest(changeId, CURRENT, FILE_NAME)
.withBase(previousPatchSetId)
.withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
.get();
// We don't list the full file contents here as that is not the focus of this test.
assertThat(diffInfo)
.content()
.element(0)
.commonLines()
.containsAllOf("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
.inOrder();
}
@Test
public void diffOfNonExistentFileIsAnEmptyDiffResult() throws Exception {
addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
DiffInfo diffInfo =
getDiffRequest(changeId, CURRENT, "a_non-existent_file.txt")
.withBase(initialPatchSetId)
.withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
.get();
assertThat(diffInfo).content().isEmpty();
}
@Test
public void requestingDiffForOldFileNameOfRenamedFileYieldsReasonableResult() throws Exception {
addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
String newFilePath = "a_new_file.txt";
gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath);
gApi.changes().id(changeId).edit().publish();
DiffInfo diffInfo =
getDiffRequest(changeId, CURRENT, FILE_NAME)
.withBase(previousPatchSetId)
.withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
.get();
// This behavior has been present in Gerrit for quite some time. It differs from the results
// returned for other cases (e.g. requesting the diff with whole file context for an unmodified
// file; requesting the diff with whole file context for a non-existent file). However, it's not
// completely clear what should be returned. The closest would be the result of a file deletion
// but that might also be misleading for users as actually a file rename occurred. In fact,
// requesting the diff result for the old file name of a renamed file is not a reasonable use
// case at all. We at least guarantee that we don't run into an internal error.
assertThat(diffInfo).content().element(0).commonLines().isNull();
assertThat(diffInfo).content().element(0).numberOfSkippedLines().isGreaterThan(0);
}
@Test
public void requestingDiffForOldFileNameOfRenamedFileWithCommentOnOldFileYieldsReasonableResult()
throws Exception {
addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
ReviewInput reviewInput = new ReviewInput();
reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment));
gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput);
String newFilePath = "a_new_file.txt";
gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath);
gApi.changes().id(changeId).edit().publish();
DiffInfo diffInfo =
getDiffRequest(changeId, CURRENT, FILE_NAME)
.withBase(previousPatchSetId)
.withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
.get();
// See comment for requestingDiffForOldFileNameOfRenamedFileYieldsReasonableResult().
// This test should additionally ensure that we also don't run into an internal error when
// a comment is present.
assertThat(diffInfo).content().element(0).commonLines().isNull();
assertThat(diffInfo).content().element(0).numberOfSkippedLines().isGreaterThan(0);
}
private static CommentInput createCommentInput(
int startLine, int startCharacter, int endLine, int endCharacter, String message) {
CommentInput comment = new CommentInput();
comment.range = new Comment.Range();
comment.range.startLine = startLine;
comment.range.startCharacter = startCharacter;
comment.range.endLine = endLine;
comment.range.endCharacter = endCharacter;
comment.message = message;
return comment;
}
private void assertDiffForNewFile(
PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
DiffInfo diff =