Support to import tasks from group refs

This change adds support to import a single task from any group
ref (All-Users.git:/refs/groups/..) by specifying the group name or
group uuid in task config file.

See documentation for usage details.

Release-Notes: tasks can now be imported from group refs
Release-Notes: a task reference cannot start with '%' unless referring to a group
Change-Id: Ie0b346acff17de14768676ca0c64b61f8d345610
diff --git a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4 b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
index fc461d7..3869643 100644
--- a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
+++ b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
@@ -19,6 +19,8 @@
  * TASK_REFERENCE = [
  *                    [ // TASK_FILE_PATH ] |
  *                    [ @USERNAME [ TASK_FILE_PATH ] ] |
+ *                    [ %GROUP_NAME [ TASK_FILE_PATH ] ] |
+ *                    [ %%GROUP_UUID [ TASK_FILE_PATH ] ] |
  *                    [ TASK_FILE_PATH ]
  *                  ] '^' TASK_NAME
  *
@@ -63,6 +65,26 @@
  * reference: //^simple
  * Implied task:
  *     file: All-Projects:refs/meta/config:task.config task: sample
+ *
+ * Suppose a8341ade45d83e867c24a2d37f47b410cfdbea6d is the UUID of 'CI System Owners' group.
+ * file: Any projects, ref, file
+ * reference: %CI System Owners^sample
+ * Implied task:
+ *     file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task.config
+ *     task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: %CI System Owners/foo^simple
+ * Implied task:
+ *     file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task/foo^simple
+ *     task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: %%a8341ade45d83e867c24a2d37f47b410cfdbea6d^sample
+ * Implied task:
+ *     file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task.config
+ *     task: sample
+ *
  */
 
 grammar TaskReference;
@@ -79,6 +101,8 @@
  : ALL_PROJECTS_ROOT
  | FWD_SLASH absolute TASK_DELIMETER
  | user absolute? TASK_DELIMETER
+ | group_name absolute? TASK_DELIMETER
+ | group_uuid absolute? TASK_DELIMETER
  | (absolute| relative)? TASK_DELIMETER
  ;
 
@@ -86,6 +110,14 @@
  : '@' NAME
  ;
 
+group_name
+ : '%' (NAME | NAME_WITH_SPACES)
+ ;
+
+group_uuid
+ : '%%' INTERNAL_GROUP_UUID
+ ;
+
 absolute
  : FWD_SLASH relative
  ;
@@ -102,23 +134,43 @@
  : (~'^')+ EOF
  ;
 
+INTERNAL_GROUP_UUID
+ : HEX_10 HEX_10 HEX_10 HEX_10
+ ;
+
+fragment HEX_10
+ : HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX
+ ;
+
+fragment HEX
+ : [0-9a-f]
+ ;
+
 NAME
- : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH*
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH*
+ ;
+
+NAME_WITH_SPACES
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE (URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH | SPACE)*
  ;
 
 fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH
- : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT
- | '@'
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE
+ | '@' | '%'
  ;
 
-fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT
+fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE
  : ':' | '?' | '#' | '[' | ']'
  |'!' | '$' | '&' | '\'' | '(' | ')'
- | '*' | '+' | ',' | ';' | '=' | '%'
+ | '*' | '+' | ',' | ';' | '='
  | 'A'..'Z' | 'a'..'z' | '0'..'9'
  | '_' | '.' | '\\' | '-' | '~'
  ;
 
+fragment SPACE
+ : ' '
+ ;
+
 TASK_DELIMETER
  : '^'
  ;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
index a6bc7e1..bd0b683 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -16,9 +16,11 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Preconditions;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import java.nio.file.Path;
@@ -66,16 +68,19 @@
     protected BranchNameKey branch;
     protected String file;
     protected String task;
+    protected GroupCache groupCache;
 
     Builder(
         FileKey relativeTo,
         AllProjectsName allProjectsName,
         AllUsersName allUsersName,
-        AccountCache accountCache) {
+        AccountCache accountCache,
+        GroupCache groupCache) {
       this.relativeTo = relativeTo;
       this.allProjectsName = allProjectsName;
       this.allUsersName = allUsersName;
       this.accountCache = accountCache;
+      this.groupCache = groupCache;
     }
 
     public TaskKey buildTaskKey() {
@@ -134,6 +139,34 @@
                       .id()));
     }
 
+    public void setGroupName(String groupName) throws ConfigInvalidException {
+      branch =
+          BranchNameKey.create(
+              allUsersName,
+              RefNames.refsGroups(
+                  groupCache
+                      .get(AccountGroup.nameKey(groupName))
+                      .orElseThrow(
+                          () ->
+                              new ConfigInvalidException(
+                                  String.format("Cannot resolve group name: %s", groupName)))
+                      .getGroupUUID()));
+    }
+
+    public void setGroupUUID(String uuid) throws ConfigInvalidException {
+      branch =
+          BranchNameKey.create(
+              allUsersName,
+              RefNames.refsGroups(
+                  groupCache
+                      .get(AccountGroup.uuid(uuid))
+                      .orElseThrow(
+                          () ->
+                              new ConfigInvalidException(
+                                  String.format("Cannot resolve group uuid: %s", uuid)))
+                      .getGroupUUID()));
+    }
+
     public void setReferringAllProjectsTask() {
       branch = BranchNameKey.create(allProjectsName, RefNames.REFS_CONFIG);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
index 91c6f10..a7824ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
@@ -16,6 +16,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.inject.Inject;
@@ -46,11 +47,16 @@
       AllProjectsNameProvider allProjectsNameProvider,
       AllUsersNameProvider allUsersNameProvider,
       AccountCache accountCache,
+      GroupCache groupCache,
       @Assisted FileKey relativeTo,
       @Assisted String reference) {
     this(
         new TaskKey.Builder(
-            relativeTo, allProjectsNameProvider.get(), allUsersNameProvider.get(), accountCache),
+            relativeTo,
+            allProjectsNameProvider.get(),
+            allUsersNameProvider.get(),
+            accountCache,
+            groupCache),
         reference);
   }
 
@@ -148,5 +154,27 @@
         throw new RuntimeConfigInvalidException(e);
       }
     }
+
+    @Override
+    public void enterGroup_name(TaskReferenceParser.Group_nameContext ctx) {
+      try {
+        String groupName =
+            ctx.NAME() == null
+                ? (ctx.NAME_WITH_SPACES() == null ? "" : ctx.NAME_WITH_SPACES().getText())
+                : ctx.NAME().getText();
+        builder.setGroupName(groupName);
+      } catch (ConfigInvalidException e) {
+        throw new RuntimeConfigInvalidException(e);
+      }
+    }
+
+    @Override
+    public void enterGroup_uuid(TaskReferenceParser.Group_uuidContext ctx) {
+      try {
+        builder.setGroupUUID(ctx.INTERNAL_GROUP_UUID().getText());
+      } catch (ConfigInvalidException e) {
+        throw new RuntimeConfigInvalidException(e);
+      }
+    }
   }
 }
diff --git a/src/main/resources/Documentation/task_expression.md b/src/main/resources/Documentation/task_expression.md
index 66afaf8..b631cb3 100644
--- a/src/main/resources/Documentation/task_expression.md
+++ b/src/main/resources/Documentation/task_expression.md
@@ -40,6 +40,8 @@
  TASK_REFERENCE = [
                     [ // TASK_FILE_PATH ]
                     [ @USERNAME [ TASK_FILE_PATH ] ] |
+                    [ %GROUP_NAME [ TASK_FILE_PATH ] ] |
+                    [ %%GROUP_UUID [ TASK_FILE_PATH ] ] |
                     [ TASK_FILE_PATH ]
                   ] '^' TASK_NAME
 ```
@@ -181,3 +183,38 @@
     preload-task = //^root task
     ...
 ```
+
+To reference a task from a specific group ref (All-Users.git:refs/groups/<sharded-group-uuid>),
+specify the group name with `%` or group uuid with `%%`.
+
+When referencing from group refs, to get task from top level task.config on a group ref use
+`%<group_name>^<task_name>` or `%%<group_uuid>^<task_name>` and to get any task under the
+task directory use the relative path,
+like: `%<group_name>/<relative path from task dir>^<task_name>` or
+`%%<group_uuid>/<relative path from task dir>^<task_name>`.
+It doesn't matter which project, ref and file one is referencing from while using this syntax.
+
+Example:
+Assumption: Group uuid of group_a is 720269095421a08a24889e29d092df1839a7a706
+
+All-Users:refs/groups/72/720269095421a08a24889e29d092df1839a7a706:task.config
+```
+    ...
+    [task "top level task"]
+    ...
+```
+
+All-Users:refs/groups/72/720269095421a08a24889e29d092df1839a7a706:/task/dir/common.config
+```
+    ...
+    [task "common task"]
+    ...
+```
+
+All-Projects:refs/meta/config:/task.config
+```
+    ...
+    preload-task = %group_a^top level task
+    preload-task = %%720269095421a08a24889e29d092df1839a7a706/dir/common.config^common task
+    ...
+```
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
new file mode 100644
index 0000000..504d5a8
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -0,0 +1,52 @@
+# --task-preview root file with subtask pointing to a non-secret group ref with subtask pointing to a secret group ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview NON-SECRET group subtask with SECRET group subtask"]
+     applicable = "is:open"
+     pass = True
++    subtask = %{non_secret_group_name}/secret_external.config^NON-SECRET with SECRET subtask
+```
+
+file: `All-Users.git:refs/groups/{sharded_non_secret_group_uuid}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET subtask"]
+    applicable = is:open
+    pass = True
+    subtask = %{secret_group_name}/secret.config^SECRET task
+```
+
+file: `All-Users:refs/groups/{sharded_secret_group_uuid}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview NON-SECRET group subtask with SECRET group subtask",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "NON-SECRET with SECRET subtask",
+         "status" : "WAITING",
+         "subTasks" : [
+            {
+               "name" : "UNKNOWN",            # Only Test Suite: non-secret
+               "status" : "UNKNOWN"           # Only Test Suite: non-secret
+               "applicable" : true,           # Only Test Suite: secret
+               "hasPass" : true,              # Only Test Suite: secret
+               "name" : "SECRET task",        # Only Test Suite: secret
+               "status" : "READY"             # Only Test Suite: secret
+            }
+         ]
+      }
+   ]
+}
+```
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
new file mode 100644
index 0000000..9a1932c
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
@@ -0,0 +1,36 @@
+# --task-preview root file with subtask pointing to secret group ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external group"]
+     applicable = is:open
+     pass = True
++    subtask = %{secret_group_name}/secret.config^SECRET Task
+```
+
+file: `All-Users.git:refs/groups/{sharded_secret_group_uuid}:task/secret.config`
+```
+[task "SECRET Task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview SECRET external group",
+   "status" : "WAITING",
+   "subTasks" : [
+      {
+         "name" : "UNKNOWN",                  # Only Test Suite: non-secret
+         "status" : "UNKNOWN"                 # Only Test Suite: non-secret
+         "applicable" : true,                 # Only Test Suite: secret
+         "hasPass" : true,                    # Only Test Suite: secret
+         "name" : "SECRET Task",              # Only Test Suite: secret
+         "status" : "READY"                   # Only Test Suite: secret
+      }
+   ]
+}
+```
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index f4f8842..2df1263 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -2318,11 +2318,67 @@
    ]
 }
 
+[root "Root Import group tasks"]
+  applicable = is:open
+  subtask = %{non_secret_group_name_without_space}/foo/bar.config^Absolute Task 1
+  subtask = %{non_secret_group_name_without_space}^task in group root config file 1
+  subtask = %{non_secret_group_name_with_space}/foo/bar.config^Absolute Task 3
+  subtask = %{non_secret_group_name_with_space}^task in group root config file 3
+  subtask = %%{non_secret_group_uuid}/foo/bar.config^Absolute Task 2
+  subtask = %%{non_secret_group_uuid}^task in group root config file 2
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Import group tasks",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task 1",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in group root config file 1",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task 3",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in group root config file 3",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task 2",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in group root config file 2",
+         "status" : "PASS"
+      }
+   ]
+}
+
 [root "Root Reference tasks from All-Projects"]
   applicable = is:open
   subtask = //^Subtask PASS
   subtask = @testuser/dir/relative.config^Import All-Projects root task
   subtask = @testuser/dir/relative.config^Import All-Projects non-root task
+  subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import All-Projects root task - groups
+  subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import All-Projects non-root task - groups
 
 {
    "applicable" : true,
@@ -2363,6 +2419,34 @@
                "status" : "PASS"
             }
          ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Import All-Projects root task - groups",
+         "status" : "PASS",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Subtask PASS",
+               "status" : "PASS"
+            }
+         ]
+      },
+      {
+         "applicable" : true,
+         "hasPass" : false,
+         "name" : "Import All-Projects non-root task - groups",
+         "status" : "PASS",
+         "subTasks" : [
+            {
+               "applicable" : true,
+               "hasPass" : true,
+               "name" : "Sample relative task in sub dir",
+               "status" : "PASS"
+            }
+         ]
       }
    ]
 }
@@ -3206,3 +3290,50 @@
   applicable = is:open
   pass = is:open
 ```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/dir/relative.config`
+```
+[task "Import All-Projects root task - groups"]
+  applicable = is:open
+  subtask = //^Subtask PASS
+
+[task "Import All-Projects non-root task - groups"]
+  applicable = is:open
+  subtask = //dir/common.config^Sample relative task in sub dir
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/foo/bar.config`
+```
+[task "Absolute Task 1"]
+  applicable = is:open
+  pass = is:open
+
+[task "Absolute Task 2"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task.config`
+```
+[task "task in group root config file 1"]
+  applicable = is:open
+  pass = is:open
+
+[task "task in group root config file 2"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_with_space}:task/foo/bar.config`
+```
+[task "Absolute Task 3"]
+  applicable = is:open
+  pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_with_space}:task.config`
+```
+[task "task in group root config file 3"]
+  applicable = is:open
+  pass = is:open
+```
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
index 87042a9..8ea0009 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import java.util.Iterator;
@@ -233,6 +234,7 @@
 
   protected TaskExpression getTaskExpression(FileKey file, String expression) {
     AccountCache accountCache = Mockito.mock(AccountCache.class);
+    GroupCache groupCache = Mockito.mock(GroupCache.class);
     TaskReference.Factory factory = Mockito.mock(TaskReference.Factory.class);
     Mockito.when(factory.create(Mockito.any(), Mockito.any()))
         .thenAnswer(
@@ -242,7 +244,8 @@
                         (FileKey) invocation.getArguments()[0],
                         new AllProjectsName("All-Projects"),
                         new AllUsersName("All-Users"),
-                        accountCache),
+                        accountCache,
+                        groupCache),
                     (String) invocation.getArguments()[1]));
     return new TaskExpression(factory, file, expression);
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
index 9da1f44..e08240d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
@@ -14,12 +14,16 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.InternalGroup;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import java.sql.Timestamp;
@@ -54,6 +58,41 @@
   public static final FileKey TEST_USER_COMMON_CFG =
       createFileKey(ALL_USERS, TEST_USER_REF, COMMON);
 
+  public static final AccountGroup.NameKey TEST_GROUP1_NAME = AccountGroup.nameKey("testgroup");
+  public static final AccountGroup.NameKey TEST_GROUP2_NAME = AccountGroup.nameKey("test group");
+  public static final String TEST_GROUP1_UUID = "526d2bf882635380fbd3b72320464e342fc14533";
+  public static final String TEST_GROUP2_UUID = "62aa5663241f31b9483bad66132bd5d416b2bef9";
+  public static final InternalGroup TEST_GROUP1 =
+      buildTestGroup(AccountGroup.id(1), TEST_GROUP1_NAME, AccountGroup.uuid(TEST_GROUP1_UUID));
+  public static final InternalGroup TEST_GROUP2 =
+      buildTestGroup(AccountGroup.id(2), TEST_GROUP2_NAME, AccountGroup.uuid(TEST_GROUP2_UUID));
+  public static final String TEST_GROUP1_REF =
+      "refs/groups/" + TEST_GROUP1_UUID.substring(0, 2) + "/" + TEST_GROUP1_UUID;
+  public static final String TEST_GROUP2_REF =
+      "refs/groups/" + TEST_GROUP2_UUID.substring(0, 2) + "/" + TEST_GROUP2_UUID;
+  public static final FileKey TEST_GROUP1_ROOT_CFG =
+      createFileKey(ALL_USERS, TEST_GROUP1_REF, ROOT);
+  public static final FileKey TEST_GROUP1_COMMON_CFG =
+      createFileKey(ALL_USERS, TEST_GROUP1_REF, COMMON);
+  public static final FileKey TEST_GROUP2_ROOT_CFG =
+      createFileKey(ALL_USERS, TEST_GROUP2_REF, ROOT);
+  public static final FileKey TEST_GROUP2_COMMON_CFG =
+      createFileKey(ALL_USERS, TEST_GROUP2_REF, COMMON);
+
+  static InternalGroup buildTestGroup(
+      AccountGroup.Id id, AccountGroup.NameKey nameKey, AccountGroup.UUID uuid) {
+    return InternalGroup.builder()
+        .setGroupUUID(uuid)
+        .setNameKey(nameKey)
+        .setOwnerGroupUUID(uuid)
+        .setId(id)
+        .setVisibleToAll(true)
+        .setCreatedOn(new Timestamp(0L))
+        .setMembers(ImmutableSet.of())
+        .setSubgroups(ImmutableSet.of())
+        .build();
+  }
+
   @Test
   public void testReferencingTaskFromSameFile() throws Exception {
     assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, SIMPLE));
@@ -137,17 +176,98 @@
     assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, empty));
   }
 
+  @Test
+  public void testReferencingRootGroupNameWithoutSpaceTask() throws Exception {
+    String reference = "%" + TEST_GROUP1_NAME.get() + "^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP1_ROOT_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRootGroupNameWithSpaceTask() throws Exception {
+    String reference = "%" + TEST_GROUP2_NAME.get() + "^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP2_ROOT_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingGroupNameWithoutSpaceTaskDir() throws Exception {
+    String reference = "%" + TEST_GROUP1_NAME.get() + "/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP1_COMMON_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingUnknownGroupName() throws Exception {
+    String reference = "%unknown^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingEmptyGroupName() throws Exception {
+    String reference = "%^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingGroupNameWithSpaceTaskDir() throws Exception {
+    String reference = "%" + TEST_GROUP2_NAME.get() + "/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP2_COMMON_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingRootGroupUUIDTask() throws Exception {
+    String reference = "%%" + TEST_GROUP1_UUID + "^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP1_ROOT_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingGroupUUIDTaskDir() throws Exception {
+    String reference = "%%" + TEST_GROUP1_UUID + "/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_GROUP1_COMMON_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingUnknownGroupUUID() throws Exception {
+    String reference = "%%a8341ade45d83e867c24a2d37f47b410cfdbea6d^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingEmptyGroupUUID() throws Exception {
+    String reference = "%%^" + SIMPLE;
+    assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
   protected static TaskKey getTaskFromReference(FileKey file, String expression) {
     AccountCache accountCache = Mockito.mock(AccountCache.class);
+    GroupCache groupCache = Mockito.mock(GroupCache.class);
     Mockito.when(accountCache.getByUsername(TEST_USER))
         .thenReturn(Optional.of(AccountState.forAccount(TEST_USER_ACCOUNT)));
+    Mockito.when(groupCache.get(TEST_GROUP1_NAME)).thenReturn(Optional.of(TEST_GROUP1));
+    Mockito.when(groupCache.get(TEST_GROUP2_NAME)).thenReturn(Optional.of(TEST_GROUP2));
+    Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP1_UUID)))
+        .thenReturn(Optional.of(TEST_GROUP1));
+    Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP2_UUID)))
+        .thenReturn(Optional.of(TEST_GROUP2));
+
     try {
       return new TaskReference(
               new TaskKey.Builder(
                   file,
                   new AllProjectsName(ALL_PROJECTS),
                   new AllUsersName(ALL_USERS),
-                  accountCache),
+                  accountCache,
+                  groupCache),
               expression)
           .getTaskKey();
     } catch (ConfigInvalidException e) {
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index fc7e4c6..1e262b3 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -23,14 +23,31 @@
 # 6. All-Users.git - must have 'read' rights on refs/users/${shardeduserid} for Registered Users
 # 7. All-Users.git - must have 'create' rights on refs/users/${shardeduserid} for Registered Users
 # 8. All-Users.git - must deny 'read' rights on refs/* for Anonymous Users
+# 9. GERRIT_GIT_DIR environment variable must have the path to gerrit
+#    site's git directory (as group ref updates are done directly to git).
 
 create_configs_from_task_states() {
     for marker in $(md_file_markers "$DOC_STATES") ; do
-        local project="$OUT/$(md_file_marker_project "$marker")"
+        local project_name="$(md_file_marker_project "$marker")"
+        local project_dir="$OUT/$project_name"
         local file="$(md_file_marker_file "$marker")"
+        local ref="$(md_file_marker_ref "$marker")"
 
-        mkdir -p "$(dirname "$project/$file")"
-        md_marker_content "$DOC_STATES" "$marker" | replace_user | testdoc_2_cfg > "$project/$file"
+        if [[ "$ref" == refs/groups/* ]] ; then
+            project_dir="$project_dir-${ref:(-7)}}"
+            q_setup setup_repo "$project_dir" "$REMOTE_USERS" "$ref"
+        fi
+
+        mkdir -p "$(dirname "$project_dir/$file")"
+        md_marker_content "$DOC_STATES" "$marker" | replace_user \
+            | testdoc_2_cfg > "$project_dir/$file"
+
+        if [[ "$ref" == refs/groups/* ]] ; then
+            # As support for pushing a change to group refs [1] is not yet in any release,
+            # push the update behind gerrit's back, directly into git.
+            # [1] https://gerrit-review.googlesource.com/c/gerrit/+/390614
+            q_setup update_repo "$project_dir" "$GERRIT_GIT_DIR/All-Users.git" "$ref"
+        fi
     done
 }
 
@@ -53,12 +70,14 @@
     "$MYPROG" --server <gerrit_host> --non-secret-user <non-secret user>
     --untrusted-user <untrusted user>
 
-    --help|-h                     help text
-    --server|-s                   gerrit host
-    --non-secret-user             user who don't have permission
-                                  to view other user refs.
-    --untrusted-user              user who doesn't have permission
-                                  to view refs/meta/config ref on All-Projects repo
+    --help|-h                         help text
+    --server|-s                       gerrit host
+    --non-secret-user                 user who don't have permission
+                                      to view other user refs.
+    --untrusted-user                  user who doesn't have permission
+                                      to view refs/meta/config ref on All-Projects repo
+    --non-secret-group-without-space  non-secret group name without spaces
+    --non-secret-group-with-space     non-secret group name with spaces
 EOF
 
     [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; }
@@ -90,11 +109,13 @@
 
 while (( "$#" )) ; do
     case "$1" in
-        --help|-h)                usage ;;
-        --server|-s)              shift ; SERVER=$1 ;;
-        --non-secret-user)        shift ; NON_SECRET_USER=$1 ;;
-        --untrusted-user)         shift ; UNTRUSTED_USER=$1 ;;
-        *)                        usage "invalid argument $1" ;;
+        --help|-h)                        usage ;;
+        --server|-s)                      shift ; SERVER=$1 ;;
+        --non-secret-user)                shift ; NON_SECRET_USER=$1 ;;
+        --untrusted-user)                 shift ; UNTRUSTED_USER=$1 ;;
+        --non-secret-group-without-space) shift ; GROUP_NAME_WITHOUT_SPACE=$1 ;;
+        --non-secret-group-with-space)    shift ; GROUP_NAME_WITH_SPACE=$1 ;;
+        *)                                usage "invalid argument $1" ;;
     esac
     shift
 done
@@ -102,6 +123,9 @@
 [ -z "$SERVER" ] && usage "You must specify --server"
 [ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user"
 [ -z "$UNTRUSTED_USER" ] && usage "You must specify --untrusted-user"
+[ -z "$GROUP_NAME_WITHOUT_SPACE" ] && usage "You must specify --non-secret-group-without-space"
+[ -z "$GROUP_NAME_WITH_SPACE" ] && usage "You must specify --non-secret-group-with-space"
+[ -z "$GERRIT_GIT_DIR" ] && usage "GERRIT_GIT_DIR environment variable not set"
 
 
 PORT=29418
@@ -120,6 +144,13 @@
 declare -A USER_REFS
 USER_REFS["{testuser_user_ref}"]="$(get_user_ref "$USER")"
 
+declare -A GROUP_EXPANDED_BY_PLACEHOLDER
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name_without_space}"]="$GROUP_NAME_WITHOUT_SPACE"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name_with_space}"]="$GROUP_NAME_WITH_SPACE"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_uuid}"]="$(get_group_uuid "$GROUP_NAME_WITHOUT_SPACE")"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid_without_space}"]="$(get_sharded_group_uuid "$GROUP_NAME_WITHOUT_SPACE")"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid_with_space}"]="$(get_sharded_group_uuid "$GROUP_NAME_WITH_SPACE")"
+
 mkdir -p "$OUT" "$ALL_TASKS" "$USER_TASKS"
 
 q_setup setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
diff --git a/test/check_task_visibility.sh b/test/check_task_visibility.sh
index b736337..1dabec4 100755
--- a/test/check_task_visibility.sh
+++ b/test/check_task_visibility.sh
@@ -23,6 +23,8 @@
 # 6. All-Users.git - must have 'read' rights on refs/users/${shardeduserid} for Registered Users
 # 7. All-Users.git - must have 'create' rights on refs/users/${shardeduserid} for Registered Users
 # 8. All-Users.git - must deny 'read' rights on refs/* for Anonymous Users
+# 9. GERRIT_GIT_DIR environment variable must have the path to gerrit
+#    site's git directory (as group ref updates are done directly to git).
 
 readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
 MYDIR=$(dirname -- "$(readlink -f -- "$0")")
@@ -139,7 +141,14 @@
 
         echo "$tip_content" > "$project/$file"
         config_ensure "$project/$file"
-        q_setup update_repo "$project" "$(get_remote "$project")" "$ref"
+        if [[ "$ref" == refs/groups/* ]] ; then
+            # As support for pushing a change to group refs [1] is not yet in any release,
+            # push the update behind gerrit's back, directly into git.
+            # [1] https://gerrit-review.googlesource.com/c/gerrit/+/390614
+            q_setup update_repo "$project" "$GERRIT_GIT_DIR/All-Users.git" "$ref"
+        else
+            q_setup update_repo "$project" "$(get_remote "$project")" "$ref"
+        fi
     done
 }
 
@@ -166,6 +175,8 @@
     --server|-s                   gerrit host
     --non-secret-user             user who doesn't have permission
                                   to view other user refs.
+    --non-secret-group            non-secret group name
+    --secret-group                secret group name
 EOF
 
     [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; }
@@ -177,6 +188,8 @@
         --help|-h)                usage ;;
         --server|-s)              shift ; SERVER=$1 ;;
         --non-secret-user)        shift ; NON_SECRET_USER=$1 ;;
+        --non-secret-group)       shift ; NON_SECRET_GROUP_NAME=$1 ;;
+        --secret-group)           shift ; SECRET_GROUP_NAME=$1 ;;
         *)                        usage "invalid argument $1" ;;
     esac
     shift
@@ -184,6 +197,9 @@
 
 [ -z "$SERVER" ] && usage "You must specify --server"
 [ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user"
+[ -z "$NON_SECRET_GROUP_NAME" ] && usage "You must specify --non-secret-group"
+[ -z "$SECRET_GROUP_NAME" ] && usage "You must specify --secret-group"
+[ -z "$GERRIT_GIT_DIR" ] && usage "GERRIT_GIT_DIR environment variable not set"
 
 RESULT=0
 PORT=29418
@@ -202,6 +218,12 @@
 USERS["{non_secret_user}"]="$NON_SECRET_USER"
 USER_REFS["{non_secret_user_ref}"]="$(get_user_ref "$NON_SECRET_USER")"
 
+declare -A GROUP_EXPANDED_BY_PLACEHOLDER
+GROUP_EXPANDED_BY_PLACEHOLDER["{secret_group_name}"]="$SECRET_GROUP_NAME"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_secret_group_uuid}"]="$(get_sharded_group_uuid "$SECRET_GROUP_NAME")"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name}"]="$NON_SECRET_GROUP_NAME"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid}"]="$(get_sharded_group_uuid "$NON_SECRET_GROUP_NAME")"
+
 mkdir -p "$OUT"
 trap 'rm -rf "$OUT"' EXIT
 
@@ -213,10 +235,12 @@
 "non_root_with_subtask_from_root_task.md"
 "subtask_using_user_syntax/root_with_subtask_secret_ref.md"
 "subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md"
+"subtask_using_group_syntax/root_with_subtask_secret_ref.md"
+"subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md"
 )
 
 for test in "${TESTS[@]}" ; do
-    TEST_DOC="$(replace_user_refs < "$TEST_DOC_DIR/$test" | replace_users)"
+    TEST_DOC="$(replace_user_refs < "$TEST_DOC_DIR/$test" | replace_users | replace_groups)"
     init_configs
     test_change
 done
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
index 634cde4..a228122 100755
--- a/test/docker/docker-compose.yaml
+++ b/test/docker/docker-compose.yaml
@@ -11,6 +11,7 @@
       - gerrit-net
     volumes:
       - "gerrit-site-etc:/var/gerrit/etc"
+      - "gerrit-site-git:/var/gerrit/git"
 
   run_tests:
     build: run_tests
@@ -19,10 +20,12 @@
     volumes:
       - "../../:/task:ro"
       - "gerrit-site-etc:/server-ssh-key:ro"
+      - "gerrit-site-git:/gerrit-site-git"
     depends_on:
       - gerrit-01
     environment:
       - GERRIT_HOST=gerrit-01
+      - GERRIT_GIT_DIR=/gerrit-site-git
 
 networks:
   gerrit-net:
@@ -30,3 +33,4 @@
 
 volumes:
   gerrit-site-etc:
+  gerrit-site-git:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index d638517..72132d7 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -8,3 +8,4 @@
 COPY artifacts /tmp/
 RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar"
 RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
+RUN chmod 777 "$GERRIT_SITE/git"
diff --git a/test/docker/run_tests/create-one-time-test-data.sh b/test/docker/run_tests/create-one-time-test-data.sh
index 6f5562b..d949bb2 100755
--- a/test/docker/run_tests/create-one-time-test-data.sh
+++ b/test/docker/run_tests/create-one-time-test-data.sh
@@ -15,6 +15,13 @@
         --email "$UNTRUSTED_USER"@example.com --ssh-key - < ~/.ssh/id_rsa.pub
 
     gssh create-group "Visible-All-Projects-Config" --member "$NON_SECRET_USER"
+
+    local secret_user=$USER
+    gssh create-group "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" \
+        --member "$NON_SECRET_USER" --member "$secret_user"
+    gssh create-group "\"$NON_SECRET_GROUP_NAME_WITH_SPACE\"" \
+        --member "$NON_SECRET_USER" --member "$secret_user"
+    gssh create-group "$SECRET_GROUP_NAME" --member "$secret_user"
 }
 
 setup_all_projects_repo() {
@@ -44,17 +51,23 @@
 USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
 while (( "$#" )) ; do
    case "$1" in
-       --non-secret-user)               shift ; NON_SECRET_USER="$1" ;;
-       --untrusted-user)                shift ; UNTRUSTED_USER="$1" ;;
-       *)                               die "invalid argument '$1'" ;;
+       --non-secret-user)                 shift ; NON_SECRET_USER="$1" ;;
+       --untrusted-user)                  shift ; UNTRUSTED_USER="$1" ;;
+       --non-secret-group-without-space)  shift ; NON_SECRET_GROUP_NAME_WITHOUT_SPACE="$1" ;;
+       --non-secret-group-with-space)     shift ; NON_SECRET_GROUP_NAME_WITH_SPACE="$1" ;;
+       --secret-group)                    shift ; SECRET_GROUP_NAME="$1" ;;
+       *)                                 die "invalid argument '$1'" ;;
    esac
    shift
 done
 
 [ -z "$NON_SECRET_USER" ] && die "non-secret-user not set"
 [ -z "$UNTRUSTED_USER" ] && die "untrusted-user not set"
+[ -z "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" ] && die "non-secret-group-without-space not set"
+[ -z "$NON_SECRET_GROUP_NAME_WITH_SPACE" ] && die "non-secret-group-with-space not set"
+[ -z "$SECRET_GROUP_NAME" ] && die "secret-group not set"
 
 "$USER_RUN_TESTS_DIR"/create-test-project-and-changes.sh
 "$USER_RUN_TESTS_DIR"/update-all-users-project.sh
 create_test_users_and_group
-setup_all_projects_repo
\ No newline at end of file
+setup_all_projects_repo
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
index 1bd5970..4a73f24 100755
--- a/test/docker/run_tests/start.sh
+++ b/test/docker/run_tests/start.sh
@@ -32,8 +32,12 @@
 
 NON_SECRET_USER="non_secret_user"
 UNTRUSTED_USER="untrusted_user"
+GROUP_NAME_WITHOUT_SPACE="test.group"
+GROUP_NAME_WITH_SPACE="test group"
+SECRET_GROUP="private_group"
 "$USER_RUN_TESTS_DIR"/create-one-time-test-data.sh --non-secret-user "$NON_SECRET_USER" \
-    --untrusted-user "$UNTRUSTED_USER"
+    --untrusted-user "$UNTRUSTED_USER" --non-secret-group-without-space "$GROUP_NAME_WITHOUT_SPACE" \
+    --non-secret-group-with-space "$GROUP_NAME_WITH_SPACE" --secret-group "$SECRET_GROUP"
 
 echo "Running Task plugin tests ..."
 
@@ -41,9 +45,11 @@
 
 "$USER_RUN_TESTS_DIR"/../../check_task_statuses.sh \
     --server "$GERRIT_HOST" --non-secret-user "$NON_SECRET_USER" \
-    --untrusted-user "$UNTRUSTED_USER" || RESULT=1
+    --untrusted-user "$UNTRUSTED_USER" --non-secret-group-without-space "$GROUP_NAME_WITHOUT_SPACE" \
+    --non-secret-group-with-space "$GROUP_NAME_WITH_SPACE" || RESULT=1
 
 "$USER_RUN_TESTS_DIR"/../../check_task_visibility.sh --server "$GERRIT_HOST" \
-    --non-secret-user "$NON_SECRET_USER" || RESULT=1
+    --non-secret-user "$NON_SECRET_USER" --non-secret-group "$GROUP_NAME_WITHOUT_SPACE" \
+    --secret-group "$SECRET_GROUP" || RESULT=1
 
 exit $RESULT
diff --git a/test/docker/run_tests/update-all-users-project.sh b/test/docker/run_tests/update-all-users-project.sh
index e8912c4..cfe2def 100755
--- a/test/docker/run_tests/update-all-users-project.sh
+++ b/test/docker/run_tests/update-all-users-project.sh
@@ -13,4 +13,4 @@
    access."refs/*".read "deny group Anonymous Users"
 echo -e "global:Registered-Users\tRegistered Users" >> groups
 echo -e "global:Anonymous-Users\tAnonymous Users" >> groups
-git add . && git commit -m "project config update" && git push origin HEAD:refs/meta/config
\ No newline at end of file
+git add . && git commit -m "project config update" && git push origin HEAD:refs/meta/config
diff --git a/test/lib/lib_helper.sh b/test/lib/lib_helper.sh
index 6ee1b89..efb6de9 100644
--- a/test/lib/lib_helper.sh
+++ b/test/lib/lib_helper.sh
@@ -184,6 +184,23 @@
     replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}"
 }
 
+replace_groups() { # < text_with_groups > test_with_expanded_groups
+    local text="$(< /dev/stdin)"
+    for placeholder in "${!GROUP_EXPANDED_BY_PLACEHOLDER[@]}" ; do
+        text="${text//"$placeholder"/${GROUP_EXPANDED_BY_PLACEHOLDER["$placeholder"]}}"
+    done
+    echo "$text"
+}
+
+get_group_uuid() { # group_name > group_uuid
+    gssh ls-groups -v | awk '-F\t' '$1 == "'"$1"'" {print $2}'
+}
+
+get_sharded_group_uuid() { # group_name > sharded_group_uuid
+    local group_id=$(get_group_uuid "$1")
+    echo "${group_id:0:2}/$group_id"
+}
+
 replace_users() { # < text_with_users > test_with_expanded_users
   local text="$(< /dev/stdin)"
   for user in "${!USERS[@]}" ; do
@@ -211,7 +228,7 @@
 }
 
 replace_tokens() { # < text > text with replacing all tokens(changes, user)
-    replace_default_changes | replace_user_refs | replace_user
+    replace_default_changes | replace_user_refs | replace_user | replace_groups
 }
 
 strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json