Initial version of the depends-on plugin

The depends-on plugin for Gerrit is designed for groups using
the 'Depends-on:' change comment to manage inter-project change
dependencies.

When a new change is created via the cherry-pick API, this plugin
propagates dependencies by copying the 'Depends-on:' comment from
the source change to the new destination change.

NOTE: Only the latest 'Depends-on:' comment is considered and the
      older ones are ignored.

This plugin builds against Gerrit 3.2.10(using bazel) and includes
docker based functional tests.

Change-Id: I9a4b8b1499422464310cd6fd54e01fe0d1cf6714
diff --git a/.bazelrc b/.bazelrc
new file mode 100644
index 0000000..3ae03ff
--- /dev/null
+++ b/.bazelrc
@@ -0,0 +1,2 @@
+build --workspace_status_command="python ./tools/workspace_status.py"
+test --build_tests_only
diff --git a/.bazelversion b/.bazelversion
new file mode 100644
index 0000000..fcdb2e1
--- /dev/null
+++ b/.bazelversion
@@ -0,0 +1 @@
+4.0.0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..922d9e7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+/eclipse-out
+/target
+/.bazel_path
+/.classpath
+/.settings
+/.project
+/.DS_Store
+/bazel-*
+*.iml
+/.apt_generated/
+/.apt_generated_tests/
diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 0000000..8a8e161
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,10 @@
+- job:
+    name: plugins-depends-on-build
+    parent: gerrit-plugin-build
+    pre-run:
+        tools/playbooks/install_docker.yaml
+
+- project:
+    check:
+      jobs:
+        - plugins-depends-on-build
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..f82e6a8
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,97 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
+load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
+
+plugin_name = "depends-on"
+
+java_plugin(
+    name = "auto-annotation-plugin",
+    processor_class = "com.google.auto.value.processor.AutoAnnotationProcessor",
+    deps = [
+        "@auto-value-annotations//jar",
+        "@auto-value//jar",
+    ],
+)
+
+java_plugin(
+    name = "auto-value-plugin",
+    processor_class = "com.google.auto.value.processor.AutoValueProcessor",
+    deps = [
+        "@auto-value-annotations//jar",
+        "@auto-value//jar",
+    ],
+)
+
+java_library(
+    name = "auto-value",
+    exported_plugins = [
+        ":auto-annotation-plugin",
+        ":auto-value-plugin",
+    ],
+    visibility = ["//visibility:public"],
+    exports = ["@auto-value//jar"],
+)
+
+java_library(
+    name = "auto-value-annotations",
+    exported_plugins = [
+        ":auto-annotation-plugin",
+        ":auto-value-plugin",
+    ],
+    visibility = ["//visibility:public"],
+    exports = ["@auto-value-annotations//jar"],
+)
+
+gerrit_plugin(
+    name = plugin_name,
+    srcs = glob(["src/main/java/**/*.java"]),
+    manifest_entries = [
+        "Gerrit-PluginName: " + plugin_name,
+        "Implementation-Title: Depends-on Plugin",
+        "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/" + plugin_name,
+        "Gerrit-Module: com.googlesource.gerrit.plugins.depends.on.Module",
+        "Gerrit-SshModule: com.googlesource.gerrit.plugins.depends.on.SshModule",
+        "Gerrit-HttpModule: com.googlesource.gerrit.plugins.depends.on.HttpModule",
+    ],
+    resources = glob(["src/main/resources/**/*"]),
+    deps = [
+        ":auto-value",
+        ":auto-value-annotations",
+    ],
+)
+
+junit_tests(
+    name = "depends-on_tests",
+    srcs = glob(["src/test/java/**/*Test.java"]),
+    tags = [plugin_name],
+    deps = [":depends-on__plugin_test_deps"],
+)
+
+java_library(
+    name = "depends-on__plugin_test_deps",
+    testonly = True,
+    srcs = glob(
+        ["src/test/java/**/*.java"],
+        exclude = ["src/test/java/**/*Test.java"],
+    ),
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [plugin_name],
+)
+
+sh_test(
+    name = "docker-tests",
+    size = "medium",
+    srcs = ["test/docker/run.sh"],
+    args = [
+        "--depends-on-plugin-jar",
+        "$(location :depends-on)",
+    ],
+    data = [plugin_name] + glob(["test/**"]),
+    local = True,
+    tags = ["docker"],
+)
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/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..24670df
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,19 @@
+workspace(name = "depends-on")
+
+load("//:bazlets.bzl", "load_bazlets")
+
+load_bazlets(
+    commit = "f96f4bce9ffafeaa200fc009a378921c512fcb0a",
+)
+
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+    "gerrit_api",
+)
+
+# Load release Plugin API
+gerrit_api()
+
+load("//:external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl
new file mode 100644
index 0000000..f089af4
--- /dev/null
+++ b/bazlets.bzl
@@ -0,0 +1,18 @@
+load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
+
+NAME = "com_googlesource_gerrit_bazlets"
+
+def load_bazlets(
+        commit,
+        local_path = None):
+    if not local_path:
+        git_repository(
+            name = NAME,
+            remote = "https://gerrit.googlesource.com/bazlets",
+            commit = commit,
+        )
+    else:
+        native.local_repository(
+            name = NAME,
+            path = local_path,
+        )
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..d574618
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,16 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+    AUTO_VALUE_VERSION = "1.7"
+
+    maven_jar(
+        name = "auto-value",
+        artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
+        sha1 = "fe8387764ed19460eda4f106849c664f51c07121",
+    )
+
+    maven_jar(
+        name = "auto-value-annotations",
+        artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
+        sha1 = "5be124948ebdc7807df68207f35a0f23ce427f29",
+    )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/ChangeMessageStore.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/ChangeMessageStore.java
new file mode 100644
index 0000000..23bfcfc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/ChangeMessageStore.java
@@ -0,0 +1,152 @@
+// Copyright (C) 2020 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.depends.on;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.ChangeMessage;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.restapi.change.PostReview;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.depends.on.extensions.DependencyResolver;
+import com.googlesource.gerrit.plugins.depends.on.formats.Comment;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ChangeMessageStore implements DependencyResolver {
+  private static final Logger log = LoggerFactory.getLogger(ChangeMessageStore.class);
+
+  public interface Factory {
+    ChangeMessageStore create();
+  }
+
+  protected final Provider<PostReview> reviewProvider;
+  protected final ChangeResource.Factory changeResourceFactory;
+  protected final CurrentUser currentUser;
+  protected final Resolver resolver;
+  protected final ChangeNotes.Factory changeNotesFactory;
+  protected final ChangeMessagesUtil cmUtil;
+
+  @Inject
+  public ChangeMessageStore(
+      Provider<PostReview> reviewProvider,
+      ChangeResource.Factory changeResourceFactory,
+      CurrentUser currentUser,
+      Resolver resolver,
+      ChangeNotes.Factory changeNotesFactory,
+      ChangeMessagesUtil cmUtil) {
+    this.reviewProvider = reviewProvider;
+    this.changeResourceFactory = changeResourceFactory;
+    this.currentUser = currentUser;
+    this.resolver = resolver;
+    this.changeNotesFactory = changeNotesFactory;
+    this.cmUtil = cmUtil;
+  }
+
+  /**
+   * Load the current DependsOn from the DB for a specific change. "Current" is defined as the last
+   * Depends-on defined. Older Depends-ons are assumed to be overriden by the last one. If the last
+   * Depends-on is blank, it deletes any previous dependencies.
+   *
+   * <p>return empty set means no dependencies found.
+   */
+  public Set<DependsOn> load(Change.Id cid) {
+    ChangeNotes changeNote = changeNotesFactory.createChecked(cid);
+    List<ChangeMessage> messages = cmUtil.byChange(changeNote);
+    List<ChangeMessage> sortedChangeMessages =
+        messages.stream()
+            .sorted(Comparator.comparing(ChangeMessage::getWrittenOn).reversed())
+            .collect(Collectors.toCollection(ArrayList::new));
+    for (ChangeMessage message : sortedChangeMessages) {
+      Optional<Set<DependsOn>> deps = Comment.from(message.getMessage());
+      if (deps.isPresent()) {
+        return deps.get();
+      }
+    }
+    return Collections.emptySet();
+  }
+
+  /** If needed, create a comment on the change with a DependsOn for the dependencies. */
+  @Override
+  public boolean resolveDependencies(PatchSet.Id patchSetId, Set<Set<BranchNameKey>> deliverables)
+      throws InvalidChangeOperationException, StorageException {
+    Change.Id cid = patchSetId.changeId();
+    Set<DependsOn> deps = load(cid);
+    if (Resolver.isResolved(deps)) {
+      return false;
+    }
+    Set<DependsOn> resolved = resolver.resolve(deps, deliverables);
+    if (resolved.equals(deps)) {
+      return false; // Nothing resolved this pass
+    }
+    // ToDo: add info about the resolved depends-on (deliverable, branch, and ChangeId?)
+    store(patchSetId, resolved, "Auto-updating resolved Depends-on");
+    return true;
+  }
+
+  @Override
+  public boolean hasUnresolvedDependsOn(Change.Id changeId) {
+    return !Resolver.isResolved(load(changeId));
+  }
+
+  /** Create a comment on the change with a DependsOn for the deps. */
+  public void store(PatchSet.Id patchSetId, Set<DependsOn> deps, String message)
+      throws InvalidChangeOperationException, StorageException {
+    StringBuilder comment = new StringBuilder();
+    if (message != null) {
+      comment.append(message + "\n\n");
+    }
+    comment.append(Comment.getMessages(deps));
+    ReviewInput review = new ReviewInput();
+    review.message = Strings.emptyToNull(comment.toString());
+    ChangeNotes changeNotes =
+        changeNotesFactory.createChecked(patchSetId.changeId());
+    ChangeResource changeResource = changeResourceFactory.create(changeNotes, currentUser);
+    PatchSet patchSet = changeNotes.load().getPatchSets().get(patchSetId);
+    try {
+      reviewProvider.get().apply(new RevisionResource(changeResource, patchSet), review);
+    } catch (RestApiException
+        | UpdateException
+        | IOException
+        | PermissionBackendException
+        | ConfigInvalidException
+        | PatchListNotAvailableException e) {
+      log.error("Unable to post auto-copied review comment", e);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/CoreListener.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/CoreListener.java
new file mode 100644
index 0000000..53f79ff
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/CoreListener.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2020 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.depends.on;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Change.Id;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventListener;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.inject.Inject;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CoreListener implements EventListener {
+  private static final Logger log = LoggerFactory.getLogger(CoreListener.class);
+
+  protected final Propagator propagator;
+  protected final ChangeNotes.Factory changeNotesFactory;
+
+  @Inject
+  public CoreListener(Propagator propagator, ChangeNotes.Factory changeNotesFactory) {
+    this.propagator = propagator;
+    this.changeNotesFactory = changeNotesFactory;
+  }
+
+  @Override
+  public void onEvent(Event event) {
+    if (event instanceof PatchSetCreatedEvent) {
+      PatchSetCreatedEvent patchSetCreatedEvent = (PatchSetCreatedEvent) event;
+      ChangeAttribute change = patchSetCreatedEvent.change.get();
+      if (change.cherryPickOfChange != null && patchSetCreatedEvent.patchSet.get().number == 1) {
+        try {
+          Optional<Id> sourceId = Change.Id.tryParse(change.cherryPickOfChange.toString());
+          Optional<Id> destId = Change.Id.tryParse(Integer.toString(change.number));
+          if (sourceId.isPresent() && destId.isPresent()) {
+            Change sourceChange =
+                changeNotesFactory.createChecked(sourceId.get()).getChange();
+            Change destChange =
+                changeNotesFactory.createChecked(destId.get()).getChange();
+            propagator.propagateFromSourceToDestination(sourceChange, destChange);
+          }
+        } catch (InvalidChangeOperationException | NoSuchChangeException e) {
+          log.error("Unable to propagate dependencies", e);
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOn.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOn.java
new file mode 100644
index 0000000..a27ad5e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOn.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2020 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.depends.on;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Change.Id;
+import java.util.Optional;
+
+/**
+ * Represents a non-git change dependency.
+ *
+ * <p>If populated with a Change.Id, then this is an explicit dependency. Populating it with only a
+ * Change.Key is documenting a dependency that may point to 0 or more changes and thus relies on
+ * custom intelligence, or human knowledge to figure out which project and branch this Change.Key is
+ * referring to.
+ */
+@AutoValue
+public abstract class DependsOn {
+
+  @Nullable
+  public abstract Change.Id id();
+
+  @Nullable
+  public abstract Change.Key key();
+
+  public static DependsOn create(String change) {
+    Optional<Id> id = null;
+    Change.Key key = null;
+    id = Change.Id.tryParse(change);
+    if (!id.isPresent()) {
+      return create(Change.Key.parse(change));
+    }
+    return create(id.get(), key);
+  }
+
+  public static DependsOn create(Change.Key key) {
+    return create(null, key);
+  }
+
+  public static DependsOn create(Change.Id id, Change.Key key) {
+    return new AutoValue_DependsOn(id, key);
+  }
+
+  public boolean isResolved() {
+    return id() != null;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/HttpModule.java
new file mode 100644
index 0000000..999dcc6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/HttpModule.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 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.depends.on;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.restapi.change.QueryChanges;
+
+public class HttpModule extends HttpPluginModule {
+  @Override
+  protected void configureServlets() {
+    bind(DynamicBean.class)
+        .annotatedWith(Exports.named(QueryChanges.class))
+        .to(ChangeMessageStore.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/Module.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/Module.java
new file mode 100644
index 0000000..102eb9c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/Module.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2021 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.depends.on;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.events.EventListener;
+import com.google.inject.AbstractModule;
+
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), EventListener.class).to(CoreListener.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/Propagator.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/Propagator.java
new file mode 100644
index 0000000..dfbb2e1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/Propagator.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2020 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.depends.on;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.inject.Inject;
+import java.util.HashSet;
+import java.util.Set;
+
+/*
+ * Propagates dependencies added on source change using "Depends-on:" tag to
+ * copied change. Dependencies on copied change are added using Change-Ids.
+ */
+public class Propagator {
+  protected final ChangeMessageStore changeMessageStore;
+  protected final ChangeNotes.Factory changeNotesFactory;
+
+  @Inject
+  public Propagator(ChangeMessageStore changeMessageStore, ChangeNotes.Factory changeNotesFactory) {
+    this.changeMessageStore = changeMessageStore;
+    this.changeNotesFactory = changeNotesFactory;
+  }
+
+  public void propagateFromSourceToDestination(Change srcChange, Change destChange)
+      throws InvalidChangeOperationException, NoSuchChangeException {
+
+    Set<DependsOn> deps = changeMessageStore.load(srcChange.getId());
+    if (!deps.isEmpty()) {
+      Set<DependsOn> keyDeps = new HashSet<DependsOn>(deps.size());
+      for (DependsOn dep : deps) {
+        keyDeps.add(DependsOn.create(loadChangeKey(dep)));
+      }
+      changeMessageStore.store(
+          destChange.currentPatchSetId(),
+          keyDeps,
+          "Dependencies propagated from " + srcChange.currentPatchSetId());
+    }
+  }
+
+  /**
+   * Get the Change-Id for a DependsOn. Look it up in the DB if the DependsOn Change-Id is not known
+   */
+  protected Change.Key loadChangeKey(DependsOn dep) {
+    Change.Key changeKey = dep.key();
+    if (changeKey != null) {
+      return changeKey;
+    }
+    Change c = changeNotesFactory.createChecked(dep.id()).getChange();
+    if (c != null) {
+      return c.getKey();
+    }
+    // Since the ChangeKey parser will accept any random string
+    // as a Key, it will inherently carry along random strings.
+    // This is thus used to carry unidentified dependencies to
+    // propagated changes (the assumption is that the user needs
+    // to fix it). Piggy back here on this idea for change-nums
+    // that lead to unidentified changes (treat them as bad
+    // strings, and throw them into the change-id to get
+    // propagated).
+    return Change.Key.parse(Integer.toString(dep.id().get()));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/Resolver.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/Resolver.java
new file mode 100644
index 0000000..b63d0af
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/Resolver.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2021 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.depends.on;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Resolver {
+  private final Provider<InternalChangeQuery> queryProvider;
+
+  @Inject
+  public Resolver(Provider<InternalChangeQuery> queryProvider) {
+    this.queryProvider = queryProvider;
+  }
+
+  /** Are all the deps resolved to a specific change? */
+  protected static boolean isResolved(Set<DependsOn> deps) {
+    for (DependsOn dep : deps) {
+      if (dep.id() == null) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  protected Set<DependsOn> resolve(Set<DependsOn> deps, Set<Set<BranchNameKey>> deliverables) {
+    /* ToDo: optimize and query all changes with a given Key up front */
+    Set<DependsOn> current = new HashSet<>();
+    for (DependsOn dep : deps) {
+      if (dep.id() == null) {
+        for (Set<BranchNameKey> deliverable : deliverables) {
+          Set<DependsOn> resolved = resolve(dep, deliverable);
+          if (resolved.isEmpty()) {
+            current.add(dep);
+          } else {
+            current.addAll(resolved);
+          }
+        }
+      } else {
+        current.add(dep);
+      }
+    }
+    return current;
+  }
+
+  protected Set<DependsOn> resolve(DependsOn dep, Set<BranchNameKey> deliverable) {
+    Set<DependsOn> found = new HashSet<>();
+    // Although we expect at most one change for each deliverable,
+    // it is possible that someone re-used a change id across different projects,
+    // therefore return all changes and let the caller decide what to do with them.
+    for (BranchNameKey branch : deliverable) {
+      for (ChangeData change : queryProvider.get().byBranchKey(branch, dep.key())) {
+        found.add(DependsOn.create(String.valueOf(change.getId())));
+      }
+    }
+    return found;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/SshModule.java
new file mode 100644
index 0000000..1631433
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/SshModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2021 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.depends.on;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.sshd.PluginCommandModule;
+import com.google.gerrit.sshd.commands.Query;
+
+public class SshModule extends PluginCommandModule {
+  @Override
+  public void configureCommands() {
+    bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(ChangeMessageStore.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/extensions/DependencyResolver.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/extensions/DependencyResolver.java
new file mode 100644
index 0000000..bf92899
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/extensions/DependencyResolver.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 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.depends.on.extensions;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import java.util.Set;
+
+public interface DependencyResolver extends DynamicBean {
+  public boolean resolveDependencies(PatchSet.Id patchSetId, Set<Set<BranchNameKey>> deliverables)
+      throws InvalidChangeOperationException, StorageException;
+
+  public boolean hasUnresolvedDependsOn(Change.Id changeId) throws StorageException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/formats/Comment.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/formats/Comment.java
new file mode 100644
index 0000000..6703965
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/formats/Comment.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2020 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.depends.on.formats;
+
+import com.googlesource.gerrit.plugins.depends.on.DependsOn;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/*
+ * Extract DependsOn(s) from comment string using Depends-on pattern.
+ * Print DependsOn(s) in its preferred form for comments.
+ */
+public class Comment {
+  protected static final Pattern DEPENDS_ON_PATTERN = Pattern.compile("^Depends-on:(.*)$");
+
+  /** return empty Optional instance means no dependencies found */
+  public static Optional<Set<DependsOn>> from(String comment) {
+    for (String line : comment.split("\n", -1)) {
+      Matcher match = DEPENDS_ON_PATTERN.matcher(line);
+      if (match.find()) {
+        // Sample: "Depends-on: 1234, 4444"
+        String changes = match.group(1); // -> "1234, 4444"
+        changes = changes.replace(",", " "); // -> "1234 4444"
+        return Optional.of(
+            Arrays.stream(changes.split("\\s+", -1)) // -> ["1234", "4444"]
+                .filter(c -> !c.isEmpty())
+                .map(c -> DependsOn.create(c))
+                .collect(Collectors.toSet()));
+      }
+    }
+    return Optional.empty();
+  }
+
+  public static StringBuilder getMessages(Set<DependsOn> dependsons) {
+    StringBuilder dependencies = new StringBuilder("Depends-on:");
+    for (DependsOn dep : dependsons) {
+      dependencies.append(" " + getMessage(dep));
+    }
+    return dependencies;
+  }
+
+  /**
+   * Print a DependsOn in its preferred form for comments. The preferred form is generally the most
+   * precise form currently supported based on the data available in the DependsOn.
+   */
+  public static String getMessage(DependsOn dependency) {
+    if (dependency.isResolved()) {
+      return String.valueOf(dependency.id().get());
+    }
+    return dependency.key().get();
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..930a0a6
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,47 @@
+@PLUGIN@
+========
+
+This plugin provides a way to mark a change dependent on other change(s). To
+mark a change dependent, post a comment with the list of dependencies under a
+change in the Gerrit page (not an inline diff comment) in a single line with
+the format `Depends-on: c1 c2 c3 ....` where c1, c2 and c3 are gerrit change
+numbers. Any number of changes can be provided after the `Depends-on:` tag.
+The `Depends-on:` tag is case-sensitive. Only changes listed in the most
+recent `Depends-on:` tag are considered as valid dependencies and older tags
+are ignored. To remove existing dependencies, a `Depends-on:` tag with no
+changes must be added.
+
+PROPAGATION
+-----------
+
+When a change is propagated, the @PLUGIN@ plugin adds a `Depends-on:` tag
+to the propagated change. `Depends-on:` created via change propagation have
+Change-Ids rather than actual change numbers. This plugin doesn't automatically
+propagate dependencies as there is no generic way to determine what the right
+destination branches are. A new `Depends-on:` tag can be added manually by
+updating the Change-Ids to the right change numbers if they resolve to changes
+destined for the desired branches.
+
+EXAMPLES
+--------
+
+Adding below as a change comment makes the change dependent on two other
+changes, 123 and 124.
+```
+Depends-on: 123 124
+```
+
+When the change is propagated, following tag is added on the destination
+change, where *Ibd61365f87a4d7fbb5d62ffbe4f563f675e000c5* and
+*I9a4b8b1499422464310cd6fd54e01fe0d1cf6714* are the Change-Ids of 123 and 124
+respectively.
+```
+Depends-on: Ibd61365f87a4d7fbb5d62ffbe4f563f675e000c5 I9a4b8b1499422464310cd6fd54e01fe0d1cf6714
+```
+
+Adding below as a change comment makes the change not dependent on any other
+changes. When such a change is propagated, no Depends-on tag is added to the
+propagated change.
+```
+Depends-on:
+```
diff --git a/src/test/java/com/googlesource/gerrit/plugins/depends/on/DependsOnParsingTest.java b/src/test/java/com/googlesource/gerrit/plugins/depends/on/DependsOnParsingTest.java
new file mode 100644
index 0000000..52de2a7
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/depends/on/DependsOnParsingTest.java
@@ -0,0 +1,204 @@
+// Copyright (C) 2020 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.depends.on;
+
+import com.google.gerrit.testing.InMemoryModule;
+import com.googlesource.gerrit.plugins.depends.on.formats.Comment;
+import java.util.Optional;
+import java.util.Set;
+import junit.framework.TestCase;
+import org.junit.Test;
+
+public class DependsOnParsingTest extends TestCase {
+  public static final String NUM = "1234";
+  public static final String NUM2 = "345";
+  public static final String KEY = "Iabcdef7890abcdef7890abcdef7890abcdef7890";
+  public static final String KEY2 = "I1234567890ABCDEFe7890ABC7868ABCDEF122233";
+
+  public static DependsOn NUM_DEP;
+  public static DependsOn KEY_DEP;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    new InMemoryModule().inject(this); // Needed to setup KeyUtil.ENCODER_IMPL
+    NUM_DEP = DependsOn.create(NUM);
+    KEY_DEP = DependsOn.create(KEY);
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    super.tearDown();
+  }
+
+  @Test
+  public void testCommentMessageChangeNum() {
+    assertTrue(NUM.equals(Comment.getMessage(NUM_DEP)));
+  }
+
+  @Test
+  public void testCommentMessageChangeKey() {
+    assertTrue(KEY.equals(Comment.getMessage(KEY_DEP)));
+  }
+
+  @Test
+  public void testParseNum() {
+    DependsOn dep = DependsOn.create(NUM);
+    assertTrue(NUM.equals("" + dep.id().get()));
+  }
+
+  @Test
+  public void testParseKey() {
+    DependsOn dep = DependsOn.create(KEY);
+    assertTrue(KEY.equals(dep.key().get()));
+  }
+
+  @Test
+  public void testParseNoneComment() {
+    String comment = "My Very Educated Mother Just Served Us Nothing!";
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertFalse(deps.isPresent());
+  }
+
+  @Test
+  public void testParseEmptyComment() {
+    String comment = "Depends-on:";
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 0);
+  }
+
+  @Test
+  public void testParseOneNumComment() {
+    String comment = "Depends-on:" + NUM;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    for (DependsOn dep : deps.get()) {
+      assertTrue(NUM.equals("" + dep.id().get()));
+      return;
+    }
+    assertTrue(false);
+  }
+
+  @Test
+  public void testParseOneKeyComment() {
+    String comment = "Depends-on:" + KEY;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    for (DependsOn dep : deps.get()) {
+      assertTrue(KEY.equals("" + dep.key().get()));
+      return;
+    }
+    assertTrue(false);
+  }
+
+  @Test
+  public void testParseTwoNumsComment() {
+    String comment = "Depends-on:" + NUM + " " + NUM2;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 2);
+    int found = 0;
+    for (DependsOn dep : deps.get()) {
+      assertTrue(NUM.equals("" + dep.id().get()) || NUM2.equals("" + dep.id().get()));
+      found++;
+    }
+    assertTrue(found == 2);
+  }
+
+  @Test
+  public void testParseTwoKeyComments() {
+    String comment = "Depends-on:" + KEY + " " + KEY2;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 2);
+    int found = 0;
+    for (DependsOn dep : deps.get()) {
+      assertTrue(KEY.equals("" + dep.key().get()) || KEY2.equals("" + dep.key().get()));
+      found++;
+    }
+    assertTrue(found == 2);
+  }
+
+  @Test
+  public void testParseNumAndKeyComment() {
+    String comment = "Depends-on:" + NUM + " " + KEY;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 2);
+    int found = 0;
+    for (DependsOn dep : deps.get()) {
+      try {
+        assertTrue(NUM.equals("" + dep.id().get()));
+      } catch (Exception e) {
+        assertTrue(KEY.equals("" + dep.key().get()));
+      }
+      found++;
+    }
+    assertTrue(found == 2);
+  }
+
+  public void testParseTwoNumsCommaComment() {
+    String comment = "Depends-on:" + NUM + "," + NUM2;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 2);
+    int found = 0;
+    for (DependsOn dep : deps.get()) {
+      assertTrue(NUM.equals("" + dep.id().get()) || NUM2.equals("" + dep.id().get()));
+      found++;
+    }
+    assertTrue(found == 2);
+  }
+
+  public void testParseTwoNumsCommaSpaceComment() {
+    String comment = "Depends-on:" + NUM + ", " + NUM2;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 2);
+    int found = 0;
+    for (DependsOn dep : deps.get()) {
+      assertTrue(NUM.equals("" + dep.id().get()) || NUM2.equals("" + dep.id().get()));
+      found++;
+    }
+    assertTrue(found == 2);
+  }
+
+  public void testParseTwoNumsWhiteComment() {
+    String comment = "Depends-on:" + NUM + ", \t" + NUM2;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 2);
+    int found = 0;
+    for (DependsOn dep : deps.get()) {
+      assertTrue(NUM.equals("" + dep.id().get()) || NUM2.equals("" + dep.id().get()));
+      found++;
+    }
+    assertTrue(found == 2);
+  }
+
+  public void testParseTwoNumsNewLineWhiteComment() {
+    // Should stop processing at newline
+    String comment = "Depends-on:" + NUM + ", \t\n" + NUM2;
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 1);
+    for (DependsOn dep : deps.get()) {
+      assertTrue(NUM.equals("" + dep.id().get()));
+    }
+  }
+
+  public void testParseEmbeddedComment() {
+    String comment = "Patch Set 2:\n\nDepends-on:" + NUM + " " + NUM2 + "\nHey\n";
+    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    assertTrue(deps.get().size() == 2);
+    int found = 0;
+    for (DependsOn dep : deps.get()) {
+      assertTrue(NUM.equals("" + dep.id().get()) || NUM2.equals("" + dep.id().get()));
+      found++;
+    }
+    assertTrue(found == 2);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/depends/on/ResolveDependsOnTest.java b/src/test/java/com/googlesource/gerrit/plugins/depends/on/ResolveDependsOnTest.java
new file mode 100644
index 0000000..9fc4a05
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/depends/on/ResolveDependsOnTest.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2020 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.depends.on;
+
+import com.google.gerrit.testing.InMemoryModule;
+import java.util.HashSet;
+import java.util.Set;
+import junit.framework.TestCase;
+import org.junit.Test;
+
+public class ResolveDependsOnTest extends TestCase {
+  public static final String NUM = "1234";
+  public static final String NUM2 = "345";
+  public static final String KEY = "Iabcdef7890abcdef7890abcdef7890abcdef7890";
+  public static final String KEY2 = "I1234567890ABCDEFe7890ABC7868ABCDEF122233";
+
+  public static DependsOn NUM_DEP;
+  public static DependsOn NUM2_DEP;
+  public static DependsOn KEY_DEP;
+  public static DependsOn KEY2_DEP;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    new InMemoryModule().inject(this); // Needed to setup KeyUtil.ENCODER_IMPL
+    NUM_DEP = DependsOn.create(NUM);
+    NUM2_DEP = DependsOn.create(NUM2);
+    KEY_DEP = DependsOn.create(KEY);
+    KEY2_DEP = DependsOn.create(KEY2);
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    super.tearDown();
+  }
+
+  @Test
+  public void testResolved2Nums() {
+    Set<DependsOn> deps = new HashSet<DependsOn>();
+    deps.add(NUM_DEP);
+    deps.add(NUM2_DEP);
+    assertTrue(Resolver.isResolved(deps));
+  }
+
+  @Test
+  public void testResolved2Keys() {
+    Set<DependsOn> deps = new HashSet<DependsOn>();
+    deps.add(KEY_DEP);
+    deps.add(KEY2_DEP);
+    assertFalse(Resolver.isResolved(deps));
+  }
+
+  @Test
+  public void testResolvedNumAndKey() {
+    Set<DependsOn> deps = new HashSet<DependsOn>();
+    deps.add(NUM_DEP);
+    deps.add(KEY_DEP);
+    assertFalse(Resolver.isResolved(deps));
+  }
+}
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
new file mode 100755
index 0000000..910d95c
--- /dev/null
+++ b/test/docker/docker-compose.yaml
@@ -0,0 +1,32 @@
+version: '3'
+services:
+
+  gerrit-01:
+    build:
+      context: gerrit
+      args:
+        - GERRIT_WAR
+        - DEPENDS_ON_PLUGIN_JAR
+    networks:
+      - gerrit-net
+    volumes:
+      - "gerrit-site-etc:/var/gerrit/etc"
+
+  run_tests:
+    build: run_tests
+    networks:
+      - gerrit-net
+    volumes:
+      - "../../:/depends_on:ro"
+      - "gerrit-site-etc:/server-ssh-key:ro"
+    depends_on:
+      - gerrit-01
+    environment:
+      - GERRIT_HOST=gerrit-01
+
+networks:
+  gerrit-net:
+    driver: bridge
+
+volumes:
+  gerrit-site-etc:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
new file mode 100755
index 0000000..e35f98f
--- /dev/null
+++ b/test/docker/gerrit/Dockerfile
@@ -0,0 +1,13 @@
+FROM gerritcodereview/gerrit:3.2.10-ubuntu20
+
+USER root
+
+ENV GERRIT_SITE /var/gerrit
+RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \
+    DEVELOPMENT_BECOME_ANY_ACCOUNT
+
+COPY artifacts /tmp/
+RUN cp /tmp/depends-on.jar "$GERRIT_SITE/plugins/depends-on.jar"
+RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
+
+USER gerrit
diff --git a/test/docker/run.sh b/test/docker/run.sh
new file mode 100755
index 0000000..abf59b9
--- /dev/null
+++ b/test/docker/run.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+
+readlink --canonicalize / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
+MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+ARTIFACTS=$MYDIR/gerrit/artifacts
+
+die() { echo -e "\nERROR: $@" ; kill $$ ; exit 1 ; } # error_message
+
+progress() { # message cmd [args]...
+    local message=$1 ; shift
+    echo -n "$message"
+    "$@" &
+    local pid=$!
+    while kill -0 $pid 2> /dev/null ; do
+        echo -n "."
+        sleep 2
+    done
+    echo
+    wait "$pid"
+}
+
+usage() { # [error_message]
+    local prog=$(basename "$0")
+    cat <<EOF
+Usage:
+    $prog [--depends-on-plugin-jar|-t <FILE_PATH>] [--gerrit-war|-g <FILE_PATH>]
+
+    This tool runs the plugin functional tests in a Docker environment built
+    from the gerritcodereview/gerrit base Docker image.
+
+    The depends-on plugin JAR and optionally a Gerrit WAR are expected to be in the
+    $ARTIFACTS dir;
+    however, the --depends-on-plugin-jar and --gerrit-war switches may be used as
+    helpers to specify which files to copy there.
+
+    Options:
+    --help|-h
+    --gerrit-war|-g                 path to Gerrit WAR file
+    --depends-on-plugin-jar|-e      path to depends-on plugin JAR file
+
+EOF
+
+    [ -n "$1" ] && echo -e "\nERROR: $1" && exit 1
+    exit 0
+}
+
+check_prerequisite() {
+    docker --version > /dev/null || die "docker is not installed"
+    docker-compose --version > /dev/null || die "docker-compose is not installed"
+}
+
+build_images() {
+    docker-compose "${COMPOSE_ARGS[@]}" build --quiet
+}
+
+run_depends_on_plugin_tests() {
+    docker-compose "${COMPOSE_ARGS[@]}" up --detach
+    docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=gerrit_admin run_tests \
+        '/depends_on/test/docker/run_tests/start.sh'
+}
+
+cleanup() {
+    docker-compose "${COMPOSE_ARGS[@]}" down -v --rmi local 2>/dev/null
+}
+
+while (( "$#" )); do
+    case "$1" in
+        --help|-h)                    usage ;;
+        --gerrit-war|-g)              shift ; GERRIT_WAR=$1 ;;
+        --depends-on-plugin-jar|-e)   shift ; DEPENDS_ON_PLUGIN_JAR=$1 ;;
+        *)                            usage "invalid argument $1" ;;
+    esac
+    shift
+done
+
+PROJECT_NAME="depends_on_$$"
+COMPOSE_YAML="$MYDIR/docker-compose.yaml"
+COMPOSE_ARGS=(--project-name "$PROJECT_NAME" -f "$COMPOSE_YAML")
+check_prerequisite
+mkdir -p -- "$ARTIFACTS"
+[ -n "$DEPENDS_ON_PLUGIN_JAR" ] && cp -f "$DEPENDS_ON_PLUGIN_JAR" "$ARTIFACTS/depends-on.jar"
+if [ ! -e "$ARTIFACTS/depends-on.jar" ] ; then
+    MISSING="Missing $ARTIFACTS/depends-on.jar"
+    [ -n "$DEPENDS_ON_PLUGIN_JAR" ] && die "$MISSING, check for copy failure?"
+    usage "$MISSING, did you forget --depends-on-plugin-jar?"
+fi
+[ -n "$GERRIT_WAR" ] && cp -f "$GERRIT_WAR" "$ARTIFACTS/gerrit.war"
+progress "Building docker images" build_images
+run_depends_on_plugin_tests ; RESULT=$?
+cleanup
+
+exit "$RESULT"
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
new file mode 100755
index 0000000..b3062da
--- /dev/null
+++ b/test/docker/run_tests/Dockerfile
@@ -0,0 +1,24 @@
+FROM alpine:3.11
+
+ARG UID=1000
+ARG GID=1000
+ENV USER gerrit_admin
+ENV USER_HOME /home/$USER
+ENV WORKSPACE $USER_HOME/workspace
+
+RUN apk --update add --no-cache openssh bash git util-linux openssl shadow curl jq python
+RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
+
+RUN groupadd -f -g $GID users2
+RUN useradd -u $UID -g $GID $USER
+RUN mkdir -p $WORKSPACE $USER_HOME/.ssh
+RUN chown -R $USER $USER_HOME
+
+USER $USER
+
+RUN ssh-keygen -P '' -f "$USER_HOME"/.ssh/id_rsa
+RUN chmod 400 "$USER_HOME"/.ssh/id_rsa
+RUN git config --global user.name "Gerrit Admin"
+RUN git config --global user.email "gerrit_admin@example.com"
+
+ENTRYPOINT ["tail", "-f", "/dev/null"]
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
new file mode 100755
index 0000000..8aea640
--- /dev/null
+++ b/test/docker/run_tests/start.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+PORT=29418
+TEST_PROJECT=test-project
+
+setup_test_project() {
+    echo "Creating a test project ..."
+    ssh -p "$PORT" -x "$GERRIT_HOST" gerrit create-project "${TEST_PROJECT}".git \
+        --owner "Administrators" --submit-type "MERGE_IF_NECESSARY"
+    git clone ssh://"$GERRIT_HOST":"$PORT"/"$TEST_PROJECT" "$WORKSPACE"
+    pushd "$WORKSPACE" > /dev/null
+    git commit -m "Initial commit" --allow-empty
+    git push ssh://"$GERRIT_HOST":"$PORT"/"$TEST_PROJECT" HEAD:refs/heads/master
+    popd > /dev/null
+}
+
+cp -r /depends_on "$USER_HOME"/
+
+cd "$USER_HOME"/depends_on/test
+./docker/run_tests/wait-for-it.sh "$GERRIT_HOST":"$PORT" \
+    -t 60 -- echo "Gerrit is up"
+
+echo "Creating a default user account ..."
+
+cat "$USER_HOME"/.ssh/id_rsa.pub | ssh -p 29418 -i /server-ssh-key/ssh_host_rsa_key \
+  "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit create-account \
+     --ssh-key - --email "gerrit_admin@localdomain"  --group "Administrators" "gerrit_admin"
+
+setup_test_project
+
+HTTP_PASSWD=$(uuidgen)
+ssh -p 29418 "$GERRIT_HOST" gerrit set-account "$USER" --http-password "$HTTP_PASSWD"
+cat <<EOT >> ~/.netrc
+machine $GERRIT_HOST
+login $USER
+password $HTTP_PASSWD
+EOT
+
+./test_dependson.sh --server "$GERRIT_HOST" --project "$TEST_PROJECT"
diff --git a/test/docker/run_tests/wait-for-it.sh b/test/docker/run_tests/wait-for-it.sh
new file mode 100755
index 0000000..d7b6e3c
--- /dev/null
+++ b/test/docker/run_tests/wait-for-it.sh
@@ -0,0 +1,162 @@
+#!/usr/bin/env bash
+# https://github.com/vishnubob/wait-for-it/blob/master/wait-for-it.sh
+#   Use this script to test if a given TCP host/port are available
+
+cmdname=$(basename $0)
+
+echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+    cat << USAGE >&2
+Usage:
+    $cmdname host:port [-s] [-t timeout] [-- command args]
+    -h HOST | --host=HOST       Host or IP under test
+    -p PORT | --port=PORT       TCP port under test
+                                Alternatively, you specify the host and port as host:port
+    -s | --strict               Only execute subcommand if the test succeeds
+    -q | --quiet                Don't output any status messages
+    -t TIMEOUT | --timeout=TIMEOUT
+                                Timeout in seconds, zero for no timeout
+    -- COMMAND ARGS             Execute command with args after the test finishes
+USAGE
+    exit 1
+}
+
+wait_for()
+{
+    if [[ $TIMEOUT -gt 0 ]]; then
+        echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
+    else
+        echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
+    fi
+    start_ts=$(date +%s)
+    while :
+    do
+        (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
+        result=$?
+        if [[ $result -eq 0 ]]; then
+            end_ts=$(date +%s)
+            echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
+            break
+        fi
+        sleep 1
+    done
+    return $result
+}
+
+wait_for_wrapper()
+{
+    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+    if [[ $QUIET -eq 1 ]]; then
+        timeout $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    else
+        timeout $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    fi
+    PID=$!
+    trap "kill -INT -$PID" INT
+    wait $PID
+    RESULT=$?
+    if [[ $RESULT -ne 0 ]]; then
+        echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
+    fi
+    return $RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+    case "$1" in
+        *:* )
+        hostport=(${1//:/ })
+        HOST=${hostport[0]}
+        PORT=${hostport[1]}
+        shift 1
+        ;;
+        --child)
+        CHILD=1
+        shift 1
+        ;;
+        -q | --quiet)
+        QUIET=1
+        shift 1
+        ;;
+        -s | --strict)
+        STRICT=1
+        shift 1
+        ;;
+        -h)
+        HOST="$2"
+        if [[ $HOST == "" ]]; then break; fi
+        shift 2
+        ;;
+        --host=*)
+        HOST="${1#*=}"
+        shift 1
+        ;;
+        -p)
+        PORT="$2"
+        if [[ $PORT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --port=*)
+        PORT="${1#*=}"
+        shift 1
+        ;;
+        -t)
+        TIMEOUT="$2"
+        if [[ $TIMEOUT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --timeout=*)
+        TIMEOUT="${1#*=}"
+        shift 1
+        ;;
+        --)
+        shift
+        CLI="$@"
+        break
+        ;;
+        --help)
+        usage
+        ;;
+        *)
+        echoerr "Unknown argument: $1"
+        usage
+        ;;
+    esac
+done
+
+if [[ "$HOST" == "" || "$PORT" == "" ]]; then
+    echoerr "Error: you need to provide a host and port to test."
+    usage
+fi
+
+TIMEOUT=${TIMEOUT:-15}
+STRICT=${STRICT:-0}
+CHILD=${CHILD:-0}
+QUIET=${QUIET:-0}
+
+if [[ $CHILD -gt 0 ]]; then
+    wait_for
+    RESULT=$?
+    exit $RESULT
+else
+    if [[ $TIMEOUT -gt 0 ]]; then
+        wait_for_wrapper
+        RESULT=$?
+    else
+        wait_for
+        RESULT=$?
+    fi
+fi
+
+if [[ $CLI != "" ]]; then
+    if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then
+        echoerr "$cmdname: strict mode, refusing to execute subprocess"
+        exit $RESULT
+    fi
+    exec $CLI
+else
+    exit $RESULT
+fi
diff --git a/test/lib_result.sh b/test/lib_result.sh
new file mode 100755
index 0000000..9efe493
--- /dev/null
+++ b/test/lib_result.sh
@@ -0,0 +1,40 @@
+# ---- TEST RESULTS ----
+
+RESULT=0
+
+result() { # test [error_message]
+    local result=$?
+    local outcome="FAIL"
+    if [ $result -eq 0 ] ; then
+        echo "PASSED - $1 test"
+        outcome="PASS"
+    else
+        echo "*** FAILED *** - $1 test"
+        RESULT=$result
+        [ $# -gt 1 ] && echo "$2"
+    fi
+    [ -n "$RESULT_CALLBACK" ] &&
+        "$RESULT_CALLBACK" "$(basename "$0")" "$1" "$outcome"
+}
+
+# output must match expected to pass
+result_out() { # test expected output
+    local disp=$(echo "Expected Output:" ;\
+                 echo "    $2" ;\
+                 echo "Actual Output:" ;\
+                 echo "    $3")
+
+    [ "$2" = "$3" ]
+    result "$1" "$disp"
+}
+
+# output must not match unallowed to pass
+result_not_out() { # test unallowed output
+    local disp=$(echo "Unallowed Output:" ;\
+                 echo "    $2" ;\
+                 echo "Actual Output:" ;\
+                 echo "    $3")
+
+    [ "$2" != "$3" ]
+    result "$1" "$disp"
+}
diff --git a/test/test_dependson.sh b/test/test_dependson.sh
new file mode 100755
index 0000000..bf55203
--- /dev/null
+++ b/test/test_dependson.sh
@@ -0,0 +1,176 @@
+#!/usr/bin/env bash
+
+# run a gerrit ssh command
+gssh() { ssh -x -p "$PORT" "$SERVER" "$@" ; 2>&1 ; } # [args]...
+
+query() {
+    gssh gerrit query --format=json "$@" | head -1 | \
+        python -c 'import sys,json; print json.dumps(json.load(sys.stdin))'
+}
+
+q() { "$@" > /dev/null 2>&1 ; } # cmd [args...]  # quiet a command
+
+die() { echo -e "$@" ; exit 1 ; } # error_message
+
+mygit() { git --work-tree="$REPO_DIR" --git-dir="$GIT_DIR" "$@" ; } # [args...]
+
+# > uuid
+gen_uuid() { uuidgen | openssl dgst -sha1 -binary | xxd -p; }
+
+gen_commit_msg() { # msg > commit_msg
+    local msg=$1
+    echo "$msg
+
+Change-Id: I$(gen_uuid)"
+}
+
+get_open_changes() {
+    curl --netrc --silent "http://$SERVER:8080/a/changes/?q=status:open"
+}
+
+get_branch_revision() { # prj branch > revision
+    curl --netrc --silent \
+        "http://$SERVER:8080/a/projects/$1/branches/$2" | \
+        tail -n +2 | jq --raw-output '.revision'
+}
+
+create_branch() { # prj revision dest_branch
+    curl --netrc --silent --data "revision=$2" \
+        "http://$SERVER:8080/a/projects/$1/branches/$3"
+}
+
+get_change_num() { # < gerrit_push_response > changenum
+    local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }')
+    echo "${url##*\/}" | tr -d -c '[:digit:]'
+}
+
+cherry_pick_change() { # change_num dest_branch > changenum
+    curl -X POST --netrc --silent --header 'Content-Type: application/json' \
+        --data '{"message" : "Copied Change", "destination" : "'$2'"}' \
+        "http://$SERVER:8080/a/changes/$1/revisions/current/cherrypick" | \
+        tail -n +2 | jq --raw-output '._number'
+}
+
+create_change() { # branch file [commit_message] > changenum
+    local branch=$1 tmpfile=$2 msg=$3 out rtn
+    local content=$RANDOM dest=refs/for/$branch
+
+    out=$(mygit fetch "$GITURL" "$branch" 2>&1) ||\
+       die "Failed to fetch $branch: $out"
+    out=$(mygit checkout FETCH_HEAD 2>&1) ||\
+       die "Failed to checkout $branch: $out"
+
+    echo -e "$content" > "$tmpfile"
+
+    out=$(mygit add "$tmpfile" 2>&1) || die "Failed to git add: $out"
+
+    msg=$(gen_commit_msg "Add $tmpfile")
+
+    out=$(mygit commit -m "$msg" 2>&1) ||\
+        die "Failed to commit change: $out"
+    [ -n "$VERBOSE" ] && echo "  commit:$out" >&2
+
+    out=$(mygit push "$GITURL" "HEAD:$dest" 2>&1) ||\
+        die "Failed to push change: $out"
+    out=$(echo "$out" | get_change_num) ; rtn=$? ; echo "$out"
+    [ -n "$VERBOSE" ] && echo "  change:$out" >&2
+    return $rtn
+}
+
+get_depends_on_tag() { # change > depends-on tag
+    local change_number=$1
+    IFS=$'\n'
+    local comments=( $(gssh gerrit query --comments "$change_number") )
+    IFS=''
+    for ((i = "${#comments[@]}" -1 ; i >= 0 ; i--)) ; do
+        if [[ "${comments[i]}" =~ 'Depends-on:' ]] ; then
+            echo "${comments[i]}" | sed 's/^ *//g'
+            break
+        fi
+    done
+}
+
+# ------------------------- Usage ---------------------------
+
+usage() { # [error_message]
+    cat <<-EOF
+Usage: $MYPROG [-s|--server <server>] [-p|--project <project>]
+             [-r|--srcref <ref branch>] [-d|--destref <ref branch>] [-h|--help]
+
+       -h|--help                 usage/help
+       -s|--server <server>      server to use for the test (default: localhost)
+       -p|--project <project>    git project to use (default: project0)
+       -r|--srcref <ref branch>  reference branch used to create changes (default: master)
+       -d|--destref <ref branch> reference branch used to propagate change (default: foo)
+EOF
+
+    [ -n "$1" ] && echo -e '\n'"ERROR: $1"
+    exit 1
+}
+
+parseArgs() {
+    SERVER="localhost"
+    PROJECT="tools/test/project0"
+    SRC_REF_BRANCH="master"
+    DEST_REF_BRANCH="foo"
+    while (( "$#" )) ; do
+        case "$1" in
+            --server|-s)  shift; SERVER=$1 ;;
+            --project|-p) shift; PROJECT=$1 ;;
+            --srcref|-r)  shift; SRC_REF_BRANCH=$1 ;;
+            --destref|-d) shift; DEST_REF_BRANCH=$1 ;;
+            --help|-h)    usage ;;
+            --verbose|-v) VERBOSE=$1 ;;
+            *)            usage "invalid argument '$1'" ;;
+        esac
+        shift
+    done
+
+    [ -n "$SERVER" ]     || usage "server not set"
+    [ -n "$PROJECT" ]    || usage "project not set"
+    [ -n "$SRC_REF_BRANCH" ] || usage "source ref branch not set"
+    [ -n "$DEST_REF_BRANCH" ] || usage "dest ref branch not set"
+}
+
+MYPROG=$(basename "$0")
+MYDIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+
+source "$MYDIR/lib_result.sh"
+PORT=29418
+
+parseArgs "$@"
+
+TEST_DIR="$MYDIR/../target/test"
+rm -rf "$TEST_DIR"
+mkdir -p "$TEST_DIR"
+
+GITURL=ssh://$SERVER:$PORT/$PROJECT
+
+# We need to do an initial REST call, as the first REST call after a server is
+# brought up results in being anonymous despite providing proper authentication.
+q get_open_changes
+
+q create_branch "$PROJECT" "$(get_branch_revision "$PROJECT" "$SRC_REF_BRANCH")" "$DEST_REF_BRANCH"
+
+SRC_REF=$SRC_REF_BRANCH
+echo "$SRC_REF_BRANCH" | grep -q '^refs/' || SRC_REF=refs/heads/$SRC_REF_BRANCH
+git ls-remote "$GITURL" | grep -q "$SRC_REF" || usage "invalid project/server/srcref"
+DEST_REF=$DEST_REF_BRANCH
+echo "$DEST_REF_BRANCH" | grep -q '^refs/' || DEST_REF=refs/heads/$DEST_REF_BRANCH
+git ls-remote "$GITURL" | grep -q "$DEST_REF" || usage "invalid project/server/destref"
+
+REPO_DIR=$TEST_DIR/repo
+q git init "$REPO_DIR"
+GIT_DIR=$REPO_DIR/.git
+FILE_A=$REPO_DIR/fileA
+
+# ------------------------- Depends-on Test ---------------------------
+base_change=$(create_change "$SRC_REF_BRANCH" "$FILE_A") || exit
+src_change=$(create_change "$SRC_REF_BRANCH" "$FILE_A") || exit
+gssh gerrit review --message \'"Depends-on: $base_change"\' "$src_change",1
+dest_change=$(cherry_pick_change "$src_change" "$DEST_REF")
+expected="Depends-on: $(query "$base_change" | jq --raw-output '.id')"
+actual=$(get_depends_on_tag "$dest_change")
+result_out "propagate depends-on" "$expected" "$actual"
+
+exit $RESULT
diff --git a/tools/BUILD b/tools/BUILD
new file mode 100644
index 0000000..cc10083
--- /dev/null
+++ b/tools/BUILD
@@ -0,0 +1 @@
+# Empty file - bazel treat directories with BUILD file as a package
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
new file mode 100644
index 0000000..f40498e
--- /dev/null
+++ b/tools/bzl/BUILD
@@ -0,0 +1 @@
+# Empty BUILD file, needed by Bazel.
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
new file mode 100644
index 0000000..c921d01
--- /dev/null
+++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,6 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+    _classpath_collector = "classpath_collector",
+)
+
+classpath_collector = _classpath_collector
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..97307bd
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,6 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+    _junit_tests = "junit_tests",
+)
+
+junit_tests = _junit_tests
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..ed1504d
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1,4 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", _gerrit = "GERRIT", _maven_jar = "maven_jar")
+
+maven_jar = _maven_jar
+GERRIT = _gerrit
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..4d2dbdd
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,10 @@
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
+    _gerrit_plugin = "gerrit_plugin",
+    _plugin_deps = "PLUGIN_DEPS",
+    _plugin_test_deps = "PLUGIN_TEST_DEPS",
+)
+
+gerrit_plugin = _gerrit_plugin
+PLUGIN_DEPS = _plugin_deps
+PLUGIN_TEST_DEPS = _plugin_test_deps
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
new file mode 100644
index 0000000..4460b1f
--- /dev/null
+++ b/tools/eclipse/BUILD
@@ -0,0 +1,9 @@
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS")
+load("//tools/bzl:classpath.bzl", "classpath_collector")
+
+classpath_collector(
+    name = "main_classpath_collect",
+    deps = PLUGIN_DEPS + [
+        "//:depends-on__plugin",
+    ],
+)
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
new file mode 100755
index 0000000..10e8131
--- /dev/null
+++ b/tools/eclipse/project.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Copyright (C) 2018 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.
+`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n depends-on -r .
diff --git a/tools/playbooks/install_docker.yaml b/tools/playbooks/install_docker.yaml
new file mode 100644
index 0000000..89cf315
--- /dev/null
+++ b/tools/playbooks/install_docker.yaml
@@ -0,0 +1,8 @@
+- hosts: all
+  roles:
+    - name: ensure-docker
+  tasks:
+    - name: Install compose
+      shell: |
+        sudo curl -L "https://github.com/docker/compose/releases/download/1.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+        sudo chmod +x /usr/local/bin/docker-compose
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
new file mode 100644
index 0000000..e76bc4b
--- /dev/null
+++ b/tools/workspace_status.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+from __future__ import print_function
+import subprocess
+import sys
+
+CMD = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty']
+
+
+def revision():
+    try:
+        return subprocess.check_output(CMD).strip().decode("utf-8")
+    except OSError as err:
+        print('could not invoke git: %s' % err, file=sys.stderr)
+        sys.exit(1)
+    except subprocess.CalledProcessError as err:
+        print('error using git: %s' % err, file=sys.stderr)
+        sys.exit(1)
+
+
+print("STABLE_BUILD_DEPENDS-ON_LABEL %s" % revision())