Allow to configure hideRefs for extra refs prefix to hide/show

Introduce the 'git-refs-filter' configuration settings
in gerrit.config which allows to specify `hideRefs' configs
similarly to what Git does, see [1]

[1] https://git-scm.com/docs/git-config/2.17.0#Documentation/git-config.txt-receivehideRefs

Change-Id: Ib6a703ecf2c977b49689743826b95b8b985cf734
diff --git a/README.md b/README.md
index debe9c9..c172399 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,21 @@
 - All '/meta' refs of all changes
 - All non-published edits of any changes
 
+It is also possible to define additional refs prefixes to be hidden or explicitly shown,
+using a similar syntax to the [hideRefs](https://git-scm.com/docs/git-config/2.17.0#Documentation/git-config.txt-receivehideRefs)
+setting, adding a set of `git-refs-filter.hideRefs` configuration settings in
+`gerrit.config`.
+
+Example of how to hide all `refs/backup/*` and `refs/sandbox/*` from being advertised
+but still show `refs/sandbox/mines/`:
+
+````
+[git-refs-filter]
+  hideRefs = refs/backup/
+  hideRefs = refs/sandbox/
+  hideRefs = !refs/sandbox/mine/
+```
+
 To enable a group of users of getting a "filtered list" of refs (e.g. CI jobs):
 - Define a new group of users (e.g. Builders)
 - Add a user to that group (e.g. Add 'jenkins' to the Builders group)
diff --git a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/FilterRefsConfig.java b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/FilterRefsConfig.java
new file mode 100644
index 0000000..2524f5e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/FilterRefsConfig.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2021 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.googlesource.gerrit.modules.gitrefsfilter;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
+
+public class FilterRefsConfig {
+  public static final String SECTION_GIT_REFS_FILTER = "git-refs-filter";
+  public static final String KEY_HIDE_REFS = "hideRefs";
+
+  private final List<String> hideRefs;
+  private final List<String> showRefs;
+
+  @Inject
+  public FilterRefsConfig(@GerritServerConfig Config gerritConfig) {
+
+    List<String> hideRefsConfig =
+        Arrays.asList(gerritConfig.getStringList(SECTION_GIT_REFS_FILTER, null, KEY_HIDE_REFS));
+
+    hideRefs =
+        ImmutableList.copyOf(
+            hideRefsConfig.stream()
+                .filter(s -> !s.startsWith("!"))
+                .map(String::trim)
+                .collect(Collectors.toList()));
+    showRefs =
+        ImmutableList.copyOf(
+            hideRefsConfig.stream()
+                .filter(s -> s.startsWith("!"))
+                .map(s -> s.substring(1))
+                .map(String::trim)
+                .collect(Collectors.toList()));
+  }
+
+  public boolean isRefToShow(Ref ref) {
+    String refName = ref.getName();
+
+    for (String refToShow : showRefs) {
+      if (refName.startsWith(refToShow)) {
+        return true;
+      }
+    }
+
+    for (String refToHide : hideRefs) {
+      if (refName.startsWith(refToHide)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/ForProjectWrapper.java b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/ForProjectWrapper.java
index b477670..0809840 100644
--- a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/ForProjectWrapper.java
+++ b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/ForProjectWrapper.java
@@ -41,6 +41,7 @@
   private final ForProject defaultForProject;
   private final Project.NameKey project;
   private final ChangeNotes.Factory changeNotesFactory;
+  private final FilterRefsConfig config;
 
   public interface Factory {
     ForProjectWrapper get(ForProject defaultForProject, Project.NameKey project);
@@ -49,11 +50,13 @@
   @Inject
   public ForProjectWrapper(
       ChangeNotes.Factory changeNotesFactory,
+      FilterRefsConfig config,
       @Assisted ForProject defaultForProject,
       @Assisted Project.NameKey project) {
     this.defaultForProject = defaultForProject;
     this.project = project;
     this.changeNotesFactory = changeNotesFactory;
+    this.config = config;
   }
 
   @Override
@@ -79,8 +82,9 @@
     return defaultForProject
         .filter(refs, repo, opts)
         .parallelStream()
-        .filter((ref) -> !ref.getName().startsWith(RefNames.REFS_USERS))
-        .filter((ref) -> !ref.getName().startsWith(RefNames.REFS_CACHE_AUTOMERGE))
+        .filter(ref -> !ref.getName().startsWith(RefNames.REFS_USERS))
+        .filter(ref -> !ref.getName().startsWith(RefNames.REFS_CACHE_AUTOMERGE))
+        .filter(config::isRefToShow)
         .filter(
             (ref) -> {
               String refName = ref.getName();
diff --git a/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/AbstractGitDaemonTest.java b/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/AbstractGitDaemonTest.java
index 8b1bd0b..ea1d61e 100644
--- a/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/AbstractGitDaemonTest.java
+++ b/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/AbstractGitDaemonTest.java
@@ -76,6 +76,11 @@
 
   protected TestRepository<InMemoryRepository> cloneProjectChangesRefs(TestAccount testAccount)
       throws Exception {
+    return cloneProjectRefs(testAccount, REFS_CHANGES);
+  }
+
+  protected TestRepository<InMemoryRepository> cloneProjectRefs(
+      TestAccount testAccount, String refsSpec) throws Exception {
     DfsRepositoryDescription desc = new DfsRepositoryDescription("clone of " + project.get());
 
     FS fs = FS.detect();
@@ -94,7 +99,7 @@
     Config cfg = dest.getConfig();
     String uri = registerRepoConnection(project, testAccount);
     cfg.setString("remote", "origin", "url", uri);
-    cfg.setString("remote", "origin", "fetch", REFS_CHANGES);
+    cfg.setString("remote", "origin", "fetch", refsSpec);
     TestRepository<InMemoryRepository> testRepo = GitUtil.newTestRepository(dest);
     FetchResult result = testRepo.git().fetch().setRemote("origin").call();
     String originMaster = "refs/remotes/origin/master";
diff --git a/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/GitRefsFilterTest.java b/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/GitRefsFilterTest.java
index d9d458b..957d4f7 100644
--- a/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/GitRefsFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/GitRefsFilterTest.java
@@ -20,9 +20,15 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
+import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.googlesource.gerrit.modules.gitrefsfilter.RefsFilterModule;
+import java.io.IOException;
+import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -38,6 +44,7 @@
 @NoHttpd
 @Sandboxed
 public class GitRefsFilterTest extends AbstractGitDaemonTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Override
   public Module createModule() {
@@ -83,6 +90,32 @@
     assertThat(getRefs(cloneProjectChangesRefs(admin))).isNotEmpty();
   }
 
+  @Test
+  @GerritConfig(name = "git-refs-filter.hideRefs", value = "refs/heads/sandbox/")
+  public void testUserWithHideRefsShouldNotSeeSandboxBranches() throws Exception {
+    String sandboxPrefix = "refs/heads/sandbox/";
+    requestScopeOperations.setApiUser(admin.id());
+    createBranch(BranchNameKey.create(project, "sandbox/foo"));
+
+    assertThat(getRefs(cloneProjectRefs(admin, "+refs/heads/*:refs/heads/*"), sandboxPrefix))
+        .isNotEmpty();
+    assertThat(getRefs(cloneProjectRefs(user, "+refs/heads/*:refs/heads/*"), sandboxPrefix))
+        .isEmpty();
+  }
+
+  @Test
+  @GerritConfig(
+      name = "git-refs-filter.hideRefs",
+      values = {"refs/heads/sandbox/", "!refs/heads/sandbox/mine"})
+  public void testUserWithHideRefsShouldSeeItsOwnSandboxBranch() throws Exception {
+    String sandboxPrefix = "refs/heads/sandbox/";
+    requestScopeOperations.setApiUser(admin.id());
+    createBranch(BranchNameKey.create(project, "sandbox/mine"));
+
+    assertThat(getRefs(cloneProjectRefs(user, "+refs/heads/*:refs/heads/*"), sandboxPrefix))
+        .isNotEmpty();
+  }
+
   protected Stream<String> fetchAllRefs(TestAccount testAccount) throws Exception {
     DfsRepositoryDescription desc = new DfsRepositoryDescription("clone of " + project.get());
 
@@ -99,4 +132,9 @@
     FetchResult result = testRepo.git().fetch().setRemote("origin").call();
     return result.getAdvertisedRefs().stream().map(Ref::getName);
   }
+
+  protected List<Ref> getRefs(TestRepository<InMemoryRepository> repo, String prefix)
+      throws IOException {
+    return repo.getRepository().getRefDatabase().getRefsByPrefix(prefix);
+  }
 }