Add support for external tasks
The new 'subtasks-external' keyword adds all the subtasks from another
file referenced by a new 'external' section. The 'external' section
supports the 'user' and 'file' keywords to reference a file under the
task directory on a user's ref in the All-Users repo.
Change-Id: Id53265e596057f8495eb1c233b2679a06ca506d4
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
index 8ebb50f..95439e2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -17,13 +17,18 @@
import com.google.gerrit.extensions.common.PluginDefinedInfo;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.gerrit.server.query.change.ChangeQueryProcessor.ChangeAttributeFactory;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.task.TaskConfig.External;
import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
import java.io.IOException;
import java.nio.file.Path;
@@ -64,11 +69,19 @@
protected static final String TASK_DIR = "task";
+ protected final AccountResolver accountResolver;
+ protected final AllUsersNameProvider allUsers;
protected final TaskConfigFactory taskFactory;
protected final ChangeQueryBuilder cqb;
@Inject
- public TaskAttributeFactory(TaskConfigFactory taskFactory, ChangeQueryBuilder cqb) {
+ public TaskAttributeFactory(
+ AccountResolver accountResolver,
+ AllUsersNameProvider allUsers,
+ TaskConfigFactory taskFactory,
+ ChangeQueryBuilder cqb) {
+ this.accountResolver = accountResolver;
+ this.allUsers = allUsers;
this.taskFactory = taskFactory;
this.cqb = cqb;
}
@@ -149,6 +162,18 @@
subTasks.add(invalid());
}
}
+ for (String external : parent.subTasksExternals) {
+ try {
+ External ext = parent.config.getExternal(external);
+ if (ext == null) {
+ subTasks.add(invalid());
+ } else {
+ tasks.addAll(getTasks(ext));
+ }
+ } catch (ConfigInvalidException | IOException e) {
+ subTasks.add(invalid());
+ }
+ }
for (Task task : tasks) {
addApplicableTasks(subTasks, c, path, task);
@@ -180,6 +205,11 @@
return tasks;
}
+ protected List<Task> getTasks(External external)
+ throws ConfigInvalidException, IOException, OrmException {
+ return getTasks(resolveUserBranch(external.user), resolveTaskFileName(external.file));
+ }
+
protected List<Task> getTasks(Branch.NameKey branch, String file)
throws ConfigInvalidException, IOException {
return taskFactory.getTaskConfig(branch, file).getTasks();
@@ -196,6 +226,18 @@
return p.toString();
}
+ protected Branch.NameKey resolveUserBranch(String user)
+ throws ConfigInvalidException, IOException, OrmException {
+ if (user == null) {
+ throw new ConfigInvalidException("External user not defined");
+ }
+ Account acct = accountResolver.find(user);
+ if (acct == null) {
+ throw new ConfigInvalidException("Cannot resolve user: " + user);
+ }
+ return new Branch.NameKey(allUsers.get(), RefNames.refsUsers(acct.getId()));
+ }
+
protected Status getStatus(ChangeData c, Task task, TaskAttribute a)
throws OrmException, QueryParseException {
if (task.pass == null && a.subTasks == null) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
index d1902b3..86d6a2e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -39,6 +39,7 @@
public String pass;
public String readyHint;
public List<String> subTasks;
+ public List<String> subTasksExternals;
public List<String> subTasksFiles;
public Task(SubSection s) {
@@ -49,20 +50,37 @@
pass = getString(s, KEY_PASS, null);
readyHint = getString(s, KEY_READY_HINT, null);
subTasks = getStringList(s, KEY_SUBTASK);
+ subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL);
subTasksFiles = getStringList(s, KEY_SUBTASKS_FILE);
}
}
+ public class External extends Section {
+ public String name;
+ public String file;
+ public String user;
+
+ public External(SubSection s) {
+ name = s.subSection;
+ file = getString(s, KEY_FILE, null);
+ user = getString(s, KEY_USER, null);
+ }
+ }
+
+ protected static final String SECTION_EXTERNAL = "external";
protected static final String SECTION_ROOT = "root";
protected static final String SECTION_TASK = "task";
protected static final String KEY_APPLICABLE = "applicable";
protected static final String KEY_FAIL = "fail";
+ protected static final String KEY_FILE = "file";
protected static final String KEY_IN_PROGRESS = "in-progress";
protected static final String KEY_NAME = "name";
protected static final String KEY_PASS = "pass";
protected static final String KEY_READY_HINT = "ready-hint";
protected static final String KEY_SUBTASK = "subtask";
+ protected static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
protected static final String KEY_SUBTASKS_FILE = "subtasks-file";
+ protected static final String KEY_USER = "user";
public TaskConfig(Branch.NameKey branch, String fileName) {
super(branch, fileName);
@@ -85,10 +103,27 @@
return tasks;
}
+ public List<External> getExternals() {
+ List<External> externals = new ArrayList<>();
+ // No need to get an external with no name (what would we call it?)
+ for (String external : cfg.getSubsections(SECTION_EXTERNAL)) {
+ externals.add(getExternal(external));
+ }
+ return externals;
+ }
+
public Task getTask(String name) {
return new Task(new SubSection(SECTION_TASK, name));
}
+ public External getExternal(String name) {
+ return getExternal(new SubSection(SECTION_EXTERNAL, name));
+ }
+
+ protected External getExternal(SubSection s) {
+ return new External(s);
+ }
+
protected String getString(SubSection s, String key, String def) {
String v = cfg.getString(s.section, s.subSection, key);
return v != null ? v : def;
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index 2b3ff24..439580c 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -36,6 +36,12 @@
can do, or perhaps who they need to talk to to ensure that their changes do
make progress through all their hoops.
+Task definitions can be split up across multiple files/refs, and even
+across multiple projects. This splitting of task definitions allows the
+control of task definitions to be delegated to different entities. By
+aligning ref boundaries with controlling entities, the standard gerrit ref
+ACL mechanisms may be used to control who can define tasks on which changes.
+
Task Status
-----------
Task status is used to indicate either the readiness of a task for execution
@@ -132,6 +138,20 @@
subtask = "License Approval"
```
+`subtasks-external`
+
+: This key defines a file containing subtasks of the current task. This
+key may be used several times in a task section to define more than one file
+containing subtasks for a particular task. The subtasks-external key points
+to an external file defined by external section. Note: all of the tasks in
+the referenced file will be included as subtasks of the current task!
+
+Example:
+
+```
+ subtasks-external = my-external
+```
+
`subtasks-file`
: This key defines a file containing subtasks of the current task. This
@@ -191,6 +211,36 @@
fail = label:code-review-2
```
+External Entries
+----------------
+A name for external task files on other projects and branches may be given
+by defining an `external` section in a task file. This later allows this
+external name to then be referenced by other definitions. The following
+keys may be defined in an external section. External references are limited
+to files under the top level task directory.
+
+`file`
+
+: This key defines the name of the external task file under the
+task directory referenced.
+
+Example:
+
+```
+ file = common.config # references the file named task/common.config
+```
+
+`user`
+
+: This key defines the username of the user's ref in the `All-Users` project
+of the external file referenced.
+
+Example:
+
+```
+ user = first-user # references the sharded user ref refs/users/01/1000001
+```
+
Change Query Output
-------------------
Changes which have tasks applicable to them will have a "task" section
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
index a87c82c..ec5c4f8 100644
--- a/src/main/resources/Documentation/task_states.md
+++ b/src/main/resources/Documentation/task_states.md
@@ -87,6 +87,25 @@
subtasks-file = common.config
subtasks-file = missing
+[root "Subtasks External"]
+ applicable = is:open
+ subtasks-external = user special
+
+[root "Subtasks External (Missing)"]
+ applicable = is:open
+ subtasks-external = user special
+ subtasks-external = missing
+
+[root "Subtasks External (User Missing)"]
+ applicable = is:open
+ subtasks-external = user special
+ subtasks-external = user missing
+
+[root "Subtasks External (File Missing)"]
+ applicable = is:open
+ subtasks-external = user special
+ subtasks-external = file missing
+
[task "Subtask FAIL"]
applicable = is:open
fail = is:open
@@ -103,6 +122,18 @@
[task "Subtask INVALID"]
applicable = is:open
+
+[external "user special"]
+ user = current-user
+ file = special.config
+
+[external "user missing"]
+ user = missing
+ file = special.config
+
+[external "file missing"]
+ user = current-user
+ file = missing
```
`task/common.config` file in project `All-Projects` on ref `refs/meta/config`.
@@ -118,6 +149,19 @@
pass = is:open
```
+`task/special.config` file in project `All-Users` on ref `refs/users/self`.
+
+```
+[task "userfile task/special.config PASS"]
+ applicable = is:open
+ pass = is:open
+
+[task "userfile task/special.config FAIL"]
+ applicable = is:open
+ fail = is:open
+ pass = is:open
+```
+
The expected output for the above task config looks like:
```
@@ -291,6 +335,70 @@
"status" : "FAIL"
}
]
+ },
+ {
+ "name" : "Subtasks External",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "userfile task/special.config PASS",
+ "status" : "PASS"
+ },
+ {
+ "name" : "userfile task/special.config FAIL",
+ "status" : "FAIL"
+ }
+ ]
+ },
+ {
+ "name" : "Subtasks External (Missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "userfile task/special.config PASS",
+ "status" : "PASS"
+ },
+ {
+ "name" : "userfile task/special.config FAIL",
+ "status" : "FAIL"
+ }
+ ]
+ },
+ {
+ "name" : "Subtasks External (User Missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
+ "name" : "userfile task/special.config PASS",
+ "status" : "PASS"
+ },
+ {
+ "name" : "userfile task/special.config FAIL",
+ "status" : "FAIL"
+ }
+ ]
+ },
+ {
+ "name" : "Subtasks External (File Missing)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "userfile task/special.config PASS",
+ "status" : "PASS"
+ },
+ {
+ "name" : "userfile task/special.config FAIL",
+ "status" : "FAIL"
+ }
+ ]
}
]
}
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index b3af567..16167a4 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -1,6 +1,7 @@
#!/bin/bash
# Usage:
+# All-Users.git - refs/users/self must already exist
# All-Projects.git - must have 'Push' rights on refs/meta/config
example() { # example_num
@@ -43,12 +44,16 @@
ALL=$OUT/All-Projects
ALL_TASKS=$ALL/task
+USERS=$OUT/All-Users
+USER_TASKS=$USERS/task
+
DOC_STATES=$DOCS/task_states.md
EXPECTED=$OUT/expected
STATUSES=$OUT/statuses
ROOT_CFG=$ALL/task.config
COMMON_CFG=$ALL_TASKS/common.config
+USER_SPECIAL_CFG=$USER_TASKS/special.config
# --- Args ----
SERVER=$1
@@ -56,21 +61,27 @@
PORT=29418
REMOTE_ALL=ssh://$SERVER:$PORT/All-Projects
+REMOTE_USERS=ssh://$SERVER:$PORT/All-Users
REF_ALL=refs/meta/config
+REF_USERS=refs/users/self
mkdir -p "$OUT"
setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+setup_repo "$USERS" "$REMOTE_USERS" "$REF_USERS"
-mkdir -p "$ALL_TASKS"
+mkdir -p "$ALL_TASKS" "$USER_TASKS"
-example 1 > "$ROOT_CFG"
+example 1 |sed -e"s/current-user/$USER/" > "$ROOT_CFG"
example 2 > "$COMMON_CFG"
+example 3 > "$USER_SPECIAL_CFG"
update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+update_repo "$USERS" "$REMOTE_USERS" "$REF_USERS"
-example 3 |tail -n +5| awk 'NR>1{print P};{P=$0}' > "$EXPECTED"
+
+example 4 |tail -n +5| awk 'NR>1{print P};{P=$0}' > "$EXPECTED"
query_plugins "status:open limit:1" > "$STATUSES"
diff "$EXPECTED" "$STATUSES"