Task Plugin

Change-Id: I233cbf915b1c00e84b92ed34d9d3220803dc8ff5
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f3f0d6b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/.settings/org.eclipse.core.resources.prefs
+/.settings/org.eclipse.jdt.core.prefs
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..a5f6065
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>com.googlesource.gerrit.plugins.task</groupId>
+  <artifactId>task</artifactId>
+  <packaging>jar</packaging>
+  <version>2.14-SNAPSHOT</version>
+  <name>task</name>
+
+  <properties>
+    <Gerrit-ApiType>plugin</Gerrit-ApiType>
+    <Gerrit-ApiVersion>${project.version}</Gerrit-ApiVersion>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <version>2.4</version>
+        <configuration>
+          <archive>
+            <manifestEntries>
+              <Gerrit-Module>com.googlesource.gerrit.plugins.task.Modules$Module</Gerrit-Module>
+              <Gerrit-HttpModule>com.googlesource.gerrit.plugins.task.Modules$HttpModule</Gerrit-HttpModule>
+              <Gerrit-SshModule>com.googlesource.gerrit.plugins.task.Modules$SshModule</Gerrit-SshModule>
+              <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
+              <Implementation-URL>http://code.google.com/p/gerrit/</Implementation-URL>
+
+              <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
+              <Implementation-Version>${project.version}</Implementation-Version>
+
+              <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+              <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>2.3.2</version>
+        <configuration>
+          <source>1.7</source>
+          <target>1.7</target>
+          <encoding>UTF-8</encoding>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
+      <version>${Gerrit-ApiVersion}</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+
+  <repositories>
+    <repository>
+      <id>gerrit-api-repository</id>
+      <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+    </repository>
+  </repositories>
+</project>
diff --git a/src/main/java/com/google/gerrit/common/Container.java b/src/main/java/com/google/gerrit/common/Container.java
new file mode 100644
index 0000000..680be65
--- /dev/null
+++ b/src/main/java/com/google/gerrit/common/Container.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import java.lang.IllegalAccessException;
+import java.lang.reflect.Field;
+import java.util.Objects;
+import java.util.List;
+import java.util.ArrayList;
+
+/* A data container, all fields considered in equals and hash */
+public class Container {
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    try {
+      for (Field field : getClass().getDeclaredFields()) {
+        field.setAccessible(true);
+        if (!Objects.deepEquals(field.get(this), field.get(o))) {
+          return false;
+        }
+      }
+    } catch (IllegalArgumentException | IllegalAccessException e) {
+      throw new RuntimeException();
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    List values = new ArrayList();
+    try {
+      for (Field field : getClass().getDeclaredFields()) {
+        field.setAccessible(true);
+        values.add(field.get(this));
+      }
+    } catch (IllegalArgumentException | IllegalAccessException e) {
+    }
+    return Objects.hash(values);
+  }
+}
diff --git a/src/main/java/com/google/gerrit/server/util/Git.java b/src/main/java/com/google/gerrit/server/util/Git.java
new file mode 100644
index 0000000..400ac4d
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/util/Git.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Git {
+  public final GitRepositoryManager repos;
+
+  @Inject
+  public Git(GitRepositoryManager repos) {
+    this.repos = repos;
+  }
+
+  public ObjectId getObjectId(Branch.NameKey branch) throws IOException,
+      NoSuchRefException, RepositoryNotFoundException {
+    Repository repo = repos.openRepository(branch.getParentKey());
+    try {
+      return repo.getRef(branch.get()).getObjectId();
+    } finally {
+      repo.close();
+    }
+  }
+
+  public Map<Branch.NameKey, ObjectId> getObjectIdsByBranch(
+      Iterable<Branch.NameKey> branches) throws IOException,
+      NoSuchRefException, RepositoryNotFoundException {
+    Map<Branch.NameKey, ObjectId> idsByBranch =
+        new HashMap<Branch.NameKey, ObjectId>();
+    for (Branch.NameKey branch : branches) {
+      idsByBranch.put(branch, getObjectId(branch));
+    }
+    return idsByBranch;
+  }
+}
diff --git a/src/main/java/com/google/gerrit/server/util/RefUpdater.java b/src/main/java/com/google/gerrit/server/util/RefUpdater.java
new file mode 100644
index 0000000..d436bd4
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/util/RefUpdater.java
@@ -0,0 +1,179 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.TagCache;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class RefUpdater {
+  private static final Logger log = LoggerFactory.getLogger(RefUpdater.class);
+
+  public class Args {
+    public final Branch.NameKey branch;
+    public ObjectId expectedOldObjectId;
+    public ObjectId newObjectId;
+    public boolean isForceUpdate;
+    public PersonIdent refLogIdent;
+
+    public Args(Branch.NameKey branch) {
+      this.branch = branch;
+      CurrentUser user = userProvider.get();
+      if (user instanceof IdentifiedUser) {
+        refLogIdent = ((IdentifiedUser) user).newRefLogIdent();
+      } else {
+        refLogIdent = gerrit;
+      }
+    }
+  }
+
+  private Provider<CurrentUser> userProvider;
+  private @GerritPersonIdent PersonIdent gerrit;
+  private GitRepositoryManager repoManager;
+  private GitReferenceUpdated gitRefUpdated;
+  private IdentifiedUser user;
+  private TagCache tagCache;
+
+  @Inject
+  RefUpdater(Provider<CurrentUser> userProvider,
+      @GerritPersonIdent PersonIdent gerrit, GitRepositoryManager repoManager,
+      TagCache tagCache, GitReferenceUpdated gitRefUpdated) {
+    this.userProvider = userProvider;
+    this.gerrit = gerrit;
+    this.repoManager = repoManager;
+    this.tagCache = tagCache;
+    this.gitRefUpdated = gitRefUpdated;
+  }
+
+  public void update(Branch.NameKey branch, ObjectId oldRefId,
+      ObjectId newRefId) throws IOException, RepositoryNotFoundException {
+    Args args = new Args(branch);
+    args.expectedOldObjectId = oldRefId;
+    args.newObjectId = newRefId;
+    update(args);
+  }
+
+  public void forceUpdate(Branch.NameKey branch, ObjectId newRefId) throws IOException, RepositoryNotFoundException {
+    Args args = new Args(branch);
+    args.newObjectId = newRefId;
+    args.isForceUpdate = true;
+    update(args);
+  }
+
+  public void delete(Branch.NameKey branch)
+      throws IOException, RepositoryNotFoundException {
+    Args args = new Args(branch);
+    args.newObjectId = ObjectId.zeroId();
+    args.isForceUpdate = true;
+    update(args);
+  }
+
+  public void update(Args args) throws IOException {
+    new Update(args).update();
+  }
+
+  private class Update {
+    Repository repo;
+    Args args;
+    RefUpdate update;
+    Branch.NameKey branch;
+    Project.NameKey project;
+    boolean delete;
+
+    Update(Args args) throws IOException {
+      this.args = args;
+      branch = args.branch;
+      project = branch.getParentKey();
+      delete = args.newObjectId.equals(ObjectId.zeroId());
+    }
+
+    void update() throws IOException {
+      repo = repoManager.openRepository(project);
+      try {
+        initUpdate();
+        handleResult(runUpdate());
+      } catch (IOException err) {
+        log.error("RefUpdate failed: branch not updated: " + branch.get(), err);
+        throw err;
+      } finally {
+        repo.close();
+        repo = null;
+      }
+    }
+
+    void initUpdate() throws IOException {
+      update = repo.updateRef(branch.get());
+      update.setExpectedOldObjectId(args.expectedOldObjectId);
+      update.setNewObjectId(args.newObjectId);
+      update.setRefLogIdent(args.refLogIdent);
+      update.setForceUpdate(args.isForceUpdate);
+    }
+
+    RefUpdate.Result runUpdate() throws IOException {
+      if (delete) {
+        return update.delete();
+      }
+      return update.update();
+    }
+
+    void handleResult(RefUpdate.Result result) throws IOException {
+      switch (result) {
+        case FORCED:
+          if (!delete && !args.isForceUpdate) {
+            throw new IOException(result.name());
+          }
+        case FAST_FORWARD:
+        case NEW:
+        case NO_CHANGE:
+          onUpdated(update, args);
+          break;
+        default:
+          throw new IOException(result.name());
+      }
+    }
+
+    void onUpdated(RefUpdate update, Args args) {
+      if (update.getResult() == RefUpdate.Result.FAST_FORWARD) {
+        tagCache.updateFastForward(project, update.getName(),
+            update.getOldObjectId(), args.newObjectId);
+      }
+
+      CurrentUser user = userProvider.get();
+      if (user instanceof IdentifiedUser) {
+        Account account = ((IdentifiedUser) user).getAccount();
+        gitRefUpdated.fire(project, update, account);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/AbstractVersionedMetaData.java b/src/main/java/com/googlesource/gerrit/plugins/task/AbstractVersionedMetaData.java
new file mode 100644
index 0000000..59f58eb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/AbstractVersionedMetaData.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+/** Versioned Configuration file living in git */
+public class AbstractVersionedMetaData extends VersionedMetaData {
+  protected final Branch.NameKey branch;
+  protected final String fileName;
+  protected Config cfg;
+
+  public AbstractVersionedMetaData(Branch.NameKey branch, String fileName) {
+    this.branch = branch;
+    this.fileName = fileName;
+  }
+
+  @Override
+  protected String getRefName() {
+    return branch.get();
+  }
+
+  protected Branch.NameKey getBranch() {
+    return branch;
+  }
+
+  protected String getFileName() {
+    return fileName;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    cfg = readConfig(fileName);
+  }
+
+  public Config get() {
+    if (cfg == null) {
+      cfg = new Config();
+    }
+    return cfg;
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    if (commit.getMessage() == null || "".equals(commit.getMessage())) {
+      commit.setMessage("Updated configuration\n");
+    }
+    saveConfig(fileName, cfg);
+    return true;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
new file mode 100644
index 0000000..f1d4335
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.query.change.QueryChanges;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor.ChangeAttributeFactory;
+import com.google.gerrit.sshd.commands.Query;
+import com.google.inject.AbstractModule;
+
+import org.kohsuke.args4j.Option;
+
+public class Modules {
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(ChangeAttributeFactory.class)
+          .annotatedWith(Exports.named("task"))
+          .to(TaskAttributeFactory.class);
+    }
+  }
+
+  public static class SshModule extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(DynamicBean.class)
+          .annotatedWith(Exports.named(Query.class))
+          .to(MyOptions.class);
+    }
+  }
+
+  public static class HttpModule extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(DynamicBean.class)
+          .annotatedWith(Exports.named(QueryChanges.class))
+          .to(MyOptions.class);
+    }
+  }
+
+  static class MyOptions implements DynamicBean {
+    @Option(name = "--applicable", usage = "Include applicable tasks")
+    boolean include = false;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
new file mode 100644
index 0000000..b208f51
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -0,0 +1,372 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.googlesource.gerrit.plugins.task.TaskConfig.External;
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.query.Matchable;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+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.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class TaskAttributeFactory implements ChangeAttributeFactory {
+  private static final Logger log =
+      LoggerFactory.getLogger(TaskAttributeFactory.class);
+
+  public enum Status {
+    INVALID, WAITING, READY, PASS, FAIL;
+  }
+
+  private static final String TASK_DIR = "task";
+
+  private ReviewDb db;
+  private AccountResolver accountResolver;
+  private AllUsersNameProvider allUsers;
+  private CurrentUser user;
+  private Provider<ChangeQueryBuilder> cqb;
+  private TaskConfigFactory taskFactory;
+
+  @Inject
+  public TaskAttributeFactory(ReviewDb db, AccountResolver accountResolver,
+      AllUsersNameProvider allUsers,  AnonymousUser anonymousUser,
+      CurrentUser user, TaskConfigFactory taskFactory,
+      Provider<ChangeQueryBuilder> cqb) {
+    this.db = db;
+    this.accountResolver = accountResolver;
+    this.allUsers = allUsers;
+    this.user = user != null ? user : anonymousUser;
+    this.taskFactory = taskFactory;
+    this.cqb = cqb;
+  }
+
+  @Override
+  public PluginDefinedInfo create(ChangeData c, ChangeQueryProcessor qp,
+      String plugin) {
+    Modules.MyOptions options = (Modules.MyOptions) qp.getDynamicBean(plugin);
+    if (options != null && options.include) {
+      try {
+        return createWithExceptions(c);
+      } catch (OrmException e) {
+        log.error("Cannot load tasks for: " + c, e);
+      }
+    }
+    return null;
+  }
+
+  private PluginDefinedInfo createWithExceptions(ChangeData c) throws
+      OrmException {
+    TaskPluginAttribute a = new TaskPluginAttribute();
+    try {
+      LinkedList<Task> path = new LinkedList<Task>();
+      for (Task task : getRootTasks()) {
+        addApplicableTasks(a.roots, c, path, task);
+      }
+    } catch (ConfigInvalidException | IOException e) {
+      a.roots.add(invalid());
+    } catch (Exception e) {
+      log.error("Error processing tasks", e);
+    }
+
+    if (a.roots.isEmpty()) {
+      return null;
+    }
+    return a;
+  }
+
+  private void addApplicableTasks(List<TaskAttribute> tasks, ChangeData c,
+      LinkedList<Task> path, Task def) throws OrmException {
+    if (path.contains(def)) { // looping definition
+      tasks.add(invalid());
+      return;
+    }
+    path.addLast(def);
+    addApplicableTasksNoLoopCheck(tasks, c, path, def);
+    path.removeLast();
+  }
+
+  private void addApplicableTasksNoLoopCheck(List<TaskAttribute> tasks,
+      ChangeData c, LinkedList<Task> path, Task def) throws OrmException {
+    try {
+      if (match(c, def.applicable)) {
+        TaskAttribute task = new TaskAttribute(def.name);
+        if (def.inProgress != null) {
+          task.inProgress = match(c, def.inProgress);
+        }
+        task.subTasks = getSubTasks(c, path, def);
+        task.status = getStatus(c, def, task);
+        if (task.status != null) { // task still applies
+          task.hint = getHint(task.status, def);
+          tasks.add(task);
+        }
+      }
+    } catch(QueryParseException e) {
+      tasks.add(invalid()); // bad applicability query
+    }
+  }
+
+  private List<TaskAttribute> getSubTasks(ChangeData c,
+      LinkedList<Task> path, Task parent) throws OrmException {
+    List<Task> tasks = getSubTasks(parent);
+
+    List<TaskAttribute> subTasks = new ArrayList<TaskAttribute>();
+    for (String file : parent.subTasksFiles) {
+      try {
+        tasks.addAll(getTasks(parent.config.getBranch(),
+            resolveTaskFileName(file)));
+      } catch (ConfigInvalidException | IOException e) {
+        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);
+    }
+
+    if (subTasks.isEmpty()) {
+      return null;
+    }
+    return subTasks;
+  }
+
+  private static TaskAttribute invalid() {
+    // For security reasons, do not expose the task name without knowing
+    // the visibility which is derived from its applicability.
+    TaskAttribute a = new TaskAttribute("UNKNOWN");
+    a.status = Status.INVALID;
+    return a;
+  }
+
+  private List<Task> getRootTasks()
+      throws ConfigInvalidException, IOException {
+    return taskFactory.getRootConfig().getRootTasks();
+  }
+
+  private List<Task> getSubTasks(Task parent) {
+    List<Task> tasks = new ArrayList<Task> ();
+    for (String name : parent.subTasks) {
+      tasks.add(parent.config.getTask(name));
+    }
+    return tasks;
+  }
+
+  private List<Task> getTasks(External external)
+      throws ConfigInvalidException, IOException, OrmException {
+    return getTasks(resolveUserBranch(external.user),
+        resolveTaskFileName(external.file));
+  }
+
+  private List<Task> getTasks(Branch.NameKey branch,
+      String file) throws ConfigInvalidException, IOException {
+    return taskFactory.getTaskConfig(branch, file).getTasks();
+  }
+
+  private String resolveTaskFileName(String file) throws ConfigInvalidException {
+    if (file == null) {
+      throw new ConfigInvalidException("External file not defined");
+    }
+    Path p = Paths.get(TASK_DIR, file);
+    if (!p.startsWith(TASK_DIR)) {
+      throw new ConfigInvalidException("task file not under " + TASK_DIR
+          + " directory: " + file);
+    }
+    return p.toString();
+  }
+
+  private Branch.NameKey resolveUserBranch(String user)
+      throws ConfigInvalidException, OrmException {
+    if (user == null) {
+      throw new ConfigInvalidException("External user not defined");
+    }
+    Account acct = accountResolver.find(db, user);
+    if (acct == null) {
+      throw new ConfigInvalidException("Cannot resolve user: " + user);
+    }
+    return new Branch.NameKey(allUsers.get(), RefNames.refsUsers(acct.getId()));
+  }
+
+  private Status getStatus(ChangeData c, Task task, TaskAttribute a)
+      throws OrmException {
+    try {
+      return getStatusWithExceptions(c, task, a);
+    } catch(QueryParseException e) {
+      return Status.INVALID;
+    }
+  }
+
+  private Status getStatusWithExceptions(ChangeData c, Task task,
+      TaskAttribute a) throws OrmException, QueryParseException {
+    if (isAllNull(task.pass, task.fail, a.subTasks)) {
+      // A leaf task has no defined subtasks.
+      boolean hasDefinedSubtasks = ! (task.subTasks.isEmpty()
+          && task.subTasksFiles.isEmpty() && task.subTasksExternals.isEmpty());
+      if (hasDefinedSubtasks) {
+        // Remove 'Grouping" tasks (tasks with subtasks but no PASS
+        // or FAIL criteria) from the output if none of their subtasks
+        // are applicable.  i.e. grouping tasks only really apply if at
+        // least one of their subtasks apply.
+        return null;
+      }
+      // A leaf configuration without a PASS or FAIL criteria is a
+      // missconfiguration.  Either someone forgot to add subtasks, or
+      // they forgot to add a PASS or FAIL criteria.
+      return Status.INVALID;
+    }
+
+    if (task.fail != null) {
+      if (match(c, task.fail)) {
+        // A FAIL definition is meant to be a hard blocking criteria
+        // (like a CodeReview -2).  Thus, if hard blocked, it is
+        // irrelevant what the subtask states, or the PASS criteria are.
+        //
+        // It is also important that FAIL be useable to indicate that
+        // the task has actually executed.  Thus subtask status,
+        // including a subtask FAIL should not appear as a FAIL on the
+        // parent task.  This means that this is should be the only path
+        // to make a task have a FAIL status.
+        return Status.FAIL;
+      }
+      if (task.pass == null) {
+        // A task with a FAIL but no PASS criteria is a PASS-FAIL task
+        // (they are never "READY").  It didn't fail, so pass.
+        return Status.PASS;
+      }
+    }
+
+    if (a.subTasks != null && !isAll(a.subTasks, Status.PASS)) {
+      // It is possible for a subtask's PASS criteria to change while
+      // a parent task is executing, or even after the parent task
+      // completes.  This can result in the parent PASS criteria being
+      // met while one or more of its subtasks no longer meets its PASS
+      // criteria (the subtask may now even meet a FAIL criteria).  We
+      // never want the parent task to reflect a PASS criteria in these
+      // cases, thus we can safely return here without ever evaluating
+      // the task's PASS criteria.
+      return Status.WAITING;
+    }
+
+    if (task.pass != null && ! match(c, task.pass)) {
+      // Non-leaf tasks with no PASS criteria are supported in order
+      // to support "grouping tasks" (tasks with no function aside from
+      // organizing tasks).  A task without a PASS criteria, cannot ever
+      // be expected to execute (how would you know if it has?), thus a
+      // pass criteria is required to possibly even be considered for
+      // READY.
+      return Status.READY;
+    }
+
+    return Status.PASS;
+  }
+
+  private String getHint(Status status, Task task) {
+    if (status == Status.READY) {
+      return task.readyHint;
+    } else if (status == Status.FAIL) {
+      return task.failHint;
+    }
+    return null;
+  }
+
+  private static boolean isAll(Iterable<TaskAttribute> tasks, Status state) {
+    for (TaskAttribute task : tasks) {
+      if (task.status != state) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean match(ChangeData c, String query) throws
+      OrmException, QueryParseException {
+    if (query == null || query.equalsIgnoreCase("true")) {
+      return true;
+    }
+    if (query == null || query.equalsIgnoreCase("true")) {
+      throw new QueryParseException(query);
+    }
+
+    c.notes(); // ChangeData bug, it fails to load notes for has:drafts
+    Predicate<ChangeData> pred = cqb.get().parse(query);
+    if (pred instanceof Matchable) {
+      return pred.asMatchable().match(c);
+    }
+    return false;
+  }
+
+  private static boolean isAllNull(Object... vals) {
+    for (Object val : vals) {
+      if (val != null) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  static class TaskAttribute {
+    String name;
+    Boolean inProgress;
+    Status status;
+    String hint;
+    List<TaskAttribute> subTasks;
+
+    public TaskAttribute(String name) {
+      this.name = name;
+    }
+  }
+
+  static class TaskPluginAttribute extends PluginDefinedInfo {
+    public List roots = new ArrayList<TaskAttribute>();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
new file mode 100644
index 0000000..1d4883d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.common.Container;
+import com.google.gerrit.reviewdb.client.Branch;
+import org.eclipse.jgit.lib.Config;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Task Configuration file living in git */
+public class TaskConfig extends AbstractVersionedMetaData {
+  private class Section extends Container {
+    public TaskConfig config;
+
+    public Section() {
+      this.config = TaskConfig.this;
+    }
+  }
+
+  public class Task extends Section {
+    public String name;
+    public String applicable;
+    public String fail;
+    public String inProgress;
+    public String pass;
+    public String readyHint;
+    public String failHint;
+    public List<String> subTasks;
+    public List<String> subTasksExternals;
+    public List<String> subTasksFiles;
+
+    public Task(SubSection s) {
+      name = getString(s, KEY_NAME, s.subSection);
+      applicable = getString(s, KEY_APPLICABLE, null);
+      fail = getString(s, KEY_FAIL, null);
+      inProgress = getString(s, KEY_IN_PROGRESS, null);
+      pass = getString(s, KEY_PASS, null);
+      readyHint = getString(s, KEY_READY_HINT, null);
+      failHint = getString(s, KEY_FAIL_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);
+    }
+  }
+
+  private static final String SECTION_EXTERNAL = "external";
+  private static final String SECTION_ROOT = "root";
+  private static final String SECTION_TASK = "task";
+  private static final String KEY_APPLICABLE = "applicable";
+  private static final String KEY_FAIL = "fail";
+  private static final String KEY_FILE = "file";
+  private static final String KEY_IN_PROGRESS = "in-progress";
+  private static final String KEY_NAME = "name";
+  private static final String KEY_PASS = "pass";
+  private static final String KEY_READY_HINT = "ready-hint";
+  private static final String KEY_FAIL_HINT = "fail-hint";
+  private static final String KEY_SUBTASK = "subtask";
+  private static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
+  private static final String KEY_SUBTASKS_FILE = "subtasks-file";
+  private static final String KEY_USER = "user";
+
+  public TaskConfig(Branch.NameKey branch, String fileName) {
+    super(branch, fileName);
+  }
+
+  public List<Task> getRootTasks() {
+    return getTasks(SECTION_ROOT);
+  }
+
+  public List<Task> getTasks() {
+    return getTasks(SECTION_TASK);
+  }
+
+  private List<Task> getTasks(String type) {
+    List<Task> tasks = new ArrayList<Task>();
+    // No need to get a task with no name (what would we call it?)
+    for (String task : cfg.getSubsections(type)) {
+      tasks.add(new Task(new SubSection(type, task)));
+    }
+    return tasks;
+  }
+
+  public List<External> getExternals() {
+    List<External> externals = new ArrayList<External>();
+    // 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));
+  }
+
+  private External getExternal(SubSection s) {
+    return new External(s);
+  }
+
+  private String getString(SubSection s, String key, String def) {
+    String v = cfg.getString(s.section, s.subSection, key);
+    return v != null ? v : def;
+  }
+
+  private List<String> getStringList(SubSection s, String key) {
+    return Arrays.asList(cfg.getStringList(s.section, s.subSection, key));
+  }
+
+  private static class SubSection {
+    final String section;
+    final String subSection;
+
+    SubSection(String section, String subSection) {
+      this.section = section;
+      this.subSection = subSection;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
new file mode 100644
index 0000000..a9cdbcc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class TaskConfigFactory {
+  private static final Logger log =
+      LoggerFactory.getLogger(TaskConfigFactory.class);
+
+  private static final String EXTENSION = ".config";
+  private static final String DEFAULT = "task" + EXTENSION;
+  private static final String LEGACY_DEFAULT = "tasks" + EXTENSION;
+
+  private final GitRepositoryManager gitMgr;
+  private final AllProjectsName allProjects;
+
+  @Inject
+  TaskConfigFactory(GitRepositoryManager gitMgr) {
+    // Injecting AllProjectsName doesn't work here
+    this.allProjects = new AllProjectsName("All-Projects");
+    this.gitMgr = gitMgr;
+  }
+
+  public TaskConfig getRootConfig() throws ConfigInvalidException, IOException {
+    TaskConfig cfg = getTaskConfig(getRootBranch(), DEFAULT);
+    if (cfg.getRootTasks().isEmpty()) {
+      return getTaskConfig(getRootBranch(), LEGACY_DEFAULT);
+    }
+    return cfg;
+  }
+
+  private Branch.NameKey getRootBranch() {
+    return new Branch.NameKey(allProjects, "refs/meta/config");
+  }
+
+  public TaskConfig getTaskConfig(Branch.NameKey branch, String fileName)
+      throws ConfigInvalidException, IOException {
+    TaskConfig cfg = new TaskConfig(branch, fileName);
+    Project.NameKey project = branch.getParentKey();
+    try {
+      Repository git = gitMgr.openRepository(project);
+      try {
+        cfg.load(git);
+      } finally {
+        git.close();
+      }
+    } catch (IOException e) {
+      log.warn("Failed to load " + fileName + " for " + project.get(), e);
+      throw e;
+    } catch (ConfigInvalidException e) {
+      throw e;
+    }
+    return cfg;
+  }
+}
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
new file mode 100644
index 0000000..7db4c66
--- /dev/null
+++ b/src/main/resources/Documentation/task.md
@@ -0,0 +1,294 @@
+@PLUGIN@
+========
+
+The @PLUGIN@ plugin provides a mechanism to manage tasks which need to be
+performed on changes.  The task plugin creates a common place where tasks can
+be defined, along with a common way to expose and query this information.
+Task definition includes defining which changes each task applies to, and how
+to determine the status for each task.  Tasks are organized hierarchically.
+This hierarchy is considered for task applicability and status.
+
+An important use case of the task plugin is to have a common place for CI
+systems to define which changes they will operate on, and when they will do
+so.  This makes it possible for independent and unrelated teams to setup
+entirely independent CI systems which operate on different sets of changes,
+all while exposing these applicability relations to Gerrit and other teams and
+users.  This also makes it possible for work for a single change to be split
+across multiple cooperating CI systems so that assessments can be staged and
+gated upon various other tasks or assessments first completing (or passing).
+
+Exposing task applicability information helps users determine via Gerrit which,
+if any, system will "take care" of their changes.  Users can thus figure this
+out without having the knowledge of "how to", or even "the ability to" query
+the configuration of external CI systems.  This also makes it possible for
+conflicting systems to more easily be detected in cases when more than one
+system is mistakenly configured to be responsible for the same changes.
+
+Exposing task hierarchy information via Gerrit helps users understand the
+workflow that is expected of their changes.  It helps them visualize task
+requirements and which tasks are expected to be completed before another task
+will even be attempted.  It helps them understand how their changes are, or
+are not progressing through the outlined stages.
+
+Exposing task status information helps users and CI systems determine via
+Gerrit when it is appropriate for them to take action (to perform their task).
+It helps them identify blocking tasks.  It helps them figure out what they
+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
+if it has not yet completed execution, or the outcome of the task if it has
+completed execution.  Tasks generally progress from `WAITING` to `READY` as
+their subtasks complete, and then from `READY` to `PASS` once the task itself
+completes.
+
+A task with a `WAITING` status is not yet ready to execute.  A task in this
+state is blocked by its subtasks which are not yet in the `PASS` state.
+
+A task with a `READY` status is ready to be executed.  All of its subtasks are
+in the `PASS` state.
+
+A task with a `PASS` status meets all the criteria for `READY`, and has
+executed and was successful.
+
+A task with a `FAIL` status has executed and was unsuccessful.
+
+A task with a `INVALID` status has an invalid definition.
+
+Tasks
+-----
+The applicability of each task is constrained to specific changes using an
+"applicable" query.  Since tasks are defined hierarchically, the applicability
+of subtasks is inherently limited by the applicability of all tasks above them
+in the hierarchy.  Tasks can either be root tasks, or subtasks.  Tasks with no
+defined pass criteria and with defined subtasks are valid, but they are only
+applicable when at least one subtask is applicable.
+
+Tasks are defined in the `All-Projects` project, on the `refs/meta/config`
+branch, in a file named `task.config`.  This file uses the gitconfig
+format to define root tasks and subtasks.  The special "True" keyword
+may be used as any query definition to indicate an always matchign query.
+The following keys may be defined in any task section:
+
+`applicable`
+
+: This key defines a query that is used to determine whether a task is
+applicable to each change.
+
+Example:
+```
+    applicable = status:open
+```
+
+`fail`
+
+: This key defines a query that is used to determine whether a task has
+already executed and failed for each change.
+
+Example:
+```
+    fail = label:verified-1
+```
+
+`in-progress`
+
+: This key defines a query that is used to determine whether a task is
+currently in-progress or not.  A CI system may use this to ensure that it
+only runs one verification instance for a specific change. Either a pass
+or fail key is mandatory for leaf tasks.  A task with a fail criteria,
+but no pass criteria, will pass if it otherwise would be ready.  Setting
+this to "True" is useful for defining blocking criteria that do not
+actually have a task to execute.
+
+Example:
+```
+    in-progress = label:patchset-lock,user=jenkins
+```
+
+`pass`
+
+: This key defines a query that is used to determine whether a task has
+already executed and passed for each change.  Either a pass or fail
+key is mandatory for leaf tasks.  Setting this to "True" is useful for
+defining informational tasks that are not really expected to execute.
+
+Example:
+```
+    pass = label:verified+1
+```
+
+Example:
+```
+    in-progress = label:patchset-lock,user=jenkins
+```
+
+`ready-hint`
+
+: This key defines a hint when a task is `READY` describing what
+accomplishing the tasks entails.  This is meant to be a hint for humans
+and may be used as a tool-tip.
+
+Example:
+```
+    ready-hint = Needs to be verified by Jenkins
+```
+
+`fail-hint`
+
+: This key defines a hint when a task is in the `FAIL` state describing why
+the tasks is failing.  This is meant to be a hint for humans
+and may be used as a tool-tip.
+
+Example:
+```
+    fail-hint = Blocked by a negative review score
+```
+
+`subtask`
+
+: This key lists the name of a subtask of the current task.  This key may be
+used several times in a task section to define more than one subtask for a
+particular task.
+
+Example:
+
+```
+    subtask = "Code Review"
+    subtask = "License Approval"
+```
+
+`subtasks-external`
+
+: This key indicate 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 sub tasks of the current task!
+
+Example:
+
+```
+    subtasks-external = my-external
+```
+
+`subtasks-file`
+
+: This key indicate 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-file key points to
+a file under the top level task directory in the same project and ref as the
+current task file.  Note: all of the tasks in the referenced file will be
+included as sub tasks of the current task!
+
+Example:
+
+```
+    subtasks-file = common.config  # references the file named task/common.config
+```
+
+Root Tasks
+----------
+Root tasks typically define the "final verification" tasks for changes.  Each
+root task likely defines a single CI system which is responsible for verifying
+and possibly submitting the changes which are managed by that CI system.
+Applicable queries for all root tasks should generally be defined in a non
+overlapping fashion.
+
+Root tasks are defined using "root" sections.  A sample task.config which
+defines 3 non overlapping CI systems might look like this:
+
+```
+[root "Jenkins Build"]
+   applicable = status:open AND (project:a OR project:b) AND -branch:master
+   ...
+
+[root "Jenkins Build and Test"]
+   applicable = status:open AND (project:a OR project:b) AND branch:master
+   ...
+
+[root "Buildbot"]
+   applicable = status:open AND project:c
+   ...
+```
+
+Subtasks
+--------
+Subtasks define tasks that must pass before their parent task state is
+considered `READY` or `PASS`.  Subtasks make it possible to define task
+execution dependencies and ordering.  Subtasks typically define all the
+things that are required for change submission except for the final criteria
+that will be assessed by the final verification defined by the change's root
+task.  This may include tasks that need to be executed by humans, such as
+approvals like `code-review`, along with automated tasks such as tests, or
+static analysis tool executions.
+
+Subtasks are defined using a "task" section.  An example subtask definition:
+
+```
+[task "Code Review"]
+    pass = label:code-review+2
+    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
+which will include applicable tasks for the change added to their output.
+
+```
+  $ ssh -x -p 29418 example.com gerrit query change:123
+  change I9fdfb1315610a8e3d5c48e4321193b7c265f30ae
+  ...
+  plugins:
+    name: task
+    roots:
+      name: Jenkins Build and Test
+      inProgress: false
+      status: READY
+      subTasks:
+        name: code review
+        status: PASS
+```
+
+Examples
+--------
+See [task_states](task_states.html) for a comprehensive list of examples
+of task configs and their states.
diff --git a/src/main/resources/Documentation/task_states.md b/src/main/resources/Documentation/task_states.md
new file mode 100644
index 0000000..826b0a1
--- /dev/null
+++ b/src/main/resources/Documentation/task_states.md
@@ -0,0 +1,464 @@
+@PLUGIN@ States
+===============
+
+Below are sample config files which illustrate many examples of how task
+states are affected by their own criteria and their subtasks' states.
+
+`task.config` file in project `All-Project` on ref `refs/meta/config`.
+
+```
+[root "Root PASS"]
+  applicable = has:draft
+  pass = True
+
+[root "Root FAIL"]
+  applicable = has:draft
+  fail = True
+  fail-hint = Change has a draft
+
+[root "Root straight PASS"]
+  applicable = has:draft
+  pass = has:draft
+
+[root "Root straight FAIL"]
+  applicable = has:draft
+  fail = has:draft
+  pass = has:draft
+
+[root "Root PASS-fail"]
+  applicable = has:draft
+  fail = NOT has:draft
+
+[root "Root pass-FAIL"]
+  applicable = has:draft
+  fail = has:draft
+
+[root "Root grouping PASS (subtask PASS)"]
+  applicable = has:draft
+  subtask = Subtask PASS
+
+[root "Root grouping WAITING (subtask READY)"]
+  applicable = has:draft
+  subtask = Subtask READY
+
+[root "Root grouping WAITING (subtask FAIL)"]
+  applicable = has:draft
+  subtask = Subtask FAIL
+
+[root "Root grouping NA (subtask NA)"]
+  applicable = has:draft
+  subtask = Subtask NA
+
+[root "Root READY (subtask PASS)"]
+  applicable = has:draft
+  pass = -has:draft
+  subtask = Subtask PASS
+  ready-hint = You must now run the ready task
+
+[root "Root WAITING (subtask READY)"]
+  applicable = has:draft
+  pass = has:draft
+  subtask = Subtask READY
+
+[root "Root WAITING (subtask FAIL)"]
+  applicable = has:draft
+  pass = has:draft
+  subtask = Subtask FAIL
+
+[root "Root IN PROGRESS"]
+   applicable = has:draft
+   in-progress = has:draft
+   pass = -has:draft
+
+[root "Root NOT IN PROGRESS"]
+   applicable = has:draft
+   in-progress = -has:draft
+   pass = -has:draft
+
+[root "Subtasks File"]
+  applicable = has:draft
+  subtasks-file = common.config
+
+[root "Subtasks File (Missing)"]
+  applicable = has:draft
+  subtasks-file = common.config
+  subtasks-file = missing
+
+[root "Subtasks External"]
+  applicable = has:draft
+  subtasks-external = user special
+
+[root "Subtasks External (Missing)"]
+  applicable = has:draft
+  subtasks-external = user special
+  subtasks-external = missing
+
+[root "Subtasks External (User Missing)"]
+  applicable = has:draft
+  subtasks-external = user special
+  subtasks-external = user missing
+
+[root "Subtasks External (File Missing)"]
+  applicable = has:draft
+  subtasks-external = user special
+  subtasks-external = file missing
+
+[root "INVALIDS"]
+  applicable = has:draft
+  subtasks-file = invalids.config
+
+[task "Subtask FAIL"]
+  applicable = has:draft
+  fail = has:draft
+  pass = has:draft
+
+[task "Subtask READY"]
+  applicable = has:draft
+  pass = -has:draft
+  subtask = Subtask PASS
+
+[task "Subtask PASS"]
+  applicable = has:draft
+  pass = has:draft
+
+[task "Subtask NA"]
+  applicable = NOT has:draft
+
+[external "user special"]
+  user = mfick
+  file = special.config
+
+[external "user missing"]
+  user = missing
+  file = special.config
+
+[external "file missing"]
+  user = mfick
+  file = missing
+```
+
+`task/common.config` file in project `All-Projects` on ref `refs/meta/config`.
+
+```
+[task "file task/common.config PASS"]
+  applicable = has:draft
+  pass = has:draft
+
+[task "file task/common.config FAIL"]
+  applicable = has:draft
+  fail = has:draft
+  pass = has:draft
+```
+
+`task/invalids.config` file in project `All-Projects` on ref `refs/meta/config`.
+
+```
+[task "No PASS criteria"]
+  applicable = has:draft
+
+[task "WAITING (subtask INVALID)"]
+  applicable = has:draft
+  pass = has:draft
+  subtask = Subtask INVALID
+
+[task "WAITING (subtask missing)"]
+  applicable = has:draft
+  pass = has:draft
+  subtask = MISSING # security bug: subtask name appears in output
+
+[task "Grouping WAITING (subtask INVALID)"]
+  applicable = has:draft
+  subtask = Subtask INVALID
+
+[task "Grouping WAITING (subtask missing)"]
+  applicable = has:draft
+  subtask = MISSING  # security bug: subtask name appears in output
+
+[task "Subtask INVALID"]
+  applicable = has:draft
+
+```
+
+`task/special.config` file in project `All-Users` on ref `refs/users/01/1000001`.
+
+```
+[task "userfile task/special.config PASS"]
+  applicable = has:draft
+  pass = has:draft
+
+[task "userfile task/special.config FAIL"]
+  applicable = has:draft
+  fail = has:draft
+  pass = has:draft
+```
+
+The expeced output for the above task configs looks like:
+
+```
+ $  ssh -x -p 29418 review-example gerrit query has:draft \
+     --task--applicable --format json|head -1 |json_pp
+{
+   ...,
+   "plugins" : [
+      {
+         "roots" : [
+            {
+               "status" : "PASS",
+               "name" : "Root PASS"
+            },
+            {
+               "hint" : "Change has a draft",
+               "status" : "FAIL",
+               "name" : "Root FAIL"
+            },
+            {
+               "status" : "PASS",
+               "name" : "Root straight PASS"
+            },
+            {
+               "status" : "FAIL",
+               "name" : "Root straight FAIL"
+            },
+            {
+               "status" : "PASS",
+               "name" : "Root PASS-fail"
+            },
+            {
+               "status" : "FAIL",
+               "name" : "Root pass-FAIL"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "PASS",
+                     "name" : "Subtask PASS"
+                  }
+               ],
+               "status" : "PASS",
+               "name" : "Root grouping PASS (subtask PASS)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "subTasks" : [
+                        {
+                           "status" : "PASS",
+                           "name" : "Subtask PASS"
+                        }
+                     ],
+                     "status" : "READY",
+                     "name" : "Subtask READY"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Root grouping WAITING (subtask READY)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "FAIL",
+                     "name" : "Subtask FAIL"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Root grouping WAITING (subtask FAIL)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "PASS",
+                     "name" : "Subtask PASS"
+                  }
+               ],
+               "hint" : "You must now run the ready task",
+               "status" : "READY",
+               "name" : "Root READY (subtask PASS)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "subTasks" : [
+                        {
+                           "status" : "PASS",
+                           "name" : "Subtask PASS"
+                        }
+                     ],
+                     "status" : "READY",
+                     "name" : "Subtask READY"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Root WAITING (subtask READY)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "FAIL",
+                     "name" : "Subtask FAIL"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Root WAITING (subtask FAIL)"
+            },
+            {
+               "inProgress" : true,
+               "status" : "READY",
+               "name" : "Root IN PROGRESS"
+            },
+            {
+               "inProgress" : false,
+               "status" : "READY",
+               "name" : "Root NOT IN PROGRESS"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "PASS",
+                     "name" : "file task/common.config PASS"
+                  },
+                  {
+                     "status" : "FAIL",
+                     "name" : "file task/common.config FAIL"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Subtasks File"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "PASS",
+                     "name" : "file task/common.config PASS"
+                  },
+                  {
+                     "status" : "FAIL",
+                     "name" : "file task/common.config FAIL"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Subtasks File (Missing)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "PASS",
+                     "name" : "userfile task/special.config PASS"
+                  },
+                  {
+                     "status" : "FAIL",
+                     "name" : "userfile task/special.config FAIL"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Subtasks External"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "INVALID",
+                     "name" : "UNKNOWN"
+                  },
+                  {
+                     "status" : "PASS",
+                     "name" : "userfile task/special.config PASS"
+                  },
+                  {
+                     "status" : "FAIL",
+                     "name" : "userfile task/special.config FAIL"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Subtasks External (Missing)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "INVALID",
+                     "name" : "UNKNOWN"
+                  },
+                  {
+                     "status" : "PASS",
+                     "name" : "userfile task/special.config PASS"
+                  },
+                  {
+                     "status" : "FAIL",
+                     "name" : "userfile task/special.config FAIL"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Subtasks External (User Missing)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "PASS",
+                     "name" : "userfile task/special.config PASS"
+                  },
+                  {
+                     "status" : "FAIL",
+                     "name" : "userfile task/special.config FAIL"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "Subtasks External (File Missing)"
+            },
+            {
+               "subTasks" : [
+                  {
+                     "status" : "INVALID",
+                     "name" : "No PASS criteria"
+                  },
+                  {
+                     "subTasks" : [
+                        {
+                           "status" : "INVALID",
+                           "name" : "Subtask INVALID"
+                        }
+                     ],
+                     "status" : "WAITING",
+                     "name" : "WAITING (subtask INVALID)"
+                  },
+                  {
+                     "subTasks" : [
+                        {
+                           "status" : "INVALID",
+                           "name" : "MISSING"
+                        }
+                     ],
+                     "status" : "WAITING",
+                     "name" : "WAITING (subtask missing)"
+                  },
+                  {
+                     "subTasks" : [
+                        {
+                           "status" : "INVALID",
+                           "name" : "Subtask INVALID"
+                        }
+                     ],
+                     "status" : "WAITING",
+                     "name" : "Grouping WAITING (subtask INVALID)"
+                  },
+                  {
+                     "subTasks" : [
+                        {
+                           "status" : "INVALID",
+                           "name" : "MISSING"
+                        }
+                     ],
+                     "status" : "WAITING",
+                     "name" : "Grouping WAITING (subtask missing)"
+                  },
+                  {
+                     "status" : "INVALID",
+                     "name" : "Subtask INVALID"
+                  }
+               ],
+               "status" : "WAITING",
+               "name" : "INVALIDS"
+            }
+         ],
+         "name" : "task"
+      }
+   ],
+   ...
+```
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
new file mode 100755
index 0000000..88b5042
--- /dev/null
+++ b/test/check_task_statuses.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+
+example() { # example_num
+    awk '/```/{Q++;E=(Q+1)/2};E=='"$1" < "$DOC_STATES" | grep -v '```'
+}
+
+setup_repo() { # repo remote ref
+    local repo=$1 remote=$2 ref=$3
+    git init "$repo"
+    (
+        cd "$repo"
+        git fetch "$remote" "$ref"
+        git checkout FETCH_HEAD
+    )
+}
+
+update_repo() { # repo remote ref
+    local repo=$1 remote=$2 ref=$3
+    (
+        cd "$repo"
+        git add .
+        git commit -m 'Testing task plugin'
+        git push "$remote" HEAD:"$ref"
+    )
+}
+
+query() { # query
+    ssh -x -p "$PORT" "$SERVER" gerrit query "$1" \
+            --format json --task--applicable| head -1 | json_pp
+}
+
+query_plugins() { query "$1" | awk '$0=="   \"plugins\" : [",$0=="   ],"' ; }
+
+MYDIR=$(dirname "$0")
+DOCS=$MYDIR/.././src/main/resources/Documentation
+OUT=$MYDIR/../target/tests
+
+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
+INVALIDS_CFG=$ALL_TASKS/invalids.config
+USER_SPECIAL_CFG=$USER_TASKS/special.config
+
+# --- Args ----
+SERVER=$1
+[ -z "$SERVER" ] && { echo "You must specify a server" ; exit ; }
+
+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" "$USER_TASKS"
+
+example 1 > "$ROOT_CFG"
+example 2 > "$COMMON_CFG"
+example 3 > "$INVALIDS_CFG"
+example 4 > "$USER_SPECIAL_CFG"
+
+update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+update_repo "$USERS" "$REMOTE_USERS" "$REF_USERS"
+
+
+example 5 |tail -n +5| awk 'NR>1{print P};{P=$0}' > "$EXPECTED"
+query_plugins has:draft > "$STATUSES"
+
+diff "$EXPECTED" "$STATUSES"