package hudson.plugins.mercurial; import com.google.common.base.Objects; import com.google.common.collect.Lists; import hudson.Extension; import hudson.model.AbstractModelObject; import hudson.model.Item; import hudson.model.UnprotectedRootAction; import hudson.scm.SCM; import hudson.security.ACL; import hudson.triggers.SCMTrigger; import jenkins.scm.api.SCMEvent; import jenkins.scm.api.SCMHeadEvent; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletException; import java.io.IOException; import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import static javax.servlet.http.HttpServletResponse.*; import jenkins.model.Jenkins; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.SCMSourceOwners; import jenkins.triggers.SCMTriggerItem; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; /** * Information screen for the use of Mercurial in Jenkins. */ @Extension public class MercurialStatus extends AbstractModelObject implements UnprotectedRootAction { public static final String URL_NAME = "mercurial"; public String getDisplayName() { return Messages.MercurialStatus_mercurial(); } public String getSearchUrl() { return getUrlName(); } public String getIconFileName() { return null; } public String getUrlName() { return URL_NAME; } static private boolean isUnexpandedEnvVar(String str) { return str.startsWith("$"); } static boolean looselyMatches(URI notifyUri, String repository) { boolean result = false; try { if (!isUnexpandedEnvVar(repository)) { URI repositoryUri = new URI(repository); result = Objects.equal(notifyUri.getHost(), repositoryUri.getHost()) && Objects.equal(StringUtils.stripEnd(notifyUri.getPath(), "/"), StringUtils.stripEnd(repositoryUri.getPath(), "/")) && Objects.equal(notifyUri.getQuery(), repositoryUri.getQuery()); } } catch ( URISyntaxException ex ) { LOGGER.log(Level.SEVERE, "could not parse repository uri " + repository, ex); } return result; } /** * Handles the incoming commit notification. <strong>NOTE:</strong> This handles two types of notification: * <ul> * <li>Legacy notification such as from hooks like: * <pre> * commit.jenkins = wget -q -O /dev/null <jenkins root>/mercurial/notifyCommit?url=<repository remote url> * </pre> * </li> * <li>Modern notifications such as from hooks like: * <pre> * commit.jenkins = python:<path to hook.py> * </pre> * using an in-process hook such as either * <pre> * import urilib * import urilib2 * * def commit(ui, repo, node, **kwargs): * data = { * 'url': '<repository remote url>', * 'branch': repo[node].branch(), * 'changesetId': node, * } * req = urllib2.Request('<jenkins root>/mercurial/notifyCommit') * urllib2.urlopen(req, urllib.urlencode(data)).read() * pass * </pre> * or * <pre> * import requests * * def commit(ui, repo, node, **kwargs): * requests.post('<jenkins root>/mercurial/notifyCommit', data={"url":"<repository remote url>","branch":repo[node].branch(),"changesetId":node}) * pass * </pre> * </li> * </ul> * When used with a legacy notification, multi-branch jobs will be forced to perform full indexing, whereas when * used with a modern notification that includes the branch and changesetId then the notification will be processed * using the SCM API event subsystem resulting in a much more scoped and efficient processing of the event. * * @param url the URL of the mercurial repository * @param branch (optional) branch name of the commit. * @param changesetId (optional) changesetId of the commit. * @return the HTTP response * @throws ServletException if something goes wrong. * @throws IOException if something goes wrong. */ @Restricted(NoExternalUse.class) // Exposed by Stapler, not for direct invocation public HttpResponse doNotifyCommit(@QueryParameter(required=true) final String url, @QueryParameter String branch, @QueryParameter String changesetId) throws ServletException, IOException { String origin = SCMEvent.originOf(Stapler.getCurrentRequest()); // run in high privilege to see all the projects anonymous users don't see. // this is safe because we only initiate polling. SecurityContext securityContext = ACL.impersonate(ACL.SYSTEM); try { if (StringUtils.isNotBlank(branch) && StringUtils.isNotBlank(changesetId)) { SCMHeadEvent.fireNow(new MercurialSCMHeadEvent( SCMEvent.Type.UPDATED, new MercurialCommitPayload(new URI(url), branch, changesetId), origin)); return HttpResponses.ok(); } return handleNotifyCommit(origin, new URI(url)); } catch ( URISyntaxException ex ) { throw HttpResponses.error(SC_BAD_REQUEST, ex); } finally { SecurityContextHolder.setContext(securityContext); } } private HttpResponse handleNotifyCommit(String origin, URI url) throws ServletException, IOException { final List<Item> projects = Lists.newArrayList(); boolean scmFound = false, urlFound = false; final Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { return HttpResponses.error(SC_SERVICE_UNAVAILABLE, "Jenkins instance is not ready"); } for (Item project : jenkins.getAllItems()) { SCMTriggerItem scmTriggerItem = SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(project); if (scmTriggerItem == null) { continue; } SCMS: for (SCM scm : scmTriggerItem.getSCMs()) { if (!(scm instanceof MercurialSCM)) { continue; } scmFound = true; MercurialSCM hg = (MercurialSCM) scm; String repository = hg.getSource(); if (repository == null) { LOGGER.log(Level.WARNING, "project {0} is using source control but does not identify a repository", project.getFullName()); continue; } LOGGER.log(Level.INFO, "for {0}: {1} vs. {2}", new Object[] {project.getFullName(), url, repository}); if (!looselyMatches(url, repository)) { continue; } urlFound = true; SCMTrigger trigger = scmTriggerItem.getSCMTrigger(); if (trigger == null || trigger.isIgnorePostCommitHooks()) { // Do not send message to HTTP response because this is the normal case for a multibranch component project. LOGGER.log(Level.INFO, "No SCMTrigger on {0}", project.getFullName()); continue; } LOGGER.log(Level.INFO, "Triggering polling of {0} after event from {1}", new Object[]{ project.getFullName(), origin }); trigger.run(); projects.add(project); break SCMS; } } for (SCMSourceOwner project : SCMSourceOwners.all()) { for (SCMSource source : project.getSCMSources()) { if (!(source instanceof MercurialSCMSource)) { continue; } scmFound = true; MercurialSCMSource hgSource = (MercurialSCMSource) source; String repository = hgSource.getSource(); if (repository == null) { LOGGER.log(Level.WARNING, "project {0} is using source control but does not identify a repository", project.getFullName()); continue; } LOGGER.log(Level.INFO, "for {0}: {1} vs. {2}", new Object[] {project.getFullName(), url, repository}); if (!looselyMatches(url, repository)) { continue; } urlFound = true; LOGGER.log(Level.INFO, "Scheduling {0} for refresh after event from {1}", new Object[]{ project.getFullName(), origin }); project.onSCMSourceUpdated(source); projects.add(project); } } final String msg; if (!scmFound) { msg = "No Mercurial jobs found"; } else if (!urlFound) { msg = "No Mercurial jobs found using repository: " + url; } else { msg = null; } return new HttpResponse() { @SuppressWarnings("deprecation") public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { rsp.setStatus(SC_OK); rsp.setContentType("text/plain"); for (Item p : projects) { rsp.addHeader("Triggered", p.getAbsoluteUrl()); } PrintWriter w = rsp.getWriter(); for (Item p : projects) { w.println("Scheduled polling of " + p.getFullName()); } if (msg!=null) w.println(msg); } }; } private static final Logger LOGGER = Logger.getLogger(MercurialStatus.class.getName()); }