Merge branch 'stable-2.11' into stable-2.12

* stable-2.11:
  Update 2.11.8 release notes
  Prevent NPE in the SshLog
  Clear the input box after cancelling add reviewer action
  ReviewCommand: Don't add message twice on abandon or restore
  Set version to 2.11.8
  Release notes for Gerrit 2.11.8
  Fix keyboard shortcuts for non-US keyboards
  Update commons-collections to 3.2.2

Change-Id: Ieef066a51f72556958be6850276361f4ab526fec
diff --git a/ReleaseNotes/ReleaseNotes-2.11.8.txt b/ReleaseNotes/ReleaseNotes-2.11.8.txt
new file mode 100644
index 0000000..0f9dc21
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.8.txt
@@ -0,0 +1,43 @@
+Release notes for Gerrit 2.11.8
+===============================
+
+Gerrit 2.11.8 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.8.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.8.war]
+
+There are no schema changes from link:ReleaseNotes-2.11.7.html[2.11.7].
+
+Bug Fixes
+---------
+
+* Upgrade Apache commons-collections to version 3.2.2.
++
+Includes a fix for a link:https://issues.apache.org/jira/browse/COLLECTIONS-580[
+remote code execution exploit].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1207[Issue 1207]:
+Fix keyboard shortcuts for non-US keyboards on side-by-side diff screen.
++
+The forward/backward navigation keys `[` and `]` only worked on keyboards where
+these characters could be typed without using any modifier key (like CTRL, ALT,
+etc.).
++
+Note that the problem still exists on the unified diff screen.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]:
+Explicitly set parent project to 'All-Projects' when a project is created
+without giving the parent.
+
+* Don't add message twice on abandon or restore via ssh review command.
++
+When abandoning or reviewing a change via the ssh `review` command, and
+providing a message with the `--message` option, the message was added to
+the change twice.
+
+* Clear the input box after cancelling add reviewer action.
++
+When the action was cancelled, the content of the input box was still
+there when opening it again.
+
+* Fix internal server error when aborting ssh command.
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 357aae7..3d4a627 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -10,6 +10,7 @@
 [[2_11]]
 Version 2.11.x
 --------------
+* link:ReleaseNotes-2.11.8.html[2.11.8]
 * link:ReleaseNotes-2.11.7.html[2.11.7]
 * link:ReleaseNotes-2.11.6.html[2.11.6]
 * link:ReleaseNotes-2.11.5.html[2.11.5]
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
new file mode 100644
index 0000000..e07405f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2016 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.acceptance.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+@NoHttpd
+public class AbandonRestoreIT extends AbstractDaemonTest {
+
+  @Test
+  public void withMessage() throws Exception {
+    Result result = createChange();
+    String commit = result.getCommit().name();
+    executeCmd(commit, "abandon", "'abandon it'");
+    executeCmd(commit, "restore", "'restore it'");
+    assertChangeMessages(result.getChangeId(), ImmutableList.of(
+        "Uploaded patch set 1.",
+        "Abandoned\n\nabandon it",
+        "Restored\n\nrestore it"));
+  }
+
+  @Test
+  public void withoutMessage() throws Exception {
+    Result result = createChange();
+    String commit = result.getCommit().name();
+    executeCmd(commit, "abandon", null);
+    executeCmd(commit, "restore", null);
+    assertChangeMessages(result.getChangeId(), ImmutableList.of(
+        "Uploaded patch set 1.",
+        "Abandoned",
+        "Restored"));
+  }
+
+  private void executeCmd(String commit, String op, String message)
+      throws Exception {
+    StringBuilder command = new StringBuilder("gerrit review ")
+        .append(commit)
+        .append(" --")
+        .append(op);
+    if (message != null) {
+      command.append(" --message ").append(message);
+    }
+    String response = sshSession.exec(command.toString());
+    assert_()
+      .withFailureMessage(sshSession.getError())
+      .that(sshSession.hasError())
+      .isFalse();
+    assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
+  }
+
+  private void assertChangeMessages(String changeId, List<String> expected)
+      throws Exception {
+    ChangeInfo c = get(changeId);
+    Iterable<ChangeMessageInfo> messages = c.messages;
+    assertThat(messages).isNotNull();
+    assertThat(messages).hasSize(expected.size());
+    List<String> actual = new ArrayList<>();
+    for (ChangeMessageInfo info : messages) {
+      actual.add(info.message);
+    }
+    assertThat(actual).containsExactlyElementsIn(expected);
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index 3937ade..3ba7ea5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -134,6 +134,7 @@
     openForm.setVisible(true);
     UIObject.setVisible(form, false);
     suggestBox.setFocus(false);
+    suggestBox.setText("");
   }
 
   private void addReviewer(final String reviewer, boolean confirmed) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 3a8a1f5..c2b6a16 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -152,6 +152,9 @@
   }
 
   private Multimap<String, ?> extractParameters(DispatchCommand dcmd) {
+    if (dcmd == null) {
+      return ArrayListMultimap.create(0, 0);
+    }
     String[] cmdArgs = dcmd.getArguments();
     String paramName = null;
     int argPos = 0;
@@ -269,6 +272,9 @@
   }
 
   private String extractWhat(DispatchCommand dcmd) {
+    if (dcmd == null) {
+      return "Command was already destroyed";
+    }
     StringBuilder commandName = new StringBuilder(dcmd.getCommandName());
     String[] args = dcmd.getArguments();
     for (int i = 1; i < args.length; i++) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 54e07aa..1e3cf67 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -264,9 +264,6 @@
   }
 
   private void reviewPatchSet(final PatchSet patchSet) throws Exception {
-    if (changeComment == null) {
-      changeComment = "";
-    }
     if (notify == null) {
       notify = NotifyHandling.ALL;
     }
@@ -285,22 +282,20 @@
     }
     review.labels.putAll(customLabels);
 
-    // If review labels are being applied, the comment will be included
-    // on the review note. We don't need to add it again on the abandon
-    // or restore comment.
-    if (!review.labels.isEmpty() && (abandonChange || restoreChange)) {
-      changeComment = null;
+    // We don't need to add the review comment when abandoning/restoring.
+    if (abandonChange || restoreChange) {
+      review.message = null;
     }
 
     try {
       if (abandonChange) {
         AbandonInput input = new AbandonInput();
-        input.message = changeComment;
+        input.message = Strings.emptyToNull(changeComment);
         applyReview(patchSet, review);
         changeApi(patchSet).abandon(input);
       } else if (restoreChange) {
         RestoreInput input = new RestoreInput();
-        input.message = changeComment;
+        input.message = Strings.emptyToNull(changeComment);
         changeApi(patchSet).restore(input);
         applyReview(patchSet, review);
       } else {