Initial commit of maintainer plugin
Change-Id: If479ac289e4386f72a274c39bb8423a4d6e99df7
Signed-off-by: Jan Srnicek <jsrnicek@cisco.com>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6143e53
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..07b4548
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <annotationProcessing>
+ <profile name="Maven default annotation processors profile" enabled="true">
+ <sourceOutputDir name="target/generated-sources/annotations" />
+ <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
+ <outputRelativeToContentRoot value="true" />
+ <module name="maintainer-plugin" />
+ </profile>
+ </annotationProcessing>
+ <bytecodeTargetLevel>
+ <module name="maintainer-plugin" target="1.8" />
+ </bytecodeTargetLevel>
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/copyright/Cisco.xml b/.idea/copyright/Cisco.xml
new file mode 100644
index 0000000..377bbcb
--- /dev/null
+++ b/.idea/copyright/Cisco.xml
@@ -0,0 +1,6 @@
+<component name="CopyrightManager">
+ <copyright>
+ <option name="myName" value="Cisco" />
+ <option name="notice" value="Copyright (c) 2017 Cisco and/or its affiliates. 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." />
+ </copyright>
+</component>
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..1c24f9a
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="KotlinCommonCompilerArguments">
+ <option name="languageVersion" value="1.1" />
+ <option name="apiVersion" value="1.1" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/libraries/Maven__com_google_gerrit_gerrit_acceptance_framework_2_14.xml b/.idea/libraries/Maven__com_google_gerrit_gerrit_acceptance_framework_2_14.xml
new file mode 100644
index 0000000..b501556
--- /dev/null
+++ b/.idea/libraries/Maven__com_google_gerrit_gerrit_acceptance_framework_2_14.xml
@@ -0,0 +1,13 @@
+<component name="libraryTable">
+ <library name="Maven: com.google.gerrit:gerrit-acceptance-framework:2.14">
+ <CLASSES>
+ <root url="jar://$MAVEN_REPOSITORY$/com/google/gerrit/gerrit-acceptance-framework/2.14/gerrit-acceptance-framework-2.14.jar!/" />
+ </CLASSES>
+ <JAVADOC>
+ <root url="jar://$MAVEN_REPOSITORY$/com/google/gerrit/gerrit-acceptance-framework/2.14/gerrit-acceptance-framework-2.14-javadoc.jar!/" />
+ </JAVADOC>
+ <SOURCES>
+ <root url="jar://$MAVEN_REPOSITORY$/com/google/gerrit/gerrit-acceptance-framework/2.14/gerrit-acceptance-framework-2.14-sources.jar!/" />
+ </SOURCES>
+ </library>
+</component>
\ No newline at end of file
diff --git a/.idea/libraries/Maven__com_google_gerrit_gerrit_plugin_api_2_14.xml b/.idea/libraries/Maven__com_google_gerrit_gerrit_plugin_api_2_14.xml
new file mode 100644
index 0000000..5e81b30
--- /dev/null
+++ b/.idea/libraries/Maven__com_google_gerrit_gerrit_plugin_api_2_14.xml
@@ -0,0 +1,13 @@
+<component name="libraryTable">
+ <library name="Maven: com.google.gerrit:gerrit-plugin-api:2.14">
+ <CLASSES>
+ <root url="jar://$MAVEN_REPOSITORY$/com/google/gerrit/gerrit-plugin-api/2.14/gerrit-plugin-api-2.14.jar!/" />
+ </CLASSES>
+ <JAVADOC>
+ <root url="jar://$MAVEN_REPOSITORY$/com/google/gerrit/gerrit-plugin-api/2.14/gerrit-plugin-api-2.14-javadoc.jar!/" />
+ </JAVADOC>
+ <SOURCES>
+ <root url="jar://$MAVEN_REPOSITORY$/com/google/gerrit/gerrit-plugin-api/2.14/gerrit-plugin-api-2.14-sources.jar!/" />
+ </SOURCES>
+ </library>
+</component>
\ No newline at end of file
diff --git a/.idea/libraries/Maven__junit_junit_4_11.xml b/.idea/libraries/Maven__junit_junit_4_11.xml
new file mode 100644
index 0000000..f33320d
--- /dev/null
+++ b/.idea/libraries/Maven__junit_junit_4_11.xml
@@ -0,0 +1,13 @@
+<component name="libraryTable">
+ <library name="Maven: junit:junit:4.11">
+ <CLASSES>
+ <root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.11/junit-4.11.jar!/" />
+ </CLASSES>
+ <JAVADOC>
+ <root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.11/junit-4.11-javadoc.jar!/" />
+ </JAVADOC>
+ <SOURCES>
+ <root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.11/junit-4.11-sources.jar!/" />
+ </SOURCES>
+ </library>
+</component>
\ No newline at end of file
diff --git a/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml b/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml
new file mode 100644
index 0000000..f58bbc1
--- /dev/null
+++ b/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml
@@ -0,0 +1,13 @@
+<component name="libraryTable">
+ <library name="Maven: org.hamcrest:hamcrest-core:1.3">
+ <CLASSES>
+ <root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar!/" />
+ </CLASSES>
+ <JAVADOC>
+ <root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3-javadoc.jar!/" />
+ </JAVADOC>
+ <SOURCES>
+ <root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3-sources.jar!/" />
+ </SOURCES>
+ </library>
+</component>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..97947a5
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="MavenProjectsManager">
+ <option name="originalFiles">
+ <list>
+ <option value="$PROJECT_DIR$/pom.xml" />
+ </list>
+ </option>
+ </component>
+ <component name="ProjectRootManager" version="2" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/out" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..3df72c9
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/maintainer-plugin.iml" filepath="$PROJECT_DIR$/maintainer-plugin.iml" />
+ </modules>
+ </component>
+</project>
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /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/README.md b/README.md
new file mode 100644
index 0000000..b328169
--- /dev/null
+++ b/README.md
@@ -0,0 +1,23 @@
+# maintainer-plugin
+Complex reviewer management for gerrit
+
+Basic plugin features
+
+- add reviewers based on component matching by maintainers file
+- add +2 review if all components has been +1 by respecive maintainers
+- auto submit
+- add warnings if file review changes component of affected file
+- complex info about which file falls under which component and
+ which files has not been matched
+
+Configuration
+ Uses standard gerrit plugin configuration like so[maintainer.config]
+
+ [branch "refs/heads/*"]
+ - pluginuser = maintainer-plugin - user on whos behalf plugin actions will be done
+ - maintainerfileref = master/HEAD - reference for maintainer file that should be used
+ - maintainerfile = MAINTAINER - absolute path within repo where maintainer file is stored
+ - autoaddreviewers = true - if true, automaticaly matchses pachset files under their component based of maintainers file configuration
+ - allowmaintainersubmit = true - if true, automaticaly post +2 on patch after all respective component maintainers have added +1
+ - autosubmit = true - if true, after previous step automaticaly submits patch
+
diff --git a/maintainer-plugin.iml b/maintainer-plugin.iml
new file mode 100644
index 0000000..c0be6ae
--- /dev/null
+++ b/maintainer-plugin.iml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+ <output url="file://$MODULE_DIR$/target/classes" />
+ <output-test url="file://$MODULE_DIR$/target/test-classes" />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
+ <excludeFolder url="file://$MODULE_DIR$/target" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.gerrit:gerrit-plugin-api:2.14" level="project" />
+ <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.11" level="project" />
+ <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
+ <orderEntry type="library" scope="TEST" name="Maven: com.google.gerrit:gerrit-acceptance-framework:2.14" level="project" />
+ </component>
+</module>
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..7f03f63
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>io.fd.gerrit</groupId>
+ <artifactId>maintainer-plugin</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+ <properties>
+ <Gerrit-ApiType>plugin</Gerrit-ApiType>
+ <Gerrit-ApiVersion>2.14</Gerrit-ApiVersion>
+ <GWT-Version>2.8.0</GWT-Version>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.4</version>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Gerrit-PluginName>maintainer-plugin</Gerrit-PluginName>
+ <Gerrit-Module>io.fd.maintainer.plugin.MaintainerPluginModule</Gerrit-Module>
+ <Gerrit-ReloadMode>restart</Gerrit-ReloadMode>
+
+ <Implementation-Vendor>Cisco and/or its affiliates</Implementation-Vendor>
+ <Implementation-URL>https://wiki.fd.io/view/Maintainer</Implementation-URL>
+
+ <Implementation-Title>Gerrit review plugin</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+
+ <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+ <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.8</source>
+ <target>1.8</target>
+ <encoding>UTF-8</encoding>
+ </configuration>
+ </plugin>
+ </plugins>
+
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
+ <version>${Gerrit-ApiVersion}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.11</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-acceptance-framework</artifactId>
+ <version>${Gerrit-ApiVersion}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
diff --git a/src/main/java/io/fd/maintainer/plugin/MaintainerPluginModule.java b/src/main/java/io/fd/maintainer/plugin/MaintainerPluginModule.java
new file mode 100644
index 0000000..e179088
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/MaintainerPluginModule.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin;
+
+import com.google.gerrit.common.EventListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.AbstractModule;
+import io.fd.maintainer.plugin.events.OnCommittersToBeAddedListener;
+import io.fd.maintainer.plugin.events.OnPatchsetVerifiedListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MaintainerPluginModule extends AbstractModule {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MaintainerPluginModule.class);
+
+ @Override
+ protected void configure() {
+ LOG.info("Configuring ComponentInfo plugin module");
+ DynamicSet.bind(binder(), EventListener.class).to(OnCommittersToBeAddedListener.class);
+ DynamicSet.bind(binder(), EventListener.class).to(OnPatchsetVerifiedListener.class);
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/events/OnCommittersToBeAddedListener.java b/src/main/java/io/fd/maintainer/plugin/events/OnCommittersToBeAddedListener.java
new file mode 100644
index 0000000..d0a2e90
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/events/OnCommittersToBeAddedListener.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.events;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import io.fd.maintainer.plugin.parser.ComponentPath;
+import io.fd.maintainer.plugin.service.MaintainersProvider;
+import io.fd.maintainer.plugin.service.SettingsProvider;
+import io.fd.maintainer.plugin.service.dto.PluginBranchSpecificSettings;
+import io.fd.maintainer.plugin.service.push.ReviewerPusher;
+import io.fd.maintainer.plugin.service.push.WarningPusher;
+import io.fd.maintainer.plugin.util.CommonTasks;
+import io.fd.maintainer.plugin.util.MaintainersIndex;
+import io.fd.maintainer.plugin.util.WarningGenerator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.parboiled.common.Tuple2;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Filters out events that should cause assignment of committers to patch
+ */
+public class OnCommittersToBeAddedListener extends SelfDescribingEventListener implements CommonTasks {
+
+ private static final Logger LOG = LoggerFactory.getLogger(OnCommittersToBeAddedListener.class);
+
+ @Inject
+ private SchemaFactory<ReviewDb> schemaFactory;
+
+ @Inject
+ private PatchListCache patchListCache;
+
+ @Inject
+ private MaintainersProvider maintainersProvider;
+
+ @Inject
+ private ChangesCollection changes;
+
+ @Inject
+ private Revisions revisions;
+
+ @Inject
+ private Provider<PostReviewers> reviewersProvider;
+
+ @Inject
+ private Provider<PostReview> reviewProvider;
+
+ @Inject
+ private SettingsProvider settingsProvider;
+
+ @Inject
+ private ReviewerPusher reviewerPusher;
+
+ @Inject
+ private WarningPusher warningPusher;
+
+ OnCommittersToBeAddedListener() {
+
+ }
+
+ @Override
+ protected void consumeDescribedEvent(final Event event) {
+ final PatchSetCreatedEvent patchSetCreatedEvent = PatchSetCreatedEvent.class.cast(event);
+
+ final ChangeAttribute changeAttributes = patchSetCreatedEvent.change.get();
+ final PluginBranchSpecificSettings settings =
+ settingsProvider.getBranchSpecificSettings(changeAttributes.branch);
+
+ if (!settings.isAutoAddReviewers()) {
+ LOG.warn("Auto add reviewers option turned off");
+ return;
+ }
+
+ try (final ReviewDb reviewDb = schemaFactory.open()) {
+ final Change.Id changeId = new Change.Id(changeAttributes.number);
+ final Change change = reviewDb.changes().get(changeId);
+
+ final PatchSet mostCurrentPatchSet = reviewDb.patchSets().get(change.currentPatchSetId());
+
+ LOG.info("Processing change {} | patchset {}", change.getId(), mostCurrentPatchSet.getId());
+ final MaintainersIndex index =
+ new MaintainersIndex(
+ maintainersProvider.getMaintainersInfo(changeAttributes.branch, changeAttributes.number));
+
+ reviewerPusher.addRelevantReviewers(index, change, mostCurrentPatchSet, settings.getPluginUserName());
+ LOG.info("Reviewers for change {} successfully added", change.getId());
+
+ final PatchList patchList = getPatchList(patchListCache, change, mostCurrentPatchSet);
+ final List<PatchListEntry> patches = getRelevantPatchListEntries(patchList);
+
+ final Map<PatchListEntry, Tuple2<Set<ComponentPath>, Set<ComponentPath>>> renamedEntryToComponentsIndex =
+ renamedEntriesToComponentIndex(index, patches);
+
+ final Set<WarningGenerator.ComponentChangeWarning> warnings =
+ generateComponentChangeWarnings(index, renamedEntryToComponentsIndex);
+ warningPusher.sendWarnings(warnings, change, mostCurrentPatchSet, settings.getPluginUserName());
+ LOG.info("Warnings for change {} successfully added", change.getId());
+ } catch (OrmException e) {
+ throw new IllegalStateException("Unable to open review DB", e);
+ }
+ LOG.info("Change {} successfully processed", patchSetCreatedEvent.changeKey);
+ }
+
+ @Override
+ protected boolean canConsume(final Event event) {
+ return event instanceof PatchSetCreatedEvent;
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/events/OnPatchsetVerifiedListener.java b/src/main/java/io/fd/maintainer/plugin/events/OnPatchsetVerifiedListener.java
new file mode 100644
index 0000000..b430e43
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/events/OnPatchsetVerifiedListener.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.events;
+
+import static io.fd.maintainer.plugin.service.PatchsetReviewInfo.ReviewState.ALL_COMPONENTS_REVIEWED;
+import static java.lang.String.format;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import io.fd.maintainer.plugin.service.MaintainersProvider;
+import io.fd.maintainer.plugin.service.PatchsetReviewInfo;
+import io.fd.maintainer.plugin.service.SettingsProvider;
+import io.fd.maintainer.plugin.service.dto.PluginBranchSpecificSettings;
+import io.fd.maintainer.plugin.service.push.ApprovalPusher;
+import io.fd.maintainer.plugin.service.push.SubmitPusher;
+import io.fd.maintainer.plugin.util.MaintainersIndex;
+import io.fd.maintainer.plugin.util.PatchListProcessing;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OnPatchsetVerifiedListener extends SelfDescribingEventListener implements PatchListProcessing {
+
+ private static final Logger LOG = LoggerFactory.getLogger(OnPatchsetVerifiedListener.class);
+
+ @Inject
+ private SchemaFactory<ReviewDb> schemaFactory;
+
+ @Inject
+ private MaintainersProvider maintainersProvider;
+
+ @Inject
+ private PatchListCache patchListCache;
+
+ @Inject
+ private ChangesCollection changes;
+
+ @Inject
+ private SettingsProvider settingsProvider;
+
+ @Inject
+ private ApprovalPusher approvalPusher;
+
+ @Inject
+ private SubmitPusher submitPusher;
+
+ private static String formatUser(final AccountAttribute author) {
+ return format("%s(%s)<%s>", author.name, author.username, author.email);
+ }
+
+ @Override
+ protected void consumeDescribedEvent(final Event event) {
+ CommentAddedEvent commentAddedEvent = CommentAddedEvent.class.cast(event);
+
+ final PluginBranchSpecificSettings settings =
+ settingsProvider.getBranchSpecificSettings(commentAddedEvent.change.get().branch);
+
+ if (!settings.isAllowMaintainersSubmit()) {
+ LOG.warn("Maintainers submit is turned off");
+ return;
+ }
+
+ final Optional<ApprovalAttribute> patchSetVerification = getPatchListVerifications(commentAddedEvent);
+
+ // patchset has been +1
+ if (patchSetVerification.isPresent()) {
+ LOG.info("User {} just verified change {}", formatUser(commentAddedEvent.author.get()),
+ commentAddedEvent.changeKey.get());
+
+ try (final ReviewDb reviewDb = schemaFactory.open()) {
+ final int changeNumber = commentAddedEvent.change.get().number;
+ final Change.Id changeId = new Change.Id(changeNumber);
+ final Change change = reviewDb.changes().get(changeId);
+
+ final PatchSet currentPatchset = reviewDb.patchSets().get(change.currentPatchSetId());
+ final PatchSet.Id currentPatchsetId = currentPatchset.getId();
+
+ final int currentPatchsetNr = currentPatchset.getPatchSetId();
+ final int processedPatchsetNr = commentAddedEvent.patchSet.get().number;
+
+ // to filter out reviews on older patchsets
+ if (currentPatchsetNr != processedPatchsetNr) {
+ LOG.warn("Event for older patchset {}, most current {}, ignoring", processedPatchsetNr,
+ currentPatchsetNr);
+ } else {
+ final List<PatchSetApproval> currentPatchsetVerifications = getPatchListCurrentVerifications(
+ reviewDb.patchSetApprovals().byChange(changeId).toList(),
+ currentPatchsetId);
+
+ if (currentPatchsetVerifications.isEmpty()) {
+ LOG.warn("No verifications found for patchset {}", currentPatchset.getId());
+ } else {
+ LOG.info("Building maintainers index for patchset {}", currentPatchset.getId());
+ final MaintainersIndex maintainersIndex =
+ new MaintainersIndex(maintainersProvider
+ .getMaintainersInfo(commentAddedEvent.getBranchNameKey().get(),
+ changeNumber));
+
+ LOG.info("Getting current patch list for patchset {}", currentPatchset.getId());
+ final PatchList patchList = getPatchList(patchListCache, change, currentPatchset);
+
+ LOG.info("Getting current reviewers for patchset {}", currentPatchset.getId());
+ final HashSet<Account> currentVerificators =
+ new HashSet<>(reviewDb.accounts().get(currentPatchsetVerifications
+ .stream()
+ .map(PatchSetApproval::getAccountId)
+ .collect(Collectors.toSet())).toList());
+
+ LOG.info("Getting patch review info for patchset {}", currentPatchset.getId());
+ // Note that you only need one MAINTAINER per component.
+ // Also note a single reviewer may be a MAINTAINER for multiple components
+ final PatchsetReviewInfo patchsetReviewInfo =
+ new PatchsetReviewInfo(maintainersIndex, patchList, currentVerificators);
+
+ if (patchsetReviewInfo.getReviewState() == ALL_COMPONENTS_REVIEWED) {
+ LOG.info("All relevant component reviewers verified patchset {}", currentPatchset.getId());
+ approvalPusher.approvePatchset(change, currentPatchset, settings.getPluginUserName());
+
+ if (settings.isAutoSubmit()) {
+ LOG.info("Submitting change {}", change.getId());
+ submitPusher.submitPatch(change, settings.getPluginUserName());
+ } else {
+ LOG.warn("Auto submit turned off");
+ }
+ } else {
+ LOG.info(
+ "Patchset {} does not have verifications from following components yet : {}",
+ currentPatchset.getId(), patchsetReviewInfo.getMissingComponentReview());
+ }
+ }
+ }
+ } catch (OrmException e) {
+ LOG.error("Error accessing review DB", e);
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ @Override
+ protected boolean canConsume(final Event event) {
+ return event instanceof CommentAddedEvent;
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/events/SelfDescribingEventListener.java b/src/main/java/io/fd/maintainer/plugin/events/SelfDescribingEventListener.java
new file mode 100644
index 0000000..46dd0a5
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/events/SelfDescribingEventListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.events;
+
+import com.google.gerrit.common.EventListener;
+import com.google.gerrit.server.events.Event;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Event listener that automatically describes event that it received
+ */
+public abstract class SelfDescribingEventListener implements EventListener {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SelfDescribingEventListener.class);
+
+ @Override
+ public void onEvent(final Event event) {
+ if (canConsume(event)) {
+ LOG.info("Event[type={},created={}] has been triggered, consuming ...", event.getType(),
+ event.eventCreatedOn);
+ consumeDescribedEvent(event);
+ LOG.info("Event[type={},created={}] successfully processed", event.getType(), event.eventCreatedOn);
+ }
+ }
+
+ /**
+ * Consumes event that has been already described
+ */
+ protected abstract void consumeDescribedEvent(final Event event);
+
+ /**
+ * Returns true if listener can consume following type of event
+ */
+ protected abstract boolean canConsume(final Event event);
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/parser/ComponentInfo.java b/src/main/java/io/fd/maintainer/plugin/parser/ComponentInfo.java
new file mode 100644
index 0000000..218eaff
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/parser/ComponentInfo.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.parser;
+
+import static java.util.Collections.emptySet;
+
+import java.util.Set;
+
+public final class ComponentInfo {
+
+ private final String title;
+ private final Set<String> comments;
+ private final Set<Maintainer> maintainers;
+ private final Set<ComponentPath> paths;
+
+ private ComponentInfo(final String title, final Set<String> contactEmails, final Set<Maintainer> maintainers,
+ final Set<ComponentPath> paths) {
+ this.title = title;
+ this.comments = contactEmails == null
+ ? emptySet()
+ : contactEmails;
+ this.maintainers = maintainers == null
+ ? emptySet()
+ : maintainers;
+ this.paths = paths == null
+ ? emptySet()
+ : paths;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public Set<String> getComments() {
+ return comments;
+ }
+
+ public Set<Maintainer> getMaintainers() {
+ return maintainers;
+ }
+
+ public Set<ComponentPath> getPaths() {
+ return paths;
+ }
+
+ public static class ComponentInfoBuilder {
+ private String title;
+ private Set<String> contactEmails;
+ private Set<Maintainer> maintainers;
+ private Set<ComponentPath> paths;
+
+ public ComponentInfoBuilder setTitle(final String title) {
+ this.title = title;
+ return this;
+ }
+
+ public ComponentInfoBuilder setComments(final Set<String> contactEmails) {
+ this.contactEmails = contactEmails;
+ return this;
+ }
+
+ public ComponentInfoBuilder setMaintainers(final Set<Maintainer> maintainers) {
+ this.maintainers = maintainers;
+ return this;
+ }
+
+ public ComponentInfoBuilder setPaths(final Set<ComponentPath> paths) {
+ this.paths = paths;
+ return this;
+ }
+
+ public ComponentInfo createMaintainer() {
+ return new ComponentInfo(title, contactEmails, maintainers, paths);
+ }
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/parser/ComponentPath.java b/src/main/java/io/fd/maintainer/plugin/parser/ComponentPath.java
new file mode 100644
index 0000000..e39a4c5
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/parser/ComponentPath.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.parser;
+
+import static io.fd.maintainer.plugin.parser.ComponentPath.MatchLevel.FULL;
+import static io.fd.maintainer.plugin.parser.ComponentPath.MatchLevel.NONE;
+import static io.fd.maintainer.plugin.parser.ComponentPath.MatchLevel.PARTIAL;
+import static io.fd.maintainer.plugin.parser.ComponentPath.MatchLevel.WILDCARD_ONLY;
+import static io.fd.maintainer.plugin.parser.ComponentPath.MatchLevel.WILDCARD_WITH_EXTENSION;
+
+import java.util.Comparator;
+
+public class ComponentPath {
+
+ private final String path;
+
+ public ComponentPath(final String path) {
+ this.path = path;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public MatchLevel matchAgainst(final String path) {
+ // initial match level is NONE unless full match
+ if (path == null) {
+ return NONE;
+ }
+
+ MatchLevel matchLevel = this.path.equals(path)
+ ? FULL
+ : NONE;
+ // trims down wildcard
+
+ if (matchLevel == NONE) {
+ final int indexOfWildcard = this.path.indexOf("*");
+
+ if (-1 != indexOfWildcard) {
+ final String wildcardLess = this.path.replace(this.path.substring(indexOfWildcard), "");
+ if (path.contains(wildcardLess)) {
+ matchLevel = this.path.equals(wildcardLess)
+ // if path does not have wildcard, its partial match
+ ? PARTIAL
+ // if it has, by previous match its proven that it matches wildcard
+ : WILDCARD_ONLY;
+
+ final int extensionStart = this.path.lastIndexOf(".");
+ final String componentExtension = extension(extensionStart, this.path);
+
+ if (!componentExtension.isEmpty()) {
+ // meaning that index of last dot was found,therefore extension is present
+ if (-1 != extensionStart && extension(path).equals(componentExtension)) {
+ matchLevel = WILDCARD_WITH_EXTENSION;
+ } else {
+ // matches wildcard but not the extension
+ matchLevel = NONE;
+ }
+ }
+ }
+ } else {
+ // not a wildcard path ,therefore attempts match it as direct child
+ final String[] componentPathParts = this.path.split("/");
+ final String[] matchedPathParts = path.split("/");
+
+ matchLevel = matchedPathParts.length - componentPathParts.length > 1
+ ?
+ NONE
+ : matchPathsAsDirectChild(componentPathParts, matchedPathParts);
+ }
+ }
+ return matchLevel;
+ }
+
+ private MatchLevel matchPathsAsDirectChild(final String[] componentPath, final String[] matchedPath) {
+ for (int i = 0; i < componentPath.length; i++) {
+ if (componentPath[i].equals(matchedPath[i])) {
+ // dir equals,continue
+ continue;
+ }
+ // as soon as dir is not equal, return NONE
+ return NONE;
+ }
+ // everything has been matched, therefore partial
+ return PARTIAL;
+ }
+
+ private String extension(final int extensionStart, final String path) {
+ if (-1 == extensionStart) {
+ return "";
+ }
+ return path.substring(extensionStart + 1);
+ }
+
+ private String extension(final String path) {
+ return extension(path.lastIndexOf("."), path);
+ }
+
+ @Override
+ public String toString() {
+ return "ComponentPath{" +
+ "path='" + path + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ComponentPath that = (ComponentPath) o;
+
+ return path != null
+ ? path.equals(that.path)
+ : that.path == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return path != null
+ ? path.hashCode()
+ : 0;
+ }
+
+ public enum MatchLevel {
+ FULL(4), // full equality match
+ WILDCARD_WITH_EXTENSION(3),// matches wildcarded path with extension for ex.: foo/bar/*.mk
+ PARTIAL(2),// matches part of the path
+ WILDCARD_ONLY(1),// matches wildcarded path for ex.: foo/bar/*
+ NONE(0); // no match
+
+ public static final Comparator<MatchLevel> MAX = Comparator.comparingInt(MatchLevel::getValue);
+
+ private final int value;
+
+ private MatchLevel(final int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/parser/Maintainer.java b/src/main/java/io/fd/maintainer/plugin/parser/Maintainer.java
new file mode 100644
index 0000000..d97ff42
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/parser/Maintainer.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.parser;
+
+
+public class Maintainer {
+
+ private final String name;
+ private final String email;
+
+ public Maintainer(final String name, final String email) {
+ this.name = name;
+ this.email = email;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ @Override
+ public String toString() {
+ return "Maintainer{" +
+ "name='" + name + '\'' +
+ ", email='" + email + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final Maintainer that = (Maintainer) o;
+
+ if (name != null
+ ? !name.equals(that.name)
+ : that.name != null) {
+ return false;
+ }
+ return email != null
+ ? email.equals(that.email)
+ : that.email == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null
+ ? name.hashCode()
+ : 0;
+ result = 31 * result + (email != null
+ ? email.hashCode()
+ : 0);
+ return result;
+ }
+
+ public static class MaintainerBuilder {
+ private String name;
+ private String email;
+
+ public MaintainerBuilder setName(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public MaintainerBuilder setEmail(final String email) {
+ this.email = email;
+ return this;
+ }
+
+ public Maintainer createMaintainer() {
+ return new Maintainer(name, email);
+ }
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/parser/MaintainerMismatchException.java b/src/main/java/io/fd/maintainer/plugin/parser/MaintainerMismatchException.java
new file mode 100644
index 0000000..fea3520
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/parser/MaintainerMismatchException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.parser;
+
+/**
+ * Common exception for parsing mismatches
+ */
+public class MaintainerMismatchException extends Exception {
+
+ public MaintainerMismatchException(String cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/parser/MaintainersParser.java b/src/main/java/io/fd/maintainer/plugin/parser/MaintainersParser.java
new file mode 100644
index 0000000..7c172ab
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/parser/MaintainersParser.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.parser;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+
+
+public final class MaintainersParser {
+
+ private static final String HEADER_SPLITTER = "-----";
+ private static final String MAINTAINER_TOKEN = "M:";
+ private static final String FILEPATH_TOKEN = "F:";
+ private static final String COMMENT_TOKEN = "C:";
+ private static final String EMAIL_START_TOKEN = "<";
+ private static final String EMAIL_END_TOKEN = ">";
+
+ private static ComponentInfo parseBlock(final Set<String> blockLines) throws MaintainerMismatchException {
+ checkState(blockLines.size() >= 3, "Unable to parse block from %s", blockLines);
+
+ String title = null;
+ Set<Maintainer> maintainers = new HashSet<>();
+ Set<ComponentPath> paths = new HashSet<>();
+ Set<String> comments = new HashSet<>();
+
+ for (String line : blockLines) {
+ if (line.startsWith(MAINTAINER_TOKEN)) {
+ maintainers.add(new Maintainer.MaintainerBuilder()
+ .setName(extractMaintainer(line))
+ .setEmail(extractEmail(line))
+ .createMaintainer());
+ continue;
+ }
+
+ if (line.startsWith(FILEPATH_TOKEN)) {
+ paths.add(new ComponentPath(extractComponentPath(line)));
+ continue;
+ }
+
+ if (line.startsWith(COMMENT_TOKEN)) {
+ comments.add(line);
+ continue;
+ }
+
+ if (title != null) {
+ throw new MaintainerMismatchException(format("Multiple title specified for block %s", blockLines));
+ }
+
+ title = line.trim();
+ }
+
+ return new ComponentInfo.ComponentInfoBuilder()
+ .setTitle(title)
+ .setMaintainers(maintainers)
+ .setPaths(paths)
+ .setComments(comments)
+ .createMaintainer();
+ }
+
+ // raw input in format : M: Name Surname <example@example.com>
+ private static String extractMaintainer(final String rawString) {
+ return rawString.substring(0, rawString.indexOf(EMAIL_START_TOKEN)).replace(MAINTAINER_TOKEN, "").trim();
+ }
+
+ // raw input in format : .... <example@example.com>
+ private static String extractEmail(final String rawString) {
+ return rawString.substring(rawString.indexOf(EMAIL_START_TOKEN) + 1, rawString.indexOf(EMAIL_END_TOKEN)).trim();
+ }
+
+ // raw input in format : F: src/tools/perftool/
+ private static String extractComponentPath(final String rawString) {
+ return rawString.replace(FILEPATH_TOKEN, "").trim();
+ }
+
+ public List<ComponentInfo> parseMaintainers(@Nonnull final String rawContent) throws MaintainerMismatchException {
+ final List<String> lines =
+ Arrays.stream(rawContent.split(System.lineSeparator())).collect(Collectors.toList());
+ int lastHeaderLine;
+ for (lastHeaderLine = 0; lastHeaderLine < lines.size(); lastHeaderLine++) {
+ if (lines.get(lastHeaderLine).contains(HEADER_SPLITTER)) {
+ break;
+ }
+ }
+
+ List<String> headerLessLines = lines.stream().skip(lastHeaderLine + 1).collect(Collectors.toList());
+ final List<Set<String>> blocks = new LinkedList<>();
+
+ while (!headerLessLines.isEmpty()) {
+ final int nextBlockEnd = nextBlockEnd(headerLessLines);
+ final Set<String> nextBlock = headerLessLines.stream().limit(nextBlockEnd + 1).map(
+ String::trim).filter(line -> !line.isEmpty()).collect(Collectors.toSet());
+ blocks.add(nextBlock);
+ headerLessLines =
+ headerLessLines.stream().unordered().skip(nextBlockEnd + 1).collect(Collectors.toList());
+ }
+
+ List<ComponentInfo> componentInfos = new ArrayList<>();
+ for (Set<String> block : blocks) {
+ if (block.size() > 0) {
+ componentInfos.add(parseBlock(block));
+ }
+ }
+ return componentInfos;
+ }
+
+ private int nextBlockEnd(final List<String> lines) {
+
+ for (int end = 0; end < lines.size(); end++) {
+ if (lines.get(end).trim().isEmpty()) {
+ return end;
+ }
+ }
+ //EOF
+ return lines.size();
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/ComponentReviewInfo.java b/src/main/java/io/fd/maintainer/plugin/service/ComponentReviewInfo.java
new file mode 100644
index 0000000..0d0dcfb
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/ComponentReviewInfo.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service;
+
+import static io.fd.maintainer.plugin.service.ComponentReviewInfo.ComponentReviewInfoState.COMPONENT_FOUND;
+import static io.fd.maintainer.plugin.service.ComponentReviewInfo.ComponentReviewInfoState.COMPONENT_NOT_FOUND;
+import static java.util.Objects.isNull;
+
+import io.fd.maintainer.plugin.parser.Maintainer;
+import java.util.Set;
+
+public class ComponentReviewInfo {
+
+ private final String affectedFile;
+ private final ComponentReviewInfoState state;
+ private final String componentName;
+ private final Set<Maintainer> componentMaintainers;
+
+ private ComponentReviewInfo(final String affectedFile, final String componentName,
+ final Set<Maintainer> componentMaintainers) {
+ this.affectedFile = affectedFile;
+ this.state = isNull(componentName)
+ ? COMPONENT_NOT_FOUND
+ : COMPONENT_FOUND;
+ this.componentName = componentName;
+ this.componentMaintainers = componentMaintainers;
+ }
+
+ public String getAffectedFile() {
+ return affectedFile;
+ }
+
+ public String getComponentName() {
+ return componentName;
+ }
+
+ public Set<Maintainer> getComponentMaintainers() {
+ return componentMaintainers;
+ }
+
+ public ComponentReviewInfoState getState() {
+ return state;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ComponentReviewInfo that = (ComponentReviewInfo) o;
+
+ if (affectedFile != null
+ ? !affectedFile.equals(that.affectedFile)
+ : that.affectedFile != null) {
+ return false;
+ }
+ if (state != that.state) {
+ return false;
+ }
+ if (componentName != null
+ ? !componentName.equals(that.componentName)
+ : that.componentName != null) {
+ return false;
+ }
+ return componentMaintainers != null
+ ? componentMaintainers.equals(that.componentMaintainers)
+ : that.componentMaintainers == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = affectedFile != null
+ ? affectedFile.hashCode()
+ : 0;
+ result = 31 * result + (state != null
+ ? state.hashCode()
+ : 0);
+ result = 31 * result + (componentName != null
+ ? componentName.hashCode()
+ : 0);
+ result = 31 * result + (componentMaintainers != null
+ ? componentMaintainers.hashCode()
+ : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ComponentReviewInfo{" +
+ "affectedFile='" + affectedFile + '\'' +
+ ", state=" + state +
+ ", componentName='" + componentName + '\'' +
+ ", componentMaintainers=" + componentMaintainers +
+ '}';
+ }
+
+ public enum ComponentReviewInfoState {
+ COMPONENT_FOUND,
+ COMPONENT_NOT_FOUND;
+ }
+
+ public static class ComponentReviewInfoBuilder {
+ private String affectedFile;
+ private String componentName;
+ private Set<Maintainer> componentMaintainers;
+
+ public ComponentReviewInfoBuilder setAffectedFile(final String affectedFile) {
+ this.affectedFile = affectedFile;
+ return this;
+ }
+
+ public ComponentReviewInfoBuilder setComponentName(final String componentName) {
+ this.componentName = componentName;
+ return this;
+ }
+
+ public ComponentReviewInfoBuilder setComponentMaintainers(
+ final Set<Maintainer> componentMaintainers) {
+ this.componentMaintainers = componentMaintainers;
+ return this;
+ }
+
+ public ComponentReviewInfo createComponentReviewInfo() {
+ return new ComponentReviewInfo(affectedFile, componentName, componentMaintainers);
+ }
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/MaintainersProvider.java b/src/main/java/io/fd/maintainer/plugin/service/MaintainersProvider.java
new file mode 100644
index 0000000..b2d940a
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/MaintainersProvider.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service;
+
+import static java.lang.String.format;
+import static java.util.Objects.nonNull;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fd.maintainer.plugin.parser.ComponentInfo;
+import io.fd.maintainer.plugin.parser.MaintainerMismatchException;
+import io.fd.maintainer.plugin.parser.MaintainersParser;
+import io.fd.maintainer.plugin.service.dto.PluginBranchSpecificSettings;
+import io.fd.maintainer.plugin.util.ClosestMatch;
+import io.fd.maintainer.plugin.util.PatchListProcessing;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class MaintainersProvider implements ClosestMatch, PatchListProcessing {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MaintainersProvider.class);
+ final MaintainersParser maintainersParser;
+ @Inject
+ private GitRepositoryManager manager;
+ @Inject
+ private SettingsProvider settingsProvider;
+ @Inject
+ private SchemaFactory<ReviewDb> schemaFactory;
+
+ public MaintainersProvider() {
+ maintainersParser = new MaintainersParser();
+ }
+
+ @Nonnull
+ public List<ComponentInfo> getMaintainersInfo(@Nonnull final String branchName, final int changeNumber) {
+
+ // get configuration for branch of change
+ final PluginBranchSpecificSettings settings = settingsProvider.getBranchSpecificSettings(branchName);
+
+ try (final ReviewDb reviewDb = schemaFactory.open()) {
+ final Change change = reviewDb.changes().get(new Change.Id(changeNumber));
+ final String fullFileRef = settings.fullFileRef();
+
+ try (final Repository repository = manager.openRepository(change.getProject())) {
+
+ final Ref ref = Optional.ofNullable(repository.findRef(fullFileRef))
+ .orElseThrow(() -> new IllegalStateException(
+ format("Unable to get ref %s", fullFileRef)));
+
+ final RevCommit revCommit = new RevWalk(repository).parseCommit(ref.getObjectId());
+
+ final String maintainersFileContent =
+ findMostRecentMaintainersChangeContent(settings.getLocalFilePath(), repository,
+ new RevWalk(repository), revCommit);
+
+ if (nonNull(maintainersFileContent)) {
+ return maintainersParser.parseMaintainers(maintainersFileContent);
+ } else {
+ throw new IllegalStateException(
+ format("Unable to find file %s in branch %s", settings.getLocalFilePath(),
+ fullFileRef));
+ }
+ } catch (IOException | MaintainerMismatchException e) {
+ throw new IllegalStateException(e);
+ }
+
+ } catch (OrmException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ // skips head commit
+ private String findMostRecentMaintainersChangeContent(
+ final String maintainersFileName,
+ final Repository repository,
+ final RevWalk revWalk,
+ final RevCommit headCommit) {
+ LOG.info("Starting search at {}", headCommit);
+
+ final RevCommit parent = getRevCommit(revWalk, headCommit.getParent(0).getId());
+ LOG.info("Finding most recent maintainers file in {}", parent);
+
+
+ final String parentIdName = parent.getId().getName();
+ LOG.info("Parent id name {}", parentIdName);
+
+ try (TreeWalk treeWalk = new TreeWalk(repository)) {
+ treeWalk.addTree(parent.getTree());
+ treeWalk.setRecursive(true);
+ treeWalk.setFilter(PathFilter.create(maintainersFileName));
+ LOG.info("Attempting to find {}", maintainersFileName);
+
+ if (treeWalk.next()) {
+ LOG.info("Maintainers file found in commit {}", parent.getId());
+ ObjectId objectId = treeWalk.getObjectId(0);
+ ObjectLoader loader = repository.open(objectId);
+
+ // and then one can the loader to read the file
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ loader.copyTo(out);
+ revWalk.dispose();
+ return new String(out.toByteArray());
+ }
+
+ LOG.info("Maintainers file not found in commit {}, going deep", parent.getId());
+ if (parent.getParents() == null) {
+ throw new IllegalStateException(format("Root of branch reached with commit %s", parent));
+ }
+ return findMostRecentMaintainersChangeContent(maintainersFileName, repository, revWalk, parent);
+ } catch (IOException e) {
+ throw new IllegalStateException(format("Unable to detect maintainers file in %s", parent.getId()));
+ }
+ }
+
+ private RevCommit getRevCommit(final RevWalk revWalk, final ObjectId id) {
+ try {
+ return revWalk.parseCommit(id);
+ } catch (IOException e) {
+ throw new IllegalStateException(format("Unable to parse commit %s", id));
+ }
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/PatchsetReviewInfo.java b/src/main/java/io/fd/maintainer/plugin/service/PatchsetReviewInfo.java
new file mode 100644
index 0000000..ff3e08c
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/PatchsetReviewInfo.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.patch.PatchList;
+import io.fd.maintainer.plugin.parser.ComponentPath;
+import io.fd.maintainer.plugin.util.MaintainersIndex;
+import io.fd.maintainer.plugin.util.PatchListProcessing;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import org.parboiled.common.Tuple2;
+
+public class PatchsetReviewInfo implements PatchListProcessing {
+
+ private final ReviewState reviewState;
+ private final Set<String> missingComponentReview;
+
+ public PatchsetReviewInfo(@Nonnull final MaintainersIndex index,
+ @Nonnull final PatchList patchList,
+ @Nonnull final Set<Account> currentVerificationAuthors) {
+ final Set<String> componentsForPatchlist = getRelevantPatchListEntries(patchList)
+ .stream()
+ .map(patchListEntry -> {
+ final Tuple2<Set<ComponentPath>, Set<ComponentPath>> componentTuple =
+ index.getComponentPathsForEntry(patchListEntry);
+ if (getRelevantChangeName(patchListEntry).equals(patchListEntry.getOldName())) {
+ return componentTuple.a;
+ } else {
+ return componentTuple.b;
+ }
+ })
+ .flatMap(Collection::stream)
+ .map(index::getComponentForPath)
+ .filter(index::isReviewComponent)
+ .collect(Collectors.toSet());
+ final Set<String> componentsCurrentlyReviewed = currentVerificationAuthors.stream()
+ .map(account -> index.getComponentsForMaintainer(account.getFullName()))
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+
+ if (componentsCurrentlyReviewed.containsAll(componentsForPatchlist)) {
+ reviewState = ReviewState.ALL_COMPONENTS_REVIEWED;
+ missingComponentReview = Collections.emptySet();
+ } else {
+ reviewState = ReviewState.MISSING_COMPONENT_REVIEW;
+ missingComponentReview = componentsForPatchlist.stream()
+ .filter(component -> !componentsCurrentlyReviewed.contains(component))
+ .collect(Collectors.toSet());
+ }
+ }
+
+ public ReviewState getReviewState() {
+ return reviewState;
+ }
+
+ public Set<String> getMissingComponentReview() {
+ return missingComponentReview;
+ }
+
+ public enum ReviewState {
+ ALL_COMPONENTS_REVIEWED,
+ MISSING_COMPONENT_REVIEW;
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/SettingsProvider.java b/src/main/java/io/fd/maintainer/plugin/service/SettingsProvider.java
new file mode 100644
index 0000000..052874b
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/SettingsProvider.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service;
+
+import static java.lang.String.format;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fd.maintainer.plugin.service.dto.PluginBranchSpecificSettings;
+import io.fd.maintainer.plugin.util.ClosestMatch;
+import java.util.Optional;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public final class SettingsProvider implements ClosestMatch {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
+
+ private static final String MAINTAINER_PLUGIN = "maintainer";
+ private static final String BRANCH_SECTION = "branch";
+
+ private static final String PLUGIN_USER = "pluginuser";
+
+ private static final String MAINTAINERS_FILE_PATH_REF = "maintainerfileref";
+ private static final String DEFAULT_MAINTAINERS_FILE_PATH_REF = "master/HEAD";
+
+ private static final String MAINTAINERS_FILE_REF = "maintainerfile";
+ private static final String DEFAULT_MAINTAINERS_FILE_REF = "MAINTAINERS";
+
+ private static final String ALLOW_SUBMIT = "allowmaintainersubmit";
+ private static final boolean DEFAULT_ALLOW_SUBMIT = false;
+
+ private static final String AUTO_ADD_REVIEWERS = "autoaddreviewers";
+ private static final boolean DEFAULT_AUTO_ADD_REVIEWERS = false;
+
+ private static final String AUTO_SUBMIT = "autosubmit";
+ private static final boolean DEFAULT_AUTO_SUBMIT = false;
+
+ @Inject
+ private PluginConfigFactory cfg;
+
+ public PluginBranchSpecificSettings getBranchSpecificSettings(@Nonnull final String branchName) {
+
+ final String fullBranchName = branchName.startsWith(RefNames.REFS_HEADS)
+ ? branchName
+ : RefNames.REFS_HEADS.concat(branchName);
+
+ LOG.info("Reading configuration for branch {}", fullBranchName);
+ return getSettingsForBranch(fullBranchName, closesBranchMatch(fullBranchName));
+ }
+
+ private PluginBranchSpecificSettings getSettingsForBranch(final String branchName, final String closestBranch) {
+ return new PluginBranchSpecificSettings.PluginSettingsBuilder()
+ .setPluginUserName(pluginUserOrThrow(branchName, closestBranch))
+ .setLocalFilePath(fileNameRefOrDefault(branchName, closestBranch))
+ .setFileRef(filePathRefOrDefault(branchName, closestBranch))
+ .setAllowMaintainersSubmit(allowMaintainersSubmitOrDefault(branchName, closestBranch))
+ .setAutoAddReviewers(autoAddReviewersOrDefault(branchName, closestBranch))
+ .setAutoSubmit(autoSubmitOrDefault(branchName, closestBranch))
+ .setBranch(globalPluginConfig().getSubsections(BRANCH_SECTION)
+ .stream()
+ .filter(subSection -> subSection.equals(branchName))
+ .findAny()
+ .orElse(closestBranch))
+ .createPluginSettings();
+ }
+
+ private Boolean autoAddReviewersOrDefault(final String branch, final String closesBranch) {
+ return getKey(branch, closesBranch, AUTO_ADD_REVIEWERS, DEFAULT_AUTO_ADD_REVIEWERS, Boolean::valueOf);
+ }
+
+ private Boolean autoSubmitOrDefault(final String branch, final String closestBranch) {
+ return getKey(branch, closestBranch, AUTO_SUBMIT, DEFAULT_AUTO_SUBMIT, Boolean::valueOf);
+ }
+
+ private Boolean allowMaintainersSubmitOrDefault(final String branch, final String closesBranch) {
+ return getKey(branch, closesBranch, ALLOW_SUBMIT, DEFAULT_ALLOW_SUBMIT, Boolean::valueOf);
+ }
+
+ private String fileNameRefOrDefault(final String branch, final String closesBranch) {
+ return getKey(branch, closesBranch, MAINTAINERS_FILE_REF, DEFAULT_MAINTAINERS_FILE_REF, String::valueOf);
+ }
+
+ private String filePathRefOrDefault(final String branch, final String closesBranch) {
+ return getKey(branch, closesBranch, MAINTAINERS_FILE_PATH_REF, DEFAULT_MAINTAINERS_FILE_PATH_REF,
+ String::valueOf);
+ }
+
+ private String pluginUserOrThrow(final String branch,
+ final String alternativeBranch) {
+ final Config config = globalPluginConfig();
+ return Optional.ofNullable(config.getString(BRANCH_SECTION, branch, PLUGIN_USER))
+ .orElse(Optional.ofNullable(config.getString(BRANCH_SECTION, alternativeBranch, PLUGIN_USER))
+ .orElseThrow(() -> {
+ LOG.error("Plugin user not specified for branch {}", branch);
+ return new IllegalStateException(format("Plugin user not specified for branch %s", branch));
+ }));
+ }
+
+ private <T> T getKey(final String branch,
+ final String alternativeBranch,
+ final String subKey,
+ final T defaultValue,
+ final Function<String, T> mapTo) {
+ return Optional.ofNullable(globalPluginConfig()
+ .getString(BRANCH_SECTION, branch, subKey))
+ .map(mapTo)
+ .orElse(Optional.ofNullable(
+ globalPluginConfig().getString(BRANCH_SECTION, alternativeBranch, subKey))
+ .map(mapTo)
+ .orElse(defaultValue));
+ }
+
+ private Config globalPluginConfig() {
+ return cfg.getGlobalPluginConfig(MAINTAINER_PLUGIN);
+ }
+
+ // match by the number of changes needed to change one String into another
+ private String closesBranchMatch(final String branchName) {
+ return globalPluginConfig().getSubsections(BRANCH_SECTION).stream()
+ .reduce((branchOne, branchTwo) -> closestMatch(branchName, branchOne, branchTwo))
+ // if non use default
+ .orElse(RefNames.REFS_HEADS);
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/dto/PluginBranchSpecificSettings.java b/src/main/java/io/fd/maintainer/plugin/service/dto/PluginBranchSpecificSettings.java
new file mode 100644
index 0000000..49960ab
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/dto/PluginBranchSpecificSettings.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service.dto;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+
+public class PluginBranchSpecificSettings {
+
+ private static final String HEAD_PART = "/" + RefNames.HEAD;
+
+ private final String pluginUserName;
+ private final String branch;
+ private final String fileRef;
+ private final String localFilePath;
+ private final boolean allowMaintainersSubmit;
+ private final boolean autoAddReviewers;
+ private final boolean autoSubmit;
+
+ private PluginBranchSpecificSettings(final String pluginUserName,
+ final String branch,
+ final String fileRef,
+ final String localFilePath,
+ final boolean allowMaintainersSubmit,
+ final boolean autoAddReviewers,
+ final boolean autoSubmit) {
+ this.pluginUserName = pluginUserName;
+ this.branch = branch;
+ this.fileRef = fileRef;
+ this.localFilePath = localFilePath;
+ this.allowMaintainersSubmit = allowMaintainersSubmit;
+ this.autoAddReviewers = autoAddReviewers;
+ this.autoSubmit = autoSubmit;
+ }
+
+ public String getFileRef() {
+ return fileRef;
+ }
+
+ public String getLocalFilePath() {
+ return localFilePath;
+ }
+
+ public boolean isAllowMaintainersSubmit() {
+ return allowMaintainersSubmit;
+ }
+
+ public boolean isAutoAddReviewers() {
+ return autoAddReviewers;
+ }
+
+ public boolean isAutoSubmit() {
+ return autoSubmit;
+ }
+
+ public String fullFileRef() {
+ return branch.concat(fileRef);
+ }
+
+ public String getPluginUserName() {
+ return pluginUserName;
+ }
+
+ @Override
+ public String toString() {
+ return "PluginBranchSpecificSettings{" +
+ "branch='" + branch + '\'' +
+ ", fileRef='" + fileRef + '\'' +
+ ", localFilePath='" + localFilePath + '\'' +
+ ", allowMaintainersSubmit=" + allowMaintainersSubmit +
+ ", autoAddReviewers=" + autoAddReviewers +
+ '}';
+ }
+
+ public static class PluginSettingsBuilder {
+ private String pluginUserName;
+ private String branch;
+ private String fileRef;
+ private String localFilePath;
+ private boolean allowMaintainersSubmit;
+ private boolean autoAddReviewers;
+ private boolean autoSubmit;
+
+ private static String reduceWildcard(String input) {
+ return input.contains("*")
+ ? input.substring(0, input.indexOf("*"))
+ : input;
+ }
+
+ private static String addEndSlash(String input) {
+ return input.endsWith("/")
+ ? input
+ : input.concat("/");
+ }
+
+ public PluginSettingsBuilder setPluginUserName(final String pluginUserName) {
+ this.pluginUserName = pluginUserName;
+ return this;
+ }
+
+ public PluginSettingsBuilder setFileRef(final String fileRef) {
+ // TODO - remove this replace if configuration will be changed
+ this.fileRef = fileRef.replace(HEAD_PART, "");
+ return this;
+ }
+
+ public PluginSettingsBuilder setLocalFilePath(final String localFilePath) {
+ this.localFilePath = localFilePath;
+ return this;
+ }
+
+ public PluginSettingsBuilder setAllowMaintainersSubmit(final boolean allowMaintainersSubmit) {
+ this.allowMaintainersSubmit = allowMaintainersSubmit;
+ return this;
+ }
+
+ public PluginSettingsBuilder setAutoAddReviewers(final boolean autoAddReviewers) {
+ this.autoAddReviewers = autoAddReviewers;
+ return this;
+ }
+
+ public PluginSettingsBuilder setBranch(final String branch) {
+ this.branch = addEndSlash(reduceWildcard(branch));
+ return this;
+ }
+
+ public PluginSettingsBuilder setAutoSubmit(final boolean autoSubmit) {
+ this.autoSubmit = autoSubmit;
+ return this;
+ }
+
+ public PluginBranchSpecificSettings createPluginSettings() {
+ return new PluginBranchSpecificSettings(pluginUserName, branch, fileRef, localFilePath,
+ allowMaintainersSubmit, autoAddReviewers, autoSubmit);
+ }
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/push/ApprovalPusher.java b/src/main/java/io/fd/maintainer/plugin/service/push/ApprovalPusher.java
new file mode 100644
index 0000000..cb5c5d9
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/push/ApprovalPusher.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service.push;
+
+import static java.lang.String.format;
+
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.annotation.Nonnull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class ApprovalPusher {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ApprovalPusher.class);
+
+ @Inject
+ private ChangesCollection changes;
+
+ @Inject
+ private Revisions revisions;
+
+ @Inject
+ private Provider<PostReview> reviewProvider;
+
+ public void approvePatchset(@Nonnull final Change change,
+ @Nonnull final PatchSet patchSet,
+ @Nonnull final String onBehalfOf) {
+ try {
+ ChangeResource changeResource = changes.parse(change.getId());
+ final RevisionResource revisionResource = revisions.parse(changeResource, IdString.fromUrl("current"));
+
+ final PostReview post = reviewProvider.get();
+
+ ReviewInput review =
+ ReviewInput.approve()
+ .message(format(" All relevant component maintainers verified patchset %s",
+ patchSet.getPatchSetId()));// review +2
+ review.onBehalfOf = onBehalfOf;
+
+ post.apply(revisionResource, review);
+
+ } catch (OrmException | IOException | RestApiException | UpdateException e) {
+ LOG.error("Unable to approve patchset {}", patchSet.getId(),
+ e);
+ return;
+ }
+ LOG.info("Patchset {} successfully approved", patchSet.getId());
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/push/ReviewerPusher.java b/src/main/java/io/fd/maintainer/plugin/service/push/ReviewerPusher.java
new file mode 100644
index 0000000..4354df3
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/push/ReviewerPusher.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service.push;
+
+import static io.fd.maintainer.plugin.service.ComponentReviewInfo.ComponentReviewInfoState.COMPONENT_FOUND;
+import static java.util.stream.Collectors.toMap;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import io.fd.maintainer.plugin.parser.Maintainer;
+import io.fd.maintainer.plugin.service.ComponentReviewInfo;
+import io.fd.maintainer.plugin.util.CommonTasks;
+import io.fd.maintainer.plugin.util.MaintainersIndex;
+import io.fd.maintainer.plugin.util.PatchListProcessing;
+import io.fd.maintainer.plugin.util.WarningGenerator;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class ReviewerPusher implements WarningGenerator, PatchListProcessing, CommonTasks {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ReviewerPusher.class);
+
+ @Inject
+ private ChangesCollection changesCollection;
+
+ @Inject
+ private Provider<PostReviewers> reviewersProvider;
+
+ @Inject
+ private Provider<PostReview> reviewProvider;
+
+ @Inject
+ private PatchListCache patchListCache;
+
+ @Inject
+ private Revisions revisions;
+
+ @Inject
+ private SchemaFactory<ReviewDb> schemaFactory;
+
+
+ public void addRelevantReviewers(@Nonnull final MaintainersIndex maintainersIndex,
+ @Nonnull final Change change,
+ @Nonnull final PatchSet mostCurrentPatchSet,
+ @Nonnull final String onBehalfOf) throws OrmException {
+
+ final Set<ComponentReviewInfo> reviewInfoSet =
+ getRelevantPatchListEntries(getPatchList(patchListCache, change, mostCurrentPatchSet))
+ .stream()
+ .map(this::getRelevantChangeName)
+ .map(maintainersIndex::getReviewInfoForPath)
+ .collect(Collectors.toSet());
+
+ final ReviewDb reviewDb = schemaFactory.open();
+ final Map<String, Account.Id> accountIndex = reviewDb.accounts().all().toList().stream()
+ .collect(toMap(Account::getFullName, Account::getId));
+
+ final Set<Account.Id> reviewersToBeAdded = reviewInfoSet.stream()
+ .filter(reviewInfo -> reviewInfo.getState() == COMPONENT_FOUND)
+ .map(ComponentReviewInfo::getComponentMaintainers)
+ .flatMap(Collection::stream)
+ .map(Maintainer::getName)
+ .map(accountIndex::get)
+ .collect(Collectors.toSet());
+
+ LOG.info("Adding reviewers for change {}", change.getId());
+ addReviewers(reviewersProvider.get(), reviewersToBeAdded, changesCollection, change);
+ sendReviewersInfo(reviewInfoSet, change, changesCollection, revisions, reviewProvider.get(), onBehalfOf);
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/push/SubmitPusher.java b/src/main/java/io/fd/maintainer/plugin/service/push/SubmitPusher.java
new file mode 100644
index 0000000..2235b60
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/push/SubmitPusher.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service.push;
+
+import static java.lang.String.format;
+
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.change.Submit;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.annotation.Nonnull;
+
+@Singleton
+public class SubmitPusher {
+
+ @Inject
+ private Submit submitApi;
+
+ @Inject
+ private ChangesCollection changesCollection;
+
+ @Inject
+ private Revisions revisions;
+
+ public void submitPatch(@Nonnull final Change change,
+ @Nonnull final String onBehalfOf) {
+ SubmitInput request = new SubmitInput();
+ request.onBehalfOf = onBehalfOf;
+
+ try {
+ ChangeResource changeResource = changesCollection.parse(change.getId());
+ final RevisionResource revisionResource = revisions.parse(changeResource, IdString.fromUrl("current"));
+ submitApi.apply(revisionResource, request);
+ } catch (OrmException | RestApiException | IOException e) {
+ throw new IllegalStateException(format("Unable to submit change %s", change.getId()));
+ }
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/service/push/WarningPusher.java b/src/main/java/io/fd/maintainer/plugin/service/push/WarningPusher.java
new file mode 100644
index 0000000..464a016
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/service/push/WarningPusher.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.service.push;
+
+import static java.lang.String.format;
+
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import io.fd.maintainer.plugin.util.CommonTasks;
+import io.fd.maintainer.plugin.util.WarningGenerator;
+import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class WarningPusher implements CommonTasks {
+
+ private static final Logger LOG = LoggerFactory.getLogger(WarningPusher.class);
+
+ @Inject
+ private ChangesCollection changesCollection;
+
+ @Inject
+ private Revisions revisions;
+
+ @Inject
+ private Provider<PostReview> reviewProvider;
+
+ private static String formatComments(final Set<WarningGenerator.ComponentChangeWarning> comments) {
+ return "Following entries are now no longer part of their components. Maintainers file update is recommended."
+ + LINE_SEPARATOR + LINE_SEPARATOR
+ + comments.stream()
+ .map(warning -> format("File %s renamed to %s - invalid components %s", warning.getOldName(),
+ warning.getNewName(), warning.getInvalidComponents()
+ .stream().map(componentWithPath -> format("Component %s[path=%s]",
+ componentWithPath.getComponentTitle(), componentWithPath.getPath().getPath()))
+ .collect(Collectors.toSet())))
+ .collect(Collectors.joining(LINE_SEPARATOR));
+ }
+
+ public void sendWarnings(@Nonnull final Set<ComponentChangeWarning> comments,
+ @Nonnull final Change change,
+ @Nonnull final PatchSet patchSet,
+ @Nonnull final String onBehalfOf) throws OrmException {
+ if (comments.isEmpty()) {
+ LOG.warn("No warnings");
+ return;
+ }
+
+ try {
+ ChangeResource changeResource = changesCollection.parse(change.getId());
+ final RevisionResource revisionResource = revisions.parse(changeResource, IdString.fromUrl("current"));
+
+ ReviewInput review = ReviewInput.dislike()
+ .message(formatComments(comments));// review -1
+ review.onBehalfOf = onBehalfOf;
+
+ reviewProvider.get().apply(revisionResource, review);
+ } catch (IOException | RestApiException | UpdateException e) {
+ throw new IllegalStateException(
+ format("Unable to add warning comments for change %s / patchset %s", change.getId(),
+ patchSet.getId()), e);
+ }
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/util/ClosestMatch.java b/src/main/java/io/fd/maintainer/plugin/util/ClosestMatch.java
new file mode 100644
index 0000000..f1baca0
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/util/ClosestMatch.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.util;
+
+import static org.apache.commons.lang3.StringUtils.getLevenshteinDistance;
+
+import javax.annotation.Nonnull;
+
+public interface ClosestMatch {
+
+ @Nonnull
+ default String closestMatch(@Nonnull final String baseBranch,
+ @Nonnull final String branchOptionOne,
+ @Nonnull final String branchOptionTwo) {
+ return getLevenshteinDistance(branchOptionOne, baseBranch) > getLevenshteinDistance(branchOptionTwo, baseBranch)
+ ? branchOptionTwo
+ : branchOptionOne;
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/util/CommonTasks.java b/src/main/java/io/fd/maintainer/plugin/util/CommonTasks.java
new file mode 100644
index 0000000..aed5e96
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/util/CommonTasks.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.util;
+
+
+import static io.fd.maintainer.plugin.service.ComponentReviewInfo.ComponentReviewInfoState.COMPONENT_FOUND;
+import static io.fd.maintainer.plugin.service.ComponentReviewInfo.ComponentReviewInfoState.COMPONENT_NOT_FOUND;
+import static java.lang.String.format;
+import static java.util.Objects.nonNull;
+import static java.util.stream.Collectors.toMap;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import io.fd.maintainer.plugin.parser.ComponentPath;
+import io.fd.maintainer.plugin.parser.Maintainer;
+import io.fd.maintainer.plugin.service.ComponentReviewInfo;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import org.parboiled.common.Tuple2;
+
+/**
+ * Task to build complex index that is used to detect path components,etc...
+ */
+public interface CommonTasks extends WarningGenerator, PatchListProcessing {
+
+ String LINE_SEPARATOR = System.lineSeparator();
+
+ static String formatReviewerInfo(final Set<ComponentReviewInfo> reviewInfoSet) {
+
+ final Multimap<String, String> componentToAffectedFileIndex = LinkedListMultimap.create();
+ final Set<ComponentReviewInfo> componentBoundReviewInfoSet = reviewInfoSet.stream()
+ .filter(reviewInfo -> reviewInfo.getState() == COMPONENT_FOUND).collect(Collectors.toSet());
+ componentBoundReviewInfoSet
+ .forEach(reviewInfo -> componentToAffectedFileIndex
+ .put(reviewInfo.getComponentName(), reviewInfo.getAffectedFile()));
+
+ final Map<String, Set<Maintainer>> componentToMaintainers = new HashMap<>();
+ componentBoundReviewInfoSet.forEach(reviewInfo -> {
+ if (!componentToMaintainers.containsKey(reviewInfo.getComponentName())) {
+ componentToMaintainers.put(reviewInfo.getComponentName(), reviewInfo.getComponentMaintainers());
+ }
+ });
+
+ final List<ComponentReviewInfo> componentNotFoundReviewInfos = reviewInfoSet.stream()
+ .filter(reviewInfo -> reviewInfo.getState() == COMPONENT_NOT_FOUND)
+ .collect(Collectors.toList());
+
+ final String messageComponentsFound = componentToAffectedFileIndex.keySet()
+ .stream()
+ .map(key -> format(
+ "Component %s%s%s" +
+ "Maintainers :%s%s%s" +
+ "Affected files :%s%s%s",
+ key, LINE_SEPARATOR, LINE_SEPARATOR,
+ LINE_SEPARATOR, formatMaintainers(componentToMaintainers.get(key)), LINE_SEPARATOR,
+ LINE_SEPARATOR, formatFiles(componentToAffectedFileIndex.get(key)), LINE_SEPARATOR))
+ .collect(Collectors.joining(LINE_SEPARATOR));
+
+ final String messageComponentsNotFound =
+ format("No component found for following files%s%s", LINE_SEPARATOR,
+ formatFilesWithNoComponent(componentNotFoundReviewInfos));
+
+ if (nonNull(messageComponentsNotFound)) {
+ return messageComponentsFound.concat(LINE_SEPARATOR).concat(messageComponentsNotFound);
+ } else {
+ return messageComponentsFound;
+ }
+ }
+
+ static String formatFilesWithNoComponent(final List<ComponentReviewInfo> componentNotFoundReviewInfos) {
+ return componentNotFoundReviewInfos.stream()
+ .map(ComponentReviewInfo::getAffectedFile)
+ .map(" "::concat)
+ .collect(Collectors.joining(LINE_SEPARATOR));
+ }
+
+ static String formatMaintainers(final Set<Maintainer> maintainers) {
+ return maintainers.stream()
+ .map(maintainer -> format(" %s<%s>", maintainer.getName(), maintainer.getEmail()))
+ .collect(Collectors.joining(LINE_SEPARATOR));
+ }
+
+ static String formatFiles(final Collection<String> files) {
+ return files.stream()
+ .map(file -> format(" Path: %s", file))
+ .collect(Collectors.joining(LINE_SEPARATOR));
+ }
+
+ default Map<PatchListEntry, Tuple2<Set<ComponentPath>, Set<ComponentPath>>> renamedEntriesToComponentIndex(
+ final @Nonnull MaintainersIndex maintainersIndex, final List<PatchListEntry> patches) {
+ return patches.stream()
+ // only renames
+ .filter(entry -> entry.getChangeType() == Patch.ChangeType.RENAMED)
+ .collect(toMap(entry -> entry, maintainersIndex::getComponentPathsForEntry));
+ }
+
+ default void sendReviewersInfo(@Nonnull final Set<ComponentReviewInfo> reviewInfoSet,
+ @Nonnull final Change change,
+ @Nonnull final ChangesCollection changesCollection,
+ @Nonnull final Revisions revisions,
+ @Nonnull final PostReview reviewApi,
+ @Nonnull final String onBehalfOf) throws OrmException {
+ try {
+ ChangeResource changeResource = changesCollection.parse(change.getId());
+ final RevisionResource revisionResource = revisions.parse(changeResource, IdString.fromUrl("current"));
+ ReviewInput review = ReviewInput.noScore().message(formatReviewerInfo(reviewInfoSet));
+ review.onBehalfOf = onBehalfOf;
+
+ reviewApi.apply(revisionResource, review);
+ } catch (IOException | RestApiException | UpdateException e) {
+ throw new IllegalStateException(
+ format("Unable to add reviewers info for patchset %s", change.currentPatchSetId()));
+ }
+ }
+
+ default void addReviewers(final PostReviewers reviewersApi,
+ final Set<Account.Id> reviewers,
+ final ChangesCollection changes,
+ final Change change) {
+ try {
+ ChangeResource changeResource = changes.parse(change.getId());
+ for (Account.Id accountId : reviewers) {
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = accountId.toString();
+ reviewersApi.apply(changeResource, input);
+ }
+ } catch (Exception ex) {
+ throw new IllegalStateException("Couldn't add reviewers to the change", ex);
+ }
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/util/MaintainersIndex.java b/src/main/java/io/fd/maintainer/plugin/util/MaintainersIndex.java
new file mode 100644
index 0000000..a9a6478
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/util/MaintainersIndex.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.util;
+
+import static io.fd.maintainer.plugin.parser.ComponentPath.MatchLevel.MAX;
+import static io.fd.maintainer.plugin.parser.ComponentPath.MatchLevel.NONE;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multiset;
+import com.google.gerrit.server.patch.PatchListEntry;
+import io.fd.maintainer.plugin.parser.ComponentInfo;
+import io.fd.maintainer.plugin.parser.ComponentPath;
+import io.fd.maintainer.plugin.parser.ComponentPath.MatchLevel;
+import io.fd.maintainer.plugin.parser.Maintainer;
+import io.fd.maintainer.plugin.service.ComponentReviewInfo;
+import io.fd.maintainer.plugin.service.ComponentReviewInfo.ComponentReviewInfoBuilder;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import org.apache.commons.lang3.StringUtils;
+import org.parboiled.common.Tuple2;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class MaintainersIndex implements ClosestMatch {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MaintainersIndex.class);
+
+ private Map<ComponentPath, Set<Maintainer>> pathToMaintainersIndex;
+ private Map<String, String> pathToComponentIndex;
+ private Multimap<String, String> maintainerNameToComponentIndex;
+ private Map<String, Boolean> reviewComponentIndex;
+
+ public MaintainersIndex(@Nonnull final List<ComponentInfo> maintainers) {
+ pathToMaintainersIndex = maintainers.stream()
+ .flatMap(maintainersInfo -> maintainersInfo.getPaths().stream()
+ .map(componentPath -> new Tuple2<>(componentPath, maintainersInfo.getMaintainers())))
+ .collect(Collectors.toMap(tuple -> tuple.a, tuple -> tuple.b));
+
+ pathToComponentIndex = new HashMap<>();
+ maintainers.forEach(maintainersInfo -> maintainersInfo.getPaths()
+ .forEach(
+ componentPath ->
+ pathToComponentIndex.put(componentPath.getPath(), maintainersInfo.getTitle())
+ ));
+ maintainerNameToComponentIndex = LinkedListMultimap.create();
+ maintainers.forEach(maintainersInfo -> maintainersInfo.getMaintainers().forEach(maintainer ->
+ maintainerNameToComponentIndex.put(maintainer.getName(), maintainersInfo.getTitle())));
+
+ reviewComponentIndex = maintainers.stream()
+ .collect(Collectors.toMap(ComponentInfo::getTitle, component -> !component.getMaintainers().isEmpty()));
+ }
+
+ private static int getPathLength(final String path) {
+ return StringUtils.countMatches(path, "/");
+ }
+
+ /**
+ * Tells whether component has maintainers configured
+ */
+ public boolean isReviewComponent(@Nonnull final String component) {
+ return reviewComponentIndex.get(component);
+ }
+
+ public Set<String> getComponentsForMaintainer(@Nonnull final String name) {
+ return new HashSet<>(maintainerNameToComponentIndex.get(name));
+ }
+
+ public String getComponentForPath(@Nonnull final ComponentPath path) {
+ return pathToComponentIndex.get(path.getPath());
+ }
+
+ public Tuple2<Set<ComponentPath>, Set<ComponentPath>> getComponentPathsForEntry(
+ @Nonnull final PatchListEntry entry) {
+ final LinkedListMultimap<MatchLevel, ComponentPath> byMatchIndexOld = LinkedListMultimap.create();
+ final LinkedListMultimap<MatchLevel, ComponentPath> byMatchIndexNew = LinkedListMultimap.create();
+ pathToMaintainersIndex.forEach((key, value) -> byMatchIndexOld.put(key.matchAgainst(entry.getOldName()),
+ key));
+
+ pathToMaintainersIndex.forEach((key, value) -> byMatchIndexNew.put(key.matchAgainst(entry.getNewName()),
+ key));
+
+ final MatchLevel maxMatchLevelOld = maxMatchLevel(byMatchIndexOld.keys());
+ final MatchLevel maxMatchLevelNew = maxMatchLevel(byMatchIndexNew.keys());
+
+ final int mostSpecificLengthOld = mostSpecificPathLengthFromComponent(maxMatchLevelOld, byMatchIndexOld);
+ final int mostSpecificLengthNew = mostSpecificPathLengthFromComponent(maxMatchLevelOld, byMatchIndexOld);
+
+ final Set<ComponentPath> oldComponents = NONE == maxMatchLevelOld
+ ? Collections.emptySet()
+ : new HashSet<>(byMatchIndexOld.get(maxMatchLevelOld)
+ .stream()
+ .filter(componentPath -> getPathLength(componentPath.getPath()) == mostSpecificLengthOld)
+ .collect(Collectors.toList()));
+
+ final Set<ComponentPath> newComponents = NONE == maxMatchLevelNew
+ ? Collections.emptySet()
+ : new HashSet<>(byMatchIndexNew.get(maxMatchLevelNew).stream()
+ .filter(componentPath -> getPathLength(componentPath.getPath()) == mostSpecificLengthNew)
+ .collect(Collectors.toList()));
+
+ return new Tuple2<>(oldComponents, newComponents);
+ }
+
+ public ComponentReviewInfo getReviewInfoForPath(final String path) {
+ LOG.debug("Getting maintainers for path {}", path);
+ final LinkedListMultimap<MatchLevel, Tuple2<ComponentPath, Maintainer>> byMatchIndex =
+ LinkedListMultimap.create();
+
+ pathToMaintainersIndex.forEach((key, value) -> value
+ .forEach((entry -> byMatchIndex.put(key.matchAgainst(path), new Tuple2<>(key, entry)))));
+
+ final MatchLevel maximumMatchLevel = maxMatchLevel(byMatchIndex.keys());
+ LOG.debug("Maximum match level for path {} = {}", path, maximumMatchLevel);
+
+ // out of all that have maximum match level, we need only those that are most basically longest
+ // allows to get /foo/bar/* over * or /foo/*
+ final int mostSpecificPathLength = mostSpecificPathLengthFromTuple(maximumMatchLevel, byMatchIndex);
+
+ if (NONE == maximumMatchLevel) {
+ return new ComponentReviewInfoBuilder()
+ .setAffectedFile(path).createComponentReviewInfo();
+ } else {
+ return byMatchIndex.get(maximumMatchLevel).stream()
+ .filter(tuple -> getPathLength(tuple.a.getPath()) == mostSpecificPathLength)
+ .peek(maintainer -> LOG
+ .debug("Maintainer found [component={},reviewer={}]", maintainer.a, maintainer.b))
+ .map(tuple -> new ComponentReviewInfoBuilder()
+ .setAffectedFile(path)
+ .setComponentName(getComponentForPath(tuple.a))
+ .setComponentMaintainers(pathToMaintainersIndex.get(tuple.a))
+ .createComponentReviewInfo())
+ .findAny().orElse(new ComponentReviewInfoBuilder()
+ .setAffectedFile(path).createComponentReviewInfo());
+ }
+ }
+
+ private MatchLevel maxMatchLevel(final Multiset<MatchLevel> keys) {
+ return keys.stream().max(MAX).orElse(NONE);
+ }
+
+ private int mostSpecificPathLengthFromTuple(final MatchLevel maximumMatchLevel,
+ final LinkedListMultimap<MatchLevel, Tuple2<ComponentPath, Maintainer>> byMatchIndex) {
+ return byMatchIndex.get(maximumMatchLevel)
+ .stream()
+ .map(tuple -> tuple.a)
+ .map(ComponentPath::getPath)
+ .map(MaintainersIndex::getPathLength)
+ .max(Comparator.comparingInt(integer -> integer))
+ .orElse(0);
+ }
+
+ private int mostSpecificPathLengthFromComponent(final MatchLevel maximumMatchLevel,
+ final LinkedListMultimap<MatchLevel, ComponentPath> byMatchIndex) {
+ return byMatchIndex.get(maximumMatchLevel)
+ .stream()
+ .map(ComponentPath::getPath)
+ .map(MaintainersIndex::getPathLength)
+ .max(Comparator.comparingInt(integer -> integer))
+ .orElse(0);
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/util/PatchListProcessing.java b/src/main/java/io/fd/maintainer/plugin/util/PatchListProcessing.java
new file mode 100644
index 0000000..6d4bc30
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/util/PatchListProcessing.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.util;
+
+import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
+import static java.lang.String.format;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+
+/**
+ * Common logic for patchlist processing
+ */
+public interface PatchListProcessing {
+
+ String CODE_REVIEW_LABEL = "Code-Review";
+ int PATCHSET_VERIFIED = 1;
+
+ static boolean isCodeReview(final PatchSetApproval approval) {
+ return CODE_REVIEW_LABEL.equals(approval.getLabel());
+ }
+
+ static boolean isVerifyPatchset(final PatchSetApproval approval) {
+ return PATCHSET_VERIFIED == (int) approval.getValue();
+ }
+
+ static boolean isCodeReview(final ApprovalAttribute approvalAttribute) {
+ return CODE_REVIEW_LABEL.equals(approvalAttribute.type);
+ }
+
+ static boolean isVerifyPatchset(final ApprovalAttribute approvalAttribute) {
+ return PATCHSET_VERIFIED == Integer.valueOf(approvalAttribute.value);
+ }
+
+ /**
+ * Gets relevant patch list entries for processing
+ */
+ default List<PatchListEntry> getRelevantPatchListEntries(@Nonnull final PatchList patchList) {
+ return patchList.getPatches().stream()
+ // filters out commit msg
+ .filter(entry -> !COMMIT_MSG.equals(entry.getNewName()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * By design of plugin, matching of files per component should be done by old name.
+ * Only in case of create of new file ,new name is used
+ */
+ default String getRelevantChangeName(@Nonnull final PatchListEntry entry) {
+ return entry.getOldName() == null
+ ? entry.getNewName()
+ : entry.getOldName();
+ }
+
+ /**
+ * Attempts to find patchset changes in cache
+ */
+ default PatchList getPatchList(@Nonnull final PatchListCache patchListCache,
+ @Nonnull final Change change,
+ @Nonnull final PatchSet mostCurrentPatchSet) {
+ try {
+ return patchListCache.get(change, mostCurrentPatchSet);
+ } catch (PatchListNotAvailableException e) {
+ throw new IllegalStateException(
+ format("Unable to get patchlist for patchset %s", mostCurrentPatchSet.getId()), e);
+ }
+ }
+
+ /**
+ * Filters out only approvals that are labeled Code-Review+1
+ */
+ default Optional<ApprovalAttribute> getPatchListVerifications(final CommentAddedEvent commentAddedEvent) {
+ return Arrays.stream(commentAddedEvent.approvals.get())
+ .filter(PatchListProcessing::isCodeReview)
+ .filter(PatchListProcessing::isVerifyPatchset)
+ .findFirst();
+ }
+
+ default List<PatchSetApproval> getPatchListCurrentVerifications(final List<PatchSetApproval> patchSetApprovals,
+ final PatchSet.Id currentPatchsetId) {
+ return patchSetApprovals.stream()
+ .filter(approval -> approval.getPatchSetId().equals(currentPatchsetId))
+ .filter(PatchListProcessing::isCodeReview)
+ .filter(PatchListProcessing::isVerifyPatchset)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/io/fd/maintainer/plugin/util/WarningGenerator.java b/src/main/java/io/fd/maintainer/plugin/util/WarningGenerator.java
new file mode 100644
index 0000000..c3917e9
--- /dev/null
+++ b/src/main/java/io/fd/maintainer/plugin/util/WarningGenerator.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.util;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.patch.PatchListEntry;
+import io.fd.maintainer.plugin.parser.ComponentPath;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import org.parboiled.common.Tuple2;
+
+public interface WarningGenerator {
+
+ static Set<ComponentWithPath> getInvalidComponents(
+ final MaintainersIndex mappingIndex,
+ final Map.Entry<PatchListEntry, Tuple2<Set<ComponentPath>, Set<ComponentPath>>> entry) {
+ final Set<ComponentPath> oldComponents = entry.getValue().a;
+ final Set<ComponentPath> newComponents = entry.getValue().b;
+ final Sets.SetView<ComponentPath> difference = Sets.difference(oldComponents, newComponents);
+
+ return difference.immutableCopy().stream()
+ .map(path -> new ComponentWithPath(mappingIndex.getComponentForPath(path), path))
+ .collect(Collectors.toSet());
+ }
+
+ default Set<ComponentChangeWarning> generateComponentChangeWarnings(
+ @Nonnull final MaintainersIndex mappingIndex,
+ @Nonnull final Map<PatchListEntry, Tuple2<Set<ComponentPath>, Set<ComponentPath>>> renamesIndex) {
+ return renamesIndex.entrySet().stream()
+ .map(entry -> {
+ final PatchListEntry key = entry.getKey();
+ return new ComponentChangeWarning(key.getOldName(), key.getNewName(),
+ getInvalidComponents(mappingIndex, entry));
+ })
+ // if no invalid components, its valid rename/move
+ .filter(warning -> !warning.getInvalidComponents().isEmpty())
+ .collect(Collectors.toSet());
+ }
+
+ class ComponentChangeWarning {
+ private final String oldName;
+ private final String newName;
+ private final Set<ComponentWithPath> invalidComponents;
+
+ ComponentChangeWarning(final String oldName, final String newName,
+ final Set<ComponentWithPath> invalidComponents) {
+ this.oldName = oldName;
+ this.newName = newName;
+ this.invalidComponents = invalidComponents;
+ }
+
+ public String getOldName() {
+ return oldName;
+ }
+
+ public String getNewName() {
+ return newName;
+ }
+
+ public Set<ComponentWithPath> getInvalidComponents() {
+ return invalidComponents;
+ }
+ }
+
+ class ComponentWithPath {
+ private final String componentTitle;
+ private final ComponentPath path;
+
+ ComponentWithPath(final String componentTitle,
+ final ComponentPath path) {
+ this.componentTitle = componentTitle;
+ this.path = path;
+ }
+
+ public String getComponentTitle() {
+ return componentTitle;
+ }
+
+ public ComponentPath getPath() {
+ return path;
+ }
+ }
+}
diff --git a/src/test/java/io/fd/maintainer/plugin/parser/ComponentPathTest.java b/src/test/java/io/fd/maintainer/plugin/parser/ComponentPathTest.java
new file mode 100644
index 0000000..abc7062
--- /dev/null
+++ b/src/test/java/io/fd/maintainer/plugin/parser/ComponentPathTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.parser;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ComponentPathTest {
+
+ @Test
+ public void testMatchFull() {
+ final ComponentPath base = new ComponentPath("foo/bar");
+ Assert.assertEquals(ComponentPath.MatchLevel.FULL, base.matchAgainst("foo/bar"));
+ }
+
+ @Test
+ public void testMatchNone() {
+ final ComponentPath base = new ComponentPath("foo/bar");
+ Assert.assertEquals(ComponentPath.MatchLevel.NONE, base.matchAgainst("foo/bar2"));
+ }
+
+ @Test
+ public void testMatchWildcardOnly() {
+ final ComponentPath base = new ComponentPath("foo/*");
+ Assert.assertEquals(ComponentPath.MatchLevel.WILDCARD_ONLY, base.matchAgainst("foo/bar"));
+ }
+
+ @Test
+ public void testMatchWildcardOnlyLower() {
+ final ComponentPath base = new ComponentPath("foo/*");
+ Assert.assertEquals(ComponentPath.MatchLevel.WILDCARD_ONLY, base.matchAgainst("foo/de/re/li/bar"));
+ }
+
+ @Test
+ public void testMatchWildcardOnlyMixed() {
+ final ComponentPath base = new ComponentPath("foo/*");
+ Assert.assertEquals(ComponentPath.MatchLevel.WILDCARD_ONLY, base.matchAgainst("foo/de/re/li/bar.mk"));
+ }
+
+ @Test
+ public void testMatchWildcardWithExtension() {
+ final ComponentPath base = new ComponentPath("foo/*.mk");
+ Assert.assertEquals(ComponentPath.MatchLevel.WILDCARD_WITH_EXTENSION, base.matchAgainst("foo/bar2.mk"));
+ }
+
+ @Test
+ public void testMatchWildcardWithExtensionLower() {
+ final ComponentPath base = new ComponentPath("foo/*.mk");
+ Assert.assertEquals(ComponentPath.MatchLevel.WILDCARD_WITH_EXTENSION, base.matchAgainst("foo/de/bar2.mk"));
+ }
+
+ @Test
+ public void testMatchPartial() {
+ final ComponentPath base = new ComponentPath("src/vlib/");
+ Assert.assertEquals(ComponentPath.MatchLevel.PARTIAL, base.matchAgainst("src/vlib/vlib-new.file"));
+ }
+
+ @Test
+ public void testMatchNoneWildcardExtension() {
+ final ComponentPath base = new ComponentPath("src/*.am");
+ Assert.assertEquals(ComponentPath.MatchLevel.NONE, base.matchAgainst("src/vlib/vlib-new.file"));
+ }
+
+ @Test
+ public void testMatchRootWildcard() {
+ final ComponentPath base = new ComponentPath("*");
+ Assert.assertEquals(ComponentPath.MatchLevel.WILDCARD_ONLY, base.matchAgainst("src/vlib/vlib-new.file"));
+ }
+
+ @Test
+ public void testMatchRootWildcardSubdir() {
+ final ComponentPath base = new ComponentPath("*/");
+ Assert.assertEquals(ComponentPath.MatchLevel.WILDCARD_ONLY, base.matchAgainst("lisp/new-file"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/io/fd/maintainer/plugin/parser/MaintainersParserTest.java b/src/test/java/io/fd/maintainer/plugin/parser/MaintainersParserTest.java
new file mode 100644
index 0000000..0166de4
--- /dev/null
+++ b/src/test/java/io/fd/maintainer/plugin/parser/MaintainersParserTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * 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 io.fd.maintainer.plugin.parser;
+
+
+import static com.google.common.collect.ImmutableSet.of;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.junit.Test;
+
+public class MaintainersParserTest {
+
+ private static ComponentInfo vnetBfd() {
+ return componentNoComment("VNET Bidirectonal Forwarding Detection (BFD)",
+ of(m("Klement Sekera", "ksekera@cisco.com")),
+ of(p("src/vnet/bfd/")));
+ }
+
+ private static ComponentInfo vlibApiLibraries() {
+ return componentNoComment("VLIB API Libraries",
+ of(m("Dave Barach", "dave@barachs.net")),
+ of(p("src/vlibapi/"), p("src/vlibmemory/"), p("src/vlibsocket/")));
+ }
+
+ private static ComponentInfo vlibLibrary() {
+ return componentNoComment("VLIB Library",
+ of(m("Damjan Marion", "damarion@cisco.com"),
+ m("Dave Barach", "dave@barachs.net")),
+ of(p("src/vlib/")));
+ }
+
+ private static ComponentInfo infrastractureLibrary() {
+ return componentNoComment("Infrastructure Library",
+ of(m("Dave Barach", "dave@barachs.net")),
+ of(p("src/vppinfra/")));
+ }
+
+ private static ComponentInfo dpdkDevelopmentPackaging() {
+ return componentNoComment("DPDK Development Packaging",
+ of(m("Damjan Marion", "damarion@cisco.com")),
+ of(p("dpdk/"), p("dpdk/*")));
+ }
+
+ private static ComponentInfo doxygen() {
+ return componentNoComment("Doxygen",
+ of(m("Chris Luke", "chrisy@flirble.org")),
+ of(p("doxygen/")));
+ }
+
+ private static ComponentInfo buildSystemInternal() {
+ return componentNoComment("Build System Internal",
+ of(m("Dave Barach", "dave@barachs.net")),
+ of(p("build-root/Makefile"), p("build-data/*")));
+ }
+
+ private static ComponentInfo buildSystem() {
+ return componentNoComment("Build System",
+ of(m("Damjan Marion", "damarion@cisco.com")),
+ of(p("Makefile"), p("src/*.ac"), p("src/*.am"), p("src/*.mk"), p("src/m4/")));
+ }
+
+ private static Maintainer m(final String name, final String mail) {
+ return new Maintainer(name, mail);
+ }
+
+ private static ComponentPath p(final String path) {
+ return new ComponentPath(path);
+ }
+
+ private static ComponentInfo componentNoComment(final String componentTitle,
+ final Set<Maintainer> maintainers,
+ final Set<ComponentPath> components) {
+ return new ComponentInfo.ComponentInfoBuilder()
+ .setTitle(componentTitle)
+ .setMaintainers(maintainers)
+ .setPaths(components)
+ .createMaintainer();
+ }
+
+ @Test
+ public void testParse() throws URISyntaxException, IOException, MaintainerMismatchException {
+ final MaintainersParser parser = new MaintainersParser();
+
+ final URL url = this.getClass().getResource("/maintainers");
+ final String content =
+ Files.readLines(new File(url.toURI()), StandardCharsets.UTF_8).stream()
+ .collect(Collectors.joining(System.lineSeparator()));
+ final List<ComponentInfo> maintainers = parser.parseMaintainers(content);
+ assertTrue(!maintainers.isEmpty());
+
+ // tests couple of entries
+ assertTrue(compare(maintainers.get(0), buildSystem()));
+ assertTrue(compare(maintainers.get(1), buildSystemInternal()));
+ assertTrue(compare(maintainers.get(2), doxygen()));
+ assertTrue(compare(maintainers.get(3), dpdkDevelopmentPackaging()));
+ assertTrue(compare(maintainers.get(4), infrastractureLibrary()));
+ assertTrue(compare(maintainers.get(5), vlibLibrary()));
+ assertTrue(compare(maintainers.get(6), vlibApiLibraries()));
+ assertTrue(compare(maintainers.get(7), vnetBfd()));
+ assertEquals(32, maintainers.size());
+ }
+
+ private boolean compare(final ComponentInfo first, final ComponentInfo second) {
+ return new EqualsBuilder()
+ .append(first.getTitle(), second.getTitle())
+ .append(true, first.getMaintainers().containsAll(second.getMaintainers()))
+ .append(true, second.getMaintainers().containsAll(first.getMaintainers()))
+ .append(true, first.getPaths().containsAll(second.getPaths()))
+ .append(true, second.getPaths().containsAll(first.getPaths()))
+ .append(true, first.getComments().containsAll(second.getComments()))
+ .append(true, second.getComments().containsAll(first.getComments()))
+ .build();
+ }
+}
diff --git a/src/test/resources/maintainers b/src/test/resources/maintainers
new file mode 100644
index 0000000..6c7049a
--- /dev/null
+++ b/src/test/resources/maintainers
@@ -0,0 +1,160 @@
+Descriptions of section entries:
+
+ M: Maintainer Full name and E-mail address: Full Name <address@domain>
+ One maintainer per line. Multiple M: lines acceptable.
+ F: Files and directories with wildcard patterns.
+ A trailing slash includes all files and subdirectory files.
+ F: drivers/net/ all files in and below drivers/net
+ F: drivers/net/* all files in drivers/net, but not below
+ One pattern per line. Multiple F: lines acceptable.
+ C: Single line comment related to current section.
+
+ -----------------------------------
+
+Build System
+M: Damjan Marion <damarion@cisco.com>
+F: Makefile
+F: src/*.ac
+F: src/*.am
+F: src/*.mk
+F: src/m4/
+
+Build System Internal
+M: Dave Barach <dave@barachs.net>
+F: build-root/Makefile
+F: build-data/*
+
+Doxygen
+M: Chris Luke <chrisy@flirble.org>
+F: doxygen/
+
+DPDK Development Packaging
+M: Damjan Marion <damarion@cisco.com>
+F: dpdk/
+F: dpdk/*
+
+Infrastructure Library
+M: Dave Barach <dave@barachs.net>
+F: src/vppinfra/
+
+VLIB Library
+M: Dave Barach <dave@barachs.net>
+M: Damjan Marion <damarion@cisco.com>
+F: src/vlib/
+
+VLIB API Libraries
+M: Dave Barach <dave@barachs.net>
+F: src/vlibapi/
+F: src/vlibmemory/
+F: src/vlibsocket/
+
+VNET Bidirectonal Forwarding Detection (BFD)
+M: Klement Sekera <ksekera@cisco.com>
+F: src/vnet/bfd/
+
+VNET Device Drivers
+M: Damjan Marion <damarion@cisco.com>
+F: src/vnet/devices/
+
+VNET Device Drivers - DPDK Crypto
+M: Sergio Gonzalez Monroy <sergio.gonzalez.monroy@intel.com>
+F: src/devices/dpdk/ipsec/
+
+VNET Feature Arcs
+M: Dave Barach <dave@barachs.net>
+M: Damjan Marion <damarion@cisco.com>
+F: src/vnet/feature/
+
+VNET FIB
+M: Neale Ranns <nranns@cisco.com>
+F: src/vnet/fib/
+F: src/vnet/mfib/
+F: src/vnet/dpo
+F: src/vnet/adj
+
+VNET IPv4 and IPv6 LPM
+M: Dave Barach <dave@barachs.net>
+F: src/vnet/ip/
+
+VNET Segment Routing (IPv6 and MPLS)
+M: Pablo Camarillo <pcamaril@cisco.com>
+F: src/vnet/srv6/
+F: src/vnet/srmpls/
+F: src/examples/srv6-sample-localsid/
+
+VNET IPSec
+M: Sergio Gonzalez Monroy <sergio.gonzalez.monroy@intel.com>
+M: Matus Fabian <matfabia@cisco.com>
+F: src/vnet/ipsec/
+
+VNET L2
+M: John Lo <loj@cisco.com>
+F: src/vnet/l2/
+
+VNET Link Layer Discovery Protocol (LLDP)
+M: Klement Sekera <ksekera@cisco.com>
+F: src/vnet/lldp/
+
+VNET LISP
+M: Florin Coras <fcoras@cisco.com>
+F: src/vnet/lisp-cp/
+F: src/vnet/lisp-gpe/
+
+VNET MAP
+M: Ole Troan <ot@cisco.com>
+F: src/vnet/map
+
+VNET MPLS
+M: Neale Ranns <nranns@cisco.com>
+F: src/vnet/mpls/
+
+VNET VXLAN
+M: John Lo <loj@cisco.com>
+F: src/vnet/vxlan/
+
+Plugin - flowperpkt
+M: Dave Barach <dave@barachs.net>
+F: src/plugins/flowperpkt/
+F: src/plugins/flowperpkt.am
+
+Plugin - SIXRD
+M: Ole Troan <ot@cisco.com>
+F: src/plugins/sixrd/
+F: src/plugins/sixrd.am
+
+Test Infrastructure
+M: Klement Sekera <ksekera@cisco.com>
+F: test/
+
+SVM Library
+M: Dave Barach <dave@barachs.net>
+F: src/svm
+
+VPP API TEST
+M: Dave Barach <dave@barachs.net>
+F: src/vat/
+
+VPP Executable
+M: Dave Barach <dave@barachs.net>
+F: src/vpp/
+
+Graphical Event Viewer
+M: Dave Barach <dave@barachs.net>
+F: src/tools/g2/
+
+Performance Tooling
+M: Dave Barach <dave@barachs.net>
+F: src/tools/perftool/
+
+Binary API Compiler
+M: Dave Barach <dave@barachs.net>
+F: src/tools/vppapigen/
+
+Ganglia Telemetry Module
+M: Dave Barach <dave@barachs.net>
+F: gmod/
+
+THE REST
+C: Contact vpp-dev Mailing List <vpp-dev@fd.io>
+F: *
+F: */
\ No newline at end of file
diff --git a/target/test-classes/maintainers b/target/test-classes/maintainers
new file mode 100644
index 0000000..6c7049a
--- /dev/null
+++ b/target/test-classes/maintainers
@@ -0,0 +1,160 @@
+Descriptions of section entries:
+
+ M: Maintainer Full name and E-mail address: Full Name <address@domain>
+ One maintainer per line. Multiple M: lines acceptable.
+ F: Files and directories with wildcard patterns.
+ A trailing slash includes all files and subdirectory files.
+ F: drivers/net/ all files in and below drivers/net
+ F: drivers/net/* all files in drivers/net, but not below
+ One pattern per line. Multiple F: lines acceptable.
+ C: Single line comment related to current section.
+
+ -----------------------------------
+
+Build System
+M: Damjan Marion <damarion@cisco.com>
+F: Makefile
+F: src/*.ac
+F: src/*.am
+F: src/*.mk
+F: src/m4/
+
+Build System Internal
+M: Dave Barach <dave@barachs.net>
+F: build-root/Makefile
+F: build-data/*
+
+Doxygen
+M: Chris Luke <chrisy@flirble.org>
+F: doxygen/
+
+DPDK Development Packaging
+M: Damjan Marion <damarion@cisco.com>
+F: dpdk/
+F: dpdk/*
+
+Infrastructure Library
+M: Dave Barach <dave@barachs.net>
+F: src/vppinfra/
+
+VLIB Library
+M: Dave Barach <dave@barachs.net>
+M: Damjan Marion <damarion@cisco.com>
+F: src/vlib/
+
+VLIB API Libraries
+M: Dave Barach <dave@barachs.net>
+F: src/vlibapi/
+F: src/vlibmemory/
+F: src/vlibsocket/
+
+VNET Bidirectonal Forwarding Detection (BFD)
+M: Klement Sekera <ksekera@cisco.com>
+F: src/vnet/bfd/
+
+VNET Device Drivers
+M: Damjan Marion <damarion@cisco.com>
+F: src/vnet/devices/
+
+VNET Device Drivers - DPDK Crypto
+M: Sergio Gonzalez Monroy <sergio.gonzalez.monroy@intel.com>
+F: src/devices/dpdk/ipsec/
+
+VNET Feature Arcs
+M: Dave Barach <dave@barachs.net>
+M: Damjan Marion <damarion@cisco.com>
+F: src/vnet/feature/
+
+VNET FIB
+M: Neale Ranns <nranns@cisco.com>
+F: src/vnet/fib/
+F: src/vnet/mfib/
+F: src/vnet/dpo
+F: src/vnet/adj
+
+VNET IPv4 and IPv6 LPM
+M: Dave Barach <dave@barachs.net>
+F: src/vnet/ip/
+
+VNET Segment Routing (IPv6 and MPLS)
+M: Pablo Camarillo <pcamaril@cisco.com>
+F: src/vnet/srv6/
+F: src/vnet/srmpls/
+F: src/examples/srv6-sample-localsid/
+
+VNET IPSec
+M: Sergio Gonzalez Monroy <sergio.gonzalez.monroy@intel.com>
+M: Matus Fabian <matfabia@cisco.com>
+F: src/vnet/ipsec/
+
+VNET L2
+M: John Lo <loj@cisco.com>
+F: src/vnet/l2/
+
+VNET Link Layer Discovery Protocol (LLDP)
+M: Klement Sekera <ksekera@cisco.com>
+F: src/vnet/lldp/
+
+VNET LISP
+M: Florin Coras <fcoras@cisco.com>
+F: src/vnet/lisp-cp/
+F: src/vnet/lisp-gpe/
+
+VNET MAP
+M: Ole Troan <ot@cisco.com>
+F: src/vnet/map
+
+VNET MPLS
+M: Neale Ranns <nranns@cisco.com>
+F: src/vnet/mpls/
+
+VNET VXLAN
+M: John Lo <loj@cisco.com>
+F: src/vnet/vxlan/
+
+Plugin - flowperpkt
+M: Dave Barach <dave@barachs.net>
+F: src/plugins/flowperpkt/
+F: src/plugins/flowperpkt.am
+
+Plugin - SIXRD
+M: Ole Troan <ot@cisco.com>
+F: src/plugins/sixrd/
+F: src/plugins/sixrd.am
+
+Test Infrastructure
+M: Klement Sekera <ksekera@cisco.com>
+F: test/
+
+SVM Library
+M: Dave Barach <dave@barachs.net>
+F: src/svm
+
+VPP API TEST
+M: Dave Barach <dave@barachs.net>
+F: src/vat/
+
+VPP Executable
+M: Dave Barach <dave@barachs.net>
+F: src/vpp/
+
+Graphical Event Viewer
+M: Dave Barach <dave@barachs.net>
+F: src/tools/g2/
+
+Performance Tooling
+M: Dave Barach <dave@barachs.net>
+F: src/tools/perftool/
+
+Binary API Compiler
+M: Dave Barach <dave@barachs.net>
+F: src/tools/vppapigen/
+
+Ganglia Telemetry Module
+M: Dave Barach <dave@barachs.net>
+F: gmod/
+
+THE REST
+C: Contact vpp-dev Mailing List <vpp-dev@fd.io>
+F: *
+F: */
\ No newline at end of file