Merge "Add a highlight.js loaded hook"
diff --git a/.mailmap b/.mailmap
index c863847..b5c119c 100644
--- a/.mailmap
+++ b/.mailmap
@@ -6,6 +6,7 @@
 Alex Ryazantsev <alex.ryazantsev@gmail.com>                                                 alex.ryazantsev <alex.ryazantsev@gmail.com>
 Andrew Bonventre <andybons@chromium.org>                                                    <andybons@google.com>
 Becky Siegel <beckysiegel@google.com>                                                       beckysiegel <beckysiegel@google.com>
+Ben Rohlfs <brohlfs@google.com>                                                             brohlfs <brohlfs@google.com>
 Brad Larson <bklarson@gmail.com>                                                            <brad.larson@garmin.com>
 Bruce Zu <bruce.zu.run10@gmail.com>                                                         <bruce.zu@sonyericsson.com>
 Bruce Zu <bruce.zu.run10@gmail.com>                                                         <bruce.zu@sonymobile.com>
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index cda02fb..3a80910 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -9,6 +9,10 @@
 link:config-gerrit.html#plugins.checkFrequency[a few minutes] until
 the server picks up new and updated plugins.
 
+Due to caching, you might need to flush your browser cache after
+installing a plugin. Users will usually see the result within
+several minutes.
+
 Plugins can also be installed via
 link:rest-api-plugins.html#install-plugin[REST] and
 link:cmd-plugin-install.html[SSH].
diff --git a/Documentation/config-themes.txt b/Documentation/config-themes.txt
index 38bfc46..a83c747 100644
--- a/Documentation/config-themes.txt
+++ b/Documentation/config-themes.txt
@@ -4,34 +4,28 @@
 the browser, allowing organizations to alter the look and
 feel of the application to fit with their general scheme.
 
-Configuration can either be sitewide or per-project. Projects without a
-specified theme inherit from their parents, or from the sitewide theme
-for `All-Projects`.
+== HTML Header/Footer and CSS
 
-Sitewide themes are stored in `'$site_path'/etc`, and per-project
-themes are stored in `'$site_path'/themes/{project-name}`. Files are
-only served from a single theme directory; if you want to modify or
-extend an inherited theme, you must copy it into the appropriate
-per-project directory.
-
-== HTML Header/Footer
+The HTML header, footer and CSS may be customized for login
+screens (LDAP, OAuth, OpenId) and the internally managed
+Gitweb servlet.
 
 At startup Gerrit reads the following files (if they exist) and
 uses them to customize the HTML page it sends to clients:
 
-* `<theme-dir>/GerritSiteHeader.html`
+* `etc/GerritSiteHeader.html`
 +
 HTML is inserted below the menu bar, but above any page content.
 This is a good location for an organizational logo, or links to
 other systems like bug tracking.
 
-* `<theme-dir>/GerritSiteFooter.html`
+* `etc/GerritSiteFooter.html`
 +
 HTML is inserted at the bottom of the page, below all other content,
 but just above the footer rule and the "Powered by Gerrit Code
 Review (v....)" message shown at the extreme bottom.
 
-* `<theme-dir>/GerritSite.css`
+* `etc/GerritSite.css`
 +
 The CSS rules are inlined into the top of the HTML page, inside
 of a `<style>` tag.  These rules can be used to support styling
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index ca47690..b87cbf4 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -107,7 +107,9 @@
 === Copyright
 Copy the folder `$(gerrit_source_code)/tools/intellij/copyright` (not just the
 contents) to `$(project_data_directory)/.idea`. If it already exists, replace
-it.
+it. Then go to *File -> Settings -> Editor -> Copyright -> Copyright Profiles*,
+and import `Gerrit_Copyright.xml` to IntelliJ in case it doesn't pick the
+copyright up automatically.
 
 === File header
 By default, IntelliJ adds a file header containing the name of the author and
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index cdc9c4e..1f98291 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -614,18 +614,6 @@
 are inherited by the child projects. A child project can overwrite an
 inherited download command, or remove it by assigning no value to it.
 
-[[theme]]
-== Theme
-
-Gerrit supports project-specific themes for customizing the appearance
-of the change screen and the diff screens. It is possible to define an
-HTML header and footer and to adapt Gerrit's CSS. Details about themes
-are explained in the link:config-themes.html[Themes] section.
-
-Project-specific themes can only be installed by Gerrit administrators
-since the theme files must be copied into the Gerrit installation
-folder.
-
 [[tool-integration]]
 == Integration with other tools
 
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 544039b..17c9a61 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -548,7 +548,8 @@
 ----
 Alternatively, click *Ready* from the Change screen.
 
-Only change owners, project owners and site administrators can mark changes as
+Change owners, project owners, site administrators and members of a group that
+was granted "Toggle Work In Progress state" permission can mark changes as
 `work-in-progress` and `ready`.
 
 [[wip-polygerrit]]
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index cb1041f8..cb80c74 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -149,11 +149,44 @@
 === change-list-header
 The `change-list-header` extension point adds a header to the change list view.
 
-
 === change-list-item-cell
 The `change-list-item-cell` extension point adds a cell to the change list item.
 
+In addition to default parameters, the following are available:
+
 * `change`
 +
 current change of the row, an instance of
 link:rest-api-changes.html#change-info[ChangeInfo]
+
+=== change-view-tab-header
+The `change-view-tab-header` extension point adds a primary tab to the change
+view. This must be used in conjunction with `change-view-tab-content`.
+
+In addition to default parameters, the following are available:
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
+
+=== change-view-tab-content
+The `change-view-tab-content` extension point adds primary tab content to
+the change view. This must be used in conjunction with `change-view-tab-header`.
+
+In addition to default parameters, the following are available:
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 9c24c79..9803c19 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3135,9 +3135,6 @@
 Map with the comment link configurations of the project. The name of
 the comment link configuration is mapped to a link:#commentlink-info[
 CommentlinkInfo] entity.
-|`theme`                                   |optional|
-The theme that is configured for the project as a link:#theme-info[
-ThemeInfo] entity.
 |`plugin_config`                           |optional|
 Plugin configuration as map which maps the plugin name to a map of
 parameter names to link:#config-parameter-info[ConfigParameterInfo]
@@ -3689,21 +3686,6 @@
 |=========================
 
 
-[[theme-info]]
-=== ThemeInfo
-The `ThemeInfo` entity describes a theme.
-
-[options="header",cols="1,^2,4"]
-|=============================
-|Field Name      ||Description
-|`css`           |optional|
-The path to the `GerritSite.css` file.
-|`header`        |optional|
-The path to the `GerritSiteHeader.html` file.
-|`footer`        |optional|
-The path to the `GerritSiteFooter.html` file.
-|=============================
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 5d7a78b..cafd5ca 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -216,7 +216,8 @@
 [[hashtag]]
 hashtag:'HASHTAG'::
 +
-Changes whose link:intro-user.html#hashtags[hashtag] matches 'HASHTAG' exactly.
+Changes whose link:intro-user.html#hashtags[hashtag] matches 'HASHTAG'.
+The match is case-insensitive.
 
 [[ref]]
 ref:'REF'::
diff --git a/WORKSPACE b/WORKSPACE
index 7aa4dce..a984c41 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -47,11 +47,11 @@
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "ee5fe78fe417c685ecb77a0a725dc9f6040ae5beb44a0ba4ddb55453aad23a8a",
-    url = "https://github.com/bazelbuild/rules_go/releases/download/0.16.0/rules_go-0.16.0.tar.gz",
+    sha256 = "6776d68ebb897625dead17ae510eac3d5f6342367327875210df44dbe2aeeb19",
+    url = "https://github.com/bazelbuild/rules_go/releases/download/0.17.1/rules_go-0.17.1.tar.gz",
 )
 
-load("@io_bazel_rules_go//go:def.bzl", "go_register_toolchains", "go_rules_dependencies")
+load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
 
 go_rules_dependencies()
 
@@ -59,8 +59,8 @@
 
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "c0a5739d12c6d05b6c1ad56f2200cb0b57c5a70e03ebd2f7b87ce88cabf09c7b",
-    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.14.0/bazel-gazelle-0.14.0.tar.gz"],
+    sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687",
+    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.17.0/bazel-gazelle-0.17.0.tar.gz"],
 )
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
@@ -859,30 +859,30 @@
     sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
 )
 
-TRUTH_VERS = "0.42"
+TRUTH_VERS = "0.43"
 
 maven_jar(
     name = "truth",
     artifact = "com.google.truth:truth:" + TRUTH_VERS,
-    sha1 = "b5768f644b114e6cf5c3962c2ebcb072f788dcbb",
+    sha1 = "0cb9105957368f68d5fd771045bfa27d4a534836",
 )
 
 maven_jar(
     name = "truth-java8-extension",
     artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
-    sha1 = "4d01dfa5b3780632a3d109e14e101f01d10cce2c",
+    sha1 = "0707dd0b4eb2101aa85c4bb7caf52d9ae32f0a43",
 )
 
 maven_jar(
     name = "truth-liteproto-extension",
     artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
-    sha1 = "c231e6735aa6c133c7e411ae1c1c90b124900a8b",
+    sha1 = "67d833098345fc744c6fb2c38c739557b2d73742",
 )
 
 maven_jar(
     name = "truth-proto-extension",
     artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
-    sha1 = "c41d22e8b4a61b4171e57c44a2959ebee0091a14",
+    sha1 = "b9641436e7b48b3642ac7aa7b8f3d1b9d04bc44b",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/acceptance/LogThreshold.java b/java/com/google/gerrit/acceptance/LogThreshold.java
index da1fcc5..36831f3 100644
--- a/java/com/google/gerrit/acceptance/LogThreshold.java
+++ b/java/com/google/gerrit/acceptance/LogThreshold.java
@@ -17,11 +17,13 @@
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
+import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
 @Target({TYPE, METHOD})
 @Retention(RUNTIME)
+@Inherited
 public @interface LogThreshold {
   String level() default "DEBUG";
 }
diff --git a/java/com/google/gerrit/common/data/Permission.java b/java/com/google/gerrit/common/data/Permission.java
index 2e9c2d6..3ba0ba7 100644
--- a/java/com/google/gerrit/common/data/Permission.java
+++ b/java/com/google/gerrit/common/data/Permission.java
@@ -46,6 +46,7 @@
   public static final String REMOVE_REVIEWER = "removeReviewer";
   public static final String SUBMIT = "submit";
   public static final String SUBMIT_AS = "submitAs";
+  public static final String TOGGLE_WORK_IN_PROGRESS_STATE = "toggleWipState";
   public static final String VIEW_PRIVATE_CHANGES = "viewPrivateChanges";
 
   private static final List<String> NAMES_LC;
@@ -78,6 +79,7 @@
     NAMES_LC.add(REMOVE_REVIEWER.toLowerCase());
     NAMES_LC.add(SUBMIT.toLowerCase());
     NAMES_LC.add(SUBMIT_AS.toLowerCase());
+    NAMES_LC.add(TOGGLE_WORK_IN_PROGRESS_STATE.toLowerCase());
     NAMES_LC.add(VIEW_PRIVATE_CHANGES.toLowerCase());
 
     LABEL_INDEX = NAMES_LC.indexOf(Permission.LABEL);
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index 08ba486..fb2a0fe 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -48,7 +48,6 @@
   public Map<String, ActionInfo> actions;
 
   public Map<String, CommentLinkInfo> commentlinks;
-  public ThemeInfo theme;
 
   public Map<String, List<String>> extensionPanelNames;
 
diff --git a/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java b/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java
deleted file mode 100644
index d5d520f..0000000
--- a/java/com/google/gerrit/extensions/api/projects/ThemeInfo.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2013 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.extensions.api.projects;
-
-public class ThemeInfo {
-  public static final ThemeInfo INHERIT = new ThemeInfo(null, null, null);
-
-  public final String css;
-  public final String header;
-  public final String footer;
-
-  public ThemeInfo(String css, String header, String footer) {
-    this.css = css;
-    this.header = header;
-    this.footer = footer;
-  }
-}
diff --git a/java/com/google/gerrit/extensions/config/CapabilityDefinition.java b/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
index aafb583..c76ec6c 100644
--- a/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
+++ b/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
@@ -18,7 +18,4 @@
 
 /** Specifies a capability declared by a plugin. */
 @ExtensionPoint
-public abstract class CapabilityDefinition {
-  /** @return description of the capability. */
-  public abstract String getDescription();
-}
+public abstract class CapabilityDefinition implements PluginPermissionDefinition {}
diff --git a/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java b/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java
new file mode 100644
index 0000000..11b1981
--- /dev/null
+++ b/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2019 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.extensions.config;
+
+/** Specifies a permission declared by a plugin. */
+public interface PluginPermissionDefinition {
+  /**
+   * Gets the description of a permission declared by a plugin.
+   *
+   * @return description of the permission.
+   */
+  String getDescription();
+}
diff --git a/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java b/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
new file mode 100644
index 0000000..d1d9f9e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2019 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.extensions.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Specifies a repository permission declared by a plugin. */
+@ExtensionPoint
+public abstract class PluginProjectPermissionDefinition implements PluginPermissionDefinition {}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 2b00d7c..d6dca48 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -392,8 +392,12 @@
             if (isRead(req)) {
               viewData = new ViewData(null, c.list());
             } else if (isPost(req)) {
+              // TODO: Here and on other collection methods: There is a bug that binds child views
+              // with pluginName="gerrit" instead of the real plugin name. This has never worked
+              // correctly and should be fixed where the binding gets created (DynamicMapProvider)
+              // and here.
               RestView<RestResource> restCollectionView =
-                  c.views().get(viewData.pluginName, "POST_ON_COLLECTION./");
+                  c.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
               if (restCollectionView != null) {
                 viewData = new ViewData(null, restCollectionView);
               } else {
@@ -401,7 +405,7 @@
               }
             } else if (isDelete(req)) {
               RestView<RestResource> restCollectionView =
-                  c.views().get(viewData.pluginName, "DELETE_ON_COLLECTION./");
+                  c.views().get(PluginName.GERRIT, "DELETE_ON_COLLECTION./");
               if (restCollectionView != null) {
                 viewData = new ViewData(null, restCollectionView);
               } else {
@@ -1264,6 +1268,8 @@
       return new ViewData(PluginName.GERRIT, core);
     }
 
+    // Check if we want to delegate to a child collection. Child collections are bound with
+    // GET.name so we have to check for this since we haven't found any other views.
     core = views.get(PluginName.GERRIT, "GET." + p.get(0));
     if (core != null) {
       return new ViewData(PluginName.GERRIT, core);
@@ -1277,6 +1283,17 @@
       }
     }
 
+    if (r.isEmpty()) {
+      // Check if we want to delegate to a child collection. Child collections are bound with
+      // GET.name so we have to check for this since we haven't found any other views.
+      for (String plugin : views.plugins()) {
+        RestView<RestResource> action = views.get(plugin, "GET." + p.get(0));
+        if (action != null) {
+          r.put(plugin, action);
+        }
+      }
+    }
+
     if (r.size() == 1) {
       Map.Entry<String, RestView<RestResource>> entry = Iterables.getOnlyElement(r.entrySet());
       return new ViewData(entry.getKey(), entry.getValue());
diff --git a/java/com/google/gerrit/server/ProjectUtil.java b/java/com/google/gerrit/server/ProjectUtil.java
index fc14768..1db4aa3 100644
--- a/java/com/google/gerrit/server/ProjectUtil.java
+++ b/java/com/google/gerrit/server/ProjectUtil.java
@@ -45,15 +45,26 @@
     }
   }
 
+  public static String sanitizeProjectName(String name) {
+    name = stripGitSuffix(name);
+    name = stripTrailingSlash(name);
+    return name;
+  }
+
   public static String stripGitSuffix(String name) {
     if (name.endsWith(".git")) {
       // Be nice and drop the trailing ".git" suffix, which we never keep
       // in our database, but clients might mistakenly provide anyway.
       //
       name = name.substring(0, name.length() - 4);
-      while (name.endsWith("/")) {
-        name = name.substring(0, name.length() - 1);
-      }
+      name = stripTrailingSlash(name);
+    }
+    return name;
+  }
+
+  private static String stripTrailingSlash(String name) {
+    while (name.endsWith("/")) {
+      name = name.substring(0, name.length() - 1);
     }
     return name;
   }
diff --git a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index 5a30113..9d29888 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -69,7 +69,7 @@
     try {
       return api.create(accounts.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(id)));
     } catch (Exception e) {
-      throw asRestApiException("Cannot parse change", e);
+      throw asRestApiException("Cannot parse account", e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 02870fb..e5fdd8f 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -18,20 +18,14 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
@@ -57,34 +51,6 @@
     WorkInProgressOp create(boolean workInProgress, Input in);
   }
 
-  public static void checkPermissions(
-      PermissionBackend permissionBackend, CurrentUser user, Change change)
-      throws PermissionBackendException, AuthException {
-    if (!user.isIdentifiedUser()) {
-      throw new AuthException("Authentication required");
-    }
-
-    if (change.getOwner().equals(user.asIdentifiedUser().getAccountId())) {
-      return;
-    }
-
-    try {
-      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-      return;
-    } catch (AuthException e) {
-      // Skip.
-    }
-
-    try {
-      permissionBackend
-          .user(user)
-          .project(change.getProject())
-          .check(ProjectPermission.WRITE_CONFIG);
-    } catch (AuthException exp) {
-      throw new AuthException("not allowed to toggle work in progress");
-    }
-  }
-
   private final ChangeMessagesUtil cmUtil;
   private final EmailReviewComments.Factory email;
   private final PatchSetUtil psUtil;
diff --git a/java/com/google/gerrit/server/config/ConfigResource.java b/java/com/google/gerrit/server/config/ConfigResource.java
index ec0e0c2..4275dc4 100644
--- a/java/com/google/gerrit/server/config/ConfigResource.java
+++ b/java/com/google/gerrit/server/config/ConfigResource.java
@@ -14,11 +14,22 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;
+import java.util.concurrent.TimeUnit;
 
 public class ConfigResource implements RestResource {
   public static final TypeLiteral<RestView<ConfigResource>> CONFIG_KIND =
       new TypeLiteral<RestView<ConfigResource>>() {};
+
+  /**
+   * Default cache control that gets set on the 'Cache-Control' header for responses on this
+   * resource that are cachable.
+   *
+   * <p>Not all resources are cacheable and in fact the vast majority might not be. Caching is a
+   * trade-off between the freshness of data and the number of QPS that the web UI sends.
+   */
+  public static CacheControl DEFAULT_CACHE_CONTROL = CacheControl.PRIVATE(300, TimeUnit.SECONDS);
 }
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 4158346..711efaf 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.config.ExternalIncludedIn;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.AgreementSignupListener;
 import com.google.gerrit.extensions.events.AssigneeChangedListener;
@@ -309,6 +310,7 @@
     DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
     DynamicSet.setOf(binder(), CacheRemovalListener.class);
     DynamicMap.mapOf(binder(), CapabilityDefinition.class);
+    DynamicMap.mapOf(binder(), PluginProjectPermissionDefinition.class);
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), AssigneeChangedListener.class);
     DynamicSet.setOf(binder(), ChangeAbandonedListener.class);
diff --git a/java/com/google/gerrit/server/config/SitePaths.java b/java/com/google/gerrit/server/config/SitePaths.java
index 11ec50c..47b6336 100644
--- a/java/com/google/gerrit/server/config/SitePaths.java
+++ b/java/com/google/gerrit/server/config/SitePaths.java
@@ -43,7 +43,6 @@
   public final Path mail_dir;
   public final Path hooks_dir;
   public final Path static_dir;
-  public final Path themes_dir;
   public final Path index_dir;
 
   public final Path gerrit_sh;
@@ -67,8 +66,7 @@
   public final Path site_css;
   public final Path site_header;
   public final Path site_footer;
-  // For PolyGerrit UI only.
-  public final Path site_theme;
+  public final Path site_theme; // For PolyGerrit UI only.
   public final Path site_gitweb;
 
   /** {@code true} if {@link #site_path} has not been initialized. */
@@ -90,7 +88,6 @@
     mail_dir = etc_dir.resolve("mail");
     hooks_dir = p.resolve("hooks");
     static_dir = p.resolve("static");
-    themes_dir = p.resolve("themes");
     index_dir = p.resolve("index");
 
     gerrit_sh = bin_dir.resolve("gerrit.sh");
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index f9fb251..f0077cd 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -351,6 +351,7 @@
     } else {
       List<Change.Id> autoclosed = resultChangeIds.get(ResultChangeIds.Key.AUTOCLOSED);
       metrics.changes.record(ResultChangeIds.Key.AUTOCLOSED, autoclosed.size());
+      totalChanges += autoclosed.size();
     }
 
     if (totalChanges > 0) {
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 250ca03..309bf51 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -176,6 +176,14 @@
     return refControl.canForceEditTopicName();
   }
 
+  /** Can this user toggle WorkInProgress state? */
+  private boolean canToggleWorkInProgressState() {
+    return isOwner()
+        || getProjectControl().isOwner()
+        || refControl.canPerform(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+        || getProjectControl().isAdmin();
+  }
+
   /** Can this user edit the description? */
   private boolean canEditDescription() {
     if (getChange().isNew()) {
@@ -299,6 +307,8 @@
             return canRestore();
           case SUBMIT:
             return refControl.canSubmit(isOwner());
+          case TOGGLE_WORK_IN_PROGRESS_STATE:
+            return canToggleWorkInProgressState();
 
           case REMOVE_REVIEWER:
           case SUBMIT_AS:
diff --git a/java/com/google/gerrit/server/permissions/ChangePermission.java b/java/com/google/gerrit/server/permissions/ChangePermission.java
index ca1c460..2fba4ef 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermission.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -55,7 +55,8 @@
    */
   REBASE,
   SUBMIT,
-  SUBMIT_AS("submit on behalf of other users");
+  SUBMIT_AS("submit on behalf of other users"),
+  TOGGLE_WORK_IN_PROGRESS_STATE;
 
   private final String description;
 
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
index ece29df..d56bc78 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -97,6 +97,9 @@
           .put(ChangePermission.REBASE, Permission.REBASE)
           .put(ChangePermission.SUBMIT, Permission.SUBMIT)
           .put(ChangePermission.SUBMIT_AS, Permission.SUBMIT_AS)
+          .put(
+              ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE,
+              Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
           .build();
 
   private static <T extends Enum<T>> void checkMapContainsAllEnumValues(
diff --git a/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.java b/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.java
new file mode 100644
index 0000000..a837ed7
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginPermissionDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.Extension;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.regex.Pattern;
+
+/** Utilities for plugin permissions. */
+@Singleton
+public final class PluginPermissionsUtil {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final String PLUGIN_NAME_PATTERN_STRING = "[a-zA-Z0-9-]+";
+
+  /**
+   * Name pattern for a plugin non-capability permission stored in the config file.
+   *
+   * <p>This pattern requires a plugin declared permission to have a name in the access section of
+   * {@code ProjectConfig} with a format like "plugin-{pluginName}-{permissionName}", which makes it
+   * easier to tell if a config name represents a plugin permission or not. Note "-" isn't clear
+   * enough for this purpose since some core permissions, e.g. "label-", also contain "-".
+   */
+  private static final Pattern PLUGIN_PERMISSION_NAME_IN_CONFIG_PATTERN =
+      Pattern.compile("^plugin-" + PLUGIN_NAME_PATTERN_STRING + "-[a-zA-Z]+$");
+
+  /** Name pattern for a Gerrit plugin. */
+  private static final Pattern PLUGIN_NAME_PATTERN =
+      Pattern.compile("^" + PLUGIN_NAME_PATTERN_STRING + "$");
+
+  private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
+  private final DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions;
+
+  @Inject
+  PluginPermissionsUtil(
+      DynamicMap<CapabilityDefinition> capabilityDefinitions,
+      DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions) {
+    this.capabilityDefinitions = capabilityDefinitions;
+    this.pluginProjectPermissionDefinitions = pluginProjectPermissionDefinitions;
+  }
+
+  /**
+   * Collects all the plugin declared capabilities.
+   *
+   * @return a map of plugin declared capabilities with "pluginName" as its keys and
+   *     "pluginName-{permissionName}" as its values.
+   */
+  public ImmutableMap<String, String> collectPluginCapabilities() {
+    return collectPermissions(capabilityDefinitions, "");
+  }
+
+  /**
+   * Collects all the plugin declared project permissions.
+   *
+   * @return a map of plugin declared project permissions with "{pluginName}" as its keys and
+   *     "plugin-{pluginName}-{permissionName}" as its values.
+   */
+  public ImmutableMap<String, String> collectPluginProjectPermissions() {
+    return collectPermissions(pluginProjectPermissionDefinitions, "plugin-");
+  }
+
+  private static <T extends PluginPermissionDefinition>
+      ImmutableMap<String, String> collectPermissions(DynamicMap<T> definitions, String prefix) {
+    ImmutableMap.Builder<String, String> permissionIdNames = ImmutableMap.builder();
+
+    for (Extension<T> extension : definitions) {
+      String pluginName = extension.getPluginName();
+      if (!PLUGIN_NAME_PATTERN.matcher(pluginName).matches()) {
+        logger.atWarning().log(
+            "Plugin name '%s' must match '%s' to use permissions; rename the plugin",
+            pluginName, PLUGIN_NAME_PATTERN.pattern());
+        continue;
+      }
+
+      String id = prefix + pluginName + "-" + extension.getExportName();
+      permissionIdNames.put(id, extension.get().getDescription());
+    }
+
+    return permissionIdNames.build();
+  }
+
+  /**
+   * Checks if a given name matches the plugin declared permission name pattern for configs.
+   *
+   * @param name a config name which may stand for a plugin permission.
+   * @return whether the name matches the plugin permission name pattern for configs.
+   */
+  public static boolean isPluginPermission(String name) {
+    return PLUGIN_PERMISSION_NAME_IN_CONFIG_PATTERN.matcher(name).matches();
+  }
+}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index c4ef32e..439e7c0 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -18,6 +18,7 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.common.data.Permission.isPermission;
 import static com.google.gerrit.reviewdb.client.Project.DEFAULT_SUBMIT_TYPE;
+import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isPluginPermission;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
@@ -747,7 +748,7 @@
         for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
           for (String n : Splitter.on(EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN).split(varName)) {
             n = convertLegacyPermission(n);
-            if (isPermission(n)) {
+            if (isCoreOrPluginPermission(n)) {
               as.getPermission(n, true).setExclusiveGroup(true);
             }
           }
@@ -755,7 +756,7 @@
 
         for (String varName : rc.getNames(ACCESS, refName)) {
           String convertedName = convertLegacyPermission(varName);
-          if (isPermission(convertedName)) {
+          if (isCoreOrPluginPermission(convertedName)) {
             Permission perm = as.getPermission(convertedName, true);
             loadPermissionRules(
                 rc,
@@ -784,6 +785,12 @@
     }
   }
 
+  private boolean isCoreOrPluginPermission(String permission) {
+    // Since plugins are loaded dynamically, here we can't load all plugin permissions and verify
+    // their existence.
+    return isPermission(permission) || isPluginPermission(permission);
+  }
+
   private boolean isValidRegex(String refPattern) {
     try {
       RefPattern.validateRegExp(refPattern);
@@ -1360,7 +1367,7 @@
       }
 
       for (String varName : rc.getNames(ACCESS, refName)) {
-        if (isPermission(convertLegacyPermission(varName))
+        if (isCoreOrPluginPermission(convertLegacyPermission(varName))
             && !have.contains(varName.toLowerCase())) {
           rc.unset(ACCESS, refName, varName);
         }
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 78505ab..73c222d 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -15,12 +15,10 @@
 package com.google.gerrit.server.project;
 
 import static com.google.gerrit.common.data.PermissionRule.Action.ALLOW;
-import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
@@ -32,7 +30,6 @@
 import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
-import com.google.gerrit.extensions.api.projects.ThemeInfo;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.index.project.ProjectData;
@@ -49,7 +46,6 @@
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.TransferConfig;
@@ -57,8 +53,6 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -84,7 +78,6 @@
 
   private final boolean isAllProjects;
   private final boolean isAllUsers;
-  private final SitePaths sitePaths;
   private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
   private final GitRepositoryManager gitMgr;
@@ -105,11 +98,6 @@
   /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
   private volatile List<SectionMatcher> localAccessSections;
 
-  // TODO(dborowitz): Delete when the GWT UI gets deleted; in the meantime, don't bother with any
-  // refactoring.
-  /** Theme information loaded from site_path/themes. */
-  private volatile ThemeInfo theme;
-
   /** If this is all projects, the capabilities used by the server. */
   private final CapabilityCollection capabilities;
 
@@ -118,7 +106,6 @@
 
   @Inject
   public ProjectState(
-      SitePaths sitePaths,
       ProjectCache projectCache,
       AllProjectsName allProjectsName,
       AllUsersName allUsersName,
@@ -128,7 +115,6 @@
       TransferConfig transferConfig,
       MetricMaker metricMaker,
       @Assisted ProjectConfig config) {
-    this.sitePaths = sitePaths;
     this.projectCache = projectCache;
     this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
     this.isAllUsers = config.getProject().getNameKey().equals(allUsersName);
@@ -548,24 +534,6 @@
     return ret;
   }
 
-  public ThemeInfo getTheme() {
-    ThemeInfo theme = this.theme;
-    if (theme == null) {
-      synchronized (this) {
-        theme = this.theme;
-        if (theme == null) {
-          theme = loadTheme();
-          this.theme = theme;
-        }
-      }
-    }
-    if (theme == ThemeInfo.INHERIT) {
-      ProjectState parent = Iterables.getFirst(parents(), null);
-      return parent != null ? parent.getTheme() : null;
-    }
-    return theme;
-  }
-
   public Set<GroupReference> getAllGroups() {
     return getGroups(getAllSections());
   }
@@ -597,26 +565,6 @@
     return all;
   }
 
-  private ThemeInfo loadTheme() {
-    String name = getConfig().getProject().getName();
-    Path dir = sitePaths.themes_dir.resolve(name);
-    if (!Files.exists(dir)) {
-      return ThemeInfo.INHERIT;
-    } else if (!Files.isDirectory(dir)) {
-      logger.atWarning().log("Bad theme for %s: not a directory", name);
-      return ThemeInfo.INHERIT;
-    }
-    try {
-      return new ThemeInfo(
-          readFile(dir.resolve(SitePaths.CSS_FILENAME)),
-          readFile(dir.resolve(SitePaths.HEADER_FILENAME)),
-          readFile(dir.resolve(SitePaths.FOOTER_FILENAME)));
-    } catch (IOException e) {
-      logger.atSevere().withCause(e).log("Error reading theme for %s", name);
-      return ThemeInfo.INHERIT;
-    }
-  }
-
   public ProjectData toProjectData() {
     ProjectData project = null;
     for (ProjectState state : treeInOrder()) {
@@ -625,10 +573,6 @@
     return project;
   }
 
-  private String readFile(Path p) throws IOException {
-    return Files.exists(p) ? new String(Files.readAllBytes(p), UTF_8) : null;
-  }
-
   private LabelTypes loadLabelTypes() {
     Map<String, LabelType> types = new LinkedHashMap<>();
     for (ProjectState s : treeInOrder()) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
index 1eb2770..7428e3a 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -17,9 +17,22 @@
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.Predicate;
 
 public abstract class ChangeIndexPredicate extends IndexPredicate<ChangeData>
     implements Matchable<ChangeData> {
+  /**
+   * Returns an index predicate that matches no changes in the index.
+   *
+   * <p>This predicate should be used in preference to a non-index predicate (such as {@code
+   * Predicate.not(Predicate.any())}), since it can be matched efficiently against the index.
+   *
+   * @return an index predicate matching no changes.
+   */
+  public static Predicate<ChangeData> none() {
+    return ChangeStatusPredicate.NONE;
+  }
+
   protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String value) {
     super(def, value);
   }
diff --git a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 8dc17d3..5a7efb8 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -41,7 +41,7 @@
  */
 public final class ChangeStatusPredicate extends ChangeIndexPredicate {
   private static final String INVALID_STATUS = "__invalid__";
-  private static final Predicate<ChangeData> NONE = new ChangeStatusPredicate(null);
+  static final Predicate<ChangeData> NONE = new ChangeStatusPredicate(null);
 
   private static final TreeMap<String, Predicate<ChangeData>> PREDICATES;
   private static final Predicate<ChangeData> CLOSED;
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index dfbc221..ada9f27 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.flogger.LazyArgs.lazy;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.index.query.Predicate;
@@ -21,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -41,20 +47,27 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class ConflictsPredicate {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   // UI code may depend on this string, so use caution when changing.
   protected static final String TOO_MANY_FILES = "too many files to find conflicts";
 
   private ConflictsPredicate() {}
 
   public static Predicate<ChangeData> create(Arguments args, String value, Change c)
-      throws QueryParseException, OrmException {
+      throws QueryParseException {
     ChangeData cd;
     List<String> files;
     try {
       cd = args.changeDataFactory.create(c);
       files = cd.currentFilePaths();
-    } catch (IOException e) {
-      throw new OrmException(e);
+    } catch (IOException | OrmException e) {
+      warnWithOccasionalStackTrace(
+          e,
+          "Error constructing conflicts predicates for change %s in %s",
+          c.getId(),
+          c.getProject());
+      return ChangeIndexPredicate.none();
     }
 
     if (3 + files.size() > args.indexConfig.maxTerms()) {
@@ -95,52 +108,66 @@
     }
 
     @Override
-    public boolean match(ChangeData object) throws OrmException {
-      Change otherChange = object.change();
-      if (otherChange == null || !otherChange.getDest().equals(dest)) {
-        return false;
-      }
-
-      SubmitTypeRecord str = object.submitTypeRecord();
-      if (!str.isOk()) {
-        return false;
-      }
-
-      ProjectState projectState;
+    public boolean match(ChangeData object) {
+      Change.Id id = object.getId();
+      Project.NameKey otherProject = null;
+      ObjectId other = null;
       try {
-        projectState = changeDataCache.getProjectState();
-      } catch (NoSuchProjectException e) {
-        return false;
-      }
+        Change otherChange = object.change();
+        if (otherChange == null || !otherChange.getDest().equals(dest)) {
+          return false;
+        }
+        otherProject = otherChange.getProject();
 
-      ObjectId other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
-      ConflictKey conflictsKey =
-          ConflictKey.create(
-              changeDataCache.getTestAgainst(),
-              other,
-              str.type,
-              projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
-      Boolean maybeConflicts = args.conflictsCache.getIfPresent(conflictsKey);
-      if (maybeConflicts != null) {
-        return maybeConflicts;
-      }
+        SubmitTypeRecord str = object.submitTypeRecord();
+        if (!str.isOk()) {
+          return false;
+        }
 
-      try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
-          CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-        boolean conflicts =
-            !args.submitDryRun.run(
-                null,
-                str.type,
-                repo,
-                rw,
-                otherChange.getDest(),
+        ProjectState projectState;
+        try {
+          projectState = changeDataCache.getProjectState();
+        } catch (NoSuchProjectException e) {
+          return false;
+        }
+
+        other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
+        ConflictKey conflictsKey =
+            ConflictKey.create(
                 changeDataCache.getTestAgainst(),
                 other,
-                getAlreadyAccepted(repo, rw));
-        args.conflictsCache.put(conflictsKey, conflicts);
-        return conflicts;
-      } catch (IntegrationException | NoSuchProjectException | IOException e) {
-        throw new OrmException(e);
+                str.type,
+                projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
+        Boolean maybeConflicts = args.conflictsCache.getIfPresent(conflictsKey);
+        if (maybeConflicts != null) {
+          return maybeConflicts;
+        }
+
+        try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
+            CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+          boolean conflicts =
+              !args.submitDryRun.run(
+                  null,
+                  str.type,
+                  repo,
+                  rw,
+                  otherChange.getDest(),
+                  changeDataCache.getTestAgainst(),
+                  other,
+                  getAlreadyAccepted(repo, rw));
+          args.conflictsCache.put(conflictsKey, conflicts);
+          return conflicts;
+        }
+      } catch (IntegrationException | NoSuchProjectException | OrmException | IOException e) {
+        ObjectId finalOther = other;
+        warnWithOccasionalStackTrace(
+            e,
+            "Merge failure checking conflicts of change %s in %s (%s): %s",
+            id,
+            firstNonNull(otherProject, "unknown project"),
+            lazy(() -> finalOther != null ? finalOther.name() : "unknown commit"),
+            e.getMessage());
+        return false;
       }
     }
 
@@ -202,4 +229,13 @@
       return alreadyAccepted;
     }
   }
+
+  private static void warnWithOccasionalStackTrace(Throwable cause, String format, Object... args) {
+    logger.atWarning().logVarargs(format, args);
+    logger
+        .atWarning()
+        .withCause(cause)
+        .atMostEvery(1, MINUTES)
+        .logVarargs("(Re-logging with stack trace) " + format, args);
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 8afaa45..ff27bfa 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -340,8 +340,10 @@
           return Response.withStatusCode(SC_BAD_REQUEST, output);
         }
 
-        WorkInProgressOp.checkPermissions(
-            permissionBackend, revision.getUser(), revision.getChange());
+        revision
+            .getChangeResource()
+            .permissions()
+            .check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
 
         if (input.ready) {
           output.ready = true;
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index 94914b0..aacf58b 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
-import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
 
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -25,48 +24,36 @@
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.change.WorkInProgressOp.Input;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
     implements UiAction<ChangeResource> {
   private final WorkInProgressOp.Factory opFactory;
-  private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Inject
-  SetReadyForReview(
-      RetryHelper retryHelper,
-      WorkInProgressOp.Factory opFactory,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user) {
+  SetReadyForReview(RetryHelper retryHelper, WorkInProgressOp.Factory opFactory) {
     super(retryHelper);
     this.opFactory = opFactory;
-    this.permissionBackend = permissionBackend;
-    this.user = user;
   }
 
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
-    WorkInProgressOp.checkPermissions(permissionBackend, user.get(), rsrc.getChange());
+    rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
 
     Change change = rsrc.getChange();
     if (!change.isNew()) {
@@ -94,15 +81,6 @@
         .setVisible(
             and(
                 rsrc.getChange().isNew() && rsrc.getChange().isWorkInProgress(),
-                or(
-                    rsrc.isUserOwner(),
-                    or(
-                        permissionBackend
-                            .currentUser()
-                            .testCond(GlobalPermission.ADMINISTRATE_SERVER),
-                        permissionBackend
-                            .currentUser()
-                            .project(rsrc.getProject())
-                            .testCond(ProjectPermission.WRITE_CONFIG)))));
+                rsrc.permissions().testCond(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE)));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index d087e47..852813e 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
-import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
 
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -25,48 +24,36 @@
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.change.WorkInProgressOp.Input;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, Input, Response<?>>
     implements UiAction<ChangeResource> {
   private final WorkInProgressOp.Factory opFactory;
-  private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Inject
-  SetWorkInProgress(
-      WorkInProgressOp.Factory opFactory,
-      RetryHelper retryHelper,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user) {
+  SetWorkInProgress(WorkInProgressOp.Factory opFactory, RetryHelper retryHelper) {
     super(retryHelper);
     this.opFactory = opFactory;
-    this.permissionBackend = permissionBackend;
-    this.user = user;
   }
 
   @Override
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
-    WorkInProgressOp.checkPermissions(permissionBackend, user.get(), rsrc.getChange());
+    rsrc.permissions().check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
 
     Change change = rsrc.getChange();
     if (!change.isNew()) {
@@ -94,15 +81,6 @@
         .setVisible(
             and(
                 rsrc.getChange().isNew() && !rsrc.getChange().isWorkInProgress(),
-                or(
-                    rsrc.isUserOwner(),
-                    or(
-                        permissionBackend
-                            .currentUser()
-                            .testCond(GlobalPermission.ADMINISTRATE_SERVER),
-                        permissionBackend
-                            .currentUser()
-                            .project(rsrc.getProject())
-                            .testCond(ProjectPermission.WRITE_CONFIG)))));
+                rsrc.permissions().testCond(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE)));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetVersion.java b/java/com/google/gerrit/server/restapi/config/GetVersion.java
index 8135719..ee206d6 100644
--- a/java/com/google/gerrit/server/restapi/config/GetVersion.java
+++ b/java/com/google/gerrit/server/restapi/config/GetVersion.java
@@ -15,19 +15,22 @@
 package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
 
 @Singleton
 public class GetVersion implements RestReadView<ConfigResource> {
   @Override
-  public String apply(ConfigResource resource) throws ResourceNotFoundException {
+  public Response<String> apply(ConfigResource resource) throws ResourceNotFoundException {
     String version = Version.getVersion();
     if (version == null) {
       throw new ResourceNotFoundException();
     }
-    return version;
+    return Response.ok(version).caching(CacheControl.PRIVATE(30, TimeUnit.SECONDS));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
index fa9bfde..9bb2e6d 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
@@ -14,38 +14,32 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+
 import com.google.common.collect.ImmutableMap;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.config.CapabilityDefinition;
-import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.CapabilityConstants;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PluginPermissionsUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.regex.Pattern;
 
 /** List capabilities visible to the calling user. */
 @Singleton
 public class ListCapabilities implements RestReadView<ConfigResource> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private static final Pattern PLUGIN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$");
-
   private final PermissionBackend permissionBackend;
-  private final DynamicMap<CapabilityDefinition> pluginCapabilities;
+  private final PluginPermissionsUtil pluginPermissionsUtil;
 
   @Inject
   public ListCapabilities(
-      PermissionBackend permissionBackend, DynamicMap<CapabilityDefinition> pluginCapabilities) {
+      PermissionBackend permissionBackend, PluginPermissionsUtil pluginPermissionsUtil) {
     this.permissionBackend = permissionBackend;
-    this.pluginCapabilities = pluginCapabilities;
+    this.pluginPermissionsUtil = pluginPermissionsUtil;
   }
 
   @Override
@@ -59,21 +53,16 @@
   }
 
   public Map<String, CapabilityInfo> collectPluginCapabilities() {
-    Map<String, CapabilityInfo> output = new HashMap<>();
-    for (String pluginName : pluginCapabilities.plugins()) {
-      if (!PLUGIN_NAME_PATTERN.matcher(pluginName).matches()) {
-        logger.atWarning().log(
-            "Plugin name '%s' must match '%s' to use capabilities; rename the plugin",
-            pluginName, PLUGIN_NAME_PATTERN.pattern());
-        continue;
-      }
-      for (Map.Entry<String, Provider<CapabilityDefinition>> entry :
-          pluginCapabilities.byPlugin(pluginName).entrySet()) {
-        String id = String.format("%s-%s", pluginName, entry.getKey());
-        output.put(id, new CapabilityInfo(id, entry.getValue().get().getDescription()));
-      }
-    }
-    return output;
+    return convertToPermissionInfos(pluginPermissionsUtil.collectPluginCapabilities());
+  }
+
+  private static ImmutableMap<String, CapabilityInfo> convertToPermissionInfos(
+      ImmutableMap<String, String> permissionIdNames) {
+    return permissionIdNames
+        .entrySet()
+        .stream()
+        .collect(
+            toImmutableMap(Map.Entry::getKey, e -> new CapabilityInfo(e.getKey(), e.getValue())));
   }
 
   private Map<String, CapabilityInfo> collectCoreCapabilities()
diff --git a/java/com/google/gerrit/server/restapi/config/ListTopMenus.java b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
index c296a7d..2feb580 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.extensions.webui.TopMenu.MenuEntry;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
@@ -33,9 +35,9 @@
   }
 
   @Override
-  public List<TopMenu.MenuEntry> apply(ConfigResource resource) {
+  public Response<List<MenuEntry>> apply(ConfigResource resource) {
     List<TopMenu.MenuEntry> entries = new ArrayList<>();
     extensions.runEach(extension -> entries.addAll(extension.getEntries()));
-    return entries;
+    return Response.ok(entries).caching(ConfigResource.DEFAULT_CACHE_CONTROL);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
index e179896..37bc265 100644
--- a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
@@ -101,7 +101,6 @@
     for (UiAction.Description d : uiActions.from(views, new ProjectResource(projectState, user))) {
       actions.put(d.getId(), new ActionInfo(d));
     }
-    this.theme = projectState.getTheme();
 
     this.extensionPanelNames = projectState.getConfig().getExtensionPanelSections();
   }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index c703fe2..6844cac 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -115,7 +115,7 @@
     }
 
     CreateProjectArgs args = new CreateProjectArgs();
-    args.setProjectName(ProjectUtil.stripGitSuffix(name));
+    args.setProjectName(ProjectUtil.sanitizeProjectName(name));
 
     String parentName =
         MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index e06b406..31c90e5 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -42,7 +43,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import org.eclipse.jgit.lib.Constants;
 
 @Singleton
 public class ProjectsCollection
@@ -136,9 +136,7 @@
   @Nullable
   private ProjectResource _parse(String id, boolean checkAccess)
       throws IOException, PermissionBackendException, ResourceConflictException {
-    if (id.endsWith(Constants.DOT_GIT_EXT)) {
-      id = id.substring(0, id.length() - Constants.DOT_GIT_EXT.length());
-    }
+    id = ProjectUtil.sanitizeProjectName(id);
 
     Project.NameKey nameKey = new Project.NameKey(id);
     ProjectState state = projectCache.checkedGet(nameKey);
diff --git a/java/com/google/gerrit/server/util/CommitMessageUtil.java b/java/com/google/gerrit/server/util/CommitMessageUtil.java
index fa55597..e984f46 100644
--- a/java/com/google/gerrit/server/util/CommitMessageUtil.java
+++ b/java/com/google/gerrit/server/util/CommitMessageUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.util;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 
 /** Utility functions to manipulate commit messages. */
@@ -23,18 +24,22 @@
   private CommitMessageUtil() {}
 
   /**
-   * Checks for null or empty commit messages and appends a newline character to the commit message.
+   * Checks for invalid (empty or containing \0) commit messages and appends a newline character to
+   * the commit message.
    *
    * @throws BadRequestException if the commit message is null or empty
    * @returns the trimmed message with a trailing newline character
    */
-  public static String checkAndSanitizeCommitMessage(String commitMessage)
+  public static String checkAndSanitizeCommitMessage(@Nullable String commitMessage)
       throws BadRequestException {
-    String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim();
-    if (wellFormedMessage.isEmpty()) {
+    String trimmed = Strings.nullToEmpty(commitMessage).trim();
+    if (trimmed.isEmpty()) {
       throw new BadRequestException("Commit message cannot be null or empty");
     }
-    wellFormedMessage = wellFormedMessage + "\n";
-    return wellFormedMessage;
+    if (trimmed.indexOf(0) >= 0) {
+      throw new BadRequestException("Commit message cannot have NUL character");
+    }
+    trimmed = trimmed + "\n";
+    return trimmed;
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index d90c4bd..c38f686 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -277,7 +277,7 @@
 
     requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(changeId).setWorkInProgress();
   }
 
@@ -323,7 +323,7 @@
 
     requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(changeId).setReadyForReview();
   }
 
@@ -500,6 +500,37 @@
   }
 
   @Test
+  public void toggleWorkInProgressStateByNonOwnerWithPermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String refactor = "Needs some refactoring";
+    String ptal = "PTAL";
+
+    grant(
+        project,
+        "refs/heads/master",
+        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
+        false,
+        REGISTERED_USERS);
+
+    requestScopeOperations.setApiUser(user.getId());
+    gApi.changes().id(changeId).setWorkInProgress(refactor);
+
+    ChangeInfo info = gApi.changes().id(changeId).get();
+
+    assertThat(info.workInProgress).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).contains(refactor);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
+
+    gApi.changes().id(changeId).setReadyForReview(ptal);
+
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.workInProgress).isNull();
+    assertThat(Iterables.getLast(info.messages).message).contains(ptal);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
+  }
+
+  @Test
   public void reviewAndStartReview() throws Exception {
     PushOneCommit.Result r = createWorkInProgressChange();
     r.assertOkStatus();
@@ -588,17 +619,33 @@
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(r.getChangeId()).current().review(in);
   }
 
   @Test
+  public void reviewWithWorkInProgressByNonOwnerWithPermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
+    grant(
+        project,
+        "refs/heads/master",
+        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
+        false,
+        REGISTERED_USERS);
+    requestScopeOperations.setApiUser(user.getId());
+    gApi.changes().id(r.getChangeId()).current().review(in);
+    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    assertThat(info.workInProgress).isTrue();
+  }
+
+  @Test
   public void reviewWithReadyByNonOwnerReturnsError() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setReady(true);
     requestScopeOperations.setApiUser(user.getId());
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to toggle work in progress");
+    exception.expectMessage("toggle work in progress state not permitted");
     gApi.changes().id(r.getChangeId()).current().review(in);
   }
 
@@ -3533,6 +3580,18 @@
   }
 
   @Test
+  public void changeCommitMessageNullNotAllowed() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(getCommitMessage(r.getChangeId()))
+        .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("NUL character");
+    gApi.changes()
+        .id(r.getChangeId())
+        .setMessage("test\0commit\n\nChange-Id: " + r.getChangeId() + "\n");
+  }
+
+  @Test
   public void changeCommitMessageWithWrongChangeIdFails() throws Exception {
     PushOneCommit.Result otherChange = createChange();
     PushOneCommit.Result r = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
new file mode 100644
index 0000000..27df565
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2019 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.rest.binding;
+
+import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import org.junit.Test;
+
+/**
+ * Tests for checking plugin-provided REST API bindings nested under a core collection.
+ *
+ * <p>These tests only verify that the plugin-provided REST endpoints are correctly bound, they do
+ * not test the functionality of the plugin REST endpoints.
+ */
+public class PluginProvidedChildRestApiBindingsIT extends AbstractDaemonTest {
+
+  /** Resource to bind a child collection. */
+  public static final TypeLiteral<RestView<TestPluginResource>> TEST_KIND =
+      new TypeLiteral<RestView<TestPluginResource>>() {};
+
+  private static final String PLUGIN_NAME = "my-plugin";
+
+  private static final ImmutableSet<RestCall> TEST_CALLS =
+      ImmutableSet.of(
+          // Calls that have the plugin name as part of the collection name
+          RestCall.get("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/"),
+          RestCall.get("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/1/detail"),
+          RestCall.post("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/"),
+          RestCall.post("/changes/%s/revisions/%s/" + PLUGIN_NAME + "~test-collection/1/update"),
+          // Same tests but without the plugin name as part of the collection name. This works as
+          // long as there is no core collection with the same name (which takes precedence) and no
+          // other plugin binds a collection with the same name. We highly encourage plugin authors
+          // to use the fully qualified collection name instead.
+          RestCall.get("/changes/%s/revisions/%s/test-collection/"),
+          RestCall.get("/changes/%s/revisions/%s/test-collection/1/detail"),
+          RestCall.post("/changes/%s/revisions/%s/test-collection/"),
+          RestCall.post("/changes/%s/revisions/%s/test-collection/1/update"));
+
+  /**
+   * Module for all sys bindings.
+   *
+   * <p>TODO: This should actually just move into MyPluginHttpModule. However, that doesn't work
+   * currently. This TODO is for fixing this bug.
+   */
+  static class MyPluginSysModule extends AbstractModule {
+    @Override
+    public void configure() {
+      install(
+          new RestApiModule() {
+            @Override
+            public void configure() {
+              DynamicMap.mapOf(binder(), TEST_KIND);
+              child(REVISION_KIND, "test-collection").to(TestChildCollection.class);
+
+              postOnCollection(TEST_KIND).to(TestPostOnCollection.class);
+              post(TEST_KIND, "update").to(TestPost.class);
+              get(TEST_KIND, "detail").to(TestGet.class);
+            }
+          });
+    }
+  }
+
+  static class TestPluginResource implements RestResource {}
+
+  @Singleton
+  static class TestChildCollection
+      implements ChildCollection<RevisionResource, TestPluginResource> {
+    private final DynamicMap<RestView<TestPluginResource>> views;
+
+    @Inject
+    TestChildCollection(DynamicMap<RestView<TestPluginResource>> views) {
+      this.views = views;
+    }
+
+    @Override
+    public RestView<RevisionResource> list() throws RestApiException {
+      return (RestReadView<RevisionResource>) resource -> ImmutableList.of("one", "two");
+    }
+
+    @Override
+    public TestPluginResource parse(RevisionResource parent, IdString id) throws Exception {
+      return new TestPluginResource();
+    }
+
+    @Override
+    public DynamicMap<RestView<TestPluginResource>> views() {
+      return views;
+    }
+  }
+
+  @Singleton
+  static class TestPostOnCollection
+      implements RestCollectionModifyView<RevisionResource, TestPluginResource, String> {
+    @Override
+    public Object apply(RevisionResource parentResource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestPost implements RestModifyView<TestPluginResource, String> {
+    @Override
+    public String apply(TestPluginResource resource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestGet implements RestReadView<TestPluginResource> {
+    @Override
+    public String apply(TestPluginResource resource) throws Exception {
+      return "test";
+    }
+  }
+
+  @Test
+  public void testEndpoints() throws Exception {
+    PatchSet.Id patchSetId = createChange().getPatchSetId();
+    try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, MyPluginSysModule.class, null, null)) {
+      RestApiCallHelper.execute(
+          adminRestSession,
+          TEST_CALLS.asList(),
+          String.valueOf(patchSetId.changeId.id),
+          String.valueOf(patchSetId.patchSetId));
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRestApiBindingsIT.java
deleted file mode 100644
index 82c065a..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRestApiBindingsIT.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2019 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.rest.binding;
-
-import static javax.servlet.http.HttpServletResponse.SC_OK;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
-import com.google.gerrit.acceptance.rest.util.RestCall;
-import com.google.inject.Singleton;
-import com.google.inject.servlet.ServletModule;
-import java.io.IOException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.junit.Test;
-
-/**
- * Tests for checking plugin-provided REST API bindings.
- *
- * <p>These tests only verify that the plugin-provided REST endpoints are correctly bound, they do
- * not test the functionality of the plugin REST endpoints.
- */
-public class PluginProvidedRestApiBindingsIT extends AbstractDaemonTest {
-
-  /**
-   * Plugin REST endpoints bound by {@link MyPluginModule} with Guice serlvet definitions.
-   *
-   * <p>Each URL contains a placeholder for the plugin identifier.
-   *
-   * <p>Currently does not include any resource or documentation URLs, since those would require
-   * installing a plugin from a jar, which is trickier than just defining a module in this file.
-   */
-  private static final ImmutableList<RestCall> SERVER_TOP_LEVEL_PLUGIN_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/plugins/%s/hello"));
-
-  static class MyPluginModule extends ServletModule {
-    @Override
-    public void configureServlets() {
-      serve("/hello").with(HelloServlet.class);
-    }
-  }
-
-  @Singleton
-  static class HelloServlet extends HttpServlet {
-    private static final long serialVersionUID = 1L;
-
-    @Override
-    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
-      res.setStatus(SC_OK);
-      res.getWriter().println("Hello world");
-    }
-  }
-
-  @Test
-  public void serverPluginTopLevelEndpoints() throws Exception {
-    String pluginName = "my-plugin";
-    try (AutoCloseable ignored = installPlugin(pluginName, null, MyPluginModule.class, null)) {
-      RestApiCallHelper.execute(adminRestSession, SERVER_TOP_LEVEL_PLUGIN_ENDPOINTS, pluginName);
-    }
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
new file mode 100644
index 0000000..8a8ea9b
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
@@ -0,0 +1,199 @@
+// Copyright (C) 2019 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.rest.binding;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.servlet.ServletModule;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+
+/**
+ * Tests for checking plugin-provided REST API bindings directly under {@code /}.
+ *
+ * <p>These tests only verify that the plugin-provided REST endpoints are correctly bound, they do
+ * not test the functionality of the plugin REST endpoints.
+ */
+public class PluginProvidedRootRestApiBindingsIT extends AbstractDaemonTest {
+
+  /** Resource to bind a child collection. */
+  public static final TypeLiteral<RestView<TestPluginResource>> TEST_KIND =
+      new TypeLiteral<RestView<TestPluginResource>>() {};
+
+  private static final String PLUGIN_NAME = "my-plugin";
+
+  private static final ImmutableSet<RestCall> TEST_CALLS =
+      ImmutableSet.of(
+          RestCall.get("/plugins/" + PLUGIN_NAME + "/test-collection/"),
+          RestCall.get("/plugins/" + PLUGIN_NAME + "/test-collection/1/detail"),
+          RestCall.post("/plugins/" + PLUGIN_NAME + "/test-collection/"),
+          RestCall.post("/plugins/" + PLUGIN_NAME + "/test-collection/1/update"));
+
+  /** Module for all HTTP bindings. */
+  static class MyPluginHttpModule extends ServletModule {
+    @Override
+    public void configureServlets() {
+      bind(TestRootCollection.class);
+
+      install(
+          new RestApiModule() {
+            @Override
+            public void configure() {
+              DynamicMap.mapOf(binder(), TEST_KIND);
+
+              postOnCollection(TEST_KIND).to(TestPostOnCollection.class);
+              post(TEST_KIND, "update").to(TestPost.class);
+              get(TEST_KIND, "detail").to(TestGet.class);
+            }
+          });
+
+      serveRegex("/(?:a/)?test-collection/(.*)$").with(TestRestApiServlet.class);
+    }
+  }
+
+  @Singleton
+  static class TestRestApiServlet extends RestApiServlet {
+    private static final long serialVersionUID = 1L;
+
+    @Inject
+    TestRestApiServlet(RestApiServlet.Globals globals, Provider<TestRootCollection> collection) {
+      super(globals, collection);
+    }
+
+    @Override
+    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
+        throws ServletException, IOException {
+      // This is...unfortunate. HttpPluginServlet (and/or ContextMapper) doesn't properly set the
+      // servlet path on the wrapped request. Based on what RestApiServlet produces for non-plugin
+      // requests, it should be:
+      //   contextPath = "/plugins/checks"
+      //   servletPath = "/checkers/"
+      //   pathInfo = checkerUuid
+      // Instead it does:
+      //   contextPath = "/plugins/checks"
+      //   servletPath = ""
+      //   pathInfo = "/checkers/" + checkerUuid
+      // This results in RestApiServlet splitting the pathInfo into ["", "checkers", checkerUuid],
+      // and it passes the "" to CheckersCollection#parse, which understandably, but unfortunately,
+      // fails.
+      //
+      // This frankly seems like a bug that should be fixed, but it would quite likely break
+      // existing plugins in confusing ways. So, we work around it by introducing our own request
+      // wrapper with the correct paths.
+      HttpServletRequest req = (HttpServletRequest) servletRequest;
+      String pathInfo = req.getPathInfo();
+      String correctServletPath = "/test-collection/";
+      String fixedPathInfo = pathInfo.substring(correctServletPath.length());
+      HttpServletRequestWrapper wrapped =
+          new HttpServletRequestWrapper(req) {
+            @Override
+            public String getServletPath() {
+              return correctServletPath;
+            }
+
+            @Override
+            public String getPathInfo() {
+              return fixedPathInfo;
+            }
+          };
+
+      super.service(wrapped, (HttpServletResponse) servletResponse);
+    }
+  }
+
+  static class TestPluginResource implements RestResource {}
+
+  @Singleton
+  static class TestRootCollection implements ChildCollection<TopLevelResource, TestPluginResource> {
+    private final DynamicMap<RestView<TestPluginResource>> views;
+
+    @Inject
+    TestRootCollection(DynamicMap<RestView<TestPluginResource>> views) {
+      this.views = views;
+    }
+
+    @Override
+    public RestView<TopLevelResource> list() throws RestApiException {
+      return (RestReadView<TopLevelResource>) resource -> ImmutableList.of("one", "two");
+    }
+
+    @Override
+    public TestPluginResource parse(TopLevelResource parent, IdString id) throws Exception {
+      return new TestPluginResource();
+    }
+
+    @Override
+    public DynamicMap<RestView<TestPluginResource>> views() {
+      return views;
+    }
+  }
+
+  @Singleton
+  static class TestPostOnCollection
+      implements RestCollectionModifyView<TopLevelResource, TestPluginResource, String> {
+    @Override
+    public Object apply(TopLevelResource parentResource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestPost implements RestModifyView<TestPluginResource, String> {
+    @Override
+    public String apply(TestPluginResource resource, String input) throws Exception {
+      return "test";
+    }
+  }
+
+  @Singleton
+  static class TestGet implements RestReadView<TestPluginResource> {
+    @Override
+    public String apply(TestPluginResource resource) throws Exception {
+      return "test";
+    }
+  }
+
+  @Test
+  public void testEndpoints() throws Exception {
+    try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, null, MyPluginHttpModule.class, null)) {
+      RestApiCallHelper.execute(adminRestSession, TEST_CALLS.asList());
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index cd29bf8..7d36e71 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -182,6 +182,28 @@
   }
 
   @Test
+  public void createProjectThatEndsWithSlash() throws Exception {
+    String newProjectName = name("newProject");
+    ProjectInfo p = gApi.projects().create(newProjectName + "/").get();
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
+  @Test
+  public void createProjectThatContainsSlash() throws Exception {
+    String newProjectName = name("newProject/newProject");
+    ProjectInfo p = gApi.projects().create(newProjectName).get();
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
+  @Test
   public void createProjectWithProperties() throws Exception {
     String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
index 1e6afa8..e7663f7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
@@ -26,15 +26,21 @@
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.permissions.PluginPermissionsUtil;
 import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
 import com.google.inject.Module;
+import java.util.Set;
 import org.junit.Test;
 
-public class PluginAccessIT extends AbstractDaemonTest {
+public final class PluginAccessIT extends AbstractDaemonTest {
+  private static final String TEST_PLUGIN_NAME = "gerrit";
+  private static final String TEST_PLUGIN_CAPABILITY = "aPluginCapability";
+  private static final String TEST_PLUGIN_PROJECT_PERMISSION = "aPluginProjectPermission";
 
-  private static final String CORE_PLUGIN_PREFIX = "gerrit-";
-  private static final String PLUGIN_CAPABILITY = "printHello";
+  @Inject PluginPermissionsUtil pluginPermissionsUtil;
 
   @Override
   public Module createModule() {
@@ -42,12 +48,21 @@
       @Override
       protected void configure() {
         bind(CapabilityDefinition.class)
-            .annotatedWith(Exports.named(PLUGIN_CAPABILITY))
+            .annotatedWith(Exports.named(TEST_PLUGIN_CAPABILITY))
             .toInstance(
                 new CapabilityDefinition() {
                   @Override
                   public String getDescription() {
-                    return "Print Hello";
+                    return "A Plugin Capability";
+                  }
+                });
+        bind(PluginProjectPermissionDefinition.class)
+            .annotatedWith(Exports.named(TEST_PLUGIN_PROJECT_PERMISSION))
+            .toInstance(
+                new PluginProjectPermissionDefinition() {
+                  @Override
+                  public String getDescription() {
+                    return "A Plugin Project Permission";
                   }
                 });
       }
@@ -55,24 +70,47 @@
   }
 
   @Test
-  public void addPluginCapability() throws Exception {
-    ProjectAccessInput accessInput = new ProjectAccessInput();
-    AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
-    PermissionInfo email = new PermissionInfo(null, null);
-    PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+  public void setAccessAddPluginCapabilitySucceed() throws Exception {
+    String pluginCapability = TEST_PLUGIN_NAME + "-" + TEST_PLUGIN_CAPABILITY;
+    ProjectAccessInput accessInput =
+        createAccessInput(AccessSection.GLOBAL_CAPABILITIES, pluginCapability);
 
-    email.rules = ImmutableMap.of(SystemGroupBackend.REGISTERED_USERS.get(), pri);
-    accessSectionInfo.permissions = ImmutableMap.of(CORE_PLUGIN_PREFIX + PLUGIN_CAPABILITY, email);
-    accessInput.add = ImmutableMap.of(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
-    ProjectAccessInfo updatedAccessSectionInfo =
+    ProjectAccessInfo projectAccessInfo =
         gApi.projects().name(allProjects.get()).access(accessInput);
-    assertThat(
-            updatedAccessSectionInfo
-                .local
-                .get(AccessSection.GLOBAL_CAPABILITIES)
-                .permissions
-                .keySet())
-        .containsAllIn(accessSectionInfo.permissions.keySet());
+
+    Set<String> capabilities =
+        projectAccessInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions.keySet();
+    assertThat(capabilities).contains(pluginCapability);
+    // Verifies the plugin defined capability could be listed.
+    assertThat(pluginPermissionsUtil.collectPluginCapabilities()).containsKey(pluginCapability);
+  }
+
+  @Test
+  public void setAccessAddPluginProjectPermissionSucceed() throws Exception {
+    String pluginProjectPermission =
+        "plugin-" + TEST_PLUGIN_NAME + "-" + TEST_PLUGIN_PROJECT_PERMISSION;
+    String accessSection = "refs/heads/plugin-permission";
+    ProjectAccessInput accessInput = createAccessInput(accessSection, pluginProjectPermission);
+
+    ProjectAccessInfo projectAccessInfo =
+        gApi.projects().name(allProjects.get()).access(accessInput);
+
+    Set<String> permissions = projectAccessInfo.local.get(accessSection).permissions.keySet();
+    assertThat(permissions).contains(pluginProjectPermission);
+    // Verifies the plugin defined capability could be listed.
+    assertThat(pluginPermissionsUtil.collectPluginProjectPermissions())
+        .containsKey(pluginProjectPermission);
+  }
+
+  private static ProjectAccessInput createAccessInput(String accessSection, String permissionName) {
+    ProjectAccessInput accessInput = new ProjectAccessInput();
+    PermissionRuleInfo ruleInfo = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+    PermissionInfo email = new PermissionInfo(null, null);
+    email.rules = ImmutableMap.of(SystemGroupBackend.REGISTERED_USERS.get(), ruleInfo);
+    AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
+    accessSectionInfo.permissions = ImmutableMap.of(permissionName, email);
+    accessInput.add = ImmutableMap.of(accessSection, accessSectionInfo);
+
+    return accessInput;
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
index e583179..0ce05b0 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
@@ -49,4 +49,38 @@
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNull();
   }
+
+  @Test
+  public void withDotGit() throws Exception {
+    String newGroupName = "newGroup";
+    adminRestSession.put("/groups/" + newGroupName);
+    String newProjectName = name("newProject");
+    adminSshSession.exec(
+        "gerrit create-project --branch master --owner "
+            + newGroupName
+            + " "
+            + newProjectName
+            + ".git");
+    adminSshSession.assertSuccess();
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertThat(projectState.getName()).isEqualTo(newProjectName);
+  }
+
+  @Test
+  public void withTrailingSlash() throws Exception {
+    String newGroupName = "newGroup";
+    adminRestSession.put("/groups/" + newGroupName);
+    String newProjectName = name("newProject");
+    adminSshSession.exec(
+        "gerrit create-project --branch master --owner "
+            + newGroupName
+            + " "
+            + newProjectName
+            + "/");
+    adminSshSession.assertSuccess();
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertThat(projectState.getName()).isEqualTo(newProjectName);
+  }
 }
diff --git a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
index 081a2f7..30fabdc 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -16,9 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
@@ -44,8 +46,18 @@
           @Override
           protected void configure() {
             DynamicMap.mapOf(binder(), CapabilityDefinition.class);
+            DynamicMap.mapOf(binder(), PluginProjectPermissionDefinition.class);
             bind(CapabilityDefinition.class)
-                .annotatedWith(Exports.named("printHello"))
+                .annotatedWith(Exports.named("foo"))
+                .toInstance(
+                    new CapabilityDefinition() {
+                      @Override
+                      public String getDescription() {
+                        return "Print Hello";
+                      }
+                    });
+            bind(CapabilityDefinition.class)
+                .annotatedWith(Exports.named("bar"))
                 .toInstance(
                     new CapabilityDefinition() {
                       @Override
@@ -69,10 +81,11 @@
       assertThat(m.get(id).name).isNotNull();
     }
 
-    String pluginCapability = "gerrit-printHello";
-    assertThat(m).containsKey(pluginCapability);
-    assertThat(m.get(pluginCapability).id).isEqualTo(pluginCapability);
-    assertThat(m.get(pluginCapability).name).isEqualTo("Print Hello");
+    for (String pluginCapability : ImmutableSet.of("gerrit-foo", "gerrit-bar")) {
+      assertThat(m).containsKey(pluginCapability);
+      assertThat(m.get(pluginCapability).id).isEqualTo(pluginCapability);
+      assertThat(m.get(pluginCapability).name).isEqualTo("Print Hello");
+    }
   }
 
   @Singleton
diff --git a/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java b/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java
new file mode 100644
index 0000000..85d85fb
--- /dev/null
+++ b/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isPluginPermission;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+/** Small tests for {@link PluginPermissionsUtil}. */
+public final class PluginPermissionsUtilTest {
+
+  @Test
+  public void isPluginPermissionReturnsTrueForValidName() {
+    // "-" is allowed for a plugin name. Here "foo-a" should be the name of the plugin.
+    ImmutableList<String> validPluginPermissions =
+        ImmutableList.of("plugin-foo-a", "plugin-foo-a-b");
+
+    for (String permission : validPluginPermissions) {
+      assertThat(isPluginPermission(permission))
+          .named("valid plugin permission: %s", permission)
+          .isTrue();
+    }
+  }
+
+  @Test
+  public void isPluginPermissionReturnsFalseForInvalidName() {
+    ImmutableList<String> inValidPluginPermissions =
+        ImmutableList.of(
+            "create",
+            "label-Code-Review",
+            "plugin-foo",
+            "plugin-foo",
+            "plugin-foo-a-",
+            "plugin-foo-a1");
+
+    for (String permission : inValidPluginPermissions) {
+      assertThat(isPluginPermission(permission))
+          .named("invalid plugin permission: %s", permission)
+          .isFalse();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 872cd91..3a9037c 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -54,7 +54,6 @@
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
 import com.google.gerrit.server.project.ProjectCache;
@@ -931,7 +930,6 @@
   }
 
   private InMemoryRepository add(ProjectConfig pc) {
-    SitePaths sitePaths = null;
     List<CommentLinkInfo> commentLinks = null;
 
     InMemoryRepository repo;
@@ -946,7 +944,6 @@
     all.put(
         pc.getName(),
         new ProjectState(
-            sitePaths,
             projectCache,
             allProjectsName,
             allUsersName,
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 5eecd0f..100c25e 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
@@ -69,6 +70,7 @@
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -3134,6 +3136,26 @@
     assertQuery("assignee:self", change);
   }
 
+  @Test
+  public void none() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(repo, newChange(repo));
+
+    assertQuery(ChangeIndexPredicate.none());
+
+    for (Predicate<ChangeData> matchingOneChange :
+        ImmutableList.of(
+            // One index query, one post-filtering query.
+            queryBuilder.parse(change.getId().toString()),
+            queryBuilder.parse("ownerin:Administrators"))) {
+      assertQuery(matchingOneChange, change);
+      assertQuery(Predicate.or(ChangeIndexPredicate.none(), matchingOneChange), change);
+      assertQuery(Predicate.and(ChangeIndexPredicate.none(), matchingOneChange));
+      assertQuery(
+          Predicate.and(Predicate.not(ChangeIndexPredicate.none()), matchingOneChange), change);
+    }
+  }
+
   protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
     return newChange(repo, null, null, null, null, false);
   }
@@ -3307,21 +3329,35 @@
     List<ChangeInfo> result = query.get();
     Iterable<Change.Id> ids = ids(result);
     assertThat(ids)
-        .named(format(query, ids, changes))
+        .named(format(query.getQuery(), ids, changes))
         .containsExactlyElementsIn(Arrays.asList(changes))
         .inOrder();
     return result;
   }
 
-  private String format(
-      QueryRequest query, Iterable<Change.Id> actualIds, Change.Id... expectedChanges)
+  protected void assertQuery(Predicate<ChangeData> predicate, Change... changes) throws Exception {
+    ImmutableList<Change.Id> actualIds =
+        queryProvider
+            .get()
+            .query(predicate)
+            .stream()
+            .map(ChangeData::getId)
+            .collect(toImmutableList());
+    Change.Id[] expectedIds = Arrays.stream(changes).map(Change::getId).toArray(Change.Id[]::new);
+    assertThat(actualIds)
+        .named(format(predicate.toString(), actualIds, expectedIds))
+        .containsExactlyElementsIn(expectedIds)
+        .inOrder();
+  }
+
+  private String format(String query, Iterable<Change.Id> actualIds, Change.Id... expectedChanges)
       throws RestApiException {
-    StringBuilder b = new StringBuilder();
-    b.append("query '").append(query.getQuery()).append("' with expected changes ");
-    b.append(format(Arrays.asList(expectedChanges)));
-    b.append(" and result ");
-    b.append(format(actualIds));
-    return b.toString();
+    return "query '"
+        + query
+        + "' with expected changes "
+        + format(Arrays.asList(expectedChanges))
+        + " and result "
+        + format(actualIds);
   }
 
   private String format(Iterable<Change.Id> changeIds) throws RestApiException {
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index c4cf42b..0ffcc1c 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit c4cf42b96a049a0fb854bcbcb85b56a82d91a009
+Subproject commit 0ffcc1c5865bbfecb811d8631e010251fae04317
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
index fb0c685..e949a87 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
@@ -120,6 +120,10 @@
             id: 'submitAs',
             name: 'Submit (On Behalf Of)',
           },
+          toggleWipState: {
+            id: 'toggleWipState',
+            name: 'Toggle Work In Progress State',
+          },
           viewDrafts: {
             id: 'viewDrafts',
             name: 'View Drafts',
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
index 987b63d..da1c871 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -32,9 +32,6 @@
   <template>
     <style include="shared-styles"></style>
     <style include="gr-form-styles">
-      :host {
-        display: inline-block;
-      }
       input:not([type="checkbox"]),
       gr-autocomplete,
       iron-autogrow-textarea {
@@ -43,13 +40,6 @@
       .value {
         width: 32em;
       }
-      section {
-        align-items: center;
-        display: flex;
-      }
-      #description {
-        align-items: initial;
-      }
       gr-autocomplete {
         --gr-autocomplete: {
           padding: 0 .15em;
@@ -58,68 +48,71 @@
       .hide {
         display: none;
       }
+      @media only screen and (max-width: 40em) {
+        .value {
+          width: 29em;
+        }
+      }
     </style>
     <div class="gr-form-styles">
-      <div id="form">
-        <section class$="[[_computeBranchClass(baseChange)]]">
-          <span class="title">Select branch for new change</span>
-          <span class="value">
-            <gr-autocomplete
-                id="branchInput"
-                text="{{branch}}"
-                query="[[_query]]"
-                placeholder="Destination branch">
-            </gr-autocomplete>
-          </span>
-        </section>
-        <section class$="[[_computeBranchClass(baseChange)]]">
-          <span class="title">Provide base commit sha1 for change</span>
-          <span class="value">
-            <input
-                is="iron-input"
-                id="baseCommitInput"
-                maxlength="40"
-                placeholder="(optional)"
-                bind-value="{{baseCommit}}">
-          </span>
-        </section>
-        <section>
-          <span class="title">Enter topic for new change</span>
-          <span class="value">
-            <input
-                is="iron-input"
-                id="tagNameInput"
-                maxlength="1024"
-                placeholder="(optional)"
-                bind-value="{{topic}}">
-          </span>
-        </section>
-        <section id="description">
-          <span class="title">Description</span>
-          <span class="value">
-            <iron-autogrow-textarea
-                id="messageInput"
-                class="message"
-                autocomplete="on"
-                rows="4"
-                max-rows="15"
-                bind-value="{{subject}}"
-                placeholder="Insert the description of the change.">
-            </iron-autogrow-textarea>
-          </span>
-        </section>
-        <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
-          <label
-              class="title"
-              for="privateChangeCheckBox">Private change</label>
-          <span class="value">
-            <input
-                type="checkbox"
-                id="privateChangeCheckBox"
-                checked$="[[_formatBooleanString(privateByDefault)]]">
-          </span>
-        </section>
-      </div>
+      <section class$="[[_computeBranchClass(baseChange)]]">
+        <span class="title">Select branch for new change</span>
+        <span class="value">
+          <gr-autocomplete
+              id="branchInput"
+              text="{{branch}}"
+              query="[[_query]]"
+              placeholder="Destination branch">
+          </gr-autocomplete>
+        </span>
+      </section>
+      <section class$="[[_computeBranchClass(baseChange)]]">
+        <span class="title">Provide base commit sha1 for change</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              id="baseCommitInput"
+              maxlength="40"
+              placeholder="(optional)"
+              bind-value="{{baseCommit}}">
+        </span>
+      </section>
+      <section>
+        <span class="title">Enter topic for new change</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              id="tagNameInput"
+              maxlength="1024"
+              placeholder="(optional)"
+              bind-value="{{topic}}">
+        </span>
+      </section>
+      <section id="description">
+        <span class="title">Description</span>
+        <span class="value">
+          <iron-autogrow-textarea
+              id="messageInput"
+              class="message"
+              autocomplete="on"
+              rows="4"
+              max-rows="15"
+              bind-value="{{subject}}"
+              placeholder="Insert the description of the change.">
+          </iron-autogrow-textarea>
+        </span>
+      </section>
+      <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
+        <label
+            class="title"
+            for="privateChangeCheckBox">Private change</label>
+        <span class="value">
+          <input
+              type="checkbox"
+              id="privateChangeCheckBox"
+              checked$="[[_formatBooleanString(privateByDefault)]]">
+        </span>
+      </section>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 6b6e90b..278875e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -32,6 +32,7 @@
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html">
 <link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
+<link rel="import" href="../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html">
 <link rel="import" href="../gr-confirm-move-dialog/gr-confirm-move-dialog.html">
 <link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
 <link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html">
@@ -187,6 +188,15 @@
           on-cancel="_handleConfirmDialogCancel"
           project="[[change.project]]"
           hidden></gr-confirm-cherrypick-dialog>
+      <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict"
+          class="confirmDialog"
+          change-status="[[changeStatus]]"
+          commit-message="[[commitMessage]]"
+          commit-num="[[commitNum]]"
+          on-confirm="_handleCherrypickConflictConfirm"
+          on-cancel="_handleConfirmDialogCancel"
+          project="[[change.project]]"
+          hidden></gr-confirm-cherrypick-conflict-dialog>
       <gr-confirm-move-dialog id="confirmMove"
           class="confirmDialog"
           on-confirm="_handleMoveConfirm"
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 368b11b..3e5c4f7 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
@@ -991,6 +991,14 @@
     },
 
     _handleCherrypickConfirm() {
+      this._handleCherryPickRestApi(false);
+    },
+
+    _handleCherrypickConflictConfirm() {
+      this._handleCherryPickRestApi(true);
+    },
+
+    _handleCherryPickRestApi(conflicts) {
       const el = this.$.confirmCherrypick;
       if (!el.branch) {
         // TODO(davido): Fix error handling
@@ -1011,6 +1019,7 @@
             destination: el.branch,
             base: el.baseCommit ? el.baseCommit : null,
             message: el.message,
+            allow_conflicts: conflicts,
           }
       );
     },
@@ -1113,8 +1122,9 @@
     _fireAction(endpoint, action, revAction, opt_payload) {
       const cleanupFn =
           this._setLoadingOnButtonWithKey(action.__type, action.__key);
-      this._send(action.method, opt_payload, endpoint, revAction, cleanupFn)
-          .then(this._handleResponse.bind(this, action));
+
+      this._send(action.method, opt_payload, endpoint, revAction, cleanupFn,
+          action).then(this._handleResponse.bind(this, action));
     },
 
     _showActionDialog(dialog) {
@@ -1171,7 +1181,14 @@
       });
     },
 
-    _handleResponseError(response) {
+    _handleResponseError(action, response, body) {
+      if (action && action.__key === RevisionActions.CHERRYPICK) {
+        if (response && response.status === 409 &&
+            body && !body.allow_conflicts) {
+          return this._showActionDialog(
+              this.$.confirmCherrypickConflict);
+        }
+      }
       return response.text().then(errText => {
         this.fire('show-error',
             {message: `Could not perform action: ${errText}`});
@@ -1187,13 +1204,12 @@
      * @param {string} actionEndpoint
      * @param {boolean} revisionAction
      * @param {?Function} cleanupFn
-     * @param {?Function=} opt_errorFn
+     * @param {!Object|undefined} action
      */
-    _send(method, payload, actionEndpoint, revisionAction, cleanupFn,
-        opt_errorFn) {
+    _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) {
       const handleError = response => {
         cleanupFn.call(this);
-        this._handleResponseError(response);
+        this._handleResponseError(action, response, payload);
       };
 
       return this.fetchChangeUpdates(this.change, this.$.restAPI)
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 1e63d53..1d83819 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -599,6 +599,38 @@
             destination: 'master',
             base: null,
             message: 'foo message',
+            allow_conflicts: false,
+          },
+        ]);
+      });
+
+      test('cherry pick even with conflicts', () => {
+        element._handleCherrypickTap();
+        const action = {
+          __key: 'cherrypick',
+          __type: 'revision',
+          __primary: false,
+          enabled: true,
+          label: 'Cherry pick',
+          method: 'POST',
+          title: 'Cherry pick change to a different branch',
+        };
+
+        element.$.confirmCherrypick.branch = 'master';
+
+        // Add attributes that are used to determine the message.
+        element.$.confirmCherrypick.commitMessage = 'foo message';
+        element.$.confirmCherrypick.changeStatus = 'OPEN';
+        element.$.confirmCherrypick.commitNum = '123';
+
+        element._handleCherrypickConflictConfirm();
+
+        assert.deepEqual(fireActionStub.lastCall.args, [
+          '/cherrypick', action, true, {
+            destination: 'master',
+            base: null,
+            message: 'foo message',
+            allow_conflicts: true,
           },
         ]);
       });
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 fc0cab0..32b1152 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
@@ -24,7 +24,6 @@
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
-<link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../../edit/gr-edit-constants.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
@@ -40,6 +39,7 @@
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
+<link rel="import" href="../../shared/revision-info/revision-info.html">
 <link rel="import" href="../gr-change-actions/gr-change-actions.html">
 <link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
 <link rel="import" href="../gr-commit-info/gr-commit-info.html">
@@ -519,13 +519,33 @@
           </div>
         </div>
       </section>
+
       <section class="patchInfo">
-        <gr-file-list-header
+        <template is="dom-if" if="[[_showPrimaryTabs]]">
+          <paper-tabs id="primaryTabs" on-selected-changed="_handleFileTabChange">
+            <paper-tab>Files</paper-tab>
+            <template is="dom-repeat" items="[[_dynamicTabHeaderEndpoints]]"
+              as="tabHeader">
+              <paper-tab>
+                  <gr-endpoint-decorator name$="[[tabHeader]]">
+                      <gr-endpoint-param name="change" value="[[_change]]">
+                      </gr-endpoint-param>
+                      <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+                      </gr-endpoint-param>
+                  </gr-endpoint-decorator>
+              </paper-tab>
+            </template>
+          </paper-tabs>
+        </template>
+
+        <div hidden$="[[!_showFileTabContent]]">
+          <gr-file-list-header
             id="fileListHeader"
             account="[[_account]]"
             all-patch-sets="[[_allPatchSets]]"
             change="[[_change]]"
             change-num="[[_changeNum]]"
+            revision-info="[[_revisionInfo]]"
             change-comments="[[_changeComments]]"
             commit-info="[[_commitInfo]]"
             change-url="[[_computeChangeUrl(_change)]]"
@@ -539,6 +559,7 @@
             base-patch-num="{{_patchRange.basePatchNum}}"
             files-expanded="[[_filesExpanded]]"
             diff-prefs-disabled="[[_diffPrefsDisabled]]"
+            show-title="[[!_showPrimaryTabs]]"
             on-open-diff-prefs="_handleOpenDiffPrefs"
             on-open-download-dialog="_handleOpenDownloadDialog"
             on-open-upload-help-dialog="_handleOpenUploadHelpDialog"
@@ -566,7 +587,18 @@
             on-files-shown-changed="_setShownFiles"
             on-file-action-tap="_handleFileActionTap"
             on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
+        </div>
+
+        <template is="dom-if" if="[[!_showFileTabContent]]">
+            <gr-endpoint-decorator name$="[[_selectedFilesTabPluginEndpoint]]">
+                <gr-endpoint-param name="change" value="[[_change]]">
+                </gr-endpoint-param>
+                <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+                </gr-endpoint-param>
+            </gr-endpoint-decorator>
+        </template>
       </section>
+
       <gr-endpoint-decorator name="change-view-integration">
         <gr-endpoint-param name="change" value="[[_change]]">
         </gr-endpoint-param>
@@ -575,7 +607,7 @@
       </gr-endpoint-decorator>
       <paper-tabs
           id="commentTabs"
-          on-selected-changed="_handleTabChange">
+          on-selected-changed="_handleCommentTabChange">
         <paper-tab class="changeLog">Change Log</paper-tab>
         <paper-tab
             class="commentThreads">
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 bb51fcc..123a7b5 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
@@ -132,6 +132,7 @@
         type: Object,
         value: {},
       },
+      _prefs: Object,
       /** @type {?} */
       _changeComments: Object,
       _canStartReview: {
@@ -144,6 +145,10 @@
         type: Object,
         observer: '_changeChanged',
       },
+      _revisionInfo: {
+        type: Object,
+        computed: '_getRevisionInfo(_change)',
+      },
       /** @type {?} */
       _commitInfo: Object,
       _currentRevision: {
@@ -253,6 +258,25 @@
         type: Boolean,
         value: true,
       },
+      _showFileTabContent: {
+        type: Boolean,
+        value: true,
+      },
+      /** @type {Array<string>} */
+      _dynamicTabHeaderEndpoints: {
+        type: Array,
+      },
+      _showPrimaryTabs: {
+        type: Boolean,
+        computed: '_computeShowPrimaryTabs(_dynamicTabHeaderEndpoints)',
+      },
+      /** @type {Array<string>} */
+      _dynamicTabContentEndpoints: {
+        type: Array,
+      },
+      _selectedFilesTabPluginEndpoint: {
+        type: String,
+      },
     },
 
     behaviors: [
@@ -272,6 +296,7 @@
       'fullscreen-overlay-closed': '_handleShowBackgroundContent',
       'diff-comments-modified': '_handleReloadCommentThreads',
     },
+
     observers: [
       '_labelsChanged(_change.labels.*)',
       '_paramsAndChangeChanged(params, _change)',
@@ -310,6 +335,17 @@
         this._setDiffViewMode();
       });
 
+      Gerrit.awaitPluginsLoaded().then(() => {
+        this._dynamicTabHeaderEndpoints =
+            Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header');
+        this._dynamicTabContentEndpoints =
+            Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content');
+        if (this._dynamicTabContentEndpoints.length
+            !== this._dynamicTabHeaderEndpoints.length) {
+          console.warn('Different number of tab headers and tab content.');
+        }
+      });
+
       this.addEventListener('comment-save', this._handleCommentSave.bind(this));
       this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
       this.addEventListener('comment-discard',
@@ -345,7 +381,7 @@
     _setDiffViewMode(opt_reset) {
       if (!opt_reset && this.viewState.diffViewMode) { return; }
 
-      return this.$.restAPI.getPreferences().then( prefs => {
+      return this._getPreferences().then( prefs => {
         if (!this.viewState.diffMode) {
           this.set('viewState.diffMode', prefs.default_diff_view);
         }
@@ -368,10 +404,18 @@
       }
     },
 
-    _handleTabChange() {
+    _handleCommentTabChange() {
       this._showMessagesView = this.$.commentTabs.selected === 0;
     },
 
+    _handleFileTabChange() {
+      const selectedIndex = this.$$('#primaryTabs').selected;
+      this._showFileTabContent = selectedIndex === 0;
+      // Initial tab is the static files list.
+      this._selectedFilesTabPluginEndpoint =
+          this._dynamicTabContentEndpoints[selectedIndex - 1];
+    },
+
     _handleEditCommitMessage(e) {
       this._editingCommitMessage = true;
       this.$.commitMessageEditor.focusTextarea();
@@ -699,6 +743,8 @@
       // Selected has to be set after the paper-tabs are visible because
       // the selected underline depends on calculations made by the browser.
       this.$.commentTabs.selected = 0;
+      const primaryTabs = this.$$('#primaryTabs');
+      if (primaryTabs) primaryTabs.selected = 0;
 
       this.async(() => {
         if (this.viewState.scrollTop) {
@@ -807,10 +853,11 @@
 
     _changeChanged(change) {
       if (!change || !this._patchRange || !this._allPatchSets) { return; }
-      this.set('_patchRange.basePatchNum',
-          this._patchRange.basePatchNum || 'PARENT');
-      this.set('_patchRange.patchNum',
-          this._patchRange.patchNum ||
+
+      const parent = this._getBasePatchNum(change, this._patchRange);
+
+      this.set('_patchRange.basePatchNum', parent);
+      this.set('_patchRange.patchNum', this._patchRange.patchNum ||
               this.computeLatestPatchNum(this._allPatchSets));
 
       // Reset the related changes toggle in the event it was previously
@@ -821,6 +868,39 @@
       this.fire('title-change', {title});
     },
 
+    /**
+     * Gets base patch number, if is a parent try and
+     * decide from preference weather to default to `auto merge`
+     * or `Parent 1`.
+     * @param {Object} change
+     * @param {Object} patchRange
+     * @return {number|string}
+     */
+    _getBasePatchNum(change, patchRange) {
+      if (patchRange.basePatchNum &&
+          patchRange.basePatchNum !== 'PARENT') {
+        return patchRange.basePatchNum;
+      }
+
+      const revisionInfo = this._getRevisionInfo(change);
+      if (!revisionInfo) return 'PARENT';
+
+      const parentCounts = revisionInfo.getParentCountMap();
+      // check that there is at least 2 parents otherwise fall back to 1,
+      // which means there is only one parent.
+      const parentCount = parentCounts.hasOwnProperty(1) ?
+          parentCounts[1] : 1;
+
+      const preferFirst = this._prefs &&
+          this._prefs.default_base_for_merges === 'FIRST_PARENT';
+
+      return parentCount > 1 && preferFirst ? -1 : 'PARENT';
+    },
+
+    _computeShowPrimaryTabs(dynamicTabContentEndpoints) {
+      return dynamicTabContentEndpoints.length > 0;
+    },
+
     _computeChangeUrl(change) {
       return Gerrit.Nav.getUrlForChange(change);
     },
@@ -1076,6 +1156,10 @@
           });
     },
 
+    _getPreferences() {
+      return this.$.restAPI.getPreferences();
+    },
+
     _updateRebaseAction(revisionActions) {
       if (revisionActions && revisionActions.rebase) {
         revisionActions.rebase.rebaseOnCurrent =
@@ -1129,9 +1213,12 @@
       const detailCompletes = this.$.restAPI.getChangeDetail(
           this._changeNum, this._handleGetChangeDetailError.bind(this));
       const editCompletes = this._getEdit();
+      const prefCompletes = this._getPreferences();
 
-      return Promise.all([detailCompletes, editCompletes])
-          .then(([change, edit]) => {
+      return Promise.all([detailCompletes, editCompletes, prefCompletes])
+          .then(([change, edit, prefs]) => {
+            this._prefs = prefs;
+
             if (!change) {
               return '';
             }
@@ -1680,6 +1767,10 @@
           e.detail.starred);
     },
 
+    _getRevisionInfo(change) {
+      return new Gerrit.RevisionInfo(change);
+    },
+
     _computeCurrentRevision(currentRevision, revisions) {
       return revisions && revisions[currentRevision];
     },
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 a88142e..b8b48e7 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
@@ -297,7 +297,8 @@
       });
 
       test(', should open diff preferences', () => {
-        const stub = sandbox.stub(element.$.fileList.$.diffPreferences, 'open');
+        const stub = sandbox.stub(
+            element.$.fileList.$.diffPreferencesDialog, 'open');
         element._loggedIn = false;
         element.disableDiffPrefs = true;
         MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
@@ -1604,8 +1605,8 @@
     });
 
     test('_selectedRevision updates when patchNum is changed', () => {
-      const revision1 = {_number: 1, commit: {}};
-      const revision2 = {_number: 2, commit: {}};
+      const revision1 = {_number: 1, commit: {parents: []}};
+      const revision2 = {_number: 2, commit: {parents: []}};
       sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
           Promise.resolve({
             revisions: {
@@ -1618,6 +1619,7 @@
             change_id: 'loremipsumdolorsitamet',
           }));
       sandbox.stub(element, '_getEdit').returns(Promise.resolve());
+      sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
       element._patchRange = {patchNum: '2'};
       return element._getChangeDetail().then(() => {
         assert.strictEqual(element._selectedRevision, revision2);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
new file mode 100644
index 0000000..cd196ec
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
@@ -0,0 +1,51 @@
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
+
+<dom-module id="gr-confirm-cherrypick-conflict-dialog">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+      }
+      :host([disabled]) {
+        opacity: .5;
+        pointer-events: none;
+      }
+      .main {
+        display: flex;
+        flex-direction: column;
+        width: 100%;
+      }
+    </style>
+    <gr-dialog
+        confirm-label="Continue"
+        on-confirm="_handleConfirmTap"
+        on-cancel="_handleCancelTap">
+      <div class="header" slot="header">Cherry Pick Conflict!</div>
+      <div class="main" slot="main">
+        <span>Cherry Pick failed! (merge conflicts)</span>
+
+        <span>Please select "Continue" to continue with conflicts or select "cancel" to close the dialog.</span>
+      </div>
+    </gr-dialog>
+  </template>
+  <script src="gr-confirm-cherrypick-conflict-dialog.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..355aa78
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-confirm-cherrypick-conflict-dialog',
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    _handleConfirmTap(e) {
+      e.preventDefault();
+      this.fire('confirm', null, {bubbles: false});
+    },
+
+    _handleCancelTap(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
new file mode 100644
index 0000000..77b102c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-confirm-cherrypick-conflict-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-confirm-cherrypick-conflict-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-confirm-cherrypick-conflict-dialog></gr-confirm-cherrypick-conflict-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-confirm-cherrypick-conflict-dialog tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('_handleConfirmTap', () => {
+      const confirmHandler = sandbox.stub();
+      element.addEventListener('confirm', confirmHandler);
+      sandbox.stub(element, '_handleConfirmTap');
+      element.$$('gr-dialog').fire('confirm');
+      assert.isTrue(confirmHandler.called);
+      assert.isTrue(element._handleConfirmTap.called);
+    });
+
+    test('_handleCancelTap', () => {
+      const cancelHandler = sandbox.stub();
+      element.addEventListener('cancel', cancelHandler);
+      sandbox.stub(element, '_handleCancelTap');
+      element.$$('gr-dialog').fire('cancel');
+      assert.isTrue(cancelHandler.called);
+      assert.isTrue(element._handleCancelTap.called);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index 5e82cda..28d25d2 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -19,8 +19,8 @@
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
 
 <dom-module id="gr-download-dialog">
   <template>
@@ -87,7 +87,7 @@
           selected-scheme="{{_selectedScheme}}"></gr-download-commands>
     </section>
     <section class="flexContainer">
-      <div class="patchFiles">
+      <div class="patchFiles" hidden="[[_computeHidePatchFile(change, patchNum)]]" hidden>
         <label>Patch file</label>
         <div>
           <a
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 5fc81e8..4fe6d8f 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
@@ -138,6 +138,17 @@
       return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64');
     },
 
+    _computeHidePatchFile(change, patchNum) {
+      for (const rev of Object.values(change.revisions || {})) {
+        if (this.patchNumEquals(rev._number, patchNum)) {
+          const parentLength = rev.commit && rev.commit.parents ?
+                rev.commit.parents.length : 0;
+          return parentLength == 0;
+        }
+      }
+      return false;
+    },
+
     _computeArchiveDownloadLink(change, patchNum, format) {
       return this.changeBaseURL(change.project, change._number, patchNum) +
           '/archive?format=' + format;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 2915e29..ee284b9 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -45,6 +45,9 @@
       revisions: {
         '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
           _number: 1,
+          commit: {
+            parents: [],
+          },
           fetch: {
             repo: {
               commands: {
@@ -105,6 +108,9 @@
       revisions: {
         '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
           _number: 1,
+          commit: {
+            parents: [],
+          },
           fetch: {},
         },
       },
@@ -188,5 +194,25 @@
       assert.equal(element._computeShowDownloadCommands([]), 'hidden');
       assert.equal(element._computeShowDownloadCommands(['test']), '');
     });
+
+    test('_computeHidePatchFile', () => {
+      const patchNum = '1';
+
+      const change1 = {
+        revisions: {
+          r1: {_number: 1, commit: {parents: []}},
+        },
+      };
+      assert.isTrue(element._computeHidePatchFile(change1, patchNum));
+
+      const change2 = {
+        revisions: {
+          r1: {_number: 1, commit: {parents: [
+            {commit: 'p1'},
+          ]}},
+        },
+      };
+      assert.isFalse(element._computeHidePatchFile(change2, patchNum));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index 924ddab..f7d90eb 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -28,7 +28,6 @@
 <link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/revision-info/revision-info.html">
 <link rel="import" href="../gr-file-list-constants.html">
 
 <dom-module id="gr-file-list-header">
@@ -154,7 +153,10 @@
     </style>
     <div class$="patchInfo-header [[_computeEditModeClass(editMode)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
       <div class="patchInfo-left">
-        <h3 class="label">Files</h3>
+        <template is="dom-if"
+            if="[[showTitle]]">
+          <h3 class="label">Files</h3>
+        </template>
         <div class="patchInfoContent">
           <gr-patch-range-select
               id="rangeSelect"
@@ -164,7 +166,7 @@
               base-patch-num="[[basePatchNum]]"
               available-patches="[[allPatchSets]]"
               revisions="[[change.revisions]]"
-              revision-info="[[_revisionInfo]]"
+              revision-info="[[revisionInfo]]"
               on-patch-range-change="_handlePatchChange">
           </gr-patch-range-select>
           <span class="separator"></span>
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 b9e6288..7b79a63 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
@@ -81,14 +81,15 @@
         type: String,
         value: '',
       },
+      showTitle: {
+        type: Boolean,
+        value: true,
+      },
       _descriptionReadOnly: {
         type: Boolean,
         computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
       },
-      _revisionInfo: {
-        type: Object,
-        computed: '_getRevisionInfo(change)',
-      },
+      revisionInfo: Object,
     },
 
     behaviors: [
@@ -230,10 +231,6 @@
       return 'patchInfoOldPatchSet';
     },
 
-    _getRevisionInfo(change) {
-      return new Gerrit.RevisionInfo(change);
-    },
-
     _hideIncludedIn(change) {
       return change && change.status === MERGED_STATUS ? '' : 'hide';
     },
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 4b6ec4c..29a12df 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -23,8 +23,9 @@
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../diff/gr-diff-host/gr-diff-host.html">
 <link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html">
+<link rel="import" href="../../diff/gr-diff-host/gr-diff-host.html">
+<link rel="import" href="../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
 <link rel="import" href="../../edit/gr-edit-file-controls/gr-edit-file-controls.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
@@ -464,10 +465,11 @@
         </gr-button><!--
   --></gr-tooltip-content>
     </div>
-    <gr-diff-preferences
-        id="diffPreferences"
-        prefs="{{diffPrefs}}"
-        local-prefs="{{_localPrefs}}"></gr-diff-preferences>
+    <gr-diff-preferences-dialog
+        id="diffPreferencesDialog"
+        diff-prefs="{{diffPrefs}}"
+        on-reload-diff-preference="_handleReloadingDiffPreference">
+    </gr-diff-preferences-dialog>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
     <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
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 8bab9d0..460512a 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
@@ -125,7 +125,6 @@
       },
       /** @type {?} */
       _userPrefs: Object,
-      _localPrefs: Object,
       _showInlineDiffs: Boolean,
       numFilesShown: {
         type: Number,
@@ -269,7 +268,6 @@
         });
       }));
 
-      this._localPrefs = this.$.storage.getPreferences();
       promises.push(this._getDiffPreferences().then(prefs => {
         this.diffPrefs = prefs;
       }));
@@ -297,7 +295,7 @@
     },
 
     openDiffPrefs() {
-      this.$.diffPreferences.open();
+      this.$.diffPreferencesDialog.open();
     },
 
     _calculatePatchChange(files) {
@@ -1255,5 +1253,11 @@
 
       return 'Mark as reviewed (shortcut: r)';
     },
+
+    _handleReloadingDiffPreference() {
+      this._getDiffPreferences().then(prefs => {
+        this.diffPrefs = prefs;
+      });
+    },
   });
 })();
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 6699bd1..17f883f 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
@@ -65,6 +65,7 @@
     'is:reviewed',
     'is:reviewer',
     'is:starred',
+    'is:submittable',
     'is:watched',
     'is:wip',
     'label:',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 2227b45..29cc3b9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -80,12 +80,6 @@
          */
 
         /**
-         * Fired when the diff is rendered.
-         *
-         * @event render
-         */
-
-        /**
          * Fired when the diff finishes rendering text content, but not
          * necessarily syntax highlights.
          *
@@ -169,8 +163,6 @@
                 return this.$.syntaxLayer.process().then(() => {
                   reporting.timeEnd(TimingLabel.SYNTAX);
                   reporting.timeEnd(TimingLabel.TOTAL);
-                  this.dispatchEvent(
-                      new CustomEvent('render', {bubbles: true}));
                 });
               });
         },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 0fc4748..67d8c19 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -850,14 +850,13 @@
         assert.strictEqual(sections[1], section[1]);
       });
 
-      test('render-start and render are fired', done => {
+      test('render-start and render-content are fired', done => {
         const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
         element.render(keyLocations, {}).then(() => {
           const firedEventTypes = dispatchEventStub.getCalls()
               .map(c => { return c.args[0].type; });
           assert.include(firedEventTypes, 'render-start');
           assert.include(firedEventTypes, 'render-content');
-          assert.include(firedEventTypes, 'render');
           done();
         });
       });
@@ -869,10 +868,6 @@
       test('rendering large diff disables syntax', done => {
         // Before it renders, set the first diff line to 500 '*' characters.
         element.diff.content[0].a = [new Array(501).join('*')];
-        element.addEventListener('render', () => {
-          assert.isFalse(element.$.syntaxLayer.enabled);
-          done();
-        });
         const prefs = {
           line_length: 10,
           show_tabs: true,
@@ -880,7 +875,10 @@
           context: -1,
           syntax_highlighting: true,
         };
-        element.render(keyLocations, prefs);
+        element.render(keyLocations, prefs).then(() => {
+          assert.isFalse(element.$.syntaxLayer.enabled);
+          done();
+        });
       });
 
       test('cancel', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
new file mode 100644
index 0000000..ae53f76
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
@@ -0,0 +1,80 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+
+<dom-module id="gr-diff-preferences-dialog">
+  <template>
+    <style include="shared-styles">
+      .diffHeader,
+      .diffActions {
+        padding: 1em 1.5em;
+      }
+      .diffHeader,
+      .diffActions {
+        background-color: var(--dialog-background-color);
+      }
+      .diffHeader {
+        border-bottom: 1px solid var(--border-color);
+        font-weight: var(--font-weight-bold);
+      }
+      .diffActions {
+        border-top: 1px solid var(--border-color);
+        display: flex;
+        justify-content: flex-end;
+      }
+      .diffPrefsOverlay gr-button {
+        margin-left: 1em;
+      }
+      div.edited:after {
+        color: var(--deemphasized-text-color);
+        content: ' *';
+      }
+      #diffPreferences {
+        display: flex;
+        padding: .35em 1.5em;
+      }
+    </style>
+    <gr-overlay id="diffPrefsOverlay" with-backdrop>
+      <div class$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]">Diff Preferences</div>
+      <gr-diff-preferences
+          id="diffPreferences"
+          diff-prefs="{{diffPrefs}}"
+          has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
+      <div class="diffActions">
+        <gr-button
+            id="cancelButton"
+            link
+            on-tap="_handleCancelDiff">
+            Cancel
+        </gr-button>
+        <gr-button
+            id="saveButton"
+            link primary
+            on-tap="_handleSaveDiffPreferences"
+            disabled$="[[!_diffPrefsChanged]]">
+            Save
+        </gr-button>
+      </div>
+    </gr-overlay>
+  </template>
+  <script src="gr-diff-preferences-dialog.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..b50ef69
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff-preferences-dialog',
+
+    properties: {
+      /** @type {?} */
+      diffPrefs: Object,
+
+      _diffPrefsChanged: Boolean,
+    },
+
+    getFocusStops() {
+      return {
+        start: this.$.contextSelect,
+        end: this.$.saveButton,
+      };
+    },
+
+    resetFocus() {
+      this.$.contextSelect.focus();
+    },
+
+    _computeHeaderClass(changed) {
+      return changed ? 'edited' : '';
+    },
+
+    _handleCancelDiff(e) {
+      e.stopPropagation();
+      this.$.diffPrefsOverlay.close();
+    },
+
+    open() {
+      this.$.diffPrefsOverlay.open().then(() => {
+        const focusStops = this.getFocusStops();
+        this.$.diffPrefsOverlay.setFocusStops(focusStops);
+        this.resetFocus();
+      });
+    },
+
+    _handleSaveDiffPreferences() {
+      this.$.diffPreferences.save().then(() => {
+        this.fire('reload-diff-preference', null, {bubbles: false});
+
+        this.$.diffPrefsOverlay.close();
+      });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
deleted file mode 100644
index a22f689..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ /dev/null
@@ -1,173 +0,0 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-diff-preferences">
-  <template>
-    <style include="shared-styles">
-      :host {
-        display: block;
-      }
-      :host([disabled]) {
-        opacity: .5;
-        pointer-events: none;
-      }
-      input,
-      select {
-        font: inherit;
-      }
-      input[type="number"] {
-        width: 4em;
-      }
-      .header,
-      .actions {
-        padding: 1em 1.5em;
-      }
-      .header,
-      .mainContainer,
-      .actions {
-        background-color: var(--dialog-background-color);
-      }
-      .header {
-        border-bottom: 1px solid var(--border-color);
-        font-weight: var(--font-weight-bold);
-      }
-      .mainContainer {
-        padding: 1em 0;
-      }
-      .pref {
-        align-items: center;
-        display: flex;
-        padding: .35em 1.5em;
-        width: 25em;
-      }
-      .pref:hover {
-        background-color: var(--hover-background-color);
-      }
-      .pref label {
-        cursor: pointer;
-        flex: 1;
-      }
-      .actions {
-        border-top: 1px solid var(--border-color);
-        display: flex;
-        justify-content: flex-end;
-      }
-      gr-button {
-        margin-left: 1em;
-      }
-    </style>
-    <gr-overlay id="prefsOverlay" with-backdrop>
-      <div class="header">
-        Diff View Preferences
-      </div>
-      <div class="mainContainer">
-        <div class="pref">
-          <label for="contextSelect">Context</label>
-          <select id="contextSelect" on-change="_handleContextSelectChange">
-            <option value="3">3 lines</option>
-            <option value="10">10 lines</option>
-            <option value="25">25 lines</option>
-            <option value="50">50 lines</option>
-            <option value="75">75 lines</option>
-            <option value="100">100 lines</option>
-            <option value="-1">Whole file</option>
-          </select>
-        </div>
-        <div class="pref">
-          <label for="lineWrappingInput">Fit to screen</label>
-          <input
-              is="iron-input"
-              type="checkbox"
-              id="lineWrappingInput"
-              on-tap="_handlelineWrappingTap">
-        </div>
-        <div class="pref" id="columnsPref">
-          <label for="columnsInput">Diff width</label>
-          <input is="iron-input" type="number" id="columnsInput"
-              prevent-invalid-input
-              allowed-pattern="[0-9]"
-              bind-value="{{_newPrefs.line_length}}">
-        </div>
-        <div class="pref">
-          <label for="tabSizeInput">Tab width</label>
-          <input is="iron-input" type="number" id="tabSizeInput"
-              prevent-invalid-input
-              allowed-pattern="[0-9]"
-              bind-value="{{_newPrefs.tab_size}}">
-        </div>
-        <div class="pref" hidden$="[[!_newPrefs.font_size]]">
-          <label for="fontSizeInput">Font size</label>
-          <input is="iron-input" type="number" id="fontSizeInput"
-                prevent-invalid-input
-                allowed-pattern="[0-9]"
-                bind-value="{{_newPrefs.font_size}}">
-        </div>
-        <div class="pref">
-          <label for="showTabsInput">Show tabs</label>
-          <input is="iron-input" type="checkbox" id="showTabsInput"
-              on-tap="_handleShowTabsTap">
-        </div>
-        <div class="pref">
-          <label for="showTrailingWhitespaceInput">
-            Show trailing whitespace</label>
-          <input is="iron-input" type="checkbox"
-              id="showTrailingWhitespaceInput"
-              on-tap="_handleShowTrailingWhitespaceTap">
-        </div>
-        <div class="pref">
-          <label for="syntaxHighlightInput">Syntax highlighting</label>
-          <input is="iron-input" type="checkbox" id="syntaxHighlightInput"
-              on-tap="_handleSyntaxHighlightTap">
-        </div>
-        <div class="pref">
-          <label for="automaticReviewInput">Automatically mark viewed files reviewed</label>
-          <input
-              is="iron-input"
-              id="automaticReviewInput"
-              type="checkbox"
-              on-tap="_handleAutomaticReviewTap">
-        </div>
-        <div class="pref">
-          <label for="ignoreWhitespace">Ignore Whitespace</label>
-          <select id="ignoreWhitespace" on-change="_handleIgnoreWhitespaceChange">
-            <option value="IGNORE_NONE">None</option>
-            <option value="IGNORE_TRAILING">Trailing</option>
-            <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
-            <option value="IGNORE_ALL">All</option>
-          </select>
-        </div>
-      </div>
-      <div class="actions">
-        <gr-button id="cancelButton" link on-tap="_handleCancel">
-            Cancel</gr-button>
-        <gr-button id="saveButton" link primary on-tap="_handleSave">
-            Save</gr-button>
-      </div>
-    </gr-overlay>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-    <gr-storage id="storage"></gr-storage>
-  </template>
-  <script src="gr-diff-preferences.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
deleted file mode 100644
index 8fc90b9..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-diff-preferences',
-
-    properties: {
-      prefs: {
-        type: Object,
-        notify: true,
-      },
-      localPrefs: {
-        type: Object,
-        notify: true,
-      },
-      disabled: {
-        type: Boolean,
-        value: false,
-        reflectToAttribute: true,
-      },
-
-      /** @type {?} */
-      _newPrefs: Object,
-      _newLocalPrefs: Object,
-    },
-
-    observers: [
-      '_prefsChanged(prefs.*)',
-      '_localPrefsChanged(localPrefs.*)',
-    ],
-
-    getFocusStops() {
-      return {
-        start: this.$.contextSelect,
-        end: this.$.saveButton,
-      };
-    },
-
-    resetFocus() {
-      this.$.contextSelect.focus();
-    },
-
-    _prefsChanged(changeRecord) {
-      const prefs = changeRecord.base;
-      // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
-      // an object as a value, it must be marked enumerable.
-      this._newPrefs = Object.assign({}, prefs);
-      this.$.contextSelect.value = prefs.context;
-      this.$.showTabsInput.checked = prefs.show_tabs;
-      this.$.showTrailingWhitespaceInput.checked = prefs.show_whitespace_errors;
-      this.$.lineWrappingInput.checked = prefs.line_wrapping;
-      this.$.syntaxHighlightInput.checked = prefs.syntax_highlighting;
-      this.$.automaticReviewInput.checked = !prefs.manual_review;
-      this.$.ignoreWhitespace.value = prefs.ignore_whitespace;
-    },
-
-    _localPrefsChanged(changeRecord) {
-      const localPrefs = changeRecord.base || {};
-      this._newLocalPrefs = Object.assign({}, localPrefs);
-    },
-
-    _handleContextSelectChange(e) {
-      const selectEl = Polymer.dom(e).rootTarget;
-      this.set('_newPrefs.context', parseInt(selectEl.value, 10));
-    },
-
-    _handleIgnoreWhitespaceChange(e) {
-      const selectEl = Polymer.dom(e).rootTarget;
-      this.set('_newPrefs.ignore_whitespace', selectEl.value);
-    },
-
-    _handleShowTabsTap(e) {
-      this.set('_newPrefs.show_tabs', Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handleShowTrailingWhitespaceTap(e) {
-      this.set('_newPrefs.show_whitespace_errors',
-          Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handleSyntaxHighlightTap(e) {
-      this.set('_newPrefs.syntax_highlighting',
-          Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handlelineWrappingTap(e) {
-      this.set('_newPrefs.line_wrapping', Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handleAutomaticReviewTap(e) {
-      this.set('_newPrefs.manual_review', !Polymer.dom(e).rootTarget.checked);
-    },
-
-    _handleSave(e) {
-      e.stopPropagation();
-      this.prefs = this._newPrefs;
-      this.localPrefs = this._newLocalPrefs;
-      const el = Polymer.dom(e).rootTarget;
-      el.disabled = true;
-      this.$.storage.savePreferences(this._localPrefs);
-      this._saveDiffPreferences().then(response => {
-        el.disabled = false;
-        if (!response.ok) { return response; }
-
-        this.$.prefsOverlay.close();
-      }).catch(err => {
-        el.disabled = false;
-      });
-    },
-
-    _handleCancel(e) {
-      e.stopPropagation();
-      this.$.prefsOverlay.close();
-    },
-
-    open() {
-      this.$.prefsOverlay.open().then(() => {
-        const focusStops = this.getFocusStops();
-        this.$.prefsOverlay.setFocusStops(focusStops);
-        this.resetFocus();
-      });
-    },
-
-    _saveDiffPreferences() {
-      return this.$.restAPI.saveDiffPreferences(this.prefs);
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
deleted file mode 100644
index d9e14c0..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ /dev/null
@@ -1,110 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-diff-preferences</title>
-
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-diff-preferences.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-preferences></gr-diff-preferences>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-diff-preferences tests', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('model changes', () => {
-      element.prefs = {
-        context: 10,
-        font_size: 12,
-        line_length: 100,
-        show_tabs: true,
-        tab_size: 8,
-        show_whitespace_errors: true,
-        syntax_highlighting: true,
-      };
-      assert.deepEqual(element.prefs, element._newPrefs);
-
-      element.$.contextSelect.value = '50';
-      element.fire('change', {}, {node: element.$.contextSelect});
-      element.$.columnsInput.bindValue = 80;
-      element.$.fontSizeInput.bindValue = 10;
-      element.$.tabSizeInput.bindValue = 4;
-      MockInteractions.tap(element.$.showTabsInput);
-      MockInteractions.tap(element.$.showTrailingWhitespaceInput);
-      MockInteractions.tap(element.$.syntaxHighlightInput);
-      MockInteractions.tap(element.$.lineWrappingInput);
-
-      assert.equal(element._newPrefs.context, 50);
-      assert.equal(element._newPrefs.font_size, 10);
-      assert.equal(element._newPrefs.line_length, 80);
-      assert.equal(element._newPrefs.tab_size, 4);
-      assert.isFalse(element._newPrefs.show_tabs);
-      assert.isFalse(element._newPrefs.show_whitespace_errors);
-      assert.isTrue(element._newPrefs.line_wrapping);
-      assert.isFalse(element._newPrefs.syntax_highlighting);
-    });
-
-    test('clicking save button calls _handleSave function', () => {
-      const savePrefs = sinon.stub(element, '_handleSave');
-      MockInteractions.tap(element.$.saveButton);
-      flushAsynchronousOperations();
-      assert(savePrefs.calledOnce);
-      savePrefs.restore();
-    });
-
-    test('save button', () => {
-      element.prefs = {
-        font_size: '11',
-      };
-      element._newPrefs = {
-        font_size: '12',
-      };
-      const saveStub = sandbox.stub(element.$.restAPI, 'saveDiffPreferences',
-          () => { return Promise.resolve(); });
-
-      MockInteractions.tap(element.$$('gr-button[primary]'));
-      assert.deepEqual(element.prefs, element._newPrefs);
-      assert.deepEqual(saveStub.lastCall.args[0], element._newPrefs);
-    });
-
-    test('cancel button', () => {
-      const closeStub = sandbox.stub(element.$.prefsOverlay, 'close');
-      MockInteractions.tap(element.$$('gr-button:not([primary])'));
-      assert.isTrue(closeStub.called);
-    });
-  });
-</script>
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 b3210cc..57525e1 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
@@ -34,9 +34,9 @@
 <link rel="import" href="../../shared/revision-info/revision-info.html">
 <link rel="import" href="../gr-comment-api/gr-comment-api.html">
 <link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html">
-<link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html">
-<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../gr-diff-host/gr-diff-host.html">
+<link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html">
+<link rel="import" href="../gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
 <link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
 
 <dom-module id="gr-diff-view">
@@ -338,10 +338,11 @@
         on-comment-anchor-tap="_onLineSelected"
         on-line-selected="_onLineSelected">
     </gr-diff-host>
-    <gr-diff-preferences
-        id="diffPreferences"
-        prefs="{{_prefs}}"
-        local-prefs="{{_localPrefs}}"></gr-diff-preferences>
+    <gr-diff-preferences-dialog
+        id="diffPreferencesDialog"
+        diff-prefs="{{_prefs}}"
+        on-reload-diff-preference="_handleReloadingDiffPreference">
+    </gr-diff-preferences-dialog>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
     <gr-diff-cursor id="cursor"></gr-diff-cursor>
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 dadf8a7..c8b3c4b 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
@@ -266,7 +266,9 @@
     },
 
     _getDiffPreferences() {
-      return this.$.restAPI.getDiffPreferences();
+      return this.$.restAPI.getDiffPreferences().then(prefs => {
+        this._prefs = prefs;
+      });
     },
 
     _getPreferences() {
@@ -466,7 +468,7 @@
       if (this._diffPrefsDisabled) { return; }
 
       e.preventDefault();
-      this.$.diffPreferences.open();
+      this.$.diffPreferencesDialog.open();
     },
 
     _handleToggleDiffMode(e) {
@@ -617,10 +619,7 @@
 
       const promises = [];
 
-      this._localPrefs = this.$.storage.getPreferences();
-      promises.push(this._getDiffPreferences().then(prefs => {
-        this._prefs = prefs;
-      }));
+      promises.push(this._getDiffPreferences());
 
       promises.push(this._getPreferences().then(prefs => {
         this._userPrefs = prefs;
@@ -846,22 +845,7 @@
 
     _handlePrefsTap(e) {
       e.preventDefault();
-      this.$.diffPreferences.open();
-    },
-
-    _handlePrefsSave(e) {
-      e.stopPropagation();
-      const el = Polymer.dom(e).rootTarget;
-      el.disabled = true;
-      this.$.storage.savePreferences(this._localPrefs);
-      this._saveDiffPreferences().then(response => {
-        el.disabled = false;
-        if (!response.ok) { return response; }
-
-        this.$.prefsOverlay.close();
-      }).catch(err => {
-        el.disabled = false;
-      });
+      this.$.diffPreferencesDialog.open();
     },
 
     /**
@@ -1049,5 +1033,9 @@
           (file === this._path || !this._reviewedFiles.has(file)));
       this._navToFile(this._path, unreviewedFiles, 1);
     },
+
+    _handleReloadingDiffPreference() {
+      this._getDiffPreferences();
+    },
   });
 })();
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 0274330..c4d6c95 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
@@ -173,7 +173,7 @@
       assert.isTrue(element._loading);
 
       const showPrefsStub =
-          sandbox.stub(element.$.diffPreferences.$.prefsOverlay, 'open',
+          sandbox.stub(element.$.diffPreferencesDialog, 'open',
               () => Promise.resolve());
 
       MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
@@ -386,7 +386,7 @@
 
     test('prefsButton opens gr-diff-preferences', () => {
       const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
-      const overlayOpenStub = sandbox.stub(element.$.diffPreferences,
+      const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog,
           'open');
       const prefsButton =
           Polymer.dom(element.root).querySelector('.prefsButton');
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 d5fb878..5ecc2fc3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -116,11 +116,18 @@
      * @event show-auth-required
      */
 
-     /**
-      * Fired when a comment is created
-      *
-      * @event create-comment
-      */
+    /**
+     * Fired when a comment is created
+     *
+     * @event create-comment
+     */
+
+    /**
+     * Fired when rendering, including syntax highlighting, is done. Also fired
+     * when no rendering can be done because required preferences are not set.
+     *
+     * @event render
+     */
 
     properties: {
       changeNum: String,
@@ -700,7 +707,11 @@
       this._showWarning = false;
 
       const keyLocations = this._computeKeyLocations();
-      this.$.diffBuilder.render(keyLocations, this._getBypassPrefs());
+      this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
+          .then(() => {
+            this.dispatchEvent(
+                new CustomEvent('render', {bubbles: true}));
+          });
     },
 
     _handleRenderContent() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 300807c..849ad0d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -943,7 +943,8 @@
       setup(() => {
         element = fixture('basic');
         element.prefs = {};
-        renderStub = sandbox.stub(element.$.diffBuilder, 'render');
+        renderStub = sandbox.stub(element.$.diffBuilder, 'render')
+            .returns(new Promise(() => {}));
       });
 
       test('lineOfInterest is a key location', () => {
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 fe5d427..a7e7377 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
@@ -97,6 +97,7 @@
     'gr-diff gr-syntax gr-syntax-attribute': true,
     'gr-diff gr-syntax gr-syntax-built_in': true,
     'gr-diff gr-syntax gr-syntax-comment': true,
+    'gr-diff gr-syntax gr-syntax-function': true,
     'gr-diff gr-syntax gr-syntax-keyword': true,
     'gr-diff gr-syntax gr-syntax-link': true,
     'gr-diff gr-syntax gr-syntax-literal': true,
@@ -104,6 +105,7 @@
     'gr-diff gr-syntax gr-syntax-meta-keyword': true,
     'gr-diff gr-syntax gr-syntax-name': true,
     'gr-diff gr-syntax gr-syntax-number': true,
+    'gr-diff gr-syntax gr-syntax-params': true,
     'gr-diff gr-syntax gr-syntax-regexp': true,
     'gr-diff gr-syntax gr-syntax-selector-attr': true,
     'gr-diff gr-syntax gr-syntax-selector-class': true,
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
index 41d3804..a122113 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
@@ -29,6 +29,12 @@
       .contentText {
         color: var(--syntax-default-color);
       }
+      .gr-syntax-attribute {
+        color: var(--syntax-attribute-color);
+      }
+      .gr-syntax-function {
+        color: var(--syntax-function-color);
+      }
       .gr-syntax-meta {
         color: var(--syntax-meta-color);
       }
@@ -94,6 +100,9 @@
       .gr-syntax-template-tag {
         color: var(--syntax-template-tag-color);
       }
+      .gr-syntax-param {
+        color: var(--syntax-param-color);
+      }
     </style>
   </template>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 533136c..0301a13 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -42,6 +42,7 @@
     safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
   });
 </script>
+<script src="../bower_components/moment/moment.js"></script>
 
 <link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 799cf59..3c4270f 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -27,6 +27,7 @@
 <link rel="import" href="../../settings/gr-change-table-editor/gr-change-table-editor.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-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
@@ -194,6 +195,18 @@
               </gr-select>
             </span>
           </section>
+          <section hidden$="[[!_localPrefs.default_base_for_merges]]">
+            <span class="title">Default Base For Merges</span>
+            <span class="value">
+              <gr-select
+                  bind-value="{{_localPrefs.default_base_for_merges}}">
+                <select>
+                  <option value="AUTO_MERGE">Auto Merge</option>
+                  <option value="FIRST_PARENT">First Parent</option>
+                </select>
+              </gr-select>
+            </span>
+          </section>
           <section>
             <span class="title">Diff view</span>
             <span class="value">
@@ -259,110 +272,9 @@
           Diff Preferences
         </h2>
         <fieldset id="diffPreferences">
-          <section>
-            <span class="title">Context</span>
-            <span class="value">
-              <gr-select bind-value="{{_diffPrefs.context}}">
-                <select>
-                  <option value="3">3 lines</option>
-                  <option value="10">10 lines</option>
-                  <option value="25">25 lines</option>
-                  <option value="50">50 lines</option>
-                  <option value="75">75 lines</option>
-                  <option value="100">100 lines</option>
-                  <option value="-1">Whole file</option>
-                </select>
-              </gr-select>
-            </span>
-          </section>
-          <section>
-            <span class="title">Fit to screen</span>
-            <span class="value">
-              <input
-                  id="diffLineWrapping"
-                  type="checkbox"
-                  checked$="[[_diffPrefs.line_wrapping]]"
-                  on-change="_handleDiffLineWrappingChanged">
-            </span>
-          </section>
-          <section id="columnsPref" hidden$="[[_diffPrefs.line_wrapping]]">
-            <span class="title">Diff width</span>
-            <span class="value">
-              <input
-                  is="iron-input"
-                  type="number"
-                  prevent-invalid-input
-                  allowed-pattern="[0-9]"
-                  bind-value="{{_diffPrefs.line_length}}">
-            </span>
-          </section>
-          <section>
-            <span class="title">Tab width</span>
-            <span class="value">
-              <input
-                  is="iron-input"
-                  type="number"
-                  prevent-invalid-input
-                  allowed-pattern="[0-9]"
-                  bind-value="{{_diffPrefs.tab_size}}">
-            </span>
-          </section>
-          <section hidden$="[[!_diffPrefs.font_size]]">
-            <span class="title">Font size</span>
-            <span class="value">
-              <input
-                  is="iron-input"
-                  type="number"
-                  prevent-invalid-input
-                  allowed-pattern="[0-9]"
-                  bind-value="{{_diffPrefs.font_size}}">
-            </span>
-          </section>
-          <section>
-            <span class="title">Show tabs</span>
-            <span class="value">
-              <input
-                  id="diffShowTabs"
-                  type="checkbox"
-                  checked$="[[_diffPrefs.show_tabs]]"
-                  on-change="_handleDiffShowTabsChanged">
-            </span>
-          </section>
-          <section>
-            <span class="title">Show trailing whitespace</span>
-            <span class="value">
-              <input
-                  id="showTrailingWhitespace"
-                  type="checkbox"
-                  checked$="[[_diffPrefs.show_whitespace_errors]]"
-                  on-change="_handleShowTrailingWhitespaceChanged">
-            </span>
-          </section>
-          <section>
-            <span class="title">Syntax highlighting</span>
-            <span class="value">
-              <input
-                  id="diffSyntaxHighlighting"
-                  type="checkbox"
-                  checked$="[[_diffPrefs.syntax_highlighting]]"
-                  on-change="_handleDiffSyntaxHighlightingChanged">
-            </span>
-          </section>
-          <section>
-            <div class="pref">
-              <span class="title">Ignore Whitespace</span>
-              <span class="value">
-                <gr-select bind-value="{{_diffPrefs.ignore_whitespace}}">
-                  <select>
-                    <option value="IGNORE_NONE">None</option>
-                    <option value="IGNORE_TRAILING">Trailing</option>
-                    <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
-                    <option value="IGNORE_ALL">All</option>
-                  </select>
-                </gr-select>
-              </span>
-            </div>
-          </section>
+          <gr-diff-preferences
+              id="diffPrefs"
+              has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
           <gr-button
               id="saveDiffPrefs"
               on-tap="_handleSaveDiffPreferences"
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 0ce8ce0..916f97f 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
@@ -25,6 +25,7 @@
     'diff_view',
     'publish_comments_on_push',
     'work_in_progress_by_default',
+    'default_base_for_merges',
     'signed_off_by',
     'email_format',
     'size_bar_in_change_table',
@@ -64,8 +65,6 @@
       },
       _accountNameMutable: Boolean,
       _accountInfoChanged: Boolean,
-      /** @type {?} */
-      _diffPrefs: Object,
       _changeTableColumnsNotDisplayed: Array,
       /** @type {?} */
       _localPrefs: {
@@ -92,10 +91,8 @@
         type: Boolean,
         value: false,
       },
-      _diffPrefsChanged: {
-        type: Boolean,
-        value: false,
-      },
+      /** @type {?} */
+      _diffPrefsChanged: Boolean,
       /** @type {?} */
       _editPrefsChanged: Boolean,
       _menuChanged: {
@@ -149,7 +146,6 @@
 
     observers: [
       '_handlePrefsChanged(_localPrefs.*)',
-      '_handleDiffPrefsChanged(_diffPrefs.*)',
       '_handleMenuChanged(_localMenu.splices)',
       '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)',
     ],
@@ -166,6 +162,7 @@
         this.$.httpPass.loadData(),
         this.$.identities.loadData(),
         this.$.editPrefs.loadData(),
+        this.$.diffPrefs.loadData(),
       ];
 
       promises.push(this.$.restAPI.getPreferences().then(prefs => {
@@ -176,10 +173,6 @@
         this._cloneChangeTableColumns();
       }));
 
-      promises.push(this.$.restAPI.getDiffPreferences().then(prefs => {
-        this._diffPrefs = prefs;
-      }));
-
       promises.push(this.$.restAPI.getConfig().then(config => {
         this._serverConfig = config;
         const configPromises = [];
@@ -277,11 +270,6 @@
       this._prefsChanged = true;
     },
 
-    _handleDiffPrefsChanged() {
-      if (this._isLoading()) { return; }
-      this._diffPrefsChanged = true;
-    },
-
     _handleShowSizeBarsInFileListChanged() {
       this.set('_localPrefs.size_bar_in_change_table',
           this.$.showSizeBarsInFileList.checked);
@@ -318,24 +306,6 @@
       });
     },
 
-    _handleDiffLineWrappingChanged() {
-      this.set('_diffPrefs.line_wrapping', this.$.diffLineWrapping.checked);
-    },
-
-    _handleDiffShowTabsChanged() {
-      this.set('_diffPrefs.show_tabs', this.$.diffShowTabs.checked);
-    },
-
-    _handleShowTrailingWhitespaceChanged() {
-      this.set('_diffPrefs.show_whitespace_errors',
-          this.$.showTrailingWhitespace.checked);
-    },
-
-    _handleDiffSyntaxHighlightingChanged() {
-      this.set('_diffPrefs.syntax_highlighting',
-          this.$.diffSyntaxHighlighting.checked);
-    },
-
     _handleSaveChangeTable() {
       this.set('prefs.change_table', this._localChangeTableColumns);
       this.set('prefs.legacycid_in_change_table', this._showNumber);
@@ -346,10 +316,7 @@
     },
 
     _handleSaveDiffPreferences() {
-      return this.$.restAPI.saveDiffPreferences(this._diffPrefs)
-          .then(() => {
-            this._diffPrefsChanged = false;
-          });
+      this.$.diffPrefs.save();
     },
 
     _handleSaveEditPreferences() {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index f47816f..b0ef704 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -43,7 +43,6 @@
     let element;
     let account;
     let preferences;
-    let diffPreferences;
     let config;
     let sandbox;
 
@@ -88,6 +87,7 @@
         diff_view: 'UNIFIED_DIFF',
         email_strategy: 'ENABLED',
         email_format: 'HTML_PLAINTEXT',
+        default_base_for_merges: 'FIRST_PARENT',
         size_bar_in_change_table: true,
 
         my: [
@@ -96,31 +96,12 @@
         ],
         change_table: [],
       };
-      diffPreferences = {
-        context: 10,
-        tab_size: 8,
-        font_size: 12,
-        line_length: 100,
-        cursor_blink_rate: 0,
-        line_wrapping: false,
-        intraline_difference: true,
-        show_line_endings: true,
-        show_tabs: true,
-        show_whitespace_errors: true,
-        syntax_highlighting: true,
-        auto_hide_diff_table_header: true,
-        theme: 'DEFAULT',
-        ignore_whitespace: 'IGNORE_NONE',
-      };
       config = {auth: {editable_account_fields: []}};
 
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(true); },
         getAccount() { return Promise.resolve(account); },
         getPreferences() { return Promise.resolve(preferences); },
-        getDiffPreferences() {
-          return Promise.resolve(diffPreferences);
-        },
         getWatchedProjects() {
           return Promise.resolve([]);
         },
@@ -168,6 +149,8 @@
           .firstElementChild.bindValue, preferences.email_strategy);
       assert.equal(valueOf('Email format', 'preferences')
           .firstElementChild.bindValue, preferences.email_format);
+      assert.equal(valueOf('Default Base For Merges', 'preferences')
+          .firstElementChild.bindValue, preferences.default_base_for_merges);
       assert.equal(valueOf('Diff view', 'preferences')
           .firstElementChild.bindValue, preferences.diff_view);
       assert.equal(valueOf('Show size bars in file list', 'preferences')
@@ -261,56 +244,6 @@
       });
     });
 
-    test('diff preferences', done => {
-      // Rendered with the expected preferences selected.
-      assert.equal(valueOf('Context', 'diffPreferences')
-          .firstElementChild.bindValue, diffPreferences.context);
-      assert.equal(valueOf('Diff width', 'diffPreferences')
-          .firstElementChild.bindValue, diffPreferences.line_length);
-      assert.equal(valueOf('Tab width', 'diffPreferences')
-          .firstElementChild.bindValue, diffPreferences.tab_size);
-      assert.equal(valueOf('Font size', 'diffPreferences')
-          .firstElementChild.bindValue, diffPreferences.font_size);
-      assert.equal(valueOf('Show tabs', 'diffPreferences')
-          .firstElementChild.checked, diffPreferences.show_tabs);
-      assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
-          .firstElementChild.checked, diffPreferences.show_whitespace_errors);
-      assert.equal(valueOf('Fit to screen', 'diffPreferences')
-          .firstElementChild.checked, diffPreferences.line_wrapping);
-
-      assert.isFalse(element._diffPrefsChanged);
-
-      const showTabsCheckbox = valueOf('Show tabs', 'diffPreferences')
-          .firstElementChild;
-      showTabsCheckbox.checked = false;
-      element._handleDiffShowTabsChanged();
-
-      assert.isTrue(element._diffPrefsChanged);
-
-      stub('gr-rest-api-interface', {
-        saveDiffPreferences(prefs) {
-          assert.equal(prefs.show_tabs, false);
-          return Promise.resolve();
-        },
-      });
-
-      // Save the change.
-      element._handleSaveDiffPreferences().then(() => {
-        assert.isFalse(element._diffPrefsChanged);
-        done();
-      });
-    });
-
-    test('columns input is hidden with fit to scsreen is selected', () => {
-      assert.isFalse(element.$.columnsPref.hidden);
-
-      MockInteractions.tap(element.$.diffLineWrapping);
-      assert.isTrue(element.$.columnsPref.hidden);
-
-      MockInteractions.tap(element.$.diffLineWrapping);
-      assert.isFalse(element.$.columnsPref.hidden);
-    });
-
     test('menu', done => {
       assert.isFalse(element._menuChanged);
       assert.isFalse(element._prefsChanged);
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 f32e940b..bf56382 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -41,25 +41,30 @@
 
     attached() {
       Promise.all([
-        this.$.restAPI.getConfig(),
+        this._getConfig(),
         Gerrit.awaitPluginsLoaded(),
       ]).then(([cfg]) => {
         this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
-        if (this._hasAvatars && this.account) {
-          // src needs to be set if avatar becomes visible
-          this._updateAvatarURL();
-        } else {
-          this.hidden = true;
-        }
+
+        this._updateAvatarURL();
       });
     },
 
+    _getConfig() {
+      return this.$.restAPI.getConfig();
+    },
+
     _accountChanged(account) {
       this._updateAvatarURL();
     },
 
     _updateAvatarURL() {
-      if (this.hidden || !this._hasAvatars) { return; }
+      if (!this._hasAvatars || !this.account) {
+        this.hidden = true;
+        return;
+      }
+      this.hidden = false;
+
       const url = this._buildAvatarURL(this.account);
       if (url) {
         this.style.backgroundImage = 'url("' + url + '")';
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index f137c7f..5ce17c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -35,14 +35,17 @@
 <script>
   suite('gr-avatar tests', () => {
     let element;
+    let sandbox;
 
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getConfig() { return Promise.resolve({plugin: {has_avatars: true}}); },
-      });
+      sandbox = sinon.sandbox.create();
       element = fixture('basic');
     });
 
+    teardown(() => {
+      sandbox.restore();
+    });
+
     test('methods', () => {
       assert.equal(element._buildAvatarURL(
           {
@@ -94,22 +97,32 @@
             ],
           }),
           '/accounts/123/avatar?s=16');
+      assert.equal(element._buildAvatarURL(undefined), '');
     });
 
     test('dom for existing account', () => {
       assert.isFalse(element.hasAttribute('hidden'));
+
+      sandbox.stub(element, '_getConfig', () => {
+        return Promise.resolve({plugin: {has_avatars: true}});
+      });
+
       element.imageSize = 64;
       element.account = {
         _account_id: 123,
       };
+
       assert.strictEqual(element.style.backgroundImage, '');
+
       // Emulate plugins loaded.
       Gerrit._setPluginsPending([]);
-      return Promise.all([
+
+      Promise.all([
         element.$.restAPI.getConfig(),
         Gerrit.awaitPluginsLoaded(),
       ]).then(() => {
         assert.isFalse(element.hasAttribute('hidden'));
+
         assert.isTrue(
             element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
       });
@@ -117,10 +130,57 @@
 
     test('dom for non available account', () => {
       assert.isFalse(element.hasAttribute('hidden'));
-      element.account = null;
-      assert.isFalse(element.hasAttribute('hidden'));
+
+      sandbox.stub(element, '_getConfig', () => {
+        return Promise.resolve({plugin: {has_avatars: true}});
+      });
+
       // Emulate plugins loaded.
       Gerrit._setPluginsPending([]);
+
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        Gerrit.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isTrue(element.hasAttribute('hidden'));
+
+        assert.strictEqual(element.style.backgroundImage, '');
+      });
+    });
+
+    test('avatar config not set and account not set', () => {
+      assert.isFalse(element.hasAttribute('hidden'));
+
+      sandbox.stub(element, '_getConfig', () => {
+        return Promise.resolve({});
+      });
+
+      // Emulate plugins loaded.
+      Gerrit._setPluginsPending([]);
+
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        Gerrit.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isTrue(element.hasAttribute('hidden'));
+      });
+    });
+
+    test('avatar config not set and account set', () => {
+      assert.isFalse(element.hasAttribute('hidden'));
+
+      sandbox.stub(element, '_getConfig', () => {
+        return Promise.resolve({});
+      });
+
+      element.imageSize = 64;
+      element.account = {
+        _account_id: 123,
+      };
+
+      // Emulate plugins loaded.
+      Gerrit._setPluginsPending([]);
+
       return Promise.all([
         element.$.restAPI.getConfig(),
         Gerrit.awaitPluginsLoaded(),
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index 1090fea..481dd2f 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -20,7 +20,6 @@
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
-<script src="../../../bower_components/moment/moment.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <dom-module id="gr-date-formatter">
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
new file mode 100644
index 0000000..d7bc704
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
@@ -0,0 +1,163 @@
+<!--
+@license
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-select/gr-select.html">
+
+<dom-module id="gr-diff-preferences">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles"></style>
+    <div id="diffPreferences" class="gr-form-styles">
+      <section>
+        <span class="title">Context</span>
+        <span class="value">
+          <gr-select
+              id="contextSelect"
+              bind-value="{{diffPrefs.context}}">
+            <select
+                on-keypress="_handleDiffPrefsChanged"
+                on-change="_handleDiffPrefsChanged">
+              <option value="3">3 lines</option>
+              <option value="10">10 lines</option>
+              <option value="25">25 lines</option>
+              <option value="50">50 lines</option>
+              <option value="75">75 lines</option>
+              <option value="100">100 lines</option>
+              <option value="-1">Whole file</option>
+            </select>
+          </gr-select>
+        </span>
+      </section>
+      <section>
+        <span class="title">Fit to screen</span>
+        <span class="value">
+          <input
+              id="lineWrappingInput"
+              type="checkbox"
+              checked$="[[diffPrefs.line_wrapping]]"
+              on-change="_handleLineWrappingTap">
+        </span>
+      </section>
+      <section>
+        <span class="title">Diff width</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              id="columnsInput"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{diffPrefs.line_length}}"
+              on-keypress="_handleDiffPrefsChanged"
+              on-change="_handleDiffPrefsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Tab width</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              id="tabSizeInput"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{diffPrefs.tab_size}}"
+              on-keypress="_handleDiffPrefsChanged"
+              on-change="_handleDiffPrefsChanged">
+        </span>
+      </section>
+      <section hidden$="[[!diffPrefs.font_size]]">
+        <span class="title">Font size</span>
+        <span class="value">
+          <input
+              is="iron-input"
+              type="number"
+              id="fontSizeInput"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{diffPrefs.font_size}}"
+              on-keypress="_handleDiffPrefsChanged"
+              on-change="_handleDiffPrefsChanged">
+        </span>
+      </section>
+      <section>
+        <span class="title">Show tabs</span>
+        <span class="value">
+          <input
+              id="showTabsInput"
+              type="checkbox"
+              checked$="[[diffPrefs.show_tabs]]"
+              on-change="_handleShowTabsTap">
+        </span>
+      </section>
+      <section>
+        <span class="title">Show trailing whitespace</span>
+        <span class="value">
+          <input
+              id="showTrailingWhitespaceInput"
+              type="checkbox"
+              checked$="[[diffPrefs.show_whitespace_errors]]"
+              on-change="_handleShowTrailingWhitespaceTap">
+        </span>
+      </section>
+      <section>
+        <span class="title">Syntax highlighting</span>
+        <span class="value">
+          <input
+              id="syntaxHighlightInput"
+              type="checkbox"
+              checked$="[[diffPrefs.syntax_highlighting]]"
+              on-change="_handleSyntaxHighlightTap">
+        </span>
+      </section>
+      <section>
+        <span class="title">Automatically mark viewed files reviewed</span>
+        <span class="value">
+          <input
+              id="automaticReviewInput"
+              type="checkbox"
+              checked$="[[diffPrefs.manual_review]]"
+              on-change="_handleAutomaticReviewTap">
+        </span>
+      </section>
+      <section>
+        <div class="pref">
+          <span class="title">Ignore Whitespace</span>
+          <span class="value">
+            <gr-select bind-value="{{diffPrefs.ignore_whitespace}}">
+              <select
+                  on-keypress="_handleDiffPrefsChanged"
+                  on-change="_handleDiffPrefsChanged">
+                <option value="IGNORE_NONE">None</option>
+                <option value="IGNORE_TRAILING">Trailing</option>
+                <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
+                <option value="IGNORE_ALL">All</option>
+              </select>
+            </gr-select>
+          </span>
+        </div>
+      </section>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-diff-preferences.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..e2278c2
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -0,0 +1,78 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff-preferences',
+
+    properties: {
+      hasUnsavedChanges: {
+        type: Boolean,
+        notify: true,
+        value: false,
+      },
+
+      /** @type {?} */
+      diffPrefs: Object,
+    },
+
+    loadData() {
+      return this.$.restAPI.getDiffPreferences().then(prefs => {
+        this.diffPrefs = prefs;
+      });
+    },
+
+    _handleDiffPrefsChanged() {
+      this.hasUnsavedChanges = true;
+    },
+
+    _handleLineWrappingTap() {
+      this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    _handleShowTabsTap() {
+      this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    _handleShowTrailingWhitespaceTap() {
+      this.set('diffPrefs.show_whitespace_errors',
+          this.$.showTrailingWhitespaceInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    _handleSyntaxHighlightTap() {
+      this.set('diffPrefs.syntax_highlighting',
+          this.$.syntaxHighlightInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    _handleAutomaticReviewTap() {
+      this.set('diffPrefs.manual_review',
+          this.$.automaticReviewInput.checked);
+      this._handleDiffPrefsChanged();
+    },
+
+    save() {
+      return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => {
+        this.hasUnsavedChanges = false;
+      });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
new file mode 100644
index 0000000..511737f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff-preferences</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-diff-preferences.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-diff-preferences></gr-diff-preferences>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-diff-preferences tests', () => {
+    let element;
+    let sandbox;
+    let diffPreferences;
+
+    function valueOf(title, fieldsetid) {
+      const sections = element.$[fieldsetid].querySelectorAll('section');
+      let titleEl;
+      for (let i = 0; i < sections.length; i++) {
+        titleEl = sections[i].querySelector('.title');
+        if (titleEl.textContent.trim() === title) {
+          return sections[i].querySelector('.value');
+        }
+      }
+    }
+
+    setup(() => {
+      diffPreferences = {
+        context: 10,
+        line_wrapping: false,
+        line_length: 100,
+        tab_size: 8,
+        font_size: 12,
+        show_tabs: true,
+        show_whitespace_errors: true,
+        syntax_highlighting: true,
+        manual_review: false,
+        ignore_whitespace: 'IGNORE_NONE',
+      };
+
+      stub('gr-rest-api-interface', {
+        getDiffPreferences() {
+          return Promise.resolve(diffPreferences);
+        },
+      });
+
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+      return element.loadData();
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('renders', () => {
+      // Rendered with the expected preferences selected.
+      assert.equal(valueOf('Context', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.context);
+      assert.equal(valueOf('Fit to screen', 'diffPreferences')
+          .firstElementChild.checked, diffPreferences.line_wrapping);
+      assert.equal(valueOf('Diff width', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.line_length);
+      assert.equal(valueOf('Tab width', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.tab_size);
+      assert.equal(valueOf('Font size', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.font_size);
+      assert.equal(valueOf('Show tabs', 'diffPreferences')
+          .firstElementChild.checked, diffPreferences.show_tabs);
+      assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
+          .firstElementChild.checked, diffPreferences.show_whitespace_errors);
+      assert.equal(valueOf('Syntax highlighting', 'diffPreferences')
+          .firstElementChild.checked, diffPreferences.syntax_highlighting);
+      assert.equal(
+          valueOf('Automatically mark viewed files reviewed', 'diffPreferences')
+              .firstElementChild.checked, diffPreferences.manual_review);
+      assert.equal(valueOf('Ignore Whitespace', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.ignore_whitespace);
+
+      assert.isFalse(element.hasUnsavedChanges);
+    });
+
+    test('save changes', () => {
+      sandbox.stub(element.$.restAPI, 'saveDiffPreferences')
+          .returns(Promise.resolve());
+      const showTrailingWhitespaceCheckbox =
+          valueOf('Show trailing whitespace', 'diffPreferences')
+          .firstElementChild;
+      showTrailingWhitespaceCheckbox.checked = false;
+      element._handleShowTrailingWhitespaceTap();
+
+      assert.isTrue(element.hasUnsavedChanges);
+
+      // Save the change.
+      return element.save().then(() => {
+        assert.isFalse(element.hasUnsavedChanges);
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index fba3e97..ec445ae 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -109,6 +109,8 @@
   --tooltip-text-color: #fff;
 
   --syntax-default-color: var(--primary-text-color);
+  --syntax-attribute-color: var(--primary-text-color);
+  --syntax-function-color: #e90;
   --syntax-meta-color: #FF1717;
   --syntax-keyword-color: #9E0069;
   --syntax-number-color: #164;
@@ -130,6 +132,7 @@
   --syntax-regexp-color: #FA8602;
   --syntax-selector-attr-color: #FA8602;
   --syntax-template-tag-color: #FA8602;
+  --syntax-param-color: var(--primary-text-color);
 }
 @media screen and (max-width: 50em) {
   :root {
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index 92b99e3..c5979fa 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -60,3 +60,4 @@
 <link rel="import"
     href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
 <link rel="import" href="test-router.html" />
+<script src="../bower_components/moment/moment.js"></script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 3b91650..d9e1238 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -71,6 +71,7 @@
     'change/gr-commit-info/gr-commit-info_test.html',
     'change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html',
     'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
+    'change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html',
     'change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html',
     'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
     'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
@@ -107,7 +108,6 @@
     'diff/gr-diff-highlight/gr-annotation_test.html',
     'diff/gr-diff-highlight/gr-diff-highlight_test.html',
     'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html',
-    'diff/gr-diff-preferences/gr-diff-preferences_test.html',
     'diff/gr-diff-processor/gr-diff-processor_test.html',
     'diff/gr-diff-selection/gr-diff-selection_test.html',
     'diff/gr-diff-view/gr-diff-view_test.html',
@@ -161,6 +161,7 @@
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-dialog/gr-dialog_test.html',
+    'shared/gr-diff-preferences/gr-diff-preferences_test.html',
     'shared/gr-download-commands/gr-download-commands_test.html',
     'shared/gr-dropdown-list/gr-dropdown-list_test.html',
     'shared/gr-editable-content/gr-editable-content_test.html',
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 9801b44..e4f699c 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -16,16 +16,14 @@
 
 {namespace com.google.gerrit.httpd.raw}
 
-/**
- * @param canonicalPath
- * @param staticResourcePath
- * @param? assetsPath {string} URL to static assets root, if served from CDN.
- * @param? assetsBundle {string} Assets bundle .html file, served from $assetsPath.
- * @param? faviconPath
- * @param? versionInfo
- * @param? deprecateGwtUi
- */
 {template .Index}
+  {@param canonicalPath: ?}
+  {@param staticResourcePath: ?}
+  {@param? assetsPath: ?}  /** {string} URL to static assets root, if served from CDN. */
+  {@param? assetsBundle: ?}  /** {string} Assets bundle .html file, served from $assetsPath. */
+  {@param? faviconPath: ?}
+  {@param? versionInfo: ?}
+  {@param? deprecateGwtUi: ?}
   <!DOCTYPE html>{\n}
   <html lang="en">{\n}
   <meta charset="utf-8">{\n}