Merge "Additional AccountExternalId when creating account"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 6019150..01dfcc2 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1200,20 +1200,16 @@
   [...]
   // update change
   ReviewDb db = dbProvider.get();
-  db.changes().beginTransaction(change.getId());
-  try {
-    change = db.changes().atomicUpdate(
-      change.getId(),
-      new AtomicUpdate<Change>() {
-        @Override
-        public Change update(Change change) {
-          ChangeUtil.updated(change);
-          return change;
-        }
-      });
-    db.commit();
-  } finally {
-    db.rollback();
+  try (BatchUpdate bu = batchUpdateFactory.create(
+      db, project.getNameKey(), user, TimeUtil.nowTs())) {
+    bu.addOp(change.getId(), new BatchUpdate.Op() {
+      @Override
+      public boolean updateChange(BatchUpdate.ChangeContext ctx) {
+        ctx.saveChange();
+        return true;
+      }
+    });
+    bu.execute();
   }
   [...]
 }
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 50770a9..3114814 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -25,9 +25,9 @@
 
 == Project Management
 . link:project-configuration.html[Project Configuration]
-. link:access-control.html[Access Controls]
 .. link:config-labels.html[Review Labels]
-.. link:config-project-config.html[Access Controls Configuration Format]
+.. link:config-project-config.html[Project Configuration File Format]
+. link:access-control.html[Access Controls]
 . Multi-project management
 .. link:user-submodules.html[Submodules]
 .. link:https://source.android.com/source/using-repo.html[Repo] (external)
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index e0df4ca..336c7ca 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -174,13 +174,12 @@
 
 [[suggest-group]]
 ==== Suggest Group
-The `suggest` option indicates a user-entered string that
+The `suggest` or `s` option indicates a user-entered string that
 should be auto-completed to group names.
 If this option is set and `n` is not set, then `n` defaults to 10.
 
-When using this option,
-the `project` or `p` option can be used to name the current project,
-to allow context-dependent suggestions.
+When using this option, the `project` or `p` option can be used to
+name the current project, to allow context-dependent suggestions.
 
 Not compatible with `visible-to-all`, `owned`, `user`, `match`, `q`,
 or `S`.
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 5788ec9..35e86c5 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -37,7 +37,6 @@
 
 import org.eclipse.jgit.api.TagCommand;
 import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
@@ -254,10 +253,6 @@
       return changeId;
     }
 
-    public ObjectId getCommitId() {
-      return commit;
-    }
-
     public RevCommit getCommit() {
       return commit;
     }
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 0baf527..0bd1df1 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
@@ -238,7 +238,8 @@
         .rebase();
 
     // Second change should have 2 patch sets
-    assertThat(r2.getPatchSetId().get()).isEqualTo(2);
+    ChangeInfo c2 = gApi.changes().id(r2.getChangeId()).get();
+    assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2);
 
     // ...and the committer should be correct
     ChangeInfo info = gApi.changes()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index ebca92b..46e8959 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -634,7 +634,7 @@
         Locale.US);
     String date = df.format(rev.commit.author.date);
     assertThat(res).isEqualTo(
-        String.format(PATCH, r.getCommitId().name(), date, r.getChangeId()));
+        String.format(PATCH, r.getCommit().name(), date, r.getChangeId()));
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 4dbd6ad..b7dd532 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -190,7 +190,7 @@
     r.assertOkStatus();
 
     git().push()
-        .setRefSpecs(new RefSpec(r.getCommitId().name() + ":refs/heads/master"))
+        .setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master"))
         .call();
     assertCommit(project, "refs/heads/master");
     assertThat(getSubmitter(r.getPatchSetId())).isNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 1d99bfd..85495ed 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -138,7 +138,7 @@
   public void submitToEmptyRepo() throws Exception {
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommitId());
+    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index dc1c8a8..12afff2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -52,7 +52,7 @@
     RevCommit head = getRemoteHead();
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
-    assertThat(head.getParent(1)).isEqualTo(change2.getCommitId());
+    assertThat(head.getParent(1)).isEqualTo(change2.getCommit());
   }
 
   @Test
@@ -66,14 +66,14 @@
     submit(change2.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    testRepo.reset(change.getCommitId());
+    testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 =
         createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
-    assertThat(head.getParent(1)).isEqualTo(change3.getCommitId());
+    assertThat(head.getParent(1)).isEqualTo(change3.getCommit());
   }
 
   @Test
@@ -104,7 +104,7 @@
     PushOneCommit.Result change2 = createChange();
     approve(change1.getChangeId());
     submit(change2.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change2.getCommitId());
+    assertThat(getRemoteHead().getId()).isEqualTo(change2.getCommit());
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
index 3728a51..54eca70 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
@@ -95,6 +95,6 @@
   }
 
   private String commitId(int i) {
-    return results.get(i).getCommitId().name();
+    return results.get(i).getCommit().name();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index f285070..04f926e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -89,7 +89,7 @@
     submit(change2.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    testRepo.reset(change.getCommitId());
+    testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 =
         createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
@@ -121,7 +121,7 @@
         "upload the rebased commit for review.");
 
     assertThat(getRemoteHead()).isEqualTo(oldHead);
-    assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
+    assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
     assertNoSubmitter(change2.getChangeId(), 1);
   }
 
@@ -166,7 +166,7 @@
         "upload the rebased commit for review.");
 
     assertThat(getRemoteHead()).isEqualTo(oldHead);
-    assertCurrentRevision(change3.getChangeId(), 1, change3.getCommitId());
+    assertCurrentRevision(change3.getChangeId(), 1, change3.getCommit());
     assertNoSubmitter(change3.getChangeId(), 1);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index 1380214..2ca006d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -49,7 +49,7 @@
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
-    assertThat(head.getId()).isEqualTo(change.getCommitId());
+    assertThat(head.getId()).isEqualTo(change.getCommit());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertSubmitter(change.getChangeId(), 1);
   }
@@ -65,8 +65,8 @@
     submit(id2);
 
     RevCommit head = getRemoteHead();
-    assertThat(head.getId()).isEqualTo(change2.getCommitId());
-    assertThat(head.getParent(0).getId()).isEqualTo(change.getCommitId());
+    assertThat(head.getId()).isEqualTo(change2.getCommit());
+    assertThat(head.getParent(0).getId()).isEqualTo(change.getCommit());
     assertSubmitter(change.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 1);
     assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index eb1d16b..ae47f261 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -39,7 +39,7 @@
     RevCommit head = getRemoteHead();
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
-    assertThat(head.getParent(1)).isEqualTo(change.getCommitId());
+    assertThat(head.getParent(1)).isEqualTo(change.getCommit());
     assertSubmitter(change.getChangeId(), 1);
     assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
     assertPersonEquals(serverIdent.get(), head.getCommitterIdent());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 7fcf46f..7e772ea 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -26,7 +26,7 @@
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
-    assertThat(head.getId()).isEqualTo(change.getCommitId());
+    assertThat(head.getId()).isEqualTo(change.getCommit());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertSubmitter(change.getChangeId(), 1);
     assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index 636399f..f67fec0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -49,7 +49,7 @@
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
-    assertThat(head.getId()).isEqualTo(change.getCommitId());
+    assertThat(head.getId()).isEqualTo(change.getCommit());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change.getChangeId());
     assertCurrentRevision(change.getChangeId(), 1, head);
@@ -130,7 +130,7 @@
     submit(change2.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    testRepo.reset(change.getCommitId());
+    testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 =
         createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
@@ -158,7 +158,7 @@
     submitWithConflict(change2.getChangeId(), "Merge Conflict");
     RevCommit head = getRemoteHead();
     assertThat(head).isEqualTo(oldHead);
-    assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
+    assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
     assertNoSubmitter(change2.getChangeId(), 1);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index d32be8b..54371f2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -96,8 +96,8 @@
         .to("refs/for/master");
     r.assertOkStatus();
 
-    CommitInfo info = getCommit(r.getCommitId());
-    assertThat(info.commit).isEqualTo(r.getCommitId().name());
+    CommitInfo info = getCommit(r.getCommit());
+    assertThat(info.commit).isEqualTo(r.getCommit().name());
     assertThat(info.subject).isEqualTo("test commit");
     assertThat(info.message).isEqualTo(
         "test commit\n\nChange-Id: " + r.getChangeId() + "\n");
@@ -119,7 +119,7 @@
     PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), testRepo)
         .to("refs/for/master");
     r.assertOkStatus();
-    assertNotFound(r.getCommitId());
+    assertNotFound(r.getCommit());
   }
 
   private void unblockRead() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 33cfe99..ed67d66 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -112,18 +112,18 @@
 
     TagInfo t = result.get(0);
     assertThat(t.ref).isEqualTo(Constants.R_TAGS + tag1.name);
-    assertThat(t.revision).isEqualTo(r1.getCommitId().getName());
+    assertThat(t.revision).isEqualTo(r1.getCommit().getName());
 
     t = result.get(1);
     assertThat(t.ref).isEqualTo(Constants.R_TAGS + tag2.name);
-    assertThat(t.object).isEqualTo(r2.getCommitId().getName());
+    assertThat(t.object).isEqualTo(r2.getCommit().getName());
     assertThat(t.message).isEqualTo(tag2.message);
     assertThat(t.tagger.name).isEqualTo(tag2.tagger.getName());
     assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
 
     t = result.get(2);
     assertThat(t.ref).isEqualTo(tag3Ref);
-    assertThat(t.object).isEqualTo(r2.getCommitId().getName());
+    assertThat(t.object).isEqualTo(r2.getCommit().getName());
     assertThat(t.message).isEqualTo(tag2.message);
     assertThat(t.tagger.name).isEqualTo(tag2.tagger.getName());
     assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
@@ -207,15 +207,15 @@
     List<TagInfo> result = getTags().get();
     assertThat(result).hasSize(2);
     assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
-    assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
+    assertThat(result.get(0).revision).isEqualTo(r1.getCommit().getName());
     assertThat(result.get(1).ref).isEqualTo("refs/tags/" + tag2.name);
-    assertThat(result.get(1).revision).isEqualTo(r2.getCommitId().getName());
+    assertThat(result.get(1).revision).isEqualTo(r2.getCommit().getName());
 
     blockRead("refs/heads/hidden");
     result = getTags().get();
     assertThat(result).hasSize(1);
     assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
-    assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
+    assertThat(result.get(0).revision).isEqualTo(r1.getCommit().getName());
   }
 
   @Test
@@ -232,7 +232,7 @@
 
     TagInfo tagInfo = getTag(tag1.name);
     assertThat(tagInfo.ref).isEqualTo("refs/tags/" + tag1.name);
-    assertThat(tagInfo.revision).isEqualTo(r1.getCommitId().getName());
+    assertThat(tagInfo.revision).isEqualTo(r1.getCommit().getName());
   }
 
   private void createTags() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index c16b60e..1a3b644 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -365,7 +365,7 @@
     revision(r).submit();
     try (Repository repo = repoManager.openRepository(project)) {
       assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(
-          r.getCommitId());
+          r.getCommit());
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index 88821ce..a087b22 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -55,7 +55,7 @@
   @Test
   public void zipFormat() throws Exception {
     PushOneCommit.Result r = createChange();
-    String abbreviated = r.getCommitId().abbreviate(8).name();
+    String abbreviated = r.getCommit().abbreviate(8).name();
     String c = command(r, abbreviated);
 
     InputStream out =
@@ -98,7 +98,7 @@
 
   private void archiveNotPermitted() throws Exception {
     PushOneCommit.Result r = createChange();
-    String abbreviated = r.getCommitId().abbreviate(8).name();
+    String abbreviated = r.getCommit().abbreviate(8).name();
     String c = command(r, abbreviated);
 
     InputStream out =
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index ad09027..2fce1cd 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common;
 
-import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -51,10 +50,6 @@
   public static final String MY_GROUPS = "/groups/self";
   public static final String DOCUMENTATION = "/Documentation/";
 
-  public static String toChange(final ChangeInfo c) {
-    return toChange(c.getId());
-  }
-
   public static String toChangeInEditMode(Change.Id c) {
     return "/c/" + c + ",edit/";
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
index 8614be5..e8f9fd5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
@@ -76,51 +76,4 @@
   public String getUsername() {
     return username;
   }
-
-  /**
-   * Formats an account name.
-   * <p>
-   * If the account has a full name, it returns only the full name. Otherwise it
-   * returns a longer form that includes the email address.
-   */
-  public String getName(String anonymousCowardName) {
-    if (getFullName() != null) {
-      return getFullName();
-    }
-    if (getPreferredEmail() != null) {
-      return getPreferredEmail();
-    }
-    return getNameEmail(anonymousCowardName);
-  }
-
-  /**
-   * Formats an account as a name and an email address.
-   * <p>
-   * Example output:
-   * <ul>
-   * <li>{@code A U. Thor &lt;author@example.com&gt;}: full populated</li>
-   * <li>{@code A U. Thor (12)}: missing email address</li>
-   * <li>{@code Anonymous Coward &lt;author@example.com&gt;}: missing name</li>
-   * <li>{@code Anonymous Coward (12)}: missing name and email address</li>
-   * </ul>
-   */
-  public String getNameEmail(String anonymousCowardName) {
-    String name = getFullName();
-    if (name == null) {
-      name = anonymousCowardName;
-    }
-
-    final StringBuilder b = new StringBuilder();
-    b.append(name);
-    if (getPreferredEmail() != null) {
-      b.append(" <");
-      b.append(getPreferredEmail());
-      b.append(">");
-    } else if (getId() != null) {
-      b.append(" (");
-      b.append(getId().get());
-      b.append(")");
-    }
-    return b.toString();
-  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
deleted file mode 100644
index d0f8cd3..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (C) 2008 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.common.data;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-
-import java.sql.Timestamp;
-
-public class ChangeInfo {
-  protected Change.Id id;
-  protected Change.Key key;
-  protected Account.Id owner;
-  protected String subject;
-  protected Change.Status status;
-  protected ProjectInfo project;
-  protected String branch;
-  protected String topic;
-  protected boolean starred;
-  protected Timestamp lastUpdatedOn;
-  protected PatchSet.Id patchSetId;
-  protected boolean latest;
-
-  public ChangeInfo() {
-  }
-
-  public ChangeInfo(final Change c, final PatchSet.Id patchId) {
-    set(c, patchId);
-  }
-
-  public void set(final Change c, final PatchSet.Id patchId) {
-    id = c.getId();
-    key = c.getKey();
-    owner = c.getOwner();
-    subject = c.getSubject();
-    status = c.getStatus();
-    project = new ProjectInfo(c.getProject());
-    branch = c.getDest().getShortName();
-    topic = c.getTopic();
-    lastUpdatedOn = c.getLastUpdatedOn();
-    patchSetId = patchId;
-    latest = patchSetId == null || patchSetId.equals(c.currentPatchSetId());
-  }
-
-  public ChangeInfo(final Change c) {
-    this(c, null);
-  }
-
-  public Change.Id getId() {
-    return id;
-  }
-
-  public Change.Key getKey() {
-    return key;
-  }
-
-  public Account.Id getOwner() {
-    return owner;
-  }
-
-  public String getSubject() {
-    return subject;
-  }
-
-  public Change.Status getStatus() {
-    return status;
-  }
-
-  public ProjectInfo getProject() {
-    return project;
-  }
-
-  public String getBranch() {
-    return branch;
-  }
-
-  public String getTopic() {
-    return topic;
-  }
-
-  public boolean isStarred() {
-    return starred;
-  }
-
-  public void setStarred(final boolean s) {
-    starred = s;
-  }
-
-  public PatchSet.Id getPatchSetId() {
-    return patchSetId;
-  }
-
-  public boolean isLatest() {
-    return latest;
-  }
-
-  public Timestamp getLastUpdatedOn() {
-    return lastUpdatedOn;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/IncludedInDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/IncludedInDetail.java
deleted file mode 100644
index 9365db8..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/IncludedInDetail.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2010 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.common.data;
-
-import java.util.Collections;
-import java.util.List;
-
-public class IncludedInDetail {
-  private List<String> branches;
-  private List<String> tags;
-
-  public IncludedInDetail() {
-  }
-
-  public void setBranches(final List<String> b) {
-    Collections.sort(b);
-    branches = b;
-  }
-
-  public List<String> getBranches() {
-    return branches;
-  }
-
-  public void setTags(final List<String> t) {
-    Collections.sort(t);
-    tags = t;
-  }
-
-  public List<String> getTags() {
-    return tags;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java
deleted file mode 100644
index dfef806..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2008 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.common.data;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-public class ProjectInfo {
-  protected Project.NameKey key;
-
-  protected ProjectInfo() {
-  }
-
-  public ProjectInfo(final Project.NameKey key) {
-    this.key = key;
-  }
-
-  public Project.NameKey getKey() {
-    return key;
-  }
-
-  public String getName() {
-    return key.get();
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
deleted file mode 100644
index 76785d8..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
+++ /dev/null
@@ -1,134 +0,0 @@
-// Copyright (C) 2011 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.common.data;
-
-import com.google.gerrit.reviewdb.client.Change;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Result from performing a review (comment, abandon, etc.)
- */
-public class ReviewResult {
-  protected List<Error> errors;
-  protected Change.Id changeId;
-
-  public ReviewResult() {
-    errors = new ArrayList<>();
-  }
-
-  public void addError(final Error e) {
-    errors.add(e);
-  }
-
-  public List<Error> getErrors() {
-    return errors;
-  }
-
-  public Change.Id getChangeId() {
-    return changeId;
-  }
-
-  public void setChangeId(Change.Id changeId) {
-    this.changeId = changeId;
-  }
-
-  public static class Error {
-    public static enum Type {
-      /** Not permitted to abandon this change. */
-      ABANDON_NOT_PERMITTED,
-
-      /** Not permitted to restore this change. */
-      RESTORE_NOT_PERMITTED,
-
-      /** Not permitted to submit this change. */
-      SUBMIT_NOT_PERMITTED,
-
-      /** Approvals or dependencies are lacking for submission. */
-      SUBMIT_NOT_READY,
-
-      /** Review operation invalid because change is closed. */
-      CHANGE_IS_CLOSED,
-
-      /** Review operation invalid because change is not abandoned. */
-      CHANGE_NOT_ABANDONED,
-
-      /** Not permitted to publish this draft patch set */
-      PUBLISH_NOT_PERMITTED,
-
-      /** Not permitted to delete this draft patch set */
-      DELETE_NOT_PERMITTED,
-
-      /** Review operation not permitted by rule. */
-      RULE_ERROR,
-
-      /** Review operation invalid because patch set is not a draft. */
-      NOT_A_DRAFT,
-
-      /** Error writing change to git repository */
-      GIT_ERROR,
-
-      /** The destination branch does not exist */
-      DEST_BRANCH_NOT_FOUND,
-
-      /** Not permitted to edit the topic name */
-      EDIT_TOPIC_NAME_NOT_PERMITTED,
-
-      /** Not permitted to edit the hashtags */
-      EDIT_HASHTAGS_NOT_PERMITTED
-    }
-
-    protected Type type;
-    protected String message;
-
-    protected Error() {
-    }
-
-    public Error(final Type type) {
-      this.type = type;
-      this.message = null;
-    }
-
-    public Error(final Type type, final String message) {
-      this.type = type;
-      this.message = message;
-    }
-
-    public Type getType() {
-      return type;
-    }
-
-    public String getMessage() {
-      return message;
-    }
-
-    public String getMessageOrType() {
-      if (message != null) {
-        return message;
-      }
-      return "" + type;
-    }
-
-    @Override
-    public String toString() {
-      String ret = type + "";
-      if (message != null) {
-        ret += " " + message;
-      }
-      return ret;
-    }
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
deleted file mode 100644
index 7b25a23..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2008 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.common.data;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-import java.util.List;
-
-@RpcImpl(version = Version.V2_0)
-public interface SuggestService extends RemoteJsonService {
-  void suggestAccountGroupForProject(Project.NameKey project, String query,
-      int limit, AsyncCallback<List<GroupReference>> callback);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index b9baccc..91107d3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.client.admin;
 
+import com.google.gerrit.client.groups.GroupMap;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.SuggestUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupInfo;
@@ -241,14 +241,16 @@
       // If the oracle didn't get to complete a UUID, resolve it now.
       //
       addRule.setEnabled(false);
-      SuggestUtil.SVC.suggestAccountGroupForProject(
-          projectName, ref.getName(), 1,
-          new GerritCallback<List<GroupReference>>() {
+      GroupMap.suggestAccountGroupForProject(
+          projectName.get(), ref.getName(), 1,
+          new GerritCallback<GroupMap>() {
             @Override
-            public void onSuccess(List<GroupReference> result) {
+            public void onSuccess(GroupMap result) {
               addRule.setEnabled(true);
-              if (result.size() == 1) {
-                addGroup(result.get(0));
+              if (result.values().length() == 1) {
+                addGroup(new GroupReference(
+                    result.values().get(0).getGroupUUID(),
+                    result.values().get(0).name()));
               } else {
                 groupToAdd.setFocus(true);
               }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
index f28fb86..5532285 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
@@ -38,6 +38,21 @@
     call.get(NativeMap.copyKeysIntoChildren(cb));
   }
 
+  public static void suggestAccountGroupForProject(String project, String query,
+      int limit, AsyncCallback<GroupMap> cb) {
+    RestApi call = groups();
+    if (project != null) {
+      call.addParameter("p", project);
+    }
+    if (query != null) {
+      call.addParameter("s", query);
+    }
+    if (limit > 0) {
+      call.addParameter("n", limit);
+    }
+    call.get(NativeMap.copyKeysIntoChildren(cb));
+  }
+
   public static void myOwned(AsyncCallback<GroupMap> cb) {
     myOwnedGroups().get(NativeMap.copyKeysIntoChildren(cb));
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index 5f4081b..aa76128 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.groups.GroupMap;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.user.client.ui.SuggestOracle;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 /** Suggestion Oracle for AccountGroup entities. */
@@ -34,26 +34,20 @@
 
   @Override
   public void _onRequestSuggestions(final Request req, final Callback callback) {
-    RpcStatus.hide(new Runnable() {
-      @Override
-      public void run() {
-        SuggestUtil.SVC.suggestAccountGroupForProject(
-            projectName, req.getQuery(), req.getLimit(),
-            new GerritCallback<List<GroupReference>>() {
-              @Override
-              public void onSuccess(final List<GroupReference> result) {
-                priorResults.clear();
-                final ArrayList<AccountGroupSuggestion> r =
-                    new ArrayList<>(result.size());
-                for (final GroupReference p : result) {
-                  r.add(new AccountGroupSuggestion(p));
-                  priorResults.put(p.getName(), p.getUUID());
-                }
-                callback.onSuggestionsReady(req, new Response(r));
-              }
-            });
-      }
-    });
+    GroupMap.suggestAccountGroupForProject(
+        projectName.get(), req.getQuery(), req.getLimit(),
+        new GerritCallback<GroupMap>() {
+          @Override
+          public void onSuccess(GroupMap result) {
+            priorResults.clear();
+            ArrayList<AccountGroupSuggestion> r = new ArrayList<>(result.size());
+            for (GroupInfo group : Natives.asList(result.values())) {
+              r.add(new AccountGroupSuggestion(group));
+              priorResults.put(group.name(), group.getGroupUUID());
+            }
+            callback.onSuggestionsReady(req, new Response(r));
+          }
+        });
   }
 
   public void setProject(Project.NameKey projectName) {
@@ -62,20 +56,20 @@
 
   private static class AccountGroupSuggestion implements
       SuggestOracle.Suggestion {
-    private final GroupReference info;
+    private final GroupInfo info;
 
-    AccountGroupSuggestion(final GroupReference k) {
+    AccountGroupSuggestion(final GroupInfo k) {
       info = k;
     }
 
     @Override
     public String getDisplayString() {
-      return info.getName();
+      return info.name();
     }
 
     @Override
     public String getReplacementString() {
-      return info.getName();
+      return info.name();
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index 288549f..8c80e55 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -19,24 +19,12 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gwt.user.client.ui.FlowPanel;
 
 /** Link to any user's account dashboard. */
 public class AccountLinkPanel extends FlowPanel {
-  /** Create a link after locating account details from an active cache. */
-  public static AccountLinkPanel link(AccountInfoCache cache, Account.Id id) {
-    com.google.gerrit.common.data.AccountInfo ai = cache.get(id);
-    return ai != null ? new AccountLinkPanel(ai) : null;
-  }
-
-  public AccountLinkPanel(com.google.gerrit.common.data.AccountInfo ai) {
-    this(FormatUtil.asInfo(ai));
-  }
-
   public AccountLinkPanel(UserIdentity ident) {
     this(AccountInfo.create(
         ident.getAccount().get(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
index 72c80ac..241b354 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -42,17 +41,6 @@
     psid = ps;
   }
 
-  public ChangeLink(final String text, final ChangeInfo info) {
-    super(text, getTarget(info));
-    cid = info.getId();
-    psid = info.getPatchSetId();
-  }
-
-  public static String getTarget(final ChangeInfo info) {
-    PatchSet.Id ps = info.getPatchSetId();
-    return (ps == null) ? PageLinks.toChange(info) : PageLinks.toChange(ps);
-  }
-
   @Override
   public void go() {
     Gerrit.display(getTargetHistoryToken());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestUtil.java
deleted file mode 100644
index 6e81b71..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestUtil.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2008 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.client.ui;
-
-import com.google.gerrit.common.data.SuggestService;
-import com.google.gwt.core.client.GWT;
-import com.google.gwtjsonrpc.client.JsonUtil;
-
-public class SuggestUtil {
-  public static final SuggestService SVC;
-
-  static {
-    SVC = GWT.create(SuggestService.class);
-    JsonUtil.bind(SVC, "rpc/SuggestService");
-  }
-
-  private SuggestUtil() {
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
deleted file mode 100644
index 69db233..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2008 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.httpd.rpc;
-
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.SuggestService;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.Collections;
-import java.util.List;
-
-class SuggestServiceImpl extends BaseServiceImplementation implements
-    SuggestService {
-  private final ProjectControl.Factory projectControlFactory;
-  private final GroupBackend groupBackend;
-
-  @Inject
-  SuggestServiceImpl(final Provider<ReviewDb> schema,
-      final Provider<CurrentUser> currentUser,
-      final ProjectControl.Factory projectControlFactory,
-      final GroupBackend groupBackend) {
-    super(schema, currentUser);
-    this.projectControlFactory = projectControlFactory;
-    this.groupBackend = groupBackend;
-  }
-
-  @Override
-  public void suggestAccountGroupForProject(final Project.NameKey project,
-      final String query, final int limit,
-      final AsyncCallback<List<GroupReference>> callback) {
-    run(callback, new Action<List<GroupReference>>() {
-      @Override
-      public List<GroupReference> run(final ReviewDb db) {
-        ProjectControl projectControl = null;
-        if (project != null) {
-          try {
-            projectControl = projectControlFactory.controlFor(project);
-          } catch (NoSuchProjectException e) {
-            return Collections.emptyList();
-          }
-        }
-        return suggestAccountGroup(projectControl, query, limit);
-      }
-    });
-  }
-
-  private List<GroupReference> suggestAccountGroup(
-      @Nullable final ProjectControl projectControl, final String query, final int limit) {
-    return Lists.newArrayList(Iterables.limit(
-        groupBackend.suggest(query, projectControl),
-        limit <= 0 ? 10 : Math.min(limit, 10)));
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
index 08e1582..f3577b9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/UiRpcModule.java
@@ -27,7 +27,6 @@
 
   @Override
   protected void configureServlets() {
-    rpc(SuggestServiceImpl.class);
     rpc(SystemInfoServiceImpl.class);
 
     install(new AccountModule());
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index 295239f..2bb6702 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -243,6 +243,49 @@
     preferredEmail = addr;
   }
 
+  /**
+   * Formats an account name.
+   * <p>
+   * If the account has a full name, it returns only the full name. Otherwise it
+   * returns a longer form that includes the email address.
+   */
+  public String getName(String anonymousCowardName) {
+    if (fullName != null) {
+      return fullName;
+    }
+    if (preferredEmail != null) {
+      return preferredEmail;
+    }
+    return getNameEmail(anonymousCowardName);
+  }
+
+  /**
+   * Get the name and email address.
+   * <p>
+   * Example output:
+   * <ul>
+   * <li>{@code A U. Thor &lt;author@example.com&gt;}: full populated</li>
+   * <li>{@code A U. Thor (12)}: missing email address</li>
+   * <li>{@code Anonymous Coward &lt;author@example.com&gt;}: missing name</li>
+   * <li>{@code Anonymous Coward (12)}: missing name and email address</li>
+   * </ul>
+   */
+  public String getNameEmail(String anonymousCowardName) {
+    String name = fullName != null ? fullName : anonymousCowardName;
+    StringBuilder b = new StringBuilder();
+    b.append(name);
+    if (preferredEmail != null) {
+      b.append(" <");
+      b.append(preferredEmail);
+      b.append(">");
+    } else if (accountId != null) {
+      b.append(" (");
+      b.append(accountId.get());
+      b.append(")");
+    }
+    return b.toString();
+  }
+
   /** Get the date and time the user first registered. */
   public Timestamp getRegisteredOn() {
     return registeredOn;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index bf93329..b694cd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Function;
 import com.google.common.collect.Ordering;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -87,10 +86,6 @@
     }
   }
 
-  public static void updated(Change c) {
-    c.setLastUpdatedOn(TimeUtil.nowTs());
-  }
-
   public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
       PatchSet.Id id) {
     PatchSet.Id next = nextPatchSetId(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 8d31c11..2e93ce7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -18,7 +18,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -308,12 +307,8 @@
     return validEmails;
   }
 
-  public String getName() {
-    return new AccountInfo(getAccount()).getName(anonymousCowardName);
-  }
-
   public String getNameEmail() {
-    return new AccountInfo(getAccount()).getNameEmail(anonymousCowardName);
+    return getAccount().getNameEmail(anonymousCowardName);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 4a9d263..9ac20f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index b413e81..f466fa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InvalidUserNameException.java
similarity index 95%
rename from gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/InvalidUserNameException.java
index f1c35a8..d60b7af 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InvalidUserNameException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.server.account;
 
 import com.google.gerrit.reviewdb.client.Account;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
index 9506b01..6f20e9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
index ac842e5..37d400c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.data.IncludedInDetail;
 import com.google.gerrit.extensions.config.ExternalIncludedIn;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -80,7 +79,7 @@
         throw new ResourceConflictException(err.getMessage());
       }
 
-      IncludedInDetail d = IncludedInResolver.resolve(r, rw, rev);
+      IncludedInResolver.Result d = IncludedInResolver.resolve(r, rw, rev);
       Map<String, Collection<String>> external = new HashMap<>();
       for (DynamicMap.Entry<ExternalIncludedIn> i : includedIn) {
         external.put(i.getExportName(),
@@ -96,7 +95,7 @@
     Collection<String> tags;
     Map<String, Collection<String>> external;
 
-    IncludedInInfo(IncludedInDetail in, Map<String, Collection<String>> e) {
+    IncludedInInfo(IncludedInResolver.Result in, Map<String, Collection<String>> e) {
       branches = in.getBranches();
       tags = in.getTags();
       external = e;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
index fcac76d..2c5eda4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -18,7 +18,6 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.IncludedInDetail;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -47,7 +46,7 @@
   private static final Logger log = LoggerFactory
       .getLogger(IncludedInResolver.class);
 
-  public static IncludedInDetail resolve(final Repository repo,
+  public static Result resolve(final Repository repo,
       final RevWalk rw, final RevCommit commit) throws IOException {
     RevFlag flag = newFlag(rw);
     try {
@@ -87,7 +86,7 @@
     this.containsTarget = containsTarget;
   }
 
-  private IncludedInDetail resolve() throws IOException {
+  private Result resolve() throws IOException {
     RefDatabase refDb = repo.getRefDatabase();
     Collection<Ref> tags = refDb.getRefs(Constants.R_TAGS).values();
     Collection<Ref> branches = refDb.getRefs(Constants.R_HEADS).values();
@@ -98,7 +97,7 @@
     parseCommits(allTagsAndBranches);
     Set<String> allMatchingTagsAndBranches = includedIn(tipsByCommitTime, 0);
 
-    IncludedInDetail detail = new IncludedInDetail();
+    Result detail = new Result();
     detail
         .setBranches(getMatchingRefNames(allMatchingTagsAndBranches, branches));
     detail.setTags(getMatchingRefNames(allMatchingTagsAndBranches, tags));
@@ -228,4 +227,30 @@
       }
     });
   }
+
+  public static class Result {
+    private List<String> branches;
+    private List<String> tags;
+
+    public Result() {
+    }
+
+    public void setBranches(final List<String> b) {
+      Collections.sort(b);
+      branches = b;
+    }
+
+    public List<String> getBranches() {
+      return branches;
+    }
+
+    public void setTags(final List<String> t) {
+      Collections.sort(t);
+      tags = t;
+    }
+
+    public List<String> getTags() {
+      return tags;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index 4baaa63..34bc00a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -101,34 +101,16 @@
     }
 
     ChangeData cd = changeDataFactory.create(db.get(), resource.getControl());
-    SubmitTypeRecord rec = new SubmitRuleEvaluator(cd)
-        .setPatchSet(ps)
-        .getSubmitType();
-    if (rec.status != SubmitTypeRecord.Status.OK) {
-      throw new OrmException("Submit type rule failed: " + rec);
-    }
-    result.submitType = rec.type;
+    result.submitType = getSubmitType(cd, ps);
 
     try (Repository git = gitManager.openRepository(change.getProject())) {
       ObjectId commit = toId(ps);
-      if (commit == null) {
-        result.mergeable = false;
-        return result;
-      }
-
       Ref ref = git.getRefDatabase().exactRef(change.getDest().get());
       ProjectState projectState = projectCache.get(change.getProject());
       String strategy = mergeUtilFactory.create(projectState)
           .mergeStrategyName();
-      Boolean old =
-          cache.getIfPresent(commit, ref, result.submitType, strategy);
-
-      if (old == null) {
-        result.mergeable = refresh(change, commit, ref, result.submitType,
-            strategy, git, old);
-      } else {
-        result.mergeable = old;
-      }
+      result.mergeable =
+          isMergable(git, change, commit, ref, result.submitType, strategy);
 
       if (otherBranches) {
         result.mergeableInto = new ArrayList<>();
@@ -153,6 +135,31 @@
     return result;
   }
 
+  private SubmitType getSubmitType(ChangeData cd, PatchSet patchSet)
+      throws OrmException {
+    SubmitTypeRecord rec =
+        new SubmitRuleEvaluator(cd).setPatchSet(patchSet).getSubmitType();
+    if (rec.status != SubmitTypeRecord.Status.OK) {
+      throw new OrmException("Submit type rule failed: " + rec);
+    }
+    return rec.type;
+  }
+
+  private boolean isMergable(Repository git, Change change, ObjectId commit,
+      Ref ref, SubmitType submitType, String strategy)
+          throws IOException, OrmException {
+    if (commit == null) {
+      return false;
+    }
+
+    Boolean old = cache.getIfPresent(commit, ref, submitType, strategy);
+    if (old != null) {
+      return old;
+    }
+    return refresh(change, commit, ref, submitType,
+          strategy, git, old);
+  }
+
   private static ObjectId toId(PatchSet ps) {
     try {
       return ObjectId.fromString(ps.getRevision().get());
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 16d80a5..a12bd39 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
@@ -37,11 +37,12 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.RevertedSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.RefControl;
@@ -50,8 +51,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
@@ -77,7 +76,6 @@
   private final GitRepositoryManager repoManager;
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeMessagesUtil cmUtil;
-  private final ChangeUpdate.Factory changeUpdateFactory;
   private final BatchUpdate.Factory updateFactory;
   private final Sequences seq;
   private final PatchSetUtil psUtil;
@@ -90,7 +88,6 @@
       GitRepositoryManager repoManager,
       ChangeInserter.Factory changeInserterFactory,
       ChangeMessagesUtil cmUtil,
-      ChangeUpdate.Factory changeUpdateFactory,
       BatchUpdate.Factory updateFactory,
       Sequences seq,
       PatchSetUtil psUtil,
@@ -101,7 +98,6 @@
     this.repoManager = repoManager;
     this.changeInserterFactory = changeInserterFactory;
     this.cmUtil = cmUtil;
-    this.changeUpdateFactory = changeUpdateFactory;
     this.updateFactory = updateFactory;
     this.seq = seq;
     this.psUtil = psUtil;
@@ -122,28 +118,20 @@
       throw new ResourceConflictException("change is " + status(change));
     }
 
-    Change.Id revertedChangeId;
-    try {
-      revertedChangeId = revert(req.getControl(),
-            change.currentPatchSetId(),
-            Strings.emptyToNull(input.message));
-    } catch (NoSuchChangeException e) {
-      throw new ResourceNotFoundException(e.getMessage());
-    }
+    Change.Id revertedChangeId =
+        revert(req.getControl(), Strings.emptyToNull(input.message));
     return json.create(ChangeJson.NO_OPTIONS).format(req.getProject(),
         revertedChangeId);
   }
 
-  private Change.Id revert(ChangeControl ctl, PatchSet.Id patchSetId,
-      String message) throws NoSuchChangeException, OrmException,
-          MissingObjectException, IncorrectObjectTypeException, IOException,
-          RestApiException, UpdateException {
-    Change.Id changeIdToRevert = patchSetId.getParentKey();
+  private Change.Id revert(ChangeControl ctl, String message)
+      throws OrmException, IOException, RestApiException, UpdateException {
+    Change.Id changeIdToRevert = ctl.getChange().getId();
+    PatchSet.Id patchSetId = ctl.getChange().currentPatchSetId();
     PatchSet patch = psUtil.get(db.get(), ctl.getNotes(), patchSetId);
     if (patch == null) {
-      throw new NoSuchChangeException(changeIdToRevert);
+      throw new ResourceNotFoundException(changeIdToRevert.toString());
     }
-    Change changeToRevert = db.get().changes().get(changeIdToRevert);
 
     Project.NameKey project = ctl.getProject().getNameKey();
     CurrentUser user = ctl.getUser();
@@ -169,6 +157,7 @@
       revertCommitBuilder.setAuthor(authorIdent);
       revertCommitBuilder.setCommitter(authorIdent);
 
+      Change changeToRevert = ctl.getChange();
       if (message == null) {
         message = MessageFormat.format(
             ChangeMessages.get().revertChangeDefaultMessage,
@@ -181,54 +170,31 @@
       revertCommitBuilder.setMessage(
           ChangeIdUtil.insertId(message, computedChangeId, true));
 
-      RevCommit revertCommit;
-      ChangeInserter ins;
       Change.Id changeId = new Change.Id(seq.nextChangeId());
       try (ObjectInserter oi = git.newObjectInserter()) {
         ObjectId id = oi.insert(revertCommitBuilder);
         oi.flush();
-        revertCommit = revWalk.parseCommit(id);
+        RevCommit revertCommit = revWalk.parseCommit(id);
 
-        ins = changeInserterFactory.create(
+        ChangeInserter ins = changeInserterFactory.create(
             changeId, revertCommit, ctl.getChange().getDest().get())
             .setValidatePolicy(CommitValidators.Policy.GERRIT)
             .setTopic(changeToRevert.getTopic());
-
-        ChangeMessage changeMessage = new ChangeMessage(
-            new ChangeMessage.Key(
-                patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
-                user.getAccountId(), now, patchSetId);
-        StringBuilder msgBuf = new StringBuilder();
-        msgBuf.append("Patch Set ").append(patchSetId.get()).append(": Reverted");
-        msgBuf.append("\n\n");
-        msgBuf.append("This patchset was reverted in change: ")
-              .append("I").append(computedChangeId.name());
-        changeMessage.setMessage(msgBuf.toString());
-        ChangeUpdate update = changeUpdateFactory.create(ctl, now);
-        cmUtil.addChangeMessage(db.get(), update, changeMessage);
-        update.commit();
-
         ins.setMessage("Uploaded patch set 1.");
+
         try (BatchUpdate bu = updateFactory.create(
             db.get(), project, user, now)) {
           bu.setRepository(git, revWalk, oi);
           bu.insertChange(ins);
+          bu.addOp(changeId, new SendEmailOp(ins));
+          bu.addOp(changeToRevert.getId(),
+              new PostRevertedMessageOp(computedChangeId));
           bu.execute();
         }
       }
-
-      try {
-        RevertedSender cm = revertedSenderFactory.create(project, changeId);
-        cm.setFrom(user.getAccountId());
-        cm.setChangeMessage(ins.getChangeMessage());
-        cm.send();
-      } catch (Exception err) {
-        log.error("Cannot send email for revert change " + changeId, err);
-      }
-
       return changeId;
     } catch (RepositoryNotFoundException e) {
-      throw new NoSuchChangeException(changeIdToRevert, e);
+      throw new ResourceNotFoundException(changeIdToRevert.toString(), e);
     }
   }
 
@@ -243,5 +209,55 @@
 
   private static String status(Change change) {
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
-   }
- }
+  }
+
+  private class SendEmailOp extends BatchUpdate.Op {
+    private final ChangeInserter ins;
+
+    public SendEmailOp(ChangeInserter ins) {
+      this.ins = ins;
+    }
+
+    @Override
+    public void postUpdate(Context ctx) throws Exception {
+      Change.Id changeId = ins.getChange().getId();
+      try {
+        RevertedSender cm =
+            revertedSenderFactory.create(ctx.getProject(), changeId);
+        cm.setFrom(ctx.getUser().getAccountId());
+        cm.setChangeMessage(ins.getChangeMessage());
+        cm.send();
+      } catch (Exception err) {
+        log.error("Cannot send email for revert change " + changeId, err);
+      }
+    }
+  }
+
+  private class PostRevertedMessageOp extends BatchUpdate.Op {
+    private final ObjectId computedChangeId;
+
+    public PostRevertedMessageOp(ObjectId computedChangeId) {
+      this.computedChangeId = computedChangeId;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws Exception {
+      Change change = ctx.getChange();
+      PatchSet.Id patchSetId = change.currentPatchSetId();
+      ChangeMessage changeMessage = new ChangeMessage(
+          new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(db.get())),
+          ctx.getUser().getAccountId(), ctx.getWhen(), patchSetId);
+      StringBuilder msgBuf = new StringBuilder();
+      msgBuf.append("Patch Set ").append(patchSetId.get())
+          .append(": Reverted");
+      msgBuf.append("\n\n");
+      msgBuf.append("This patchset was reverted in change: ")
+          .append("I").append(computedChangeId.name());
+      changeMessage.setMessage(msgBuf.toString());
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(patchSetId),
+          changeMessage);
+      return true;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 9936330..ed462ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -86,6 +86,7 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
+import com.google.gerrit.server.git.ReplaceOp;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
@@ -318,6 +319,7 @@
     factory(ProjectConfigValidator.Factory.class);
     factory(NotesBranchUtil.Factory.class);
     factory(SubmoduleSectionParser.Factory.class);
+    factory(ReplaceOp.Factory.class);
 
     bind(AccountManager.class);
     factory(ChangeUserName.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index eefba9d..d7b407b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -51,23 +51,20 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.git.strategy.SubmitStrategyListener;
 import com.google.gerrit.server.git.validators.MergeValidationException;
 import com.google.gerrit.server.git.validators.MergeValidators;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -308,10 +305,7 @@
     }
   }
 
-  private final ChangeControl.GenericFactory changeControlFactory;
-  private final ChangeIndexer indexer;
   private final ChangeMessagesUtil cmUtil;
-  private final ChangeUpdate.Factory changeUpdateFactory;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final GitRepositoryManager repoManager;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
@@ -343,10 +337,7 @@
   private SubmitInput submitInput;
 
   @Inject
-  MergeOp(ChangeControl.GenericFactory changeControlFactory,
-      ChangeIndexer indexer,
-      ChangeMessagesUtil cmUtil,
-      ChangeUpdate.Factory changeUpdateFactory,
+  MergeOp(ChangeMessagesUtil cmUtil,
       BatchUpdate.Factory batchUpdateFactory,
       GitRepositoryManager repoManager,
       IdentifiedUser.GenericFactory identifiedUserFactory,
@@ -356,10 +347,7 @@
       InternalChangeQuery internalChangeQuery,
       SubmitStrategyFactory submitStrategyFactory,
       Provider<SubmoduleOp> subOpProvider) {
-    this.changeControlFactory = changeControlFactory;
-    this.indexer = indexer;
     this.cmUtil = cmUtil;
-    this.changeUpdateFactory = changeUpdateFactory;
     this.batchUpdateFactory = batchUpdateFactory;
     this.repoManager = repoManager;
     this.identifiedUserFactory = identifiedUserFactory;
@@ -874,67 +862,56 @@
       } catch (NoSuchProjectException noProject) {
         logWarn("Project " + noProject.project() + " no longer exists, "
             + "abandoning open changes");
-        abandonAllOpenChanges(noProject.project());
+        abandonAllOpenChangeForDeletedProject(noProject.project());
       } catch (IOException e) {
         throw new IntegrationException("Error opening project " + project, e);
       }
     }
   }
 
-  private void abandonAllOpenChanges(Project.NameKey destProject) {
+  private void abandonAllOpenChangeForDeletedProject(
+      Project.NameKey destProject) {
     try {
       for (ChangeData cd : internalChangeQuery.byProjectOpen(destProject)) {
-        abandonOneChange(cd.change());
+        //TODO: Use InternalUser instead of change owner
+        try (BatchUpdate bu = batchUpdateFactory.create(db, destProject,
+            identifiedUserFactory.create(cd.change().getOwner()),
+            TimeUtil.nowTs())) {
+          bu.addOp(cd.getId(), new BatchUpdate.Op() {
+            @Override
+            public boolean updateChange(ChangeContext ctx) throws OrmException {
+              Change change = ctx.getChange();
+              if (!change.getStatus().isOpen()) {
+                return false;
+              }
+
+              change.setStatus(Change.Status.ABANDONED);
+
+              ChangeMessage msg = new ChangeMessage(
+                  new ChangeMessage.Key(change.getId(),
+                      ChangeUtil.messageUUID(ctx.getDb())),
+                  null, change.getLastUpdatedOn(), change.currentPatchSetId());
+              msg.setMessage("Project was deleted.");
+              cmUtil.addChangeMessage(ctx.getDb(),
+                  ctx.getUpdate(change.currentPatchSetId()), msg);
+
+              ctx.saveChange();
+              return true;
+            }
+          });
+          try {
+            bu.execute();
+          } catch (UpdateException | RestApiException e) {
+            logWarn("Cannot abandon changes for deleted project " + destProject,
+                e);
+          }
+        }
       }
-    } catch (NoSuchChangeException | IOException | OrmException e) {
+    } catch (OrmException e) {
       logWarn("Cannot abandon changes for deleted project " + destProject, e);
     }
   }
 
-  private void abandonOneChange(Change change) throws OrmException,
-      NoSuchChangeException, IOException {
-    db.changes().beginTransaction(change.getId());
-
-    //TODO(dborowitz): support InternalUser in ChangeUpdate
-    ChangeControl control = changeControlFactory.controlFor(db, change,
-        identifiedUserFactory.create(change.getOwner()));
-    // TODO(dborowitz): Convert to BatchUpdate.
-    ChangeUpdate update = changeUpdateFactory.create(control);
-    try {
-      change = db.changes().atomicUpdate(
-        change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus().isOpen()) {
-              change.setStatus(Change.Status.ABANDONED);
-              return change;
-            }
-            return null;
-          }
-        });
-
-      if (change != null) {
-        ChangeMessage msg = new ChangeMessage(
-            new ChangeMessage.Key(
-                change.getId(),
-                ChangeUtil.messageUUID(db)),
-            null,
-            change.getLastUpdatedOn(),
-            change.currentPatchSetId());
-        msg.setMessage("Project was deleted.");
-
-        //TODO(yyonas): atomic change is not propagated.
-        cmUtil.addChangeMessage(db, update, msg);
-        db.commit();
-        indexer.index(db, change);
-      }
-    } finally {
-      db.rollback();
-    }
-    update.commit();
-  }
-
   private void logDebug(String msg, Object... args) {
     if (log.isDebugEnabled()) {
       log.debug("[" + submissionId + "]" + msg, args);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java
index d6bda29..32faeac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java
@@ -39,11 +39,15 @@
 
   private static BaseRepositoryBuilder<?, ?> builder(Repository r) {
     checkNotNull(r);
-    return new BaseRepositoryBuilder<>()
+    BaseRepositoryBuilder<?, ?> builder = new BaseRepositoryBuilder<>()
         .setFS(r.getFS())
-        .setGitDir(r.getDirectory())
-        .setWorkTree(r.getWorkTree())
-        .setIndexFile(r.getIndexFile());
+        .setGitDir(r.getDirectory());
+
+    if (!r.isBare()) {
+      builder.setWorkTree(r.getWorkTree())
+          .setIndexFile(r.getIndexFile());
+    }
+    return builder;
   }
 
   private final Repository delegate;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index d9932b7..bb1940e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
@@ -81,7 +80,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalCopier;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -91,8 +89,6 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeInserter;
-import com.google.gerrit.server.change.ChangeKind;
-import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.SetHashtagsOp;
@@ -110,10 +106,8 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.MergedSender;
-import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -130,7 +124,6 @@
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
@@ -172,7 +165,6 @@
 
 import java.io.IOException;
 import java.io.StringWriter;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -292,19 +284,14 @@
   private final ReviewDb db;
   private final Sequences seq;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final ChangeData.Factory changeDataFactory;
   private final ChangeNotes.Factory notesFactory;
-  private final ChangeUpdate.Factory updateFactory;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final AccountResolver accountResolver;
   private final CmdLineParser.Factory optionParserFactory;
   private final MergedSender.Factory mergedSenderFactory;
-  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final GitReferenceUpdated gitRefUpdated;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeHooks hooks;
-  private final ApprovalsUtil approvalsUtil;
-  private final ApprovalCopier approvalCopier;
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
   private final GitRepositoryManager repoManager;
@@ -318,14 +305,13 @@
   private final ExecutorService sendEmailExecutor;
   private final ListeningExecutorService changeUpdateExector;
   private final RequestScopePropagator requestScopePropagator;
-  private final ChangeIndexer indexer;
   private final SshInfo sshInfo;
   private final AllProjectsName allProjectsName;
   private final ReceiveConfig receiveConfig;
   private final DynamicSet<ReceivePackInitializer> initializers;
-  private final ChangeKindCache changeKindCache;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final SetHashtagsOp.Factory hashtagsFactory;
+  private final ReplaceOp.Factory replaceOpFactory;
 
   private final ProjectControl projectControl;
   private final Project project;
@@ -367,18 +353,13 @@
       final Sequences seq,
       final Provider<InternalChangeQuery> queryProvider,
       final SchemaFactory<ReviewDb> schemaFactory,
-      final ChangeData.Factory changeDataFactory,
       final ChangeNotes.Factory notesFactory,
-      final ChangeUpdate.Factory updateFactory,
       final AccountResolver accountResolver,
       final CmdLineParser.Factory optionParserFactory,
       final MergedSender.Factory mergedSenderFactory,
-      final ReplacePatchSetSender.Factory replacePatchSetFactory,
       final GitReferenceUpdated gitRefUpdated,
       final PatchSetInfoFactory patchSetInfoFactory,
       final ChangeHooks hooks,
-      final ApprovalsUtil approvalsUtil,
-      final ApprovalCopier approvalCopier,
       final ChangeMessagesUtil cmUtil,
       final PatchSetUtil psUtil,
       final ProjectCache projectCache,
@@ -393,7 +374,6 @@
       @SendEmailExecutor final ExecutorService sendEmailExecutor,
       @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       final RequestScopePropagator requestScopePropagator,
-      final ChangeIndexer indexer,
       final SshInfo sshInfo,
       final AllProjectsName allProjectsName,
       ReceiveConfig receiveConfig,
@@ -405,29 +385,24 @@
       final Provider<SubmoduleOp> subOpProvider,
       final Provider<Submit> submitProvider,
       final Provider<MergeOp> mergeOpProvider,
-      final ChangeKindCache changeKindCache,
       final DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       final NotesMigration notesMigration,
       final ChangeEditUtil editUtil,
       final BatchUpdate.Factory batchUpdateFactory,
-      final SetHashtagsOp.Factory hashtagsFactory) throws IOException {
+      final SetHashtagsOp.Factory hashtagsFactory,
+      final ReplaceOp.Factory replaceOpFactory) throws IOException {
     this.user = projectControl.getUser().asIdentifiedUser();
     this.db = db;
     this.seq = seq;
     this.queryProvider = queryProvider;
-    this.changeDataFactory = changeDataFactory;
     this.notesFactory = notesFactory;
-    this.updateFactory = updateFactory;
     this.schemaFactory = schemaFactory;
     this.accountResolver = accountResolver;
     this.optionParserFactory = optionParserFactory;
     this.mergedSenderFactory = mergedSenderFactory;
-    this.replacePatchSetFactory = replacePatchSetFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.hooks = hooks;
-    this.approvalsUtil = approvalsUtil;
-    this.approvalCopier = approvalCopier;
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
     this.projectCache = projectCache;
@@ -441,14 +416,13 @@
     this.sendEmailExecutor = sendEmailExecutor;
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
-    this.indexer = indexer;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
     this.receiveConfig = receiveConfig;
     this.initializers = initializers;
-    this.changeKindCache = changeKindCache;
     this.batchUpdateFactory = batchUpdateFactory;
     this.hashtagsFactory = hashtagsFactory;
+    this.replaceOpFactory = replaceOpFactory;
 
     this.projectControl = projectControl;
     this.labelTypes = projectControl.getLabelTypes();
@@ -1157,7 +1131,7 @@
     }
   }
 
-  private static class MagicBranchInput {
+  static class MagicBranchInput {
     private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings();
 
     final ReceiveCommand cmd;
@@ -1956,7 +1930,6 @@
     final ObjectId newCommitId;
     final ReceiveCommand inputCommand;
     final boolean checkMergedInto;
-    final Timestamp createdOn;
     Change change;
     ChangeControl changeCtl;
     BiMap<RevCommit, PatchSet.Id> revisions;
@@ -1964,8 +1937,6 @@
     ReceiveCommand prev;
     ReceiveCommand cmd;
     PatchSetInfo info;
-    ChangeMessage msg;
-    String mergedIntoRef;
     boolean skip;
     private PatchSet.Id priorPatchSet;
     List<String> groups = ImmutableList.of();
@@ -1976,7 +1947,6 @@
       this.newCommitId = newCommit.copy();
       this.inputCommand = cmd;
       this.checkMergedInto = checkMergedInto;
-      createdOn = TimeUtil.nowTs();
 
       revisions = HashBiMap.create();
       for (Ref ref : refs(toChange)) {
@@ -2177,54 +2147,6 @@
       return Futures.makeChecked(future, INSERT_EXCEPTION);
     }
 
-    private ChangeMessage newChangeMessage(ReviewDb db, ChangeKind changeKind,
-        Map<String, Short> approvals)
-        throws OrmException {
-      msg =
-          new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
-              .messageUUID(db)), user.getAccountId(), createdOn, psId);
-
-      msg.setMessage(renderMessageWithApprovals(psId.get(),
-          changeKindMessage(changeKind), approvals, scanLabels(db, approvals)));
-
-      return msg;
-    }
-
-    private String changeKindMessage(ChangeKind changeKind) {
-      switch (changeKind) {
-        case MERGE_FIRST_PARENT_UPDATE:
-        case TRIVIAL_REBASE:
-        case NO_CHANGE:
-          return ": Patch Set " + priorPatchSet.get() + " was rebased";
-        case NO_CODE_CHANGE:
-          return ": Commit message was updated";
-        case REWORK:
-        default:
-          return null;
-      }
-    }
-
-    private Map<String, PatchSetApproval> scanLabels(ReviewDb db,
-        Map<String, Short> approvals)
-        throws OrmException {
-      Map<String, PatchSetApproval> current = new HashMap<>();
-      // We optimize here and only retrieve current when approvals provided
-      if (!approvals.isEmpty()) {
-        for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
-            db, changeCtl, priorPatchSet, user.getAccountId())) {
-          if (a.isSubmit()) {
-            continue;
-          }
-
-          LabelType lt = labelTypes.byLabel(a.getLabelId());
-          if (lt != null) {
-            current.put(lt.getName(), a);
-          }
-        }
-      }
-      return current;
-    }
-
     PatchSet.Id upsertEdit() {
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
@@ -2234,187 +2156,41 @@
 
     PatchSet.Id insertPatchSet(RequestState state)
         throws OrmException, IOException, RestApiException, UpdateException {
-      ReviewDb db = state.db;
-      Repository repo = state.repo;
-      final RevCommit newCommit = state.rw.parseCommit(newCommitId);
+      RevCommit newCommit = state.rw.parseCommit(newCommitId);
       state.rw.parseBody(newCommit);
-      final Account.Id me = user.getAccountId();
-      final List<FooterLine> footerLines = newCommit.getFooterLines();
-      final MailRecipients recipients = new MailRecipients();
-      final PatchSet newPatchSet;
 
-      Map<String, Short> approvals = new HashMap<>();
-      ChangeUpdate update = updateFactory.create(changeCtl, createdOn);
-      update.setSubjectForCommit("Create patch set " + psId.get());
-      update.setPatchSetId(psId);
+      RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
 
-      if (magicBranch != null) {
-        recipients.add(magicBranch.getMailRecipients());
-        approvals = magicBranch.labels;
-        Set<String> hashtags = magicBranch.hashtags;
-        ChangeNotes notes = changeCtl.getNotes().load();
-        if (!hashtags.isEmpty()) {
-          hashtags.addAll(notes.getHashtags());
-          update.setHashtags(hashtags);
-        }
-        if (magicBranch.topic != null
-            && !magicBranch.topic.equals(notes.getChange().getTopic())) {
-          update.setTopic(magicBranch.topic);
-        }
+      ReplaceOp replaceOp = replaceOpFactory.create(requestScopePropagator,
+          projectControl, checkMergedInto, priorPatchSet, priorCommit, psId,
+          newCommit, info, groups, magicBranch, rp.getPushCertificate());
+      try (BatchUpdate bu = batchUpdateFactory.create(state.db, project.getNameKey(),
+          user, TimeUtil.nowTs())) {
+        bu.setRepository(state.repo, state.rw, state.ins);
+        bu.addOp(change.getId(), replaceOp);
+        bu.execute();
       }
 
-      db.changes().beginTransaction(change.getId());
-      ChangeKind changeKind = ChangeKind.REWORK;
-      try {
-        change = db.changes().get(change.getId());
-        if (change == null || change.getStatus().isClosed()) {
-          reject(inputCommand, "change is closed");
-          return null;
-        }
-
-        List<String> newGroups = groups;
-        if (newGroups.isEmpty()) {
-          PatchSet prevPs = psUtil.current(db, update.getChangeNotes());
-          newGroups = prevPs != null
-              ? prevPs.getGroups()
-              : ImmutableList.<String> of();
-        }
-
-        boolean draft = magicBranch != null && magicBranch.draft;
-        newPatchSet = psUtil.insert(
-            db, state.rw, update, psId, newCommit, draft, newGroups,
-            rp.getPushCertificate() != null
-              ? rp.getPushCertificate().toTextWithSignature()
-              : null);
-
-        if (checkMergedInto) {
-          final Ref mergedInto = findMergedInto(change.getDest().get(), newCommit);
-          mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
-        }
-
-        recipients.add(getRecipientsFromFooters(
-            accountResolver, draft, footerLines));
-        recipients.remove(me);
-        ChangeData cd = changeDataFactory.create(db, changeCtl);
-        MailRecipients oldRecipients =
-            getRecipientsFromReviewers(cd.reviewers());
-        approvalCopier.copy(db, changeCtl, newPatchSet);
-        approvalsUtil.addReviewers(db, update, labelTypes, change, newPatchSet,
-            info, recipients.getReviewers(), oldRecipients.getAll());
-        approvalsUtil.addApprovals(db, update, labelTypes, newPatchSet,
-            changeCtl, approvals);
-        recipients.add(oldRecipients);
-
-        RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
-        changeKind = changeKindCache.getChangeKind(
-            projectControl.getProjectState(), repo, priorCommit, newCommit);
-
-        cmUtil.addChangeMessage(db, update, newChangeMessage(db, changeKind,
-            approvals));
-
-        if (mergedIntoRef == null) {
-          // Change should be new, so it can go through review again.
-          //
-          change =
-              db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-                @Override
-                public Change update(Change change) {
-                  if (change.getStatus().isClosed()) {
-                    return null;
-                  }
-
-                  if (!change.currentPatchSetId().equals(priorPatchSet)) {
-                    return change;
-                  }
-
-                  if (magicBranch != null && magicBranch.topic != null) {
-                    change.setTopic(magicBranch.topic);
-                  }
-                  if (change.getStatus() == Change.Status.DRAFT && newPatchSet.isDraft()) {
-                    // Leave in draft status.
-                  } else {
-                    change.setStatus(Change.Status.NEW);
-                  }
-                  change.setCurrentPatchSet(info);
-
-                  final List<String> idList = newCommit.getFooterLines(CHANGE_ID);
-                  if (idList.isEmpty()) {
-                    change.setKey(new Change.Key("I" + newCommit.name()));
-                  } else {
-                    change.setKey(new Change.Key(idList.get(idList.size() - 1).trim()));
-                  }
-
-                  ChangeUtil.updated(change);
-                  return change;
-                }
-              });
-          if (change == null) {
-            db.patchSets().delete(Collections.singleton(newPatchSet));
-            db.changeMessages().delete(Collections.singleton(msg));
-            reject(inputCommand, "change is closed");
-            return null;
-          }
-        }
-
-        db.commit();
-      } finally {
-        db.rollback();
+      if (replaceOp.getRejectMessage() != null) {
+        reject(inputCommand, replaceOp.getRejectMessage());
+        return null;
       }
-      update.commit();
+      groups = replaceOp.getGroups();
 
-      if (mergedIntoRef != null) {
+      //TODO(ekempin): mark changes as merged inside of ReplaceOp
+      if (replaceOp.getMergedIntoRef() != null) {
         // Change was already submitted to a branch, close it.
         //
-        markChangeMergedByPush(db, info, mergedIntoRef);
+        markChangeMergedByPush(db, info, replaceOp.getMergedIntoRef());
       }
 
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
       }
-      indexer.index(db, change);
-      if (changeKind != ChangeKind.TRIVIAL_REBASE) {
-        sendEmailExecutor.submit(requestScopePropagator.wrap(new Runnable() {
-          @Override
-          public void run() {
-            try {
-              ReplacePatchSetSender cm = replacePatchSetFactory
-                  .create(project.getNameKey(), change.getId());
-              cm.setFrom(me);
-              cm.setPatchSet(newPatchSet, info);
-              cm.setChangeMessage(msg);
-              if (magicBranch != null) {
-                cm.setNotify(magicBranch.notify);
-              }
-              cm.addReviewers(recipients.getReviewers());
-              cm.addExtraCC(recipients.getCcOnly());
-              cm.send();
-            } catch (Exception e) {
-              log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
-            }
-            if (mergedIntoRef != null) {
-              sendMergedEmail(newPatchSet, info);
-            }
-          }
 
-          @Override
-          public String toString() {
-            return "send-email newpatchset";
-          }
-        }));
-      }
-
+      PatchSet newPatchSet = replaceOp.getPatchSet();
       gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
           ObjectId.zeroId(), newCommit);
-      hooks.doPatchsetCreatedHook(change, newPatchSet, db);
-      if (mergedIntoRef != null) {
-        hooks.doChangeMergedHook(
-            change, user.getAccount(), newPatchSet, db, newCommit.getName());
-      }
-
-      if (!approvals.isEmpty()) {
-        hooks.doCommentAddedHook(change, user.getAccount(), newPatchSet,
-            null, approvals, db);
-      }
 
       if (magicBranch != null && magicBranch.submit) {
         submit(changeCtl, newPatchSet);
@@ -2549,33 +2325,6 @@
     }
   }
 
-  private Ref findMergedInto(final String first, final RevCommit commit) {
-    try {
-      final Map<String, Ref> all = repo.getRefDatabase().getRefs(ALL);
-      Ref firstRef = all.get(first);
-      if (firstRef != null && isMergedInto(commit, firstRef)) {
-        return firstRef;
-      }
-      for (Ref ref : all.values()) {
-        if (isHead(ref)) {
-          if (isMergedInto(commit, ref)) {
-            return ref;
-          }
-        }
-      }
-      return null;
-    } catch (IOException e) {
-      log.warn("Can't check for already submitted change", e);
-      return null;
-    }
-  }
-
-  private boolean isMergedInto(final RevCommit commit, final Ref ref)
-      throws IOException {
-    final RevWalk rw = rp.getRevWalk();
-    return rw.isMergedInto(commit, rw.parseCommit(ref.getObjectId()));
-  }
-
   private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) {
     if (ctl.canForgeAuthor()
         && ctl.canForgeCommitter()
@@ -2845,10 +2594,6 @@
     commandProgress.update(1);
   }
 
-  private static boolean isHead(final Ref ref) {
-    return ref.getName().startsWith(Constants.R_HEADS);
-  }
-
   private static boolean isHead(final ReceiveCommand cmd) {
     return cmd.getRefName().startsWith(Constants.R_HEADS);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
new file mode 100644
index 0000000..b38bd97
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -0,0 +1,465 @@
+// 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.server.git;
+
+import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.server.ApprovalCopier;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.change.ChangeKind;
+import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.ReceiveCommits.MagicBranchInput;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.MergedSender;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.util.LabelVote;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+public class ReplaceOp extends BatchUpdate.Op {
+  public interface Factory {
+    ReplaceOp create(
+        RequestScopePropagator requestScopePropagator,
+        ProjectControl projectControl,
+        boolean checkMergedInto,
+        @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
+        @Assisted("priorCommit") RevCommit priorCommit,
+        @Assisted("patchSetId") PatchSet.Id patchSetId,
+        @Assisted("commit") RevCommit commit,
+        PatchSetInfo info,
+        List<String> groups,
+        @Nullable MagicBranchInput magicBranch,
+        @Nullable PushCertificate pushCertificate);
+  }
+
+  private static final Logger log =
+      LoggerFactory.getLogger(ReplaceOp.class);
+
+  private static final String CHANGE_IS_CLOSED = "change is closed";
+
+  private final PatchSetUtil psUtil;
+  private final ChangeData.Factory changeDataFactory;
+  private final ChangeKindCache changeKindCache;
+  private final ChangeMessagesUtil cmUtil;
+  private final ChangeHooks hooks;
+  private final ApprovalsUtil approvalsUtil;
+  private final ApprovalCopier approvalCopier;
+  private final AccountResolver accountResolver;
+  private final ExecutorService sendEmailExecutor;
+  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+  private final MergedSender.Factory mergedSenderFactory;
+
+  private final RequestScopePropagator requestScopePropagator;
+  private final ProjectControl projectControl;
+  private final boolean checkMergedInto;
+  private final PatchSet.Id priorPatchSetId;
+  private final RevCommit priorCommit;
+  private final PatchSet.Id patchSetId;
+  private final RevCommit commit;
+  private final PatchSetInfo info;
+  private final MagicBranchInput magicBranch;
+  private final PushCertificate pushCertificate;
+  private List<String> groups = ImmutableList.of();
+
+  private final Map<String, Short> approvals = new HashMap<>();
+  private final MailRecipients recipients = new MailRecipients();
+  private Change change;
+  private PatchSet newPatchSet;
+  private ChangeKind changeKind;
+  private ChangeMessage msg;
+  private String rejectMessage;
+  private String mergedIntoRef;
+
+  @AssistedInject
+  ReplaceOp(PatchSetUtil psUtil,
+      ChangeData.Factory changeDataFactory,
+      ChangeKindCache changeKindCache,
+      ChangeMessagesUtil cmUtil,
+      ChangeHooks hooks,
+      ApprovalsUtil approvalsUtil,
+      ApprovalCopier approvalCopier,
+      AccountResolver accountResolver,
+      @SendEmailExecutor ExecutorService sendEmailExecutor,
+      ReplacePatchSetSender.Factory replacePatchSetFactory,
+      MergedSender.Factory mergedSenderFactory,
+      @Assisted RequestScopePropagator requestScopePropagator,
+      @Assisted ProjectControl projectControl,
+      @Assisted boolean checkMergedInto,
+      @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
+      @Assisted("priorCommit") RevCommit priorCommit,
+      @Assisted("patchSetId") PatchSet.Id patchSetId,
+      @Assisted("commit") RevCommit commit,
+      @Assisted PatchSetInfo info,
+      @Assisted List<String> groups,
+      @Assisted @Nullable MagicBranchInput magicBranch,
+      @Assisted @Nullable PushCertificate pushCertificate) {
+    this.psUtil = psUtil;
+    this.changeDataFactory = changeDataFactory;
+    this.changeKindCache = changeKindCache;
+    this.cmUtil = cmUtil;
+    this.hooks = hooks;
+    this.approvalsUtil = approvalsUtil;
+    this.approvalCopier = approvalCopier;
+    this.accountResolver = accountResolver;
+    this.sendEmailExecutor = sendEmailExecutor;
+    this.replacePatchSetFactory = replacePatchSetFactory;
+    this.mergedSenderFactory = mergedSenderFactory;
+
+    this.requestScopePropagator = requestScopePropagator;
+    this.projectControl = projectControl;
+    this.checkMergedInto = checkMergedInto;
+    this.priorPatchSetId = priorPatchSetId;
+    this.priorCommit = priorCommit;
+    this.patchSetId = patchSetId;
+    this.commit = commit;
+    this.info = info;
+    this.groups = groups;
+    this.magicBranch = magicBranch;
+    this.pushCertificate = pushCertificate;
+  }
+
+  @Override
+  public void updateRepo(RepoContext ctx) throws Exception {
+    changeKind = changeKindCache.getChangeKind(projectControl.getProjectState(),
+        ctx.getRepository(), priorCommit, commit);
+  }
+
+  @Override
+  public boolean updateChange(BatchUpdate.ChangeContext ctx)
+      throws OrmException, IOException {
+    change = ctx.getChange();
+    if (change == null || change.getStatus().isClosed()) {
+      rejectMessage = CHANGE_IS_CLOSED;
+      return false;
+    }
+    if (groups.isEmpty()) {
+      PatchSet prevPs = psUtil.current(ctx.getDb(), ctx.getNotes());
+      groups = prevPs != null
+          ? prevPs.getGroups()
+          : ImmutableList.<String> of();
+    }
+
+    ChangeUpdate update = ctx.getUpdate(patchSetId);
+    update.setSubjectForCommit("Create patch set " + patchSetId.get());
+
+    if (magicBranch != null) {
+      recipients.add(magicBranch.getMailRecipients());
+      approvals.putAll(magicBranch.labels);
+      Set<String> hashtags = magicBranch.hashtags;
+      if (hashtags != null && !hashtags.isEmpty()) {
+        hashtags.addAll(ctx.getNotes().getHashtags());
+        update.setHashtags(hashtags);
+      }
+      if (magicBranch.topic != null
+          && !magicBranch.topic.equals(ctx.getChange().getTopic())) {
+        update.setTopic(magicBranch.topic);
+      }
+    }
+
+    boolean draft = magicBranch != null && magicBranch.draft;
+    newPatchSet = psUtil.insert(
+        ctx.getDb(), ctx.getRevWalk(), update, patchSetId, commit, draft, groups,
+        pushCertificate != null
+          ? pushCertificate.toTextWithSignature()
+          : null);
+
+    if (checkMergedInto) {
+      Ref mergedInto = findMergedInto(ctx, change.getDest().get(), commit);
+      mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
+    }
+
+    recipients.add(getRecipientsFromFooters(
+        accountResolver, draft, commit.getFooterLines()));
+    recipients.remove(ctx.getUser().getAccountId());
+    ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getControl());
+    MailRecipients oldRecipients =
+        getRecipientsFromReviewers(cd.reviewers());
+    approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet);
+    approvalsUtil.addReviewers(ctx.getDb(), update,
+        projectControl.getLabelTypes(), change, newPatchSet, info,
+        recipients.getReviewers(), oldRecipients.getAll());
+    approvalsUtil.addApprovals(ctx.getDb(), update,
+        projectControl.getLabelTypes(), newPatchSet, ctx.getControl(),
+        approvals);
+    recipients.add(oldRecipients);
+
+    msg = new ChangeMessage(
+        new ChangeMessage.Key(change.getId(),
+            ChangeUtil.messageUUID(ctx.getDb())),
+        ctx.getUser().getAccountId(), ctx.getWhen(), patchSetId);
+    msg.setMessage(renderMessageWithApprovals(patchSetId.get(),
+        changeKindMessage(changeKind), approvals, scanLabels(ctx, approvals)));
+    cmUtil.addChangeMessage(ctx.getDb(), update, msg);
+
+    if (mergedIntoRef == null) {
+      resetChange(ctx, msg);
+    }
+    ctx.saveChange();
+
+    return true;
+  }
+
+  private String changeKindMessage(ChangeKind changeKind) {
+    switch (changeKind) {
+      case MERGE_FIRST_PARENT_UPDATE:
+      case TRIVIAL_REBASE:
+      case NO_CHANGE:
+        return ": Patch Set " + priorPatchSetId.get() + " was rebased";
+      case NO_CODE_CHANGE:
+        return ": Commit message was updated";
+      case REWORK:
+      default:
+        return null;
+    }
+  }
+
+  private static String renderMessageWithApprovals(int patchSetId,
+      String suffix, Map<String, Short> n, Map<String, PatchSetApproval> c) {
+    StringBuilder msgs = new StringBuilder("Uploaded patch set " + patchSetId);
+    if (!n.isEmpty()) {
+      boolean first = true;
+      for (Map.Entry<String, Short> e : n.entrySet()) {
+        if (c.containsKey(e.getKey())
+            && c.get(e.getKey()).getValue() == e.getValue()) {
+          continue;
+        }
+        if (first) {
+          msgs.append(":");
+          first = false;
+        }
+        msgs.append(" ")
+            .append(LabelVote.create(e.getKey(), e.getValue()).format());
+      }
+    }
+
+    if (!Strings.isNullOrEmpty(suffix)) {
+      msgs.append(suffix);
+    }
+
+    return msgs.append('.').toString();
+  }
+
+  private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx,
+      Map<String, Short> approvals) throws OrmException {
+    Map<String, PatchSetApproval> current = new HashMap<>();
+    // We optimize here and only retrieve current when approvals provided
+    if (!approvals.isEmpty()) {
+      for (PatchSetApproval a : approvalsUtil.byPatchSetUser(ctx.getDb(),
+          ctx.getControl(), priorPatchSetId,
+          ctx.getUser().getAccountId())) {
+        if (a.isSubmit()) {
+          continue;
+        }
+
+        LabelType lt = projectControl.getLabelTypes().byLabel(a.getLabelId());
+        if (lt != null) {
+          current.put(lt.getName(), a);
+        }
+      }
+    }
+    return current;
+  }
+
+  private void resetChange(ChangeContext ctx, ChangeMessage msg)
+      throws OrmException {
+    Change change = ctx.getChange();
+    if (change.getStatus().isClosed()) {
+      ctx.getDb().patchSets().delete(Collections.singleton(newPatchSet));
+      ctx.getDb().changeMessages().delete(Collections.singleton(msg));
+      rejectMessage = CHANGE_IS_CLOSED;
+      return;
+    }
+
+    if (!change.currentPatchSetId().equals(priorPatchSetId)) {
+      return;
+    }
+
+    if (magicBranch != null && magicBranch.topic != null) {
+      change.setTopic(magicBranch.topic);
+    }
+    if (change.getStatus() == Change.Status.DRAFT && newPatchSet.isDraft()) {
+      // Leave in draft status.
+    } else {
+      change.setStatus(Change.Status.NEW);
+    }
+    change.setCurrentPatchSet(info);
+
+    List<String> idList = commit.getFooterLines(CHANGE_ID);
+    if (idList.isEmpty()) {
+      change.setKey(new Change.Key("I" + commit.name()));
+    } else {
+      change.setKey(new Change.Key(idList.get(idList.size() - 1).trim()));
+    }
+  }
+
+  @Override
+  public void postUpdate(final Context ctx) throws Exception {
+    if (changeKind != ChangeKind.TRIVIAL_REBASE) {
+      Runnable sender = new Runnable() {
+        @Override
+        public void run() {
+          try {
+            ReplacePatchSetSender cm = replacePatchSetFactory.create(
+                projectControl.getProject().getNameKey(), change.getId());
+            cm.setFrom(ctx.getUser().getAccountId());
+            cm.setPatchSet(newPatchSet, info);
+            cm.setChangeMessage(msg);
+            if (magicBranch != null && magicBranch.notify != null) {
+              cm.setNotify(magicBranch.notify);
+            }
+            cm.addReviewers(recipients.getReviewers());
+            cm.addExtraCC(recipients.getCcOnly());
+            cm.send();
+          } catch (Exception e) {
+            log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+          }
+          if (mergedIntoRef != null) {
+            sendMergedEmail(ctx);
+          }
+        }
+
+        @Override
+        public String toString() {
+          return "send-email newpatchset";
+        }
+      };
+
+      if (requestScopePropagator != null) {
+        sendEmailExecutor.submit(requestScopePropagator.wrap(sender));
+      } else {
+        sender.run();
+      }
+    }
+
+    Account account = ctx.getUser().asIdentifiedUser().getAccount();
+    hooks.doPatchsetCreatedHook(change, newPatchSet, ctx.getDb());
+    if (mergedIntoRef != null) {
+      hooks.doChangeMergedHook(change, account, newPatchSet, ctx.getDb(),
+          commit.getName());
+    }
+    if (!approvals.isEmpty()) {
+      hooks.doCommentAddedHook(change, account, newPatchSet, null, approvals,
+          ctx.getDb());
+    }
+  }
+
+  private void sendMergedEmail(final Context ctx) {
+    sendEmailExecutor.submit(requestScopePropagator.wrap(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          MergedSender cm = mergedSenderFactory
+              .create(projectControl.getProject().getNameKey(), change.getId());
+          cm.setFrom(ctx.getUser().getAccountId());
+          cm.setPatchSet(newPatchSet, info);
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for submitted patch set "
+              + patchSetId, e);
+        }
+      }
+
+      @Override
+      public String toString() {
+        return "send-email merged";
+      }
+    }));
+  }
+
+  public PatchSet getPatchSet() {
+    return newPatchSet;
+  }
+
+  public List<String> getGroups() {
+    return groups;
+  }
+
+  public String getMergedIntoRef() {
+    return mergedIntoRef;
+  }
+
+  public String getRejectMessage() {
+    return rejectMessage;
+  }
+
+  private Ref findMergedInto(ChangeContext ctx, String first, RevCommit commit) {
+    try {
+      RefDatabase refDatabase = ctx.getRepository().getRefDatabase();
+
+      Ref firstRef = refDatabase.exactRef(first);
+      if (firstRef != null
+          && isMergedInto(ctx.getRevWalk(), commit, firstRef)) {
+        return firstRef;
+      }
+
+      for (Ref ref : refDatabase.getRefs(Constants.R_HEADS).values()) {
+        if (isMergedInto(ctx.getRevWalk(), commit, ref)) {
+          return ref;
+        }
+      }
+      return null;
+    } catch (IOException e) {
+      log.warn("Can't check for already submitted change", e);
+      return null;
+    }
+  }
+
+  private static boolean isMergedInto(RevWalk rw, RevCommit commit, Ref ref)
+      throws IOException {
+    return rw.isMergedInto(commit, rw.parseCommit(ref.getObjectId()));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index f3a2ea2..1b1ddbf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -127,7 +127,8 @@
     this.matchSubstring = matchSubstring;
   }
 
-  @Option(name = "--suggest", usage = "to get a suggestion of groups")
+  @Option(name = "--suggest", aliases = {"-s"},
+      usage = "to get a suggestion of groups")
   public void setSuggest(String suggest) {
     this.suggest = suggest;
   }
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 3199a47..f6f7dd5 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
@@ -16,7 +16,6 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -62,7 +61,7 @@
   public static PersonIdent newIdent(Account author, Date when,
       PersonIdent serverIdent, String anonymousCowardName) {
     return new PersonIdent(
-        new AccountInfo(author).getName(anonymousCowardName),
+        author.getName(anonymousCowardName),
         author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
         when, serverIdent.getTimeZone());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
index a35e7b9..b59386b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
@@ -26,7 +26,6 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.CommentRange;
@@ -364,7 +363,7 @@
 
   private PersonIdent newIdent(Account author, Date when) {
     return new PersonIdent(
-        new AccountInfo(author).getName(anonymousCowardName),
+        author.getName(anonymousCowardName),
         author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
         when, serverIdent.getTimeZone());
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
index 4cd31ab..3e19366 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.data.IncludedInDetail;
-
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
 import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -138,7 +136,7 @@
   @Test
   public void resolveLatestCommit() throws Exception {
     // Check tip commit
-    IncludedInDetail detail = resolve(commit_v2_5);
+    IncludedInResolver.Result detail = resolve(commit_v2_5);
 
     // Check that only tags and branches which refer the tip are returned
     expTags.add(TAG_2_5);
@@ -152,7 +150,7 @@
   @Test
   public void resolveFirstCommit() throws Exception {
     // Check first commit
-    IncludedInDetail detail = resolve(commit_initial);
+    IncludedInResolver.Result detail = resolve(commit_initial);
 
     // Check whether all tags and branches are returned
     expTags.add(TAG_1_0);
@@ -176,7 +174,7 @@
   @Test
   public void resolveBetwixtCommit() throws Exception {
     // Check a commit somewhere in the middle
-    IncludedInDetail detail = resolve(commit_v1_3);
+    IncludedInResolver.Result detail = resolve(commit_v1_3);
 
     // Check whether all succeeding tags and branches are returned
     expTags.add(TAG_1_3);
@@ -190,7 +188,7 @@
     assertEquals(expBranches, detail.getBranches());
   }
 
-  private IncludedInDetail resolve(RevCommit commit) throws Exception {
+  private IncludedInResolver.Result resolve(RevCommit commit) throws Exception {
     return IncludedInResolver.resolve(db, revWalk, commit);
   }
 
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
index 1773497..4250ce2 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
@@ -56,7 +56,8 @@
              target.tagName == 'TEXTAREA' ||
              target.tagName == 'SELECT' ||
              target.tagName == 'BUTTON' ||
-             target.tagName == 'A';
+             target.tagName == 'A' ||
+             target.tagName == 'GR-BUTTON';
     },
   };
 
diff --git a/polygerrit-ui/app/elements/gr-account-dropdown.html b/polygerrit-ui/app/elements/gr-account-dropdown.html
index 887cb0c..99850f6 100644
--- a/polygerrit-ui/app/elements/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/gr-account-dropdown.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="gr-button.html">
 
 <dom-module id="gr-account-dropdown">
   <style>
@@ -23,8 +24,7 @@
       display: inline-block;
     }
     .dropdown-trigger {
-      color: #00e;
-      cursor: pointer;
+      text-decoration: none;
     }
     .dropdown-content {
       background-color: #fff;
@@ -58,8 +58,8 @@
     }
   </style>
   <template>
-    <button class="dropdown-trigger" id="trigger"
-        on-tap="_showDropdownTapHandler">[[account.name]]</button>
+    <gr-button link class="dropdown-trigger" id="trigger"
+        on-tap="_showDropdownTapHandler">[[account.name]]</gr-button>
     <iron-dropdown id="dropdown"
         vertical-align="top"
         vertical-offset="25"
diff --git a/polygerrit-ui/app/elements/gr-button.html b/polygerrit-ui/app/elements/gr-button.html
new file mode 100644
index 0000000..2af6a5d
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-button.html
@@ -0,0 +1,147 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
+
+<dom-module id="gr-button">
+  <template strip-whitespace>
+    <style>
+      :host {
+        background-color: #fff;
+        border: 1px solid #d1d2d3;
+        border-radius: 2px;
+        box-sizing: border-box;
+        color: #333;
+        cursor: pointer;
+        display: inline-block;
+        font-family: var(--font-family);
+        font-size: 13px;
+        font-weight: bold;
+        outline-width: 0;
+        padding: .3em .65em;
+        position: relative;
+        text-align: center;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        -webkit-user-select: none;
+        user-select: none;
+      }
+      :host([hidden]) {
+        display: none;
+      }
+      :host([primary]) {
+        background-color: #4d90fe;
+        border-color: #3079ed;
+        color: #fff;
+      }
+      :host([small]) {
+        font-size: 12px;
+      }
+      :host([link]) {
+        background-color: transparent;
+        border: none;
+        color: #00f;
+        font-size: inherit;
+        font-weight: normal;
+        padding: 0;
+        text-decoration: underline;
+      }
+      :host([loading]),
+      :host([disabled]) {
+        background-color: #efefef;
+        color: #aaa;
+      }
+      :host([disabled]) {
+        cursor: default;
+        pointer-events: none;
+      }
+      :host([loading]),
+      :host([loading][disabled]) {
+        cursor: wait;
+      }
+      :host(:focus),
+      :host(:hover) {
+        border-color: #666;
+      }
+      :host(:active) {
+        border-color: #d1d2d3;
+        color: #aaa;
+      }
+      :host([primary]:focus),
+      :host([primary]:hover) {
+        border-color: #00F;
+      }
+      :host([primary]:active) {
+        border-color: #0c2188;
+        color: #fff;
+      }
+      :host([primary][loading]),
+      :host([primary][disabled]) {
+        background-color: #7caeff;
+        border-color: transparent;
+        color: #fff;
+      }
+    </style>
+    <content></content>
+  </template>
+  <script>
+  (function() {
+    'use strict';
+
+    Polymer({
+      is: 'gr-button',
+
+      properties: {
+        disabled: {
+          type: Boolean,
+          observer: '_disabledChanged',
+          reflectToAttribute: true,
+        },
+        _enabledTabindex: {
+          type: String,
+          value: '0',
+        },
+      },
+
+      behaviors: [
+        Gerrit.KeyboardShortcutBehavior,
+      ],
+
+      hostAttributes: {
+        role: 'button',
+        tabindex: '0',
+      },
+
+      _disabledChanged: function(disabled) {
+        if (disabled) {
+          this._enabledTabindex = this.getAttribute('tabindex');
+        }
+        this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex);
+      },
+
+      _handleKey: function(e) {
+        switch (e.keyCode) {
+          case 32:  // 'spacebar'
+          case 13:  // 'enter'
+            e.preventDefault();
+            this.click();
+        }
+      },
+    });
+  })();
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-actions.html b/polygerrit-ui/app/elements/gr-change-actions.html
index 997bfb22..9e9b078 100644
--- a/polygerrit-ui/app/elements/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/gr-change-actions.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../behaviors/rest-client-behavior.html">
 <link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-button.html">
 <link rel="import" href="gr-confirm-rebase-dialog.html">
 <link rel="import" href="gr-overlay.html">
 <link rel="import" href="gr-request.html">
@@ -28,39 +29,12 @@
       :host {
         display: block;
       }
-      .primary {
-        background-color: #448aff;
-        border-color: #448aff;
-        color: #fff;
-      }
-      button:before {
+      gr-button:before {
         content: attr(data-label);
       }
-      button {
-        background-color: #f1f2f3;
-        border: 1px solid #aaa;
-        border-radius: 2px;
-        cursor: pointer;
-        font: inherit;
-        padding: .5em .75em;
-      }
-      button[loading],
-      button[disabled] {
-        opacity: .75;
-      }
-      button[loading] {
-        cursor: wait;
-      }
-      button[loading]:before {
+      gr-button[loading]:before {
         content: attr(data-loading-label);
       }
-      button[disabled] {
-        cursor: default;
-      }
-      button[loading],
-      button[loading][disabled] {
-        cursor: wait;
-      }
     </style>
     <gr-ajax id="actionsXHR"
         url="[[_computeRevisionActionsPath(changeNum, patchNum)]]"
@@ -68,21 +42,21 @@
         loading="{{_loading}}"></gr-ajax>
     <div>
       <template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action">
-        <button title$="[[action.title]]"
-            class$="[[_computeButtonClass(action.__key)]]"
+        <gr-button title$="[[action.title]]"
+            primary$="[[_computePrimary(action.__key)]]"
             hidden$="[[!action.enabled]]"
             data-action-key$="[[action.__key]]"
             data-label$="[[action.label]]"
-            on-tap="_handleActionTap"></button>
+            on-tap="_handleActionTap"></gr-button>
       </template>
       <template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action">
-        <button title$="[[action.title]]"
-            class$="[[_computeButtonClass(action.__key)]]"
+        <gr-button title$="[[action.title]]"
+            primary$="[[_computePrimary(action.__key)]]"
             disabled$="[[!action.enabled]]"
             data-action-key$="[[action.__key]]"
             data-label$="[[action.label]]"
             data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
-            on-tap="_handleActionTap"></button>
+            on-tap="_handleActionTap"></gr-button>
       </template>
     </div>
     <gr-overlay id="overlay" with-backdrop>
@@ -183,6 +157,10 @@
         }[action];
       },
 
+      _computePrimary: function(actionKey) {
+        return actionKey == 'submit';
+      },
+
       _computeButtonClass: function(action) {
         return action == 'submit' ? 'primary' : '';
       },
diff --git a/polygerrit-ui/app/elements/gr-change-star.html b/polygerrit-ui/app/elements/gr-change-star.html
index b3ff00d..ad377ef 100644
--- a/polygerrit-ui/app/elements/gr-change-star.html
+++ b/polygerrit-ui/app/elements/gr-change-star.html
@@ -34,7 +34,7 @@
         outline: none;
       }
       .starButton svg {
-        fill: #aab8c2;
+        fill: #ccc;
         width: 1em;
         height: 1em;
       }
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
index f16732c..04f52b1 100644
--- a/polygerrit-ui/app/elements/gr-change-view.html
+++ b/polygerrit-ui/app/elements/gr-change-view.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../behaviors/rest-client-behavior.html">
 <link rel="import" href="gr-account-link.html">
 <link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-button.html">
 <link rel="import" href="gr-change-actions.html">
 <link rel="import" href="gr-change-star.html">
 <link rel="import" href="gr-date-formatter.html">
@@ -199,7 +200,7 @@
               permitted-labels="[[_change.permitted_labels]]"
               on-send="_handleReplySent"
               hidden$="[[!_loggedIn]]">Reply</gr-reply-dropdown>
-          <a class="download" href="#" on-tap="_handleDownloadTap">Download</a>
+          <gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button>
           <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
           <select id="patchSetSelect" on-change="_handlePatchChange">
             <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
diff --git a/polygerrit-ui/app/elements/gr-confirm-dialog.html b/polygerrit-ui/app/elements/gr-confirm-dialog.html
index beea8d2..0ba36d6 100644
--- a/polygerrit-ui/app/elements/gr-confirm-dialog.html
+++ b/polygerrit-ui/app/elements/gr-confirm-dialog.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-button.html">
 
 <dom-module id="gr-confirm-dialog">
   <template>
@@ -35,25 +36,12 @@
         display: flex;
         justify-content: space-between;
       }
-      button {
-        background-color: #f1f2f3;
-        border: 1px solid #aaa;
-        border-radius: 2px;
-        cursor: pointer;
-        font: inherit;
-        padding: .5em .75em;
-      }
-      .confirm {
-        background-color: #448aff;
-        border-color: #448aff;
-        color: #fff;
-      }
     </style>
     <div class="header"><content select=".header"></content></div>
     <div class="mainContent"><content select=".main"></content></div>
     <div class="footer">
-      <button class="confirm" on-tap="_handleConfirmTap">[[confirmLabel]]</button>
-      <button class="cancel" on-tap="_handleCancelTap">Cancel</button>
+      <gr-button primary on-tap="_handleConfirmTap">[[confirmLabel]]</gr-button>
+      <gr-button on-tap="_handleCancelTap">Cancel</gr-button>
     </div>
   </template>
   <script>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment.html b/polygerrit-ui/app/elements/gr-diff-comment.html
index 82b2ace..c544a8d 100644
--- a/polygerrit-ui/app/elements/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/gr-diff-comment.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="gr-button.html">
 <link rel="import" href="gr-date-formatter.html">
 <link rel="import" href="gr-linked-text.html">
 <link rel="import" href="gr-request.html">
@@ -70,9 +71,7 @@
         padding-top: 0;
       }
       .action {
-        cursor: pointer;
         margin-right: .5em;
-        padding: .2em .6em .3em;
       }
       .danger {
         display: flex;
@@ -137,15 +136,15 @@
           content="[[comment.message]]"
           config="[[projectConfig.commentlinks]]"></gr-linked-text>
       <div class="actions" hidden$="[[!showActions]]">
-        <button class="action reply" on-tap="_handleReply">Reply</button>
-        <button class="action quote" on-tap="_handleQuote">Quote</button>
-        <button class="action done" on-tap="_handleDone">Done</button>
-        <button class="action edit" on-tap="_handleEdit">Edit</button>
-        <button class="action save" on-tap="_handleSave"
-            disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</button>
-        <button class="action cancel" on-tap="_handleCancel" hidden>Cancel</button>
+        <gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
+        <gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button>
+        <gr-button class="action done" on-tap="_handleDone">Done</gr-button>
+        <gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button>
+        <gr-button class="action save" on-tap="_handleSave"
+            disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</gr-button>
+        <gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button>
         <div class="danger">
-          <button class="action discard" on-tap="_handleDiscard">Discard</button>
+          <gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button>
         </div>
       </div>
     </div>
diff --git a/polygerrit-ui/app/elements/gr-diff-preferences.html b/polygerrit-ui/app/elements/gr-diff-preferences.html
index 7656403..e320620 100644
--- a/polygerrit-ui/app/elements/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/gr-diff-preferences.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../bower_components/iron-input/iron-input.html">
+<link rel="import" href="gr-button.html">
 
 <dom-module id="gr-diff-preferences">
   <template>
@@ -27,7 +28,6 @@
         opacity: .5;
         pointer-events: none;
       }
-      button,
       input,
       select {
         font: inherit;
@@ -35,19 +35,6 @@
       input[type="number"] {
         width: 4em;
       }
-      button {
-        background-color: #f1f2f3;
-        border: 1px solid #aaa;
-        border-radius: 2px;
-        cursor: pointer;
-        font: inherit;
-        padding: .2em .75em;
-      }
-      button.primary {
-        background: #448aff;
-        border: none;
-        color: #fff;
-      }
       .header,
       .actions {
         padding: 1em 1.5em;
@@ -75,11 +62,7 @@
       .actions {
         border-top: 1px solid #ddd;
         display: flex;
-      }
-      .cancel {
-        display: flex;
-        flex: 1;
-        justify-content: flex-end;
+        justify-content: space-between;
       }
     </style>
     <div class="header">
@@ -119,10 +102,8 @@
       </div>
     </div>
     <div class="actions">
-      <button class="primary" on-tap="_handleSave">Save</button>
-      <div class="cancel">
-        <button on-tap="_handleCancel">Cancel</button>
-      </div>
+      <gr-button primary on-tap="_handleSave">Save</gr-button>
+      <gr-button on-tap="_handleCancel">Cancel</gr-button>
     </div>
   </template>
   <script>
diff --git a/polygerrit-ui/app/elements/gr-diff-view.html b/polygerrit-ui/app/elements/gr-diff-view.html
index 9d30c4a0..1e656ac 100644
--- a/polygerrit-ui/app/elements/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/gr-diff-view.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../behaviors/rest-client-behavior.html">
 <link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-button.html">
 <link rel="import" href="gr-diff.html">
 <link rel="import" href="gr-request.html">
 
@@ -33,6 +34,11 @@
         margin-top: 1em;
         padding: .75em var(--default-horizontal-margin);
       }
+      .reviewed {
+        display: inline-block;
+        margin: 0 .25em;
+        vertical-align: .15em;
+      }
       .jumpToFileContainer {
         display: inline-block;
       }
@@ -77,11 +83,10 @@
         background-color: #fff;
         color: #000;
       }
-      button {
-        background: none;
-        border: none;
+      gr-button {
         font: inherit;
         padding: .3em 0;
+        text-decoration: none;
       }
     </style>
     <gr-ajax id="changeDetailXHR"
@@ -100,13 +105,16 @@
     <h3>
       <a href$="[[_computeChangePath(_changeNum)]]">[[_changeNum]]</a><span>:</span>
       <span>[[_change.subject]]</span> —
-      <input id="reviewed" type="checkbox" on-change="_handleReviewedChange"
+      <input id="reviewed"
+          class="reviewed"
+          type="checkbox"
+          on-change="_handleReviewedChange"
           hidden$="[[!_loggedIn]]" hidden>
       <div class="jumpToFileContainer">
-        <button class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
+        <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
           <span>[[_path]]</span>
           <span class="downArrow">&#9660;</span>
-        </button>
+        </gr-button>
         <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
           <div class="dropdown-content">
             <template is="dom-repeat" items="[[_fileList]]" as="path">
diff --git a/polygerrit-ui/app/elements/gr-diff.html b/polygerrit-ui/app/elements/gr-diff.html
index 50dc87f..61cb1c9 100644
--- a/polygerrit-ui/app/elements/gr-diff.html
+++ b/polygerrit-ui/app/elements/gr-diff.html
@@ -17,6 +17,7 @@
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../behaviors/rest-client-behavior.html">
 <link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-button.html">
 <link rel="import" href="gr-diff-preferences.html">
 <link rel="import" href="gr-diff-side.html">
 <link rel="import" href="gr-overlay.html">
@@ -77,11 +78,11 @@
             change-num="[[changeNum]]"
             patch-range="[[patchRange]]"
             available-patches="[[availablePatches]]"></gr-patch-range-select>
-        <a class="prefsButton"
-           href="#"
+        <gr-button link
+           class="prefsButton"
            on-tap="_handlePrefsTap"
            hidden$="[[!prefs]]"
-           hidden>Diff View Preferences</a>
+           hidden>Diff View Preferences</gr-button>
       </div>
       <gr-overlay id="prefsOverlay" with-backdrop>
         <gr-diff-preferences
diff --git a/polygerrit-ui/app/elements/gr-download-dialog.html b/polygerrit-ui/app/elements/gr-download-dialog.html
index 533f265..b45c562 100644
--- a/polygerrit-ui/app/elements/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/gr-download-dialog.html
@@ -17,6 +17,7 @@
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../behaviors/rest-client-behavior.html">
+<link rel="import" href="gr-button.html">
 
 <dom-module id="gr-download-dialog">
   <template>
@@ -34,7 +35,7 @@
         margin: 0;
         padding: 0;
       }
-      li a {
+      li gr-button {
         margin-right: 1em;
       }
       label,
@@ -50,7 +51,7 @@
         margin-bottom: .5em;
         width: 60em;
       }
-      li[selected] a {
+      li[selected] gr-button {
         color: #000;
         font-weight: bold;
         text-decoration: none;
@@ -91,14 +92,14 @@
       <ul hidden$="[[!_schemes.length]]" hidden>
         <template is="dom-repeat" items="[[_schemes]]" as="scheme">
           <li selected$="[[_computeSchemeSelected(scheme, _selectedScheme)]]">
-            <a href="#" data-scheme$="[[scheme]]" on-tap="_handleSchemeTap">
+            <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap">
               [[scheme]]
-            </a>
+            </gr-button>
           </li>
         </template>
       </ul>
       <span class="closeButtonContainer">
-        <a href="#" on-tap="_handleCloseTap">Close</a>
+        <gr-button link on-tap="_handleCloseTap">Close</gr-button>
       </span>
     </div>
     <div class="commands" hidden$="[[!_schemes.length]]" hidden>
diff --git a/polygerrit-ui/app/elements/gr-message.html b/polygerrit-ui/app/elements/gr-message.html
index fe94c3c..056546f 100644
--- a/polygerrit-ui/app/elements/gr-message.html
+++ b/polygerrit-ui/app/elements/gr-message.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="gr-account-link.html">
+<link rel="import" href="gr-button.html">
 <link rel="import" href="gr-comment-list.html">
 <link rel="import" href="gr-date-formatter.html">
 <link rel="import" href="gr-linked-text.html">
@@ -91,9 +92,6 @@
       .replyContainer {
         padding: .5em 0 1em;
       }
-      .reply {
-        padding: .2em .6em .3em;
-      }
     </style>
     <div class$="[[_computeClass(expanded, showAvatar)]]">
       <gr-avatar account="[[message.author]]" image-size="100"></gr-avatar>
@@ -115,7 +113,7 @@
         </a>
       </div>
       <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
-        <button class="reply" on-tap="_handleReplyTap">Reply</button>
+        <gr-button small on-tap="_handleReplyTap">Reply</gr-button>
       </div>
     </div>
   </template>
diff --git a/polygerrit-ui/app/elements/gr-reply-dropdown.html b/polygerrit-ui/app/elements/gr-reply-dropdown.html
index 9562f7f..6f5d6b9 100644
--- a/polygerrit-ui/app/elements/gr-reply-dropdown.html
+++ b/polygerrit-ui/app/elements/gr-reply-dropdown.html
@@ -20,6 +20,7 @@
 <link rel="import" href="../bower_components/iron-selector/iron-selector.html">
 <link rel="import" href="../behaviors/rest-client-behavior.html">
 <link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-button.html">
 <link rel="import" href="gr-request.html">
 
 <dom-module id="gr-reply-dropdown">
@@ -38,14 +39,6 @@
       box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
       width: 40em;
     }
-    button {
-      background-color: #f1f2f3;
-      border: 1px solid #aaa;
-      border-radius: 2px;
-      cursor: pointer;
-      font: inherit;
-      padding: .2em .5em;
-    }
     section {
       border-top: 1px solid #ddd;
       padding: .5em .75em;
@@ -73,25 +66,18 @@
     iron-selector {
       display: inline-flex;
     }
-    iron-selector > button {
-      background-color: #fff;
-      border: 1px solid #ddd;
-      border-left: none;
-      padding: .25em 0;
-      cursor: pointer;
-      width: 3em;
-      text-align: center;
+    iron-selector > gr-button {
+      margin-right: .25em;
     }
-    iron-selector > button:first-of-type {
+    iron-selector > gr-button:first-of-type {
       border-top-left-radius: 2px;
       border-bottom-left-radius: 2px;
-      border-left: 1px solid #ddd;
     }
-    iron-selector > button:last-of-type {
+    iron-selector > gr-button:last-of-type {
       border-top-right-radius: 2px;
       border-bottom-right-radius: 2px;
     }
-    iron-selector > button.iron-selected {
+    iron-selector > gr-button.iron-selected {
       background-color: #ddd;
     }
     .draftsContainer h3 {
@@ -99,22 +85,18 @@
     }
     .actionsContainer {
       display: flex;
+      justify-content: space-between;
     }
     .action:link,
     .action:visited {
       color: #00e;
     }
-    .danger {
-      display: flex;
-      flex: 1;
-      justify-content: flex-end;
-    }
   </style>
   <template>
     <gr-ajax id="draftsXHR"
         url="[[_computeDraftsURL(changeNum)]]"
         last-response="{{_drafts}}"></gr-ajax>
-    <button id="trigger" on-tap="_showPopupTapHandler">Reply</button>
+    <gr-button id="trigger" on-tap="_showPopupTapHandler">Reply</gr-button>
     <iron-dropdown id="dropdown"
         vertical-align="top"
         vertical-offset="25"
@@ -139,7 +121,7 @@
                 <template is="dom-repeat"
                     items="[[_computePermittedLabelValues(permittedLabels, label)]]"
                     as="value">
-                  <button data-value$="[[value]]">[[value]]</button>
+                  <gr-button data-value$="[[value]]">[[value]]</gr-button>
                 </template>
               </iron-selector>
             </div>
@@ -153,10 +135,8 @@
               patch-num="[[patchNum]]"></gr-comment-list>
         </section>
         <section class="actionsContainer">
-          <button class="action send" on-tap="_sendTapHandler">Send</button>
-          <div class="danger">
-            <button class="action cancel" on-tap="_cancelTapHandler">Cancel</button>
-          </div>
+          <gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button>
+          <gr-button class="action cancel" on-tap="_cancelTapHandler">Cancel</gr-button>
         </section>
       </div>
     </iron-dropdown>
diff --git a/polygerrit-ui/app/elements/gr-reviewer-list.html b/polygerrit-ui/app/elements/gr-reviewer-list.html
index 385ffea..8cec337 100644
--- a/polygerrit-ui/app/elements/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/gr-reviewer-list.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-button.html">
 <link rel="import" href="gr-request.html">
 
 <dom-module id="gr-reviewer-list">
@@ -76,12 +77,12 @@
 
     <template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
       <div class="reviewer">
-        <gr-account-link account="[[reviewer]]"></gr-account-link>
-        <a class="remove"
-            href="#"
+        <gr-account-link account="[[reviewer]]" show-email></gr-account-link>
+        <gr-button link
+            class="remove"
             data-account-id$="[[reviewer._account_id]]"
             on-tap="_handleRemoveTap"
-            hidden$="[[_computeCannotRemoveReviewer(reviewer, mutable)]]">remove</a>
+            hidden$="[[_computeCannotRemoveReviewer(reviewer, mutable)]]">remove</gr-buttom>
       </div>
     </template>
     <div class="controlsContainer" hidden$="[[!mutable]]">
@@ -89,7 +90,7 @@
         <div class="inputContainer">
           <input is="iron-input" id="input"
               bind-value="{{_inputVal}}" disabled$="[[disabled]]">
-          <a href="#" class="cancel" on-tap="_handleCancelTap">×</a>
+          <gr-button link class="cancel" on-tap="_handleCancelTap">×</gr-button>
         </div>
         <div class="dropdown" hidden$="[[_hideAutocomplete]]">
           <template is="dom-repeat" items="[[_autocompleteData]]" as="reviewer">
@@ -109,8 +110,8 @@
           </template>
         </div>
       </div>
-      <a href="#" id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
-          hidden$="[[_showInput]]">Add reviewer</a>
+      <gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
+          hidden$="[[_showInput]]">Add reviewer</gr-button>
     </div>
   </template>
   <script>
diff --git a/polygerrit-ui/app/elements/gr-search-bar.html b/polygerrit-ui/app/elements/gr-search-bar.html
index a238054..44d1b31 100644
--- a/polygerrit-ui/app/elements/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/gr-search-bar.html
@@ -17,6 +17,7 @@
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="gr-button.html">
 
 <dom-module id="gr-search-bar">
   <template>
@@ -28,17 +29,16 @@
         display: flex;
         margin-left: 3em;
       }
-      input,
-      button {
-        border: 1px solid #aaa;
-        font: inherit;
-        padding: .2em .5em;
+      input {
+        border: 1px solid #d1d2d3;
+        outline: none;
       }
       input {
         flex: 1;
+        font: inherit;
         border-radius: 2px 0 0 2px;
       }
-      button {
+      gr-button {
         background-color: #f1f2f3;
         border-radius: 0 2px 2px 0;
         border-left-width: 0;
@@ -46,7 +46,7 @@
     </style>
     <form>
       <input is="iron-input" id="searchInput" bind-value="{{_inputVal}}">
-      <button type="submit" id="searchButton">Search</button>
+      <gr-button id="searchButton">Search</gr-button>
     </form>
   </template>
   <script>
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index c7b5c91..3f806a4 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -30,11 +30,12 @@
   '>': '&gt;',
   '"': '&quot;',
   '\'': '&#39;',
-  '/': '&#x2F;'
+  '/': '&#x2F;',
+  '`': '&#96;',
 };
 
 util.escapeHTML = function(str) {
-  return str.replace(/[&<>"'\/]/g, function(s) {
+  return str.replace(/[&<>"'`\/]/g, function(s) {
     return util.htmlEntityMap[s];
   });
 };
diff --git a/polygerrit-ui/app/test/gr-change-actions-test.html b/polygerrit-ui/app/test/gr-change-actions-test.html
index 3fc5ef1..e510e34 100644
--- a/polygerrit-ui/app/test/gr-change-actions-test.html
+++ b/polygerrit-ui/app/test/gr-change-actions-test.html
@@ -98,7 +98,7 @@
 
     test('submit and rebase buttons show', function(done) {
       element.async(function() {
-        var buttonEls = Polymer.dom(element.root).querySelectorAll('button');
+        var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button');
         assert.equal(buttonEls.length, 2);
         assert.isFalse(element.hidden);
         done();
@@ -107,7 +107,7 @@
 
     test('submit change', function(done) {
       element.async(function() {
-        var submitButton = element.$$('button[data-action-key="submit"]');
+        var submitButton = element.$$('gr-button[data-action-key="submit"]');
         assert.ok(submitButton);
         MockInteractions.tap(submitButton);
         server.respond();
@@ -121,7 +121,7 @@
 
     test('rebase change', function(done) {
       element.async(function() {
-        var rebaseButton = element.$$('button[data-action-key="rebase"]');
+        var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
         MockInteractions.tap(rebaseButton);
 
         element.$.confirmRebase.base = '1234';
diff --git a/polygerrit-ui/app/test/gr-confirm-dialog-test.html b/polygerrit-ui/app/test/gr-confirm-dialog-test.html
index b61bb17..d19118e 100644
--- a/polygerrit-ui/app/test/gr-confirm-dialog-test.html
+++ b/polygerrit-ui/app/test/gr-confirm-dialog-test.html
@@ -45,8 +45,8 @@
       element.addEventListener('confirm', handler);
       element.addEventListener('cancel', handler);
 
-      MockInteractions.tap(element.$$('.confirm'));
-      MockInteractions.tap(element.$$('.cancel'));
+      MockInteractions.tap(element.$$('gr-button[primary]'));
+      MockInteractions.tap(element.$$('gr-button:not([primary])'));
     });
 
   });
diff --git a/polygerrit-ui/app/test/gr-diff-preferences-test.html b/polygerrit-ui/app/test/gr-diff-preferences-test.html
index c689834..9256f2f 100644
--- a/polygerrit-ui/app/test/gr-diff-preferences-test.html
+++ b/polygerrit-ui/app/test/gr-diff-preferences-test.html
@@ -68,8 +68,8 @@
       Promise.all([savePromise, cancelPromise]).then(function() {
         done();
       });
-      MockInteractions.tap(element.$$('button.primary'));
-      MockInteractions.tap(element.$$('.cancel button'));
+      MockInteractions.tap(element.$$('gr-button[primary]'));
+      MockInteractions.tap(element.$$('gr-button:not([primary])'));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/test/gr-download-dialog-test.html b/polygerrit-ui/app/test/gr-download-dialog-test.html
index 0f674e5..fe26371 100644
--- a/polygerrit-ui/app/test/gr-download-dialog-test.html
+++ b/polygerrit-ui/app/test/gr-download-dialog-test.html
@@ -98,7 +98,7 @@
       element.addEventListener('close', function() {
         done();
       });
-      MockInteractions.tap(element.$$('.closeButtonContainer a'));
+      MockInteractions.tap(element.$$('.closeButtonContainer gr-button'));
     });
 
     test('tab selection', function() {
diff --git a/polygerrit-ui/app/test/gr-message-test.html b/polygerrit-ui/app/test/gr-message-test.html
index 1bc65f6..503da1b 100644
--- a/polygerrit-ui/app/test/gr-message-test.html
+++ b/polygerrit-ui/app/test/gr-message-test.html
@@ -57,7 +57,7 @@
         assert.deepEqual(e.detail.message, element.message);
         done();
       });
-      MockInteractions.tap(element.$$('button.reply'));
+      MockInteractions.tap(element.$$('.replyContainer gr-button'));
     });
 
   });
diff --git a/polygerrit-ui/app/test/gr-reply-dropdown-test.html b/polygerrit-ui/app/test/gr-reply-dropdown-test.html
index ca6e541..47fe03d 100644
--- a/polygerrit-ui/app/test/gr-reply-dropdown-test.html
+++ b/polygerrit-ui/app/test/gr-reply-dropdown-test.html
@@ -126,9 +126,11 @@
         }
         element.draft = 'I wholeheartedly disapprove';
         MockInteractions.tap(element.$$(
-            'iron-selector[data-label="Code-Review"] > button[data-value="-1"]'));
+            'iron-selector[data-label="Code-Review"] > ' +
+            'gr-button[data-value="-1"]'));
         MockInteractions.tap(element.$$(
-            'iron-selector[data-label="Verified"] > button[data-value="-1"]'));
+            'iron-selector[data-label="Verified"] > ' +
+            'gr-button[data-value="-1"]'));
 
         // This is needed on non-Blink engines most likely due to the ways in
         // which the dom-repeat elements are stamped.