Merge "Merge branch 'stable-3.3' into master"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 529718a..38720fe 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -869,6 +869,11 @@
 private changes (even without having the `View Private Changes` access
 right assigned).
 
+**NOTE**: If link:config-gerrit.html#auth.skipFullRefEvaluationIfAllRefsAreVisible[
+auth.skipFullRefEvaluationIfAllRefsAreVisible] is `true` (which is the case by
+default) privates changes and all change edit refs are also visible to users
+that have read access on `refs/*`.
+
 [[category_toggle_work_in_progress_state]]
 === Toggle Work In Progress state
 
diff --git a/Documentation/concept-patch-sets.txt b/Documentation/concept-patch-sets.txt
index 8609afd..274fbb0 100644
--- a/Documentation/concept-patch-sets.txt
+++ b/Documentation/concept-patch-sets.txt
@@ -89,7 +89,7 @@
 set description does not become a part of the project's history.
 
 To add a patch set description, click *Add a patch set description*, located in
-the file list.
+the file list, or provide it link:user-upload.html#patch_set_description[on upload].
 
 GERRIT
 ------
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index e7c0d13..ac5e3b7 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -684,8 +684,11 @@
 
 [[auth.skipFullRefEvaluationIfAllRefsAreVisible]]auth.skipFullRefEvaluationIfAllRefsAreVisible::
 +
-Whether to skip the full ref visibility checks as a performance shortcut when all refs are
-visible to a user. Full ref filtering would filter out things like pending edits.
+Whether to skip the full ref visibility checks as a performance shortcut when a
+user has READ permission for all refs.
++
+The full ref filtering would filter out refs for pending edits, private changes
+and auto merge commits.
 +
 By default, true.
 
@@ -742,6 +745,24 @@
 +
 Default is false.
 
+[[cache.openFiles]]cache.openFiles::
++
+The number of file descriptors to add to the limit set by the Gerrit daemon.
++
+Persistent caches are stored on the file system and as such participate in the
+file descriptors utilization. The number of file descriptors can vary depending
+on the cache configuration and the specific backend used.
++
+The additional file descriptors required by the cache should be accounted for
+via this setting, so that the Gerrit daemon can adjust the ulimit accordingly.
++
+If you increase this to a larger setting you may need to also adjust
+the ulimit on file descriptors for the host JVM, as Gerrit needs
+additional file descriptors available for network sockets and other
+repository data manipulation.
++
+Default is 0.
+
 [[cache.name.maxAge]]cache.<name>.maxAge::
 +
 Maximum age to keep an entry in the cache. Entries are removed from
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 3040348..8a95bab 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -66,6 +66,11 @@
 * `caches/disk_hit_ratio`: Disk hit ratio for persistent cache.
 * `caches/refresh_count`: The number of refreshes per cache with an indicator if a reload was necessary.
 
+Cache disk metrics are expensive to compute on larger installations and are not
+computed by default. They can be enabled via the
+link:config.gerrit.html#cache.enableDiskStatMetrics[`cache.enableDiskStatMetrics`]
+setting.
+
 === Change
 
 * `change/submit_rule_evaluation`: Latency for evaluating submit rules on a change.
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 926aa71..cdaf155 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -315,11 +315,11 @@
 preference is set so the default behavior is to create `work-in-progress`
 changes, this can be overridden with the `ready` option.
 
-[[message]]
-==== Message
+[[patch_set_description]]
+==== Patch Set Description
 
-A comment message can be applied to the change by using the `message` (or `m`)
-option:
+A link:concept-patch-sets.html#_description[patch set description] can be
+applied by using the `message` (or `m`) option:
 
 ----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%m=This_is_a_rebase_on_master%21
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index 400861c..5595bc7 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -288,10 +288,16 @@
    * Whether the ref is managed by Gerrit. Covers all Gerrit-internal refs like refs/cache-automerge
    * and refs/meta as well as refs/changes. Does not cover user-created refs like branches or custom
    * ref namespaces like refs/my-company.
+   *
+   * <p>Any ref for which this method evaluates to true will be served to users who have the {@code
+   * ACCESS_DATABASE} capability.
+   *
+   * <p><b>Caution</b>Any ref not in this list will be served if the user was granted a READ
+   * permission on it using Gerrit's permission model.
    */
   public static boolean isGerritRef(String ref) {
     return ref.startsWith(REFS_CHANGES)
-        || ref.startsWith(REFS_META)
+        || ref.startsWith(REFS_EXTERNAL_IDS)
         || ref.startsWith(REFS_CACHE_AUTOMERGE)
         || ref.startsWith(REFS_DRAFT_COMMENTS)
         || ref.startsWith(REFS_DELETED_GROUPS)
@@ -299,7 +305,8 @@
         || ref.startsWith(REFS_GROUPS)
         || ref.startsWith(REFS_GROUPNAMES)
         || ref.startsWith(REFS_USERS)
-        || ref.startsWith(REFS_STARRED_CHANGES);
+        || ref.startsWith(REFS_STARRED_CHANGES)
+        || ref.startsWith(REFS_REJECT_COMMITS);
   }
 
   static Integer parseShardedRefPart(String name) {
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index af00b20..1ad94be 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -391,11 +391,7 @@
   }
 
   private SMTPClient open() throws EmailException {
-    final AuthSMTPClient client = new AuthSMTPClient(UTF_8.name());
-
-    if (smtpEncryption == Encryption.SSL) {
-      client.enableSSL(sslVerify);
-    }
+    final AuthSMTPClient client = new AuthSMTPClient(smtpEncryption == Encryption.SSL, sslVerify);
 
     client.setConnectTimeout(connectTimeout);
     try {
@@ -411,7 +407,7 @@
       }
 
       if (smtpEncryption == Encryption.TLS) {
-        if (!client.startTLS(smtpHost, smtpPort, sslVerify)) {
+        if (!client.execTLS()) {
           throw new EmailException("SMTP server does not support TLS");
         }
         if (!client.login()) {
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 0b4828b..37c773a 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -61,11 +61,12 @@
   }
 
   /** Can this user see this change? */
-  private boolean isVisible(ChangeData cd) {
-    if (getChange().isPrivate() && !isPrivateVisible(cd)) {
+  boolean isVisible() {
+    if (getChange().isPrivate() && !isPrivateVisible(changeData)) {
       return false;
     }
-    return refControl.isVisible();
+    // Does the user have READ permission on the destination?
+    return refControl.asForRef().testOrFalse(RefPermission.READ);
   }
 
   /** Can this user abandon this change? */
@@ -201,17 +202,13 @@
 
     private ForChangeImpl() {}
 
-    private ChangeData changeData() {
-      return changeData;
-    }
-
     @Override
     public String resourcePath() {
       if (resourcePath == null) {
         resourcePath =
             String.format(
                 "/projects/%s/+changes/%s",
-                getProjectControl().getProjectState().getName(), changeData().getId().get());
+                getProjectControl().getProjectState().getName(), changeData.getId().get());
       }
       return resourcePath;
     }
@@ -256,7 +253,7 @@
       try {
         switch (perm) {
           case READ:
-            return isVisible(changeData());
+            return isVisible();
           case ABANDON:
             return canAbandon();
           case DELETE:
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index defec4b..edd3cb1 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -17,10 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.flogger.LazyArgs.lazy;
-import static com.google.gerrit.entities.RefNames.REFS_CACHE_AUTOMERGE;
 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.toCollection;
 
 import com.google.auto.value.AutoValue;
@@ -30,8 +27,6 @@
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
@@ -42,13 +37,10 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TagMatcher;
-import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -80,8 +72,8 @@
   private final TagCache tagCache;
   private final ChangeNotes.Factory changeNotesFactory;
   @Nullable private final SearchingChangeCacheImpl changeCache;
-  private final GroupCache groupCache;
   private final PermissionBackend permissionBackend;
+  private final RefVisibilityControl refVisibilityControl;
   private final ProjectControl projectControl;
   private final CurrentUser user;
   private final ProjectState projectState;
@@ -97,16 +89,16 @@
       TagCache tagCache,
       ChangeNotes.Factory changeNotesFactory,
       @Nullable SearchingChangeCacheImpl changeCache,
-      GroupCache groupCache,
       PermissionBackend permissionBackend,
+      RefVisibilityControl refVisibilityControl,
       @GerritServerConfig Config config,
       MetricMaker metricMaker,
       @Assisted ProjectControl projectControl) {
     this.tagCache = tagCache;
     this.changeNotesFactory = changeNotesFactory;
     this.changeCache = changeCache;
-    this.groupCache = groupCache;
     this.permissionBackend = permissionBackend;
+    this.refVisibilityControl = refVisibilityControl;
     this.skipFullRefEvaluationIfAllRefsAreVisible =
         config.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true);
     this.projectControl = projectControl;
@@ -227,131 +219,56 @@
     logger.atFinest().log("Doing full ref filtering");
     fullFilterCount.increment();
 
-    boolean viewMetadata;
-    boolean isAdmin;
-    Account.Id userId;
-    IdentifiedUser identifiedUser;
-    PermissionBackend.WithUser withUser = permissionBackend.user(user);
-    if (user.isIdentifiedUser()) {
-      viewMetadata = withUser.testOrFalse(GlobalPermission.ACCESS_DATABASE);
-      isAdmin = withUser.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
-      identifiedUser = user.asIdentifiedUser();
-      userId = identifiedUser.getAccountId();
-      logger.atFinest().log(
-          "Account = %d; can view metadata = %s; is admin = %s",
-          userId.get(), viewMetadata, isAdmin);
-    } else {
-      logger.atFinest().log("User is anonymous");
-      viewMetadata = false;
-      isAdmin = false;
-      userId = null;
-      identifiedUser = null;
-    }
-
+    boolean hasAccessDatabase =
+        permissionBackend
+            .user(projectControl.getUser())
+            .testOrFalse(GlobalPermission.ACCESS_DATABASE);
     List<Ref> resultRefs = new ArrayList<>(refs.size());
     List<Ref> deferredTags = new ArrayList<>();
     for (Ref ref : refs) {
-      String name = ref.getName();
+      String refName = ref.getName();
       Change.Id changeId;
-      Account.Id accountId;
-      AccountGroup.UUID accountGroupUuid;
-      if (name.startsWith(REFS_CACHE_AUTOMERGE)) {
-        continue;
-      } else if (opts.filterMeta() && isMetadata(name)) {
-        logger.atFinest().log("Filter out metadata ref %s", name);
-        continue;
-      } else if (RefNames.isRefsEdit(name)) {
-        // 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.add(ref);
-        } else {
-          logger.atFinest().log("Filter out edit ref %s", name);
-        }
-      } else if ((changeId = Change.Id.fromRef(name)) != null) {
-        // Change ref is visible only if the change is visible.
-        if (viewMetadata || visible(repo, changeId)) {
-          logger.atFinest().log("Include change ref %s", name);
-          resultRefs.add(ref);
-        } else {
-          logger.atFinest().log("Filter out change ref %s", name);
-        }
-      } else if ((accountId = Account.Id.fromRef(name)) != null) {
-        // 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.add(ref);
-        } else {
-          logger.atFinest().log("Filter out user ref %s", name);
-        }
-      } else if ((accountGroupUuid = AccountGroup.UUID.fromRef(name)) != null) {
-        // Group ref is visible only to the corresponding owner group.
-        InternalGroup group = groupCache.get(accountGroupUuid).orElse(null);
-        if (viewMetadata
-            || (group != null
-                && isGroupOwner(group, identifiedUser, isAdmin)
-                && canReadRef(name))) {
-          logger.atFinest().log("Include group ref %s", name);
-          resultRefs.add(ref);
-        } else {
-          logger.atFinest().log("Filter out group ref %s", name);
-        }
+      if (opts.filterMeta() && isMetadata(refName)) {
+        logger.atFinest().log("Filter out metadata ref %s", refName);
       } else if (isTag(ref)) {
         if (hasReadOnRefsStar) {
-          // The user has READ on refs/*. This is the broadest permission one can assign. There is
-          // no way to grant access to (specific) tags in Gerrit, so we have to assume that these
-          // users can see all tags because there could be tags that aren't reachable by any visible
-          // ref while the user can see all non-Gerrit refs. This matches Gerrit's historic
-          // behavior.
+          // The user has READ on refs/* with no effective block permission. This is the broadest
+          // permission one can assign. There is no way to grant access to (specific) tags in
+          // Gerrit,
+          // so we have to assume that these users can see all tags because there could be tags that
+          // aren't reachable by any visible ref while the user can see all non-Gerrit refs. This
+          // matches Gerrit's historic behavior.
           // This makes it so that these users could see commits that they can't see otherwise
           // (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on
           // 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);
+          logger.atFinest().log("Include tag ref %s because user has read on refs/*", refName);
           resultRefs.add(ref);
         } else {
           // If its a tag, consider it later.
           if (ref.getObjectId() != null) {
-            logger.atFinest().log("Defer tag ref %s", name);
+            logger.atFinest().log("Defer tag ref %s", refName);
             deferredTags.add(ref);
           } else {
-            logger.atFinest().log("Filter out tag ref %s that is not a tag", name);
+            logger.atFinest().log("Filter out tag ref %s that is not a tag", refName);
           }
         }
-      } else if (name.startsWith(RefNames.REFS_SEQUENCES)) {
-        // Sequences are internal database implementation details.
-        if (viewMetadata) {
-          logger.atFinest().log("Include sequence ref %s", name);
+      } else if ((changeId = Change.Id.fromRef(refName)) != null) {
+        // This is a mere performance optimization. RefVisibilityControl could determine the
+        // visibility of these refs just fine. But instead, we use highly-optimized logic that
+        // looks only on the last 10k most recent changes using the change index and a cache.
+        if (hasAccessDatabase) {
           resultRefs.add(ref);
+        } else if (!visible(repo, changeId)) {
+          logger.atFinest().log("Filter out invisible change ref %s", refName);
+        } else if (RefNames.isRefsEdit(refName) && !visibleEdit(repo, refName)) {
+          logger.atFinest().log("Filter out invisible change edit ref %s", refName);
         } else {
-          logger.atFinest().log("Filter out sequence ref %s", name);
-        }
-      } else if (projectState.isAllUsers()
-          && (name.equals(RefNames.REFS_EXTERNAL_IDS) || name.equals(RefNames.REFS_GROUPNAMES))) {
-        // The notes branches with the external IDs / group names must not be exposed to normal
-        // users.
-        if (viewMetadata) {
-          logger.atFinest().log("Include external IDs branch %s", name);
+          // Change is visible
           resultRefs.add(ref);
-        } else {
-          logger.atFinest().log("Filter out external IDs branch %s", name);
         }
-      } else if (canReadRef(ref.getLeaf().getName())) {
-        // Use the leaf to lookup the control data. If the reference is
-        // symbolic we want the control around the final target. If its
-        // 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());
+      } else if (refVisibilityControl.isVisible(projectControl, ref.getLeaf().getName())) {
         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.add(ref);
-        }
-      } else {
-        logger.atFinest().log("Filter out ref %s", name);
       }
     }
     Result result = new AutoValue_DefaultRefFilter_Result(resultRefs, deferredTags);
@@ -374,7 +291,8 @@
               r ->
                   !RefNames.isGerritRef(r.getName())
                       && !r.getName().startsWith(RefNames.REFS_TAGS)
-                      && !r.isSymbolic())
+                      && !r.isSymbolic()
+                      && !r.getName().equals(RefNames.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.
@@ -520,10 +438,6 @@
     return ref.getLeaf().getName().startsWith(Constants.R_TAGS);
   }
 
-  private static boolean isRefsUsersSelf(Ref ref) {
-    return ref.getName().startsWith(REFS_USERS_SELF);
-  }
-
   private boolean canReadRef(String ref) throws PermissionBackendException {
     try {
       permissionBackendForProject.ref(ref).check(RefPermission.READ);
@@ -544,17 +458,6 @@
     return true;
   }
 
-  private boolean isGroupOwner(
-      InternalGroup group, @Nullable IdentifiedUser user, boolean isAdmin) {
-    requireNonNull(group);
-
-    // Keep this logic in sync with GroupControl#isOwner().
-    boolean isGroupOwner =
-        isAdmin || (user != null && user.getEffectiveGroups().contains(group.getOwnerGroupUUID()));
-    logger.atFinest().log("User is owner of group %s = %s", group.getGroupUUID(), isGroupOwner);
-    return isGroupOwner;
-  }
-
   /**
    * Returns true if the user can see the provided change ref. Uses NoteDb for evaluation, hence
    * does not suffer from the limitations documented in {@link SearchingChangeCacheImpl}.
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 724017db..a92fde0 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -36,8 +36,10 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
@@ -68,11 +70,14 @@
   private final Set<AccountGroup.UUID> uploadGroups;
   private final Set<AccountGroup.UUID> receiveGroups;
   private final PermissionBackend permissionBackend;
+  private final RefVisibilityControl refVisibilityControl;
+  private final GitRepositoryManager repositoryManager;
   private final CurrentUser user;
   private final ProjectState state;
   private final PermissionCollection.Factory permissionFilter;
   private final DefaultRefFilter.Factory refFilterFactory;
   private final ChangeData.Factory changeDataFactory;
+  private final AllUsersName allUsersName;
 
   private List<SectionMatcher> allSections;
   private Map<String, RefControl> refControls;
@@ -84,16 +89,22 @@
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       PermissionCollection.Factory permissionFilter,
       PermissionBackend permissionBackend,
+      RefVisibilityControl refVisibilityControl,
+      GitRepositoryManager repositoryManager,
       DefaultRefFilter.Factory refFilterFactory,
       ChangeData.Factory changeDataFactory,
+      AllUsersName allUsersName,
       @Assisted CurrentUser who,
       @Assisted ProjectState ps) {
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
     this.permissionBackend = permissionBackend;
+    this.refVisibilityControl = refVisibilityControl;
+    this.repositoryManager = repositoryManager;
     this.refFilterFactory = refFilterFactory;
     this.changeDataFactory = changeDataFactory;
+    this.allUsersName = allUsersName;
     user = who;
     state = ps;
   }
@@ -117,7 +128,9 @@
     RefControl ctl = refControls.get(refName);
     if (ctl == null) {
       PermissionCollection relevant = permissionFilter.filter(access(), refName, user);
-      ctl = new RefControl(changeDataFactory, this, refName, relevant);
+      ctl =
+          new RefControl(
+              changeDataFactory, refVisibilityControl, this, repositoryManager, refName, relevant);
       refControls.put(refName, ctl);
     }
     return ctl;
@@ -164,7 +177,9 @@
   }
 
   boolean allRefsAreVisible(Set<String> ignore) {
-    return user.isInternalUser() || canPerformOnAllRefs(Permission.READ, ignore);
+    return user.isInternalUser()
+        || (!getProject().getNameKey().equals(allUsersName)
+            && canPerformOnAllRefs(Permission.READ, ignore));
   }
 
   /** Can the user run upload pack? */
@@ -442,7 +457,7 @@
           return canPushToAtLeastOneRef();
 
         case READ_CONFIG:
-          return controlForRef(RefNames.REFS_CONFIG).isVisible();
+          return controlForRef(RefNames.REFS_CONFIG).hasReadPermissionOnRef(false);
 
         case BAN_COMMIT:
         case READ_REFLOG:
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index e704a99..ad4188f 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Permission;
@@ -28,6 +29,7 @@
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.logging.CallerFinder;
 import com.google.gerrit.server.logging.LoggingContext;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -35,17 +37,24 @@
 import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.MagicBranch;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
 
 /** Manages access control for Git references (aka branches, tags). */
 class RefControl {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ChangeData.Factory changeDataFactory;
+  private final RefVisibilityControl refVisibilityControl;
   private final ProjectControl projectControl;
+  private final GitRepositoryManager repositoryManager;
   private final String refName;
 
   /** All permissions that apply to this reference. */
@@ -58,15 +67,19 @@
   private Boolean owner;
   private Boolean canForgeAuthor;
   private Boolean canForgeCommitter;
-  private Boolean isVisible;
+  private Boolean hasReadPermissionOnRef;
 
   RefControl(
       ChangeData.Factory changeDataFactory,
+      RefVisibilityControl refVisibilityControl,
       ProjectControl projectControl,
+      GitRepositoryManager repositoryManager,
       String ref,
       PermissionCollection relevant) {
     this.changeDataFactory = changeDataFactory;
+    this.refVisibilityControl = refVisibilityControl;
     this.projectControl = projectControl;
+    this.repositoryManager = repositoryManager;
     this.refName = ref;
     this.relevant = relevant;
     this.callerFinder =
@@ -99,12 +112,27 @@
     return owner;
   }
 
-  /** Can this user see this reference exists? */
-  boolean isVisible() {
-    if (isVisible == null) {
-      isVisible = getUser().isInternalUser() || canPerform(Permission.READ);
+  /**
+   * Returns {@code true} if the user has permission to read the ref. This method evaluates {@link
+   * RefPermission#READ} only. Hence, it is not authoritative. For example, it does not tell if the
+   * user can see NoteDb refs such as {@code refs/meta/external-ids} which requires {@link
+   * GlobalPermission#ACCESS_DATABASE} and deny access in this case.
+   */
+  boolean hasReadPermissionOnRef(boolean allowNoteDbRefs) {
+    // Don't allow checking for NoteDb refs unless instructed otherwise.
+    if (!allowNoteDbRefs
+        && (refName.startsWith(Constants.R_TAGS) || RefNames.isGerritRef(refName))) {
+      logger.atWarning().atMostEvery(30, TimeUnit.SECONDS).log(
+          "%s: Can't determine visibility of %s in RefControl. Denying access. "
+              + "This case should have been handled before.",
+          projectControl.getProject().getName(), refName);
+      return false;
     }
-    return isVisible;
+
+    if (hasReadPermissionOnRef == null) {
+      hasReadPermissionOnRef = getUser().isInternalUser() || canPerform(Permission.READ);
+    }
+    return hasReadPermissionOnRef;
   }
 
   /** @return true if this user can add a new patch set to this ref */
@@ -591,7 +619,10 @@
     private boolean can(RefPermission perm) throws PermissionBackendException {
       switch (perm) {
         case READ:
-          return isVisible();
+          if (refName.startsWith(Constants.R_TAGS)) {
+            return isTagVisible();
+          }
+          return refVisibilityControl.isVisible(projectControl, refName);
         case CREATE:
           // TODO This isn't an accurate test.
           return canPerform(refPermissionName(perm));
@@ -641,6 +672,38 @@
       }
       throw new PermissionBackendException(perm + " unsupported");
     }
+
+    private boolean isTagVisible() throws PermissionBackendException {
+      if (projectControl.asForProject().test(ProjectPermission.READ)) {
+        // The user has READ on refs/* with no effective block permission. This is the broadest
+        // permission one can assign. There is no way to grant access to (specific) tags in Gerrit,
+        // so we have to assume that these users can see all tags because there could be tags that
+        // aren't reachable by any visible ref while the user can see all non-Gerrit refs. This
+        // matches Gerrit's historic behavior.
+        // This makes it so that these users could see commits that they can't see otherwise
+        // (e.g. a private change ref) if a tag was attached to it. Tags are meant to be used on
+        // the regular Git tree that users interact with, not on any of the Gerrit trees, so this
+        // is a negligible risk.
+        return true;
+      }
+
+      try (Repository repo =
+          repositoryManager.openRepository(projectControl.getProject().getNameKey())) {
+        // Tag visibility requires going through RefFilter because it entails loading all taggable
+        // refs and filtering them all by visibility.
+        Ref resolvedRef = repo.getRefDatabase().exactRef(refName);
+        if (resolvedRef == null) {
+          return false;
+        }
+        return projectControl.asForProject()
+            .filter(
+                ImmutableList.of(resolvedRef), repo, PermissionBackend.RefFilterOptions.defaults())
+            .stream()
+            .anyMatch(r -> refName.equals(r.getName()));
+      } catch (IOException e) {
+        throw new PermissionBackendException(e);
+      }
+    }
   }
 
   private static String refPermissionName(RefPermission refPermission) {
diff --git a/java/com/google/gerrit/server/permissions/RefVisibilityControl.java b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
new file mode 100644
index 0000000..4744037
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
@@ -0,0 +1,181 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.entities.RefNames.REFS_CACHE_AUTOMERGE;
+
+import com.google.common.base.Throwables;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.change.ChangeData;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jgit.lib.Constants;
+
+/**
+ * This class is a component that is internal to {@link DefaultPermissionBackend}. It can
+ * authoritatively tell if a ref is accessible by a user.
+ */
+@Singleton
+class RefVisibilityControl {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final PermissionBackend permissionBackend;
+  private final GroupControl.GenericFactory groupControlFactory;
+  private final ChangeData.Factory changeDataFactory;
+
+  @Inject
+  RefVisibilityControl(
+      PermissionBackend permissionBackend,
+      GroupControl.GenericFactory groupControlFactory,
+      ChangeData.Factory changeDataFactory) {
+    this.permissionBackend = permissionBackend;
+    this.groupControlFactory = groupControlFactory;
+    this.changeDataFactory = changeDataFactory;
+  }
+
+  /**
+   * Returns an authoritative answer if the ref is visible to the user. Does not have support for
+   * tags and will throw a {@link PermissionBackendException} if asked for tags visibility.
+   */
+  boolean isVisible(ProjectControl projectControl, String refName)
+      throws PermissionBackendException {
+    if (refName.startsWith(Constants.R_TAGS)) {
+      throw new PermissionBackendException(
+          "can't check tags through RefVisibilityControl. Use PermissionBackend#filter instead.");
+    }
+    if (!RefNames.isGerritRef(refName)) {
+      // This is not a special Gerrit ref and not a NoteDb ref. Likely, it's just a ref under
+      // refs/heads or another ref the user created. Apply the regular permissions with inheritance.
+      return projectControl.controlForRef(refName).hasReadPermissionOnRef(false);
+    }
+
+    if (refName.startsWith(REFS_CACHE_AUTOMERGE)) {
+      // Internal cache state that is accessible to no one.
+      return false;
+    }
+
+    boolean hasAccessDatabase =
+        permissionBackend
+            .user(projectControl.getUser())
+            .testOrFalse(GlobalPermission.ACCESS_DATABASE);
+    if (hasAccessDatabase) {
+      return true;
+    }
+
+    // Change and change edit visibility
+    Change.Id changeId;
+    if ((changeId = Change.Id.fromRef(refName)) != null) {
+      // Change ref is visible only if the change is visible.
+      ChangeData cd;
+      try {
+        cd = changeDataFactory.create(projectControl.getProject().getNameKey(), changeId);
+        checkState(cd.change().getId().equals(changeId));
+      } catch (StorageException e) {
+        if (Throwables.getCausalChain(e).stream()
+            .anyMatch(e2 -> e2 instanceof NoSuchChangeException)) {
+          // The change was deleted or is otherwise not accessible anymore.
+          // If the caller can see all refs and is allowed to see private changes on refs/, allow
+          // access. This is an escape hatch for receivers of "ref deleted" events.
+          PermissionBackend.ForProject forProject = projectControl.asForProject();
+          return forProject.test(ProjectPermission.READ)
+              && forProject.ref("refs/").test(RefPermission.READ_PRIVATE_CHANGES);
+        }
+        throw new PermissionBackendException(e);
+      }
+      if (RefNames.isRefsEdit(refName)) {
+        // Edits are visible only to the owning user, if change is visible.
+        return visibleEdit(refName, projectControl, cd);
+      }
+      return projectControl.controlFor(cd).isVisible();
+    }
+
+    // Account visibility
+    CurrentUser user = projectControl.getUser();
+    Account.Id currentUserAccountId = user.isIdentifiedUser() ? user.getAccountId() : null;
+    Account.Id accountId;
+    if ((accountId = Account.Id.fromRef(refName)) != null) {
+      // Account ref is visible only to the corresponding account.
+      if (accountId.equals(currentUserAccountId)
+          && projectControl.controlForRef(refName).hasReadPermissionOnRef(true)) {
+        return true;
+      }
+      return false;
+    }
+
+    // Group visibility
+    AccountGroup.UUID accountGroupUuid;
+    if ((accountGroupUuid = AccountGroup.UUID.fromRef(refName)) != null) {
+      // Group ref is visible only to the corresponding owner group.
+      try {
+        return projectControl.controlForRef(refName).hasReadPermissionOnRef(true)
+            && groupControlFactory.controlFor(user, accountGroupUuid).isOwner();
+      } catch (NoSuchGroupException e) {
+        // The group is broken, but the ref is still around. Pretend the ref is not visible.
+        logger.atWarning().withCause(e).log("Found group ref %s but group isn't parsable", refName);
+        return false;
+      }
+    }
+
+    // We are done checking all cases where we would allow access to Gerrit-managed refs. Deny
+    // access in case we got this far.
+    logger.atFine().log(
+        "Denying access to %s because user doesn't have access to this Gerrit ref", refName);
+    return false;
+  }
+
+  private boolean visibleEdit(String refName, ProjectControl projectControl, ChangeData cd)
+      throws PermissionBackendException {
+    Change.Id id = Change.Id.fromEditRefPart(refName);
+    if (id == null) {
+      throw new IllegalStateException("unable to parse change id from edit ref " + refName);
+    }
+
+    if (!projectControl.controlFor(cd).isVisible()) {
+      // The user can't see the change so they can't see any edits.
+      return false;
+    }
+
+    if (projectControl.getUser().isIdentifiedUser()
+        && refName.startsWith(
+            RefNames.refsEditPrefix(projectControl.getUser().asIdentifiedUser().getAccountId()))) {
+      logger.atFinest().log("Own change edit ref is visible: %s", refName);
+      return true;
+    }
+
+    try {
+      // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
+      projectControl
+          .asForProject()
+          .ref(cd.change().getDest().branch())
+          .check(RefPermission.READ_PRIVATE_CHANGES);
+      logger.atFinest().log("Foreign change edit ref is visible: %s", refName);
+      return true;
+    } catch (AuthException e) {
+      logger.atFinest().log("Foreign change edit ref is not visible: %s", refName);
+      return false;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java b/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java
index 6dc1006..88845ef 100644
--- a/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java
+++ b/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java
@@ -20,7 +20,6 @@
 import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
-import java.security.cert.X509Certificate;
 import javax.net.SocketFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocketFactory;
@@ -32,19 +31,7 @@
   private static final BlindSSLSocketFactory INSTANCE;
 
   static {
-    final X509TrustManager dummyTrustManager =
-        new X509TrustManager() {
-          @Override
-          public X509Certificate[] getAcceptedIssuers() {
-            return null;
-          }
-
-          @Override
-          public void checkClientTrusted(X509Certificate[] chain, String authType) {}
-
-          @Override
-          public void checkServerTrusted(X509Certificate[] chain, String authType) {}
-        };
+    final X509TrustManager dummyTrustManager = new BlindTrustManager();
 
     try {
       final SSLContext context = SSLContext.getInstance("SSL");
diff --git a/java/com/google/gerrit/util/ssl/BlindTrustManager.java b/java/com/google/gerrit/util/ssl/BlindTrustManager.java
new file mode 100644
index 0000000..2db091a
--- /dev/null
+++ b/java/com/google/gerrit/util/ssl/BlindTrustManager.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 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.util.ssl;
+
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+/** TrustManager implementation that accepts all certificates without validation. */
+public class BlindTrustManager implements X509TrustManager {
+
+  @Override
+  public X509Certificate[] getAcceptedIssuers() {
+    return null;
+  }
+
+  @Override
+  public void checkClientTrusted(X509Certificate[] chain, String authType) {}
+
+  @Override
+  public void checkServerTrusted(X509Certificate[] chain, String authType) {}
+}
diff --git a/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index 85e4dbf..0f8c1f4 100644
--- a/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -17,68 +17,66 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.io.BaseEncoding;
-import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
+import com.google.gerrit.util.ssl.BlindTrustManager;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
-import java.net.SocketException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import java.util.List;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
-import javax.net.ssl.SSLParameters;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
 
-public class AuthSMTPClient extends SMTPClient {
+/**
+ * SMTP Client with authentication support and optional SSL processing and verification. {@link
+ * org.apache.commons.net.smtp.SMTPSClient} is used for the SSL handshake and hostname verification.
+ *
+ * <p>If shouldHandshakeOnConnect mode is selected, SSL/TLS negotiation starts right after the
+ * connection has been established. Otherwise SSL/TLS negotiation will only occur if {@link
+ * AuthSMTPClient#execTLS} is explicitly called and the server accepts the command.
+ *
+ * <p>Examples:
+ *
+ * <ul>
+ *   <li>For SSL connection:
+ *       <pre>
+ *       AuthSMTPClient c = new AuthSMTPClient(true, sslVerify);
+ *       c.connect("127.0.0.1", 465);
+ *     </pre>
+ *   <li>For TLS connection:
+ *       <pre>
+ *       AuthSMTPClient c = new AuthSMTPClient(false, sslVerify);
+ *       c.connect("127.0.0.1", 25);
+ *       if (c.execTLS()) { /rest of the commands here/ }
+ *     </pre>
+ *   <li>If SSL encryption is not required:
+ *       <pre>
+ *       AuthSMTPClient c = new AuthSMTPClient(false, false);
+ *       c.connect("127.0.0.1", port);
+ *     </pre>
+ */
+public class AuthSMTPClient extends SMTPSClient {
+
   private String authTypes;
 
-  public AuthSMTPClient(String charset) {
-    super(charset);
-  }
-
-  public void enableSSL(boolean verify) {
-    _socketFactory_ = sslFactory(verify);
-  }
-
-  public boolean startTLS(String hostname, int port, boolean verify)
-      throws SocketException, IOException {
-    if (sendCommand("STARTTLS") != 220) {
-      return false;
+  /**
+   * Constructs AuthSMTPClient.
+   *
+   * @param shouldHandshakeOnConnect the SSL processing mode, {@code true} if SSL negotiation should
+   *     start right after connect, {@code false} if it will be started by the user explicitly or
+   *     SSL negotiation is not required.
+   * @param sslVerificationEnabled {@code true} if the SMTP server's SSL certificate and hostname
+   *     should be verified, {@code false} otherwise.
+   */
+  public AuthSMTPClient(boolean shouldHandshakeOnConnect, boolean sslVerificationEnabled) {
+    // If SSL Encryption is required, SMTPSClient is used for the handshake.
+    // Otherwise, use  SMTPSClient in 'explicit' mode without calling execTLS().
+    // See SMTPSClient._connectAction_ in commons-net-3.6.
+    super("TLS", shouldHandshakeOnConnect, UTF_8.name());
+    this.setEndpointCheckingEnabled(sslVerificationEnabled);
+    if (!sslVerificationEnabled) {
+      this.setTrustManager(new BlindTrustManager());
     }
-
-    _socket_ = sslFactory(verify).createSocket(_socket_, hostname, port, true);
-
-    if (verify) {
-      SSLParameters sslParams = new SSLParameters();
-      sslParams.setEndpointIdentificationAlgorithm("HTTPS");
-      ((SSLSocket) _socket_).setSSLParameters(sslParams);
-    }
-
-    // XXX: Can't call _connectAction_() because SMTP server doesn't
-    // give banner information again after STARTTLS, thus SMTP._connectAction_()
-    // will wait on __getReply() forever, see source code of commons-net-2.2.
-    //
-    // The lines below are copied from SocketClient._connectAction_() and
-    // SMTP._connectAction_() in commons-net-2.2.
-    _socket_.setSoTimeout(_timeout_);
-    _input_ = _socket_.getInputStream();
-    _output_ = _socket_.getOutputStream();
-    _reader = new BufferedReader(new InputStreamReader(_input_, UTF_8));
-    _writer = new BufferedWriter(new OutputStreamWriter(_output_, UTF_8));
-    return true;
-  }
-
-  private static SSLSocketFactory sslFactory(boolean verify) {
-    if (verify) {
-      return (SSLSocketFactory) SSLSocketFactory.getDefault();
-    }
-    return (SSLSocketFactory) BlindSSLSocketFactory.getDefault();
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index ccfa60e..f59fba0 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1061,6 +1061,11 @@
       com.google.gerrit.acceptance.TestAccount deleteAs)
       throws Exception {
     try {
+      projectOperations
+          .project(projectName)
+          .forUpdate()
+          .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(ANONYMOUS_USERS))
+          .update();
       requestScopeOperations.setApiUser(owner.id());
       ChangeInput in = new ChangeInput();
       in.project = projectName.get();
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 436ad7c..e4bb73a 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -1194,6 +1194,11 @@
 
   @Test
   public void pushToDeletedGroupBranchIsRejectedForAllUsersRepo() throws Exception {
+    // refs/deleted-groups is only visible with ACCESS_DATABASE
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     String groupRef =
         RefNames.refsDeletedGroups(AccountGroup.uuid(gApi.groups().create(name("foo")).get().id));
     createBranch(allUsers, groupRef);
@@ -1386,6 +1391,11 @@
 
   @Test
   public void cannotDeleteDeletedGroupBranch() throws Exception {
+    // refs/deleted-groups is only visible with ACCESS_DATABASE
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     String groupRef = RefNames.refsDeletedGroups(AccountGroup.uuid(name("foo")));
     createBranch(allUsers, groupRef);
     testCannotDeleteGroupBranch(RefNames.REFS_DELETED_GROUPS + "*", groupRef);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index 191d5c5..531357a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -31,7 +31,9 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
@@ -52,6 +54,13 @@
     }
   }
 
+  @ConfigSuite.Config
+  public static Config skipFalse() {
+    Config config = new Config();
+    config.setBoolean("auth", null, "skipFullRefEvaluationIfAllRefsAreVisible", false);
+    return config;
+  }
+
   @Inject private ProjectOperations projectOperations;
 
   private RevCommit initialHead;
@@ -93,6 +102,20 @@
   }
 
   @Test
+  public void createTagForExistingCommit_withoutGlobalReadPermissions() throws Exception {
+    removeReadAccessOnRefsStar();
+    grantReadAccessOnRefsHeadsStar();
+    createTagForExistingCommit();
+  }
+
+  @Test
+  public void createTagForNewCommit_withoutGlobalReadPermissions() throws Exception {
+    removeReadAccessOnRefsStar();
+    grantReadAccessOnRefsHeadsStar();
+    createTagForNewCommit();
+  }
+
+  @Test
   public void fastForward() throws Exception {
     allowTagCreation();
     String tagName = pushTagForExistingCommit(Status.OK);
@@ -109,6 +132,15 @@
     fastForwardTagToExistingCommit(tagName, expectedStatus);
     fastForwardTagToNewCommit(tagName, expectedStatus);
 
+    // Above we just fast-forwarded the tag to a new commit which is not part of any branch. By
+    // default this tag is not visible, as users can only see tags that point to commits that are
+    // part of visible branches, which is not the case for this tag. It's odd that we allow the user
+    // to create such a tag that is then not visible to the creator. Below we want to fast-forward
+    // this tag, but this is only possible if the tag is visible. To make it visible we must allow
+    // the user to read all tags, regardless of whether it points to a commit that is part of a
+    // visible branch.
+    allowReadingAllTag();
+
     allowForcePushOnRefsTags();
     fastForwardTagToExistingCommit(tagName, Status.OK);
     fastForwardTagToNewCommit(tagName, Status.OK);
@@ -234,6 +266,49 @@
     assertWithMessage(tagType.name()).that(refUpdate.getStatus()).isEqualTo(expectedStatus);
   }
 
+  private void removeReadAccessOnRefsStar() {
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .remove(permissionKey(Permission.READ).ref("refs/*"))
+        .update();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(permissionKey(Permission.READ).ref("refs/*"))
+        .update();
+  }
+
+  private void grantReadAccessOnRefsHeadsStar() {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+  }
+
+  private void allowReadingAllTag() throws Exception {
+    // Tags are only visible if the commits to which they point are part of a visible branch.
+    // To make all tags visible, including tags that point to commits that are not part of a visible
+    // branch, either auth.skipFullRefEvaluationIfAllRefsAreVisible in gerrit.config needs to be
+    // true, or the user must have READ access for all refs in the repository.
+
+    if (cfg.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true)) {
+      return;
+    }
+
+    // By default READ access in the All-Projects project is granted to registered users on refs/*,
+    // which makes all refs, except refs/meta/config, visible to them. refs/meta/config is not
+    // visible since by default READ access to it is exclusively granted to the project owners only.
+    // This means to make all refs, and thus all tags, visible, we must allow registered users to
+    // see the refs/meta/config branch.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS))
+        .update();
+  }
+
   private void allowTagCreation() throws Exception {
     projectOperations
         .project(project)
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
new file mode 100644
index 0000000..b4b1be0
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
@@ -0,0 +1,594 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInput;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class GetBranchIT extends AbstractDaemonTest {
+  @Inject private ChangeOperations changeOperations;
+  @Inject private GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  @ConfigSuite.Config
+  public static Config skipFalse() {
+    Config config = new Config();
+    config.setBoolean("auth", null, "skipFullRefEvaluationIfAllRefsAreVisible", false);
+    return config;
+  }
+
+  @Test
+  public void cannotGetNonExistingBranch() {
+    assertBranchNotFound(project, RefNames.fullName("non-existing"));
+  }
+
+  @Test
+  public void getBranch() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchFound(project, RefNames.fullName("master"));
+  }
+
+  @Test
+  public void getBranchByShortName() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchFound(project, "master");
+  }
+
+  @Test
+  public void cannotGetNonVisibleBranch() {
+    String branchName = "master";
+
+    // block read access to the branch
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(ANONYMOUS_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(project, RefNames.fullName(branchName));
+  }
+
+  @Test
+  public void cannotGetNonVisibleBranchByShortName() {
+    String branchName = "master";
+
+    // block read access to the branch
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(ANONYMOUS_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(project, branchName);
+  }
+
+  @Test
+  public void getChangeRef() throws Exception {
+    // create a change
+    Change.Id changeId = changeOperations.newChange().project(project).create();
+
+    // a user without the 'Access Database' capability can see the change ref
+    requestScopeOperations.setApiUser(user.id());
+    String changeRef = RefNames.patchSetRef(PatchSet.id(changeId, 1));
+    assertBranchFound(project, changeRef);
+  }
+
+  @Test
+  public void getChangeRefOfNonVisibleChange() throws Exception {
+    // create a change
+    String branchName = "master";
+    Change.Id changeId = changeOperations.newChange().project(project).branch(branchName).create();
+
+    // block read access to the branch
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(ANONYMOUS_USERS))
+        .update();
+
+    // a user without the 'Access Database' capability cannot see the change ref
+    requestScopeOperations.setApiUser(user.id());
+    String changeRef = RefNames.patchSetRef(PatchSet.id(changeId, 1));
+    assertBranchNotFound(project, changeRef);
+
+    // a user with the 'Access Database' capability can see the change ref
+    testGetRefWithAccessDatabase(project, changeRef);
+  }
+
+  @Test
+  public void getChangeEditRef() throws Exception {
+    TestAccount user2 = accountCreator.user2();
+
+    // create a change
+    Change.Id changeId = changeOperations.newChange().project(project).create();
+
+    // create a change edit by 'user'
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId.get()).edit().create();
+
+    // every user can see their own change edit refs
+    String changeEditRef = RefNames.refsEdit(user.id(), changeId, PatchSet.id(changeId, 1));
+    assertBranchFound(project, changeEditRef);
+
+    // a user without the 'Access Database' capability cannot see the change edit ref of another
+    // user
+    requestScopeOperations.setApiUser(user2.id());
+    assertBranchNotFound(project, changeEditRef);
+
+    // a user with the 'Access Database' capability can see the change edit ref of another user
+    testGetRefWithAccessDatabase(project, changeEditRef);
+  }
+
+  @Test
+  public void cannotGetChangeEditRefOfNonVisibleChange() throws Exception {
+    // create a change
+    String branchName = "master";
+    Change.Id changeId = changeOperations.newChange().project(project).branch(branchName).create();
+
+    // create a change edit by 'user'
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(changeId.get()).edit().create();
+
+    // make the change non-visible by blocking read access on the destination
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(ANONYMOUS_USERS))
+        .update();
+
+    // user cannot see their own change edit refs if the change is no longer visible
+    String changeEditRef = RefNames.refsEdit(user.id(), changeId, PatchSet.id(changeId, 1));
+    assertBranchNotFound(project, changeEditRef);
+
+    // a user with the 'Access Database' capability can see the change edit ref
+    testGetRefWithAccessDatabase(project, changeEditRef);
+  }
+
+  @Test
+  public void getChangeMetaRef() throws Exception {
+    // create a change
+    Change.Id changeId = changeOperations.newChange().project(project).create();
+
+    // A user without the 'Access Database' capability can see the change meta ref.
+    // This may be surprising, as 'Access Database' guards access to meta refs and the change meta
+    // ref is a meta ref, however change meta refs have been always visible to all users that can
+    // see the change and some tools rely on seeing these refs, so we have to keep the current
+    // behaviour.
+    requestScopeOperations.setApiUser(user.id());
+    String changeMetaRef = RefNames.changeMetaRef(changeId);
+    assertBranchFound(project, changeMetaRef);
+  }
+
+  @Test
+  public void getRefsMetaConfig() throws Exception {
+    // a non-project owner cannot get the refs/meta/config branch
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(project, RefNames.REFS_CONFIG);
+
+    // a non-project owner cannot get the refs/meta/config branch even with the 'Access Database'
+    // capability
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    try {
+      assertBranchNotFound(project, RefNames.REFS_CONFIG);
+    } finally {
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(
+              capabilityKey(GlobalCapability.ACCESS_DATABASE)
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
+    }
+
+    requestScopeOperations.setApiUser(user.id());
+
+    // a project owner can get the refs/meta/config branch
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    assertBranchFound(project, RefNames.REFS_CONFIG);
+  }
+
+  @Test
+  public void getUserRefOfOtherUser() throws Exception {
+    String userRef = RefNames.refsUsers(admin.id());
+
+    // a user without the 'Access Database' capability cannot see the user ref of another user
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(allUsers, userRef);
+
+    // a user with the 'Access Database' capability can see the user ref of another user
+    testGetRefWithAccessDatabase(allUsers, userRef);
+  }
+
+  @Test
+  public void getOwnUserRef() throws Exception {
+    // every user can see the own user ref
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchFound(allUsers, RefNames.refsUsers(user.id()));
+
+    // TODO: every user can see the own user ref via the magic ref/users/self ref
+    // requestScopeOperations.setApiUser(user.id());
+    // assertBranchFound(allUsers, RefNames.REFS_USERS_SELF);
+  }
+
+  @Test
+  public void getExternalIdsRefs() throws Exception {
+    // a user without the 'Access Database' capability cannot see the refs/meta/external-ids ref
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(allUsers, RefNames.REFS_EXTERNAL_IDS);
+
+    // a user with the 'Access Database' capability can see the refs/meta/external-ids ref
+    testGetRefWithAccessDatabase(allUsers, RefNames.REFS_EXTERNAL_IDS);
+  }
+
+  @Test
+  public void getGroupRef() throws Exception {
+    // create a group
+    AccountGroup.UUID ownerGroupUuid =
+        groupOperations.newGroup().name("owner-group").addMember(admin.id()).create();
+    AccountGroup.UUID testGroupUuid =
+        groupOperations.newGroup().name("test-group").ownerGroupUuid(ownerGroupUuid).create();
+
+    // a non-group owner without the 'Access Database' capability cannot see the group ref
+    requestScopeOperations.setApiUser(user.id());
+    String groupRef = RefNames.refsGroups(testGroupUuid);
+    assertBranchNotFound(allUsers, groupRef);
+
+    // a non-group owner with the 'Access Database' capability can see the group ref
+    testGetRefWithAccessDatabase(allUsers, groupRef);
+
+    // a group owner can see the group ref if the group ref is visible
+    groupOperations.group(ownerGroupUuid).forUpdate().addMember(user.id()).update();
+    assertBranchFound(allUsers, groupRef);
+
+    // A group owner cannot see the group ref if the group ref is not visible.
+    // The READ access for refs/groups/* must be blocked on All-Projects rather than All-Users.
+    // This is because READ access for refs/groups/* on All-Users is by default granted to
+    // REGISTERED_USERS, and if an ALLOW rule and a BLOCK rule are on the same project and ref,
+    // the ALLOW rule takes precedence.
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/groups/*").group(ANONYMOUS_USERS))
+        .update();
+    assertBranchNotFound(allUsers, groupRef);
+  }
+
+  @Test
+  public void getGroupNamesRef() throws Exception {
+    // a user without the 'Access Database' capability cannot see the refs/meta/group-names ref
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(allUsers, RefNames.REFS_GROUPNAMES);
+
+    // a user with the 'Access Database' capability can see the refs/meta/group-names ref
+    testGetRefWithAccessDatabase(allUsers, RefNames.REFS_GROUPNAMES);
+  }
+
+  @Test
+  public void getDeletedGroupRef() throws Exception {
+    // Create a deleted group ref. We must create a directly in the repo, since group deletion is
+    // not supported yet.
+    String deletedGroupRef = RefNames.refsDeletedGroups(AccountGroup.uuid("deleted-group"));
+    try (TestRepository<Repository> testRepo =
+        new TestRepository<>(repoManager.openRepository(allUsers))) {
+      testRepo
+          .branch(deletedGroupRef)
+          .commit()
+          .message("Some Message")
+          .add("group.config", "content")
+          .create();
+    }
+
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(allUsers, deletedGroupRef);
+
+    // a user with the 'Access Database' capability can see the deleted group ref
+    testGetRefWithAccessDatabase(allUsers, deletedGroupRef);
+  }
+
+  @Test
+  public void getDraftCommentsRef() throws Exception {
+    TestAccount user2 = accountCreator.user2();
+
+    // create a change
+    String fileName = "a.txt";
+    Change change = createChange("A Change", fileName, "content").getChange().change();
+
+    // create a draft comment by the by 'user'
+    requestScopeOperations.setApiUser(user.id());
+    DraftInput draftInput = new DraftInput();
+    draftInput.path = fileName;
+    draftInput.line = 0;
+    draftInput.message = "Some Comment";
+    gApi.changes().id(change.getChangeId()).current().createDraft(draftInput);
+
+    // every user can see their own draft comments refs
+    // TODO: is this a bug?
+    String draftCommentsRef = RefNames.refsDraftComments(change.getId(), user.id());
+    assertBranchFound(allUsers, draftCommentsRef);
+
+    // a user without the 'Access Database' capability cannot see the draft comments ref of another
+    // user
+    requestScopeOperations.setApiUser(user2.id());
+    assertBranchNotFound(allUsers, draftCommentsRef);
+
+    // a user with the 'Access Database' capability can see the draft comments ref of another user
+    testGetRefWithAccessDatabase(allUsers, draftCommentsRef);
+  }
+
+  @Test
+  public void getStarredChangesRef() throws Exception {
+    TestAccount user2 = accountCreator.user2();
+
+    // create a change
+    Change change = createChange().getChange().change();
+
+    // let user star the change
+    requestScopeOperations.setApiUser(user.id());
+    gApi.accounts().self().starChange(Integer.toString(change.getChangeId()));
+
+    // every user can see their own starred changes refs
+    // TODO: is this a bug?
+    String starredChangesRef = RefNames.refsStarredChanges(change.getId(), user.id());
+    assertBranchFound(allUsers, starredChangesRef);
+
+    // a user without the 'Access Database' capability cannot see the starred changes ref of another
+    // user
+    requestScopeOperations.setApiUser(user2.id());
+    assertBranchNotFound(allUsers, starredChangesRef);
+
+    // a user with the 'Access Database' capability can see the starred changes ref of another user
+    testGetRefWithAccessDatabase(allUsers, starredChangesRef);
+  }
+
+  @Test
+  public void getTagRef() throws Exception {
+    // create a tag
+    TagInput input = new TagInput();
+    input.message = "My Tag";
+    input.revision = projectOperations.project(project).getHead("master").name();
+    TagInfo tagInfo = gApi.projects().name(project.get()).tag("my-tag").create(input).get();
+
+    // any user who can see the project, can see the tag
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchFound(project, tagInfo.ref);
+  }
+
+  @Test
+  public void cannotGetTagRefThatPointsToNonVisibleBranch() throws Exception {
+    // create a tag
+    TagInput input = new TagInput();
+    input.message = "My Tag";
+    input.revision = projectOperations.project(project).getHead("master").name();
+    TagInfo tagInfo = gApi.projects().name(project.get()).tag("my-tag").create(input).get();
+
+    // block read access to the branch
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName("master")).group(ANONYMOUS_USERS))
+        .update();
+
+    // if the user cannot see the project, the tag is not visible
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(project, tagInfo.ref);
+  }
+
+  @Test
+  public void getSymbolicRef() throws Exception {
+    // 'HEAD' is visible since it points to 'master' that is visible
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchFound(project, "HEAD");
+  }
+
+  @Test
+  public void cannotGetSymbolicRefThatPointsToNonVisibleBranch() {
+    // block read access to the branch to which HEAD points by default
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.fullName("master")).group(ANONYMOUS_USERS))
+        .update();
+
+    // since 'master' is not visible, 'HEAD' which points to 'master' is also not visible
+    requestScopeOperations.setApiUser(user.id());
+    assertBranchNotFound(project, "HEAD");
+  }
+
+  @Test
+  public void getAccountSequenceRef() throws Exception {
+    // a user without the 'Access Database' capability cannot see the refs/sequences/accounts ref
+    requestScopeOperations.setApiUser(user.id());
+    String accountSequenceRef = RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS;
+    assertBranchNotFound(allUsers, accountSequenceRef);
+
+    // a user with the 'Access Database' capability can see the refs/sequences/accounts ref
+    testGetRefWithAccessDatabase(allUsers, accountSequenceRef);
+  }
+
+  @Test
+  public void getChangeSequenceRef() throws Exception {
+    // a user without the 'Access Database' capability cannot see the refs/sequences/changes ref
+    requestScopeOperations.setApiUser(user.id());
+    String changeSequenceRef = RefNames.REFS_SEQUENCES + Sequences.NAME_CHANGES;
+    assertBranchNotFound(allProjects, changeSequenceRef);
+
+    // a user with the 'Access Database' capability can see the refs/sequences/changes ref
+    testGetRefWithAccessDatabase(allProjects, changeSequenceRef);
+  }
+
+  @Test
+  public void getGroupSequenceRef() throws Exception {
+    // a user without the 'Access Database' capability cannot see the refs/sequences/groups ref
+    requestScopeOperations.setApiUser(user.id());
+    String groupSequenceRef = RefNames.REFS_SEQUENCES + Sequences.NAME_GROUPS;
+    assertBranchNotFound(allUsers, groupSequenceRef);
+
+    // a user with the 'Access Database' capability can see the refs/sequences/groups ref
+    testGetRefWithAccessDatabase(allUsers, groupSequenceRef);
+  }
+
+  @Test
+  public void getVersionMetaRef() throws Exception {
+    // TODO: a user without the 'Access Database' capability cannot see the refs/meta/version ref
+    // requestScopeOperations.setApiUser(user.id());
+    // assertBranchNotFound(allProjects, RefNames.REFS_VERSION);
+
+    // a user with the 'Access Database' capability can see the refs/meta/vaersion ref
+    testGetRefWithAccessDatabase(allProjects, RefNames.REFS_VERSION);
+  }
+
+  @Test
+  public void cannotGetAutoMergeRef() throws Exception {
+    String file = "foo/a.txt";
+
+    // Create a base change.
+    Change.Id baseChange =
+        changeOperations
+            .newChange()
+            .project(project)
+            .branch("master")
+            .file(file)
+            .content("base content")
+            .create();
+    approve(Integer.toString(baseChange.get()));
+    gApi.changes().id(baseChange.get()).current().submit();
+
+    // Create another branch
+    String branchName = "foo";
+    createBranchWithRevision(
+        BranchNameKey.create(project, branchName),
+        projectOperations.project(project).getHead("master").name());
+
+    // Create a change in master that touches the file.
+    Change.Id changeInMaster =
+        changeOperations
+            .newChange()
+            .project(project)
+            .branch("master")
+            .file(file)
+            .content("master content")
+            .create();
+    approve(Integer.toString(changeInMaster.get()));
+    gApi.changes().id(changeInMaster.get()).current().submit();
+
+    // Create a change in the other branch and that touches the file.
+    Change.Id changeInOtherBranch =
+        changeOperations
+            .newChange()
+            .project(project)
+            .branch(branchName)
+            .file(file)
+            .content("other content")
+            .create();
+    approve(Integer.toString(changeInOtherBranch.get()));
+    gApi.changes().id(changeInOtherBranch.get()).current().submit();
+
+    // Create a merge change with a conflict resolution for the file.
+    Change.Id mergeChange =
+        changeOperations
+            .newChange()
+            .project(project)
+            .branch("master")
+            .mergeOfButBaseOnFirst()
+            .tipOfBranch("master")
+            .and()
+            .tipOfBranch(branchName)
+            .file(file)
+            .content("merged content")
+            .create();
+
+    String mergeRevision =
+        changeOperations.change(mergeChange).currentPatchset().get().commitId().name();
+    assertBranchNotFound(project, RefNames.refsCacheAutomerge(mergeRevision));
+  }
+
+  private void testGetRefWithAccessDatabase(Project.NameKey project, String ref)
+      throws RestApiException {
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    try {
+      requestScopeOperations.setApiUser(user.id());
+      assertBranchFound(project, ref);
+    } finally {
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(
+              capabilityKey(GlobalCapability.ACCESS_DATABASE)
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
+    }
+  }
+
+  private void assertBranchNotFound(Project.NameKey project, String ref) {
+    ResourceNotFoundException exception =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.projects().name(project.get()).branch(ref).get());
+    assertThat(exception).hasMessageThat().isEqualTo("Not found: " + ref);
+  }
+
+  private void assertBranchFound(Project.NameKey project, String ref) throws RestApiException {
+    BranchInfo branchInfo = gApi.projects().name(project.get()).branch(ref).get();
+    assertThat(branchInfo.ref).isEqualTo(RefNames.fullName(ref));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index ab2bb12..e269fc2 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -17,8 +17,11 @@
         "//lib:junit",
         "//lib/guice",
         "//lib/httpcomponents:httpcore",
+        "//lib/jackson:jackson-annotations",
         "//lib/log:api",
         "//lib/testcontainers",
+        "//lib/testcontainers:docker-java-api",
+        "//lib/testcontainers:docker-java-transport",
         "//lib/testcontainers:testcontainers-elasticsearch",
     ],
 )
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 86829b9..48295ea 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -19,6 +19,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.utility.DockerImageName;
 
 /* Helper class for running ES integration tests in docker container */
 public class ElasticContainer extends ElasticsearchContainer {
@@ -39,7 +40,7 @@
   private static String getImageName(ElasticVersion version) {
     switch (version) {
       case V6_8:
-        return "blacktop/elasticsearch:6.8.12";
+        return "blacktop/elasticsearch:6.8.13";
       case V7_0:
         return "blacktop/elasticsearch:7.0.1";
       case V7_1:
@@ -63,7 +64,9 @@
   }
 
   private ElasticContainer(ElasticVersion version) {
-    super(getImageName(version));
+    super(
+        DockerImageName.parse(getImageName(version))
+            .asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch"));
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 81cb732..65196bf 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
 import com.google.gerrit.server.project.ProjectCache;
@@ -63,6 +64,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Optional;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
@@ -90,6 +92,18 @@
     assertWithMessage("not owner").that(u.isOwner()).isFalse();
   }
 
+  private void assertAllRefsAreVisible(ProjectControl u) {
+    assertWithMessage("all refs visible")
+        .that(u.allRefsAreVisible(Collections.emptySet()))
+        .isTrue();
+  }
+
+  private void assertAllRefsAreNotVisible(ProjectControl u) {
+    assertWithMessage("all refs NOT visible")
+        .that(u.allRefsAreVisible(Collections.emptySet()))
+        .isFalse();
+  }
+
   private void assertNotOwner(String ref, ProjectControl u) {
     assertWithMessage("NOT OWN " + ref).that(u.controlForRef(ref).isOwner()).isFalse();
   }
@@ -105,11 +119,21 @@
   }
 
   private void assertCanRead(String ref, ProjectControl u) {
-    assertWithMessage("can read " + ref).that(u.controlForRef(ref).isVisible()).isTrue();
+    assertWithMessage("can read " + ref)
+        .that(
+            u.controlForRef(ref)
+                .hasReadPermissionOnRef(
+                    true)) // This should be false but the test relies on inheritance into refs/tags
+        .isTrue();
   }
 
   private void assertCannotRead(String ref, ProjectControl u) {
-    assertWithMessage("cannot read " + ref).that(u.controlForRef(ref).isVisible()).isFalse();
+    assertWithMessage("cannot read " + ref)
+        .that(
+            u.controlForRef(ref)
+                .hasReadPermissionOnRef(
+                    true)) // This should be false but the test relies on inheritance into refs/tags
+        .isFalse();
   }
 
   private void assertCanSubmit(String ref, ProjectControl u) {
@@ -171,6 +195,7 @@
   private final Project.NameKey parentKey = Project.nameKey("parent");
 
   @Inject private AllProjectsName allProjectsName;
+  @Inject private AllUsersName allUsersName;
   @Inject private InMemoryRepositoryManager repoManager;
   @Inject private MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject private ProjectCache projectCache;
@@ -262,6 +287,32 @@
   }
 
   @Test
+  public void allRefsAreVisibleForRegularProject() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(DEVS))
+        .add(allow(READ).ref("refs/groups/*").group(DEVS))
+        .add(allow(READ).ref("refs/users/default").group(DEVS))
+        .update();
+
+    assertAllRefsAreVisible(user(localKey, DEVS));
+  }
+
+  @Test
+  public void allRefsAreNotVisibleForAllUsers() throws Exception {
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(DEVS))
+        .add(allow(READ).ref("refs/groups/*").group(DEVS))
+        .add(allow(READ).ref("refs/users/default").group(DEVS))
+        .update();
+
+    assertAllRefsAreNotVisible(user(allUsersName, DEVS));
+  }
+
+  @Test
   public void branchDelegation1() throws Exception {
     projectOperations
         .project(localKey)
diff --git a/lib/jackson/BUILD b/lib/jackson/BUILD
index d5253a0..f11b96d 100644
--- a/lib/jackson/BUILD
+++ b/lib/jackson/BUILD
@@ -1,6 +1,14 @@
 load("@rules_java//java:defs.bzl", "java_library")
 
 java_library(
+    name = "jackson-annotations",
+    testonly = True,
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@jackson-annotations//jar"],
+)
+
+java_library(
     name = "jackson-core",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = [
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index 0cdad1a..8369024 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -12,6 +12,8 @@
 
 cat << EOF > $TMP/want
 cglib-3_2
+docker-java-api
+docker-java-transport
 dropwizard-core
 duct-tape
 eddsa
@@ -22,6 +24,7 @@
 httpasyncclient
 httpcore-nio
 j2objc
+jackson-annotations
 jackson-core
 jna
 jruby
diff --git a/lib/testcontainers/BUILD b/lib/testcontainers/BUILD
index a37b733..693a386 100644
--- a/lib/testcontainers/BUILD
+++ b/lib/testcontainers/BUILD
@@ -1,6 +1,22 @@
 load("@rules_java//java:defs.bzl", "java_library")
 
 java_library(
+    name = "docker-java-api",
+    testonly = True,
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@docker-java-api//jar"],
+)
+
+java_library(
+    name = "docker-java-transport",
+    testonly = True,
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@docker-java-transport//jar"],
+)
+
+java_library(
     name = "duct-tape",
     testonly = True,
     data = ["//lib:LICENSE-testcontainers"],
diff --git a/modules/jgit b/modules/jgit
index dd16976..e2663a8 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit dd169769bf42115e1dee749efeecab84544b28c4
+Subproject commit e2663a8b85cf92f6a84d72834257243a84066e9d
diff --git a/resources/com/google/gerrit/pgm/init/gerrit.sh b/resources/com/google/gerrit/pgm/init/gerrit.sh
index ce858d5..87a6c05 100755
--- a/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -296,6 +296,11 @@
 GERRIT_FDS=`expr $FDS_MULTIPLIER \* $GERRIT_FDS`
 test $GERRIT_FDS -lt 1024 && GERRIT_FDS=1024
 
+CACHE_FDS=`get_config --get cache.openFiles`
+if test -n "$CACHE_FDS"; then
+  GERRIT_FDS=`expr $CACHE_FDS \+ $GERRIT_FDS`
+fi
+
 GERRIT_STARTUP_TIMEOUT=`get_config --get container.startupTimeout`
 test -z "$GERRIT_STARTUP_TIMEOUT" && GERRIT_STARTUP_TIMEOUT=90  # seconds
 
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index a3cc66e..459143d 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -23,8 +23,8 @@
 
     maven_jar(
         name = "dropwizard-core",
-        artifact = "io.dropwizard.metrics:metrics-core:4.1.14",
-        sha1 = "14cf9dd67619a0390812dddb232df339e3383d35",
+        artifact = "io.dropwizard.metrics:metrics-core:4.1.12.1",
+        sha1 = "cb2f351bf4463751201f43bb99865235d5ba07ca",
     )
 
     SSHD_VERS = "2.4.0"
@@ -143,18 +143,40 @@
         sha1 = "dc13ae4faca6df981fc7aeb5a522d9db446d5d50",
     )
 
-    TESTCONTAINERS_VERSION = "1.14.3"
+    DOCKER_JAVA_VERS = "3.2.5"
+
+    maven_jar(
+        name = "docker-java-api",
+        artifact = "com.github.docker-java:docker-java-api:" + DOCKER_JAVA_VERS,
+        sha1 = "8fe5c5e39f940ce58620e77cedc0a2a52d76f9d8",
+    )
+
+    maven_jar(
+        name = "docker-java-transport",
+        artifact = "com.github.docker-java:docker-java-transport:" + DOCKER_JAVA_VERS,
+        sha1 = "27af0ee7ebc2f5672e23ea64769497b5d55ce3ac",
+    )
+
+    # https://github.com/docker-java/docker-java/blob/3.2.5/pom.xml#L61
+    # <=> DOCKER_JAVA_VERS
+    maven_jar(
+        name = "jackson-annotations",
+        artifact = "com.fasterxml.jackson.core:jackson-annotations:2.10.3",
+        sha1 = "0f63b3b1da563767d04d2e4d3fc1ae0cdeffebe7",
+    )
+
+    TESTCONTAINERS_VERSION = "1.15.0"
 
     maven_jar(
         name = "testcontainers",
         artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
-        sha1 = "071fc82ba663f469447a19434e7db90f3a872753",
+        sha1 = "b627535b444d88e7b14953bb953d80d9b7b3bd76",
     )
 
     maven_jar(
         name = "testcontainers-elasticsearch",
         artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
-        sha1 = "3709e2ebb0b6aa4e2ba2b6ca92ffdd3bf637a86c",
+        sha1 = "2bd79fd915e5c7bcf9b5d86cd8e0b7a0fff4b8ce",
     )
 
     maven_jar(