Merge "Clarify error message about FORGE_SERVER permission."
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
index 0f687bf..938d101 100644
--- a/Documentation/rest-api-plugins.txt
+++ b/Documentation/rest-api-plugins.txt
@@ -47,6 +47,7 @@
     "delete-project": {
       "id": "delete-project",
       "index_url": "plugins/delete-project/",
+      "filename": "delete-project.jar",
       "version": "2.9-SNAPSHOT"
     }
   }
@@ -73,11 +74,13 @@
     "delete-project": {
       "id": "delete-project",
       "index_url": "plugins/delete-project/",
+      "filename": "delete-project.jar",
       "version": "2.9-SNAPSHOT"
     },
     "reviewers-by-blame": {
       "id": "reviewers-by-blame",
       "index_url": "plugins/reviewers-by-blame/",
+      "filename": "reviewers-by-blame.jar",
       "version": "2.9-SNAPSHOT",
       "disabled": true
     }
@@ -105,6 +108,7 @@
     "delete-project": {
       "id": "delete-project",
       "index_url": "plugins/delete-project/",
+      "filename": "delete-project.jar",
       "version": "2.9-SNAPSHOT"
     }
   }
@@ -134,6 +138,7 @@
     "delete-project": {
       "id": "delete-project",
       "index_url": "plugins/delete-project/",
+      "filename": "delete-project.jar",
       "version": "2.9-SNAPSHOT"
     }
   }
@@ -168,11 +173,13 @@
     "some-plugin": {
       "id": "some-plugin",
       "index_url": "plugins/some-plugin/",
+      "filename": "some-plugin.jar",
       "version": "2.9-SNAPSHOT"
     },
     "some-other-plugin": {
       "id": "some-other-plugin",
       "index_url": "plugins/some-other-plugin/",
+      "filename": "some-other-plugin.jar",
       "version": "2.9-SNAPSHOT"
     }
   }
@@ -200,6 +207,7 @@
     "reviewers-by-blame": {
       "id": "reviewers-by-blame",
       "index_url": "plugins/reviewers-by-blame/",
+      "filename": "reviewers-by-blame.jar",
       "version": "2.9-SNAPSHOT",
       "disabled": true
     }
@@ -229,6 +237,7 @@
     "delete-project": {
       "id": "delete-project",
       "index_url": "plugins/delete-project/",
+      "filename": "delete-project.jar",
       "version": "2.9-SNAPSHOT"
     }
   }
@@ -429,6 +438,7 @@
 |`id`       ||The ID of the plugin.
 |`version`  ||The version of the plugin.
 |`index_url`|optional|URL of the plugin's default page.
+|`filename` |optional|The plugin's filename.
 |`disabled` |not set if `false`|Whether the plugin is disabled.
 |=======================
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 3e1b2cb..0fa09af 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -68,6 +68,7 @@
       assertThat(info.id).isEqualTo(name);
       assertThat(info.version).isEqualTo(pluginVersion(plugin));
       assertThat(info.indexUrl).isEqualTo(String.format("plugins/%s/", name));
+      assertThat(info.filename).isEqualTo(plugin);
       assertThat(info.disabled).isNull();
     }
     assertPlugins(list().get(), PLUGINS);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginInfo.java
index bcb957e..0df6235 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginInfo.java
@@ -18,12 +18,14 @@
   public final String id;
   public final String version;
   public final String indexUrl;
+  public final String filename;
   public final Boolean disabled;
 
-  public PluginInfo(String id, String version, String indexUrl, Boolean disabled) {
+  public PluginInfo(String id, String version, String indexUrl, String filename, Boolean disabled) {
     this.id = id;
     this.version = version;
     this.indexUrl = indexUrl;
+    this.filename = filename;
     this.disabled = disabled;
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 49fd1f9..f750fdb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -73,6 +73,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 import javax.sql.DataSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -387,14 +388,25 @@
       schemaUpdater.update(
           new UpdateUI() {
             @Override
-            public void message(String msg) {
-              System.err.println(msg);
+            public void message(String message) {
+              System.err.println(message);
               System.err.flush();
             }
 
             @Override
-            public boolean yesno(boolean def, String msg) {
-              return ui.yesno(def, msg);
+            public boolean yesno(boolean defaultValue, String message) {
+              return ui.yesno(defaultValue, message);
+            }
+
+            @Override
+            public void waitForUser() {
+              ui.waitForUser();
+            }
+
+            @Override
+            public String readString(
+                String defaultValue, Set<String> allowedValues, String message) {
+              return ui.readString(defaultValue, allowedValues, message);
             }
 
             @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 70801c3..165cbb6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -40,13 +40,16 @@
     IncludingGroupMembership create(IdentifiedUser user);
   }
 
+  private final GroupCache groupCache;
   private final GroupIncludeCache includeCache;
   private final IdentifiedUser user;
   private final Map<AccountGroup.UUID, Boolean> memberOf;
   private Set<AccountGroup.UUID> knownGroups;
 
   @Inject
-  IncludingGroupMembership(GroupIncludeCache includeCache, @Assisted IdentifiedUser user) {
+  IncludingGroupMembership(
+      GroupCache groupCache, GroupIncludeCache includeCache, @Assisted IdentifiedUser user) {
+    this.groupCache = groupCache;
     this.includeCache = includeCache;
     this.user = user;
 
@@ -88,6 +91,10 @@
         }
 
         memberOf.put(id, false);
+        AccountGroup group = groupCache.get(id);
+        if (group == null) {
+          continue;
+        }
         if (search(includeCache.subgroupsOf(id))) {
           memberOf.put(id, true);
           return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
index 75fb350..a955abe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
@@ -66,7 +66,7 @@
         list.setMatchPrefix(this.getPrefix());
         list.setMatchSubstring(this.getSubstring());
         list.setMatchRegex(this.getRegex());
-        return list.apply();
+        return list.apply(TopLevelResource.INSTANCE);
       }
     };
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index 0e514d6..97d728d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -15,11 +15,8 @@
 package com.google.gerrit.server.plugins;
 
 import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.toList;
 
-import com.google.common.base.Strings;
 import com.google.common.collect.Streams;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.common.PluginInfo;
@@ -27,16 +24,12 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
-import java.io.PrintWriter;
-import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.kohsuke.args4j.Option;
 
@@ -52,10 +45,6 @@
   private String matchSubstring;
   private String matchRegex;
 
-  @Deprecated
-  @Option(name = "--format", usage = "(deprecated) output format")
-  private OutputFormat format = OutputFormat.TEXT;
-
   @Option(
     name = "--all",
     aliases = {"-a"},
@@ -115,29 +104,8 @@
     this.pluginLoader = pluginLoader;
   }
 
-  public OutputFormat getFormat() {
-    return format;
-  }
-
-  public ListPlugins setFormat(OutputFormat fmt) {
-    this.format = fmt;
-    return this;
-  }
-
   @Override
-  public Object apply(TopLevelResource resource) throws BadRequestException {
-    format = OutputFormat.JSON;
-    return display(null);
-  }
-
-  public SortedMap<String, PluginInfo> apply() throws BadRequestException {
-    format = OutputFormat.JSON;
-    return display(null);
-  }
-
-  public SortedMap<String, PluginInfo> display(@Nullable PrintWriter stdout)
-      throws BadRequestException {
-    SortedMap<String, PluginInfo> output = new TreeMap<>();
+  public SortedMap<String, PluginInfo> apply(TopLevelResource resource) throws BadRequestException {
     Stream<Plugin> s = Streams.stream(pluginLoader.getPlugins(all));
     if (matchPrefix != null) {
       checkMatchOptions(matchSubstring == null && matchRegex == null);
@@ -158,38 +126,7 @@
     if (limit > 0) {
       s = s.limit(limit);
     }
-    List<Plugin> plugins = s.collect(toList());
-
-    if (!format.isJson()) {
-      stdout.format("%-30s %-10s %-8s %s\n", "Name", "Version", "Status", "File");
-      stdout.print(
-          "-------------------------------------------------------------------------------\n");
-    }
-
-    for (Plugin p : plugins) {
-      PluginInfo info = toPluginInfo(p);
-      if (format.isJson()) {
-        output.put(p.getName(), info);
-      } else {
-        stdout.format(
-            "%-30s %-10s %-8s %s\n",
-            p.getName(),
-            Strings.nullToEmpty(info.version),
-            p.isDisabled() ? "DISABLED" : "ENABLED",
-            p.getSrcFile().getFileName());
-      }
-    }
-
-    if (stdout == null) {
-      return output;
-    } else if (format.isJson()) {
-      format
-          .newGson()
-          .toJson(output, new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
-      stdout.print('\n');
-    }
-    stdout.flush();
-    return null;
+    return new TreeMap<>(s.collect(Collectors.toMap(p -> p.getName(), p -> toPluginInfo(p))));
   }
 
   private void checkMatchOptions(boolean cond) throws BadRequestException {
@@ -202,13 +139,20 @@
     String id;
     String version;
     String indexUrl;
+    String filename;
     Boolean disabled;
 
     id = Url.encode(p.getName());
     version = p.getVersion();
     disabled = p.isDisabled() ? true : null;
-    indexUrl = p.getSrcFile() != null ? String.format("plugins/%s/", p.getName()) : null;
+    if (p.getSrcFile() != null) {
+      indexUrl = String.format("plugins/%s/", p.getName());
+      filename = p.getSrcFile().getFileName().toString();
+    } else {
+      indexUrl = null;
+      filename = null;
+    }
 
-    return new PluginInfo(id, version, indexUrl, disabled);
+    return new PluginInfo(id, version, indexUrl, filename, disabled);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 15fd190..e262881 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -30,7 +30,7 @@
 public class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> {
   protected final Provider<ReviewDb> db;
   protected final ChangeNotes.Factory notesFactory;
-  protected final ChangeControl.GenericFactory changeControl;
+  protected final ChangeControl.GenericFactory changeControlFactory;
   protected final CurrentUser user;
   protected final PermissionBackend permissionBackend;
 
@@ -43,7 +43,7 @@
     super(ChangeQueryBuilder.FIELD_VISIBLETO, describe(user));
     this.db = db;
     this.notesFactory = notesFactory;
-    this.changeControl = changeControlFactory;
+    this.changeControlFactory = changeControlFactory;
     this.user = user;
     this.permissionBackend = permissionBackend;
   }
@@ -53,19 +53,20 @@
     if (cd.fastIsVisibleTo(user)) {
       return true;
     }
-    Change change;
+    Change change = cd.change();
+    if (change == null) {
+      return false;
+    }
+
+    ChangeControl changeControl;
+    ChangeNotes notes = notesFactory.createFromIndexedChange(change);
     try {
-      change = cd.change();
-      if (change == null) {
-        return false;
-      }
+      changeControl = changeControlFactory.controlFor(notes, user);
     } catch (NoSuchChangeException e) {
       // Ignored
       return false;
     }
 
-    ChangeNotes notes = notesFactory.createFromIndexedChange(change);
-    ChangeControl cc = changeControl.controlFor(notes, user);
     boolean visible;
     try {
       visible =
@@ -78,10 +79,9 @@
       throw new OrmException("unable to check permissions", e);
     }
     if (visible) {
-      cd.cacheVisibleTo(cc);
+      cd.cacheVisibleTo(changeControl);
       return true;
     }
-
     return false;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index 5a3e76d..70fbf5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -41,6 +41,12 @@
 
 public abstract class JdbcAccountPatchReviewStore
     implements AccountPatchReviewStore, LifecycleListener {
+  private static final String ACCOUNT_PATCH_REVIEW_DB = "accountPatchReviewDb";
+  private static final String H2_DB = "h2";
+  private static final String MARIADB = "mariadb";
+  private static final String MYSQL = "mysql";
+  private static final String POSTGRESQL = "postgresql";
+  private static final String URL = "url";
   private static final Logger log = LoggerFactory.getLogger(JdbcAccountPatchReviewStore.class);
 
   public static class Module extends LifecycleModule {
@@ -52,20 +58,20 @@
 
     @Override
     protected void configure() {
-      String url = cfg.getString("accountPatchReviewDb", null, "url");
-      if (url == null || url.contains("h2")) {
+      String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
+      if (url == null || url.contains(H2_DB)) {
         DynamicItem.bind(binder(), AccountPatchReviewStore.class)
             .to(H2AccountPatchReviewStore.class);
         listener().to(H2AccountPatchReviewStore.class);
-      } else if (url.contains("postgresql")) {
+      } else if (url.contains(POSTGRESQL)) {
         DynamicItem.bind(binder(), AccountPatchReviewStore.class)
             .to(PostgresqlAccountPatchReviewStore.class);
         listener().to(PostgresqlAccountPatchReviewStore.class);
-      } else if (url.contains("mysql")) {
+      } else if (url.contains(MYSQL)) {
         DynamicItem.bind(binder(), AccountPatchReviewStore.class)
             .to(MysqlAccountPatchReviewStore.class);
         listener().to(MysqlAccountPatchReviewStore.class);
-      } else if (url.contains("mariadb")) {
+      } else if (url.contains(MARIADB)) {
         DynamicItem.bind(binder(), AccountPatchReviewStore.class)
             .to(MariaDBAccountPatchReviewStore.class);
         listener().to(MariaDBAccountPatchReviewStore.class);
@@ -80,19 +86,21 @@
 
   public static JdbcAccountPatchReviewStore createAccountPatchReviewStore(
       Config cfg, SitePaths sitePaths) {
-    String url = cfg.getString("accountPatchReviewDb", null, "url");
-    if (url == null || url.contains("h2")) {
+    String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
+    if (url == null || url.contains(H2_DB)) {
       return new H2AccountPatchReviewStore(cfg, sitePaths);
-    } else if (url.contains("postgresql")) {
-      return new PostgresqlAccountPatchReviewStore(cfg, sitePaths);
-    } else if (url.contains("mysql")) {
-      return new MysqlAccountPatchReviewStore(cfg, sitePaths);
-    } else if (url.contains("mariadb")) {
-      return new MariaDBAccountPatchReviewStore(cfg, sitePaths);
-    } else {
-      throw new IllegalArgumentException(
-          "unsupported driver type for account patch reviews db: " + url);
     }
+    if (url.contains(POSTGRESQL)) {
+      return new PostgresqlAccountPatchReviewStore(cfg, sitePaths);
+    }
+    if (url.contains(MYSQL)) {
+      return new MysqlAccountPatchReviewStore(cfg, sitePaths);
+    }
+    if (url.contains(MARIADB)) {
+      return new MariaDBAccountPatchReviewStore(cfg, sitePaths);
+    }
+    throw new IllegalArgumentException(
+        "unsupported driver type for account patch reviews db: " + url);
   }
 
   protected JdbcAccountPatchReviewStore(Config cfg, SitePaths sitePaths) {
@@ -104,7 +112,7 @@
   }
 
   private static String getUrl(@GerritServerConfig Config cfg, SitePaths sitePaths) {
-    String url = cfg.getString("accountPatchReviewDb", null, "url");
+    String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
     if (url == null) {
       return H2.createUrl(sitePaths.db_dir.resolve("account_patch_reviews"));
     }
@@ -113,13 +121,13 @@
 
   protected static DataSource createDataSource(String url) {
     BasicDataSource datasource = new BasicDataSource();
-    if (url.contains("postgresql")) {
+    if (url.contains(POSTGRESQL)) {
       datasource.setDriverClassName("org.postgresql.Driver");
-    } else if (url.contains("h2")) {
+    } else if (url.contains(H2_DB)) {
       datasource.setDriverClassName("org.h2.Driver");
-    } else if (url.contains("mysql")) {
+    } else if (url.contains(MYSQL)) {
       datasource.setDriverClassName("com.mysql.jdbc.Driver");
-    } else if (url.contains("mariadb")) {
+    } else if (url.contains(MARIADB)) {
       datasource.setDriverClassName("org.mariadb.jdbc.Driver");
     }
     datasource.setUrl(url);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
index b43aaa6..0c02607 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
@@ -17,11 +17,24 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.StatementExecutor;
 import java.util.List;
+import java.util.Set;
 
 public interface UpdateUI {
-  void message(String msg);
 
-  boolean yesno(boolean def, String msg);
+  void message(String message);
+
+  /** Requests the user to answer a yes/no question. */
+  boolean yesno(boolean defaultValue, String message);
+
+  /** Prints a message asking the user to let us know when it's safe to continue. */
+  void waitForUser();
+
+  /**
+   * Prompts the user for a string, suggesting a default.
+   *
+   * @return the chosen string from the list of allowed values.
+   */
+  String readString(String defaultValue, Set<String> allowedValues, String message);
 
   boolean isBatch();
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index d9fc7e5..5b86f46 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -34,9 +34,9 @@
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryH2Type;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.TestUpdateUI;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StatementExecutor;
 import com.google.inject.Guice;
 import com.google.inject.Key;
 import com.google.inject.ProvisionException;
@@ -45,7 +45,6 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.List;
 import java.util.UUID;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -133,28 +132,7 @@
       }
     }
 
-    u.update(
-        new UpdateUI() {
-          @Override
-          public void message(String msg) {}
-
-          @Override
-          public boolean yesno(boolean def, String msg) {
-            return def;
-          }
-
-          @Override
-          public boolean isBatch() {
-            return true;
-          }
-
-          @Override
-          public void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException {
-            for (String sql : pruneList) {
-              e.execute(sql);
-            }
-          }
-        });
+    u.update(new TestUpdateUI());
 
     db.assertSchemaVersion();
     final SystemConfig sc = db.getSystemConfig();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestUpdateUI.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestUpdateUI.java
index 644f8e2..d4acbcb 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestUpdateUI.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestUpdateUI.java
@@ -18,21 +18,34 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.StatementExecutor;
 import java.util.List;
+import java.util.Set;
 
 public class TestUpdateUI implements UpdateUI {
   @Override
-  public void message(String msg) {}
+  public void message(String message) {}
 
   @Override
-  public boolean yesno(boolean def, String msg) {
-    return false;
+  public boolean yesno(boolean defaultValue, String message) {
+    return defaultValue;
+  }
+
+  @Override
+  public void waitForUser() {}
+
+  @Override
+  public String readString(String defaultValue, Set<String> allowedValues, String message) {
+    return defaultValue;
   }
 
   @Override
   public boolean isBatch() {
-    return false;
+    return true;
   }
 
   @Override
-  public void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException {}
+  public void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException {
+    for (String sql : pruneList) {
+      e.execute(sql);
+    }
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index 78c9526..0fdd105 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -16,25 +16,68 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.common.PluginInfo;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.plugins.ListPlugins;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
+import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
+import java.util.Map;
+import org.kohsuke.args4j.Option;
 
 @RequiresCapability(GlobalCapability.VIEW_PLUGINS)
 @CommandMetaData(name = "ls", description = "List the installed plugins", runsAt = MASTER_OR_SLAVE)
 final class PluginLsCommand extends SshCommand {
-  @Inject private ListPlugins impl;
+  @Inject private ListPlugins list;
+
+  @Option(
+    name = "--all",
+    aliases = {"-a"},
+    usage = "List all plugins, including disabled plugins"
+  )
+  private boolean all;
+
+  @Option(name = "--format", usage = "output format")
+  private OutputFormat format = OutputFormat.TEXT;
 
   @Override
   public void run() throws Exception {
-    impl.display(stdout);
+    list.setAll(all);
+    Map<String, PluginInfo> output = list.apply(TopLevelResource.INSTANCE);
+
+    if (format.isJson()) {
+      format
+          .newGson()
+          .toJson(output, new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
+      stdout.print('\n');
+    } else {
+      stdout.format("%-30s %-10s %-8s %s\n", "Name", "Version", "Status", "File");
+      stdout.print(
+          "-------------------------------------------------------------------------------\n");
+      for (Map.Entry<String, PluginInfo> p : output.entrySet()) {
+        PluginInfo info = p.getValue();
+        stdout.format(
+            "%-30s %-10s %-8s %s\n",
+            p.getKey(),
+            Strings.nullToEmpty(info.version),
+            status(info.disabled),
+            Strings.nullToEmpty(info.filename));
+      }
+    }
+    stdout.flush();
+  }
+
+  private String status(Boolean disabled) {
+    return disabled != null && disabled.booleanValue() ? "DISABLED" : "ENABLED";
   }
 
   @Override
   protected void parseCommandLine() throws UnloggedFailure {
-    parseCommandLine(impl);
+    parseCommandLine(this);
   }
 }
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 3e6d16b..276a925 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
@@ -50,6 +50,10 @@
         observer: '_computeGroupName',
       },
       _groupName: String,
+      _groupOwner: {
+        type: Boolean,
+        value: false,
+      },
       _filteredLinks: Array,
       _showDownload: {
         type: Boolean,
@@ -123,16 +127,19 @@
             name: this._groupName,
             view: 'gr-group',
             url: `/admin/groups/${this.encodeURL(this._groupId, true)}`,
-            children: [
-              {
-                name: 'Audit Log',
-                detailType: 'audit-log',
-                view: 'gr-group-audit-log',
-                url: `/admin/groups/${this.encodeURL(this._groupId, true)}` +
-                      ',audit-log',
-              },
-            ],
+            children: [],
           };
+          if (this._groupOwner) {
+            linkCopy.subsection.children.push(
+                {
+                  name: 'Audit Log',
+                  detailType: 'audit-log',
+                  view: 'gr-group-audit-log',
+                  url: `/admin/groups/${this.encodeURL(this._groupId, true)}` +
+                        ',audit-log',
+                }
+            );
+          }
         }
         filteredLinks.push(linkCopy);
       }
@@ -203,6 +210,13 @@
       this.$.restAPI.getGroupConfig(groupId).then(group => {
         this._groupName = group.name;
         this.reload();
+        this.$.restAPI.getIsGroupOwner(group.name).then(
+            configs => {
+              if (configs.hasOwnProperty(group.name)) {
+                this._groupOwner = true;
+                this.reload();
+              }
+            });
       });
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index b347890..6129e9b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -90,10 +90,12 @@
       setup(done => {
         const pluginHost = fixture('plugin-host');
         pluginHost.config = {
-          js_resource_paths: [],
-          html_resource_paths: [
-            new URL('test/plugin.html', window.location.href).toString(),
-          ],
+          plugin: {
+            js_resource_paths: [],
+            html_resource_paths: [
+              new URL('test/plugin.html', window.location.href).toString(),
+            ],
+          },
         };
         element = fixture('element');
         const importSpy = sandbox.spy(element.$.externalStyle, '_import');
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 78f59db..cbbe828 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -14,9 +14,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
+<link rel="import" href="../../shared/gr-account-label/gr-account-label.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
@@ -135,6 +134,11 @@
       .negativeVote {
         box-shadow: inset 0 4.4em #ffd4d4;
       }
+      gr-account-label {
+        --gr-account-label-text-style: {
+          font-weight: bold;
+        };
+      }
     </style>
     <div class$="[[_computeClass(_expanded, showAvatar, message)]]">
       <gr-avatar account="[[author]]" image-size="100"></gr-avatar>
@@ -144,7 +148,10 @@
             <span class="name">[[message.real_author.name]]</span>
             on behalf of
           </span>
-          <span class="name">[[_authorOrAnon(author)]]</span>
+          <gr-account-label
+              account="[[author]]"
+              show-anonymous
+              hide-avatar></gr-account-label>
         </div>
         <template is="dom-if" if="[[message.message]]">
           <div class="content">
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 2593869..e6a761a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -90,10 +90,6 @@
       },
     },
 
-    behaviors: [
-      Gerrit.AnonymousNameBehavior,
-    ],
-
     observers: [
       '_updateExpandedClass(message.expanded)',
     ],
@@ -235,16 +231,6 @@
       this.fire('reply', {message: this.message});
     },
 
-    _authorOrAnon(author) {
-      if (author && author.name) {
-        return author.name;
-      } else if (author && author.email) {
-        return author.email;
-      }
-
-      return this.getAnonymousName(this.config);
-    },
-
     _projectNameChanged(name) {
       this.$.restAPI.getProjectConfig(name).then(config => {
         this._commentLinks = config.commentlinks;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index cfeb9cd..aa18c4e 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -65,37 +65,6 @@
       MockInteractions.tap(element.$$('.replyContainer gr-button'));
     });
 
-    test('reviewer update', () => {
-      const author = {
-        _account_id: 1115495,
-        name: 'Andrew Bonventre',
-        email: 'andybons@chromium.org',
-      };
-      const reviewer = {
-        _account_id: 123456,
-        name: 'Foo Bar',
-        email: 'barbar@chromium.org',
-      };
-      element.message = {
-        id: 0xDEADBEEF,
-        author,
-        reviewer,
-        date: '2016-01-12 20:24:49.448000000',
-        type: 'REVIEWER_UPDATE',
-        updates: [
-          {
-            message: 'Added to CC:',
-            reviewers: [reviewer],
-          },
-        ],
-      };
-      flushAsynchronousOperations();
-      const content = element.$$('.contentContainer');
-      assert.isOk(content);
-      assert.strictEqual(element.$$('gr-account-chip').account, reviewer);
-      assert.equal(author.name, element.$$('.author > .name').textContent);
-    });
-
     test('autogenerated prefix hiding', () => {
       element.message = {
         tag: 'autogenerated:gerrit:test',
@@ -211,27 +180,5 @@
       };
       assert.isOk(Polymer.dom(element.root).querySelector('.positiveVote'));
     });
-
-    test('test for Anonymous Coward user and replace with Anonymous', () => {
-      element.config = {
-        user: {
-          anonymous_coward_name: 'Anonymous Coward',
-        },
-      };
-      element.account = {};
-      assert.deepEqual(
-          element._authorOrAnon(element.account), 'Anonymous');
-    });
-
-    test('test for anonymous_coward_name', () => {
-      element.config = {
-        user: {
-          anonymous_coward_name: 'TestAnon',
-        },
-      };
-      element.account = {};
-      assert.deepEqual(
-          element._authorOrAnon(element.account, element.config), 'TestAnon');
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index e76a9e5..babd95c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -132,10 +132,12 @@
     test('lgtm plugin', done => {
       const pluginHost = fixture('plugin-host');
       pluginHost.config = {
-        js_resource_paths: [],
-        html_resource_paths: [
-          new URL('test/plugin.html', window.location.href).toString(),
-        ],
+        plugin: {
+          js_resource_paths: [],
+          html_resource_paths: [
+            new URL('test/plugin.html', window.location.href).toString(),
+          ],
+        },
       };
       element = fixture('basic');
       setupElement(element);
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index 3fc2c22..3083789 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -184,6 +184,7 @@
         return this._getUrlFor({
           view: Gerrit.Nav.View.CHANGE,
           changeNum: change._number,
+          project: change.project,
           patchNum: opt_patchNum,
           basePatchNum: opt_basePatchNum,
         });
@@ -215,9 +216,9 @@
       },
 
       /**
-       * @param {!number} change The change object.
-       * @param {!string} projectName The name of the project.
-       * @param {!string} path The file path.
+       * @param {number} changeNum
+       * @param {string} project The name of the project.
+       * @param {string} path The file path.
        * @param {number=} opt_patchNum
        * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
        *     used for none.
@@ -225,7 +226,7 @@
        * @param {boolean=} opt_leftSide
        * @return {string}
        */
-      getUrlForDiffById(changeNum, projectName, path, opt_patchNum,
+      getUrlForDiffById(changeNum, project, path, opt_patchNum,
           opt_basePatchNum, opt_lineNum, opt_leftSide) {
         if (opt_basePatchNum === PARENT_PATCHNUM) {
           opt_basePatchNum = undefined;
@@ -235,7 +236,7 @@
         return this._getUrlFor({
           view: Gerrit.Nav.View.DIFF,
           changeNum,
-          projectName,
+          project,
           path,
           patchNum: opt_patchNum,
           basePatchNum: opt_basePatchNum,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 2908f85..440f44b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -153,6 +153,9 @@
         -webkit-user-select: text;
         user-select: text;
       }
+      .editLoaded .hideOnEdit {
+        display: none;
+      }
       @media screen and (max-width: 50em) {
         header {
           padding: .5em var(--default-horizontal-margin);
@@ -200,6 +203,7 @@
       }
     </style>
     <gr-fixed-panel
+        class$="[[_computeContainerClass(_editLoaded)]]"
         floating-disabled="[[_panelFloatingDisabled]]"
         keep-on-scroll
         ready-for-measure="[[!_loading]]">
@@ -210,10 +214,10 @@
           <span>[[_change.subject]]</span>
           <span class="dash">—</span>
           <input id="reviewed"
-                 class="reviewed"
-                 type="checkbox"
-                 on-change="_handleReviewedChange"
-                 hidden$="[[!_loggedIn]]" hidden>
+              class="reviewed hideOnEdit"
+              type="checkbox"
+              on-change="_handleReviewedChange"
+              hidden$="[[!_loggedIn]]" hidden>
           <div class="jumpToFileContainer desktop">
             <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
               <span>[[_computeFileDisplayName(_path)]]</span>
@@ -231,9 +235,9 @@
                     as="path"
                     initial-count="75">
                   <a href$="[[_computeDiffURL(_change, _patchRange.*, path)]]"
-                     selected$="[[_computeFileSelected(path, _path)]]"
-                     data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
-                     on-tap="_handleFileTap">[[_computeFileDisplayName(path)]]</a>
+                    selected$="[[_computeFileSelected(path, _path)]]"
+                    data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
+                    on-tap="_handleFileTap">[[_computeFileDisplayName(path)]]</a>
                 </template>
               </div>
             </iron-dropdown>
@@ -277,7 +281,7 @@
           <span class="download desktop">
             <span class="separator">/</span>
             <a class="downloadLink"
-               href$="[[_computeDownloadLink(_changeNum, _patchRange, _path)]]">
+              href$="[[_computeDownloadLink(_changeNum, _patchRange, _path)]]">
               Download
             </a>
           </span>
@@ -306,7 +310,7 @@
       </div>
       <div class="fileNav mobile">
         <a class="mobileNavLink"
-           href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
+          href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
           &lt;</a>
         <div class="fullFileName mobile">[[_computeFileDisplayName(_path)]]
         </div>
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 0f6a07d..c12f700 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
@@ -113,6 +113,10 @@
         type: Boolean,
         value: () => { return window.PANEL_FLOATING_DISABLED; },
       },
+      _editLoaded: {
+        type: Boolean,
+        computed: '_computeEditLoaded(_patchRange.*)',
+      },
     },
 
     behaviors: [
@@ -205,6 +209,7 @@
     },
 
     _setReviewed(reviewed) {
+      if (this._editLoaded) { return; }
       this.$.reviewed.checked = reviewed;
       this._saveReviewedState(reviewed).catch(err => {
         this.fire('show-alert', {message: ERR_REVIEW_STATUS});
@@ -767,5 +772,20 @@
         return 'noOverflow';
       }
     },
+
+    /**
+     * @param {!Object} patchRangeRecord
+     */
+    _computeEditLoaded(patchRangeRecord) {
+      const patchRange = patchRangeRecord.base || {};
+      return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
+    },
+
+    /**
+     * @param {boolean} editLoaded
+     */
+    _computeContainerClass(editLoaded) {
+      return editLoaded ? 'editLoaded' : '';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 8ff75c1..49e82d4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -494,6 +494,17 @@
       });
     });
 
+    test('file review status with edit loaded', () => {
+      const saveReviewedStub = sandbox.stub(element, '_saveReviewedState');
+
+      element._patchRange = {patchNum: element.EDIT_NAME};
+      flushAsynchronousOperations();
+
+      assert.isTrue(element._editLoaded);
+      element._setReviewed();
+      assert.isFalse(saveReviewedStub.called);
+    });
+
     test('hash is determined from params', done => {
       stub('gr-rest-api-interface', {
         getDiffComments() { return Promise.resolve({}); },
@@ -746,5 +757,34 @@
         assert.isFalse(upgradeStub.called);
       });
     });
+
+    test('_computeEditLoaded', () => {
+      const callCompute = range => element._computeEditLoaded({base: range});
+      assert.isFalse(callCompute({}));
+      assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}));
+      assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}));
+      assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}));
+    });
+
+    suite('editLoaded behavior', () => {
+      setup(() => {
+        element._loggedIn = true;
+      });
+
+      const isVisible = el => {
+        assert.ok(el);
+        return getComputedStyle(el).getPropertyValue('display') !== 'none';
+      };
+
+      test('reviewed checkbox', () => {
+        element._patchRange = {patchNum: '1'};
+        // Reviewed checkbox should be shown.
+        assert.isTrue(isVisible(element.$.reviewed));
+        element.set('_patchRange.patchNum', element.EDIT_NAME);
+        flushAsynchronousOperations();
+
+        assert.isFalse(isVisible(element.$.reviewed));
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 07ecb8d..defbe8a 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -206,7 +206,7 @@
     <gr-reporting id="reporting"></gr-reporting>
     <gr-router id="router"></gr-router>
     <gr-plugin-host id="plugins"
-        config="[[_serverConfig.plugin]]">
+        config="[[_serverConfig]]">
     </gr-plugin-host>
     <gr-external-style id="externalStyle" name="app-theme"></gr-external-style>
   </template>
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 905c5c4..3712ffa 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -50,7 +50,7 @@
         getConfig() {
           return Promise.resolve({
             gerrit: {web_uis: ['GWT', 'POLYGERRIT']},
-            plugin: {js_resource_paths: []},
+            plugin: {},
           });
         },
         getPreferences() { return Promise.resolve({my: []}); },
@@ -100,11 +100,9 @@
       });
     });
 
-    test('passes config to gr-plugin-host', done => {
-      element.$.restAPI.getConfig.lastCall.returnValue.then(config => {
-        const pluginConfig = config.plugin;
-        assert.deepEqual(element.$.plugins.config, pluginConfig);
-        done();
+    test('passes config to gr-plugin-host', () => {
+      return element.$.restAPI.getConfig.lastCall.returnValue.then(config => {
+        assert.deepEqual(element.$.plugins.config, config);
       });
     });
   });
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 03b6d56..1ce8620 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
@@ -29,8 +29,9 @@
     ],
 
     _configChanged(config) {
-      const jsPlugins = config.js_resource_paths || [];
-      const htmlPlugins = config.html_resource_paths || [];
+      const plugins = config.plugin;
+      const jsPlugins = plugins.js_resource_paths || [];
+      const htmlPlugins = plugins.html_resource_paths || [];
       const defaultTheme = config.default_theme;
       if (defaultTheme) {
         // Make theme first to be first to load.
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index 2cacede..27adbe1 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -48,15 +48,17 @@
     test('counts plugins', () => {
       sandbox.stub(Gerrit, '_setPluginsCount');
       element.config = {
-        html_resource_paths: ['foo/bar', 'baz'],
-        js_resource_paths: ['42'],
+        plugin: {
+          html_resource_paths: ['foo/bar', 'baz'],
+          js_resource_paths: ['42'],
+        },
       };
       assert.isTrue(Gerrit._setPluginsCount.calledWith(3));
     });
 
     test('imports relative html plugins from config', () => {
       element.config = {
-        html_resource_paths: ['foo/bar', 'baz'],
+        plugin: {html_resource_paths: ['foo/bar', 'baz']},
       };
       assert.isTrue(element.importHref.calledWith(
           '/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
@@ -67,8 +69,7 @@
     test('imports relative html plugins from config with a base url', () => {
       sandbox.stub(element, 'getBaseUrl').returns('/the-base');
       element.config = {
-        html_resource_paths: ['foo/bar', 'baz'],
-      };
+        plugin: {html_resource_paths: ['foo/bar', 'baz']}};
       assert.isTrue(element.importHref.calledWith(
           '/the-base/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
           true));
@@ -79,10 +80,12 @@
 
     test('imports absolute html plugins from config', () => {
       element.config = {
-        html_resource_paths: [
-          'http://example.com/foo/bar',
-          'https://example.com/baz',
-        ],
+        plugin: {
+          html_resource_paths: [
+            'http://example.com/foo/bar',
+            'https://example.com/baz',
+          ],
+        },
       };
       assert.isTrue(element.importHref.calledWith(
           'http://example.com/foo/bar', Gerrit._pluginInstalled,
@@ -93,17 +96,13 @@
     });
 
     test('adds js plugins from config to the body', () => {
-      element.config = {
-        js_resource_paths: ['foo/bar', 'baz'],
-      };
+      element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
       assert.isTrue(document.body.appendChild.calledTwice);
     });
 
     test('imports relative js plugins from config', () => {
       sandbox.stub(element, '_createScriptTag');
-      element.config = {
-        js_resource_paths: ['foo/bar', 'baz'],
-      };
+      element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
       assert.isTrue(element._createScriptTag.calledWith('/foo/bar'));
       assert.isTrue(element._createScriptTag.calledWith('/baz'));
     });
@@ -111,9 +110,7 @@
     test('imports relative html plugins from config with a base url', () => {
       sandbox.stub(element, '_createScriptTag');
       sandbox.stub(element, 'getBaseUrl').returns('/the-base');
-      element.config = {
-        js_resource_paths: ['foo/bar', 'baz'],
-      };
+      element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
       assert.isTrue(element._createScriptTag.calledWith('/the-base/foo/bar'));
       assert.isTrue(element._createScriptTag.calledWith('/the-base/baz'));
     });
@@ -121,10 +118,12 @@
     test('imports absolute html plugins from config', () => {
       sandbox.stub(element, '_createScriptTag');
       element.config = {
-        js_resource_paths: [
-          'http://example.com/foo/bar',
-          'https://example.com/baz',
-        ],
+        plugin: {
+          js_resource_paths: [
+            'http://example.com/foo/bar',
+            'https://example.com/baz',
+          ],
+        },
       };
       assert.isTrue(element._createScriptTag.calledWith(
           'http://example.com/foo/bar'));
@@ -135,7 +134,9 @@
     test('default theme is loaded with html plugins', () => {
       element.config = {
         default_theme: '/oof',
-        html_resource_paths: ['some'],
+        plugin: {
+          html_resource_paths: ['some'],
+        },
       };
       assert.isTrue(element.importHref.calledWith(
           '/oof', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index a93198c..3811a91 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -14,8 +14,11 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../gr-avatar/gr-avatar.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-account-label">
@@ -33,15 +36,20 @@
         margin-right: .15em;
         vertical-align: -.25em;
       }
+      .text {
+        @apply(--gr-account-label-text-style);
+      }
       .text:hover {
         @apply(--gr-account-label-text-hover-style);
       }
     </style>
-    <span title$="[[_computeAccountTitle(account)]]">
-      <gr-avatar account="[[account]]"
-          image-size="[[avatarImageSize]]"></gr-avatar>
+    <span>
+      <template is="dom-if" if="[[!hideAvatar]]">
+        <gr-avatar account="[[account]]"
+            image-size="[[avatarImageSize]]"></gr-avatar>
+      </template>
       <span class="text">
-        <span>[[account.name]]</span>
+        <span>[[_computeName(account, _serverConfig, showAnonymous)]]</span>
         <span hidden$="[[!_computeShowEmail(showEmail, account)]]">
           [[_computeEmailStr(account)]]
         </span>
@@ -50,6 +58,7 @@
         </template>
       </span>
     </span>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="../../../scripts/util.js"></script>
   <script src="gr-account-label.js"></script>
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 9ebef82..0071e15 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
@@ -27,6 +27,50 @@
         type: Boolean,
         value: false,
       },
+      title: {
+        type: String,
+        reflectToAttribute: true,
+        computed: '_computeAccountTitle(account)',
+      },
+      hasTooltip: {
+        type: Boolean,
+        reflectToAttribute: true,
+        computed: '_computeHasTooltip(account)',
+      },
+      hideAvatar: {
+        type: Boolean,
+        value: false,
+      },
+      showAnonymous: {
+        type: Boolean,
+        value: false,
+      },
+      _serverConfig: {
+        type: Object,
+        value: null,
+      },
+    },
+
+    behaviors: [
+      Gerrit.AnonymousNameBehavior,
+      Gerrit.TooltipBehavior,
+    ],
+
+    ready() {
+      if (this.showAnonymous) {
+        this.$.restAPI.getConfig()
+            .then(config => { this._serverConfig = config; });
+      }
+    },
+
+    _computeName(account, config, showAnonymous) {
+      if (account && account.name) {
+        return account.name;
+      }
+      if (showAnonymous) {
+        return this.getAnonymousName(config);
+      }
+      return '';
     },
 
     _computeAccountTitle(account) {
@@ -54,5 +98,10 @@
       }
       return account.email;
     },
+
+    _computeHasTooltip(account) {
+      // If an account has loaded to fire this method, then set to true.
+      return !!account;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index 3b1e1de..7095be3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -89,5 +89,40 @@
           element._computeEmailStr({name: 'test', email: 'test'}), '(test)');
       assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test');
     });
+
+    suite('_computeName', () => {
+      test('not showing anonymous', () => {
+        const account = {name: 'Wyatt'};
+        assert.deepEqual(element._computeName(account, null, false), 'Wyatt');
+      });
+
+      test('showing anonymous but no config', () => {
+        const account = {};
+        assert.deepEqual(element._computeName(account, null, true),
+            'Anonymous');
+      });
+
+      test('test for Anonymous Coward user and replace with Anonymous', () => {
+        const config = {
+          user: {
+            anonymous_coward_name: 'Anonymous Coward',
+          },
+        };
+        const account = {};
+        assert.deepEqual(element._computeName(account, config, true),
+            'Anonymous');
+      });
+
+      test('test for anonymous_coward_name', () => {
+        const config = {
+          user: {
+            anonymous_coward_name: 'TestAnon',
+          },
+        };
+        const account = {};
+        assert.deepEqual(element._computeName(account, config, true),
+            'TestAnon');
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index a631c2f..d1b9417 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -67,12 +67,16 @@
     const base = Gerrit.BaseUrlBehavior.getBaseUrl();
 
     this._url = new URL(opt_url);
-    if (!this._url.pathname.startsWith(base + '/plugins')) {
+    const pathname = this._url.pathname.replace(base, '');
+    // Site theme is server from predefined path.
+    if (pathname === '/static/gerrit-theme.html') {
+      this._name = 'gerrit-theme';
+    } else if (!pathname.startsWith('/plugins')) {
       console.warn('Plugin not being loaded from /plugins base path:',
           this._url.href, '— Unable to determine name.');
       return;
     }
-    this._name = this._url.pathname.replace(base, '').split('/')[2];
+    this._name = pathname.split('/')[2];
   }
 
   Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');
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 5ddd0fe..6e780ab 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
@@ -94,10 +94,12 @@
      * Doesn't do error checking. Supports cancel condition. Performs auth.
      * Validates auth expiry errors.
      * @param {string} url
-     * @param {function(response, error)} opt_errFn
-     * @param {function()} opt_cancelCondition
-     * @param {Object=} opt_params URL params, key-value hash.
-     * @param {Object=} opt_options Fetch options.
+     * @param {?function(?Response, string=)=} opt_errFn
+     *    passed as null sometimes.
+     * @param {?function()=} opt_cancelCondition
+     *    passed as null sometimes.
+     * @param {?Object=} opt_params URL params, key-value hash.
+     * @param {?Object=} opt_options Fetch options.
      */
     _fetchRawJSON(url, opt_errFn, opt_cancelCondition, opt_params,
         opt_options) {
@@ -119,7 +121,7 @@
           return;
         }
         if (opt_errFn) {
-          opt_errFn.call(null, null, err);
+          opt_errFn.call(undefined, null, err);
         } else {
           this.fire('network-error', {error: err});
         }
@@ -132,10 +134,12 @@
      * Returns a Promise that resolves to a parsed response.
      * Same as {@link _fetchRawJSON}, plus error handling.
      * @param {string} url
-     * @param {function(response, error)} opt_errFn
-     * @param {function()} opt_cancelCondition
-     * @param {Object=} opt_params URL params, key-value hash.
-     * @param {Object=} opt_options Fetch options.
+     * @param {?function(?Response, string=)=} opt_errFn
+     *    passed as null sometimes.
+     * @param {?function()=} opt_cancelCondition
+     *    passed as null sometimes.
+     * @param {?Object=} opt_params URL params, key-value hash.
+     * @param {?Object=} opt_options Fetch options.
      */
     fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params, opt_options) {
       return this._fetchRawJSON(
@@ -156,6 +160,11 @@
           });
     },
 
+    /**
+     * @param {string} url
+     * @param {?Object=} opt_params URL params, key-value hash.
+     * @return {string}
+     */
     _urlWithParams(url, opt_params) {
       if (!opt_params) { return this.getBaseUrl() + url; }
 
@@ -172,6 +181,10 @@
       return this.getBaseUrl() + url + '?' + params.join('&');
     },
 
+    /**
+     * @param {!Object} response
+     * @return {?}
+     */
     getResponseObject(response) {
       return response.text().then(text => {
         let result;
@@ -204,6 +217,11 @@
           opt_errFn, opt_ctx);
     },
 
+    /**
+     * @param {?Object} config
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     createProject(config, opt_errFn, opt_ctx) {
       if (!config.name) { return ''; }
       const encodeName = encodeURIComponent(config.name);
@@ -211,6 +229,11 @@
           opt_ctx);
     },
 
+    /**
+     * @param {?Object} config
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     createGroup(config, opt_errFn, opt_ctx) {
       if (!config.name) { return ''; }
       const encodeName = encodeURIComponent(config.name);
@@ -223,6 +246,12 @@
       return this._fetchSharedCacheURL('/groups/' + encodeName + '/detail');
     },
 
+    /**
+     * @param {string} project
+     * @param {string} ref
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     deleteProjectBranches(project, ref, opt_errFn, opt_ctx) {
       if (!project || !ref) {
         return '';
@@ -234,6 +263,12 @@
           opt_errFn, opt_ctx);
     },
 
+    /**
+     * @param {string} project
+     * @param {string} ref
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     deleteProjectTags(project, ref, opt_errFn, opt_ctx) {
       if (!project || !ref) {
         return '';
@@ -245,6 +280,13 @@
           opt_errFn, opt_ctx);
     },
 
+    /**
+     * @param {string} name
+     * @param {string} branch
+     * @param {string} revision
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     createProjectBranch(name, branch, revision, opt_errFn, opt_ctx) {
       if (!name || !branch || !revision) { return ''; }
       const encodeName = encodeURIComponent(name);
@@ -254,6 +296,13 @@
           revision, opt_errFn, opt_ctx);
     },
 
+    /**
+     * @param {string} name
+     * @param {string} tag
+     * @param {string} revision
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     createProjectTag(name, tag, revision, opt_errFn, opt_ctx) {
       if (!name || !tag || !revision) { return ''; }
       const encodeName = encodeURIComponent(name);
@@ -324,6 +373,11 @@
       });
     },
 
+    /**
+     * @param {?Object} prefs
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     savePreferences(prefs, opt_errFn, opt_ctx) {
       // Note (Issue 5142): normalize the download scheme with lower case before
       // saving.
@@ -335,6 +389,11 @@
           opt_ctx);
     },
 
+    /**
+     * @param {?Object} prefs
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     saveDiffPreferences(prefs, opt_errFn, opt_ctx) {
       // Invalidate the cache.
       this._cache['/accounts/self/preferences.diff'] = undefined;
@@ -354,16 +413,31 @@
       return this._fetchSharedCacheURL('/accounts/self/emails');
     },
 
+    /**
+     * @param {string} email
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     addAccountEmail(email, opt_errFn, opt_ctx) {
       return this.send('PUT', '/accounts/self/emails/' +
           encodeURIComponent(email), null, opt_errFn, opt_ctx);
     },
 
+    /**
+     * @param {string} email
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     deleteAccountEmail(email, opt_errFn, opt_ctx) {
       return this.send('DELETE', '/accounts/self/emails/' +
           encodeURIComponent(email), null, opt_errFn, opt_ctx);
     },
 
+    /**
+     * @param {string} email
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     setPreferredAccountEmail(email, opt_errFn, opt_ctx) {
       return this.send('PUT', '/accounts/self/emails/' +
           encodeURIComponent(email) + '/preferred', null,
@@ -384,6 +458,11 @@
           });
     },
 
+    /**
+     * @param {string} name
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     setAccountName(name, opt_errFn, opt_ctx) {
       return this.send('PUT', '/accounts/self/name', {name}, opt_errFn,
           opt_ctx).then(response => {
@@ -401,6 +480,11 @@
           });
     },
 
+    /**
+     * @param {string} status
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     setAccountStatus(status, opt_errFn, opt_ctx) {
       return this.send('PUT', '/accounts/self/status', {status},
           opt_errFn, opt_ctx).then(response => {
@@ -426,6 +510,9 @@
       return this._fetchSharedCacheURL('/accounts/self/agreements');
     },
 
+    /**
+     * @param {string=} opt_params
+     */
     getAccountCapabilities(opt_params) {
       let queryString = '';
       if (opt_params) {
@@ -500,6 +587,11 @@
       return this._fetchSharedCacheURL('/accounts/self/watched.projects');
     },
 
+    /**
+     * @param {string} projects
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     saveWatchedProjects(projects, opt_errFn, opt_ctx) {
       return this.send('POST', '/accounts/self/watched.projects', projects,
           opt_errFn, opt_ctx)
@@ -508,11 +600,20 @@
           });
     },
 
+    /**
+     * @param {string} projects
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     deleteWatchedProjects(projects, opt_errFn, opt_ctx) {
       return this.send('POST', '/accounts/self/watched.projects:delete',
           projects, opt_errFn, opt_ctx);
     },
 
+    /**
+     * @param {string} url
+     * @param {function(?Response, string=)=} opt_errFn
+     */
     _fetchSharedCacheURL(url, opt_errFn) {
       if (this._sharedFetchPromises[url]) {
         return this._sharedFetchPromises[url];
@@ -540,13 +641,13 @@
     },
 
     /**
-     * @param {!number} opt_changesPerPage
-     * @param {!string|Array<string>} opt_query A query or an array of queries.
-     * @param {!number} opt_offset
-     * @param {!Object} opt_options
-     * @return {Array<Object>|Array<Array<Object>>} If opt_query is an array,
-     *     fetchJSON will return an array of arrays of changeInfos. If it is
-     *     unspecified or a string, fetchJSON will return an array of
+     * @param {number=} opt_changesPerPage
+     * @param {string|!Array<string>=} opt_query A query or an array of queries.
+     * @param {number|string=} opt_offset
+     * @param {!Object=} opt_options
+     * @return {?Array<!Object>|?Array<!Array<!Object>>} If opt_query is an
+     *     array, fetchJSON will return an array of arrays of changeInfos. If it
+     *     is unspecified or a string, fetchJSON will return an array of
      *     changeInfos.
      */
     getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) {
@@ -587,7 +688,7 @@
 
     /**
      * Inserts a change into _projectLookup iff it has a valid structure.
-     * @param {!Object} change
+     * @param {?{ _number: (number|string) }} change
      */
     _maybeInsertInLookup(change) {
       if (change && change.project && change._number) {
@@ -595,17 +696,31 @@
       }
     },
 
+    /**
+     * TODO (beckysiegel) this needs to be rewritten with the optional param
+     * at the end.
+     *
+     * @param {number|string} changeNum
+     * @param {?number|string=} opt_patchNum passed as null sometimes.
+     * @param {?=} endpoint
+     * @return {!Promise<string>}
+     */
     getChangeActionURL(changeNum, opt_patchNum, endpoint) {
       return this._changeBaseURL(changeNum, opt_patchNum)
           .then(url => url + endpoint);
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {function()=} opt_cancelCondition
+     */
     getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
       const options = this.listChangesOptionsToHex(
+          this.ListChangesOption.ALL_COMMITS,
           this.ListChangesOption.ALL_REVISIONS,
           this.ListChangesOption.CHANGE_ACTIONS,
           this.ListChangesOption.CURRENT_ACTIONS,
-          this.ListChangesOption.CURRENT_COMMIT,
           this.ListChangesOption.DOWNLOAD_COMMANDS,
           this.ListChangesOption.SUBMITTABLE,
           this.ListChangesOption.WEB_LINKS
@@ -615,6 +730,11 @@
           .then(GrReviewerUpdatesParser.parse);
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {function()=} opt_cancelCondition
+     */
     getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
       const params = this.listChangesOptionsToHex(
           this.ListChangesOption.ALL_REVISIONS
@@ -623,6 +743,11 @@
           opt_cancelCondition);
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {function()=} opt_cancelCondition
+     */
     _getChangeDetail(changeNum, params, opt_errFn,
         opt_cancelCondition) {
       return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
@@ -651,10 +776,18 @@
       });
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {number|string} patchNum
+     */
     getChangeCommitInfo(changeNum, patchNum) {
       return this._getChangeURLAndFetch(changeNum, '/commit?links', patchNum);
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {!Promise<?Object>} patchRange
+     */
     getChangeFiles(changeNum, patchRange) {
       let endpoint = '/files';
       if (patchRange.basePatchNum !== 'PARENT') {
@@ -669,12 +802,22 @@
           this._normalizeChangeFilesResponse.bind(this));
     },
 
+    /**
+     * The closure compiler doesn't realize this.specialFilePathCompare is
+     * valid.
+     * @suppress {checkTypes}
+     */
     getChangeFilePathsAsSpeciallySortedArray(changeNum, patchRange) {
       return this.getChangeFiles(changeNum, patchRange).then(files => {
         return Object.keys(files).sort(this.specialFilePathCompare);
       });
     },
 
+    /**
+     * The closure compiler doesn't realize this.specialFilePathCompare is
+     * valid.
+     * @suppress {checkTypes}
+     */
     _normalizeChangeFilesResponse(response) {
       if (!response) { return []; }
       const paths = Object.keys(response).sort(this.specialFilePathCompare);
@@ -702,6 +845,11 @@
           });
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {string} inputVal
+     * @param {function(?Response, string=)=} opt_errFn
+     */
     getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
       const params = {n: 10};
       if (inputVal) { params.q = inputVal; }
@@ -720,6 +868,12 @@
       return filter;
     },
 
+    /**
+     * @param {string} filter
+     * @param {number} groupsPerPage
+     * @param {number=} opt_offset
+     * @return {!Promise<?Object>}
+     */
     getGroups(filter, groupsPerPage, opt_offset) {
       const offset = opt_offset || 0;
 
@@ -729,6 +883,12 @@
       );
     },
 
+    /**
+     * @param {string} filter
+     * @param {number} projectsPerPage
+     * @param {number=} opt_offset
+     * @return {!Promise<?Object>}
+     */
     getProjects(filter, projectsPerPage, opt_offset) {
       const offset = opt_offset || 0;
 
@@ -743,6 +903,13 @@
           'PUT', `/projects/${encodeURIComponent(project)}/HEAD`, {ref});
     },
 
+    /**
+     * @param {string} filter
+     * @param {string} project
+     * @param {number} projectsBranchesPerPage
+     * @param {number=} opt_offset
+     * @return {!Promise<?Object>}
+     */
     getProjectBranches(filter, project, projectsBranchesPerPage, opt_offset) {
       const offset = opt_offset || 0;
 
@@ -753,6 +920,13 @@
       );
     },
 
+    /**
+     * @param {string} filter
+     * @param {string} project
+     * @param {number} projectsTagsPerPage
+     * @param {number=} opt_offset
+     * @return {!Promise<?Object>}
+     */
     getProjectTags(filter, project, projectsTagsPerPage, opt_offset) {
       const offset = opt_offset || 0;
 
@@ -763,6 +937,12 @@
       );
     },
 
+    /**
+     * @param {string} filter
+     * @param {number} pluginsPerPage
+     * @param {number=} opt_offset
+     * @return {!Promise<?Object>}
+     */
     getPlugins(filter, pluginsPerPage, opt_offset) {
       const offset = opt_offset || 0;
 
@@ -772,12 +952,24 @@
       );
     },
 
+    /**
+     * @param {string} inputVal
+     * @param {number} opt_n
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     getSuggestedGroups(inputVal, opt_n, opt_errFn, opt_ctx) {
       const params = {s: inputVal};
       if (opt_n) { params.n = opt_n; }
       return this.fetchJSON('/groups/', opt_errFn, opt_ctx, params);
     },
 
+    /**
+     * @param {string} inputVal
+     * @param {number} opt_n
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     getSuggestedProjects(inputVal, opt_n, opt_errFn, opt_ctx) {
       const params = {
         m: inputVal,
@@ -788,6 +980,12 @@
       return this.fetchJSON('/projects/', opt_errFn, opt_ctx, params);
     },
 
+    /**
+     * @param {string} inputVal
+     * @param {number} opt_n
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     getSuggestedAccounts(inputVal, opt_n, opt_errFn, opt_ctx) {
       if (!inputVal) {
         return Promise.resolve([]);
@@ -880,6 +1078,14 @@
       return this._getChangeURLAndFetch(changeNum, '/files?reviewed', patchNum);
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {number|string} patchNum
+     * @param {string} path
+     * @param {boolean} reviewed
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn, opt_ctx) {
       const method = reviewed ? 'PUT' : 'DELETE';
       const e = `/files/${encodeURIComponent(path)}/reviewed`;
@@ -887,6 +1093,13 @@
           opt_errFn, opt_ctx);
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {number|string} patchNum
+     * @param {!Object} review
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     */
     saveChangeReview(changeNum, patchNum, review, opt_errFn, opt_ctx) {
       const promises = [
         this.awaitPendingDiffDrafts(),
@@ -963,12 +1176,23 @@
       return this.send(method, url);
     },
 
+    /**
+     * @param {string} method
+     * @param {string} url
+     * @param {?string|number|Object=} opt_body passed as null sometimes
+     *    and also apparently a number. TODO (beckysiegel) remove need for
+     *    number at least.
+     * @param {?function(?Response, string=)=} opt_errFn
+     *    passed as null sometimes.
+     * @param {?=} opt_ctx
+     * @param {?string=} opt_contentType
+     */
     send(method, url, opt_body, opt_errFn, opt_ctx, opt_contentType) {
       const options = {method};
       if (opt_body) {
-        options.headers = new Headers({
-          'Content-Type': opt_contentType || 'application/json',
-        });
+        options.headers = new Headers();
+        options.headers.set(
+            'Content-Type', opt_contentType || 'application/json');
         if (typeof opt_body !== 'string') {
           opt_body = JSON.stringify(opt_body);
         }
@@ -994,6 +1218,14 @@
           });
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {number|string} basePatchNum
+     * @param {number|string} patchNum
+     * @param {string} path
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {function()=} opt_cancelCondition
+     */
     getDiff(changeNum, basePatchNum, patchNum, path,
         opt_errFn, opt_cancelCondition) {
       const params = {
@@ -1010,6 +1242,12 @@
           opt_errFn, opt_cancelCondition, params);
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {number|string=} opt_basePatchNum
+     * @param {number|string=} opt_patchNum
+     * @param {string=} opt_path
+     */
     getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
       return this._getDiffComments(changeNum, '/comments', opt_basePatchNum,
           opt_patchNum, opt_path);
@@ -1026,10 +1264,10 @@
      * empty object.
      *
      * @param {number|string} changeNum
-     * @param {number|string} opt_basePatchNum
-     * @param {number|string} opt_patchNum
-     * @param {string} opt_path
-     * @return {Promise<Object>}
+     * @param {number|string=} opt_basePatchNum
+     * @param {number|string=} opt_patchNum
+     * @param {string=} opt_path
+     * @return {!Promise<?Object>}
      */
     getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
       return this.getLoggedIn().then(loggedIn => {
@@ -1062,17 +1300,24 @@
       return comments;
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {string} endpoint
+     * @param {number|string=} opt_basePatchNum
+     * @param {number|string=} opt_patchNum
+     * @param {string=} opt_path
+     */
     _getDiffComments(changeNum, endpoint, opt_basePatchNum,
         opt_patchNum, opt_path) {
       /**
        * Fetches the comments for a given patchNum.
        * Helper function to make promises more legible.
        *
-       * @param {string|number} patchNum
+       * @param {string|number=} opt_patchNum
        * @return {!Object} Diff comments response.
        */
-      const fetchComments = patchNum => {
-        return this._getChangeURLAndFetch(changeNum, endpoint, patchNum);
+      const fetchComments = opt_patchNum => {
+        return this._getChangeURLAndFetch(changeNum, endpoint, opt_patchNum);
       };
 
       if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
@@ -1122,6 +1367,11 @@
       });
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {string} endpoint
+     * @param {number|string=} opt_patchNum
+     */
     _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) {
       return this._changeBaseURL(changeNum, opt_patchNum)
           .then(url => url + endpoint);
@@ -1144,8 +1394,8 @@
     },
 
     /**
-     * @returns {Promise} A promise that resolves when all pending diff draft
-     *    sends have resolved.
+     * @returns {!Promise<undefined>} A promise that resolves when all pending
+     *    diff draft sends have resolved.
      */
     awaitPendingDiffDrafts() {
       return Promise.all(this._pendingRequests[Requests.SEND_DIFF_DRAFT] || [])
@@ -1192,6 +1442,12 @@
           });
     },
 
+    /**
+     * @param {string} changeId
+     * @param {string|number} patchNum
+     * @param {string} path
+     * @param {number=} opt_parentIndex
+     */
     getChangeFileContents(changeId, patchNum, path, opt_parentIndex) {
       const parent = typeof opt_parentIndex === 'number' ?
           '?parent=' + opt_parentIndex : '';
@@ -1244,8 +1500,8 @@
     },
 
     /**
-     * @param {string} changeNum
-     * @param {number|string=} opt_patchNum
+     * @param {number|string} changeNum
+     * @param {?number|string=} opt_patchNum passed as null sometimes.
      * @param {string=} opt_project
      * @return {!Promise<string>}
      */
@@ -1264,12 +1520,22 @@
       });
     },
 
+    /**
+     * @suppress {checkTypes}
+     * Resulted in error: Promise.prototype.then does not match formal
+     * parameter.
+     */
     setChangeTopic(changeNum, topic) {
       const p = {topic};
       return this.getChangeURLAndSend(changeNum, 'PUT', null, '/topic', p)
           .then(this.getResponseObject);
     },
 
+    /**
+     * @suppress {checkTypes}
+     * Resulted in error: Promise.prototype.then does not match formal
+     * parameter.
+     */
     setChangeHashtag(changeNum, hashtag) {
       return this.getChangeURLAndSend(changeNum, 'POST', null, '/hashtags',
           hashtag).then(this.getResponseObject);
@@ -1279,6 +1545,11 @@
       return this.send('DELETE', '/accounts/self/password.http');
     },
 
+    /**
+     * @suppress {checkTypes}
+     * Resulted in error: Promise.prototype.then does not match formal
+     * parameter.
+     */
     generateAccountHttpPassword() {
       return this.send('PUT', '/accounts/self/password.http', {generate: true})
           .then(this.getResponseObject);
@@ -1344,6 +1615,10 @@
           });
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {number|string=} opt_message
+     */
     startWorkInProgress(changeNum, opt_message) {
       const payload = {};
       if (opt_message) {
@@ -1357,11 +1632,21 @@
           });
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {number|string=} opt_body
+     * @param {function(?Response, string=)=} opt_errFn
+     */
     startReview(changeNum, opt_body, opt_errFn) {
       return this.getChangeURLAndSend(changeNum, 'POST', null, '/ready',
           opt_body, opt_errFn);
     },
 
+    /**
+     * @suppress {checkTypes}
+     * Resulted in error: Promise.prototype.then does not match formal
+     * parameter.
+     */
     deleteComment(changeNum, patchNum, commentID, reason) {
       const endpoint = `/comments/${commentID}/delete`;
       const payload = {reason};
@@ -1372,8 +1657,8 @@
     /**
      * Given a changeNum, gets the change.
      *
-     * @param {string} changeNum
-     * @return {Promise<Object>} The change
+     * @param {number|string} changeNum
+     * @return {!Promise<?Object>} The change
      */
     getChange(changeNum) {
       // Cannot use _changeBaseURL, as this function is used by _projectLookup.
@@ -1382,7 +1667,7 @@
 
     /**
      * @param {string|number} changeNum
-     * @param {string} project
+     * @param {string=} project
      */
     setInProjectLookup(changeNum, project) {
       if (this._projectLookup[changeNum] &&
@@ -1399,7 +1684,7 @@
      * _projectLookup with the project for that change, and returns the project.
      *
      * @param {string|number} changeNum
-     * @return {Promise<string>}
+     * @return {!Promise<string|undefined>}
      */
     _getFromProjectLookup(changeNum) {
       const project = this._projectLookup[changeNum];
@@ -1413,18 +1698,21 @@
 
     /**
      * Alias for _changeBaseURL.then(send).
-     *
+     * @TODO(beckysiegel) clean up comments
      * @param {string|number} changeNum
      * @param {string} method
-     * @param {string} endpoint
-     * @param {string|number=} opt_patchNum
-     * @param {!Object=} opt_payload
-     * @param {function(?Response, string)=} opt_errFn
+     * @param {?string|number} patchNum gets passed as null.
+     * @param {?string} endpoint gets passed as null.
+     * @param {?Object|number|string=} opt_payload gets passed as null, string,
+     *    Object, or number.
+     * @param {function(?Response, string=)=} opt_errFn
+     * @param {?=} opt_ctx
+     * @param {?=} opt_contentType
      * @return {!Promise<!Object>}
      */
-    getChangeURLAndSend(changeNum, method, opt_patchNum, endpoint, opt_payload,
+    getChangeURLAndSend(changeNum, method, patchNum, endpoint, opt_payload,
         opt_errFn, opt_ctx, opt_contentType) {
-      return this._changeBaseURL(changeNum, opt_patchNum).then(url => {
+      return this._changeBaseURL(changeNum, patchNum).then(url => {
         return this.send(method, url + endpoint, opt_payload, opt_errFn,
             opt_ctx, opt_contentType);
       });
@@ -1432,13 +1720,13 @@
 
    /**
     * Alias for _changeBaseURL.then(fetchJSON).
-    *
+     * @TODO(beckysiegel) clean up comments
     * @param {string|number} changeNum
     * @param {string} endpoint
-    * @param {string|number=} opt_patchNum
-    * @param {function(?Response, string)=} opt_errFn
-    * @param {!function()=} opt_cancelCondition
-    * @param {!Object=} opt_params
+    * @param {?string|number=} opt_patchNum gets passed as null.
+    * @param {?function(?Response, string=)=} opt_errFn gets passed as null.
+    * @param {?function()=} opt_cancelCondition gets passed as null.
+    * @param {?Object=} opt_params gets passed as null.
     * @param {!Object=} opt_options
     * @return {!Promise<!Object>}
     */