/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Fulvio Cavarretta,
* Jean-Baptiste Quenot, Luca Domenico Milanesio, Renaud Bruyeron, Stephen Connolly,
* Tom Huybrechts, Yahoo! Inc., Manufacture Francaise des Pneumatiques Michelin,
* Romain Seguy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.scm.subversion;
import hudson.Extension;
import hudson.model.Hudson;
import hudson.scm.SubversionSCM.External;
import hudson.scm.SubversionSCM.ModuleLocation;
import hudson.scm.SubversionSCM.SvnInfo;
import hudson.triggers.SCMTrigger;
import org.apache.commons.lang.time.FastDateFormat;
import org.kohsuke.stapler.DataBoundConstructor;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.wc.SVNInfo;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNUpdateClient;
import org.tmatesoft.svn.core.wc.SVNWCClient;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* {@link WorkspaceUpdater} that uses "svn update" as much as possible.
*
* @author Kohsuke Kawaguchi
*/
public class UpdateUpdater extends WorkspaceUpdater {
private static final long serialVersionUID = 1451258464864424355L;
private static final FastDateFormat fmt = FastDateFormat.getInstance("''yyyy-MM-dd'T'HH:mm:ss.SSS Z''");
@DataBoundConstructor
public UpdateUpdater() {
}
@Override
public UpdateTask createTask() {
return new TaskImpl();
}
public static class TaskImpl extends UpdateTask {
/**
*
*/
private static final long serialVersionUID = -5766470969352844330L;
/**
* Returns whether we can do a "svn update" or a "svn switch" or a "svn checkout"
*/
protected SvnCommandToUse getSvnCommandToUse() throws IOException {
String moduleName = location.getLocalDir();
File module = new File(ws, moduleName).getCanonicalFile(); // canonicalize to remove ".." and ".". See #474
if (!module.exists()) {
listener.getLogger().println("Checking out a fresh workspace because " + module + " doesn't exist");
return SvnCommandToUse.CHECKOUT;
}
try {
SVNInfo svnkitInfo = parseSvnInfo(module);
SvnInfo svnInfo = new SvnInfo(svnkitInfo);
String url = location.getSVNURL().toString();
if (!svnInfo.url.equals(url)) {
if (isSameRepository(location, svnkitInfo)) {
listener.getLogger().println("Switching from " + svnInfo.url + " to " + url);
return SvnCommandToUse.SWITCH;
} else {
listener.getLogger().println("Checking out a fresh workspace because the workspace is not " + url);
return SvnCommandToUse.CHECKOUT;
}
}
} catch (SVNException e) {
if (e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_NOT_DIRECTORY) {
listener.getLogger().println("Checking out a fresh workspace because there's no workspace at " + module);
} else {
listener.getLogger().println("Checking out a fresh workspace because Jenkins failed to detect the current workspace " + module);
e.printStackTrace(listener.error(e.getMessage()));
}
return SvnCommandToUse.CHECKOUT;
}
return SvnCommandToUse.UPDATE;
}
private boolean isSameRepository(ModuleLocation location, SVNInfo svnkitInfo) throws SVNException {
return location.getSVNURL().toString().startsWith(svnkitInfo.getRepositoryRootURL().toString());
}
/**
* Gets the SVN metadata for the given local workspace.
*
* @param workspace
* The target to run "svn info".
*/
private SVNInfo parseSvnInfo(File workspace) throws SVNException {
final SVNWCClient svnWc = clientManager.getWCClient();
return svnWc.doInfo(workspace,SVNRevision.WORKING);
}
@Override
public List<External> perform() throws IOException, InterruptedException {
SvnCommandToUse svnCommand = getSvnCommandToUse();
if (svnCommand == SvnCommandToUse.CHECKOUT) {
return delegateTo(new CheckoutUpdater());
}
final SVNUpdateClient svnuc = clientManager.getUpdateClient();
final List<External> externals = new ArrayList<External>(); // store discovered externals to here
try {
File local = new File(ws, location.getLocalDir());
SubversionUpdateEventHandler eventHandler = new SubversionUpdateEventHandler(listener.getLogger(), externals, local, location.getLocalDir());
svnuc.setEventHandler(eventHandler);
svnuc.setExternalsHandler(eventHandler);
SVNRevision r = getRevision(location);
String revisionName = r.getDate() != null ?
fmt.format(r.getDate()) : r.toString();
svnuc.setIgnoreExternals(location.isIgnoreExternalsOption());
preUpdate(location, local);
SVNDepth svnDepth = getSvnDepth(location.getDepthOption());
switch (svnCommand) {
case UPDATE:
listener.getLogger().println("Updating " + location.remote + " at revision " + revisionName);
svnuc.doUpdate(local.getCanonicalFile(), r, svnDepth, true, true);
break;
case SWITCH:
listener.getLogger().println("Switching to " + location.remote + " at revision " + revisionName);
svnuc.doSwitch(local.getCanonicalFile(), location.getSVNURL(), r, r, svnDepth, true, true, true);
break;
case CHECKOUT:
// This case is handled by the (svnCommand == SvnCommandToUse.CHECKOUT) above.
break;
}
} catch (SVNCancelException e) {
if (isAuthenticationFailedError(e)) {
e.printStackTrace(listener.error("Failed to check out " + location.remote));
return null;
} else {
listener.error("Subversion update has been canceled");
throw (InterruptedException)new InterruptedException().initCause(e);
}
} catch (final SVNException e) {
SVNException cause = e;
do {
SVNErrorCode errorCode = cause.getErrorMessage().getErrorCode();
if (errorCode == SVNErrorCode.WC_LOCKED) {
// work space locked. try fresh check out
listener.getLogger().println("Workspace appear to be locked, so getting a fresh workspace");
return delegateTo(new CheckoutUpdater());
}
if (errorCode == SVNErrorCode.WC_OBSTRUCTED_UPDATE) {
// HUDSON-1882. If existence of local files cause an update to fail,
// revert to fresh check out
listener.getLogger().println(e.getMessage()); // show why this happened. Sometimes this is caused by having a build artifact in the repository.
listener.getLogger().println("Updated failed due to local files. Getting a fresh workspace");
return delegateTo(new CheckoutUpdater());
}
if (errorCode == SVNErrorCode.WC_CORRUPT_TEXT_BASE || errorCode == SVNErrorCode.WC_CORRUPT || errorCode == SVNErrorCode.WC_UNWIND_EMPTY) {
// JENKINS-14550. if working copy is corrupted, revert to fresh check out
listener.getLogger().println(e.getMessage()); // show why this happened. Sometimes this is caused by having a build artifact in the repository.
listener.getLogger().println("Updated failed due to working copy corruption. Getting a fresh workspace");
return delegateTo(new CheckoutUpdater());
}
// trouble-shooting probe for #591
if (errorCode == SVNErrorCode.WC_NOT_LOCKED) {
Hudson instance = Hudson.getInstance();
if (instance != null) {
listener.getLogger().println("Polled jobs are " + instance.getDescriptorByType(SCMTrigger.DescriptorImpl.class).getItemsBeingPolled());
}
return delegateTo(new CheckoutUpdater());
}
// recurse as long as we encounter nested SVNException
} while (null != (cause = getNestedSVNException(cause)));
e.printStackTrace(listener.error("Failed to update " + location.remote));
listener.error("Subversion update failed");
throw (IOException) new IOException().initCause(new UpdaterException("failed to perform svn update", e));
}
return externals;
}
/**
* Retrieve nested SVNException.
* lib.svnkit use to hide the root cause within nested {@link SVNException}. Also, SVNException cause in many cases
* is a {@link SVNErrorMessage}, that itself has a lower level SVNException as cause, and so on.
*/
private SVNException getNestedSVNException(Throwable e) {
Throwable t = e.getCause();
if (t instanceof SVNException) return (SVNException) t;
return null;
}
/**
* Hook for subtype to perform some cleanup activity before "svn update" takes place.
*
* @param module
* Remote repository that corresponds to the workspace.
* @param local
* Local directory that gets the update from the module.
*/
protected void preUpdate(ModuleLocation module, File local) throws SVNException, IOException {
// noop by default
}
}
@Extension(ordinal=100) // this is the default, so given a higher ordinal
public static class DescriptorImpl extends WorkspaceUpdaterDescriptor {
@Override
public String getDisplayName() {
return Messages.UpdateUpdater_DisplayName();
}
}
private static enum SvnCommandToUse {
UPDATE,
SWITCH,
CHECKOUT
}
}