package hudson.plugins.svn_tag; import groovy.lang.Binding; import groovy.lang.GroovyShell; import hudson.Launcher; import hudson.model.*; import hudson.scm.SubversionSCM; import org.apache.commons.lang.StringUtils; import org.codehaus.groovy.control.CompilerConfiguration; import org.tmatesoft.svn.core.SVNCommitInfo; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNProperties; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider; import org.tmatesoft.svn.core.wc.SVNCommitClient; import org.tmatesoft.svn.core.wc.SVNCopyClient; import org.tmatesoft.svn.core.wc.SVNCopySource; import org.tmatesoft.svn.core.wc.SVNRevision; import org.tmatesoft.svn.core.wc.SVNWCUtil; import java.io.*; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Consolidates the work common in Publisher and MavenReporter. * * @author Kenji Nakamura */ @SuppressWarnings( {"UtilityClass", "ImplicitCallToSuper", "MethodReturnOfConcreteClass", "MethodParameterOfConcreteClass", "InstanceofInterfaces"}) public class SvnTagPlugin { /** * Creates a new SvnTagPlugin object. */ private SvnTagPlugin() { } /** * True if the operation was successful. * * @param abstractBuild build * @param launcher launcher * @param buildListener build listener * @param tagBaseURLStr tag base URL string * @param tagComment tag comment * @return true if the operation was successful */ @SuppressWarnings({"FeatureEnvy", "UnusedDeclaration", "TypeMayBeWeakened", "LocalVariableOfConcreteClass"}) public static boolean perform(AbstractBuild<?,?> abstractBuild, Launcher launcher, BuildListener buildListener, String tagBaseURLStr, String tagComment, String tagDeleteComment) { PrintStream logger = buildListener.getLogger(); if (!Result.SUCCESS.equals(abstractBuild.getResult())) { logger.println(Messages.UnsuccessfulBuild()); return true; } AbstractProject<?, ?> rootProject = abstractBuild.getProject().getRootProject(); Map<String, String> env; if (!(rootProject.getScm() instanceof SubversionSCM)) { logger.println(Messages.NotSubversion(rootProject.getScm().toString())); return true; } SubversionSCM scm = SubversionSCM.class.cast(rootProject.getScm()); try { env = abstractBuild.getEnvironment(buildListener); } catch (Exception e) { logger.println( "Failed to get environment. " + e.getLocalizedMessage()); return false; } // Let SubversionSCM fill revision number. // It is guaranteed for getBuilds() return the latest build (i.e. // current build) at first // The passed in abstractBuild may be the sub maven module and not // have revision.txt holding Svn revision information, so need to use // the build associated with the root level project. scm.buildEnvVars(rootProject.getBuilds().get(0), env); SubversionSCM.ModuleLocation[] moduleLocations = scm.getLocations(); // environment variable "SVN_REVISION" doesn't contain revision number when multiple modules are // specified. Instead, parse revision.txt and obtain the corresponding revision numbers. Map<String, Long> revisions; try { revisions = parseRevisionFile(abstractBuild); } catch (IOException e) { logger.println( "Failed to parse revision.txt. " + e.getLocalizedMessage()); return false; } ISVNAuthenticationProvider sap = scm.getDescriptor().createAuthenticationProvider(); if (sap == null) { logger.println("Subversion authentication info is not set."); return false; } ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); sam.setAuthenticationProvider(sap); SVNCommitClient commitClient = new SVNCommitClient(sam, null); for (SubversionSCM.ModuleLocation ml : moduleLocations) { logger.println("moduleLocation: Remote ->" + ml.remote); List locationPathElements = Arrays.asList(StringUtils.split(ml.remote, "/")); String evaledTagBaseURLStr = evalGroovyExpression(env, tagBaseURLStr, locationPathElements); URI repoURI; try { repoURI = new URI(StringUtils.replace(ml.remote, " ", "%20")); } catch (URISyntaxException e) { logger.println("Failed to parse SVN repo URL. " + e.getLocalizedMessage()); return false; } SVNURL parsedTagBaseURL = null; try { parsedTagBaseURL = SVNURL.parseURIEncoded( repoURI.resolve(evaledTagBaseURLStr).toString()); logger.println( "Tag Base URL: '" + parsedTagBaseURL.toString() + "'."); } catch (SVNException e) { logger.println( "Failed to parse tag base URL '" + evaledTagBaseURLStr + "'. " + e.getLocalizedMessage()); } try { String evalDeleteComment = evalGroovyExpression( env, tagDeleteComment, locationPathElements); SVNCommitInfo deleteInfo = commitClient.doDelete(new SVNURL[]{parsedTagBaseURL}, evalDeleteComment); SVNErrorMessage deleteErrMsg = deleteInfo.getErrorMessage(); if (null != deleteErrMsg) { logger.println(deleteErrMsg.getMessage()); } else { logger.println(Messages.DeleteOldTag(evaledTagBaseURLStr)); } } catch (SVNException e) { logger.println(Messages.NoOldTag(evaledTagBaseURLStr)); } SVNCopyClient copyClient = new SVNCopyClient(sam, null); try { String evalComment = evalGroovyExpression( env, tagComment, locationPathElements); SVNRevision rev = SVNRevision.create(Long.valueOf(revisions.get(ml.remote))); SVNCommitInfo commitInfo = copyClient.doCopy(new SVNCopySource[] { new SVNCopySource(rev, rev, SVNURL.parseURIEncoded(ml.remote)) }, parsedTagBaseURL, false, true, false, evalComment, new SVNProperties()); SVNErrorMessage errorMsg = commitInfo.getErrorMessage(); if (null != errorMsg) { logger.println(errorMsg.getFullMessage()); return false; } else { logger.println(Messages.Tagged(commitInfo.getNewRevision())); } } catch (SVNException e) { logger.println(Messages.CopyFailed(e.getLocalizedMessage())); return false; } } return true; } @SuppressWarnings({"StaticMethodOnlyUsedInOneClass", "TypeMayBeWeakened"}) static String evalGroovyExpression(Map<String, String> env, String evalText, List locationPathElements) { Binding binding = new Binding(); binding.setVariable("env", env); binding.setVariable("sys", System.getProperties()); if (locationPathElements == null) { binding.setVariable("repoURL", Arrays.asList(StringUtils.split( "http://svn.example.com/path1/path2/path3/path4/path5/path6/path7/path8/path9/path10"), "/")); } else { binding.setVariable("repoURL", locationPathElements); } CompilerConfiguration config = new CompilerConfiguration(); GroovyShell shell = new GroovyShell(binding, config); Object result = shell.evaluate("return \"" + evalText + "\""); if (result == null) { return ""; } else { return result.toString().trim(); } } /** * Reads the revision file of the specified build. * * @param build build object * @return map from Subversion URL to its revision. * @throws java.io.IOException thrown when operation failed */ /*package*/ @SuppressWarnings({"NestedAssignment"}) static Map<String, Long> parseRevisionFile(AbstractBuild build) throws IOException { Map<String, Long> revisions = new HashMap<String, Long>(); // module -> revision // read the revision file of the last build File file = SubversionSCM.getRevisionFile(build); if (!file.exists()) // nothing to compare against { return revisions; } BufferedReader br = new BufferedReader(new FileReader(file)); try { String line; while ((line = br.readLine()) != null) { int index = line.lastIndexOf('/'); if (index < 0) { continue; // invalid line? } try { revisions.put(line.substring(0, index), Long.parseLong(line.substring(index + 1))); } catch (NumberFormatException e) { // perhaps a corrupted line. ignore } } } finally { br.close(); } return revisions; } }