diff --git a/.gitignore b/.gitignore index 94eab12..c4ff305 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ pom.xml.tag pom.xml.releaseBackup pom.xml.next release.properties +work diff --git a/gitlab-jenkins.iml b/gitlab-jenkins.iml index 2afdf58..28e214f 100644 --- a/gitlab-jenkins.iml +++ b/gitlab-jenkins.iml @@ -4,128 +4,148 @@ + - + + + - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -180,13 +200,10 @@ - - - @@ -210,7 +227,6 @@ - @@ -224,7 +240,6 @@ - diff --git a/pom.xml b/pom.xml index 40507e0..d340739 100644 --- a/pom.xml +++ b/pom.xml @@ -1,22 +1,17 @@ 4.0.0 - - - org.jenkins-ci.main - jenkins-core - 1.532.3 - - - org.jenkins-ci.plugins - plugin - 1.532.3 - + org.jenkins-ci.plugins + plugin + 1.532.3 + com.dabsquared gitlab-jenkins 1.0-SNAPSHOT hpi + GitLab Plugin + http://wiki.jenkins-ci.org/display/JENKINS/Gitlab+Plugin @@ -25,12 +20,30 @@ + + + bass_rock + Daniel Brooks + + + + + scm:git:git://github.com/DABSquared/gitlab-jenkins-plugin.git + scm:git:git@github.com:DABSquared/gitlab-jenkins-plugin.git + https://github.com/DABSquared/gitlab-jenkins-plugin + + - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + jgit-repository + Eclipse JGit Repository + http://download.eclipse.org/jgit/maven + @@ -40,4 +53,32 @@ + + + org.jenkins-ci.plugins + git + 2.2.1 + + + org.jenkins-ci.plugins + multiple-scms + 0.2 + true + + + junit + junit + 4.11 + test + + + org.mockito + mockito-all + 1.9.5 + test + + + + + diff --git a/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushCause.java b/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushCause.java deleted file mode 100644 index ae80e91..0000000 --- a/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushCause.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.dabsquared.gitlabjenkins; - -import hudson.triggers.SCMTrigger; - -import java.io.File; -import java.io.IOException; - -/** - * UI object that says a build is started by GitHub post-commit hook. - * - * @author Daniel Brooks - */ -public class GitLabPushCause extends SCMTrigger.SCMTriggerCause { - /** - * The name of the user who pushed to GitHub. - */ - private String pushedBy; - - public GitLabPushCause(String pusher) { - this("", pusher); - } - - public GitLabPushCause(String pollingLog, String pusher) { - super(pollingLog); - pushedBy = pusher; - } - - public GitLabPushCause(File pollingLog, String pusher) throws IOException { - super(pollingLog); - pushedBy = pusher; - } - - @Override - public String getShortDescription() { - String pusher = pushedBy != null ? pushedBy : ""; - return "Started by GitLab push by " + pusher; - } -} diff --git a/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushRequest.java b/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushRequest.java new file mode 100644 index 0000000..54bda91 --- /dev/null +++ b/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushRequest.java @@ -0,0 +1,304 @@ +package com.dabsquared.gitlabjenkins; + +import net.sf.json.JSONObject; +import net.sf.json.JsonConfig; +import net.sf.json.util.JavaIdentifierTransformer; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents for WebHook payload + * + * @author Daniel Brooks + */ +public class GitLabPushRequest { + + private User pusher; + + private String ref; + + private List commits; + + private Repository repository; + + public static GitLabPushRequest create(String payload) { + if (payload == null) { + throw new IllegalArgumentException("payload should not be null"); + } + return create(JSONObject.fromObject(payload)); + } + + public static GitLabPushRequest create(JSONObject payload) { + if (payload == null || payload.isNullObject()) { + throw new IllegalArgumentException("payload should not be null"); + } + + JsonConfig config = createJsonConfig(); + return (GitLabPushRequest) JSONObject.toBean(payload, config); + } + + private static JsonConfig createJsonConfig() { + JsonConfig config = new JsonConfig(); + config.setRootClass(GitLabPushRequest.class); + + Map> classMap = new HashMap>(); + classMap.put("commits", Commit.class); + classMap.put("added", String.class); + classMap.put("removed", String.class); + classMap.put("modified", String.class); + config.setClassMap(classMap); + + config.setJavaIdentifierTransformer(new JavaIdentifierTransformer() { + + @Override + public String transformToJavaIdentifier(String param) { + if (param == null) { + return null; + } + if ("private".equals(param)) { + return "private_"; + } + return param; + } + + }); + + return config; + } + + public GitLabPushRequest() { + } + + public User getPusher() { + return pusher; + } + + public void setPusher(User pusher) { + this.pusher = pusher; + } + + public String getRef() { + return ref; + } + + public void setRef(String ref) { + this.ref = ref; + } + + public List getCommits() { + return commits; + } + + public Commit getLastCommit() { + if (commits.isEmpty()) { + return null; + } + return commits.get(commits.size() - 1); + } + + public void setCommits(List commits) { + this.commits = commits; + } + + public Repository getRepository() { + return repository; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + public static class Repository { + + private String name; + + private String url; + + private String description; + + private Integer forks; + + private boolean private_; + + private User owner; + + public Repository() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getForks() { + return forks; + } + + public void setForks(Integer forks) { + this.forks = forks; + } + + public boolean isPrivate_() { + return private_; + } + + public void setPrivate_(boolean private_) { + this.private_ = private_; + } + + public User getOwner() { + return owner; + } + + public void setOwner(User owner) { + this.owner = owner; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + } + + public static class Commit { + + private String id; + + private String message; + + private String timestamp; + + private String url; + + private List added; + + private List removed; + + private List modified; + + public Commit() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public List getAdded() { + return added; + } + + public void setAdded(List added) { + this.added = added; + } + + public List getRemoved() { + return removed; + } + + public void setRemoved(List removed) { + this.removed = removed; + } + + public List getModified() { + return modified; + } + + public void setModified(List modified) { + this.modified = modified; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + } + + public static class User { + + private String name; + + private String email; + + public User() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + } +} diff --git a/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java b/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java index 4f10c8c..53e2c68 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/GitLabPushTrigger.java @@ -5,141 +5,191 @@ import hudson.Util; import hudson.console.AnnotatedLargeText; import hudson.model.AbstractProject; import hudson.model.Action; -import hudson.model.Hudson; import hudson.model.Item; +import hudson.plugins.git.RevisionParameterAction; +import hudson.triggers.SCMTrigger; +import hudson.triggers.SCMTrigger.SCMTriggerCause; + import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.util.SequentialExecutionQueue; import hudson.util.StreamTaskListener; -import net.sf.json.JSONObject; -import org.apache.commons.jelly.XMLOutput; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.StaplerRequest; -import sun.misc.Cleaner; - import java.io.File; import java.io.IOException; import java.io.PrintStream; -import java.net.MalformedURLException; -import java.net.URL; import java.nio.charset.Charset; import java.text.DateFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.DataBoundConstructor; + +import jenkins.model.Jenkins.MasterComputer; + +import org.apache.commons.jelly.XMLOutput; + +import com.dabsquared.gitlabjenkins.GitLabPushRequest.Commit; + /** - * Triggers a build when we receive a GitHub post-commit webhook. + * Triggers a build when we receive a GitLab WebHook. * * @author Daniel Brooks */ -public class GitLabPushTrigger extends Trigger> implements GitLabTrigger { +public class GitLabPushTrigger extends Trigger> { + @DataBoundConstructor public GitLabPushTrigger() { } - /** - * Called when a POST is made. - */ - @Deprecated - public void onPost() { - onPost(""); - } - - /** - * Called when a POST is made. - */ - public void onPost(String triggeredByUser) { - final String pushBy = triggeredByUser; + public void onPost(final GitLabPushRequest req) { getDescriptor().queue.execute(new Runnable() { - private boolean runPolling() { + private boolean polling() { try { StreamTaskListener listener = new StreamTaskListener(getLogFile()); try { PrintStream logger = listener.getLogger(); + long start = System.currentTimeMillis(); - logger.println("Started on "+ DateFormat.getDateTimeInstance().format(new Date())); + logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date())); boolean result = job.poll(listener).hasChanges(); - logger.println("Done. Took "+ Util.getTimeSpanString(System.currentTimeMillis() - start)); - if(result) + logger.println("Done. Took " + Util.getTimeSpanString(System.currentTimeMillis() - start)); + + if (result) { logger.println("Changes found"); - else + } else { logger.println("No changes"); + } + return result; } catch (Error e) { e.printStackTrace(listener.error("Failed to record SCM polling")); - LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); + LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e); throw e; } catch (RuntimeException e) { e.printStackTrace(listener.error("Failed to record SCM polling")); - LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); + LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e); throw e; } finally { - listener.close(); + listener.closeQuietly(); } } catch (IOException e) { - LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); + LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e); } + return false; } public void run() { - if (runPolling()) { - String name = " #"+job.getNextBuildNumber(); - GitLabPushCause cause; - try { - cause = new GitLabPushCause(getLogFile(), pushBy); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to parse the polling log",e); - cause = new GitLabPushCause(pushBy); - } - if (job.scheduleBuild(cause)) { - LOGGER.info("SCM changes detected in "+ job.getName()+". Triggering "+name); + LOGGER.log(Level.INFO, "{0} triggered.", job.getName()); + if (polling()) { + String name = " #" + job.getNextBuildNumber(); + GitLabPushCause cause = createGitLabPushCause(req); + if (job.scheduleBuild(job.getQuietPeriod(), cause)) { + LOGGER.log(Level.INFO, "SCM changes detected in {0}. Triggering {1}", new String[]{job.getName(), name}); } else { - LOGGER.info("SCM changes detected in "+ job.getName()+". Job is already in the queue"); + LOGGER.log(Level.INFO, "SCM changes detected in {0}. Job is already in the queue.", job.getName()); } } } + + private GitLabPushCause createGitLabPushCause(GitLabPushRequest req) { + GitLabPushCause cause; + String triggeredByUser = req.getPusher().getName(); + try { + cause = new GitLabPushCause(triggeredByUser, getLogFile()); + } catch (IOException ex) { + cause = new GitLabPushCause(triggeredByUser); + } + return cause; + } + }); } - /** - * Returns the file that records the last/current polling activity. - */ - public File getLogFile() { - return new File(job.getRootDir(),"gitlab-polling.log"); - } - @Override - public void start(AbstractProject project, boolean newInstance) { - super.start(project, newInstance); - if (newInstance) { - - } + public Collection getProjectActions() { + return Collections.singletonList(new GitLabWebHookPollingAction()); } - - @Override - public void stop() { - - } - - @Override public DescriptorImpl getDescriptor() { - return (DescriptorImpl)super.getDescriptor(); + return DescriptorImpl.get(); + } + + public File getLogFile() { + return new File(job.getRootDir(), "gitlab-polling.log"); + } + + private static final Logger LOGGER = Logger.getLogger(GitLabPushTrigger.class.getName()); + + + public class GitLabWebHookPollingAction implements Action { + + public AbstractProject getOwner() { + return job; + } + + public String getIconFileName() { + return "/plugin/gitlab/images/24x24/gitlab-log.png"; + } + + public String getDisplayName() { + return "GitLab Hook Log"; + } + + public String getUrlName() { + return "GitLabPollLog"; + } + + public String getLog() throws IOException { + return Util.loadFile(getLogFile()); + } + + public void writeLogTo(XMLOutput out) throws IOException { + new AnnotatedLargeText( + getLogFile(), Charset.defaultCharset(), true, this).writeHtmlTo(0, out.asWriter()); + } + } + + public static class GitLabPushCause extends SCMTriggerCause { + + private final String pushedBy; + + public GitLabPushCause(String pushedBy) { + this.pushedBy = pushedBy; + } + + public GitLabPushCause(String pushedBy, File logFile) throws IOException { + super(logFile); + this.pushedBy = pushedBy; + } + + public GitLabPushCause(String pushedBy, String pollingLog) { + super(pollingLog); + this.pushedBy = pushedBy; + } + + @Override + public String getShortDescription() { + if (pushedBy == null) { + return "Started by GitLab push"; + } else { + return String.format("Started by GitLab push by %s", pushedBy); + } + } } @Extension public static class DescriptorImpl extends TriggerDescriptor { - private transient final SequentialExecutionQueue queue = new SequentialExecutionQueue(Hudson.MasterComputer.threadPoolForRemoting); - private String hookUrl; - - public DescriptorImpl() { - load(); - } + private transient final SequentialExecutionQueue queue = new SequentialExecutionQueue(Jenkins.MasterComputer.threadPoolForRemoting); @Override public boolean isApplicable(Item item) { @@ -151,44 +201,9 @@ public class GitLabPushTrigger extends Trigger> implements return "Build when a change is pushed to GitLab"; } - /** - * Returns the URL that GitLab should post. - */ - public URL getHookUrl() throws MalformedURLException { - return hookUrl!=null ? new URL(hookUrl) : new URL(Hudson.getInstance().getRootUrl()+GitLabWebHook.get().getUrlName()+'/'); - } - - public boolean hasOverrideURL() { - return hookUrl!=null; - } - - - @Override - public boolean configure(StaplerRequest req, JSONObject json) throws FormException { - JSONObject hookMode = json.getJSONObject("hookMode"); - JSONObject o = hookMode.getJSONObject("c"); - if (o!=null && !o.isNullObject()) { - hookUrl = o.getString("url"); - } else { - hookUrl = null; - } - save(); - return true; - } - public static DescriptorImpl get() { return Trigger.all().get(DescriptorImpl.class); } - public static boolean allowsHookUrlOverride() { - return ALLOW_HOOKURL_OVERRIDE; - } } - - /** - * Set to false to prevent the user from overriding the hook URL. - */ - public static boolean ALLOW_HOOKURL_OVERRIDE = !Boolean.getBoolean(GitLabPushTrigger.class.getName()+".disableOverride"); - - private static final Logger LOGGER = Logger.getLogger(GitLabPushTrigger.class.getName()); } diff --git a/src/main/java/com/dabsquared/gitlabjenkins/GitLabWebHook.java b/src/main/java/com/dabsquared/gitlabjenkins/GitLabWebHook.java index c5a0707..c8ae803 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/GitLabWebHook.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/GitLabWebHook.java @@ -1,10 +1,26 @@ package com.dabsquared.gitlabjenkins; +import hudson.Extension; import hudson.ExtensionPoint; +import hudson.model.AbstractProject; import hudson.model.Hudson; import hudson.model.RootAction; import hudson.model.UnprotectedRootAction; +import hudson.security.ACL; +import hudson.security.csrf.CrumbExclusion; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.acegisecurity.Authentication; +import org.acegisecurity.context.SecurityContextHolder; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.interceptor.RequirePOST; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -12,6 +28,10 @@ import java.util.logging.Logger; * @author Daniel Brooks */ public class GitLabWebHook implements UnprotectedRootAction { + + public static final String WEBHOOK_URL = "gitlab-webhook"; + + public String getIconFileName() { return null; } @@ -21,32 +41,74 @@ public class GitLabWebHook implements UnprotectedRootAction { } public String getUrlName() { - return "/projects/"; + return WEBHOOK_URL; + } + + @RequirePOST + public void doIndex(StaplerRequest req) { + LOGGER.log(Level.FINE, "WebHook called."); + + String payload = req.getParameter("payload"); + if (payload == null) { + throw new IllegalArgumentException( + "Not intended to be browsed interactively (must specify payload parameter)"); + } + + //processPayload(payload); + } + + + private void processPayload(String payload) { + JSONObject json = JSONObject.fromObject(payload); + LOGGER.log(Level.FINE, "payload: {0}", json.toString(4)); + + GitLabPushRequest req = GitLabPushRequest.create(json); + + String repositoryUrl = req.getRepository().getUrl(); + if (repositoryUrl == null) { + LOGGER.log(Level.WARNING, "No repository url found."); + return; + } + + Authentication old = SecurityContextHolder.getContext().getAuthentication(); + SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM); + try { + for (AbstractProject job : Jenkins.getInstance().getAllItems(AbstractProject.class)) { + GitLabPushTrigger trigger = job.getTrigger(GitLabPushTrigger.class); + if (trigger == null) { + continue; + } + //if (RepositoryUrlCollector.collect(job).contains(repositoryUrl.toLowerCase())) { + trigger.onPost(req); + //} + } + } finally { + SecurityContextHolder.getContext().setAuthentication(old); + } + } + + + @Extension + public static class GitLabWebHookCrumbExclusion extends CrumbExclusion { + + @Override + public boolean process(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws IOException, ServletException { + String pathInfo = req.getPathInfo(); + LOGGER.log(Level.FINE, "path: {0}", pathInfo); + + if (pathInfo != null && pathInfo.equals(getExclusionPath())) { + chain.doFilter(req, resp); + return true; + } + return false; + } + + private String getExclusionPath() { + return '/' + WEBHOOK_URL + '/'; + } } private static final Logger LOGGER = Logger.getLogger(GitLabWebHook.class.getName()); - public static GitLabWebHook get() { - return Hudson.getInstance().getExtensionList(RootAction.class).get(GitLabWebHook.class); - } - - /** - * Other plugins may be interested in listening for these updates. - * - * @since 1.8 - */ - public static abstract class Listener implements ExtensionPoint { - - /** - * Called when there is a change notification on a specific repository. - * - * @param pusherName the pusher name. - * @param changedRepository the changed repository. - * @since 1.8 - */ - public abstract void onPushRepositoryChanged(String pusherName, String changedRepository); - } - - } diff --git a/src/main/java/com/dabsquared/gitlabjenkins/GitlabTrigger.java b/src/main/java/com/dabsquared/gitlabjenkins/GitlabTrigger.java deleted file mode 100644 index dd2bb4d..0000000 --- a/src/main/java/com/dabsquared/gitlabjenkins/GitlabTrigger.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.dabsquared.gitlabjenkins; - -import hudson.Extension; -import hudson.Util; -import hudson.model.AbstractProject; - -import java.util.Collection; -import java.util.Set; - -/** - * Optional interface that can be implemented by {@link hudson.triggers.Trigger} that watches out for a change in GitHub - * and triggers a build. - * - * @author Daniel Brooks - */ -public interface GitLabTrigger { - - @Deprecated - public void onPost(); - - // TODO: document me - public void onPost(String triggeredByUser); - -} \ No newline at end of file diff --git a/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/GitLabWebHookPollingAction/index.jelly b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/GitLabWebHookPollingAction/index.jelly new file mode 100644 index 0000000..b742e86 --- /dev/null +++ b/src/main/resources/com/dabsquared/gitlabjenkins/GitLabPushTrigger/GitLabWebHookPollingAction/index.jelly @@ -0,0 +1,44 @@ + + + + + + +

${%Last GitLab Push}

+ + + + ${%Polling has not run yet.} + + +
+            
+            ${it.writeLogTo(output)}
+          
+
+
+
+
+
\ No newline at end of file