package hudson.plugins.jira; import com.atlassian.jira.rest.client.api.RestClientException; import com.atlassian.jira.rest.client.api.domain.Issue; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import hudson.Util; import hudson.model.Hudson; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.jira.model.JiraIssue; import hudson.plugins.jira.selector.AbstractIssueSelector; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.AffectedFile; import hudson.scm.ChangeLogSet.Entry; import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import org.apache.commons.lang.StringUtils; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.Method; import java.net.URL; import java.rmi.RemoteException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import static java.lang.String.format; /** * Actual JIRA update logic. * * @author Kohsuke Kawaguchi */ class Updater { private SCM scm; private List<String> labels; private static final Logger LOGGER = Logger.getLogger(Updater.class.getName()); /** * Debug flag. */ public static boolean debug = false; public Updater(SCM scm) { this(scm, new ArrayList<String>()); } public Updater(SCM scm, List<String> labels) { super(); this.scm = scm; if (labels == null) { this.labels = new ArrayList<String>(); } else { this.labels = labels; } } boolean perform(Run<?, ?> build, TaskListener listener, AbstractIssueSelector selector) { PrintStream logger = listener.getLogger(); Set<JiraIssue> issues = null; try { JiraSite site = JiraSite.get(build.getParent()); if (site == null) { logger.println(Messages.NoJiraSite()); build.setResult(Result.FAILURE); return true; } String rootUrl = Hudson.getInstance().getRootUrl(); if (rootUrl == null) { logger.println(Messages.NoJenkinsUrl()); build.setResult(Result.FAILURE); return true; } Set<String> ids = selector.findIssueIds(build, site, listener); if (ids.isEmpty()) { if (debug) logger.println("No JIRA issues found."); return true; // nothing found here. } JiraSession session = null; try { session = site.getSession(); } catch (IOException e) { listener.getLogger().println(Messages.FailedToConnect()); e.printStackTrace(listener.getLogger()); } if (session == null) { logger.println(Messages.NoRemoteAccess()); build.setResult(Result.FAILURE); return true; } boolean doUpdate = false; //in case of workflow, it may be null if (site.updateJiraIssueForAllStatus || build.getResult() == null) { doUpdate = true; } else { doUpdate = build.getResult().isBetterOrEqualTo(Result.UNSTABLE); } boolean useWikiStyleComments = site.supportsWikiStyleComment; issues = getJiraIssues(ids, session, logger); build.addAction(new JiraBuildAction(build, issues)); if (doUpdate) { submitComments(build, logger, rootUrl, issues, session, useWikiStyleComments, site.recordScmChanges, site.groupVisibility, site.roleVisibility); } else { // this build didn't work, so carry forward the issues to the next build build.addAction(new JiraCarryOverAction(issues)); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Error updating JIRA issues. Saving issues for next build.", e); logger.println("Error updating JIRA issues. Saving issues for next build.\n" + e); if (issues != null && !issues.isEmpty()) { // updating issues failed, so carry forward issues to the next build build.addAction(new JiraCarryOverAction(issues)); } } return true; } /** * Submits comments for the given issues. * Remvoes from <code>issues</code> issues which have been successfully updated or are invalid * * @param build * @param logger * @param jenkinsRootUrl * @param session * @param useWikiStyleComments * @param recordScmChanges * @param groupVisibility * @throws RestClientException */ void submitComments( Run<?, ?> build, PrintStream logger, String jenkinsRootUrl, Set<JiraIssue> issues, JiraSession session, boolean useWikiStyleComments, boolean recordScmChanges, String groupVisibility, String roleVisibility) throws RestClientException { // copy to prevent ConcurrentModificationException Set<JiraIssue> copy = ImmutableSet.copyOf(issues); for (JiraIssue issue : copy) { logger.println(Messages.UpdatingIssue(issue.getKey())); try { session.addComment( issue.getKey(), createComment(build, useWikiStyleComments, jenkinsRootUrl, recordScmChanges, issue), groupVisibility, roleVisibility ); if (!labels.isEmpty()) { session.addLabels(issue.getKey(), labels); } } catch (RestClientException e) { if (e.getStatusCode().or(0).equals(404)) { logger.println(issue.getKey() + " - JIRA issue not found. Dropping comment from update queue."); } if (e.getStatusCode().or(0).equals(403)) { logger.println(issue.getKey() + " - Jenkins JIRA user does not have permissions to comment on this issue. Preserving comment for future update."); continue; } if (e.getStatusCode().or(0).equals(401)) { logger.println(issue.getKey() + " - Jenkins JIRA authentication problem. Preserving comment for future update."); continue; } logger.println(Messages.FailedToUpdateIssueWithCarryOver(issue.getKey())); logger.println(e.getLocalizedMessage()); } // if no exception is thrown during update, remove from the list as succesfully updated issues.remove(issue); } } private static Set<JiraIssue> getJiraIssues(Set<String> ids, JiraSession session, PrintStream logger) throws RemoteException { Set<JiraIssue> issues = new LinkedHashSet<>(ids.size()); for (String id : ids) { Issue issue = session.getIssue(id); if (issue == null) { logger.println(id + " issue doesn't exist in JIRA"); continue; } issues.add(new JiraIssue(issue)); } return issues; } /** * Creates a comment to be used in JIRA for the build. * For example: * <pre> * SUCCESS: Integrated in Job #nnnn (See [http://jenkins.domain/job/Job/nnnn/])\r * JIRA-XXXX: Commit message. (Author _author@email.domain_: * [https://bitbucket.org/user/repo/changeset/9af8e4c4c909/])\r * </pre> */ private String createComment(Run<?, ?> build, boolean wikiStyle, String jenkinsRootUrl, boolean recordScmChanges, JiraIssue jiraIssue) { Result result = build.getResult(); //if we run from workflow we dont known final result if(result == null) return format( wikiStyle ? "Integrated in [%2$s|%3$s]\n%4$s" : "Integrated in Jenkins build %2$s (See [%3$s])\n%4$s", jenkinsRootUrl, build, Util.encode(jenkinsRootUrl + build.getUrl()), getScmComments(wikiStyle, build, recordScmChanges, jiraIssue)); else return format( wikiStyle ? "%6$s: Integrated in !%1$simages/16x16/%3$s! [%2$s|%4$s]\n%5$s" : "%6$s: Integrated in Jenkins build %2$s (See [%4$s])\n%5$s", jenkinsRootUrl, build, result != null ? result.color.getImage() : null, Util.encode(jenkinsRootUrl + build.getUrl()), getScmComments(wikiStyle, build, recordScmChanges, jiraIssue), result.toString()); } private String getScmComments(boolean wikiStyle, Run<?, ?> run, boolean recordScmChanges, JiraIssue jiraIssue) { StringBuilder comment = new StringBuilder(); RepositoryBrowser repoBrowser = getRepositoryBrowser(run); for (ChangeLogSet<? extends Entry> set : RunScmChangeExtractor.getChanges(run)) { for (Entry change : set) { if (jiraIssue != null && !StringUtils.containsIgnoreCase(change.getMsg(), jiraIssue.getKey())) { continue; } comment.append(createScmChangeEntryDescription(run, change, wikiStyle, recordScmChanges)); } } if (jiraIssue != null) { final Run<?, ?> prev = run.getPreviousBuild(); if (prev != null) { final JiraCarryOverAction a = prev.getAction(JiraCarryOverAction.class); if (a != null && a.getIDs().contains(jiraIssue.getKey())) { comment.append(getScmComments(wikiStyle, prev, recordScmChanges, jiraIssue)); } } } return comment.toString(); } protected String createScmChangeEntryDescription(Run<?, ?> run, Entry change, boolean wikiStyle, boolean recordScmChanges) { StringBuilder description = new StringBuilder(); RepositoryBrowser repoBrowser = getRepositoryBrowser(run); JiraSite site = JiraSite.get(run.getParent()); if(change.getMsg() != null) description.append(change.getMsg()); String revision = getRevision(change); if (revision != null) { description.append(" ("); appendAuthorToDescription(change, description); if (site.isAppendChangeTimestamp() && change.getTimestamp() > 0) { appendChangeTimestampToDescription(description, site, change.getTimestamp()); description.append(" "); } appendRevisionToDescription(change, wikiStyle, description, repoBrowser, revision); description.append(")"); } description.append("\n"); if (recordScmChanges) { appendAffectedFilesToDescription(change, description); } return description.toString(); } protected void appendAuthorToDescription(Entry change, StringBuilder description) { if (change.getAuthor() != null) { change.getAuthor(); String uid = change.getAuthor().getId(); if (StringUtils.isNotBlank(uid)) { description.append(uid).append(": "); } } } protected void appendRevisionToDescription(Entry change, boolean wikiStyle, StringBuilder description, RepositoryBrowser repoBrowser, String revision) { URL url = null; if (repoBrowser != null) { try { url = repoBrowser.getChangeSetLink(change); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to calculate SCM repository browser link", e); } } if (url != null && StringUtils.isNotBlank(url.toExternalForm())) { if (wikiStyle) { description.append("[").append(revision).append("|"); description.append(url.toExternalForm()).append("]"); } else { description.append("[").append(url.toExternalForm()).append("]"); } } else { description.append("rev ").append(revision); } } protected void appendAffectedFilesToDescription(Entry change, StringBuilder description) { // see http://issues.jenkins-ci.org/browse/JENKINS-2508 // added additional try .. catch; getAffectedFiles is not supported // by all SCM implementations try { for (AffectedFile affectedFile : change.getAffectedFiles()) { description.append("* "); if(affectedFile.getEditType() != null) description.append("(").append(affectedFile.getEditType().getName()).append(") "); if(affectedFile.getPath() != null) description.append(affectedFile.getPath()); description.append("\n"); } } catch (UnsupportedOperationException e) { LOGGER.warning("Unsupported SCM operation 'getAffectedFiles'. Fall back to getAffectedPaths."); for (String affectedPath : change.getAffectedPaths()) { description.append("* ").append(affectedPath).append("\n"); } } } protected void appendChangeTimestampToDescription(StringBuilder description, JiraSite site, long timestamp) { DateFormat df = null; if (!Strings.isNullOrEmpty(site.getDateTimePattern())) { df = new SimpleDateFormat(site.getDateTimePattern()); } else { // default format for current locale df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()); } Date changeDate = new Date(timestamp); String dateTimeString = df.format(changeDate); description.append(dateTimeString); } private RepositoryBrowser<?> getRepositoryBrowser(Run<?, ?> run) { SCM scm = getScm(); if (scm != null) { return scm.getEffectiveBrowser(); } return null; } private static String getRevision(Entry entry) { String commitId = entry.getCommitId(); if (commitId != null) { return commitId; } // fall back to old SVN-specific solution, if we have only installed an old subversion-plugin // which doesn't implement getCommitId, yet try { Class<?> clazz = entry.getClass(); Method method = clazz.getMethod("getRevision", (Class[]) null); if (method == null) { return null; } Object revObj = method.invoke(entry, (Object[]) null); return (revObj != null) ? revObj.toString() : null; } catch (Exception e) { return null; } } private SCM getScm() { return scm; } }