Issue tracking hooks using Gerrit 2.6 commit validation
Has a common reusable infrastructure to build the issue-tracking
association logic to other Issue-Trackers (i.e. BugZilla, JIRA).
The generic issue-tracker association plugin is hooks-its and
provides the logic for:
> Insertion of comment links using regex patterns
> Enforcement of issue-ids in git commits using
Gerrit 2.6 commit validation listeners
> Automation of issue-tracker status transition
based on Gerrit code-review status
hooks-its does not make assumption on the underlying issue-tracker
system but provides only an abstract facade interface ItsFacade to
access and perform actions on the issue-tracker.
Change-Id: I82ceb9a5eae825e6216121365f15ed4d4d59acc4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..67742f2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.idea
+target/
+.classpath
+.project
+.settings
+
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/hooks-its/pom.xml b/hooks-its/pom.xml
new file mode 100644
index 0000000..1a472c7
--- /dev/null
+++ b/hooks-its/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.googlesource.gerrit.plugins.its</groupId>
+ <artifactId>hooks-its-parent</artifactId>
+ <version>2.6-SNAPSHOT</version>
+ </parent>
+ <artifactId>hooks-its</artifactId>
+ <name>Gerrit Code Review - Commit validation and Workflow</name>
+ <url>http://maven.apache.org</url>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-plugin-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
new file mode 100644
index 0000000..fa41a86
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks;
+
+import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.inject.AbstractModule;
+import com.googlesource.gerrit.plugins.hooks.its.ItsName;
+import com.googlesource.gerrit.plugins.hooks.validation.ItsValidateComment;
+import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddComment;
+import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddRelatedLinkToChangeId;
+import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddRelatedLinkToGitWeb;
+import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterChangeState;
+
+public class ItsHookModule extends AbstractModule {
+
+ private String itsName;
+
+ public ItsHookModule(String itsName) {
+ this.itsName = itsName;
+ }
+
+ @Override
+ protected void configure() {
+ bind(String.class).annotatedWith(ItsName.class).toInstance(itsName);
+ DynamicSet.bind(binder(), ChangeListener.class).to(
+ GerritHookFilterAddRelatedLinkToChangeId.class);
+ DynamicSet.bind(binder(), ChangeListener.class).to(
+ GerritHookFilterAddComment.class);
+ DynamicSet.bind(binder(), ChangeListener.class).to(
+ GerritHookFilterChangeState.class);
+ DynamicSet.bind(binder(), ChangeListener.class).to(
+ GerritHookFilterAddRelatedLinkToGitWeb.class);
+ DynamicSet.bind(binder(), CommitValidationListener.class).to(
+ ItsValidateComment.class);
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InitIts.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InitIts.java
new file mode 100644
index 0000000..46db02c
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InitIts.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.its;
+
+import com.google.gerrit.pgm.init.InitStep;
+import com.google.gerrit.pgm.init.Section;
+import com.google.gerrit.pgm.util.ConsoleUI;
+
+public class InitIts implements InitStep {
+
+ public static String COMMENT_LINK_SECTION = "commentLink";
+
+ public static enum YesNoEnum {
+ Y, N;
+ }
+
+ public static enum TrueFalseEnum {
+ TRUE, FALSE;
+ }
+
+ @Override
+ public void run() throws Exception {
+ }
+
+ public boolean isConnectivityRequested(ConsoleUI ui, String url) {
+ YesNoEnum wantToTest =
+ ui.readEnum(YesNoEnum.N, "Test connectivity to %s", url);
+ return wantToTest == YesNoEnum.Y;
+ }
+
+ public boolean enterSSLVerify(Section section) {
+ return TrueFalseEnum.TRUE == section.select("Verify SSL Certificates",
+ "sslVerify", TrueFalseEnum.TRUE);
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InvalidTransitionException.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InvalidTransitionException.java
new file mode 100644
index 0000000..38b3178
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InvalidTransitionException.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.its;
+
+import java.io.IOException;
+
+public class InvalidTransitionException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ public InvalidTransitionException(String message) {
+ super(message);
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsFacade.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsFacade.java
new file mode 100644
index 0000000..2ee72a3
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsFacade.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.its;
+
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * A simple facade to an issue tracking system (its)
+ */
+public interface ItsFacade {
+
+ public enum Check {
+ SYSINFO,
+ ACCESS
+ }
+
+ public String name();
+
+ public String healthCheck(Check check)
+ throws IOException;
+
+ public void addRelatedLink(String issueId, URL relatedUrl, String description)
+ throws IOException;
+
+ public void addComment(String issueId, String comment)
+ throws IOException;
+
+ public void performAction(String issueId, String actionName)
+ throws IOException;
+
+ public boolean exists(final String issueId)
+ throws IOException;
+
+ public String createLinkForWebui(String url, String text);
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsName.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsName.java
new file mode 100644
index 0000000..55f49e6
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsName.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.its;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import com.google.inject.BindingAnnotation;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ItsName {
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/NoopItsFacade.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/NoopItsFacade.java
new file mode 100644
index 0000000..49d2e03
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/its/NoopItsFacade.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.its;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An ITS facade doing nothing, it's configured when no ITS are referenced in
+ * config
+ */
+public class NoopItsFacade implements ItsFacade {
+
+ private Logger log = LoggerFactory.getLogger(NoopItsFacade.class);
+
+ @Override
+ public void addComment(String issueId, String comment) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("addComment({},{})", issueId, comment);
+ }
+ }
+
+ @Override
+ public void addRelatedLink(String issueId, URL relatedUrl, String description)
+ throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("addRelatedLink({},{},{})", new Object[] {issueId, relatedUrl,
+ description});
+ }
+ }
+
+ @Override
+ public boolean exists(String issueId) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("exists({})", issueId);
+ }
+ return false;
+ }
+
+ @Override
+ public void performAction(String issueId, String actionName)
+ throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("performAction({},{})", issueId, actionName);
+ }
+ }
+
+ @Override
+ public String healthCheck(Check check) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("healthCheck()");
+ }
+ return "{\"status\"=\"ok\",\"system\"=\"not configured\",}";
+ }
+
+ @Override
+ public String createLinkForWebui(String url, String text) {
+ if (log.isDebugEnabled()) {
+ log.debug("createLinkForWebui({},{})", url, text);
+ }
+ return "";
+ }
+
+ @Override
+ public String name() {
+ return "not configured";
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsAssociationPolicy.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsAssociationPolicy.java
new file mode 100644
index 0000000..30788c2
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsAssociationPolicy.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.validation;
+
+public enum ItsAssociationPolicy {
+ MANDATORY, SUGGESTED, OPTIONAL;
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateComment.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateComment.java
new file mode 100644
index 0000000..cfe8868
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateComment.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.validation;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+
+public class ItsValidateComment implements CommitValidationListener {
+
+ private static final Logger log = LoggerFactory
+ .getLogger(ItsValidateComment.class);
+
+ @Inject
+ private ItsFacade client;
+
+ @Inject
+ @GerritServerConfig
+ private Config gerritConfig;
+
+ public List<CommitValidationMessage> validCommit(ReceiveCommand cmd, RevCommit commit) throws CommitValidationException {
+
+ HashMap<Pattern, ItsAssociationPolicy> regexes = getCommentRegexMap();
+ if (regexes.size() == 0) {
+ return Collections.emptyList();
+ }
+
+ String message = commit.getFullMessage();
+ log.debug("Searching comment " + message.trim() + " for patterns "
+ + regexes);
+
+ String issueId = null;
+ ItsAssociationPolicy associationPolicy = ItsAssociationPolicy.OPTIONAL;
+ Pattern pattern = null;
+ for (Entry<Pattern, ItsAssociationPolicy> entry : regexes.entrySet()) {
+ pattern = entry.getKey();
+ Matcher matcher = pattern.matcher(message);
+ associationPolicy = entry.getValue();
+ if (matcher.find()) {
+ issueId = extractMatchedWorkItems(matcher);
+ log.debug("Pattern matched on comment '{}' with issue id '{}'",
+ message.trim(), issueId);
+ break;
+ }
+ }
+
+ String validationMessage = null;
+ if (pattern != null && issueId == null) {
+ validationMessage =
+ "Missing issue-id in commit message\n"
+ + "Commit "
+ + commit.getId().getName()
+ + " not associated to any issue\n"
+ + "\n"
+ + "Hint: insert one or more issue-id anywhere in the commit message.\n"
+ + " Issue-ids are strings matching " + pattern.pattern() + "\n"
+ + " and are pointing to existing tickets on "
+ + client.name() + " Issue-Tracker";
+ } else if (pattern != null && !isWorkitemPresent(issueId, message)) {
+ validationMessage =
+ "Issue " + issueId + " not found or visible in " + client.name()
+ + " Issue-Tracker";
+ } else {
+ return Collections.emptyList();
+ }
+
+ switch (associationPolicy) {
+ case MANDATORY:
+ throw new CommitValidationException(validationMessage.split("\n")[0],
+ Collections.singletonList(new CommitValidationMessage("\n"
+ + validationMessage + "\n", false)));
+
+ case SUGGESTED:
+ return Collections.singletonList(new CommitValidationMessage("\n"
+ + validationMessage + "\n", false));
+
+ default:
+ return Collections.emptyList();
+ }
+ }
+
+ private boolean isWorkitemPresent(String issueId, String comment) {
+ boolean exist = false;
+ if (issueId != null) {
+ try {
+ if (!client.exists(issueId)) {
+ log.warn("Workitem " + issueId + " declared in the comment "
+ + comment + " but not found on ITS");
+ } else {
+ exist = true;
+ log.warn("Workitem " + issueId + " found");
+ }
+ } catch (IOException ex) {
+ log.warn("Unexpected error accessint ITS", ex);
+ }
+ } else {
+ log.debug("Rejecting commit: no pattern matched on comment " + comment);
+ }
+ return exist;
+ }
+
+ private HashMap<Pattern, ItsAssociationPolicy> getCommentRegexMap() {
+ HashMap<Pattern, ItsAssociationPolicy> regexMap = new HashMap<Pattern, ItsAssociationPolicy>();
+
+ Set<String> linkSubsections = gerritConfig.getSubsections("commentLink");
+ for (String string : linkSubsections) {
+ String match = gerritConfig.getString("commentLink", string, "match");
+ if (match != null) {
+ regexMap
+ .put(Pattern.compile(match), gerritConfig.getEnum("commentLink",
+ string, "association", ItsAssociationPolicy.OPTIONAL));
+ }
+ }
+
+ return regexMap;
+ }
+
+ private String extractMatchedWorkItems(Matcher matcher) {
+ int groupCount = matcher.groupCount();
+ if (groupCount >= 1)
+ return matcher.group(1);
+ else
+ return null;
+ }
+
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(
+ CommitReceivedEvent receiveEvent) throws CommitValidationException {
+ return validCommit(receiveEvent.command, receiveEvent.commit);
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java
new file mode 100644
index 0000000..83c1544
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java
@@ -0,0 +1,185 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.workflow;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.events.ChangeAbandonedEvent;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.ChangeRestoredEvent;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.hooks.its.ItsName;
+
+public class GerritHookFilter implements ChangeListener {
+ private static final Logger log = LoggerFactory.getLogger(GerritHookFilter.class);
+
+ @Inject @GerritServerConfig
+ private Config gerritConfig;
+
+ @Inject @ItsName
+ private String itsName;
+
+ @Inject
+ private GitRepositoryManager repoManager;
+
+ public String getComment(String projectName, String commitId)
+ throws IOException {
+
+ final Repository repo =
+ repoManager.openRepository(new NameKey(projectName));
+ try {
+ RevWalk revWalk = new RevWalk(repo);
+ RevCommit commit = revWalk.parseCommit(ObjectId.fromString(commitId));
+
+ return commit.getFullMessage();
+ } finally {
+ repo.close();
+ }
+ }
+
+ protected String[] getIssueIds(String gitComment) {
+ List<Pattern> commentRegexList = getCommentRegexList();
+ if (commentRegexList == null) return new String[] {};
+
+ log.debug("Matching '" + gitComment + "' against " + commentRegexList);
+
+ ArrayList<String> issues = new ArrayList<String>();
+ for (Pattern pattern : commentRegexList) {
+ Matcher matcher = pattern.matcher(gitComment);
+
+ while (matcher.find()) {
+ int groupCount = matcher.groupCount();
+ for (int i = 1; i <= groupCount; i++) {
+ String group = matcher.group(i);
+ issues.add(group);
+ }
+ }
+ }
+
+ return issues.toArray(new String[issues.size()]);
+ }
+
+ protected Long[] getWorkItems(String gitComment) {
+ List<Pattern> commentRegexList = getCommentRegexList();
+ if (commentRegexList == null) return new Long[] {};
+
+ log.debug("Matching '" + gitComment + "' against " + commentRegexList);
+
+ ArrayList<Long> workItems = new ArrayList<Long>();
+
+ for (Pattern pattern : commentRegexList) {
+ Matcher matcher = pattern.matcher(gitComment);
+
+ while (matcher.find()) {
+ addMatchedWorkItems(workItems, matcher);
+ }
+ }
+
+ return workItems.toArray(new Long[workItems.size()]);
+ }
+
+ private void addMatchedWorkItems(ArrayList<Long> workItems, Matcher matcher) {
+ int groupCount = matcher.groupCount();
+ for (int i = 1; i <= groupCount; i++) {
+
+ String group = matcher.group(i);
+ try {
+ Long workItem = new Long(group);
+ workItems.add(workItem);
+ } catch (NumberFormatException e) {
+ log.debug("matched string '" + group
+ + "' is not a work item > skipping");
+ }
+ }
+ }
+
+ private List<Pattern> getCommentRegexList() {
+ ArrayList<Pattern> regexList = new ArrayList<Pattern>();
+
+ String match = gerritConfig.getString("commentLink", itsName, "match");
+ if (match != null) {
+ regexList.add(Pattern.compile(match));
+ }
+
+ return regexList;
+ }
+
+ public void doFilter(PatchSetCreatedEvent hook) throws IOException {
+ }
+
+ public void doFilter(CommentAddedEvent hook) throws IOException {
+ }
+
+ public void doFilter(ChangeMergedEvent hook) throws IOException {
+ }
+
+ public void doFilter(ChangeAbandonedEvent changeAbandonedHook)
+ throws IOException {
+ }
+
+ public void doFilter(ChangeRestoredEvent changeRestoredHook)
+ throws IOException {
+ }
+
+ public void doFilter(RefUpdatedEvent refUpdatedHook) throws IOException {
+ }
+
+ @Override
+ public void onChangeEvent(ChangeEvent event) {
+ try {
+ if (event instanceof PatchSetCreatedEvent) {
+ doFilter((PatchSetCreatedEvent) event);
+ } else if (event instanceof CommentAddedEvent) {
+ doFilter((CommentAddedEvent) event);
+ } else if (event instanceof ChangeMergedEvent) {
+ doFilter((ChangeMergedEvent) event);
+ } else if (event instanceof ChangeAbandonedEvent) {
+ doFilter((ChangeAbandonedEvent) event);
+ } else if (event instanceof ChangeRestoredEvent) {
+ doFilter((ChangeRestoredEvent) event);
+ } else if (event instanceof RefUpdatedEvent) {
+ doFilter((RefUpdatedEvent) event);
+ } else {
+ log.info("Event " + event + " not recognised and ignored");
+ }
+ } catch (Throwable e) {
+ log.error("Event " + e + " processing failed", e);
+ }
+ }
+
+ public String getUrl(PatchSetCreatedEvent hook) {
+ return null;
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddComment.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddComment.java
new file mode 100644
index 0000000..0c8be83
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddComment.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.workflow;
+
+import java.io.IOException;
+
+import com.google.gerrit.server.events.AccountAttribute;
+import com.google.gerrit.server.events.ApprovalAttribute;
+import com.google.gerrit.server.events.ChangeAbandonedEvent;
+import com.google.gerrit.server.events.ChangeAttribute;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.ChangeRestoredEvent;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+
+public class GerritHookFilterAddComment extends GerritHookFilter {
+
+ @Inject
+ private ItsFacade its;
+
+ @Override
+ public void doFilter(CommentAddedEvent hook) throws IOException {
+ String comment = getComment(hook);
+ addComment(hook.change, comment);
+ }
+
+ @Override
+ public void doFilter(ChangeMergedEvent hook) throws IOException {
+ String comment = getComment(hook);
+ addComment(hook.change, comment);
+ }
+
+ @Override
+ public void doFilter(ChangeAbandonedEvent hook) throws IOException {
+ String comment = getComment(hook);
+ addComment(hook.change, comment);
+ }
+
+ @Override
+ public void doFilter(ChangeRestoredEvent hook) throws IOException {
+ String comment = getComment(hook);
+ addComment(hook.change, comment);
+ }
+
+ private String getCommentPrefix(ChangeAttribute change) {
+ return getChangeIdUrl(change) + " | ";
+ }
+
+ private String getComment(ChangeAttribute change, ChangeEvent hook, AccountAttribute who, String what) {
+ return getCommentPrefix(change) + "change " + what + " [by " + who + "]";
+ }
+
+ private String getComment(ChangeRestoredEvent hook) {
+ return getComment(hook.change, hook, hook.restorer, "RESTORED");
+ }
+
+ private String getComment(ChangeAbandonedEvent hook) {
+ return getComment(hook.change, hook, hook.abandoner, "ABANDONED");
+ }
+
+ private String getComment(ChangeMergedEvent hook) {
+ return getComment(hook.change, hook, hook.submitter, "APPROVED and MERGED");
+ }
+
+ private String getChangeIdUrl(ChangeAttribute change) {
+ final String url = change.url;
+ String changeId = change.id;
+ return its.createLinkForWebui(url, "Gerrit Change " + changeId);
+ }
+
+ private String getComment(CommentAddedEvent commentAdded) {
+ StringBuilder comment = new StringBuilder(getCommentPrefix(commentAdded.change));
+
+ if (commentAdded.approvals.length > 0) {
+ comment.append("Code-Review: ");
+ for (ApprovalAttribute approval : commentAdded.approvals) {
+ String value = getApprovalValue(approval);
+ if (value != null) {
+ comment.append(getApprovalType(approval) + ":" + value + " ");
+ }
+ }
+ }
+
+ comment.append(commentAdded.comment + " ");
+ comment.append("[by " + commentAdded.author + "]");
+ return comment.toString();
+ }
+
+ private String getApprovalValue(ApprovalAttribute approval) {
+ if (approval.value.equals("0")) {
+ return null;
+ }
+
+ if (approval.value.charAt(0) != '-') {
+ return "+" + approval.value;
+ } else {
+ return approval.value;
+ }
+ }
+
+ private String getApprovalType(ApprovalAttribute approval) {
+ if (approval.type.equalsIgnoreCase("CRVW")) {
+ return "Reviewed";
+ } else if (approval.type.equalsIgnoreCase("VRIF")) {
+ return "Verified";
+ } else
+ return approval.type;
+ }
+
+ private void addComment(ChangeAttribute change, String comment)
+ throws IOException {
+ String gitComment = change.subject;;
+ String[] issues = getIssueIds(gitComment);
+
+ for (String issue : issues) {
+ its.addComment(issue, comment);
+ }
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java
new file mode 100644
index 0000000..fe83bc2
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.workflow;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+
+public class GerritHookFilterAddRelatedLinkToChangeId extends
+ GerritHookFilter {
+
+ Logger log = LoggerFactory
+ .getLogger(GerritHookFilterAddRelatedLinkToChangeId.class);
+
+ @Inject
+ private ItsFacade its;
+
+ @Override
+ public void doFilter(PatchSetCreatedEvent patchsetCreated) throws IOException {
+
+ String gitComment =
+ getComment(patchsetCreated.change.project,
+ patchsetCreated.patchSet.revision);
+ String[] issues = getIssueIds(gitComment);
+
+ for (String issue : issues) {
+ its.addRelatedLink(issue, new URL(patchsetCreated.change.url),
+ "Gerrit Patch-Set: " + patchsetCreated.change.id + "/"
+ + patchsetCreated.patchSet.number);
+ }
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToGitWeb.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToGitWeb.java
new file mode 100644
index 0000000..17a1759
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToGitWeb.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.workflow;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.common.data.GitWebType;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.httpd.GitWebConfig;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+
+public class GerritHookFilterAddRelatedLinkToGitWeb extends GerritHookFilter {
+
+ Logger log = LoggerFactory
+ .getLogger(GerritHookFilterAddRelatedLinkToGitWeb.class);
+
+ @Inject
+ @GerritServerConfig
+ private Config gerritConfig;
+
+ @Inject
+ private ItsFacade its;
+
+ @Inject
+ private GitWebConfig gitWebConfig;
+
+
+ @Override
+ public void doFilter(RefUpdatedEvent hook) throws IOException {
+
+ String gitComment = getComment(hook.refUpdate.project, hook.refUpdate.newRev);
+ log.debug("Git commit " + hook.refUpdate.newRev + ": " + gitComment);
+
+ URL gitUrl = getGitUrl(hook);
+ String[] issues = getIssueIds(gitComment);
+
+ for (String issue : issues) {
+ log.debug("Adding GitWeb URL " + gitUrl + " to issue " + issue);
+
+ its.addRelatedLink(issue, gitUrl, "Git: "
+ + hook.refUpdate.newRev);
+ }
+ }
+
+
+ private URL getGitUrl(RefUpdatedEvent hook) throws MalformedURLException,
+ UnsupportedEncodingException {
+ String gerritCanonicalUrl =
+ gerritConfig.getString("gerrit", null, "canonicalWebUrl");
+ if(!gerritCanonicalUrl.endsWith("/")) {
+ gerritCanonicalUrl += "/";
+ }
+
+ String gitWebUrl = gitWebConfig.getUrl();
+ if (!gitWebUrl.startsWith("http")) {
+ gitWebUrl = gerritCanonicalUrl + gitWebUrl;
+ }
+
+ GitWebType gitWebType = gitWebConfig.getGitWebType();
+ String revUrl = gitWebType.getRevision();
+
+ ParameterizedString pattern = new ParameterizedString(revUrl);
+ final Map<String, String> p = new HashMap<String, String>();
+ p.put("project", URLEncoder.encode(
+ gitWebType.replacePathSeparator(hook.refUpdate.project), "US-ASCII"));
+ p.put("commit", hook.refUpdate.newRev);
+ return new URL(gitWebUrl + pattern.replace(p));
+ }
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterChangeState.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterChangeState.java
new file mode 100644
index 0000000..8fc99ef
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterChangeState.java
@@ -0,0 +1,262 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.workflow;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.events.ApprovalAttribute;
+import com.google.gerrit.server.events.ChangeAbandonedEvent;
+import com.google.gerrit.server.events.ChangeAttribute;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.ChangeRestoredEvent;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.hooks.its.InvalidTransitionException;
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+
+public class GerritHookFilterChangeState extends GerritHookFilter {
+ private static final Logger log = LoggerFactory
+ .getLogger(GerritHookFilterChangeState.class);
+
+ @Inject
+ private ItsFacade its;
+
+ @Inject
+ @SitePath
+ private File sitePath;
+
+ @Override
+ public void doFilter(PatchSetCreatedEvent hook) throws IOException {
+ performAction(hook.change, new Condition("change", "created"));
+ }
+
+ @Override
+ public void doFilter(CommentAddedEvent hook) throws IOException {
+ try {
+ List<Condition> conditions = new ArrayList<Condition>();
+ conditions.add(new Condition("change", "commented"));
+
+ for (ApprovalAttribute approval : hook.approvals) {
+ addApprovalCategoryCondition(conditions, approval.type, approval.value);
+ };
+
+ performAction(hook.change,
+ conditions.toArray(new Condition[conditions.size()]));
+ } catch (InvalidTransitionException ex) {
+ log.warn(ex.getMessage());
+ }
+ }
+
+ @Override
+ public void doFilter(ChangeMergedEvent hook) throws IOException {
+ performAction(hook.change, new Condition("change", "merged"));
+ }
+
+ @Override
+ public void doFilter(ChangeAbandonedEvent hook) throws IOException {
+ performAction(hook.change, new Condition("change", "abandoned"));
+ }
+
+ @Override
+ public void doFilter(ChangeRestoredEvent hook) throws IOException {
+ performAction(hook.change, new Condition("change", "restored"));
+ }
+
+ private void addApprovalCategoryCondition(List<Condition> conditions,
+ String name, String value) {
+ value = toConditionValue(value);
+ if (value == null) return;
+
+ conditions.add(new Condition(name, value));
+ }
+
+ private String toConditionValue(String text) {
+ if (text == null) return null;
+
+ try {
+ int val = Integer.parseInt(text);
+ if (val > 0)
+ return "+" + val;
+ else
+ return text;
+ } catch (Exception any) {
+ return null;
+ }
+ }
+
+ private void performAction(ChangeAttribute change, Condition... conditionArgs)
+ throws IOException {
+
+ List<Condition> conditions = Arrays.asList(conditionArgs);
+
+ log.debug("Checking suitable transition for: " + conditions);
+
+ Transition transition = null;
+ List<Transition> transitions = loadTransitions();
+ for (Transition tx : transitions) {
+
+ log.debug("Checking transition: " + tx);
+ if (tx.matches(conditions)) {
+ log.debug("Transition FOUND > " + tx.getAction());
+ transition = tx;
+ break;
+ }
+ }
+
+ if (transition == null) {
+ log.debug("Nothing to perform, transition not found for conditions "
+ + conditions);
+ return;
+ }
+
+ String gitComment = change.subject;
+ String[] issues = getIssueIds(gitComment);
+
+ for (String issue : issues) {
+ its.performAction(issue, transition.getAction());
+ }
+ }
+
+ private List<Transition> loadTransitions() {
+ File configFile = new File(sitePath, "etc/issue-state-transition.config");
+ FileBasedConfig cfg = new FileBasedConfig(configFile, FS.DETECTED);
+ try {
+ cfg.load();
+ } catch (IOException e) {
+ log.error("Cannot load transitions configuration file " + cfg, e);
+ return Collections.emptyList();
+ } catch (ConfigInvalidException e) {
+ log.error("Invalid transitions configuration file" + cfg, e);
+ return Collections.emptyList();
+ }
+
+ List<Transition> transitions = new ArrayList<Transition>();
+ Set<String> sections = cfg.getSubsections("action");
+ for (String section : sections) {
+ List<Condition> conditions = new ArrayList<Condition>();
+ Set<String> keys = cfg.getNames("action", section);
+ for (String key : keys) {
+ String val = cfg.getString("action", section, key);
+ conditions.add(new Condition(key.trim(), val.trim().split(",")));
+ }
+ transitions.add(new Transition(toAction(section), conditions));
+ }
+ return transitions;
+ }
+
+ private String toAction(String name) {
+ name = name.trim();
+ try {
+ int i = name.lastIndexOf(' ');
+ Integer.parseInt(name.substring(i + 1));
+ name = name.substring(0, i);
+ } catch (Exception ignore) {
+ }
+ return name;
+ }
+
+ public class Condition {
+ private String key;
+ private String[] val;
+
+ public Condition(String key, String[] values) {
+ super();
+ this.key = key;
+ this.val = values;
+ }
+
+ public Condition(String key, String value) {
+ this(key, new String[] {value});
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String[] getVal() {
+ return val;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ try {
+ Condition other = (Condition) o;
+ if (!(key.equals(other.key))) return false;
+
+ boolean valMatch = false;
+ List<String> otherVals = Arrays.asList(other.val);
+ for (String value : val) {
+ if (otherVals.contains(value)) valMatch = true;
+ }
+
+ return valMatch;
+ } catch (Exception any) {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return key + "=" + Arrays.asList(val);
+ }
+ }
+
+ public class Transition {
+ private String action;
+ private List<Condition> conditions;
+
+ public Transition(String action, List<Condition> conditions) {
+ super();
+ this.action = action;
+ this.conditions = conditions;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public List<Condition> getCondition() {
+ return conditions;
+ }
+
+ public boolean matches(List<Condition> eventConditions) {
+
+ for (Condition condition : conditions) {
+ if (!eventConditions.contains(condition)) return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "action=\"" + action + "\", conditions=" + conditions;
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..eb0d14f
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,268 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2008 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>com.googlesource.gerrit.plugins.its</groupId>
+ <artifactId>hooks-its-parent</artifactId>
+ <packaging>pom</packaging>
+ <version>2.6-SNAPSHOT</version>
+
+ <name>Gerrit Code Review - Issue tracker support</name>
+
+ <modules>
+ <module>hooks-its</module>
+ </modules>
+
+ <licenses>
+ <license>
+ <name>Apache License, 2.0</name>
+ <comments>
+ 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.
+ </comments>
+ </license>
+ </licenses>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ <encoding>UTF-8</encoding>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>1.6</version>
+ </plugin>
+ </plugins>
+ </build>
+
+ <repositories>
+ <repository>
+ <id>gerrit-maven</id>
+ <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
+ </repository>
+ </repositories>
+</project>