Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-10678

Increased functionality of Subversion Plugins Dropdown Build Parameter

XMLWordPrintable

    • Icon: Patch Patch
    • Resolution: Fixed
    • Icon: Major Major
    • subversion-plugin
    • None

      Added the following functionality to the Subversion Drop-Down Build Parameter:
      1) When provided SVN URL contains the trunk, branches, and tags directories - merge the contents of all three locations into one list.
      2) Added input Default Value - this will allow SVN Polling jobs to run.
      3) Added input Max Tags - when many branch / tag locations exists, this input will trim the return list's size.

      Files changed:

      updated | hudson.scm.listtagsparameter.ListSubversionTagsParameterDefinition
      updated | hudson.scm.listtagsparameter config.jelly
      added | help-defaultValue.html
      added | help-maxTags.html

      hudson.scm.listtagsparameter.ListSubversionTagsParameterDefinition

      /*
       * The MIT License
       *
       * Copyright (c) 2010-2011, Manufacture Francaise des Pneumatiques Michelin,
       * Romain Seguy
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy
       * of this software and associated documentation files (the "Software"), to deal
       * in the Software without restriction, including without limitation the rights
       * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
       * copies of the Software, and to permit persons to whom the Software is
       * furnished to do so, subject to the following conditions:
       *
       * The above copyright notice and this permission notice shall be included in
       * all copies or substantial portions of the Software.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
       * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
       * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
       * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
       * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
       * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
       * THE SOFTWARE.
       */
      
      package hudson.scm.listtagsparameter;
      
      import hudson.Extension;
      import hudson.Util;
      import hudson.model.AbstractProject;
      import hudson.model.Hudson;
      import hudson.model.ParameterDefinition;
      import hudson.model.ParameterValue;
      import hudson.model.ParametersDefinitionProperty;
      import hudson.scm.SubversionSCM;
      import hudson.util.FormValidation;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.UUID;
      import java.util.logging.Level;
      import java.util.logging.Logger;
      import java.util.regex.Pattern;
      import java.util.regex.PatternSyntaxException;
      import net.sf.json.JSONObject;
      import org.jvnet.localizer.ResourceBundleHolder;
      import org.kohsuke.stapler.AncestorInPath;
      import org.kohsuke.stapler.DataBoundConstructor;
      import org.kohsuke.stapler.QueryParameter;
      import org.kohsuke.stapler.StaplerRequest;
      import org.tmatesoft.svn.core.SVNDirEntry;
      import org.tmatesoft.svn.core.SVNException;
      import org.tmatesoft.svn.core.SVNURL;
      import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
      import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider;
      import org.tmatesoft.svn.core.io.SVNRepository;
      import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
      import org.tmatesoft.svn.core.wc.SVNLogClient;
      import org.tmatesoft.svn.core.wc.SVNRevision;
      import org.apache.commons.lang.StringUtils;
      
      /**
       * Defines a new {@link ParameterDefinition} to be displayed at the top of the
       * configuration page of {@link AbstractProject}s.
       * 
       * <p>When used, this parameter will request the user to select a Subversion tag
       * at build-time by displaying a drop-down list. See
       * {@link ListSubversionTagsParameterValue}.</p>
       * 
       * @author Romain Seguy (http://openromain.blogspot.com)
       */
      public class ListSubversionTagsParameterDefinition extends ParameterDefinition implements Comparable<ListSubversionTagsParameterDefinition> {
      
        /**
         * The Subversion repository which contains the tags to be listed.
         */
        private final String tagsDir;
        private final String tagsFilter;
        private final boolean reverseByDate;
        private final boolean reverseByName;
        private final String defaultValue;
        private final String maxTags;
        private static final String SVN_BRANCHES = "branches";
        private static final String SVN_TAGS = "tags";
        private static final String SVN_TRUNK = "trunk";
        
        
        /**
         * We use a UUID to uniquely identify each use of this parameter: We need this
         * to find the project using this parameter in the getTags() method (which is
         * called before the build takes place).
         */
        private final UUID uuid;
      
        @DataBoundConstructor
        public ListSubversionTagsParameterDefinition(String name, String tagsDir, String tagsFilter, String defaultValue, String maxTags, boolean reverseByDate, boolean reverseByName, String uuid) {
          super(name, ResourceBundleHolder.get(ListSubversionTagsParameterDefinition.class).format("TagDescription"));
          this.tagsDir = Util.removeTrailingSlash(tagsDir);
          this.tagsFilter = tagsFilter;
          this.reverseByDate = reverseByDate;
          this.reverseByName = reverseByName;
          this.defaultValue = defaultValue;
          this.maxTags = maxTags;
      
          if(uuid == null || uuid.length() == 0) {
            this.uuid = UUID.randomUUID();
          }
          else {
            this.uuid = UUID.fromString(uuid);
          }
        }
      
        // This method is invoked from a GET or POST HTTP request
        @Override
        public ParameterValue createValue(StaplerRequest req) {
          String[] values = req.getParameterValues(getName());
          if(values == null || values.length != 1) {
              return this.getDefaultParameterValue(); 
          }
          else {
            return new ListSubversionTagsParameterValue(getName(), getTagsDir(), values[0]);
          }
        }
      
        // This method is invoked when the user clicks on the "Build" button of Hudon's GUI
        @Override
        public ParameterValue createValue(StaplerRequest req, JSONObject formData) {
          ListSubversionTagsParameterValue value = req.bindJSON(ListSubversionTagsParameterValue.class, formData);
          value.setTagsDir(getTagsDir());
          // here, we could have checked for the value of the "tag" attribute of the
          // parameter value, but it's of no use because if we return null the build
          // still goes on...
          return value;
        }
        
      	@Override
      	public ParameterValue getDefaultParameterValue() {
      		if (StringUtils.isEmpty(this.defaultValue)) {
      			return null;
      		}
      		return new ListSubversionTagsParameterValue(getName(), getTagsDir(), this.defaultValue);
      	}  
      
        @Override
        public DescriptorImpl getDescriptor() {
          return (DescriptorImpl) super.getDescriptor();
        }
      
        /**
         * Returns a list of Subversion dirs to be displayed in
         * {@code ListSubversionTagsParameterDefinition/index.jelly}.
         *
         * <p>This method plainly reuses settings that must have been preivously
         * defined when configuring the Subversion SCM.</p>
         *
         * <p>This method never returns {@code null}. In case an error happens, the
         * returned list contains an error message surrounded by &lt; and &gt;.</p>
         */
        public List<String> getTags() {
          AbstractProject context = null;
          List<AbstractProject> jobs = Hudson.getInstance().getItems(AbstractProject.class);
      
          // which project is this parameter bound to? (I should take time to move
          // this code to Hudson core one day)
          for(AbstractProject project : jobs) {
            ParametersDefinitionProperty property = (ParametersDefinitionProperty) project.getProperty(ParametersDefinitionProperty.class);
            if(property != null) {
              List<ParameterDefinition> parameterDefinitions = property.getParameterDefinitions();
              if(parameterDefinitions != null) {
                for(ParameterDefinition pd : parameterDefinitions) {
                  if(pd instanceof ListSubversionTagsParameterDefinition && ((ListSubversionTagsParameterDefinition) pd).compareTo(this) == 0) {
                    context = project;
                    break;
                  }
                }
              }
            }
          }
      
          SimpleSVNDirEntryHandler dirEntryHandler = new SimpleSVNDirEntryHandler(tagsFilter);
          List<String> dirs = new ArrayList<String>();
      
          try {
            ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(context);
            ISVNAuthenticationManager authManager = SubversionSCM.createSvnAuthenticationManager(authProvider);
            SVNURL repoURL = SVNURL.parseURIDecoded(getTagsDir());
      
            SVNRepository repo = SVNRepositoryFactory.create(repoURL);
            repo.setAuthenticationManager(authManager);
            SVNLogClient logClient = new SVNLogClient(authManager, null);
            
            if (isSVNRepositoryProjectRoot(repo)) {
      			dirs = this.getSVNRootRepoDirectories(logClient, repoURL);
      	  } else {
      			logClient.doList(repoURL, SVNRevision.HEAD, SVNRevision.HEAD, false, false, dirEntryHandler);
      			dirs = dirEntryHandler.getDirs(isReverseByDate(), isReverseByName());
      	  }
          }
          catch(SVNException e) {
            // logs are not translated (IMO, this is a bad idea to translate logs)
            LOGGER.log(Level.SEVERE, "An SVN exception occurred while listing the directory entries at " + getTagsDir(), e);
            return new ArrayList() {{
              add("&lt;" + ResourceBundleHolder.get(ListSubversionTagsParameterDefinition.class).format("SVNException") + "&gt;");
            }};
          }
      
          // SVNKit's doList() method returns also the parent dir, so we need to remove it
          if(dirs != null) {
            removeParentDir(dirs);
          } else {
            LOGGER.log(Level.INFO, "No directory entries were found for the following SVN repository: {0}", getTagsDir());
            return new ArrayList() {{
              add("&lt;" + ResourceBundleHolder.get(ListSubversionTagsParameterDefinition.class).format("NoDirectoryEntriesFound") + "&gt;");
            }};
          }
          
          // Conform list to the maxTags option.
          Integer max = (isInt(this.maxTags) ? Integer.parseInt(this.maxTags) : null);
          if((max != null) && (dirs.size() > max)) {
          	dirs = dirs.subList(0, max);
          }
      
          return dirs;
        }
      
        public String getTagsDir() {
          return tagsDir;
        }
      
        public String getTagsFilter() {
          return tagsFilter;
        }
      
        public boolean isReverseByDate() {
          return reverseByDate;
        }
      
        public boolean isReverseByName() {
          return reverseByName;
        }
        
      public String getDefaultValue() {
      	return defaultValue;
      }
      
      public String getMaxTags() {
      	return maxTags;
      }
      
      	/**
      	 * Checks to see if given repository contains a trunk, branches, and tags
      	 * directories.
      	 * 
      	 * @param repo
      	 *            Repository to check.
      	 * @return True if trunk, branches, and tags exist.
      	 */
      	private boolean isSVNRepositoryProjectRoot(SVNRepository repo) {
      		try {
      			SVNDirEntry trunkEntry = repo.info("./" + SVN_TRUNK, SVNRevision.HEAD.getNumber());
      			SVNDirEntry branchesEntry = repo.info("./" + SVN_BRANCHES, SVNRevision.HEAD.getNumber());
      			SVNDirEntry tagsEntry = repo.info("./" + SVN_TAGS, SVNRevision.HEAD.getNumber());
      
      			if ((trunkEntry != null) && (branchesEntry != null) && (tagsEntry != null)) {
      				return true;
      			}
      
      		} catch (SVNException e) {
      			return false;
      		}
      
      		return false;
      
      	}
      
      	/**
      	 * Appends the target directory to all entries in a list. I.E. 1.2 -->
      	 * branches/1.2
      	 * 
      	 * @param targetDir
      	 *            The target directory to append.
      	 * @param dirs
      	 *            List of directory entries
      	 */
      	private void appendTargetDir(String targetDir, List<String> dirs) {
      		if ((targetDir != null) && (dirs != null) && (dirs.size() > 0)) {
      			for (int i = 0; i < dirs.size(); i++) {
      				dirs.set(i, targetDir + System.getProperty("file.separator") + dirs.get(i));
      			}
      		}
      	}
      	
      	private boolean isInt(String value) {
      		boolean isInteger = false;
      
      		try {
      			Integer.parseInt(value);
      			isInteger = true;
      		} catch (NumberFormatException e) {
      			isInteger = false;
      		}
      
      		return isInteger;
      	}	
      
      	/**
      	 * Returns a list of contents from the trunk, branches, and tags
      	 * directories.
      	 * 
      	 * @param logClient
      	 * @param repoURL
      	 * @return List of directories.
      	 * @throws SVNException
      	 */
      	private List<String> getSVNRootRepoDirectories(SVNLogClient logClient, SVNURL repoURL) throws SVNException {
      
      		// Get the branches repository contents
      		List<String> dirs = null;
      		SVNURL branchesRepo = repoURL.appendPath(SVN_BRANCHES, true);
      		SimpleSVNDirEntryHandler branchesEntryHandler = new SimpleSVNDirEntryHandler(null);
      		logClient.doList(branchesRepo, SVNRevision.HEAD, SVNRevision.HEAD, false, false, branchesEntryHandler);
      		List<String> branches = branchesEntryHandler.getDirs(isReverseByDate(), isReverseByName());
      		branches.remove(SVN_BRANCHES);
      		appendTargetDir(SVN_BRANCHES, branches);
      
      		// Get the tags repository contents
      		SVNURL tagsRepo = repoURL.appendPath(SVN_TAGS, true);
      		SimpleSVNDirEntryHandler tagsEntryHandler = new SimpleSVNDirEntryHandler(null);
      		logClient.doList(tagsRepo, SVNRevision.HEAD, SVNRevision.HEAD, false, false, tagsEntryHandler);
      		List<String> tags = tagsEntryHandler.getDirs(isReverseByDate(), isReverseByName());
      		tags.remove(SVN_TAGS);
      		appendTargetDir(SVN_TAGS, tags);
      
      		// Merge trunk with the contents of branches and tags.
      		dirs = new ArrayList<String>();
      		dirs.add(SVN_TRUNK);
      		
      		if (branches != null) {
      			dirs.addAll(branches);
      		}
      
      		if (tags != null) {
      			dirs.addAll(tags);
      		}
      
      		// Filter out any unwanted repository locations.
      		if (StringUtils.isNotBlank(tagsFilter)) {
      			Pattern filterPattern = Pattern.compile(tagsFilter);
      
      			if ((dirs != null) && (dirs.size() > 0) && (filterPattern != null)) {
      				List<String> temp = new ArrayList<String>();
      				for (String dir : dirs) {
      					if (filterPattern.matcher(dir).matches()) {
      						temp.add(dir);
      					}
      				}
      				dirs = temp;
      			}
      		}
      
      		return dirs;
        }  
      
        /**
         * Removes the parent directory (that is, the tags directory) from a list of
         * directories.
         */
        protected void removeParentDir(List<String> dirs) {
          List<String> dirsToRemove = new ArrayList<String>();
          for(String dir : dirs) {
            if(getTagsDir().endsWith(dir)) {
              dirsToRemove.add(dir);
            }
          }
          dirs.removeAll(dirsToRemove);
        }
      
        public int compareTo(ListSubversionTagsParameterDefinition pd) {
          if(pd.uuid.equals(uuid)) {
            return 0;
          }
          return -1;
        }
      
        @Extension
        public static class DescriptorImpl extends ParameterDescriptor {
      
          // we reuse as much as possible settings defined at the SCM level
          private SubversionSCM.DescriptorImpl scmDescriptor;
      
          public ISVNAuthenticationProvider createAuthenticationProvider(AbstractProject context) {
            return getSubversionSCMDescriptor().createAuthenticationProvider(context);
          }
      
          public FormValidation doCheckTagsDir(StaplerRequest req, @AncestorInPath AbstractProject context, @QueryParameter String value) {
            return getSubversionSCMDescriptor().doCheckRemote(req, context, value);
          }
      
          public FormValidation doCheckTagsFilter(@QueryParameter String value) {
            if(value != null && value.length() == 0) {
              try {
                Pattern.compile(value);
              }
              catch(PatternSyntaxException pse) {
                FormValidation.error(ResourceBundleHolder.get(ListSubversionTagsParameterDefinition.class).format("NotValidRegex"));
              }
            }
            return FormValidation.ok();
          }
      
          @Override
          public String getDisplayName() {
            return ResourceBundleHolder.get(ListSubversionTagsParameterDefinition.class).format("DisplayName");
          }
      
          /**
           * Returns the descriptor of {@link SubversionSCM}.
           */
          public SubversionSCM.DescriptorImpl getSubversionSCMDescriptor() {
            if(scmDescriptor == null) {
              scmDescriptor = (SubversionSCM.DescriptorImpl) Hudson.getInstance().getDescriptor(SubversionSCM.class);
            }
            return scmDescriptor;
          }
      
        }
      
        private final static Logger LOGGER = Logger.getLogger(ListSubversionTagsParameterDefinition.class.getName());
      
      }
      
      

      config.jelly

      <!--
        - The MIT License
        -
        - Copyright (c) 2010-2011, Manufacture Francaise des Pneumatiques Michelin,
        - Romain Seguy
        -
        - Permission is hereby granted, free of charge, to any person obtaining a copy
        - of this software and associated documentation files (the "Software"), to deal
        - in the Software without restriction, including without limitation the rights
        - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        - copies of the Software, and to permit persons to whom the Software is
        - furnished to do so, subject to the following conditions:
        -
        - The above copyright notice and this permission notice shall be included in
        - all copies or substantial portions of the Software.
        -
        - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
        - THE SOFTWARE.
        -->
      
      <!-- this is the page fragment displayed to set up a job -->
      <j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
          <f:entry title="${%Name}" field="name">
              <f:textbox name="parameter.name" value="${instance.name}" />
          </f:entry>
          <f:entry title="${%Repository URL}" field="tagsDir">
              <f:textbox name="parameter.tagsDir" value="${instance.tagsDir}" />
          </f:entry>
          <f:entry title="${%Tags filter}" field="tagsFilter">
              <f:textbox field="tagsFilter"/>
          </f:entry>
          <f:entry title="${%Default Value}" field="defaultValue">
              <f:textbox field="parameter.defaultValue" value="${instance.defaultValue}" />
          </f:entry> 
          <f:entry title="${%Maximum Tags to Display}" field="maxTags">
              <f:textbox field="parameter.maxTags" value="${instance.maxTags}" />
          </f:entry>       
          <f:entry field="reverseByDate">
              <f:checkbox name="reverseByDate" checked="${instance.reverseByDate}"/>
              <label class="attach-previous">${%Sort newest first}</label>
          </f:entry>
          <f:entry field="reverseByName">
              <f:checkbox name="reverseByName" checked="${instance.reverseByName}"/>
              <label class="attach-previous">${%Sort Z to A}</label>
          </f:entry>
      </j:jelly>
      

      help-defaultValue.html

      <!--
        - The MIT License
        -
        - Copyright (c) 2010, Manufacture Francaise des Pneumatiques Michelin, Romain Seguy
        - Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe
        -
        - 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.
        -->
      
      <div>
        For features such as SVN Polling a default value is required.  If job will only be started manually, this field is not necessary.
      </div>
      

      help-maxTags.html

      <!--
        - The MIT License
        -
        - Copyright (c) 2010, Manufacture Francaise des Pneumatiques Michelin, Romain Seguy
        - Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe
        -
        - 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.
        -->
      
      <div>
        The maximum number of tags to display in the dropdown.  Any non-number value will default to all.
      </div>
      

            andre_schramm André Schramm
            jeff_blaisdell Jeff Blaisdell
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

              Created:
              Updated:
              Resolved: