Support importing tasks from user refs

This change adds support to import a single task from any user
ref (All-Users.git:/refs/users/..) by specifying their username
in task config file.

See documentation for usage details.

Change-Id: If40f6dbaba2a0ed6c9fe33e15a4fe2c61d79bb2e
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 5631e07..b360040 100644
--- a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
+++ b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
@@ -16,7 +16,10 @@
  *
  * This file defines the grammar used for Task Reference
  *
- * TASK_REF = [ [ TASK_FILE_PATH ] '^' ] TASK_NAME
+ * TASK_REFERENCE = [
+ *                    [ @USERNAME [ TASK_FILE_PATH ] ] |
+ *                    [ TASK_FILE_PATH ]
+ *                  ] '^' TASK_NAME
  *
  * Examples:
  *
@@ -40,6 +43,16 @@
  * Implied task:
  *     file: All-Projects:refs/meta/config:task.config task: sample
  *
+ * file: Any projects, ref, file
+ * reference: @jim^sample
+ * Implied task:
+ *     file: All-Users:refs/users/<jim>:task.config task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: @jim/foo^simple
+ * Implied task:
+ *     file: All-Users:refs/users/<jim>:task/foo^simple task: sample
+ *
  */
 
 grammar TaskReference;
@@ -53,7 +66,12 @@
   ;
 
 file_path
- : (absolute| relative)? TASK_DELIMETER
+ : user absolute? TASK_DELIMETER
+ | (absolute| relative)? TASK_DELIMETER
+ ;
+
+user
+ : '@' NAME
  ;
 
 absolute
@@ -73,11 +91,16 @@
  ;
 
 NAME
- : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH+
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH*
  ;
 
 fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH
- : ':' | '?' | '#' | '[' | ']' | '@'
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT
+ | '@'
+ ;
+
+fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT
+ : ':' | '?' | '#' | '[' | ']'
  |'!' | '$' | '&' | '\'' | '(' | ')'
  | '*' | '+' | ',' | ';' | '=' | '%'
  | 'A'..'Z' | 'a'..'z' | '0'..'9'
@@ -86,4 +109,4 @@
 
 TASK_DELIMETER
  : '^'
- ;
\ No newline at end of file
+ ;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
index 2d44757..9856075 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
@@ -16,10 +16,15 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
 
 /** An immutable reference to a fully qualified file in gerrit repo. */
 @AutoValue
 public abstract class FileKey {
+  public static FileKey create(Project.NameKey project, String branch, String file) {
+    return new AutoValue_FileKey(BranchNameKey.create(project, branch), file);
+  }
+
   public static FileKey create(BranchNameKey branch, String file) {
     return new AutoValue_FileKey(branch, file);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index 44564ed..d28e529 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -39,6 +39,8 @@
           .annotatedWith(Exports.named(ViewPathsCapability.VIEW_PATHS))
           .to(ViewPathsCapability.class);
       factory(TaskPath.Factory.class);
+      factory(TaskReference.Factory.class);
+      factory(TaskExpression.Factory.class);
 
       bind(ChangePluginDefinedInfoFactory.class)
           .annotatedWith(Exports.named("task"))
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
index 3361464..e996193 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -39,14 +39,17 @@
   }
 
   protected final TaskConfigFactory taskConfigFactory;
+  protected final TaskExpression.Factory taskExpressionFactory;
   protected final StatisticsMap<TaskExpressionKey, Optional<Task>> optionalTaskByExpression =
       new HitHashMap<>();
 
   protected Statistics statistics;
 
   @Inject
-  public Preloader(TaskConfigFactory taskConfigFactory) {
+  public Preloader(
+      TaskConfigFactory taskConfigFactory, TaskExpression.Factory taskExpressionFactory) {
     this.taskConfigFactory = taskConfigFactory;
+    this.taskExpressionFactory = taskExpressionFactory;
   }
 
   public List<Task> getRootTasks() throws IOException, ConfigInvalidException {
@@ -109,7 +112,7 @@
         statistics.preloaded++;
       }
       Optional<Task> preloadFrom =
-          getOptionalTask(new TaskExpression(definition.file(), expression));
+          getOptionalTask(taskExpressionFactory.create(definition.file(), expression));
       if (preloadFrom.isPresent()) {
         return preloadFrom(definition, preloadFrom.get());
       }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
index 759caba..5b9bef0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.regex.Matcher;
@@ -47,11 +49,21 @@
  * </ul>
  */
 public class TaskExpression implements Iterable<TaskKey> {
+  public interface Factory {
+    TaskExpression create(FileKey key, String expression);
+  }
+
   protected static final Pattern EXPRESSION_PATTERN = Pattern.compile("([^ |]+[^|]*)(\\|)?");
   protected final TaskExpressionKey key;
+  protected final TaskReference.Factory taskReferenceFactory;
 
-  public TaskExpression(FileKey key, String expression) {
+  @Inject
+  public TaskExpression(
+      TaskReference.Factory taskReferenceFactory,
+      @Assisted FileKey key,
+      @Assisted String expression) {
     this.key = TaskExpressionKey.create(key, expression);
+    this.taskReferenceFactory = taskReferenceFactory;
   }
 
   @Override
@@ -84,7 +96,7 @@
         }
         hasNext = null;
         try {
-          return new TaskReference(key.file(), m.group(1)).getTaskKey();
+          return taskReferenceFactory.create(key.file(), m.group(1)).getTaskKey();
         } catch (ConfigInvalidException e) {
           throw new RuntimeConfigInvalidException(e);
         }
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 6acab3e..8cddac0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -17,6 +17,9 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Preconditions;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AllUsersName;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -55,15 +58,32 @@
   }
 
   public static class Builder {
+    protected final AccountCache accountCache;
+    protected final AllUsersName allUsersName;
     protected final FileKey relativeTo;
+    protected BranchNameKey branch;
     protected String file;
     protected String task;
 
-    Builder(FileKey relativeTo) {
+    Builder(FileKey relativeTo, AllUsersName allUsersName, AccountCache accountCache) {
       this.relativeTo = relativeTo;
+      this.allUsersName = allUsersName;
+      this.accountCache = accountCache;
     }
 
     public TaskKey buildTaskKey() {
+      return isReferencingAnotherRef() ? getAnotherRefTask() : getSameRefTask();
+    }
+
+    protected TaskKey getAnotherRefTask() {
+      return TaskKey.create(
+          isReferencingRootFile()
+              ? FileKey.create(branch, TaskFileConstants.TASK_CFG)
+              : FileKey.create(branch, file),
+          task);
+    }
+
+    protected TaskKey getSameRefTask() {
       return TaskKey.create(
           isRelativePath() ? relativeTo : FileKey.create(relativeTo.branch(), file), task);
     }
@@ -94,6 +114,19 @@
       this.task = task;
     }
 
+    public void setUsername(String username) throws ConfigInvalidException {
+      branch =
+          BranchNameKey.create(
+              allUsersName,
+              RefNames.refsUsers(
+                  accountCache
+                      .getByUsername(username)
+                      .orElseThrow(
+                          () -> new ConfigInvalidException("Cannot resolve username: " + username))
+                      .account()
+                      .id()));
+    }
+
     protected void throwIfInvalidPath() throws ConfigInvalidException {
       Path path = Paths.get(file);
       if (!path.startsWith(TaskFileConstants.TASK_DIR)
@@ -115,5 +148,13 @@
     protected boolean isFileAlreadySet() {
       return file != null;
     }
+
+    protected boolean isReferencingRootFile() {
+      return file == null;
+    }
+
+    protected boolean isReferencingAnotherRef() {
+      return branch != null;
+    }
   }
 }
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 139b27d..308bae8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
@@ -14,6 +14,11 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 import java.nio.file.Paths;
 import java.util.NoSuchElementException;
 import org.antlr.v4.runtime.BaseErrorListener;
@@ -28,11 +33,25 @@
 
 /** This class is used by TaskExpression to decode the task from task reference. */
 public class TaskReference {
-  protected FileKey currentFile;
-  protected String reference;
+  protected final String reference;
+  protected final TaskKey.Builder taskKeyBuilder;
 
-  public TaskReference(FileKey originalFile, String reference) {
-    currentFile = originalFile;
+  interface Factory {
+    TaskReference create(FileKey relativeTo, String reference);
+  }
+
+  @Inject
+  public TaskReference(
+      AllUsersNameProvider allUsersNameProvider,
+      AccountCache accountCache,
+      @Assisted FileKey relativeTo,
+      @Assisted String reference) {
+    this(new TaskKey.Builder(relativeTo, allUsersNameProvider.get(), accountCache), reference);
+  }
+
+  @VisibleForTesting
+  public TaskReference(TaskKey.Builder taskKeyBuilder, String reference) {
+    this.taskKeyBuilder = taskKeyBuilder;
     this.reference = reference.trim();
     if (reference.isEmpty()) {
       throw new NoSuchElementException();
@@ -40,14 +59,13 @@
   }
 
   public TaskKey getTaskKey() throws ConfigInvalidException {
-    TaskKey.Builder builder = new TaskKey.Builder(currentFile);
     ParseTreeWalker walker = new ParseTreeWalker();
     try {
-      walker.walk(new TaskReferenceListener(builder), parse());
+      walker.walk(new TaskReferenceListener(taskKeyBuilder), parse());
     } catch (RuntimeConfigInvalidException e) {
       throw e.checkedException;
     }
-    return builder.buildTaskKey();
+    return taskKeyBuilder.buildTaskKey();
   }
 
   protected ParseTree parse() {
@@ -112,5 +130,14 @@
         }
       }
     }
+
+    @Override
+    public void enterUser(TaskReferenceParser.UserContext ctx) {
+      try {
+        builder.setUsername(ctx.NAME().getText());
+      } catch (ConfigInvalidException e) {
+        throw new RuntimeConfigInvalidException(e);
+      }
+    }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
index f2e651e..44fcadc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -90,6 +90,7 @@
   protected final PredicateCache predicateCache;
   protected final MatchCache matchCache;
   protected final Preloader preloader;
+  protected final TaskExpression.Factory taskExpressionFactory;
   protected final NodeList root = new NodeList();
   protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
   protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
@@ -112,6 +113,7 @@
       Provider<ChangeQueryBuilder> changeQueryBuilderProvider,
       Provider<ChangeQueryProcessor> changeQueryProcessorProvider,
       PredicateCache predicateCache,
+      TaskExpression.Factory taskExpressionFactory,
       Preloader preloader) {
     this.accountResolver = accountResolver;
     this.allUsers = allUsers;
@@ -120,6 +122,7 @@
     this.changeQueryBuilderProvider = changeQueryBuilderProvider;
     this.predicateCache = predicateCache;
     this.matchCache = new MatchCache(predicateCache);
+    this.taskExpressionFactory = taskExpressionFactory;
     this.preloader = preloader;
   }
 
@@ -365,7 +368,7 @@
         for (String expression : task.subTasks) {
           try {
             Optional<Task> def =
-                preloader.getOptionalTask(new TaskExpression(task.file(), expression));
+                preloader.getOptionalTask(taskExpressionFactory.create(task.file(), expression));
             if (def.isPresent()) {
               addPreloaded(def.get());
             }
diff --git a/src/main/resources/Documentation/task_expression.md b/src/main/resources/Documentation/task_expression.md
index f9febcf..498d2b4 100644
--- a/src/main/resources/Documentation/task_expression.md
+++ b/src/main/resources/Documentation/task_expression.md
@@ -4,7 +4,7 @@
 
 The tasks in subtask and preload-task can be defined using a Task Expression.
 Each task expression can contain multiple tasks (all can be optional). Tasks
-from other files can be referenced using [Task Reference](#task_reference).
+from other files and refs can be referenced using [Task Reference](#task_reference).
 
 ```
 TASK_EXPR = TASK_REFERENCE [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ]
@@ -33,10 +33,14 @@
 ---------
 
 Tasks reference can be a simple task name when the defined task is intended to be in
-the same file, tasks from other files can also be referenced by syntax explained below.
+the same file, tasks from other files and refs can also be referenced by syntax explained
+below.
 
 ```
-TASK_REFERENCE = [ [ TASK_FILE_PATH ] '^' ] TASK_NAME
+ TASK_REFERENCE = [
+                    [ @USERNAME [ TASK_FILE_PATH ] ] |
+                    [ TASK_FILE_PATH ]
+                  ] '^' TASK_NAME
 ```
 
 To reference a task from root task.config (top level task.config file of a repository)
@@ -113,3 +117,36 @@
     [task "Relative from Root Example Task"]
     ...
 ```
+
+To reference a task from a specific user ref (All-Users.git:refs/users/<user>), specify the
+username with `@`.
+
+when referencing from user refs, to get task from top level task.config on a user ref use
+`@<username>^<task_name>` and to get any task under the task directory use the relative
+path, like: `@<username>/<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: Account id of user_a is 1000000
+
+All-Users:refs/users/00/1000000:task.config
+```
+    ...
+    [task "top level task"]
+    ...
+```
+
+All-Users:refs/users/00/1000000:/task/dir/common.config
+```
+    ...
+    [task "common task"]
+    ...
+```
+
+All-Projects:refs/meta/config:/task.config
+```
+    ...
+    preload-task = @user_a_username^top level task
+    preload-task = @user_a_username/dir/common.config^common task
+    ...
+```
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
new file mode 100644
index 0000000..fad2f1d
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_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 user ref with subtask pointing to a secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview NON-SECRET subtask with SECRET subtask"]
+     applicable = "is:open"
+     pass = True
++    subtask = @{non_secret_user}/secret_external.config^NON-SECRET with SECRET subtask
+```
+
+file: `All-Users.git:{non_secret_user_ref}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET subtask"]
+    applicable = is:open
+    pass = True
+    subtask = @{secret_user}/secret.config^SECRET task
+```
+
+file: `All-Users:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview NON-SECRET subtask with SECRET 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
+            }
+         ]
+      }
+   ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
new file mode 100644
index 0000000..d2b3349
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
@@ -0,0 +1,36 @@
+# --task-preview root file with subtask pointing to secret user ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external"]
+     applicable = is:open
+     pass = True
++    subtask = @{secret_user}/secret.config^SECRET Task
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET Task"]
+    applicable = is:open
+    pass = Fail
+```
+
+json:
+```
+{
+   "applicable" : true,
+   "hasPass" : true,
+   "name" : "Root Preview SECRET external",
+   "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
+      }
+   ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index 0b8debb..e325a9a 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -2272,6 +2272,32 @@
    ]
 }
 
+[root "Root Import user tasks"]
+  applicable = is:open
+  subtask = @testuser/foo/bar.config^Absolute Task
+  subtask = @testuser^task in user root config file
+
+{
+   "applicable" : true,
+   "hasPass" : false,
+   "name" : "Root Import user tasks",
+   "status" : "PASS",
+   "subTasks" : [
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "Absolute Task",
+         "status" : "PASS"
+      },
+      {
+         "applicable" : true,
+         "hasPass" : true,
+         "name" : "task in user root config file",
+         "status" : "PASS"
+      }
+   ]
+}
+
 [root "Root INVALID Preload"]
   preload-task = missing
 
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 90b3dc3..fe01ee2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
@@ -16,9 +16,12 @@
 
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AllUsersName;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import junit.framework.TestCase;
+import org.mockito.Mockito;
 
 /*
  * <ul>
@@ -228,7 +231,18 @@
   }
 
   protected TaskExpression getTaskExpression(FileKey file, String expression) {
-    return new TaskExpression(file, expression);
+    AccountCache accountCache = Mockito.mock(AccountCache.class);
+    TaskReference.Factory factory = Mockito.mock(TaskReference.Factory.class);
+    Mockito.when(factory.create(Mockito.any(), Mockito.any()))
+        .thenAnswer(
+            invocation ->
+                new TaskReference(
+                    new TaskKey.Builder(
+                        (FileKey) invocation.getArguments()[0],
+                        new AllUsersName("All-Users"),
+                        accountCache),
+                    (String) invocation.getArguments()[1]));
+    return new TaskExpression(factory, file, expression);
   }
 
   protected static FileKey createFileKey(String file) {
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 5f309d0..2036f16 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
@@ -14,14 +14,23 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.gerrit.entities.Account;
 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.AccountState;
+import com.google.gerrit.server.config.AllUsersName;
+import java.sql.Timestamp;
 import java.util.NoSuchElementException;
+import java.util.Optional;
 import junit.framework.TestCase;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Test;
+import org.mockito.Mockito;
 
 public class TaskReferenceTest extends TestCase {
+  private static final String ALL_USERS = "All-Users";
+
   public static String SIMPLE = "simple";
   public static String ROOT = "task.config";
   public static String COMMON = "task/common.config";
@@ -30,64 +39,94 @@
   public static FileKey COMMON_CFG = createFileKey("project", "branch", COMMON);
   public static FileKey SUB_COMMON_CFG = createFileKey("project", "branch", SUB_COMMON);
 
+  public static final String TEST_USER = "testuser";
+  public static final int TEST_USER_ID = 100000;
+  public static final Account TEST_USER_ACCOUNT =
+      Account.builder(Account.id(TEST_USER_ID), new Timestamp(0L)).build();
+  public static final String TEST_USER_REF =
+      "refs/users/" + String.format("%02d", TEST_USER_ID % 100) + "/" + TEST_USER_ID;
+  public static final FileKey TEST_USER_ROOT_CFG = createFileKey(ALL_USERS, TEST_USER_REF, ROOT);
+  public static final FileKey TEST_USER_COMMON_CFG =
+      createFileKey(ALL_USERS, TEST_USER_REF, COMMON);
+
   @Test
-  public void testReferencingTaskFromSameFile() {
+  public void testReferencingTaskFromSameFile() throws Exception {
     assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, SIMPLE));
   }
 
   @Test
-  public void testReferencingTaskFromRootConfig() {
+  public void testReferencingTaskFromRootConfig() throws Exception {
     String reference = "^" + SIMPLE;
     assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
   }
 
   @Test
-  public void testReferencingRelativeTaskFromRootConfig() {
+  public void testReferencingRelativeTaskFromRootConfig() throws Exception {
     String reference = " dir/common.config^" + SIMPLE;
     assertEquals(createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, reference));
   }
 
   @Test
-  public void testReferencingAbsoluteTaskFromRootConfig() {
+  public void testReferencingAbsoluteTaskFromRootConfig() throws Exception {
     String reference = " /common.config^" + SIMPLE;
     assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, reference));
   }
 
   @Test
-  public void testReferencingRelativeDirTask() {
+  public void testReferencingRelativeDirTask() throws Exception {
     String reference = " dir/common.config^" + SIMPLE;
     assertEquals(
         createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference));
   }
 
   @Test
-  public void testReferencingRelativeFileTask() {
+  public void testReferencingRelativeFileTask() throws Exception {
     String reference = "common.config^" + SIMPLE;
     assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference));
   }
 
   @Test
-  public void testReferencingAbsoluteTask() {
+  public void testReferencingAbsoluteTask() throws Exception {
     String reference = " /common.config^" + SIMPLE;
     assertEquals(
         createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
   }
 
   @Test
-  public void testMultipleUpchars() {
+  public void testReferencingRootUserTask() throws Exception {
+    String reference = "@" + TEST_USER + "^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_USER_ROOT_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testReferencingUserTaskDir() throws Exception {
+    String reference = "@" + TEST_USER + "/common.config^" + SIMPLE;
+    assertEquals(
+        createTaskKey(TEST_USER_COMMON_CFG, SIMPLE),
+        getTaskFromReference(SUB_COMMON_CFG, reference));
+  }
+
+  @Test
+  public void testMultipleUpchars() throws Exception {
     String reference = " ^ /common.config^" + SIMPLE;
     assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
   }
 
   @Test
-  public void testEmptyReference() {
+  public void testEmptyReference() throws Exception {
     String empty = "";
     assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, empty));
   }
 
   protected static TaskKey getTaskFromReference(FileKey file, String expression) {
+    AccountCache accountCache = Mockito.mock(AccountCache.class);
+    Mockito.when(accountCache.getByUsername(TEST_USER))
+        .thenReturn(Optional.of(AccountState.forAccount(TEST_USER_ACCOUNT)));
     try {
-      return new TaskReference(file, expression).getTaskKey();
+      return new TaskReference(
+              new TaskKey.Builder(file, new AllUsersName(ALL_USERS), accountCache), expression)
+          .getTaskKey();
     } catch (ConfigInvalidException e) {
       throw new NoSuchElementException();
     }
@@ -101,7 +140,7 @@
     return FileKey.create(BranchNameKey.create(Project.NameKey.parse(project), branch), file);
   }
 
-  protected static void assertNoSuchElementException(Runnable f) {
+  protected static void assertNoSuchElementException(Executable f) throws Exception {
     try {
       f.run();
       assertTrue(false);
@@ -109,4 +148,9 @@
       assertTrue(true);
     }
   }
+
+  @FunctionalInterface
+  interface Executable {
+    void run() throws Exception;
+  }
 }
diff --git a/test/check_task_visibility.sh b/test/check_task_visibility.sh
index 1be8b96..aa9f25c 100755
--- a/test/check_task_visibility.sh
+++ b/test/check_task_visibility.sh
@@ -201,6 +201,8 @@
 "root_with_external_non-secret_ref_with_external_secret_ref.md"
 "root_with_external_secret_ref.md"
 "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"
 )
 
 for test in "${TESTS[@]}" ; do