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())