/* * Copyright (C) 2011 JFrog Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jfrog.hudson.release.scm.svn; import hudson.FilePath; import hudson.maven.agent.AbortException; import hudson.model.AbstractBuild; import hudson.model.TaskListener; import hudson.remoting.VirtualChannel; import hudson.scm.SubversionSCM; import org.jfrog.hudson.release.scm.AbstractScmManager; import org.tmatesoft.svn.core.*; import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider; import org.tmatesoft.svn.core.wc.*; import java.io.File; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; /** * Performs commit operations on Subversion repository configured for the project. * * @author Yossi Shaul */ public class SubversionManager extends AbstractScmManager<SubversionSCM> { private static Logger debuggingLogger = Logger.getLogger(SubversionManager.class.getName()); public SubversionManager(AbstractBuild<?, ?> abstractBuild, TaskListener buildListener) { super(abstractBuild, buildListener); } public void prepare() { // nothing to do here } /** * Commits the working copy. * * @param commitMessage@return The commit info upon successful operation. * @throws IOException On IO of SVN failure */ public void commitWorkingCopy(final String commitMessage) throws IOException, InterruptedException { build.getWorkspace().act(new SVNCommitWorkingCopyCallable(commitMessage, getLocation(), getSvnAuthenticationProvider(build), buildListener)); } /** * Creates a tag directly from the working copy. * * @param tagUrl The URL of the tag to create. * @param commitMessage Commit message * @return The commit info upon successful operation. * @throws IOException On IO of SVN failure */ public void createTag(final String tagUrl, final String commitMessage) throws IOException, InterruptedException { build.getWorkspace() .act(new SVNCreateTagCallable(tagUrl, commitMessage, getLocation(), getSvnAuthenticationProvider(build), buildListener)); } /** * Revert all the working copy changes. */ public void revertWorkingCopy() throws IOException, InterruptedException { build.getWorkspace() .act(new RevertWorkingCopyCallable(getLocation(), getSvnAuthenticationProvider(build), buildListener)); } /** * Attempts to revert the working copy. In case of failure it just logs the error. */ public void safeRevertWorkingCopy() { try { revertWorkingCopy(); } catch (Exception e) { debuggingLogger.log(Level.FINE, "Failed to revert working copy", e); log("Failed to revert working copy: " + e.getLocalizedMessage()); Throwable cause = e.getCause(); if (!(cause instanceof SVNException)) { return; } SVNException svnException = (SVNException) cause; if (svnException.getErrorMessage().getErrorCode() == SVNErrorCode.WC_LOCKED) { // work space locked attempt cleanup and try to revert again try { cleanupWorkingCopy(); } catch (Exception unlockException) { debuggingLogger.log(Level.FINE, "Failed to cleanup working copy", e); log("Failed to cleanup working copy: " + e.getLocalizedMessage()); return; } try { revertWorkingCopy(); } catch (Exception revertException) { log("Failed to revert working copy on the 2nd attempt: " + e.getLocalizedMessage()); } } } } private void cleanupWorkingCopy() throws IOException, InterruptedException { build.getWorkspace().act(new CleanupCallable(getLocation(), getSvnAuthenticationProvider(build), buildListener)); } public void safeRevertTag(String tagUrl, String commitMessageSuffix) { try { log("Reverting subversion tag: " + tagUrl); SVNURL svnUrl = SVNURL.parseURIEncoded(tagUrl); SVNCommitClient commitClient = new SVNCommitClient(createAuthenticationManager(), null); SVNCommitInfo commitInfo = commitClient.doDelete(new SVNURL[]{svnUrl}, COMMENT_PREFIX + commitMessageSuffix); SVNErrorMessage errorMessage = commitInfo.getErrorMessage(); if (errorMessage != null) { log("Failed to revert '" + tagUrl + "': " + errorMessage.getFullMessage()); } } catch (SVNException e) { log("Failed to revert '" + tagUrl + "': " + e.getLocalizedMessage()); } } public String getRemoteUrl(String defaultRemoteUrl) { return getLocation().remote; } public SubversionSCM.ModuleLocation getLocation() { return getJenkinsScm().getLocations()[0]; } private ISVNAuthenticationManager createAuthenticationManager() { ISVNAuthenticationProvider sap = getSvnAuthenticationProvider(build); ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); sam.setAuthenticationProvider(sap); return sam; } private ISVNAuthenticationProvider getSvnAuthenticationProvider(AbstractBuild<?, ?> build) { ISVNAuthenticationProvider sap; try { sap = getJenkinsScm().createAuthenticationProvider(build.getParent(), getLocation()); } catch (NoSuchMethodError e) { //fallback for versions under 2.x of org.jenkins-ci.plugins:subversion buildListener.getLogger().println( "[RELEASE] You are using an old subversion jenkins plugin, please consider upgrading."); sap = getJenkinsScm().getDescriptor().createAuthenticationProvider(build.getParent()); } if (sap == null) { throw new AbortException("Subversion authentication info is not set."); } return sap; } private static class SVNCommitWorkingCopyCallable implements FilePath.FileCallable<Object> { private static final long serialVersionUID = 1L; private final String commitMessage; private final SubversionSCM.ModuleLocation location; private final ISVNAuthenticationProvider authProvider; private final TaskListener buildListener; public SVNCommitWorkingCopyCallable(String commitMessage, SubversionSCM.ModuleLocation location, ISVNAuthenticationProvider provider, TaskListener listener) { this.commitMessage = commitMessage; this.location = location; authProvider = provider; buildListener = listener; } public Object invoke(File ws, VirtualChannel channel) throws IOException, InterruptedException { File workingCopy = new File(ws, location.getLocalDir()).getCanonicalFile(); try { ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); sam.setAuthenticationProvider(authProvider); SVNCommitClient commitClient = new SVNCommitClient(sam, null); buildListener.getLogger().println("[RELEASE] " + commitMessage); debuggingLogger.fine(String.format("Committing working copy: '%s'", workingCopy)); SVNCommitInfo commitInfo = commitClient.doCommit(new File[]{workingCopy}, true, commitMessage, null, null, true, true, SVNDepth.INFINITY); SVNErrorMessage errorMessage = commitInfo.getErrorMessage(); if (errorMessage != null) { throw new IOException("Failed to commit working copy: " + errorMessage.getFullMessage()); } return null; } catch (SVNException e) { debuggingLogger.log(Level.FINE, "Failed to Commit WorkingCopy", e); throw new IOException(e.getMessage()); } } } /** * Creates a tag directly from the working copy. */ private static class SVNCreateTagCallable implements FilePath.FileCallable<Object> { private final String tagUrl; private final String commitMessage; private final SubversionSCM.ModuleLocation location; private final ISVNAuthenticationProvider authProvider; private final TaskListener buildListener; public SVNCreateTagCallable(String tagUrl, String commitMessage, SubversionSCM.ModuleLocation location, ISVNAuthenticationProvider provider, TaskListener listener) { this.tagUrl = tagUrl; this.commitMessage = commitMessage; this.location = location; authProvider = provider; buildListener = listener; } public Object invoke(File ws, VirtualChannel channel) throws IOException, InterruptedException { File workingCopy = new File(ws, location.getLocalDir()).getCanonicalFile(); try { SVNURL svnUrl = SVNURL.parseURIEncoded(tagUrl); SVNCopyClient copyClient; try { copyClient = SubversionSCM.createClientManager(authProvider).getCopyClient(); } catch (NoSuchMethodError e) { //todo remove when backward compatibility not needed //fallback for older versions of org.jenkins-ci.plugins:subversion buildListener.getLogger().println( "[RELEASE] You are using an old subversion jenkins plugin, please consider upgrading."); copyClient = SubversionSCM.createSvnClientManager(authProvider).getCopyClient(); } buildListener.getLogger().println("[RELEASE] Creating subversion tag: " + tagUrl); SVNCopySource source = new SVNCopySource(SVNRevision.WORKING, SVNRevision.WORKING, workingCopy); SVNCommitInfo commitInfo = copyClient.doCopy(new SVNCopySource[]{source}, svnUrl, false, true, true, commitMessage, new SVNProperties()); SVNErrorMessage errorMessage = commitInfo.getErrorMessage(); if (errorMessage != null) { throw new IOException("Failed to create tag: " + errorMessage.getFullMessage()); } return null; } catch (SVNException e) { debuggingLogger.log(Level.FINE, "Failed to create tag", e); throw new IOException("Subversion tag creation failed: " + e.getMessage()); } } } private static class RevertWorkingCopyCallable implements FilePath.FileCallable<Object> { private final SubversionSCM.ModuleLocation location; private final ISVNAuthenticationProvider authProvider; private final TaskListener listener; public RevertWorkingCopyCallable(SubversionSCM.ModuleLocation location, ISVNAuthenticationProvider authProvider, TaskListener listener) { this.location = location; this.authProvider = authProvider; this.listener = listener; } public Object invoke(File ws, VirtualChannel channel) throws IOException, InterruptedException { File workingCopy = new File(ws, location.getLocalDir()).getCanonicalFile(); try { log(listener, "Reverting working copy: " + workingCopy); ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); sam.setAuthenticationProvider(authProvider); SVNWCClient wcClient = new SVNWCClient(sam, null); wcClient.doRevert(new File[]{workingCopy}, SVNDepth.INFINITY, null); return null; } catch (SVNException e) { debuggingLogger.log(Level.FINE, "Failed Revert WorkingCopy ", e); throw new IOException(e.getMessage()); } } } private static class CleanupCallable implements FilePath.FileCallable<Object> { private final SubversionSCM.ModuleLocation location; private final ISVNAuthenticationProvider authProvider; private final TaskListener listener; private CleanupCallable(SubversionSCM.ModuleLocation location, ISVNAuthenticationProvider authProvider, TaskListener listener) { this.location = location; this.authProvider = authProvider; this.listener = listener; } public Object invoke(File ws, VirtualChannel channel) throws IOException, InterruptedException { File workingCopy = new File(ws, location.getLocalDir()).getCanonicalFile(); try { log(listener, "Cleanup working copy: " + workingCopy); ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); sam.setAuthenticationProvider(authProvider); SVNWCClient wcClient = new SVNWCClient(sam, null); wcClient.doCleanup(workingCopy); return null; } catch (SVNException e) { debuggingLogger.log(Level.FINE, "Failed Cleanup ", e); throw new IOException(e.getMessage()); } } } }