Merge "Fix more to support undefined behaviour"
diff --git a/Documentation/user-request-tracing.txt b/Documentation/user-request-tracing.txt
index 1123775..e684b85 100644
--- a/Documentation/user-request-tracing.txt
+++ b/Documentation/user-request-tracing.txt
@@ -87,8 +87,7 @@
 `AutoRetry`. For each auto-retry that happened this should match 1 or 2
 log entries:
 
-* one `ERROR` log entry with the exception that triggered the
-  auto-retry
+* one `FINE` log entry with the exception that triggered the auto-retry
 * one `FINE` log entry with the exception that happened on auto-retry
   (if this log entry is not present the operation succeeded on
   auto-retry)
diff --git a/Jenkinsfile b/Jenkinsfile
index 756c1d4..7417bb3 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -63,17 +63,15 @@
     }
 
     def getCheckResultFromBuild() {
-        switch(build.result) {
-            case 'SUCCESS':
-                return "SUCCESSFUL"
-            case 'NOT_BUILT':
-            case 'ABORTED':
-                return "NOT_STARTED"
-            case 'FAILURE':
-            case 'UNSTABLE':
-            default:
-                return "FAILED"
+        def resultString = build.result.toString()
+        if (resultString == 'SUCCESS') {
+            return "SUCCESSFUL"
+        } else if (resultString == 'NOT_BUILT' || resultString == 'ABORTED') {
+            return "NOT_STARTED"
         }
+
+        // Remaining options: 'FAILURE' or 'UNSTABLE':
+        return "FAILED"
     }
 }
 
diff --git a/java/com/google/gerrit/acceptance/AccountIndexedCounter.java b/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
new file mode 100644
index 0000000..88b97c7
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.util.concurrent.AtomicLongMap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.events.AccountIndexedListener;
+
+/** Checks if an account is indexed the correct number of times. */
+public class AccountIndexedCounter implements AccountIndexedListener {
+  private final AtomicLongMap<Integer> countsByAccount = AtomicLongMap.create();
+
+  @Override
+  public void onAccountIndexed(int id) {
+    countsByAccount.incrementAndGet(id);
+  }
+
+  public void clear() {
+    countsByAccount.clear();
+  }
+
+  public void assertReindexOf(TestAccount testAccount) {
+    assertReindexOf(testAccount, 1);
+  }
+
+  public void assertReindexOf(AccountInfo accountInfo) {
+    assertReindexOf(Account.id(accountInfo._accountId), 1);
+  }
+
+  public void assertReindexOf(TestAccount testAccount, long expectedCount) {
+    assertThat(countsByAccount.asMap()).containsExactly(testAccount.id().get(), expectedCount);
+    clear();
+  }
+
+  public void assertReindexOf(Account.Id accountId, long expectedCount) {
+    assertThat(countsByAccount.asMap()).containsEntry(accountId.get(), expectedCount);
+    countsByAccount.remove(accountId.get());
+  }
+
+  public void assertNoReindex() {
+    assertThat(countsByAccount.asMap()).isEmpty();
+  }
+}
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
index 060000a..8c9ebeb 100644
--- a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
+++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
@@ -50,7 +49,6 @@
 
   private final PermissionBackend.ForProject forProject;
 
-  @Inject
   PermissionAwareReadOnlyRefDatabase(
       Repository delegateRepository, PermissionBackend.ForProject forProject) {
     super(delegateRepository);
@@ -100,16 +98,14 @@
   @SuppressWarnings("deprecation")
   @Override
   public Map<String, Ref> getRefs(String prefix) throws IOException {
-    Map<String, Ref> refs = getDelegate().getRefDatabase().getRefs(prefix);
+    List<Ref> refs = getDelegate().getRefDatabase().getRefsByPrefix(prefix);
     if (refs.isEmpty()) {
-      return refs;
+      return Collections.emptyMap();
     }
 
     Collection<Ref> result;
     try {
-      // The security filtering assumes to receive the same refMap, independently from the ref
-      // prefix offset
-      result = forProject.filter(refs.values(), getDelegate(), RefFilterOptions.defaults());
+      result = forProject.filter(refs, getDelegate(), RefFilterOptions.defaults());
     } catch (PermissionBackendException e) {
       throw new IOException("");
     }
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index bcd3bea..66482a9 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.ArrayList;
@@ -416,14 +417,7 @@
             update.fireGitRefUpdatedEvent(ru);
             return revision;
           case LOCK_FAILURE:
-            throw new LockFailureException(
-                "Cannot update "
-                    + ru.getName()
-                    + " in "
-                    + db.getDirectory()
-                    + ": "
-                    + ru.getResult(),
-                ru);
+            throw new LockFailureException(errorMsg(ru, db.getDirectory()), ru);
           case FORCED:
           case IO_FAILURE:
           case NOT_ATTEMPTED:
@@ -434,19 +428,18 @@
           case REJECTED_MISSING_OBJECT:
           case REJECTED_OTHER_REASON:
           default:
-            throw new GitUpdateFailureException(
-                "Cannot update "
-                    + ru.getName()
-                    + " in "
-                    + db.getDirectory()
-                    + ": "
-                    + ru.getResult(),
-                ru);
+            throw new GitUpdateFailureException(errorMsg(ru, db.getDirectory()), ru);
         }
       }
     };
   }
 
+  private String errorMsg(RefUpdate ru, File location) {
+    return String.format(
+        "Cannot update %s in %s : %s (%s)",
+        ru.getName(), location, ru.getResult(), ru.getRefLogMessage());
+  }
+
   protected DirCache readTree(RevTree tree)
       throws IOException, MissingObjectException, IncorrectObjectTypeException {
     DirCache dc = DirCache.newInCore();
diff --git a/java/com/google/gerrit/server/restapi/account/AddSshKey.java b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
index 1fcf0bd..f18cc67 100644
--- a/java/com/google/gerrit/server/restapi/account/AddSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
@@ -44,6 +44,11 @@
 import java.io.InputStream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to add an SSH key for an account.
+ *
+ * <p>This REST endpoint handles {@code POST /accounts/<account-identifier>/sshkeys/} requests.
+ */
 @Singleton
 public class AddSshKey
     implements RestCollectionModifyView<AccountResource, AccountResource.SshKey, SshKeyInput> {
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index ae45b68..fee5eab 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.extensions.client.AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
@@ -47,6 +48,26 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint for registering a new email address for an account.
+ *
+ * <p>This REST endpoint handles {@code PUT
+ * /accounts/<account-identifier>/emails/<email-identifier>} requests if the specified email doesn't
+ * exist for the account yet. If it already exists, the request is handled by {@link PutEmail}.
+ *
+ * <p>Whether an email address can be registered for the account depends on whether the used {@link
+ * Realm} supports this.
+ *
+ * <p>When a new email address is registered an email with a confirmation link is sent to that
+ * address. Only when the receiver confirms the email by clicking on the confirmation link, the
+ * email address is added to the account (see {@link
+ * com.google.gerrit.server.restapi.config.ConfirmEmail}). Confirming an email address for an
+ * account creates an external ID that links the email address to the account. An email address can
+ * only be added to an account if it is not assigned to any other account yet.
+ *
+ * <p>In some cases it is allowed to skip the email confirmation and add the email directly (calling
+ * user has 'Modify Account' capability or server is running in dev mode).
+ */
 @Singleton
 public class CreateEmail
     implements RestCollectionCreateView<AccountResource, AccountResource.Email, EmailInput> {
@@ -101,6 +122,7 @@
   }
 
   /** To be used from plugins that want to create emails without permission checks. */
+  @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
   public EmailInfo apply(IdentifiedUser user, IdString id, EmailInput input)
       throws RestApiException, EmailException, MethodNotAllowedException, IOException,
           ConfigInvalidException, PermissionBackendException {
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteActive.java b/java/com/google/gerrit/server/restapi/account/DeleteActive.java
index ffd7893..7295370 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteActive.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteActive.java
@@ -30,6 +30,15 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to mark an account as inactive.
+ *
+ * <p>This REST endpoint handles {@code DELETE /accounts/<account-identifier>/active} requests.
+ *
+ * <p>Inactive accounts cannot login into Gerrit.
+ *
+ * <p>Marking an account as active is handled by {@link PutActive}.
+ */
 @RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 @Singleton
 public class DeleteActive implements RestModifyView<AccountResource, Input> {
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
index 82b445f..442b6a4 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
@@ -44,6 +44,12 @@
 import java.util.function.Function;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to delete external IDs from an account.
+ *
+ * <p>This REST endpoint handles {@code POST /accounts/<account-identifier>/external.ids:delete}
+ * requests.
+ */
 @Singleton
 public class DeleteExternalIds implements RestModifyView<AccountResource, List<String>> {
   private final PermissionBackend permissionBackend;
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
index b470be8..f73f00a 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
@@ -36,6 +36,12 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
+/**
+ * REST endpoint to delete an SSH key of an account.
+ *
+ * <p>This REST endpoint handles {@code DELETE
+ * /accounts/<account-identifier>/sshkeys/<ssh-key-identifier>} requests.
+ */
 @Singleton
 public class DeleteSshKey implements RestModifyView<AccountResource.SshKey, Input> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
index 9f38b97..e070522 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
@@ -39,6 +39,12 @@
 import java.util.Objects;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to delete project watches from an account.
+ *
+ * <p>This REST endpoint handles {@code POST /accounts/<account-identifier>/watched.projects:delete}
+ * requests.
+ */
 @Singleton
 public class DeleteWatchedProjects
     implements RestModifyView<AccountResource, List<ProjectWatchInfo>> {
diff --git a/java/com/google/gerrit/server/restapi/account/GetAccount.java b/java/com/google/gerrit/server/restapi/account/GetAccount.java
index 898b0bb..2b6a9e6 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAccount.java
@@ -23,6 +23,15 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>} requests.
+ *
+ * <p>In the response only a subset of fields is populated (see {@link
+ * AccountLoader#DETAILED_OPTIONS}). In contrast to this {@link GetDetail} populates all fields in
+ * the response.
+ */
 @Singleton
 public class GetAccount implements RestReadView<AccountResource> {
   private final AccountLoader.Factory infoFactory;
diff --git a/java/com/google/gerrit/server/restapi/account/GetActive.java b/java/com/google/gerrit/server/restapi/account/GetActive.java
index 66493f8..38c740c 100644
--- a/java/com/google/gerrit/server/restapi/account/GetActive.java
+++ b/java/com/google/gerrit/server/restapi/account/GetActive.java
@@ -19,6 +19,13 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get the active state of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/active} requests.
+ *
+ * <p>Only active accounts can login into Gerrit.
+ */
 @Singleton
 public class GetActive implements RestReadView<AccountResource> {
   @Override
diff --git a/java/com/google/gerrit/server/restapi/account/GetAgreements.java b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
index 572b489..aeaeb1c 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAgreements.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
@@ -42,6 +42,14 @@
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
 
+/**
+ * REST endpoint to get all contributor agreements that have been signed by an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/agreements} requests.
+ *
+ * <p>Contributor agreements are only available if contributor agreements have been enabled in
+ * {@code gerrit.config} (see {@code auth.contributorAgreements}).
+ */
 @Singleton
 public class GetAgreements implements RestReadView<AccountResource> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/restapi/account/GetAvatar.java b/java/com/google/gerrit/server/restapi/account/GetAvatar.java
index 3c1752d..5256d68 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAvatar.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAvatar.java
@@ -26,6 +26,13 @@
 import java.util.concurrent.TimeUnit;
 import org.kohsuke.args4j.Option;
 
+/**
+ * REST endpoint to get the avatar image of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/avatar} requests.
+ *
+ * <p>Avatar images are only available if an {@link AvatarProvider} plugin is installed.
+ */
 public class GetAvatar implements RestReadView<AccountResource> {
   private final DynamicItem<AvatarProvider> avatarProvider;
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetAvatarChangeUrl.java b/java/com/google/gerrit/server/restapi/account/GetAvatarChangeUrl.java
index e97e0a0..a26df64 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAvatarChangeUrl.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAvatarChangeUrl.java
@@ -24,6 +24,15 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get the URL for changing the avatar image of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/avatar.change.url}
+ * requests.
+ *
+ * <p>Avatar images are only available if an {@link AvatarProvider} plugin is installed. Not all
+ * avatar plugins provide a URL for changing avatar images.
+ */
 @Singleton
 public class GetAvatarChangeUrl implements RestReadView<AccountResource> {
   private final DynamicItem<AvatarProvider> avatarProvider;
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index fa9ab18..f3d9557 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -49,6 +49,11 @@
 import java.util.Set;
 import org.kohsuke.args4j.Option;
 
+/**
+ * REST endpoint to list the global capabilities that are assigned to an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/capabilities/} requests.
+ */
 public class GetCapabilities implements RestReadView<AccountResource> {
   @Option(name = "-q", metaVar = "CAP", usage = "Capability to inspect")
   void addQuery(String name) {
@@ -159,6 +164,12 @@
     }
   }
 
+  /**
+   * REST endpoint to check if a global capability is assigned to an account.
+   *
+   * <p>This REST endpoint handles {@code GET
+   * /accounts/<account-identifier>/capabilities/<capability-identifier>} requests.
+   */
   @Singleton
   public static class CheckOne implements RestReadView<AccountResource.Capability> {
     private final PermissionBackend permissionBackend;
diff --git a/java/com/google/gerrit/server/restapi/account/GetDetail.java b/java/com/google/gerrit/server/restapi/account/GetDetail.java
index b19559e..1091599 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDetail.java
@@ -27,6 +27,14 @@
 import java.util.Collections;
 import java.util.EnumSet;
 
+/**
+ * REST endpoint to get details of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/detail} requests.
+ *
+ * <p>In the response all fields are populated. In contrast to this {@link GetAccount} populates
+ * only a subset of the fields in the response.
+ */
 @Singleton
 public class GetDetail implements RestReadView<AccountResource> {
   private final InternalAccountDirectory directory;
diff --git a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
index 670ef3b..53ce94b 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
@@ -34,6 +34,18 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to get the diff preferences of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/preferences.diff}
+ * requests.
+ *
+ * <p>General preferences can be retrieved by {@link GetPreferences} and edit preferences can be
+ * retrieved by {@link GetEditPreferences}.
+ *
+ * <p>Default diff preferences that apply for all accounts can be retrieved by {@link
+ * com.google.gerrit.server.restapi.config.GetDiffPreferences}.
+ */
 @Singleton
 public class GetDiffPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
index 409209e..0f117eb 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
@@ -34,6 +34,18 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to get the edit preferences of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/preferences.edit}
+ * requests.
+ *
+ * <p>General preferences can be retrieved by {@link GetPreferences} and diff preferences can be
+ * retrieved by {@link GetDiffPreferences}.
+ *
+ * <p>Default edit preferences that apply for all accounts can be retrieved by {@link
+ * com.google.gerrit.server.restapi.config.GetEditPreferences}.
+ */
 @Singleton
 public class GetEditPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmail.java b/java/com/google/gerrit/server/restapi/account/GetEmail.java
index afcdac2..4a71858 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmail.java
@@ -21,6 +21,12 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get an email of an account.
+ *
+ * <p>This REST endpoint handles {@code GET
+ * /accounts/<account-identifier>/emails/<email-identifier>} requests.
+ */
 @Singleton
 public class GetEmail implements RestReadView<AccountResource.Email> {
   @Inject
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmails.java b/java/com/google/gerrit/server/restapi/account/GetEmails.java
index 9db9f05..4d70eb9 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmails.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmails.java
@@ -32,6 +32,11 @@
 import java.util.List;
 import java.util.Objects;
 
+/**
+ * REST endpoint to list the emails of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/emails/} requests.
+ */
 @Singleton
 public class GetEmails implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index 0e52af2..c5b4454 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -39,6 +39,11 @@
 import java.util.List;
 import java.util.Optional;
 
+/**
+ * REST endpoint to get the external IDs of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/external.ids} requests.
+ */
 @Singleton
 public class GetExternalIds implements RestReadView<AccountResource> {
   private final PermissionBackend permissionBackend;
diff --git a/java/com/google/gerrit/server/restapi/account/GetGroups.java b/java/com/google/gerrit/server/restapi/account/GetGroups.java
index 22492a7..47fe96f 100644
--- a/java/com/google/gerrit/server/restapi/account/GetGroups.java
+++ b/java/com/google/gerrit/server/restapi/account/GetGroups.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.exceptions.NoSuchGroupException;
@@ -31,8 +32,19 @@
 import java.util.List;
 import java.util.Set;
 
+/**
+ * REST endpoint to get all known groups of an account (groups that contain the account as member).
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/groups} requests.
+ *
+ * <p>The response may not contain all groups of the account as not all groups may be known (see
+ * {@link com.google.gerrit.server.account.GroupMembership#getKnownGroups()}). In addition groups
+ * that are not visible to the calling user are filtered out.
+ */
 @Singleton
 public class GetGroups implements RestReadView<AccountResource> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final GroupControl.Factory groupControlFactory;
   private final GroupJson json;
 
@@ -54,11 +66,22 @@
       try {
         ctl = groupControlFactory.controlFor(uuid);
       } catch (NoSuchGroupException e) {
+        logger.atFine().log("skipping non-existing group %s", uuid);
         continue;
       }
-      if (ctl.isVisible() && ctl.canSeeMember(userId)) {
-        visibleGroups.add(json.format(ctl.getGroup()));
+
+      if (!ctl.isVisible()) {
+        logger.atFine().log("skipping non-visible group %s", uuid);
+        continue;
       }
+
+      if (!ctl.canSeeMember(userId)) {
+        logger.atFine().log(
+            "skipping group %s because member %d cannot be seen", uuid, userId.get());
+        continue;
+      }
+
+      visibleGroups.add(json.format(ctl.getGroup()));
     }
     return Response.ok(visibleGroups);
   }
diff --git a/java/com/google/gerrit/server/restapi/account/GetName.java b/java/com/google/gerrit/server/restapi/account/GetName.java
index ca33887..0e73058 100644
--- a/java/com/google/gerrit/server/restapi/account/GetName.java
+++ b/java/com/google/gerrit/server/restapi/account/GetName.java
@@ -20,6 +20,11 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get the full name of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/name} requests.
+ */
 @Singleton
 public class GetName implements RestReadView<AccountResource> {
   @Override
diff --git a/java/com/google/gerrit/server/restapi/account/GetPreferences.java b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
index d4d73c5..bfc6852 100644
--- a/java/com/google/gerrit/server/restapi/account/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
@@ -35,6 +35,17 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get the general preferences of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/preferences} requests.
+ *
+ * <p>Diff preferences can be retrieved by {@link GetDiffPreferences} and edit preferences can be
+ * retrieved by {@link GetEditPreferences}.
+ *
+ * <p>Default general preferences that apply for all accounts can be retrieved by {@link
+ * com.google.gerrit.server.restapi.config.GetPreferences}.
+ */
 @Singleton
 public class GetPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKey.java b/java/com/google/gerrit/server/restapi/account/GetSshKey.java
index 58b5d12..70c1327 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKey.java
@@ -21,6 +21,12 @@
 import com.google.gerrit.server.account.AccountResource.SshKey;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get an SSH key of an account.
+ *
+ * <p>This REST endpoint handles {@code GET
+ * /accounts/<account-identifier>/sshkeys/<ssh-key-identifier>} requests.
+ */
 @Singleton
 public class GetSshKey implements RestReadView<AccountResource.SshKey> {
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
index 0ca9b9e..6df6c3c 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
@@ -36,6 +36,11 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
+/**
+ * REST endpoint to list the SSH keys of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/sshkeys/} requests.
+ */
 @Singleton
 public class GetSshKeys implements RestReadView<AccountResource> {
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetStatus.java b/java/com/google/gerrit/server/restapi/account/GetStatus.java
index 447ad76..d29cdcd 100644
--- a/java/com/google/gerrit/server/restapi/account/GetStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/GetStatus.java
@@ -20,6 +20,14 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get the status of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/status} requests.
+ *
+ * <p>The account status is a free-form text that a user can set for the own account (e.g. the 'OOO'
+ * string is often used to signal that the user is out-of-office).
+ */
 @Singleton
 public class GetStatus implements RestReadView<AccountResource> {
   @Override
diff --git a/java/com/google/gerrit/server/restapi/account/GetUsername.java b/java/com/google/gerrit/server/restapi/account/GetUsername.java
index 7e58f94..c582039 100644
--- a/java/com/google/gerrit/server/restapi/account/GetUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/GetUsername.java
@@ -22,6 +22,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint to get the username of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/username} requests.
+ */
 @Singleton
 public class GetUsername implements RestReadView<AccountResource> {
   @Inject
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index 353e3f6..beb5e8f 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -41,6 +41,12 @@
 import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to get the project watches of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/watched.projects}
+ * requests.
+ */
 @Singleton
 public class GetWatchedProjects implements RestReadView<AccountResource> {
   private final PermissionBackend permissionBackend;
diff --git a/java/com/google/gerrit/server/restapi/account/Index.java b/java/com/google/gerrit/server/restapi/account/Index.java
index 6ddfc0f4..14c9f40 100644
--- a/java/com/google/gerrit/server/restapi/account/Index.java
+++ b/java/com/google/gerrit/server/restapi/account/Index.java
@@ -29,6 +29,14 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 
+/**
+ * REST endpoint to (re)index an account.
+ *
+ * <p>This REST endpoint handles {@code POST /accounts/<account-identifier>/index} requests.
+ *
+ * <p>If the document of an account in the account index is stale, this REST endpoint can be used to
+ * update the index.
+ */
 @Singleton
 public class Index implements RestModifyView<AccountResource, Input> {
 
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index 5236174..b2859e6 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -41,6 +41,12 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to set project watches for an account.
+ *
+ * <p>This REST endpoint handles {@code POST /accounts/<account-identifier>/watched.projects}
+ * requests.
+ */
 @Singleton
 public class PostWatchedProjects
     implements RestModifyView<AccountResource, List<ProjectWatchInfo>> {
diff --git a/java/com/google/gerrit/server/restapi/account/PutActive.java b/java/com/google/gerrit/server/restapi/account/PutActive.java
index a6ffaa6..a80ab3f 100644
--- a/java/com/google/gerrit/server/restapi/account/PutActive.java
+++ b/java/com/google/gerrit/server/restapi/account/PutActive.java
@@ -27,6 +27,15 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to mark an account as active.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/active} requests.
+ *
+ * <p>Only active accounts can login into Gerrit.
+ *
+ * <p>Marking an account as inactive is handled by {@link DeleteActive}.
+ */
 @RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 @Singleton
 public class PutActive implements RestModifyView<AccountResource, Input> {
diff --git a/java/com/google/gerrit/server/restapi/account/PutAgreement.java b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
index 1663d00..459ff26 100644
--- a/java/com/google/gerrit/server/restapi/account/PutAgreement.java
+++ b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
@@ -42,6 +42,11 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 
+/**
+ * REST endpoint to sign a contributor agreement for an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/agreements} requests.
+ */
 @Singleton
 public class PutAgreement implements RestModifyView<AccountResource, AgreementInput> {
   private final ProjectCache projectCache;
diff --git a/java/com/google/gerrit/server/restapi/account/PutEmail.java b/java/com/google/gerrit/server/restapi/account/PutEmail.java
index 6ee9003..747f848 100644
--- a/java/com/google/gerrit/server/restapi/account/PutEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/PutEmail.java
@@ -21,6 +21,22 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.inject.Singleton;
 
+/**
+ * REST endpoint for updating an existing email address of an account.
+ *
+ * <p>This REST endpoint handles {@code PUT
+ * /accounts/<account-identifier>/emails/<email-identifier>} requests if the specified email address
+ * already exists for the account. If it doesn't exist yet, the request is handled by {@link
+ * CreateEmail}.
+ *
+ * <p>We do not support email address updates via this path, hence this REST endpoint always throws
+ * a {@link ResourceConflictException} which results in a {@code 409 Conflict} response.
+ *
+ * <p>This REST endpoint solely exists to avoid user confusion if they create a new email address
+ * with {@code PUT /accounts/<account-identifier>/emails/<email-identifier>} and then repeat the
+ * same request. Without this REST endpoint the second request would fail with {@code 404 Not
+ * Found}, which would be surprising to the user.
+ */
 @Singleton
 public class PutEmail implements RestModifyView<AccountResource.Email, EmailInput> {
   @Override
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index 36a0c71..11bcf74 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -41,12 +41,23 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to set/delete the password for HTTP access of an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/password.http} and {@code
+ * DELETE /accounts/<account-identifier>/password.http} requests.
+ *
+ * <p>Gerrit only stores the hash of the HTTP password, hence if an HTTP password was set it's not
+ * possible to get it back from Gerrit.
+ */
+@Singleton
 public class PutHttpPassword implements RestModifyView<AccountResource, HttpPasswordInput> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
diff --git a/java/com/google/gerrit/server/restapi/account/PutName.java b/java/com/google/gerrit/server/restapi/account/PutName.java
index d5f6333c..c7496b9 100644
--- a/java/com/google/gerrit/server/restapi/account/PutName.java
+++ b/java/com/google/gerrit/server/restapi/account/PutName.java
@@ -38,6 +38,13 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to set the full name of an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/name} requests.
+ *
+ * <p>Whether a full name can be set depends on whether the used {@link Realm} supports this.
+ */
 @Singleton
 public class PutName implements RestModifyView<AccountResource, NameInput> {
   private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index c781246..32b5ff2 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -45,6 +45,15 @@
 import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to set an email address as preferred email address for an account.
+ *
+ * <p>This REST endpoint handles {@code PUT
+ * /accounts/<account-identifier>/emails/<email-identifier>/preferred} requests.
+ *
+ * <p>Users can only set an email address as preferred that is assigned to their account as external
+ * ID.
+ */
 @Singleton
 public class PutPreferred implements RestModifyView<AccountResource.Email, Input> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/restapi/account/PutStatus.java b/java/com/google/gerrit/server/restapi/account/PutStatus.java
index 7e27489..106f39e 100644
--- a/java/com/google/gerrit/server/restapi/account/PutStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/PutStatus.java
@@ -35,6 +35,14 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to set the status of an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/status} requests.
+ *
+ * <p>The account status is a free-form text that a user can set for the own account (e.g. the 'OOO'
+ * string is often used to signal that the user is out-of-office).
+ */
 @Singleton
 public class PutStatus implements RestModifyView<AccountResource, StatusInput> {
   private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index f9e753c..0d12fd4 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -53,6 +53,14 @@
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
+/**
+ * REST endpoint to query accounts.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/} requests.
+ *
+ * <p>The account queries are parsed by {@link AccountQueryBuilder} and executed by {@link
+ * AccountQueryProcessor}.
+ */
 public class QueryAccounts implements RestReadView<TopLevelResource> {
   private static final int MAX_SUGGEST_RESULTS = 100;
 
diff --git a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
index cf56965..3f25b3b 100644
--- a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
@@ -37,6 +37,18 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
+/**
+ * REST endpoint to set diff preferences for an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/preferences.diff}
+ * requests.
+ *
+ * <p>General preferences can be set by {@link SetPreferences} and edit preferences can be set by
+ * {@link SetEditPreferences}.
+ *
+ * <p>Default diff preferences that apply for all accounts can be set by {@link
+ * com.google.gerrit.server.restapi.config.SetDiffPreferences}.
+ */
 @Singleton
 public class SetDiffPreferences implements RestModifyView<AccountResource, DiffPreferencesInfo> {
   private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
index 085adaa..a15a751 100644
--- a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
@@ -37,6 +37,18 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
+/**
+ * REST endpoint to set edit preferences for an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/preferences.edit}
+ * requests.
+ *
+ * <p>General preferences can be set by {@link SetPreferences} and diff preferences can be set by
+ * {@link SetDiffPreferences}.
+ *
+ * <p>Default edit preferences that apply for all accounts can be set by {@link
+ * com.google.gerrit.server.restapi.config.SetEditPreferences}.
+ */
 @Singleton
 public class SetEditPreferences implements RestModifyView<AccountResource, EditPreferencesInfo> {
 
diff --git a/java/com/google/gerrit/server/restapi/account/SetPreferences.java b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
index 3f2211e..a2b29ae 100644
--- a/java/com/google/gerrit/server/restapi/account/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
@@ -41,6 +41,17 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to set general preferences for an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/preferences} requests.
+ *
+ * <p>Diff preferences can be set by {@link SetDiffPreferences} and edit preferences can be set by
+ * {@link SetEditPreferences}.
+ *
+ * <p>Default general preferences that apply for all accounts can be set by {@link
+ * com.google.gerrit.server.restapi.config.SetPreferences}.
+ */
 @Singleton
 public class SetPreferences implements RestModifyView<AccountResource, GeneralPreferencesInfo> {
   private final Provider<CurrentUser> self;
diff --git a/java/com/google/gerrit/server/restapi/change/GetPatch.java b/java/com/google/gerrit/server/restapi/change/GetPatch.java
index ece8c68..31785be 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPatch.java
@@ -66,6 +66,7 @@
     boolean close = true;
     try {
       final RevWalk rw = new RevWalk(repo);
+      BinaryResult bin = null;
       try {
         final RevCommit commit = rw.parseCommit(rsrc.getPatchSet().commitId());
         RevCommit[] parents = commit.getParents();
@@ -77,7 +78,7 @@
         final RevCommit base = parents[0];
         rw.parseBody(base);
 
-        BinaryResult bin =
+        bin =
             new BinaryResult() {
               @Override
               public void writeTo(OutputStream out) throws IOException {
@@ -135,6 +136,7 @@
       } finally {
         if (close) {
           rw.close();
+          bin.close();
         }
       }
     } finally {
diff --git a/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
index 4a74fe9..03715c9 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
@@ -32,6 +32,18 @@
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * REST endpoint to confirm an email address for an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /config/server/email.confirm} requests.
+ *
+ * <p>When a user registers a new email address for their account (see {@link
+ * com.google.gerrit.server.restapi.account.CreateEmail}) an email with a confirmation link is sent
+ * to that address. When the receiver confirms the email by clicking on the confirmation link, this
+ * REST endpoint is invoked and the email address is added to the account. Confirming an email
+ * address for an account creates an external ID that links the email address to the account. An
+ * email address can only be added to an account if it is not assigned to any other account yet.
+ */
 @Singleton
 public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
   public static class Input {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 203f4a8..07e74d4 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -22,7 +22,6 @@
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
-import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
@@ -57,6 +56,7 @@
 import com.google.common.util.concurrent.AtomicLongMap;
 import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountIndexedCounter;
 import com.google.gerrit.acceptance.ExtensionRegistry;
 import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -89,7 +89,6 @@
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
@@ -103,7 +102,6 @@
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
-import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -122,8 +120,6 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.Emails;
-import com.google.gerrit.server.account.ProjectWatches;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
@@ -140,7 +136,6 @@
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.server.validators.AccountActivationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
@@ -154,7 +149,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -1486,670 +1480,6 @@
   }
 
   @Test
-  public void pushToUserBranch() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-      allUsersRepo.reset("userRef");
-      PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
-      push.to(RefNames.refsUsers(admin.id())).assertOkStatus();
-      accountIndexedCounter.assertReindexOf(admin);
-
-      push = pushFactory.create(admin.newIdent(), allUsersRepo);
-      push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
-      accountIndexedCounter.assertReindexOf(admin);
-    }
-  }
-
-  @Test
-  public void pushToUserBranchForReview() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      String userRefName = RefNames.refsUsers(admin.id());
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, userRefName + ":userRef");
-      allUsersRepo.reset("userRef");
-      PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
-      PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
-      r.assertOkStatus();
-      accountIndexedCounter.assertNoReindex();
-      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRefName);
-      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-      gApi.changes().id(r.getChangeId()).current().submit();
-      accountIndexedCounter.assertReindexOf(admin);
-
-      push = pushFactory.create(admin.newIdent(), allUsersRepo);
-      r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
-      r.assertOkStatus();
-      accountIndexedCounter.assertNoReindex();
-      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRefName);
-      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-      gApi.changes().id(r.getChangeId()).current().submit();
-      accountIndexedCounter.assertReindexOf(admin);
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewAndSubmit() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      String userRef = RefNames.refsUsers(admin.id());
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  admin.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  ac.toText())
-              .to(MagicBranch.NEW_CHANGE + userRef);
-      r.assertOkStatus();
-      accountIndexedCounter.assertNoReindex();
-      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
-
-      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-      gApi.changes().id(r.getChangeId()).current().submit();
-      accountIndexedCounter.assertReindexOf(admin);
-
-      AccountInfo info = gApi.accounts().self().get();
-      assertThat(info.email).isEqualTo(admin.email());
-      assertThat(info.name).isEqualTo(admin.fullName());
-      assertThat(info.status).isEqualTo("out-of-office");
-    }
-  }
-
-  @Test
-  public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
-      throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-      String userRef = RefNames.refsUsers(foo.id());
-      accountIndexedCounter.clear();
-
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      String email = "some.email@example.com";
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  foo.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  ac.toText())
-              .to(MagicBranch.NEW_CHANGE + userRef);
-      r.assertOkStatus();
-      accountIndexedCounter.assertNoReindex();
-      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
-
-      requestScopeOperations.setApiUser(foo.id());
-      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-      gApi.changes().id(r.getChangeId()).current().submit();
-
-      accountIndexedCounter.assertReindexOf(foo);
-
-      AccountInfo info = gApi.accounts().self().get();
-      assertThat(info.email).isEqualTo(email);
-      assertThat(info.name).isEqualTo(foo.fullName());
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfConfigIsInvalid()
-      throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      String userRef = RefNames.refsUsers(admin.id());
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  admin.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  "invalid config")
-              .to(MagicBranch.NEW_CHANGE + userRef);
-      r.assertOkStatus();
-      accountIndexedCounter.assertNoReindex();
-      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
-
-      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-      ResourceConflictException thrown =
-          assertThrows(
-              ResourceConflictException.class,
-              () -> gApi.changes().id(r.getChangeId()).current().submit());
-      assertThat(thrown)
-          .hasMessageThat()
-          .contains(
-              String.format(
-                  "invalid account configuration: commit '%s' has an invalid '%s' file for account"
-                      + " '%s': Invalid config file %s in commit %s",
-                  r.getCommit().name(),
-                  AccountProperties.ACCOUNT_CONFIG,
-                  admin.id(),
-                  AccountProperties.ACCOUNT_CONFIG,
-                  r.getCommit().name()));
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfPreferredEmailIsInvalid()
-      throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      String userRef = RefNames.refsUsers(admin.id());
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      String noEmail = "no.email";
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  admin.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  ac.toText())
-              .to(MagicBranch.NEW_CHANGE + userRef);
-      r.assertOkStatus();
-      accountIndexedCounter.assertNoReindex();
-      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
-
-      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-      ResourceConflictException thrown =
-          assertThrows(
-              ResourceConflictException.class,
-              () -> gApi.changes().id(r.getChangeId()).current().submit());
-      assertThat(thrown)
-          .hasMessageThat()
-          .contains(
-              String.format(
-                  "invalid account configuration: invalid preferred email '%s' for account '%s'",
-                  noEmail, admin.id()));
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfOwnAccountIsDeactivated()
-      throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      String userRef = RefNames.refsUsers(admin.id());
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  admin.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  ac.toText())
-              .to(MagicBranch.NEW_CHANGE + userRef);
-      r.assertOkStatus();
-      accountIndexedCounter.assertNoReindex();
-      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
-
-      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-      ResourceConflictException thrown =
-          assertThrows(
-              ResourceConflictException.class,
-              () -> gApi.changes().id(r.getChangeId()).current().submit());
-      assertThat(thrown)
-          .hasMessageThat()
-          .contains("invalid account configuration: cannot deactivate own account");
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchForReviewDeactivateOtherAccount() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      projectOperations
-          .allProjectsForUpdate()
-          .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
-          .update();
-
-      TestAccount foo = accountCreator.create(name("foo"));
-      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
-      String userRef = RefNames.refsUsers(foo.id());
-      accountIndexedCounter.clear();
-
-      projectOperations
-          .project(allUsers)
-          .forUpdate()
-          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
-          .add(allowLabel("Code-Review").ref(userRef).group(adminGroupUuid()).range(-2, 2))
-          .add(allow(Permission.SUBMIT).ref(userRef).group(adminGroupUuid()))
-          .update();
-
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  admin.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  ac.toText())
-              .to(MagicBranch.NEW_CHANGE + userRef);
-      r.assertOkStatus();
-      accountIndexedCounter.assertNoReindex();
-      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
-
-      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
-      gApi.changes().id(r.getChangeId()).current().submit();
-      accountIndexedCounter.assertReindexOf(foo);
-
-      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
-    }
-  }
-
-  @Test
-  public void pushWatchConfigToUserBranch() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      Config wc = new Config();
-      wc.setString(
-          ProjectWatches.PROJECT,
-          project.get(),
-          ProjectWatches.KEY_NOTIFY,
-          ProjectWatches.NotifyValue.create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
-      PushOneCommit push =
-          pushFactory.create(
-              admin.newIdent(),
-              allUsersRepo,
-              "Add project watch",
-              ProjectWatches.WATCH_CONFIG,
-              wc.toText());
-      push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
-      accountIndexedCounter.assertReindexOf(admin);
-
-      String invalidNotifyValue = "]invalid[";
-      wc.setString(
-          ProjectWatches.PROJECT, project.get(), ProjectWatches.KEY_NOTIFY, invalidNotifyValue);
-      push =
-          pushFactory.create(
-              admin.newIdent(),
-              allUsersRepo,
-              "Add invalid project watch",
-              ProjectWatches.WATCH_CONFIG,
-              wc.toText());
-      PushOneCommit.Result r = push.to(RefNames.REFS_USERS_SELF);
-      r.assertErrorStatus("invalid account configuration");
-      r.assertMessage(
-          String.format(
-              "%s: Invalid project watch of account %d for project %s: %s",
-              ProjectWatches.WATCH_CONFIG, admin.id().get(), project.get(), invalidNotifyValue));
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranch() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestAccount oooUser = accountCreator.create("away", "away@mail.invalid", "Ambrose Way");
-      requestScopeOperations.setApiUser(oooUser.id());
-
-      // Must clone as oooUser to ensure the push is allowed.
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, oooUser);
-      fetch(allUsersRepo, RefNames.refsUsers(oooUser.id()) + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
-
-      accountIndexedCounter.clear();
-      pushFactory
-          .create(
-              oooUser.newIdent(),
-              allUsersRepo,
-              "Update account config",
-              AccountProperties.ACCOUNT_CONFIG,
-              ac.toText())
-          .to(RefNames.refsUsers(oooUser.id()))
-          .assertOkStatus();
-
-      accountIndexedCounter.assertReindexOf(oooUser);
-
-      AccountInfo info = gApi.accounts().self().get();
-      assertThat(info.email).isEqualTo(oooUser.email());
-      assertThat(info.name).isEqualTo(oooUser.fullName());
-      assertThat(info.status).isEqualTo("out-of-office");
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchIsRejectedIfConfigIsInvalid() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  admin.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  "invalid config")
-              .to(RefNames.REFS_USERS_SELF);
-      r.assertErrorStatus("invalid account configuration");
-      r.assertMessage(
-          String.format(
-              "commit '%s' has an invalid '%s' file for account '%s':"
-                  + " Invalid config file %s in commit %s",
-              r.getCommit().name(),
-              AccountProperties.ACCOUNT_CONFIG,
-              admin.id(),
-              AccountProperties.ACCOUNT_CONFIG,
-              r.getCommit().name()));
-      accountIndexedCounter.assertNoReindex();
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchIsRejectedIfPreferredEmailIsInvalid() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      String noEmail = "no.email";
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  admin.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  ac.toText())
-              .to(RefNames.REFS_USERS_SELF);
-      r.assertErrorStatus("invalid account configuration");
-      r.assertMessage(
-          String.format("invalid preferred email '%s' for account '%s'", noEmail, admin.id()));
-      accountIndexedCounter.assertNoReindex();
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchInvalidPreferredEmailButNotChanged() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-      String userRef = RefNames.refsUsers(foo.id());
-
-      String noEmail = "no.email";
-      accountsUpdateProvider
-          .get()
-          .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(noEmail));
-      accountIndexedCounter.clear();
-
-      projectOperations
-          .project(allUsers)
-          .forUpdate()
-          .add(allow(Permission.PUSH).ref(userRef).group(REGISTERED_USERS))
-          .update();
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      String status = "in vacation";
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, status);
-
-      pushFactory
-          .create(
-              foo.newIdent(),
-              allUsersRepo,
-              "Update account config",
-              AccountProperties.ACCOUNT_CONFIG,
-              ac.toText())
-          .to(userRef)
-          .assertOkStatus();
-      accountIndexedCounter.assertReindexOf(foo);
-
-      AccountInfo info = gApi.accounts().id(foo.id().get()).get();
-      assertThat(info.email).isEqualTo(noEmail);
-      assertThat(info.name).isEqualTo(foo.fullName());
-      assertThat(info.status).isEqualTo(status);
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchIfPreferredEmailDoesNotExistAsExtId() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
-      String userRef = RefNames.refsUsers(foo.id());
-      accountIndexedCounter.clear();
-
-      projectOperations
-          .project(allUsers)
-          .forUpdate()
-          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
-          .update();
-
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      String email = "some.email@example.com";
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
-
-      pushFactory
-          .create(
-              foo.newIdent(),
-              allUsersRepo,
-              "Update account config",
-              AccountProperties.ACCOUNT_CONFIG,
-              ac.toText())
-          .to(userRef)
-          .assertOkStatus();
-      accountIndexedCounter.assertReindexOf(foo);
-
-      AccountInfo info = gApi.accounts().id(foo.id().get()).get();
-      assertThat(info.email).isEqualTo(email);
-      assertThat(info.name).isEqualTo(foo.fullName());
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchIsRejectedIfOwnAccountIsDeactivated() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
-
-      PushOneCommit.Result r =
-          pushFactory
-              .create(
-                  admin.newIdent(),
-                  allUsersRepo,
-                  "Update account config",
-                  AccountProperties.ACCOUNT_CONFIG,
-                  ac.toText())
-              .to(RefNames.REFS_USERS_SELF);
-      r.assertErrorStatus("invalid account configuration");
-      r.assertMessage("cannot deactivate own account");
-      accountIndexedCounter.assertNoReindex();
-    }
-  }
-
-  @Test
-  public void pushAccountConfigToUserBranchDeactivateOtherAccount() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      projectOperations
-          .allProjectsForUpdate()
-          .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
-          .update();
-
-      TestAccount foo = accountCreator.create(name("foo"));
-      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
-      String userRef = RefNames.refsUsers(foo.id());
-      accountIndexedCounter.clear();
-
-      projectOperations
-          .project(allUsers)
-          .forUpdate()
-          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
-          .update();
-
-      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-      fetch(allUsersRepo, userRef + ":userRef");
-      allUsersRepo.reset("userRef");
-
-      Config ac = getAccountConfig(allUsersRepo);
-      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
-
-      pushFactory
-          .create(
-              admin.newIdent(),
-              allUsersRepo,
-              "Update account config",
-              AccountProperties.ACCOUNT_CONFIG,
-              ac.toText())
-          .to(userRef)
-          .assertOkStatus();
-      accountIndexedCounter.assertReindexOf(foo);
-
-      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
-    }
-  }
-
-  @Test
-  public void cannotCreateUserBranch() throws Exception {
-    projectOperations
-        .project(allUsers)
-        .forUpdate()
-        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
-        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
-        .update();
-
-    String userRef = RefNames.refsUsers(Account.id(seq.nextAccountId()));
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
-    r.assertErrorStatus();
-    assertThat(r.getMessage()).contains("Not allowed to create user branch.");
-
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      assertThat(repo.exactRef(userRef)).isNull();
-    }
-  }
-
-  @Test
-  public void createUserBranchWithAccessDatabaseCapability() throws Exception {
-    projectOperations
-        .allProjectsForUpdate()
-        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
-        .update();
-    projectOperations
-        .project(allUsers)
-        .forUpdate()
-        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
-        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
-        .update();
-
-    String userRef = RefNames.refsUsers(Account.id(seq.nextAccountId()));
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef).assertOkStatus();
-
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      assertThat(repo.exactRef(userRef)).isNotNull();
-    }
-  }
-
-  @Test
-  public void cannotCreateNonUserBranchUnderRefsUsersWithAccessDatabaseCapability()
-      throws Exception {
-    projectOperations
-        .allProjectsForUpdate()
-        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
-        .update();
-    projectOperations
-        .project(allUsers)
-        .forUpdate()
-        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
-        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
-        .update();
-
-    String userRef = RefNames.REFS_USERS + "foo";
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
-    r.assertErrorStatus();
-    assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/.");
-
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      assertThat(repo.exactRef(userRef)).isNull();
-    }
-  }
-
-  @Test
   public void createDefaultUserBranch() throws Exception {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNull();
@@ -3501,62 +2831,6 @@
     assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.id());
   }
 
-  private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
-    Config ac = new Config();
-    try (TreeWalk tw =
-        TreeWalk.forPath(
-            allUsersRepo.getRepository(),
-            AccountProperties.ACCOUNT_CONFIG,
-            getHead(allUsersRepo.getRepository(), "HEAD").getTree())) {
-      assertThat(tw).isNotNull();
-      ac.fromText(
-          new String(
-              allUsersRepo
-                  .getRevWalk()
-                  .getObjectReader()
-                  .open(tw.getObjectId(0), OBJ_BLOB)
-                  .getBytes(),
-              UTF_8));
-    }
-    return ac;
-  }
-
-  /** Checks if an account is indexed the correct number of times. */
-  private static class AccountIndexedCounter implements AccountIndexedListener {
-    private final AtomicLongMap<Integer> countsByAccount = AtomicLongMap.create();
-
-    @Override
-    public void onAccountIndexed(int id) {
-      countsByAccount.incrementAndGet(id);
-    }
-
-    void clear() {
-      countsByAccount.clear();
-    }
-
-    void assertReindexOf(TestAccount testAccount) {
-      assertReindexOf(testAccount, 1);
-    }
-
-    void assertReindexOf(AccountInfo accountInfo) {
-      assertReindexOf(Account.id(accountInfo._accountId), 1);
-    }
-
-    void assertReindexOf(TestAccount testAccount, long expectedCount) {
-      assertThat(countsByAccount.asMap()).containsExactly(testAccount.id().get(), expectedCount);
-      clear();
-    }
-
-    void assertReindexOf(Account.Id accountId, long expectedCount) {
-      assertThat(countsByAccount.asMap()).containsEntry(accountId.get(), expectedCount);
-      countsByAccount.remove(accountId.get());
-    }
-
-    void assertNoReindex() {
-      assertThat(countsByAccount.asMap()).isEmpty();
-    }
-  }
-
   private static class RefUpdateCounter implements GitReferenceUpdatedListener {
     private final AtomicLongMap<String> countsByProjectRefs = AtomicLongMap.create();
 
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index a6f7aa0..51dee72 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -2056,7 +2056,7 @@
 
     /* Assert the tags - PS#2 comments do not have tags, PS#3 upload is autogenerated */
     List<String> messagesTags = allMessages.stream().map(m -> m.tag).collect(toList());
-    ;
+
     assertThat(messagesTags.get(2)).isEqualTo("autogenerated:gerrit:newPatchSet");
     assertThat(messagesTags.get(3)).isNull();
 
diff --git a/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java b/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java
new file mode 100644
index 0000000..c48eb3b
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java
@@ -0,0 +1,764 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountIndexedCounter;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountProperties;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.ProjectWatches;
+import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.EnumSet;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Test;
+
+/** Tests account behavior when users push to accounts refs. */
+public class PushAccountIT extends AbstractDaemonTest {
+
+  @ConfigSuite.Default
+  public static Config enableSignedPushConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("receive", null, "enableSignedPush", true);
+
+    // Disable the staleness checker so that tests that verify the number of expected index events
+    // are stable.
+    cfg.setBoolean("index", null, "autoReindexIfStale", false);
+
+    return cfg;
+  }
+
+  @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private Sequences seq;
+
+  @Test
+  public void pushToUserBranch() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+      PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
+      push.to(RefNames.refsUsers(admin.id())).assertOkStatus();
+      accountIndexedCounter.assertReindexOf(admin);
+
+      push = pushFactory.create(admin.newIdent(), allUsersRepo);
+      push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
+      accountIndexedCounter.assertReindexOf(admin);
+    }
+  }
+
+  @Test
+  public void pushToUserBranchForReview() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRefName = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRefName + ":userRef");
+      allUsersRepo.reset("userRef");
+      PushOneCommit push = pushFactory.create(admin.newIdent(), allUsersRepo);
+      PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRefName);
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+      accountIndexedCounter.assertReindexOf(admin);
+
+      push = pushFactory.create(admin.newIdent(), allUsersRepo);
+      r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRefName);
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+      accountIndexedCounter.assertReindexOf(admin);
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewAndSubmit() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRef = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+      accountIndexedCounter.assertReindexOf(admin);
+
+      AccountInfo info = gApi.accounts().self().get();
+      assertThat(info.email).isEqualTo(admin.email());
+      assertThat(info.name).isEqualTo(admin.fullName());
+      assertThat(info.status).isEqualTo("out-of-office");
+    }
+  }
+
+  @Test
+  public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
+      throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
+      String userRef = RefNames.refsUsers(foo.id());
+      accountIndexedCounter.clear();
+
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String email = "some.email@example.com";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  foo.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      requestScopeOperations.setApiUser(foo.id());
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+
+      accountIndexedCounter.assertReindexOf(foo);
+
+      AccountInfo info = gApi.accounts().self().get();
+      assertThat(info.email).isEqualTo(email);
+      assertThat(info.name).isEqualTo(foo.fullName());
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfConfigIsInvalid()
+      throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRef = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  "invalid config")
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.changes().id(r.getChangeId()).current().submit());
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains(
+              String.format(
+                  "invalid account configuration: commit '%s' has an invalid '%s' file for account"
+                      + " '%s': Invalid config file %s in commit %s",
+                  r.getCommit().name(),
+                  AccountProperties.ACCOUNT_CONFIG,
+                  admin.id(),
+                  AccountProperties.ACCOUNT_CONFIG,
+                  r.getCommit().name()));
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfPreferredEmailIsInvalid()
+      throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRef = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String noEmail = "no.email";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.changes().id(r.getChangeId()).current().submit());
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains(
+              String.format(
+                  "invalid account configuration: invalid preferred email '%s' for account '%s'",
+                  noEmail, admin.id()));
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfOwnAccountIsDeactivated()
+      throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      String userRef = RefNames.refsUsers(admin.id());
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.changes().id(r.getChangeId()).current().submit());
+      assertThat(thrown)
+          .hasMessageThat()
+          .contains("invalid account configuration: cannot deactivate own account");
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchForReviewDeactivateOtherAccount() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      projectOperations
+          .allProjectsForUpdate()
+          .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+          .update();
+
+      TestAccount foo = accountCreator.create(name("foo"));
+      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
+      String userRef = RefNames.refsUsers(foo.id());
+      accountIndexedCounter.clear();
+
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+          .add(allowLabel("Code-Review").ref(userRef).group(adminGroupUuid()).range(-2, 2))
+          .add(allow(Permission.SUBMIT).ref(userRef).group(adminGroupUuid()))
+          .update();
+
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(MagicBranch.NEW_CHANGE + userRef);
+      r.assertOkStatus();
+      accountIndexedCounter.assertNoReindex();
+      assertThat(r.getChange().change().getDest().branch()).isEqualTo(userRef);
+
+      gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+      gApi.changes().id(r.getChangeId()).current().submit();
+      accountIndexedCounter.assertReindexOf(foo);
+
+      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
+    }
+  }
+
+  @Test
+  public void pushWatchConfigToUserBranch() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config wc = new Config();
+      wc.setString(
+          ProjectWatches.PROJECT,
+          project.get(),
+          ProjectWatches.KEY_NOTIFY,
+          ProjectWatches.NotifyValue.create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
+      PushOneCommit push =
+          pushFactory.create(
+              admin.newIdent(),
+              allUsersRepo,
+              "Add project watch",
+              ProjectWatches.WATCH_CONFIG,
+              wc.toText());
+      push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
+      accountIndexedCounter.assertReindexOf(admin);
+
+      String invalidNotifyValue = "]invalid[";
+      wc.setString(
+          ProjectWatches.PROJECT, project.get(), ProjectWatches.KEY_NOTIFY, invalidNotifyValue);
+      push =
+          pushFactory.create(
+              admin.newIdent(),
+              allUsersRepo,
+              "Add invalid project watch",
+              ProjectWatches.WATCH_CONFIG,
+              wc.toText());
+      PushOneCommit.Result r = push.to(RefNames.REFS_USERS_SELF);
+      r.assertErrorStatus("invalid account configuration");
+      r.assertMessage(
+          String.format(
+              "%s: Invalid project watch of account %d for project %s: %s",
+              ProjectWatches.WATCH_CONFIG, admin.id().get(), project.get(), invalidNotifyValue));
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranch() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount oooUser = accountCreator.create("away", "away@mail.invalid", "Ambrose Way");
+      requestScopeOperations.setApiUser(oooUser.id());
+
+      // Must clone as oooUser to ensure the push is allowed.
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, oooUser);
+      fetch(allUsersRepo, RefNames.refsUsers(oooUser.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
+
+      accountIndexedCounter.clear();
+      pushFactory
+          .create(
+              oooUser.newIdent(),
+              allUsersRepo,
+              "Update account config",
+              AccountProperties.ACCOUNT_CONFIG,
+              ac.toText())
+          .to(RefNames.refsUsers(oooUser.id()))
+          .assertOkStatus();
+
+      accountIndexedCounter.assertReindexOf(oooUser);
+
+      AccountInfo info = gApi.accounts().self().get();
+      assertThat(info.email).isEqualTo(oooUser.email());
+      assertThat(info.name).isEqualTo(oooUser.fullName());
+      assertThat(info.status).isEqualTo("out-of-office");
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchIsRejectedIfConfigIsInvalid() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  "invalid config")
+              .to(RefNames.REFS_USERS_SELF);
+      r.assertErrorStatus("invalid account configuration");
+      r.assertMessage(
+          String.format(
+              "commit '%s' has an invalid '%s' file for account '%s':"
+                  + " Invalid config file %s in commit %s",
+              r.getCommit().name(),
+              AccountProperties.ACCOUNT_CONFIG,
+              admin.id(),
+              AccountProperties.ACCOUNT_CONFIG,
+              r.getCommit().name()));
+      accountIndexedCounter.assertNoReindex();
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchIsRejectedIfPreferredEmailIsInvalid() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String noEmail = "no.email";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(RefNames.REFS_USERS_SELF);
+      r.assertErrorStatus("invalid account configuration");
+      r.assertMessage(
+          String.format("invalid preferred email '%s' for account '%s'", noEmail, admin.id()));
+      accountIndexedCounter.assertNoReindex();
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchInvalidPreferredEmailButNotChanged() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
+      String userRef = RefNames.refsUsers(foo.id());
+
+      String noEmail = "no.email";
+      accountsUpdateProvider
+          .get()
+          .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(noEmail));
+      accountIndexedCounter.clear();
+
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref(userRef).group(REGISTERED_USERS))
+          .update();
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String status = "in vacation";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, status);
+
+      pushFactory
+          .create(
+              foo.newIdent(),
+              allUsersRepo,
+              "Update account config",
+              AccountProperties.ACCOUNT_CONFIG,
+              ac.toText())
+          .to(userRef)
+          .assertOkStatus();
+      accountIndexedCounter.assertReindexOf(foo);
+
+      AccountInfo info = gApi.accounts().id(foo.id().get()).get();
+      assertThat(info.email).isEqualTo(noEmail);
+      assertThat(info.name).isEqualTo(foo.fullName());
+      assertThat(info.status).isEqualTo(status);
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchIfPreferredEmailDoesNotExistAsExtId() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
+      String userRef = RefNames.refsUsers(foo.id());
+      accountIndexedCounter.clear();
+
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+          .update();
+
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      String email = "some.email@example.com";
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
+
+      pushFactory
+          .create(
+              foo.newIdent(),
+              allUsersRepo,
+              "Update account config",
+              AccountProperties.ACCOUNT_CONFIG,
+              ac.toText())
+          .to(userRef)
+          .assertOkStatus();
+      accountIndexedCounter.assertReindexOf(foo);
+
+      AccountInfo info = gApi.accounts().id(foo.id().get()).get();
+      assertThat(info.email).isEqualTo(email);
+      assertThat(info.name).isEqualTo(foo.fullName());
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchIsRejectedIfOwnAccountIsDeactivated() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
+
+      PushOneCommit.Result r =
+          pushFactory
+              .create(
+                  admin.newIdent(),
+                  allUsersRepo,
+                  "Update account config",
+                  AccountProperties.ACCOUNT_CONFIG,
+                  ac.toText())
+              .to(RefNames.REFS_USERS_SELF);
+      r.assertErrorStatus("invalid account configuration");
+      r.assertMessage("cannot deactivate own account");
+      accountIndexedCounter.assertNoReindex();
+    }
+  }
+
+  @Test
+  public void pushAccountConfigToUserBranchDeactivateOtherAccount() throws Exception {
+    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+      projectOperations
+          .allProjectsForUpdate()
+          .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+          .update();
+
+      TestAccount foo = accountCreator.create(name("foo"));
+      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
+      String userRef = RefNames.refsUsers(foo.id());
+      accountIndexedCounter.clear();
+
+      projectOperations
+          .project(allUsers)
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+          .update();
+
+      TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+      fetch(allUsersRepo, userRef + ":userRef");
+      allUsersRepo.reset("userRef");
+
+      Config ac = getAccountConfig(allUsersRepo);
+      ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
+
+      pushFactory
+          .create(
+              admin.newIdent(),
+              allUsersRepo,
+              "Update account config",
+              AccountProperties.ACCOUNT_CONFIG,
+              ac.toText())
+          .to(userRef)
+          .assertOkStatus();
+      accountIndexedCounter.assertReindexOf(foo);
+
+      assertThat(gApi.accounts().id(foo.id().get()).getActive()).isFalse();
+    }
+  }
+
+  @Test
+  public void cannotCreateNonUserBranchUnderRefsUsersWithAccessDatabaseCapability()
+      throws Exception {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
+
+    String userRef = RefNames.REFS_USERS + "foo";
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
+    r.assertErrorStatus();
+    assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/.");
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNull();
+    }
+  }
+
+  @Test
+  public void cannotCreateUserBranch() throws Exception {
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
+
+    String userRef = RefNames.refsUsers(Account.id(seq.nextAccountId()));
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef);
+    r.assertErrorStatus();
+    assertThat(r.getMessage()).contains("Not allowed to create user branch.");
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNull();
+    }
+  }
+
+  @Test
+  public void createUserBranchWithAccessDatabaseCapability() throws Exception {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
+
+    String userRef = RefNames.refsUsers(Account.id(seq.nextAccountId()));
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    pushFactory.create(admin.newIdent(), allUsersRepo).to(userRef).assertOkStatus();
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNotNull();
+    }
+  }
+
+  private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
+    Config ac = new Config();
+    try (TreeWalk tw =
+        TreeWalk.forPath(
+            allUsersRepo.getRepository(),
+            AccountProperties.ACCOUNT_CONFIG,
+            getHead(allUsersRepo.getRepository(), "HEAD").getTree())) {
+      assertThat(tw).isNotNull();
+      ac.fromText(
+          new String(
+              allUsersRepo
+                  .getRevWalk()
+                  .getObjectReader()
+                  .open(tw.getObjectId(0), OBJ_BLOB)
+                  .getBytes(),
+              UTF_8));
+    }
+    return ac;
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
index 74173a6..b65f4d2 100644
--- a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
+++ b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.schema;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.schema.AllProjectsInput.getDefaultCodeReviewLabel;
 import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.assertSectionEquivalent;
 import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.assertTwoConfigsEquivalent;
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 2cb6242..e645972 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -53,12 +53,6 @@
 
 Then visit http://localhost:8081
 
-## Local UI, Test Data
-
-```sh
-./polygerrit-ui/run-server.sh --plugins=plugins/my_plugin/static/my_plugin.js,plugins/my_plugin/static/my_plugin.html
-```
-
 This method is based on a
 [simple hand-written Go webserver](https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/server.go).
 Mostly it just switches between serving files locally and proxying the real
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 73b8261..19d08ff 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -49,6 +49,7 @@
       },
     },
 
+    /** @override */
     detached() {
       this._handleHideTooltip();
     },
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 d4e2c72..8a1ed87 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
@@ -540,6 +540,7 @@
         }
       },
 
+      /** @override */
       attached() {
         const shortcuts = shortcutManager.attachHost(this);
         if (!shortcuts) { return; }
@@ -559,6 +560,7 @@
         }
       },
 
+      /** @override */
       detached() {
         if (shortcutManager.detachHost(this)) {
           this.removeOwnKeyBindings();
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index 8c17c49..a421043 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -40,6 +40,7 @@
   /**
    * @appliesMixin Gerrit.AccessMixin
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrAccessSection extends Polymer.mixinBehaviors( [
     Gerrit.AccessBehavior,
@@ -84,6 +85,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('access-saved',
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 5a62584..96008b7 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.ListViewMixin
+   * @extends Polymer.Element
    */
   class GrAdminGroupList extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -77,6 +78,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getCreateGroupCapability();
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 afc373d..e300c90 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
@@ -23,6 +23,7 @@
    * @appliesMixin Gerrit.AdminNavMixin
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrAdminView extends Polymer.mixinBehaviors( [
     Gerrit.AdminNavBehavior,
@@ -82,6 +83,7 @@
       ];
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.reload();
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
index 7400d2a..3fde410 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -25,6 +25,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrConfirmDeleteItemDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index eb96794..3b85304 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -24,6 +24,7 @@
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrCreateChangeDialog extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -64,6 +65,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       if (!this.repoName) { return Promise.resolve(); }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
index 4072986..8a4edab 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrCreateGroupDialog extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
index cf59af8..2d6b4aa 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -25,6 +25,7 @@
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrCreatePointerDialog extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
index a6da3a1..290f025 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrCreateRepoDialog extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
index 57b9689..11517d6 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -22,6 +22,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.ListViewMixin
+   * @extends Polymer.Element
    */
   class GrGroupAuditLog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -42,11 +43,13 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.fire('title-change', {title: 'Audit Log'});
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._getAuditLogs();
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index f3a7ec7..8c29f73 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -27,6 +27,7 @@
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrGroupMembers extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -76,6 +77,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._loadGroupDetails();
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 6597b4d..42846f4 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -32,6 +32,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrGroup extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -105,6 +106,7 @@
       ];
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._loadGroup();
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 040eabf..e719154 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -37,6 +37,7 @@
    * Fired when a permission that was previously added was removed.
    *
    * @event added-permission-removed
+   * @extends Polymer.Element
    */
   class GrPermission extends Polymer.mixinBehaviors( [
     Gerrit.AccessBehavior,
@@ -94,12 +95,14 @@
       ];
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('access-saved',
           () => this._handleAccessSaved());
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._setupValues();
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
index c0ad9d42..92a8655 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrPluginConfigArrayEditor extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
index 262f3d5..5dd6ec2 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.ListViewMixin
+   * @extends Polymer.Element
    */
   class GrPluginList extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -74,6 +75,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.fire('title-change', {title: 'Plugins'});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index bffad1c..b350702 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -72,6 +72,7 @@
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrRepoAccess extends Polymer.mixinBehaviors( [
     Gerrit.AccessBehavior,
@@ -128,6 +129,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('access-modified',
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
index 48e4c01..622bfe4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrRepoCommand extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index 558f004..80b187a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -28,6 +28,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrRepoCommands extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -50,6 +51,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._loadRepo();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
index 00611b6..8e09263 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrRepoDashboards extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
index 109aa10..ccfdfc6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -28,6 +28,7 @@
    * @appliesMixin Gerrit.ListViewMixin
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrRepoDetailList extends Polymer.mixinBehaviors( [
     Gerrit.ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
index 3e706b5..c509717 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.ListViewMixin
+   * @extends Polymer.Element
    */
   class GrRepoList extends Polymer.mixinBehaviors( [
     Gerrit.ListViewBehavior,
@@ -78,6 +79,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getCreateRepoCapability();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
index 404f93c..7368eb8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.RepoPluginConfigMixin
+   * @extends Polymer.Element
    */
   class GrRepoPluginConfig extends Polymer.mixinBehaviors( [
     Gerrit.RepoPluginConfig,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index a2097f9..f6328de 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -53,6 +53,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrRepo extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -123,6 +124,7 @@
       ];
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._loadRepo();
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index 24e95b6..ac98d33 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -69,6 +69,7 @@
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrRuleEditor extends Polymer.mixinBehaviors( [
     Gerrit.AccessBehavior,
@@ -118,12 +119,14 @@
       ];
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('access-saved',
           () => this._handleAccessSaved());
     }
 
+    /** @override */
     ready() {
       super.ready();
       // Called on ready rather than the observer because when new rules are
@@ -132,6 +135,7 @@
       this._setupValues(this.rule);
     }
 
+    /** @override */
     attached() {
       super.attached();
       if (!this.rule) { return; } // Check needed for test purposes.
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 9ce5e46..013b44b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -30,6 +30,7 @@
    * @appliesMixin Gerrit.PathListMixin
    * @appliesMixin Gerrit.RESTClientMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrChangeListItem extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -74,6 +75,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       Gerrit.awaitPluginsLoaded().then(() => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 4e08b5c..e3a3b31 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -33,6 +33,7 @@
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrChangeListView extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -133,6 +134,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('next-page',
@@ -141,6 +143,7 @@
           () => this._handlePreviousPage());
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._loadPreferences();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index bde400b..8fd4214 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -29,6 +29,7 @@
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Gerrit.RESTClientMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrChangeList extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -135,17 +136,20 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('keydown',
           e => this._scopedKeydownHandler(e));
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._ensureAttribute('tabindex', 0);
     }
 
+    /** @override */
     attached() {
       super.attached();
       Gerrit.awaitPluginsLoaded().then(() => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
index e6caacb..64d2486 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrCreateChangeHelp extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
index 303128b..7abd784 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -23,6 +23,7 @@
     PUSH_PREFIX: 'git push origin HEAD:refs/for/',
   };
 
+  /** @extends Polymer.Element */
   class GrCreateCommandsDialog extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
index cbc7bcd..35f7450 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -22,6 +22,7 @@
    * name and the branch name.
    *
    * @event confirm
+   * @extends Polymer.Element
    */
   class GrCreateDestinationDialog extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index 05f1abe..d0e1db2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -22,6 +22,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrDashboardView extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -94,6 +95,7 @@
       );
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._loadPreferences();
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
index 96dcb98..de0a56e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrEmbedDashboard extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
index d4bd1df..c0e472a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrRepoHeader extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index 40f0b73..2fc8170 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -17,6 +17,9 @@
 (function() {
   'use strict';
 
+  /**
+   * @extends Polymer.Element
+   */
   class GrUserHeader extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 1034389..2c4ca82 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -196,6 +196,7 @@
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrChangeActions extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -432,6 +433,7 @@
       ];
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('fullscreen-overlay-opened',
@@ -440,6 +442,7 @@
           () => this._handleShowBackgroundContent());
     }
 
+    /** @override */
     ready() {
       super.ready();
       this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index e13b462..3237d72 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -50,6 +50,7 @@
 
   /**
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrChangeMetadata extends Polymer.mixinBehaviors( [
     Gerrit.RESTClientBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index 346fee3..a413c6f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrChangeRequirements extends Polymer.mixinBehaviors( [
     Gerrit.RESTClientBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index cbdd83f..471560d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -93,6 +93,9 @@
         flex: 1;
         font-size: var(--font-size-h3);
       }
+      .changeNumberColon {
+        color: transparent;
+      }
       .headerTitle .headerSubject {
         font-weight: var(--font-weight-bold);
       }
@@ -101,6 +104,7 @@
       }
       gr-change-star {
         margin-right: var(--spacing-xs);
+        margin-left: var(--spacing-l);
       }
       gr-reply-dialog {
         width: 60em;
@@ -190,8 +194,6 @@
         max-height: 36em;
         overflow: hidden;
       }
-      #relatedChanges {
-      }
       #relatedChanges.collapsed {
         margin-bottom: var(--spacing-l);
         max-height: var(--relation-chain-max-height, 2em);
@@ -364,11 +366,6 @@
         hidden$="{{_loading}}">
       <div class$="[[_computeHeaderClass(_editMode)]]">
         <div class="headerTitle">
-          <gr-change-star
-              id="changeStar"
-              change="{{_change}}"
-              on-toggle-star="_handleToggleStar"
-              hidden$="[[!_loggedIn]]"></gr-change-star>
           <div class="changeStatuses">
             <template is="dom-repeat" items="[[_changeStatuses]]" as="status">
               <gr-change-status
@@ -387,11 +384,20 @@
                   server-config="[[_serverConfig]]"></gr-commit-info>
             </template>
           </div>
-          <span class="separator"></span>
+          <gr-change-star
+              id="changeStar"
+              change="{{_change}}"
+              on-toggle-star="_handleToggleStar"
+              hidden$="[[!_loggedIn]]"></gr-change-star>
+
           <a aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]"
               href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a>
-          <pre>: </pre>
+          <span class="changeNumberColon">:&nbsp;</span>
           <span class="headerSubject">[[_change.subject]]</span>
+          <gr-copy-clipboard
+            hide-input
+            text="[[_computeCopyTextForTitle(_change)]]">
+          </gr-copy-clipboard>
         </div><!-- end headerTitle -->
         <div class="commitActions" hidden$="[[!_loggedIn]]">
           <gr-change-actions
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 4d2f1db..058bce8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -68,6 +68,7 @@
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrChangeView extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -324,6 +325,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
 
@@ -346,6 +348,7 @@
           () => this._handleReloadCommentThreads());
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getServerConfig().then(config => {
@@ -391,6 +394,7 @@
       this.listen(document, 'visibilitychange', '_handleVisibilityChange');
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unlisten(window, 'scroll', '_handleScroll');
@@ -1593,6 +1597,17 @@
       return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
     }
 
+    /**
+     * Returns the text to be copied when
+     * click the copy icon next to change subject
+     *
+     * @param {!Object} change
+     */
+    _computeCopyTextForTitle(change) {
+      return `${change._number}: ${change.subject}` +
+       ` | https://${location.host}${this._computeChangeUrl(change)}`;
+    }
+
     _toggleCommitCollapsed() {
       this._commitCollapsed = !this._commitCollapsed;
       if (this._commitCollapsed) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 7e776b5..9c23d0e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -803,6 +803,24 @@
       assert.deepEqual(commit, {commit: 2});
     });
 
+    test('_computeCopyTextForTitle', () => {
+      const change = {
+        _number: 123,
+        subject: 'test subject',
+        revisions: {
+          rev1: {_number: 1},
+          rev3: {_number: 3},
+        },
+        current_revision: 'rev3',
+      };
+      sandbox.stub(Gerrit.Nav, 'getUrlForChange')
+          .returns('/change/123');
+      assert.equal(
+          element._computeCopyTextForTitle(change),
+          '123: test subject | https://localhost:8081/change/123'
+      );
+    });
+
     test('get latest revision', () => {
       let change = {
         revisions: {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 3de51a7..58bdbbc 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -21,6 +21,7 @@
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.PathListMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrCommentList extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
index 2a3fe89..a339865 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrCommitInfo extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
index eb6f5f3..555c605 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
+   * @extends Polymer.Element
    */
   class GrConfirmAbandonDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
index 0b3639f..35e9afb 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrConfirmCherrypickConflictDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index 6b01465..2b10a97 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -21,6 +21,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrConfirmCherrypickDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
index d595f48..feb3d2f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -21,6 +21,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrConfirmMoveDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
index 7169437..607f587 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrConfirmRebaseDialog extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 379226a..bf727ec 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -22,6 +22,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrConfirmRevertDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
index 51698be..d59f5cd 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
@@ -23,6 +23,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrConfirmRevertSubmissionDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
index b4d3595..ea8bdb5 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrConfirmSubmitDialog extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index 72ad5d2..17c6f50 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -21,6 +21,7 @@
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrDownloadDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -54,6 +55,7 @@
       };
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._ensureAttribute('role', 'dialog');
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index a611250..5828006 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -24,6 +24,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.PatchSetMixin
+   * @extends Polymer.Element
    */
   class GrFileListHeader extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 90ab96c..0bc1a22 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -48,6 +48,7 @@
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.PathListMixin
+   * @extends Polymer.Element
    */
   class GrFileList extends Polymer.mixinBehaviors( [
     Gerrit.AsyncForeachBehavior,
@@ -236,12 +237,14 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('keydown',
           e => this._scopedKeydownHandler(e));
     }
 
+    /** @override */
     attached() {
       super.attached();
       Gerrit.awaitPluginsLoaded().then(() => {
@@ -265,6 +268,7 @@
       });
     }
 
+    /** @override */
     detached() {
       super.detached();
       this._cancelDiffs();
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index 134ce10..01c9b6e 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrIncludedInDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index abfe469..1f9845e 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrLabelScoreRow extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
index ce2a3ef..0cbf2c7 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrLabelScores extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 28348ba..59f12f5 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -22,6 +22,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrMessage extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -113,12 +114,14 @@
       ];
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('click',
           e => this._handleClick(e));
     }
 
+    /** @override */
     ready() {
       super.ready();
       this.$.restAPI.getConfig().then(config => {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 49ff60a..2c47dc1 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -25,6 +25,7 @@
     SHOW_MORE: 'show-more-messages',
   };
 
+  /** @extends Polymer.Element */
   class GrMessagesList extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index c925142..d4a2398 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -21,6 +21,7 @@
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrRelatedChangesList extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 1828808..90f8540 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -58,6 +58,7 @@
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrReplyDialog extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -249,6 +250,7 @@
       ];
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getAccount().then(account => {
@@ -256,6 +258,7 @@
       });
     }
 
+    /** @override */
     ready() {
       super.ready();
       this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 7bef5dd..ddc6275 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrReviewerList extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index de58d72..9e42de1 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -21,6 +21,7 @@
    * Fired when a comment is saved or deleted
    *
    * @event thread-list-modified
+   * @extends Polymer.Element
    */
   class GrThreadList extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index 0441129..60cbd42 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -29,6 +29,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrUploadHelpDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -65,6 +66,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.$.restAPI.getLoggedIn()
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 1cbbf3e..66c00f9 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -21,6 +21,7 @@
 
   /**
    * @appliesMixin Gerrit.DisplayNameMixin
+   * @extends Polymer.Element
    */
   class GrAccountDropdown extends Polymer.mixinBehaviors( [
     Gerrit.DisplayNameBehavior,
@@ -50,6 +51,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._handleLocationChange();
@@ -66,6 +68,7 @@
       });
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unlisten(window, 'location-change', '_handleLocationChange');
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
index 2655140..db70d57 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrErrorDialog extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 2a87708..9ef2461 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -28,6 +28,7 @@
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrErrorManager extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -74,6 +75,7 @@
       this._authErrorHandlerDeregistrationHook;
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.listen(document, 'server-error', '_handleServerError');
@@ -89,6 +91,7 @@
           });
     }
 
+    /** @override */
     detached() {
       super.detached();
       this._clearHideAlertHandle();
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
index 2b86170..3d424bc 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrKeyBindingDisplay extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
index 788c9e0..4630ca7 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -22,6 +22,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
+   * @extends Polymer.Element
    */
   class GrKeyboardShortcutsDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -59,17 +60,20 @@
       };
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._ensureAttribute('role', 'dialog');
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.addKeyboardShortcutDirectoryListener(
           this._onDirectoryUpdated.bind(this));
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.removeKeyboardShortcutDirectoryListener(
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index f868596..53c5ef5 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -74,6 +74,7 @@
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.DocsUrlMixin
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrMainHeader extends Polymer.mixinBehaviors( [
     Gerrit.AdminNavBehavior,
@@ -150,11 +151,13 @@
       ];
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._ensureAttribute('role', 'banner');
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._loadAccount();
@@ -162,6 +165,7 @@
       this.listen(window, 'location-change', '_handleLocationChange');
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unlisten(window, 'location-change', '_handleLocationChange');
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 1dfa973..2bd403b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -214,6 +214,7 @@
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrRouter extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
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 956beea..335813a 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
@@ -107,6 +107,7 @@
   /**
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrSearchBar extends Polymer.mixinBehaviors( [
     Gerrit.KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index 662e1b1..664d59f 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -23,6 +23,7 @@
 
   /**
    * @appliesMixin Gerrit.DisplayNameMixin
+   * @extends Polymer.Element
    */
   class GrSmartSearch extends Polymer.mixinBehaviors( [
     Gerrit.DisplayNameBehavior,
@@ -56,6 +57,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.$.restAPI.getConfig().then(cfg => {
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index f2b3d90..490367a 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -466,6 +466,7 @@
 
   /**
    * @appliesMixin Gerrit.PatchSetMixin
+   * @extends Polymer.Element
    */
   class GrCommentApi extends Polymer.mixinBehaviors( [
     Gerrit.PatchSetBehavior,
@@ -480,6 +481,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('reload-drafts',
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
index 201da05..1bc4674 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -24,6 +24,7 @@
     [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
   ]);
 
+  /** @extends Polymer.Element */
   class GrCoverageLayer extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 591ebfe..05a7525 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -35,6 +35,7 @@
   const LEFT_SIDE_CLASS = 'target-side-left';
   const RIGHT_SIDE_CLASS = 'target-side-right';
 
+  /** @extends Polymer.Element */
   class GrDiffCursor extends Polymer.mixinBehaviors([Gerrit.FireBehavior],
       Polymer.GestureEventListeners(
           Polymer.LegacyElementMixin(Polymer.Element))) {
@@ -105,6 +106,7 @@
       ];
     }
 
+    /** @override */
     ready() {
       super.ready();
       Polymer.RenderStatus.afterNextRender(this, () => {
@@ -122,12 +124,14 @@
       });
     }
 
+    /** @override */
     attached() {
       super.attached();
       // Catch when users are scrolling as the view loads.
       this.listen(window, 'scroll', '_handleWindowScroll');
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unlisten(window, 'scroll', '_handleWindowScroll');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index adb1be8..e524cd8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrDiffHighlight extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -57,6 +58,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('comment-thread-mouseleave',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index 300e72a..cb23308 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -77,6 +77,8 @@
    *
    * Webcomponent fetching diffs and related data from restAPI and passing them
    * to the presentational gr-diff for rendering.
+   *
+   * @extends Polymer.Element
    */
   class GrDiffHost extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -249,6 +251,7 @@
       ];
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener(
@@ -276,6 +279,7 @@
           event => this._handleDiffContextExpanded(event));
     }
 
+    /** @override */
     ready() {
       super.ready();
       if (this._canReload()) {
@@ -283,6 +287,7 @@
       }
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getLoggedIn().then(loggedIn => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
index ae7ef6d..68bca23 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrDiffModeSelector extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
index 7ed27dd..6aad66c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrDiffPreferencesDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index bfccfe9..cdd0c7d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -63,6 +63,8 @@
    *    "expand context" widget. This may require splitting a chunk/group so
    *    that the part that is within the context or has comments is shown, while
    *    the rest is not.
+   *
+   * @extends Polymer.Element
    */
   class GrDiffProcessor extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
@@ -119,11 +121,13 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.listen(window, 'scroll', '_handleWindowScroll');
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.cancel();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 37d54c9..359a039 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -32,6 +32,7 @@
 
   /**
    * @appliesMixin Gerrit.DomUtilMixin
+   * @extends Polymer.Element
    */
   class GrDiffSelection extends Polymer.mixinBehaviors( [
     Gerrit.DomUtilBehavior,
@@ -58,6 +59,7 @@
       ];
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('copy',
@@ -66,6 +68,7 @@
           e => this._handleDown(e));
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.classList.add(SelectionClass.RIGHT);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 3122370..5087d74 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -39,6 +39,7 @@
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.PathListMixin
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrDiffView extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -244,6 +245,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getLoggedIn().then(loggedIn => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 4b075e7..ef22188 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -94,6 +94,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.PatchSetMixin
+   * @extends Polymer.Element
    */
   class GrDiff extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -284,6 +285,7 @@
       ];
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('create-range-comment',
@@ -292,11 +294,13 @@
           () => this._handleRenderContent());
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._observeNodes();
     }
 
+    /** @override */
     detached() {
       super.detached();
       this._unobserveIncrementalNodes();
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 88c3a0e..d24e0bc 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -30,6 +30,7 @@
    *
    * @property {string} patchNum
    * @property {string} basePatchNum
+   * @extends Polymer.Element
    */
   class GrPatchRangeSelect extends Polymer.mixinBehaviors( [
     Gerrit.PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index d9ad795..fd94b61 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -23,6 +23,7 @@
   const RANGE_HIGHLIGHT = 'style-scope gr-diff range';
   const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
 
+  /** @extends Polymer.Element */
   class GrRangedCommentLayer extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index 261cd4d..3d831c9 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrSelectionActionBox extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -42,6 +43,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
 
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index 9a15703..34bda26 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -131,6 +131,7 @@
   const GO_BACKSLASH_LITERAL = '\'\\\\\'';
   const GLOBAL_LT_PATTERN = /</g;
 
+  /** @extends Polymer.Element */
   class GrSyntaxLayer extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
index f317459..022a985 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.ListViewMixin
+   * @extends Polymer.Element
    */
   class GrDocumentationSearch extends Polymer.mixinBehaviors( [
     Gerrit.ListViewBehavior,
@@ -55,6 +56,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.dispatchEvent(
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
index ce9db61..73dbaf8 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrDefaultEditor extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index 052c51c..e655f7b 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.PatchSetMixin
+   * @extends Polymer.Element
    */
   class GrEditControls extends Polymer.mixinBehaviors( [
     Gerrit.PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index ddd2d29..d59fcf7 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrEditFileControls extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index aaafa46..12e16cf 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -29,6 +29,7 @@
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.PathListMixin
+   * @extends Polymer.Element
    */
   class GrEditorView extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -92,12 +93,14 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('content-change',
           e => this._handleContentChange(e));
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getEditPrefs().then(prefs => { this._prefs = prefs; });
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 14570f2..d39ba58 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
+   * @extends Polymer.Element
    */
   class GrAppElement extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -114,6 +115,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this._bindKeyboardShortcuts();
@@ -127,6 +129,7 @@
           e => this._handleRpcLog(e));
     }
 
+    /** @override */
     ready() {
       super.ready();
       this.$.reporting.appStarted();
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 46a81ee..da54ac4 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrApp extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 946147d..600421b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -41,6 +41,7 @@
     return this._hooks[hookName];
   };
 
+  /** @constructor */
   function GrDomHook(hookName, opt_moduleName) {
     this._instances = [];
     this._callbacks = [];
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index fcf4ce8..5a2f104 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -19,6 +19,7 @@
 
   const INIT_PROPERTIES_TIMEOUT_MS = 10000;
 
+  /** @extends Polymer.Element */
   class GrEndpointDecorator extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -46,6 +47,7 @@
       };
     }
 
+    /** @override */
     detached() {
       super.detached();
       for (const [el, domHook] of this._domHooks) {
@@ -145,6 +147,7 @@
       });
     }
 
+    /** @override */
     ready() {
       super.ready();
       Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this));
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index 3010750..bcad7f9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrEndpointParam extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index aa9b13e..2e3bee1 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrExternalStyle extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -73,11 +74,13 @@
       });
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._importAndApply();
     }
 
+    /** @override */
     ready() {
       super.ready();
       Gerrit.awaitPluginsLoaded().then(() => this._importAndApply());
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index cae3a91..da050fb 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrPluginHost extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
index 412e788a..30bf6c8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -17,6 +17,7 @@
 (function(window) {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrPluginPopup extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
index 9401a15..588facb 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -65,6 +65,7 @@
       setup(() => {
         fixture('header-title');
         stub('gr-custom-plugin-header', {
+          /** @override */
           ready() { customHeader = this; },
         });
         Gerrit._loadPlugins([]);
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 9b17fd7..7bf641d 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrAccountInfo extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index 2886c87..67dc0c4 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
+   * @extends Polymer.Element
    */
   class GrAgreementsList extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -33,6 +34,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.loadData();
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index aa4e0c5..8521126 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.ChangeTableMixin
+   * @extends Polymer.Element
    */
   class GrChangeTableEditor extends Polymer.mixinBehaviors( [
     Gerrit.ChangeTableBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index e146a90..cff1d54 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrClaView extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -45,6 +46,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.loadData();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 36e53b0..9523136 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrEditPreferences extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
index 8ec1067..c60568c 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrEmailEditor extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
index d45edff..9f04915 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrGpgEditor extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
index 6c3a448..c7b5faa 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrGroupList extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
index 5c0a059..efd0c39 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrHttpPassword extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -30,6 +31,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.loadData();
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index 8b74130..ac4f9e4 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -24,6 +24,7 @@
 
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
+   * @extends Polymer.Element
    */
   class GrIdentities extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index 5b6c978..0ee232b 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrMenuEditor extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index 25499d6..4bb98d0 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrRegistrationDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -67,6 +68,7 @@
       };
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._ensureAttribute('role', 'dialog');
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
index 9702280..bae1f38 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrSettingsItem extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
index e08e3d8..d5a7eb7 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrSettingsMenuItem extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 0236261..78bad8c 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -49,6 +49,7 @@
    * @appliesMixin Gerrit.DocsUrlMixin
    * @appliesMixin Gerrit.ChangeTableMixin
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrSettingsView extends Polymer.mixinBehaviors( [
     Gerrit.DocsUrlBehavior,
@@ -164,6 +165,7 @@
       ];
     }
 
+    /** @override */
     attached() {
       super.attached();
       // Polymer 2: anchor tag won't work on shadow DOM
@@ -231,6 +233,7 @@
       });
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unlisten(window, 'location-change', '_handleLocationChange');
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
index f8e5a5f..44fb48c 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrSshEditor extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
index f638a07..df115ca 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -25,6 +25,7 @@
     {name: 'Abandons', key: 'notify_abandoned_changes'},
   ];
 
+  /** @extends Polymer.Element */
   class GrWatchedProjectsEditor extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 2f9572c..8cd2021 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrAccountChip extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -63,6 +64,7 @@
       };
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._getHasAvatars().then(hasAvatars => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
index 0ed3f19..d2a111a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -20,6 +20,8 @@
   /**
    * gr-account-entry is an element for entering account
    * and/or group with autocomplete support.
+   *
+   * @extends Polymer.Element
    */
   class GrAccountEntry extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 1c0cb65..34c4cb6 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.DisplayNameMixin
    * @appliesMixin Gerrit.TooltipMixin
+   * @extends Polymer.Element
    */
   class GrAccountLabel extends Polymer.mixinBehaviors( [
     Gerrit.DisplayNameBehavior,
@@ -61,6 +62,7 @@
       };
     }
 
+    /** @override */
     ready() {
       super.ready();
       if (!this.additionalText) { this.additionalText = ''; }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index f23959a..b0ce04c 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
+   * @extends Polymer.Element
    */
   class GrAccountLink extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 0a4100c..7955d50 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -21,6 +21,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrAccountList extends Polymer.mixinBehaviors( [
     // Used in the tests for gr-account-list and other elements tests.
@@ -113,6 +114,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('remove',
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index 433a57b..215f469 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrAlert extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -52,11 +53,13 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.addEventListener('transitionend', this._boundTransitionEndHandler);
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.removeEventListener('transitionend',
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index f536666..5ca95e1 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -21,6 +21,7 @@
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Polymer.IronFitMixin
+   * @extends Polymer.Element
    */
   class GrAutocompleteDropdown extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
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 e2f42b3..60985c1 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -23,6 +23,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
+   * @extends Polymer.Element
    */
   class GrAutocomplete extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -192,11 +193,13 @@
       return this.$.input.$.nativeInput || this.$.input.inputElement;
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.listen(document.body, 'click', '_handleBodyClick');
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unlisten(document.body, 'click', '_handleBodyClick');
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 4238eed..efa97cf 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
+   * @extends Polymer.Element
    */
   class GrAvatar extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -44,6 +45,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       Promise.all([
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 75efcdc..abfbb18 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -20,6 +20,7 @@
   /**
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Gerrit.TooltipMixin
+   * @extends Polymer.Element
    */
   class GrButton extends Polymer.mixinBehaviors( [
     Gerrit.KeyboardShortcutBehavior,
@@ -68,6 +69,7 @@
       ];
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('click',
@@ -76,6 +78,7 @@
           e => this._handleKeydown(e));
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._ensureAttribute('role', 'button');
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 05e759f..001632f 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrChangeStar extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 35b8ef8..7052a6a 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -36,6 +36,7 @@
   const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
       'current reviewers (or anyone with "View Private Changes" permission).';
 
+  /** @extends Polymer.Element */
   class GrChangeStatus extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index a1165ca..d8a56f8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -24,6 +24,7 @@
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
    * @appliesMixin Gerrit.PathListMixin
+   * @extends Polymer.Element
    */
   class GrCommentThread extends Polymer.mixinBehaviors( [
     /**
@@ -157,12 +158,14 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('comment-update',
           e => this._handleCommentUpdate(e));
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getLoggedIn().then(loggedIn => {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 6449416..3f4cc69 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -34,6 +34,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
+   * @extends Polymer.Element
    */
   class GrComment extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -186,6 +187,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       if (this.editing) {
@@ -198,6 +200,7 @@
       });
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.cancelDebouncer('fire-update');
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
index 6a4232f..8d50fe0 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrConfirmDeleteCommentDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
index 6c062f6..2ce03e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -19,6 +19,7 @@
 
   const COPY_TIMEOUT_MS = 1000;
 
+  /** @extends Polymer.Element */
   class GrCopyClipboard extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 04724df..374204b 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -22,6 +22,7 @@
     KEEP_VISIBLE: 'keep-visible',
   };
 
+  /** @extends Polymer.Element */
   class GrCursorManager extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -91,6 +92,7 @@
       };
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unsetCursor();
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 374844d..8c247e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -33,6 +33,7 @@
 
   /**
    * @appliesMixin Gerrit.TooltipMixin
+   * @extends Polymer.Element
    */
   class GrDateFormatter extends Polymer.mixinBehaviors( [
     Gerrit.TooltipBehavior,
@@ -73,6 +74,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._loadPreferences();
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
index d36aa37..8d00452 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrDialog extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -60,6 +61,7 @@
       };
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._ensureAttribute('role', 'dialog');
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
index 6e34bbc..c408e5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrDiffPreferences extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
index 9e90ae3..17fafce 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrDownloadCommands extends Polymer.mixinBehaviors( [
     Gerrit.RESTClientBehavior,
@@ -43,6 +44,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this._getLoggedIn().then(loggedIn => {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 1fa0f1b..06b4a72 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -45,6 +45,7 @@
    */
   Defs.item;
 
+  /** @extends Polymer.Element */
   class GrDropdownList extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 619005a..531f2e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -23,6 +23,7 @@
   /**
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
+   * @extends Polymer.Element
    */
   class GrDropdown extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index 0c2d4f5..7d3308e 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -22,6 +22,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrEditableContent extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index 13000b4..ef5bb8c 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -23,6 +23,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
+   * @extends Polymer.Element
    */
   class GrEditableLabel extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -76,6 +77,7 @@
       };
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._ensureAttribute('tabindex', '0');
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index 1e8f767..02e23e8 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrFixedPanel extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -61,6 +62,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       if (this.floatingDisabled) {
@@ -76,6 +78,7 @@
       this._observer.observe(this.$.header, {childList: true, subtree: true});
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unlisten(window, 'scroll', '_updateOnScroll');
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
index 39b114d..7483590 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -20,6 +20,7 @@
   // eslint-disable-next-line no-unused-vars
   const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
 
+  /** @extends Polymer.Element */
   class GrFormattedText extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -45,6 +46,7 @@
       ];
     }
 
+    /** @override */
     ready() {
       super.ready();
       if (this.noTrailingMargin) {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
index 37972f9..ce34d3a 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -25,6 +25,7 @@
    */
   const DIAGONAL_OVERFLOW = 15;
 
+  /** @extends Polymer.Element */
   class GrHovercard extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -91,6 +92,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       if (!this._target) { this._target = this.target; }
@@ -101,12 +103,14 @@
       this.listen(this._target, 'click', 'hide');
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('mouseleave',
           e => this.hide(e));
     }
 
+    /** @override */
     ready() {
       super.ready();
       // First, check to see if the container has already been created.
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index b1675cc..6f9268c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -39,6 +39,7 @@
 
   /**
    * @appliesMixin Gerrit.PatchSetMixin
+   * @extends Polymer.Element
    */
   class GrJsApiInterface extends Polymer.mixinBehaviors( [
     Gerrit.PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
index 6e5fd5b..6e99e01 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrLabelInfo extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
index 440e8e9..b594757 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.TooltipMixin
+   * @extends Polymer.Element
    */
   class GrLabel extends Polymer.mixinBehaviors( [
     Gerrit.TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
index c02c598..cb5ad7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrLabeledAutocomplete extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index 4c221bc..defc6bd 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -20,6 +20,7 @@
   const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
   const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
 
+  /** @extends Polymer.Element */
   class GrLibLoader extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
index a57f331..ee032f6 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -18,13 +18,13 @@
   'use strict';
 
   /**
-   * @appliesMixin Gerrit.TooltipMixin
-   */
-  /*
    * The gr-limited-text element is for displaying text with a maximum length
    * (in number of characters) to display. If the length of the text exceeds the
    * configured limit, then an ellipsis indicates that the text was truncated
    * and a tooltip containing the full text is enabled.
+   *
+   * @appliesMixin Gerrit.TooltipMixin
+   * @extends Polymer.Element
    */
   class GrLimitedText extends Polymer.mixinBehaviors( [
     Gerrit.TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
index bef9698..ccab685 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrLinkedChip extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 5d2a1ca..bbe525e 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrLinkedText extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index dd825b3..8913cd8 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -23,6 +23,7 @@
    * @appliesMixin Gerrit.BaseUrlMixin
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrListView extends Polymer.mixinBehaviors( [
     Gerrit.BaseUrlBehavior,
@@ -48,6 +49,7 @@
       };
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.cancelDebouncer('reload');
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index fc9dc99..8821bcb 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -23,6 +23,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrOverlay extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -52,6 +53,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('iron-overlay-closed',
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
index 23389ee..ac876c4 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrPageNav extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
@@ -28,11 +29,13 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       this.listen(window, 'scroll', '_handleBodyScroll');
     }
 
+    /** @override */
     detached() {
       super.detached();
       this.unlisten(window, 'scroll', '_handleBodyScroll');
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
index ea39d0e..18e2596 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -22,6 +22,7 @@
 
   /**
    * @appliesMixin Gerrit.URLEncodingMixin
+   * @extends Polymer.Element
    */
   class GrRepoBranchPicker extends Polymer.mixinBehaviors( [
     Gerrit.URLEncodingBehavior,
@@ -57,6 +58,7 @@
       };
     }
 
+    /** @override */
     attached() {
       super.attached();
       if (this.repo) {
@@ -64,6 +66,7 @@
       }
     }
 
+    /** @override */
     ready() {
       super.ready();
       this._branchDisabled = !this.repo;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
index 0af1d97..2978193 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
@@ -39,6 +39,10 @@
       this._last_auth_check_time = Date.now();
     }
 
+    get baseUrl() {
+      return Gerrit.BaseUrlBehavior.getBaseUrl();
+    }
+
     /**
      * Returns if user is authed or not.
      *
@@ -49,7 +53,7 @@
         (Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS)
       ) {
         // Refetch after last check expired
-        this._authCheckPromise = fetch('/auth-check');
+        this._authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
         this._last_auth_check_time = Date.now();
       }
 
@@ -76,7 +80,7 @@
     }
 
     /**
-     * @param {string} status
+     * @param {Auth.STATUS} status
      */
     _setStatus(status) {
       if (this._status === status) return;
@@ -209,7 +213,7 @@
 
       if (accessToken) {
         params.push(`access_token=${accessToken}`);
-        const baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl();
+        const baseUrl = this.baseUrl;
         const pathname = baseUrl ?
           url.substring(url.indexOf(baseUrl) + baseUrl.length) : url;
         if (!pathname.startsWith('/a/')) {
@@ -252,6 +256,7 @@
     ACCESS_TOKEN: 'access_token',
   };
 
+  /** @enum {number} */
   Auth.STATUS = {
     UNDETERMINED: 0,
     AUTHED: 1,
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 0e2bef6..472a7ea 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
@@ -44,6 +44,7 @@
    * @appliesMixin Gerrit.PathListMixin
    * @appliesMixin Gerrit.PatchSetMixin
    * @appliesMixin Gerrit.RESTClientMixin
+   * @extends Polymer.Element
    */
   class GrRestApiInterface extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -105,6 +106,7 @@
       };
     }
 
+    /** @override */
     created() {
       super.created();
       this._auth = Gerrit.Auth;
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index c9f13de..ea838a0 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.FireMixin
+   * @extends Polymer.Element
    */
   class GrSelect extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -63,6 +64,7 @@
       this.nativeSelect.focus();
     }
 
+    /** @override */
     created() {
       super.created();
       this.addEventListener('change',
@@ -71,6 +73,7 @@
           () => this._updateValue());
     }
 
+    /** @override */
     ready() {
       super.ready();
       // If not set via the property, set bind-value to the element value.
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
index 9456991..4b31086 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrShellCommand extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 7950e51..53311b5 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -28,6 +28,7 @@
     'editablecontent:',
   ];
 
+  /** @extends Polymer.Element */
   class GrStorage extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index d3119b7..a604bcf 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -55,6 +55,7 @@
   /**
    * @appliesMixin Gerrit.FireMixin
    * @appliesMixin Gerrit.KeyboardShortcutMixin
+   * @extends Polymer.Element
    */
   class GrTextarea extends Polymer.mixinBehaviors( [
     Gerrit.FireBehavior,
@@ -125,6 +126,7 @@
       };
     }
 
+    /** @override */
     ready() {
       super.ready();
       if (this.monospace) {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
index 22a5ecb..90e6a08 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
@@ -19,6 +19,7 @@
 
   /**
    * @appliesMixin Gerrit.TooltipMixin
+   * @extends Polymer.Element
    */
   class GrTooltipContent extends Polymer.mixinBehaviors( [
     Gerrit.TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
index 170a442..6f458d1 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -17,6 +17,7 @@
 (function() {
   'use strict';
 
+  /** @extends Polymer.Element */
   class GrTooltip extends Polymer.GestureEventListeners(
       Polymer.LegacyElementMixin(
           Polymer.Element)) {
diff --git a/polygerrit-ui/app/samples/bind-parameters.html b/polygerrit-ui/app/samples/bind-parameters.html
index a28c462..e6bf9d1 100644
--- a/polygerrit-ui/app/samples/bind-parameters.html
+++ b/polygerrit-ui/app/samples/bind-parameters.html
@@ -22,6 +22,7 @@
           computed: '_computeExample(revision._number)',
         },
       },
+      /** @override */
       attached() {
         this.plugin.attributeHelper(this).bind(
             'revision', this._onRevisionChanged.bind(this));
diff --git a/polygerrit-ui/app/samples/repo-command.html b/polygerrit-ui/app/samples/repo-command.html
index afbc78c..5b3ee2c 100644
--- a/polygerrit-ui/app/samples/repo-command.html
+++ b/polygerrit-ui/app/samples/repo-command.html
@@ -31,6 +31,7 @@
     Polymer({
       is: 'repo-command-low',
 
+      /** @override */
       attached() {
         console.log(this.repoName);
         console.log(this.config);
diff --git a/polygerrit-ui/app/samples/some-screen.html b/polygerrit-ui/app/samples/some-screen.html
index da025a2..593b8ab 100644
--- a/polygerrit-ui/app/samples/some-screen.html
+++ b/polygerrit-ui/app/samples/some-screen.html
@@ -42,6 +42,7 @@
       properties: {
         rootUrl: String,
       },
+      /** @override */
       attached() {
         this.rootUrl = `${this.plugin.screenUrl()}`;
       },
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index 45882cf..0dec63f 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -43,8 +43,8 @@
   /* background colors */
   /* primary background colors */
   --background-color-primary: #ffffff;
-  --background-color-secondary: rgba(0, 0, 0, 0.025);
-  --background-color-tertiary: rgba(0, 0, 0, 0.05);
+  --background-color-secondary: #f8f9fa;
+  --background-color-tertiary: #f1f3f4;
   /* directly derived from primary background colors */
   --chip-background-color: var(--background-color-tertiary);
   --default-button-background-color: var(--background-color-primary);
@@ -59,15 +59,17 @@
   --view-background-color: var(--background-color-primary);
   /* unique background colors */
   --assignee-highlight-color: #fcfad6;
-  --comment-background-color: #fcfad6;
-  --robot-comment-background-color: #e8f0fe;
   --edit-mode-background-color: #ebf5fb;
   --emphasis-color: #fff9c4;
   --hover-background-color: rgba(161, 194, 250, 0.2);
   --primary-button-background-color: #2a66d9;
   --selection-background-color: rgba(161, 194, 250, 0.1);
   --tooltip-background-color: #333;
-  --unresolved-comment-background-color: #fcfaa6;
+  /* comment background colors */
+  --comment-background-color: #fef7f0;
+  --robot-comment-background-color: #e8f0fe;
+  --unresolved-comment-background-color: #e8eaed;
+  /* vote background colors */
   --vote-color-approved: #9fcc6b;
   --vote-color-disliked: #f7c4cb;
   --vote-color-neutral: #ebf5fb;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 61b2dc6..57ceb32 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -43,21 +43,23 @@
       /* background colors */
       /* primary background colors */
       --background-color-primary: #202124;
-      --background-color-secondary: rgba(255, 255, 255, 0.05);
-      --background-color-tertiary: rgba(255, 255, 255, 0.1);
+      --background-color-secondary: #2f3034;
+      --background-color-tertiary: #3b3d3f;
       /* directly derived from primary background colors */
       /*   empty, because inheriting from app-theme is just fine
       /* unique background colors */
       --assignee-highlight-color: #3a361c;
-      --comment-background-color: #0b162b;
-      --robot-comment-background-color: rgba(232, 234, 237, 0.08);
       --edit-mode-background-color: #5c0a36;
       --emphasis-color: #383f4a;
       --hover-background-color: rgba(161, 194, 250, 0.2);
       --primary-button-background-color: var(--link-color);
       --selection-background-color: rgba(161, 194, 250, 0.1);
       --tooltip-background-color: #111;
-      --unresolved-comment-background-color: #385a9a;
+      /* comment background colors */
+      --comment-background-color: #303134;
+      --robot-comment-background-color: #40454e;
+      --unresolved-comment-background-color: #4b463a;
+      /* vote background colors */
       --vote-color-approved: #7fb66b;
       --vote-color-disliked: #bf6874;
       --vote-color-neutral: #597280;