/*
* 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 com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.domains.DomainSpecification;
import com.cloudbees.plugins.credentials.domains.HostnameRequirement;
import com.cloudbees.plugins.credentials.domains.HostnameSpecification;
import com.cloudbees.plugins.credentials.domains.SchemeRequirement;
import com.cloudbees.plugins.credentials.domains.SchemeSpecification;
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.BulkChange;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.Functions;
import hudson.Launcher;
import hudson.Util;
import hudson.init.InitMilestone;
import hudson.model.*;
import java.io.ByteArrayOutputStream;
import java.nio.charset.UnsupportedCharsetException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.WeakHashMap;
import hudson.security.ACL;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
import jenkins.model.Jenkins.MasterComputer;
import hudson.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.scm.UserProvidedCredential.AuthenticationManagerImpl;
import hudson.scm.subversion.CheckoutUpdater;
import hudson.scm.subversion.Messages;
import hudson.scm.subversion.SvnHelper;
import hudson.scm.subversion.UpdateUpdater;
import hudson.scm.subversion.UpdateWithRevertUpdater;
import hudson.scm.subversion.WorkspaceUpdater;
import hudson.scm.subversion.WorkspaceUpdater.UpdateTask;
import hudson.scm.subversion.WorkspaceUpdaterDescriptor;
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 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.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
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.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.servlet.ServletException;
import javax.xml.transform.stream.StreamResult;
import net.sf.json.JSONObject;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
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.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
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.*;
import org.tmatesoft.svn.core.auth.*;
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.io.ISVNSession;
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.trilead.ssh2.DebugLogger;
import com.trilead.ssh2.SCPClient;
import com.trilead.ssh2.crypto.Base64;
import javax.annotation.Nonnull;
/**
* 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];
/**
* Additional credentials to use when checking out svn:externals
* @since 2.0
*/
@CheckForNull
private List<AdditionalCredentials> additionalCredentials;
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<Job, 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, null);
}
@DataBoundConstructor
public SubversionSCM(List<ModuleLocation> locations, WorkspaceUpdater workspaceUpdater,
SubversionRepositoryBrowser browser, String excludedRegions, String excludedUsers,
String excludedRevprop, String excludedCommitMessages,
String includedRegions, boolean ignoreDirPropChanges, boolean filterChangelog,
List<AdditionalCredentials> additionalCredentials) {
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()]);
if (additionalCredentials == null) {
this.additionalCredentials = null;
} else {
this.additionalCredentials = new ArrayList<AdditionalCredentials>(additionalCredentials);
}
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, null, ".");
}
/**
* Convenience constructor, especially during testing.
*/
public SubversionSCM(String svnUrl, String local) {
this(svnUrl, null, local);
}
/**
* Convenience constructor, especially during testing.
*/
public SubversionSCM(String svnUrl, String credentialId, String local) {
this(new String[]{svnUrl}, new String[]{credentialId}, new String[]{local});
}
/**
* Convenience constructor, especially during testing.
*/
public SubversionSCM(String[] svnUrls, String[] locals) {
this(svnUrls, null, locals);
}
/**
* Convenience constructor, especially during testing.
*/
public SubversionSCM(String[] svnUrls, String[] credentialIds, String[] locals) {
this(ModuleLocation.parse(svnUrls, credentialIds, locals, null,null), true, false, null, 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);
}
@Override public String getKey() {
StringBuilder b = new StringBuilder("svn");
for (ModuleLocation loc : getLocations()) {
b.append(' ').append(loc.getURL());
}
return b.toString();
}
public List<AdditionalCredentials> getAdditionalCredentials() {
List<AdditionalCredentials> result = new ArrayList<AdditionalCredentials>();
if (additionalCredentials != null) {
result.addAll(additionalCredentials);
}
return result;
}
@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, Run)} 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.
* Both may be defined, in which case the variables are combined.
* @param env If non-null, variable expansions are performed against these vars
* @param build If non-null, variable expansions are
* performed against the build parameters
*/
public ModuleLocation[] getLocations(EnvVars env, Run<?,?> 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];
EnvVars env2 = env != null ? new EnvVars(env) : new EnvVars();
if (build instanceof AbstractBuild) {
env2.putAll(((AbstractBuild<?,?>) build).getBuildVariables());
}
EnvVars.resolve(env2);
for (int i = 0; i < outLocations.length; i++) {
outLocations[i] = locations[i].getExpandedLocation(env2);
}
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(Job 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(Job context) throws IOException {
Map<Job, List<External>> projectExternalsCache = getProjectExternalsCache();
List<External> projectExternals;
synchronized (projectExternalsCache) {
projectExternals = projectExternalsCache.get(context);
}
if (projectExternals == null) {
projectExternals = SvnExternalsFileManager.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;
}
private String getExcludedRevpropNormalized() {
String s = fixEmptyAndTrim(getExcludedRevprop());
if (s!=null) return s;
return getDescriptor().getGlobalExcludedRevprop();
}
@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(new EnvVars(env), build);
try {
Map<String,Long> revisions = parseSvnRevisionFile(build);
Set<String> knownURLs = revisions.keySet();
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 if (!knownURLs.isEmpty()) {
LOGGER.log(WARNING, "no revision found corresponding to {0}; known: {1}", new Object[] {url, knownURLs});
}
}
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 if (!knownURLs.isEmpty()) {
LOGGER.log(WARNING, "no revision found corresponding to {0}; known: {1}", new Object[] {url, knownURLs});
}
}
} catch (IOException e) {
LOGGER.log(WARNING, "error building environment variables", e);
}
}
/**
* Called after checkout/update has finished to compute the changelog.
*/
private void calcChangeLog(Run<?,?> build, FilePath workspace, File changelogFile, SCMRevisionState baseline, TaskListener listener, List<External> externals, EnvVars env) throws IOException, InterruptedException {
if (baseline == null) {
// nothing to compare against
createEmptyChangeLog(changelogFile, listener, "log");
return;
}
// 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, workspace, (SVNRevisionState) baseline, env, listener, this).run(externals, new StreamResult(os));
} finally {
os.close();
}
if(!created)
createEmptyChangeLog(changelogFile, listener, "log");
}
/**
* Please consider using the non-static version {@link #parseSvnRevisionFile(Run)}!
*/
/*package*/ static Map<String,Long> parseRevisionFile(Run<?,?> build) throws IOException {
return parseRevisionFile(build,true,false);
}
/*package*/ Map<String,Long> parseSvnRevisionFile(Run<?,?> 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(Run<?,?> build, boolean findClosest, boolean prunePinnedExternals) throws IOException {
Map<String,Long> revisions = new HashMap<String,Long>(); // module -> revision
if (findClosest) {
for (Run<?,?> 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;
}
/**
* Polling can happen on the master and does not require a workspace.
*/
@Override
public boolean requiresWorkspaceForPolling() {
return false;
}
@SuppressWarnings("unchecked")
@Override public void checkout(Run build, Launcher launcher, FilePath workspace, final TaskListener listener, File changelogFile, SCMRevisionState baseline) throws IOException, InterruptedException {
EnvVars env = build.getEnvironment(listener);
if (build instanceof AbstractBuild) {
EnvVarsUtils.overrideAll(env, ((AbstractBuild) build).getBuildVariables());
}
List<External> externals = null;
externals = checkout(build,workspace,listener,env);
// 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
SvnExternalsFileManager.writeExternalsFile(build.getParent(), externals);
Map<Job, List<External>> projectExternalsCache = getProjectExternalsCache();
synchronized (projectExternalsCache) {
projectExternalsCache.put(build.getParent(), externals);
}
if (changelogFile != null) {
calcChangeLog(build, workspace, changelogFile, baseline, 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.lib.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(Run build, FilePath workspace, TaskListener listener, EnvVars env) throws IOException, InterruptedException {
if (repositoryLocationsNoLongerExist(build, listener, env)) {
Run lsb = build.getParent().getLastSuccessfulBuild();
if (build instanceof AbstractBuild && 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.getParent().getName() + ", project will be disabled.");
disableProject(((AbstractBuild) build).getProject(), listener);
return null;
}
}
List<External> externals = new ArrayList<External>();
Set<String> unauthenticatedRealms = new LinkedHashSet<String>();
for (ModuleLocation location : getLocations(env, build)) {
CheckOutTask checkOutTask =
new CheckOutTask(build, this, location, build.getTimestamp().getTime(), listener, env);
externals.addAll(workspace.act(checkOutTask));
unauthenticatedRealms.addAll(checkOutTask.getUnauthenticatedRealms());
// 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 ) );
}*/
}
if (additionalCredentials != null) {
for (AdditionalCredentials c : additionalCredentials) {
unauthenticatedRealms.remove(c.getRealm());
}
}
if (!unauthenticatedRealms.isEmpty()) {
listener.getLogger().println("WARNING: The following realms could not be authenticated:");
for (String realm : unauthenticatedRealms) {
listener.getLogger().println(" * " + realm);
}
if (build == build.getParent().getLastBuild()) {
if (additionalCredentials == null) {
additionalCredentials = new ArrayList<AdditionalCredentials>();
}
for (String realm : unauthenticatedRealms) {
additionalCredentials.add(new AdditionalCredentials(realm, null));
}
try {
listener.getLogger().println("Adding missing realms to configuration...");
build.getParent().save();
listener.getLogger().println("Updated project configuration saved.");
} catch (IOException e) {
listener.getLogger().println("Could not update project configuration: " + e.getMessage());
}
}
}
return externals;
}
private synchronized Map<Job, List<External>> getProjectExternalsCache() {
if (projectExternalsCache == null) {
projectExternalsCache = new WeakHashMap<Job, 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(Run<?, ?> build, SubversionSCM parent, ModuleLocation location, Date timestamp, TaskListener listener, EnvVars env) {
this.authProvider = parent.createAuthenticationProvider(build.getParent(), location);
this.timestamp = timestamp;
this.listener = listener;
this.location = location;
this.revisions = build.getAction(RevisionParameterAction.class);
this.task = parent.getWorkspaceUpdater().createTask();
}
public Set<String> getUnauthenticatedRealms() {
if (authProvider instanceof CredentialsSVNAuthenticationProviderImpl) {
return ((CredentialsSVNAuthenticationProviderImpl) authProvider).getUnauthenticatedRealms();
}
return Collections.emptySet();
}
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 #createAuthenticationProvider(Job,ModuleLocation)}.
* 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 = descriptor();
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 = new SVNAuthenticationManager(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(descriptor().createAuthenticationProvider(context)));
}
/**
* Creates {@link ISVNAuthenticationProvider}.
* 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(Job<?, ?> inContextOf,
ModuleLocation location) {
return CredentialsSVNAuthenticationProviderImpl.createAuthenticationProvider(inContextOf, this, location);
}
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;
}
/**
* 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 defaultAuthProvider;
private final Map<String,ISVNAuthenticationProvider> authProviders;
private final TaskListener listener;
private final List<External> externals;
private final ModuleLocation[] locations;
public BuildRevisionMapTask(Run<?, ?> build, SubversionSCM parent, TaskListener listener, List<External> externals, EnvVars env) {
this.listener = listener;
this.externals = externals;
this.locations = parent.getLocations(env, build);
this.defaultAuthProvider = parent.createAuthenticationProvider(build.getParent(), null);
this.authProviders = new LinkedHashMap<String, ISVNAuthenticationProvider>();
for (ModuleLocation loc: locations) {
authProviders.put(loc.remote, parent.createAuthenticationProvider(build.getParent(), loc));
}
}
public List<SvnInfoP> invoke(File ws, VirtualChannel channel) throws IOException {
List<SvnInfoP> revisions = new ArrayList<SvnInfoP>();
for (ModuleLocation module : locations) {
ISVNAuthenticationProvider authProvider = authProviders.get(module.remote);
if (authProvider == null) {
authProvider = defaultAuthProvider;
}
final SvnClientManager manager = createClientManager(authProvider);
try {
final SVNWCClient svnWc = manager.getWCClient();
// invoke the "svn info"
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));
}
} finally {
manager.dispose();
}
}
final SvnClientManager manager = createClientManager(defaultAuthProvider);
try {
final SVNWCClient svnWc = manager.getWCClient();
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(Run build) {
return new File(build.getRootDir(),"revision.txt");
}
@Override
public SCMRevisionState calcRevisionsFromBuild(Run<?, ?> build, FilePath workspace, 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
public PollingResult compareRemoteRevisionWith(Job<?,?> project, Launcher launcher, FilePath workspace, final TaskListener listener, SCMRevisionState _baseline) throws IOException, InterruptedException {
final SVNRevisionState baseline;
if (_baseline instanceof SVNRevisionState) {
baseline = (SVNRevisionState)_baseline;
}
else if (project.getLastBuild()!=null) {
baseline = (SVNRevisionState)calcRevisionsFromBuild(project.getLastBuild(), launcher != null ? workspace : null, launcher, listener);
}
else {
baseline = new SVNRevisionState(null);
}
if (project.getLastBuild() == null) {
listener.getLogger().println(Messages.SubversionSCM_pollChanges_noBuilds());
return BUILD_NOW;
}
Run<?,?> lastCompletedBuild = project.getLastCompletedBuild();
if (lastCompletedBuild!=null) {
EnvVars env = lastCompletedBuild.getEnvironment(listener);
if (lastCompletedBuild instanceof AbstractBuild) {
EnvVarsUtils.overrideAll(env, ((AbstractBuild) lastCompletedBuild).getBuildVariables());
}
if (project instanceof AbstractProject && repositoryLocationsNoLongerExist(lastCompletedBuild, listener, env)) {
// Disable this project, see HUDSON-763
listener.getLogger().println(
Messages.SubversionSCM_pollChanges_locationsNoLongerExist(project));
disableProject((AbstractProject) project, listener);
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;
if (workspace != null && !isPollFromMaster()) {
ch = workspace.getChannel();
}
if (ch==null) ch= MasterComputer.localChannel;
final String nodeName = ch instanceof Channel ? ((Channel) ch).getName() : "master";
final SVNLogHandler logHandler = new SVNLogHandler(createSVNLogFilter(), listener);
final Map<String,ISVNAuthenticationProvider> authProviders = new LinkedHashMap<String,
ISVNAuthenticationProvider>();
for (ModuleLocation loc: getLocations()) {
String url;
try {
url = loc.getExpandedLocation(project).getSVNURL().toDecodedString();
} catch (SVNException ex) {
ex.printStackTrace(listener.error(Messages.SubversionSCM_pollChanges_exception(loc.getURL())));
return BUILD_NOW;
}
authProviders.put(url, createAuthenticationProvider(project, loc));
}
final ISVNAuthenticationProvider defaultAuthProvider = createAuthenticationProvider(project, null);
// figure out the remote revisions
return ch.call(new CompareAgainstBaselineCallable(baseline, logHandler, project.getName(), listener, defaultAuthProvider, authProviders, nodeName));
}
public SVNLogFilter createSVNLogFilter() {
return new DefaultSVNLogFilter(getExcludedRegionsPatterns(), getIncludedRegionsPatterns(),
getExcludedUsersNormalized(), getExcludedRevpropNormalized(), 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
}
@hudson.init.Initializer(after = InitMilestone.JOB_LOADED, before = InitMilestone.COMPLETED)
public static void perJobCredentialsMigration() {
DescriptorImpl descriptor = descriptor();
if (descriptor != null) {
descriptor.migratePerJobCredentials();
}
}
@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 transient Map<String,Credential> credentials;
private boolean mayHaveLegacyPerJobCredentials;
/**
* Stores name of Subversion revision property to globally exclude
*/
private String globalExcludedRevprop = null;
private int workspaceFormat = SVNAdminAreaFactory.WC_FORMAT_14;
/**
* 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;
@Override
public void load() {
super.load();
if (credentials != null && !credentials.isEmpty()) {
SecurityContext oldContext = ACL.impersonate(ACL.SYSTEM);
try {
BulkChange bc = new BulkChange(this);
try {
mayHaveLegacyPerJobCredentials = true;
for (Map.Entry<String, Credential> e : credentials.entrySet()) {
migrateCredentials(Jenkins.getInstance(), e.getKey(), e.getValue());
}
save();
bc.commit();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not migrate stored credentials", e);
} finally {
bc.abort();
}
} finally {
SecurityContextHolder.setContext(oldContext);
}
}
}
/*package*/ void migratePerJobCredentials() {
if (credentials == null && !mayHaveLegacyPerJobCredentials ) {
// nothing to do here
return;
}
boolean allOk = true;
for (AbstractProject<?,?> job: Jenkins.getInstance().getAllItems(AbstractProject.class)) {
File jobCredentials = new File(job.getRootDir(), "subversion.credentials");
if (jobCredentials.isFile()) {
try {
new PerJobCredentialStore(job).migrateCredentials(this);
if (!jobCredentials.delete()) {
LOGGER.log(Level.WARNING, "Could not remove legacy per-job credentials store file: {0}",
jobCredentials);
allOk = false;
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not migrate per-job credentials for " + job.getFullName(), e);
allOk = false;
}
}
}
mayHaveLegacyPerJobCredentials = !allOk;
save();
}
/*package*/ StandardCredentials migrateCredentials(ModelObject context, String legacyRealm, Credential legacyCredential)
throws IOException {
CredentialsStore store = CredentialsProvider.lookupStores(context).iterator().next();
StandardCredentials credential = legacyCredential.toCredentials(null, legacyRealm);
if (credential != null) {
return credential;
}
credential = legacyCredential.toCredentials(legacyRealm);
if (store.isDomainsModifiable()) {
Matcher matcher = Pattern.compile("\\s*<([^>]+)>.*").matcher(legacyRealm);
if (matcher.matches()) {
String url = matcher.group(1);
if (url.startsWith("http:") || url.startsWith("svn:") || url.startsWith("https:") || url
.startsWith("svn+ssh:")) {
// this is a reasonably valid URL
List<DomainRequirement> requirements = URIRequirementBuilder.fromUri(url).build();
HostnameRequirement hostnameRequirement = null;
SchemeRequirement schemeRequirement = null;
for (DomainRequirement r : requirements) {
if (hostnameRequirement == null && r instanceof HostnameRequirement) {
hostnameRequirement = (HostnameRequirement) r;
}
if (schemeRequirement == null && r instanceof SchemeRequirement) {
schemeRequirement = (SchemeRequirement) r;
}
if (schemeRequirement != null && hostnameRequirement != null) {
break;
}
}
Domain domain = null;
if (hostnameRequirement != null) {
for (Domain d : store.getDomains()) {
HostnameSpecification spec = null;
for (DomainSpecification s : d.getSpecifications()) {
if (s instanceof HostnameSpecification) {
spec = (HostnameSpecification) s;
break;
}
}
if (spec != null && spec.test(hostnameRequirement).isMatch() && d.test(requirements)) {
domain = d;
break;
}
}
}
if (domain == null) {
if (hostnameRequirement != null) {
List<DomainSpecification> specs = new ArrayList<DomainSpecification>();
specs.add(
new HostnameSpecification(hostnameRequirement.getHostname(), null));
if (schemeRequirement != null) {
specs.add(new SchemeSpecification(schemeRequirement.getScheme()));
}
domain = new Domain(hostnameRequirement.getHostname(), null, specs);
if (store.addDomain(domain, credential)) {
return credential;
}
}
} else {
if (store.addCredentials(domain, credential)) {
return credential;
}
}
}
}
}
store.addCredentials(Domain.global(), credential);
return credential;
}
/**
* 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 AuthenticationManager},
* indicating what subtype of {@link SVNAuthentication} is expected.
*/
public abstract SVNAuthentication createSVNAuthentication(String kind) throws SVNException;
public abstract StandardCredentials toCredentials(String description) throws IOException;
public abstract StandardCredentials toCredentials(ModelObject context, String description) throws IOException;
protected ItemGroup findItemGroup(ModelObject context) {
if (context instanceof ItemGroup) return (ItemGroup) context;
if (context instanceof Item) return ((Item) context).getParent();
return Jenkins.getInstance();
}
}
/**
* Username/password based authentication.
*/
public static final class PasswordCredential extends Credential {
/**
*
*/
private static final long serialVersionUID = -1676145651108866745L;
private final String userName;
private final Secret password; // for historical reasons, scrambled by base64 in addition to using 'Secret'
public PasswordCredential(String userName, String password) {
this.userName = userName;
this.password = Secret.fromString(Scrambler.scramble(password));
}
@Override
public SVNAuthentication createSVNAuthentication(String kind) {
if(kind.equals(ISVNAuthenticationManager.SSH))
return new SVNSSHAuthentication(userName, getPassword(),-1,false);
else
return new SVNPasswordAuthentication(userName, getPassword(),false);
}
@Override
public StandardCredentials toCredentials(String description) {
return new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null, description, userName,
getPassword());
}
@Override
public StandardCredentials toCredentials(ModelObject context, String description) throws IOException {
for (StandardUsernamePasswordCredentials c : CredentialsProvider.lookupCredentials(
StandardUsernamePasswordCredentials.class,
findItemGroup(context),
ACL.SYSTEM,
Collections.<DomainRequirement>emptyList())) {
if (userName.equals(c.getUsername())
&& getPassword().equals(c.getPassword().getPlainText())) {
return c;
}
}
return null;
}
private String getPassword() {
return Scrambler.descramble(Secret.toString(password));
}
}
/**
* 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 Secret passphrase; // for historical reasons, scrambled by base64 in addition to using 'Secret'
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 = Secret.fromString(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") ,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(Secret.toString(passphrase)),-1,false);
} catch (IOException e) {
throw new SVNException(
SVNErrorMessage.create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE,
"Unable to load private key"), e);
} catch (InterruptedException e) {
throw new SVNException(
SVNErrorMessage.create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE,
"Unable to load private key"), e);
}
} else
return null; // unknown
}
@Override
public StandardCredentials toCredentials(String description) throws IOException {
try {
return new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, null, userName,
new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(
FileUtils.readFileToString(getKeyFile(), "iso-8859-1")
),
Scrambler.descramble(Secret.toString(passphrase)), description);
} catch (UnsupportedCharsetException e) {
throw new IllegalStateException(
"Java Language Specification lists ISO-8859-1 as a required standard charset",
e
);
}
}
@Override
public StandardCredentials toCredentials(ModelObject context, String description) throws IOException {
String key = FileUtils.readFileToString(getKeyFile(), "iso-8859-1");
for (SSHUserPrivateKey c : CredentialsProvider.lookupCredentials(
SSHUserPrivateKey.class,
findItemGroup(context),
ACL.SYSTEM,
Collections.<DomainRequirement>emptyList())) {
if (userName.equals(c.getUsername()) && c.getPrivateKeys().contains(key)) {
return c;
}
}
return null;
}
}
/**
* SSL client certificate based authentication.
*/
public static final class SslClientCertificateCredential extends Credential {
/**
*
*/
private static final long serialVersionUID = 5455755079546887446L;
private final Secret certificate;
private final Secret password; // for historical reasons, scrambled by base64 in addition to using 'Secret'
public SslClientCertificateCredential(File certificate, String password) throws IOException {
this.password = Secret.fromString(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(
String.valueOf(Base64.decode(certificate.getPlainText().toCharArray())),
Scrambler.descramble(Secret.toString(password)), false, null, 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
}
@Override
public StandardCertificateCredentials toCredentials(String description) {
return new CertificateCredentialsImpl(CredentialsScope.GLOBAL, null, description,
Scrambler.descramble(Secret.toString(password)),
new CertificateCredentialsImpl.UploadedKeyStoreSource(certificate.getEncryptedValue()));
}
@Override
public StandardCredentials toCredentials(ModelObject context, String description) throws IOException {
StandardCertificateCredentials result = toCredentials(description);
for (StandardCertificateCredentials c : CredentialsProvider.lookupCredentials(
StandardCertificateCredentials.class,
findItemGroup(context),
ACL.SYSTEM,
Collections.<DomainRequirement>emptyList())) {
if (c.getPassword().equals(result.getPassword())) {
// now for the more complex Keystore comparison
KeyStore s1 = c.getKeyStore();
KeyStore s2 = result.getKeyStore();
try {
// if the aliases differ we know it's not a match, this is a faster test than serial form
Set<String> a1 = new HashSet<String>(Collections.list(s1.aliases()));
Set<String> a2 = new HashSet<String>(Collections.list(s2.aliases()));
if (!a1.equals(a2)) {
continue;
}
// this may give false misses but it will not give false hits
ByteArrayOutputStream bos1 = new ByteArrayOutputStream();
ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
s1.store(bos1, c.getPassword().getPlainText().toCharArray());
s2.store(bos2, c.getPassword().getPlainText().toCharArray());
if (Arrays.equals(bos1.toByteArray(), bos2.toByteArray())) {
return c;
}
} catch (KeyStoreException e) {
continue;
} catch (NoSuchAlgorithmException e) {
continue;
} catch (CertificateException e) {
continue;
}
}
}
return null;
}
}
/**
* 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);
}
}
@Override
public SCM newInstance(StaplerRequest staplerRequest, JSONObject jsonObject) throws FormException {
return super.newInstance(staplerRequest, jsonObject);
}
@Override public boolean isApplicable(Job project) {
return true;
}
public DescriptorImpl() {
super(SubversionRepositoryBrowser.class);
load();
}
@SuppressWarnings("unchecked")
protected DescriptorImpl(Class clazz, Class<? extends RepositoryBrowser> repositoryBrowser) {
super(clazz, repositoryBrowser);
}
public String getDisplayName() {
return "Subversion";
}
@Restricted(NoExternalUse.class)
void setGlobalExcludedRevprop(String revprop) {
globalExcludedRevprop = revprop;
}
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");
// 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)
* @deprecated as of 2.0
*/
@Deprecated
public ISVNAuthenticationProvider createAuthenticationProvider(AbstractProject<?,?> inContextOf) {
SubversionSCM scm = null;
if (inContextOf != null && inContextOf.getScm() instanceof SubversionSCM) {
scm = (SubversionSCM)inContextOf.getScm();
}
return CredentialsSVNAuthenticationProviderImpl.createAuthenticationProvider(inContextOf, scm, null);
}
/**
* @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.
*/
@Deprecated
public ISVNAuthenticationProvider createAuthenticationProvider() {
return CredentialsSVNAuthenticationProviderImpl.createAuthenticationProvider(null, null, null);
}
/**
* 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) lib.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, lib.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();
}
}
/**
* @deprecated retained for API compatibility only
*/
@Deprecated
public FormValidation doCheckRemote(StaplerRequest req, @AncestorInPath AbstractProject context, @QueryParameter String value, @QueryParameter String credentialsId) {
return Jenkins.getInstance().getDescriptorByType(ModuleLocation.DescriptorImpl.class).doCheckCredentialsId(
req, context, value, credentialsId);
}
/**
* @deprecated use {@link #checkRepositoryPath(hudson.model.Job, org.tmatesoft.svn.core.SVNURL, com.cloudbees.plugins.credentials.common.StandardCredentials)}
*/
@Deprecated
public SVNNodeKind checkRepositoryPath(AbstractProject context, SVNURL repoURL) throws SVNException {
return checkRepositoryPath(context, repoURL, null);
}
@Deprecated
public SVNNodeKind checkRepositoryPath(Job context, SVNURL repoURL, StandardCredentials credentials) throws SVNException {
return checkRepositoryPath((Item) context, repoURL, credentials);
}
public SVNNodeKind checkRepositoryPath(Item context, SVNURL repoURL, StandardCredentials credentials) throws SVNException {
SVNRepository repository = null;
try {
repository = getRepository(context,repoURL,credentials, Collections.<String, Credentials>emptyMap(), null);
repository.testConnection();
long rev = repository.getLatestRevision();
String repoPath = getRelativePath(repoURL, repository);
return repository.checkPath(repoPath, rev);
} catch (SVNException e) {
if (LOGGER.isLoggable(Level.FINE)) {
LogRecord lr = new LogRecord(Level.FINE,
"Could not check repository path {0} using credentials {1} ({2})");
lr.setThrown(e);
lr.setParameters(new Object[]{
repoURL,
credentials == null ? null : CredentialsNameProvider.name(credentials),
credentials
});
LOGGER.log(lr);
}
throw e;
} finally {
if (repository != null)
repository.closeSession();
}
}
/**
* @deprecated Use {@link #getRepository(hudson.model.Job, org.tmatesoft.svn.core.SVNURL, com.cloudbees.plugins.credentials.common.StandardCredentials, java.util.Map, org.tmatesoft.svn.core.io.ISVNSession)}
*/
@Deprecated
protected SVNRepository getRepository(AbstractProject context, SVNURL repoURL) throws SVNException {
return getRepository(context, repoURL, null, Collections.<String, Credentials>emptyMap(), null);
}
/**
* @deprecated Use {@link #getRepository(hudson.model.Job, org.tmatesoft.svn.core.SVNURL, com.cloudbees.plugins.credentials.common.StandardCredentials, java.util.Map, org.tmatesoft.svn.core.io.ISVNSession)}
*/
@Deprecated
protected SVNRepository getRepository(AbstractProject context, SVNURL repoURL, ISVNSession session) throws SVNException {
return getRepository(context, repoURL, null, Collections.<String, Credentials>emptyMap(), null);
}
/**
* @deprecated Use {@link #getRepository(hudson.model.Job, org.tmatesoft.svn.core.SVNURL, com.cloudbees.plugins.credentials.common.StandardCredentials, java.util.Map, org.tmatesoft.svn.core.io.ISVNSession)}
*/
@Deprecated
protected SVNRepository getRepository(AbstractProject context, SVNURL repoURL, StandardCredentials credentials,
Map<String, Credentials> additionalCredentials) throws SVNException {
return getRepository(context, repoURL, credentials, additionalCredentials, null);
}
@Deprecated
protected SVNRepository getRepository(Job context, SVNURL repoURL, StandardCredentials credentials,
Map<String, Credentials> additionalCredentials, ISVNSession session) throws SVNException {
return getRepository((Item) context, repoURL, credentials, additionalCredentials, session);
}
protected SVNRepository getRepository(Item context, SVNURL repoURL, StandardCredentials credentials,
Map<String, Credentials> additionalCredentials, ISVNSession session) throws SVNException {
SVNRepository repository = SVNRepositoryFactory.create(repoURL, session);
ISVNAuthenticationManager sam = createSvnAuthenticationManager(
new CredentialsSVNAuthenticationProviderImpl(credentials, additionalCredentials)
);
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;
}
/**
* 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\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 Item context,
@QueryParameter String value,
@QueryParameter String credentialsId,
@QueryParameter String excludedRevprop) throws IOException, ServletException {
String v = Util.fixNull(value).trim();
if (v.length() == 0)
return FormValidation.ok();
String revprop = Util.fixNull(excludedRevprop).trim();
if (revprop.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(new EnvVars(EnvVars.masterEnvVars).expand(v));
StandardCredentials credentials = lookupCredentials(context, credentialsId, repoURL);
SVNNodeKind node = null;
try {
node = checkRepositoryPath(context,repoURL, credentials);
} catch (SVNCancelException ce) {
if (isAuthenticationFailedError(ce)) {
// don't care about this here, another field's validation will show this
return FormValidation.ok();
}
throw ce;
}
if (node!=SVNNodeKind.NONE)
// something exists
return FormValidation.ok();
SVNRepository repository = null;
try {
repository = getRepository(context,repoURL, credentials, Collections.<String, Credentials>emptyMap(), null);
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();
}
}
// copied from WorkspaceUpdater:
private static boolean isAuthenticationFailedError(SVNCancelException e) {
// this is very ugly. SVNKit (1.7.4 at least) reports missing authentication data as a cancel exception
// "No credential to try. Authentication failed"
// See DefaultSVNAuthenticationManager#getFirstAuthentication
if (String.valueOf(e.getMessage()).contains("No credential to try")) {
return true;
}
Throwable cause = e.getCause();
if (cause instanceof SVNCancelException) {
return isAuthenticationFailedError((SVNCancelException) cause);
} else {
return false;
}
}
private static DescriptorImpl descriptor() {
return Jenkins.getInstance() == null ? null : Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class);
}
/**
* @deprecated 1.34
*/
public boolean repositoryLocationsNoLongerExist(AbstractBuild<?,?> build, TaskListener listener) {
return repositoryLocationsNoLongerExist(build, listener, null);
}
/**
* @since 1.34
*/
public boolean repositoryLocationsNoLongerExist(Run<?,?> build, TaskListener listener, EnvVars env) {
PrintStream out = listener.getLogger();
for (ModuleLocation l : getLocations(env, build))
try {
if (getDescriptor().checkRepositoryPath(build.getParent(),
l.getSVNURL(),
lookupCredentials(build.getParent(), l.credentialsId, 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;
}
/**
* Disables the project if it is possible and prints messages to the log.
* @param project Project to be disabled
* @param listener Logger
* @throws IOException Cannot disable the project
*/
private void disableProject(@NonNull AbstractProject project, @NonNull TaskListener listener)
throws IOException {
if (project.supportsMakeDisabled()) {
project.makeDisabled(true);
listener.getLogger().println(Messages.SubversionSCM_disableProject_disabled());
} else {
listener.getLogger().println(Messages.SubversionSCM_disableProject_unsupported());
}
}
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("lib.svnkit.ssh2.persistent")==null)
System.setProperty("lib.svnkit.ssh2.persistent","false");
// push Negotiate to the end because it requires a valid Kerberos configuration.
// see HUDSON-8153
if(System.getProperty("lib.svnkit.http.methods")==null)
System.setProperty("lib.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 extends AbstractDescribableImpl<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;
/**
* The credentials to checkout with.
*/
public final String credentialsId;
/**
* 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.
*/
@Deprecated
public ModuleLocation(String remote, String local) {
this(remote, null, local, null, false);
}
/**
* Constructor to support backwards compatibility.
*/
@Deprecated
public ModuleLocation(String remote, String local, String depthOption, boolean ignoreExternalsOption) {
this(remote,null,local,depthOption,ignoreExternalsOption);
}
@DataBoundConstructor
public ModuleLocation(String remote, String credentialsId, String local, String depthOption, boolean ignoreExternalsOption) {
this.remote = Util.removeTrailingSlash(Util.fixNull(remote).trim());
this.credentialsId = credentialsId;
this.local = fixEmptyAndTrim(local);
this.depthOption = StringUtils.isEmpty(depthOption) ? SVNDepth.INFINITY.getName() : depthOption;
this.ignoreExternalsOption = ignoreExternalsOption;
}
public ModuleLocation withRemote(String remote) {
return new ModuleLocation(remote, credentialsId, local, depthOption, ignoreExternalsOption);
}
public ModuleLocation withCredentialsId(String credentialsId) {
return new ModuleLocation(remote, credentialsId, local, depthOption, ignoreExternalsOption);
}
public ModuleLocation withLocal(String local) {
return new ModuleLocation(remote, credentialsId, local, depthOption, ignoreExternalsOption);
}
public ModuleLocation withDepthOption(String depthOption) {
return new ModuleLocation(remote, credentialsId, local, depthOption, ignoreExternalsOption);
}
public ModuleLocation withIgnoreExternalsOption(boolean ignoreExternalsOption) {
return new ModuleLocation(remote, credentialsId, local, depthOption, 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(Job context, SCM scm) throws SVNException {
if(repositoryUUID==null || repositoryRoot==null) {
LOGGER.fine("UUID of " + remote + " not cached for " + context);
synchronized (this) {
// don't keep connections open for further use to prevent having too many open at the same time.
SVNRepository r = openRepository(context, scm, false);
if (r.getRepositoryUUID(false) == null)
r.testConnection(); // make sure values are fetched
repositoryUUID = UUID.fromString(r.getRepositoryUUID(false));
repositoryRoot = r.getRepositoryRoot(false);
}
}
return repositoryUUID;
}
@Deprecated
public UUID getUUID(AbstractProject context) throws SVNException {
return getUUID(context, context.getScm());
}
@Deprecated
public SVNRepository openRepository(AbstractProject context) throws SVNException {
return openRepository(context, true);
}
@Deprecated
public SVNRepository openRepository(AbstractProject context, boolean keepConnection) throws SVNException {
return openRepository(context, context.getScm(), true);
}
public SVNRepository openRepository(Job context, SCM scm, boolean keepConnection) throws SVNException {
SVNURL repoURL = getSVNURL();
StandardCredentials creds = lookupCredentials(context, credentialsId, repoURL);
Map<String, Credentials> additional = new HashMap<String, Credentials>();
if (creds == null) {
// we should add additional credentials, this looks like it's going to be an external
// TODO only necessary with externals, or can we always do this?
List<AdditionalCredentials> additionalCredentialsList = ((SubversionSCM) scm).getAdditionalCredentials();
for (AdditionalCredentials c : additionalCredentialsList) {
if (c.getCredentialsId() != null) {
StandardCredentials cred = CredentialsMatchers
.firstOrNull(CredentialsProvider.lookupCredentials(StandardCredentials.class, context,
ACL.SYSTEM, Collections.<DomainRequirement>emptyList()),
CredentialsMatchers.allOf(CredentialsMatchers.withId(c.getCredentialsId()),
CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(
StandardCredentials.class), CredentialsMatchers.instanceOf(
SSHUserPrivateKey.class))));
if (cred != null) {
additional.put(c.getRealm(), cred);
}
}
}
}
if (keepConnection) {
return descriptor().getRepository(context, repoURL, creds, additional, null);
}
return descriptor().getRepository(context, repoURL, creds, additional, new ISVNSession() {
public boolean keepConnection(SVNRepository repository) {
return false;
}
public void saveCommitMessage(SVNRepository repository, long revision, String message) {
}
public String getCommitMessage(SVNRepository repository, long revision) {
return null;
}
public boolean hasCommitMessage(SVNRepository repository, long revision) {
return false;
}
});
}
@Deprecated
public SVNURL getRepositoryRoot(AbstractProject context) throws SVNException {
return getRepositoryRoot(context, context.getScm());
}
public SVNURL getRepositoryRoot(Job context, SCM scm) throws SVNException {
getUUID(context, scm);
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) {
EnvVars env = new EnvVars(EnvVars.masterEnvVars);
env.putAll(build.getBuildVariables());
return getExpandedLocation(env);
}
/**
* 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), credentialsId, env.expand(getLocalDir()), getDepthOption(),
isIgnoreExternalsOption());
}
@Override
public String toString() {
return remote;
}
private static final long serialVersionUID = 1L;
@Deprecated
public static List<ModuleLocation> parse(String[] remoteLocations, String[] localLocations, String[] depthOptions, boolean[] isIgnoreExternals) {
return parse(remoteLocations, null, localLocations, depthOptions, isIgnoreExternals);
}
public static List<ModuleLocation> parse(String[] remoteLocations, String[] credentialIds,
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,
credentialIds != null && credentialIds.length > i ? credentialIds[i] : null,
Util.nullify(localLocations[i]),
depthOptions != null ? depthOptions[i] : null,
isIgnoreExternals != null && isIgnoreExternals[i]));
}
}
}
return modules;
}
/**
* If a subversion remote uses $VAR or ${VAR} as a parameterized build,
* we expand the url. This will expand using the DEFAULT item. If there
* is a choice parameter, it will expand with the FIRST item.
*/
public ModuleLocation getExpandedLocation(Job<?, ?> project) {
String url = this.getURL();
String returnURL = url;
for (JobProperty property : project.getProperties().values()) {
if (property instanceof ParametersDefinitionProperty) {
ParametersDefinitionProperty pdp = (ParametersDefinitionProperty) property;
for (String propertyName : pdp.getParameterDefinitionNames()) {
if (url.contains(propertyName)) {
ParameterDefinition pd = pdp.getParameterDefinition(propertyName);
String replacement = String.valueOf(pd.getDefaultParameterValue().createVariableResolver(null).resolve(propertyName));
returnURL = returnURL.replace("${" + propertyName + "}", replacement);
returnURL = returnURL.replace("$" + propertyName, replacement);
}
}
}
}
return new ModuleLocation(returnURL, credentialsId, getLocalDir(), getDepthOption(),
isIgnoreExternalsOption());
}
@Extension
public static class DescriptorImpl extends Descriptor<ModuleLocation> {
@Override
public String getDisplayName() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item context,
@QueryParameter String remote) {
if (context == null || !context.hasPermission(Item.CONFIGURE)) {
return new StandardListBoxModel();
}
return fillCredentialsIdItems(context, remote);
}
public ListBoxModel fillCredentialsIdItems(@Nonnull Item context, String remote) {
List<DomainRequirement> domainRequirements;
if (remote == null) {
domainRequirements = Collections.<DomainRequirement>emptyList();
} else {
domainRequirements = URIRequirementBuilder.fromUri(remote.trim()).build();
}
return new StandardListBoxModel()
.withEmptySelection()
.withMatching(
CredentialsMatchers.anyOf(
CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class),
CredentialsMatchers.instanceOf(StandardCertificateCredentials.class),
CredentialsMatchers.instanceOf(SSHUserPrivateKey.class)
),
CredentialsProvider.lookupCredentials(StandardCredentials.class,
context,
ACL.SYSTEM,
domainRequirements)
);
}
/**
* validate the value for a remote (repository) location.
*/
public FormValidation doCheckRemote(StaplerRequest req, @AncestorInPath Item context, @QueryParameter String remote) {
// syntax check first
String url = Util.fixEmptyAndTrim(remote);
if (url == null)
return FormValidation.error(Messages.SubversionSCM_doCheckRemote_required());
if(descriptor().isValidateRemoteUpToVar()) {
url = (url.indexOf('$') != -1) ? url.substring(0, url.indexOf('$')) : url;
} else {
url = new EnvVars(EnvVars.masterEnvVars).expand(url);
}
if(!URL_PATTERN.matcher(url).matches())
return FormValidation.errorWithMarkup(
Messages.SubversionSCM_doCheckRemote_invalidUrl());
return FormValidation.ok();
}
/**
* validate the value for a remote (repository) location.
*/
public FormValidation doCheckCredentialsId(StaplerRequest req, @AncestorInPath Item context, @QueryParameter String remote, @QueryParameter String value) {
// Test the connection only if we have job configure permission
if (context == null || !context.hasPermission(Item.CONFIGURE)) {
return FormValidation.ok();
}
return checkCredentialsId(req, context, remote, value);
}
/**
* validate the value for a remote (repository) location.
*/
public FormValidation checkCredentialsId(StaplerRequest req, @Nonnull Item context, String remote, String value) {
// if check remote is reporting an issue then we don't need to
String url = Util.fixEmptyAndTrim(remote);
if (url == null)
return FormValidation.ok();
if(descriptor().isValidateRemoteUpToVar()) {
url = (url.indexOf('$') != -1) ? url.substring(0, url.indexOf('$')) : url;
} else {
url = new EnvVars(EnvVars.masterEnvVars).expand(url);
}
if(!URL_PATTERN.matcher(url).matches())
return FormValidation.ok();
try {
String urlWithoutRevision = SvnHelper.getUrlWithoutRevision(url);
SVNURL repoURL = SVNURL.parseURIDecoded(urlWithoutRevision);
StandardCredentials credentials = lookupCredentials(context, value, repoURL);
if (descriptor().checkRepositoryPath(context,repoURL, credentials)!=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 = descriptor().getRepository(context,repoURL, credentials, Collections.<String, Credentials>emptyMap(), null);
long rev = repository.getLatestRevision();
// now go back the tree and find if there's anything that exists
String repoPath = descriptor().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>";
return FormValidation.errorWithMarkup(message);
}
}
/**
* 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();
}
}
}
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;
}
private static StandardCredentials lookupCredentials(Item context, String credentialsId, SVNURL repoURL) {
return credentialsId == null ? null :
CredentialsMatchers.firstOrNull(CredentialsProvider
.lookupCredentials(StandardCredentials.class, context, ACL.SYSTEM,
URIRequirementBuilder.fromUri(repoURL.toString()).build()),
CredentialsMatchers.withId(credentialsId));
}
public static class AdditionalCredentials extends AbstractDescribableImpl<AdditionalCredentials> {
@NonNull
private final String realm;
@CheckForNull
private final String credentialsId;
@DataBoundConstructor
public AdditionalCredentials(@NonNull String realm, @CheckForNull String credentialsId) {
realm.getClass(); // throw NPE if null
this.realm = realm;
this.credentialsId = credentialsId;
}
@NonNull
public String getRealm() {
return realm;
}
@CheckForNull
public String getCredentialsId() {
return credentialsId;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AdditionalCredentials)) {
return false;
}
AdditionalCredentials that = (AdditionalCredentials) o;
if (!realm.equals(that.realm)) {
return false;
}
if (credentialsId != null ? !credentialsId.equals(that.credentialsId) : that.credentialsId != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = realm.hashCode();
result = 31 * result + (credentialsId != null ? credentialsId.hashCode() : 0);
return result;
}
@Extension
public static class DescriptorImpl extends Descriptor<AdditionalCredentials> {
@Override
public String getDisplayName() {
return null;
}
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item context,
@QueryParameter String realm) {
if (context == null || !context.hasPermission(Item.CONFIGURE)) {
return new StandardListBoxModel();
}
List<DomainRequirement> domainRequirements;
if (realm == null) {
domainRequirements = Collections.<DomainRequirement>emptyList();
} else {
if (realm.startsWith("<") && realm.contains(">")) {
int index = realm.indexOf('>');
assert index > 1;
domainRequirements = URIRequirementBuilder.fromUri(realm.substring(1, index).trim()).build();
} else {
domainRequirements = Collections.<DomainRequirement>emptyList();
}
}
return new StandardListBoxModel()
.withEmptySelection()
.withMatching(
CredentialsMatchers.anyOf(
CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class),
CredentialsMatchers.instanceOf(StandardCertificateCredentials.class),
CredentialsMatchers.instanceOf(SSHUserPrivateKey.class)
),
CredentialsProvider.lookupCredentials(StandardCredentials.class,
context,
ACL.SYSTEM,
domainRequirements)
);
}
}
}
}