Merge changes from topic 'plugin-api'

* changes:
  ServerInfoIT: Use API to install plugin
  Add implementation of PluginApi
  PluginLoader: Ensure that $site/plugins folder exists
  Plugins#ListRequest: Remove parameter from all() method
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 8b4e529..b307efa 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5729,6 +5729,8 @@
 When present, change is marked as Work In Progress.
 |`has_review_started` |optional, not set if `false`|
 When present, change has been marked Ready at some point in time.
+|`revert_of`          |optional|
+The numeric Change-Id of the change that this change reverts.
 |==================================
 
 [[change-input]]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 74ae568..bbca364 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -129,6 +129,11 @@
 Changes that have the given user CC'ed on them. The special case of `cc:self`
 will find changes where the caller has been CC'ed.
 
+[[revertof]]
+revertof:'ID'::
++
+Changes that revert the change specified by the numeric 'ID'.
+
 [[reviewerin]]
 reviewerin:'GROUP'::
 +
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 4dc8e51..6d0ba19 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -617,6 +617,7 @@
 
     assertThat(revertChange.messages).hasSize(1);
     assertThat(revertChange.messages.iterator().next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(revertChange.revertOf).isEqualTo(gApi.changes().id(r.getChangeId()).get()._number);
   }
 
   @Test
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index f6d9f4c..a4f85e5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -48,6 +48,7 @@
   public Boolean isPrivate;
   public Boolean workInProgress;
   public Boolean hasReviewStarted;
+  public Integer revertOf;
 
   public int _number;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 753d421..a5fc4f3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -154,6 +154,8 @@
 
     suggestions.add("unresolved:");
 
+    suggestions.add("revertof:");
+
     if (Gerrit.isNoteDbEnabled()) {
       suggestions.add("cc:");
       suggestions.add("hashtag:");
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 1a08d17..4252c72 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -524,6 +524,10 @@
   @Column(id = 22)
   protected boolean reviewStarted;
 
+  /** References a change that this change reverts. */
+  @Column(id = 23, notNull = false)
+  protected Id revertOf;
+
   /** @see com.google.gerrit.server.notedb.NoteDbChangeState */
   @Column(id = 101, notNull = false, length = Integer.MAX_VALUE)
   protected String noteDbState;
@@ -564,6 +568,7 @@
     workInProgress = other.workInProgress;
     reviewStarted = other.reviewStarted;
     noteDbState = other.noteDbState;
+    revertOf = other.revertOf;
   }
 
   /** Legacy 32 bit integer identity for a change. */
@@ -733,6 +738,14 @@
     this.reviewStarted = reviewStarted;
   }
 
+  public void setRevertOf(Id revertOf) {
+    this.revertOf = revertOf;
+  }
+
+  public Id getRevertOf() {
+    return this.revertOf;
+  }
+
   public String getNoteDbState() {
     return noteDbState;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index bf417d0..3e8a146 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -129,6 +129,7 @@
   private boolean fireRevisionCreated;
   private boolean sendMail;
   private boolean updateRef;
+  private Change.Id revertOf;
 
   // Fields set during the insertion process.
   private ReceiveCommand cmd;
@@ -198,6 +199,7 @@
     change.setPrivate(isPrivate);
     change.setWorkInProgress(workInProgress);
     change.setReviewStarted(!workInProgress);
+    change.setRevertOf(revertOf);
     return change;
   }
 
@@ -319,6 +321,11 @@
     return this;
   }
 
+  public ChangeInserter setRevertOf(Change.Id revertOf) {
+    this.revertOf = revertOf;
+    return this;
+  }
+
   public void setPushCertificate(String cert) {
     pushCert = cert;
   }
@@ -390,6 +397,9 @@
     update.setPsDescription(patchSetDescription);
     update.setPrivate(isPrivate);
     update.setWorkInProgress(workInProgress);
+    if (revertOf != null) {
+      update.setRevertOf(revertOf.get());
+    }
 
     boolean draft = status == Change.Status.DRAFT;
     List<String> newGroups = groups;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 33a1565..80bf1c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -542,6 +542,7 @@
     out.submitted = getSubmittedOn(cd);
     out.plugins =
         pluginDefinedAttributesFactory != null ? pluginDefinedAttributesFactory.create(cd) : null;
+    out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null;
 
     if (out.labels != null && has(DETAILED_LABELS)) {
       // If limited to specific patch sets but not the current patch set, don't
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index af06054..53e09d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -218,6 +218,7 @@
       Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
       ccs.remove(user.getAccountId());
       ins.setExtraCC(ccs);
+      ins.setRevertOf(changeIdToRevert);
 
       try (BatchUpdate bu = updateFactory.create(db.get(), project, user, now)) {
         bu.setRepository(git, revWalk, oi);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index 7a8cc72..3fe2d13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -207,6 +207,11 @@
           .stored()
           .buildRepeatable(cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()));
 
+  /** References a change that this change reverts. */
+  public static final FieldDef<ChangeData, Integer> REVERT_OF =
+      integer(ChangeQueryBuilder.FIELD_REVERTOF)
+          .build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null);
+
   @VisibleForTesting
   static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
     List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index bb0118b..2f03779 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -78,6 +78,7 @@
   static final Schema<ChangeData> V43 =
       schema(V42, ChangeField.EXACT_AUTHOR, ChangeField.EXACT_COMMITTER);
 
+  @Deprecated
   static final Schema<ChangeData> V44 =
       schema(
           V43,
@@ -85,6 +86,8 @@
           ChangeField.PENDING_REVIEWER,
           ChangeField.PENDING_REVIEWER_BY_EMAIL);
 
+  static final Schema<ChangeData> V45 = schema(V44, ChangeField.REVERT_OF);
+
   public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 472eda1..6dfe7a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -57,6 +57,7 @@
 
   protected PatchSet.Id psId;
   private ObjectId result;
+  protected boolean rootOnly;
 
   protected AbstractChangeUpdate(
       Config cfg,
@@ -190,6 +191,11 @@
   /** Whether no updates have been done. */
   public abstract boolean isEmpty();
 
+  /** Wether this update can only be a root commit. */
+  public boolean isRootOnly() {
+    return rootOnly;
+  }
+
   /**
    * @return the NameKey for the project where the update will be stored, which is not necessarily
    *     the same as the change's project.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
index d56baed..526f3c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -233,7 +233,8 @@
     // last time this file was updated.
     checkColumns(Change.Id.class, 1);
 
-    checkColumns(Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 20, 21, 22, 101);
+    checkColumns(
+        Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 20, 21, 22, 23, 101);
     checkColumns(ChangeMessage.Key.class, 1, 2);
     checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6, 7);
     checkColumns(PatchSet.Id.class, 1, 2);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index ce3b664..b23c1de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -83,6 +83,7 @@
   public static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
   public static final FooterKey FOOTER_TAG = new FooterKey("Tag");
   public static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress");
+  public static final FooterKey FOOTER_REVERT_OF = new FooterKey("Revert-of");
 
   private static final String AUTHOR = "Author";
   private static final String BASE_PATCH_SET = "Base-for-patch-set";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 85e49ce..b01a8b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -623,6 +623,10 @@
     return state.isWorkInProgress();
   }
 
+  public Change.Id getRevertOf() {
+    return state.revertOf();
+  }
+
   public boolean hasReviewStarted() {
     return state.hasReviewStarted();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index eb366bb..4b239ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -27,6 +27,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REVERT_OF;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
@@ -169,6 +170,7 @@
   private Boolean hasReviewStarted;
   private ReviewerSet pendingReviewers;
   private ReviewerByEmailSet pendingReviewersByEmail;
+  private Change.Id revertOf;
 
   ChangeNotesParser(
       Change.Id changeId,
@@ -267,7 +269,8 @@
         readOnlyUntil,
         isPrivate,
         workInProgress,
-        hasReviewStarted);
+        hasReviewStarted,
+        revertOf);
   }
 
   private PatchSet.Id buildCurrentPatchSetId() {
@@ -415,6 +418,10 @@
       parseIsPrivate(commit);
     }
 
+    if (revertOf == null) {
+      revertOf = parseRevertOf(commit);
+    }
+
     previousWorkInProgressFooter = null;
     parseWorkInProgress(commit);
 
@@ -1022,6 +1029,18 @@
     throw invalidFooter(FOOTER_WORK_IN_PROGRESS, raw);
   }
 
+  private Change.Id parseRevertOf(ChangeNotesCommit commit) throws ConfigInvalidException {
+    String footer = parseOneFooter(commit, FOOTER_REVERT_OF);
+    if (footer == null) {
+      return null;
+    }
+    Integer revertOf = Ints.tryParse(footer);
+    if (revertOf == null) {
+      throw invalidFooter(FOOTER_REVERT_OF, footer);
+    }
+    return new Change.Id(revertOf);
+  }
+
   private void pruneReviewers() {
     Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Timestamp>> rit =
         reviewers.cellSet().iterator();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index f9899e5..1dd944d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -78,7 +78,8 @@
         null,
         null,
         null,
-        true);
+        true,
+        null);
   }
 
   static ChangeNotesState create(
@@ -113,7 +114,8 @@
       @Nullable Timestamp readOnlyUntil,
       @Nullable Boolean isPrivate,
       @Nullable Boolean workInProgress,
-      boolean hasReviewStarted) {
+      boolean hasReviewStarted,
+      @Nullable Change.Id revertOf) {
     if (hashtags == null) {
       hashtags = ImmutableSet.of();
     }
@@ -135,7 +137,8 @@
             status,
             isPrivate,
             workInProgress,
-            hasReviewStarted),
+            hasReviewStarted,
+            revertOf),
         ImmutableSet.copyOf(pastAssignees),
         ImmutableSet.copyOf(hashtags),
         ImmutableList.copyOf(patchSets.entrySet()),
@@ -153,7 +156,8 @@
         readOnlyUntil,
         isPrivate,
         workInProgress,
-        hasReviewStarted);
+        hasReviewStarted,
+        revertOf);
   }
 
   /**
@@ -205,6 +209,9 @@
 
     @Nullable
     abstract Boolean hasReviewStarted();
+
+    @Nullable
+    abstract Change.Id revertOf();
   }
 
   // Only null if NoteDb is disabled.
@@ -258,6 +265,9 @@
   @Nullable
   abstract Boolean hasReviewStarted();
 
+  @Nullable
+  abstract Change.Id revertOf();
+
   Change newChange(Project.NameKey project) {
     ChangeColumns c = checkNotNull(columns(), "columns are required");
     Change change =
@@ -318,6 +328,7 @@
     change.setPrivate(c.isPrivate() == null ? false : c.isPrivate());
     change.setWorkInProgress(c.isWorkInProgress() == null ? false : c.isWorkInProgress());
     change.setReviewStarted(c.hasReviewStarted() == null ? false : c.hasReviewStarted());
+    change.setRevertOf(c.revertOf());
 
     if (!patchSets().isEmpty()) {
       change.setCurrentPatchSet(c.currentPatchSetId(), c.subject(), c.originalSubject());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index fcde617..d692dff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -32,6 +32,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REVERT_OF;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
@@ -156,6 +157,7 @@
   private Timestamp readOnlyUntil;
   private Boolean isPrivate;
   private Boolean workInProgress;
+  private Integer revertOf;
 
   private ChangeDraftUpdate draftUpdate;
   private RobotCommentUpdate robotCommentUpdate;
@@ -512,6 +514,13 @@
     this.groups = groups;
   }
 
+  public void setRevertOf(int revertOf) {
+    int ownId = getChange().getId().get();
+    checkArgument(ownId != revertOf, "A change cannot revert itself");
+    this.revertOf = revertOf;
+    rootOnly = true;
+  }
+
   /** @return the tree id for the updated tree */
   private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
       throws ConfigInvalidException, OrmException, IOException {
@@ -755,6 +764,10 @@
       addFooter(msg, FOOTER_WORK_IN_PROGRESS, workInProgress);
     }
 
+    if (revertOf != null) {
+      addFooter(msg, FOOTER_REVERT_OF, revertOf);
+    }
+
     cb.setMessage(msg.toString());
     try {
       ObjectId treeId = storeRevisionNotes(rw, ins, curr);
@@ -804,7 +817,8 @@
         && !currentPatchSet
         && readOnlyUntil == null
         && isPrivate == null
-        && workInProgress == null;
+        && workInProgress == null
+        && revertOf == null;
   }
 
   ChangeDraftUpdate getDraftUpdate() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 45cf244..eef16fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -688,6 +688,9 @@
 
       ObjectId curr = old;
       for (U u : updates) {
+        if (u.isRootOnly() && !old.equals(ObjectId.zeroId())) {
+          throw new OrmException("Given ChangeUpdate is only allowed on initial commit");
+        }
         ObjectId next = u.apply(or.rw, or.tempIns, curr);
         if (next == null) {
           continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index 9c7f3bd..7aff213 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -618,6 +618,9 @@
     update.setChangeId(change.getKey().get());
     update.setBranch(change.getDest().get());
     update.setSubject(change.getOriginalSubject());
+    if (change.getRevertOf() != null) {
+      update.setRevertOf(change.getRevertOf().get());
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 96f6b5d..9a939bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -365,6 +365,7 @@
   private PersonIdent author;
   private PersonIdent committer;
   private Integer unresolvedCommentCount;
+  private Change.Id revertOf;
 
   private ImmutableList<byte[]> refStates;
   private ImmutableList<byte[]> refStatePatterns;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 161233e..aecfc42 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -179,6 +179,7 @@
   public static final String FIELD_VISIBLETO = "visibleto";
   public static final String FIELD_WATCHEDBY = "watchedby";
   public static final String FIELD_WIP = "wip";
+  public static final String FIELD_REVERTOF = "revertof";
 
   public static final String ARG_ID_USER = "user";
   public static final String ARG_ID_GROUP = "group";
@@ -1179,6 +1180,14 @@
     return new IsUnresolvedPredicate(value);
   }
 
+  @Operator
+  public Predicate<ChangeData> revertof(String value) throws QueryParseException {
+    if (args.getSchema().hasField(ChangeField.REVERT_OF)) {
+      return new RevertOfPredicate(value);
+    }
+    throw new QueryParseException("'revertof' operator is not supported by change index version");
+  }
+
   @Override
   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevertOfPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
new file mode 100644
index 0000000..7f4ade0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2017 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.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+
+public class RevertOfPredicate extends ChangeIndexPredicate {
+  public RevertOfPredicate(String revertOf) {
+    super(ChangeField.REVERT_OF, revertOf);
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    if (cd.change().getRevertOf() == null) {
+      return false;
+    }
+    return cd.change().getRevertOf().toString().equals(value);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 647a205..2a74aee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_155> C = Schema_155.class;
+  public static final Class<Schema_156> C = Schema_156.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_156.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_156.java
new file mode 100644
index 0000000..fd8fc00
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_156.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2017 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.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/** Add revertOf field to change. */
+public class Schema_156 extends SchemaVersion {
+  @Inject
+  Schema_156(Provider<Schema_155> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 3d12680..db8ec25 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -3494,6 +3494,43 @@
     assertThat(notes.getPendingReviewersByEmail().asTable()).isEmpty();
   }
 
+  @Test
+  public void revertOfIsNullByDefault() throws Exception {
+    Change c = newChange();
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getRevertOf()).isNull();
+  }
+
+  @Test
+  public void setRevertOfPersistsValue() throws Exception {
+    Change changeToRevert = newChange();
+    Change c = TestChanges.newChange(project, changeOwner.getAccountId());
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeId(c.getKey().get());
+    update.setRevertOf(changeToRevert.getId().get());
+    update.commit();
+    assertThat(newNotes(c).getRevertOf()).isEqualTo(changeToRevert.getId());
+  }
+
+  @Test
+  public void setRevertOfToCurrentChangeFails() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    exception.expect(IllegalArgumentException.class);
+    exception.expectMessage("A change cannot revert itself");
+    update.setRevertOf(c.getId().get());
+  }
+
+  @Test
+  public void setRevertOfOnChildCommitFails() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    exception.expect(OrmException.class);
+    exception.expectMessage("Given ChangeUpdate is only allowed on initial commit");
+    update.setRevertOf(newChange().getId().get());
+    update.commit();
+  }
+
   private boolean testJson() {
     return noteUtil.getWriteJson();
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index d5e4aa7..044dbbe 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -32,6 +32,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Streams;
 import com.google.common.truth.ThrowableSubject;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
@@ -51,6 +52,7 @@
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -2207,6 +2209,31 @@
     assertQuery("us");
   }
 
+  @Test
+  public void revertOf() throws Exception {
+    if (getSchemaVersion() < 45) {
+      assertMissingField(ChangeField.REVERT_OF);
+      assertFailingQuery(
+          "revertof:1", "'revertof' operator is not supported by change index version");
+      return;
+    }
+
+    TestRepository<Repo> repo = createProject("repo");
+    // Create two commits and revert second commit (initial commit can't be reverted)
+    Change initial = insert(repo, newChange(repo));
+    gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(initial.getChangeId()).current().submit();
+
+    ChangeInfo changeToRevert =
+        gApi.changes().create(new ChangeInput("repo", "master", "commit to revert")).get();
+    gApi.changes().id(changeToRevert.id).current().review(ReviewInput.approve());
+    gApi.changes().id(changeToRevert.id).current().submit();
+
+    ChangeInfo changeThatReverts = gApi.changes().id(changeToRevert.id).revert().get();
+    assertQueryByIds(
+        "revertof:" + changeToRevert._number, new Change.Id(changeThatReverts._number));
+  }
+
   protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
     return newChange(repo, null, null, null, null, false);
   }
@@ -2341,36 +2368,47 @@
     return assertQuery(newQuery(query), changes);
   }
 
+  protected List<ChangeInfo> assertQueryByIds(Object query, Change.Id... changes) throws Exception {
+    return assertQueryByIds(newQuery(query), changes);
+  }
+
   protected List<ChangeInfo> assertQuery(QueryRequest query, Change... changes) throws Exception {
+    return assertQueryByIds(
+        query, Arrays.stream(changes).map(Change::getId).toArray(Change.Id[]::new));
+  }
+
+  protected List<ChangeInfo> assertQueryByIds(QueryRequest query, Change.Id... changes)
+      throws Exception {
     List<ChangeInfo> result = query.get();
-    Iterable<Integer> ids = ids(result);
+    Iterable<Change.Id> ids = ids(result);
     assertThat(ids)
         .named(format(query, ids, changes))
-        .containsExactlyElementsIn(ids(changes))
+        .containsExactlyElementsIn(Arrays.asList(changes))
         .inOrder();
     return result;
   }
 
-  private String format(QueryRequest query, Iterable<Integer> actualIds, Change... expectedChanges)
+  private String format(
+      QueryRequest query, Iterable<Change.Id> actualIds, Change.Id... expectedChanges)
       throws RestApiException {
     StringBuilder b = new StringBuilder();
     b.append("query '").append(query.getQuery()).append("' with expected changes ");
-    b.append(format(Arrays.stream(expectedChanges).map(Change::getChangeId).iterator()));
+    b.append(format(Arrays.asList(expectedChanges)));
     b.append(" and result ");
     b.append(format(actualIds));
     return b.toString();
   }
 
-  private String format(Iterable<Integer> changeIds) throws RestApiException {
+  private String format(Iterable<Change.Id> changeIds) throws RestApiException {
     return format(changeIds.iterator());
   }
 
-  private String format(Iterator<Integer> changeIds) throws RestApiException {
+  private String format(Iterator<Change.Id> changeIds) throws RestApiException {
     StringBuilder b = new StringBuilder();
     b.append("[");
     while (changeIds.hasNext()) {
-      int id = changeIds.next();
-      ChangeInfo c = gApi.changes().id(id).get();
+      Change.Id id = changeIds.next();
+      ChangeInfo c = gApi.changes().id(id.get()).get();
       b.append("{")
           .append(id)
           .append(" (")
@@ -2393,12 +2431,12 @@
     return b.toString();
   }
 
-  protected static Iterable<Integer> ids(Change... changes) {
-    return FluentIterable.from(Arrays.asList(changes)).transform(in -> in.getId().get());
+  protected static Iterable<Change.Id> ids(Change... changes) {
+    return Arrays.stream(changes).map(c -> c.getId()).collect(toList());
   }
 
-  protected static Iterable<Integer> ids(Iterable<ChangeInfo> changes) {
-    return FluentIterable.from(changes).transform(in -> in._number);
+  protected static Iterable<Change.Id> ids(Iterable<ChangeInfo> changes) {
+    return Streams.stream(changes).map(c -> new Change.Id(c._number)).collect(toList());
   }
 
   protected static long lastUpdatedMs(Change c) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 120dbd5..91282f0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -118,6 +118,7 @@
         this._changesPerPage = prefs.changes_per_page;
         return this._getChanges();
       }).then(changes => {
+        changes = changes || [];
         if (this._query && changes.length === 1) {
           for (const query in LookupQueryPatterns) {
             if (LookupQueryPatterns.hasOwnProperty(query) &&
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 75f6749..bf55fc6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -166,7 +166,8 @@
     ],
 
     keyBindings: {
-      esc: '_handleEscKey',
+      'esc': '_handleEscKey',
+      'ctrl+enter meta+enter': '_handleEnterKey',
     },
 
     observers: [
@@ -229,6 +230,10 @@
       this.cancel();
     },
 
+    _handleEnterKey(e) {
+      this._submit();
+    },
+
     _ccsChanged(splices) {
       if (splices && splices.indexSplices) {
         this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC);
@@ -622,6 +627,10 @@
 
     _sendTapHandler(e) {
       e.preventDefault();
+      this._submit();
+    },
+
+    _submit() {
       if (this._ccsEnabled && !this.$$('#ccs').submitEntryText()) {
         // Do not proceed with the send if there is an invalid email entry in
         // the text field of the CC entry.
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 97fa033..5d9a2db 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -701,6 +701,18 @@
       assert.isTrue(cancelHandler.called);
     });
 
+    test('should not send on enter key', () => {
+      element.addEventListener('send', () => assert.fail('wrongly called'));
+      MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+      flushAsynchronousOperations();
+    });
+
+    test('emit send on ctrl+enter key', done => {
+      element.addEventListener('send', () => done());
+      MockInteractions.pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter');
+      flushAsynchronousOperations();
+    });
+
     test('_computeMessagePlaceholder', () => {
       assert.equal(
           element._computeMessagePlaceholder(false),