Merge "Replace `$$` with `shadowRoot.querySelector` for all ids"
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
index 8421e54..f7472b9 100644
--- a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
+++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -15,15 +15,17 @@
 package com.google.gerrit.server.git;
 
 import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
 
 import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import java.io.IOException;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -75,10 +77,9 @@
       return null;
     }
 
-    Map<String, Ref> result;
+    Collection<Ref> result;
     try {
-      result =
-          forProject.filter(ImmutableMap.of(name, ref), getDelegate(), RefFilterOptions.defaults());
+      result = forProject.filter(ImmutableList.of(ref), getDelegate(), RefFilterOptions.defaults());
     } catch (PermissionBackendException e) {
       if (e.getCause() instanceof IOException) {
         throw (IOException) e.getCause();
@@ -91,7 +92,7 @@
 
     Preconditions.checkState(
         result.size() == 1, "Only one element expected, but was: " + result.size());
-    return Iterables.getOnlyElement(result.values());
+    return Iterables.getOnlyElement(result);
   }
 
   @SuppressWarnings("deprecation")
@@ -102,13 +103,13 @@
       return refs;
     }
 
-    Map<String, Ref> result;
+    Collection<Ref> result;
     try {
-      result = forProject.filter(refs, getDelegate(), RefFilterOptions.defaults());
+      result = forProject.filter(refs.values(), getDelegate(), RefFilterOptions.defaults());
     } catch (PermissionBackendException e) {
       throw new IOException("");
     }
-    return result;
+    return result.stream().collect(toMap(Ref::getName, r -> r));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 16bbdaf..47a48b7 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -20,11 +20,10 @@
 import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
 import static com.google.gerrit.entities.RefNames.REFS_USERS_SELF;
 import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toCollection;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
@@ -60,6 +59,7 @@
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -129,7 +129,7 @@
   }
 
   /** Filters given refs and tags by visibility. */
-  Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+  Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
       throws PermissionBackendException {
     logger.atFinest().log(
         "Filter refs for repository %s by visibility (options = %s, refs = %s)",
@@ -145,10 +145,10 @@
 
     // See if we can get away with a single, cheap ref evaluation.
     if (refs.size() == 1) {
-      String refName = Iterables.getOnlyElement(refs.values()).getName();
+      String refName = Iterables.getOnlyElement(refs).getName();
       if (opts.filterMeta() && isMetadata(refName)) {
         logger.atFinest().log("Filter out metadata ref %s", refName);
-        return ImmutableMap.of();
+        return ImmutableList.of();
       }
       if (RefNames.isRefsChanges(refName)) {
         boolean isChangeRefVisisble = canSeeSingleChangeRef(refName);
@@ -157,18 +157,18 @@
           return refs;
         }
         logger.atFinest().log("Filter out non-visible change ref %s", refName);
-        return ImmutableMap.of();
+        return ImmutableList.of();
       }
     }
 
     // Perform an initial ref filtering with all the refs the caller asked for. If we find tags that
     // we have to investigate separately (deferred tags) then perform a reachability check starting
     // from all visible branches (refs/heads/*).
-    Result initialRefFilter = filterRefs(refs, repo, opts);
-    Map<String, Ref> visibleRefs = initialRefFilter.visibleRefs();
+    Result initialRefFilter = filterRefs(new ArrayList<>(refs), repo, opts);
+    List<Ref> visibleRefs = initialRefFilter.visibleRefs();
     if (!initialRefFilter.deferredTags().isEmpty()) {
       try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) {
-        Result allVisibleBranches = filterRefs(getTaggableRefsMap(repo), repo, opts);
+        Result allVisibleBranches = filterRefs(getTaggableRefs(repo), repo, opts);
         checkState(
             allVisibleBranches.deferredTags().isEmpty(),
             "unexpected tags found when filtering refs/heads/* "
@@ -177,12 +177,12 @@
         TagMatcher tags =
             tagCache
                 .get(projectState.getNameKey())
-                .matcher(tagCache, repo, allVisibleBranches.visibleRefs().values());
+                .matcher(tagCache, repo, allVisibleBranches.visibleRefs());
         for (Ref tag : initialRefFilter.deferredTags()) {
           try {
             if (tags.isReachable(tag)) {
               logger.atFinest().log("Include reachable tag %s", tag.getName());
-              visibleRefs.put(tag.getName(), tag);
+              visibleRefs.add(tag);
             } else {
               logger.atFinest().log("Filter out non-reachable tag %s", tag.getName());
             }
@@ -202,7 +202,7 @@
    * separately for later rev-walk-based visibility computation. Tags where visibility is trivial to
    * compute will be returned as part of {@link Result#visibleRefs()}.
    */
-  Result filterRefs(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+  Result filterRefs(List<Ref> refs, Repository repo, RefFilterOptions opts)
       throws PermissionBackendException {
     logger.atFinest().log("Filter refs (refs = %s)", refs);
 
@@ -252,9 +252,9 @@
       identifiedUser = null;
     }
 
-    Map<String, Ref> resultRefs = new HashMap<>();
+    List<Ref> resultRefs = new ArrayList<>(refs.size());
     List<Ref> deferredTags = new ArrayList<>();
-    for (Ref ref : refs.values()) {
+    for (Ref ref : refs) {
       String name = ref.getName();
       Change.Id changeId;
       Account.Id accountId;
@@ -268,7 +268,7 @@
         // Edits are visible only to the owning user, if change is visible.
         if (viewMetadata || visibleEdit(repo, name)) {
           logger.atFinest().log("Include edit ref %s", name);
-          resultRefs.put(name, ref);
+          resultRefs.add(ref);
         } else {
           logger.atFinest().log("Filter out edit ref %s", name);
         }
@@ -276,7 +276,7 @@
         // Change ref is visible only if the change is visible.
         if (viewMetadata || visible(repo, changeId)) {
           logger.atFinest().log("Include change ref %s", name);
-          resultRefs.put(name, ref);
+          resultRefs.add(ref);
         } else {
           logger.atFinest().log("Filter out change ref %s", name);
         }
@@ -284,7 +284,7 @@
         // Account ref is visible only to the corresponding account.
         if (viewMetadata || (accountId.equals(userId) && canReadRef(name))) {
           logger.atFinest().log("Include user ref %s", name);
-          resultRefs.put(name, ref);
+          resultRefs.add(ref);
         } else {
           logger.atFinest().log("Filter out user ref %s", name);
         }
@@ -296,7 +296,7 @@
                 && isGroupOwner(group, identifiedUser, isAdmin)
                 && canReadRef(name))) {
           logger.atFinest().log("Include group ref %s", name);
-          resultRefs.put(name, ref);
+          resultRefs.add(ref);
         } else {
           logger.atFinest().log("Filter out group ref %s", name);
         }
@@ -312,7 +312,7 @@
           // the regular Git tree that users interact with, not on any of the Gerrit trees, so this
           // is a negligible risk.
           logger.atFinest().log("Include tag ref %s because user has read on refs/*", name);
-          resultRefs.put(name, ref);
+          resultRefs.add(ref);
         } else {
           // If its a tag, consider it later.
           if (ref.getObjectId() != null) {
@@ -326,7 +326,7 @@
         // Sequences are internal database implementation details.
         if (viewMetadata) {
           logger.atFinest().log("Include sequence ref %s", name);
-          resultRefs.put(name, ref);
+          resultRefs.add(ref);
         } else {
           logger.atFinest().log("Filter out sequence ref %s", name);
         }
@@ -336,7 +336,7 @@
         // users.
         if (viewMetadata) {
           logger.atFinest().log("Include external IDs branch %s", name);
-          resultRefs.put(name, ref);
+          resultRefs.add(ref);
         } else {
           logger.atFinest().log("Filter out external IDs branch %s", name);
         }
@@ -346,13 +346,13 @@
         // not symbolic then getLeaf() is a no-op returning ref itself.
         logger.atFinest().log(
             "Include ref %s because its leaf %s is readable", name, ref.getLeaf().getName());
-        resultRefs.put(name, ref);
+        resultRefs.add(ref);
       } else if (isRefsUsersSelf(ref)) {
         // viewMetadata allows to see all account refs, hence refs/users/self should be included as
         // well
         if (viewMetadata) {
           logger.atFinest().log("Include ref %s", REFS_USERS_SELF);
-          resultRefs.put(name, ref);
+          resultRefs.add(ref);
         }
       } else {
         logger.atFinest().log("Filter out ref %s", name);
@@ -370,38 +370,39 @@
    * <p>We exclude symbolic refs because their target will be included and this will suffice for
    * computing reachability.
    */
-  private static Map<String, Ref> getTaggableRefsMap(Repository repo)
-      throws PermissionBackendException {
+  private static List<Ref> getTaggableRefs(Repository repo) throws PermissionBackendException {
     try {
-      return repo.getRefDatabase().getRefs().stream()
+      List<Ref> allRefs = repo.getRefDatabase().getRefs();
+      return allRefs.stream()
           .filter(
               r ->
                   !RefNames.isGerritRef(r.getName())
                       && !r.getName().startsWith(RefNames.REFS_TAGS)
                       && !r.isSymbolic())
-          .collect(toMap(Ref::getName, r -> r));
+          // Don't use the default Java Collections.toList() as that is not size-aware and would
+          // expand an array list as new elements are added. Instead, provide a list that has the
+          // right size. This spares incremental list expansion which is quadratic in complexity.
+          .collect(toCollection(() -> new ArrayList<>(allRefs.size())));
     } catch (IOException e) {
       throw new PermissionBackendException(e);
     }
   }
 
-  private Map<String, Ref> fastHideRefsMetaConfig(Map<String, Ref> refs)
-      throws PermissionBackendException {
-    if (refs.containsKey(REFS_CONFIG) && !canReadRef(REFS_CONFIG)) {
-      Map<String, Ref> r = new HashMap<>(refs);
-      r.remove(REFS_CONFIG);
-      return r;
+  private List<Ref> fastHideRefsMetaConfig(List<Ref> refs) throws PermissionBackendException {
+    if (!canReadRef(REFS_CONFIG)) {
+      return refs.stream()
+          .filter(r -> !r.getName().equals(REFS_CONFIG))
+          // Don't use the default Java Collections.toList() as that is not size-aware and would
+          // expand an array list as new elements are added. Instead, provide a list that has the
+          // right size. This spares incremental list expansion which is quadratic in complexity.
+          .collect(toCollection(() -> new ArrayList<>(refs.size())));
     }
     return refs;
   }
 
-  private Map<String, Ref> addUsersSelfSymref(Repository repo, Map<String, Ref> refs)
+  private List<Ref> addUsersSelfSymref(Repository repo, List<Ref> refs)
       throws PermissionBackendException {
     if (user.isIdentifiedUser()) {
-      // User self symref is already there
-      if (refs.containsKey(REFS_USERS_SELF)) {
-        return refs;
-      }
       String refName = RefNames.refsUsers(user.getAccountId());
       try {
         Ref r = repo.exactRef(refName);
@@ -411,8 +412,8 @@
         }
 
         SymbolicRef s = new SymbolicRef(REFS_USERS_SELF, r);
-        refs = new HashMap<>(refs);
-        refs.put(s.getName(), s);
+        refs = new ArrayList<>(refs);
+        refs.add(s);
         logger.atFinest().log("Added %s as alias for user ref %s", REFS_USERS_SELF, refName);
       } catch (IOException e) {
         throw new PermissionBackendException(e);
@@ -614,7 +615,7 @@
   @AutoValue
   abstract static class Result {
     /** Subset of the refs passed into the computation that is visible to the user. */
-    abstract Map<String, Ref> visibleRefs();
+    abstract List<Ref> visibleRefs();
 
     /**
      * List of tags where we couldn't figure out visibility in the first pass and need to do an
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 0800d6b..2344781 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend.WithUser;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.util.Collection;
-import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -142,7 +141,7 @@
     }
 
     @Override
-    public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+    public Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
         throws PermissionBackendException {
       throw new PermissionBackendException(message, cause);
     }
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 9149a1d..d831ab6 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.permissions;
 
 import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toMap;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.auto.value.AutoValue;
@@ -41,7 +40,6 @@
 import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Ref;
@@ -329,33 +327,18 @@
     public abstract BooleanCondition testCond(CoreOrPluginProjectPermission perm);
 
     /**
-     * Filter a map of references by visibility.
-     *
-     * @param refs a map of references to filter.
-     * @param repo an open {@link Repository} handle for this instance's project
-     * @param opts further options for filtering.
-     * @return a partition of the provided refs that are visible to the user that this instance is
-     *     scoped to.
-     * @throws PermissionBackendException if failure consulting backend configuration.
-     */
-    public abstract Map<String, Ref> filter(
-        Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
-        throws PermissionBackendException;
-
-    /**
      * Filter a list of references by visibility.
      *
-     * @param refs a list of references to filter.
+     * @param refs a collection of references to filter.
      * @param repo an open {@link Repository} handle for this instance's project
      * @param opts further options for filtering.
      * @return a partition of the provided refs that are visible to the user that this instance is
      *     scoped to.
      * @throws PermissionBackendException if failure consulting backend configuration.
      */
-    public Map<String, Ref> filter(List<Ref> refs, Repository repo, RefFilterOptions opts)
-        throws PermissionBackendException {
-      return filter(refs.stream().collect(toMap(Ref::getName, r -> r, (a, b) -> b)), repo, opts);
-    }
+    public abstract Collection<Ref> filter(
+        Collection<Ref> refs, Repository repo, RefFilterOptions opts)
+        throws PermissionBackendException;
   }
 
   /** Options for filtering refs using {@link ForProject}. */
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index cc3b666..145e0b6 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -404,7 +404,7 @@
     }
 
     @Override
-    public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+    public Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
         throws PermissionBackendException {
       if (refFilter == null) {
         refFilter = refFilterFactory.create(ProjectControl.this);
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 6d28646a..57e9a7e 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -26,8 +26,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -56,7 +56,7 @@
   public boolean fromRefs(
       Project.NameKey project, Repository repo, RevCommit commit, List<Ref> refs) {
     try (RevWalk rw = new RevWalk(repo)) {
-      Map<String, Ref> filtered =
+      Collection<Ref> filtered =
           permissionBackend
               .currentUser()
               .project(project)
@@ -68,7 +68,7 @@
           TraceContext.newTimer(
               "IncludedInResolver.includedInAny",
               Metadata.builder().projectName(project.get()).resourceCount(refs.size()).build())) {
-        return IncludedInResolver.includedInAny(repo, rw, commit, filtered.values());
+        return IncludedInResolver.includedInAny(repo, rw, commit, filtered);
       }
     } catch (IOException | PermissionBackendException e) {
       logger.atSevere().withCause(e).log(
diff --git a/java/com/google/gerrit/server/project/RefUtil.java b/java/com/google/gerrit/server/project/RefUtil.java
index 1dac751..5d6379a 100644
--- a/java/com/google/gerrit/server/project/RefUtil.java
+++ b/java/com/google/gerrit/server/project/RefUtil.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -46,16 +47,15 @@
     try {
       ObjectId revid = repo.resolve(baseRevision);
       if (revid == null) {
-        throw new InvalidRevisionException();
+        throw new InvalidRevisionException(baseRevision);
       }
       return revid;
     } catch (IOException err) {
       logger.atSevere().withCause(err).log(
           "Cannot resolve \"%s\" in project \"%s\"", baseRevision, projectName.get());
-      throw new InvalidRevisionException();
+      throw new InvalidRevisionException(baseRevision);
     } catch (RevisionSyntaxException err) {
-      logger.atSevere().withCause(err).log("Invalid revision syntax \"%s\"", baseRevision);
-      throw new InvalidRevisionException();
+      throw new InvalidRevisionException(baseRevision);
     }
   }
 
@@ -66,7 +66,7 @@
       try {
         rw.markStart(rw.parseCommit(revid));
       } catch (IncorrectObjectTypeException err) {
-        throw new InvalidRevisionException();
+        throw new InvalidRevisionException(revid.name());
       }
       RefDatabase refDb = repo.getRefDatabase();
       Iterable<Ref> refs =
@@ -86,11 +86,11 @@
       rw.checkConnectivity();
       return rw;
     } catch (IncorrectObjectTypeException | MissingObjectException err) {
-      throw new InvalidRevisionException();
+      throw new InvalidRevisionException(revid.name());
     } catch (IOException err) {
       logger.atSevere().withCause(err).log(
           "Repository \"%s\" may be corrupt; suggest running git fsck", repo.getDirectory());
-      throw new InvalidRevisionException();
+      throw new InvalidRevisionException(revid.name());
     }
   }
 
@@ -125,8 +125,8 @@
 
     public static final String MESSAGE = "Invalid Revision";
 
-    InvalidRevisionException() {
-      super(MESSAGE);
+    InvalidRevisionException(@Nullable String invalidRevision) {
+      super(MESSAGE + ": " + invalidRevision);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index 56948c1..67213c5 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.entities.RefNames.isConfigRef;
 
+import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
@@ -93,7 +94,10 @@
     if (input.ref != null && !ref.equals(input.ref)) {
       throw new BadRequestException("ref must match URL");
     }
-    if (input.revision == null) {
+    if (input.revision != null) {
+      input.revision = input.revision.trim();
+    }
+    if (Strings.isNullOrEmpty(input.revision)) {
       input.revision = Constants.HEAD;
     }
     while (ref.startsWith("/")) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index dca6e9a..8fdf5e4 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -88,7 +88,10 @@
     if (input.ref != null && !ref.equals(input.ref)) {
       throw new BadRequestException("ref must match URL");
     }
-    if (input.revision == null) {
+    if (input.revision != null) {
+      input.revision = input.revision.trim();
+    }
+    if (Strings.isNullOrEmpty(input.revision)) {
       input.revision = Constants.HEAD;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 36cc1ac..8cea7f5 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -41,8 +41,8 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -127,10 +127,10 @@
         permissionBackend.currentUser().project(resource.getNameKey());
     try (Repository repo = getRepository(resource.getNameKey());
         RevWalk rw = new RevWalk(repo)) {
-      Map<String, Ref> all =
+      Collection<Ref> all =
           visibleTags(
               resource.getNameKey(), repo, repo.getRefDatabase().getRefsByPrefix(Constants.R_TAGS));
-      for (Ref ref : all.values()) {
+      for (Ref ref : all) {
         tags.add(
             createTagInfo(perm.ref(ref.getName()), ref, rw, resource.getProjectState(), links));
       }
@@ -223,7 +223,7 @@
     }
   }
 
-  private Map<String, Ref> visibleTags(Project.NameKey project, Repository repo, List<Ref> tags)
+  private Collection<Ref> visibleTags(Project.NameKey project, Repository repo, List<Ref> tags)
       throws PermissionBackendException {
     return permissionBackend
         .currentUser()
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 8946688..b4a2b42 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -35,7 +35,7 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.util.Map;
+import java.util.Collection;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Ref;
@@ -89,14 +89,14 @@
     try (Repository repo = repoManager.openRepository(projectName);
         ManualRequestContext ctx = requestContext.openAs(userAccountId)) {
       try {
-        Map<String, Ref> refsMap =
+        Collection<Ref> refsMap =
             permissionBackend
                 .user(ctx.getUser())
                 .project(projectName)
                 .filter(repo.getRefDatabase().getRefs(), repo, RefFilterOptions.defaults());
 
-        for (String ref : refsMap.keySet()) {
-          if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) {
+        for (Ref ref : refsMap) {
+          if (!onlyRefsHeads || ref.getName().startsWith(RefNames.REFS_HEADS)) {
             stdout.println(ref);
           }
         }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index b375f22..bc441ba 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -1478,11 +1478,9 @@
   public void refsUsersSelfIsAdvertised() throws Exception {
     try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
       assertThat(
-              permissionBackend
-                  .currentUser()
-                  .project(allUsers)
-                  .filter(ImmutableList.of(), allUsersRepo, RefFilterOptions.defaults())
-                  .keySet())
+              permissionBackend.currentUser().project(allUsers)
+                  .filter(ImmutableList.of(), allUsersRepo, RefFilterOptions.defaults()).stream()
+                  .map(Ref::getName))
           .containsExactly(RefNames.REFS_USERS_SELF);
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index abfc23d..7ecbe69 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -179,15 +179,15 @@
     assertThat(toChangeNumbers(query("is:abandoned"))).containsExactly(id4);
   }
 
+  /**
+   * When indexMergeable is disabled then the abandonIfMergeable option is ineffective and the auto
+   * abandon behaves as though it were set to its default value (true).
+   */
   @Test
   @UseClockStep
   @GerritConfig(name = "changeCleanup.abandonAfter", value = "1w")
   @GerritConfig(name = "changeCleanup.abandonIfMergeable", value = "false")
   @GerritConfig(name = "index.change.indexMergeable", value = "false")
-  /**
-   * When indexMergeable is disabled then the abandonIfMergeable option is ineffective and the auto
-   * abandon behaves as though it were set to its default value (true).
-   */
   public void abandonedIfMergeableWhenMergeableOperatorIsDisabled() throws Exception {
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
 
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index d1d197b..9e44753 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance.git;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
@@ -24,10 +25,8 @@
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toMap;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -59,12 +58,10 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
 import java.util.function.Predicate;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.junit.TestRepository;
@@ -1365,15 +1362,18 @@
     expectedAllRefs.addAll(expectedMetaRefs);
 
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      Map<String, Ref> all = getAllRefs(repo);
-
       PermissionBackend.ForProject forProject = newFilter(allUsers, admin);
-      assertThat(forProject.filter(all, repo, RefFilterOptions.defaults()).keySet())
+      assertThat(
+              names(
+                  forProject.filter(
+                      repo.getRefDatabase().getRefs(), repo, RefFilterOptions.defaults())))
           .containsExactlyElementsIn(expectedAllRefs);
       assertThat(
-              forProject
-                  .filter(all, repo, RefFilterOptions.builder().setFilterMeta(true).build())
-                  .keySet())
+              names(
+                  forProject.filter(
+                      repo.getRefDatabase().getRefs(),
+                      repo,
+                      RefFilterOptions.builder().setFilterMeta(true).build())))
           .containsExactlyElementsIn(expectedNonMetaRefs);
     }
   }
@@ -1384,8 +1384,8 @@
     String patchSetRef = change.getPatchSetId().toRefName();
     try (AutoCloseable ignored = disableChangeIndex();
         Repository repo = repoManager.openRepository(project)) {
-      Map<String, Ref> singleRef = ImmutableMap.of(patchSetRef, repo.exactRef(patchSetRef));
-      Map<String, Ref> filteredRefs =
+      Collection<Ref> singleRef = ImmutableList.of(repo.exactRef(patchSetRef));
+      Collection<Ref> filteredRefs =
           permissionBackend
               .user(user(admin))
               .project(project)
@@ -1482,8 +1482,7 @@
     return AccountGroup.uuid(gApi.groups().create(groupInput).get().id);
   }
 
-  private static Map<String, Ref> getAllRefs(Repository repo) throws IOException {
-    return repo.getRefDatabase().getRefs().stream()
-        .collect(toMap(Ref::getName, Function.identity()));
+  private static Collection<String> names(Collection<Ref> refs) {
+    return refs.stream().map(Ref::getName).collect(toImmutableList());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index e5ef5ba..85d383e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -36,9 +36,11 @@
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.inject.Inject;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -146,6 +148,116 @@
         "Not allowed to create group branch.");
   }
 
+  @Test
+  public void createWithRevision() throws Exception {
+    RevCommit revision = projectOperations.project(project).getHead("master");
+
+    // update master so that points to a different revision than the revision on which we create the
+    // new branch
+    pushTo("refs/heads/master");
+    assertThat(projectOperations.project(project).getHead("master")).isNotEqualTo(revision);
+
+    BranchInput input = new BranchInput();
+    input.revision = revision.name();
+    BranchInfo created = branch(testBranch).create(input).get();
+    assertThat(created.ref).isEqualTo(testBranch.branch());
+    assertThat(created.revision).isEqualTo(revision.name());
+    assertThat(projectOperations.project(project).getHead(testBranch.branch())).isEqualTo(revision);
+  }
+
+  @Test
+  public void createWithoutSpecifyingRevision() throws Exception {
+    // If revision is not specified, the branch is created based on HEAD, which points to master.
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
+
+    BranchInput input = new BranchInput();
+    input.revision = null;
+    BranchInfo created = branch(testBranch).create(input).get();
+    assertThat(created.ref).isEqualTo(testBranch.branch());
+    assertThat(created.revision).isEqualTo(expectedRevision.name());
+    assertThat(projectOperations.project(project).getHead(testBranch.branch()))
+        .isEqualTo(expectedRevision);
+  }
+
+  @Test
+  public void createWithEmptyRevision() throws Exception {
+    // If revision is not specified, the branch is created based on HEAD, which points to master.
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
+
+    BranchInput input = new BranchInput();
+    input.revision = "";
+    BranchInfo created = branch(testBranch).create(input).get();
+    assertThat(created.ref).isEqualTo(testBranch.branch());
+    assertThat(created.revision).isEqualTo(expectedRevision.name());
+    assertThat(projectOperations.project(project).getHead(testBranch.branch()))
+        .isEqualTo(expectedRevision);
+  }
+
+  @Test
+  public void createRevisionIsTrimmed() throws Exception {
+    RevCommit revision = projectOperations.project(project).getHead("master");
+
+    BranchInput input = new BranchInput();
+    input.revision = "\t" + revision.name();
+    BranchInfo created = branch(testBranch).create(input).get();
+    assertThat(created.ref).isEqualTo(testBranch.branch());
+    assertThat(created.revision).isEqualTo(revision.name());
+    assertThat(projectOperations.project(project).getHead(testBranch.branch())).isEqualTo(revision);
+  }
+
+  @Test
+  public void createWithBranchNameAsRevision() throws Exception {
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
+
+    BranchInput input = new BranchInput();
+    input.revision = "master";
+    BranchInfo created = branch(testBranch).create(input).get();
+    assertThat(created.ref).isEqualTo(testBranch.branch());
+    assertThat(created.revision).isEqualTo(expectedRevision.name());
+    assertThat(projectOperations.project(project).getHead(testBranch.branch()))
+        .isEqualTo(expectedRevision);
+  }
+
+  @Test
+  public void createWithFullBranchNameAsRevision() throws Exception {
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
+
+    BranchInput input = new BranchInput();
+    input.revision = "refs/heads/master";
+    BranchInfo created = branch(testBranch).create(input).get();
+    assertThat(created.ref).isEqualTo(testBranch.branch());
+    assertThat(created.revision).isEqualTo(expectedRevision.name());
+    assertThat(projectOperations.project(project).getHead(testBranch.branch()))
+        .isEqualTo(expectedRevision);
+  }
+
+  @Test
+  public void cannotCreateWithNonExistingBranchNameAsRevision() throws Exception {
+    assertCreateFails(
+        testBranch,
+        "refs/heads/non-existing",
+        BadRequestException.class,
+        "invalid revision \"refs/heads/non-existing\"");
+  }
+
+  @Test
+  public void cannotCreateWithNonExistingRevision() throws Exception {
+    assertCreateFails(
+        testBranch,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+        BadRequestException.class,
+        "invalid revision \"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\"");
+  }
+
+  @Test
+  public void cannotCreateWithInvalidRevision() throws Exception {
+    assertCreateFails(
+        testBranch,
+        "invalid\trevision",
+        BadRequestException.class,
+        "invalid revision \"invalid\trevision\"");
+  }
+
   private void blockCreateReference() throws Exception {
     projectOperations
         .project(project)
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 3d1a148..3becb81 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -41,6 +41,7 @@
 import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.util.List;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 @NoHttpd
@@ -357,6 +358,53 @@
     assertThat(thrown).hasMessageThat().contains("Invalid base revision");
   }
 
+  @Test
+  public void noBaseRevision() throws Exception {
+    grantTagPermissions();
+
+    // If revision is not specified, the tag is created based on HEAD, which points to master.
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
+
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.revision = null;
+
+    TagInfo result = tag(input.ref).create(input).get();
+    assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
+    assertThat(result.revision).isEqualTo(expectedRevision.name());
+  }
+
+  @Test
+  public void emptyBaseRevision() throws Exception {
+    grantTagPermissions();
+
+    // If revision is not specified, the tag is created based on HEAD, which points to master.
+    RevCommit expectedRevision = projectOperations.project(project).getHead("master");
+
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.revision = "";
+
+    TagInfo result = tag(input.ref).create(input).get();
+    assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
+    assertThat(result.revision).isEqualTo(expectedRevision.name());
+  }
+
+  @Test
+  public void baseRevisionIsTrimmed() throws Exception {
+    grantTagPermissions();
+
+    RevCommit revision = projectOperations.project(project).getHead("master");
+
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.revision = "\t" + revision.name();
+
+    TagInfo result = tag(input.ref).create(input).get();
+    assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
+    assertThat(result.revision).isEqualTo(revision.name());
+  }
+
   private void assertTagList(FluentIterable<String> expected, List<TagInfo> actual)
       throws Exception {
     assertThat(actual).hasSize(expected.size());
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index 955930b..6521166 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -30,6 +30,11 @@
     "//lib:jgit",
 ]
 
+HTTP_TEST_DEPS = [
+    "//lib/httpcomponents:httpasyncclient",
+    "//lib/httpcomponents:httpclient",
+]
+
 QUERY_TESTS_DEP = "//javatests/com/google/gerrit/server/query/%s:abstract_query_tests"
 
 TYPES = [
@@ -66,7 +71,7 @@
     size = "large",
     srcs = [src],
     tags = ELASTICSEARCH_TAGS,
-    deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name],
+    deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name] + HTTP_TEST_DEPS,
 ) for name, src in ELASTICSEARCH_TESTS_V6.items()]
 
 [junit_tests(
@@ -74,10 +79,7 @@
     size = "large",
     srcs = [src],
     tags = ELASTICSEARCH_TAGS,
-    deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name] + [
-        "//lib/httpcomponents:httpasyncclient",
-        "//lib/httpcomponents:httpclient",
-    ],
+    deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name] + HTTP_TEST_DEPS,
 ) for name, src in ELASTICSEARCH_TESTS_V7.items()]
 
 junit_tests(
diff --git a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
index 2485613..de23ef4 100644
--- a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
+++ b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import java.util.Collection;
-import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -85,7 +84,7 @@
     }
 
     @Override
-    public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+    public Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
         throws PermissionBackendException {
       throw new UnsupportedOperationException("not implemented");
     }
diff --git a/modules/jgit b/modules/jgit
index 0356613..a7e454b 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 0356613f48ebee2e3d2d65780e71d9e0b43a752e
+Subproject commit a7e454bc51d359c2d46b19fd559f770cad8fd7d4
diff --git a/plugins/gitiles b/plugins/gitiles
index 0f8de56..3531010 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 0f8de56cc0f68047e38ac62e11271dbc6d76aa29
+Subproject commit 3531010e04d9d548fe1fd93662ca85ae25d4a9a6
diff --git a/plugins/replication b/plugins/replication
index 4689b41..c72a720 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 4689b419eab61ea204daf2dfca47296667ac317c
+Subproject commit c72a72058ff03d9d83b09882a9044a75467c4b1f
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
index 46fa3cf..cb87bd9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -247,7 +247,7 @@
     <div id="container" class="container">
       <div class="header" id="header" on-click="_handleToggleCollapsed">
         <div class="headerLeft">
-          <span class="authorName">[[comment.author.name]]</span>
+          <span class="authorName">[[_computeAuthorName(comment)]]</span>
           <span class="draftLabel">DRAFT</span>
           <gr-tooltip-content class="draftTooltip"
               has-tooltip
@@ -258,6 +258,13 @@
         <div class="headerMiddle">
           <span class="collapsedContent">[[comment.message]]</span>
         </div>
+        <div hidden$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message">
+          <div class="runIdInformation">
+            <a class="robotRunLink" href$="[[comment.url]]">
+              <span class="robotRun link">Run Details</span>
+            </a>
+          </div>
+        </div>
         <gr-button
             id="deleteBtn"
             link
@@ -284,10 +291,9 @@
         </div>
       </div>
       <div class="body">
-        <template is="dom-if" if="[[comment.robot_id]]">
+        <template is="dom-if" if="[[isRobotComment]]">
           <div class="robotId" hidden$="[[collapsed]]">
-            <iron-icon class="robotIcon" icon="gr-icons:robot"></iron-icon>
-            [[comment.robot_id]]
+            [[comment.author.name]]
           </div>
         </template>
         <template is="dom-if" if="[[editing]]">
@@ -306,19 +312,6 @@
             content="[[comment.message]]"
             no-trailing-margin="[[!comment.__draft]]"
             config="[[projectConfig.commentlinks]]"></gr-formatted-text>
-        <div hidden$="[[!comment.robot_run_id]]" class="message">
-          <div class="runIdInformation" hidden$="[[collapsed]]">
-            Run ID:
-            <template is="dom-if" if="[[comment.url]]">
-              <a class="robotRunLink" href$="[[comment.url]]">
-                <span class="robotRun link">[[comment.robot_run_id]]</span>
-              </a>
-            </template>
-            <template is="dom-if" if="[[!comment.url]]">
-              <span class="robotRun text">[[comment.robot_run_id]]</span>
-            </template>
-          </div>
-        </div>
         <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
           <div class="action resolve hideOnPublished">
             <label>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 266280f..7951d20 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -75,7 +75,7 @@
     static get properties() {
       return {
         changeNum: String,
-        /** @type {?} */
+        /** @type {!Gerrit.Comment} */
         comment: {
           type: Object,
           notify: true,
@@ -674,6 +674,19 @@
       return overlay.open();
     }
 
+    _computeAuthorName(comment) {
+      if (!comment) return '';
+      if (comment.robot_id) {
+        return comment.robot_id;
+      }
+      return comment.author && comment.author.name;
+    }
+
+    _computeHideRunDetails(comment, collapsed) {
+      if (!comment) return true;
+      return !(comment.robot_id && comment.url && !collapsed);
+    }
+
     _closeOverlay(overlay) {
       Polymer.dom(Gerrit.getRootElement()).removeChild(overlay);
       overlay.close();
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index de75b40..ad8b5e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -456,17 +456,13 @@
       element.editing = false;
       element.collapsed = false;
       flushAsynchronousOperations();
-      assert.isNotOk(element.$$('.robotRun.link'));
-      assert.notEqual(getComputedStyle(element.$$('.robotRun.text')).display,
-          'none');
+      assert.isTrue(element.$$('.robotRun.link').textContent === 'Run Details');
 
       // A robot comment with run ID and url should display a link.
       element.set(['comment', 'url'], '/path/to/run');
       flushAsynchronousOperations();
       assert.notEqual(getComputedStyle(element.$$('.robotRun.link')).display,
           'none');
-      assert.equal(getComputedStyle(element.$$('.robotRun.text')).display,
-          'none');
     });
 
     test('collapsible drafts', () => {
@@ -527,6 +523,37 @@
           'header middle content is not visible');
     });
 
+    test('robot comment layout', () => {
+      const comment = Object.assign({
+        robot_id: 'happy_robot_id',
+        url: '/robot/comment',
+        author: {
+          name: 'Happy Robot',
+        },
+      }, element.comment);
+      element.comment = comment;
+      element.collapsed = false;
+      flushAsynchronousOperations();
+
+      let runIdMessage;
+      runIdMessage = element.$$('.runIdMessage');
+      assert.isFalse(runIdMessage.hidden);
+
+      const runDetailsLink = element.$$('.robotRunLink');
+      assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1);
+
+      const robotServiceName = element.$$('.authorName');
+      assert.isTrue(robotServiceName.textContent === 'happy_robot_id');
+
+      const authorName = element.$$('.robotId');
+      assert.isTrue(authorName.innerText === 'Happy Robot');
+
+      element.collapsed = true;
+      flushAsynchronousOperations();
+      runIdMessage = element.$$('.runIdMessage');
+      assert.isTrue(runIdMessage.hidden);
+    });
+
     test('draft creation/cancellation', done => {
       assert.isFalse(element.editing);
       MockInteractions.tap(element.$$('.edit'));
diff --git a/polygerrit-ui/app/types/types.js b/polygerrit-ui/app/types/types.js
index 8dd65cb..3b91407 100644
--- a/polygerrit-ui/app/types/types.js
+++ b/polygerrit-ui/app/types/types.js
@@ -273,4 +273,27 @@
  *    makeSuggestionItem: function(Object): Gerrit.GrSuggestionItem,
  * }}
  */
-Gerrit.GrSuggestionsProvider;
\ No newline at end of file
+Gerrit.GrSuggestionsProvider;
+
+/**
+ * @typedef {{
+ *  patch_set: ?number,
+ *  id: ?string,
+ *  path: ?Object,
+ *  side: ?string,
+ *  parent: ?number,
+ *  line: ?Object,
+ *  in_reply_to: ?string,
+ *  message: ?Object,
+ *  updated: ?string,
+ *  author: ?Object,
+ *  tag: ?Object,
+ *  unresolved: ?boolean,
+ *  robot_id: ?string,
+ *  robot_run_id: ?string,
+ *  url: ?string,
+ *  properties: ?Object,
+ *  fix_suggestions: ?Object,
+ *  }}
+ */
+Gerrit.Comment;