Merge "Fix layout of "has been merged" warning in reply dialog"
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 032f08e..b5035d8 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
 import static java.util.Objects.requireNonNull;
 
@@ -38,6 +39,7 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -96,7 +98,8 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
-public class RevertSubmission implements RestModifyView<ChangeResource, RevertInput> {
+public class RevertSubmission
+    implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<InternalChangeQuery> queryProvider;
@@ -527,6 +530,40 @@
             potentialCommitToReturn.getName(), changeNotes.getChange().getChangeId()));
   }
 
+  @Override
+  public Description getDescription(ChangeResource rsrc) {
+    Change change = rsrc.getChange();
+    boolean projectStatePermitsWrite = false;
+    try {
+      projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log(
+          "Failed to check if project state permits write: %s", rsrc.getProject());
+    }
+    return new UiAction.Description()
+        .setLabel("Revert submission")
+        .setTitle(
+            "Revert this change and all changes that have been submitted together with this change")
+        .setVisible(
+            and(
+                change.isMerged()
+                    && change.getSubmissionId() != null
+                    && isChangePartOfSubmission(change.getSubmissionId())
+                    && projectStatePermitsWrite,
+                permissionBackend
+                    .user(rsrc.getUser())
+                    .ref(change.getDest())
+                    .testCond(CREATE_CHANGE)));
+  }
+
+  /**
+   * @param submissionId the submission id of the change.
+   * @return True if the submission has more than one change, false otherwise.
+   */
+  private Boolean isChangePartOfSubmission(String submissionId) {
+    return (queryProvider.get().setLimit(2).bySubmissionId(submissionId).size() > 1);
+  }
+
   private class CreateCherryPickOp implements BatchUpdateOp {
     private final ObjectId revCommitId;
     private final String topic;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 28b8cae..911a04d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -78,6 +78,22 @@
     gApi.changes().id(changeId).current().submit();
     Map<String, ActionInfo> actions = getChangeActions(changeId);
     assertThat(actions).containsKey("revert");
+    assertThat(actions).doesNotContainKey("revert_submission");
+  }
+
+  @Test
+  public void changeActionTwoMergedChangesHaveReverts() throws Exception {
+    String changeId1 = createChangeWithTopic().getChangeId();
+    String changeId2 = createChangeWithTopic().getChangeId();
+    gApi.changes().id(changeId1).current().review(ReviewInput.approve());
+    gApi.changes().id(changeId2).current().review(ReviewInput.approve());
+    gApi.changes().id(changeId2).current().submit();
+    Map<String, ActionInfo> actions1 = getChangeActions(changeId1);
+    assertThat(actions1).containsKey("revert");
+    assertThat(actions1).containsKey("revert_submission");
+    Map<String, ActionInfo> actions2 = getChangeActions(changeId2);
+    assertThat(actions2).containsKey("revert");
+    assertThat(actions2).containsKey("revert_submission");
   }
 
   @Test
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 11e2217..5228578f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -637,6 +637,7 @@
             threads="[[_commentThreads]]"
             change="[[_change]]"
             change-num="[[_changeNum]]"
+            comment-tab="[[_currentView]]"
             logged-in="[[_loggedIn]]"
             only-show-robot-comments-with-human-reply
             on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
@@ -654,6 +655,7 @@
             change="[[_change]]"
             change-num="[[_changeNum]]"
             logged-in="[[_loggedIn]]"
+            comment-tab="[[_currentView]]"
             hide-toggle-buttons
             on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
       </template>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
index a096aec..eacc0d0 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
@@ -76,7 +76,7 @@
     </template>
     <div id="threads">
       <template is="dom-if" if="[[!threads.length]]">
-        There are no inline comment threads on any diff for this change.
+        [[_computeNoThreadsMessage(commentTab)]]
       </template>
       <template
           is="dom-repeat"
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index e449fad..d8c7b61 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -23,6 +23,17 @@
    * @event thread-list-modified
    * @extends Polymer.Element
    */
+  const NO_THREADS_MESSAGE = 'There are no inline comment threads on any diff '
+    + 'for this change.';
+  const NO_ROBOT_COMMENTS_THREADS_MESSAGE = 'There are no findings for this ' +
+    'patchset.';
+
+  const CommentTabs = {
+    CHANGE_LOG: 0,
+    COMMENT_THREADS: 1,
+    ROBOT_COMMENTS: 2,
+  };
+
   class GrThreadList extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -62,6 +73,7 @@
           type: Boolean,
           value: false,
         },
+        commentTab: Number,
       };
     }
 
@@ -71,6 +83,13 @@
       return loggedIn ? 'show' : '';
     }
 
+    _computeNoThreadsMessage(commentTab) {
+      if (commentTab === CommentTabs.ROBOT_COMMENTS) {
+        return NO_ROBOT_COMMENTS_THREADS_MESSAGE;
+      }
+      return NO_THREADS_MESSAGE;
+    }
+
     /**
      * Order as follows:
      *  - Unresolved threads with drafts (reverse chronological)