Merge "Fix improper autocomplete focus behavior"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 955629c..a7e4c94 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -610,7 +610,7 @@
 See <<database.h2.cachesize,database.h2.cachesize>> for a detailed discussion.
 +
 Default is unset, using up to half of the available memory.
-
++
 H2 will persist this value in the database, so to unset explicitly specify 0.
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
@@ -1732,7 +1732,7 @@
 link:http://www.h2database.com/html/features.html#cache_settings[here]
 +
 Default is unset, using up to half of the available memory.
-
++
 H2 will persist this value in the database, so to unset explicitly specify 0.
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
@@ -2566,14 +2566,16 @@
 Maximum idle time for a connection, which roughly translates to the
 TCP socket `SO_TIMEOUT`.
 +
-The max idle time is applied:
-* When waiting for a new message to be received on a connection
-* When waiting for a new message to be sent on a connection
-+
 This value is interpreted as the maximum time between some progress
 being made on the connection. So if a single byte is read or written,
 then the timeout is reset.
 +
+The max idle time is applied:
++
+* When waiting for a new message to be received on a connection
+* When waiting for a new message to be sent on a connection
+
++
 By default, 30 seconds.
 
 [[httpd.robotsFile]]httpd.robotsFile::
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index d43c863..5f95cb3 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -38,15 +38,21 @@
 
 * Generate and publish a PGP key
 +
+A PGP key is needed to be able to sign the release artifacts before
+the upload to Maven Central, and to sign the release announcement email.
++
 Generate and publish a PGP key as described in
 link:http://central.sonatype.org/pages/working-with-pgp-signatures.html[
-Working with PGP Signatures].
+Working with PGP Signatures]. In addition to the keyserver mentioned
+there it is recommended to also publish the key to the
+link:https://keyserver.ubuntu.com/[Ubuntu key server].
 +
 Please be aware that after publishing your public key it may take a
 while until it is visible to the Sonatype server.
 +
-The PGP key is needed to be able to sign the artifacts before the
-upload to Maven Central.
+Add an entry for the public key in the
+link:https://gerrit.googlesource.com/homepage/+/md-pages/releases/public-keys.md[key list]
+on the homepage.
 +
 The PGP passphrase can be put in `~/.m2/settings.xml`:
 +
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 5e07fc7..ce89aa2 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -326,28 +326,20 @@
 [[announce]]
 ==== Announce on Mailing List
 
-* Send an email to the mailing list to announce the release, consider
-including some or all of the following in the email:
-** A link to the release and the release notes
-** A link to the docs
-** Describe the type of release (stable, bug fix, RC)
-** Hash values (SHA1, SHA256, MD5) for the release WAR file.
+Send an email, signed with the same PGP key used to sign the release
+artifacts, to the mailing list to announce the release.
+
+Consider including some or all of the following in the email:
+* A link to the release and the release notes
+* A link to the docs
+* Describe the type of release (stable, bug fix, RC)
+* Hash values (SHA1, SHA256, MD5) for the release WAR file.
 +
 The SHA1 and MD5 can be taken from the artifact page on Sonatype. The
 SHA256 can be generated with
 `openssl sha -sha256 bazel-bin/release.war` or an equivalent
 command.
 
-* Update the new discussion group announcement to be sticky
-** Go to: http://groups.google.com/group/repo-discuss/topics
-** Click on the announcement thread
-** Near the top right, click on actions
-** Under actions, click the "Display this top first" checkbox
-
-* Update the previous discussion group announcement to no longer be sticky
-** See above (unclick checkbox)
-
-
 [[increase-version]]
 === Increase Gerrit Version for Current Development
 
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 275b06a..b4f55cb 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
@@ -624,12 +624,18 @@
     revision.review(ReviewInput.approve());
     revision.submit();
 
+    // Add an approval whose score should be copied on trivial rebase
+    gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+
     String changeId = r2.getChangeId();
     // Rebase the second change
     rebase.call(changeId);
 
-    // Second change should have 2 patch sets
-    ChangeInfo c2 = gApi.changes().id(changeId).get();
+    // Second change should have 2 patch sets and an approval
+    ChangeInfo c2 =
+        gApi.changes()
+            .id(changeId)
+            .get(EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.DETAILED_LABELS));
     assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2);
 
     // ...and the committer and description should be correct
@@ -643,6 +649,20 @@
     String description = info.revisions.get(info.currentRevision).description;
     assertThat(description).isEqualTo("Rebase");
 
+    // ...and the approval was copied
+    LabelInfo cr = c2.labels.get("Code-Review");
+    assertThat(cr).isNotNull();
+    assertThat(cr.all).hasSize(1);
+    assertThat(cr.all.get(0).value).isEqualTo(1);
+
+    if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) {
+      // Ensure record was actually copied under ReviewDb
+      List<PatchSetApproval> psas =
+          db.patchSetApprovals().byPatchSet(new PatchSet.Id(new Change.Id(c2._number), 2)).toList();
+      assertThat(psas).hasSize(1);
+      assertThat(psas.get(0).getValue()).isEqualTo((short) 1);
+    }
+
     // Rebasing the second change again should fail
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("Change is already up to date");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 2c6b32f..1ece7fb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Branch;
 import org.junit.Before;
 import org.junit.Test;
@@ -92,8 +93,22 @@
     grantDelete();
     String ref = branch.getShortName();
     assertThat(ref).doesNotMatch(R_HEADS);
-    RestResponse r = userRestSession.delete("/projects/" + project.get() + "/branches/" + ref);
-    r.assertNoContent();
+    assertDeleteByRestSucceeds(ref);
+  }
+
+  @Test
+  public void deleteBranchByRestWithEncodedFullName() throws Exception {
+    grantDelete();
+    assertDeleteByRestSucceeds(Url.encode(branch.get()));
+  }
+
+  @Test
+  public void deleteBranchByRestFailsWithUnencodedFullName() throws Exception {
+    grantDelete();
+    RestResponse r =
+        userRestSession.delete("/projects/" + project.get() + "/branches/" + branch.get());
+    r.assertNotFound();
+    branch().get();
   }
 
   private void blockForcePush() throws Exception {
@@ -116,6 +131,13 @@
     return gApi.projects().name(branch.getParentKey().get()).branch(branch.get());
   }
 
+  private void assertDeleteByRestSucceeds(String ref) throws Exception {
+    RestResponse r = userRestSession.delete("/projects/" + project.get() + "/branches/" + ref);
+    r.assertNoContent();
+    exception.expect(ResourceNotFoundException.class);
+    branch().get();
+  }
+
   private void assertDeleteSucceeds() throws Exception {
     String branchRev = branch().get().revision;
     branch().delete();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index 876f85a..b88097c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -105,167 +105,170 @@
    */
   class NotImplemented implements AccountApi {
     @Override
-    public AccountInfo get() {
+    public AccountInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public boolean getActive() {
+    public boolean getActive() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setActive(boolean active) {
+    public void setActive(boolean active) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public String getAvatarUrl(int size) {
+    public String getAvatarUrl(int size) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GeneralPreferencesInfo getPreferences() {
+    public GeneralPreferencesInfo getPreferences() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) {
+    public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in)
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DiffPreferencesInfo getDiffPreferences() {
+    public DiffPreferencesInfo getDiffPreferences() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) {
+    public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public EditPreferencesInfo getEditPreferences() {
+    public EditPreferencesInfo getEditPreferences() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) {
+    public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<ProjectWatchInfo> getWatchedProjects() {
+    public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in) {
+    public List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in)
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteWatchedProjects(List<ProjectWatchInfo> in) {
+    public void deleteWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void starChange(String changeId) {
+    public void starChange(String changeId) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void unstarChange(String changeId) {
+    public void unstarChange(String changeId) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setStars(String changeId, StarsInput input) {
+    public void setStars(String changeId, StarsInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SortedSet<String> getStars(String changeId) {
+    public SortedSet<String> getStars(String changeId) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<ChangeInfo> getStarredChanges() {
+    public List<ChangeInfo> getStarredChanges() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<EmailInfo> getEmails() {
+    public List<EmailInfo> getEmails() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void addEmail(EmailInput input) {
+    public void addEmail(EmailInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteEmail(String email) {
+    public void deleteEmail(String email) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setStatus(String status) {
+    public void setStatus(String status) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<SshKeyInfo> listSshKeys() {
+    public List<SshKeyInfo> listSshKeys() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SshKeyInfo addSshKey(String key) {
+    public SshKeyInfo addSshKey(String key) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteSshKey(int seq) {
+    public void deleteSshKey(int seq) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove) {
+    public Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove)
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GpgKeyApi gpgKey(String id) {
+    public GpgKeyApi gpgKey(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, GpgKeyInfo> listGpgKeys() {
+    public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<AgreementInfo> listAgreements() {
+    public List<AgreementInfo> listAgreements() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void signAgreement(String agreementName) {
+    public void signAgreement(String agreementName) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void index() {
+    public void index() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<AccountExternalIdInfo> getExternalIds() {
+    public List<AccountExternalIdInfo> getExternalIds() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteExternalIds(List<String> externalIds) {
+    public void deleteExternalIds(List<String> externalIds) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index eab3233..e92d229 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -204,47 +204,47 @@
    */
   class NotImplemented implements Accounts {
     @Override
-    public AccountApi id(String id) {
+    public AccountApi id(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AccountApi id(int id) {
+    public AccountApi id(int id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AccountApi self() {
+    public AccountApi self() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AccountApi create(String username) {
+    public AccountApi create(String username) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AccountApi create(AccountInput input) {
+    public AccountApi create(AccountInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SuggestAccountsRequest suggestAccounts() {
+    public SuggestAccountsRequest suggestAccounts() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SuggestAccountsRequest suggestAccounts(String query) {
+    public SuggestAccountsRequest suggestAccounts(String query) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public QueryRequest query() {
+    public QueryRequest query() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public QueryRequest query(String query) {
+    public QueryRequest query(String query) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
index a8a761d..6757a05 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
@@ -29,12 +29,12 @@
    */
   class NotImplemented implements GpgKeyApi {
     @Override
-    public GpgKeyInfo get() {
+    public GpgKeyInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void delete() {
+    public void delete() throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index d52cd98..aad10a9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -289,253 +289,254 @@
     }
 
     @Override
-    public RevisionApi current() {
+    public RevisionApi current() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public RevisionApi revision(int id) {
+    public RevisionApi revision(int id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ReviewerApi reviewer(String id) {
+    public ReviewerApi reviewer(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public RevisionApi revision(String id) {
+    public RevisionApi revision(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void abandon() {
+    public void abandon() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void abandon(AbandonInput in) {
+    public void abandon(AbandonInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void restore() {
+    public void restore() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void restore(RestoreInput in) {
+    public void restore(RestoreInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void move(String destination) {
+    public void move(String destination) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void move(MoveInput in) {
+    public void move(MoveInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setPrivate(boolean value, @Nullable String message) {
+    public void setPrivate(boolean value, @Nullable String message) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setWorkInProgress(String message) {
+    public void setWorkInProgress(String message) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setReadyForReview(String message) {
+    public void setReadyForReview(String message) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi revert() {
+    public ChangeApi revert() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi revert(RevertInput in) {
+    public ChangeApi revert(RevertInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void publish() {
+    public void publish() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void rebase() {
+    public void rebase() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void rebase(RebaseInput in) {
+    public void rebase(RebaseInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void delete() {
+    public void delete() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public String topic() {
+    public String topic() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void topic(String topic) {
+    public void topic(String topic) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public IncludedInInfo includedIn() {
+    public IncludedInInfo includedIn() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AddReviewerResult addReviewer(AddReviewerInput in) {
+    public AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AddReviewerResult addReviewer(String in) {
+    public AddReviewerResult addReviewer(String in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SuggestedReviewersRequest suggestReviewers() {
+    public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SuggestedReviewersRequest suggestReviewers(String query) {
+    public SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeInfo get(EnumSet<ListChangesOption> options) {
+    public ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeInfo get() {
+    public ChangeInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeInfo info() {
+    public ChangeInfo info() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setMessage(String message) {
+    public void setMessage(String message) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public EditInfo getEdit() {
+    public EditInfo getEdit() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeEditApi edit() {
+    public ChangeEditApi edit() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setHashtags(HashtagsInput input) {
+    public void setHashtags(HashtagsInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Set<String> getHashtags() {
+    public Set<String> getHashtags() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AccountInfo setAssignee(AssigneeInput input) {
+    public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AccountInfo getAssignee() {
+    public AccountInfo getAssignee() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<AccountInfo> getPastAssignees() {
+    public List<AccountInfo> getPastAssignees() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AccountInfo deleteAssignee() {
+    public AccountInfo deleteAssignee() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, List<CommentInfo>> comments() {
+    public Map<String, List<CommentInfo>> comments() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, List<RobotCommentInfo>> robotComments() {
+    public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, List<CommentInfo>> drafts() {
+    public Map<String, List<CommentInfo>> drafts() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeInfo check() {
+    public ChangeInfo check() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeInfo check(FixInput fix) {
+    public ChangeInfo check(FixInput fix) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void index() {
+    public void index() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<ChangeInfo> submittedTogether() {
+    public List<ChangeInfo> submittedTogether() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SubmittedTogetherInfo submittedTogether(EnumSet<SubmittedTogetherOption> options) {
+    public SubmittedTogetherInfo submittedTogether(EnumSet<SubmittedTogetherOption> options)
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
     public SubmittedTogetherInfo submittedTogether(
-        EnumSet<ListChangesOption> a, EnumSet<SubmittedTogetherOption> b) {
+        EnumSet<ListChangesOption> a, EnumSet<SubmittedTogetherOption> b) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeInfo createMergePatchSet(MergePatchSetInput in) {
+    public ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void ignore(boolean ignore) {
+    public void ignore(boolean ignore) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void mute(boolean mute) {
+    public void mute(boolean mute) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
index 0a7c01d..0708ef5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -147,27 +147,27 @@
    */
   class NotImplemented implements Changes {
     @Override
-    public ChangeApi id(int id) {
+    public ChangeApi id(int id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi id(String triplet) {
+    public ChangeApi id(String triplet) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi id(String project, String branch, String id) {
+    public ChangeApi id(String project, String branch, String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi id(String project, int id) {
+    public ChangeApi id(String project, int id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi create(ChangeInput in) {
+    public ChangeApi create(ChangeInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
index 46827e5..889175e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -38,12 +38,12 @@
    */
   class NotImplemented implements CommentApi {
     @Override
-    public CommentInfo get() {
+    public CommentInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public CommentInfo delete(DeleteCommentInput input) {
+    public CommentInfo delete(DeleteCommentInput input) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
index d31e4ae..fa663a5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
@@ -29,12 +29,12 @@
    */
   class NotImplemented extends CommentApi.NotImplemented implements DraftApi {
     @Override
-    public CommentInfo update(DraftInput in) {
+    public CommentInfo update(DraftInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void delete() {
+    public void delete() throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
index a319cbe..e2bd074 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -89,27 +89,27 @@
    */
   class NotImplemented implements FileApi {
     @Override
-    public BinaryResult content() {
+    public BinaryResult content() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DiffInfo diff() {
+    public DiffInfo diff() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DiffInfo diff(String base) {
+    public DiffInfo diff(String base) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DiffInfo diff(int parent) {
+    public DiffInfo diff(int parent) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DiffRequest diffRequest() {
+    public DiffRequest diffRequest() throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
index 078f828..70e456d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
@@ -36,27 +36,27 @@
    */
   class NotImplemented implements ReviewerApi {
     @Override
-    public Map<String, Short> votes() {
+    public Map<String, Short> votes() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteVote(String label) {
+    public void deleteVote(String label) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteVote(DeleteVoteInput input) {
+    public void deleteVote(DeleteVoteInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void remove() {
+    public void remove() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void remove(DeleteReviewerInput input) {
+    public void remove(DeleteReviewerInput input) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 4672694..7a7444f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -154,87 +154,87 @@
    */
   class NotImplemented implements RevisionApi {
     @Override
-    public void delete() {
+    public void delete() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ReviewResult review(ReviewInput in) {
+    public ReviewResult review(ReviewInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void submit() {
+    public void submit() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void submit(SubmitInput in) {
+    public void submit(SubmitInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void publish() {
+    public void publish() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi cherryPick(CherryPickInput in) {
+    public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi rebase() {
+    public ChangeApi rebase() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChangeApi rebase(RebaseInput in) {
+    public ChangeApi rebase(RebaseInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public boolean canRebase() {
+    public boolean canRebase() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public RevisionReviewerApi reviewer(String id) {
+    public RevisionReviewerApi reviewer(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void setReviewed(String path, boolean reviewed) {
+    public void setReviewed(String path, boolean reviewed) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Set<String> reviewed() {
+    public Set<String> reviewed() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public MergeableInfo mergeable() {
+    public MergeableInfo mergeable() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public MergeableInfo mergeableOtherBranches() {
+    public MergeableInfo mergeableOtherBranches() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, FileInfo> files(String base) {
+    public Map<String, FileInfo> files(String base) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, FileInfo> files(int parentNum) {
+    public Map<String, FileInfo> files(int parentNum) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, FileInfo> files() {
+    public Map<String, FileInfo> files() throws RestApiException {
       throw new NotImplementedException();
     }
 
@@ -244,117 +244,117 @@
     }
 
     @Override
-    public CommitInfo commit(boolean addLinks) {
+    public CommitInfo commit(boolean addLinks) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, List<CommentInfo>> comments() {
+    public Map<String, List<CommentInfo>> comments() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, List<RobotCommentInfo>> robotComments() {
+    public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<CommentInfo> commentsAsList() {
+    public List<CommentInfo> commentsAsList() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<CommentInfo> draftsAsList() {
+    public List<CommentInfo> draftsAsList() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<RobotCommentInfo> robotCommentsAsList() {
+    public List<RobotCommentInfo> robotCommentsAsList() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public EditInfo applyFix(String fixId) {
+    public EditInfo applyFix(String fixId) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, List<CommentInfo>> drafts() {
+    public Map<String, List<CommentInfo>> drafts() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DraftApi createDraft(DraftInput in) {
+    public DraftApi createDraft(DraftInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DraftApi draft(String id) {
+    public DraftApi draft(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public CommentApi comment(String id) {
+    public CommentApi comment(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public RobotCommentApi robotComment(String id) {
+    public RobotCommentApi robotComment(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public BinaryResult patch() {
+    public BinaryResult patch() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public BinaryResult patch(String path) {
+    public BinaryResult patch(String path) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public Map<String, ActionInfo> actions() {
+    public Map<String, ActionInfo> actions() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SubmitType submitType() {
+    public SubmitType submitType() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public BinaryResult submitPreview() {
+    public BinaryResult submitPreview() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public BinaryResult submitPreview(String format) {
+    public BinaryResult submitPreview(String format) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public SubmitType testSubmitType(TestSubmitRuleInput in) {
+    public SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public MergeListRequest getMergeList() {
+    public MergeListRequest getMergeList() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void description(String description) {
+    public void description(String description) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public String description() {
+    public String description() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public String etag() {
+    public String etag() throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java
index 681ef4f..ec2d5d6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java
@@ -31,17 +31,17 @@
    */
   class NotImplemented implements RevisionReviewerApi {
     @Override
-    public Map<String, Short> votes() {
+    public Map<String, Short> votes() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteVote(String label) {
+    public void deleteVote(String label) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteVote(DeleteVoteInput input) {
+    public void deleteVote(DeleteVoteInput input) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
index 23e65ae..e44f21f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
@@ -27,7 +27,7 @@
    */
   class NotImplemented implements RobotCommentApi {
     @Override
-    public RobotCommentInfo get() {
+    public RobotCommentInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
index 2280396..de59cee 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -44,42 +44,44 @@
    */
   class NotImplemented implements Server {
     @Override
-    public String getVersion() {
+    public String getVersion() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ServerInfo getInfo() {
+    public ServerInfo getInfo() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GeneralPreferencesInfo getDefaultPreferences() {
+    public GeneralPreferencesInfo getDefaultPreferences() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in) {
+    public GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in)
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DiffPreferencesInfo getDefaultDiffPreferences() {
+    public DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) {
+    public DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in)
+        throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) {
+    public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public AccessCheckInfo checkAccess(AccessCheckInput in) {
+    public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
index 93effe2..0d4742b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -155,57 +155,57 @@
    */
   class NotImplemented implements GroupApi {
     @Override
-    public GroupInfo get() {
+    public GroupInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GroupInfo detail() {
+    public GroupInfo detail() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public String name() {
+    public String name() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void name(String name) {
+    public void name(String name) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GroupInfo owner() {
+    public GroupInfo owner() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void owner(String owner) {
+    public void owner(String owner) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public String description() {
+    public String description() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void description(String description) {
+    public void description(String description) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GroupOptionsInfo options() {
+    public GroupOptionsInfo options() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void options(GroupOptionsInfo options) {
+    public void options(GroupOptionsInfo options) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<AccountInfo> members() {
+    public List<AccountInfo> members() throws RestApiException {
       throw new NotImplementedException();
     }
 
@@ -215,27 +215,27 @@
     }
 
     @Override
-    public void addMembers(String... members) {
+    public void addMembers(String... members) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void removeMembers(String... members) {
+    public void removeMembers(String... members) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<GroupInfo> includedGroups() {
+    public List<GroupInfo> includedGroups() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void addGroups(String... groups) {
+    public void addGroups(String... groups) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void removeGroups(String... groups) {
+    public void removeGroups(String... groups) throws RestApiException {
       throw new NotImplementedException();
     }
 
@@ -245,7 +245,7 @@
     }
 
     @Override
-    public void index() {
+    public void index() throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
index c874061..a560fdf 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -271,17 +271,17 @@
    */
   class NotImplemented implements Groups {
     @Override
-    public GroupApi id(String id) {
+    public GroupApi id(String id) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GroupApi create(String name) {
+    public GroupApi create(String name) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public GroupApi create(GroupInput input) {
+    public GroupApi create(GroupInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index 42d47cb..a1f7327 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -37,27 +37,27 @@
    */
   class NotImplemented implements BranchApi {
     @Override
-    public BranchApi create(BranchInput in) {
+    public BranchApi create(BranchInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public BranchInfo get() {
+    public BranchInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void delete() {
+    public void delete() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public BinaryResult file(String path) {
+    public BinaryResult file(String path) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public List<ReflogEntryInfo> reflog() {
+    public List<ReflogEntryInfo> reflog() throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
index 88cca66..146ef27 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
@@ -29,12 +29,12 @@
    */
   class NotImplemented implements ChildProjectApi {
     @Override
-    public ProjectInfo get() {
+    public ProjectInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ProjectInfo get(boolean recursive) {
+    public ProjectInfo get(boolean recursive) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java
index 85bd952..6084962 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java
@@ -26,7 +26,7 @@
   /** A default implementation for source compatibility when adding new methods to the interface. */
   class NotImplemented implements CommitApi {
     @Override
-    public ChangeApi cherryPick(CherryPickInput input) {
+    public ChangeApi cherryPick(CherryPickInput input) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 6db13fc..1401e06 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -138,47 +138,47 @@
    */
   class NotImplemented implements ProjectApi {
     @Override
-    public ProjectApi create() {
+    public ProjectApi create() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ProjectApi create(ProjectInput in) {
+    public ProjectApi create(ProjectInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ProjectInfo get() {
+    public ProjectInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public String description() {
+    public String description() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ProjectAccessInfo access() {
+    public ProjectAccessInfo access() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ConfigInfo config() {
+    public ConfigInfo config() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ConfigInfo config(ConfigInput in) {
+    public ConfigInfo config(ConfigInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ProjectAccessInfo access(ProjectAccessInput p) {
+    public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void description(DescriptionInput in) {
+    public void description(DescriptionInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
@@ -198,37 +198,37 @@
     }
 
     @Override
-    public List<ProjectInfo> children(boolean recursive) {
+    public List<ProjectInfo> children(boolean recursive) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ChildProjectApi child(String name) {
+    public ChildProjectApi child(String name) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public BranchApi branch(String ref) {
+    public BranchApi branch(String ref) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public TagApi tag(String ref) {
+    public TagApi tag(String ref) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteBranches(DeleteBranchesInput in) {
+    public void deleteBranches(DeleteBranchesInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void deleteTags(DeleteTagsInput in) {
+    public void deleteTags(DeleteTagsInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public CommitApi commit(String commit) {
+    public CommitApi commit(String commit) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
index bb9ebc9..e4a659c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -177,17 +177,17 @@
    */
   class NotImplemented implements Projects {
     @Override
-    public ProjectApi name(String name) {
+    public ProjectApi name(String name) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ProjectApi create(ProjectInput in) {
+    public ProjectApi create(ProjectInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public ProjectApi create(String name) {
+    public ProjectApi create(String name) throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
index ad30e03..39efeac 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -30,17 +30,17 @@
    */
   class NotImplemented implements TagApi {
     @Override
-    public TagApi create(TagInput input) {
+    public TagApi create(TagInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public TagInfo get() {
+    public TagInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public void delete() {
+    public void delete() throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index e6dd1db..6771616 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Table;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Account;
@@ -27,8 +28,8 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeKindCache;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LabelNormalizer;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
@@ -41,8 +42,8 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.TreeMap;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 
 /**
@@ -53,7 +54,6 @@
  */
 @Singleton
 public class ApprovalCopier {
-  private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final ChangeKindCache changeKindCache;
   private final LabelNormalizer labelNormalizer;
@@ -62,13 +62,11 @@
 
   @Inject
   ApprovalCopier(
-      GitRepositoryManager repoManager,
       ProjectCache projectCache,
       ChangeKindCache changeKindCache,
       LabelNormalizer labelNormalizer,
       ChangeData.Factory changeDataFactory,
       PatchSetUtil psUtil) {
-    this.repoManager = repoManager;
     this.projectCache = projectCache;
     this.changeKindCache = changeKindCache;
     this.labelNormalizer = labelNormalizer;
@@ -82,10 +80,18 @@
    * @param db review database.
    * @param ctl change control for user uploading PatchSet
    * @param ps new PatchSet
+   * @param rw open walk that can read the patch set commit; null to open the repo on demand.
+   * @param repoConfig repo config used for change kind detection; null to read from repo on demand.
    * @throws OrmException
    */
-  public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps) throws OrmException {
-    copy(db, ctl, ps, Collections.<PatchSetApproval>emptyList());
+  public void copyInReviewDb(
+      ReviewDb db,
+      ChangeControl ctl,
+      PatchSet ps,
+      @Nullable RevWalk rw,
+      @Nullable Config repoConfig)
+      throws OrmException {
+    copyInReviewDb(db, ctl, ps, rw, repoConfig, Collections.emptyList());
   }
 
   /**
@@ -94,31 +100,56 @@
    * @param db review database.
    * @param ctl change control for user uploading PatchSet
    * @param ps new PatchSet
+   * @param rw open walk that can read the patch set commit; null to open the repo on demand.
+   * @param repoConfig repo config used for change kind detection; null to read from repo on demand.
    * @param dontCopy PatchSetApprovals indicating which (account, label) pairs should not be copied
    * @throws OrmException
    */
-  public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps, Iterable<PatchSetApproval> dontCopy)
+  public void copyInReviewDb(
+      ReviewDb db,
+      ChangeControl ctl,
+      PatchSet ps,
+      @Nullable RevWalk rw,
+      @Nullable Config repoConfig,
+      Iterable<PatchSetApproval> dontCopy)
       throws OrmException {
-    db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps, dontCopy));
-  }
-
-  Iterable<PatchSetApproval> getForPatchSet(ReviewDb db, ChangeControl ctl, PatchSet.Id psId)
-      throws OrmException {
-    return getForPatchSet(db, ctl, psId, Collections.<PatchSetApproval>emptyList());
+    if (PrimaryStorage.of(ctl.getChange()) == PrimaryStorage.REVIEW_DB) {
+      db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps, rw, repoConfig, dontCopy));
+    }
   }
 
   Iterable<PatchSetApproval> getForPatchSet(
-      ReviewDb db, ChangeControl ctl, PatchSet.Id psId, Iterable<PatchSetApproval> dontCopy)
+      ReviewDb db,
+      ChangeControl ctl,
+      PatchSet.Id psId,
+      @Nullable RevWalk rw,
+      @Nullable Config repoConfig)
+      throws OrmException {
+    return getForPatchSet(db, ctl, psId, rw, repoConfig, Collections.<PatchSetApproval>emptyList());
+  }
+
+  Iterable<PatchSetApproval> getForPatchSet(
+      ReviewDb db,
+      ChangeControl ctl,
+      PatchSet.Id psId,
+      @Nullable RevWalk rw,
+      @Nullable Config repoConfig,
+      Iterable<PatchSetApproval> dontCopy)
       throws OrmException {
     PatchSet ps = psUtil.get(db, ctl.getNotes(), psId);
     if (ps == null) {
       return Collections.emptyList();
     }
-    return getForPatchSet(db, ctl, ps, dontCopy);
+    return getForPatchSet(db, ctl, ps, rw, repoConfig, dontCopy);
   }
 
   private Iterable<PatchSetApproval> getForPatchSet(
-      ReviewDb db, ChangeControl ctl, PatchSet ps, Iterable<PatchSetApproval> dontCopy)
+      ReviewDb db,
+      ChangeControl ctl,
+      PatchSet ps,
+      @Nullable RevWalk rw,
+      @Nullable Config repoConfig,
+      Iterable<PatchSetApproval> dontCopy)
       throws OrmException {
     checkNotNull(ps, "ps should not be null");
     ChangeData cd = changeDataFactory.create(db, ctl);
@@ -141,41 +172,38 @@
 
       TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
 
-      try (Repository repo = repoManager.openRepository(project.getProject().getNameKey());
-          RevWalk rw = new RevWalk(repo)) {
-        // Walk patch sets strictly less than current in descending order.
-        Collection<PatchSet> allPrior =
-            patchSets.descendingMap().tailMap(ps.getId().get(), false).values();
-        for (PatchSet priorPs : allPrior) {
-          List<PatchSetApproval> priorApprovals = all.get(priorPs.getId());
-          if (priorApprovals.isEmpty()) {
+      // Walk patch sets strictly less than current in descending order.
+      Collection<PatchSet> allPrior =
+          patchSets.descendingMap().tailMap(ps.getId().get(), false).values();
+      for (PatchSet priorPs : allPrior) {
+        List<PatchSetApproval> priorApprovals = all.get(priorPs.getId());
+        if (priorApprovals.isEmpty()) {
+          continue;
+        }
+
+        ChangeKind kind =
+            changeKindCache.getChangeKind(
+                project.getProject().getNameKey(),
+                rw,
+                repoConfig,
+                ObjectId.fromString(priorPs.getRevision().get()),
+                ObjectId.fromString(ps.getRevision().get()));
+
+        for (PatchSetApproval psa : priorApprovals) {
+          if (wontCopy.contains(psa.getLabel(), psa.getAccountId())) {
             continue;
           }
-
-          ChangeKind kind =
-              changeKindCache.getChangeKind(
-                  project.getProject().getNameKey(),
-                  rw,
-                  repo.getConfig(),
-                  ObjectId.fromString(priorPs.getRevision().get()),
-                  ObjectId.fromString(ps.getRevision().get()));
-
-          for (PatchSetApproval psa : priorApprovals) {
-            if (wontCopy.contains(psa.getLabel(), psa.getAccountId())) {
-              continue;
-            }
-            if (byUser.contains(psa.getLabel(), psa.getAccountId())) {
-              continue;
-            }
-            if (!canCopy(project, psa, ps.getId(), kind)) {
-              wontCopy.put(psa.getLabel(), psa.getAccountId(), psa);
-              continue;
-            }
-            byUser.put(psa.getLabel(), psa.getAccountId(), copy(psa, ps.getId()));
+          if (byUser.contains(psa.getLabel(), psa.getAccountId())) {
+            continue;
           }
+          if (!canCopy(project, psa, ps.getId(), kind)) {
+            wontCopy.put(psa.getLabel(), psa.getAccountId(), psa);
+            continue;
+          }
+          byUser.put(psa.getLabel(), psa.getAccountId(), copy(psa, ps.getId()));
         }
-        return labelNormalizer.normalize(ctl, byUser.values()).getNormalized();
       }
+      return labelNormalizer.normalize(ctl, byUser.values()).getNormalized();
     } catch (IOException e) {
       throw new OrmException(e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index d63f36a..e4cca59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Shorts;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
@@ -60,6 +61,8 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -376,20 +379,31 @@
     return notes.load().getApprovals();
   }
 
-  public Iterable<PatchSetApproval> byPatchSet(ReviewDb db, ChangeControl ctl, PatchSet.Id psId)
+  public Iterable<PatchSetApproval> byPatchSet(
+      ReviewDb db,
+      ChangeControl ctl,
+      PatchSet.Id psId,
+      @Nullable RevWalk rw,
+      @Nullable Config repoConfig)
       throws OrmException {
     if (!migration.readChanges()) {
       return sortApprovals(db.patchSetApprovals().byPatchSet(psId));
     }
-    return copier.getForPatchSet(db, ctl, psId);
+    return copier.getForPatchSet(db, ctl, psId, rw, repoConfig);
   }
 
   public Iterable<PatchSetApproval> byPatchSetUser(
-      ReviewDb db, ChangeControl ctl, PatchSet.Id psId, Account.Id accountId) throws OrmException {
+      ReviewDb db,
+      ChangeControl ctl,
+      PatchSet.Id psId,
+      Account.Id accountId,
+      @Nullable RevWalk rw,
+      @Nullable Config repoConfig)
+      throws OrmException {
     if (!migration.readChanges()) {
       return sortApprovals(db.patchSetApprovals().byPatchSetUser(psId, accountId));
     }
-    return filterApprovals(byPatchSet(db, ctl, psId), accountId);
+    return filterApprovals(byPatchSet(db, ctl, psId, rw, repoConfig), accountId);
   }
 
   public PatchSetApproval getSubmitter(ReviewDb db, ChangeNotes notes, PatchSet.Id c) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 2a7247b..33a1565 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -1068,7 +1068,7 @@
     Map<String, Short> result = new HashMap<>();
     for (PatchSetApproval psa :
         approvalsUtil.byPatchSetUser(
-            db.get(), ctl, cd.change().currentPatchSetId(), user.getAccountId())) {
+            db.get(), ctl, cd.change().currentPatchSetId(), user.getAccountId(), null, null)) {
       result.put(psa.getLabel(), psa.getValue());
     }
     return result;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index 6dd1e2c..a09d22e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -53,6 +53,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -142,7 +143,7 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, AuthException, ResourceNotFoundException {
+        throws OrmException, AuthException, ResourceNotFoundException, IOException {
       ChangeControl ctl = ctx.getControl();
       change = ctl.getChange();
       PatchSet.Id psId = change.currentPatchSetId();
@@ -151,7 +152,9 @@
       boolean found = false;
       LabelTypes labelTypes = ctx.getControl().getLabelTypes();
 
-      for (PatchSetApproval a : approvalsUtil.byPatchSetUser(ctx.getDb(), ctl, psId, accountId)) {
+      for (PatchSetApproval a :
+          approvalsUtil.byPatchSetUser(
+              ctx.getDb(), ctl, psId, accountId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
         if (labelTypes.byLabel(a.getLabelId()) == null) {
           continue; // Ignore undefined labels.
         } else if (!a.getLabel().equals(label)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index a5fe978..6463bed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -276,7 +276,8 @@
     }
     change.setCurrentPatchSet(patchSetInfo);
     if (copyApprovals) {
-      approvalCopier.copy(db, ctl, patchSet);
+      approvalCopier.copyInReviewDb(
+          db, ctl, patchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig());
     }
     if (changeMessage != null) {
       cmUtil.addChangeMessage(db, update, changeMessage);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index f5a1856..b11db15 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -789,7 +789,7 @@
 
     @Override
     public boolean updateChange(ChangeContext ctx)
-        throws OrmException, ResourceConflictException, UnprocessableEntityException {
+        throws OrmException, ResourceConflictException, UnprocessableEntityException, IOException {
       user = ctx.getIdentifiedUser();
       notes = ctx.getNotes();
       ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
@@ -1062,7 +1062,8 @@
       return false;
     }
 
-    private boolean updateLabels(ChangeContext ctx) throws OrmException, ResourceConflictException {
+    private boolean updateLabels(ChangeContext ctx)
+        throws OrmException, ResourceConflictException, IOException {
       Map<String, Short> inLabels =
           MoreObjects.firstNonNull(in.labels, Collections.<String, Short>emptyMap());
 
@@ -1242,12 +1243,18 @@
     }
 
     private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx, List<PatchSetApproval> del)
-        throws OrmException {
+        throws OrmException, IOException {
       LabelTypes labelTypes = ctx.getControl().getLabelTypes();
       Map<String, PatchSetApproval> current = new HashMap<>();
 
       for (PatchSetApproval a :
-          approvalsUtil.byPatchSetUser(ctx.getDb(), ctx.getControl(), psId, user.getAccountId())) {
+          approvalsUtil.byPatchSetUser(
+              ctx.getDb(),
+              ctx.getControl(),
+              psId,
+              user.getAccountId(),
+              ctx.getRevWalk(),
+              ctx.getRepoView().getConfig())) {
         if (a.isLegacySubmit()) {
           continue;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index 3c0896d..1473472 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -103,7 +103,8 @@
         out,
         perm,
         cd,
-        approvalsUtil.byPatchSetUser(db.get(), ctl, psId, new Account.Id(out._accountId)));
+        approvalsUtil.byPatchSetUser(
+            db.get(), ctl, psId, new Account.Id(out._accountId), null, null));
   }
 
   public ReviewerInfo format(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
index b2ca405..ddf48fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
@@ -86,7 +86,9 @@
               db.get(),
               rsrc.getControl(),
               rsrc.getChange().currentPatchSetId(),
-              rsrc.getReviewerUser().getAccountId());
+              rsrc.getReviewerUser().getAccountId(),
+              null,
+              null);
       for (PatchSetApproval psa : byPatchSetUser) {
         votes.put(psa.getLabel(), psa.getValue());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index f0ec192..8026cae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -450,7 +450,7 @@
 
   private Iterable<PatchSetApproval> safeGetApprovals(ChangeControl ctl, PatchSet.Id psId) {
     try {
-      return approvalsUtil.byPatchSet(db.get(), ctl, psId);
+      return approvalsUtil.byPatchSet(db.get(), ctl, psId, null, null);
     } catch (OrmException e) {
       log.error("Can't read approval records for " + psId, e);
       return Collections.emptyList();
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
index 71c2ab1..50044bd 100644
--- 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
@@ -303,7 +303,13 @@
             newPatchSet,
             ctx.getControl(),
             approvals);
-    approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet, newApprovals);
+    approvalCopier.copyInReviewDb(
+        ctx.getDb(),
+        ctx.getControl(),
+        newPatchSet,
+        ctx.getRevWalk(),
+        ctx.getRepoView().getConfig(),
+        newApprovals);
     approvalsUtil.addReviewers(
         ctx.getDb(),
         update,
@@ -336,7 +342,7 @@
   }
 
   private ChangeMessage createChangeMessage(ChangeContext ctx, String reviewMessage)
-      throws OrmException {
+      throws OrmException, IOException {
     String approvalMessage =
         ApprovalsUtil.renderMessageWithApprovals(
             patchSetId.get(), approvals, scanLabels(ctx, approvals));
@@ -379,13 +385,18 @@
   }
 
   private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx, Map<String, Short> approvals)
-      throws OrmException {
+      throws OrmException, IOException {
     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.getAccountId())) {
+              ctx.getDb(),
+              ctx.getControl(),
+              priorPatchSetId,
+              ctx.getAccountId(),
+              ctx.getRevWalk(),
+              ctx.getRepoView().getConfig())) {
         if (a.isLegacySubmit()) {
           continue;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index aaad040..a3163c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -329,7 +329,8 @@
         null);
   }
 
-  private void setApproval(ChangeContext ctx, IdentifiedUser user) throws OrmException {
+  private void setApproval(ChangeContext ctx, IdentifiedUser user)
+      throws OrmException, IOException {
     Change.Id id = ctx.getChange().getId();
     List<SubmitRecord> records = args.commitStatus.getSubmitRecords(id);
     PatchSet.Id oldPsId = toMerge.getPatchsetId();
@@ -351,11 +352,12 @@
   }
 
   private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
-      throws OrmException {
+      throws OrmException, IOException {
     PatchSet.Id psId = update.getPatchSetId();
     Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
     for (PatchSetApproval psa :
-        args.approvalsUtil.byPatchSet(ctx.getDb(), ctx.getControl(), psId)) {
+        args.approvalsUtil.byPatchSet(
+            ctx.getDb(), ctx.getControl(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
       byKey.put(psa.getKey(), psa);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 37bb221..35953b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.index.change;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.util.concurrent.Futures.successfulAsList;
 import static com.google.common.util.concurrent.Futures.transform;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@@ -29,6 +30,7 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MultiProgressMonitor;
@@ -43,10 +45,10 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.concurrent.Callable;
@@ -96,16 +98,17 @@
   }
 
   private static class ProjectHolder implements Comparable<ProjectHolder> {
-    private Project.NameKey name;
-    private int size;
+    final Project.NameKey name;
+    private final long size;
 
-    ProjectHolder(Project.NameKey name, int size) {
+    ProjectHolder(Project.NameKey name, long size) {
       this.name = name;
       this.size = size;
     }
 
     @Override
     public int compareTo(ProjectHolder other) {
+      // Sort projects based on size first to maximize utilization of threads early on.
       return ComparisonChain.start()
           .compare(other.size, size)
           .compare(other.name.get(), name.get())
@@ -122,7 +125,7 @@
     Stopwatch sw = Stopwatch.createStarted();
     for (Project.NameKey name : projectCache.all()) {
       try (Repository repo = repoManager.openRepository(name)) {
-        int size = ChangeNotes.Factory.scan(repo).size();
+        long size = estimateSize(repo);
         changeCount += size;
         projects.add(new ProjectHolder(name, size));
       } catch (IOException e) {
@@ -137,21 +140,30 @@
     return indexAll(index, projects);
   }
 
-  public SiteIndexer.Result indexAll(ChangeIndex index, Iterable<ProjectHolder> projects) {
-    Stopwatch sw = Stopwatch.createStarted();
-    final MultiProgressMonitor mpm = new MultiProgressMonitor(progressOut, "Reindexing changes");
-    final Task projTask =
-        mpm.beginSubTask(
-            "projects",
-            (projects instanceof Collection)
-                ? ((Collection<?>) projects).size()
-                : MultiProgressMonitor.UNKNOWN);
-    final Task doneTask =
-        mpm.beginSubTask(null, totalWork >= 0 ? totalWork : MultiProgressMonitor.UNKNOWN);
-    final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+  private long estimateSize(Repository repo) throws IOException {
+    // Estimate size based on IDs that show up in ref names. This is not perfect, since patch set
+    // refs may exist for changes whose metadata was never successfully stored. But that's ok, as
+    // the estimate is just used as a heuristic for sorting projects.
+    return repo.getRefDatabase()
+        .getRefs(RefNames.REFS_CHANGES)
+        .values()
+        .stream()
+        .map(r -> Change.Id.fromRef(r.getName()))
+        .filter(Objects::nonNull)
+        .distinct()
+        .count();
+  }
 
-    final List<ListenableFuture<?>> futures = new ArrayList<>();
-    final AtomicBoolean ok = new AtomicBoolean(true);
+  private SiteIndexer.Result indexAll(ChangeIndex index, SortedSet<ProjectHolder> projects) {
+    Stopwatch sw = Stopwatch.createStarted();
+    MultiProgressMonitor mpm = new MultiProgressMonitor(progressOut, "Reindexing changes");
+    Task projTask = mpm.beginSubTask("projects", projects.size());
+    checkState(totalWork >= 0);
+    Task doneTask = mpm.beginSubTask(null, totalWork);
+    Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+
+    List<ListenableFuture<?>> futures = new ArrayList<>();
+    AtomicBoolean ok = new AtomicBoolean(true);
 
     for (ProjectHolder project : projects) {
       ListenableFuture<?> future =
@@ -196,11 +208,11 @@
   }
 
   public Callable<Void> reindexProject(
-      final ChangeIndexer indexer,
-      final Project.NameKey project,
-      final Task done,
-      final Task failed,
-      final PrintWriter verboseWriter) {
+      ChangeIndexer indexer,
+      Project.NameKey project,
+      Task done,
+      Task failed,
+      PrintWriter verboseWriter) {
     return new Callable<Void>() {
       @Override
       public Void call() throws Exception {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 7ad282b..a7b5501 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -325,7 +325,13 @@
       // Get previous approvals from this user
       Map<String, Short> approvals = new HashMap<>();
       approvalsUtil
-          .byPatchSetUser(ctx.getDb(), changeControl, psId, ctx.getAccountId())
+          .byPatchSetUser(
+              ctx.getDb(),
+              changeControl,
+              psId,
+              ctx.getAccountId(),
+              ctx.getRevWalk(),
+              ctx.getRepoView().getConfig())
           .forEach(a -> approvals.put(a.getLabel(), a.getValue()));
       // Fire Gerrit event. Note that approvals can't be granted via email, so old and new approvals
       // are always the same here.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java
index 4d48990..9c2e39d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -70,7 +70,7 @@
       Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
       for (PatchSetApproval ca :
           args.approvalsUtil.byPatchSet(
-              args.db.get(), changeData.changeControl(), patchSet.getId())) {
+              args.db.get(), changeData.changeControl(), patchSet.getId(), null, null)) {
         LabelType lt = labelTypes.byLabel(ca.getLabelId());
         if (lt == null) {
           continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 20e27b4..62ee879 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 import static java.util.Comparator.comparing;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -32,6 +33,8 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.metrics.Timer1;
@@ -67,7 +70,6 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -314,10 +316,14 @@
 
     private List<ChangeNotes> scanDb(Repository repo, ReviewDb db)
         throws OrmException, IOException {
-      Set<Change.Id> ids = scan(repo);
+      // Scan IDs that might exist in ReviewDb, assuming that each change has at least one patch set
+      // ref. Not all changes might exist: some patch set refs might have been written where the
+      // corresponding ReviewDb write failed. These will be silently filtered out by the batch get
+      // call below, which is intended.
+      Set<Change.Id> ids = scanChangeIds(repo).fromPatchSetRefs();
       List<ChangeNotes> notes = new ArrayList<>(ids.size());
-      // A batch size of N may overload get(Iterable), so use something smaller,
-      // but still >1.
+
+      // A batch size of N may overload get(Iterable), so use something smaller, but still >1.
       for (List<Change.Id> batch : Iterables.partition(ids, 30)) {
         for (Change change : ReviewDbUtil.unwrapDb(db).changes().get(batch)) {
           notes.add(createFromChangeOnlyWhenNoteDbDisabled(change));
@@ -328,14 +334,20 @@
 
     private List<ChangeNotes> scanNoteDb(Repository repo, ReviewDb db, Project.NameKey project)
         throws OrmException, IOException {
-      Set<Change.Id> ids = scan(repo);
-      List<ChangeNotes> changeNotes = new ArrayList<>(ids.size());
+      ScanResult sr = scanChangeIds(repo);
+      List<ChangeNotes> changeNotes = new ArrayList<>(sr.fromPatchSetRefs().size());
+
       PrimaryStorage defaultStorage = args.migration.changePrimaryStorage();
-      for (Change.Id id : ids) {
+      for (Change.Id id : sr.all()) {
         Change change = readOneReviewDbChange(db, id);
         if (change == null) {
           if (defaultStorage == PrimaryStorage.REVIEW_DB) {
-            log.warn("skipping change {} found in project {} but not in ReviewDb", id, project);
+            // If changes should exist in ReviewDb, it's worth warning about a meta ref with no
+            // corresponding ReviewDb data. But stray patch set refs can happen due to normal error
+            // conditions, e.g. failed push processing, so aren't worth even a warning.
+            if (sr.fromMetaRefs().contains(id)) {
+              log.warn("skipping change {} found in project {} but not in ReviewDb", id, project);
+            }
             continue;
           }
           // TODO(dborowitz): See discussion in BatchUpdate#newChangeContext.
@@ -354,16 +366,27 @@
       return changeNotes;
     }
 
-    public static Set<Change.Id> scan(Repository repo) throws IOException {
-      Map<String, Ref> refs = repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES);
-      Set<Change.Id> ids = new HashSet<>(refs.size());
-      for (Ref r : refs.values()) {
+    @AutoValue
+    abstract static class ScanResult {
+      abstract ImmutableSet<Change.Id> fromPatchSetRefs();
+
+      abstract ImmutableSet<Change.Id> fromMetaRefs();
+
+      SetView<Change.Id> all() {
+        return Sets.union(fromPatchSetRefs(), fromMetaRefs());
+      }
+    }
+
+    private static ScanResult scanChangeIds(Repository repo) throws IOException {
+      ImmutableSet.Builder<Change.Id> fromPs = ImmutableSet.builder();
+      ImmutableSet.Builder<Change.Id> fromMeta = ImmutableSet.builder();
+      for (Ref r : repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES).values()) {
         Change.Id id = Change.Id.fromRef(r.getName());
         if (id != null) {
-          ids.add(id);
+          (r.getName().endsWith(RefNames.META_SUFFIX) ? fromMeta : fromPs).add(id);
         }
       }
-      return ids;
+      return new AutoValue_ChangeNotes_Factory_ScanResult(fromPs.build(), fromMeta.build());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index d338e73..94f5ebf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -342,7 +342,7 @@
     }
 
     for (PatchSetApproval ap :
-        approvalsUtil.byPatchSet(db, this, getChange().currentPatchSetId())) {
+        approvalsUtil.byPatchSet(db, this, getChange().currentPatchSetId(), null, null)) {
       LabelType type = getLabelTypes().byLabel(ap.getLabel());
       if (type != null
           && ap.getValue() == 1
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index a5773fc..6bc6573 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -814,7 +814,7 @@
         try {
           currentApprovals =
               ImmutableList.copyOf(
-                  approvalsUtil.byPatchSet(db, changeControl(), c.currentPatchSetId()));
+                  approvalsUtil.byPatchSet(db, changeControl(), c.currentPatchSetId(), null, null));
         } catch (OrmException e) {
           if (e.getCause() instanceof NoSuchChangeException) {
             currentApprovals = Collections.emptyList();
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index f0930e9..be803eb 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit f0930e9dfc56644105cb21e7246096097eeaa385
+Subproject commit be803eb40fdcd5bfa11d9a808863585f86228e06
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
index 59c884d..ca94495 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -42,7 +42,7 @@
 
     shouldSuppressKeyboardShortcut(e) {
       e = getKeyboardEvent(e);
-      const tagName = e.target.tagName;
+      const tagName = Polymer.dom(event).rootTarget.tagName;
       if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
         return true;
       }
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html
index 2425c6a..3a1cc1d 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html
@@ -35,7 +35,6 @@
   suite('gr-admin-create-project tests', () => {
     let element;
     let sandbox;
-    const PROJECT = 'test-project';
 
     setup(() => {
       sandbox = sinon.sandbox.create();
@@ -69,8 +68,6 @@
           });
 
       const button = element.$.submitBtn;
-
-      flushAsynchronousOperations();
       element.$.projectNameInput.bindValue = configInputObj.name;
       element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
       element.$.initalCommit.bindValue =
@@ -83,13 +80,9 @@
       assert.deepEqual(element._projectConfig, configInputObj);
 
       element._handleCreateProject().then(() => {
-        assert.isTrue(button.hasAttribute('disabled'));
-        assert.isTrue(saveStub.lastCall.calledWithExactly(PROJECT,
-            configInputObj));
+        assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
         done();
       });
-      MockInteractions.tap(button);
-      done();
     });
 
     test('test for button being called', done => {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index 91946a6..458418a 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -28,7 +28,7 @@
 <link rel="import" href="../gr-admin-plugin-list/gr-admin-plugin-list.html">
 <link rel="import" href="../gr-admin-project-list/gr-admin-project-list.html">
 <link rel="import" href="../gr-admin-project/gr-admin-project.html">
-<link rel="import" href="../gr-project-branches/gr-project-branches.html">
+<link rel="import" href="../gr-project-detail-list/gr-project-detail-list.html">
 
 <dom-module id="gr-admin-view">
   <template>
@@ -57,7 +57,7 @@
             <!--Loop through the links in the sub-section.-->
             <template is="dom-repeat"
                 items="[[item.subsection.children]]" as="child">
-              <li class$="subsectionItem [[_computeSelectedClass(child.view, params)]]">
+              <li class$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]">
                 <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a>
               </li>
             </template>
@@ -94,11 +94,12 @@
             id="createProject"></gr-admin-create-project>
       </main>
     </template>
-    <template is="dom-if" if="[[_showProjectBranches]]" restamp="true">
+    <template is="dom-if" if="[[_showProjectDetailList]]" restamp="true">
       <main class="table">
-        <gr-project-branches
+        <gr-project-detail-list
             params="[[params]]"
-            class="table"></gr-project-branches>
+            class="table"
+            detail-type="[[params.detailType]]"></gr-project-detail-list>
       </main>
     </template>
     <template is="dom-if" if="[[params.placeholder]]" restamp="true">
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index e69d5be..3be1a2b 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -107,14 +107,20 @@
             name: `${this._project}`,
             view: 'gr-admin-project',
             url: `/admin/projects/${this.encodeURL(this._project, true)}`,
-            children: [
-              {
-                name: 'Branches',
-                view: 'gr-project-branches',
-                url: `/admin/projects/${this.encodeURL(this._project, true)}` +
+            children: [{
+              name: 'Branches',
+              detailType: 'branches',
+              view: 'gr-project-detail-list',
+              url: `/admin/projects/${this.encodeURL(this._project, true)}` +
                     ',branches',
-              },
-            ],
+            },
+            {
+              name: 'Tags',
+              detailType: 'tags',
+              view: 'gr-project-detail-list',
+              url: `/admin/projects/${this.encodeURL(this._project, true)}` +
+                    ',tags',
+            }],
           };
         }
         filteredLinks.push(linkCopy);
@@ -138,8 +144,8 @@
       this.set('_showProjectMain', params.adminView === 'gr-admin-project');
       this.set('_showProjectList',
           params.adminView === 'gr-admin-project-list');
-      this.set('_showProjectBranches',
-          params.adminView === 'gr-project-branches');
+      this.set('_showProjectDetailList',
+          params.adminView === 'gr-project-detail-list');
       this.set('_showGroupList', params.adminView === 'gr-admin-group-list');
       this.set('_showPluginList', params.adminView === 'gr-admin-plugin-list');
       if (params.project !== this._project) {
@@ -169,7 +175,10 @@
       return this._computeRelativeURL(link.url);
     },
 
-    _computeSelectedClass(itemView, params) {
+    _computeSelectedClass(itemView, params, opt_detailType) {
+      if (params.detailType && params.detailType !== opt_detailType) {
+        return '';
+      }
       return itemView === params.adminView ? 'selected' : '';
     },
   });
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js
deleted file mode 100644
index 97e3c4d..0000000
--- a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-project-branches',
-
-    properties: {
-      /**
-       * URL params passed from the router.
-       */
-      params: {
-        type: Object,
-        observer: '_paramsChanged',
-      },
-
-      /**
-       * Offset of currently visible query results.
-       */
-      _offset: Number,
-      _project: Object,
-      _branches: Array,
-      /**
-       * Because  we request one more than the projectsPerPage, _shownProjects
-       * maybe one less than _projects.
-       * */
-      _shownBranches: {
-        type: Array,
-        computed: 'computeShownItems(_branches)',
-      },
-      _branchesPerPage: {
-        type: Number,
-        value: 25,
-      },
-      _loading: {
-        type: Boolean,
-        value: true,
-      },
-      _filter: String,
-    },
-
-    behaviors: [
-      Gerrit.ListViewBehavior,
-      Gerrit.URLEncodingBehavior,
-    ],
-
-    _paramsChanged(params) {
-      this._loading = true;
-      if (!params || !params.project) { return; }
-
-      this._project = params.project;
-
-      this._filter = this.getFilterValue(params);
-      this._offset = this.getOffsetValue(params);
-
-      return this._getBranches(this._filter, this._project,
-          this._branchesPerPage, this._offset);
-    },
-
-    _getBranches(filter, project, projectsPerPage, offset) {
-      this._projectsBranches = [];
-      return this.$.restAPI.getProjectBranches(
-          filter, project, projectsPerPage, offset) .then(branches => {
-            if (!branches) { return; }
-            this._branches = branches;
-            this._loading = false;
-          });
-    },
-
-    _getPath(project) {
-      return '/admin/projects/' + this.encodeURL(project, true) + ',branches';
-    },
-
-    _computeWeblink(project) {
-      if (!project.web_links) { return ''; }
-      const webLinks = project.web_links;
-      return webLinks.length ? webLinks : null;
-    },
-
-    _stripRefsHeads(item) {
-      return item.replace('refs/heads/', '');
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html
deleted file mode 100644
index 707a536..0000000
--- a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html
+++ /dev/null
@@ -1,150 +0,0 @@
-<!DOCTYPE html>
-<!--
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-project-branches</title>
-
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-project-branches.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-project-branches></gr-project-branches>
-  </template>
-</test-fixture>
-
-<script>
-  let counter;
-  const branchGenerator = () => {
-    return {
-      ref: `refs/heads/test${++counter}`,
-      revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
-      web_links: [
-        {
-          name: 'diffusion',
-          url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
-        },
-      ],
-    };
-  };
-
-  suite('gr-project-branches tests', () => {
-    let element;
-    let branches;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
-      counter = 0;
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    suite('list with project branches', () => {
-      setup(done => {
-        branches = _.times(26, branchGenerator);
-
-        stub('gr-rest-api-interface', {
-          getProjectBranches(num, project, offset) {
-            return Promise.resolve(branches);
-          },
-        });
-
-        const params = {
-          project: 'test',
-        };
-
-        element._paramsChanged(params).then(() => { flush(done); });
-      });
-
-      test('test for test branch in the list', done => {
-        flush(() => {
-          assert.equal(element._branches[1].ref, 'refs/heads/test2');
-          done();
-        });
-      });
-
-      test('test for test web links in the branches list', done => {
-        flush(() => {
-          assert.equal(element._branches[1].web_links[0].url,
-              'https://git.example.org/branch/test;refs/heads/test2');
-          done();
-        });
-      });
-
-      test('test for refs/heads/ being striped from ref', done => {
-        flush(() => {
-          assert.equal(element._stripRefsHeads(element._branches[1].ref),
-              'test2');
-          done();
-        });
-      });
-
-      test('_shownBranches', () => {
-        assert.equal(element._shownBranches.length, 25);
-      });
-    });
-
-    suite('list with less then 25 branches', () => {
-      setup(done => {
-        branches = _.times(25, branchGenerator);
-
-        stub('gr-rest-api-interface', {
-          getProjectBranches(num, project, offset) {
-            return Promise.resolve(branches);
-          },
-        });
-
-        const params = {
-          project: 'test',
-        };
-
-        element._paramsChanged(params).then(() => { flush(done); });
-      });
-
-      test('_shownProjectsBranches', () => {
-        assert.equal(element._shownBranches.length, 25);
-      });
-    });
-
-    suite('filter', () => {
-      test('_paramsChanged', done => {
-        sandbox.stub(element.$.restAPI, 'getProjectBranches', () => {
-          return Promise.resolve(branches);
-        });
-        const params = {
-          project: 'test',
-          filter: 'test',
-          offset: 25,
-        };
-        element._paramsChanged(params).then(() => {
-          assert.isTrue(element.$.restAPI.getProjectBranches.lastCall
-              .calledWithExactly('test', 'test', 25, 25));
-          done();
-        });
-      });
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
similarity index 74%
rename from polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html
rename to polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
index 063b311..ddae595 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
@@ -24,31 +24,39 @@
 <link rel="import" href="../../../styles/shared-styles.html">
 
 
-<dom-module id="gr-project-branches">
+<dom-module id="gr-project-detail-list">
   <template>
-    <style include="shared-styles"></style>
+    <style include="shared-styles">
+      .repositoryBrowser {
+        display: none;
+      }
+      .repositoryBrowser.show{
+        display: table-cell;
+      }
+    </style>
     <gr-list-view
         filter="[[_filter]]"
-        items-per-page="[[_branchesPerPage]]"
-        items="[[_branches]]"
+        items-per-page="[[_itemsPerPage]]"
+        items="[[_items]]"
         loading="[[_loading]]"
         offset="[[_offset]]"
         path="[[_getPath(_project)]]">
       <table id="list">
         <tr class="headerRow">
-          <th class="name topHeader">Branch Name</th>
+          <th class="name topHeader">Name</th>
           <th class="description topHeader">Revision</th>
-          <th class="repositoryBrowser topHeader">Repository Browser</th>
+          <th class$="repositoryBrowser topHeader [[computeBrowserClass(detailType)]]">
+            Repository Browser</th>
         </tr>
         <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
           <td>Loading...</td>
         </tr>
-        <template is="dom-repeat" items="[[_shownBranches]]"
+        <template is="dom-repeat" items="[[_shownItems]]"
             class$="[[computeLoadingClass(_loading)]]">
           <tr class="table">
-            <td class="name">[[_stripRefsHeads(item.ref)]]</td>
+            <td class="name">[[_stripRefs(item.ref, detailType)]]</td>
             <td class="description">[[item.revision]]</td>
-            <td class="repositoryBrowser">
+            <td class$="repositoryBrowser [[computeBrowserClass(detailType)]]">
               <template is="dom-repeat"
                   items="[[_computeWeblink(item)]]" as="link">
                 <a href$="[[link.url]]"
@@ -65,5 +73,5 @@
     </gr-list-view>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
-  <script src="gr-project-branches.js"></script>
+  <script src="gr-project-detail-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js
new file mode 100644
index 0000000..704380a
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js
@@ -0,0 +1,128 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  const DETAIL_TYPES = {
+    BRANCHES: 'branches',
+    TAGS: 'tags',
+  };
+
+  Polymer({
+    is: 'gr-project-detail-list',
+
+    properties: {
+      /**
+       * URL params passed from the router.
+       */
+      params: {
+        type: Object,
+        observer: '_paramsChanged',
+      },
+      /**
+       * The kind of detail we are displaying, possibilities are determined by
+       * the const DETAIL_TYPES.
+       */
+      detailType: String,
+
+      /**
+       * Offset of currently visible query results.
+       */
+      _offset: Number,
+      _project: Object,
+      _items: Array,
+      /**
+       * Because  we request one more than the projectsPerPage, _shownProjects
+       * maybe one less than _projects.
+       * */
+      _shownItems: {
+        type: Array,
+        computed: 'computeShownItems(_items)',
+      },
+      _itemsPerPage: {
+        type: Number,
+        value: 25,
+      },
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _filter: String,
+    },
+
+    behaviors: [
+      Gerrit.ListViewBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
+    _paramsChanged(params) {
+      this._loading = true;
+      if (!params || !params.project) { return; }
+
+      this._project = params.project;
+
+      this._filter = this.getFilterValue(params);
+      this._offset = this.getOffsetValue(params);
+
+      return this._getItems(this._filter, this._project,
+          this._itemsPerPage, this._offset, this.detailType);
+    },
+
+    _getItems(filter, project, itemsPerPage, offset, detailType) {
+      this._items = [];
+      Polymer.dom.flush();
+      if (detailType === DETAIL_TYPES.BRANCHES) {
+        return this.$.restAPI.getProjectBranches(
+            filter, project, itemsPerPage, offset) .then(items => {
+              if (!items) { return; }
+              this._items = items;
+              this._loading = false;
+            });
+      } else if (detailType === DETAIL_TYPES.TAGS) {
+        return this.$.restAPI.getProjectTags(
+            filter, project, itemsPerPage, offset) .then(items => {
+              if (!items) { return; }
+              this._items = items;
+              this._loading = false;
+            });
+      }
+    },
+
+    _getPath(project) {
+      return `/admin/projects/${this.encodeURL(project, true)},` +
+          `${this.detailType}`;
+    },
+
+    _computeWeblink(project) {
+      if (!project.web_links) { return ''; }
+      const webLinks = project.web_links;
+      return webLinks.length ? webLinks : null;
+    },
+
+    computeBrowserClass(detailType) {
+      if (detailType === DETAIL_TYPES.BRANCHES) {
+        return 'show';
+      }
+      return '';
+    },
+
+    _stripRefs(item, detailType) {
+      if (detailType === DETAIL_TYPES.BRANCHES) {
+        return item.replace('refs/heads/', '');
+      } else if (detailType === DETAIL_TYPES.TAGS) {
+        return item.replace('refs/tags/', '');
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html
new file mode 100644
index 0000000..a452091
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html
@@ -0,0 +1,251 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-project-detail-list</title>
+
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-project-detail-list.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-project-detail-list></gr-project-detail-list>
+  </template>
+</test-fixture>
+
+<script>
+  let counter;
+  const branchGenerator = () => {
+    return {
+      ref: `refs/heads/test${++counter}`,
+      revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+      web_links: [
+        {
+          name: 'diffusion',
+          url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
+        },
+      ],
+    };
+  };
+  const tagGenerator = () => {
+    return {
+      ref: `refs/tags/test${++counter}`,
+      revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+    };
+  };
+
+  suite('Branches', () => {
+    let element;
+    let branches;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      element.detailType = 'branches';
+      counter = 0;
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('list of project branches', () => {
+      setup(done => {
+        branches = _.times(26, branchGenerator);
+
+        stub('gr-rest-api-interface', {
+          getProjectBranches(num, project, offset) {
+            return Promise.resolve(branches);
+          },
+        });
+
+        const params = {
+          project: 'test',
+        };
+
+        element._paramsChanged(params).then(() => { flush(done); });
+      });
+
+      test('test for test branch in the list', done => {
+        flush(() => {
+          assert.equal(element._items[1].ref, 'refs/heads/test2');
+          done();
+        });
+      });
+
+      test('test for test web links in the branches list', done => {
+        flush(() => {
+          assert.equal(element._items[1].web_links[0].url,
+              'https://git.example.org/branch/test;refs/heads/test2');
+          done();
+        });
+      });
+
+      test('test for refs/heads/ being striped from ref', done => {
+        flush(() => {
+          assert.equal(element._stripRefs(element._items[1].ref,
+              element.detailType), 'test2');
+          done();
+        });
+      });
+
+      test('_shownItems', () => {
+        assert.equal(element._shownItems.length, 25);
+      });
+    });
+
+    suite('list with less then 25 branches', () => {
+      setup(done => {
+        branches = _.times(25, branchGenerator);
+
+        stub('gr-rest-api-interface', {
+          getProjectBranches(num, project, offset) {
+            return Promise.resolve(branches);
+          },
+        });
+
+        const params = {
+          project: 'test',
+        };
+
+        element._paramsChanged(params).then(() => { flush(done); });
+      });
+
+      test('_shownProjectsBranches', () => {
+        assert.equal(element._shownItems.length, 25);
+      });
+    });
+
+    suite('filter', () => {
+      test('_paramsChanged', done => {
+        sandbox.stub(element.$.restAPI, 'getProjectBranches', () => {
+          return Promise.resolve(branches);
+        });
+        const params = {
+          project: 'test',
+          filter: 'test',
+          offset: 25,
+        };
+        element._paramsChanged(params).then(() => {
+          assert.isTrue(element.$.restAPI.getProjectBranches.lastCall
+              .calledWithExactly('test', 'test', 25, 25));
+          done();
+        });
+      });
+    });
+  });
+
+  suite('Tags', () => {
+    let element;
+    let tags;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      element.detailType = 'tags';
+      counter = 0;
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('list of project tags', () => {
+      setup(done => {
+        tags = _.times(26, tagGenerator);
+
+        stub('gr-rest-api-interface', {
+          getProjectTags(num, project, offset) {
+            return Promise.resolve(tags);
+          },
+        });
+
+        const params = {
+          project: 'test',
+        };
+
+        element._paramsChanged(params).then(() => { flush(done); });
+      });
+
+      test('test for test tag in the list', done => {
+        flush(() => {
+          assert.equal(element._items[1].ref, 'refs/tags/test2');
+          done();
+        });
+      });
+
+      test('test for refs/tags/ being striped from ref', done => {
+        flush(() => {
+          assert.equal(element._stripRefs(element._items[1].ref,
+              element.detailType), 'test2');
+          done();
+        });
+      });
+
+      test('_shownItems', () => {
+        assert.equal(element._shownItems.length, 25);
+      });
+    });
+
+    suite('list with less then 25 tags', () => {
+      setup(done => {
+        tags = _.times(25, tagGenerator);
+
+        stub('gr-rest-api-interface', {
+          getProjectTags(num, project, offset) {
+            return Promise.resolve(tags);
+          },
+        });
+
+        const params = {
+          project: 'test',
+        };
+
+        element._paramsChanged(params).then(() => { flush(done); });
+      });
+
+      test('_shownItems', () => {
+        assert.equal(element._shownItems.length, 25);
+      });
+    });
+
+    suite('filter', () => {
+      test('_paramsChanged', done => {
+        sandbox.stub(element.$.restAPI, 'getProjectTags', () => {
+          return Promise.resolve(tags);
+        });
+        const params = {
+          project: 'test',
+          filter: 'test',
+          offset: 25,
+        };
+        element._paramsChanged(params).then(() => {
+          assert.isTrue(element.$.restAPI.getProjectTags.lastCall
+              .calledWithExactly('test', 'test', 25, 25));
+          done();
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
index be66587..a1aaac4 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -38,9 +38,10 @@
        */
       allowAnyUser: Boolean,
 
+      // suggestFrom = 0 to enable default suggestions.
       suggestFrom: {
         type: Number,
-        value: 3,
+        value: 0,
       },
 
       query: {
@@ -100,6 +101,7 @@
     },
 
     _getReviewerSuggestions(input) {
+      if (!this.change) { return Promise.resolve([]); }
       const api = this.$.restAPI;
       const xhr = this.allowAnyUser ?
           api.getSuggestedAccounts(input) :
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 1b89783..2e3d30c 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -174,11 +174,12 @@
       });
     });
 
-    // Matches /admin/projects/<project/branches[,<offset>].
+    // Matches /admin/projects/<project>,branches[,<offset>].
     page(/^\/admin\/projects\/(.+),branches(,(.+))?$/, loadUser, data => {
       app.params = {
         view: 'gr-admin-view',
-        adminView: 'gr-project-branches',
+        adminView: 'gr-project-detail-list',
+        detailType: 'branches',
         project: data.params[0],
         offset: data.params[2] || 0,
         filter: null,
@@ -189,7 +190,8 @@
         loadUser, data => {
           app.params = {
             view: 'gr-admin-view',
-            adminView: 'gr-project-branches',
+            adminView: 'gr-project-detail-list',
+            detailType: 'branches',
             project: data.params.project,
             offset: data.params.offset,
             filter: data.params.filter,
@@ -200,7 +202,43 @@
         loadUser, data => {
           app.params = {
             view: 'gr-admin-view',
-            adminView: 'gr-project-branches',
+            adminView: 'gr-project-detail-list',
+            detailType: 'branches',
+            project: data.params.project,
+            filter: data.params.filter || null,
+          };
+        });
+
+    // Matches /admin/projects/<project>,tags[,<offset>].
+    page(/^\/admin\/projects\/(.+),tags(,(.+))?$/, loadUser, data => {
+      app.params = {
+        view: 'gr-admin-view',
+        adminView: 'gr-project-detail-list',
+        detailType: 'tags',
+        project: data.params[0],
+        offset: data.params[2] || 0,
+        filter: null,
+      };
+    });
+
+    page('/admin/projects/:project,tags/q/filter::filter,:offset',
+        loadUser, data => {
+          app.params = {
+            view: 'gr-admin-view',
+            adminView: 'gr-project-detail-list',
+            detailType: 'tags',
+            project: data.params.project,
+            offset: data.params.offset,
+            filter: data.params.filter,
+          };
+        });
+
+    page('/admin/projects/:project,tags/q/filter::filter',
+        loadUser, data => {
+          app.params = {
+            view: 'gr-admin-view',
+            adminView: 'gr-project-detail-list',
+            detailType: 'tags',
             project: data.params.project,
             filter: data.params.filter || null,
           };
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index 516f700..12f9fea 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -55,6 +55,7 @@
           allowNonSuggestedValues
           multi
           borderless
+          threshold="[[_threshold]]"
           tab-complete-without-commit></gr-autocomplete>
       <gr-button id="searchButton">Search</gr-button>
       <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index ee08e75..69a247b 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -122,6 +122,10 @@
         },
       },
       _inputVal: String,
+      _threshold: {
+        type: Number,
+        value: 3,
+      },
     },
 
     _valueChanged(value) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 1b5f260..4670c73 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -61,7 +61,7 @@
 
       /**
        * The number of characters that must be typed before suggestions are
-       * made.
+       * made. If threshold is zero, default suggestions are enabled.
        */
       threshold: {
         type: Number,
@@ -192,10 +192,10 @@
     },
 
     _updateSuggestions() {
-      if (!this.text || this._disableSuggestions) { return; }
-      if (this.text.length < this.threshold) {
+      if (this._disableSuggestions) { return; }
+      if (this.text === undefined || this.text.length < this.threshold) {
         this._suggestions = [];
-        this.value = null;
+        this.value = '';
         return;
       }
       const text = this.text;
@@ -211,7 +211,7 @@
         this._suggestions = suggestions;
         Polymer.dom.flush();
         if (this._index === -1) {
-          this.value = null;
+          this.value = '';
         }
       });
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 4f83086..b710cfa 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -499,10 +499,9 @@
         opt_ctx) {
       const url =
           this.getChangeActionURL(changeNum, null, '/suggest_reviewers');
-      return this.fetchJSON(url, opt_errFn, opt_ctx, {
-        n: 10,  // Return max 10 results
-        q: inputVal,
-      });
+      const req = {n: 10};
+      if (inputVal) { req.q = inputVal; }
+      return this.fetchJSON(url, opt_errFn, opt_ctx, req);
     },
 
     getGroups(filter, groupsPerPage, opt_offset) {
@@ -533,6 +532,16 @@
       );
     },
 
+    getProjectTags(filter, project, projectsTagsPerPage, opt_offset) {
+      const offset = opt_offset || 0;
+      filter = filter ? '&m=' + filter : '';
+
+      return this._fetchSharedCacheURL(
+          `/projects/${project}/tags?n=${projectsTagsPerPage + 1}&s=` +
+          `${offset}${filter}`
+      );
+    },
+
     getPlugins() {
       return this._fetchSharedCacheURL('/plugins/?all');
     },
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index f31e9be..1f51774 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -36,7 +36,7 @@
     'admin/gr-admin-project/gr-admin-project_test.html',
     'admin/gr-admin-project-list/gr-admin-project-list_test.html',
     'admin/gr-admin-view/gr-admin-view_test.html',
-    'admin/gr-project-branches/gr-project-branches_test.html',
+    'admin/gr-project-detail-list/gr-project-detail-list_test.html',
     'change-list/gr-change-list-item/gr-change-list-item_test.html',
     'change-list/gr-change-list-view/gr-change-list-view_test.html',
     'change-list/gr-change-list/gr-change-list_test.html',