Merge "Add documentation for file edits"
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 1ab67d8..e8c5213 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -90,7 +90,7 @@
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
 created.
 
-== Change Deleted
+=== Change Deleted
 
 Sent when a change has been deleted.
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index f51fdd1..aa1de1d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1240,6 +1240,23 @@
 +
 By default 1000.
 
+[[change.move]]change.move::
++
+Whether the link:rest-api-changes.html#move-change[Move Change] REST
+endpoint is enabled.
++
+The move change functionality has some corner cases with undesired side
+effects. Hence administrators may decide to disable this functionality.
+In particular, if a change that has dependencies on other changes is
+moved to a new branch, and the moved change gets submitted to the new
+branch, the changes on which the change depends are silently merged
+into the new branch, although these changes have not been moved to that
+branch (see details in
+link:https://bugs.chromium.org/p/gerrit/issues/detail?id=9877[issue
+9877]).
++
+By default true.
+
 [[change.replyLabel]]change.replyLabel::
 +
 Label name for the reply button. In the user interface an ellipsis (…)
@@ -3044,6 +3061,31 @@
 +
 Not set by default.
 
+[[event]]
+=== Section event
+
+[[event.payload.listChangeOptions]]events.payload.listChangeOptions::
++
+List of options that Gerrit applies when rendering the payload of an
+internal event. This is the same set of options that are documented
+link:rest-api-changes.html#query-options[here].
++
+Depending on the setup, these events might get serialized using stream
+events.
++
+This can be set to the set of minimal options that consumers of Gerrit's
+events need. A minimal set would be (`SKIP_MERGEABLE`,`SKIP_DIFFSTAT`).
++
+Every option that gets added here will have a performance impact. The
+general recommendation is therefore to set this to a minimal set of
+required options.
++
+Defaults to all available options minus `CHANGE_ACTIONS`,
+`CURRENT_ACTIONS` and `CHECK`. This is a rich default to make sure the
+config is backwards compatible with what the default was before the config
+was added.
+
+
 [[ldap]]
 === Section ldap
 
diff --git a/Documentation/dev-design-docs.txt b/Documentation/dev-design-docs.txt
index ac53bf8..5e3f7a9 100644
--- a/Documentation/dev-design-docs.txt
+++ b/Documentation/dev-design-docs.txt
@@ -85,6 +85,13 @@
 but in this case the implementation is only done if someone volunteers
 to do it (which is not guaranteed to happen).
 
+Only very few maintainers actively watch out for uploaded design docs.
+To raise awareness you may want to send a notification to the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+mailing list about your uploaded design doc. But the discussion should
+not take place on the mailing list, comments should be made by reviewing
+the change in Gerrit.
+
 [[review]]
 == Design doc review
 
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 258ded2..030541d 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -459,35 +459,6 @@
 });
 ----
 
-[[Gerrit_getCurrentUser]]
-=== Gerrit.getCurrentUser()
-Returns the currently signed in user's AccountInfo data; empty account
-data if no user is currently signed in.
-
-[[Gerrit_getPluginName]]
-=== Gerrit.getPluginName()
-Returns the name this plugin was installed as by the server
-administrator. The plugin name is required to access REST API
-views installed by the plugin, or to access resources.
-
-Unlike link:#self_getPluginName[`self.getPluginName()`] this method
-must guess the name from the JavaScript call stack. Plugins are
-encouraged to use `self.getPluginName()` whenever possible.
-
-[[Gerrit_go]]
-=== Gerrit.go()
-Updates the web UI to display the screen identified by the supplied
-URL token. The URL token is the text after `#` in the browser URL.
-
-[source,javascript]
-----
-Gerrit.go('/admin/projects/');
-----
-
-If the URL passed matches `http://...`, `https://...`, or `//...`
-the current browser window will navigate to the non-Gerrit URL.
-The user can return to Gerrit with the back button.
-
 [[Gerrit_install]]
 === Gerrit.install()
 Registers a new plugin by invoking the supplied initialization
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 075ea8f..6b8281a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5957,9 +5957,13 @@
 created change. If set, it must be a merged commit on the destination branch.
 Mutually exclusive with `base_change`.
 |`new_branch`         |optional, default to `false`|
-Allow creating a new branch when set to `true`.
+Allow creating a new branch when set to `true`. Using this option is
+only possible for non-merge commits (if the `merge` field is not set).
 |`merge`              |optional|
 The detail of a merge commit as a link:#merge-input[MergeInput] entity.
+If set, the target branch (see  `branch` field) must exist (it is not
+possible to create it automatically by setting the `new_branch` field
+to `true`.
 |`notify`             |optional|
 Notify handling that defines to whom email notifications should be sent
 after the change is created. +
@@ -6926,6 +6930,9 @@
 |`notify_details`|optional|
 Additional information about whom to notify about the revert as a map
 of recipient type to link:#notify-info[NotifyInfo] entity.
+|`topic`         |optional|
+Name of the topic for the revert change. If not set, the default is the topic
+of the change being reverted.
 |=============================
 
 [[review-info]]
diff --git a/WORKSPACE b/WORKSPACE
index 2801f5f..7b843b6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -32,9 +32,9 @@
 
 http_archive(
     name = "io_bazel_rules_closure",
-    sha256 = "39b7bec43e6178d065875987b18623d476acd54f355d7711ce9dce4a3eec0795",
-    strip_prefix = "rules_closure-0.25",
-    urls = ["https://github.com/davido/rules_closure/archive/v0.25.tar.gz"],
+    sha256 = "eecd37c0eec79e12652c70f2d2e120623cba64616d759ddcedb19e614df618fa",
+    strip_prefix = "rules_closure-d53c0d7755426349d3c443eea4aeedbda27a11be",
+    urls = ["https://github.com/bazelbuild/rules_closure/archive/d53c0d7755426349d3c443eea4aeedbda27a11be.tar.gz"],
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -56,19 +56,21 @@
 
 check_bazel_version()
 
-load("@io_bazel_rules_closure//closure:repositories.bzl", "closure_repositories")
+load("@io_bazel_rules_closure//closure:repositories.bzl", "rules_closure_dependencies", "rules_closure_toolchains")
 
 # Prevent redundant loading of dependencies.
 # TODO(davido): Omit re-fetching ancient args4j version when these PRs are merged:
 # https://github.com/bazelbuild/rules_closure/pull/262
 # https://github.com/google/closure-templates/pull/155
-closure_repositories(
+rules_closure_dependencies(
     omit_aopalliance = True,
     omit_bazel_skylib = True,
     omit_javax_inject = True,
     omit_rules_cc = True,
 )
 
+rules_closure_toolchains()
+
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
@@ -910,12 +912,6 @@
     sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
 )
 
-maven_jar(
-    name = "cglib-3_2",
-    artifact = "cglib:cglib-nodep:3.2.6",
-    sha1 = "92bf48723d277d6efd1150b2f7e9e1e92cb56caf",
-)
-
 JETTY_VERS = "9.4.18.v20190429"
 
 maven_jar(
@@ -1007,8 +1003,8 @@
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.3.2",
-    sha1 = "38721e908cad8a30fa3f8e659c0571150a60cab3",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.4.0",
+    sha1 = "481fedd31088ec6ba79a2aeffec3eccae4c0772b",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index baf3e38..309ee3e5 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -29,7 +29,8 @@
   V7_0("7.0.*"),
   V7_1("7.1.*"),
   V7_2("7.2.*"),
-  V7_3("7.3.*");
+  V7_3("7.3.*"),
+  V7_4("7.4.*");
 
   private final String version;
   private final Pattern pattern;
diff --git a/java/com/google/gerrit/extensions/api/changes/RevertInput.java b/java/com/google/gerrit/extensions/api/changes/RevertInput.java
index c1be9b0..c4272e4 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevertInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevertInput.java
@@ -24,4 +24,6 @@
   public NotifyHandling notify = NotifyHandling.ALL;
 
   public Map<RecipientType, NotifyInfo> notifyDetails;
+
+  public String topic;
 }
diff --git a/java/com/google/gerrit/extensions/events/ChangeEvent.java b/java/com/google/gerrit/extensions/events/ChangeEvent.java
index 0b51052..a0bdd07 100644
--- a/java/com/google/gerrit/extensions/events/ChangeEvent.java
+++ b/java/com/google/gerrit/extensions/events/ChangeEvent.java
@@ -20,6 +20,10 @@
 
 /** Interface to be extended by Events with a Change. */
 public interface ChangeEvent extends GerritEvent {
+  /**
+   * Information about the change. Some fields might be null. {@see
+   * com.google.gerrit.server.extensions.events.EventUtil}.
+   */
   ChangeInfo getChange();
 
   AccountInfo getWho();
diff --git a/java/com/google/gerrit/extensions/events/RevisionEvent.java b/java/com/google/gerrit/extensions/events/RevisionEvent.java
index f0cfa2c..7f824ac 100644
--- a/java/com/google/gerrit/extensions/events/RevisionEvent.java
+++ b/java/com/google/gerrit/extensions/events/RevisionEvent.java
@@ -18,5 +18,10 @@
 
 /** Interface to be extended by Events with a Revision. */
 public interface RevisionEvent extends ChangeEvent {
+
+  /**
+   * Information about the revision. Some fields might be null. {@see
+   * com.google.gerrit.server.extensions.events.EventUtil}.
+   */
   RevisionInfo getRevision();
 }
diff --git a/java/com/google/gerrit/server/ApprovalInference.java b/java/com/google/gerrit/server/ApprovalInference.java
index 808bb0f..f5cc956 100644
--- a/java/com/google/gerrit/server/ApprovalInference.java
+++ b/java/com/google/gerrit/server/ApprovalInference.java
@@ -29,6 +29,9 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.LabelNormalizer;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
@@ -37,6 +40,7 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Map;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevWalk;
 
@@ -69,7 +73,13 @@
   Iterable<PatchSetApproval> forPatchSet(
       ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
     ProjectState project;
-    try {
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Computing labels for patch set",
+            Metadata.builder()
+                .changeId(notes.load().getChangeId().get())
+                .patchSetId(psId.get())
+                .build())) {
       project = projectCache.checkedGet(notes.getProjectName());
       Collection<PatchSetApproval> approvals =
           getForPatchSetWithoutNormalization(notes, project, psId, rw, repoConfig);
@@ -144,13 +154,14 @@
     // set at a time if configs and change kind allow so. Once an approval is held back - for
     // example because the patch set is a REWORK - it will not be picked up again in a future
     // patch set.
-    PatchSet priorPatchSet = notes.load().getPatchSets().lowerEntry(psId).getValue();
+    Map.Entry<PatchSet.Id, PatchSet> priorPatchSet = notes.load().getPatchSets().lowerEntry(psId);
     if (priorPatchSet == null) {
       return resultByUser.values();
     }
 
     Iterable<PatchSetApproval> priorApprovals =
-        getForPatchSetWithoutNormalization(notes, project, priorPatchSet.id(), rw, repoConfig);
+        getForPatchSetWithoutNormalization(
+            notes, project, priorPatchSet.getValue().id(), rw, repoConfig);
     if (!priorApprovals.iterator().hasNext()) {
       return resultByUser.values();
     }
@@ -159,7 +170,11 @@
     // and settings as well as change kind allow copying.
     ChangeKind kind =
         changeKindCache.getChangeKind(
-            project.getNameKey(), rw, repoConfig, priorPatchSet.commitId(), ps.commitId());
+            project.getNameKey(),
+            rw,
+            repoConfig,
+            priorPatchSet.getValue().commitId(),
+            ps.commitId());
     for (PatchSetApproval psa : priorApprovals) {
       if (resultByUser.contains(psa.label(), psa.accountId())) {
         continue;
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 4566919..2116f87 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -36,6 +36,9 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -209,12 +212,20 @@
 
   @Override
   public String getETag() {
-    Hasher h = Hashing.murmur3_128().newHasher();
-    if (user.isIdentifiedUser()) {
-      h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
+    try (TraceTimer ignored =
+        TraceContext.newTimer(
+            "Compute change ETag",
+            Metadata.builder()
+                .changeId(notes.getChangeId().get())
+                .projectName(notes.getProjectName().get())
+                .build())) {
+      Hasher h = Hashing.murmur3_128().newHasher();
+      if (user.isIdentifiedUser()) {
+        h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
+      }
+      prepareETag(h, user);
+      return h.hash().toString();
     }
-    prepareETag(h, user);
-    return h.hash().toString();
   }
 
   private void hashObjectId(Hasher h, ObjectId id, byte[] buf) {
diff --git a/java/com/google/gerrit/server/change/RevisionResource.java b/java/com/google/gerrit/server/change/RevisionResource.java
index efd9d2d..d47ca5d 100644
--- a/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/java/com/google/gerrit/server/change/RevisionResource.java
@@ -25,6 +25,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.inject.TypeLiteral;
@@ -89,9 +92,18 @@
 
   @Override
   public String getETag() {
-    Hasher h = Hashing.murmur3_128().newHasher();
-    prepareETag(h, getUser());
-    return h.hash().toString();
+    try (TraceTimer ignored =
+        TraceContext.newTimer(
+            "Compute revision ETag",
+            Metadata.builder()
+                .changeId(change.getId().get())
+                .patchSetId(ps.number())
+                .projectName(change.getProject().get())
+                .build())) {
+      Hasher h = Hashing.murmur3_128().newHasher();
+      prepareETag(h, getUser());
+      return h.hash().toString();
+    }
   }
 
   public void prepareETag(Hasher h, CurrentUser user) {
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 1c4cf4f..933c71a 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -29,6 +30,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.RevisionJson;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -39,42 +41,51 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
+import org.eclipse.jgit.lib.Config;
 
+/**
+ * Formats change and revision info objects to serve as payload for Gerrit events.
+ *
+ * <p>Uses configurable options ({@code event.payload.listChangeOptions}) to decide which fields to
+ * populate.
+ */
 @Singleton
 public class EventUtil {
-  private static final ImmutableSet<ListChangesOption> CHANGE_OPTIONS;
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final ImmutableSet<ListChangesOption> DEFAULT_CHANGE_OPTIONS;
 
   static {
     EnumSet<ListChangesOption> opts = EnumSet.allOf(ListChangesOption.class);
-
     // Some options, like actions, are expensive to compute because they potentially have to walk
     // lots of history and inspect lots of other changes.
     opts.remove(ListChangesOption.CHANGE_ACTIONS);
     opts.remove(ListChangesOption.CURRENT_ACTIONS);
-
     // CHECK suppresses some exceptions on corrupt changes, which is not appropriate for passing
     // through the event system as we would rather let them propagate.
     opts.remove(ListChangesOption.CHECK);
-
-    CHANGE_OPTIONS = Sets.immutableEnumSet(opts);
+    DEFAULT_CHANGE_OPTIONS = Sets.immutableEnumSet(opts);
   }
 
   private final ChangeData.Factory changeDataFactory;
   private final ChangeJson.Factory changeJsonFactory;
   private final RevisionJson.Factory revisionJsonFactory;
+  private final ImmutableSet<ListChangesOption> changeOptions;
 
   @Inject
   EventUtil(
       ChangeJson.Factory changeJsonFactory,
       RevisionJson.Factory revisionJsonFactory,
-      ChangeData.Factory changeDataFactory) {
+      ChangeData.Factory changeDataFactory,
+      @GerritServerConfig Config gerritConfig) {
     this.changeDataFactory = changeDataFactory;
     this.changeJsonFactory = changeJsonFactory;
     this.revisionJsonFactory = revisionJsonFactory;
+    this.changeOptions = parseChangeListOptions(gerritConfig);
   }
 
   public ChangeInfo changeInfo(Change change) {
-    return changeJsonFactory.create(CHANGE_OPTIONS).format(change);
+    return changeJsonFactory.create(changeOptions).format(change);
   }
 
   public RevisionInfo revisionInfo(Project project, PatchSet ps)
@@ -85,7 +96,7 @@
   public RevisionInfo revisionInfo(Project.NameKey project, PatchSet ps)
       throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     ChangeData cd = changeDataFactory.create(project, ps.id().changeId());
-    return revisionJsonFactory.create(CHANGE_OPTIONS).getRevisionInfo(cd, ps);
+    return revisionJsonFactory.create(changeOptions).getRevisionInfo(cd, ps);
   }
 
   public AccountInfo accountInfo(AccountState accountState) {
@@ -110,4 +121,21 @@
     }
     return result;
   }
+
+  private static ImmutableSet<ListChangesOption> parseChangeListOptions(Config gerritConfig) {
+    String[] config = gerritConfig.getStringList("event", "payload", "listChangeOptions");
+    if (config.length == 0) {
+      return DEFAULT_CHANGE_OPTIONS;
+    }
+
+    ImmutableSet.Builder<ListChangesOption> result = ImmutableSet.builder();
+    for (String c : config) {
+      try {
+        result.add(ListChangesOption.valueOf(c));
+      } catch (IllegalArgumentException e) {
+        logger.atWarning().withCause(e).log("could not parse list change option %s", c);
+      }
+    }
+    return result.build();
+  }
 }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index c05ef0c..e991318 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -194,8 +194,10 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.stream.Collectors;
@@ -358,7 +360,7 @@
 
   // Collections populated during processing.
   private final List<UpdateGroupsRequest> updateGroups;
-  private final List<ValidationMessage> messages;
+  private final Queue<ValidationMessage> messages;
   /** Multimap of error text to refnames that produced that error. */
   private final ListMultimap<String, String> errors;
 
@@ -485,7 +487,7 @@
 
     // Collections populated during processing.
     errors = MultimapBuilder.linkedHashKeys().arrayListValues().build();
-    messages = new ArrayList<>();
+    messages = new ConcurrentLinkedQueue<>();
     pushOptions = LinkedListMultimap.create();
     replaceByChange = new LinkedHashMap<>();
     updateGroups = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 65237ac..295dffa 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -260,7 +260,6 @@
       Account.Id accountId;
       AccountGroup.UUID accountGroupUuid;
       if (name.startsWith(REFS_CACHE_AUTOMERGE)) {
-        logger.atFinest().log("Filter out ref %s", name);
         continue;
       } else if (opts.filterMeta() && isMetadata(name)) {
         logger.atFinest().log("Filter out metadata ref %s", name);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 564f9e7..4d0b3f1 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -84,6 +84,9 @@
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.NoMergeBaseException;
+import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -275,7 +278,7 @@
     try {
       permissionBackend.currentUser().project(project).ref(refName).check(RefPermission.READ);
     } catch (AuthException e) {
-      throw new ResourceNotFoundException(String.format("ref %s not found", refName));
+      throw new ResourceNotFoundException(String.format("ref %s not found", refName), e);
     }
 
     permissionBackend
@@ -304,7 +307,8 @@
         groups = basePatchSet.groups();
       }
       ObjectId parentCommit =
-          getParentCommit(git, rw, input.branch, input.newBranch, basePatchSet, input.baseCommit);
+          getParentCommit(
+              git, rw, input.branch, input.newBranch, basePatchSet, input.baseCommit, input.merge);
 
       RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit);
 
@@ -338,7 +342,7 @@
       }
       return ins.getChange();
     } catch (IllegalArgumentException e) {
-      throw new BadRequestException(e.getMessage());
+      throw new BadRequestException(e.getMessage(), e);
     }
   }
 
@@ -352,7 +356,7 @@
     try {
       permissionBackend.currentUser().change(change).check(ChangePermission.READ);
     } catch (AuthException e) {
-      throw new UnprocessableEntityException("Read not permitted for " + baseChange);
+      throw new UnprocessableEntityException("Read not permitted for " + baseChange, e);
     }
 
     return change;
@@ -365,7 +369,8 @@
       String inputBranch,
       @Nullable Boolean newBranch,
       @Nullable PatchSet basePatchSet,
-      @Nullable String baseCommit)
+      @Nullable String baseCommit,
+      @Nullable MergeInput mergeInput)
       throws BadRequestException, IOException, UnprocessableEntityException,
           ResourceConflictException {
     if (basePatchSet != null) {
@@ -379,11 +384,22 @@
         parentCommit = ObjectId.fromString(baseCommit);
       } catch (InvalidObjectIdException e) {
         throw new UnprocessableEntityException(
-            String.format("Base %s doesn't represent a valid SHA-1", baseCommit));
+            String.format("Base %s doesn't represent a valid SHA-1", baseCommit), e);
       }
 
-      RevCommit parentRevCommit = revWalk.parseCommit(parentCommit);
+      RevCommit parentRevCommit;
+      try {
+        parentRevCommit = revWalk.parseCommit(parentCommit);
+      } catch (MissingObjectException e) {
+        throw new UnprocessableEntityException(
+            String.format("Base %s doesn't exist", baseCommit), e);
+      }
+
+      if (destRef == null) {
+        throw new BadRequestException("Destination branch does not exist");
+      }
       RevCommit destRefRevCommit = revWalk.parseCommit(destRef.getObjectId());
+
       if (!revWalk.isMergedInto(parentRevCommit, destRefRevCommit)) {
         throw new BadRequestException(
             String.format("Commit %s doesn't exist on ref %s", baseCommit, inputBranch));
@@ -397,9 +413,12 @@
         parentCommit = destRef.getObjectId();
       } else {
         if (Boolean.TRUE.equals(newBranch)) {
+          if (mergeInput != null) {
+            throw new BadRequestException("Cannot create merge: destination branch does not exist");
+          }
           parentCommit = null;
         } else {
-          throw new BadRequestException("Must provide a destination branch");
+          throw new BadRequestException("Destination branch does not exist");
         }
       }
     }
@@ -483,15 +502,24 @@
     String mergeStrategy =
         firstNonNull(Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName());
 
-    return MergeUtil.createMergeCommit(
-        oi,
-        repo.getConfig(),
-        mergeTip,
-        sourceCommit,
-        mergeStrategy,
-        authorIdent,
-        commitMessage,
-        rw);
+    try {
+      return MergeUtil.createMergeCommit(
+          oi,
+          repo.getConfig(),
+          mergeTip,
+          sourceCommit,
+          mergeStrategy,
+          authorIdent,
+          commitMessage,
+          rw);
+    } catch (NoMergeBaseException e) {
+      if (MergeBaseFailureReason.TOO_MANY_MERGE_BASES == e.getReason()
+          || MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION == e.getReason()) {
+        throw new ResourceConflictException(
+            String.format("Cannot create merge commit: %s", e.getMessage()), e);
+      }
+      throw e;
+    }
   }
 
   private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit) throws IOException {
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index c69a348..af0d4bf 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -215,7 +215,7 @@
       ChangeInserter ins =
           changeInserterFactory
               .create(changeId, revertCommit, notes.getChange().getDest().branch())
-              .setTopic(changeToRevert.getTopic());
+              .setTopic(input.topic == null ? changeToRevert.getTopic() : input.topic.trim());
       ins.setMessage("Uploaded patch set 1.");
 
       ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 61e7582..ab4f77f9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -785,6 +785,29 @@
   }
 
   @Test
+  public void revertWithDefaultTopic() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(result.getChangeId()).topic("topic");
+    gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
+    RevertInput revertInput = new RevertInput();
+    assertThat(gApi.changes().id(result.getChangeId()).revert(revertInput).topic())
+        .isEqualTo("topic");
+  }
+
+  @Test
+  public void revertWithSetTopic() throws Exception {
+    PushOneCommit.Result result = createChange();
+    gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(result.getChangeId()).topic("topic");
+    gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
+    RevertInput revertInput = new RevertInput();
+    revertInput.topic = "reverted-not-default";
+    assertThat(gApi.changes().id(result.getChangeId()).revert(revertInput).topic())
+        .isEqualTo(revertInput.topic);
+  }
+
+  @Test
   public void revertNotifications() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).addReviewer(user.email());
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index c18c092..c0f27bd 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -37,7 +37,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV7() {
-    return getConfig(ElasticVersion.V7_3);
+    return getConfig(ElasticVersion.V7_4);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index a3285be..d3e1d39 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Callable;
@@ -82,6 +83,13 @@
   }
 
   @Test
+  public void createEmptyChange_NonExistingBranch() throws Exception {
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.branch = "non-existing";
+    assertCreateFails(ci, BadRequestException.class, "Destination branch does not exist");
+  }
+
+  @Test
   public void createEmptyChange_MissingMessage() throws Exception {
     ChangeInput ci = new ChangeInput();
     ci.project = project.get();
@@ -270,6 +278,16 @@
   }
 
   @Test
+  public void createChangeWithNonExistingParentCommitFails() throws Exception {
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.baseCommit = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    assertCreateFails(
+        input,
+        UnprocessableEntityException.class,
+        String.format("Base %s doesn't exist", input.baseCommit));
+  }
+
+  @Test
   public void createChangeWithParentCommitOnWrongBranchFails() throws Exception {
     Map<String, PushOneCommit.Result> setup =
         changeInTwoBranches("foo", "foo.txt", "bar", "bar.txt");
@@ -283,6 +301,20 @@
   }
 
   @Test
+  public void createChangeWithParentCommitWithNonExistingTargetBranch() throws Exception {
+    Result initialCommit =
+        pushFactory
+            .create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
+            .to("refs/heads/master");
+    initialCommit.assertOkStatus();
+
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.branch = "non-existing";
+    input.baseCommit = initialCommit.getCommit().getName();
+    assertCreateFails(input, BadRequestException.class, "Destination branch does not exist");
+  }
+
+  @Test
   public void createChangeOnNonExistingBaseChangeFails() throws Exception {
     ChangeInput input = newChangeInput(ChangeStatus.NEW);
     input.baseChange = "999999";
@@ -349,6 +381,62 @@
   }
 
   @Test
+  public void createMergeChangeFailsWithConflictIfThereAreTooManyCommonPredecessors()
+      throws Exception {
+    // Create an initial commit in master.
+    Result initialCommit =
+        pushFactory
+            .create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
+            .to("refs/heads/master");
+    initialCommit.assertOkStatus();
+
+    String file = "shared.txt";
+    List<RevCommit> parents = new ArrayList<>();
+    // RecursiveMerger#MAX_BASES = 200, cannot use RecursiveMerger#MAX_BASES as it is not static.
+    int maxBases = 200;
+
+    // Create more than RecursiveMerger#MAX_BASES base commits.
+    for (int i = 1; i <= maxBases + 1; i++) {
+      parents.add(
+          testRepo
+              .commit()
+              .message("Base " + i)
+              .add(file, "content " + i)
+              .parent(initialCommit.getCommit())
+              .create());
+    }
+
+    // Create 2 branches.
+    String branchA = "branchA";
+    String branchB = "branchB";
+    createBranch(BranchNameKey.create(project, branchA));
+    createBranch(BranchNameKey.create(project, branchB));
+
+    // Push an octopus merge to both of the branches.
+    Result octopusA =
+        pushFactory
+            .create(user.newIdent(), testRepo)
+            .setParents(parents)
+            .to("refs/heads/" + branchA);
+    octopusA.assertOkStatus();
+
+    Result octopusB =
+        pushFactory
+            .create(user.newIdent(), testRepo)
+            .setParents(parents)
+            .to("refs/heads/" + branchB);
+    octopusB.assertOkStatus();
+
+    // Creating a merge commit for the 2 octopus commits fails, because they have more than
+    // RecursiveMerger#MAX_BASES common predecessors.
+    assertCreateFails(
+        newMergeChangeInput("branchA", "branchB", ""),
+        ResourceConflictException.class,
+        "Cannot create merge commit: No merge base could be determined."
+            + " Reason=TOO_MANY_MERGE_BASES.");
+  }
+
+  @Test
   public void invalidSource() throws Exception {
     changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
     ChangeInput in = newMergeChangeInput("branchA", "invalid", "");
@@ -448,6 +536,15 @@
   }
 
   @Test
+  public void createChangeOnNonExistingBranch() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.branch = "foo";
+    input.newBranch = true;
+    assertCreateSucceeds(input);
+  }
+
+  @Test
   public void createChangeOnNonExistingBranchNotPermitted() throws Exception {
     projectOperations
         .project(project)
@@ -465,6 +562,15 @@
   }
 
   @Test
+  public void createMergeChangeOnNonExistingBranchNotPossible() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInput input = newMergeChangeInput("foo", "master", "");
+    input.newBranch = true;
+    assertCreateFails(
+        input, BadRequestException.class, "Cannot create merge: destination branch does not exist");
+  }
+
+  @Test
   @UseSystemTime
   public void sha1sOfTwoNewChangesDiffer() throws Exception {
     ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
diff --git a/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java b/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java
new file mode 100644
index 0000000..5ff1c32
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/event/EventPayloadIT.java
@@ -0,0 +1,65 @@
+// 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.server.event;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.events.RevisionCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.inject.Inject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class EventPayloadIT extends AbstractDaemonTest {
+  @Inject private DynamicSet<RevisionCreatedListener> revisionCreatedListeners;
+
+  private RegistrationHandle eventListenerRegistration;
+  private RevisionCreatedListener.Event lastEvent;
+
+  @Before
+  public void setUp() throws Exception {
+    eventListenerRegistration = revisionCreatedListeners.add("gerrit", event -> lastEvent = event);
+  }
+
+  @After
+  public void cleanup() {
+    eventListenerRegistration.remove();
+  }
+
+  @Test
+  public void defaultOptions() throws Exception {
+    createChange();
+
+    assertThat(lastEvent.getChange().submittable).isNotNull();
+    assertThat(lastEvent.getRevision().files).isNotEmpty();
+  }
+
+  @Test
+  @GerritConfig(name = "event.payload.listChangeOptions", value = "SKIP_MERGEABLE")
+  public void configuredOptions() throws Exception {
+    createChange();
+
+    assertThat(lastEvent.getChange().submittable).isNull();
+    assertThat(lastEvent.getChange().mergeable).isNull();
+    assertThat(lastEvent.getRevision().files).isNull();
+    assertThat(lastEvent.getChange().subject).isNotEmpty();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 6ccd9e0..61a490b 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -36,7 +36,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV7() {
-    return getConfig(ElasticVersion.V7_3);
+    return getConfig(ElasticVersion.V7_4);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 27ac839..6e3d666 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -60,6 +60,8 @@
         return "blacktop/elasticsearch:7.2.1";
       case V7_3:
         return "blacktop/elasticsearch:7.3.2";
+      case V7_4:
+        return "blacktop/elasticsearch:7.4.0";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index cc8191b..021a80d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_3);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_4);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index 2b8e000..6bb2f8fb 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -51,7 +51,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_3);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_4);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
     client = HttpAsyncClients.createDefault();
     client.start();
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index b453cd4..9312f01 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_3);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_4);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index 27e39df..4b9ede8 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_3);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_4);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index 8d317ff..f9cfe35 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -57,6 +57,9 @@
 
     assertThat(ElasticVersion.forVersion("7.3.0")).isEqualTo(ElasticVersion.V7_3);
     assertThat(ElasticVersion.forVersion("7.3.1")).isEqualTo(ElasticVersion.V7_3);
+
+    assertThat(ElasticVersion.forVersion("7.4.0")).isEqualTo(ElasticVersion.V7_4);
+    assertThat(ElasticVersion.forVersion("7.4.1")).isEqualTo(ElasticVersion.V7_4);
   }
 
   @Test
@@ -85,6 +88,7 @@
     assertThat(ElasticVersion.V7_1.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V7_2.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V7_3.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
+    assertThat(ElasticVersion.V7_4.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
   }
 
   @Test
@@ -101,6 +105,7 @@
     assertThat(ElasticVersion.V7_1.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V7_2.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V7_3.isV6OrLater()).isTrue();
+    assertThat(ElasticVersion.V7_4.isV6OrLater()).isTrue();
   }
 
   @Test
@@ -117,5 +122,6 @@
     assertThat(ElasticVersion.V7_1.isV7OrLater()).isTrue();
     assertThat(ElasticVersion.V7_2.isV7OrLater()).isTrue();
     assertThat(ElasticVersion.V7_3.isV7OrLater()).isTrue();
+    assertThat(ElasticVersion.V7_4.isV7OrLater()).isTrue();
   }
 }
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 7dad316..10e6a3e 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 7dad3163ae91b1f28cdbeec87edca90441fa21b1
+Subproject commit 10e6a3e8c3b637461d9cd0e9457fc3e3f625c27a
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index cbc5f13..b79e5f3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -199,7 +199,7 @@
               section.query);
 
       if (checkForNewUser) {
-        queries.push('owner:self');
+        queries.push('owner:self limit:1');
       }
 
       return this.$.restAPI.getChanges(null, queries, null, this.options)
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 0f0a7ec..9f3e675 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -184,7 +184,7 @@
         return paramsChangedPromise.then(() => {
           assert.isTrue(
               getChangesStub.calledWith(
-                  null, ['1', '2', 'owner:self'], null, element.options));
+                  null, ['1', '2', 'owner:self limit:1'], null, element.options));
         });
       });
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 7c86143..87ac4fd7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -116,6 +116,7 @@
         margin-top: .5em;
         padding: .5em 0;
       }
+      .hashtag gr-linked-chip,
       .topic gr-linked-chip {
         --linked-chip-text-color: var(--link-color);
       }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
index 611f886..0a51d92 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -40,7 +40,13 @@
     }
 
     for (let i = 0; i < group.lines.length; ++i) {
-      sectionEl.appendChild(this._createRow(sectionEl, group.lines[i]));
+      const line = group.lines[i];
+      // If only whitespace has changed and the settings ask for whitespace to
+      // be ignored, only render the right-side line in unified diff mode.
+      if (group.ignoredWhitespaceOnly && line.type == GrDiffLine.Type.REMOVE) {
+        continue;
+      }
+      sectionEl.appendChild(this._createRow(sectionEl, line));
     }
     return sectionEl;
   };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
new file mode 100644
index 0000000..19e017d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
@@ -0,0 +1,205 @@
+<!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>GrDiffBuilderUnified</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="../../../scripts/util.js"></script>
+<script src="../gr-diff/gr-diff-line.js"></script>
+<script src="../gr-diff/gr-diff-group.js"></script>
+<script src="../gr-diff-highlight/gr-annotation.js"></script>
+<script src="gr-diff-builder.js"></script>
+<script src="gr-diff-builder-unified.js"></script>
+
+<script>void(0);</script>
+
+<script>
+  suite('GrDiffBuilderUnified tests', () => {
+    let prefs;
+    let outputEl;
+    let diffBuilder;
+
+    setup(()=> {
+      prefs = {
+        line_length: 10,
+        show_tabs: true,
+        tab_size: 4,
+      };
+      outputEl = document.createElement('div');
+      diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
+    });
+
+    suite('buildSectionElement for BOTH group', () => {
+      let lines;
+      let group;
+
+      setup(() => {
+        lines = [
+          new GrDiffLine(GrDiffLine.Type.BOTH, 1, 2),
+          new GrDiffLine(GrDiffLine.Type.BOTH, 2, 3),
+          new GrDiffLine(GrDiffLine.Type.BOTH, 3, 4),
+        ];
+        lines[0].text = 'def hello_world():';
+        lines[1].text = '  print "Hello World";';
+        lines[2].text = '  return True';
+
+        group = new GrDiffGroup(GrDiffGroup.Type.BOTH, lines);
+      });
+
+      test('creates the section', () => {
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        assert.isTrue(sectionEl.classList.contains('section'));
+        assert.isTrue(sectionEl.classList.contains('both'));
+      });
+
+      test('creates each unchanged row once', () => {
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+        assert.equal(rowEls.length, 3);
+
+        assert.equal(
+            rowEls[0].querySelector('.lineNum.left').textContent,
+            lines[0].beforeNumber);
+        assert.equal(
+            rowEls[0].querySelector('.lineNum.right').textContent,
+            lines[0].afterNumber);
+        assert.equal(
+            rowEls[0].querySelector('.content').textContent, lines[0].text);
+
+        assert.equal(
+            rowEls[1].querySelector('.lineNum.left').textContent,
+            lines[1].beforeNumber);
+        assert.equal(
+            rowEls[1].querySelector('.lineNum.right').textContent,
+            lines[1].afterNumber);
+        assert.equal(
+            rowEls[1].querySelector('.content').textContent, lines[1].text);
+
+        assert.equal(
+            rowEls[2].querySelector('.lineNum.left').textContent,
+            lines[2].beforeNumber);
+        assert.equal(
+            rowEls[2].querySelector('.lineNum.right').textContent,
+            lines[2].afterNumber);
+        assert.equal(
+            rowEls[2].querySelector('.content').textContent, lines[2].text);
+      });
+    });
+
+    suite('buildSectionElement for DELTA group', () => {
+      let lines;
+      let group;
+
+      setup(() => {
+        lines = [
+          new GrDiffLine(GrDiffLine.Type.REMOVE, 1),
+          new GrDiffLine(GrDiffLine.Type.REMOVE, 2),
+          new GrDiffLine(GrDiffLine.Type.ADD, 2),
+          new GrDiffLine(GrDiffLine.Type.ADD, 3),
+        ];
+        lines[0].text = 'def hello_world():';
+        lines[1].text = '  print "Hello World"';
+        lines[2].text = 'def hello_universe()';
+        lines[3].text = '  print "Hello Universe"';
+
+        group = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines);
+      });
+
+      test('creates the section', () => {
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        assert.isTrue(sectionEl.classList.contains('section'));
+        assert.isTrue(sectionEl.classList.contains('delta'));
+      });
+
+      test('creates the section with class if ignoredWhitespaceOnly', () => {
+        group.ignoredWhitespaceOnly = true;
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
+      });
+
+      test('creates the section with class if dueToRebase', () => {
+        group.dueToRebase = true;
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        assert.isTrue(sectionEl.classList.contains('dueToRebase'));
+      });
+
+      test('creates first the removed and then the added rows', () => {
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+        assert.equal(rowEls.length, 4);
+
+        assert.equal(
+            rowEls[0].querySelector('.lineNum.left').textContent,
+            lines[0].beforeNumber);
+        assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
+        assert.equal(
+            rowEls[0].querySelector('.content').textContent, lines[0].text);
+
+        assert.equal(
+            rowEls[1].querySelector('.lineNum.left').textContent,
+            lines[1].beforeNumber);
+        assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
+        assert.equal(
+            rowEls[1].querySelector('.content').textContent, lines[1].text);
+
+        assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
+        assert.equal(
+            rowEls[2].querySelector('.lineNum.right').textContent,
+            lines[2].afterNumber);
+        assert.equal(
+            rowEls[2].querySelector('.content').textContent, lines[2].text);
+
+        assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
+        assert.equal(
+            rowEls[3].querySelector('.lineNum.right').textContent,
+            lines[3].afterNumber);
+        assert.equal(
+            rowEls[3].querySelector('.content').textContent, lines[3].text);
+      });
+
+      test('creates only the added rows if only ignored whitespace', () => {
+        group.ignoredWhitespaceOnly = true;
+        const sectionEl = diffBuilder.buildSectionElement(group);
+        const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+        assert.equal(rowEls.length, 2);
+
+        assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
+        assert.equal(
+            rowEls[0].querySelector('.lineNum.right').textContent,
+            lines[2].afterNumber);
+        assert.equal(
+            rowEls[0].querySelector('.content').textContent, lines[2].text);
+
+        assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
+        assert.equal(
+            rowEls[1].querySelector('.lineNum.right').textContent,
+            lines[3].afterNumber);
+        assert.equal(
+            rowEls[1].querySelector('.content').textContent, lines[3].text);
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index a019388..c25e90c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -316,8 +316,6 @@
       return td;
     } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
       td.classList.add('contextLineNum');
-      td.setAttribute('data-value', '@@');
-      td.textContent = '@@';
     } else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
       td.classList.add('lineNum');
       td.setAttribute('data-value', number);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index b83dd53..7ca5f5e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -94,7 +94,7 @@
 
         <tbody class="section contextControl">
           <tr class="diff-row side-by-side" left-type="contextControl" right-type="contextControl">
-            <td class="left contextLineNum" data-value="@@"></td>
+            <td class="left contextLineNum"></td>
             <td>
               <gr-button>+10↑</gr-button>
               -
@@ -102,7 +102,7 @@
               -
               <gr-button>+10↓</gr-button>
             </td>
-            <td class="right contextLineNum" data-value="@@"></td>
+            <td class="right contextLineNum"></td>
             <td>
               <gr-button>+10↑</gr-button>
               -
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
index c5c1bfe..dbf5a03 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
@@ -19,6 +19,7 @@
 <link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.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="../../../styles/shared-styles.html">
@@ -62,6 +63,7 @@
               <th class="userIdHeader">User IDs</th>
               <th class="keyHeader">Public Key</th>
               <th></th>
+              <th></th>
             </tr>
           </thead>
           <tbody>
@@ -81,6 +83,14 @@
                       link>Click to View</gr-button>
                 </td>
                 <td>
+                  <gr-copy-clipboard
+                      has-tooltip
+                      button-title="Copy GPG public key to clipboard"
+                      hide-input
+                      text="[[key.key]]">
+                  </gr-copy-clipboard>
+                </td>
+                <td>
                   <gr-button
                       data-index$="[[index]]"
                       on-click="_handleDeleteKey">Delete</gr-button>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
index c3cd8e1..9cfbde5f 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -97,7 +97,7 @@
 
       // Get the delete button for the last row.
       const button = Polymer.dom(element.root).querySelector(
-          'tbody tr:last-of-type td:nth-child(5) gr-button');
+          'tbody tr:last-of-type td:nth-child(6) gr-button');
 
       MockInteractions.tap(button);
 
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
index 0ab1e64..e0030ba 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
@@ -19,6 +19,7 @@
 <link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.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="../../../styles/shared-styles.html">
@@ -65,6 +66,7 @@
               <th class="statusHeader">Status</th>
               <th class="keyHeader">Public key</th>
               <th></th>
+              <th></th>
             </tr>
           </thead>
           <tbody>
@@ -80,6 +82,14 @@
                       link>Click to View</gr-button>
                 </td>
                 <td>
+                  <gr-copy-clipboard
+                      has-tooltip
+                      button-title="Copy SSH public key to clipboard"
+                      hide-input
+                      text="[[key.ssh_public_key]]">
+                  </gr-copy-clipboard>
+                </td>
+                <td>
                   <gr-button
                       link
                       data-index$="[[index]]"
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index 991f17c..d313f5a 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -88,7 +88,7 @@
 
       // Get the delete button for the last row.
       const button = Polymer.dom(element.root).querySelector(
-          'tbody tr:last-of-type td:nth-child(4) gr-button');
+          'tbody tr:last-of-type td:nth-child(5) gr-button');
 
       MockInteractions.tap(button);
 
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
index 941f99c..06510dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
@@ -36,7 +36,7 @@
       .hideInput {
         display: none;
       }
-      input {
+      input#input {
         font-family: var(--monospace-font-family);
         font-size: inherit;
         @apply --text-container-style;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index bc1f9c2..3b7b80d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -506,11 +506,6 @@
     };
   }
 
-  Gerrit.getPluginName = function() {
-    console.warn('Gerrit.getPluginName is not supported in PolyGerrit.',
-        'Please use plugin.getPluginName() instead.');
-  };
-
   /**
    * @deprecated Use plugin.styles().css(rulesStr) instead. Please, consult
    * the documentation how to replace it accordingly.
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 87e4975..f0df879 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -442,8 +442,8 @@
     getIsGroupOwner(groupName) {
       const encodeName = encodeURIComponent(groupName);
       const req = {
-        url: `/groups/?owned&q=${encodeName}`,
-        anonymizedUrl: '/groups/owned&q=*',
+        url: `/groups/?owned&g=${encodeName}`,
+        anonymizedUrl: '/groups/owned&g=*',
       };
       return this._fetchSharedCacheURL(req)
           .then(configs => configs.hasOwnProperty(groupName));
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index a0cb3d8..91bcd69 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -16,137 +16,137 @@
 -->
 <custom-style><style is="custom-style">
 html {
-  /* Following vars have LTS for plugin API. */
-  --primary-text-color: #000;
-  /* For backwords compatibility we keep this as --header-background-color. */
-  --header-background-color: #eee;
-  --header-title-content: 'Gerrit';
-  --header-icon: none;
-  --header-icon-size: 0em;
-  --header-text-color: #000;
-  --header-title-font-size: 1.75rem;
-  --footer-background-color: #eee;
-  --border-color: #ddd;
-  /* This allows to add a border in custom themes */
-  --header-border-bottom: 1px solid var(--border-color);
-  --header-border-image: '';
-  --footer-border-top: 1px solid var(--border-color);
+  /**
+   * When adding a new color variable make sure to also add it to the other
+   * theme files in the same directory.
+   *
+   * For colors prefer lower case hex colors.
+   *
+   * Note that plugins might be using these variables, so removing a variable
+   * can be a breaking change that should go into the release notes.
+   */
 
-  /* Following are not part of plugin API. */
-  --selection-background-color: rgba(161, 194, 250, 0.1);
-  --hover-background-color: rgba(161, 194, 250, 0.2);
-  --expanded-background-color: #eee;
-  --view-background-color: #fff;
-  --default-horizontal-margin: 1rem;
-
+  /* text colors */
+  --primary-text-color: black;
+  --link-color: #2a66d9;
+  --comment-text-color: black;
   --deemphasized-text-color: #757575;
-  /* Used on text color for change list that doesn't need user's attention. */
-  --reviewed-text-color: var(--primary-text-color);
-  --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
-  /* Used on text for change list that needs user's attention. */
-  --font-weight-bold: 500;
-  --monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
-  --iron-overlay-backdrop: {
-    transition: none;
-  }
+  --default-button-text-color: #2a66d9;
+  --error-text-color: red;
+  --primary-button-text-color: white;
+    /* Used on text color for change list that doesn't need user's attention. */
+  --reviewed-text-color: black;
+  --secondary-button-text-color: #212121;
+  --tooltip-text-color: white;
+  --vote-text-color-recommended: #388e3c;
+  --vote-text-color-disliked: #d32f2f;
+
+  /* background colors */
+  --assignee-highlight-color: #fcfad6;
+  --chip-background-color: #eee;
+  --comment-background-color: #fcfad6;
+  --default-button-background-color: white;
+  --dialog-background-color: white;
+  --dropdown-background-color: white;
+  --edit-mode-background-color: #ebf5fb;
+  --emphasis-color: #fff9c4;
+  --expanded-background-color: #eee;
+  --hover-background-color: rgba(161, 194, 250, 0.2);
+  --primary-button-background-color: #2a66d9;
+  --secondary-button-background-color: white;
+  --select-background-color: #f8f8f8;
+  --selection-background-color: rgba(161, 194, 250, 0.1);
+  --shell-command-background-color: #f5f5f5;
+  --shell-command-decoration-background-color: #ebebeb;
   --table-header-background-color: #fafafa;
   --table-subheader-background-color: #eaeaea;
+  --tooltip-background-color: #333;
+  --unresolved-comment-background-color: #fcfaa6;
+  --view-background-color: white;
+  --vote-color-approved: #9fcc6b;
+  --vote-color-disliked: #f7c4cb;
+  --vote-color-neutral: #ebf5fb;
+  --vote-color-recommended: #c9dfaf;
+  --vote-color-rejected: #f7a1ad;
 
-  --chip-background-color: #eee;
+  /* misc colors */
+  --border-color: #ddd;
 
-  --dropdown-background-color: #fff;
-
-  --select-background-color: rgb(248, 248, 248);
-
-  --assignee-highlight-color: #fcfad6;
-
-  /* Font sizes */
+  /* fonts */
+  --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
   --font-size-normal: 1rem;
   --font-size-small: .92rem;
   --font-size-large: 1.154rem;
+    /* Used on text for change list that needs user's attention. */
+  --font-weight-bold: 500;
+  --monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
 
-  --link-color: #2a66d9;
-  --primary-button-background-color: var(--link-color);
-  --primary-button-text-color: #fff;
-  --secondary-button-background-color: #fff;
-  --secondary-button-text-color: #212121;
-  --default-button-background-color: #fff;
-  --default-button-text-color: var(--link-color);
-  --dialog-background-color: #fff;
+  /* spacing */
+  --default-horizontal-margin: 1rem;
 
-  /* Used for both the old patchset header and for indicating that a particular
-    change message was selected. */
-  --emphasis-color: #fff9c4;
+  /* header and footer */
+  --footer-background-color: #eee;
+  --footer-border-top: 1px solid var(--border-color);
+  --header-background-color: #eee;
+  --header-border-bottom: 1px solid var(--border-color);
+  --header-border-image: '';
+  --header-icon-size: 0em;
+  --header-icon: none;
+  --header-text-color: black;
+  --header-title-content: 'Gerrit';
+  --header-title-font-size: 1.75rem;
 
-  --error-text-color: red;
-
-  --vote-color-approved: #9fcc6b;
-  --vote-color-recommended: #c9dfaf;
-  --vote-color-rejected: #f7a1ad;
-  --vote-color-disliked: #f7c4cb;
-  --vote-color-neutral: #ebf5fb;
-
-  --vote-text-color-recommended: #388E3C;
-  --vote-text-color-disliked: #D32F2F;
-
-  /* Diff colors */
-  --diff-selection-background-color: #c7dbf9;
-  --light-remove-highlight-color: #FFEBEE;
-  --light-add-highlight-color: #D8FED8;
-  --light-remove-add-highlight-color: #FFF8DC;
-  --light-rebased-add-highlight-color: #EEEEFF;
-  --dark-remove-highlight-color: #FFCDD2;
-  --dark-add-highlight-color: #AAF2AA;
-  --dark-rebased-remove-highlight-color: #F7E8B7;
-  --dark-rebased-add-highlight-color: #D7D7F9;
-  --diff-context-control-color: var(--deemphasized-text-color);
+  /* diff colors */
+  --dark-add-highlight-color: #aaf2aa;
+  --dark-rebased-add-highlight-color: #d7d7f9;
+  --dark-rebased-remove-highlight-color: #f7e8b7;
+  --dark-remove-highlight-color: #ffcdd2;
+  --diff-blank-background-color: white;
   --diff-context-control-background-color: #fff7d4;
   --diff-context-control-border-color: #f6e6a5;
-  --diff-tab-indicator-color: var(--deemphasized-text-color);
-  --diff-trailing-whitespace-indicator: #ff9ad2;
+  --diff-context-control-color: var(--deemphasized-text-color);
   --diff-highlight-range-color: rgba(255, 213, 0, 0.5);
   --diff-highlight-range-hover-color: rgba(255, 255, 0, 0.5);
-  --diff-blank-background-color: #fff;
+  --diff-selection-background-color: #c7dbf9;
+  --diff-tab-indicator-color: var(--deemphasized-text-color);
+  --diff-trailing-whitespace-indicator: #ff9ad2;
+  --light-add-highlight-color: #d8fed8;
+  --light-rebased-add-highlight-color: #eef;
+  --light-remove-add-highlight-color: #fff8dc;
+  --light-remove-highlight-color: #ffebee;
 
-  --shell-command-background-color: #f5f5f5;
-  --shell-command-decoration-background-color: #ebebeb;
-
-  --comment-text-color: #000;
-  --comment-background-color: #fcfad6;
-  --unresolved-comment-background-color: #fcfaa6;
-
-  --edit-mode-background-color: #ebf5fb;
-
-  --tooltip-background-color: #333;
-  --tooltip-text-color: #fff;
-
-  --syntax-default-color: var(--primary-text-color);
-  --syntax-attribute-color: var(--primary-text-color);
-  --syntax-function-color: var(--primary-text-color);
-  --syntax-meta-color: #FF1717;
-  --syntax-keyword-color: #9E0069;
-  --syntax-number-color: #164;
-  --syntax-selector-class-color: #164;
-  --syntax-variable-color: black;
-  --syntax-template-variable-color: #0000C0;
-  --syntax-comment-color: #3F7F5F;
-  --syntax-string-color: #2A00FF;
-  --syntax-selector-id-color: #2A00FF;
-  --syntax-built_in-color: #30a;
-  --syntax-tag-color: #170;
-  --syntax-link-color: #219;
-  --syntax-meta-keyword-color: #219;
-  --syntax-type-color: var(--color-link);
-  --syntax-title-color: #0000C0;
+  /* syntax colors */
   --syntax-attr-color: #219;
+  --syntax-attribute-color: var(--primary-text-color);
+  --syntax-built_in-color: #30a;
+  --syntax-comment-color: #3f7f5f;
+  --syntax-default-color: var(--primary-text-color);
+  --syntax-function-color: var(--primary-text-color);
+  --syntax-keyword-color: #9e0069;
+  --syntax-link-color: #219;
   --syntax-literal-color: #219;
-  --syntax-selector-pseudo-color: #FA8602;
-  --syntax-regexp-color: #FA8602;
-  --syntax-selector-attr-color: #FA8602;
-  --syntax-template-tag-color: #FA8602;
+  --syntax-meta-color: #ff1717;
+  --syntax-meta-keyword-color: #219;
+  --syntax-number-color: #164;
   --syntax-param-color: var(--primary-text-color);
+  --syntax-regexp-color: #fa8602;
+  --syntax-selector-attr-color: #fa8602;
+  --syntax-selector-class-color: #164;
+  --syntax-selector-id-color: #2a00ff;
+  --syntax-selector-pseudo-color: #fa8602;
+  --syntax-string-color: #2a00ff;
+  --syntax-tag-color: #170;
+  --syntax-template-tag-color: #fa8602;
+  --syntax-template-variable-color: #0000c0;
+  --syntax-title-color: #0000c0;
+  --syntax-type-color: #2a66d9;
+  --syntax-variable-color: var(--primary-text-color);
 
+  /* misc */
   --reply-overlay-z-index: 1000;
+  --iron-overlay-backdrop: {
+    transition: none;
+  };
 }
 @media screen and (max-width: 50em) {
   html {
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 2324fd5..9027ddc 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -17,97 +17,123 @@
 <dom-module id="dark-theme">
   <custom-style><style is="custom-style">
     html {
+      /**
+       * Sections and variables must stay consistent with app-theme.html.
+       *
+       * Only modify color variables in this theme file. dark-theme extends
+       * app-theme, so there is no need to repeat all variables, but for colors
+       * it does make sense to list them all: If you override one color, then
+       * you probably want to override all.
+       */
+
+      /* text colors */
       --primary-text-color: #e8eaed;
-      --view-background-color: #131416;
-      --border-color: #5f6368;
-      --header-border-bottom: 1px solid var(--border-color);
-      --header-border-image: '';
-      --footer-border-bottom: 1px solid var(--border-color);
+      --link-color: #8ab4f8;
+      --comment-text-color: var(--primary-text-color);
+      --deemphasized-text-color: #9aa0a6;
+      --default-button-text-color: #8ab4f8;
+      --error-text-color: red;
+      --primary-button-text-color: var(--primary-text-color);
+        /* Used on text color for change list doesn't need user's attention. */
+      --reviewed-text-color: #dadce0;
+      --secondary-button-text-color: var(--deemphasized-text-color);
+      --tooltip-text-color: white;
+      --vote-text-color-recommended: #388e3c;
+      --vote-text-color-disliked: #d32f2f;
+
+      /* background colors */
+      --assignee-highlight-color: #3a361c;
+      --chip-background-color: #131416;
+      --comment-background-color: #0b162b;
+      --default-button-background-color: #3c4043;
+      --dialog-background-color: #131416;
+      --dropdown-background-color: #131416;
+      --edit-mode-background-color: #5c0a36;
+      --emphasis-color: #383f4a;
+      --expanded-background-color: #26282b;
+      --hover-background-color: rgba(161, 194, 250, 0.2);
+      --primary-button-background-color: var(--link-color);
+      --secondary-button-background-color: var(--primary-text-color);
+      --select-background-color: #3c4043;
+      --selection-background-color: rgba(161, 194, 250, 0.1);
+      --shell-command-background-color: #5f5f5f;
+      --shell-command-decoration-background-color: #999;
       --table-header-background-color: #131416;
       --table-subheader-background-color: #3c4043;
-      --header-background-color: #3c4043;
-      --header-text-color: var(--primary-text-color);
-      --deemphasized-text-color: #9aa0a6;
-      /* Used on text color for change list doesn't need user's attention. */
-      --reviewed-text-color: #DADCE0;
-      /* Used on text for change list that needs user's attention. */
-      --font-weight-bold: 900;
-      --footer-background-color: var(--table-header-background-color);
-      --expanded-background-color: #26282b;
-      --link-color: #8ab4f8;
-      --primary-button-background-color: var(--link-color);
-      --primary-button-text-color: var(--primary-text-color);
-      --secondary-button-background-color: var(--primary-text-color);
-      --secondary-button-text-color: var(--deemphasized-text-color);
-      --default-button-text-color: var(--link-color);
-      --default-button-background-color: var(--table-subheader-background-color);
-      --dropdown-background-color: var(--table-header-background-color);
-      --dialog-background-color: var(--view-background-color);
-      --chip-background-color: var(--table-header-background-color);
-      --header-title-font-size: 1.75rem;
-
-      --select-background-color: var(--table-subheader-background-color);
-
-      --assignee-highlight-color: rgb(58, 54, 28);
-
-      --diff-selection-background-color: #3A71D8;
-      --light-remove-highlight-color: #320404;
-      --light-add-highlight-color: #0F401F;
-      --light-remove-add-highlight-color: #2f3f2f;
-      --light-rebased-remove-highlight-color: rgb(60, 37, 8);
-      --light-rebased-add-highlight-color: rgb(72, 113, 101);
-      --dark-remove-highlight-color: #62110F;
-      --dark-add-highlight-color: #133820;
-      --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
-      --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
-      --diff-context-control-color: var(--deemphasized-text-color);
-      --diff-context-control-background-color: var(--table-header-background-color);
-      --diff-context-control-border-color: var(--border-color);
-      --diff-highlight-range-color: rgba(0, 100, 200, 0.5);
-      --diff-highlight-range-hover-color: rgba(0, 150, 255, 0.5);
-      --shell-command-background-color: #5f5f5f;
-      --shell-command-decoration-background-color: #999999;
-      --comment-text-color: var(--primary-text-color);
-      --comment-background-color: #0B162B;
-      --unresolved-comment-background-color: rgb(56, 90, 154);
-      --diff-blank-background-color: #212121;
-
-      --vote-color-approved: rgb(127, 182, 107);
-      --vote-color-recommended: rgb(63, 103, 50);
-      --vote-color-rejected: #ac2d3e;
+      --tooltip-background-color: #111;
+      --unresolved-comment-background-color: #385a9a;
+      --view-background-color: #131416;
+      --vote-color-approved: #7fb66b;
       --vote-color-disliked: #bf6874;
       --vote-color-neutral: #597280;
+      --vote-color-recommended: #3f6732;
+      --vote-color-rejected: #ac2d3e;
 
-      --edit-mode-background-color: rgb(92, 10, 54);
-      --emphasis-color: #383f4a;
+      /* misc colors */
+      --border-color: #5f6368;
 
-      --tooltip-background-color: #111;
+      /* fonts */
+        /* Used on text for change list that needs user's attention. */
+      --font-weight-bold: 900;
 
-      --syntax-default-color: var(--primary-text-color);
-      --syntax-meta-color: #6D7EEE;
-      --syntax-keyword-color: #CD4CF0;
-      --syntax-number-color: #00998A;
-      --syntax-selector-class-color: #FFCB68;
-      --syntax-variable-color: #F77669;
-      --syntax-template-variable-color: #F77669;
+      /* spacing */
+
+      /* header and footer */
+      --footer-background-color: #131416;
+      --footer-border-top: 1px solid var(--border-color);
+      --header-background-color: #3c4043;
+      --header-border-bottom: 1px solid var(--border-color);
+      --header-text-color: var(--primary-text-color);
+
+      /* diff colors */
+      --dark-add-highlight-color: #133820;
+      --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
+      --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
+      --dark-remove-highlight-color: #62110f;
+      --diff-blank-background-color: #212121;
+      --diff-context-control-background-color: #131416;
+      --diff-context-control-border-color: var(--border-color);
+      --diff-context-control-color: var(--deemphasized-text-color);
+      --diff-highlight-range-color: rgba(0, 100, 200, 0.5);
+      --diff-highlight-range-hover-color: rgba(0, 150, 255, 0.5);
+      --diff-selection-background-color: #3a71d8;
+      --diff-tab-indicator-color: var(--deemphasized-text-color);
+      --diff-trailing-whitespace-indicator: #ff9ad2;
+      --light-add-highlight-color: #0f401f;
+      --light-rebased-add-highlight-color: #487165;
+      --light-remove-add-highlight-color: #2f3f2f;
+      --light-remove-highlight-color: #320404;
+
+      /* syntax colors */
+      --syntax-attr-color: #80cbbf;
+      --syntax-attribute-color: var(--primary-text-color);
+      --syntax-built_in-color: #f7c369;
       --syntax-comment-color: var(--deemphasized-text-color);
-      --syntax-string-color: #C3E88D;
-      --syntax-selector-id-color: #F77669;
-      --syntax-built_in-color: rgb(247, 195, 105);
-      --syntax-tag-color: #F77669;
-      --syntax-link-color: #C792EA;
-      --syntax-meta-keyword-color: #EEFFF7;
-      --syntax-type-color: #DD5F5F;
-      --syntax-title-color: #75A5FF;
-      --syntax-attr-color: #80CBBF;
-      --syntax-literal-color: #EEFFF7;
-      --syntax-selector-pseudo-color: #C792EA;
-      --syntax-regexp-color: #F77669;
-      --syntax-selector-attr-color: #80CBBF;
-      --syntax-template-tag-color: #C792EA;
+      --syntax-default-color: var(--primary-text-color);
+      --syntax-function-color: var(--primary-text-color);
+      --syntax-keyword-color: #cd4cf0;
+      --syntax-link-color: #c792ea;
+      --syntax-literal-color: #eefff7;
+      --syntax-meta-color: #6d7eee;
+      --syntax-meta-keyword-color: #eefff7;
+      --syntax-number-color: #00998a;
+      --syntax-param-color: var(--primary-text-color);
+      --syntax-regexp-color: #f77669;
+      --syntax-selector-attr-color: #80cbbf;
+      --syntax-selector-class-color: #ffcb68;
+      --syntax-selector-id-color: #f77669;
+      --syntax-selector-pseudo-color: #c792ea;
+      --syntax-string-color: #c3e88d;
+      --syntax-tag-color: #f77669;
+      --syntax-template-tag-color: #c792ea;
+      --syntax-template-variable-color: #f77669;
+      --syntax-title-color: #75a5ff;
+      --syntax-type-color: #dd5f5f;
+      --syntax-variable-color: #f77669;
 
-      --reply-overlay-z-index: 1000;
+      /* misc */
 
+      /* rules applied to <html> */
       background-color: var(--view-background-color);
     }
   </style></custom-style>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 75380dd..10d5aff 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -107,6 +107,7 @@
     'diff/gr-comment-api/gr-comment-api_test.html',
     'diff/gr-coverage-layer/gr-coverage-layer_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
+    'diff/gr-diff-builder/gr-diff-builder-unified_test.html',
     'diff/gr-diff-cursor/gr-diff-cursor_test.html',
     'diff/gr-diff-highlight/gr-annotation_test.html',
     'diff/gr-diff-highlight/gr-diff-highlight_test.html',