Merge "ExternalIdCacheLoader: suppress warning if cache is not persisted."
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 64d156f..d312530 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -95,9 +95,6 @@
   // The version of a secondary index.
   public abstract Optional<Integer> indexVersion();
 
-  // The number of inputs to an operation, eg. Reachable.fromRefs.
-  public abstract Optional<Integer> inputSize();
-
   // The name of the implementation method.
   public abstract Optional<String> methodName();
 
@@ -303,8 +300,6 @@
 
     public abstract Builder indexVersion(int indexVersion);
 
-    public abstract Builder inputSize(int size);
-
     public abstract Builder methodName(@Nullable String methodName);
 
     public abstract Builder multiple(boolean multiple);
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index c30378b..6d28646a 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -67,7 +67,7 @@
       try (TraceTimer timer =
           TraceContext.newTimer(
               "IncludedInResolver.includedInAny",
-              Metadata.builder().projectName(project.get()).inputSize(refs.size()).build())) {
+              Metadata.builder().projectName(project.get()).resourceCount(refs.size()).build())) {
         return IncludedInResolver.includedInAny(repo, rw, commit, filtered.values());
       }
     } catch (IOException | PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 94c379b..b44bb29 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -95,11 +95,12 @@
           String.format("change is %s.", ChangeUtil.status(changeResource.getChange())));
     }
 
-    String submissionId =
-        requireNonNull(
-            changeResource.getChange().getSubmissionId(),
-            String.format("merged change %s has no submission ID", changeResource.getId()));
-
+    String submissionId = changeResource.getChange().getSubmissionId();
+    if (submissionId == null) {
+      throw new ResourceConflictException(
+          "This change is merged but doesn't have a submission id,"
+              + " meaning it was not submitted through Gerrit.");
+    }
     List<ChangeData> changeDatas = queryProvider.get().bySubmissionId(submissionId);
 
     for (ChangeData changeData : changeDatas) {
diff --git a/plugins/replication b/plugins/replication
index 4689b41..d7c09fb 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 4689b419eab61ea204daf2dfca47296667ac317c
+Subproject commit d7c09fbb4c18b1743d6060d361171c2a5237f22b
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 0817762..6703651 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -269,6 +269,25 @@
           Do you really want to delete the edit?
         </div>
       </gr-dialog>
+      <gr-dialog
+        id="showRevertSubmissionChangesDialog"
+        class="confirmDialog"
+        confirm-label="Close"
+        cancel-label=''
+        on-confirm="_handleShowRevertSubmissionChangesConfirm">
+        <div class="header" slot="header">
+          Reverted Changes
+        </div>
+        <div class="main" slot="main">
+          <template is="dom-repeat" items="[[_revertChanges]]">
+            <div>
+              <a href$="[[item.link]]" target="_blank">
+                Change [[item._number]]
+              </a>
+            </div>
+          </template>
+        </div>
+      </gr-dialog>
     </gr-overlay>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 2ce0d7d..8ad2a06 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -420,6 +420,10 @@
           type: Boolean,
           value: true,
         },
+        _revertChanges: {
+          type: Array,
+          value: [],
+        },
       };
     }
 
@@ -1258,6 +1262,7 @@
     _handleResponse(action, response) {
       if (!response) { return; }
       return this.$.restAPI.getResponseObject(response).then(obj => {
+        let revertChanges = [];
         switch (action.__key) {
           case ChangeActions.REVERT:
             this._waitForChangeReachable(obj._number)
@@ -1282,6 +1287,29 @@
           case ChangeActions.REBASE_EDIT:
             Gerrit.Nav.navigateToChange(this.change);
             break;
+          case ChangeActions.REVERT_SUBMISSION:
+            revertChanges = obj.revert_changes || [];
+            revertChanges = revertChanges.map(change => {
+              change.link = '/q/' + encodeURIComponent(change.change_id);
+              return change;
+            });
+            // list of reverted changes can never be 0
+            if (revertChanges.length === 1) {
+              // redirect to the change if only 1 change is reverted
+              const change = revertChanges[0];
+              this._waitForChangeReachable(change._number).then(success => {
+                if (success) {
+                  Gerrit.Nav.navigateToChange(change);
+                } else {
+                  console.error('Change ' + change._number + ' not reachable');
+                }
+              });
+            } else {
+              // show multiple reverted changes in a dialog
+              this._revertChanges = revertChanges;
+              this._showActionDialog(this.$.showRevertSubmissionChangesDialog);
+            }
+            break;
           default:
             this.dispatchEvent(new CustomEvent('reload-change',
                 {detail: {action: action.__key}, bubbles: false}));
@@ -1290,6 +1318,10 @@
       });
     }
 
+    _handleShowRevertSubmissionChangesConfirm() {
+      this._hideAllDialogs();
+    }
+
     _handleResponseError(action, response, body) {
       if (action && action.__key === RevisionActions.CHERRYPICK) {
         if (response && response.status === 409 &&
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index a10f028..532c573 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -1422,6 +1422,7 @@
       let payload;
       let onShowError;
       let onShowAlert;
+      let getResponseObjectStub;
 
       setup(() => {
         cleanup = sinon.stub();
@@ -1437,12 +1438,18 @@
 
       suite('happy path', () => {
         let sendStub;
-
+        let waitForChangeReachableStub;
         setup(() => {
           sandbox.stub(element, 'fetchChangeUpdates')
               .returns(Promise.resolve({isLatest: true}));
           sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction')
               .returns(Promise.resolve({}));
+          getResponseObjectStub = sandbox.stub(element.$.restAPI,
+              'getResponseObject');
+          waitForChangeReachableStub = sandbox.stub(element,
+              '_waitForChangeReachable').returns(Promise.resolve(true));
+          sandbox.stub(Gerrit.Nav,
+              'navigateToChange').returns(Promise.resolve(true));
         });
 
         test('change action', () => {
@@ -1455,6 +1462,49 @@
               });
         });
 
+        suite('single changes revert', () => {
+          setup(() => {
+            getResponseObjectStub
+                .returns(Promise.resolve({revert_changes: [
+                  {change_id: 12345},
+                ]}));
+            showActionDialogStub = sandbox.stub(element, '_showActionDialog');
+          });
+
+          test('revert submission single change', done => {
+            element._send('POST', {message: 'Revert submission'},
+                '/revert_submission', false, cleanup).then(res => {
+              element._handleResponse({__key: 'revert_submission'}, {}).
+                  then(() => {
+                    assert.isTrue(waitForChangeReachableStub.called);
+                    done();
+                  });
+            });
+          });
+        });
+
+        suite('multiple changes revert', () => {
+          let showActionDialogStub;
+          setup(() => {
+            getResponseObjectStub
+                .returns(Promise.resolve({revert_changes: [
+                  {change_id: 12345}, {change_id: 23456},
+                ]}));
+            showActionDialogStub = sandbox.stub(element, '_showActionDialog');
+          });
+
+          test('revert submission multiple change', done => {
+            element._send('POST', {message: 'Revert submission'},
+                '/revert_submission', false, cleanup).then(res => {
+              element._handleResponse({__key: 'revert_submission'}, {}).then(
+                  () => {
+                    assert.isTrue(showActionDialogStub.called);
+                    done();
+                  });
+            });
+          });
+        });
+
         test('revision action', () => {
           return element._send('DELETE', payload, '/endpoint', true, cleanup)
               .then(() => {
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index fe8384e..d29858e 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -90,7 +90,7 @@
       }
       gr-smart-search {
         flex-grow: 1;
-        margin-left: var(--spacing-m);
+        margin: 0 var(--spacing-m);
         max-width: 500px;
       }
       gr-dropdown,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
index d7510ec..89b28d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
@@ -58,6 +58,9 @@
       #container.unresolved {
         background-color: var(--unresolved-comment-background-color);
       }
+      #container.robotComment {
+        background-color: var(--robot-comment-background-color);
+      }
       #commentInfoContainer {
         border-top: 1px dotted var(--border-color);
         display: flex;
@@ -82,7 +85,7 @@
         <span class="descriptionText">Patchset [[patchNum]]</span>
       </div>
     </template>
-    <div id="container" class$="[[_computeHostClass(unresolved)]]">
+    <div id="container" class$="[[_computeHostClass(unresolved, isRobotComment)]]">
       <template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
           as="comment">
         <gr-comment
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index 72fc45d..8687e77 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -137,6 +137,11 @@
         _lastComment: Object,
         _orderedComments: Array,
         _projectConfig: Object,
+        isRobotComment: {
+          type: Boolean,
+          value: false,
+          reflectToAttribute: true,
+        },
       };
     }
 
@@ -221,6 +226,7 @@
         this._lastComment = this._getLastComment();
         this.unresolved = this._lastComment.unresolved;
         this.hasDraft = this._lastComment.__draft;
+        this.isRobotComment = !!(this._lastComment.robot_id);
       }
     }
 
@@ -498,6 +504,9 @@
     }
 
     _computeHostClass(unresolved) {
+      if (this.isRobotComment) {
+        return 'robotComment';
+      }
       return unresolved ? 'unresolved' : '';
     }
 
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index 0ccb5c5..3a620d2 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -45,6 +45,7 @@
   --assignee-highlight-color: #fcfad6;
   --chip-background-color: #eee;
   --comment-background-color: #fcfad6;
+  --robot-comment-background-color: #e8f0fe;
   --default-button-background-color: white;
   --dialog-background-color: white;
   --dropdown-background-color: white;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 513f28a..4a91774 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -45,6 +45,7 @@
       --assignee-highlight-color: #3a361c;
       --chip-background-color: #131416;
       --comment-background-color: #0b162b;
+      --robot-comment-background-color: #e8f0fe;
       --default-button-background-color: #3c4043;
       --dialog-background-color: #131416;
       --dropdown-background-color: #131416;