Merge changes I0da14436,I1e15767f

* changes:
  Remove workaround regarding newlines on GWT UI
  Fix diff regarding newline at end of file
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index fb0a0df..1c66c44 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6282,10 +6282,14 @@
 Only set if the file was renamed or copied.
 |`lines_inserted`|optional|
 Number of inserted lines. +
-Not set for binary files or if no lines were inserted.
+Not set for binary files or if no lines were inserted. +
+An empty last line is not included in the count and hence this number can
+differ by one from details provided in <<#diff-info,DiffInfo>>.
 |`lines_deleted` |optional|
 Number of deleted lines. +
-Not set for binary files or if no lines were deleted.
+Not set for binary files or if no lines were deleted. +
+An empty last line is not included in the count and hence this number can
+differ by one from details provided in <<#diff-info,DiffInfo>>.
 |`size_delta`    ||
 Number of bytes by which the file size increased/decreased.
 |`size`          ||
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index cf40762..d942c2e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -137,7 +137,10 @@
 
   private static void append(StringBuilder s, JsArrayString lines) {
     for (int i = 0; i < lines.length(); i++) {
-      s.append(lines.get(i)).append('\n');
+      if (s.length() > 0) {
+        s.append('\n');
+      }
+      s.append(lines.get(i));
     }
   }
 
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 53686a7..960b558 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1345,8 +1345,7 @@
 
   protected void assertDiffForNewFile(
       DiffInfo diff, RevCommit commit, String path, String expectedContentSideB) throws Exception {
-    List<String> expectedLines = new ArrayList<>();
-    Collections.addAll(expectedLines, expectedContentSideB.split("\n"));
+    List<String> expectedLines = ImmutableList.copyOf(expectedContentSideB.split("\n", -1));
 
     assertThat(diff.binary).isNull();
     assertThat(diff.changeType).isEqualTo(ChangeType.ADDED);
diff --git a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
index 6918325..057a1a2 100644
--- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -47,4 +47,16 @@
     DiffInfo diffInfo = actual();
     return Truth.assertThat(diffInfo.changeType).named("changeType");
   }
+
+  public FileMetaSubject metaA() {
+    isNotNull();
+    DiffInfo diffInfo = actual();
+    return FileMetaSubject.assertThat(diffInfo.metaA).named("metaA");
+  }
+
+  public FileMetaSubject metaB() {
+    isNotNull();
+    DiffInfo diffInfo = actual();
+    return FileMetaSubject.assertThat(diffInfo.metaB).named("metaB");
+  }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
new file mode 100644
index 0000000..e77eef1
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2018 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.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.Subject;
+import com.google.common.truth.Truth;
+import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
+
+public class FileMetaSubject extends Subject<FileMetaSubject, FileMeta> {
+
+  public static FileMetaSubject assertThat(FileMeta fileMeta) {
+    return assertAbout(FileMetaSubject::new).that(fileMeta);
+  }
+
+  private FileMetaSubject(FailureMetadata failureMetadata, FileMeta fileMeta) {
+    super(failureMetadata, fileMeta);
+  }
+
+  public IntegerSubject totalLineCount() {
+    isNotNull();
+    FileMeta fileMeta = actual();
+    return Truth.assertThat(fileMeta.lines).named("total line count");
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 6f3e055..b4f7251 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -37,6 +37,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Optional;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -172,6 +173,8 @@
       }
     }
 
+    correctForDifferencesInNewlineAtEnd();
+
     if (comments != null) {
       ensureCommentsVisible(comments);
     }
@@ -277,6 +280,43 @@
     }
   }
 
+  private void correctForDifferencesInNewlineAtEnd() {
+    // 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();
+
+    Optional<Edit> lastEdit = getLast(edits);
+    if (isNewlineAtEndDeleted()) {
+      Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
+      if (lastLineEdit.isPresent()) {
+        lastLineEdit.get().extendA();
+      } else {
+        Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize);
+        edits.add(newlineEdit);
+      }
+    } else if (isNewlineAtEndAdded()) {
+      Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize);
+      if (lastLineEdit.isPresent()) {
+        lastLineEdit.get().extendB();
+      } else {
+        Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1);
+        edits.add(newlineEdit);
+      }
+    }
+  }
+
+  private static <T> Optional<T> getLast(List<T> list) {
+    return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1));
+  }
+
+  private boolean isNewlineAtEndDeleted() {
+    return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd();
+  }
+
+  private boolean isNewlineAtEndAdded() {
+    return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd();
+  }
+
   private void ensureCommentsVisible(CommentDetail comments) {
     if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) {
       // No comments, no additional dummy edits are required.
@@ -396,14 +436,14 @@
     for (EditList.Hunk hunk : list.getHunks()) {
       while (hunk.next()) {
         if (hunk.isContextLine()) {
-          final String lineA = a.src.getString(hunk.getCurA());
+          String lineA = a.getSourceLine(hunk.getCurA());
           a.dst.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.
             //
-            final String lineB = b.src.getString(hunk.getCurB());
+            String lineB = b.getSourceLine(hunk.getCurB());
             if (!lineA.equals(lineB)) {
               b.dst.addLine(hunk.getCurB(), lineB);
             }
@@ -437,11 +477,22 @@
     final SparseFileContent dst = new SparseFileContent();
 
     int size() {
-      return src != null ? src.size() : 0;
+      if (src == null) {
+        return 0;
+      }
+      if (src.isMissingNewlineAtEnd()) {
+        return src.size();
+      }
+      return src.size() + 1;
     }
 
-    void addLine(int line) {
-      dst.addLine(line, src.getString(line));
+    void addLine(int lineNumber) {
+      String lineContent = getSourceLine(lineNumber);
+      dst.addLine(lineNumber, lineContent);
+    }
+
+    String getSourceLine(int lineNumber) {
+      return lineNumber >= src.size() ? "" : src.getString(lineNumber);
     }
 
     void resolve(Side other, ObjectId within) throws IOException {
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 51946e9..53cc5ad 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -32,6 +32,7 @@
 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.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.FileInfo;
@@ -112,16 +113,115 @@
   }
 
   @Test
-  public void diffDeletedFile() throws Exception {
+  public void deletedFileIsIncludedInDiff() throws Exception {
     gApi.changes().id(changeId).edit().deleteFile(FILE_NAME);
     gApi.changes().id(changeId).edit().publish();
 
     Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files();
     assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME);
+  }
 
-    DiffInfo diff = getDiffRequest(changeId, CURRENT, FILE_NAME).get();
-    assertThat(diff.metaA.lines).isEqualTo(100);
-    assertThat(diff.metaB).isNull();
+  @Test
+  public void numberOfLinesInDiffOfDeletedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+    assertThat(diffInfo).metaB().isNull();
+  }
+
+  @Test
+  public void numberOfLinesInFileInfoOfDeletedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(filePath)).linesInserted().isNull();
+    assertThat(changedFiles.get(filePath)).linesDeleted().isEqualTo(3);
+  }
+
+  @Test
+  public void numberOfLinesInDiffOfDeletedFileWithNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+    assertThat(diffInfo).metaB().isNull();
+  }
+
+  @Test
+  public void numberOfLinesInFileInfoOfDeletedFileWithNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(filePath)).linesInserted().isNull();
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(filePath)).linesDeleted().isEqualTo(3);
+  }
+
+  @Test
+  public void deletedFileWithoutNewlineAtEndResultsInOneDiffEntry() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .linesOfA()
+        .containsExactly("Line 1", "Line 2", "Line 3");
+  }
+
+  @Test
+  public void deletedFileWithNewlineAtEndResultsInOneDiffEntry() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .linesOfA()
+        .containsExactly("Line 1", "Line 2", "Line 3", "");
   }
 
   @Test
@@ -136,6 +236,91 @@
   }
 
   @Test
+  public void numberOfLinesInDiffOfAddedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).metaA().isNull();
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+  }
+
+  @Test
+  public void numberOfLinesInFileInfoOfAddedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(filePath)).linesInserted().isEqualTo(3);
+    assertThat(changedFiles.get(filePath)).linesDeleted().isNull();
+  }
+
+  @Test
+  public void numberOfLinesInDiffOfAddedFileWithNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).metaA().isNull();
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+  }
+
+  @Test
+  public void numberOfLinesInFileInfoOfAddedFileWithNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(filePath)).linesInserted().isEqualTo(3);
+    assertThat(changedFiles.get(filePath)).linesDeleted().isNull();
+  }
+
+  @Test
+  public void addedFileWithoutNewlineAtEndResultsInOneDiffEntry() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .linesOfB()
+        .containsExactly("Line 1", "Line 2", "Line 3");
+  }
+
+  @Test
+  public void addedFileWithNewlineAtEndResultsInOneDiffEntry() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .linesOfB()
+        .containsExactly("Line 1", "Line 2", "Line 3", "");
+  }
+
+  @Test
   public void renamedFileIsIncludedInDiff() throws Exception {
     String newFilePath = "a_new_file.txt";
     gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath);
@@ -193,11 +378,11 @@
 
     // automerge
     diff = getDiffRequest(r.getChangeId(), r.getCommit().name(), "foo").get();
-    assertThat(diff.metaA.lines).isEqualTo(5);
+    assertThat(diff.metaA.lines).isEqualTo(6);
     assertThat(diff.metaB.lines).isEqualTo(1);
 
     diff = getDiffRequest(r.getChangeId(), r.getCommit().name(), "bar").get();
-    assertThat(diff.metaA.lines).isEqualTo(5);
+    assertThat(diff.metaA.lines).isEqualTo(6);
     assertThat(diff.metaB.lines).isEqualTo(1);
 
     // parent 1
@@ -212,6 +397,596 @@
   }
 
   @Test
+  public void diffOfUnmodifiedFileMarksAllLinesAsCommon() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .commonLines()
+        .containsAllOf("Line 1", "Line 2", "Line 3")
+        .inOrder();
+    assertThat(diffInfo).content().onlyElement().linesOfA().isNull();
+    assertThat(diffInfo).content().onlyElement().linesOfB().isNull();
+  }
+
+  @Test
+  public void diffOfUnmodifiedFileWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().onlyElement().commonLines().lastElement().isEqualTo("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+  }
+
+  @Test
+  public void diffOfUnmodifiedFileWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().onlyElement().commonLines().lastElement().isEqualTo("Line 3");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+  }
+
+  @Test
+  public void diffOfModifiedFileWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 1\n", "Line one\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+  }
+
+  @Test
+  public void diffOfModifiedFileWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 1\n", "Line one\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("Line 3");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+  }
+
+  @Test
+  public void diffOfModifiedLastLineWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 3\n", "Line three\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+  }
+
+  @Test
+  public void diffOfModifiedLastLineWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 3", "Line three"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().lastElement().linesOfA().containsExactly("Line 3");
+    assertThat(diffInfo).content().lastElement().linesOfB().containsExactly("Line three");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+  }
+
+  @Test
+  public void addedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsConsidered() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101", "");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  public void addedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsIgnored() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().isNull();
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  public void addedNewlineAtEndOfFileMeansOneModifiedLine() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void addedLastLineWithoutNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101", "Line 102");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  public void addedLastLineWithoutNewlineBeforeAndAfterwardsMeansTwoModifiedLines()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void addedLastLineWithoutNewlineBeforeButWithOneAfterwardsIsMarkedInDiff()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .linesOfB()
+        .containsExactly("Line 101", "Line 102", "");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(103);
+  }
+
+  @Test
+  public void addedLastLineWithoutNewlineBeforeButWithOneAfterwardsMeansTwoModifiedLines()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102\n"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void addedLastLineWithNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().isNull();
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(2).commonLines().containsExactly("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  public void addedLastLineWithNewlineBeforeAndAfterwardsMeansOneInsertedLine() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101\n"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isNull();
+  }
+
+  @Test
+  public void addedLastLineWithNewlineBeforeButWithoutOneAfterwardsIsMarkedInDiff()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(101);
+  }
+
+  @Test
+  public void addedLastLineWithNewlineBeforeButWithoutOneAfterwardsMeansOneInsertedLine()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isNull();
+  }
+
+  @Test
+  public void hunkForModifiedLastLineIsCombinedWithHunkForAddedNewlineAtEnd() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 101", "Line one oh one\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one oh one", "");
+  }
+
+  @Test
+  public void intralineEditsForModifiedLastLineArePreservedWhenNewlineIsAlsoAddedAtEnd()
+      throws Exception {
+    assume().that(intraline).isTrue();
+
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 101", "Line one oh one\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .intralineEditsOfA()
+        .containsExactly(ImmutableList.of(5, 3));
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .intralineEditsOfB()
+        .containsExactly(ImmutableList.of(5, 11));
+  }
+
+  @Test
+  public void hunkForModifiedSecondToLastLineIsNotCombinedWithHunkForAddedNewlineAtEnd()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(
+        changeId,
+        FILE_NAME,
+        fileContent -> fileContent.replace("Line 100\n", "Line one hundred\n").concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one hundred");
+    assertThat(diffInfo).content().element(2).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(3).linesOfA().isNull();
+    assertThat(diffInfo).content().element(3).linesOfB().containsExactly("");
+  }
+
+  @Test
+  public void deletedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsConsidered() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100", "");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 100");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+  }
+
+  @Test
+  public void deletedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsIgnored() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("");
+    assertThat(diffInfo).content().element(1).linesOfB().isNull();
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+  }
+
+  @Test
+  public void deletedNewlineAtEndOfFileMeansOneModifiedLine() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void deletedLastLineWithoutNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100", ""));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99", "Line 100");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 99");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(100);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(99);
+  }
+
+  @Test
+  public void deletedLastLineWithoutNewlineBeforeAndAfterwardsMeansTwoModifiedLines()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100", ""));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
+  }
+
+  @Test
+  public void deletedLastLineWithoutNewlineBeforeButWithOneAfterwardsIsMarkedInDiff()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100", ""));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(100);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+  }
+
+  @Test
+  public void deletedLastLineWithoutNewlineBeforeButWithOneAfterwardsMeansOneDeletedLine()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100", ""));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isNull();
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void deletedLastLineWithNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", ""));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+    assertThat(diffInfo).content().element(1).linesOfB().isNull();
+    assertThat(diffInfo).content().element(2).commonLines().containsExactly("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+  }
+
+  @Test
+  public void deletedLastLineWithNewlineBeforeAndAfterwardsMeansOneDeletedLine() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", ""));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isNull();
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void deletedLastLineWithNewlineBeforeButWithoutOneAfterwardsIsMarkedInDiff()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100\n", ""));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99", "Line 100", "");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 99");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(99);
+  }
+
+  @Test
+  public void deletedLastLineWithNewlineBeforeButWithoutOneAfterwardsMeansTwoModifiedLines()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100\n", ""));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
+  }
+
+  @Test
+  public void hunkForModifiedLastLineIsCombinedWithHunkForDeletedNewlineAtEnd() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line one hundred"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100", "");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one hundred");
+  }
+
+  @Test
+  public void intralineEditsForModifiedLastLineArePreservedWhenNewlineIsAlsoDeletedAtEnd()
+      throws Exception {
+    assume().that(intraline).isTrue();
+
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line one hundred"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .intralineEditsOfA()
+        .containsExactly(ImmutableList.of(5, 4));
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .intralineEditsOfB()
+        .containsExactly(ImmutableList.of(5, 11));
+  }
+
+  @Test
+  public void hunkForModifiedSecondToLastLineIsNotCombinedWithHunkForDeletedNewlineAtEnd()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId,
+        FILE_NAME,
+        fileContent ->
+            fileContent
+                .replace("Line 99\n", "Line ninety-nine\n")
+                .replace("Line 100\n", "Line 100"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line ninety-nine");
+    assertThat(diffInfo).content().element(2).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(3).linesOfA().containsExactly("");
+    assertThat(diffInfo).content().element(3).linesOfB().isNull();
+  }
+
+  @Test
   public void addedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
     ObjectId commit2 = addCommit(commit1, "file_added_in_another_commit.txt", "Some file content");
 
@@ -382,7 +1157,7 @@
         .content()
         .element(1)
         .commonLines()
-        .containsExactly("Line 2", "Line 3", "Line 4", "Line 5")
+        .containsExactly("Line 2", "Line 3", "Line 4", "Line 5", "")
         .inOrder();
   }
 
@@ -430,7 +1205,7 @@
         .content()
         .element(2)
         .commonLines()
-        .containsExactly("Line 4", "Line 5", "Line 6")
+        .containsExactly("Line 4", "Line 5", "Line 6", "")
         .inOrder();
   }
 
@@ -1027,7 +1802,7 @@
     DiffInfo diffInfo =
         getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
     assertThat(diffInfo).changeType().isEqualTo(ChangeType.DELETED);
-    assertThat(diffInfo).content().element(0).linesOfA().hasSize(100);
+    assertThat(diffInfo).content().element(0).linesOfA().hasSize(101);
     assertThat(diffInfo).content().element(0).linesOfB().isNull();
     assertThat(diffInfo).content().element(0).isNotDueToRebase();
 
@@ -1050,7 +1825,7 @@
         getDiffRequest(changeId, CURRENT, newFilePath).withBase(initialPatchSetId).get();
     assertThat(diffInfo).changeType().isEqualTo(ChangeType.ADDED);
     assertThat(diffInfo).content().element(0).linesOfA().isNull();
-    assertThat(diffInfo).content().element(0).linesOfB().hasSize(3);
+    assertThat(diffInfo).content().element(0).linesOfB().hasSize(4);
     assertThat(diffInfo).content().element(0).isNotDueToRebase();
 
     Map<String, FileInfo> changedFiles =