package org.jenkinsci.plugins.ghprb; import hudson.BulkChange; import hudson.XmlFile; import hudson.model.*; import hudson.model.listeners.SaveableListener; import hudson.util.Secret; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.ghprb.extensions.GhprbCommentAppender; import org.jenkinsci.plugins.ghprb.extensions.GhprbCommitStatusException; import org.jenkinsci.plugins.ghprb.extensions.GhprbExtension; import org.jenkinsci.plugins.ghprb.extensions.comments.GhprbBuildStatus; import org.kohsuke.github.GHCommitState; import org.kohsuke.github.GHEvent; import org.kohsuke.github.GHEventPayload.IssueComment; import org.kohsuke.github.GHEventPayload.PullRequest; import org.kohsuke.github.GHHook; import org.kohsuke.github.GHIssueState; import org.kohsuke.github.GHPullRequest; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * @author Honza Brázdil jbrazdil@redhat.com */ public class GhprbRepository implements Saveable{ private static final transient Logger logger = Logger.getLogger(GhprbRepository.class.getName()); private static final transient EnumSet<GHEvent> HOOK_EVENTS = EnumSet.of(GHEvent.ISSUE_COMMENT, GHEvent.PULL_REQUEST); private final String reponame; private final Map<Integer, GhprbPullRequest> pullRequests; private transient GHRepository ghRepository; private transient GhprbTrigger trigger; public GhprbRepository(String reponame, GhprbTrigger trigger) { this.pullRequests = new ConcurrentHashMap<Integer, GhprbPullRequest>(); this.reponame = reponame; this.trigger = trigger; } public void addPullRequests(Map<Integer, GhprbPullRequest> prs) { pullRequests.putAll(prs); } public void init() { for (Entry<Integer, GhprbPullRequest> next : pullRequests.entrySet()) { GhprbPullRequest pull = next.getValue(); pull.init(trigger.getHelper(), this); } } private boolean initGhRepository() { if (ghRepository != null) { return true; } GitHub gitHub = null; try { gitHub = trigger.getGitHub(); } catch (IOException ex) { logger.log(Level.SEVERE, "Error while accessing rate limit API", ex); return false; } if (gitHub == null) { logger.log(Level.SEVERE, "No connection returned to GitHub server!"); return false; } try { if (gitHub.getRateLimit().remaining == 0) { logger.log(Level.INFO, "Exceeded rate limit for repository"); return false; } } catch (FileNotFoundException ex) { logger.log(Level.INFO, "Rate limit API not found."); return false; } catch (IOException ex) { logger.log(Level.SEVERE, "Error while accessing rate limit API", ex); return false; } try { ghRepository = gitHub.getRepository(reponame); } catch (IOException ex) { logger.log(Level.SEVERE, "Could not retrieve GitHub repository named " + reponame + " (Do you have properly set 'GitHub project' field in job configuration?)", ex); return false; } return true; } // This method is used when not running with webhooks. We pull in the // active PRs for the repo associated with the trigger and check the // comments/hashes that have been added since the last time we checked. public void check() { if (!trigger.isActive()) { logger.log(Level.FINE, "Project is not active, not checking github state"); return; } if (!initGhRepository()) { return; } GHRepository repo = getGitHubRepo(); List<GHPullRequest> openPulls; try { openPulls = repo.getPullRequests(GHIssueState.OPEN); } catch (IOException ex) { logger.log(Level.SEVERE, "Could not retrieve open pull requests.", ex); return; } Set<Integer> closedPulls = new HashSet<Integer>(pullRequests.keySet()); for (GHPullRequest pr : openPulls) { if (pr.getHead() == null) { // Not sure if we need this, but leaving it for now. try { pr = getActualPullRequest(pr.getNumber()); } catch (IOException ex) { logger.log(Level.SEVERE, "Could not retrieve pr " + pr.getNumber(), ex); return; } } check(pr); closedPulls.remove(pr.getNumber()); } // remove closed pulls so we don't check them again for (Integer id : closedPulls) { pullRequests.remove(id); } try { this.save(); } catch (IOException e) { logger.log(Level.SEVERE, "Unable to save repository!", e); } } private void check(GHPullRequest pr) { int number = pr.getNumber(); try { GhprbPullRequest pull = getPullRequest(null, number); pull.check(pr, false); } catch (IOException e) { logger.log(Level.SEVERE, "Unable to check pr: " + number, e); } try { this.save(); } catch (IOException e) { logger.log(Level.SEVERE, "Unable to save repository!", e); } } public void commentOnFailure(Run<?, ?> build, TaskListener listener, GhprbCommitStatusException ex) { PrintStream stream = null; if (listener != null) { stream = listener.getLogger(); } GHCommitState state = ex.getState(); Exception baseException = ex.getException(); String newMessage; if (baseException instanceof FileNotFoundException) { newMessage = "FileNotFoundException means that the credentials Jenkins is using is probably wrong. Or the user account does not have write access to the repo."; } else { newMessage = "Could not update commit status of the Pull Request on GitHub."; } if (stream != null) { stream.println(newMessage); baseException.printStackTrace(stream); } else { logger.log(Level.INFO, newMessage, baseException); } if (GhprbTrigger.getDscp().getUseComments()) { StringBuilder msg = new StringBuilder(ex.getMessage()); if (build != null) { msg.append("\n"); GhprbTrigger trigger = Ghprb.extractTrigger(build); for (GhprbExtension ext : Ghprb.matchesAll(trigger.getExtensions(), GhprbBuildStatus.class)) { if (ext instanceof GhprbCommentAppender) { msg.append(((GhprbCommentAppender) ext).postBuildComment(build, null)); } } } if (GhprbTrigger.getDscp().getUseDetailedComments() || (state == GHCommitState.SUCCESS || state == GHCommitState.FAILURE)) { logger.log(Level.INFO, "Trying to send comment.", baseException); addComment(ex.getId(), msg.toString()); } } else { logger.log(Level.SEVERE, "Could not update commit status of the Pull Request on GitHub."); } } public String getName() { return reponame; } public void addComment(int id, String comment) { addComment(id, comment, null, null); } public void addComment(int id, String comment, Run<?, ?> build, TaskListener listener) { if (comment.trim().isEmpty()) return; if (build != null && listener != null) { try { comment = build.getEnvironment(listener).expand(comment); } catch (Exception e) { logger.log(Level.SEVERE, "Error", e); } } try { GHRepository repo = getGitHubRepo(); GHPullRequest pr = repo.getPullRequest(id); pr.comment(comment); } catch (IOException ex) { logger.log(Level.SEVERE, "Could not add comment to pull request #" + id + ": '" + comment + "'", ex); } } public void closePullRequest(int id) { try { GHRepository repo = getGitHubRepo(); GHPullRequest pr = repo.getPullRequest(id); pr.close(); } catch (IOException ex) { logger.log(Level.SEVERE, "Could not close the pull request #" + id + ": '", ex); } } private boolean hookExist() throws IOException { GHRepository ghRepository = getGitHubRepo(); if (ghRepository == null) { throw new IOException("Unable to get repo [ " + reponame + " ]"); } for (GHHook h : ghRepository.getHooks()) { if (!"web".equals(h.getName())) { continue; } if (!getHookUrl().equals(h.getConfig().get("url"))) { continue; } return true; } return false; } public final static Object createHookLock = new Object(); public boolean createHook() { try { // Avoid a race to update the hooks in a repo (we could end up with // multiple hooks). Lock on before we try this synchronized (createHookLock) { if (hookExist()) { return true; } Map<String, String> config = new HashMap<String, String>(); String secret = getSecret(); config.put("url", new URL(getHookUrl()).toExternalForm()); config.put("insecure_ssl", "1"); if (!StringUtils.isEmpty(secret)) { config.put("secret",secret); } getGitHubRepo().createHook("web", config, HOOK_EVENTS, true); return true; } } catch (IOException ex) { logger.log(Level.SEVERE, "Could not create web hook for repository {0}. Does the user (from global configuration) have admin rights to the repository?", reponame); return false; } } private String getSecret() { Secret secret = trigger.getGitHubApiAuth().getSecret(); return secret == null ? "" : secret.getPlainText(); } private String getHookUrl() { String baseUrl = trigger.getGitHubApiAuth().getJenkinsUrl(); if (baseUrl == null) { baseUrl = Jenkins.getInstance().getRootUrl(); } return baseUrl + GhprbRootAction.URL + "/"; } public GhprbPullRequest getPullRequest(int id) { return pullRequests.get(id); } public GHPullRequest getActualPullRequest(int id) throws IOException { return getGitHubRepo().getPullRequest(id); } void onIssueCommentHook(IssueComment issueComment) throws IOException { if (!trigger.isActive()) { logger.log(Level.FINE, "Not checking comments since build is disabled"); return; } int number = issueComment.getIssue().getNumber(); logger.log(Level.FINER, "Comment on issue #{0} from {1}: {2}", new Object[] { number, issueComment.getComment().getUser(), issueComment.getComment().getBody() }); if (!"created".equals(issueComment.getAction())) { return; } GhprbPullRequest pull = getPullRequest(null, number); pull.check(issueComment.getComment()); try { this.save(); } catch (IOException e) { logger.log(Level.SEVERE, "Unable to save repository!", e); } } private GhprbPullRequest getPullRequest(GHPullRequest ghpr, Integer number) throws IOException { if (number == null) { number = ghpr.getNumber(); } synchronized (this) { GhprbPullRequest pr = pullRequests.get(number); if (pr == null) { if (ghpr == null) { GHRepository repo = getGitHubRepo(); ghpr = repo.getPullRequest(number); } pr = new GhprbPullRequest(ghpr, trigger.getHelper(), this); pullRequests.put(number, pr); } return pr; } } void onPullRequestHook(PullRequest pr) throws IOException { GHPullRequest ghpr = pr.getPullRequest(); int number = pr.getNumber(); String action = pr.getAction(); boolean doSave = false; if ("closed".equals(action)) { pullRequests.remove(number); doSave = true; } else if (!trigger.isActive()) { logger.log(Level.FINE, "Not processing Pull request since the build is disabled"); } else if ("edited".equals(action) || "opened".equals(action) || "reopened".equals(action) || "synchronize".equals(action)) { GhprbPullRequest pull = getPullRequest(ghpr, number); pull.check(ghpr, true); doSave = true; } else { logger.log(Level.WARNING, "Unknown Pull Request hook action: {0}", action); } if (doSave) { try { this.save(); } catch (IOException e) { logger.log(Level.SEVERE, "Unable to save repository!", e); } } } public GHRepository getGitHubRepo() { if (ghRepository == null && !initGhRepository()) { logger.log(Level.SEVERE, "Unable to get repository [ {0} ]", reponame); } return ghRepository; } public void load() throws IOException { XmlFile xml = getConfigXml(trigger.getActualProject()); if(xml.exists()){ xml.unmarshal(this); } save(); } public void save() throws IOException { if(BulkChange.contains(this)) { return; } XmlFile config = getConfigXml(trigger.getActualProject()); config.write(this); SaveableListener.fireOnChange(this, config); } protected XmlFile getConfigXml(Job<?, ?> project) throws IOException { try { String escapedRepoName = URLEncoder.encode(reponame, "UTF8"); File file = new File(project.getBuildDir() + "/pullrequests", escapedRepoName); return Items.getConfigFile(file); } catch (UnsupportedEncodingException e) { throw new IOException(e); } } }