diff --git a/.gitignore b/.gitignore index 830feecb..2eea90cf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ work*/ .idea/ # eclipse project file +.factorypath .settings .classpath .project diff --git a/pom.xml b/pom.xml index 229aaf63..a598c626 100644 --- a/pom.xml +++ b/pom.xml @@ -3,17 +3,23 @@ org.jenkins-ci.plugins plugin - 1.409 - ../pom.xml + 1.565.3 scm-sync-configuration SCM Sync Configuration Plugin - 0.0.8.1-SNAPSHOT + 0.0.9-SNAPSHOT hpi http://wiki.jenkins-ci.org/display/JENKINS/SCM+Sync+configuration+plugin SCM Sync Configuration Jenkins plugin is aimed at 2 main features : First, keep sync'ed your config.xml (and other ressources) jenkins files with a SCM repository (backup), Secondly, track changes (and author) made on every file with commit messages. + + + MIT + http://www.opensource.org/licenses/mit-license.php + + + fcamblor @@ -29,11 +35,9 @@ UTF-8 - 1.4.8 - repo.jenkins-ci.org @@ -51,10 +55,67 @@ - + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.codehaus.gmaven + gmaven-plugin + [1.5,) + + generateTestStubs + testCompile + + + + + false + + + + + + org.codehaus.plexus + plexus-maven-plugin + [1.3.8,) + + merge-descriptors + + + + + false + + + + + + com.google.code.maven-replacer-plugin + maven-replacer-plugin + [1.3.2,) + + replace + + + + + false + + + + + + + + maven-release-plugin - 2.5 + 2.5.1 true @@ -71,9 +132,19 @@ - + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.6 + 1.6 + + + org.codehaus.plexus plexus-maven-plugin + 1.3.8 merge @@ -89,6 +160,37 @@ + + + com.google.code.maven-replacer-plugin + maven-replacer-plugin + 1.3.2 + + + process-test-sources + + replace + + + + + + target/inject-tests/*.java + target/generated-test-sources/injected/*.java + + false + + + Map parameters = new HashMap(); + Map<String,String> parameters = new HashMap<String,String>(); + + + new org.jvnet.hudson.test.PluginAutomaticTestBuilder() + org.jvnet.hudson.test.PluginAutomaticTestBuilder + + + + @@ -97,7 +199,7 @@ org.jenkins-ci.main maven-plugin - ${project.parent.version} + 2.3 @@ -122,27 +224,25 @@ + + org.sonatype.sisu + sisu-inject-plexus + 2.6.0 + + org.apache.maven.scm maven-scm-manager-plexus 1.9.1 - org.codehaus.plexus plexus-container-default - --> - - + org.jenkins-ci.plugins subversion @@ -175,26 +275,7 @@ powermock-api-mockito ${powermock.version} test - - - - org.mockito - mockito-core - 1.8.5 - test - - junit - junit - 4.8.1 - jar - test - - - com.google.guava - guava - 12.0.1 - diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/JenkinsFilesHelper.java b/src/main/java/hudson/plugins/scm_sync_configuration/JenkinsFilesHelper.java index a12de734..da320a80 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/JenkinsFilesHelper.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/JenkinsFilesHelper.java @@ -1,22 +1,31 @@ package hudson.plugins.scm_sync_configuration; -import hudson.model.Hudson; - import java.io.File; +import jenkins.model.Jenkins; + public class JenkinsFilesHelper { - public static String buildPathRelativeToHudsonRoot(File file){ - File hudsonRoot = Hudson.getInstance().getRootDir(); - if(!file.getAbsolutePath().startsWith(hudsonRoot.getAbsolutePath())){ - throw new IllegalArgumentException("Err ! File ["+file.getAbsolutePath()+"] seems not to reside in ["+hudsonRoot.getAbsolutePath()+"] !"); - } - String truncatedPath = file.getAbsolutePath().substring(hudsonRoot.getAbsolutePath().length()+1); // "+1" because we don't need ending file separator - return truncatedPath.replaceAll("\\\\", "/"); - } + public static String buildPathRelativeToHudsonRoot(File file) { + File jenkinsRoot = Jenkins.getInstance().getRootDir(); + String jenkinsRootPath = jenkinsRoot.getAbsolutePath(); + String fileAbsolutePath = file.getAbsolutePath(); + if (fileAbsolutePath.equals(jenkinsRootPath)) { + // Hmmm. Should never occur. + throw new IllegalArgumentException("Cannot build relative path to $JENKINS_HOME for $JENKINS_HOME itself; would be empty."); + } + if (!jenkinsRootPath.endsWith(File.separator)) { + jenkinsRootPath += File.separator; + } + if (!fileAbsolutePath.startsWith(jenkinsRootPath)) { + // Oops, the file is not relative to $JENKINS_HOME + return null; + } + String truncatedPath = fileAbsolutePath.substring(jenkinsRootPath.length()); + return truncatedPath.replace(File.separatorChar, '/'); + } - public static File buildFileFromPathRelativeToHudsonRoot(String pathRelativeToHudsonRoot){ - File hudsonRoot = Hudson.getInstance().getRootDir(); - return new File(hudsonRoot.getAbsolutePath()+File.separator+pathRelativeToHudsonRoot); + public static File buildFileFromPathRelativeToHudsonRoot(String pathRelativeToJenkinsRoot){ + return new File(Jenkins.getInstance().getRootDir(), pathRelativeToJenkinsRoot); } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/SCMManipulator.java b/src/main/java/hudson/plugins/scm_sync_configuration/SCMManipulator.java index 279cbfcb..24de088f 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/SCMManipulator.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/SCMManipulator.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -32,80 +31,82 @@ public class SCMManipulator { private static final Logger LOGGER = Logger.getLogger(SCMManipulator.class.getName()); - private ScmManager scmManager; - private ScmRepository scmRepository = null; - private String scmSpecificFilename = null; - - public SCMManipulator(ScmManager _scmManager) { - this.scmManager = _scmManager; - } - - /** - * Will check if everything is settled up (useful before a scm manipulation) - * @param scmContext - * @param resetScmRepository - * @return - */ - public boolean scmConfigurationSettledUp(ScmContext scmContext, boolean resetScmRepository){ - String scmRepositoryUrl = scmContext.getScmRepositoryUrl(); - SCM scm = scmContext.getScm(); - if(scmRepositoryUrl == null || scm == null){ - return false; - } - - if(resetScmRepository){ - LOGGER.info("Creating scmRepository connection data .."); - this.scmRepository = scm.getConfiguredRepository(this.scmManager, scmRepositoryUrl); - try { - this.scmSpecificFilename = this.scmManager.getProviderByRepository(this.scmRepository).getScmSpecificFilename(); - } - catch(NoSuchScmProviderException e) { - LOGGER.throwing(ScmManager.class.getName(), "getScmSpecificFilename", e); - LOGGER.severe("[getScmSpecificFilename] Error while getScmSpecificFilename : "+e.getMessage()); - return false; - } - } - - return expectScmRepositoryInitiated(); - } - - private boolean expectScmRepositoryInitiated(){ - boolean scmRepositoryInitiated = this.scmRepository != null; - if(!scmRepositoryInitiated) LOGGER.warning("SCM Repository has not yet been initiated !"); - return scmRepositoryInitiated; - } - - public UpdateScmResult update(File root) throws ScmException { - return this.scmManager.update(scmRepository, new ScmFileSet(root)); - } - public boolean checkout(File checkoutDirectory){ - boolean checkoutOk = false; - - if(!expectScmRepositoryInitiated()){ - return checkoutOk; - } - - // Checkouting sources - LOGGER.fine("Checkouting SCM files into ["+checkoutDirectory.getAbsolutePath()+"] ..."); - try { - CheckOutScmResult result = scmManager.checkOut(this.scmRepository, new ScmFileSet(checkoutDirectory)); - if(!result.isSuccess()){ - LOGGER.severe("[checkout] Error during checkout : "+result.getProviderMessage()+" || "+result.getCommandOutput()); - return checkoutOk; - } - checkoutOk = true; - } catch (ScmException e) { - LOGGER.throwing(ScmManager.class.getName(), "checkOut", e); - LOGGER.severe("[checkout] Error during checkout : "+e.getMessage()); - return checkoutOk; - } - - if(checkoutOk){ - LOGGER.fine("Checkouted SCM files into ["+checkoutDirectory.getAbsolutePath()+"] !"); - } - - return checkoutOk; - } + private final ScmManager scmManager; + private ScmRepository scmRepository = null; + private String scmSpecificFilename = null; + + public SCMManipulator(ScmManager _scmManager) { + this.scmManager = _scmManager; + } + + /** + * Will check if everything is settled up (useful before a scm manipulation) + * @param scmContext + * @param resetScmRepository + * @return + */ + public boolean scmConfigurationSettledUp(ScmContext scmContext, boolean resetScmRepository){ + String scmRepositoryUrl = scmContext.getScmRepositoryUrl(); + SCM scm = scmContext.getScm(); + if(scmRepositoryUrl == null || scm == null){ + return false; + } + + if(resetScmRepository){ + LOGGER.info("Creating scmRepository connection data .."); + this.scmRepository = scm.getConfiguredRepository(this.scmManager, scmRepositoryUrl); + try { + this.scmSpecificFilename = this.scmManager.getProviderByRepository(this.scmRepository).getScmSpecificFilename(); + } + catch(NoSuchScmProviderException e) { + LOGGER.throwing(ScmManager.class.getName(), "getScmSpecificFilename", e); + LOGGER.severe("[getScmSpecificFilename] Error while getScmSpecificFilename : "+e.getMessage()); + return false; + } + } + + return expectScmRepositoryInitiated(); + } + + private boolean expectScmRepositoryInitiated(){ + boolean scmRepositoryInitiated = this.scmRepository != null; + if(!scmRepositoryInitiated) { + LOGGER.warning("SCM Repository has not yet been initiated !"); + } + return scmRepositoryInitiated; + } + + public UpdateScmResult update(File root) throws ScmException { + return this.scmManager.update(scmRepository, new ScmFileSet(root)); + } + public boolean checkout(File checkoutDirectory){ + boolean checkoutOk = false; + + if(!expectScmRepositoryInitiated()){ + return checkoutOk; + } + + // Checkouting sources + LOGGER.fine("Checking out SCM files into ["+checkoutDirectory.getAbsolutePath()+"] ..."); + try { + CheckOutScmResult result = scmManager.checkOut(this.scmRepository, new ScmFileSet(checkoutDirectory)); + if(!result.isSuccess()){ + LOGGER.severe("[checkout] Error during checkout : "+result.getProviderMessage()+" || "+result.getCommandOutput()); + return checkoutOk; + } + checkoutOk = true; + } catch (ScmException e) { + LOGGER.throwing(ScmManager.class.getName(), "checkOut", e); + LOGGER.severe("[checkout] Error during checkout : "+e.getMessage()); + return checkoutOk; + } + + if(checkoutOk){ + LOGGER.fine("Checked out SCM files into ["+checkoutDirectory.getAbsolutePath()+"] !"); + } + + return checkoutOk; + } public List deleteHierarchy(File hierarchyToDelete){ if(!expectScmRepositoryInitiated()){ @@ -141,32 +142,32 @@ public List deleteHierarchy(File hierarchyToDelete){ } } - public List addFile(File scmRoot, String filePathRelativeToScmRoot){ - List synchronizedFiles = new ArrayList(); - - if(!expectScmRepositoryInitiated()){ - return synchronizedFiles; - } - - LOGGER.fine("Adding SCM file ["+filePathRelativeToScmRoot+"] ..."); - - try { - // Split every directory leading through modifiedFilePathRelativeToHudsonRoot - // and try add it in the scm - String[] pathChunks = filePathRelativeToScmRoot.split("\\\\|/"); - StringBuilder currentPath = new StringBuilder(); - for(int i=0; i addFile(File scmRoot, String filePathRelativeToScmRoot){ + List synchronizedFiles = new ArrayList(); + + if(!expectScmRepositoryInitiated()){ + return synchronizedFiles; + } + + LOGGER.fine("Adding SCM file ["+filePathRelativeToScmRoot+"] ..."); + + try { + // Split every directory leading through modifiedFilePathRelativeToHudsonRoot + // and try add it in the scm + String[] pathChunks = filePathRelativeToScmRoot.split("\\\\|/"); + StringBuilder currentPath = new StringBuilder(); + for(int i=0; i addFile(File scmRoot, String filePathRelativeToScmRoot){ LOGGER.severe("Error while adding SCM files in directory : " + addResult.getCommandOutput()); } } - } else { + } else { // If addResult.isSuccess() is false, it isn't an error if it is related to path chunks (except for latest one) : // if pathChunk is already synchronized, addResult.isSuccess() will be false. Level logLevel = (i==pathChunks.length-1)?Level.SEVERE:Level.FINE; LOGGER.log(logLevel, "Error while adding SCM file : " + addResult.getCommandOutput()); } - } + } } catch (IOException e) { LOGGER.throwing(ScmFileSet.class.getName(), "init<>", e); LOGGER.warning("[addFile] Error while creating ScmFileset : "+e.getMessage()); return synchronizedFiles; - } catch (ScmException e) { - LOGGER.throwing(ScmManager.class.getName(), "add", e); - LOGGER.warning("[addFile] Error while adding file : "+e.getMessage()); - return synchronizedFiles; - } + } catch (ScmException e) { + LOGGER.throwing(ScmManager.class.getName(), "add", e); + LOGGER.warning("[addFile] Error while adding file : "+e.getMessage()); + return synchronizedFiles; + } + + if(!synchronizedFiles.isEmpty()){ + LOGGER.fine("Added SCM files : "+Arrays.toString(synchronizedFiles.toArray(new File[0]))+" !"); + } - if(!synchronizedFiles.isEmpty()){ - LOGGER.fine("Added SCM files : "+Arrays.toString(synchronizedFiles.toArray(new File[0]))+" !"); - } - - return synchronizedFiles; - } + return synchronizedFiles; + } - private List refineUpdatedFilesInScmResult(List updatedFiles){ + private List refineUpdatedFilesInScmResult(List updatedFiles){ List refinedUpdatedFiles = new ArrayList(); // Cannot use directly a List or List here, since result type will depend upon // current scm api version - Iterator scmFileIter = updatedFiles.iterator(); - while(scmFileIter.hasNext()){ - Object scmFile = scmFileIter.next(); + for (Object scmFile :updatedFiles) { if(scmFile instanceof File){ String checkoutScmDir = ScmSyncConfigurationBusiness.getCheckoutScmDirectoryAbsolutePath(); String scmPath = ((File) scmFile).getAbsolutePath(); @@ -225,42 +224,42 @@ private List refineUpdatedFilesInScmResult(List updatedFiles){ return refinedUpdatedFiles; } - - public boolean checkinFiles(File scmRoot, String commitMessage){ - boolean checkinOk = false; - - if(!expectScmRepositoryInitiated()){ - return checkinOk; - } - - LOGGER.fine("Checking in SCM files ..."); - - ScmFileSet fileSet = new ScmFileSet(scmRoot); - - // Let's commit everything ! - try { - CheckInScmResult result = this.scmManager.checkIn(this.scmRepository, fileSet, commitMessage); - if(!result.isSuccess()){ - LOGGER.severe("[checkinFiles] Problem during SCM commit : "+result.getCommandOutput()); - return checkinOk; - } - checkinOk = true; - } catch (ScmException e) { - LOGGER.throwing(ScmManager.class.getName(), "checkIn", e); - LOGGER.severe("[checkinFiles] Error while checkin : "+e.getMessage()); - return checkinOk; - } - - - if(checkinOk){ - LOGGER.fine("Checked in SCM files !"); - } - - return checkinOk; - } - - public String getScmSpecificFilename() { - return scmSpecificFilename; - } - + + public boolean checkinFiles(File scmRoot, String commitMessage){ + boolean checkinOk = false; + + if(!expectScmRepositoryInitiated()){ + return checkinOk; + } + + LOGGER.fine("Checking in SCM files ..."); + + ScmFileSet fileSet = new ScmFileSet(scmRoot); + + // Let's commit everything ! + try { + CheckInScmResult result = this.scmManager.checkIn(this.scmRepository, fileSet, commitMessage); + if(!result.isSuccess()){ + LOGGER.severe("[checkinFiles] Problem during SCM commit : "+result.getCommandOutput()); + return checkinOk; + } + checkinOk = true; + } catch (ScmException e) { + LOGGER.throwing(ScmManager.class.getName(), "checkIn", e); + LOGGER.severe("[checkinFiles] Error while checkin : "+e.getMessage()); + return checkinOk; + } + + + if(checkinOk){ + LOGGER.fine("Checked in SCM files !"); + } + + return checkinOk; + } + + public String getScmSpecificFilename() { + return scmSpecificFilename; + } + } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationBusiness.java b/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationBusiness.java index 8e43be65..a2feba7b 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationBusiness.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationBusiness.java @@ -1,7 +1,7 @@ package hudson.plugins.scm_sync_configuration; import com.google.common.io.Files; -import hudson.model.Hudson; + import hudson.model.User; import hudson.plugins.scm_sync_configuration.exceptions.LoggableException; import hudson.plugins.scm_sync_configuration.model.*; @@ -9,6 +9,7 @@ import hudson.plugins.scm_sync_configuration.utils.Checksums; import hudson.security.Permission; import hudson.util.DaemonThreadFactory; + import org.apache.commons.io.FileUtils; import org.apache.maven.scm.ScmException; import org.apache.maven.scm.manager.ScmManager; @@ -25,10 +26,12 @@ import java.util.concurrent.Future; import java.util.logging.Logger; +import jenkins.model.Jenkins; + public class ScmSyncConfigurationBusiness { - private static final String WORKING_DIRECTORY_PATH = "/scm-sync-configuration/"; + private static final String WORKING_DIRECTORY = "scm-sync-configuration"; private static final String CHECKOUT_SCM_DIRECTORY = "checkoutConfiguration"; private static final Logger LOGGER = Logger.getLogger(ScmSyncConfigurationBusiness.class.getName()); @@ -44,7 +47,7 @@ public class ScmSyncConfigurationBusiness { /*package*/ final ExecutorService writer = Executors.newFixedThreadPool(1, new DaemonThreadFactory()); // TODO: Refactor this into the plugin object ??? - private List commitsQueue = Collections.synchronizedList(new ArrayList()); + private final List commitsQueue = new ArrayList(); public ScmSyncConfigurationBusiness(){ } @@ -132,25 +135,31 @@ public Future queueChangeSet(final ScmContext scmContext, ChangeSet change if(scmManipulator == null || !scmManipulator.scmConfigurationSettledUp(scmContext, false)){ LOGGER.info("Queue of changeset "+changeset.toString()+" aborted (scm manipulator not settled !)"); return null; - } + } Commit commit = new Commit(changeset, user, userMessage, scmContext); LOGGER.finest("Queuing commit "+commit.toString()+" to SCM ..."); - commitsQueue.add(commit); - - return writer.submit(new Callable() { - public Void call() throws Exception { - processCommitsQueue(); - return null; - } - }); + synchronized(commitsQueue) { + commitsQueue.add(commit); + + return writer.submit(new Callable() { + @Override + public Void call() throws Exception { + processCommitsQueue(); + return null; + } + }); + } } private void processCommitsQueue() { File scmRoot = new File(getCheckoutScmDirectoryAbsolutePath()); // Copying shared commitQueue in order to allow conccurrent modification - List currentCommitQueue = new ArrayList(commitsQueue); + List currentCommitQueue; + synchronized (commitsQueue) { + currentCommitQueue = new ArrayList(commitsQueue); + } List checkedInCommits = new ArrayList(); try { @@ -158,9 +167,15 @@ private void processCommitsQueue() { for(Commit commit: currentCommitQueue){ String logMessage = "Processing commit : " + commit.toString(); LOGGER.finest(logMessage); - // Preparing files to add / delete + // + // Two points: + // 1. getPathContents() already returns only those paths that are not also to be deleted. + // 2. For svn, we must not run svn add for files already in the repo. For git, we should run git add to stage the + // change. The second happens to work per chance because the git checkIn implementation will use git commit -a + // if a file set without files but only some directory is given, which we do. List updatedFiles = new ArrayList(); + for(Map.Entry pathContent : commit.getChangeset().getPathContents().entrySet()){ Path pathRelativeToJenkinsRoot = pathContent.getKey(); byte[] content = pathContent.getValue(); @@ -175,7 +190,7 @@ private void processCommitsQueue() { FileUtils.copyDirectory(JenkinsFilesHelper.buildFileFromPathRelativeToHudsonRoot(pathRelativeToJenkinsRoot.getPath()), fileTranslatedInScm); } catch (IOException e) { - throw new LoggableException("Error while copying file hierarchy to SCM checkouted directory", FileUtils.class, "copyDirectory", e); + throw new LoggableException("Error while copying file hierarchy to SCM directory", FileUtils.class, "copyDirectory", e); } updatedFiles.addAll(scmManipulator.addFile(scmRoot, firstNonExistingParentScmPath)); } @@ -217,19 +232,21 @@ private void processCommitsQueue() { signal(logMessage, true); } } - // As soon as a commit doesn't goes well, we should abort commit queue processing... + // As soon as a commit doesn't goes well, we should abort commit queue processing... }catch(LoggableException e){ LOGGER.throwing(e.getClazz().getName(), e.getMethodName(), e); LOGGER.severe("Error while processing commit queue : "+e.getMessage()); signal(e.getMessage(), false); } finally { // We should remove every checkedInCommits - commitsQueue.removeAll(checkedInCommits); + synchronized (commitsQueue) { + commitsQueue.removeAll(checkedInCommits); + } } } private boolean writeScmContentOnlyIfItDiffers(Path pathRelativeToJenkinsRoot, byte[] content, File fileTranslatedInScm) - throws LoggableException { + throws LoggableException { boolean scmContentUpdated = false; boolean contentDiffer = false; try { @@ -248,7 +265,7 @@ private boolean writeScmContentOnlyIfItDiffers(Path pathRelativeToJenkinsRoot, b } private void createScmContent(Path pathRelativeToJenkinsRoot, byte[] content, File fileTranslatedInScm) - throws LoggableException { + throws LoggableException { Stack directoriesToCreate = new Stack(); File directory = fileTranslatedInScm.getParentFile(); @@ -274,7 +291,7 @@ private void createScmContent(Path pathRelativeToJenkinsRoot, byte[] content, Fi Files.write(content, fileTranslatedInScm); } } catch (IOException e) { - throw new LoggableException("Error while creating file in checkouted directory", Files.class, "write", e); + throw new LoggableException("Error while creating file in SCM directory", Files.class, "write", e); } } @@ -282,7 +299,7 @@ public void synchronizeAllConfigs(ScmSyncStrategy[] availableStrategies){ List filesToSync = new ArrayList(); // Building synced files from strategies for(ScmSyncStrategy strategy : availableStrategies){ - filesToSync.addAll(strategy.createInitializationSynchronizedFileset()); + filesToSync.addAll(strategy.collect()); } ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance(); @@ -291,7 +308,7 @@ public void synchronizeAllConfigs(ScmSyncStrategy[] availableStrategies){ for(File fileToSync : filesToSync){ String hudsonConfigPathRelativeToHudsonRoot = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(fileToSync); - plugin.getTransaction().defineCommitMessage(new WeightedMessage("Repository initialization", MessageWeight.IMPORTANT)); + plugin.getTransaction().defineCommitMessage(new WeightedMessage("New included files", MessageWeight.IMPORTANT)); plugin.getTransaction().registerPath(hudsonConfigPathRelativeToHudsonRoot); } } finally { @@ -305,21 +322,21 @@ public boolean scmCheckoutDirectorySettledUp(ScmContext scmContext){ public List reloadAllFilesFromScm() throws IOException, ScmException { this.scmManipulator.update(new File(getCheckoutScmDirectoryAbsolutePath())); - return syncDirectories(new File(getCheckoutScmDirectoryAbsolutePath() + File.separator), ""); + return syncDirectories(new File(getCheckoutScmDirectoryAbsolutePath()), ""); } private List syncDirectories(File from, String relative) throws IOException { List l = new ArrayList(); for(File f : from.listFiles()) { String newRelative = relative + File.separator + f.getName(); - File jenkinsFile = new File(Hudson.getInstance().getRootDir() + newRelative); + File jenkinsFile = new File(Jenkins.getInstance().getRootDir() + newRelative); if (f.getName().equals(scmManipulator.getScmSpecificFilename())) { // nothing to do - } - else if (f.isDirectory()) { + } else if (f.isDirectory()) { if (!jenkinsFile.exists()) { FileUtils.copyDirectory(f, jenkinsFile, new FileFilter() { + @Override public boolean accept(File f) { return !f.getName().equals(scmManipulator.getScmSpecificFilename()); } @@ -330,8 +347,7 @@ public boolean accept(File f) { else { l.addAll(syncDirectories(f, newRelative)); } - } - else { + } else { if (!jenkinsFile.exists() || !FileUtils.contentEquals(f, jenkinsFile)) { FileUtils.copyFile(f, jenkinsFile); l.add(jenkinsFile); @@ -351,20 +367,24 @@ private void signal(String operation, boolean result) { } public static String getCheckoutScmDirectoryAbsolutePath(){ - return Hudson.getInstance().getRootDir().getAbsolutePath()+WORKING_DIRECTORY_PATH+CHECKOUT_SCM_DIRECTORY; + return new File(new File(Jenkins.getInstance().getRootDir(), WORKING_DIRECTORY), CHECKOUT_SCM_DIRECTORY).getAbsolutePath(); + } + + public static String getScmDirectoryName() { + return WORKING_DIRECTORY; } public void purgeFailLogs() { - Hudson.getInstance().checkPermission(purgeFailLogPermission()); + Jenkins.getInstance().checkPermission(purgeFailLogPermission()); scmSyncConfigurationStatusManager.purgeFailLogs(); } public boolean canCurrentUserPurgeFailLogs() { - return Hudson.getInstance().hasPermission(purgeFailLogPermission()); + return Jenkins.getInstance().hasPermission(purgeFailLogPermission()); } private static Permission purgeFailLogPermission(){ // Only administrators should be able to purge logs - return Hudson.ADMINISTER; + return Jenkins.ADMINISTER; } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationDataProvider.java b/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationDataProvider.java index d45b5d0d..2e6c5267 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationDataProvider.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationDataProvider.java @@ -1,9 +1,11 @@ package hudson.plugins.scm_sync_configuration; import hudson.plugins.scm_sync_configuration.model.BotherTimeout; + import org.kohsuke.stapler.Stapler; import javax.servlet.http.HttpServletRequest; + import java.util.*; import java.util.Map.Entry; import java.util.concurrent.Callable; @@ -11,90 +13,91 @@ public class ScmSyncConfigurationDataProvider { private static final ThreadLocal CURRENT_REQUEST = new ThreadLocal(); - private static final String COMMENT_SESSION_KEY = "__commitMessage"; - private static final String BOTHER_TIMEOUTS_SESSION_KEY = "__botherTimeouts"; - - public static void provideBotherTimeout(String type, int timeoutMinutesFromNow, String currentUrl){ - // FIXME: see if it wouldn't be possible to replace "currentURL" by "current file path" - // in order to be able to target same files with different urls (jobs via views for example) - - Map botherTimeouts = retrievePurgedBotherTimeouts(); - if(botherTimeouts == null){ - botherTimeouts = new HashMap(); - } - - // Removing existing BotherTimeouts matching current url - List< Entry > entriesToDelete = new ArrayList< Entry >(); - for(Entry entry : botherTimeouts.entrySet()){ - if(entry.getKey().matchesUrl(currentUrl)){ - entriesToDelete.add(entry); - } - } - botherTimeouts.keySet().removeAll(entriesToDelete); - - // Adding new BotherTimeout with updated timeout - Calendar cal = Calendar.getInstance(); - cal.add(Calendar.MINUTE, timeoutMinutesFromNow); - BotherTimeout bt = BotherTimeout.FACTORY.createBotherTimeout(type, timeoutMinutesFromNow, currentUrl); - botherTimeouts.put(bt, cal.getTime()); - - // Updating session - currentRequest().getSession().setAttribute(BOTHER_TIMEOUTS_SESSION_KEY, botherTimeouts); - } - - public static Date retrieveBotherTimeoutMatchingUrl(String currentURL){ - Map botherTimeouts = retrievePurgedBotherTimeouts(); - Date timeoutMatchingUrl = null; - if(botherTimeouts != null){ - for(Entry entry : botherTimeouts.entrySet()){ - if(entry.getKey().matchesUrl(currentURL)){ - timeoutMatchingUrl = entry.getValue(); - break; - } - } - } - return timeoutMatchingUrl; - } - - protected static Map retrievePurgedBotherTimeouts(){ - Map botherTimeouts = (Map)retrieveObject(BOTHER_TIMEOUTS_SESSION_KEY, false); - if(botherTimeouts != null){ - purgeOutdatedBotherTimeouts(botherTimeouts); - } - return botherTimeouts; - } - - protected static void purgeOutdatedBotherTimeouts(Map botherTimeouts){ - Date now = Calendar.getInstance().getTime(); - List< Entry > entriesToDelete = new ArrayList< Entry >(); - for(Entry entry : botherTimeouts.entrySet()){ - if(entry.getValue().before(now)){ - entriesToDelete.add(entry); - } - } - botherTimeouts.entrySet().removeAll(entriesToDelete); - } - - public static void provideComment(String comment){ - currentRequest().getSession().setAttribute(COMMENT_SESSION_KEY, comment); - } - - public static String retrieveComment(boolean cleanComment){ - return (String)retrieveObject(COMMENT_SESSION_KEY, cleanComment); - } - - private static Object retrieveObject(String key, boolean cleanObject){ + private static final String COMMENT_SESSION_KEY = "__commitMessage"; + private static final String BOTHER_TIMEOUTS_SESSION_KEY = "__botherTimeouts"; + + public static void provideBotherTimeout(String type, int timeoutMinutesFromNow, String currentUrl){ + // FIXME: see if it wouldn't be possible to replace "currentURL" by "current file path" + // in order to be able to target same files with different urls (jobs via views for example) + + Map botherTimeouts = retrievePurgedBotherTimeouts(); + if(botherTimeouts == null){ + botherTimeouts = new HashMap(); + } + + // Removing existing BotherTimeouts matching current url + List< Entry > entriesToDelete = new ArrayList< Entry >(); + for(Entry entry : botherTimeouts.entrySet()){ + if(entry.getKey().matchesUrl(currentUrl)){ + entriesToDelete.add(entry); + } + } + botherTimeouts.keySet().removeAll(entriesToDelete); + + // Adding new BotherTimeout with updated timeout + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, timeoutMinutesFromNow); + BotherTimeout bt = BotherTimeout.FACTORY.createBotherTimeout(type, timeoutMinutesFromNow, currentUrl); + botherTimeouts.put(bt, cal.getTime()); + + // Updating session + currentRequest().getSession().setAttribute(BOTHER_TIMEOUTS_SESSION_KEY, botherTimeouts); + } + + public static Date retrieveBotherTimeoutMatchingUrl(String currentURL){ + Map botherTimeouts = retrievePurgedBotherTimeouts(); + Date timeoutMatchingUrl = null; + if(botherTimeouts != null){ + for(Entry entry : botherTimeouts.entrySet()){ + if(entry.getKey().matchesUrl(currentURL)){ + timeoutMatchingUrl = entry.getValue(); + break; + } + } + } + return timeoutMatchingUrl; + } + + protected static Map retrievePurgedBotherTimeouts(){ + @SuppressWarnings("unchecked") + Map botherTimeouts = (Map)retrieveObject(BOTHER_TIMEOUTS_SESSION_KEY, false); + if(botherTimeouts != null){ + purgeOutdatedBotherTimeouts(botherTimeouts); + } + return botherTimeouts; + } + + protected static void purgeOutdatedBotherTimeouts(Map botherTimeouts){ + Date now = Calendar.getInstance().getTime(); + List< Entry > entriesToDelete = new ArrayList< Entry >(); + for(Entry entry : botherTimeouts.entrySet()){ + if(entry.getValue().before(now)){ + entriesToDelete.add(entry); + } + } + botherTimeouts.entrySet().removeAll(entriesToDelete); + } + + public static void provideComment(String comment){ + currentRequest().getSession().setAttribute(COMMENT_SESSION_KEY, comment); + } + + public static String retrieveComment(boolean cleanComment){ + return (String)retrieveObject(COMMENT_SESSION_KEY, cleanComment); + } + + private static Object retrieveObject(String key, boolean cleanObject){ HttpServletRequest request = currentRequest(); Object obj = null; - // Sometimes, request can be null : when hudson starts for instance ! + // Sometimes, request can be null : when hudson starts for instance ! if(request != null){ obj = request.getSession().getAttribute(key); if(cleanObject){ request.getSession().removeAttribute(key); } } - return obj; - } + return obj; + } public static void provideRequestDuring(HttpServletRequest request, Callable callable) throws Exception { CURRENT_REQUEST.set(request); diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin.java b/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin.java index 95b9ed11..51ffd987 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin.java @@ -1,11 +1,16 @@ package hudson.plugins.scm_sync_configuration; +import com.google.common.base.Function; import com.google.common.base.Predicate; +import com.google.common.base.Strings; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + import hudson.Plugin; import hudson.model.Descriptor; import hudson.model.Descriptor.FormException; -import hudson.model.Hudson; import hudson.model.Saveable; import hudson.model.User; import hudson.plugins.scm_sync_configuration.extensions.ScmSyncConfigurationFilter; @@ -20,44 +25,59 @@ import hudson.plugins.scm_sync_configuration.transactions.ThreadedTransaction; import hudson.plugins.scm_sync_configuration.xstream.ScmSyncConfigurationXStreamConverter; import hudson.plugins.scm_sync_configuration.xstream.migration.ScmSyncConfigurationPOJO; +import hudson.security.Permission; +import hudson.util.FormValidation; import hudson.util.PluginServletFilter; import net.sf.json.JSONObject; + import org.acegisecurity.AccessDeniedException; +import org.apache.maven.scm.CommandParameters; import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.provider.ScmProvider; +import org.apache.maven.scm.provider.ScmProviderRepository; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import javax.annotation.Nullable; import javax.servlet.ServletException; + import java.io.File; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.Future; import java.util.logging.Logger; +import java.util.regex.Pattern; + +import jenkins.model.Jenkins; public class ScmSyncConfigurationPlugin extends Plugin{ - public static final transient ScmSyncStrategy[] AVAILABLE_STRATEGIES = new ScmSyncStrategy[]{ - new JenkinsConfigScmSyncStrategy(), - new BasicPluginsConfigScmSyncStrategy(), - new JobConfigScmSyncStrategy(), - new UserConfigScmSyncStrategy(), - new ManualIncludesScmSyncStrategy() - }; + public static final transient ScmSyncStrategy[] AVAILABLE_STRATEGIES = new ScmSyncStrategy[]{ + new JenkinsConfigScmSyncStrategy(), + new BasicPluginsConfigScmSyncStrategy(), + new JobConfigScmSyncStrategy(), + new UserConfigScmSyncStrategy(), + new ManualIncludesScmSyncStrategy() + }; /** * Strategies that cannot be updated by user */ - public static final transient List DEFAULT_STRATEGIES = new ArrayList(){{ - addAll(Collections2.filter(Arrays.asList(AVAILABLE_STRATEGIES), new Predicate() { - public boolean apply(@Nullable ScmSyncStrategy scmSyncStrategy) { - return !( scmSyncStrategy instanceof ManualIncludesScmSyncStrategy ); - } - })); - }}; + public static final transient List DEFAULT_STRATEGIES = ImmutableList.copyOf( + Collections2.filter(Arrays.asList(AVAILABLE_STRATEGIES), new Predicate() { + @Override + public boolean apply(@Nullable ScmSyncStrategy scmSyncStrategy) { + return !( scmSyncStrategy instanceof ManualIncludesScmSyncStrategy ); + } + })); public void purgeFailLogs() { business.purgeFailLogs(); @@ -69,7 +89,7 @@ public static interface AtomicTransactionFactory { private static final Logger LOGGER = Logger.getLogger(ScmSyncConfigurationPlugin.class.getName()); - private transient ScmSyncConfigurationBusiness business; + private transient ScmSyncConfigurationBusiness business; /** * Flag allowing to process commit synchronously instead of asynchronously (default) @@ -85,12 +105,10 @@ public static interface AtomicTransactionFactory { */ private transient ThreadLocal transaction = new ThreadLocal(); - private transient Future latestCommitFuture; - - private String scmRepositoryUrl; - private SCM scm; - private boolean noUserCommitMessage; - private boolean displayStatus = true; + private String scmRepositoryUrl; + private SCM scm; + private boolean noUserCommitMessage; + private boolean displayStatus = true; // The [message] is a magic string that will be replaced with commit message // when commit occurs private String commitMessagePattern = "[message]"; @@ -102,69 +120,78 @@ public ScmSyncConfigurationPlugin(){ this(false); } - public ScmSyncConfigurationPlugin(boolean synchronousTransactions){ + public ScmSyncConfigurationPlugin(boolean synchronousTransactions){ this.synchronousTransactions = synchronousTransactions; - setBusiness(new ScmSyncConfigurationBusiness()); + setBusiness(new ScmSyncConfigurationBusiness()); try { PluginServletFilter.addFilter(new ScmSyncConfigurationFilter()); } catch (ServletException e) { throw new RuntimeException(e); } - } + } public List getManualSynchronizationIncludes(){ return manualSynchronizationIncludes; } - @Override - public void start() throws Exception { - super.start(); + @Override + public void start() throws Exception { + super.start(); - Hudson.XSTREAM.registerConverter(new ScmSyncConfigurationXStreamConverter()); + Jenkins.XSTREAM.registerConverter(new ScmSyncConfigurationXStreamConverter()); - this.load(); + this.load(); - // If scm has not been read in scm-sync-configuration.xml, let's initialize it - // to the "no scm" SCM - if(this.scm == null){ - this.scm = SCM.valueOf(ScmSyncNoSCM.class); - this.scmRepositoryUrl = null; - } + // If scm has not been read in scm-sync-configuration.xml, let's initialize it + // to the "no scm" SCM + if(this.scm == null){ + this.scm = SCM.valueOf(ScmSyncNoSCM.class); + this.scmRepositoryUrl = null; + } - // SCMManagerFactory.start() must be called here instead of ScmSyncConfigurationItemListener.onLoaded() - // because, for some unknown reasons, we reach plexus bootstraping exceptions when - // calling Embedder.start() when everything is loaded (very strange...) - SCMManagerFactory.getInstance().start(); - } + // SCMManagerFactory.start() must be called here instead of ScmSyncConfigurationItemListener.onLoaded() + // because, for some unknown reasons, we reach plexus bootstraping exceptions when + // calling Embedder.start() when everything is loaded (very strange...) + SCMManagerFactory.getInstance().start(); + initialInit(); + } - public void loadData(ScmSyncConfigurationPOJO pojo){ - this.scmRepositoryUrl = pojo.getScmRepositoryUrl(); - this.scm = pojo.getScm(); - this.noUserCommitMessage = pojo.isNoUserCommitMessage(); - this.displayStatus = pojo.isDisplayStatus(); + public void loadData(ScmSyncConfigurationPOJO pojo){ + this.scmRepositoryUrl = pojo.getScmRepositoryUrl(); + this.scm = pojo.getScm(); + this.noUserCommitMessage = pojo.isNoUserCommitMessage(); + this.displayStatus = pojo.isDisplayStatus(); this.commitMessagePattern = pojo.getCommitMessagePattern(); this.manualSynchronizationIncludes = pojo.getManualSynchronizationIncludes(); - } - - public void init() { - try { - this.business.init(createScmContext()); - } catch (Exception e) { - throw new RuntimeException("Error during ScmSyncConfiguration initialisation !", e); - } - } - - @Override - public void stop() throws Exception { - SCMManagerFactory.getInstance().stop(); - super.stop(); - } - - @Override - public void configure(StaplerRequest req, JSONObject formData) - throws IOException, ServletException, FormException { - super.configure(req, formData); + } + + protected void initialInit() throws Exception { + // We need to init() here in addition to ScmSyncConfigurationItemListener.onLoaded() to ensure that we do + // indeed create the SCM work directory when we are loaded. Otherwise, the plugin can be installed but + // then fails to operate until the next time Jenkins is restarted. Using postInitialize() for this might + // be too late if the plugin is copied to the plugin directory and then Jenkins is started. + this.business.init(createScmContext()); + } + + public void init() { + try { + this.business.init(createScmContext()); + } catch (Exception e) { + throw new RuntimeException("Error during ScmSyncConfiguration initialisation !", e); + } + } + + @Override + public void stop() throws Exception { + SCMManagerFactory.getInstance().stop(); + super.stop(); + } + + @Override + public void configure(StaplerRequest req, JSONObject formData) + throws IOException, ServletException, FormException { + super.configure(req, formData); boolean repoInitializationRequired = false; boolean configsResynchronizationRequired = false; @@ -175,12 +202,12 @@ public void configure(StaplerRequest req, JSONObject formData) this.commitMessagePattern = req.getParameter("commitMessagePattern"); String oldScmRepositoryUrl = this.scmRepositoryUrl; - String scmType = req.getParameter("scm"); - if(scmType != null){ - this.scm = SCM.valueOf(scmType); - String newScmRepositoryUrl = this.scm.createScmUrlFromRequest(req); + String scmType = req.getParameter("scm"); + if(scmType != null){ + this.scm = SCM.valueOf(scmType); + String newScmRepositoryUrl = this.scm.createScmUrlFromRequest(req); - this.scmRepositoryUrl = newScmRepositoryUrl; + this.scmRepositoryUrl = newScmRepositoryUrl; // If something changed, let's reinitialize repository in working directory ! repoInitializationRequired = newScmRepositoryUrl != null && !newScmRepositoryUrl.equals(oldScmRepositoryUrl); @@ -210,39 +237,54 @@ public void configure(StaplerRequest req, JSONObject formData) this.business.synchronizeAllConfigs(AVAILABLE_STRATEGIES); } if(repoCleaningRequired){ - // Cleaning checkouted repository this.business.cleanChekoutScmDirectory(); } // Persisting plugin data // Note that save() is made _after_ the synchronizeAllConfigs() because, otherwise, scm-sync-configuration.xml // file would be commited _before_ every other jenkins configuration file, which doesn't seem "natural" - this.save(); - } - - public void doReloadAllFilesFromScm(StaplerRequest req, StaplerResponse res) throws ServletException, IOException { - try { - filesModifiedByLastReload = business.reloadAllFilesFromScm(); - req.getView(this, "/hudson/plugins/scm_sync_configuration/reload.jelly").forward(req, res); - } - catch(ScmException e) { - throw new ServletException("Unable to reload SCM " + scm.getTitle() + ":" + getScmUrl(), e); - } - } - - public void doSubmitComment(StaplerRequest req, StaplerResponse res) throws ServletException, IOException { - // TODO: complexify this in order to pass a strategy identifier in the session key - ScmSyncConfigurationDataProvider.provideComment(req.getParameter("comment")); - if(Boolean.valueOf(req.getParameter("dontBotherMe")).booleanValue()){ - ScmSyncConfigurationDataProvider.provideBotherTimeout(req.getParameter("botherType"), - Integer.valueOf(req.getParameter("botherTime")), req.getParameter("currentURL")); - } - } - - // TODO: do retrieve help file with an action ! - public void doHelpForRepositoryUrl(StaplerRequest req, StaplerResponse res) throws ServletException, IOException{ - req.getView(this, SCM.valueOf(req.getParameter("scm")).getRepositoryUrlHelpPath()).forward(req, res); - } + this.save(); + } + + public Iterable collectAllFilesForScm() { + return Iterables.concat(Iterables.transform(Lists.newArrayList(AVAILABLE_STRATEGIES), new Function>() { + @Override + public Iterable apply(ScmSyncStrategy strategy) { + return strategy.collect(); + }})); + } + + public Iterable collectAllFilesForScm(final File fromSubDirectory) { + return Iterables.concat(Iterables.transform(Lists.newArrayList(AVAILABLE_STRATEGIES), new Function>() { + @Override + public Iterable apply(ScmSyncStrategy strategy) { + return strategy.collect(fromSubDirectory); + }})); + } + + public void doReloadAllFilesFromScm(StaplerRequest req, StaplerResponse res) throws ServletException, IOException { + try { + filesModifiedByLastReload = business.reloadAllFilesFromScm(); + req.getView(this, "/hudson/plugins/scm_sync_configuration/reload.jelly").forward(req, res); + } + catch(ScmException e) { + throw new ServletException("Unable to reload SCM " + scm.getTitle() + ":" + getScmUrl(), e); + } + } + + public void doSubmitComment(StaplerRequest req, StaplerResponse res) throws ServletException, IOException { + // TODO: complexify this in order to pass a strategy identifier in the session key + ScmSyncConfigurationDataProvider.provideComment(req.getParameter("comment")); + if(Boolean.valueOf(req.getParameter("dontBotherMe")).booleanValue()){ + ScmSyncConfigurationDataProvider.provideBotherTimeout(req.getParameter("botherType"), + Integer.valueOf(req.getParameter("botherTime")), req.getParameter("currentURL")); + } + } + + // TODO: do retrieve help file with an action ! + public void doHelpForRepositoryUrl(StaplerRequest req, StaplerResponse res) throws ServletException, IOException{ + req.getView(this, SCM.valueOf(req.getParameter("scm")).getRepositoryUrlHelpPath()).forward(req, res); + } // Help url for manualSynchronizationIncludes field is a jelly script and not a html file // because we need default includes list to be displayed in it ! @@ -254,6 +296,11 @@ public void doSynchronizeFile(@QueryParameter String path){ getTransaction().registerPath(path); } + /** + * This method is invoked via jelly to display a list of all the default includes. + * + * @return a list of explanatory strings about the patterns matched by a specific strategy's matcher. + */ public List getDefaultIncludes(){ List includes = new ArrayList(); for(ScmSyncStrategy strategy : DEFAULT_STRATEGIES){ @@ -262,106 +309,123 @@ public List getDefaultIncludes(){ return includes; } - private User getCurrentUser(){ - User user = null; - try { - user = Hudson.getInstance().getMe(); - }catch(AccessDeniedException e){} - return user; - } - - public static ScmSyncConfigurationPlugin getInstance(){ - return Hudson.getInstance().getPlugin(ScmSyncConfigurationPlugin.class); - } - - public ScmSyncStrategy getStrategyForSaveable(Saveable s, File f){ - for(ScmSyncStrategy strat : AVAILABLE_STRATEGIES){ - if(strat.isSaveableApplicable(s, f)){ - return strat; - } - } - // Strategy not found ! - return null; - } - - public ScmContext createScmContext(){ - return new ScmContext(this.scm, this.scmRepositoryUrl, this.commitMessagePattern); - } - - public boolean shouldDecorationOccursOnURL(String url){ - // Removing comment from session here... - ScmSyncConfigurationDataProvider.retrieveComment(true); - - // Displaying commit message popup is based on following tests : - // Zero : never ask for a commit message - // First : no botherTimeout should match with current url - // Second : a strategy should exist, matching current url - // Third : SCM Sync should be settled up - return !noUserCommitMessage && ScmSyncConfigurationDataProvider.retrieveBotherTimeoutMatchingUrl(url) == null + private User getCurrentUser(){ + User user = null; + try { + user = Jenkins.getInstance().getMe(); + }catch(AccessDeniedException e){} + return user; + } + + public static ScmSyncConfigurationPlugin getInstance(){ + return Jenkins.getInstance().getPlugin(ScmSyncConfigurationPlugin.class); + } + + public ScmSyncStrategy getStrategyForSaveable(Saveable s, File f){ + for(ScmSyncStrategy strat : AVAILABLE_STRATEGIES){ + if(strat.isSaveableApplicable(s, f)){ + return strat; + } + } + // Strategy not found ! + return null; + } + + /** + * Tries to find at least one strategy that would have applied to a deleted item. + * + * @param s the saveable that was deleted. It still exists in Jenkins' model, but has already been eradicated from disk. + * @param pathRelativeToRoot where the item had lived on disk + * @param wasDirectory whether it was a directory + * @return a strategy that thinks it might have applied + */ + public ScmSyncStrategy getStrategyForDeletedSaveable(Saveable s, String pathRelativeToRoot, boolean wasDirectory) { + for (ScmSyncStrategy strategy : AVAILABLE_STRATEGIES) { + if (strategy.mightHaveBeenApplicableToDeletedSaveable(s, pathRelativeToRoot, wasDirectory)) { + return strategy; + } + } + return null; + } + + public ScmContext createScmContext(){ + return new ScmContext(this.scm, this.scmRepositoryUrl, this.commitMessagePattern); + } + + public boolean shouldDecorationOccursOnURL(String url){ + // Removing comment from session here... + ScmSyncConfigurationDataProvider.retrieveComment(true); + + // Displaying commit message popup is based on following tests : + // Zero : never ask for a commit message + // First : no botherTimeout should match with current url + // Second : a strategy should exist, matching current url + // Third : SCM Sync should be settled up + return !noUserCommitMessage && ScmSyncConfigurationDataProvider.retrieveBotherTimeoutMatchingUrl(url) == null && getStrategyForURL(url) != null && this.business.scmCheckoutDirectorySettledUp(createScmContext()); - } - - public ScmSyncStrategy getStrategyForURL(String url){ - for(ScmSyncStrategy strat : AVAILABLE_STRATEGIES){ - if(strat.isCurrentUrlApplicable(url)){ - return strat; - } - } - // Strategy not found ! - return null; - } - - public boolean isNoUserCommitMessage() { - return noUserCommitMessage; - } - - public SCM[] getScms(){ - return SCM.values(); - } - - public void setBusiness(ScmSyncConfigurationBusiness business) { - this.business = business; - } - - public ScmSyncConfigurationStatusManager getScmSyncConfigurationStatusManager() { - return business.getScmSyncConfigurationStatusManager(); - } - - public String getScmRepositoryUrl() { - return scmRepositoryUrl; - } - - public boolean isScmSelected(SCM _scm){ - return this.scm == _scm; - } - - public SCM getSCM(){ - return this.scm; - } - - public String getScmUrl(){ - if(this.scm != null){ - return this.scm.extractScmUrlFrom(this.scmRepositoryUrl); - } else { - return null; - } - } - - public List getFilesModifiedByLastReload() { - return filesModifiedByLastReload; - } - - public boolean isDisplayStatus() { - return displayStatus; - } + } + + public ScmSyncStrategy getStrategyForURL(String url){ + for(ScmSyncStrategy strat : AVAILABLE_STRATEGIES){ + if(strat.isCurrentUrlApplicable(url)){ + return strat; + } + } + // Strategy not found ! + return null; + } + + public boolean isNoUserCommitMessage() { + return noUserCommitMessage; + } + + public SCM[] getScms(){ + return SCM.values(); + } + + public void setBusiness(ScmSyncConfigurationBusiness business) { + this.business = business; + } + + public ScmSyncConfigurationStatusManager getScmSyncConfigurationStatusManager() { + return business.getScmSyncConfigurationStatusManager(); + } + + public String getScmRepositoryUrl() { + return scmRepositoryUrl; + } + + public boolean isScmSelected(SCM _scm){ + return this.scm == _scm; + } + + public SCM getSCM(){ + return this.scm; + } + + public String getScmUrl(){ + if(this.scm != null){ + return this.scm.extractScmUrlFrom(this.scmRepositoryUrl); + } else { + return null; + } + } + + public List getFilesModifiedByLastReload() { + return filesModifiedByLastReload; + } + + public boolean isDisplayStatus() { + return displayStatus; + } public String getCommitMessagePattern() { return commitMessagePattern; } - public Descriptor getDescriptorForSCM(String scmName){ - return SCM.valueOf(scmName).getSCMDescriptor(); - } + public Descriptor getDescriptorForSCM(String scmName){ + return SCM.valueOf(scmName).getSCMDescriptor(); + } public void startThreadedTransaction(){ this.setTransaction(new ThreadedTransaction(synchronousTransactions)); @@ -370,8 +434,7 @@ public void startThreadedTransaction(){ public Future commitChangeset(ChangeSet changeset){ try { if(!changeset.isEmpty()){ - latestCommitFuture = this.business.queueChangeSet(createScmContext(), changeset, getCurrentUser(), ScmSyncConfigurationDataProvider.retrieveComment(false)); - return latestCommitFuture; + return this.business.queueChangeSet(createScmContext(), changeset, getCurrentUser(), ScmSyncConfigurationDataProvider.retrieveComment(false)); } else { return null; } @@ -393,13 +456,82 @@ protected void setTransaction(ScmTransaction transactionToRegister){ LOGGER.warning("Existing threaded transaction will be overriden !"); } transaction.set(transactionToRegister); - } + } public boolean currentUserCannotPurgeFailLogs() { return !business.canCurrentUserPurgeFailLogs(); } - public Future getLatestCommitFuture() { - return latestCommitFuture; + private static final Pattern STARTS_WITH_DRIVE_LETTER = Pattern.compile("^[a-zA-Z]:"); + + /** + * UI form validation for the git repository URL. Must be non-empty, a valid URL, and git must be able to access the repository through it. + * + * @param value from the UI form + * @return the validation status, with possible error or warning messages. + */ + public FormValidation doCheckGitUrl(@QueryParameter String value) { + if (Strings.isNullOrEmpty(value)) { + return FormValidation.error(Messages.ScmSyncConfigurationsPlugin_gitRepoUrlEmpty()); + } + String trimmed = value.trim(); + // Plain file paths are valid URIs, except maybe on windows if starting with a drive letter + if (!isValidUrl (trimmed)) { + // We have two more possibilities: + // - a plain file path starting with a drive letter and a colon on windows(?). Just delegate to the repository access below. + // - a ssh-like short form like [user@]host.domain.tld:repository + if (!STARTS_WITH_DRIVE_LETTER.matcher(trimmed).find()) { + // Possible ssh short form? + if (trimmed.indexOf("://") < 0 && trimmed.indexOf(':') > 0) { + if (!isValidUrl("ssh://" + trimmed.replaceFirst(":", "/"))) { + return FormValidation.error(Messages.ScmSyncConfigurationsPlugin_gitRepoUrlInvalid()); + } + } else { + return FormValidation.error(Messages.ScmSyncConfigurationsPlugin_gitRepoUrlInvalid()); + } + } + } + // Try to access the repository... + if (Jenkins.getInstance().hasPermission(Permission.CONFIGURE)) { + try { + ScmProvider scmProvider = SCMManagerFactory.getInstance().createScmManager().getProviderByUrl("scm:git:" + trimmed); + // Stupid interface. Why do I have to pass a delimiter if the URL must already be without "scm:git:" prefix?? + ScmProviderRepository remoteRepo = scmProvider.makeProviderScmRepository(trimmed, ':'); + // File set and parameters are ignored by the maven SCM gitexe implementation (for now...) + scmProvider.remoteInfo(remoteRepo, new ScmFileSet(Jenkins.getInstance().getRootDir()), new CommandParameters()); + // We actually don't care about the result. If this cannot access the repo, it'll raise an exception. + } catch (ComponentLookupException e) { + LOGGER.warning("Cannot validate repository URL: no ScmManager: " + e.getMessage()); + // And otherwise ignore + } catch (ScmException e) { + LOGGER.warning("Repository at " + trimmed + " is inaccessible"); + return FormValidation.error(Messages.ScmSyncConfigurationsPlugin_gitRepoUrlInaccessible(trimmed)); + } + } + if (trimmed.length() != value.length()) { + return FormValidation.warning(Messages.ScmSyncConfigurationsPlugin_gitRepoUrlWhitespaceWarning()); + } + return FormValidation.ok(); + } + + /** + * Determines whether the given string is a valid URL. + * + * @param input to check + * @return {@code true} if the input string is a vlid URL, {@code false} otherwise. + */ + private boolean isValidUrl(String input) { + try { + // There might be no "stream handler" in URL for ssh or git. We always replace the protocol by http for this check. + String httpUrl = input.replaceFirst("^[a-zA-Z]+://", "http://"); + new URI(httpUrl).toURL(); + return true; + } catch (MalformedURLException e) { + return false; + } catch (URISyntaxException e) { + return false; + } catch (IllegalArgumentException e) { + return false; + } } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationStatusManager.java b/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationStatusManager.java index ed26afe2..056e35de 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationStatusManager.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationStatusManager.java @@ -1,75 +1,75 @@ package hudson.plugins.scm_sync_configuration; -import hudson.model.Hudson; - import java.io.File; import java.io.IOException; import java.util.Date; import java.util.logging.Logger; +import jenkins.model.Jenkins; + import org.codehaus.plexus.util.FileUtils; public class ScmSyncConfigurationStatusManager { - private static final Logger LOGGER = Logger.getLogger(ScmSyncConfigurationStatusManager.class.getName()); - - public static final String LOG_SUCCESS_FILENAME = "scm-sync-configuration.success.log"; - - public static final String LOG_FAIL_FILENAME = "scm-sync-configuration.fail.log"; - - private File fail; - private File success; - - public ScmSyncConfigurationStatusManager() { - fail = new File(Hudson.getInstance().getRootDir().getAbsolutePath()+File.separator+LOG_FAIL_FILENAME); - success = new File(Hudson.getInstance().getRootDir().getAbsolutePath()+File.separator+LOG_SUCCESS_FILENAME); - } - - public String getLastFail() { - return readFile(fail); - } - - public String getLastSuccess() { - return readFile(success); - } - - public void signalSuccess() { - writeFile(success, new Date().toString()); - } - - public void signalFailed(String description) { - appendFile(fail, new Date().toString() + " : " + description + "
"); - } - - private static String readFile(File f) { - try { - if(f.exists()) { - return FileUtils.fileRead(f); - } - } - catch(IOException e) { - LOGGER.severe("Unable to read file " + f.getAbsolutePath() + " : " + e.getMessage()); - } - return null; - } - - private static void writeFile(File f, String data) { - try { - FileUtils.fileWrite(f.getAbsolutePath(), data); - } - catch(IOException e) { - LOGGER.severe("Unable to write file " + f.getAbsolutePath() + " : " + e.getMessage()); - } - } - - private static void appendFile(File f, String data) { - try { - FileUtils.fileAppend(f.getAbsolutePath(), data); - } - catch(IOException e) { - LOGGER.severe("Unable to write file " + f.getAbsolutePath() + " : " + e.getMessage()); - } - } + private static final Logger LOGGER = Logger.getLogger(ScmSyncConfigurationStatusManager.class.getName()); + + public static final String LOG_SUCCESS_FILENAME = "scm-sync-configuration.success.log"; + + public static final String LOG_FAIL_FILENAME = "scm-sync-configuration.fail.log"; + + private final File fail; + private final File success; + + public ScmSyncConfigurationStatusManager() { + fail = new File(Jenkins.getInstance().getRootDir().getAbsolutePath(), LOG_FAIL_FILENAME); + success = new File(Jenkins.getInstance().getRootDir().getAbsolutePath(), LOG_SUCCESS_FILENAME); + } + + public String getLastFail() { + return readFile(fail); + } + + public String getLastSuccess() { + return readFile(success); + } + + public void signalSuccess() { + writeFile(success, new Date().toString()); + } + + public void signalFailed(String description) { + appendFile(fail, new Date().toString() + " : " + description + "
"); + } + + private static String readFile(File f) { + try { + if(f.exists()) { + return FileUtils.fileRead(f); + } + } + catch(IOException e) { + LOGGER.severe("Unable to read file " + f.getAbsolutePath() + " : " + e.getMessage()); + } + return null; + } + + private static void writeFile(File f, String data) { + try { + FileUtils.fileWrite(f.getAbsolutePath(), data); + } + catch(IOException e) { + LOGGER.severe("Unable to write file " + f.getAbsolutePath() + " : " + e.getMessage()); + } + } + + private static void appendFile(File f, String data) { + try { + FileUtils.fileAppend(f.getAbsolutePath(), data); + } + catch(IOException e) { + LOGGER.severe("Unable to write file " + f.getAbsolutePath() + " : " + e.getMessage()); + } + } public void purgeFailLogs() { fail.delete(); diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/exceptions/LoggableException.java b/src/main/java/hudson/plugins/scm_sync_configuration/exceptions/LoggableException.java index 67f97998..f0df1247 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/exceptions/LoggableException.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/exceptions/LoggableException.java @@ -5,22 +5,25 @@ * Exception which will be easily loggable, by providing both class and method called, causing the exception */ public class LoggableException extends RuntimeException { - Class clazz; - String methodName; - public LoggableException(String message, Class clazz, String methodName, Throwable cause) { + private static final long serialVersionUID = 442135528912013310L; + + private final Class clazz; + private final String methodName; + + public LoggableException(String message, Class clazz, String methodName, Throwable cause) { super(message, cause); this.clazz = clazz; this.methodName = methodName; } - public LoggableException(String message, Class clazz, String methodName) { + public LoggableException(String message, Class clazz, String methodName) { super(message); this.clazz = clazz; this.methodName = methodName; } - public Class getClazz() { + public Class getClazz() { return clazz; } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationFilter.java b/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationFilter.java index 484e6b5e..4a900706 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationFilter.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationFilter.java @@ -20,9 +20,11 @@ public class ScmSyncConfigurationFilter implements Filter { private static final java.util.logging.Logger LOG = java.util.logging.Logger.getLogger(ScmSyncConfigurationFilter.class.getName()); + @Override public void init(FilterConfig filterConfig) throws ServletException { } + @Override public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { // In the beginning of every http request, we should create a new threaded transaction final ScmSyncConfigurationPlugin plugin; @@ -39,8 +41,9 @@ public void doFilter(final ServletRequest request, final ServletResponse respons try { // Providing current ServletRequest in ScmSyncConfigurationDataProvider's thread local // in order to be able to access it from everywhere inside this call - ScmSyncConfigurationDataProvider.provideRequestDuring((HttpServletRequest)request, new Callable() { - public Object call() throws Exception { + ScmSyncConfigurationDataProvider.provideRequestDuring((HttpServletRequest)request, new Callable() { + @Override + public Void call() throws Exception { try { // Handling "normally" http request chain.doFilter(request, response); @@ -64,6 +67,7 @@ public Object call() throws Exception { } } + @Override public void destroy() { } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationItemListener.java b/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationItemListener.java index 6d86292e..3a0cce4a 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationItemListener.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationItemListener.java @@ -2,74 +2,96 @@ import hudson.Extension; import hudson.model.Item; +import hudson.model.TopLevelItem; import hudson.model.listeners.ItemListener; import hudson.plugins.scm_sync_configuration.JenkinsFilesHelper; import hudson.plugins.scm_sync_configuration.ScmSyncConfigurationPlugin; import hudson.plugins.scm_sync_configuration.model.WeightedMessage; import hudson.plugins.scm_sync_configuration.strategies.ScmSyncStrategy; +import hudson.plugins.scm_sync_configuration.transactions.ScmTransaction; import java.io.File; +import jenkins.model.DirectlyModifiableTopLevelItemGroup; +import jenkins.model.Jenkins; + @Extension public class ScmSyncConfigurationItemListener extends ItemListener { - @Override - public void onLoaded() { - super.onLoaded(); - - // After every plugin is loaded, let's init ScmSyncConfigurationPlugin - // Init is needed after plugin loads since it relies on scm implementations plugins loaded + @Override + public void onLoaded() { + super.onLoaded(); + + // After every plugin is loaded, let's init ScmSyncConfigurationPlugin + // Init is needed after plugin loads since it relies on scm implementations plugins loaded ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance(); - if(plugin != null){ + if (plugin != null) { plugin.init(); } - } - - @Override - public void onDeleted(Item item) { - super.onDeleted(item); - - ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance(); - if(plugin != null){ - ScmSyncStrategy strategy = plugin.getStrategyForSaveable(item, null); + } - if(strategy != null){ + @Override + public void onDeleted(Item item) { + super.onDeleted(item); + + ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance(); + if(plugin != null){ + String path = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(item.getRootDir()); + ScmSyncStrategy strategy = plugin.getStrategyForDeletedSaveable(item, path, true); + if (strategy != null) { WeightedMessage message = strategy.getCommitMessageFactory().getMessageWhenItemDeleted(item); - plugin.getTransaction().defineCommitMessage(message); - String path = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(item.getRootDir()); - plugin.getTransaction().registerPathForDeletion(path); + ScmTransaction transaction = plugin.getTransaction(); + transaction.defineCommitMessage(message); + transaction.registerPathForDeletion(path); } } - } - - @Override - public void onCreated(Item item) { - super.onCreated(item); - } - - @Override - public void onCopied(Item src, Item item) { - super.onCopied(src, item); - } - - @Override - public void onRenamed(Item item, String oldName, String newName) { - super.onRenamed(item, oldName, newName); - ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance(); - if(plugin != null){ - ScmSyncStrategy strategy = plugin.getStrategyForSaveable(item, null); - - if(strategy != null){ - File parentDir = item.getRootDir().getParentFile(); - File oldDir = new File( parentDir.getAbsolutePath()+File.separator+oldName ); - File newDir = new File( parentDir.getAbsolutePath()+File.separator+newName ); + } - String oldPath = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(oldDir); - String newPath = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(newDir); - WeightedMessage message = strategy.getCommitMessageFactory().getMessageWhenItemRenamed(item, oldPath, newPath); - plugin.getTransaction().defineCommitMessage(message); - plugin.getTransaction().registerRenamedPath(oldPath, newPath); + @Override + public void onLocationChanged(Item item, String oldFullName, String newFullName) { + super.onLocationChanged(item, oldFullName, newFullName); + ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance(); + if (plugin == null) { + return; + } + // Figure out where the item previously might have been. + File oldDir = null; + Jenkins jenkins = Jenkins.getInstance(); + int i = oldFullName.lastIndexOf('/'); + String oldSimpleName = i > 0 ? oldFullName.substring(i+1) : oldFullName; + Object oldParent = i > 0 ? jenkins.getItemByFullName(oldFullName.substring(0, i)) : jenkins; + Object newParent = item.getParent(); + if (newParent == null) { + // Shouldn't happen. + newParent = jenkins; + } + if (oldParent == newParent && oldParent != null) { + // Simple rename within the same directory + oldDir = new File (item.getRootDir().getParentFile(), oldSimpleName); + } else if (oldParent instanceof DirectlyModifiableTopLevelItemGroup && item instanceof TopLevelItem) { + oldDir = ((DirectlyModifiableTopLevelItemGroup) oldParent).getRootDirFor((TopLevelItem) item); + oldDir = new File (oldDir.getParentFile(), oldSimpleName); + } + ScmSyncStrategy oldStrategy = null; + if (oldDir != null) { + oldStrategy = plugin.getStrategyForDeletedSaveable(item, JenkinsFilesHelper.buildPathRelativeToHudsonRoot(oldDir), true); + } + File newDir = item.getRootDir(); + ScmSyncStrategy newStrategy = plugin.getStrategyForSaveable(item, newDir); + ScmTransaction transaction = plugin.getTransaction(); + if (newStrategy == null) { + if (oldStrategy != null) { + // Delete old + WeightedMessage message = oldStrategy.getCommitMessageFactory().getMessageWhenItemRenamed(item, oldFullName, newFullName); + transaction.defineCommitMessage(message); + transaction.registerPathForDeletion(JenkinsFilesHelper.buildPathRelativeToHudsonRoot(oldDir)); } + } else { + String newPathRelativeToRoot = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(newDir); + // Something moved to a place where we do cover it. + WeightedMessage message = newStrategy.getCommitMessageFactory().getMessageWhenItemRenamed(item, oldFullName, newFullName); + transaction.defineCommitMessage(message); + transaction.registerRenamedPath(oldStrategy != null ? JenkinsFilesHelper.buildPathRelativeToHudsonRoot(oldDir) : null, newPathRelativeToRoot); } - } + } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationPageDecorator.java b/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationPageDecorator.java index ca56d155..d193dfdf 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationPageDecorator.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationPageDecorator.java @@ -3,21 +3,23 @@ import hudson.Extension; import hudson.model.PageDecorator; import hudson.plugins.scm_sync_configuration.ScmSyncConfigurationPlugin; + import org.kohsuke.stapler.bind.JavaScriptMethod; @Extension public class ScmSyncConfigurationPageDecorator extends PageDecorator{ - public ScmSyncConfigurationPageDecorator(){ - super(ScmSyncConfigurationPageDecorator.class); - } - - public ScmSyncConfigurationPlugin getScmSyncConfigPlugin(){ - return ScmSyncConfigurationPlugin.getInstance(); - } + @SuppressWarnings("deprecation") // Super constructor is deprecated. Unsure if default constructor would work, though. + public ScmSyncConfigurationPageDecorator(){ + super(ScmSyncConfigurationPageDecorator.class); + } + + public ScmSyncConfigurationPlugin getScmSyncConfigPlugin(){ + return ScmSyncConfigurationPlugin.getInstance(); + } @JavaScriptMethod - public void purgeScmSyncConfigLogs() { + public void purgeScmSyncConfigLogs() { getScmSyncConfigPlugin().purgeFailLogs(); - } + } } \ No newline at end of file diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationSaveableListener.java b/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationSaveableListener.java index 37062f77..cd3709af 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationSaveableListener.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/extensions/ScmSyncConfigurationSaveableListener.java @@ -11,13 +11,13 @@ @Extension public class ScmSyncConfigurationSaveableListener extends SaveableListener{ - - @Override - public void onChange(Saveable o, XmlFile file) { - - super.onChange(o, file); - - ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance(); + + @Override + public void onChange(Saveable o, XmlFile file) { + + super.onChange(o, file); + + ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance(); if(plugin != null){ ScmSyncStrategy strategy = plugin.getStrategyForSaveable(o, file.getFile()); @@ -28,5 +28,5 @@ public void onChange(Saveable o, XmlFile file) { plugin.getTransaction().registerPath(path); } } - } + } } \ No newline at end of file diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/model/ChangeSet.java b/src/main/java/hudson/plugins/scm_sync_configuration/model/ChangeSet.java index 9b39f18c..d2927b29 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/model/ChangeSet.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/model/ChangeSet.java @@ -8,14 +8,12 @@ import java.io.File; import java.io.IOException; import java.util.*; -import java.util.logging.Logger; /** * @author fcamblor * POJO representing a Changeset built during a scm transaction */ public class ChangeSet { - private static final Logger LOGGER = Logger.getLogger(ChangeSet.class.getName()); // Changeset commit message WeightedMessage message = null; @@ -57,11 +55,6 @@ public void registerPath(String path) { } } - public void registerRenamedPath(String oldPath, String newPath) { - registerPathForDeletion(oldPath); - registerPath(newPath); - } - public void registerPathForDeletion(String path){ // We should determine if path is a directory by watching scm path (and not hudson path) because in most of time, // when we are here, directory is already deleted in hudson hierarchy... @@ -101,7 +94,7 @@ public List getPathsToDelete(){ public void defineMessage(WeightedMessage weightedMessage) { // Defining message only once ! - if(this.message == null || weightedMessage.getWeight().weighterThan(message.getWeight())){ + if(this.message == null || weightedMessage.getWeight().compareTo(message.getWeight()) > 0){ this.message = weightedMessage; } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/model/Commit.java b/src/main/java/hudson/plugins/scm_sync_configuration/model/Commit.java index 885e1cd9..8682f5cc 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/model/Commit.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/model/Commit.java @@ -1,5 +1,10 @@ package hudson.plugins.scm_sync_configuration.model; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; + +import com.google.common.base.Strings; + import hudson.model.User; /** @@ -34,22 +39,46 @@ public ScmContext getScmContext(){ } private static String createCommitMessage(ScmContext context, String messagePrefix, User user, String userComment){ - StringBuilder commitMessage = new StringBuilder(); - commitMessage.append(messagePrefix); - if(user != null){ - commitMessage.append(" by ").append(user.getId()); - } - if(userComment != null && !"".equals(userComment)){ - commitMessage.append(" with following comment : ").append(userComment); - } - String message = commitMessage.toString(); - - if(context.getCommitMessagePattern() == null || "".equals(context.getCommitMessagePattern())){ - return message; - } else { - return context.getCommitMessagePattern().replaceAll("\\[message\\]", message.replaceAll("\\$", "\\\\\\$")); - } - } + StringBuilder commitMessage = new StringBuilder(); + if (user != null) { + commitMessage.append(user.getId()).append(": "); + } + commitMessage.append(messagePrefix).append('\n'); + if (user != null) { + commitMessage.append('\n').append("Change performed by ").append(user.getDisplayName()).append('\n'); + } + if (userComment != null && !"".equals(userComment.trim())){ + commitMessage.append('\n').append(userComment.trim()); + } + String message = commitMessage.toString(); + + if (!Strings.isNullOrEmpty(context.getCommitMessagePattern())) { + message = context.getCommitMessagePattern().replaceAll("\\[message\\]", message.replaceAll("\\$", "\\\\\\$")); + } + return wrapText(message, 72); + } + + private static String wrapText(String str, int lineLength) { + if (str == null) { + return null; + } + int i = 0; + int max = str.length(); + StringBuilder text = new StringBuilder(); + while (i < max) { + int next = str.indexOf('\n', i); + if (next < 0) { + next = max; + } + String line = StringUtils.stripEnd(str.substring(i, next), null); + if (line.length() > lineLength) { + line = WordUtils.wrap(line, lineLength, "\n", false); + } + text.append(line).append('\n'); + i = next+1; + } + return text.toString(); + } @Override public String toString() { diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/model/MessageWeight.java b/src/main/java/hudson/plugins/scm_sync_configuration/model/MessageWeight.java index fec0e617..e512f4dd 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/model/MessageWeight.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/model/MessageWeight.java @@ -5,13 +5,8 @@ * Message weight should be used to prioritize messages into a Scm Transaction */ public enum MessageWeight { - MINIMAL(0),NORMAL(1),IMPORTANT(2),MORE_IMPORTANT(3); - - private int weight; - private MessageWeight(int _weight){ - this.weight = _weight; - } - public boolean weighterThan(MessageWeight ms){ - return this.weight > ms.weight; - } + MINIMAL, + NORMAL, + IMPORTANT, + MORE_IMPORTANT; } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/model/Path.java b/src/main/java/hudson/plugins/scm_sync_configuration/model/Path.java index b572714c..462f933c 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/model/Path.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/model/Path.java @@ -12,8 +12,8 @@ */ public class Path { - private String path; - private boolean isDirectory; + private final String path; + private final boolean isDirectory; public Path(String path){ this(JenkinsFilesHelper.buildFileFromPathRelativeToHudsonRoot(path)); @@ -24,7 +24,7 @@ public Path(File hudsonFile){ } public Path(String path, boolean isDirectory) { - this.path = path; + this.path = path.replace(File.separatorChar, '/'); // Make sure we use the system-independent separator. this.isDirectory = isDirectory; } @@ -39,7 +39,7 @@ public File getHudsonFile(){ public File getScmFile(){ // TODO: Externalize ScmSyncConfigurationBusiness.getCheckoutScmDirectoryAbsolutePath() // in another class ? - return new File(ScmSyncConfigurationBusiness.getCheckoutScmDirectoryAbsolutePath()+File.separator+getPath()); + return new File(ScmSyncConfigurationBusiness.getCheckoutScmDirectoryAbsolutePath(), getPath()); } public String getFirstNonExistingParentScmPath(){ @@ -59,18 +59,37 @@ public boolean isDirectory() { } public boolean contains(Path p){ - return this.isDirectory() && p.getPath().startsWith(this.getPath()); + if (this.isDirectory()) { + String path = this.getPath(); + if (!path.endsWith("/")) { + path += '/'; + } + String otherPath = p.getPath(); + if (p.isDirectory() && !otherPath.endsWith("/")) { + otherPath += '/'; + } + return otherPath.startsWith(path); + } + return false; } @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Path)) return false; + if (this == o) { + return true; + } + if (!(o instanceof Path)) { + return false; + } Path path1 = (Path) o; - if (isDirectory != path1.isDirectory) return false; - if (path != null ? !path.equals(path1.path) : path1.path != null) return false; + if (isDirectory != path1.isDirectory) { + return false; + } + if (path != null ? !path.equals(path1.path) : path1.path != null) { + return false; + } return true; } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/model/WeightedMessage.java b/src/main/java/hudson/plugins/scm_sync_configuration/model/WeightedMessage.java index 52dcfb9d..b470cbdc 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/model/WeightedMessage.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/model/WeightedMessage.java @@ -7,15 +7,19 @@ * have only the more important commit message kept during the transaction */ public class WeightedMessage { - String message; - MessageWeight weight; + + private final String message; + private final MessageWeight weight; + public WeightedMessage(String message, MessageWeight weight) { this.message = message; this.weight = weight; } + public String getMessage() { return message; } + public MessageWeight getWeight() { return weight; } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/SCM.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/SCM.java index f473948d..1c927270 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/scms/SCM.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/SCM.java @@ -1,12 +1,12 @@ package hudson.plugins.scm_sync_configuration.scms; import hudson.model.Descriptor; -import hudson.model.Hudson; -import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; +import jenkins.model.Jenkins; + import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.maven.scm.manager.NoSuchScmProviderException; import org.apache.maven.scm.manager.ScmManager; @@ -17,64 +17,67 @@ import org.codehaus.plexus.util.StringUtils; import org.kohsuke.stapler.StaplerRequest; +import com.google.common.collect.ImmutableList; + public abstract class SCM { - - protected static final Logger LOGGER = Logger.getLogger(SCM.class.getName()); - - protected static final List SCM_IMPLEMENTATIONS = new ArrayList(){ { - add(new ScmSyncNoSCM()); - add(new ScmSyncSubversionSCM()); - add(new ScmSyncGitSCM()); - } }; - - transient protected String title; - transient protected String scmClassName; - transient protected String configPage; - transient protected String repositoryUrlHelpPath; - - protected SCM(String _title, String _configPage, String _scmClassName, String _repositoryUrlHelpPath){ - this.title = _title; - this.configPage = _configPage; - this.scmClassName = _scmClassName; - this.repositoryUrlHelpPath = _repositoryUrlHelpPath; - } - - public String getTitle(){ - return this.title; - } - - public String getConfigPage(){ - return this.configPage; - } - - public String getSCMClassName() { - return this.scmClassName; - } - - public Descriptor getSCMDescriptor(){ - return Hudson.getInstance().getDescriptorByName(getSCMClassName()); - } - - public String getRepositoryUrlHelpPath() { - return this.repositoryUrlHelpPath; - } - - public ScmRepository getConfiguredRepository(ScmManager scmManager, String scmRepositoryURL) { - SCMCredentialConfiguration credentials = extractScmCredentials( extractScmUrlFrom(scmRepositoryURL) ); - - LOGGER.info("Creating SCM repository object for url : "+scmRepositoryURL); + + protected static final Logger LOGGER = Logger.getLogger(SCM.class.getName()); + + protected static final List SCM_IMPLEMENTATIONS = ImmutableList.of( + new ScmSyncNoSCM(), + new ScmSyncSubversionSCM(), + new ScmSyncGitSCM() + ); + + transient protected String title; + transient protected String scmClassName; + transient protected String configPage; + transient protected String repositoryUrlHelpPath; + + protected SCM(String _title, String _configPage, String _scmClassName, String _repositoryUrlHelpPath){ + this.title = _title; + this.configPage = _configPage; + this.scmClassName = _scmClassName; + this.repositoryUrlHelpPath = _repositoryUrlHelpPath; + } + + public String getTitle(){ + return this.title; + } + + public String getConfigPage(){ + return this.configPage; + } + + public String getSCMClassName() { + return this.scmClassName; + } + + @SuppressWarnings("unchecked") + public Descriptor getSCMDescriptor(){ + return Jenkins.getInstance().getDescriptorByName(getSCMClassName()); + } + + public String getRepositoryUrlHelpPath() { + return this.repositoryUrlHelpPath; + } + + public ScmRepository getConfiguredRepository(ScmManager scmManager, String scmRepositoryURL) { + SCMCredentialConfiguration credentials = extractScmCredentials( extractScmUrlFrom(scmRepositoryURL) ); + + LOGGER.info("Creating SCM repository object for url : "+scmRepositoryURL); ScmRepository repository = null; try { - repository = scmManager.makeScmRepository( scmRepositoryURL ); - } catch (ScmRepositoryException e) { - LOGGER.throwing(ScmManager.class.getName(), "makeScmRepository", e); - LOGGER.severe("Error creating ScmRepository : "+e.getMessage()); - } catch (NoSuchScmProviderException e) { - LOGGER.throwing(ScmManager.class.getName(), "makeScmRepository", e); - LOGGER.severe("Error creating ScmRepository : "+e.getMessage()); - } + repository = scmManager.makeScmRepository( scmRepositoryURL ); + } catch (ScmRepositoryException e) { + LOGGER.throwing(ScmManager.class.getName(), "makeScmRepository", e); + LOGGER.severe("Error creating ScmRepository : "+e.getMessage()); + } catch (NoSuchScmProviderException e) { + LOGGER.throwing(ScmManager.class.getName(), "makeScmRepository", e); + LOGGER.severe("Error creating ScmRepository : "+e.getMessage()); + } if(repository == null){ - return null; + return null; } ScmProviderRepository scmRepo = repository.getProviderRepository(); @@ -83,81 +86,83 @@ public ScmRepository getConfiguredRepository(ScmManager scmManager, String scmRe //scmRepo.setPersistCheckout( false ); // TODO: instead of creating a SCMCredentialConfiguration, create a ScmProviderRepository - if ( repository.getProviderRepository() instanceof ScmProviderRepositoryWithHost ) - { - LOGGER.info("Populating host data into SCM repository object ..."); - ScmProviderRepositoryWithHost repositoryWithHost = - (ScmProviderRepositoryWithHost) repository.getProviderRepository(); - String host = repositoryWithHost.getHost(); + // XXX: host & port are unused afterwards? + // if ( repository.getProviderRepository() instanceof ScmProviderRepositoryWithHost ) + // { + // LOGGER.info("Populating host data into SCM repository object ..."); + // ScmProviderRepositoryWithHost repositoryWithHost = + // (ScmProviderRepositoryWithHost) repository.getProviderRepository(); + // String host = repositoryWithHost.getHost(); + // + // int port = repositoryWithHost.getPort(); + // + // if ( port > 0 ) + // { + // host += ":" + port; + // } + // } - int port = repositoryWithHost.getPort(); + if(credentials != null){ + LOGGER.info("Populating credentials data into SCM repository object ..."); + if ( !StringUtils.isEmpty( credentials.getUsername() ) ) + { + scmRepo.setUser( credentials.getUsername() ); + } + if ( !StringUtils.isEmpty( credentials.getPassword() ) ) + { + scmRepo.setPassword( credentials.getPassword() ); + } - if ( port > 0 ) + if ( scmRepo instanceof ScmProviderRepositoryWithHost ) { - host += ":" + port; + ScmProviderRepositoryWithHost repositoryWithHost = (ScmProviderRepositoryWithHost) scmRepo; + if ( !StringUtils.isEmpty( credentials.getPrivateKey() ) ) + { + repositoryWithHost.setPrivateKey( credentials.getPrivateKey() ); + } + + if ( !StringUtils.isEmpty( credentials.getPassphrase() ) ) + { + repositoryWithHost.setPassphrase( credentials.getPassphrase() ); + } } } - if(credentials != null){ - LOGGER.info("Populating credentials data into SCM repository object ..."); - if ( !StringUtils.isEmpty( credentials.getUsername() ) ) - { - scmRepo.setUser( credentials.getUsername() ); - } - if ( !StringUtils.isEmpty( credentials.getPassword() ) ) - { - scmRepo.setPassword( credentials.getPassword() ); - } - - if ( scmRepo instanceof ScmProviderRepositoryWithHost ) - { - ScmProviderRepositoryWithHost repositoryWithHost = (ScmProviderRepositoryWithHost) scmRepo; - if ( !StringUtils.isEmpty( credentials.getPrivateKey() ) ) - { - repositoryWithHost.setPrivateKey( credentials.getPrivateKey() ); - } - - if ( !StringUtils.isEmpty( credentials.getPassphrase() ) ) - { - repositoryWithHost.setPassphrase( credentials.getPassphrase() ); - } - } - } - return repository; - } - - public abstract String createScmUrlFromRequest(StaplerRequest req); - public abstract String extractScmUrlFrom(String scmUrl); - public abstract SCMCredentialConfiguration extractScmCredentials(String scmRepositoryURL); - - public static SCM valueOf(Class clazz){ - return valueOf(getId(clazz)); - } - - public static SCM valueOf(String scmId){ - for(SCM scm : SCM_IMPLEMENTATIONS){ - if(scmId.equals(scm.getId())){ - return scm; - } - } - return null; - } - - public static SCM[] values(){ - return SCM_IMPLEMENTATIONS.toArray(new SCM[0]); - } - + } + + public abstract String createScmUrlFromRequest(StaplerRequest req); + public abstract String extractScmUrlFrom(String scmUrl); + public abstract SCMCredentialConfiguration extractScmCredentials(String scmRepositoryURL); + + public static SCM valueOf(Class clazz){ + return valueOf(getId(clazz)); + } + + public static SCM valueOf(String scmId){ + for(SCM scm : SCM_IMPLEMENTATIONS){ + if(scmId.equals(scm.getId())){ + return scm; + } + } + return null; + } + + public static SCM[] values(){ + return SCM_IMPLEMENTATIONS.toArray(new SCM[0]); + } + + @Override public String toString(){ return new ToStringBuilder(this).append("class", getClass().getName()).append("title", title).append("scmClassName", scmClassName) .append("configPage", configPage).append("repositoryUrlHelpPath", repositoryUrlHelpPath).toString(); } - + private static String getId(Class clazz){ - return clazz.getName(); + return clazz.getName(); } - + public String getId(){ - return getId(getClass()); + return getId(getClass()); } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/ScmSyncGitSCM.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/ScmSyncGitSCM.java index 1352fe24..ff2354ed 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/scms/ScmSyncGitSCM.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/ScmSyncGitSCM.java @@ -4,28 +4,30 @@ public class ScmSyncGitSCM extends SCM { - private static final String SCM_URL_PREFIX="scm:git:"; - - ScmSyncGitSCM(){ - super("Git", "git/config.jelly", "hudson.plugins.git.GitSCM", "/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help.jelly"); - } - - public String createScmUrlFromRequest(StaplerRequest req) { - String repoURL = req.getParameter("gitRepositoryUrl"); - if(repoURL == null){ - return null; - } - else { - return SCM_URL_PREFIX+repoURL; - } - } - - public String extractScmUrlFrom(String scmUrl) { - return scmUrl.substring(SCM_URL_PREFIX.length()); - } - - public SCMCredentialConfiguration extractScmCredentials(String scmUrl) { - return null; - } - + private static final String SCM_URL_PREFIX="scm:git:"; + + ScmSyncGitSCM(){ + super("Git", "git/config.jelly", "hudson.plugins.git.GitSCM", "/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help.jelly"); + } + + @Override + public String createScmUrlFromRequest(StaplerRequest req) { + String repoURL = req.getParameter("gitRepositoryUrl"); + if (repoURL == null) { + return null; + } else { + return SCM_URL_PREFIX + repoURL.trim(); + } + } + + @Override + public String extractScmUrlFrom(String scmUrl) { + return scmUrl.substring(SCM_URL_PREFIX.length()); + } + + @Override + public SCMCredentialConfiguration extractScmCredentials(String scmUrl) { + return null; + } + } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/ScmSyncSubversionSCM.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/ScmSyncSubversionSCM.java index 6048132f..7eae1a61 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/scms/ScmSyncSubversionSCM.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/ScmSyncSubversionSCM.java @@ -24,121 +24,130 @@ public class ScmSyncSubversionSCM extends SCM { - private static final String SCM_URL_PREFIX="scm:svn:"; - - ScmSyncSubversionSCM(){ - super("Subversion", "svn/config.jelly", "hudson.scm.SubversionSCM", "/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help.jelly"); - } - - public String createScmUrlFromRequest(StaplerRequest req) { - String repoURL = req.getParameter("repositoryUrl"); - if(repoURL == null){ return null; } - else { return SCM_URL_PREFIX+repoURL; } - } - public String extractScmUrlFrom(String scmUrl) { - return scmUrl.substring(SCM_URL_PREFIX.length()); - } - public SCMCredentialConfiguration extractScmCredentials(String scmUrl) { - LOGGER.info("Extracting SVN Credentials for url : "+scmUrl); - String realm = retrieveRealmFor(scmUrl); - if(realm != null){ - LOGGER.fine("Extracted realm from "+scmUrl+" is ["+realm+"]"); - SubversionSCM.DescriptorImpl subversionDescriptor = (SubversionSCM.DescriptorImpl)getSCMDescriptor(); - try { - Field credentialField = SubversionSCM.DescriptorImpl.class.getDeclaredField("credentials"); - credentialField.setAccessible(true); - Map credentials = (Map)credentialField.get(subversionDescriptor); - Credential cred = credentials.get(realm); - if(cred == null){ - LOGGER.severe("No credentials are stored in Hudson for realm ["+realm+"] !"); - return null; - } - String kind = ISVNAuthenticationManager.PASSWORD; - if(scmUrl.startsWith("svn+ssh")){ - kind = ISVNAuthenticationManager.SSH; - } - return createSCMCredentialConfiguration(cred.createSVNAuthentication(kind)); - } catch (SecurityException e) { - LOGGER.log(Level.SEVERE, "'credentials' field not readable on SubversionSCM.DescriptorImpl !"); - } catch (NoSuchFieldException e) { - LOGGER.log(Level.SEVERE, "'credentials' field not readable on SubversionSCM.DescriptorImpl !"); - } catch (IllegalArgumentException e) { - LOGGER.log(Level.SEVERE, "'credentials' field not accessible on "+String.valueOf(subversionDescriptor)+" !"); - } catch (IllegalAccessException e) { - LOGGER.log(Level.SEVERE, "'credentials' field not accessible on "+String.valueOf(subversionDescriptor)+" !"); - } catch (SVNException e) { - LOGGER.log(Level.WARNING, "Error creating SVN authentication from realm ["+realm+"] !", e); - } - } else { - LOGGER.warning("No credential (realm) found for url ["+scmUrl+"] : it seems that you should enter your credentials in the UI at " - +"this url"); - } - return null; - } - - private String retrieveRealmFor(String scmURL) { - final String[] realms = new String[]{ null }; - + private static final String SCM_URL_PREFIX="scm:svn:"; + + ScmSyncSubversionSCM(){ + super("Subversion", "svn/config.jelly", "hudson.scm.SubversionSCM", "/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help.jelly"); + } + + @Override + public String createScmUrlFromRequest(StaplerRequest req) { + String repoURL = req.getParameter("repositoryUrl"); + if (repoURL == null) { + return null; + } else { + return SCM_URL_PREFIX + repoURL.trim(); + } + } + + @Override + public String extractScmUrlFrom(String scmUrl) { + return scmUrl.substring(SCM_URL_PREFIX.length()); + } + + @Override + public SCMCredentialConfiguration extractScmCredentials(String scmUrl) { + LOGGER.info("Extracting SVN Credentials for url : "+scmUrl); + String realm = retrieveRealmFor(scmUrl); + if(realm != null){ + LOGGER.fine("Extracted realm from "+scmUrl+" is ["+realm+"]"); + SubversionSCM.DescriptorImpl subversionDescriptor = (SubversionSCM.DescriptorImpl)getSCMDescriptor(); + try { + Field credentialField = SubversionSCM.DescriptorImpl.class.getDeclaredField("credentials"); + credentialField.setAccessible(true); + @SuppressWarnings("unchecked") + Map credentials = (Map)credentialField.get(subversionDescriptor); + Credential cred = credentials.get(realm); + if(cred == null){ + LOGGER.severe("No credentials are stored in Hudson for realm ["+realm+"] !"); + return null; + } + String kind = ISVNAuthenticationManager.PASSWORD; + if(scmUrl.startsWith("svn+ssh")){ + kind = ISVNAuthenticationManager.SSH; + } + return createSCMCredentialConfiguration(cred.createSVNAuthentication(kind)); + } catch (SecurityException e) { + LOGGER.log(Level.SEVERE, "'credentials' field not readable on SubversionSCM.DescriptorImpl !"); + } catch (NoSuchFieldException e) { + LOGGER.log(Level.SEVERE, "'credentials' field not readable on SubversionSCM.DescriptorImpl !"); + } catch (IllegalArgumentException e) { + LOGGER.log(Level.SEVERE, "'credentials' field not accessible on "+String.valueOf(subversionDescriptor)+" !"); + } catch (IllegalAccessException e) { + LOGGER.log(Level.SEVERE, "'credentials' field not accessible on "+String.valueOf(subversionDescriptor)+" !"); + } catch (SVNException e) { + LOGGER.log(Level.WARNING, "Error creating SVN authentication from realm ["+realm+"] !", e); + } + } else { + LOGGER.warning("No credential (realm) found for url ["+scmUrl+"] : it seems that you should enter your credentials in the UI at " + +"this url"); + } + return null; + } + + private String retrieveRealmFor(String scmURL) { + final String[] realms = new String[]{ null }; + SVNRepository repository; try { - repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(scmURL)); + repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(scmURL)); repository.setTunnelProvider(SVNWCUtil.createDefaultOptions(true)); }catch(SVNException e){ - LOGGER.throwing(SVNRepositoryFactory.class.getName(), "create", e); - LOGGER.severe("Error while creating SVNRepository : "+e.getMessage()); - return null; + LOGGER.throwing(SVNRepositoryFactory.class.getName(), "create", e); + LOGGER.severe("Error while creating SVNRepository : "+e.getMessage()); + return null; } - try { + try { repository.setAuthenticationManager(new DefaultSVNAuthenticationManager(SVNWCUtil.getDefaultConfigurationDirectory(), true, "", "", null, "") { @Override public SVNAuthentication getFirstAuthentication(String kind, String realm, SVNURL url) throws SVNException { - realms[0] = realm; - return super.getFirstAuthentication(kind, realm, url); + realms[0] = realm; + return super.getFirstAuthentication(kind, realm, url); } @Override public SVNAuthentication getNextAuthentication(String kind, String realm, SVNURL url) throws SVNException { - realms[0] = realm; - return super.getNextAuthentication(kind, realm, url); + realms[0] = realm; + return super.getNextAuthentication(kind, realm, url); } @Override public void acknowledgeAuthentication(boolean accepted, String kind, String realm, SVNErrorMessage errorMessage, SVNAuthentication authentication) throws SVNException { - realms[0] = realm; - super.acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication); + realms[0] = realm; + super.acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication); } }); repository.testConnection(); - - } catch (SVNException e) { - // If a problem happens, don't do anything, it implies realm doesn't exist in current cache - } - - return realms[0]; - } - - /** - * Ugly method to convert a SVN authentication into a SCMCredentialConfiguration - */ - public SCMCredentialConfiguration createSCMCredentialConfiguration(SVNAuthentication auth){ + + } catch (SVNException e) { + // If a problem happens, don't do anything, it implies realm doesn't exist in current cache + } + + return realms[0]; + } + + /** + * Ugly method to convert a SVN authentication into a SCMCredentialConfiguration + */ + public SCMCredentialConfiguration createSCMCredentialConfiguration(SVNAuthentication auth){ SCMCredentialConfiguration credentials = null; - if(auth instanceof SVNPasswordAuthentication){ - SVNPasswordAuthentication passAuth = (SVNPasswordAuthentication)auth; - credentials = new SCMCredentialConfiguration(passAuth.getUserName(), passAuth.getPassword()); - } else if(auth instanceof SVNSSHAuthentication){ - SVNSSHAuthentication sshAuth = (SVNSSHAuthentication)auth; - credentials = new SCMCredentialConfiguration(sshAuth.getUserName(), sshAuth.getPassword(), sshAuth.getPassphrase(), sshAuth.getPrivateKey()); - } else if(auth instanceof SVNSSLAuthentication){ - SVNSSLAuthentication sslAuth = (SVNSSLAuthentication)auth; - credentials = new SCMCredentialConfiguration(sslAuth.getUserName(), sslAuth.getPassword()); - } else if(auth instanceof SVNUserNameAuthentication){ - SVNUserNameAuthentication unameAuth = (SVNUserNameAuthentication)auth; - credentials = new SCMCredentialConfiguration(unameAuth.getUserName()); - } + if(auth instanceof SVNPasswordAuthentication){ + SVNPasswordAuthentication passAuth = (SVNPasswordAuthentication)auth; + credentials = new SCMCredentialConfiguration(passAuth.getUserName(), passAuth.getPassword()); + } else if(auth instanceof SVNSSHAuthentication){ + SVNSSHAuthentication sshAuth = (SVNSSHAuthentication)auth; + credentials = new SCMCredentialConfiguration(sshAuth.getUserName(), sshAuth.getPassword(), sshAuth.getPassphrase(), sshAuth.getPrivateKey()); + } else if(auth instanceof SVNSSLAuthentication){ + SVNSSLAuthentication sslAuth = (SVNSSLAuthentication)auth; + credentials = new SCMCredentialConfiguration(sslAuth.getUserName(), sslAuth.getPassword()); + } else if(auth instanceof SVNUserNameAuthentication){ + SVNUserNameAuthentication unameAuth = (SVNUserNameAuthentication)auth; + credentials = new SCMCredentialConfiguration(unameAuth.getUserName()); + } if(credentials != null){ LOGGER.info("Created SCM Credentials for user "+credentials.getUsername()+"..."); } - return credentials; - } + return credentials; + } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/FixedGitStatusConsumer.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/FixedGitStatusConsumer.java new file mode 100644 index 00000000..54fd8dee --- /dev/null +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/FixedGitStatusConsumer.java @@ -0,0 +1,205 @@ +package hudson.plugins.scm_sync_configuration.scms.customproviders.git.gitexe; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; +import org.apache.maven.scm.ScmFile; +import org.apache.maven.scm.ScmFileStatus; +import org.apache.maven.scm.log.ScmLogger; +import org.codehaus.plexus.util.cli.StreamConsumer; + +import com.google.common.base.Strings; + +/** + * Copied from org.apache.maven.scm.provider.git.gitexe.command.status.GitStatusConsumer (maven-scm 1.9.1) + * and fixed to account for https://issues.apache.org/jira/browse/SCM-772 . + */ +public class FixedGitStatusConsumer +implements StreamConsumer +{ + + /** + * The pattern used to match added file lines + */ + private static final Pattern ADDED_PATTERN = Pattern.compile( "^A[ M]* (.*)$" ); + + /** + * The pattern used to match modified file lines + */ + private static final Pattern MODIFIED_PATTERN = Pattern.compile( "^ *M[ M]* (.*)$" ); + + /** + * The pattern used to match deleted file lines + */ + private static final Pattern DELETED_PATTERN = Pattern.compile( "^ *D * (.*)$" ); + + /** + * The pattern used to match renamed file lines + */ + private static final Pattern RENAMED_PATTERN = Pattern.compile( "^R (.*) -> (.*)$" ); + + private final ScmLogger logger; + + private final File workingDirectory; + + /** + * Entries are relative to working directory, not to the repositoryroot + */ + private final List changedFiles = new ArrayList(); + + private String relativeRepositoryPath; + + private final File repositoryRoot; + + // ---------------------------------------------------------------------- + // + // ---------------------------------------------------------------------- + + public FixedGitStatusConsumer (ScmLogger logger, File workingDirectory, File repositoryRoot) { + this.logger = logger; + this.workingDirectory = workingDirectory; + if (repositoryRoot != null) { + String absoluteRepositoryRoot = repositoryRoot.getAbsolutePath(); // Make sure all separators are File.separator + // The revparse runs with fileset.getBasedir(). That of course must be under the repo root. + String basePath = workingDirectory.getAbsolutePath(); + if (!absoluteRepositoryRoot.endsWith(File.separator)) { + absoluteRepositoryRoot += File.separator; + } + if (basePath.startsWith(absoluteRepositoryRoot)) { + String pathInsideRepo = basePath.substring(absoluteRepositoryRoot.length()); + if (!Strings.isNullOrEmpty(pathInsideRepo)) { + relativeRepositoryPath = pathInsideRepo; + } + } + } + this.repositoryRoot = repositoryRoot; + // Either the workingDirectory == repositoryRoot: we have no relativeRepositoryPath set + // Or the working directory was a subdirectory (in the workspace!) of repositoryRoot, then + // relativeRepositoryPath contains now the relative path to the working directory. + // + // It would appear that git status --porcelain always returns paths relative to the repository + // root. + } + + // ---------------------------------------------------------------------- + // StreamConsumer Implementation + // ---------------------------------------------------------------------- + + /** + * {@inheritDoc} + */ + @Override + public void consumeLine( String line ) + { + if ( logger.isDebugEnabled() ) + { + logger.debug( line ); + } + if ( StringUtils.isEmpty( line ) ) + { + return; + } + + ScmFileStatus status = null; + + List files = new ArrayList(); + + Matcher matcher; + if ( ( matcher = ADDED_PATTERN.matcher( line ) ).find() ) + { + status = ScmFileStatus.ADDED; + files.add(ScmSyncGitUtils.dequote(matcher.group(1))); + } + else if ( ( matcher = MODIFIED_PATTERN.matcher( line ) ).find() ) + { + status = ScmFileStatus.MODIFIED; + files.add(ScmSyncGitUtils.dequote(matcher.group(1))); + } + else if ( ( matcher = DELETED_PATTERN.matcher( line ) ) .find() ) + { + status = ScmFileStatus.DELETED; + files.add(ScmSyncGitUtils.dequote(matcher.group(1))); + } + else if ( ( matcher = RENAMED_PATTERN.matcher( line ) ).find() ) + { + status = ScmFileStatus.RENAMED; + files.add(ScmSyncGitUtils.dequote(matcher.group(1))); + files.add(ScmSyncGitUtils.dequote(matcher.group(2))); + } + else + { + logger.warn( "Ignoring unrecognized line: " + line ); + return; + } + + // If the file isn't a file; don't add it. + if (files.isEmpty() || status == null) { + return; + } + File checkDir = repositoryRoot; + if (workingDirectory != null && relativeRepositoryPath != null) { + // Make all paths relative to this directory. + List relativeNames = new ArrayList(); + for (String repoRelativeName : files) { + relativeNames.add(ScmSyncGitUtils.relativizePath(relativeRepositoryPath, new File(repoRelativeName).getPath())); + } + files = relativeNames; + checkDir = workingDirectory; + } + // Now check them all against the checkDir. This check has been taken over from the base implementation + // in maven-scm's GitStatusConsumer, but I'm not really sure this makes sense. Who said the workspace + // had to be equal to the git index (staging area) here? + if (status == ScmFileStatus.RENAMED) { + String oldFilePath = files.get( 0 ); + String newFilePath = files.get( 1 ); + if (new File(checkDir, oldFilePath).isFile()) { + logger.debug("file '" + oldFilePath + "' still exists after rename"); + return; + } + if (!new File(checkDir, newFilePath).isFile()) { + logger.debug("file '" + newFilePath + "' does not exist after rename"); + return; + } + } else if (status == ScmFileStatus.DELETED) { + if (new File(checkDir, files.get(0)).isFile()) { + return; + } + } else { + if (!new File(checkDir, files.get(0)).isFile()) { + return; + } + } + + for (String file : files) { + changedFiles.add(new ScmFile(file.replace(File.separatorChar, '/'), status)); + } + } + + public List getChangedFiles() + { + return changedFiles; + } +} diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitAddCommand.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitAddCommand.java new file mode 100644 index 00000000..897f4b68 --- /dev/null +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitAddCommand.java @@ -0,0 +1,118 @@ +package hudson.plugins.scm_sync_configuration.scms.customproviders.git.gitexe; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFile; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.command.add.AddScmResult; +import org.apache.maven.scm.command.status.StatusScmResult; +import org.apache.maven.scm.provider.ScmProviderRepository; +import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils; +import org.apache.maven.scm.provider.git.gitexe.command.add.GitAddCommand; +import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository; +import org.codehaus.plexus.util.Os; +import org.codehaus.plexus.util.cli.CommandLineUtils; +import org.codehaus.plexus.util.cli.Commandline; + +public class ScmSyncGitAddCommand extends GitAddCommand { + + @Override + protected AddScmResult executeAddCommand(ScmProviderRepository repo, ScmFileSet fileSet, String message, boolean binary) + throws ScmException { + GitScmProviderRepository repository = (GitScmProviderRepository) repo; + + if (fileSet.getFileList().isEmpty()) { + throw new ScmException("You must provide at least one file/directory to add"); + } + + AddScmResult result = executeAddFileSet(fileSet); + + if (result != null) { + return result; + } + + ScmSyncGitStatusCommand statusCommand = new ScmSyncGitStatusCommand(); + statusCommand.setLogger(getLogger()); + StatusScmResult status = statusCommand.executeStatusCommand(repository, fileSet); + getLogger().warn("add - status - " + status.isSuccess()); + for (ScmFile s : status.getChangedFiles()) { + getLogger().warn("added " + s.getPath()); + } + List changedFiles = new ArrayList(); + + if (fileSet.getFileList().isEmpty()) { + changedFiles = status.getChangedFiles(); + } else { + for (ScmFile scmfile : status.getChangedFiles()) { + // if a specific fileSet is given, we have to check if the file is really tracked + for (File f : fileSet.getFileList()) { + if (FilenameUtils.separatorsToUnix(f.getPath()).equals(scmfile.getPath())) { + changedFiles.add(scmfile); + } + } + } + } + Commandline cl = createCommandLine(fileSet.getBasedir(), fileSet.getFileList()); + return new AddScmResult(cl.toString(), changedFiles); + } + + public static Commandline createCommandLine(File workingDirectory, List files) throws ScmException { + Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(workingDirectory, "add"); + + // use this separator to make clear that the following parameters are files and not revision info. + cl.createArg().setValue("--"); + + ScmSyncGitUtils.addTarget(cl, files); + + return cl; + } + + protected AddScmResult executeAddFileSet(ScmFileSet fileSet) throws ScmException { + File workingDirectory = fileSet.getBasedir(); + List files = fileSet.getFileList(); + + // command line can be too long for windows so add files individually (see SCM-697) + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + for (File file : files) { + AddScmResult result = executeAddFiles(workingDirectory, Collections.singletonList(file)); + if (result != null) { + return result; + } + } + } else { + AddScmResult result = executeAddFiles(workingDirectory, files); + if (result != null) { + return result; + } + } + + return null; + } + + private AddScmResult executeAddFiles(File workingDirectory, List files) throws ScmException { + Commandline cl = createCommandLine(workingDirectory, files); + + CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); + CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer(); + + int exitCode = -1; + try { + exitCode = GitCommandLineUtils.execute(cl, stdout, stderr, getLogger()); + } catch (Throwable t) { + getLogger().error("Failed:", t); + } + if (exitCode != 0) { + String msg = stderr.getOutput(); + getLogger().info("Add failed:" + msg); + return new AddScmResult(cl.toString(), "The git-add command failed.", msg, false); + } + + return null; + } + +} diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitCheckInCommand.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitCheckInCommand.java new file mode 100644 index 00000000..2bf64e36 --- /dev/null +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitCheckInCommand.java @@ -0,0 +1,133 @@ +package hudson.plugins.scm_sync_configuration.scms.customproviders.git.gitexe; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFile; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.ScmFileStatus; +import org.apache.maven.scm.ScmVersion; +import org.apache.maven.scm.command.add.AddScmResult; +import org.apache.maven.scm.command.checkin.CheckInScmResult; +import org.apache.maven.scm.command.status.StatusScmResult; +import org.apache.maven.scm.log.ScmLogger; +import org.apache.maven.scm.provider.ScmProviderRepository; +import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils; +import org.apache.maven.scm.provider.git.gitexe.command.branch.GitBranchCommand; +import org.apache.maven.scm.provider.git.gitexe.command.checkin.GitCheckInCommand; +import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository; +import org.codehaus.plexus.util.FileUtils; +import org.codehaus.plexus.util.cli.CommandLineUtils; +import org.codehaus.plexus.util.cli.Commandline; + +/** + * @author fcamblor Crappy hack because for the moment, in maven-scmprovider-gitext 1.8.1, when we checkIn file, checkin + * is originally made by passing pushUrl instead of a local reference (such as "origin") Problem is passing + * pushUrl doesn't update current local reference once pushed => after push, origin/ will not be + * updated to latest commit => on next push, there will be an error saying some pull is needed. This workaround + * could be betterly handled when something like "checkinAndFetch" could be implemented generically in + * maven-scm-api (see http://maven.40175.n5.nabble.com/SCM-GitExe-no-fetch-after-push-td5745064.html) + * + * @author Tom Rewritten executeCheckInCommand to account for SCM-772 and SCM-695. Make use of also + * fixed GitAddCommand instead of re-inventing the wheel once more again. + */ +public class ScmSyncGitCheckInCommand extends GitCheckInCommand { + // Retrieved implementation from GitCheckInCommande v1.8.1, only overriding call to createPushCommandLine() + // by a *custom* implementation + @Override + protected CheckInScmResult executeCheckInCommand(ScmProviderRepository repo, ScmFileSet fileSet, String message, + ScmVersion version) + throws ScmException { + GitScmProviderRepository repository = (GitScmProviderRepository) repo; + + CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); + CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer(); + + File messageFile = FileUtils.createTempFile("maven-scm-", ".commit", null); + try { + FileUtils.fileWrite(messageFile.getAbsolutePath(), message); + } catch (IOException ex) { + return new CheckInScmResult(null, "Error while making a temporary file for the commit message: " + + ex.getMessage(), null, false); + } + + try { + // if specific fileSet is given, we have to git-add them first + // otherwise we will use 'git-commit -a' later + if (!fileSet.getFileList().isEmpty()) { + ScmSyncGitAddCommand addCommand = new ScmSyncGitAddCommand(); + addCommand.setLogger(getLogger()); + AddScmResult addResult = addCommand.executeAddFileSet(fileSet); + if (addResult != null && !addResult.isSuccess()) { + return new CheckInScmResult(new ArrayList(), addResult); + } + } + // Must run status here. There might have been earlier additions!! + ScmSyncGitStatusCommand statusCommand = new ScmSyncGitStatusCommand(); + statusCommand.setLogger(getLogger()); + StatusScmResult status = statusCommand.executeStatusCommand(repository, + new ScmFileSet(fileSet.getBasedir())); + List changedFiles = null; + if (status.isSuccess()) { + changedFiles = status.getChangedFiles(); + if (changedFiles.isEmpty()) { + return new CheckInScmResult(null, changedFiles); + } + } else { + return new CheckInScmResult(new ArrayList(), status); + } + Commandline clCommit = createCommitCommandLine(repository, fileSet, messageFile); + int exitCode = GitCommandLineUtils.execute(clCommit, stdout, stderr, getLogger()); + if (exitCode != 0) { + String msg = stderr.getOutput(); + return new CheckInScmResult(clCommit.toString(), "The git-commit command failed.", msg, false); + } + if (repo.isPushChanges()) { + Commandline cl = createSpecificPushCommandLine(getLogger(), repository, fileSet, version); + exitCode = GitCommandLineUtils.execute(cl, stdout, stderr, getLogger()); + if (exitCode != 0) { + String msg = stderr.getOutput(); + return new CheckInScmResult(cl.toString(), "The git-push command failed.", msg, false); + } + } + + List checkedInFiles = new ArrayList(changedFiles.size()); + + // rewrite all detected files to now have status 'checked_in' + for (ScmFile changedFile : changedFiles) { + checkedInFiles.add(new ScmFile(changedFile.getPath(), ScmFileStatus.CHECKED_IN)); + } + + return new CheckInScmResult(clCommit.toString(), checkedInFiles); + } finally { + try { + FileUtils.forceDelete(messageFile); + } catch (IOException ex) { + // ignore + } + } + } + + public static Commandline createSpecificPushCommandLine(ScmLogger logger, GitScmProviderRepository repository, ScmFileSet fileSet, ScmVersion version) + throws ScmException { + Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(fileSet.getBasedir(), "push"); + + String branch = GitBranchCommand.getCurrentBranch(logger, repository, fileSet); + + if (branch == null || branch.length() == 0) { + throw new ScmException("Could not detect the current branch. Don't know where I should push to!"); + } + + // Overloaded branch name here : if repository.getUrl() is kept, during checkin(), current *local* branch + // reference is not updated, whereas by using origin, it will be done ! + cl.createArg().setValue("origin"); + + cl.createArg().setValue(branch + ":" + branch); + + return cl; + } + +} \ No newline at end of file diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitExeScmProvider.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitExeScmProvider.java index 49e01232..83011fd6 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitExeScmProvider.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitExeScmProvider.java @@ -1,198 +1,38 @@ package hudson.plugins.scm_sync_configuration.scms.customproviders.git.gitexe; -import org.apache.commons.io.FilenameUtils; -import org.apache.maven.scm.*; -import org.apache.maven.scm.command.checkin.CheckInScmResult; -import org.apache.maven.scm.log.ScmLogger; -import org.apache.maven.scm.provider.ScmProviderRepository; import org.apache.maven.scm.provider.git.command.GitCommand; import org.apache.maven.scm.provider.git.gitexe.GitExeScmProvider; -import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils; -import org.apache.maven.scm.provider.git.gitexe.command.add.GitAddCommand; -import org.apache.maven.scm.provider.git.gitexe.command.branch.GitBranchCommand; -import org.apache.maven.scm.provider.git.gitexe.command.checkin.GitCheckInCommand; -import org.apache.maven.scm.provider.git.gitexe.command.status.GitStatusCommand; -import org.apache.maven.scm.provider.git.gitexe.command.status.GitStatusConsumer; -import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository; -import org.codehaus.plexus.util.FileUtils; -import org.codehaus.plexus.util.cli.CommandLineUtils; -import org.codehaus.plexus.util.cli.Commandline; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; /** - * @author fcamblor - * Crappy hack because for the moment, in maven-scmprovider-gitext 1.8.1, when we checkIn file, - * checkin is originally made by passing pushUrl instead of a local reference (such as "origin") - * Problem is passing pushUrl doesn't update current local reference once pushed => after push, - * origin/ will not be updated to latest commit => on next push, there will be an - * error saying some pull is needed. - * This workaround could be betterly handled when something like "checkinAndFetch" could be - * implemented generically in maven-scm-api - * (see http://maven.40175.n5.nabble.com/SCM-GitExe-no-fetch-after-push-td5745064.html) + * Try to fix those very broken maven scm git commands. We should really move to using the git-client plugin. */ public class ScmSyncGitExeScmProvider extends GitExeScmProvider { - public static class ScmSyncGitCheckInCommand extends GitCheckInCommand { - // Retrieved implementation from GitCheckInCommande v1.8.1, only overriding call to createPushCommandLine() - // by a *custom* implementation - @Override - protected CheckInScmResult executeCheckInCommand( - ScmProviderRepository repo, ScmFileSet fileSet, String message, ScmVersion version ) throws ScmException { - - GitScmProviderRepository repository = (GitScmProviderRepository) repo; - - CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); - CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer(); - - int exitCode; - - File messageFile = FileUtils.createTempFile("maven-scm-", ".commit", null); - try - { - FileUtils.fileWrite( messageFile.getAbsolutePath(), message ); - } - catch ( IOException ex ) - { - return new CheckInScmResult( null, "Error while making a temporary file for the commit message: " - + ex.getMessage(), null, false ); - } - - try - { - if ( !fileSet.getFileList().isEmpty() ) - { - // if specific fileSet is given, we have to git-add them first - // otherwise we will use 'git-commit -a' later - - Commandline clAdd = GitAddCommand.createCommandLine(fileSet.getBasedir(), fileSet.getFileList()); - - exitCode = GitCommandLineUtils.execute(clAdd, stdout, stderr, getLogger()); - - if ( exitCode != 0 ) - { - return new CheckInScmResult( clAdd.toString(), "The git-add command failed.", stderr.getOutput(), - false ); - } - - } - - // git-commit doesn't show single files, but only summary :/ - // so we must run git-status and consume the output - // borrow a few things from the git-status command - Commandline clStatus = GitStatusCommand.createCommandLine(repository, fileSet); - - GitStatusConsumer statusConsumer = new GitStatusConsumer( getLogger(), fileSet.getBasedir() ); - exitCode = GitCommandLineUtils.execute( clStatus, statusConsumer, stderr, getLogger() ); - if ( exitCode != 0 ) - { - // git-status returns non-zero if nothing to do - if ( getLogger().isInfoEnabled() ) - { - getLogger().info( "nothing added to commit but untracked files present (use \"git add\" to " + - "track)" ); - } - } - - if ( statusConsumer.getChangedFiles().isEmpty() ) - { - return new CheckInScmResult( null, statusConsumer.getChangedFiles() ); - } - - Commandline clCommit = createCommitCommandLine( repository, fileSet, messageFile ); - - exitCode = GitCommandLineUtils.execute( clCommit, stdout, stderr, getLogger() ); - if ( exitCode != 0 ) - { - return new CheckInScmResult( clCommit.toString(), "The git-commit command failed.", stderr.getOutput(), - false ); - } - - if( repo.isPushChanges() ) - { - Commandline cl = createSpecificPushCommandLine( getLogger(), repository, fileSet, version ); - - exitCode = GitCommandLineUtils.execute( cl, stdout, stderr, getLogger() ); - if ( exitCode != 0 ) - { - return new CheckInScmResult( cl.toString(), "The git-push command failed.", stderr.getOutput(), false ); - } - } - - List checkedInFiles = new ArrayList( statusConsumer.getChangedFiles().size() ); - - // rewrite all detected files to now have status 'checked_in' - for ( ScmFile changedFile : statusConsumer.getChangedFiles() ) - { - ScmFile scmfile = new ScmFile( changedFile.getPath(), ScmFileStatus.CHECKED_IN ); - if ( fileSet.getFileList().isEmpty() ) - { - checkedInFiles.add( scmfile ); - } - else - { - // if a specific fileSet is given, we have to check if the file is really tracked - for ( File f : fileSet.getFileList() ) - { - if ( FilenameUtils.separatorsToUnix(f.getPath()).equals( scmfile.getPath() ) ) - { - checkedInFiles.add( scmfile ); - } - - } - } - } - - return new CheckInScmResult( clCommit.toString(), checkedInFiles ); - } - finally - { - try - { - FileUtils.forceDelete( messageFile ); - } - catch ( IOException ex ) - { - // ignore - } - } - - } - - // ---------------------------------------------------------------------- - // - // ---------------------------------------------------------------------- - - public static Commandline createSpecificPushCommandLine( ScmLogger logger, GitScmProviderRepository repository, - ScmFileSet fileSet, ScmVersion version ) - throws ScmException - { - Commandline cl = GitCommandLineUtils.getBaseGitCommandLine( fileSet.getBasedir(), "push" ); - - String branch = GitBranchCommand.getCurrentBranch(logger, repository, fileSet); - - if ( branch == null || branch.length() == 0 ) - { - throw new ScmException( "Could not detect the current branch. Don't know where I should push to!" ); - } - - // Overloaded branch name here : if repository.getUrl() is kept, during checkin(), current *local* branch - // reference is not updated, whereas by using origin, it will be done ! - cl.createArg().setValue( "origin" ); - - cl.createArg().setValue( branch + ":" + branch ); + @Override + protected GitCommand getCheckInCommand() { + // Push to origin (fcamblor) + // Handle quoted output from git, and fix relative path computations SCM-695, SCM-772 + return new ScmSyncGitCheckInCommand(); + } - return cl; - } + @Override + protected GitCommand getRemoveCommand() { + // Include -- in git rm + return new ScmSyncGitRemoveCommand(); + } + @Override + protected GitCommand getStatusCommand() { + // Handle quoted output from git, and fix relative path computations SCM-695, SCM-772 + return new ScmSyncGitStatusCommand(); } @Override - protected GitCommand getCheckInCommand() { - return new ScmSyncGitCheckInCommand(); + protected GitCommand getAddCommand() { + // Handle quoted output from git, and fix relative path computations SCM-695, SCM-772 + return new ScmSyncGitAddCommand(); } + // TODO: we also use checkout and update. Those call git ls-files, which parses the result wrongly... + // (doesn't account for the partial escaping done there for \t, \n, and \\ .) } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitRemoveCommand.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitRemoveCommand.java new file mode 100644 index 00000000..15355af9 --- /dev/null +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitRemoveCommand.java @@ -0,0 +1,72 @@ +package hudson.plugins.scm_sync_configuration.scms.customproviders.git.gitexe; + +import java.io.File; +import java.util.List; + +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.ScmResult; +import org.apache.maven.scm.command.remove.RemoveScmResult; +import org.apache.maven.scm.provider.ScmProviderRepository; +import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils; +import org.apache.maven.scm.provider.git.gitexe.command.remove.GitRemoveCommand; +import org.apache.maven.scm.provider.git.gitexe.command.remove.GitRemoveConsumer; +import org.codehaus.plexus.util.cli.CommandLineUtils; +import org.codehaus.plexus.util.cli.Commandline; + +/** + * Yet another crappy hack to fix maven's gitexe implementation. It doesn't pass "--" to git rm, leading to failures if + * a file starting with a dash is to be removed. Because of the poor design of that library using static methods galore, + * we cannot just override the wrong method... + */ +public class ScmSyncGitRemoveCommand extends GitRemoveCommand { + // Implementation copied from v1.9.1; single change in createCommandLine below. + + @Override + protected ScmResult executeRemoveCommand(ScmProviderRepository repo, ScmFileSet fileSet, String message) + throws ScmException { + if (fileSet.getFileList().isEmpty()) { + throw new ScmException("You must provide at least one file/directory to remove"); + } + + Commandline cl = createCommandLine(fileSet.getBasedir(), fileSet.getFileList()); + GitRemoveConsumer consumer = new GitRemoveConsumer(getLogger()); + + CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); + + int exitCode; + + exitCode = GitCommandLineUtils.execute(cl, consumer, stderr, getLogger()); + if (exitCode != 0) { + return new RemoveScmResult(cl.toString(), "The git command failed.", stderr.getOutput(), false); + } + + return new RemoveScmResult(cl.toString(), consumer.getRemovedFiles()); + } + + public static Commandline createCommandLine(File workingDirectory, List files) throws ScmException { + Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(workingDirectory, "rm"); + + for (File file : files) { + if (file.isAbsolute()) { + if (file.isDirectory()) { + cl.createArg().setValue("-r"); + break; + } + } else { + File absFile = new File(workingDirectory, file.getPath()); + if (absFile.isDirectory()) { + cl.createArg().setValue("-r"); + break; + } + } + } + + cl.createArg().setValue("--"); // This is missing upstream. + + ScmSyncGitUtils.addTarget(cl, files); + + return cl; + } + +} diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitStatusCommand.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitStatusCommand.java new file mode 100644 index 00000000..3b4d2656 --- /dev/null +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitStatusCommand.java @@ -0,0 +1,39 @@ +package hudson.plugins.scm_sync_configuration.scms.customproviders.git.gitexe; + +import java.io.File; + +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.command.status.StatusScmResult; +import org.apache.maven.scm.provider.ScmProviderRepository; +import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils; +import org.apache.maven.scm.provider.git.gitexe.command.status.GitStatusCommand; +import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository; +import org.codehaus.plexus.util.cli.CommandLineUtils; +import org.codehaus.plexus.util.cli.Commandline; + +public class ScmSyncGitStatusCommand extends GitStatusCommand { + + @Override + protected StatusScmResult executeStatusCommand(ScmProviderRepository repo, ScmFileSet fileSet) throws ScmException { + Commandline cl = createCommandLine( (GitScmProviderRepository) repo, fileSet ); + File repoRootDirectory = ScmSyncGitUtils.getRepositoryRootDirectory(fileSet.getBasedir(), getLogger()); + FixedGitStatusConsumer consumer = new FixedGitStatusConsumer(getLogger(), fileSet.getBasedir(), repoRootDirectory); + CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); + int exitCode = GitCommandLineUtils.execute( cl, consumer, stderr, getLogger() ); + if (exitCode != 0) { + if (getLogger().isInfoEnabled()) { + getLogger().info( "nothing added to commit but untracked files present (use \"git add\" to track)" ); + } + } + return new StatusScmResult( cl.toString(), consumer.getChangedFiles() ); + } + + public static Commandline createCommandLine( GitScmProviderRepository repository, ScmFileSet fileSet ) + { + Commandline cl = GitCommandLineUtils.getBaseGitCommandLine( fileSet.getBasedir(), "status" ); + cl.addArguments( new String[] { "--porcelain", "." } ); + return cl; + } + +} diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitUtils.java b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitUtils.java new file mode 100644 index 00000000..a1ece5d7 --- /dev/null +++ b/src/main/java/hudson/plugins/scm_sync_configuration/scms/customproviders/git/gitexe/ScmSyncGitUtils.java @@ -0,0 +1,193 @@ +package hudson.plugins.scm_sync_configuration.scms.customproviders.git.gitexe; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.log.ScmLogger; +import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils; +import org.codehaus.plexus.util.cli.CommandLineUtils; +import org.codehaus.plexus.util.cli.Commandline; + +import com.google.common.base.Ascii; +import com.google.common.base.Charsets; +import com.google.common.base.Strings; + +public final class ScmSyncGitUtils { + + private ScmSyncGitUtils() { + // No instantiation + } + + public static File getRepositoryRootDirectory(File someDirectoryInTheRepo, ScmLogger logger) throws ScmException { + // Factored out from GitStatusCommand. + Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(someDirectoryInTheRepo, "rev-parse"); + cl.createArg().setValue("--show-toplevel"); + + CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer(); + CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); + + int exitCode = GitCommandLineUtils.execute(cl, stdout, stderr, logger); + + if (exitCode != 0) { + // git-status returns non-zero if nothing to do + if (logger.isInfoEnabled()) { + logger.info( "Could not resolve toplevel from " + someDirectoryInTheRepo); + } + } else { + String absoluteRepositoryRoot = stdout.getOutput().trim(); // This comes back unquoted! + if (!Strings.isNullOrEmpty(absoluteRepositoryRoot)) { + return new File(absoluteRepositoryRoot); + } + } + return null; + } + + // Fix for https://issues.apache.org/jira/browse/SCM-695 + public static void addTarget(Commandline cl, List files) throws ScmException { + // Fix for https://issues.apache.org/jira/browse/SCM-695 . Function copied from + // GitCommandLineUtils. + if (files == null || files.isEmpty()) { + return; + } + final File workingDirectory = cl.getWorkingDirectory(); + try + { + String canonicalWorkingDirectory = workingDirectory.getCanonicalPath(); + if (!canonicalWorkingDirectory.endsWith(File.separator)) { + canonicalWorkingDirectory += File.separator; // Fixes SCM-695 + } + for (File file : files) { + String relativeFile = file.getPath(); + final String canonicalFile = file.getCanonicalPath(); + + if (canonicalFile.startsWith(canonicalWorkingDirectory)) { + relativeFile = canonicalFile.substring(canonicalWorkingDirectory.length()); + if (relativeFile.startsWith(File.separator)) { + relativeFile = relativeFile.substring(File.separator.length()); + } + } + // no setFile() since this screws up the working directory! + cl.createArg().setValue( relativeFile ); + } + } + catch ( IOException ex ) + { + throw new ScmException( "Could not get canonical paths for workingDirectory = " + + workingDirectory + " or files=" + files, ex ); + } + } + + public static String relativizePath (String parent, String child) { + // Fix for SCM-772. Compare FixedGitStatusConsumer. + if ( parent != null && child != null) { + if (parent.equals(child)) { + return ""; + } + if (!parent.endsWith(File.separator)) { + parent += File.separator; + } + if (child.startsWith(parent)) { + child = child.substring(parent.length()); + if (child.startsWith(File.separator)) { + child = child.substring(File.separator.length()); + } + } + } + return child; + } + + public static String dequote(String inputFromGit) { + if (inputFromGit.charAt(0) != '"') { + return inputFromGit; + } + // Per http://git-scm.com/docs/git-status : If a filename contains whitespace or other nonprintable characters, + // that field will be quoted in the manner of a C string literal: surrounded by ASCII double quote (34) characters, + // and with interior special characters backslash-escaped. + // + // Here, we have to undo this. We work on byte sequences and assume UTF-8. (Git may also give us back non-ASCII + // characters back as UTF-8 byte sequences that appear as octal or hex escapes.) + byte[] input = inputFromGit.substring(1, inputFromGit.length() - 1).getBytes(Charsets.UTF_8); + byte[] output = new byte[input.length]; // It can only get smaller + int j = 0; + for (int i = 0; i < input.length; i++, j++) { + if (input[i] == '\\') { + byte ch = input[++i]; + switch (ch) { + case '\\' : + case '"' : + output[j] = ch; + break; + case 'a' : + output[j] = Ascii.BEL; + break; + case 'b' : + output[j] = '\b'; + break; + case 'f' : + output[j] = '\f'; + break; + case 'n' : + output[j] = '\n'; + break; + case 'r' : + output[j] = '\r'; + break; + case 't' : + output[j] = '\t'; + break; + case 'v' : + output[j] = Ascii.VT; + break; + case 'x' : + // Hex escape; must be followed by two hex digits. We assume git gives us only valid sequences. + if (i + 2 < input.length) { + byte value = toHex(input[++i]); + output[j] = (byte) (value * 16 + toHex(input[++i])); + } else { + // Invalid. + output[j++] = '\\'; + output[j] = ch; + } + break; + case '0' : + case '1' : + case '2' : + case '3' : + // Octal escape; must be followed by two more octal digits. We assume git gives us only valid sequences. + if (i + 2 < input.length) { + byte value = (byte) (ch - '0'); + value = (byte) (value * 8 + (byte) (input[++i] - '0')); + output[j] = (byte) (value * 8 + (byte) (input[++i] - '0')); + } else { + // Invalid. + output[j++] = '\\'; + output[j] = ch; + } + break; + default : + // Unknown/invalid escape. + output[j++] = '\\'; + output[j] = ch; + break; + } + } else { + output[j] = input[i]; + } + } + return new String(output, 0, j, Charsets.UTF_8); + } + + private static byte toHex(byte in) { + if (in >= '0' && in <= '9') { + return (byte) (in - '0'); + } else if (in >= 'A' && in <= 'F') { + return (byte) (in - 'A'); + } else if (in >= 'a' && in <= 'f') { + return (byte) (in - 'a'); + } + return in; + } + +} diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/AbstractScmSyncStrategy.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/AbstractScmSyncStrategy.java index c218c015..4723403f 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/AbstractScmSyncStrategy.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/AbstractScmSyncStrategy.java @@ -2,88 +2,134 @@ import com.google.common.base.Function; import com.google.common.collect.Collections2; + import hudson.XmlFile; -import hudson.model.Hudson; import hudson.model.Item; import hudson.model.Saveable; +import hudson.plugins.scm_sync_configuration.JenkinsFilesHelper; import hudson.plugins.scm_sync_configuration.model.MessageWeight; import hudson.plugins.scm_sync_configuration.model.WeightedMessage; import hudson.plugins.scm_sync_configuration.strategies.model.ConfigurationEntityMatcher; import hudson.plugins.scm_sync_configuration.strategies.model.PageMatcher; import javax.annotation.Nullable; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.types.selectors.FileSelector; + import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import jenkins.model.Jenkins; + public abstract class AbstractScmSyncStrategy implements ScmSyncStrategy { private static final Function PATH_TO_FILE_IN_HUDSON = new Function() { + @Override public File apply(@Nullable String path) { - return new File(Hudson.getInstance().getRootDir()+File.separator+path); + return new File(Jenkins.getInstance().getRootDir(), path); } }; protected static class DefaultCommitMessageFactory implements CommitMessageFactory { + @Override public WeightedMessage getMessageWhenSaveableUpdated(Saveable s, XmlFile file) { return new WeightedMessage("Modification on configuration(s)", MessageWeight.MINIMAL); } + @Override public WeightedMessage getMessageWhenItemRenamed(Item item, String oldPath, String newPath) { return new WeightedMessage("Item renamed", MessageWeight.MINIMAL); } + @Override public WeightedMessage getMessageWhenItemDeleted(Item item) { return new WeightedMessage("File hierarchy deleted", MessageWeight.MINIMAL); } } - private ConfigurationEntityMatcher configEntityMatcher; - private List pageMatchers; - - protected AbstractScmSyncStrategy(ConfigurationEntityMatcher _configEntityMatcher, List _pageMatchers){ - this.configEntityMatcher = _configEntityMatcher; - this.pageMatchers = _pageMatchers; - } + private final ConfigurationEntityMatcher configEntityMatcher; + private final List pageMatchers; + private CommitMessageFactory commitMessageFactory; + + protected AbstractScmSyncStrategy(ConfigurationEntityMatcher _configEntityMatcher, List _pageMatchers){ + this.configEntityMatcher = _configEntityMatcher; + this.pageMatchers = _pageMatchers; + } protected ConfigurationEntityMatcher createConfigEntityMatcher(){ return configEntityMatcher; } - - public boolean isSaveableApplicable(Saveable saveable, File file) { - return createConfigEntityMatcher().matches(saveable, file); - } - - public PageMatcher getPageMatcherMatching(String url){ - String rootUrl = Hudson.getInstance().getRootUrlFromRequest(); - String cleanedUrl = null; - if(url.startsWith(rootUrl)){ - cleanedUrl = url.substring(rootUrl.length()); - } else { - cleanedUrl = url; - } - for(PageMatcher pm : pageMatchers){ - if(pm.getUrlRegex().matcher(cleanedUrl).matches()){ - return pm; - } - } - return null; - } - - public List createInitializationSynchronizedFileset() { - File hudsonRoot = Hudson.getInstance().getRootDir(); - String[] matchingFilePaths = createConfigEntityMatcher().matchingFilesFrom(hudsonRoot); - return new ArrayList(Collections2.transform(Arrays.asList(matchingFilePaths), PATH_TO_FILE_IN_HUDSON)); + + @Override + public boolean isSaveableApplicable(Saveable saveable, File file) { + return createConfigEntityMatcher().matches(saveable, file); } - public boolean isCurrentUrlApplicable(String url) { - return getPageMatcherMatching(url)!=null; - } + public PageMatcher getPageMatcherMatching(String url){ + String rootUrl = Jenkins.getInstance().getRootUrlFromRequest(); + String cleanedUrl = null; + if(url.startsWith(rootUrl)){ + cleanedUrl = url.substring(rootUrl.length()); + } else { + cleanedUrl = url; + } + for(PageMatcher pm : pageMatchers){ + if(pm.getUrlRegex().matcher(cleanedUrl).matches()){ + return pm; + } + } + return null; + } + @Override + public List collect() { + return collect(null); + } + + @Override + public List collect(File directory) { + File jenkinsRoot = Jenkins.getInstance().getRootDir(); + if (jenkinsRoot.equals(directory)) { + directory = null; + } + FileSelector selector = null; + if (directory != null) { + String pathRelativeToRoot = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(directory); + if (pathRelativeToRoot == null) { + throw new IllegalArgumentException(directory.getAbsolutePath() + " is not under " + jenkinsRoot.getAbsolutePath()); + } + final String restrictedPath = pathRelativeToRoot.endsWith("/") ? pathRelativeToRoot : pathRelativeToRoot + '/'; + selector = new FileSelector() { + @Override + public boolean isSelected(File basedir, String pathRelativeToBasedir, File file) throws BuildException { + // Only include directories leading to our directory (parent directories and the directory itself) and then whatever is below. + if (file.isDirectory()) { + pathRelativeToBasedir = pathRelativeToBasedir.endsWith("/") ? pathRelativeToBasedir : pathRelativeToBasedir + '/'; + } + return pathRelativeToBasedir.startsWith(restrictedPath) || restrictedPath.startsWith(pathRelativeToBasedir); + } + }; + } + String[] matchingFilePaths = createConfigEntityMatcher().matchingFilesFrom(jenkinsRoot, selector); + return new ArrayList(Collections2.transform(Arrays.asList(matchingFilePaths), PATH_TO_FILE_IN_HUDSON)); + } + + @Override + public boolean isCurrentUrlApplicable(String url) { + return getPageMatcherMatching(url)!=null; + } + + @Override public List getSyncIncludes(){ return createConfigEntityMatcher().getIncludes(); } + @Override public CommitMessageFactory getCommitMessageFactory(){ - return new DefaultCommitMessageFactory(); + if (commitMessageFactory == null) { + commitMessageFactory = new DefaultCommitMessageFactory(); + } + return commitMessageFactory; } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/ScmSyncStrategy.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/ScmSyncStrategy.java index 87e62623..ade4c617 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/ScmSyncStrategy.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/ScmSyncStrategy.java @@ -16,29 +16,50 @@ public static interface CommitMessageFactory { public WeightedMessage getMessageWhenItemDeleted(Item item); } - /** - * Is the given Saveable eligible for the current strategy ? - * @param saveable A saveable which is saved - * @param file Corresponding file to the given Saveable object - * @return true if current Saveable instance matches with current ScmSyncStrategy target, - * false otherwise - */ - boolean isSaveableApplicable(Saveable saveable, File file); - - /** - * Is the given url eligible for the current strategy ? - * @param url Current url, where hudson root url has been truncated - * @return true if current url matches with current ScmSyncStrategy target, false otherwise - */ - boolean isCurrentUrlApplicable(String url); - - /** - * @return a Fileset of file to synchronize when initializing scm repository - */ - List createInitializationSynchronizedFileset(); + /** + * Is the given Saveable eligible for the current strategy ? + * @param saveable A saveable which is saved + * @param file Corresponding file to the given Saveable object + * @return true if current Saveable instance matches with current ScmSyncStrategy target, + * false otherwise + */ + boolean isSaveableApplicable(Saveable saveable, File file); + + /** + * Determines whether the strategy might have applied to a deleted item. + * + * @param saveable that was deleted; still exists in Jenkins' model but has already been eradicated from disk + * @param pathRelativeToRoot where the item resided + * @param wasDirectory whether it was a directory + * @return + */ + boolean mightHaveBeenApplicableToDeletedSaveable(Saveable saveable, String pathRelativeToRoot, boolean wasDirectory); + + /** + * Is the given url eligible for the current strategy ? + * @param url Current url, where hudson root url has been truncated + * @return true if current url matches with current ScmSyncStrategy target, false otherwise + */ + boolean isCurrentUrlApplicable(String url); + + /** + * Collects all files, from Jenkins' root directory, that match this strategy. + * + * @return the list of files matched. + */ + List collect(); + + /** + * Collects all files in the given directory, which must be under Jenkins' root directory, that match this strategy. + * + * @param directory to search in + * @return the list of files + * @throws IllegalArgumentException if the given directory is not under Jenkins' root directory + */ + List collect(File directory); /** - * @return List of sync'ed file includes brought by current strategy + * @return List of sync'ed file includes brought by current strategy. Used only for informational purposes in the UI. */ List getSyncIncludes(); diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/BasicPluginsConfigScmSyncStrategy.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/BasicPluginsConfigScmSyncStrategy.java index d0f73375..269e4b35 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/BasicPluginsConfigScmSyncStrategy.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/BasicPluginsConfigScmSyncStrategy.java @@ -10,39 +10,44 @@ import hudson.plugins.scm_sync_configuration.strategies.model.PageMatcher; import hudson.plugins.scm_sync_configuration.strategies.model.PatternsEntityMatcher; -import java.util.ArrayList; -import java.util.List; +import java.util.Collections; public class BasicPluginsConfigScmSyncStrategy extends AbstractScmSyncStrategy { - private static final List PAGE_MATCHERS = new ArrayList(){ { - // No page matchers for this strategy ... for the moment - } }; - private static final String[] PATTERNS = new String[]{ "hudson*.xml", + "jenkins*.xml", "scm-sync-configuration.xml" }; - private static final ConfigurationEntityMatcher CONFIG_ENTITY_MATCHER = new PatternsEntityMatcher(PATTERNS); + private static final ConfigurationEntityMatcher CONFIG_ENTITY_MATCHER = new PatternsEntityMatcher(PATTERNS); - public BasicPluginsConfigScmSyncStrategy(){ - super(CONFIG_ENTITY_MATCHER, PAGE_MATCHERS); - } + public BasicPluginsConfigScmSyncStrategy(){ + super(CONFIG_ENTITY_MATCHER, Collections.emptyList()); + } + @Override public CommitMessageFactory getCommitMessageFactory(){ return new CommitMessageFactory(){ + @Override public WeightedMessage getMessageWhenSaveableUpdated(Saveable s, XmlFile file) { return new WeightedMessage("Plugin configuration files updated", MessageWeight.MINIMAL); } + @Override public WeightedMessage getMessageWhenItemRenamed(Item item, String oldPath, String newPath) { // It should never happen... but who cares how will behave *every* plugin in the jenkins land ? return new WeightedMessage("Plugin configuration files renamed", MessageWeight.MINIMAL); } + @Override public WeightedMessage getMessageWhenItemDeleted(Item item) { // It should never happen... but who cares how will behave *every* plugin in the jenkins land ? return new WeightedMessage("Plugin configuration files deleted", MessageWeight.MINIMAL); } }; } + + @Override + public boolean mightHaveBeenApplicableToDeletedSaveable(Saveable saveable, String pathRelativeToRoot, boolean wasDirectory) { + return !wasDirectory && pathRelativeToRoot != null && CONFIG_ENTITY_MATCHER.matches(saveable, pathRelativeToRoot, false); + } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/JenkinsConfigScmSyncStrategy.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/JenkinsConfigScmSyncStrategy.java index acbb659b..fc14f1e1 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/JenkinsConfigScmSyncStrategy.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/JenkinsConfigScmSyncStrategy.java @@ -10,43 +10,51 @@ import hudson.plugins.scm_sync_configuration.strategies.model.PageMatcher; import hudson.plugins.scm_sync_configuration.strategies.model.PatternsEntityMatcher; -import java.util.ArrayList; import java.util.List; +import com.google.common.collect.ImmutableList; + public class JenkinsConfigScmSyncStrategy extends AbstractScmSyncStrategy { - private static final List PAGE_MATCHERS = new ArrayList(){ { - // Global configuration page - add(new PageMatcher("^configure$", "form[name='config']")); - // View configuration pages - add(new PageMatcher("^(.+/)?view/[^/]+/configure$", "form[name='viewConfig']")); - add(new PageMatcher("^newView$", "form[name='createView'],form[name='createItem']")); - } }; - + private static final List PAGE_MATCHERS = ImmutableList.of( + // Global configuration page + new PageMatcher("^configure$", "form[name='config']"), + // View configuration pages + new PageMatcher("^(.+/)?view/[^/]+/configure$", "form[name='viewConfig']"), + new PageMatcher("^newView$", "form[name='createView'],form[name='createItem']") + ); + private static final String[] PATTERNS = new String[]{ "config.xml" }; - - private static final ConfigurationEntityMatcher CONFIG_ENTITY_MATCHER = new PatternsEntityMatcher(PATTERNS); - - public JenkinsConfigScmSyncStrategy(){ - super(CONFIG_ENTITY_MATCHER, PAGE_MATCHERS); - } + private static final ConfigurationEntityMatcher CONFIG_ENTITY_MATCHER = new PatternsEntityMatcher(PATTERNS); + + public JenkinsConfigScmSyncStrategy(){ + super(CONFIG_ENTITY_MATCHER, PAGE_MATCHERS); + } + + @Override public CommitMessageFactory getCommitMessageFactory(){ return new CommitMessageFactory(){ + @Override public WeightedMessage getMessageWhenSaveableUpdated(Saveable s, XmlFile file) { - return new WeightedMessage("Jenkins configuration files updated", - // Jenkins config update message should be considered as "important", especially - // more important than the plugin descriptors Saveable updates - MessageWeight.NORMAL); + return new WeightedMessage("Jenkins configuration files updated", MessageWeight.NORMAL); } + @Override public WeightedMessage getMessageWhenItemRenamed(Item item, String oldPath, String newPath) { throw new IllegalStateException("Jenkins configuration files should never be renamed !"); } + @Override public WeightedMessage getMessageWhenItemDeleted(Item item) { throw new IllegalStateException("Jenkins configuration files should never be deleted !"); } }; } + + @Override + public boolean mightHaveBeenApplicableToDeletedSaveable(Saveable saveable, String pathRelativeToRoot, boolean wasDirectory) { + // Uh-oh... Jenkins config should never be deleted. + return false; + } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/JobConfigScmSyncStrategy.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/JobConfigScmSyncStrategy.java index 6f18527d..330e5d39 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/JobConfigScmSyncStrategy.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/JobConfigScmSyncStrategy.java @@ -2,57 +2,47 @@ import hudson.XmlFile; import hudson.model.Item; -import hudson.model.Job; import hudson.model.Saveable; -import hudson.model.TopLevelItem; import hudson.plugins.scm_sync_configuration.model.MessageWeight; import hudson.plugins.scm_sync_configuration.model.WeightedMessage; import hudson.plugins.scm_sync_configuration.strategies.AbstractScmSyncStrategy; -import hudson.plugins.scm_sync_configuration.strategies.model.ClassAndFileConfigurationEntityMatcher; import hudson.plugins.scm_sync_configuration.strategies.model.ConfigurationEntityMatcher; +import hudson.plugins.scm_sync_configuration.strategies.model.JobOrFolderConfigurationEntityMatcher; import hudson.plugins.scm_sync_configuration.strategies.model.PageMatcher; -import java.util.ArrayList; -import java.util.List; +import java.util.Collections; public class JobConfigScmSyncStrategy extends AbstractScmSyncStrategy { - // Don't miss to take into account view urls since we can configure a job through a view ! - private static final List PAGE_MATCHERS = new ArrayList(){ { - add(new PageMatcher("^(.*view/[^/]+/)?(/job/[^/])*/job/[^/]+/configure$", "form[name='config']")); - } }; - // Only saving config.xml file located in job directory - // Some plugins (like maven release plugin) could add their own configuration files in the job directory that we don't want to synchronize - // ... at least in the current strategy ! - private static final String [] PATTERNS = new String[] { - "**/jobs/*/config.xml" - }; - private static final ConfigurationEntityMatcher CONFIG_ENTITY_MANAGER = new ClassAndFileConfigurationEntityMatcher(TopLevelItem.class, PATTERNS); - - public JobConfigScmSyncStrategy(){ - super(CONFIG_ENTITY_MANAGER, PAGE_MATCHERS); - } + private static final ConfigurationEntityMatcher CONFIG_MATCHER = new JobOrFolderConfigurationEntityMatcher(); + public JobConfigScmSyncStrategy(){ + super(CONFIG_MATCHER, Collections.singletonList(new PageMatcher("^(.*view/[^/]+/)?(job/[^/]+/)+configure$", "form[name='config']"))); + } + + @Override public CommitMessageFactory getCommitMessageFactory(){ return new CommitMessageFactory(){ + @Override public WeightedMessage getMessageWhenSaveableUpdated(Saveable s, XmlFile file) { return new WeightedMessage("Job ["+((Item)s).getName()+"] configuration updated", - // Job config update message should be considered as "important", especially - // more important than the plugin descriptors Saveable updates MessageWeight.IMPORTANT); } + @Override public WeightedMessage getMessageWhenItemRenamed(Item item, String oldPath, String newPath) { return new WeightedMessage("Job ["+item.getName()+"] hierarchy renamed from ["+oldPath+"] to ["+newPath+"]", - // Job config rename message should be considered as "important", especially - // more important than the plugin descriptors Saveable renames MessageWeight.MORE_IMPORTANT); } + @Override public WeightedMessage getMessageWhenItemDeleted(Item item) { return new WeightedMessage("Job ["+item.getName()+"] hierarchy deleted", - // Job config deletion message should be considered as "important", especially - // more important than the plugin descriptors Saveable deletions MessageWeight.MORE_IMPORTANT); } }; } + + @Override + public boolean mightHaveBeenApplicableToDeletedSaveable(Saveable saveable, String pathRelativeFromRoot, boolean wasDirectory) { + return CONFIG_MATCHER.matches(saveable, pathRelativeFromRoot, wasDirectory); + } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/ManualIncludesScmSyncStrategy.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/ManualIncludesScmSyncStrategy.java index bdd42734..1c027d25 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/ManualIncludesScmSyncStrategy.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/ManualIncludesScmSyncStrategy.java @@ -1,23 +1,20 @@ package hudson.plugins.scm_sync_configuration.strategies.impl; +import hudson.model.Saveable; import hudson.plugins.scm_sync_configuration.ScmSyncConfigurationPlugin; import hudson.plugins.scm_sync_configuration.strategies.AbstractScmSyncStrategy; import hudson.plugins.scm_sync_configuration.strategies.model.ConfigurationEntityMatcher; import hudson.plugins.scm_sync_configuration.strategies.model.PageMatcher; import hudson.plugins.scm_sync_configuration.strategies.model.PatternsEntityMatcher; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class ManualIncludesScmSyncStrategy extends AbstractScmSyncStrategy { - private static final List PAGE_MATCHERS = new ArrayList(){ { - // No page matcher for this particular implementation - } }; - - public ManualIncludesScmSyncStrategy(){ - super(null, PAGE_MATCHERS); - } + public ManualIncludesScmSyncStrategy(){ + super(null, Collections.emptyList()); + } @Override protected ConfigurationEntityMatcher createConfigEntityMatcher(){ @@ -28,4 +25,10 @@ protected ConfigurationEntityMatcher createConfigEntityMatcher(){ } return new PatternsEntityMatcher(includes); } + + @Override + public boolean mightHaveBeenApplicableToDeletedSaveable(Saveable saveable, String pathRelativeToRoot, boolean wasDirectory) { + // Best we can do here. We'll double check later on in the transaction. + return true; + } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/UserConfigScmSyncStrategy.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/UserConfigScmSyncStrategy.java index 586deee9..d860ba8d 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/UserConfigScmSyncStrategy.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/impl/UserConfigScmSyncStrategy.java @@ -2,7 +2,6 @@ import hudson.XmlFile; import hudson.model.Item; -import hudson.model.Job; import hudson.model.Saveable; import hudson.model.User; import hudson.plugins.scm_sync_configuration.model.MessageWeight; @@ -12,46 +11,52 @@ import hudson.plugins.scm_sync_configuration.strategies.model.ConfigurationEntityMatcher; import hudson.plugins.scm_sync_configuration.strategies.model.PageMatcher; -import java.util.ArrayList; import java.util.List; +import com.google.common.collect.ImmutableList; + public class UserConfigScmSyncStrategy extends AbstractScmSyncStrategy { // Don't miss to take into account view urls since we can configure a job through a view ! - private static final List PAGE_MATCHERS = new ArrayList(){ { - add(new PageMatcher("^securityRealm/addUser$", "#main-panel form")); - add(new PageMatcher("^securityRealm/user/[^/]+/configure$", "form[name='config']")); - } }; + private static final List PAGE_MATCHERS = ImmutableList.of( + new PageMatcher("^securityRealm/addUser$", "#main-panel form"), + new PageMatcher("^securityRealm/user/[^/]+/configure$", "form[name='config']") + ); + // Only saving config.xml file located in user directory - private static final String [] PATTERNS = new String[] { + private static final String [] PATTERNS = new String[] { "users/*/config.xml" - }; - private static final ConfigurationEntityMatcher CONFIG_ENTITY_MANAGER = new ClassAndFileConfigurationEntityMatcher(User.class, PATTERNS); + }; + + private static final ConfigurationEntityMatcher CONFIG_ENTITY_MATCHER = new ClassAndFileConfigurationEntityMatcher(User.class, PATTERNS); - public UserConfigScmSyncStrategy(){ - super(CONFIG_ENTITY_MANAGER, PAGE_MATCHERS); - } + public UserConfigScmSyncStrategy(){ + super(CONFIG_ENTITY_MATCHER, PAGE_MATCHERS); + } + @Override public CommitMessageFactory getCommitMessageFactory(){ return new CommitMessageFactory(){ + @Override public WeightedMessage getMessageWhenSaveableUpdated(Saveable s, XmlFile file) { return new WeightedMessage("User ["+((User)s).getDisplayName()+"] configuration updated", - // Job config update message should be considered as "important", especially - // more important than the plugin descriptors Saveable updates MessageWeight.IMPORTANT); } + @Override public WeightedMessage getMessageWhenItemRenamed(Item item, String oldPath, String newPath) { return new WeightedMessage("User ["+item.getName()+"] configuration renamed from ["+oldPath+"] to ["+newPath+"]", - // Job config rename message should be considered as "important", especially - // more important than the plugin descriptors Saveable renames MessageWeight.MORE_IMPORTANT); } + @Override public WeightedMessage getMessageWhenItemDeleted(Item item) { return new WeightedMessage("User ["+item.getName()+"] hierarchy deleted", - // Job config deletion message should be considered as "important", especially - // more important than the plugin descriptors Saveable deletions MessageWeight.MORE_IMPORTANT); } }; } + + @Override + public boolean mightHaveBeenApplicableToDeletedSaveable(Saveable saveable, String pathRelativeToRoot, boolean wasDirectory) { + return CONFIG_ENTITY_MATCHER.matches(saveable, pathRelativeToRoot, wasDirectory); + } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/ClassAndFileConfigurationEntityMatcher.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/ClassAndFileConfigurationEntityMatcher.java index c1e1861b..a501a552 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/ClassAndFileConfigurationEntityMatcher.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/ClassAndFileConfigurationEntityMatcher.java @@ -3,27 +3,27 @@ import hudson.model.Saveable; import java.io.File; -import java.util.regex.Pattern; public class ClassAndFileConfigurationEntityMatcher extends PatternsEntityMatcher { - - private Class saveableClazz; - - public ClassAndFileConfigurationEntityMatcher(Class clazz, String[] patterns){ - super(patterns); - this.saveableClazz = clazz; - } - - public boolean matches(Saveable saveable, File file) { - if(saveableClazz.isAssignableFrom(saveable.getClass())){ - if(file == null){ - return true; - } else { - return super.matches(saveable, file); - } - } - - return false; - } + + private final Class saveableClazz; + + public ClassAndFileConfigurationEntityMatcher(Class clazz, String[] patterns){ + super(patterns); + this.saveableClazz = clazz; + } + + @Override + public boolean matches(Saveable saveable, File file) { + if (saveableClazz.isAssignableFrom(saveable.getClass())){ + if (file == null) { + return true; + } else { + return super.matches(saveable, file); + } + } + + return false; + } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/ConfigurationEntityMatcher.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/ConfigurationEntityMatcher.java index aa45f19b..acce5387 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/ConfigurationEntityMatcher.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/ConfigurationEntityMatcher.java @@ -5,8 +5,45 @@ import java.io.File; import java.util.List; +import org.apache.tools.ant.types.selectors.FileSelector; + +/** + * A matcher that matches specific files under $JENKINS_HOME. + */ public interface ConfigurationEntityMatcher { - public boolean matches(Saveable saveable, File file); - public String[] matchingFilesFrom(File rootDirectory); + + /** + * Determines whether the matcher matches a given combination of saveable and file. + * + * @param saveable the file belongs to + * @param file that is to be matched + * @return {@code true} on match, {@code false} otherwise + */ + public boolean matches(Saveable saveable, File file); + + /** + * Determines whether the matcher would have matched a deleted file, of which we know only its path and possibly whether it was directory. + * + * @param saveable the file belonged to + * @param pathRelativeToRoot of the file or directory (which Jenkins has already deleted) + * @param isDirectory {@code true} if it's known that the path referred to a directory, {@code false} otherwise + * @return {@code true} on match, {@code false} otherwise + */ + public boolean matches(Saveable saveable, String pathRelativeToRoot, boolean isDirectory); + + /** + * Collects all files under the given rootDirectory that match, restricted by the given {@code link FileSelector}. + * + * @param rootDirectory to traverse + * @param selector restricting the traversal + * @return an array of all path names relative to the rootDirectory of all files that match. + */ + public String[] matchingFilesFrom(File rootDirectory, FileSelector selector); + + /** + * All patterns this matcher matches; used only for informational purposes in the UI. + * + * @return A list of explanatory messages about the pattern the matcher matches. + */ List getIncludes(); } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/JobOrFolderConfigurationEntityMatcher.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/JobOrFolderConfigurationEntityMatcher.java new file mode 100644 index 00000000..2da878c5 --- /dev/null +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/JobOrFolderConfigurationEntityMatcher.java @@ -0,0 +1,106 @@ +package hudson.plugins.scm_sync_configuration.strategies.model; + +import hudson.model.Saveable; +import hudson.model.AbstractItem; +import hudson.plugins.scm_sync_configuration.JenkinsFilesHelper; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.types.selectors.FileSelector; + +/** + * A {@link ConfigurationEntityMatcher} for job and Cloudbees Folders plugin folder configuration files. + * Matches config.xml located in jobs/project/config.xml (normal Jenkins), but also in nested directories + * below that as long as the path has the pattern (jobs/someName/)+config.xml (Cloudbees directory structure). + */ +public class JobOrFolderConfigurationEntityMatcher extends PatternsEntityMatcher { + + private static final String JOBS_DIR_NAME = "jobs"; + private static final String CONFIG_FILE_NAME = "config.xml"; + private static final String DIRECTORY_REGEXP = "(?:" + JOBS_DIR_NAME + "/[^/]+/)+"; + + private static final Pattern CONFIGS_TO_MATCH = Pattern.compile(DIRECTORY_REGEXP + CONFIG_FILE_NAME); + private static final Pattern DIRECTORIES_TO_MATCH = Pattern.compile(DIRECTORY_REGEXP); + + private static final String[] ANT_PATTERN = new String[] { JOBS_DIR_NAME + "/**/" + CONFIG_FILE_NAME }; + + public JobOrFolderConfigurationEntityMatcher() { + super(ANT_PATTERN); + // This pattern is only used for matchingFileFrom() below, and is augmented by a FileSelector enforcing the (jobs/someName/)+ pattern + } + + @Override + public boolean matches(Saveable saveable, File file) { + // The file may be null, indicating a deletion! + if (saveable instanceof AbstractItem) { + // Both jobs and folders are AbstractItems, which are Saveables. + if (file == null) { + // Deleted. + file = ((AbstractItem) saveable).getConfigFile().getFile(); + } else if (file.isDirectory()) { + file = new File(file, CONFIG_FILE_NAME); + } + return matches(saveable, JenkinsFilesHelper.buildPathRelativeToHudsonRoot(file), false); + } + return false; + } + + @Override + public boolean matches(Saveable saveable, String pathRelativeToRoot, boolean isDirectory) { + if ((saveable instanceof AbstractItem) && pathRelativeToRoot != null) { + if (isDirectory) { + if (!pathRelativeToRoot.endsWith("/")) { + pathRelativeToRoot += '/'; + } + pathRelativeToRoot += CONFIG_FILE_NAME; + } + return CONFIGS_TO_MATCH.matcher(pathRelativeToRoot).matches(); + } + return false; + } + + @Override + public List getIncludes() { + // This is used only for display in the UI. + return Collections.singletonList(ANT_PATTERN[0] + " (** restricted to real project and folder directories)"); + } + + @Override + public String[] matchingFilesFrom(File rootDirectory, FileSelector selector) { + // Create a selector that enforces the (jobs/someName)+ pattern + final FileSelector originalSelector = selector; + FileSelector combinedSelector = new FileSelector() { + @Override + public boolean isSelected(File basedir, String pathRelativeToBaseDir, File file) throws BuildException { + if (originalSelector != null && !originalSelector.isSelected(basedir, pathRelativeToBaseDir, file)) { + return false; + } + if (CONFIGS_TO_MATCH.matcher(pathRelativeToBaseDir).matches()) { + return true; + } + + if (JOBS_DIR_NAME.equals(pathRelativeToBaseDir)) { + return true; + } + if (!pathRelativeToBaseDir.endsWith("/")) { + pathRelativeToBaseDir += '/'; + } + if (pathRelativeToBaseDir.endsWith('/' + JOBS_DIR_NAME + '/')) { + pathRelativeToBaseDir = StringUtils.removeEnd(pathRelativeToBaseDir, JOBS_DIR_NAME + '/'); + return DIRECTORIES_TO_MATCH.matcher(pathRelativeToBaseDir).matches(); + } else { + // Compare https://github.com/jenkinsci/cloudbees-folder-plugin/blob/70a4d47314a36b54d522cae0a78b3c76d153e627/src/main/java/com/cloudbees/hudson/plugins/folder/Folder.java#L200 + // The Cloudbees Folders plugin prunes the hierarchy on directories not containing a config.xml. + return DIRECTORIES_TO_MATCH.matcher(pathRelativeToBaseDir).matches() && file.isDirectory() && new File(file, CONFIG_FILE_NAME).exists(); + } + } + }; + return super.matchingFilesFrom(rootDirectory, combinedSelector); + } + +} diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/PatternsEntityMatcher.java b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/PatternsEntityMatcher.java index 3bccded2..cbf74aa5 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/PatternsEntityMatcher.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/strategies/model/PatternsEntityMatcher.java @@ -1,9 +1,11 @@ package hudson.plugins.scm_sync_configuration.strategies.model; -import hudson.model.Hudson; import hudson.model.Saveable; import hudson.plugins.scm_sync_configuration.JenkinsFilesHelper; +import hudson.plugins.scm_sync_configuration.ScmSyncConfigurationBusiness; + import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.types.selectors.FileSelector; import org.springframework.util.AntPathMatcher; import java.io.File; @@ -12,35 +14,68 @@ public class PatternsEntityMatcher implements ConfigurationEntityMatcher { - private String[] includesPatterns; + private final String[] includesPatterns; + + private static String SCM_WORKING_DIRECTORY = ScmSyncConfigurationBusiness.getScmDirectoryName(); + private static String WAR_DIRECTORY = "war"; public PatternsEntityMatcher(String[] includesPatterns){ this.includesPatterns = includesPatterns; } - public boolean matches(Saveable saveable, File file) { - if (file == null) { - return false; - } - String filePathRelativeToHudsonRoot = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(file); - AntPathMatcher matcher = new AntPathMatcher(); - for(String pattern : includesPatterns) { - if(matcher.match(pattern, filePathRelativeToHudsonRoot)){ - return true; + @Override + public boolean matches(Saveable saveable, File file) { + if (file == null) { + return false; + } + return matches(saveable, JenkinsFilesHelper.buildPathRelativeToHudsonRoot(file), file.isDirectory()); + } + + @Override + public boolean matches(Saveable saveable, String pathRelativeToRoot, boolean isDirectory) { + if (pathRelativeToRoot != null) { + // Guard our own SCM workspace and the war directory. User-defined includes might inadvertently include those if they start with * or **! + if (pathRelativeToRoot.equals(SCM_WORKING_DIRECTORY) || pathRelativeToRoot.startsWith(SCM_WORKING_DIRECTORY + '/')) { + return false; + } else if (pathRelativeToRoot.equals(WAR_DIRECTORY) || pathRelativeToRoot.startsWith(WAR_DIRECTORY + '/')) { + return false; } - } - return false; - } + AntPathMatcher matcher = new AntPathMatcher(); + String directoryName = null; + for (String pattern : includesPatterns) { + if (matcher.match(pattern, pathRelativeToRoot)) { + return true; + } else if (isDirectory) { + // pathRelativeFromRoot is be a directory, and the pattern end in a file name. In this case, we must claim a match. + int i = pattern.lastIndexOf('/'); + if (directoryName == null) { + directoryName = pathRelativeToRoot.endsWith("/") ? pathRelativeToRoot.substring(0, pathRelativeToRoot.length() - 1) : pathRelativeToRoot; + } + if (i > 0 && matcher.match(pattern.substring(0, i), directoryName)) { + return true; + } + } + } + } + return false; + } + @Override public List getIncludes(){ return Arrays.asList(includesPatterns); } - public String[] matchingFilesFrom(File rootDirectory) { + @Override + public String[] matchingFilesFrom(File rootDirectory, FileSelector selector) { DirectoryScanner scanner = new DirectoryScanner(); + scanner.setExcludes(new String[] { SCM_WORKING_DIRECTORY, SCM_WORKING_DIRECTORY + '/', WAR_DIRECTORY, WAR_DIRECTORY + '/'}); // Guard special directories scanner.setIncludes(includesPatterns); scanner.setBasedir(rootDirectory); + if (selector != null) { + scanner.setSelectors(new FileSelector[] { selector}); + } scanner.scan(); return scanner.getIncludedFiles(); } + } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/transactions/ScmTransaction.java b/src/main/java/hudson/plugins/scm_sync_configuration/transactions/ScmTransaction.java index 5faa3832..82bda3b7 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/transactions/ScmTransaction.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/transactions/ScmTransaction.java @@ -1,5 +1,9 @@ package hudson.plugins.scm_sync_configuration.transactions; +import java.io.File; +import java.util.concurrent.Future; + +import hudson.plugins.scm_sync_configuration.JenkinsFilesHelper; import hudson.plugins.scm_sync_configuration.ScmSyncConfigurationPlugin; import hudson.plugins.scm_sync_configuration.model.ChangeSet; import hudson.plugins.scm_sync_configuration.model.WeightedMessage; @@ -8,10 +12,10 @@ * @author fcamblor */ public abstract class ScmTransaction { - private ChangeSet changeset; + private final ChangeSet changeset; // Flag allowing to say if transaction will be asynchronous (default) or synchronous // Synchronous commit are useful during tests execution - private boolean synchronousCommit; + private final boolean synchronousCommit; protected ScmTransaction(){ this(false); @@ -27,14 +31,13 @@ public void defineCommitMessage(WeightedMessage weightedMessage){ } public void commit(){ - ScmSyncConfigurationPlugin.getInstance().commitChangeset(changeset); - if(synchronousCommit){ - // Synchronous transactions should wait for latest commit future to be fully processed - // before going further + Future future = ScmSyncConfigurationPlugin.getInstance().commitChangeset(changeset); + if (synchronousCommit && future != null) { + // Synchronous transactions should wait for the future to be fully processed try { - ScmSyncConfigurationPlugin.getInstance().getLatestCommitFuture().get(); + future.get(); } catch (Exception e) { - throw new RuntimeException(e); + throw new RuntimeException(e); } } } @@ -48,6 +51,19 @@ public void registerPathForDeletion(String path) { } public void registerRenamedPath(String oldPath, String newPath){ - this.changeset.registerRenamedPath(oldPath, newPath); + File newFile = JenkinsFilesHelper.buildFileFromPathRelativeToHudsonRoot(newPath); + if (newFile.isDirectory()) { + for (File f : ScmSyncConfigurationPlugin.getInstance().collectAllFilesForScm(newFile)) { + String pathRelativeToRoot = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(f); + if (pathRelativeToRoot != null) { + this.changeset.registerPath(pathRelativeToRoot); + } + } + } else { + this.changeset.registerPath(newPath); + } + if (oldPath != null) { + this.changeset.registerPathForDeletion(oldPath); + } } } diff --git a/src/main/java/hudson/plugins/scm_sync_configuration/xstream/ScmSyncConfigurationXStreamConverter.java b/src/main/java/hudson/plugins/scm_sync_configuration/xstream/ScmSyncConfigurationXStreamConverter.java index ef2aa16e..ac5b7a5f 100644 --- a/src/main/java/hudson/plugins/scm_sync_configuration/xstream/ScmSyncConfigurationXStreamConverter.java +++ b/src/main/java/hudson/plugins/scm_sync_configuration/xstream/ScmSyncConfigurationXStreamConverter.java @@ -38,6 +38,7 @@ public class ScmSyncConfigurationXStreamConverter implements Converter { /** * Migrators for old versions of GlobalBuildStatsPlugin data representations */ + @SuppressWarnings("rawtypes") // Generic arrays not possible private static final ScmSyncConfigurationDataMigrator[] MIGRATORS = new ScmSyncConfigurationDataMigrator[]{ new InitialMigrator(), new V0ToV1Migrator() @@ -46,7 +47,7 @@ public class ScmSyncConfigurationXStreamConverter implements Converter { /** * Converter is only applicable on GlobalBuildStatsPlugin data */ - public boolean canConvert(Class type) { + public boolean canConvert(@SuppressWarnings("rawtypes") Class type) { // Inherited signature return ScmSyncConfigurationPlugin.class.isAssignableFrom(type); } @@ -139,7 +140,7 @@ public Object unmarshal(HierarchicalStreamReader reader, // Migrating old data into up-to-date data // Added "+1" because we take into consideration InitialMigrator for(int i=versionNumber+1; i
- ${%List of ant-like includes allowing to specify additionnal sync-ed files with repository}.
+ ${%List of ant-like includes allowing to specify additional files to synchronize with the repository}.
${%Includes should be case sensitive paths starting from your JENKINS_HOME directory, without / in the beginning; Use of wildcards} (${%* and **}) ${%is allowed}.
${%You can have a look at} ${%community shared includes in Jenkins wiki}, ${%feel free to share your owns on this page} !
${%Before creating new includes, you should be aware of some things} : diff --git a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/help/manualSynchronizationIncludes_fr.properties b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/help/manualSynchronizationIncludes_fr.properties index 89039da6..ec375ad7 100644 --- a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/help/manualSynchronizationIncludes_fr.properties +++ b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/help/manualSynchronizationIncludes_fr.properties @@ -1,20 +1,13 @@ -List\ of\ ant-like\ includes\ allowing\ to\ specify\ additionnal\ sync-ed\ files\ with\ repository=\ - Liste de chemin \u00e0 la Ant, permettant de sp\u00e9cifier des fichiers suppl\u00e9mentaires qui seront synchronis\u00e9s avec le repository -Includes\ should\ be\ case\ sensitive\ paths\ starting\ from\ your\ JENKINS_HOME\ directory,\ without\ /\ in\ the\ beginning;\ Use\ of\ wildcards=\ - Les chemins doivent \u00eatre sensibles \u00e0 la casse, bas\u00e9s depuis votre r\u00e9pertoire JENKINS_HOME, et ne d\u00e9marrant pas par un /; L''utilisation des wildcards +List\ of\ ant-like\ includes\ allowing\ to\ specify\ additional\ files\ to\ synchronize\ with\ the\ repository=Liste de chemin \u00E0 la Ant, permettant de sp\u00E9cifier des fichiers suppl\u00E9mentaires qui seront synchronis\u00E9s avec le repository +Includes\ should\ be\ case\ sensitive\ paths\ starting\ from\ your\ JENKINS_HOME\ directory,\ without\ /\ in\ the\ beginning;\ Use\ of\ wildcards=Les chemins doivent \u00EAtre sensibles \u00E0 la casse, bas\u00E9s depuis votre r\u00E9pertoire JENKINS_HOME, et ne d\u00E9marrant pas par un /; L''utilisation des wildcards *\ and\ **=* et ** -is\ allowed=est autoris\u00e9 +is\ allowed=est autoris\u00E9 You\ can\ have\ a\ look\ at=Vous pouvez jeter un oeil aux -community\ shared\ includes\ in\ Jenkins\ wiki=chemin partag\u00e9s par la communaut\u00e9 dans le wiki Jenkins -feel\ free\ to\ share\ your\ owns\ on\ this\ page=n''h\u00e9sitez pas \u00e0 partager les v\u00f4tres sur cette page -Before\ creating\ new\ includes,\ you\ should\ be\ aware\ of\ some\ things=\ - Avant de cr\u00e9er de nouveaux chemins, vous devriez \u00eatre conscient de certaines contraintes -Avoid\ includes\ for\ big\ and\ updated-often\ files=\ - Evitez les chemins pour les gros fichiers ainsi que ceux mis \u00e0 jour souvent -otherwise,\ your\ jenkins\ instance\ will\ spend\ its\ CPU\ time\ to\ commit\ your\ file=\ - autrement, votre instance Jenkins va passer son temps CPU \u00e0 commiter votre fichier -In\ order\ to\ be\ noticed\ at\ the\ right\ time,\ your\ files\ must\ be\ represented\ in\ Jenkins\ by\ a=\ - Afin d''\u00eatre notifi\u00e9 au bon moment, vos fichiers doivent correspondre, dans Jenkins, \u00e0 un +community\ shared\ includes\ in\ Jenkins\ wiki=chemin partag\u00E9s par la communaut\u00E9 dans le wiki Jenkins +feel\ free\ to\ share\ your\ owns\ on\ this\ page=n''h\u00E9sitez pas \u00E0 partager les v\u00F4tres sur cette page +Before\ creating\ new\ includes,\ you\ should\ be\ aware\ of\ some\ things=Avant de cr\u00E9er de nouveaux chemins, vous devriez \u00EAtre conscient de certaines contraintes +Avoid\ includes\ for\ big\ and\ updated-often\ files=Evitez les chemins pour les gros fichiers ainsi que ceux mis \u00E0 jour souvent +otherwise,\ your\ jenkins\ instance\ will\ spend\ its\ CPU\ time\ to\ commit\ your\ file=autrement, votre instance Jenkins va passer son temps CPU \u00E0 commiter votre fichier +In\ order\ to\ be\ noticed\ at\ the\ right\ time,\ your\ files\ must\ be\ represented\ in\ Jenkins\ by\ a=Afin d''\u00EAtre notifi\u00E9 au bon moment, vos fichiers doivent correspondre, dans Jenkins, \u00E0 un most\ of\ them\ are,\ but\ who\ knows=la plupart le sont, mais qui sait -Following\ includes\ are\ brought\ "out\ of\ the\ box"\ by\ scm-sync-configuration\ default\ includes=\ - Les chemins suivants sont fournis par d\u00e9faut par le plugin scm-sync-configuration +Following\ includes\ are\ brought\ "out\ of\ the\ box"\ by\ scm-sync-configuration\ default\ includes=Les chemins suivants sont fournis par d\u00E9faut par le plugin scm-sync-configuration diff --git a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/config.jelly b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/config.jelly index 2c74e408..c12808fd 100644 --- a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/config.jelly +++ b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/config.jelly @@ -1,7 +1,9 @@ - + \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help.properties b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help.properties index 7b68062d..888432f3 100644 --- a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help.properties +++ b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help.properties @@ -20,7 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -description.1=\ - Specify the git repository URL to synchronize your configuration files with, such as "git@github.com:mycompany/jenkins-config.git" -description.2=\ - Note that, for the moment, your MUST reference your Git repository root +description.1=Specify the git repository URL to synchronize your configuration files with, such as "git@github.com\:mycompany/jenkins-config.git" +description.2=Note that, for the moment, you MUST reference your Git repository root diff --git a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help_fr.properties b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help_fr.properties index 6d1b0fe4..36fd76bf 100644 --- a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help_fr.properties +++ b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/git/url-help_fr.properties @@ -20,7 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -description.1=\ - Spécifier l''url du repository Git vers lequel synchroniser vos fichiers de configuration, tel que "git@github.com:mycompany/jenkins-config.git" -description.2=\ - A noter qu''il est pour le moment obligatoire de référencer la racine du repository +description.1=Sp\u00E9cifiez l''url du repository Git vers lequel synchroniser vos fichiers de configuration, tel que "git@github.com\:mycompany/jenkins-config.git" +description.2=A noter qu''il est pour le moment obligatoire de r\u00E9f\u00E9rencer la racine du repository diff --git a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help.properties b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help.properties index a8dc240c..69924a3e 100644 --- a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help.properties +++ b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help.properties @@ -20,14 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -description.1=\ - Specify the subversion repository URL to synchronize your configuration files with, such as "http://yourcompany.com/repos/hudson-config/" -description.2=\ - When you enter URL, Hudson automatically checks if Hudson can connect to it. If access requires \ - authentication, it will ask you the necessary credential. If you already have a working \ - credential but would like to change it for other reasons, \ - click this link \ - and specify different credential. -description.3=\ - Each time you'll enter a new repository URL, Hudson will first synchronize all your configuration files with the repository \ - This process can take several minutes, depending on the amount of configuration files to synchronize. +description.1=Specify the subversion repository URL to synchronize your configuration files with, such as "http\://yourcompany.com/repos/hudson-config/" +description.2=When you enter a URL, Jenkins automatically checks if it can access the repository If access requires authentication, it will ask you the necessary credentials. If you already have a working credential but would like to change it for other reasons, click this link and specify different credentials. +description.3=Each time you enter a new repository URL, Jenkins will first synchronize all your configuration files with the repository. This process can take a while, depending on the amount of configuration files to synchronize. diff --git a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help_fr.properties b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help_fr.properties index 9ec3cc15..aa9f0669 100644 --- a/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help_fr.properties +++ b/src/main/resources/hudson/plugins/scm_sync_configuration/ScmSyncConfigurationPlugin/scms/svn/url-help_fr.properties @@ -20,13 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -description.1=\ - Spécifiez l''URL du repository Subversion avec lequel synchronier vos fichiers de configuration, par exemple "http://yourcompany.com/repos/hudson-config/" -description.2=\ - Quand vous entrez une URL, Hudson vérifie automatiquement s''il peut s''y connecter. Si l''accès nécessite \ - une authentification, il vous demandera les informations nécessaires. Si vous disposez déjà d''informations d''identification \ - qui marchent mais que vous voulez en changer, cliquez sur ce lien \ - et renseignez des valeurs différentes. -description.3=\ - A chaque fois que vous entrerez une nouvelle URL de repository, Hudson commencera par synchroniser tous vos fichier de configuration \ - avec le repository. Ce processus peut prendre plusieurs minutes, en fonction du nombre de fichier de configuration à synchroniser. +description.1=Sp\u00E9cifiez l''URL du repository Subversion avec lequel synchronier vos fichiers de configuration, par exemple "http\://yourcompany.com/repos/hudson-config/" +description.2=Quand vous entrez une URL, Jenkins v\u00E9rifie automatiquement s''il peut s''y connecter. Si l''acc\u00E8s n\u00E9cessite une authentification, il vous demandera les informations n\u00E9cessaires. Si vous disposez d\u00E9j\u00E0 d''informations d''identification qui marchent mais que vous voulez en changer, cliquez sur ce lien et renseignez des valeurs diff\u00E9rentes. +description.3=A chaque fois que vous entrerez une nouvelle URL de repository, Jenkins commencera par synchroniser tous vos fichier de configuration avec le repository. Ce processus peut prendre plusieurs minutes, en fonction du nombre de fichier de configuration \u00E0 synchroniser. diff --git a/src/test/java/hudson/plugins/scm_sync_configuration/basic/ScmSyncConfigurationBasicTest.java b/src/test/java/hudson/plugins/scm_sync_configuration/basic/ScmSyncConfigurationBasicTest.java index 0e15fc1e..725876d0 100644 --- a/src/test/java/hudson/plugins/scm_sync_configuration/basic/ScmSyncConfigurationBasicTest.java +++ b/src/test/java/hudson/plugins/scm_sync_configuration/basic/ScmSyncConfigurationBasicTest.java @@ -1,37 +1,62 @@ package hudson.plugins.scm_sync_configuration.basic; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import hudson.model.Hudson; +import hudson.plugins.scm_sync_configuration.JenkinsFilesHelper; import hudson.plugins.scm_sync_configuration.util.ScmSyncConfigurationBaseTest; import hudson.plugins.test.utils.scms.ScmUnderTestSubversion; import java.io.File; +import jenkins.model.Jenkins; + import org.junit.Test; public class ScmSyncConfigurationBasicTest extends ScmSyncConfigurationBaseTest { - public ScmSyncConfigurationBasicTest() { - super(new ScmUnderTestSubversion()); - } - - @Test - public void shouldRetrieveMockedHudsonInstanceCorrectly() throws Throwable { - Hudson hudsonInstance = Hudson.getInstance(); - assertThat(hudsonInstance, is(notNullValue())); - assertThat(hudsonInstance.toString().split("@")[0], is(not(equalTo("hudson.model.Hudson")))); - } - - @Test - public void shouldVerifyIfHudsonRootDirectoryExists() throws Throwable { - - Hudson hudsonInstance = Hudson.getInstance(); - File hudsonRootDir = hudsonInstance.getRootDir(); - assertThat(hudsonRootDir, is(not(equalTo(null)))); - assertThat(hudsonRootDir.exists(), is(true)); - } + public ScmSyncConfigurationBasicTest() { + super(new ScmUnderTestSubversion()); + } + + @Test + public void shouldRetrieveMockedHudsonInstanceCorrectly() throws Throwable { + Jenkins jenkins = Jenkins.getInstance(); + assertNotNull("Jenkins instance must not be null", jenkins); + assertFalse("Expected a mocked Jenkins instance", jenkins.getClass().equals(Jenkins.class) || jenkins.getClass().equals(Hudson.class)); + } + + @Test + public void shouldVerifyIfHudsonRootDirectoryExists() throws Throwable { + Jenkins jenkins = Jenkins.getInstance(); + File jenkinsRootDir = jenkins.getRootDir(); + assertNotNull("Jenkins instance must not be null", jenkinsRootDir); + assertTrue("$JENKINS_HOME must be an existing directory", jenkinsRootDir.isDirectory()); + } + + @Test + public void testPathesOutsideJenkisRoot () throws Exception { + Jenkins jenkins = Jenkins.getInstance(); + File rootDirectory = jenkins.getRootDir().getAbsoluteFile(); + File parentDirectory = rootDirectory.getParentFile(); + assertNull("File outside $JENKINS_HOME should return null", JenkinsFilesHelper.buildPathRelativeToHudsonRoot(parentDirectory)); + assertNull("File outside $JENKINS_HOME should return null", JenkinsFilesHelper.buildPathRelativeToHudsonRoot(new File(parentDirectory, "foo.txt"))); + } + + @Test + public void testPathesInsideJenkisRoot () throws Exception { + Jenkins jenkins = Jenkins.getInstance(); + File rootDirectory = jenkins.getRootDir().getAbsoluteFile(); + File pathUnderTest = new File(rootDirectory, "config.xml"); + String result = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(pathUnderTest); + assertNotNull("File inside $JENKINS_HOME must not return null path", result); + assertEquals("Path " + pathUnderTest + " should resolve properly", result, "config.xml"); + pathUnderTest = new File(new File (rootDirectory, "someDir"), "foo.txt"); + result = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(pathUnderTest); + assertNotNull("File inside $JENKINS_HOME must not return null path", result); + assertEquals("Path " + pathUnderTest + " should resolve properly", result, "someDir/foo.txt"); + } } diff --git a/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsGitTest.java b/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsGitTest.java index c672c152..89474b78 100644 --- a/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsGitTest.java +++ b/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsGitTest.java @@ -1,15 +1,11 @@ package hudson.plugins.scm_sync_configuration.repository; import hudson.plugins.test.utils.scms.ScmUnderTestGit; -import org.junit.Ignore; public class HudsonExtensionsGitTest extends HudsonExtensionsTest { - public HudsonExtensionsGitTest() { - super(new ScmUnderTestGit()); - } - - public void shouldJobRenameBeCorrectlyImpactedOnSCM() throws Throwable { - super.shouldJobRenameBeCorrectlyImpactedOnSCM(); + public HudsonExtensionsGitTest() { + super(new ScmUnderTestGit()); } + } diff --git a/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsSubversionTest.java b/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsSubversionTest.java index bf081928..401c0c80 100644 --- a/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsSubversionTest.java +++ b/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsSubversionTest.java @@ -1,12 +1,96 @@ package hudson.plugins.scm_sync_configuration.repository; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.io.File; + +import org.codehaus.plexus.util.FileUtils; +import org.junit.Test; +import org.mockito.Mockito; + +import hudson.model.Item; +import hudson.model.Job; +import hudson.plugins.scm_sync_configuration.SCMManipulator; +import hudson.plugins.scm_sync_configuration.ScmSyncConfigurationPlugin; import hudson.plugins.test.utils.scms.ScmUnderTestSubversion; public class HudsonExtensionsSubversionTest extends HudsonExtensionsTest { - public HudsonExtensionsSubversionTest() { - super(new ScmUnderTestSubversion()); - } + public HudsonExtensionsSubversionTest() { + super(new ScmUnderTestSubversion()); + } + + @Test + public void shouldJobDeleteDoesntPerformAnyScmUpdate() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + // Let's checkout current scm view ... and commit something in it ... + SCMManipulator scmManipulator = createMockedScmManipulator(); + File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__tmpHierarchyForCommit"); + scmManipulator.checkout(checkoutDirectoryForVerifications); + final File hello1 = new File(checkoutDirectoryForVerifications.getAbsolutePath()+"/jobs/hello.txt"); + final File hello2 = new File(checkoutDirectoryForVerifications.getAbsolutePath()+"/hello2.txt"); + FileUtils.fileAppend(hello1.getAbsolutePath(), "hello world !"); + FileUtils.fileAppend(hello2.getAbsolutePath(), "hello world 2 !"); + scmManipulator.addFile(checkoutDirectoryForVerifications, "jobs/hello.txt"); + scmManipulator.addFile(checkoutDirectoryForVerifications, "hello2.txt"); + assertTrue("External check-in should succeed", scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit")); + + // Deleting fakeJob + Item mockedItem = Mockito.mock(Job.class); + File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/" ); + when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); + + sscItemListener.onDeleted(mockedItem); + + // Assert no hello file is present in current hudson root + assertThat(new File(this.getCurrentScmSyncConfigurationCheckoutDirectory()+"/jobs/hello.txt").exists(), is(false)); + assertThat(new File(this.getCurrentScmSyncConfigurationCheckoutDirectory()+"/hello2.txt").exists(), is(false)); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldJobRenameDoesntPerformAnyScmUpdate() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + // Let's checkout current scm view ... and commit something in it ... + SCMManipulator scmManipulator = createMockedScmManipulator(); + File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__tmpHierarchyForCommit"); + scmManipulator.checkout(checkoutDirectoryForVerifications); + final File hello1 = new File(checkoutDirectoryForVerifications.getAbsolutePath()+"/jobs/hello.txt"); + final File hello2 = new File(checkoutDirectoryForVerifications.getAbsolutePath()+"/hello2.txt"); + FileUtils.fileAppend(hello1.getAbsolutePath(), "hello world !"); + FileUtils.fileAppend(hello2.getAbsolutePath(), "hello world 2 !"); + scmManipulator.addFile(checkoutDirectoryForVerifications, "jobs/hello.txt"); + scmManipulator.addFile(checkoutDirectoryForVerifications, "hello2.txt"); + scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit"); + + // Renaming fakeJob to newFakeJob + Item mockedItem = Mockito.mock(Job.class); + File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/newFakeJob/" ); + when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); + + sscItemListener.onLocationChanged(mockedItem, "fakeJob", "newFakeJob"); + + // Assert no hello file is present in current hudson root + assertThat(new File(this.getCurrentScmSyncConfigurationCheckoutDirectory()+"/jobs/hello.txt").exists(), is(false)); + assertThat(new File(this.getCurrentScmSyncConfigurationCheckoutDirectory()+"/hello2.txt").exists(), is(false)); + + assertStatusManagerIsOk(); + } + } diff --git a/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsTest.java b/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsTest.java index bb4ebde4..c884c0f9 100644 --- a/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsTest.java +++ b/src/test/java/hudson/plugins/scm_sync_configuration/repository/HudsonExtensionsTest.java @@ -2,9 +2,9 @@ import hudson.XmlFile; import hudson.model.Item; +import hudson.model.AbstractItem; import hudson.model.Job; import hudson.model.Saveable; -import hudson.model.TopLevelItem; import hudson.plugins.scm_sync_configuration.SCMManipulator; import hudson.plugins.scm_sync_configuration.ScmSyncConfigurationPlugin; import hudson.plugins.scm_sync_configuration.extensions.ScmSyncConfigurationItemListener; @@ -15,380 +15,455 @@ import hudson.plugins.scm_sync_configuration.strategies.impl.JobConfigScmSyncStrategy; import hudson.plugins.scm_sync_configuration.util.ScmSyncConfigurationPluginBaseTest; import hudson.plugins.test.utils.scms.ScmUnderTest; + import org.codehaus.plexus.util.FileUtils; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; import org.springframework.core.io.ClassPathResource; import java.io.File; import java.util.List; +import jenkins.model.Jenkins; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -@PrepareForTest(ScmSyncConfigurationPlugin.class) public abstract class HudsonExtensionsTest extends ScmSyncConfigurationPluginBaseTest { - private ScmSyncConfigurationItemListener sscItemListener; - private ScmSyncConfigurationSaveableListener sscConfigurationSaveableListener; - - protected HudsonExtensionsTest(ScmUnderTest scmUnderTest) { - super(scmUnderTest); - } - - @Before - public void initObjectsUnderTests() throws Throwable{ - this.sscItemListener = new ScmSyncConfigurationItemListener(); - this.sscConfigurationSaveableListener = new ScmSyncConfigurationSaveableListener(); - - // Mocking ScmSyncConfigurationPlugin.getStrategyForSaveable() - ScmSyncConfigurationPlugin sscPlugin = spy(ScmSyncConfigurationPlugin.getInstance()); - sscPlugin.setBusiness(this.sscBusiness); - PowerMockito.doReturn(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES[0]).when(sscPlugin).getStrategyForSaveable(Mockito.any(Saveable.class), Mockito.any(File.class)); - } - - @Test - public void shouldJobRenameBeCorrectlyImpactedOnSCM() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - // Renaming fakeJob to newFakeJob - Item mockedItem = Mockito.mock(TopLevelItem.class); - File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/newFakeJob/" ); - when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); - when(mockedItem.getName()).thenReturn("newFakeJob"); + protected ScmSyncConfigurationItemListener sscItemListener; + protected ScmSyncConfigurationSaveableListener sscConfigurationSaveableListener; + + protected HudsonExtensionsTest(ScmUnderTest scmUnderTest) { + super(scmUnderTest); + } + + @Before + public void initObjectsUnderTests() throws Throwable{ + this.sscItemListener = new ScmSyncConfigurationItemListener(); + this.sscConfigurationSaveableListener = new ScmSyncConfigurationSaveableListener(); + } + @Test + public void shouldJobRenameBeCorrectlyImpactedOnSCM() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + // Renaming fakeJob to newFakeJob + Item mockedItem = Mockito.mock(Job.class); + File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/newFakeJob/" ); + when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); + when(mockedItem.getName()).thenReturn("newFakeJob"); + when(mockedItem.getParent()).thenReturn(null); // We should duplicate files in fakeJob to newFakeJob File oldJobDirectory = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/"); FileUtils.copyDirectory(oldJobDirectory, mockedItemRootDir); - sscItemListener.onRenamed(mockedItem, "fakeJob", "newFakeJob"); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobRenameBeCorrectlyImpactedOnSCM/"); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldJobAddBeCorrectlyImpactedOnSCM() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - File jobDirectory = new File(getCurrentHudsonRootDirectory() + "/jobs/newFakeJob/" ); - File configFile = new File(jobDirectory.getAbsolutePath() + File.separator + "config.xml"); - jobDirectory.mkdir(); - FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobAddBeCorrectlyImpactedOnSCM/jobs/newFakeJob/config.xml").getFile(), configFile); - - // Creating fake new job - Item mockedItem = Mockito.mock(TopLevelItem.class); - when(mockedItem.getRootDir()).thenReturn(jobDirectory); - - sscItemListener.onCreated(mockedItem); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); - - sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobAddBeCorrectlyImpactedOnSCM/"); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldJobModificationBeCorrectlyImpactedOnSCM() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - File jobDirectory = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/" ); - File configFile = new File(jobDirectory.getAbsolutePath() + File.separator + "config.xml"); - - // Creating fake new job - Item mockedItem = Mockito.mock(TopLevelItem.class); - when(mockedItem.getRootDir()).thenReturn(jobDirectory); - - sscItemListener.onCreated(mockedItem); - - sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); - - FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobModificationBeCorrectlyImpactedOnSCM/jobs/fakeJob/config.xml").getFile(), configFile); - - sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobModificationBeCorrectlyImpactedOnSCM/"); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldConfigModificationBeCorrectlyImpactedOnSCM() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - File configFile = new File(getCurrentHudsonRootDirectory() + "/hudson.tasks.Shell.xml" ); - - // Creating fake new job - Item mockedItem = Mockito.mock(Item.class); - when(mockedItem.getRootDir()).thenReturn(configFile); - - sscItemListener.onCreated(mockedItem); - sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); - - FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.shouldConfigModificationBeCorrectlyImpactedOnSCM/hudson.tasks.Shell.xml").getFile(), configFile); - - sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldConfigModificationBeCorrectlyImpactedOnSCM/"); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldJobRenameDoesntPerformAnyScmUpdate() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - // Let's checkout current scm view ... and commit something in it ... - SCMManipulator scmManipulator = createMockedScmManipulator(); - File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__tmpHierarchyForCommit"); - scmManipulator.checkout(checkoutDirectoryForVerifications); - final File hello1 = new File(checkoutDirectoryForVerifications.getAbsolutePath()+"/jobs/hello.txt"); - final File hello2 = new File(checkoutDirectoryForVerifications.getAbsolutePath()+"/hello2.txt"); - FileUtils.fileAppend(hello1.getAbsolutePath(), "hello world !"); - FileUtils.fileAppend(hello2.getAbsolutePath(), "hello world 2 !"); - scmManipulator.addFile(checkoutDirectoryForVerifications, "jobs/hello.txt"); - scmManipulator.addFile(checkoutDirectoryForVerifications, "hello2.txt"); - scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit"); - - // Renaming fakeJob to newFakeJob - Item mockedItem = Mockito.mock(Item.class); - File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/newFakeJob/" ); - when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); - - sscItemListener.onRenamed(mockedItem, "fakeJob", "newFakeJob"); - - // Assert no hello file is present in current hudson root - assertThat(new File(this.getCurrentScmSyncConfigurationCheckoutDirectory()+"/jobs/hello.txt").exists(), is(false)); - assertThat(new File(this.getCurrentScmSyncConfigurationCheckoutDirectory()+"/hello2.txt").exists(), is(false)); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldJobDeleteBeCorrectlyImpactedOnSCM() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - // Deleting fakeJob - Item mockedItem = Mockito.mock(TopLevelItem.class); - File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/" ); - when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); - - sscItemListener.onDeleted(mockedItem); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobDeleteBeCorrectlyImpactedOnSCM" + getSuffixForTestFiles() + "/"); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldJobDeleteWithTwoJobsBeCorrectlyImpactedOnSCM() throws Throwable { - String newFakeJob = "expected-scm-hierarchies/HudsonExtensionsTest.shouldJobDeleteWithTwoJobsBeCorrectlyImpactedOnSCM/jobs/newFakeJob"; - FileUtils.copyDirectoryStructure(new ClassPathResource(newFakeJob).getFile(), new File(getCurrentHudsonRootDirectory() + File.separator + "jobs" + File.separator + "newFakeJob")); - - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - // Deleting fakeJob - Item mockedItem = Mockito.mock(TopLevelItem.class); - File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/" ); - when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); - - sscItemListener.onDeleted(mockedItem); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobDeleteWithTwoJobsBeCorrectlyImpactedOnSCM/"); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldJobDeleteDoesntPerformAnyScmUpdate() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - // Let's checkout current scm view ... and commit something in it ... - SCMManipulator scmManipulator = createMockedScmManipulator(); - File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__tmpHierarchyForCommit"); - scmManipulator.checkout(checkoutDirectoryForVerifications); - final File hello1 = new File(checkoutDirectoryForVerifications.getAbsolutePath()+"/jobs/hello.txt"); - final File hello2 = new File(checkoutDirectoryForVerifications.getAbsolutePath()+"/hello2.txt"); - FileUtils.fileAppend(hello1.getAbsolutePath(), "hello world !"); - FileUtils.fileAppend(hello2.getAbsolutePath(), "hello world 2 !"); - scmManipulator.addFile(checkoutDirectoryForVerifications, "jobs/hello.txt"); - scmManipulator.addFile(checkoutDirectoryForVerifications, "hello2.txt"); - scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit"); - - // Deleting fakeJob - Item mockedItem = Mockito.mock(Item.class); - File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/" ); - when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); - - sscItemListener.onDeleted(mockedItem); - - // Assert no hello file is present in current hudson root - assertThat(new File(this.getCurrentScmSyncConfigurationCheckoutDirectory()+"/jobs/hello.txt").exists(), is(false)); - assertThat(new File(this.getCurrentScmSyncConfigurationCheckoutDirectory()+"/hello2.txt").exists(), is(false)); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldReloadAllFilesUpdateScmAndReloadAllFiles() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - // Let's checkout current scm view ... and commit something in it ... - SCMManipulator scmManipulator = createMockedScmManipulator(); - File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__tmpHierarchyForCommit"); - scmManipulator.checkout(checkoutDirectoryForVerifications); - - verifyCurrentScmContentMatchesCurrentHudsonDir(true); - - final File configFile = new File(checkoutDirectoryForVerifications.getAbsolutePath() + "/config.xml"); - FileUtils.fileAppend(configFile.getAbsolutePath(), "toto"); - scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit on config file"); - - final File configJobFile = new File(checkoutDirectoryForVerifications.getAbsolutePath() + "/jobs/fakeJob/config.xml"); - FileUtils.fileAppend(configJobFile.getAbsolutePath(), "titi"); - scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit on jonb file"); - - verifyCurrentScmContentMatchesCurrentHudsonDir(false); - - // Reload config - List syncedFiles = sscBusiness.reloadAllFilesFromScm(); - - verifyCurrentScmContentMatchesCurrentHudsonDir(true); - - assertThat(syncedFiles.size(), is(2)); - assertThat(syncedFiles.contains(new File(getCurrentHudsonRootDirectory().getAbsolutePath() + "/config.xml")), is(true)); - assertThat(syncedFiles.contains(new File(getCurrentHudsonRootDirectory().getAbsolutePath() + "/jobs/fakeJob/config.xml")), is(true)); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldReloadAllFilesUpdateScmAndReloadAllFilesWithFileAdd() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - // Let's checkout current scm view ... and commit something in it ... - SCMManipulator scmManipulator = createMockedScmManipulator(); - File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__tmpHierarchyForCommit"); - scmManipulator.checkout(checkoutDirectoryForVerifications); + sscItemListener.onLocationChanged(mockedItem, "fakeJob", "newFakeJob"); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobRenameBeCorrectlyImpactedOnSCM/"); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldJobAddBeCorrectlyImpactedOnSCM() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + File jobDirectory = new File(getCurrentHudsonRootDirectory() + "/jobs/newFakeJob/" ); + File configFile = new File(jobDirectory.getAbsolutePath() + File.separator + "config.xml"); + jobDirectory.mkdir(); + FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobAddBeCorrectlyImpactedOnSCM/jobs/newFakeJob/config.xml").getFile(), configFile); + + // Creating fake new job + Item mockedItem = Mockito.mock(Job.class); + when(mockedItem.getRootDir()).thenReturn(jobDirectory); + + sscItemListener.onCreated(mockedItem); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); + + sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobAddBeCorrectlyImpactedOnSCM/"); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldJobModificationBeCorrectlyImpactedOnSCM() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + File jobDirectory = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/" ); + File configFile = new File(jobDirectory.getAbsolutePath() + File.separator + "config.xml"); + + // Creating fake new job + Item mockedItem = Mockito.mock(Job.class); + when(mockedItem.getRootDir()).thenReturn(jobDirectory); + + sscItemListener.onCreated(mockedItem); + + sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); + + FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobModificationBeCorrectlyImpactedOnSCM/jobs/fakeJob/config.xml").getFile(), configFile); + + sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobModificationBeCorrectlyImpactedOnSCM/"); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldConfigModificationBeCorrectlyImpactedOnSCM() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + File configFile = new File(getCurrentHudsonRootDirectory() + "/hudson.tasks.Shell.xml" ); + + // Creating fake new plugin config + Item mockedItem = Mockito.mock(Item.class); + + sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); + + FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.shouldConfigModificationBeCorrectlyImpactedOnSCM/hudson.tasks.Shell.xml").getFile(), configFile); + + sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldConfigModificationBeCorrectlyImpactedOnSCM/"); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldJobDeleteBeCorrectlyImpactedOnSCM() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + // Deleting fakeJob + Item mockedItem = Mockito.mock(Job.class); + File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/" ); + when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); + + sscItemListener.onDeleted(mockedItem); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobDeleteBeCorrectlyImpactedOnSCM" + getSuffixForTestFiles() + "/"); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldJobDeleteWithTwoJobsBeCorrectlyImpactedOnSCM() throws Throwable { + String newFakeJob = "expected-scm-hierarchies/HudsonExtensionsTest.shouldJobDeleteWithTwoJobsBeCorrectlyImpactedOnSCM/jobs/newFakeJob"; + FileUtils.copyDirectoryStructure(new ClassPathResource(newFakeJob).getFile(), new File(getCurrentHudsonRootDirectory() + File.separator + "jobs" + File.separator + "newFakeJob")); + + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + // Deleting fakeJob + Item mockedItem = Mockito.mock(Job.class); + File mockedItemRootDir = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/" ); + when(mockedItem.getRootDir()).thenReturn(mockedItemRootDir); + + sscItemListener.onDeleted(mockedItem); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.shouldJobDeleteWithTwoJobsBeCorrectlyImpactedOnSCM/"); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldReloadAllFilesUpdateScmAndReloadAllFiles() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + // Let's checkout current scm view ... and commit something in it ... + SCMManipulator scmManipulator = createMockedScmManipulator(); + File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__tmpHierarchyForCommit"); + scmManipulator.checkout(checkoutDirectoryForVerifications); + + verifyCurrentScmContentMatchesCurrentHudsonDir(true); + + final File configFile = new File(checkoutDirectoryForVerifications.getAbsolutePath() + "/config.xml"); + FileUtils.fileAppend(configFile.getAbsolutePath(), "toto"); + scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit on config file"); + + final File configJobFile = new File(checkoutDirectoryForVerifications.getAbsolutePath() + "/jobs/fakeJob/config.xml"); + FileUtils.fileAppend(configJobFile.getAbsolutePath(), "titi"); + scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit on jonb file"); + + verifyCurrentScmContentMatchesCurrentHudsonDir(false); + + // Reload config + List syncedFiles = sscBusiness.reloadAllFilesFromScm(); + + verifyCurrentScmContentMatchesCurrentHudsonDir(true); + + assertThat(syncedFiles.size(), is(2)); + assertThat(syncedFiles.contains(new File(getCurrentHudsonRootDirectory().getAbsolutePath() + "/config.xml")), is(true)); + assertThat(syncedFiles.contains(new File(getCurrentHudsonRootDirectory().getAbsolutePath() + "/jobs/fakeJob/config.xml")), is(true)); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldReloadAllFilesUpdateScmAndReloadAllFilesWithFileAdd() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + // Let's checkout current scm view ... and commit something in it ... + SCMManipulator scmManipulator = createMockedScmManipulator(); + File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__tmpHierarchyForCommit"); + scmManipulator.checkout(checkoutDirectoryForVerifications); // Verifying there isn't any difference between hudson and scm repo once every file are synchronized - verifyCurrentScmContentMatchesCurrentHudsonDir(true); - - final File addedFile = new File(checkoutDirectoryForVerifications.getAbsolutePath() + "/myConfigFile.xml"); - FileUtils.fileWrite(addedFile.getAbsolutePath(), "toto"); - scmManipulator.addFile(checkoutDirectoryForVerifications, "myConfigFile.xml"); - scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit for add file"); - - final String jobDir = checkoutDirectoryForVerifications.getAbsolutePath() + "/jobs/myJob"; - FileUtils.mkdir(jobDir); - final File addedJobFile = new File(jobDir + "/config.xml"); - FileUtils.fileWrite(addedJobFile.getAbsolutePath(), "titi"); - scmManipulator.addFile(checkoutDirectoryForVerifications, "jobs/myJob"); - scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit for add job file"); - - verifyCurrentScmContentMatchesCurrentHudsonDir(false); - - // Reload config - List syncedFiles = sscBusiness.reloadAllFilesFromScm(); - - verifyCurrentScmContentMatchesCurrentHudsonDir(true); - - assertThat(syncedFiles.size(), is(2)); - assertThat(syncedFiles.contains(new File(getCurrentHudsonRootDirectory().getAbsolutePath() + "/myConfigFile.xml")), is(true)); - assertThat(syncedFiles.contains(new File(getCurrentHudsonRootDirectory().getAbsolutePath() + "/jobs/myJob")), is(true)); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldFileWhichHaveToBeInSCM() throws Throwable { + verifyCurrentScmContentMatchesCurrentHudsonDir(true); + + final File addedFile = new File(checkoutDirectoryForVerifications.getAbsolutePath() + "/myConfigFile.xml"); + FileUtils.fileWrite(addedFile.getAbsolutePath(), "toto"); + scmManipulator.addFile(checkoutDirectoryForVerifications, "myConfigFile.xml"); + scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit for add file"); + + final String jobDir = checkoutDirectoryForVerifications.getAbsolutePath() + "/jobs/myJob"; + FileUtils.mkdir(jobDir); + final File addedJobFile = new File(jobDir + "/config.xml"); + FileUtils.fileWrite(addedJobFile.getAbsolutePath(), "titi"); + scmManipulator.addFile(checkoutDirectoryForVerifications, "jobs/myJob"); + scmManipulator.checkinFiles(checkoutDirectoryForVerifications, "external commit for add job file"); + + verifyCurrentScmContentMatchesCurrentHudsonDir(false); + + // Reload config + List syncedFiles = sscBusiness.reloadAllFilesFromScm(); + + verifyCurrentScmContentMatchesCurrentHudsonDir(true); + + assertThat(syncedFiles.size(), is(2)); + assertThat(syncedFiles.contains(new File(getCurrentHudsonRootDirectory().getAbsolutePath() + "/myConfigFile.xml")), is(true)); + assertThat(syncedFiles.contains(new File(getCurrentHudsonRootDirectory().getAbsolutePath() + "/jobs/myJob")), is(true)); + + assertStatusManagerIsOk(); + } + + @Test + public void testJobNameStartingWithDash() throws Exception { + createSCMMock(); + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + File jobDirectory = new File(getCurrentHudsonRootDirectory(), "jobs/-newFakeJob/" ); + File configFile = new File(jobDirectory, "config.xml"); + jobDirectory.mkdir(); + FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/-newFakeJob/config.xml").getFile(), configFile); + + // Creating fake new job + Item mockedItem = Mockito.mock(Job.class); + when(mockedItem.getName()).thenReturn("-newFakeJob"); + when(mockedItem.getRootDir()).thenReturn(jobDirectory); + + sscItemListener.onCreated(mockedItem); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); + + sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/"); + + assertStatusManagerIsOk(); + + // Now delete it again + assertTrue("Config file deletion", configFile.delete()); + assertTrue("Job dir deletion", jobDirectory.delete()); + + sscItemListener.onDeleted(mockedItem); + + verifyCurrentScmContentMatchesHierarchy("hudsonRootBaseTemplate/"); + + assertStatusManagerIsOk(); + } + + @Test + public void testJobNameWithBlanks() throws Exception { + createSCMMock(); + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + File jobDirectory = new File(getCurrentHudsonRootDirectory(), "jobs/new fake Job/" ); + File configFile = new File(jobDirectory, "config.xml"); + jobDirectory.mkdir(); + FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/jobs/new fake Job/config.xml").getFile(), configFile); + + // Creating fake new job + Item mockedItem = Mockito.mock(Job.class); + when(mockedItem.getName()).thenReturn("new fake Job"); + when(mockedItem.getRootDir()).thenReturn(jobDirectory); + + sscItemListener.onCreated(mockedItem); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); + + sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/"); + + assertStatusManagerIsOk(); + + // Now delete it again + assertTrue("Config file deletion", configFile.delete()); + assertTrue("Job dir deletion", jobDirectory.delete()); + + sscItemListener.onDeleted(mockedItem); + + verifyCurrentScmContentMatchesHierarchy("hudsonRootBaseTemplate/"); + + assertStatusManagerIsOk(); + } + + @Test + public void testJobRenameWithBlanksAndDash() throws Exception { + createSCMMock(); + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + File jobDirectory = new File(getCurrentHudsonRootDirectory(), "jobs/-newFakeJob/" ); + File configFile = new File(jobDirectory, "config.xml"); + jobDirectory.mkdir(); + FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/-newFakeJob/config.xml").getFile(), configFile); + + // Creating fake new job + Item mockedItem = Mockito.mock(Job.class); + when(mockedItem.getName()).thenReturn("-newFakeJob"); + when(mockedItem.getRootDir()).thenReturn(jobDirectory); + + sscItemListener.onCreated(mockedItem); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); + + sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(configFile)); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/"); + + assertStatusManagerIsOk(); + + // Now fake a rename + assertTrue("Config file deletion", configFile.delete()); + assertTrue("Job dir deletion", jobDirectory.delete()); + jobDirectory = new File(getCurrentHudsonRootDirectory(), "jobs/new fake Job/" ); + configFile = new File(jobDirectory, "config.xml"); + jobDirectory.mkdir(); + FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/jobs/new fake Job/config.xml").getFile(), configFile); + + Item mockedRenamedItem = Mockito.mock(Job.class); + when(mockedRenamedItem.getName()).thenReturn("new fake Job"); + when(mockedRenamedItem.getRootDir()).thenReturn(jobDirectory); + + sscItemListener.onLocationChanged(mockedRenamedItem, "-newFakeJob", "new fake Job"); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/"); + + assertStatusManagerIsOk(); + + // And while we're at it: let's rename it back + assertTrue("Config file deletion", configFile.delete()); + assertTrue("Job dir deletion", jobDirectory.delete()); + jobDirectory = new File(getCurrentHudsonRootDirectory(), "jobs/-newFakeJob/" ); + configFile = new File(jobDirectory, "config.xml"); + jobDirectory.mkdir(); + FileUtils.copyFile(new ClassPathResource("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/-newFakeJob/config.xml").getFile(), configFile); + + sscItemListener.onLocationChanged(mockedItem, "new fake Job", "-newFakeJob"); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/"); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldFileWhichHaveToBeInSCM() throws Throwable { // IMPORTANT NOTE : // For every tested files in this test, file path should exist in // HudsonExtensionsTest.shouldFileWhichHaveToBeInSCM/ directory - assertStrategy(JenkinsConfigScmSyncStrategy.class, Mockito.mock(Saveable.class), "config.xml"); - assertStrategy(BasicPluginsConfigScmSyncStrategy.class, Mockito.mock(Saveable.class), "hudson.scm.SubversionSCM.xml"); - assertStrategy(null, Mockito.mock(Saveable.class), "hudson.config.xml2"); - assertStrategy(null, Mockito.mock(Saveable.class), "nodeMonitors.xml"); - assertStrategy(null, Mockito.mock(Saveable.class), "toto" + File.separator + "hudson.config.xml"); - - assertStrategy(null, Mockito.mock(Job.class), "toto" + File.separator + "config.xml"); - assertStrategy(null, Mockito.mock(Job.class), "jobs" + File.separator + "config.xml"); - assertStrategy(null, Mockito.mock(Saveable.class), "jobs" + File.separator + "myJob" + File.separator + "config.xml"); - assertStrategy(JobConfigScmSyncStrategy.class, Mockito.mock(TopLevelItem.class), "jobs" + File.separator + "myJob" + File.separator + "config.xml"); - assertStrategy(null, Mockito.mock(Job.class), "jobs" + File.separator + "myJob" + File.separator + "config2.xml"); - } - - private void assertStrategy(Class expectedStrategyClass, Saveable saveableInstance, String targetPath) { - ScmSyncStrategy strategy = ScmSyncConfigurationPlugin.getInstance().getStrategyForSaveable(saveableInstance, new File(getCurrentHudsonRootDirectory() + File.separator + targetPath)); - if (expectedStrategyClass == null) { - assertThat(strategy, nullValue()); - } - else { - assertThat(strategy, notNullValue()); - assertThat(strategy, instanceOf(expectedStrategyClass)); - } - } + assertStrategy(JenkinsConfigScmSyncStrategy.class, Mockito.mock(Saveable.class), "config.xml"); + assertStrategy(BasicPluginsConfigScmSyncStrategy.class, Mockito.mock(Saveable.class), "hudson.scm.SubversionSCM.xml"); + assertStrategy(null, Mockito.mock(Saveable.class), "hudson.config.xml2"); + assertStrategy(null, Mockito.mock(Saveable.class), "nodeMonitors.xml"); + assertStrategy(null, Mockito.mock(Saveable.class), "toto" + File.separator + "hudson.config.xml"); + + assertStrategy(null, Mockito.mock(Job.class), "toto" + File.separator + "config.xml"); + assertStrategy(null, Mockito.mock(Job.class), "jobs" + File.separator + "config.xml"); + assertStrategy(null, Mockito.mock(Saveable.class), "jobs" + File.separator + "myJob" + File.separator + "config.xml"); + assertStrategy(JobConfigScmSyncStrategy.class, Mockito.mock(Job.class), "jobs" + File.separator + "myJob" + File.separator + "config.xml"); + assertStrategy(JobConfigScmSyncStrategy.class, Mockito.mock(Job.class), "jobs" + File.separator + "myFolder" + File.separator + "jobs" + File.separator + "myJob" + File.separator + "config.xml"); + assertStrategy(JobConfigScmSyncStrategy.class, Mockito.mock(AbstractItem.class), "jobs" + File.separator + "myFolder" + File.separator + "config.xml"); + assertStrategy(null, Mockito.mock(Job.class), "jobs" + File.separator + "myJob" + File.separator + "config2.xml"); + } + + private void assertStrategy(Class expectedStrategyClass, Saveable saveableInstance, String targetPath) { + ScmSyncStrategy strategy = ScmSyncConfigurationPlugin.getInstance().getStrategyForSaveable(saveableInstance, new File(getCurrentHudsonRootDirectory() + File.separator + targetPath)); + if (expectedStrategyClass == null) { + assertThat(strategy, nullValue()); + } + else { + assertThat(strategy, notNullValue()); + assertThat(strategy, instanceOf(expectedStrategyClass)); + } + } @Override - protected String getHudsonRootBaseTemplate(){ + protected String getHudsonRootBaseTemplate(){ if("shouldFileWhichHaveToBeInSCM".equals(testName.getMethodName())){ return "HudsonExtensionsTest.shouldFileWhichHaveToBeInSCM/"; } - return "hudsonRootBaseTemplate/"; - } + return "hudsonRootBaseTemplate/"; + } + + @Test + public void testPageMatchers() throws Exception { + assertStrategy(JobConfigScmSyncStrategy.class, Jenkins.getInstance().getRootUrl() + "job/jobName/configure"); + assertStrategy(JobConfigScmSyncStrategy.class, Jenkins.getInstance().getRootUrl() + "job/folderName/job/jobName/configure"); + assertStrategy(null, Jenkins.getInstance().getRootUrl() + "job/folderName/job/configure"); + assertStrategy(null, Jenkins.getInstance().getRootUrl() + "job/folderName/job/someThing/configure/foo"); + } + + private void assertStrategy(Class expectedStrategyClass, String url) { + ScmSyncStrategy strategy = ScmSyncConfigurationPlugin.getInstance().getStrategyForURL(url); + if (expectedStrategyClass == null) { + assertThat(strategy, nullValue()); + } + else { + assertThat(strategy, notNullValue()); + assertThat(strategy, instanceOf(expectedStrategyClass)); + } + } + } diff --git a/src/test/java/hudson/plugins/scm_sync_configuration/repository/InitRepositoryTest.java b/src/test/java/hudson/plugins/scm_sync_configuration/repository/InitRepositoryTest.java index 75107891..434a0cd0 100644 --- a/src/test/java/hudson/plugins/scm_sync_configuration/repository/InitRepositoryTest.java +++ b/src/test/java/hudson/plugins/scm_sync_configuration/repository/InitRepositoryTest.java @@ -17,78 +17,78 @@ @PrepareForTest(SCM.class) public abstract class InitRepositoryTest extends ScmSyncConfigurationPluginBaseTest { - - protected InitRepositoryTest(ScmUnderTest scmUnderTest) { - super(scmUnderTest); - } - - @Test - public void shouldNotInitializeAnyRepositoryWhenScmContextIsEmpty() throws Throwable { - ScmContext emptyContext = new ScmContext(null, null); - sscBusiness.init(emptyContext); - assertThat(sscBusiness.scmCheckoutDirectorySettledUp(emptyContext), is(false)); - - emptyContext = new ScmContext(null, getSCMRepositoryURL()); - sscBusiness.init(emptyContext); - assertThat(sscBusiness.scmCheckoutDirectorySettledUp(emptyContext), is(false)); - - createSCMMock(null); - assertThat(sscBusiness.scmCheckoutDirectorySettledUp(emptyContext), is(false)); - - assertStatusManagerIsNull(); - } - - @Test - @Ignore("Not yet implemented ! (it is difficult because svn list/log has not yet been implemented in svnjava impl") - public void shouldInitializeLocalRepositoryWhenScmContextIsCorrentAndEvenIfScmDirectoryDoesntExist() throws Throwable { - createSCMMock(); - assertThat(sscBusiness.scmCheckoutDirectorySettledUp(scmContext), is(true)); - } - - @Test - public void shouldResetCheckoutConfigurationDirectoryWhenAsked() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // After init, local checkouted repository should exists - assertThat(getCurrentScmSyncConfigurationCheckoutDirectory().exists(), is(true)); - - // Populating checkoutConfiguration directory .. - File fileWhichShouldBeDeletedAfterReset = new File(getCurrentScmSyncConfigurationCheckoutDirectory().getAbsolutePath()+"/hello.txt"); - assertThat(fileWhichShouldBeDeletedAfterReset.createNewFile(), is(true)); - FileUtils.fileWrite(fileWhichShouldBeDeletedAfterReset.getAbsolutePath(), "Hello world !"); - - // Reseting the repository, without cleanup - sscBusiness.initializeRepository(scmContext, false); - assertThat(fileWhichShouldBeDeletedAfterReset.exists(), is(true)); - - // Reseting the repository with cleanup - sscBusiness.initializeRepository(scmContext, true); - assertThat(fileWhichShouldBeDeletedAfterReset.exists(), is(false)); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldSynchronizeHudsonFiles() throws Throwable { - // Initializing the repository... - createSCMMock(); - - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); - - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); - - assertStatusManagerIsOk(); - } - - @Test - public void shouldInitializeLocalRepositoryWhenScmContextIsCorrect() - throws Throwable { - createSCMMock(); - assertThat(sscBusiness.scmCheckoutDirectorySettledUp(scmContext), is(true)); - - assertStatusManagerIsOk(); - } + + protected InitRepositoryTest(ScmUnderTest scmUnderTest) { + super(scmUnderTest); + } + + @Test + public void shouldNotInitializeAnyRepositoryWhenScmContextIsEmpty() throws Throwable { + ScmContext emptyContext = new ScmContext(null, null); + sscBusiness.init(emptyContext); + assertThat(sscBusiness.scmCheckoutDirectorySettledUp(emptyContext), is(false)); + + emptyContext = new ScmContext(null, getSCMRepositoryURL()); + sscBusiness.init(emptyContext); + assertThat(sscBusiness.scmCheckoutDirectorySettledUp(emptyContext), is(false)); + + createSCMMock(null); + assertThat(sscBusiness.scmCheckoutDirectorySettledUp(emptyContext), is(false)); + + assertStatusManagerIsNull(); + } + + @Test + @Ignore("Not yet implemented ! (it is difficult because svn list/log has not yet been implemented in svnjava impl") + public void shouldInitializeLocalRepositoryWhenScmContextIsCorrentAndEvenIfScmDirectoryDoesntExist() throws Throwable { + createSCMMock(); + assertThat(sscBusiness.scmCheckoutDirectorySettledUp(scmContext), is(true)); + } + + @Test + public void shouldResetCheckoutConfigurationDirectoryWhenAsked() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // After init, local checked out repository should exist + assertThat(getCurrentScmSyncConfigurationCheckoutDirectory().exists(), is(true)); + + // Populating checkoutConfiguration directory .. + File fileWhichShouldBeDeletedAfterReset = new File(getCurrentScmSyncConfigurationCheckoutDirectory().getAbsolutePath()+"/hello.txt"); + assertThat(fileWhichShouldBeDeletedAfterReset.createNewFile(), is(true)); + FileUtils.fileWrite(fileWhichShouldBeDeletedAfterReset.getAbsolutePath(), "Hello world !"); + + // Reseting the repository, without cleanup + sscBusiness.initializeRepository(scmContext, false); + assertThat(fileWhichShouldBeDeletedAfterReset.exists(), is(true)); + + // Reseting the repository with cleanup + sscBusiness.initializeRepository(scmContext, true); + assertThat(fileWhichShouldBeDeletedAfterReset.exists(), is(false)); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldSynchronizeHudsonFiles() throws Throwable { + // Initializing the repository... + createSCMMock(); + + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/InitRepositoryTest.shouldSynchronizeHudsonFiles/"); + + assertStatusManagerIsOk(); + } + + @Test + public void shouldInitializeLocalRepositoryWhenScmContextIsCorrect() + throws Throwable { + createSCMMock(); + assertThat(sscBusiness.scmCheckoutDirectorySettledUp(scmContext), is(true)); + + assertStatusManagerIsOk(); + } } diff --git a/src/test/java/hudson/plugins/scm_sync_configuration/strategies/impl/JobConfigScmSyncStrategyTest.java b/src/test/java/hudson/plugins/scm_sync_configuration/strategies/impl/JobConfigScmSyncStrategyTest.java index e12fcf5b..153223bc 100644 --- a/src/test/java/hudson/plugins/scm_sync_configuration/strategies/impl/JobConfigScmSyncStrategyTest.java +++ b/src/test/java/hudson/plugins/scm_sync_configuration/strategies/impl/JobConfigScmSyncStrategyTest.java @@ -1,19 +1,13 @@ package hudson.plugins.scm_sync_configuration.strategies.impl; import hudson.XmlFile; -import hudson.model.Item; import hudson.model.Job; import hudson.plugins.scm_sync_configuration.ScmSyncConfigurationPlugin; -import hudson.plugins.scm_sync_configuration.extensions.ScmSyncConfigurationItemListener; import hudson.plugins.scm_sync_configuration.extensions.ScmSyncConfigurationSaveableListener; -import hudson.plugins.scm_sync_configuration.strategies.ScmSyncStrategy; -import hudson.plugins.scm_sync_configuration.util.ScmSyncConfigurationBaseTest; import hudson.plugins.scm_sync_configuration.util.ScmSyncConfigurationPluginBaseTest; -import hudson.plugins.test.utils.scms.ScmUnderTest; import hudson.plugins.test.utils.scms.ScmUnderTestSubversion; import org.codehaus.plexus.PlexusContainerException; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; -import org.codehaus.plexus.util.FileUtils; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -35,34 +29,35 @@ public JobConfigScmSyncStrategyTest() { } @Before - public void initObjectsUnderTests() throws Throwable{ - this.sscConfigurationSaveableListener = new ScmSyncConfigurationSaveableListener(); + public void initObjectsUnderTests() throws Throwable{ + this.sscConfigurationSaveableListener = new ScmSyncConfigurationSaveableListener(); } + @Override protected String getHudsonRootBaseTemplate(){ - return "jobConfigStrategyTemplate/"; - } + return "jobConfigStrategyTemplate/"; + } // Reproducing JENKINS-17545 @Test public void shouldConfigInSubmodulesNotSynced() throws ComponentLookupException, PlexusContainerException, IOException { - // Initializing the repository... - createSCMMock(); + // Initializing the repository... + createSCMMock(); - // Synchronizing hudson config files - sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); + // Synchronizing hudson config files + sscBusiness.synchronizeAllConfigs(ScmSyncConfigurationPlugin.AVAILABLE_STRATEGIES); File subModuleConfigFile = new File(getCurrentHudsonRootDirectory() + "/jobs/fakeJob/modules/submodule/config.xml" ); // Creating fake new item - Job mockedItem = Mockito.mock(Job.class); + Job mockedItem = Mockito.mock(Job.class); when(mockedItem.getRootDir()).thenReturn(subModuleConfigFile.getParentFile()); sscConfigurationSaveableListener.onChange(mockedItem, new XmlFile(subModuleConfigFile)); - verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/JobConfigScmSyncStrategyTest.shouldConfigInSubmodulesNotSynced/"); + verifyCurrentScmContentMatchesHierarchy("expected-scm-hierarchies/JobConfigScmSyncStrategyTest.shouldConfigInSubmodulesNotSynced/"); - assertStatusManagerIsOk(); + assertStatusManagerIsOk(); } } diff --git a/src/test/java/hudson/plugins/scm_sync_configuration/util/ScmSyncConfigurationBaseTest.java b/src/test/java/hudson/plugins/scm_sync_configuration/util/ScmSyncConfigurationBaseTest.java index 5de847fa..2bef1afe 100644 --- a/src/test/java/hudson/plugins/scm_sync_configuration/util/ScmSyncConfigurationBaseTest.java +++ b/src/test/java/hudson/plugins/scm_sync_configuration/util/ScmSyncConfigurationBaseTest.java @@ -16,6 +16,7 @@ import hudson.plugins.scm_sync_configuration.xstream.migration.ScmSyncConfigurationPOJO; import hudson.plugins.test.utils.DirectoryUtils; import hudson.plugins.test.utils.scms.ScmUnderTest; + import org.codehaus.plexus.PlexusContainerException; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.codehaus.plexus.util.FileUtils; @@ -32,13 +33,15 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.core.io.ClassPathResource; +import com.google.common.collect.Lists; + import java.io.File; import java.io.IOException; import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +import jenkins.model.Jenkins; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.spy; @@ -46,191 +49,204 @@ @RunWith(PowerMockRunner.class) @PowerMockIgnore({ "org.tmatesoft.svn.*" }) -@PrepareForTest({Hudson.class, SCM.class, ScmSyncSubversionSCM.class, PluginWrapper.class}) +@PrepareForTest({Hudson.class, Jenkins.class, SCM.class, ScmSyncSubversionSCM.class, PluginWrapper.class}) public abstract class ScmSyncConfigurationBaseTest { - - @Rule protected TestName testName = new TestName(); - private File currentTestDirectory = null; - private File curentLocalRepository = null; - private File currentHudsonRootDirectory = null; - protected ScmSyncConfigurationBusiness sscBusiness = null; - protected ScmContext scmContext = null; - - private ScmUnderTest scmUnderTest; - - protected ScmSyncConfigurationBaseTest(ScmUnderTest scmUnderTest) { - this.scmUnderTest = scmUnderTest; - this.scmContext = null; - } - - @Before - public void setup() throws Throwable { - // Instantiating ScmSyncConfigurationPlugin instance for unit tests by using + + @Rule protected TestName testName = new TestName(); + + private static final String TEST_URL = "https://jenkins.example.org/"; + + private File currentTestDirectory = null; + private File curentLocalRepository = null; + private File currentHudsonRootDirectory = null; + protected ScmSyncConfigurationBusiness sscBusiness = null; + protected ScmContext scmContext = null; + + private final ScmUnderTest scmUnderTest; + + protected ScmSyncConfigurationBaseTest(ScmUnderTest scmUnderTest) { + this.scmUnderTest = scmUnderTest; + this.scmContext = null; + } + + @SuppressWarnings("deprecation") // We need to mock Hudson.getInstance() + @Before + public void setup() throws Throwable { + // Instantiating ScmSyncConfigurationPlugin instance for unit tests by using // synchronous transactions (instead of an asynchronous ones) // => this way, every commit will be processed synchronously ! - ScmSyncConfigurationPlugin scmSyncConfigPluginInstance = new ScmSyncConfigurationPlugin(true); - - // Mocking PluginWrapper attached to current ScmSyncConfigurationPlugin instance - PluginWrapper pluginWrapper = PowerMockito.mock(PluginWrapper.class); - when(pluginWrapper.getShortName()).thenReturn("scm-sync-configuration"); - // Setting field on current plugin instance - Field wrapperField = Plugin.class.getDeclaredField("wrapper"); - boolean wrapperFieldAccessibility = wrapperField.isAccessible(); - wrapperField.setAccessible(true); - wrapperField.set(scmSyncConfigPluginInstance, pluginWrapper); - wrapperField.setAccessible(wrapperFieldAccessibility); - - Field businessField = ScmSyncConfigurationPlugin.class.getDeclaredField("business"); - businessField.setAccessible(true); - sscBusiness = (ScmSyncConfigurationBusiness) businessField.get(scmSyncConfigPluginInstance); - - // Mocking Hudson root directory - currentTestDirectory = createTmpDirectory("SCMSyncConfigTestsRoot"); - currentHudsonRootDirectory = new File(currentTestDirectory.getAbsolutePath()+"/hudsonRootDir/"); - if(!(currentHudsonRootDirectory.mkdir())) { throw new IOException("Could not create hudson root directory: " + currentHudsonRootDirectory.getAbsolutePath()); } - FileUtils.copyDirectoryStructure(new ClassPathResource(getHudsonRootBaseTemplate()).getFile(), currentHudsonRootDirectory); + ScmSyncConfigurationPlugin scmSyncConfigPluginInstance = new ScmSyncConfigurationPlugin(true) { + @Override + public void initialInit() throws Exception { + // No-op. We *must not* initialize here in tests because the tests provide their own setup. + } + }; + + // Mocking PluginWrapper attached to current ScmSyncConfigurationPlugin instance + PluginWrapper pluginWrapper = PowerMockito.mock(PluginWrapper.class); + when(pluginWrapper.getShortName()).thenReturn("scm-sync-configuration"); + // Setting field on current plugin instance + Field wrapperField = Plugin.class.getDeclaredField("wrapper"); + boolean wrapperFieldAccessibility = wrapperField.isAccessible(); + wrapperField.setAccessible(true); + wrapperField.set(scmSyncConfigPluginInstance, pluginWrapper); + wrapperField.setAccessible(wrapperFieldAccessibility); + + Field businessField = ScmSyncConfigurationPlugin.class.getDeclaredField("business"); + businessField.setAccessible(true); + sscBusiness = (ScmSyncConfigurationBusiness) businessField.get(scmSyncConfigPluginInstance); + + // Mocking Hudson root directory + currentTestDirectory = createTmpDirectory("SCMSyncConfigTestsRoot"); + currentHudsonRootDirectory = new File(currentTestDirectory.getAbsolutePath()+"/hudsonRootDir/"); + if(!(currentHudsonRootDirectory.mkdir())) { throw new IOException("Could not create hudson root directory: " + currentHudsonRootDirectory.getAbsolutePath()); } + FileUtils.copyDirectoryStructure(new ClassPathResource(getHudsonRootBaseTemplate()).getFile(), currentHudsonRootDirectory); //EnvVars env = Computer.currentComputer().getEnvironment(); //env.put("HUDSON_HOME", tmpHudsonRoot.getPath() ); - // Creating local repository... - curentLocalRepository = new File(currentTestDirectory.getAbsolutePath()+"/localRepo/"); - if(!(curentLocalRepository.mkdir())) { throw new IOException("Could not create local repo directory: " + curentLocalRepository.getAbsolutePath()); } - scmUnderTest.initRepo(curentLocalRepository); - - // Mocking user - User mockedUser = Mockito.mock(User.class); - when(mockedUser.getId()).thenReturn("fcamblor"); - - // Mocking Hudson singleton instance ... - // Warning : this line will only work on Objenesis supported VMs : - // http://code.google.com/p/objenesis/wiki/ListOfCurrentlySupportedVMs - Hudson hudsonMockedInstance = spy((Hudson) new ObjenesisStd().getInstantiatorOf(Hudson.class).newInstance()); - PowerMockito.doReturn(currentHudsonRootDirectory).when(hudsonMockedInstance).getRootDir(); - PowerMockito.doReturn(mockedUser).when(hudsonMockedInstance).getMe(); - PowerMockito.doReturn(scmSyncConfigPluginInstance).when(hudsonMockedInstance).getPlugin(ScmSyncConfigurationPlugin.class); - - PowerMockito.mockStatic(Hudson.class); - PowerMockito.doReturn(hudsonMockedInstance).when(Hudson.class); Hudson.getInstance(); - //when(Hudson.getInstance()).thenReturn(hudsonMockedInstance); - } - - @After - public void teardown() throws Throwable { - // Deleting current test directory - FileUtils.deleteDirectory(currentTestDirectory); - } - - // Overridable - protected String getHudsonRootBaseTemplate(){ - return "hudsonRootBaseTemplate/"; - } - - protected static File createTmpDirectory(String directoryPrefix) throws IOException { - final File temp = File.createTempFile(directoryPrefix, Long.toString(System.nanoTime())); - if(!(temp.delete())) { throw new IOException("Could not delete temp file: " + temp.getAbsolutePath()); } - if(!(temp.mkdir())) { throw new IOException("Could not create temp directory: " + temp.getAbsolutePath()); } - return (temp); - } - - protected SCM createSCMMock(){ - return createSCMMock(getSCMRepositoryURL()); - } - - protected SCM createSCMMock(String url){ - SCM mockedSCM = spy(SCM.valueOf(getSCMClass().getName())); - - if(scmUnderTest.useCredentials()){ - SCMCredentialConfiguration mockedCredential = new SCMCredentialConfiguration("toto"); - PowerMockito.doReturn(mockedCredential).when(mockedSCM).extractScmCredentials((String)Mockito.notNull()); - } - - scmContext = new ScmContext(mockedSCM, url); - ScmSyncConfigurationPOJO config = new DefaultSSCPOJO(); - config.setScm(scmContext.getScm()); - config.setScmRepositoryUrl(scmContext.getScmRepositoryUrl()); - ScmSyncConfigurationPlugin.getInstance().loadData(config); - ScmSyncConfigurationPlugin.getInstance().init(); - - return mockedSCM; - } - - protected SCMManipulator createMockedScmManipulator() throws ComponentLookupException, PlexusContainerException{ - // Settling up scm context - SCMManipulator scmManipulator = new SCMManipulator(SCMManagerFactory.getInstance().createScmManager()); - boolean configSettledUp = scmManipulator.scmConfigurationSettledUp(scmContext, true); - assertThat(configSettledUp, is(true)); - - return scmManipulator; - } - - protected void verifyCurrentScmContentMatchesCurrentHudsonDir(boolean match) throws ComponentLookupException, PlexusContainerException, IOException{ + // Creating local repository... + curentLocalRepository = new File(currentTestDirectory.getAbsolutePath()+"/localRepo/"); + if(!(curentLocalRepository.mkdir())) { throw new IOException("Could not create local repo directory: " + curentLocalRepository.getAbsolutePath()); } + scmUnderTest.initRepo(curentLocalRepository); + + // Mocking user + User mockedUser = Mockito.mock(User.class); + when(mockedUser.getId()).thenReturn("fcamblor"); + + // Mocking Hudson singleton instance ... + // Warning : this line will only work on Objenesis supported VMs : + // http://code.google.com/p/objenesis/wiki/ListOfCurrentlySupportedVMs + Hudson hudsonMockedInstance = spy((Hudson) new ObjenesisStd().getInstantiatorOf(Hudson.class).newInstance()); + PowerMockito.doReturn(currentHudsonRootDirectory).when(hudsonMockedInstance).getRootDir(); + PowerMockito.doReturn(mockedUser).when(hudsonMockedInstance).getMe(); + PowerMockito.doReturn(scmSyncConfigPluginInstance).when(hudsonMockedInstance).getPlugin(ScmSyncConfigurationPlugin.class); + PowerMockito.doReturn(TEST_URL).when(hudsonMockedInstance).getRootUrl(); + PowerMockito.doReturn(TEST_URL).when(hudsonMockedInstance).getRootUrlFromRequest(); + + PowerMockito.mockStatic(Jenkins.class); + PowerMockito.doReturn(hudsonMockedInstance).when(Jenkins.class); Jenkins.getInstance(); + PowerMockito.mockStatic(Hudson.class); + PowerMockito.doReturn(hudsonMockedInstance).when(Hudson.class); Hudson.getInstance(); + //when(Hudson.getInstance()).thenReturn(hudsonMockedInstance); + } + + @After + public void teardown() throws Throwable { + // Deleting current test directory + FileUtils.deleteDirectory(currentTestDirectory); + } + + // Overridable + protected String getHudsonRootBaseTemplate(){ + return "hudsonRootBaseTemplate/"; + } + + protected static File createTmpDirectory(String directoryPrefix) throws IOException { + final File temp = File.createTempFile(directoryPrefix, Long.toString(System.nanoTime())); + if(!(temp.delete())) { throw new IOException("Could not delete temp file: " + temp.getAbsolutePath()); } + if(!(temp.mkdir())) { throw new IOException("Could not create temp directory: " + temp.getAbsolutePath()); } + return (temp); + } + + protected SCM createSCMMock(){ + return createSCMMock(getSCMRepositoryURL()); + } + + protected SCM createSCMMock(String url){ + SCM mockedSCM = spy(SCM.valueOf(getSCMClass().getName())); + + if(scmUnderTest.useCredentials()){ + SCMCredentialConfiguration mockedCredential = new SCMCredentialConfiguration("toto"); + PowerMockito.doReturn(mockedCredential).when(mockedSCM).extractScmCredentials((String)Mockito.notNull()); + } + + scmContext = new ScmContext(mockedSCM, url); + ScmSyncConfigurationPOJO config = new DefaultSSCPOJO(); + config.setScm(scmContext.getScm()); + config.setScmRepositoryUrl(scmContext.getScmRepositoryUrl()); + ScmSyncConfigurationPlugin.getInstance().loadData(config); + ScmSyncConfigurationPlugin.getInstance().init(); + + return mockedSCM; + } + + protected SCMManipulator createMockedScmManipulator() throws ComponentLookupException, PlexusContainerException{ + // Settling up scm context + SCMManipulator scmManipulator = new SCMManipulator(SCMManagerFactory.getInstance().createScmManager()); + boolean configSettledUp = scmManipulator.scmConfigurationSettledUp(scmContext, true); + assertThat(configSettledUp, is(true)); + + return scmManipulator; + } + + protected void verifyCurrentScmContentMatchesCurrentHudsonDir(boolean match) throws ComponentLookupException, PlexusContainerException, IOException{ verifyCurrentScmContentMatchesHierarchy(getCurrentHudsonRootDirectory(), match); } protected void verifyCurrentScmContentMatchesHierarchy(String hierarchyPath, boolean match) throws ComponentLookupException, PlexusContainerException, IOException{ - verifyCurrentScmContentMatchesHierarchy(new ClassPathResource(hierarchyPath).getFile(), match); - } + verifyCurrentScmContentMatchesHierarchy(new ClassPathResource(hierarchyPath).getFile(), match); + } protected void verifyCurrentScmContentMatchesHierarchy(File hierarchy, boolean match) throws ComponentLookupException, PlexusContainerException, IOException{ - SCMManipulator scmManipulator = createMockedScmManipulator(); - - // Checkouting scm in temp directory - File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__verifyCurrentScmContentMatchesHierarchy"); - scmManipulator.checkout(checkoutDirectoryForVerifications); + SCMManipulator scmManipulator = createMockedScmManipulator(); + + // Checkouting scm in temp directory + File checkoutDirectoryForVerifications = createTmpDirectory(this.getClass().getSimpleName()+"_"+testName.getMethodName()+"__verifyCurrentScmContentMatchesHierarchy"); + scmManipulator.checkout(checkoutDirectoryForVerifications); List diffs = DirectoryUtils.diffDirectories(checkoutDirectoryForVerifications, hierarchy, getSpecialSCMDirectoryExcludePattern(), true); - FileUtils.deleteDirectory(checkoutDirectoryForVerifications); - + FileUtils.deleteDirectory(checkoutDirectoryForVerifications); + if(match){ assertTrue("Directories doesn't match : "+diffs, diffs.isEmpty()); } else { assertFalse("Directories should _not_ match !", diffs.isEmpty()); } - } - - protected void verifyCurrentScmContentMatchesHierarchy(String hierarchyPath) throws ComponentLookupException, PlexusContainerException, IOException{ - verifyCurrentScmContentMatchesHierarchy(hierarchyPath, true); - } - - // Overridable in a near future (when dealing with multiple scms ...) - protected String getSCMRepositoryURL(){ - return scmUnderTest.createUrl(this.getCurentLocalRepository().getAbsolutePath()); - } - - protected static List getSpecialSCMDirectoryExcludePattern(){ - return new ArrayList(){{ - add(Pattern.compile("\\.svn")); - add(Pattern.compile("\\.git.*")); - add(Pattern.compile("scm-sync-configuration\\..*\\.log")); - add(Pattern.compile("scm-sync-configuration")); - }}; - } - - protected String getSuffixForTestFiles() { - return scmUnderTest.getSuffixForTestFiles(); - } - - // Overridable in a near future (when dealing with multiple scms ...) - protected Class getSCMClass(){ - return scmUnderTest.getClazz(); - } - - protected File getCurrentTestDirectory() { - return currentTestDirectory; - } - - protected File getCurentLocalRepository() { - return curentLocalRepository; - } - - public File getCurrentHudsonRootDirectory() { - return currentHudsonRootDirectory; - } - - public File getCurrentScmSyncConfigurationCheckoutDirectory(){ - return new File(currentHudsonRootDirectory.getAbsolutePath()+"/scm-sync-configuration/checkoutConfiguration/"); - } - + } + + protected void verifyCurrentScmContentMatchesHierarchy(String hierarchyPath) throws ComponentLookupException, PlexusContainerException, IOException{ + verifyCurrentScmContentMatchesHierarchy(hierarchyPath, true); + } + + // Overridable in a near future (when dealing with multiple scms ...) + protected String getSCMRepositoryURL(){ + return scmUnderTest.createUrl(this.getCurentLocalRepository().getAbsolutePath()); + } + + protected static List getSpecialSCMDirectoryExcludePattern(){ + return Lists.newArrayList( + Pattern.compile("\\.svn"), + Pattern.compile("\\.git.*"), + Pattern.compile("scm-sync-configuration\\..*\\.log"), + Pattern.compile("scm-sync-configuration") + ); + } + + protected String getSuffixForTestFiles() { + return scmUnderTest.getSuffixForTestFiles(); + } + + // Overridable in a near future (when dealing with multiple scms ...) + protected Class getSCMClass(){ + return scmUnderTest.getClazz(); + } + + protected File getCurrentTestDirectory() { + return currentTestDirectory; + } + + protected File getCurentLocalRepository() { + return curentLocalRepository; + } + + public File getCurrentHudsonRootDirectory() { + return currentHudsonRootDirectory; + } + + public File getCurrentScmSyncConfigurationCheckoutDirectory(){ + return new File(currentHudsonRootDirectory.getAbsolutePath()+"/scm-sync-configuration/checkoutConfiguration/"); + } + } diff --git a/src/test/resources/HudsonExtensionsTest.shouldFileWhichHaveToBeInSCM/jobs/myFolder/config.xml b/src/test/resources/HudsonExtensionsTest.shouldFileWhichHaveToBeInSCM/jobs/myFolder/config.xml new file mode 100644 index 00000000..f1972d75 --- /dev/null +++ b/src/test/resources/HudsonExtensionsTest.shouldFileWhichHaveToBeInSCM/jobs/myFolder/config.xml @@ -0,0 +1,21 @@ + + + + + false + + + true + false + false + + false + true + false + false + false + false + + + + \ No newline at end of file diff --git a/src/test/resources/HudsonExtensionsTest.shouldFileWhichHaveToBeInSCM/jobs/myFolder/jobs/myJob/config.xml b/src/test/resources/HudsonExtensionsTest.shouldFileWhichHaveToBeInSCM/jobs/myFolder/jobs/myJob/config.xml new file mode 100644 index 00000000..f1972d75 --- /dev/null +++ b/src/test/resources/HudsonExtensionsTest.shouldFileWhichHaveToBeInSCM/jobs/myFolder/jobs/myJob/config.xml @@ -0,0 +1,21 @@ + + + + + false + + + true + false + false + + false + true + false + false + false + false + + + + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/config.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/config.xml new file mode 100644 index 00000000..824eae41 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/config.xml @@ -0,0 +1,31 @@ + + + 1.339 + 2 + NORMAL + true + + + true + + Welcome ! + + + + 5 + 0 + + + + All + false + false + + + All + 0 + + + + + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/hudson.tasks.Shell.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/hudson.tasks.Shell.xml new file mode 100644 index 00000000..8b953697 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/hudson.tasks.Shell.xml @@ -0,0 +1,5 @@ + + + + /bin/bash + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/-newFakeJob/config.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/-newFakeJob/config.xml new file mode 100644 index 00000000..fe28eac3 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/-newFakeJob/config.xml @@ -0,0 +1,21 @@ + + + + + false + + + true + true + false + + false + true + false + true + false + false + + + + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/fakeJob/config.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/fakeJob/config.xml new file mode 100644 index 00000000..f1972d75 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/jobs/fakeJob/config.xml @@ -0,0 +1,21 @@ + + + + + false + + + true + false + false + + false + true + false + false + false + false + + + + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/scm-sync-configuration.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/scm-sync-configuration.xml new file mode 100644 index 00000000..a83018f2 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameStartingWithDash/scm-sync-configuration.xml @@ -0,0 +1,5 @@ + + + scm:svn:https://myrepo/synchronizedDirectory/ + + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/config.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/config.xml new file mode 100644 index 00000000..824eae41 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/config.xml @@ -0,0 +1,31 @@ + + + 1.339 + 2 + NORMAL + true + + + true + + Welcome ! + + + + 5 + 0 + + + + All + false + false + + + All + 0 + + + + + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/hudson.tasks.Shell.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/hudson.tasks.Shell.xml new file mode 100644 index 00000000..8b953697 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/hudson.tasks.Shell.xml @@ -0,0 +1,5 @@ + + + + /bin/bash + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/jobs/fakeJob/config.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/jobs/fakeJob/config.xml new file mode 100644 index 00000000..f1972d75 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/jobs/fakeJob/config.xml @@ -0,0 +1,21 @@ + + + + + false + + + true + false + false + + false + true + false + false + false + false + + + + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/jobs/new fake Job/config.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/jobs/new fake Job/config.xml new file mode 100644 index 00000000..fe28eac3 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/jobs/new fake Job/config.xml @@ -0,0 +1,21 @@ + + + + + false + + + true + true + false + + false + true + false + true + false + false + + + + \ No newline at end of file diff --git a/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/scm-sync-configuration.xml b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/scm-sync-configuration.xml new file mode 100644 index 00000000..a83018f2 --- /dev/null +++ b/src/test/resources/expected-scm-hierarchies/HudsonExtensionsTest.testAddJobNameWithBlanks/scm-sync-configuration.xml @@ -0,0 +1,5 @@ + + + scm:svn:https://myrepo/synchronizedDirectory/ + + \ No newline at end of file