Initial commit
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f840f74
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright 2017 Stephen Connolly.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.jenkins-ci.plugins</groupId>
+ <artifactId>plugin</artifactId>
+ <version>2.19</version>
+ <relativePath />
+ </parent>
+
+ <groupId>io.github.stephenc.apache</groupId>
+ <artifactId>asf-gitpubsub-jenkins</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>hpi</packaging>
+
+ <name>ASF GitPubSub Jenkins Notifications</name>
+ <description>Converts the Apache GitPubSub stream into SCM API Events</description>
+ <url>https://github.com/stephenc/asf-gitpubsub-jenkins-plugin</url>
+ <licenses>
+ <license>
+ <name>Apache License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <scm>
+ <connection>scm:git:git://github.com/stephenc/asf-gitpubsub-jenkins-plugin.git</connection>
+ <developerConnection>scm:git:git@github.com:stephenc/asf-gitpubsub-jenkins-plugin.git</developerConnection>
+ <url>https://github.com/stephenc/asf-gitpubsub-jenkins-plugin</url>
+ <tag>HEAD</tag>
+ </scm>
+
+ <properties>
+ <jenkins.version>1.642.3</jenkins.version>
+ </properties>
+
+ <repositories>
+ <repository>
+ <id>repo.jenkins-ci.org</id>
+ <url>https://repo.jenkins-ci.org/public/</url>
+ </repository>
+ </repositories>
+ <pluginRepositories>
+ <pluginRepository>
+ <id>repo.jenkins-ci.org</id>
+ <url>https://repo.jenkins-ci.org/public/</url>
+ </pluginRepository>
+ </pluginRepositories>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jenkins-ci.plugins</groupId>
+ <artifactId>scm-api</artifactId>
+ <version>2.1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jenkins-ci.plugins</groupId>
+ <artifactId>git</artifactId>
+ <version>3.1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jenkins-ci.plugins</groupId>
+ <artifactId>async-http-client</artifactId>
+ <version>1.7.24.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jenkins-ci.plugins</groupId>
+ <artifactId>jackson2-api</artifactId>
+ <version>2.7.3</version>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/src/main/java/org/apache/jenkins/gitpubsub/GitPubSubPoll.java b/src/main/java/org/apache/jenkins/gitpubsub/GitPubSubPoll.java
new file mode 100644
index 0000000..e8b8163
--- /dev/null
+++ b/src/main/java/org/apache/jenkins/gitpubsub/GitPubSubPoll.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2017 Stephen Connolly.
+ *
+ * Licensed under the Apache License,Version2.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 org.apache.jenkins.gitpubsub;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ning.http.client.AsyncCompletionHandlerBase;
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.HttpResponseBodyPart;
+import com.ning.http.client.Response;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.Util;
+import hudson.model.AsyncPeriodicWork;
+import hudson.model.TaskListener;
+import hudson.plugins.git.GitStatus;
+import hudson.scm.SCM;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.plugins.asynchttpclient.AHC;
+import jenkins.plugins.git.AbstractGitSCMSource;
+import jenkins.plugins.git.GitSCMSource;
+import jenkins.scm.api.SCMEvent;
+import jenkins.scm.api.SCMHead;
+import jenkins.scm.api.SCMHeadEvent;
+import jenkins.scm.api.SCMNavigator;
+import jenkins.scm.api.SCMRevision;
+import jenkins.scm.api.SCMSource;
+import org.eclipse.jgit.transport.URIish;
+
+@Extension
+public class GitPubSubPoll extends AsyncPeriodicWork {
+ private static final Logger LOGGER = Logger.getLogger(GitPubSubPoll.class.getName());
+ private static long periodSeconds = Long.getLong(GitPubSubPoll.class.getName() + ".periodSeconds", 10);
+
+ private volatile long lastTS;
+ private volatile long lastTime;
+ private Future<?> longPollRequest;
+
+ public GitPubSubPoll() {
+ super("ASG GitPubSub poll");
+ }
+
+ @Override
+ protected void execute(TaskListener listener) throws IOException, InterruptedException {
+ if (longPollRequest != null) {
+ if (lastTime - System.currentTimeMillis() > TimeUnit.SECONDS.toMillis(60)) {
+ longPollRequest.cancel(false);
+ }
+ if (!longPollRequest.isDone()) {
+ // still alive
+ return;
+ }
+ }
+ AsyncHttpClient.BoundRequestBuilder builder1 =
+ AHC.instance().prepareGet("http://gitpubsub-wip.apache.org:2069/json/*");
+ if (lastTS != 0) {
+ builder1.addHeader("X-Fetch-Since", Long.toString(lastTS));
+ }
+ longPollRequest = AHC.instance().executeRequest(builder1.build(), new JsonHandler());
+ }
+
+ @Override
+ public long getRecurrencePeriod() {
+ return TimeUnit.SECONDS.toMillis(Math.min(3600, Math.max(1, periodSeconds)));
+ }
+
+ @Override
+ protected Level getNormalLoggingLevel() {
+ return Level.FINE;
+ }
+
+ @Override
+ protected Level getSlowLoggingLevel() {
+ return Level.FINE;
+ }
+
+ @Override
+ protected Level getErrorLoggingLevel() {
+ return Level.INFO;
+ }
+
+ private class JsonHandler extends AsyncCompletionHandlerBase {
+
+ private ObjectMapper mapper = new ObjectMapper();
+
+ @Override
+ public Response onCompleted(Response response) throws Exception {
+ LOGGER.log(Level.FINE, "Received GitPubSub Closed");
+ return super.onCompleted(response);
+ }
+
+ @Override
+ public STATE onBodyPartReceived(HttpResponseBodyPart content) throws Exception {
+ JsonNode json = mapper.readTree(content.getBodyPartBytes());
+ LOGGER.log(Level.FINE, "Received GitPubSub event {0}",
+ mapper.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+ for (Iterator<Map.Entry<String, JsonNode>> it = json.fields(); it.hasNext(); ) {
+ Map.Entry<String, JsonNode> field = it.next();
+ String fieldName = field.getKey();
+ final JsonNode fieldValue = field.getValue();
+ try {
+ if ("stillalive".equals(fieldName)) {
+ lastTS = fieldValue.asLong();
+ lastTime = System.currentTimeMillis();
+ } else if ("commit".equals(fieldName)) {
+ if ("git".equals(fieldValue.get("repository").textValue())
+ && fieldValue.has("project")
+ && fieldValue.get("ref").asText().startsWith("refs/heads/")) {
+ SCMHeadEvent.fireNow(
+ new SCMHeadEvent<JsonNode>(SCMEvent.Type.UPDATED, fieldValue, "GitPubSub") {
+ @Override
+ public boolean isMatch(@NonNull SCMNavigator navigator) {
+ return false;
+ }
+
+ @NonNull
+ @Override
+ public String getSourceName() {
+ return getPayload().get("project").asText();
+ }
+
+ @NonNull
+ @Override
+ public Map<SCMHead, SCMRevision> heads(@NonNull SCMSource source) {
+ if (source instanceof GitSCMSource) {
+ GitSCMSource src = (GitSCMSource) source;
+ URIish remote;
+ try {
+ remote = new URIish(src.getRemote());
+ } catch (URISyntaxException e) {
+ return Collections.emptyMap();
+ }
+ URIish event;
+ try {
+ event = new URIish(
+ "https://"
+ + getPayload().get("server").asText() +
+ ".apache.org/repos/asf/"
+ + Util
+ .rawEncode(getPayload().get("project").asText())
+ + ".git"
+ );
+ } catch (URISyntaxException e) {
+ return Collections.emptyMap();
+ }
+
+ if (GitStatus.looselyMatches(event, remote)) {
+ String ref = getPayload().get("ref").asText();
+ SCMHead head = new SCMHead(ref.substring("refs/heads/".length()));
+ String sha1 = getPayload().get("sha").asText();
+ return Collections.<SCMHead, SCMRevision>singletonMap(head,
+ sha1 != null ? new AbstractGitSCMSource.SCMRevisionImpl(
+ head, sha1) : null);
+ }
+ }
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public boolean isMatch(@NonNull SCM scm) {
+ return false;
+ }
+ });
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.log(Level.WARNING, "Uncaught exception", e);
+ }
+ }
+ return STATE.CONTINUE;
+ }
+ }
+}
diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly
new file mode 100644
index 0000000..2c8e960
--- /dev/null
+++ b/src/main/resources/index.jelly
@@ -0,0 +1,19 @@
+<!--
+ ~ Copyright 2017 Stephen Connolly.
+ ~
+ ~ 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.
+ -->
+
+<div>
+ Converts the Apache <a href="https://www.apache.org/dev/gitpubsub.html">GitPubSub</a> stream into SCM API Events.
+</div>