### Eclipse Workspace Patch 1.0 #P hudson-core Index: src/main/resources/hudson/scm/SubversionSCM/config.jelly =================================================================== RCS file: /cvs/hudson/hudson/main/core/src/main/resources/hudson/scm/SubversionSCM/config.jelly,v retrieving revision 1.4 diff -u -r1.4 config.jelly --- src/main/resources/hudson/scm/SubversionSCM/config.jelly 13 Feb 2007 17:13:50 -0000 1.4 +++ src/main/resources/hudson/scm/SubversionSCM/config.jelly 19 Mar 2007 09:06:31 -0000 @@ -1,17 +1,32 @@ - - - - - - - \ No newline at end of file + from <a href='${rootURL}/scm/SubversionSCM/enterCredential'>here</a>."> + + + + + + + + + +
+ +
+
+
+
+ + + + + Index: src/main/java/hudson/scm/SubversionSCM.java =================================================================== RCS file: /cvs/hudson/hudson/main/core/src/main/java/hudson/scm/SubversionSCM.java,v retrieving revision 1.29 diff -u -r1.29 SubversionSCM.java --- src/main/java/hudson/scm/SubversionSCM.java 22 Feb 2007 02:16:55 -0000 1.29 +++ src/main/java/hudson/scm/SubversionSCM.java 19 Mar 2007 09:06:31 -0000 @@ -1,10 +1,10 @@ package hudson.scm; +import static hudson.Util.fixNull; import hudson.FilePath; -import hudson.FilePath.FileCallable; import hudson.Launcher; import hudson.Util; -import static hudson.Util.fixNull; +import hudson.FilePath.FileCallable; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; @@ -15,6 +15,34 @@ import hudson.remoting.VirtualChannel; import hudson.util.FormFieldValidator; import hudson.util.Scrambler; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.Vector; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.ServletException; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; + import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.tmatesoft.svn.core.SVNErrorMessage; @@ -40,647 +68,977 @@ import org.tmatesoft.svn.core.wc.xml.SVNXMLLogHandler; import org.xml.sax.helpers.LocatorImpl; -import javax.servlet.ServletException; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.sax.SAXTransformerFactory; -import javax.xml.transform.sax.TransformerHandler; -import javax.xml.transform.stream.StreamResult; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.Serializable; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.StringTokenizer; -import java.util.logging.Logger; -import java.util.logging.Level; - /** * Subversion. - * + * * Check http://svn.collab.net/repos/svn/trunk/subversion/svn/schema/ for * various output formats. - * + * * @author Kohsuke Kawaguchi */ public class SubversionSCM extends SCM implements Serializable { - private final String modules; - private boolean useUpdate; - private String username; - - /** - * @deprecated - * No longer in use but left for serialization compatibility. - */ - private transient String otherOptions; - - SubversionSCM(String modules, boolean useUpdate, String username) { - StringBuilder normalizedModules = new StringBuilder(); - StringTokenizer tokens = new StringTokenizer(modules); - while(tokens.hasMoreTokens()) { - if(normalizedModules.length()>0) normalizedModules.append(' '); - String m = tokens.nextToken(); - if(m.endsWith("/")) - // the normalized name is always without the trailing '/' - m = m.substring(0,m.length()-1); - normalizedModules.append(m); - } - - this.modules = normalizedModules.toString(); - this.useUpdate = useUpdate; - this.username = nullify(username); - } - - /** - * Whitespace-separated list of SVN URLs that represent - * modules to be checked out. - */ - public String getModules() { - return modules; - } - - public boolean isUseUpdate() { - return useUpdate; - } - - public String getUsername() { - return username; - } - - private Collection getModuleDirNames() { - List dirs = new ArrayList(); - StringTokenizer tokens = new StringTokenizer(modules); - while(tokens.hasMoreTokens()) { - dirs.add(getLastPathComponent(tokens.nextToken())); - } - return dirs; - } - - private boolean calcChangeLog(AbstractBuild build, File changelogFile, BuildListener listener) throws IOException { - if(build.getPreviousBuild()==null) { - // nothing to compare against - return createEmptyChangeLog(changelogFile, listener, "log"); - } - - PrintStream logger = listener.getLogger(); - - Map previousRevisions = parseRevisionFile(build.getPreviousBuild()); - Map thisRevisions = parseRevisionFile(build); - - boolean changelogFileCreated = false; - - SVNLogClient svnlc = createSvnClientManager(getDescriptor().createAuthenticationProvider()).getLogClient(); - - TransformerHandler th = createTransformerHandler(); - th.setResult(new StreamResult(changelogFile)); - SVNXMLLogHandler logHandler = new SVNXMLLogHandler(th); - // work around for http://svnkit.com/tracker/view.php?id=175 - th.setDocumentLocator(DUMMY_LOCATOR); - logHandler.startDocument(); - - - StringTokenizer tokens = new StringTokenizer(modules); - while(tokens.hasMoreTokens()) { - String url = tokens.nextToken(); - Long prevRev = previousRevisions.get(url); - if(prevRev==null) { - logger.println("no revision recorded for "+url+" in the previous build"); - continue; - } - Long thisRev = thisRevisions.get(url); - if(thisRev.equals(prevRev)) { - logger.println("no change for "+url+" since the previous build"); - continue; - } - - try { - svnlc.doLog(SVNURL.parseURIEncoded(url),null, - SVNRevision.create(prevRev), SVNRevision.create(prevRev+1), - SVNRevision.create(thisRev), - false, true, Long.MAX_VALUE, logHandler); - } catch (SVNException e) { - e.printStackTrace(listener.error("revision check failed on "+url)); - } - changelogFileCreated = true; - } - - if(changelogFileCreated) { - logHandler.endDocument(); - } - - if(!changelogFileCreated) - createEmptyChangeLog(changelogFile, listener, "log"); - - return true; - } - - /** - * Creates an identity transformer. - */ - private static TransformerHandler createTransformerHandler() { - try { - return ((SAXTransformerFactory) SAXTransformerFactory.newInstance()).newTransformerHandler(); - } catch (TransformerConfigurationException e) { - throw new Error(e); // impossible - } - } - - /*package*/ static Map parseRevisionFile(AbstractBuild build) throws IOException { - Map revisions = new HashMap(); // module -> revision - {// read the revision file of the last build - File file = getRevisionFile(build); - if(!file.exists()) - // nothing to compare against - return revisions; - - BufferedReader br = new BufferedReader(new FileReader(file)); - String line; - while((line=br.readLine())!=null) { - int index = line.lastIndexOf('/'); - if(index<0) { - continue; // invalid line? - } - try { - revisions.put(line.substring(0,index), Long.parseLong(line.substring(index+1))); - } catch (NumberFormatException e) { - // perhaps a corrupted line. ignore - } - } - } - - return revisions; - } - - public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, final BuildListener listener, File changelogFile) throws IOException, InterruptedException { - if(!checkout(launcher,workspace, listener)) - return false; - - // write out the revision file - PrintWriter w = new PrintWriter(new FileOutputStream(getRevisionFile(build))); - try { - Map revMap = buildRevisionMap(workspace, listener); - for (Entry e : revMap.entrySet()) { - w.println( e.getKey() +'/'+ e.getValue().revision ); - } - } finally { - w.close(); - } - - return calcChangeLog(build, changelogFile, listener); - } - - public boolean checkout(Launcher launcher, FilePath workspace, final TaskListener listener) throws IOException, InterruptedException { - if(useUpdate && isUpdatable(workspace, listener)) { - return update(launcher,workspace,listener); - } else { - final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); - return workspace.act(new FileCallable() { - public Boolean invoke(File ws, VirtualChannel channel) throws IOException { - Util.deleteContentsRecursive(ws); - SVNUpdateClient svnuc = createSvnClientManager(authProvider).getUpdateClient(); - svnuc.setEventHandler(new SubversionUpdateEventHandler(listener)); - - StringTokenizer tokens = new StringTokenizer(modules); - while(tokens.hasMoreTokens()) { - try { - SVNURL url = SVNURL.parseURIEncoded(tokens.nextToken()); - listener.getLogger().println("Checking out "+url); - - svnuc.doCheckout(url, new File(ws, getLastPathComponent(url.getPath())), SVNRevision.HEAD, SVNRevision.HEAD, true ); - } catch (SVNException e) { - e.printStackTrace(listener.error("Error in subversion")); - return false; - } - } - - return true; - } - }); - } - } - - /** - * Creates {@link SVNClientManager}. - * - *

- * This method must be executed on the slave where svn operations are performed. - * - * @param authProvider - * The value obtained from {@link DescriptorImpl#createAuthenticationProvider()}. - * If the operation runs on slaves, - * (and properly remoted, if the svn operations run on slaves.) - */ - private static SVNClientManager createSvnClientManager(ISVNAuthenticationProvider authProvider) { - ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); - sam.setAuthenticationProvider(authProvider); - return SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(true),sam); - } - - public static final class SvnInfo implements Serializable { - /** - * Decoded repository URL. - */ - final String url; - 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); - } - - private static final long serialVersionUID = 1L; - } - - /** - * Gets the SVN metadata for the given local workspace. - * - * @param workspace - * The target to run "svn info". - */ - private SVNInfo parseSvnInfo(File workspace, ISVNAuthenticationProvider authProvider) throws SVNException { - SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); - return svnWc.doInfo(workspace,SVNRevision.WORKING); - } - - /** - * Gets the SVN metadata for the remote repository. - * - * @param remoteUrl - * The target to run "svn info". - */ - private SVNInfo parseSvnInfo(SVNURL remoteUrl, ISVNAuthenticationProvider authProvider) throws SVNException { - SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); - return svnWc.doInfo(remoteUrl, SVNRevision.HEAD, SVNRevision.HEAD); - } - - /** - * 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 Map buildRevisionMap(FilePath workspace, final TaskListener listener) throws IOException, InterruptedException { - final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); - return workspace.act(new FileCallable>() { - public Map invoke(File ws, VirtualChannel channel) throws IOException { - Map revisions = new HashMap(); - - SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); - // invoke the "svn info" - for( String module : getModuleDirNames() ) { - try { - SvnInfo info = new SvnInfo(svnWc.doInfo(new File(ws,module),SVNRevision.WORKING)); - revisions.put(info.url,info); - } catch (SVNException e) { - e.printStackTrace(listener.error("Failed to parse svn info for "+module)); - } - } - - return revisions; - } - }); - } - - /** - * Gets the file that stores the revision. - */ - private static File getRevisionFile(AbstractBuild build) { - return new File(build.getRootDir(),"revision.txt"); - } - - public boolean update(Launcher launcher, FilePath workspace, final TaskListener listener) throws IOException, InterruptedException { - final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); - return workspace.act(new FileCallable() { - public Boolean invoke(File ws, VirtualChannel channel) throws IOException { - SVNUpdateClient svnuc = createSvnClientManager(authProvider).getUpdateClient(); - svnuc.setEventHandler(new SubversionUpdateEventHandler(listener)); - - StringTokenizer tokens = new StringTokenizer(modules); - while(tokens.hasMoreTokens()) { - try { - String url = tokens.nextToken(); - listener.getLogger().println("Updating "+url); - svnuc.doUpdate(new File(ws, getLastPathComponent(url)), SVNRevision.HEAD, true ); - } catch (SVNException e) { - e.printStackTrace(listener.error("Error in subversion")); - return false; - } - } - return true; - } - }); - } - - /** - * Returns true if we can use "svn update" instead of "svn checkout" - */ - private boolean isUpdatable(FilePath workspace, final TaskListener listener) throws IOException, InterruptedException { - final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); - - return workspace.act(new FileCallable() { - public Boolean invoke(File ws, VirtualChannel channel) throws IOException { - StringTokenizer tokens = new StringTokenizer(modules); - while(tokens.hasMoreTokens()) { - String url = tokens.nextToken(); - String moduleName = getLastPathComponent(url); - File module = new File(ws,moduleName); - - if(!module.exists()) { - listener.getLogger().println("Checking out a fresh workspace because "+module+" doesn't exist"); - return false; - } - - try { - SvnInfo svnInfo = new SvnInfo(parseSvnInfo(module,authProvider)); - if(!svnInfo.url.equals(url)) { - listener.getLogger().println("Checking out a fresh workspace because the workspace is not "+url); - return false; - } - } catch (SVNException e) { - listener.getLogger().println("Checking out a fresh workspace because Hudson failed to detect the current workspace "+module); - e.printStackTrace(listener.error(e.getMessage())); - return false; - } - } - return true; - } - }); - } - - public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException { - // current workspace revision - Map wsRev = buildRevisionMap(workspace, listener); - - ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); - - // check the corresponding remote revision - for (SvnInfo localInfo : wsRev.values()) { - try { - SvnInfo remoteInfo = new SvnInfo(parseSvnInfo(localInfo.getSVNURL(),authProvider)); - listener.getLogger().println("Revision:"+remoteInfo.revision); - if(remoteInfo.revision > localInfo.revision) - return true; // change found - } catch (SVNException e) { - e.printStackTrace(listener.error("Failed to check repository revision for "+localInfo.url)); - } - } - - return false; // no change - } - - public ChangeLogParser createChangeLogParser() { - return new SubversionChangeLogParser(); - } - - - public DescriptorImpl getDescriptor() { - return DescriptorImpl.DESCRIPTOR; - } - - public void buildEnvVars(Map env) { - // no environment variable - } - - public FilePath getModuleRoot(FilePath workspace) { - String s; - - // if multiple URLs are specified, pick the first one - int idx = modules.indexOf(' '); - if(idx>=0) s = modules.substring(0,idx); - else s = modules; - - return workspace.child(getLastPathComponent(s)); - } - - private static String getLastPathComponent(String s) { - String[] tokens = s.split("/"); - return tokens[tokens.length-1]; // return the last token - } - - public static final class DescriptorImpl extends Descriptor { - public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); - - /** - * Path to svn.exe. Null to default. - * - * @deprecated - * No longer in use. - */ - private volatile String svnExe; - - /** - * SVN authentication realm to its associated credentials. - */ - private final Map credentials = new Hashtable(); - - /** - * Stores {@link SVNAuthentication} for a single realm. - */ - private static abstract class Credential implements Serializable { - abstract SVNAuthentication createSVNAuthentication(); - } - - private static final class PasswordCredential extends Credential { - private final String userName; - private final String password; // scrambled by base64 - - public PasswordCredential(String userName, String password) { - this.userName = userName; - this.password = Scrambler.scramble(password); - } - - @Override - SVNPasswordAuthentication createSVNAuthentication() { - return new SVNPasswordAuthentication(userName,Scrambler.descramble(password),false); - } - } - - /** - * Remoting interface that allows remote {@link ISVNAuthenticationProvider} - * to read from local {@link DescriptorImpl#credentials}. - */ - private interface RemotableSVNAuthenticationProvider { - Credential getCredential(String realm); - } - - private final class RemotableSVNAuthenticationProviderImpl implements RemotableSVNAuthenticationProvider, Serializable { - public Credential getCredential(String realm) { - return credentials.get(realm); - } - - /** - * When sent to the remote node, send a proxy. - */ - private Object writeReplace() { - return Channel.current().export(RemotableSVNAuthenticationProvider.class, this); - } - } - - /** - * See {@link DescriptorImpl#createAuthenticationProvider()}. - */ - private static final class SVNAuthenticationProviderImpl implements ISVNAuthenticationProvider, Serializable { - private final RemotableSVNAuthenticationProvider source; - - public SVNAuthenticationProviderImpl(RemotableSVNAuthenticationProvider source) { - this.source = source; - } - - public SVNAuthentication requestClientAuthentication(String kind, SVNURL url, String realm, SVNErrorMessage errorMessage, SVNAuthentication previousAuth, boolean authMayBeStored) { - Credential cred = source.getCredential(realm); - if(cred==null) return null; - return cred.createSVNAuthentication(); - } - - public int acceptServerAuthentication(SVNURL url, String realm, Object certificate, boolean resultMayBeStored) { - return ACCEPTED_TEMPORARY; - } - - private static final long serialVersionUID = 1L; - } - - private DescriptorImpl() { - super(SubversionSCM.class); - load(); - } - - public String getDisplayName() { - return "Subversion"; - } - - public SCM newInstance(StaplerRequest req) { - return new SubversionSCM( - req.getParameter("svn_modules"), - req.getParameter("svn_use_update")!=null, - req.getParameter("svn_username") - ); - } - - /** - * Creates {@link ISVNAuthenticationProvider} backed by {@link #credentials}. - * This method must be invoked on the master, but the returned object is remotable. - */ - public ISVNAuthenticationProvider createAuthenticationProvider() { - return new SVNAuthenticationProviderImpl(new RemotableSVNAuthenticationProviderImpl()); - } - - /** - * Used in the job configuration page to check if authentication for the SVN URLs - * are available. - */ - public void doAuthenticationCheck(final StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - new FormFieldValidator(req,rsp,true) { - protected void check() throws IOException, ServletException { - StringTokenizer tokens = new StringTokenizer(fixNull(request.getParameter("value"))); - String message=""; - - while(tokens.hasMoreTokens()) { - String url = tokens.nextToken(); - - try { - SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(url)); - - ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); - sam.setAuthenticationProvider(createAuthenticationProvider()); - repository.setAuthenticationManager(sam); - - repository.testConnection(); - } catch (SVNException e) { - StringWriter sw = new StringWriter(); - e.printStackTrace(new PrintWriter(sw)); - - message += "Unable to access "+url+" : "+Util.escape( e.getErrorMessage().getFullMessage()); - message += " (show details)"; - message += "

"; - message += " (Maybe you need to enter credential?)"; - message += "
"; - logger.log(Level.INFO, "Failed to access subversion repository "+url,e); - } - } - - if(message.length()==0) - ok(); - else - error(message); - } - }.process(); - } - - /** - * Submits the authentication info. - */ - public void doPostCredential(final StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - final String url = req.getParameter("url"); - final String username = req.getParameter("username"); - final String password = req.getParameter("password"); - - try { - // the way it works with SVNKit is that - // 1) svnkit calls AuthenticationManager asking for a credential. - // this is when we can see the 'realm', which identifies the user domain. - // 2) DefaultSVNAuthenticationManager returns the username and password we set below - // 3) if the authentication is successful, svnkit calls back acknowledgeAuthentication - // (so we store the password info here) - SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(url)); - repository.setAuthenticationManager(new DefaultSVNAuthenticationManager(SVNWCUtil.getDefaultConfigurationDirectory(),true,username,password) { - public void acknowledgeAuthentication(boolean accepted, String kind, String realm, SVNErrorMessage errorMessage, SVNAuthentication authentication) throws SVNException { - if(accepted) { - credentials.put(realm,new PasswordCredential(username,password)); - save(); - } - super.acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication); - } - }); - repository.testConnection(); - rsp.sendRedirect("credentialOK"); - } catch (SVNException e) { - req.setAttribute("message",e.getErrorMessage()); - rsp.forward(Hudson.getInstance(),"error",req); - } - } - - static { new Initializer(); } - } - - private static final long serialVersionUID = 1L; - - private static final Logger logger = Logger.getLogger(SubversionSCM.class.getName()); - - private static final LocatorImpl DUMMY_LOCATOR = new LocatorImpl(); - - static { - new Initializer(); - DUMMY_LOCATOR.setLineNumber(-1); - DUMMY_LOCATOR.setColumnNumber(-1); - } - - private static final class Initializer { - static { - DAVRepositoryFactory.setup(); // http, https - SVNRepositoryFactoryImpl.setup(); // svn, svn+xxx - FSRepositoryFactory.setup(); // file - } - } + @Deprecated + private String modules; + + /** + * the locations field is used to store all configured SVN locations (with + * their local and remote part). Direct access to this filed 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.64 + */ + private ModuleLocation[] locations = new ModuleLocation[0]; + + private boolean useUpdate; + + private String username; + + /** + * @deprecated No longer in use but left for serialization compatibility. + */ + private transient String otherOptions; + + SubversionSCM(String[] remoteLocations, String[] localLocations, + boolean useUpdate, String username, String otherOptions) { + + if (remoteLocations != null && localLocations != null) { + int entries = Math.min(remoteLocations.length, + localLocations.length); + + ModuleLocation[] l = new ModuleLocation[entries]; + + for (int i = 0; i < l.length; i++) { + + // the remote (repository) location + String remoteLoc = nullify(remoteLocations[i]); + // the local filesystem location (directory to checkout into) + String localLoc = nullify(localLocations[i]); + + if (remoteLoc != null) { + remoteLoc = remoteLoc.trim(); + + if (remoteLoc.charAt(remoteLoc.length() - 1) == '/') + remoteLoc.substring(0, remoteLoc.length() - 1); + + if (localLoc == null) + localLoc = getLastPathComponent(remoteLoc); + + String errorMessage = null; + if ((errorMessage = validateRemoteLocation(remoteLoc)) == null + && (errorMessage = validateLocalLocation(remoteLoc)) == null) + // the location is not configured properly + l[i] = new ModuleLocation(remoteLoc, localLoc, + errorMessage); + else + // everything fine + l[i] = new ModuleLocation(remoteLoc, localLoc); + } + } + locations = l; + } + + this.useUpdate = useUpdate; + this.username = nullify(username); + } + + /** + * this method is beeing kept for compatibility reasons with older hudson + * installations. All data is beeing stored in the {@link #locations} filed + * which is basically a list of {@link ModuleLocations} + */ + @Deprecated + public String getModules() { + return modules; + } + + /** + * this method is beeing kept for compatibility reasons with older hudson + * installations. All data is beeing stored in the {@link #locations} filed + * which is basically a list of {@link ModuleLocations} + */ + @Deprecated + public void setModules(String modules) { + this.modules = modules; + } + + /** + * list of all configured svn locations + * + * @since 1.64 + */ + public ModuleLocation[] getLocations() { + // check if we've got a old location + if (modules != null) { + // import the old configuration + Vector oldLocations = new Vector(); + StringTokenizer tokens = new StringTokenizer(modules); + while (tokens.hasMoreTokens()) { + // the remote (repository location) + String remoteLoc = tokens.nextToken(); + if (remoteLoc.endsWith("/")) + // the normalized name is always without the trailing '/' + remoteLoc = remoteLoc.substring(0, remoteLoc.length() - 1); + + // the location in the local filesystem + String LocalLoc = getLastPathComponent(remoteLoc); + + // check if there are errors in the configuration + String errorMessage = null; + if ((errorMessage = validateRemoteLocation(remoteLoc)) == null + && (errorMessage = validateLocalLocation(remoteLoc)) == null) + // the location is not configured properly + oldLocations.add(new ModuleLocation(remoteLoc, LocalLoc, + errorMessage)); + else + // everything fine + oldLocations.add(new ModuleLocation(remoteLoc, LocalLoc)); + } + + locations = oldLocations.toArray(new ModuleLocation[oldLocations + .size()]); + modules = null; + } + return locations; + } + + public boolean isUseUpdate() { + return useUpdate; + } + + public String getUsername() { + return username; + } + + private Collection getModuleDirNames() { + List dirs = new ArrayList(); + for (ModuleLocation l : getLocations()) { + dirs.add(l.getLocal()); + } + return dirs; + } + + private boolean calcChangeLog(AbstractBuild build, + File changelogFile, BuildListener listener) throws IOException { + if (build.getPreviousBuild() == null) { + // nothing to compare against + return createEmptyChangeLog(changelogFile, listener, "log"); + } + + PrintStream logger = listener.getLogger(); + + Map previousRevisions = parseRevisionFile(build + .getPreviousBuild()); + Map thisRevisions = parseRevisionFile(build); + + boolean changelogFileCreated = false; + + SVNLogClient svnlc = createSvnClientManager( + getDescriptor().createAuthenticationProvider()).getLogClient(); + + TransformerHandler th = createTransformerHandler(); + th.setResult(new StreamResult(changelogFile)); + SVNXMLLogHandler logHandler = new SVNXMLLogHandler(th); + // work around for http://svnkit.com/tracker/view.php?id=175 + th.setDocumentLocator(DUMMY_LOCATOR); + logHandler.startDocument(); + + for (ModuleLocation l : getLocations()) { + String url = l.getRemote(); + Long prevRev = previousRevisions.get(url); + if (prevRev == null) { + logger.println("no revision recorded for " + url + + " in the previous build"); + continue; + } + Long thisRev = thisRevisions.get(url); + if (thisRev.equals(prevRev)) { + logger.println("no change for " + url + + " since the previous build"); + continue; + } + + try { + svnlc.doLog(SVNURL.parseURIEncoded(url), null, SVNRevision + .create(prevRev), SVNRevision.create(prevRev + 1), + SVNRevision.create(thisRev), false, true, + Long.MAX_VALUE, logHandler); + } catch (SVNException e) { + e.printStackTrace(listener.error("revision check failed on " + + url)); + } + changelogFileCreated = true; + } + + if (changelogFileCreated) { + logHandler.endDocument(); + } + + if (!changelogFileCreated) + createEmptyChangeLog(changelogFile, listener, "log"); + + return true; + } + + /** + * Creates an identity transformer. + */ + private static TransformerHandler createTransformerHandler() { + try { + return ((SAXTransformerFactory) SAXTransformerFactory.newInstance()) + .newTransformerHandler(); + } catch (TransformerConfigurationException e) { + throw new Error(e); // impossible + } + } + + /* package */static Map parseRevisionFile(AbstractBuild build) + throws IOException { + Map revisions = new HashMap(); // module -> + // revision + {// read the revision file of the last build + File file = getRevisionFile(build); + if (!file.exists()) + // nothing to compare against + return revisions; + + BufferedReader br = new BufferedReader(new FileReader(file)); + String line; + while ((line = br.readLine()) != null) { + int index = line.lastIndexOf('/'); + if (index < 0) { + continue; // invalid line? + } + try { + revisions.put(line.substring(0, index), Long.parseLong(line + .substring(index + 1))); + } catch (NumberFormatException e) { + // perhaps a corrupted line. ignore + } + } + } + + return revisions; + } + + public boolean checkout(AbstractBuild build, Launcher launcher, + FilePath workspace, final BuildListener listener, File changelogFile) + throws IOException, InterruptedException { + if (!checkout(launcher, workspace, listener)) + return false; + + // write out the revision file + PrintWriter w = new PrintWriter(new FileOutputStream( + getRevisionFile(build))); + try { + Map revMap = buildRevisionMap(workspace, listener); + for (Entry e : revMap.entrySet()) { + w.println(e.getKey() + '/' + e.getValue().revision); + } + } finally { + w.close(); + } + + return calcChangeLog(build, changelogFile, listener); + } + + public boolean checkout(Launcher launcher, FilePath workspace, + final TaskListener listener) throws IOException, + InterruptedException { + if (useUpdate && isUpdatable(workspace, listener)) { + return update(launcher, workspace, listener); + } else { + final ISVNAuthenticationProvider authProvider = getDescriptor() + .createAuthenticationProvider(); + return workspace.act(new FileCallable() { + public Boolean invoke(File ws, VirtualChannel channel) + throws IOException { + Util.deleteContentsRecursive(ws); + SVNUpdateClient svnuc = createSvnClientManager(authProvider) + .getUpdateClient(); + svnuc.setEventHandler(new SubversionUpdateEventHandler( + listener)); + + for (ModuleLocation l : getLocations()) { + if (l.getErrorMessage() != null) { + listener.fatalError("Unable to perform checkout: " + + l.getErrorMessage() + + ". Please validate your configuration."); + return false; + } + try { + SVNURL url = SVNURL.parseURIEncoded(l.getRemote()); + listener.getLogger().println("Checking out " + url); + + svnuc.doCheckout(url, new File(ws, l.getLocal()), + SVNRevision.HEAD, SVNRevision.HEAD, true); + } catch (SVNException e) { + e.printStackTrace(listener + .error("Error in subversion")); + return false; + } + } + + return true; + } + }); + } + } + + /** + * Creates {@link SVNClientManager}. + * + *

+ * This method must be executed on the slave where svn operations are + * performed. + * + * @param authProvider + * The value obtained from + * {@link DescriptorImpl#createAuthenticationProvider()}. If the + * operation runs on slaves, (and properly remoted, if the svn + * operations run on slaves.) + */ + private static SVNClientManager createSvnClientManager( + ISVNAuthenticationProvider authProvider) { + ISVNAuthenticationManager sam = SVNWCUtil + .createDefaultAuthenticationManager(); + sam.setAuthenticationProvider(authProvider); + return SVNClientManager.newInstance(SVNWCUtil + .createDefaultOptions(true), sam); + } + + public static final class SvnInfo implements Serializable { + /** + * Decoded repository URL. + */ + final String url; + + 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); + } + + private static final long serialVersionUID = 1L; + } + + /** + * Gets the SVN metadata for the given local workspace. + * + * @param workspace + * The target to run "svn info". + */ + private SVNInfo parseSvnInfo(File workspace, + ISVNAuthenticationProvider authProvider) throws SVNException { + SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); + return svnWc.doInfo(workspace, SVNRevision.WORKING); + } + + /** + * Gets the SVN metadata for the remote repository. + * + * @param remoteUrl + * The target to run "svn info". + */ + private SVNInfo parseSvnInfo(SVNURL remoteUrl, + ISVNAuthenticationProvider authProvider) throws SVNException { + SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); + return svnWc.doInfo(remoteUrl, SVNRevision.HEAD, SVNRevision.HEAD); + } + + /** + * 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 Map buildRevisionMap(FilePath workspace, + final TaskListener listener) throws IOException, + InterruptedException { + final ISVNAuthenticationProvider authProvider = getDescriptor() + .createAuthenticationProvider(); + return workspace.act(new FileCallable>() { + public Map invoke(File ws, VirtualChannel channel) + throws IOException { + Map revisions = new HashMap(); + + SVNWCClient svnWc = createSvnClientManager(authProvider) + .getWCClient(); + // invoke the "svn info" + for (String module : getModuleDirNames()) { + try { + SvnInfo info = new SvnInfo(svnWc.doInfo(new File(ws, + module), SVNRevision.WORKING)); + revisions.put(info.url, info); + } catch (SVNException e) { + e + .printStackTrace(listener + .error("Failed to parse svn info for " + + module)); + } + } + + return revisions; + } + }); + } + + /** + * Gets the file that stores the revision. + */ + private static File getRevisionFile(AbstractBuild build) { + return new File(build.getRootDir(), "revision.txt"); + } + + /** + * this method is beeing used to validate the repository location. If + * null is beeing returned the location is all fine and has + * been validated successfully. A String instance beeing returned indicates + * that the validation failed and the String will contain a errormessage + * + * @param remoteLocation + * the location to be validated + * @return null if the location is valid or any instance of a + * String which represents a failure message if the validation + * failed + */ + private static String validateRemoteLocation(String remoteLocation) { + String v = Util.nullify(remoteLocation); + System.err.println("v" + v); + if (v == null) { + return "Repository location is mandatory"; + } + + // remove unneeded whitespaces + v = v.trim(); + // check if the repository location has been prefixed + if (!(v.startsWith("http://") || v.startsWith("https://") + || v.startsWith("svn://") || v.startsWith("svn+ssh://") || v + .startsWith("file://"))) { + return "incorrect Repository URL. See " + + "this " + + "for information about valid URLs."; + } + return null; + } + + /** + * this method does basically the same as + * {@link #validateRemoteLocation(String)} does. The difference is that it + * will check the local location instead of the remote one + * + * @see #validateRemoteLocation(String) + */ + private static String validateLocalLocation(String localLocation) { + String v = Util.nullify(localLocation); + if (v == null) { + // local directory is optional so this is ok + return null; + } + + 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 "neither absolute nor relative paths are allowed"; + } + + // all tests passed so far + return null; + } + + public boolean update(Launcher launcher, FilePath workspace, + final TaskListener listener) throws IOException, + InterruptedException { + final ISVNAuthenticationProvider authProvider = getDescriptor() + .createAuthenticationProvider(); + return workspace.act(new FileCallable() { + public Boolean invoke(File ws, VirtualChannel channel) + throws IOException { + SVNUpdateClient svnuc = createSvnClientManager(authProvider) + .getUpdateClient(); + svnuc + .setEventHandler(new SubversionUpdateEventHandler( + listener)); + + for (ModuleLocation l : getLocations()) { + if (l.getErrorMessage() != null) { + listener.fatalError("Unable to perform update: " + + l.getErrorMessage() + + ". Please validate your configuration."); + return false; + } + try { + String url = l.getRemote(); + listener.getLogger().println("Updating " + url); + svnuc.doUpdate(new File(ws, l.getLocal()), + SVNRevision.HEAD, true); + } catch (SVNException e) { + e + .printStackTrace(listener + .error("Error in subversion")); + return false; + } + } + return true; + } + }); + } + + /** + * Returns true if we can use "svn update" instead of "svn checkout" + */ + private boolean isUpdatable(FilePath workspace, final TaskListener listener) + throws IOException, InterruptedException { + final ISVNAuthenticationProvider authProvider = getDescriptor() + .createAuthenticationProvider(); + + return workspace.act(new FileCallable() { + public Boolean invoke(File ws, VirtualChannel channel) + throws IOException { + + for (ModuleLocation l : getLocations()) { + String url = l.getRemote(); + String moduleName = l.getLocal(); + File module = new File(ws, moduleName); + + if (!module.exists()) { + listener.getLogger().println( + "Checking out a fresh workspace because " + + module + " doesn't exist"); + return false; + } + + try { + SvnInfo svnInfo = new SvnInfo(parseSvnInfo(module, + authProvider)); + if (!svnInfo.url.equals(url)) { + listener.getLogger().println( + "Checking out a fresh workspace because the workspace is not " + + url); + return false; + } + } catch (SVNException e) { + listener + .getLogger() + .println( + "Checking out a fresh workspace because Hudson failed to detect the current workspace " + + module); + e.printStackTrace(listener.error(e.getMessage())); + return false; + } + } + return true; + } + }); + } + + public boolean pollChanges(AbstractProject project, Launcher launcher, + FilePath workspace, TaskListener listener) throws IOException, + InterruptedException { + // current workspace revision + Map wsRev = buildRevisionMap(workspace, listener); + + ISVNAuthenticationProvider authProvider = getDescriptor() + .createAuthenticationProvider(); + + // check the corresponding remote revision + for (SvnInfo localInfo : wsRev.values()) { + try { + SvnInfo remoteInfo = new SvnInfo(parseSvnInfo(localInfo + .getSVNURL(), authProvider)); + listener.getLogger().println("Revision:" + remoteInfo.revision); + if (remoteInfo.revision > localInfo.revision) + return true; // change found + } catch (SVNException e) { + e.printStackTrace(listener + .error("Failed to check repository revision for " + + localInfo.url)); + } + } + + return false; // no change + } + + public ChangeLogParser createChangeLogParser() { + return new SubversionChangeLogParser(); + } + + public DescriptorImpl getDescriptor() { + return DescriptorImpl.DESCRIPTOR; + } + + public void buildEnvVars(Map env) { + // no environment variable + } + + public FilePath getModuleRoot(FilePath workspace) { + if (getLocations().length > 0) + return workspace.child(getLocations()[0].getLocal()); + return workspace; + } + + private static String getLastPathComponent(String s) { + String[] tokens = s.split("/"); + return tokens[tokens.length - 1]; // return the last token + } + + public static final class DescriptorImpl extends Descriptor { + public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + /** + * Path to svn.exe. Null to default. + * + * @deprecated No longer in use. + */ + private volatile String svnExe; + + /** + * SVN authentication realm to its associated credentials. + */ + private final Map credentials = new Hashtable(); + + /** + * Stores {@link SVNAuthentication} for a single realm. + */ + private static abstract class Credential implements Serializable { + abstract SVNAuthentication createSVNAuthentication(); + } + + private static final class PasswordCredential extends Credential { + private final String userName; + + private final String password; // scrambled by base64 + + public PasswordCredential(String userName, String password) { + this.userName = userName; + this.password = Scrambler.scramble(password); + } + + @Override + SVNPasswordAuthentication createSVNAuthentication() { + return new SVNPasswordAuthentication(userName, Scrambler + .descramble(password), false); + } + } + + /** + * Remoting interface that allows remote + * {@link ISVNAuthenticationProvider} to read from local + * {@link DescriptorImpl#credentials}. + */ + private interface RemotableSVNAuthenticationProvider { + Credential getCredential(String realm); + } + + private final class RemotableSVNAuthenticationProviderImpl implements + RemotableSVNAuthenticationProvider, Serializable { + public Credential getCredential(String realm) { + return credentials.get(realm); + } + + /** + * When sent to the remote node, send a proxy. + */ + private Object writeReplace() { + return Channel.current().export( + RemotableSVNAuthenticationProvider.class, this); + } + } + + /** + * See {@link DescriptorImpl#createAuthenticationProvider()}. + */ + private static final class SVNAuthenticationProviderImpl implements + ISVNAuthenticationProvider, Serializable { + private final RemotableSVNAuthenticationProvider source; + + public SVNAuthenticationProviderImpl( + RemotableSVNAuthenticationProvider source) { + this.source = source; + } + + public SVNAuthentication requestClientAuthentication(String kind, + SVNURL url, String realm, SVNErrorMessage errorMessage, + SVNAuthentication previousAuth, boolean authMayBeStored) { + Credential cred = source.getCredential(realm); + if (cred == null) + return null; + return cred.createSVNAuthentication(); + } + + public int acceptServerAuthentication(SVNURL url, String realm, + Object certificate, boolean resultMayBeStored) { + return ACCEPTED_TEMPORARY; + } + + private static final long serialVersionUID = 1L; + } + + private DescriptorImpl() { + super(SubversionSCM.class); + load(); + } + + public String getDisplayName() { + return "Subversion"; + } + + public SCM newInstance(StaplerRequest req) { + return new SubversionSCM(req.getParameterValues("location_remote"), + req.getParameterValues("location_local"), req + .getParameter("svn_use_update") != null, req + .getParameter("svn_username"), req + .getParameter("svn_other_options")); + } + + /** + * Creates {@link ISVNAuthenticationProvider} backed by + * {@link #credentials}. This method must be invoked on the master, but + * the returned object is remotable. + */ + public ISVNAuthenticationProvider createAuthenticationProvider() { + return new SVNAuthenticationProviderImpl( + new RemotableSVNAuthenticationProviderImpl()); + } + + /** + * Used in the job configuration page to check if authentication for the + * SVN URLs are available. + */ + public void doAuthenticationCheck(final StaplerRequest req, + StaplerResponse rsp) throws IOException, ServletException { + new FormFieldValidator(req, rsp, true) { + protected void check() throws IOException, ServletException { + StringTokenizer tokens = new StringTokenizer( + fixNull(request.getParameter("value"))); + String message = ""; + + while (tokens.hasMoreTokens()) { + String url = tokens.nextToken(); + + try { + SVNRepository repository = SVNRepositoryFactory + .create(SVNURL.parseURIDecoded(url)); + + ISVNAuthenticationManager sam = SVNWCUtil + .createDefaultAuthenticationManager(); + sam + .setAuthenticationProvider(createAuthenticationProvider()); + repository.setAuthenticationManager(sam); + + repository.testConnection(); + } catch (SVNException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + + message += "Unable to access " + + url + + " : " + + Util.escape(e.getErrorMessage() + .getFullMessage()); + message += " (show details)"; + message += "

"; + message += " (Maybe you need to enter credential?)"; + message += "
"; + logger.log(Level.INFO, + "Failed to access subversion repository " + + url, e); + } + } + + if (message.length() == 0) + ok(); + else + error(message); + } + }.process(); + } + + /** + * Submits the authentication info. + */ + public void doPostCredential(final StaplerRequest req, + StaplerResponse rsp) throws IOException, ServletException { + final String url = req.getParameter("url"); + final String username = req.getParameter("username"); + final String password = req.getParameter("password"); + + try { + // the way it works with SVNKit is that + // 1) svnkit calls AuthenticationManager asking for a + // credential. + // this is when we can see the 'realm', which identifies the + // user domain. + // 2) DefaultSVNAuthenticationManager returns the username and + // password we set below + // 3) if the authentication is successful, svnkit calls back + // acknowledgeAuthentication + // (so we store the password info here) + SVNRepository repository = SVNRepositoryFactory.create(SVNURL + .parseURIDecoded(url)); + repository + .setAuthenticationManager(new DefaultSVNAuthenticationManager( + SVNWCUtil.getDefaultConfigurationDirectory(), + true, username, password) { + public void acknowledgeAuthentication( + boolean accepted, String kind, + String realm, SVNErrorMessage errorMessage, + SVNAuthentication authentication) + throws SVNException { + if (accepted) { + credentials.put(realm, + new PasswordCredential(username, + password)); + save(); + } + super.acknowledgeAuthentication(accepted, kind, + realm, errorMessage, authentication); + } + }); + repository.testConnection(); + rsp.sendRedirect("credentialOK"); + } catch (SVNException e) { + req.setAttribute("message", e.getErrorMessage()); + rsp.forward(Hudson.getInstance(), "error", req); + } + } + + /** + * validate the value for a remote (repository) location. + * + * @param req + * @param rsp + * @throws IOException + * @throws ServletException + * @since 1.64 + */ + public void doSvnRemoteLocationCheck(StaplerRequest req, + StaplerResponse rsp) throws IOException, ServletException { + new FormFieldValidator(req, rsp, false) { + protected void check() throws IOException, ServletException { + String retMessage = validateRemoteLocation(request + .getParameter("value")); + + if (retMessage != null) { + // validation failed. the errormessage is the string + // that is beeing returned + error(retMessage); + return; + } + // validation has been successful + ok(); + } + }.process(); + } + + /** + * validate the value for a local location (local checkout directory). + * + * @param req + * @param rsp + * @throws IOException + * @throws ServletException + * @since 1.64 + */ + public void doSvnLocalLocationCheck(StaplerRequest req, + StaplerResponse rsp) throws IOException, ServletException { + new FormFieldValidator(req, rsp, false) { + protected void check() throws IOException, ServletException { + String retMessage = validateLocalLocation(request + .getParameter("value")); + if (retMessage != null) { + // validation failed. the errormessage is the string + // that is beeing returned + error(retMessage); + return; + } + } + }.process(); + } + + static { + new Initializer(); + } + } + + private static final long serialVersionUID = 1L; + + private static final Logger logger = Logger.getLogger(SubversionSCM.class + .getName()); + + private static final LocatorImpl DUMMY_LOCATOR = new LocatorImpl(); + + static { + new Initializer(); + DUMMY_LOCATOR.setLineNumber(-1); + DUMMY_LOCATOR.setColumnNumber(-1); + } + + private static final class Initializer { + static { + DAVRepositoryFactory.setup(); // http, https + SVNRepositoryFactoryImpl.setup(); // svn, svn+xxx + FSRepositoryFactory.setup(); // file + } + } + + /** + * 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 + */ + public static final class ModuleLocation { + private String remote; + + private String local; + + private String errorMessage; + + public ModuleLocation(String remote, String local) { + // null indicates that there is no error with this location + this(remote, local, null); + } + + public ModuleLocation(String remote, String local, String errorMessage) { + this.remote = remote; + this.local = local; + this.errorMessage = errorMessage; + } + + public String getLocal() { + return local; + } + + public String getRemote() { + return remote; + } + + public String getErrorMessage() { + return errorMessage; + } + } + }