hudson.scm.SubversionSCM.java Source code

Java tutorial

Introduction

Here is the source code for hudson.scm.SubversionSCM.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2004-2012, 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, OHTAKE Tomohiro
 *
 * 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;

import static hudson.Util.fixEmptyAndTrim;
import static hudson.scm.PollingResult.BUILD_NOW;
import static hudson.scm.PollingResult.NO_CHANGES;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.Functions;
import hudson.Launcher;
import hudson.Util;
import hudson.XmlFile;
import hudson.model.BuildListener;
import hudson.model.Item;
import hudson.model.TaskListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Computer;
import hudson.model.Hudson;
import hudson.model.Node;
import hudson.model.ParametersAction;
import hudson.model.Run;
import hudson.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.scm.UserProvidedCredential.AuthenticationManagerImpl;
import hudson.scm.subversion.Messages;
import hudson.scm.subversion.SvnHelper;
import hudson.scm.subversion.UpdaterException;
import hudson.scm.subversion.WorkspaceUpdaterDescriptor;
import hudson.scm.subversion.CheckoutUpdater;
import hudson.scm.subversion.UpdateUpdater;
import hudson.scm.subversion.UpdateWithRevertUpdater;
import hudson.scm.subversion.WorkspaceUpdater;
import hudson.scm.subversion.WorkspaceUpdater.UpdateTask;
import hudson.util.EditDistance;
import hudson.util.FormValidation;
import hudson.util.LogTaskListener;
import hudson.util.MultipartFormDataParser;
import hudson.util.Scrambler;
import hudson.util.Secret;
import hudson.util.TimeUnit2;
import hudson.util.XStream2;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.servlet.ServletException;
import javax.xml.transform.stream.StreamResult;

import jenkins.model.Jenkins.MasterComputer;
import net.sf.json.JSONObject;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Chmod;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNAuthenticationException;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNDirEntry;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationOutcomeListener;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider;
import org.tmatesoft.svn.core.auth.SVNAuthentication;
import org.tmatesoft.svn.core.auth.SVNPasswordAuthentication;
import org.tmatesoft.svn.core.auth.SVNSSHAuthentication;
import org.tmatesoft.svn.core.auth.SVNSSLAuthentication;
import org.tmatesoft.svn.core.auth.SVNUserNameAuthentication;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.dav.http.DefaultHTTPConnectionFactory;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminAreaFactory;
import org.tmatesoft.svn.core.io.SVNCapability;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNInfo;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;

import com.thoughtworks.xstream.XStream;
import com.trilead.ssh2.DebugLogger;
import com.trilead.ssh2.SCPClient;
import com.trilead.ssh2.crypto.Base64;

/**
 * Subversion SCM.
 *
 * <h2>Plugin Developer Notes</h2>
 * <p>
 * Plugins that interact with Subversion can use {@link DescriptorImpl#createAuthenticationProvider(AbstractProject)}
 * so that it can use the credentials (username, password, etc.) that the user entered for Hudson.
 * See the javadoc of this method for the precautions you need to take if you run Subversion operations
 * remotely on slaves.
 *
 * <h2>Implementation Notes</h2>
 * <p>
 * Because this instance refers to some other classes that are not necessarily
 * Java serializable (like {@link #browser}), remotable {@link FileCallable}s all
 * need to be declared as static inner classes.
 *
 * @author Kohsuke Kawaguchi
 */
@SuppressWarnings("rawtypes")
public class SubversionSCM extends SCM implements Serializable {
    /**
     * the locations field is used to store all configured SVN locations (with
     * their local and remote part). Direct access to this field should be
     * avoided and the getLocations() method should be used instead. This is
     * needed to make importing of old hudson-configurations possible as
     * getLocations() will check if the modules field has been set and import
     * the data.
     *
     * @since 1.91
     */
    private ModuleLocation[] locations = new ModuleLocation[0];

    private final SubversionRepositoryBrowser browser;
    private String excludedRegions;
    private String includedRegions;
    private String excludedUsers;
    /**
     * Revision property names that are ignored for the sake of polling. Whitespace separated, possibly null.
     */
    private String excludedRevprop;
    private String excludedCommitMessages;

    private WorkspaceUpdater workspaceUpdater;

    // No longer in use but left for serialization compatibility.
    @Deprecated
    private String modules;

    // No longer used but left for serialization compatibility
    @Deprecated
    private Boolean useUpdate;
    @Deprecated
    private Boolean doRevert;

    private boolean ignoreDirPropChanges;
    private boolean filterChangelog;

    /**
     * A cache of the svn:externals (keyed by project).
     */
    private transient Map<AbstractProject, List<External>> projectExternalsCache;

    private transient boolean pollFromMaster = POLL_FROM_MASTER;

    /**
     * @deprecated as of 1.286
     */
    public SubversionSCM(String[] remoteLocations, String[] localLocations, boolean useUpdate,
            SubversionRepositoryBrowser browser) {
        this(remoteLocations, localLocations, useUpdate, browser, null, null, null);
    }

    /**
     * @deprecated as of 1.311
     */
    public SubversionSCM(String[] remoteLocations, String[] localLocations, boolean useUpdate,
            SubversionRepositoryBrowser browser, String excludedRegions) {
        this(ModuleLocation.parse(remoteLocations, localLocations, null, null), useUpdate, false, browser,
                excludedRegions, null, null, null);
    }

    /**
     * @deprecated as of 1.315
     */
    public SubversionSCM(String[] remoteLocations, String[] localLocations, boolean useUpdate,
            SubversionRepositoryBrowser browser, String excludedRegions, String excludedUsers,
            String excludedRevprop) {
        this(ModuleLocation.parse(remoteLocations, localLocations, null, null), useUpdate, false, browser,
                excludedRegions, excludedUsers, excludedRevprop, null);
    }

    /**
      * @deprecated as of 1.315
      */
    public SubversionSCM(List<ModuleLocation> locations, boolean useUpdate, SubversionRepositoryBrowser browser,
            String excludedRegions) {
        this(locations, useUpdate, false, browser, excludedRegions, null, null, null);
    }

    /**
     * @deprecated as of 1.324
     */
    public SubversionSCM(List<ModuleLocation> locations, boolean useUpdate, SubversionRepositoryBrowser browser,
            String excludedRegions, String excludedUsers, String excludedRevprop) {
        this(locations, useUpdate, false, browser, excludedRegions, excludedUsers, excludedRevprop, null);
    }

    /**
     * @deprecated as of 1.328
     */
    public SubversionSCM(List<ModuleLocation> locations, boolean useUpdate, SubversionRepositoryBrowser browser,
            String excludedRegions, String excludedUsers, String excludedRevprop, String excludedCommitMessages) {
        this(locations, useUpdate, false, browser, excludedRegions, excludedUsers, excludedRevprop,
                excludedCommitMessages);
    }

    /**
     * @deprecated as of 1.xxx
     */
    public SubversionSCM(List<ModuleLocation> locations, boolean useUpdate, boolean doRevert,
            SubversionRepositoryBrowser browser, String excludedRegions, String excludedUsers,
            String excludedRevprop, String excludedCommitMessages) {
        this(locations, useUpdate, doRevert, browser, excludedRegions, excludedUsers, excludedRevprop,
                excludedCommitMessages, null);
    }

    /**
     * @deprecated  as of 1.23
     */
    public SubversionSCM(List<ModuleLocation> locations, boolean useUpdate, boolean doRevert,
            SubversionRepositoryBrowser browser, String excludedRegions, String excludedUsers,
            String excludedRevprop, String excludedCommitMessages, String includedRegions) {
        this(locations,
                useUpdate ? (doRevert ? new UpdateWithRevertUpdater() : new UpdateUpdater())
                        : new CheckoutUpdater(),
                browser, excludedRegions, excludedUsers, excludedRevprop, excludedCommitMessages, includedRegions);
    }

    /**
     *
     * @deprecated as of ...
     */
    public SubversionSCM(List<ModuleLocation> locations, WorkspaceUpdater workspaceUpdater,
            SubversionRepositoryBrowser browser, String excludedRegions, String excludedUsers,
            String excludedRevprop, String excludedCommitMessages, String includedRegions) {
        this(locations, workspaceUpdater, browser, excludedRegions, excludedUsers, excludedRevprop,
                excludedCommitMessages, includedRegions, false);
    }

    /**
     *  @deprecated
     */
    public SubversionSCM(List<ModuleLocation> locations, WorkspaceUpdater workspaceUpdater,
            SubversionRepositoryBrowser browser, String excludedRegions, String excludedUsers,
            String excludedRevprop, String excludedCommitMessages, String includedRegions,
            boolean ignoreDirPropChanges) {
        this(locations, workspaceUpdater, browser, excludedRegions, excludedUsers, excludedRevprop,
                excludedCommitMessages, includedRegions, ignoreDirPropChanges, false);
    }

    @DataBoundConstructor
    public SubversionSCM(List<ModuleLocation> locations, WorkspaceUpdater workspaceUpdater,
            SubversionRepositoryBrowser browser, String excludedRegions, String excludedUsers,
            String excludedRevprop, String excludedCommitMessages, String includedRegions,
            boolean ignoreDirPropChanges, boolean filterChangelog) {
        for (Iterator<ModuleLocation> itr = locations.iterator(); itr.hasNext();) {
            ModuleLocation ml = itr.next();
            String remote = Util.fixEmptyAndTrim(ml.remote);
            if (remote == null)
                itr.remove();
        }
        this.locations = locations.toArray(new ModuleLocation[locations.size()]);

        this.workspaceUpdater = workspaceUpdater;
        this.browser = browser;
        this.excludedRegions = excludedRegions;
        this.excludedUsers = excludedUsers;
        this.excludedRevprop = excludedRevprop;
        this.excludedCommitMessages = excludedCommitMessages;
        this.includedRegions = includedRegions;
        this.ignoreDirPropChanges = ignoreDirPropChanges;
        this.filterChangelog = filterChangelog;
    }

    /**
     * Convenience constructor, especially during testing.
     */
    public SubversionSCM(String svnUrl) {
        this(svnUrl, ".");
    }

    /**
     * Convenience constructor, especially during testing.
     */
    public SubversionSCM(String svnUrl, String local) {
        this(new String[] { svnUrl }, new String[] { local }, true, null, null, null, null);
    }

    /**
     * Convenience constructor, especially during testing.
     */
    public SubversionSCM(String[] svnUrls, String[] locals) {
        this(svnUrls, locals, true, null, null, null, null);
    }

    /**
     * @deprecated
     *      as of 1.91. Use {@link #getLocations()} instead.
     */
    public String getModules() {
        return null;
    }

    /**
     * list of all configured svn locations
     *
     * @since 1.91
     */
    @Exported
    public ModuleLocation[] getLocations() {
        return getLocations(null, null);
    }

    @Exported
    public WorkspaceUpdater getWorkspaceUpdater() {
        if (workspaceUpdater != null)
            return workspaceUpdater;

        // data must have been read from old configuration.
        if (useUpdate != null && !useUpdate)
            return new CheckoutUpdater();
        if (doRevert != null && doRevert)
            return new UpdateWithRevertUpdater();
        return new UpdateUpdater();
    }

    public void setWorkspaceUpdater(WorkspaceUpdater workspaceUpdater) {
        this.workspaceUpdater = workspaceUpdater;
    }

    /**
     * @since 1.252
     * @deprecated Use {@link #getLocations(EnvVars, AbstractBuild)} for vars
     *             expansion to be performed on all env vars rather than just
     *             build parameters.
     */
    public ModuleLocation[] getLocations(AbstractBuild<?, ?> build) {
        return getLocations(null, build);
    }

    /**
     * List of all configured svn locations, expanded according to all env vars
     * or, if none defined, according to only build parameters values.
     *
     * @param env If non-null, variable expansions are performed against these vars
     * @param build If non-null (and if env is null), variable expansions are
     *              performed against the build parameters
     */
    public ModuleLocation[] getLocations(EnvVars env, AbstractBuild<?, ?> build) {
        // check if we've got a old location
        if (modules != null) {
            // import the old configuration
            List<ModuleLocation> oldLocations = new ArrayList<ModuleLocation>();
            StringTokenizer tokens = new StringTokenizer(modules);
            while (tokens.hasMoreTokens()) {
                // the remote (repository location)
                // the normalized name is always without the trailing '/'
                String remoteLoc = Util.removeTrailingSlash(tokens.nextToken());

                oldLocations.add(new ModuleLocation(remoteLoc, null));
            }

            locations = oldLocations.toArray(new ModuleLocation[oldLocations.size()]);
            modules = null;
        }

        if (env == null && build == null)
            return locations;

        ModuleLocation[] outLocations = new ModuleLocation[locations.length];
        if (env != null) {
            for (int i = 0; i < outLocations.length; i++) {
                outLocations[i] = locations[i].getExpandedLocation(env);
            }
        } else {
            for (int i = 0; i < outLocations.length; i++) {
                outLocations[i] = locations[i].getExpandedLocation(build);
            }
        }

        return outLocations;
    }

    /**
     * Get the list of every checked-out location. This differs from {@link #getLocations()}
     * which returns only the configured locations whereas this method returns the configured
     * locations + any svn:externals locations.
     */
    public ModuleLocation[] getProjectLocations(AbstractProject project) throws IOException {
        List<External> projectExternals = getExternals(project);

        ModuleLocation[] configuredLocations = getLocations();
        if (projectExternals.isEmpty()) {
            return configuredLocations;
        }

        List<ModuleLocation> allLocations = new ArrayList<ModuleLocation>(
                configuredLocations.length + projectExternals.size());
        allLocations.addAll(Arrays.asList(configuredLocations));

        for (External external : projectExternals) {
            allLocations.add(new ModuleLocation(external.url, external.path));
        }

        return allLocations.toArray(new ModuleLocation[allLocations.size()]);
    }

    private List<External> getExternals(AbstractProject context) throws IOException {
        Map<AbstractProject, List<External>> projectExternalsCache = getProjectExternalsCache();
        List<External> projectExternals;
        synchronized (projectExternalsCache) {
            projectExternals = projectExternalsCache.get(context);
        }

        if (projectExternals == null) {
            projectExternals = parseExternalsFile(context);

            synchronized (projectExternalsCache) {
                if (!projectExternalsCache.containsKey(context)) {
                    projectExternalsCache.put(context, projectExternals);
                }
            }
        }
        return projectExternals;
    }

    @Override
    @Exported
    public SubversionRepositoryBrowser getBrowser() {
        return browser;
    }

    @Exported
    public String getExcludedRegions() {
        return excludedRegions;
    }

    public String[] getExcludedRegionsNormalized() {
        return (excludedRegions == null || excludedRegions.trim().equals("")) ? null
                : excludedRegions.split("[\\r\\n]+");
    }

    private Pattern[] getExcludedRegionsPatterns() {
        String[] excluded = getExcludedRegionsNormalized();
        if (excluded != null) {
            Pattern[] patterns = new Pattern[excluded.length];

            int i = 0;
            for (String excludedRegion : excluded) {
                patterns[i++] = Pattern.compile(excludedRegion);
            }

            return patterns;
        }

        return new Pattern[0];
    }

    @Exported
    public String getIncludedRegions() {
        return includedRegions;
    }

    public String[] getIncludedRegionsNormalized() {
        return (includedRegions == null || includedRegions.trim().equals("")) ? null
                : includedRegions.split("[\\r\\n]+");
    }

    private Pattern[] getIncludedRegionsPatterns() {
        String[] included = getIncludedRegionsNormalized();
        if (included != null) {
            Pattern[] patterns = new Pattern[included.length];

            int i = 0;
            for (String includedRegion : included) {
                patterns[i++] = Pattern.compile(includedRegion);
            }

            return patterns;
        }

        return new Pattern[0];
    }

    @Exported
    public String getExcludedUsers() {
        return excludedUsers;
    }

    public Set<String> getExcludedUsersNormalized() {
        String s = fixEmptyAndTrim(excludedUsers);
        if (s == null)
            return Collections.emptySet();

        Set<String> users = new HashSet<String>();
        for (String user : s.split("[\\r\\n]+"))
            users.add(user.trim());
        return users;
    }

    @Exported
    public String getExcludedRevprop() {
        return excludedRevprop;
    }

    @Exported
    public String getExcludedCommitMessages() {
        return excludedCommitMessages;
    }

    public String[] getExcludedCommitMessagesNormalized() {
        String s = fixEmptyAndTrim(excludedCommitMessages);
        return s == null ? new String[0] : s.split("[\\r\\n]+");
    }

    private Pattern[] getExcludedCommitMessagesPatterns() {
        String[] excluded = getExcludedCommitMessagesNormalized();
        Pattern[] patterns = new Pattern[excluded.length];

        int i = 0;
        for (String excludedCommitMessage : excluded) {
            patterns[i++] = Pattern.compile(excludedCommitMessage);
        }

        return patterns;
    }

    @Exported
    public boolean isIgnoreDirPropChanges() {
        return ignoreDirPropChanges;
    }

    @Exported
    public boolean isFilterChangelog() {
        return filterChangelog;
    }

    /**
     * Sets the <tt>SVN_REVISION_n</tt> and <tt>SVN_URL_n</tt> environment variables during the build.
     */
    @Override
    public void buildEnvVars(AbstractBuild<?, ?> build, Map<String, String> env) {
        super.buildEnvVars(build, env);

        ModuleLocation[] svnLocations = getLocations(build);

        try {
            Map<String, Long> revisions = parseSvnRevisionFile(build);
            if (svnLocations.length == 1) {
                // for backwards compatibility if there's only a single modulelocation, we also set
                // SVN_REVISION and SVN_URL without '_n'
                String url = svnLocations[0].getURL();
                Long rev = revisions.get(url);
                if (rev != null) {
                    env.put("SVN_REVISION", rev.toString());
                    env.put("SVN_URL", url);
                } else {
                    LOGGER.log(WARNING, "no revision found corresponding to {0}; known: {1}",
                            new Object[] { url, revisions.keySet() });
                }
            }

            for (int i = 0; i < svnLocations.length; i++) {
                String url = svnLocations[i].getURL();
                Long rev = revisions.get(url);
                if (rev != null) {
                    env.put("SVN_REVISION_" + (i + 1), rev.toString());
                    env.put("SVN_URL_" + (i + 1), url);
                } else {
                    LOGGER.log(WARNING, "no revision found corresponding to {0}; known: {1}",
                            new Object[] { url, revisions.keySet() });
                }
            }

        } catch (IOException e) {
            LOGGER.log(WARNING, "error building environment variables", e);
        }
    }

    /**
     * Called after checkout/update has finished to compute the changelog.
     */
    private boolean calcChangeLog(AbstractBuild<?, ?> build, File changelogFile, BuildListener listener,
            List<External> externals, EnvVars env) throws IOException, InterruptedException {
        if (build.getPreviousBuild() == null) {
            // nothing to compare against
            return createEmptyChangeLog(changelogFile, listener, "log");
        }

        // some users reported that the file gets created with size 0. I suspect
        // maybe some XSLT engine doesn't close the stream properly.
        // so let's do it by ourselves to be really sure that the stream gets closed.
        OutputStream os = new BufferedOutputStream(new FileOutputStream(changelogFile));
        boolean created;
        try {
            created = new SubversionChangeLogBuilder(build, env, listener, this).run(externals,
                    new StreamResult(os));
        } finally {
            os.close();
        }
        if (!created)
            createEmptyChangeLog(changelogFile, listener, "log");

        return true;
    }

    /**
     * Please consider using the non-static version {@link #parseSvnRevisionFile(AbstractBuild)}!
     */
    /*package*/ static Map<String, Long> parseRevisionFile(AbstractBuild<?, ?> build) throws IOException {
        return parseRevisionFile(build, true, false);
    }

    /*package*/ Map<String, Long> parseSvnRevisionFile(AbstractBuild<?, ?> build) throws IOException {
        return parseRevisionFile(build);
    }

    /**
     * Reads the revision file of the specified build (or the closest, if the flag is so specified.)
     *
     * @param findClosest
     *      If true, this method will go back the build history until it finds a revision file.
     *      A build may not have a revision file for any number of reasons (such as failure, interruption, etc.)
     * @return
     *      map from {@link SvnInfo#url Subversion URL} to its revision.  If there is more than one, choose
     *      the one with the smallest revision number
     */
    /*package*/ static Map<String, Long> parseRevisionFile(AbstractBuild<?, ?> build, boolean findClosest,
            boolean prunePinnedExternals) throws IOException {
        Map<String, Long> revisions = new HashMap<String, Long>(); // module -> revision

        if (findClosest) {
            for (AbstractBuild<?, ?> b = build; b != null; b = b.getPreviousBuild()) {
                if (getRevisionFile(b).exists()) {
                    build = b;
                    break;
                }
            }
        }

        {// read the revision file of the build
            File file = 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) {
                    boolean isPinned = false;
                    int indexLast = line.length();
                    if (line.lastIndexOf("::p") == indexLast - 3) {
                        isPinned = true;
                        indexLast -= 3;
                    }
                    int index = line.lastIndexOf('/');
                    if (index < 0) {
                        continue; // invalid line?
                    }
                    try {
                        String url = line.substring(0, index);
                        long revision = Long.parseLong(line.substring(index + 1, indexLast));
                        Long oldRevision = revisions.get(url);
                        if (isPinned) {
                            if (!prunePinnedExternals) {
                                if (oldRevision == null)
                                    // If we're writing pinned, only write if there are no unpinned
                                    revisions.put(url, revision);
                            }
                        } else {
                            // unpinned
                            if (oldRevision == null || oldRevision > revision)
                                // For unpinned, take minimum
                                revisions.put(url, revision);
                        }
                    } catch (NumberFormatException e) {
                        // perhaps a corrupted line.
                        LOGGER.log(WARNING, "Error parsing line " + line, e);
                    }
                }
            } finally {
                br.close();
            }
        }

        return revisions;
    }

    /**
     * Parses the file that stores the locations in the workspace where modules loaded by svn:external
     * is placed.
     *
     * <p>
     * Note that the format of the file has changed in 1.180 from simple text file to XML.
     *
     * @return
     *      immutable list. Can be empty but never null.
     */
    /*package*/ @SuppressWarnings("unchecked")
    static List<External> parseExternalsFile(AbstractProject project) throws IOException {
        File file = getExternalsFile(project);
        if (file.exists()) {
            try {
                return (List<External>) new XmlFile(External.XSTREAM, file).read();
            } catch (IOException e) {
                // in < 1.180 this file was a text file, so it may fail to parse as XML,
                // in which case let's just fall back
            }
        }

        return Collections.emptyList();
    }

    /**
     * Polling can happen on the master and does not require a workspace.
     */
    @Override
    public boolean requiresWorkspaceForPolling() {
        return false;
    }

    @SuppressWarnings("unchecked")
    public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace,
            final BuildListener listener, File changelogFile) throws IOException, InterruptedException {
        EnvVars env = build.getEnvironment(listener);
        EnvVarsUtils.overrideAll(env, build.getBuildVariables());

        List<External> externals = null;
        try {
            externals = checkout(build, workspace, listener, env);
        } catch (UpdaterException e) {
            return false;
        }

        // write out the revision file
        PrintWriter w = new PrintWriter(new FileOutputStream(getRevisionFile(build)));
        try {
            List<SvnInfoP> pList = workspace.act(new BuildRevisionMapTask(build, this, listener, externals, env));
            List<SvnInfo> revList = new ArrayList<SvnInfo>(pList.size());
            for (SvnInfoP p : pList) {
                if (p.pinned)
                    w.println(p.info.url + '/' + p.info.revision + "::p");
                else
                    w.println(p.info.url + '/' + p.info.revision);
                revList.add(p.info);
            }
            build.addAction(new SubversionTagAction(build, revList));
        } finally {
            w.close();
        }

        // write out the externals info
        new XmlFile(External.XSTREAM, getExternalsFile(build.getProject())).write(externals);
        Map<AbstractProject, List<External>> projectExternalsCache = getProjectExternalsCache();
        synchronized (projectExternalsCache) {
            projectExternalsCache.put(build.getProject(), externals);
        }

        return calcChangeLog(build, changelogFile, listener, externals, env);
    }

    /**
     * Performs the checkout or update, depending on the configuration and workspace state.
     *
     * <p>
     * Use canonical path to avoid SVNKit/symlink problem as described in
     * https://wiki.svnkit.com/SVNKit_FAQ
     *
     * @return null
     *      if the operation failed. Otherwise the set of local workspace paths
     *      (relative to the workspace root) that has loaded due to svn:external.
     */
    private List<External> checkout(AbstractBuild build, FilePath workspace, TaskListener listener, EnvVars env)
            throws IOException, InterruptedException {
        if (repositoryLocationsNoLongerExist(build, listener, env)) {
            Run lsb = build.getProject().getLastSuccessfulBuild();
            if (lsb != null && build.getNumber() - lsb.getNumber() > 10 && build.getTimestamp().getTimeInMillis()
                    - lsb.getTimestamp().getTimeInMillis() > TimeUnit2.DAYS.toMillis(1)) {
                // Disable this project if the location doesn't exist any more, see issue #763
                // but only do so if there was at least some successful build,
                // to make sure that initial configuration error won't disable the build. see issue #1567
                // finally, only disable a build if the failure persists for some time.
                // see http://www.nabble.com/Should-Hudson-have-an-option-for-a-content-fingerprint--td24022683.html

                listener.getLogger().println("One or more repository locations do not exist anymore for "
                        + build.getProject().getName() + ", project will be disabled.");
                build.getProject().makeDisabled(true);
                return null;
            }
        }

        List<External> externals = new ArrayList<External>();
        for (ModuleLocation location : getLocations(env, build)) {
            externals.addAll(workspace
                    .act(new CheckOutTask(build, this, location, build.getTimestamp().getTime(), listener, env)));
            // olamy: remove null check at it cause test failure
            // see https://github.com/jenkinsci/subversion-plugin/commit/de23a2b781b7b86f41319977ce4c11faee75179b#commitcomment-1551273
            /*if ( externalsFound != null ){
            externals.addAll(externalsFound);
            } else {
            externals.addAll( new ArrayList<External>( 0 ) );
            }*/
        }

        return externals;
    }

    private synchronized Map<AbstractProject, List<External>> getProjectExternalsCache() {
        if (projectExternalsCache == null) {
            projectExternalsCache = new WeakHashMap<AbstractProject, List<External>>();
        }

        return projectExternalsCache;
    }

    /**
     * Either run "svn co" or "svn up" equivalent.
     */
    private static class CheckOutTask extends UpdateTask implements FileCallable<List<External>> {
        private final UpdateTask task;

        public CheckOutTask(AbstractBuild<?, ?> build, SubversionSCM parent, ModuleLocation location,
                Date timestamp, TaskListener listener, EnvVars env) {
            this.authProvider = parent.getDescriptor().createAuthenticationProvider(build.getParent());
            this.timestamp = timestamp;
            this.listener = listener;
            this.location = location;
            this.revisions = build.getAction(RevisionParameterAction.class);
            this.task = parent.getWorkspaceUpdater().createTask();
        }

        public List<External> invoke(File ws, VirtualChannel channel) throws IOException {
            clientManager = createClientManager(authProvider);
            manager = clientManager.getCore();
            this.ws = ws;
            try {
                List<External> externals = perform();

                checkClockOutOfSync();

                return externals;

            } catch (InterruptedException e) {
                throw (InterruptedIOException) new InterruptedIOException().initCause(e);
            } finally {
                clientManager.dispose();
            }
        }

        /**
         * This round-about way of executing the task ensures that the error-prone {@link #delegateTo(UpdateTask)} method
         * correctly copies everything.
         */
        @Override
        public List<External> perform() throws IOException, InterruptedException {
            return delegateTo(task);
        }

        private void checkClockOutOfSync() {
            try {
                SVNDirEntry dir = clientManager.createRepository(location.getSVNURL(), true).info("/", -1);
                if (dir != null) {// I don't think this can ever be null, but be defensive
                    if (dir.getDate() != null && dir.getDate().after(new Date())) // see http://www.nabble.com/NullPointerException-in-SVN-Checkout-Update-td21609781.html that reported this being null.
                    {
                        listener.getLogger().println(Messages.SubversionSCM_ClockOutOfSync());
                    }
                }
            } catch (SVNAuthenticationException e) {
                // if we don't have access to '/', ignore. error
                LOGGER.log(Level.FINE, "Failed to estimate the remote time stamp", e);
            } catch (SVNException e) {
                LOGGER.log(Level.INFO, "Failed to estimate the remote time stamp", e);
            }
        }

        private static final long serialVersionUID = 1L;
    }

    /**
     *
     * @deprecated as of 1.40
     *      Use {@link #createClientManager(ISVNAuthenticationProvider)}
     */
    public static SVNClientManager createSvnClientManager(ISVNAuthenticationProvider authProvider) {
        return createClientManager(authProvider).getCore();
    }

    /**
     * Creates {@link SVNClientManager}.
     *
     * <p>
     * This method must be executed on the slave where svn operations are performed.
     *
     * @param authProvider
     *      The value obtained from {@link DescriptorImpl#createAuthenticationProvider(AbstractProject)}.
     *      If the operation runs on slaves,
     *      (and properly remoted, if the svn operations run on slaves.)
     */
    public static SvnClientManager createClientManager(ISVNAuthenticationProvider authProvider) {
        ISVNAuthenticationManager sam = createSvnAuthenticationManager(authProvider);
        return new SvnClientManager(SVNClientManager.newInstance(createDefaultSVNOptions(), sam));
    }

    /**
     * Creates the {@link DefaultSVNOptions}.
     *
     * @return the {@link DefaultSVNOptions}.
     */
    public static DefaultSVNOptions createDefaultSVNOptions() {
        DefaultSVNOptions defaultOptions = SVNWCUtil.createDefaultOptions(true);
        DescriptorImpl descriptor = Hudson.getInstance() == null ? null
                : Hudson.getInstance().getDescriptorByType(DescriptorImpl.class);
        if (defaultOptions != null && descriptor != null) {
            defaultOptions.setAuthStorageEnabled(descriptor.isStoreAuthToDisk());
        }
        return defaultOptions;
    }

    public static ISVNAuthenticationManager createSvnAuthenticationManager(
            ISVNAuthenticationProvider authProvider) {
        File configDir;
        if (CONFIG_DIR != null)
            configDir = new File(CONFIG_DIR);
        else
            configDir = SVNWCUtil.getDefaultConfigurationDirectory();

        ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(configDir, null, null);
        sam.setAuthenticationProvider(authProvider);
        SVNAuthStoreHandlerImpl.install(sam);
        return sam;
    }

    /**
     * @deprecated as of 2.0
     *      Use {@link #createClientManager(AbstractProject)}
     *
     */
    public static SVNClientManager createSvnClientManager(AbstractProject context) {
        return createClientManager(context).getCore();
    }

    /**
     * Creates {@link SVNClientManager} for code running on the master.
     * <p>
     * CAUTION: this code only works when invoked on master. On slaves, use
     * {@link #createSvnClientManager(ISVNAuthenticationProvider)} and get {@link ISVNAuthenticationProvider}
     * from the master via remoting.
     */
    public static SvnClientManager createClientManager(AbstractProject context) {
        return new SvnClientManager(createSvnClientManager(Hudson.getInstance()
                .getDescriptorByType(DescriptorImpl.class).createAuthenticationProvider(context)));
    }

    public static final class SvnInfo implements Serializable, Comparable<SvnInfo> {
        /**
         * Decoded repository URL.
         */
        public final String url;
        public final long revision;

        public SvnInfo(String url, long revision) {
            this.url = url;
            this.revision = revision;
        }

        public SvnInfo(SVNInfo info) {
            this(info.getURL().toDecodedString(), info.getCommittedRevision().getNumber());
        }

        public SVNURL getSVNURL() throws SVNException {
            return SVNURL.parseURIDecoded(url);
        }

        public int compareTo(SvnInfo that) {
            int r = this.url.compareTo(that.url);
            if (r != 0)
                return r;

            if (this.revision < that.revision)
                return -1;
            if (this.revision > that.revision)
                return +1;
            return 0;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            SvnInfo svnInfo = (SvnInfo) o;
            return revision == svnInfo.revision && url.equals(svnInfo.url);

        }

        @Override
        public int hashCode() {
            int result;
            result = url.hashCode();
            result = 31 * result + (int) (revision ^ (revision >>> 32));
            return result;
        }

        @Override
        public String toString() {
            return String.format("%s (rev.%s)", url, revision);
        }

        private static final long serialVersionUID = 1L;
    }

    /**
     * {@link SvnInfo} plus a flag if the revision is fixed.
     */
    private static final class SvnInfoP implements Serializable {
        /**
         * SvnInfo with an indicator boolean indicating whether this is a pinned external
         */
        public final SvnInfo info;
        public final boolean pinned;

        public SvnInfoP(SvnInfo info, boolean pinned) {
            this.info = info;
            this.pinned = pinned;
        }

        private static final long serialVersionUID = 1L;
    }

    /**
     * Information about svn:external
     */
    public static final class External implements Serializable {
        /**
         * Relative path within the workspace where this <tt>svn:exteranls</tt> exist.
         */
        public final String path;

        /**
         * External SVN URL to be fetched.
         */
        public final String url;

        /**
         * If the svn:external link is with the -r option, its number.
         * Otherwise -1 to indicate that the head revision of the external repository should be fetched.
         */
        public final long revision;

        public External(String path, SVNURL url, long revision) {
            this.path = path;
            this.url = url.toDecodedString();
            this.revision = revision;
        }

        /**
         * Returns true if this reference is to a fixed revision.
         */
        public boolean isRevisionFixed() {
            return revision != -1;
        }

        private static final long serialVersionUID = 1L;

        private static final XStream XSTREAM = new XStream2();
        static {
            XSTREAM.alias("external", External.class);
        }
    }

    /**
     * Gets the SVN metadata for the remote repository.
     *
     * @param remoteUrl
     *      The target to run "svn info".
     */
    static SVNInfo parseSvnInfo(SVNURL remoteUrl, ISVNAuthenticationProvider authProvider) throws SVNException {
        final SvnClientManager manager = createClientManager(authProvider);
        try {
            final SVNWCClient svnWc = manager.getWCClient();
            return svnWc.doInfo(remoteUrl, SVNRevision.HEAD, SVNRevision.HEAD);
        } finally {
            manager.dispose();
        }
    }

    /**
     * Checks .svn files in the workspace and finds out revisions of the modules
     * that the workspace has.
     *
     * @return
     *      null if the parsing somehow fails. Otherwise a map from the repository URL to revisions.
     */
    private static class BuildRevisionMapTask implements FileCallable<List<SvnInfoP>> {
        private final ISVNAuthenticationProvider authProvider;
        private final TaskListener listener;
        private final List<External> externals;
        private final ModuleLocation[] locations;

        public BuildRevisionMapTask(AbstractBuild<?, ?> build, SubversionSCM parent, TaskListener listener,
                List<External> externals, EnvVars env) {
            this.authProvider = parent.getDescriptor().createAuthenticationProvider(build.getParent());
            this.listener = listener;
            this.externals = externals;
            this.locations = parent.getLocations(env, build);
        }

        public List<SvnInfoP> invoke(File ws, VirtualChannel channel) throws IOException {
            List<SvnInfoP> revisions = new ArrayList<SvnInfoP>();

            final SvnClientManager manager = createClientManager(authProvider);
            try {
                final SVNWCClient svnWc = manager.getWCClient();
                // invoke the "svn info"
                for (ModuleLocation module : locations) {
                    try {
                        SvnInfo info = new SvnInfo(
                                svnWc.doInfo(new File(ws, module.getLocalDir()), SVNRevision.WORKING));
                        revisions.add(new SvnInfoP(info, false));
                    } catch (SVNException e) {
                        e.printStackTrace(listener.error("Failed to parse svn info for " + module.remote));
                    }
                }
                for (External ext : externals) {
                    try {
                        SvnInfo info = new SvnInfo(svnWc.doInfo(new File(ws, ext.path), SVNRevision.WORKING));
                        revisions.add(new SvnInfoP(info, ext.isRevisionFixed()));
                    } catch (SVNException e) {
                        e.printStackTrace(listener
                                .error("Failed to parse svn info for external " + ext.url + " at " + ext.path));
                    }
                }

                return revisions;
            } finally {
                manager.dispose();
            }
        }

        private static final long serialVersionUID = 1L;
    }

    /**
     * Gets the file that stores the revision.
     */
    public static File getRevisionFile(AbstractBuild build) {
        return new File(build.getRootDir(), "revision.txt");
    }

    /**
     * Gets the file that stores the externals.
     */
    private static File getExternalsFile(AbstractProject project) {
        return new File(project.getRootDir(), "svnexternals.txt");
    }

    @Override
    public SCMRevisionState calcRevisionsFromBuild(AbstractBuild<?, ?> build, Launcher launcher,
            TaskListener listener) throws IOException, InterruptedException {
        // exclude locations that are svn:external-ed with a fixed revision.
        Map<String, Long> wsRev = parseRevisionFile(build, true, true);
        return new SVNRevisionState(wsRev);
    }

    private boolean isPollFromMaster() {
        return pollFromMaster;
    }

    void setPollFromMaster(boolean pollFromMaster) {
        this.pollFromMaster = pollFromMaster;
    }

    @Override
    protected PollingResult compareRemoteRevisionWith(AbstractProject<?, ?> project, Launcher launcher,
            FilePath workspace, final TaskListener listener, SCMRevisionState _baseline)
            throws IOException, InterruptedException {
        if (getDescriptor().cancelBuildAtPostCommitHookOrPolling) {
            return PollingResult.NO_CHANGES;
        }
        final SVNRevisionState baseline;
        if (_baseline instanceof SVNRevisionState) {
            baseline = (SVNRevisionState) _baseline;
        } else if (project.getLastBuild() != null) {
            baseline = (SVNRevisionState) calcRevisionsFromBuild(project.getLastBuild(), launcher, listener);
        } else {
            baseline = new SVNRevisionState(null);
        }

        if (project.getLastBuild() == null) {
            listener.getLogger().println(Messages.SubversionSCM_pollChanges_noBuilds());
            return BUILD_NOW;
        }

        AbstractBuild<?, ?> lastCompletedBuild = project.getLastCompletedBuild();

        if (lastCompletedBuild != null) {
            EnvVars env = lastCompletedBuild.getEnvironment(listener);
            EnvVarsUtils.overrideAll(env, lastCompletedBuild.getBuildVariables());
            if (repositoryLocationsNoLongerExist(lastCompletedBuild, listener, env)) {
                // Disable this project, see HUDSON-763
                listener.getLogger().println(Messages.SubversionSCM_pollChanges_locationsNoLongerExist(project));
                project.makeDisabled(true);
                return NO_CHANGES;
            }

            // are the locations checked out in the workspace consistent with the current configuration?
            for (ModuleLocation loc : getLocations(env, lastCompletedBuild)) {
                // baseline.revisions has URIdecoded URL
                String url;
                try {
                    url = loc.getSVNURL().toDecodedString();
                } catch (SVNException ex) {
                    ex.printStackTrace(listener.error(Messages.SubversionSCM_pollChanges_exception(loc.getURL())));
                    return BUILD_NOW;
                }
                if (!baseline.revisions.containsKey(url)) {
                    listener.getLogger().println(Messages.SubversionSCM_pollChanges_locationNotInWorkspace(url));
                    return BUILD_NOW;
                }
            }
        }

        // determine where to perform polling. prefer the node where the build happened,
        // in case a cluster is non-uniform. see http://www.nabble.com/svn-connection-from-slave-only-td24970587.html
        VirtualChannel ch = null;
        Node n = null;
        if (!isPollFromMaster()) {
            n = lastCompletedBuild != null ? lastCompletedBuild.getBuiltOn() : null;
            if (n != null) {
                Computer c = n.toComputer();
                if (c != null)
                    ch = c.getChannel();
            }
        }
        if (ch == null)
            ch = MasterComputer.localChannel;

        final String nodeName = n != null ? n.getNodeName() : "master";

        final SVNLogHandler logHandler = new SVNLogHandler(createSVNLogFilter(), listener);

        final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(project);

        // figure out the remote revisions
        return ch.call(new CompareAgainstBaselineCallable(baseline, logHandler, project.getName(), listener,
                authProvider, nodeName));
    }

    public SVNLogFilter createSVNLogFilter() {
        return new DefaultSVNLogFilter(getExcludedRegionsPatterns(), getIncludedRegionsPatterns(),
                getExcludedUsersNormalized(), getExcludedRevprop(), getExcludedCommitMessagesPatterns(),
                isIgnoreDirPropChanges());
    }

    /**
     * Goes through the changes between two revisions and see if all the changes
     * are excluded.
     */
    static final class SVNLogHandler implements ISVNLogEntryHandler, Serializable {

        private boolean changesFound = false;
        private SVNLogFilter filter;

        SVNLogHandler(SVNLogFilter svnLogFilter, TaskListener listener) {
            this.filter = svnLogFilter;
            ;
            this.filter.setTaskListener(listener);
        }

        public boolean isChangesFound() {
            return changesFound;
        }

        /**
         * Checks it the revision range [from,to] has any changes that are not excluded via exclusions.
         */
        public boolean findNonExcludedChanges(SVNURL url, long from, long to,
                ISVNAuthenticationProvider authProvider) throws SVNException {
            if (from > to)
                return false; // empty revision range, meaning no change

            // if no exclusion rules are defined, don't waste time going through "svn log".
            if (!filter.hasExclusionRule())
                return true;

            final SvnClientManager manager = createClientManager(authProvider);
            try {
                manager.getLogClient().doLog(url, null, SVNRevision.UNDEFINED, SVNRevision.create(from), // get log entries from the local revision + 1
                        SVNRevision.create(to), // to the remote revision
                        false, // Don't stop on copy.
                        true, // Report paths.
                        false, // Don't included merged revisions
                        0, // Retrieve log entries for unlimited number of revisions.
                        null, // Retrieve all revprops
                        this);
            } finally {
                manager.dispose();
            }

            return isChangesFound();
        }

        /**
         * Handles a log entry passed.
         * Check for log entries that should be excluded from triggering a build.
         * If an entry is not an entry that should be excluded, set changesFound to true
         *
         * @param logEntry an {@link org.tmatesoft.svn.core.SVNLogEntry} object
         *                 that represents per revision information
         *                 (committed paths, log message, etc.)
         * @throws org.tmatesoft.svn.core.SVNException
         */
        public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
            if (filter.isIncluded(logEntry)) {
                changesFound = true;
            }
        }

        private static final long serialVersionUID = 1L;
    }

    public ChangeLogParser createChangeLogParser() {
        return new SubversionChangeLogParser(ignoreDirPropChanges);
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    /**
     * @deprecated
     */
    @Override
    @Deprecated
    public FilePath getModuleRoot(FilePath workspace) {
        if (getLocations().length > 0)
            return workspace.child(getLocations()[0].getLocalDir());
        return workspace;
    }

    @Override
    public FilePath getModuleRoot(FilePath workspace, AbstractBuild build) {
        if (build == null) {
            return getModuleRoot(workspace);
        }

        // TODO: can't I get the build listener here?
        TaskListener listener = new LogTaskListener(LOGGER, WARNING);
        final EnvVars env;
        try {
            env = build.getEnvironment(listener);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }

        if (getLocations().length > 0)
            return _getModuleRoot(workspace, getLocations()[0].getLocalDir(), env);
        return workspace;
    }

    @Deprecated
    @Override
    public FilePath[] getModuleRoots(FilePath workspace) {
        final ModuleLocation[] moduleLocations = getLocations();
        if (moduleLocations.length > 0) {
            FilePath[] moduleRoots = new FilePath[moduleLocations.length];
            for (int i = 0; i < moduleLocations.length; i++) {
                moduleRoots[i] = workspace.child(moduleLocations[i].getLocalDir());
            }
            return moduleRoots;
        }
        return new FilePath[] { getModuleRoot(workspace) };
    }

    @Override
    public FilePath[] getModuleRoots(FilePath workspace, AbstractBuild build) {
        if (build == null) {
            return getModuleRoots(workspace);
        }

        // TODO: can't I get the build listener here?
        TaskListener listener = new LogTaskListener(LOGGER, WARNING);
        final EnvVars env;
        try {
            env = build.getEnvironment(listener);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }

        final ModuleLocation[] moduleLocations = getLocations();
        if (moduleLocations.length > 0) {
            FilePath[] moduleRoots = new FilePath[moduleLocations.length];
            for (int i = 0; i < moduleLocations.length; i++) {
                moduleRoots[i] = _getModuleRoot(workspace, moduleLocations[i].getLocalDir(), env);
            }
            return moduleRoots;
        }
        return new FilePath[] { getModuleRoot(workspace, build) };

    }

    FilePath _getModuleRoot(FilePath workspace, String localDir, EnvVars env) {
        return workspace.child(env.expand(localDir));
    }

    private static String getLastPathComponent(String s) {
        String[] tokens = s.split("/");
        return tokens[tokens.length - 1]; // return the last token
    }

    @Extension
    public static class DescriptorImpl extends SCMDescriptor<SubversionSCM> implements hudson.model.ModelObject {
        /**
         * SVN authentication realm to its associated credentials.
         * This is the global credential repository.
         */
        private final Map<String, Credential> credentials = new Hashtable<String, Credential>();

        /**
         * Stores name of Subversion revision property to globally exclude
         */
        private String globalExcludedRevprop = null;

        private int workspaceFormat = SVNAdminAreaFactory.WC_FORMAT_14;

        private boolean cancelBuildAtPostCommitHookOrPolling = false;

        /**
         * When set to true, repository URLs will be validated up to the first
         * dollar sign which is encountered.
         */
        private boolean validateRemoteUpToVar = false;

        /**
         * When set to {@code false}, then auth details will never be stored on disk.
         * @since 1.27
         */
        private boolean storeAuthToDisk = true;

        /**
         * Stores {@link SVNAuthentication} for a single realm.
         *
         * <p>
         * {@link Credential} holds data in a persistence-friendly way,
         * and it's capable of creating {@link SVNAuthentication} object,
         * to be passed to SVNKit.
         */
        public static abstract class Credential implements Serializable {
            /**
             *
             */
            private static final long serialVersionUID = -3707951427730113110L;

            /**
             * @param kind
             *      One of the constants defined in {@link ISVNAuthenticationManager},
             *      indicating what subtype of {@link SVNAuthentication} is expected.
             */
            public abstract SVNAuthentication createSVNAuthentication(String kind) throws SVNException;
        }

        /**
         * Username/password based authentication.
         */
        public static final class PasswordCredential extends Credential {
            /**
             *
             */
            private static final long serialVersionUID = -1676145651108866745L;
            private final String userName;
            private final String password; // scrambled by base64

            public PasswordCredential(String userName, String password) {
                this.userName = userName;
                this.password = Scrambler.scramble(password);
            }

            @Override
            public SVNAuthentication createSVNAuthentication(String kind) {
                if (kind.equals(ISVNAuthenticationManager.SSH))
                    return new SVNSSHAuthentication(userName, Scrambler.descramble(password), -1, false);
                else
                    return new SVNPasswordAuthentication(userName, Scrambler.descramble(password), false);
            }
        }

        /**
         * Public key authentication for Subversion over SSH.
         */
        public static final class SshPublicKeyCredential extends Credential {
            /**
             *
             */
            private static final long serialVersionUID = -4649332611621900514L;
            private final String userName;
            private final String passphrase; // scrambled by base64
            private final String id;

            /**
             * @param keyFile
             *      stores SSH private key. The file will be copied.
             */
            public SshPublicKeyCredential(String userName, String passphrase, File keyFile) throws SVNException {
                this.userName = userName;
                this.passphrase = Scrambler.scramble(passphrase);

                Random r = new Random();
                StringBuilder buf = new StringBuilder();
                for (int i = 0; i < 16; i++)
                    buf.append(Integer.toHexString(r.nextInt(16)));
                this.id = buf.toString();

                try {
                    File savedKeyFile = getKeyFile();
                    FileUtils.copyFile(keyFile, savedKeyFile);
                    setFilePermissions(savedKeyFile, "600");
                } catch (IOException e) {
                    throw new SVNException(SVNErrorMessage
                            .create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE, "Unable to save private key")
                            .initCause(e));
                }
            }

            /**
             * Gets the location where the private key will be permanently stored.
             */
            private File getKeyFile() {
                File dir = new File(Hudson.getInstance().getRootDir(), "subversion-credentials");
                if (dir.mkdirs()) {
                    // make sure the directory exists. if we created it, try to set the permission to 600
                    // since this is sensitive information
                    setFilePermissions(dir, "600");
                }
                return new File(dir, id);
            }

            /**
             * Set the file permissions
             */
            private boolean setFilePermissions(File file, String perms) {
                try {
                    Chmod chmod = new Chmod();
                    chmod.setProject(new Project());
                    chmod.setFile(file);
                    chmod.setPerm(perms);
                    chmod.execute();
                } catch (BuildException e) {
                    // if we failed to set the permission, that's fine.
                    LOGGER.log(Level.WARNING, "Failed to set permission of " + file, e);
                    return false;
                }

                return true;
            }

            @Override
            public SVNSSHAuthentication createSVNAuthentication(String kind) throws SVNException {
                if (kind.equals(ISVNAuthenticationManager.SSH)) {
                    try {
                        Channel channel = Channel.current();
                        String privateKey;
                        if (channel != null) {
                            // remote
                            privateKey = channel.call(new Callable<String, IOException>() {
                                /**
                                 *
                                 */
                                private static final long serialVersionUID = -3088632649290496373L;

                                public String call() throws IOException {
                                    return FileUtils.readFileToString(getKeyFile(), "iso-8859-1");
                                }
                            });
                        } else {
                            privateKey = FileUtils.readFileToString(getKeyFile(), "iso-8859-1");
                        }
                        return new SVNSSHAuthentication(userName, privateKey.toCharArray(),
                                Scrambler.descramble(passphrase), -1, false);
                    } catch (IOException e) {
                        throw new SVNException(SVNErrorMessage
                                .create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE, "Unable to load private key")
                                .initCause(e));
                    } catch (InterruptedException e) {
                        throw new SVNException(SVNErrorMessage
                                .create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE, "Unable to load private key")
                                .initCause(e));
                    }
                } else
                    return null; // unknown
            }
        }

        /**
         * SSL client certificate based authentication.
         */
        public static final class SslClientCertificateCredential extends Credential {
            /**
             *
             */
            private static final long serialVersionUID = 5455755079546887446L;
            private final Secret certificate;
            private final String password; // scrambled by base64

            public SslClientCertificateCredential(File certificate, String password) throws IOException {
                this.password = Scrambler.scramble(password);
                this.certificate = Secret
                        .fromString(new String(Base64.encode(FileUtils.readFileToByteArray(certificate))));
            }

            @Override
            public SVNAuthentication createSVNAuthentication(String kind) {
                if (kind.equals(ISVNAuthenticationManager.SSL))
                    try {
                        SVNSSLAuthentication authentication = new SVNSSLAuthentication(
                                Base64.decode(certificate.getPlainText().toCharArray()),
                                Scrambler.descramble(password), false);
                        authentication.setCertificatePath("dummy"); // TODO: remove this JENKINS-19175 workaround
                        return authentication;
                    } catch (IOException e) {
                        throw new Error(e); // can't happen
                    }
                else
                    return null; // unexpected authentication type
            }
        }

        /**
         * Remoting interface that allows remote {@link ISVNAuthenticationProvider}
         * to read from local {@link DescriptorImpl#credentials}.
         */
        interface RemotableSVNAuthenticationProvider extends Serializable {
            Credential getCredential(SVNURL url, String realm);

            /**
             * Indicates that the specified credential worked.
             */
            void acknowledgeAuthentication(String realm, Credential credential);
        }

        /**
         * There's no point in exporting multiple {@link RemotableSVNAuthenticationProviderImpl} instances,
         * so let's just use one instance.
         */
        private transient final RemotableSVNAuthenticationProviderImpl remotableProvider = new RemotableSVNAuthenticationProviderImpl();

        private final class RemotableSVNAuthenticationProviderImpl implements RemotableSVNAuthenticationProvider {
            /**
             *
             */
            private static final long serialVersionUID = 1243451839093253666L;

            public Credential getCredential(SVNURL url, String realm) {
                for (SubversionCredentialProvider p : SubversionCredentialProvider.all()) {
                    Credential c = p.getCredential(url, realm);
                    if (c != null) {
                        LOGGER.fine(String.format("getCredential(%s)=>%s by %s", realm, c, p));
                        return c;
                    }
                }
                LOGGER.fine(String.format("getCredential(%s)=>%s", realm, credentials.get(realm)));
                return credentials.get(realm);
            }

            public void acknowledgeAuthentication(String realm, Credential credential) {
                // this notification is only used on the project-local store.
            }

            /**
             * When sent to the remote node, send a proxy.
             */
            private Object writeReplace() {
                return Channel.current().export(RemotableSVNAuthenticationProvider.class, this);
            }
        }

        /**
         * See {@link DescriptorImpl#createAuthenticationProvider(AbstractProject)}.
         */
        static final class SVNAuthenticationProviderImpl
                implements ISVNAuthenticationProvider, ISVNAuthenticationOutcomeListener, Serializable {
            /**
             * Project-scoped authentication source. For historical reasons, can be null.
             */
            private final RemotableSVNAuthenticationProvider local;

            /**
             * System-wide authentication source. Used as a fallback.
             */
            private final RemotableSVNAuthenticationProvider global;

            /**
             * The {@link Credential} used to create the last {@link SVNAuthentication} that we've tried.
             */
            private Credential lastCredential;

            public SVNAuthenticationProviderImpl(RemotableSVNAuthenticationProvider local,
                    RemotableSVNAuthenticationProvider global) {
                this.global = global;
                this.local = local;
            }

            private SVNAuthentication fromProvider(SVNURL url, String realm, String kind,
                    RemotableSVNAuthenticationProvider src, String debugName) throws SVNException {
                if (src == null)
                    return null;

                Credential cred = src.getCredential(url, realm);
                LOGGER.fine(String.format("%s.requestClientAuthentication(%s,%s,%s)=>%s", debugName, kind, url,
                        realm, cred));
                this.lastCredential = cred;
                if (cred != null)
                    return cred.createSVNAuthentication(kind);
                return null;
            }

            public SVNAuthentication requestClientAuthentication(String kind, SVNURL url, String realm,
                    SVNErrorMessage errorMessage, SVNAuthentication previousAuth, boolean authMayBeStored) {

                try {
                    SVNAuthentication auth = fromProvider(url, realm, kind, local, "local");

                    // first try the local credential, then the global credential.
                    if (auth == null || compareSVNAuthentications(auth, previousAuth))
                        auth = fromProvider(url, realm, kind, global, "global");

                    if (previousAuth != null && compareSVNAuthentications(auth, previousAuth)) {
                        // See HUDSON-2909
                        // this comparison is necessary, unlike the original fix of HUDSON-2909, since SVNKit may use
                        // other ISVNAuthenticationProviders and their failed auth might be passed to us.
                        // see HUDSON-3936
                        LOGGER.log(FINE, "Previous authentication attempt failed, so aborting: {0}", previousAuth);
                        return null;
                    }

                    if (auth == null && ISVNAuthenticationManager.USERNAME.equals(kind)) {
                        // this happens with file:// URL and svn+ssh (in this case this method gets invoked twice.)
                        // The base class does this, too.
                        // user auth shouldn't be null.
                        return new SVNUserNameAuthentication("", false);
                    }

                    return auth;
                } catch (SVNException e) {
                    LOGGER.log(Level.SEVERE, "Failed to authorize", e);
                    throw new RuntimeException("Failed to authorize", e);
                }
            }

            public void acknowledgeAuthentication(boolean accepted, String kind, String realm,
                    SVNErrorMessage errorMessage, SVNAuthentication authentication) throws SVNException {
                if (accepted && local != null && lastCredential != null)
                    local.acknowledgeAuthentication(realm, lastCredential);
            }

            public int acceptServerAuthentication(SVNURL url, String realm, Object certificate,
                    boolean resultMayBeStored) {
                return ACCEPTED_TEMPORARY;
            }

            private static final long serialVersionUID = 1L;
        }

        @Override
        public SCM newInstance(StaplerRequest staplerRequest, JSONObject jsonObject) throws FormException {
            return super.newInstance(staplerRequest, jsonObject);
        }

        public DescriptorImpl() {
            super(SubversionRepositoryBrowser.class);
            load();
        }

        @SuppressWarnings("unchecked")
        protected DescriptorImpl(Class clazz, Class<? extends RepositoryBrowser> repositoryBrowser) {
            super(clazz, repositoryBrowser);
        }

        public boolean isCancelBuildAtPostCommitHookOrPolling() {
            return cancelBuildAtPostCommitHookOrPolling;
        }

        public String getDisplayName() {
            return "Subversion";
        }

        public String getGlobalExcludedRevprop() {
            return globalExcludedRevprop;
        }

        public int getWorkspaceFormat() {
            if (workspaceFormat == 0)
                return SVNAdminAreaFactory.WC_FORMAT_14; // default
            return workspaceFormat;
        }

        public boolean isValidateRemoteUpToVar() {
            return validateRemoteUpToVar;
        }

        public boolean isStoreAuthToDisk() {
            return storeAuthToDisk;
        }

        @Override
        public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
            globalExcludedRevprop = fixEmptyAndTrim(req.getParameter("svn.global_excluded_revprop"));
            workspaceFormat = Integer.parseInt(req.getParameter("svn.workspaceFormat"));
            validateRemoteUpToVar = formData.containsKey("validateRemoteUpToVar");
            storeAuthToDisk = formData.containsKey("storeAuthToDisk");
            cancelBuildAtPostCommitHookOrPolling = formData.getBoolean("cancelBuildAtPostCommitHookOrPolling");

            // Save configuration
            save();

            return super.configure(req, formData);
        }

        @Override
        public boolean isBrowserReusable(SubversionSCM x, SubversionSCM y) {
            ModuleLocation[] xl = x.getLocations(), yl = y.getLocations();
            if (xl.length != yl.length)
                return false;
            for (int i = 0; i < xl.length; i++)
                if (!xl[i].getURL().equals(yl[i].getURL()))
                    return false;
            return true;
        }

        /**
         * Creates {@link ISVNAuthenticationProvider} backed by {@link #credentials}.
         * This method must be invoked on the master, but the returned object is remotable.
         *
         * <p>
         * Therefore, to access {@link ISVNAuthenticationProvider}, you need to call this method
         * on the master, then pass the object to the slave side, then call
         * {@link SubversionSCM#createSvnClientManager(ISVNAuthenticationProvider)} on the slave.
         *
         * @see SubversionSCM#createSvnClientManager(ISVNAuthenticationProvider)
         */
        public ISVNAuthenticationProvider createAuthenticationProvider(AbstractProject<?, ?> inContextOf) {
            return new SVNAuthenticationProviderImpl(
                    inContextOf == null ? null : new PerJobCredentialStore(inContextOf), remotableProvider);
        }

        /**
         * @deprecated as of 1.18
         *      Now that Hudson allows different credentials to be given in different jobs,
         *      The caller should use {@link #createAuthenticationProvider(AbstractProject)} to indicate
         *      the project in which the subversion operation is performed.
         */
        public ISVNAuthenticationProvider createAuthenticationProvider() {
            return new SVNAuthenticationProviderImpl(null, remotableProvider);
        }

        /**
         * Submits the authentication info.
         */
        // TODO: stapler should do multipart/form-data handling
        public void doPostCredential(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
            Hudson.getInstance().checkPermission(Item.CONFIGURE);

            MultipartFormDataParser parser = new MultipartFormDataParser(req);

            // we'll record what credential we are trying here.
            StringWriter log = new StringWriter();
            PrintWriter logWriter = new PrintWriter(log);

            UserProvidedCredential upc = UserProvidedCredential.fromForm(req, parser);

            try {
                postCredential(parser.get("url"), upc, logWriter);
                rsp.sendRedirect("credentialOK");
            } catch (SVNException e) {
                logWriter.println("FAILED: " + e.getErrorMessage());
                req.setAttribute("message", log.toString());
                req.setAttribute("pre", true);
                req.setAttribute("exception", e);
                rsp.forward(Hudson.getInstance(), "error", req);
            } finally {
                upc.close();
            }
        }

        /**
         * @deprecated as of 1.18
         *      Use {@link #postCredential(AbstractProject, String, String, String, File, PrintWriter)}
         */
        public void postCredential(String url, String username, String password, File keyFile,
                PrintWriter logWriter) throws SVNException, IOException {
            postCredential(null, url, username, password, keyFile, logWriter);
        }

        public void postCredential(AbstractProject inContextOf, String url, String username, String password,
                File keyFile, PrintWriter logWriter) throws SVNException, IOException {
            postCredential(url, new UserProvidedCredential(username, password, keyFile, inContextOf), logWriter);
        }

        /**
         * Submits the authentication info.
         *
         * This code is fairly ugly because of the way SVNKit handles credentials.
         */
        public void postCredential(String url, final UserProvidedCredential upc, PrintWriter logWriter)
                throws SVNException, IOException {
            SVNRepository repository = null;

            try {
                // the way it works with SVNKit is that
                // 1) svnkit calls AuthenticationManager asking for a credential.
                //    this is when we can see the 'realm', which identifies the user domain.
                // 2) DefaultSVNAuthenticationManager returns the username and password we set below
                // 3) if the authentication is successful, svnkit calls back acknowledgeAuthentication
                //    (so we store the password info here)
                repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(url));
                repository.setTunnelProvider(createDefaultSVNOptions());
                AuthenticationManagerImpl authManager = upc.new AuthenticationManagerImpl(logWriter) {
                    @Override
                    protected void onSuccess(String realm, Credential cred) {
                        LOGGER.info("Persisted " + cred + " for " + realm);
                        credentials.put(realm, cred);
                        save();
                        if (upc.inContextOf != null)
                            new PerJobCredentialStore(upc.inContextOf).acknowledgeAuthentication(realm, cred);

                    }
                };
                authManager.setAuthenticationForced(true);
                repository.setAuthenticationManager(authManager);
                repository.testConnection();
                authManager.checkIfProtocolCompleted();
            } finally {
                if (repository != null)
                    repository.closeSession();
            }
        }

        /**
         * validate the value for a remote (repository) location.
         */
        public FormValidation doCheckRemote(StaplerRequest req, @AncestorInPath AbstractProject context,
                @QueryParameter String value) {
            // syntax check first
            String url = Util.fixEmptyAndTrim(value);
            if (url == null)
                return FormValidation.error(Messages.SubversionSCM_doCheckRemote_required());

            if (isValidateRemoteUpToVar()) {
                url = (url.indexOf('$') != -1) ? url.substring(0, url.indexOf('$')) : url;
            }

            if (!URL_PATTERN.matcher(url).matches())
                return FormValidation.errorWithMarkup(Messages.SubversionSCM_doCheckRemote_invalidUrl());

            // Test the connection only if we have job cuonfigure permission
            if (!Hudson.getInstance().hasPermission(Item.CONFIGURE))
                return FormValidation.ok();

            try {
                String urlWithoutRevision = SvnHelper.getUrlWithoutRevision(url);

                SVNURL repoURL = SVNURL.parseURIDecoded(urlWithoutRevision);
                if (checkRepositoryPath(context, repoURL) != SVNNodeKind.NONE) {
                    // something exists; now check revision if any

                    SVNRevision revision = getRevisionFromRemoteUrl(url);
                    if (revision != null && !revision.isValid()) {
                        return FormValidation
                                .errorWithMarkup(Messages.SubversionSCM_doCheckRemote_invalidRevision());
                    }

                    return FormValidation.ok();
                }

                SVNRepository repository = null;
                try {
                    repository = getRepository(context, repoURL);
                    long rev = repository.getLatestRevision();
                    // now go back the tree and find if there's anything that exists
                    String repoPath = getRelativePath(repoURL, repository);
                    String p = repoPath;
                    while (p.length() > 0) {
                        p = SVNPathUtil.removeTail(p);
                        if (repository.checkPath(p, rev) == SVNNodeKind.DIR) {
                            // found a matching path
                            List<SVNDirEntry> entries = new ArrayList<SVNDirEntry>();
                            repository.getDir(p, rev, false, entries);

                            // build up the name list
                            List<String> paths = new ArrayList<String>();
                            for (SVNDirEntry e : entries)
                                if (e.getKind() == SVNNodeKind.DIR)
                                    paths.add(e.getName());

                            String head = SVNPathUtil.head(repoPath.substring(p.length() + 1));
                            String candidate = EditDistance.findNearest(head, paths);

                            return FormValidation.error(Messages.SubversionSCM_doCheckRemote_badPathSuggest(p, head,
                                    candidate != null ? "/" + candidate : ""));
                        }
                    }

                    return FormValidation.error(Messages.SubversionSCM_doCheckRemote_badPath(repoPath));
                } finally {
                    if (repository != null)
                        repository.closeSession();
                }
            } catch (SVNException e) {
                LOGGER.log(Level.INFO, "Failed to access subversion repository " + url, e);
                String message = Messages.SubversionSCM_doCheckRemote_exceptionMsg1(Util.escape(url),
                        Util.escape(e.getErrorMessage().getFullMessage()),
                        "javascript:document.getElementById('svnerror').style.display='block';"
                                + "document.getElementById('svnerrorlink').style.display='none';" + "return false;")
                        + "<br/><pre id=\"svnerror\" style=\"display:none\">" + Functions.printThrowable(e)
                        + "</pre>" + Messages.SubversionSCM_doCheckRemote_exceptionMsg2(
                                "descriptorByName/" + SubversionSCM.class.getName() + "/enterCredential?" + url);
                return FormValidation.errorWithMarkup(message);
            }
        }

        public SVNNodeKind checkRepositoryPath(AbstractProject context, SVNURL repoURL) throws SVNException {
            SVNRepository repository = null;

            try {
                repository = getRepository(context, repoURL);
                repository.testConnection();

                long rev = repository.getLatestRevision();
                String repoPath = getRelativePath(repoURL, repository);
                return repository.checkPath(repoPath, rev);
            } finally {
                if (repository != null)
                    repository.closeSession();
            }
        }

        protected SVNRepository getRepository(AbstractProject context, SVNURL repoURL) throws SVNException {
            SVNRepository repository = SVNRepositoryFactory.create(repoURL);

            ISVNAuthenticationManager sam = createSvnAuthenticationManager(createAuthenticationProvider(context));
            sam = new FilterSVNAuthenticationManager(sam) {
                // If there's no time out, the blocking read operation may hang forever, because TCP itself
                // has no timeout. So always use some time out. If the underlying implementation gives us some
                // value (which may come from ~/.subversion), honor that, as long as it sets some timeout value.
                @Override
                public int getReadTimeout(SVNRepository repository) {
                    int r = super.getReadTimeout(repository);
                    if (r <= 0)
                        r = DEFAULT_TIMEOUT;
                    return r;
                }
            };
            repository.setTunnelProvider(createDefaultSVNOptions());
            repository.setAuthenticationManager(sam);

            return repository;
        }

        public static String getRelativePath(SVNURL repoURL, SVNRepository repository) throws SVNException {
            String repoPath = repoURL.getPath().substring(repository.getRepositoryRoot(false).getPath().length());
            if (!repoPath.startsWith("/"))
                repoPath = "/" + repoPath;
            return repoPath;
        }

        /**
         * validate the value for a local location (local checkout directory).
         */
        public FormValidation doCheckLocal(@QueryParameter String value) throws IOException, ServletException {
            String v = Util.nullify(value);
            if (v == null)
                // local directory is optional so this is ok
                return FormValidation.ok();

            v = v.trim();

            // check if a absolute path has been supplied
            // (the last check with the regex will match windows drives)
            if (v.startsWith("/") || v.startsWith("\\") || v.startsWith("..") || v.matches("^[A-Za-z]:.*"))
                return FormValidation.error("absolute path is not allowed");

            // all tests passed so far
            return FormValidation.ok();
        }

        /**
         * Validates the excludeRegions Regex
         */
        public FormValidation doCheckExcludedRegions(@QueryParameter String value)
                throws IOException, ServletException {
            for (String region : Util.fixNull(value).trim().split("[\\r\\n]+"))
                try {
                    Pattern.compile(region);
                } catch (PatternSyntaxException e) {
                    return FormValidation.error("Invalid regular expression. " + e.getMessage());
                }
            return FormValidation.ok();
        }

        /**
         * Validates the includedRegions Regex
         */
        public FormValidation doCheckIncludedRegions(@QueryParameter String value)
                throws IOException, ServletException {
            return doCheckExcludedRegions(value);
        }

        /**
         * Regular expression for matching one username. Matches 'windows' names ('DOMAIN&#92;user') and
         * 'normal' names ('user'). Where user (and DOMAIN) has one or more characters in 'a-zA-Z_0-9')
         */
        private static final Pattern USERNAME_PATTERN = Pattern.compile("(\\w+\\\\)?+(\\w+)");

        /**
         * Validates the excludeUsers field
         */
        public FormValidation doCheckExcludedUsers(@QueryParameter String value)
                throws IOException, ServletException {
            for (String user : Util.fixNull(value).trim().split("[\\r\\n]+")) {
                user = user.trim();

                if ("".equals(user)) {
                    continue;
                }

                if (!USERNAME_PATTERN.matcher(user).matches()) {
                    return FormValidation.error("Invalid username: " + user);
                }
            }

            return FormValidation.ok();
        }

        public List<WorkspaceUpdaterDescriptor> getWorkspaceUpdaterDescriptors() {
            return WorkspaceUpdaterDescriptor.all();
        }

        /**
         * Validates the excludeCommitMessages field
         */
        public FormValidation doCheckExcludedCommitMessages(@QueryParameter String value)
                throws IOException, ServletException {
            for (String message : Util.fixNull(value).trim().split("[\\r\\n]+")) {
                try {
                    Pattern.compile(message);
                } catch (PatternSyntaxException e) {
                    return FormValidation.error("Invalid regular expression. " + e.getMessage());
                }
            }
            return FormValidation.ok();
        }

        /**
         * Validates the remote server supports custom revision properties
         */
        public FormValidation doCheckRevisionPropertiesSupported(@AncestorInPath AbstractProject context,
                @QueryParameter String value) throws IOException, ServletException {
            String v = Util.fixNull(value).trim();
            if (v.length() == 0)
                return FormValidation.ok();

            // Test the connection only if we have admin permission
            if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER))
                return FormValidation.ok();

            try {
                SVNURL repoURL = SVNURL.parseURIDecoded(v);
                if (checkRepositoryPath(context, repoURL) != SVNNodeKind.NONE)
                    // something exists
                    return FormValidation.ok();

                SVNRepository repository = null;
                try {
                    repository = getRepository(context, repoURL);
                    if (repository.hasCapability(SVNCapability.LOG_REVPROPS))
                        return FormValidation.ok();
                } finally {
                    if (repository != null)
                        repository.closeSession();
                }
            } catch (SVNException e) {
                String message = "";
                message += "Unable to access " + Util.escape(v) + " : "
                        + Util.escape(e.getErrorMessage().getFullMessage());
                LOGGER.log(Level.INFO, "Failed to access subversion repository " + v, e);
                return FormValidation.errorWithMarkup(message);
            }

            return FormValidation.warning(Messages.SubversionSCM_excludedRevprop_notSupported(v));
        }

        static {
            new Initializer();
        }

    }

    /**
     * @deprecated 1.34
     */
    public boolean repositoryLocationsNoLongerExist(AbstractBuild<?, ?> build, TaskListener listener) {
        return repositoryLocationsNoLongerExist(build, listener, null);
    }

    /**
     * @since 1.34
     */
    public boolean repositoryLocationsNoLongerExist(AbstractBuild<?, ?> build, TaskListener listener, EnvVars env) {
        PrintStream out = listener.getLogger();

        for (ModuleLocation l : getLocations(env, build))
            try {
                if (getDescriptor().checkRepositoryPath(build.getProject(), l.getSVNURL()) == SVNNodeKind.NONE) {
                    out.println("Location '" + l.remote + "' does not exist");

                    ParametersAction params = build.getAction(ParametersAction.class);
                    if (params != null) {
                        // since this is used to disable projects, be conservative
                        LOGGER.fine("Location could be expanded on build '" + build + "' parameters values:");
                        return false;
                    }
                    return true;
                }
            } catch (SVNException e) {
                // be conservative, since we are just trying to be helpful in detecting
                // non existent locations. If we can't detect that, we'll do nothing
                LOGGER.log(FINE, "Location check failed", e);
            }
        return false;
    }

    static final Pattern URL_PATTERN = Pattern.compile("(https?|svn(\\+[a-z0-9]+)?|file)://.+");

    private static final long serialVersionUID = 1L;

    // noop, but this forces the initializer to run.
    public static void init() {
    }

    static {
        new Initializer();
    }

    private static final class Initializer {
        static {
            if (Boolean.getBoolean("hudson.spool-svn"))
                DAVRepositoryFactory.setup(new DefaultHTTPConnectionFactory(null, true, null));
            else
                DAVRepositoryFactory.setup(); // http, https
            SVNRepositoryFactoryImpl.setup(); // svn, svn+xxx
            FSRepositoryFactory.setup(); // file

            // disable the connection pooling, which causes problems like
            // http://www.nabble.com/SSH-connection-problems-p12028339.html
            if (System.getProperty("svnkit.ssh2.persistent") == null)
                System.setProperty("svnkit.ssh2.persistent", "false");

            // push Negotiate to the end because it requires a valid Kerberos configuration.
            // see HUDSON-8153
            if (System.getProperty("svnkit.http.methods") == null)
                System.setProperty("svnkit.http.methods", "Digest,Basic,NTLM,Negotiate");

            // use SVN1.4 compatible workspace by default.
            SVNAdminAreaFactory.setSelector(new SubversionWorkspaceSelector());
        }
    }

    /**
     * small structure to store local and remote (repository) location
     * information of the repository. As a addition it holds the invalid field
     * to make failure messages when doing a checkout possible
     */
    @ExportedBean
    public static final class ModuleLocation implements Serializable {
        /**
         * Subversion URL to check out.
         *
         * This may include "@NNN" at the end to indicate a fixed revision.
         */
        @Exported
        public final String remote;

        /**
         * Remembers the user-given value.
         * Can be null.
         *
         * @deprecated
         *      Code should use {@link #getLocalDir()}. This field is only intended for form binding.
         */
        @Exported
        public final String local;

        /**
         * Subversion remote depth. Used as "--depth" option for checkout and update commands.
         * Default value is "infinity".
         */
        @Exported
        public final String depthOption;

        /**
         * Flag to ignore subversion externals definitions.
         */
        @Exported
        public boolean ignoreExternalsOption;

        /**
         * Cache of the repository UUID.
         */
        private transient volatile UUID repositoryUUID;
        private transient volatile SVNURL repositoryRoot;

        /**
         * Constructor to support backwards compatibility.
         */
        public ModuleLocation(String remote, String local) {
            this(remote, local, null, false);
        }

        @DataBoundConstructor
        public ModuleLocation(String remote, String local, String depthOption, boolean ignoreExternalsOption) {
            this.remote = Util.removeTrailingSlash(Util.fixNull(remote).trim());
            this.local = fixEmptyAndTrim(local);
            this.depthOption = StringUtils.isEmpty(depthOption) ? SVNDepth.INFINITY.getName() : depthOption;
            this.ignoreExternalsOption = ignoreExternalsOption;
        }

        /**
         * Local directory to place the file to.
         * Relative to the workspace root.
         */
        public String getLocalDir() {
            if (local == null)
                return getLastPathComponent(getURL());
            return local;
        }

        /**
         * Returns the pure URL portion of {@link #remote} by removing
         * possible "@NNN" suffix.
         */
        public String getURL() {
            return SvnHelper.getUrlWithoutRevision(remote);
        }

        /**
         * Gets {@link #remote} as {@link SVNURL}.
         */
        public SVNURL getSVNURL() throws SVNException {
            return SVNURL.parseURIEncoded(getURL());
        }

        /**
         * Repository UUID. Lazy computed and cached.
         */
        public UUID getUUID(AbstractProject context) throws SVNException {
            if (repositoryUUID == null || repositoryRoot == null) {
                synchronized (this) {
                    SVNRepository r = openRepository(context);
                    r.testConnection(); // make sure values are fetched
                    repositoryUUID = UUID.fromString(r.getRepositoryUUID(false));
                    repositoryRoot = r.getRepositoryRoot(false);
                }
            }
            return repositoryUUID;
        }

        public SVNRepository openRepository(AbstractProject context) throws SVNException {
            return Hudson.getInstance().getDescriptorByType(DescriptorImpl.class).getRepository(context,
                    getSVNURL());
        }

        public SVNURL getRepositoryRoot(AbstractProject context) throws SVNException {
            getUUID(context);
            return repositoryRoot;
        }

        /**
         * Figures out which revision to check out.
         *
         * If {@link #remote} is {@code url@rev}, then this method
         * returns that specific revision.
         *
         * @param defaultValue
         *      If "@NNN" portion is not in the URL, this value will be returned.
         *      Normally, this is the SVN revision timestamped at the build date.
         */
        public SVNRevision getRevision(SVNRevision defaultValue) {
            SVNRevision revision = getRevisionFromRemoteUrl(remote);
            return revision != null ? revision : defaultValue;
        }

        private String getExpandedRemote(AbstractBuild<?, ?> build) {
            String outRemote = remote;

            ParametersAction parameters = build.getAction(ParametersAction.class);
            if (parameters != null)
                outRemote = parameters.substitute(build, remote);

            return outRemote;
        }

        /**
         * @deprecated This method is used by {@link #getExpandedLocation(AbstractBuild)}
         *             which is deprecated since it expands variables only based
         *             on build parameters.
         */
        private String getExpandedLocalDir(AbstractBuild<?, ?> build) {
            String outLocalDir = getLocalDir();

            ParametersAction parameters = build.getAction(ParametersAction.class);
            if (parameters != null)
                outLocalDir = parameters.substitute(build, getLocalDir());

            return outLocalDir;
        }

        /**
         * Returns the value of remote depth option.
         *
         * @return the value of remote depth option.
         */
        public String getDepthOption() {
            return depthOption;
        }

        /**
         * Determines if subversion externals definitions should be ignored.
         *
         * @return true if subversion externals definitions should be ignored.
         */
        public boolean isIgnoreExternalsOption() {
            return ignoreExternalsOption;
        }

        /**
         * Expand location value based on Build parametric execution.
         *
         * @param build Build instance for expanding parameters into their values
         * @return Output ModuleLocation expanded according to Build parameters values.
         * @deprecated Use {@link #getExpandedLocation(EnvVars)} for vars expansion
         *             to be performed on all env vars rather than just build parameters.
         */
        public ModuleLocation getExpandedLocation(AbstractBuild<?, ?> build) {
            return new ModuleLocation(getExpandedRemote(build), getExpandedLocalDir(build));
        }

        /**
         * Expand location value based on environment variables.
         *
         * @return Output ModuleLocation expanded according to specified env vars.
         */
        public ModuleLocation getExpandedLocation(EnvVars env) {
            return new ModuleLocation(env.expand(remote), env.expand(getLocalDir()), getDepthOption(),
                    isIgnoreExternalsOption());
        }

        @Override
        public String toString() {
            return remote;
        }

        private static final long serialVersionUID = 1L;

        public static List<ModuleLocation> parse(String[] remoteLocations, String[] localLocations,
                String[] depthOptions, boolean[] isIgnoreExternals) {
            List<ModuleLocation> modules = new ArrayList<ModuleLocation>();
            if (remoteLocations != null && localLocations != null) {
                int entries = Math.min(remoteLocations.length, localLocations.length);

                for (int i = 0; i < entries; i++) {
                    // the remote (repository) location
                    String remoteLoc = Util.nullify(remoteLocations[i]);

                    if (remoteLoc != null) {// null if skipped
                        remoteLoc = Util.removeTrailingSlash(remoteLoc.trim());
                        modules.add(new ModuleLocation(remoteLoc, Util.nullify(localLocations[i]),
                                depthOptions != null ? depthOptions[i] : null,
                                isIgnoreExternals != null && isIgnoreExternals[i]));
                    }
                }
            }
            return modules;
        }
    }

    private static final Logger LOGGER = Logger.getLogger(SubversionSCM.class.getName());

    /**
     * Network timeout in milliseconds.
     * The main point of this is to prevent infinite hang, so it should be a rather long value to avoid
     * accidental time out problem.
     */
    public static int DEFAULT_TIMEOUT = Integer.getInteger(SubversionSCM.class.getName() + ".timeout", 3600 * 1000);

    /**
     * Property to control whether SCM polling happens from the slave or master
     */
    private static boolean POLL_FROM_MASTER = Boolean.getBoolean(SubversionSCM.class.getName() + ".pollFromMaster");

    /**
     * If set to non-null, read configuration from this directory instead of "~/.subversion".
     */
    public static String CONFIG_DIR = System.getProperty(SubversionSCM.class.getName() + ".configDir");

    /**
     * Enables trace logging of Ganymed SSH library.
     * <p>
     * Intended to be invoked from Groovy console.
     */
    public static void enableSshDebug(Level level) {
        if (level == null)
            level = Level.FINEST; // default

        final Level lv = level;

        com.trilead.ssh2.log.Logger.enabled = true;
        com.trilead.ssh2.log.Logger.logger = new DebugLogger() {
            private final Logger LOGGER = Logger.getLogger(SCPClient.class.getPackage().getName());

            public void log(int level, String className, String message) {
                LOGGER.log(lv, className + ' ' + message);
            }
        };
    }

    /*package*/ static boolean compareSVNAuthentications(SVNAuthentication a1, SVNAuthentication a2) {
        if (a1 == null && a2 == null)
            return true;
        if (a1 == null || a2 == null)
            return false;
        if (a1.getClass() != a2.getClass())
            return false;

        try {
            return describeBean(a1).equals(describeBean(a2));
        } catch (IllegalAccessException e) {
            return false;
        } catch (InvocationTargetException e) {
            return false;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

    /**
     * In preparation for a comparison, char[] needs to be converted that supports value equality.
     */
    @SuppressWarnings("unchecked")
    private static Map describeBean(Object o)
            throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        Map<?, ?> m = PropertyUtils.describe(o);
        for (Entry e : m.entrySet()) {
            Object v = e.getValue();
            if (v instanceof char[]) {
                char[] chars = (char[]) v;
                e.setValue(new String(chars));
            }
        }
        return m;
    }

    /**
     * Gets the revision from a remote URL - i.e. the part after '@' if any
     *
     * @return the revision or null
     */
    private static SVNRevision getRevisionFromRemoteUrl(String remoteUrlPossiblyWithRevision) {
        int idx = remoteUrlPossiblyWithRevision.lastIndexOf('@');
        int slashIdx = remoteUrlPossiblyWithRevision.lastIndexOf('/');
        if (idx > 0 && idx > slashIdx) {
            String n = remoteUrlPossiblyWithRevision.substring(idx + 1);
            return SVNRevision.parse(n);
        }

        return null;
    }

}