Merge "UserPreferencesConverter: use ProtoConverter interface"
diff --git a/java/com/google/gerrit/server/account/AccountLoader.java b/java/com/google/gerrit/server/account/AccountLoader.java
index c260401..42687987 100644
--- a/java/com/google/gerrit/server/account/AccountLoader.java
+++ b/java/com/google/gerrit/server/account/AccountLoader.java
@@ -21,6 +21,9 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
+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.permissions.PermissionBackendException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -90,7 +93,9 @@
   }
 
   public void fill() throws PermissionBackendException {
-    directory.fillAccountInfo(Iterables.concat(created.values(), provided), options);
+    try (TraceTimer timer = TraceContext.newTimer("Fill accounts", Metadata.empty())) {
+      directory.fillAccountInfo(Iterables.concat(created.values(), provided), options);
+    }
   }
 
   public void fill(Collection<? extends AccountInfo> infos) throws PermissionBackendException {
diff --git a/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java b/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
index db030f9..a05baf5 100644
--- a/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
+++ b/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
@@ -19,6 +19,9 @@
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.InternalGroup;
 import com.google.gerrit.server.IdentifiedUser;
+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.inject.AbstractModule;
 import com.google.inject.Module;
 import com.google.inject.Scopes;
@@ -63,41 +66,43 @@
 
   @Override
   public boolean isServiceUser(Account.Id user) {
-    Optional<InternalGroup> maybeGroup = groupCache.get(AccountGroup.nameKey(SERVICE_USERS));
-    if (!maybeGroup.isPresent()) {
+    try (TraceTimer timer = TraceContext.newTimer("isServiceUser", Metadata.empty())) {
+      Optional<InternalGroup> maybeGroup = groupCache.get(AccountGroup.nameKey(SERVICE_USERS));
+      if (!maybeGroup.isPresent()) {
+        return false;
+      }
+      List<AccountGroup.UUID> toTraverse = new ArrayList<>();
+      toTraverse.add(maybeGroup.get().getGroupUUID());
+      Set<AccountGroup.UUID> seen = new HashSet<>();
+      while (!toTraverse.isEmpty()) {
+        InternalGroup currentGroup =
+            groupCache
+                .get(toTraverse.remove(0))
+                .orElseThrow(() -> new IllegalStateException("invalid subgroup"));
+        if (seen.contains(currentGroup.getGroupUUID())) {
+          logger.atFine().log(
+              "Skipping %s because it's a cyclic subgroup", currentGroup.getGroupUUID());
+          continue;
+        }
+        seen.add(currentGroup.getGroupUUID());
+        if (currentGroup.getMembers().contains(user)) {
+          // The user is a member of the 'Service Users' group or a subgroup.
+          return true;
+        }
+        boolean hasExternalSubgroup =
+            currentGroup.getSubgroups().stream().anyMatch(g -> !internalGroupBackend.handles(g));
+        if (hasExternalSubgroup) {
+          // 'Service Users or a subgroup of Service User' contains an external subgroup, so we have
+          // to default to the more expensive evaluation of getting all of the user's group
+          // memberships.
+          return identifiedUserFactory
+              .create(user)
+              .getEffectiveGroups()
+              .contains(maybeGroup.get().getGroupUUID());
+        }
+        toTraverse.addAll(currentGroup.getSubgroups());
+      }
       return false;
     }
-    List<AccountGroup.UUID> toTraverse = new ArrayList<>();
-    toTraverse.add(maybeGroup.get().getGroupUUID());
-    Set<AccountGroup.UUID> seen = new HashSet<>();
-    while (!toTraverse.isEmpty()) {
-      InternalGroup currentGroup =
-          groupCache
-              .get(toTraverse.remove(0))
-              .orElseThrow(() -> new IllegalStateException("invalid subgroup"));
-      if (seen.contains(currentGroup.getGroupUUID())) {
-        logger.atFine().log(
-            "Skipping %s because it's a cyclic subgroup", currentGroup.getGroupUUID());
-        continue;
-      }
-      seen.add(currentGroup.getGroupUUID());
-      if (currentGroup.getMembers().contains(user)) {
-        // The user is a member of the 'Service Users' group or a subgroup.
-        return true;
-      }
-      boolean hasExternalSubgroup =
-          currentGroup.getSubgroups().stream().anyMatch(g -> !internalGroupBackend.handles(g));
-      if (hasExternalSubgroup) {
-        // 'Service Users or a subgroup of Service User' contains an external subgroup, so we have
-        // to default to the more expensive evaluation of getting all of the user's group
-        // memberships.
-        return identifiedUserFactory
-            .create(user)
-            .getEffectiveGroups()
-            .contains(maybeGroup.get().getGroupUUID());
-      }
-      toTraverse.addAll(currentGroup.getSubgroups());
-    }
-    return false;
   }
 }
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 03728a0..d557273 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -437,13 +437,16 @@
   if (!isElementTarget(rootTarget)) return false;
   const tagName = rootTarget.tagName;
   const type = rootTarget.getAttribute('type');
+  const editable = rootTarget.hasAttribute('contenteditable');
 
   if (
+    editable ||
     // Suppress shortcuts on <input> and <textarea>, but not on
     // checkboxes, because we want to enable workflows like 'click
     // mark-reviewed and then press ] to go to the next file'.
     (tagName === 'INPUT' && type !== 'checkbox') ||
     tagName === 'TEXTAREA' ||
+    tagName === 'GR-TEXTAREA' ||
     (e.key === 'Enter' &&
       (tagName === 'A' ||
         tagName === 'BUTTON' ||
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index fe185be..7deae3d 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -309,6 +309,14 @@
       });
     });
 
+    test('suppress shortcut event from <div contenteditable>', async () => {
+      const el = document.createElement('div');
+      el.setAttribute('contenteditable', '');
+      await keyEventOn(el, e => {
+        assert.isTrue(shouldSuppress(e));
+      });
+    });
+
     test('suppress shortcut event from <input>', async () => {
       await keyEventOn(document.createElement('input'), e => {
         assert.isTrue(shouldSuppress(e));
@@ -321,6 +329,12 @@
       });
     });
 
+    test('suppress shortcut event from <gr-textarea>', async () => {
+      await keyEventOn(document.createElement('gr-textarea'), e => {
+        assert.isTrue(shouldSuppress(e));
+      });
+    });
+
     test('do not suppress shortcut event from checkbox <input>', async () => {
       const inputEl = document.createElement('input');
       inputEl.setAttribute('type', 'checkbox');