Support named queries on group refs

By adding support for named queries on group refs allows users
to collaborate and review changes on centrally maintained named
queries. This is very helpful for Teams which require maintaining
named queries for CI systems in a common location. Update
documentation and tests accordingly.

Release-Notes: Named queries are now supported on group refs
Change-Id: Id96a721db27799e8a71c13858dd47582064980cb
diff --git a/Documentation/config-groups.txt b/Documentation/config-groups.txt
index 01e6141..40f64da 100644
--- a/Documentation/config-groups.txt
+++ b/Documentation/config-groups.txt
@@ -96,9 +96,9 @@
 Users can push changes to `refs/for/refs/groups/*`, but submit is rejected
 for changes which update group files (i.e. group.config, members, subgroups).
 It is possible for users to upload and submit changes on the named destination
-files in a group ref. Pushes that bypass Gerrit should be avoided since
-the names, IDs and UUIDs must be internally consistent between all the
-branches involved. In addition, group references should not be created
+or named query files in a group ref. Pushes that bypass Gerrit should be
+avoided since the names, IDs and UUIDs must be internally consistent between
+all the branches involved. In addition, group references should not be created
 or deleted manually either. If you attempt any of these actions
 anyway, don't forget to link:rest-api-groups.html#index-group[Index
 Group] reindex the affected groups manually.
diff --git a/Documentation/user-named-queries.txt b/Documentation/user-named-queries.txt
index c01f790..938cd53 100644
--- a/Documentation/user-named-queries.txt
+++ b/Documentation/user-named-queries.txt
@@ -1,11 +1,12 @@
 = Gerrit Code Review - Named Queries
 
-[[user-named-queries]]
-== User Named Queries
-It is possible to define named queries on a user level. To do
+[[user-or-group-named-queries]]
+== User Or Group Named Queries
+It is possible to define named queries on a user or group level. To do
 this, define the named queries in the `queries` file under the
-link:intro-user.html#user-refs[user's ref] in the `All-Users` project.  The
-user's queries file is a 2 column tab delimited file.  The left
+link:intro-user.html#user-refs[user's ref] or
+link:config-groups.html#_storage_format[group's ref] in the `All-Users`
+project. The named queries file is a 2 column tab delimited file. The left
 column represents the name of the query, and the right column
 represents the query expression represented by the name. The named queries
 can be publicly accessible by other users.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index b0c4a53..9ed4792 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -162,11 +162,13 @@
 'GROUP'.
 
 [[query]]
-query:'[name=]NAME[,user=USER]'::
+query:'[name=]NAME[,user=USER|,group=GROUP]'::
 +
-Changes which match the specified USER's query named 'NAME'. If 'USER'
-is unspecified, the current user is used. The named queries can be
-publicly accessible by other users.
+Changes which match the specified USER's or GROUP's query named 'NAME'.
+If neither 'USER' nor 'GROUP' is specified, the current user is used.
+The named queries can be publicly accessible by other users.
+The value may be wrapped in double quotes to include spaces. For example,
+`query:"myquery,group=My Group"`
 (see link:user-named-queries.html[Named Queries]).
 
 [[reviewer]]
diff --git a/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
index 5e63875..0269ccf 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountQueries.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -18,8 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import java.io.IOException;
@@ -37,8 +36,8 @@
 public class VersionedAccountQueries extends VersionedMetaData {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  public static VersionedAccountQueries forUser(Account.Id id) {
-    return new VersionedAccountQueries(RefNames.refsUsers(id));
+  public static VersionedAccountQueries forBranch(BranchNameKey branch) {
+    return new VersionedAccountQueries(branch.branch());
   }
 
   private final String ref;
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 60378b1..b3fa087 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -502,7 +502,7 @@
   protected final Arguments args;
   protected Map<String, String> hasOperandAliases = Collections.emptyMap();
   private final Map<BranchNameKey, DestinationList> destinationListByBranch = new HashMap<>();
-  private final Map<Account.Id, QueryList> queryListByAccount = new HashMap<>();
+  private final Map<BranchNameKey, QueryList> queryListByBranch = new HashMap<>();
 
   private static final Splitter RULE_SPLITTER = Splitter.on("=");
   private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
@@ -1421,11 +1421,16 @@
 
   @Operator
   public Predicate<ChangeData> query(String value) throws QueryParseException {
-    // [name=]<name>[,user=<user>] || [user=<user>,][name=]<name>
+    // [name=]NAME[,user=USER|,group=GROUP]
     PredicateArgs inputArgs = new PredicateArgs(value);
     String name = null;
     Account.Id account = null;
+    GroupDescription.Internal group = null;
 
+    if (inputArgs.keyValue.containsKey(ARG_ID_USER)
+        && inputArgs.keyValue.containsKey(ARG_ID_GROUP)) {
+      throw new QueryParseException("User and group arguments are mutually exclusive");
+    }
     // [name=]<name>
     if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
       name = inputArgs.keyValue.get(ARG_ID_NAME).value();
@@ -1449,7 +1454,23 @@
         account = self();
       }
 
-      String query = getQueryList(account).getQuery(name);
+      // [,group=<group>]
+      if (inputArgs.keyValue.containsKey(ARG_ID_GROUP)) {
+        AccountGroup.UUID groupId =
+            parseGroup(inputArgs.keyValue.get(ARG_ID_GROUP).value()).getUUID();
+        GroupDescription.Basic backendGroup = args.groupBackend.get(groupId);
+        if (!(backendGroup instanceof GroupDescription.Internal)) {
+          throw error(backendGroup.getName() + " is not an Internal group");
+        }
+        group = (GroupDescription.Internal) backendGroup;
+      }
+
+      BranchNameKey branch = BranchNameKey.create(args.allUsersName, RefNames.refsUsers(account));
+      if (group != null) {
+        branch = BranchNameKey.create(args.allUsersName, RefNames.refsGroups(group.getGroupUUID()));
+      }
+
+      String query = getQueryList(branch).getQuery(name);
       if (query != null) {
         return parse(query);
       }
@@ -1462,17 +1483,19 @@
     throw new QueryParseException("Unknown named query: " + name);
   }
 
-  protected QueryList getQueryList(Account.Id account) throws ConfigInvalidException, IOException {
-    QueryList ql = queryListByAccount.get(account);
+  protected QueryList getQueryList(BranchNameKey branch)
+      throws ConfigInvalidException, IOException {
+    QueryList ql = queryListByBranch.get(branch);
     if (ql == null) {
-      ql = loadQueryList(account);
-      queryListByAccount.put(account, ql);
+      ql = loadQueryList(branch);
+      queryListByBranch.put(branch, ql);
     }
     return ql;
   }
 
-  protected QueryList loadQueryList(Account.Id account) throws ConfigInvalidException, IOException {
-    VersionedAccountQueries q = VersionedAccountQueries.forUser(account);
+  protected QueryList loadQueryList(BranchNameKey branch)
+      throws ConfigInvalidException, IOException {
+    VersionedAccountQueries q = VersionedAccountQueries.forBranch(branch);
     try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
       q.load(args.allUsersName, git);
     }
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 693bb47..2c012fa 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -3962,11 +3962,13 @@
 
   @GerritConfig(name = "accounts.visibility", value = "NONE")
   @Test
-  public void userQuery() throws Exception {
+  public void namedQuery() throws Exception {
     repo = createAndOpenProject("repo");
     Change change1 = insert("repo", newChange(repo));
     Change change2 = insert("repo", newChangeForBranch(repo, "stable"));
 
+    String group = "test-group";
+    AccountGroup.UUID groupId = groupOperations.newGroup().name(group).create();
     Account.Id anotherUserId = createAccount("anotheruser");
     String queryListText =
         "query1\tproject:repo\n"
@@ -3983,14 +3985,20 @@
             new TestRepository<>(repoManager.openRepository(allUsersName));
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
         MetaDataUpdate anotherMd = metaDataUpdateFactory.create(allUsersName)) {
-      VersionedAccountQueries queries = VersionedAccountQueries.forUser(userId);
+      VersionedAccountQueries queries =
+          VersionedAccountQueries.forBranch(
+              BranchNameKey.create(allUsersName, RefNames.refsUsers(userId)));
       queries.load(md);
       queries.setQueryList(queryListText);
       queries.commit(md);
-      VersionedAccountQueries anotherQueries = VersionedAccountQueries.forUser(anotherUserId);
+      VersionedAccountQueries anotherQueries =
+          VersionedAccountQueries.forBranch(
+              BranchNameKey.create(allUsersName, RefNames.refsUsers(anotherUserId)));
       anotherQueries.load(anotherMd);
       anotherQueries.setQueryList(anotherQueryListText);
       anotherQueries.commit(anotherMd);
+
+      allUsers.branch(RefNames.refsGroups(groupId)).commit().add("queries", queryListText).create();
     }
 
     assertThat(gApi.accounts().self().get()._accountId).isEqualTo(userId.get());
@@ -4021,6 +4029,23 @@
     assertQuery("query:query6,user=" + anotherUserId, change1);
     assertQuery("query:user=" + anotherUserId + ",query7", change2);
     assertQuery("query:query8,user=" + anotherUserId);
+
+    // Group queries
+    assertThatQueryException("query:non-existent,group=" + group)
+        .hasMessageThat()
+        .isEqualTo("Unknown named query: non-existent");
+    assertThatQueryException("query:query1,group=non-existent-group")
+        .hasMessageThat()
+        .isEqualTo("Group non-existent-group not found");
+    assertThatQueryException("query:query1,group=" + group + ",user=" + userId)
+        .hasMessageThat()
+        .isEqualTo("User and group arguments are mutually exclusive");
+
+    assertQuery("query:name=query1,group=" + group, change1, change2);
+    assertQuery("query:query1,group=" + group, change1, change2);
+    assertQuery("query:group=" + group + ",name=query2", change2);
+    assertQuery("query:group=" + group + ",query4");
+    assertQuery("query:name=query4,group=" + group);
   }
 
   @Test