package ru.yandex.jenkins.plugins.debuilder; import hudson.EnvVars; import hudson.FilePath; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Run; import hudson.plugins.git.GitChangeSet; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.impl.RelativeTargetDirectory; import hudson.scm.*; import jenkins.model.Jenkins; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.jenkinsci.plugins.gitclient.GitClient; import org.tmatesoft.svn.core.ISVNLogEntryHandler; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider; import org.tmatesoft.svn.core.wc.SVNRevision; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import static hudson.scm.SubversionSCM.ModuleLocation; import static ru.yandex.jenkins.plugins.debuilder.DebUtils.Runner; import static ru.yandex.jenkins.plugins.debuilder.DebianPackageBuilder.DescriptorImpl; public class ChangesExtractor { public static List<Change> getChanges(AbstractBuild build, Runner runner, SCM scm, String remoteDebian, String ourMessage, VersionHelper helper) throws DebianizingException, InterruptedException { BuildListener listener = runner.getListener(); if (scm instanceof SubversionSCM) { String oldRevision = helper.getRevision(); helper.setRevision(getSVNRevision(build, runner, (SubversionSCM) scm, remoteDebian)); if ("".equals(oldRevision)) { runner.announce("No last revision known, using changes since last successful build to populate debian/changelog"); return getChangesSinceLastBuild(build, ourMessage); } else { runner.announce("Calculating changes since revision {0}.", oldRevision); return getChangesFromSubversion(build, runner, (SubversionSCM) scm, remoteDebian, oldRevision, helper.getRevision(), ourMessage); } } else if (scm instanceof GitSCM) { runner.announce("Calculating changes from git log"); return getChangesFromGit(build, listener, (GitSCM) scm, remoteDebian); } else { runner.announce("SCM in use is not Subversion nor Git (but <{0}> instead), defaulting to changes since last build", scm.getClass().getName()); return getChangesSinceLastBuild(build, ourMessage); } } static String getSVNRevision(@SuppressWarnings("rawtypes") AbstractBuild build, Runner runner, SubversionSCM scm, String remoteDebian) throws DebianizingException { ModuleLocation location = findOurLocation(build, scm, runner, remoteDebian); try { Map<String, Long> revisionsForBuild = SubversionHack.getRevisionsForBuild(scm, build); return Long.toString(revisionsForBuild.get(location.getSVNURL().toString())); } catch (IOException e) { throw new DebianizingException("IOException: " + e.getMessage(), e); } catch (SVNException e) { throw new DebianizingException("SVNException: " + e.getMessage(), e); } catch (InterruptedException e) { throw new DebianizingException("InterruptedException: " + e.getMessage(), e); } catch (IllegalArgumentException e) { throw new DebianizingException("IllegalArgumentException: " + e.getMessage(), e); } catch (IllegalAccessException e) { throw new DebianizingException("IllegalAccessException: " + e.getMessage(), e); } } static ModuleLocation findOurLocation(@SuppressWarnings("rawtypes") AbstractBuild build, SubversionSCM scm, Runner runner, String remoteDebian) throws DebianizingException { EnvVars environment; try { environment = build.getEnvironment(runner.getListener()); } catch (IOException e) { throw new DebianizingException("IOException: " + e.getMessage(), e); } catch (InterruptedException e) { throw new DebianizingException("InterruptedException: " + e.getMessage(), e); } for (ModuleLocation location: scm.getLocations(environment, build)) { if (remoteDebian.startsWith(build.getWorkspace().child(location.getLocalDir()).getRemote())) { return location; } } throw new DebianizingException("Can't find module location for remoteDebian " + remoteDebian); } static List<Change> getChangesFromSubversion(@SuppressWarnings("rawtypes") AbstractBuild build, final Runner runner, SubversionSCM scm, final String remoteDebian, String latestRevision, String currentRevision, final String ourMessage) throws DebianizingException { final List<Change> result = new ArrayList<Change>(); ModuleLocation location = findOurLocation(build, scm, runner, remoteDebian); ISVNAuthenticationProvider authenticationProvider = ((SubversionSCM)build.getProject().getScm()).createAuthenticationProvider(build.getProject(), location); SvnClientManager manager = SubversionSCM.createClientManager(authenticationProvider); try { SVNURL svnurl = location.getSVNURL(); manager.getLogClient().doLog(svnurl, null, SVNRevision.UNDEFINED, SVNRevision.create(Long.parseLong(latestRevision) + 1), SVNRevision.parse(currentRevision), false, true, 0, new ISVNLogEntryHandler() { @Override public void handleLogEntry(SVNLogEntry logEntry) throws SVNException { if (!logEntry.getMessage().equals(ourMessage)) { result.add(new Change(logEntry.getAuthor(), logEntry.getMessage())); } } }); } catch (SVNException e) { e.printStackTrace(); throw new DebianizingException("SVNException: " + e.getMessage(), e); } finally { manager.dispose(); } return result; } /** * Extract all commits from git log since last debian/changelog change * @param build * @param listener * @param scm * @param remoteDebian * @return * @throws DebianizingException */ static List<Change> getChangesFromGit(AbstractBuild build, BuildListener listener, GitSCM scm, String remoteDebian) throws DebianizingException { try { EnvVars environment = build.getEnvironment(listener); FilePath workspace = build.getWorkspace(); // method signature changed in latest Git plugin, @since 2.3.4 GitClient cli = scm.createClient(listener, environment, build, workspace); String relativeTargetDirectory = ""; if(scm.getExtensions().get(RelativeTargetDirectory.class) != null) { relativeTargetDirectory = scm.getExtensions().get(RelativeTargetDirectory.class).getRelativeTargetDir(); } DescriptorImpl descriptor = (DescriptorImpl) Jenkins.getInstance().getDescriptor(DebianPackageBuilder.class); PersonIdent account = new PersonIdent(descriptor.getAccountName(), descriptor.getAccountEmail()); return getChangesFromGit(cli, workspace, relativeTargetDirectory, remoteDebian, account); } catch (IOException e) { throw new DebianizingException("IOException: " + e.getMessage(), e); } catch (InterruptedException e) { throw new DebianizingException("InterruptedException: " + e.getMessage(), e); } } static List<Change> getChangesFromGit(GitClient cli, FilePath workspace, String relativeTargetDirectory, String remoteDebian, PersonIdent account) throws InterruptedException { String changelogPath = remoteDebian + "/changelog"; LinkedList<Change> changesSinceLastChangelogModification = new LinkedList<Change>(); LinkedList<Change> changesSinceLastChangelogModificationByPlugin = new LinkedList<Change>(); boolean firstChangelogModificationFound = false; for (ObjectId rev : cli.revListAll()) { List<String> lines = cli.showRevision(rev); GitChangeSet changeSet = new GitChangeSet(lines, true); String email = getAuthorEmailFromGitRevision(lines); Change change = new Change(changeSet.getAuthorName(), changeSet.getMsg()); for (GitChangeSet.Path path : changeSet.getPaths()) { String filePath = workspace.child(relativeTargetDirectory).child(path.getPath()).getRemote(); if (filePath.equals(changelogPath)) { if (changeSet.getAuthorName().equals(account.getName()) & email.equals(account.getEmailAddress())) { return changesSinceLastChangelogModificationByPlugin; } else { firstChangelogModificationFound = true; } } } if (!firstChangelogModificationFound) { changesSinceLastChangelogModification.addFirst(change); } changesSinceLastChangelogModificationByPlugin.addFirst(change); } return changesSinceLastChangelogModification; } /* This is temporary solution. GitChangeSet doesn't provide any method to extract email of commit author now. */ static String getAuthorEmailFromGitRevision(List<String> lines) { Pattern pattern = Pattern.compile("^author [^<]*<(.*)> .*$"); for (String line: lines) { Matcher matcher = pattern.matcher(line); if (matcher.matches()) { return matcher.group(1); } } return ""; } @SuppressWarnings({ "rawtypes", "unchecked" }) static List<Change> getChangesSinceLastBuild(AbstractBuild build, String ourMessage) throws InterruptedException, DebianizingException { List<Change> result = new ArrayList<Change>(); Run lastSuccessfulBuild = build.getProject().getLastSuccessfulBuild(); int lastSuccessNumber = lastSuccessfulBuild == null ? 0 : lastSuccessfulBuild.number; for (int num = lastSuccessNumber + 1; num <= build.number; num ++) { AbstractBuild run = build.getProject().getBuildByNumber(num); if (run == null) { continue; } ChangeLogSet<? extends ChangeLogSet.Entry> changeSet = run.getChangeSet(); for (ChangeLogSet.Entry entry : changeSet) { if (!entry.getMsg().equals(ourMessage)) { result.add(new Change(entry.getAuthor().getFullName(), entry.getMsg())); } } } return result; } /** * Pojo to store change * * @author pupssman */ public static final class Change { private final String author; private final String message; public Change(String author, String message) { this.author = author; this.message = message;} public String getAuthor() { return author; } public String getMessage() { return message; } @Override public String toString() { return author + ": " + message; } @Override public boolean equals(Object obj) { if (!(obj instanceof Change)) { return false; } Change that = (Change) obj; return this.author.equals(that.author) && this.message.equals(that.message); } } }