├── .gitignore ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── jenkins-security-scan.yml │ └── cd.yaml ├── .mvn ├── maven.config └── extensions.xml ├── src ├── main │ ├── resources │ │ ├── index.jelly │ │ └── hudson │ │ │ └── plugins │ │ │ └── filesystem_scm │ │ │ ├── FilterSettings │ │ │ ├── config.properties │ │ │ ├── help-selectors.html │ │ │ └── config.jelly │ │ │ ├── FSSCM │ │ │ ├── help-copyHidden.html │ │ │ ├── help-clearWorkspace.html │ │ │ ├── help-path.html │ │ │ ├── help-verboseLogging.html │ │ │ └── config.jelly │ │ │ ├── FilterSelector │ │ │ ├── config.jelly │ │ │ └── help-wildcard.html │ │ │ └── ChangelogSet │ │ │ ├── digest.jelly │ │ │ └── index.jelly │ └── java │ │ └── hudson │ │ └── plugins │ │ └── filesystem_scm │ │ ├── RemoteListDir.java │ │ ├── RemoteCopyDir.java │ │ ├── AllowDeleteList.java │ │ ├── SimpleAntWildcardFilter.java │ │ ├── FilesystemRepositoryBrowser.java │ │ ├── FilterSettings.java │ │ ├── FilterSelector.java │ │ ├── RemoteFolderDiff.java │ │ ├── ChangelogSet.java │ │ ├── Changelog.java │ │ ├── FSSCM.java │ │ └── FolderDiff.java └── test │ └── java │ └── hudson │ └── plugins │ └── filesystem_scm │ ├── TestUtils.java │ ├── FolderDiffFake.java │ ├── ChangelogSetXMLTest.java │ ├── FSSCMTest.java │ ├── integration │ └── pipeline │ │ └── PipelineLibraryTest.java │ ├── FolderDiffTest2.java │ ├── SimpleAntWildcardFilterTest.java │ └── FolderDiffTest.java ├── docs └── images │ ├── screenshot.png │ ├── add.svg │ ├── information.svg │ └── error.svg ├── Jenkinsfile ├── LICENSE.txt ├── CHANGELOG.md ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/filesystem_scm-plugin-developers 2 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | File System SCM 4 |
-------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/filesystem_scm-plugin/HEAD/docs/images/screenshot.png -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FilterSettings/config.properties: -------------------------------------------------------------------------------- 1 | includeFilter.title=Include filter (exclude otherwise) 2 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FSSCM/help-copyHidden.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | If true, the system will copy hidden files and folders as well. Default is false. 4 |

5 |
-------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | // Builds a module using https://github.com/jenkins-infra/pipeline-library 2 | buildPlugin( 3 | configurations: [ 4 | [platform: 'linux', jdk: 21], 5 | [platform: 'windows', jdk: 21], 6 | ]) 7 | -------------------------------------------------------------------------------- /docs/images/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FSSCM/help-clearWorkspace.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | If true, the system will delete all existing files/sub-folders in workspace before checking-out. Poll changes will not be affected by this setting. 4 |

5 |
-------------------------------------------------------------------------------- /docs/images/information.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FSSCM/help-path.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | The file path for the source code. 4 |

5 |

6 | e.g. \\Server1\project1\src or c:\myproject\src 7 |

8 |

9 | Note for distributed build environment, please make sure the path is accessible on remote node(s) 10 |

11 |
-------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: maven 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: monthly 13 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FilterSelector/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/images/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.13 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/filesystem_scm/TestUtils.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.io.File; 4 | 5 | public class TestUtils { 6 | 7 | public static String createPlatformDependentPath(String... parts) { 8 | String path = ""; 9 | for (int i = 0; i < parts.length - 1; i++) { 10 | path = path.concat(parts[i]).concat(File.separator); 11 | } 12 | path = path.concat(parts[parts.length - 1]); 13 | return path; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FilterSettings/help-selectors.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | You can apply wildcard filter(s) when detecting changes and copying files. By default, the system will filter out hidden files, on Unix, that means files/folder starting with ".", on Windows, that means files/folders with "hidden" attribute. You may want to filter out, e.g. files with ".tmp" extension. 4 |

5 |

6 | Note: filters are applied on both sides, source and destination (i.e. the workspace). E.g. if you filter out ".tmp" files, all ".tmp" files currently in workspace will not be removed. 7 |

-------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FSSCM/help-verboseLogging.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Controls the verbosity of file operation logging during checkout. 4 |

5 | 9 |

10 | For builds processing many files, unchecking this option makes build logs much more readable. 11 |

12 |
13 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FilterSelector/help-wildcard.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | ANT style wildcard. 4 |

5 |

6 | To include just *.java, set filter type to "Include" and type add "*.java" (without quote) in the wildcard. 7 | To exclude *.exe" and all JUnit test cases, set filter type to "Exclude" and add two wildcard, one for "*.dll" and one for "*Test*" 8 |

9 |

10 | To exclude a directory, set filter to "**/dir_to_exclude/**" 11 |

12 |

13 | Note: (1) the wildcard is case insensitive, (2) all backslashes (\) will be replaced with slashes (/) 14 |

15 |
-------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FilterSettings/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/ChangelogSet/digest.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ${%No changes.} 6 | 7 | 8 | Changes 9 |
    10 | 11 |
  1. 12 | ${cs.msgAnnotated} (detail) 13 |
  2. 14 |
    15 |
16 |
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/FSSCM/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/RemoteListDir.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.util.*; 4 | import java.io.File; 5 | import java.io.IOException; 6 | 7 | import org.apache.commons.io.FileUtils; 8 | 9 | import hudson.remoting.VirtualChannel; 10 | import jenkins.MasterToSlaveFileCallable; 11 | 12 | public class RemoteListDir extends MasterToSlaveFileCallable< Set > { 13 | 14 | private static final long serialVersionUID = 1452212500874165127L; 15 | 16 | public RemoteListDir() { 17 | } 18 | 19 | public Set invoke(File workspace, VirtualChannel channel) throws IOException { 20 | Collection list = FileUtils.listFiles(workspace, null, true); 21 | Set set = new HashSet(); 22 | for(File file : list) { 23 | String relativePath = FolderDiff.getRelativeName(file.getAbsolutePath(), workspace.getAbsolutePath()); 24 | set.add(relativePath); 25 | } 26 | return set; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/RemoteCopyDir.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import hudson.RestrictedSince; 4 | import java.io.File; 5 | import java.io.IOException; 6 | 7 | import org.apache.commons.io.FileUtils; 8 | 9 | import hudson.remoting.VirtualChannel; 10 | import jenkins.MasterToSlaveFileCallable; 11 | import org.kohsuke.accmod.Restricted; 12 | import org.kohsuke.accmod.restrictions.NoExternalUse; 13 | 14 | /** 15 | * @deprecated Not used anymore 16 | */ 17 | @Deprecated 18 | @Restricted(NoExternalUse.class) 19 | @RestrictedSince("1.21") 20 | public class RemoteCopyDir extends MasterToSlaveFileCallable { 21 | 22 | private static final long serialVersionUID = 1L; 23 | 24 | private String sourceDir; 25 | 26 | public RemoteCopyDir(String sourceDir) { 27 | this.sourceDir = sourceDir; 28 | } 29 | 30 | public Boolean invoke(File workspace, VirtualChannel channel) throws IOException { 31 | FileUtils.copyDirectory(new File(sourceDir), workspace); 32 | return Boolean.TRUE; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009-2018 Various contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/filesystem_scm/FolderDiffFake.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import org.apache.commons.lang3.tuple.ImmutablePair; 9 | 10 | public class FolderDiffFake extends FolderDiff { 11 | 12 | List> copyFilePairs; 13 | List deleteFiles; 14 | 15 | public FolderDiffFake(String sourcePath, String destinationPath) { 16 | copyFilePairs = new ArrayList<>(); 17 | deleteFiles = new ArrayList<>(); 18 | this.setDstPath(destinationPath); 19 | this.setSrcPath(sourcePath); 20 | } 21 | 22 | /** 23 | * Overridden for Testing purposes only log the files which should have been 24 | * copied 25 | */ 26 | @Override 27 | protected void copyFile(File src, File dst) throws IOException { 28 | copyFilePairs.add(new ImmutablePair<>(src, dst)); 29 | } 30 | 31 | /** 32 | * Overridden for Testing purposes only log the files which should have been 33 | * deleted 34 | */ 35 | @Override 36 | protected boolean deleteFile(File file) { 37 | deleteFiles.add(file); 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/filesystem_scm/ChangelogSet/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |

${%Summary}

4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
8 | 9 |
10 | 11 | ${%Version} by ${cs.author}: 12 |
13 |
14 |
${%No changes.}
${item.value}
33 |
34 | 35 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/filesystem_scm/ChangelogSetXMLTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import hudson.model.Run; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | class ChangelogSetXMLTest { 15 | 16 | private ChangelogSet changeLogSet; 17 | 18 | @BeforeEach 19 | void setUp() { 20 | List changes = new ArrayList<>(); 21 | changes.add(new FolderDiff.Entry("c:\\tmp\\del.java", FolderDiff.Entry.Type.DELETED)); 22 | changes.add(new FolderDiff.Entry("c:\\tmp\\add.java", FolderDiff.Entry.Type.NEW)); 23 | changes.add(new FolderDiff.Entry("c:\\tmp\\edit.java", FolderDiff.Entry.Type.MODIFIED)); 24 | changes.add(new FolderDiff.Entry("c:\\tmp\\cc.java", FolderDiff.Entry.Type.MODIFIED)); 25 | changeLogSet = new ChangelogSet(null, changes); 26 | } 27 | 28 | @Test 29 | void testToAndFromXML() throws IOException { 30 | ChangelogSet.XMLSerializer handler = new ChangelogSet.XMLSerializer(); 31 | File tmp = File.createTempFile("xstream", null); 32 | 33 | handler.save(changeLogSet, tmp); 34 | 35 | ChangelogSet out = handler.parse((Run) null, tmp); 36 | 37 | assertEquals(changeLogSet, out); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/filesystem_scm/FSSCMTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import hudson.plugins.filesystem_scm.FolderDiff.Entry; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.io.TempDir; 6 | 7 | import java.io.File; 8 | import java.io.FileNotFoundException; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertFalse; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | class FSSCMTest { 16 | 17 | @TempDir 18 | private File testFolder; 19 | 20 | private final FSSCM fsscm = new FSSCM("", false, false, true, null); 21 | private final List list = new ArrayList<>(); 22 | 23 | @Test 24 | void processChangelog_nullChangelogFile_NoException() throws FileNotFoundException { 25 | fsscm.processChangelog(null, null, list); 26 | } 27 | 28 | @Test 29 | void processChangelog_ChangelogFile_createdChangelogFile() throws FileNotFoundException { 30 | File changeLogFile = new File(testFolder, "changelog.xml"); 31 | assertFalse(changeLogFile.exists()); 32 | fsscm.processChangelog(null, changeLogFile, list); 33 | assertTrue(changeLogFile.exists()); 34 | } 35 | 36 | @Test 37 | void verboseLogging_GetterSetter_WorkCorrectly() { 38 | // Default value should be true 39 | assertTrue(fsscm.isVerboseLogging()); 40 | 41 | fsscm.setVerboseLogging(false); 42 | assertFalse(fsscm.isVerboseLogging()); // Should be false after setting. 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/AllowDeleteList.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.io.*; 4 | import java.util.*; 5 | 6 | import org.apache.commons.io.IOUtils; 7 | 8 | /** We will only delete file from workspace if it is in the allowDeleteList. 9 | * 10 | *

And each time we add any files to the workspace, we will add a record in this list. 11 | * 12 | */ 13 | public class AllowDeleteList { 14 | 15 | final private static String ALLOW_DELETE_LIST_BASENAME = "fsscm_allow_delete_list.dat"; 16 | 17 | private File file; 18 | private Set set; 19 | 20 | public AllowDeleteList(File projectPath) { 21 | file = new File(projectPath, ALLOW_DELETE_LIST_BASENAME); 22 | set = new HashSet(); 23 | } 24 | 25 | public boolean fileExists() { 26 | return file.exists(); 27 | } 28 | 29 | public Set getList() { 30 | return Collections.unmodifiableSet(set); 31 | } 32 | 33 | public void setList(Set list) { 34 | set.clear(); 35 | set.addAll(list); 36 | } 37 | 38 | public boolean add(String item) { 39 | return set.add(item); 40 | } 41 | 42 | public boolean remove(String item) { 43 | return set.remove(item); 44 | } 45 | 46 | public void save() throws IOException { 47 | PrintStream out = null; 48 | try { 49 | out = new PrintStream(file, "UTF-8"); 50 | for(String name : set) { 51 | out.println(name); 52 | } 53 | } finally { 54 | IOUtils.closeQuietly(out); 55 | } 56 | } 57 | 58 | public void load() throws IOException { 59 | set.clear(); 60 | BufferedReader reader = null; 61 | try { 62 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8")); 63 | while(reader.ready()) { 64 | String line = reader.readLine(); 65 | if ( null != line && line.length() > 0 ) { 66 | set.add(line); 67 | } 68 | } 69 | } finally { 70 | IOUtils.closeQuietly(reader); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/SimpleAntWildcardFilter.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.io.File; 4 | import java.util.regex.Pattern; 5 | 6 | import org.apache.commons.io.filefilter.AbstractFileFilter; 7 | 8 | public class SimpleAntWildcardFilter extends AbstractFileFilter { 9 | 10 | private Pattern wildcardPattern; 11 | 12 | public SimpleAntWildcardFilter(String antWildcard) { 13 | String line = antWildcard; 14 | line = line.replace('\\', '/'); 15 | String[] list = line.split("/"); 16 | StringBuilder buf = new StringBuilder(); 17 | for(int i=0; i { 42 | 43 | private static final long serialVersionUID = 8347582394758239475L; 44 | 45 | @Override 46 | public URL getChangeSetLink(ChangeLogSet.Entry changeSet) throws IOException { 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | # 3 | # Please find additional hints for individual trigger use case 4 | # configuration options inline this script below. 5 | # 6 | --- 7 | name: cd 8 | on: 9 | workflow_dispatch: 10 | inputs: 11 | validate_only: 12 | required: false 13 | type: boolean 14 | description: | 15 | Run validation with release drafter only 16 | → Skip the release job 17 | # Note: Change this default to true, 18 | # if the checkbox should be checked by default. 19 | default: false 20 | # If you don't want any automatic trigger in general, then 21 | # the following check_run trigger lines should all be commented. 22 | # Note: Consider the use case #2 config for 'validate_only' below 23 | # as an alternative option! 24 | check_run: 25 | types: 26 | - completed 27 | 28 | permissions: 29 | checks: read 30 | contents: write 31 | 32 | jobs: 33 | maven-cd: 34 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 35 | with: 36 | # Comment / uncomment the validate_only config appropriate to your preference: 37 | # 38 | # Use case #1 (automatic release): 39 | # - Let any successful Jenkins build trigger another release, 40 | # if there are merged pull requests of interest 41 | # - Perform a validation only run with drafting a release note, 42 | # if manually triggered AND inputs.validate_only has been checked. 43 | # 44 | validate_only: ${{ inputs.validate_only == true }} 45 | # 46 | # Alternative use case #2 (no automatic release): 47 | # - Same as use case #1 - but: 48 | # - Let any check_run trigger a validate_only run. 49 | # => enforce the release job to be skipped. 50 | # 51 | #validate_only: ${{ inputs.validate_only == true || github.event_name == 'check_run' }} 52 | secrets: 53 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 54 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### Version 2.1 (Jan 31, 2018) 4 | 5 | - [![(error)](docs/images/error.svg) JENKINS-49053](https://issues.jenkins-ci.org/browse/JENKINS-49053) - 6 | Prevent NullPointerException when writing empty changelog files to 7 | the disk 8 | 9 | ### Version 2.0 (Dec 08, 2017) 10 | 11 | - ![(plus)](docs/images/add.svg) [JENKINS-40743](https://issues.jenkins-ci.org/browse/JENKINS-40743) - 12 | Make the plugin compatible with Jenkins Pipeline and other Job types 13 | 14 | - ![(error)](docs/images/error.svg) [JENKINS-43993](https://issues.jenkins-ci.org/browse/JENKINS-43993) - 15 | Update the SCM implementation to be compatible with Stapler 16 | Databinding API 17 | - ![(error)](docs/images/error.svg) Cleanup 18 | issues reported by FindBugs and other static analysis checks 19 | - ![(info)](docs/images/information.svg) Update 20 | Jenkins core requirement to 1.642.3 21 | 22 | ### Archive 23 | 24 | Version 1.20 (Dec 5th, 2011) 25 | 26 | - Support ANT style wildcard in file filtering 27 | - Add an extra config parameter to let users to choose whether hidden 28 | files/dirs should be copied or not 29 | 30 | Version 1.10 (Apr 2, 2011) 31 | 32 | - No really user visible changes: 33 | - fixed isEmptySet() method on ChangeLog 34 | - updated to current version of Jenkins API 35 | 36 | Version 1.9 (Sep 21, 2010) 37 | 38 | - Works on Hudson core 1.337 as well 39 | 40 | Version 1.8 (Mar 29, 2010) 41 | 42 | - Bug fixed: enable clearWorkspace on the 1st jobrun will throw 43 | Exception 44 | 45 | Version 1.7 (Mar 11, 2010) 46 | 47 | - Avoid Hudson startup error when upgrading to Hudson 1.349 or newer 48 | ([JENKINS-5893](https://issues.jenkins-ci.org/browse/JENKINS-5893)) 49 | 50 | Version 1.6 (Feb 12, 2010) 51 | 52 | - Bug fixed: chmod before copying readonly files on Unix 53 | - Bug fixed: Master/Slave bug 54 | - Bug fixed: help page URL correctly handled even Winstone started 55 | with prefix 56 | 57 | Version 1.5 58 | 59 | - Preserve file permission (rwxrwxrwx) when copying files (on Unix 60 | platform only) 61 | - will only delete a file from workspace if it is copied by this 62 | plugin 63 | - ChangelogSet changed to follow the latest API 64 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/filesystem_scm/integration/pipeline/PipelineLibraryTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm.integration.pipeline; 2 | 3 | import hudson.plugins.filesystem_scm.FSSCM; 4 | import hudson.plugins.filesystem_scm.FilterSettings; 5 | import org.apache.commons.io.FileUtils; 6 | import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; 7 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Disabled; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | import org.jvnet.hudson.test.JenkinsRule; 13 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.nio.charset.StandardCharsets; 18 | import java.util.Collections; 19 | 20 | /** 21 | * Tests for Jenkins Pipeline integration. 22 | * 23 | * @author Oleg Nenashev 24 | */ 25 | @WithJenkins 26 | class PipelineLibraryTest { 27 | 28 | private JenkinsRule j; 29 | 30 | @TempDir 31 | private File tmpDir; 32 | 33 | @BeforeEach 34 | void setUp(JenkinsRule rule) { 35 | j = rule; 36 | } 37 | 38 | @Test 39 | @Disabled("JenkinsRule just hangs on mvn clean verify, passes for test file run") 40 | void shouldSupportFSSCMsJenkinsfileSource() throws Exception { 41 | // Init repo 42 | File fsscmDir = newFolder(tmpDir, "fsscm"); 43 | File jenkinsfile = new File(fsscmDir, "Jenkinsfile"); 44 | FileUtils.write(jenkinsfile, "echo `Hello`", StandardCharsets.UTF_8); 45 | 46 | // Create job 47 | WorkflowJob job = new WorkflowJob(j.jenkins, "MyPipeline"); 48 | job.setDefinition(new CpsScmFlowDefinition(new FSSCM(null, false, false, true, 49 | new FilterSettings(true, Collections.emptyList())), "Jenkinsfile")); 50 | 51 | j.buildAndAssertSuccess(job); 52 | } 53 | 54 | private static File newFolder(File root, String... subDirs) throws IOException { 55 | String subFolder = String.join("/", subDirs); 56 | File result = new File(root, subFolder); 57 | if (!result.mkdirs()) { 58 | throw new IOException("Couldn't create folders " + root); 59 | } 60 | return result; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/filesystem_scm/FolderDiffTest2.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | 10 | class FolderDiffTest2 { 11 | 12 | @Test 13 | void GetRelative_correctFileAndFolder_Subpath() throws IOException { 14 | String folder = TestUtils.createPlatformDependentPath("c:", "tmp"); 15 | String expected = TestUtils.createPlatformDependentPath("abc", "qq", "qq.java"); 16 | String actual = FolderDiff.getRelativeName(TestUtils.createPlatformDependentPath(folder, expected), folder); 17 | assertEquals(expected, actual); 18 | } 19 | 20 | @Test 21 | void GetRelative_correctFileAndDrive_Subpath() throws IOException { 22 | String folder = TestUtils.createPlatformDependentPath("c:"); 23 | String expected = TestUtils.createPlatformDependentPath("tmp", "abc", "qq", "qq.java"); 24 | String actual = FolderDiff.getRelativeName(TestUtils.createPlatformDependentPath(folder, expected), folder); 25 | assertEquals(expected, actual); 26 | } 27 | 28 | @Test 29 | void GetRelative_FileAndFolderWithALetterMissingInName_Exception() { 30 | String folder = TestUtils.createPlatformDependentPath("c:", "tm"); 31 | assertThrows(IOException.class, () -> FolderDiff 32 | .getRelativeName(TestUtils.createPlatformDependentPath("c:", "tmp", "abc", "qq", "qq.java"), folder)); 33 | } 34 | 35 | @Test 36 | void GetRelative_FileAndFolderNotParentOfFile_Exception() { 37 | String folder = TestUtils.createPlatformDependentPath("c:", "def"); 38 | String file = TestUtils.createPlatformDependentPath("c:", "tmp", "abc", "qq", "qq.java"); 39 | assertThrows(IOException.class, () -> FolderDiff.getRelativeName(file, folder)); 40 | } 41 | 42 | @Test 43 | void GetRelative_FileAndFolderWithPathSeperatorAppended_Exception() { 44 | String folder = TestUtils.createPlatformDependentPath("c:", "tmp"); 45 | String expected = TestUtils.createPlatformDependentPath("abc", "qq", "qq.java"); 46 | assertThrows(IOException.class, () -> FolderDiff.getRelativeName(TestUtils.createPlatformDependentPath(folder, expected), 47 | folder.concat("//"))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/FilterSettings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017 Oleg Nenashev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package hudson.plugins.filesystem_scm; 25 | 26 | import hudson.Extension; 27 | import hudson.model.Describable; 28 | import hudson.model.Descriptor; 29 | import java.util.ArrayList; 30 | import java.util.Collections; 31 | import java.util.List; 32 | import javax.annotation.Nonnull; 33 | import jenkins.model.Jenkins; 34 | import org.kohsuke.stapler.DataBoundConstructor; 35 | 36 | /** 37 | * Stores filter settings. 38 | * @since TODO 39 | * @author Oleg Nenashev 40 | */ 41 | public class FilterSettings implements Describable { 42 | 43 | private final boolean includeFilter; 44 | @Nonnull 45 | private final List selectors; 46 | 47 | @DataBoundConstructor 48 | public FilterSettings(boolean includeFilter, List selectors) { 49 | this.includeFilter = includeFilter; 50 | this.selectors = selectors != null ? selectors : Collections.emptyList(); 51 | } 52 | 53 | @Nonnull 54 | public List getSelectors() { 55 | return Collections.unmodifiableList(selectors); 56 | } 57 | 58 | @Nonnull 59 | public List getWildcards() { 60 | if (selectors.isEmpty()) { 61 | return Collections.emptyList(); 62 | } 63 | List res = new ArrayList<>(selectors.size()); 64 | for (FilterSelector s : selectors) { 65 | res.add(s.getWildcard()); 66 | } 67 | return res; 68 | } 69 | 70 | public boolean isIncludeFilter() { 71 | return includeFilter; 72 | } 73 | 74 | @Override 75 | public Descriptor getDescriptor() { 76 | return Jenkins.getActiveInstance().getDescriptorByType(DescriptorImpl.class); 77 | } 78 | 79 | @Extension 80 | public static class DescriptorImpl extends Descriptor { 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/FilterSelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017 Oleg Nenashev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package hudson.plugins.filesystem_scm; 25 | 26 | import hudson.Extension; 27 | import hudson.model.Describable; 28 | import hudson.model.Descriptor; 29 | import hudson.util.FormValidation; 30 | import jenkins.model.Jenkins; 31 | import org.kohsuke.accmod.Restricted; 32 | import org.kohsuke.accmod.restrictions.NoExternalUse; 33 | import org.kohsuke.stapler.DataBoundConstructor; 34 | import org.kohsuke.stapler.QueryParameter; 35 | 36 | /** 37 | * Manages particular wildcards. 38 | * @since TODO 39 | * @author Oleg Nenashev 40 | */ 41 | public class FilterSelector implements Describable { 42 | 43 | private final String wildcard; 44 | 45 | @DataBoundConstructor 46 | public FilterSelector(String wildcard) { 47 | this.wildcard = wildcard; 48 | } 49 | 50 | public String getWildcard() { 51 | return wildcard; 52 | } 53 | 54 | @Override 55 | public Descriptor getDescriptor() { 56 | return Jenkins.getActiveInstance().getDescriptorByType(DescriptorImpl.class); 57 | } 58 | 59 | @Extension 60 | public static class DescriptorImpl extends Descriptor { 61 | 62 | @Restricted(NoExternalUse.class) 63 | public FormValidation doCheckWildcard(@QueryParameter String value) { 64 | if (null == value || value.trim().length() == 0) { 65 | return FormValidation.ok(); 66 | } 67 | if (value.startsWith("/") || value.startsWith("\\") || value.matches("[a-zA-Z]:.*")) { 68 | return FormValidation.error("Pattern can't be an absolute path"); 69 | } else { 70 | try { 71 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter(value); 72 | return FormValidation.ok("Pattern is correct: " + filter.getPattern()); 73 | } catch (Exception e) { 74 | return FormValidation.error(e, "Invalid wildcard pattern"); 75 | } 76 | } 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![][ButlerImage]][homepage] 2 | 3 | # About 4 | This is the source repository for the **File System SCM** plugin for Jenkins. 5 | This plugin provides an additional SCM option to Jenkins, based on the file 6 | system. 7 | 8 | For more information see the [homepage]. 9 | 10 | # Purpose and Use Cases 11 | 12 | Use this plugin if you want to use the Jenkins SCM (source code management) 13 | functionality, but instead of retrieving code from a version control system such 14 | as CVS, Subversion or git, instead retrieve the code from a directory on the 15 | file system. 16 | 17 | A typical use case is: during development of a Jenkins job (e.g. a pipeline), 18 | you don't need/want to connect to an actual version control system, but avoid 19 | the latency by just getting things from the local file system. 20 | 21 | # How to Raise Issues 22 | 23 | If you find any bug, or if you want to file a change request, then please 24 | check out: 25 | [How to report an issue](https://wiki.jenkins.io/display/JENKINS/How+to+report+an+issue). 26 | 27 | When creating a ticket in the [Jenkins JIRA](https://issues.jenkins-ci.org/) 28 | system, select the component `filesystem_scm_plugin`. 29 | 30 | # Source 31 | The source code can be found on 32 | [GitHub](https://github.com/jenkinsci/filesystem_scm-plugin). Fork us! 33 | 34 | # Contributing 35 | 36 | Contributions are welcome! Check out the 37 | [open tickets](https://issues.jenkins-ci.org/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20%28Open%2C%20Reopened%29%20AND%20component%20%3D%20filesystem_scm-plugin) 38 | for this plugin in JIRA. 39 | 40 | If you are a newbie, and want to pick up an easier task first, you may 41 | want to start with 42 | [newbie-friendly open tickets](https://issues.jenkins-ci.org/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20%28Open%2C%20Reopened%29%20AND%20component%20%3D%20filesystem_scm-plugin%20AND%20labels%20%3D%20newbie-friendly) 43 | first (if there are any). 44 | 45 | # License 46 | This Jenkins plugin is licensed under the [MIT License](./LICENSE.txt). 47 | 48 | [ButlerImage]: https://jenkins.io/sites/default/files/jenkins_logo.png 49 | [homepage]: https://plugins.jenkins.io/filesystem_scm 50 | 51 | Use File System as SCM. 52 | 53 | Simulate File System as SCM by checking file system last modified date, 54 | checkout(), pollChanges(), ChangeLog and distributed build are all 55 | supported. 56 | 57 | ![](docs/images/screenshot.png) 58 | 59 | Folder difference is found by 60 | 61 | 1. for each file in source, check if the corresponding file in 62 | workspace exists 63 | 1. if not, it is a new file 64 | 2. if yes, further checks if the file in source is newer than file 65 | in workspace, or if source file is modified since last build, 66 | this is a modified file 67 | 2. for each file in workspace, if the corresponding file in source does 68 | not exist AND 69 | 1. it is in our self maintained "allow delete list", we will delete 70 | this file from workspace. Every times we copy a file from src to 71 | dst, we add the filename to the "allow delete list", in other 72 | words, we will only delete files that are copied by us 73 | 74 | Filtering is supported when checking for modified files. 75 | 76 | If ***Clear Workspace*** is checked, the system will delete all existing 77 | files/sub-folders in workspace before checking-out. Poll changes will 78 | not be affected by this setting. 79 | 80 | In Changelog, ***User***, i.e. who changed the file, is not supported. 81 | 82 | ## Report Bugs 83 | 84 | https://www.jenkins.io/participate/report-issue/redirect/#15698/filesystem_scm 85 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/filesystem_scm/SimpleAntWildcardFilterTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.apache.commons.io.filefilter.HiddenFileFilter; 5 | import org.apache.commons.io.filefilter.IOFileFilter; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.io.TempDir; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.Collection; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | 15 | class SimpleAntWildcardFilterTest { 16 | 17 | @TempDir 18 | private File tmpDir; 19 | 20 | @Test 21 | void test1() throws IOException { 22 | File myDir = new File(tmpDir, "abc001234/def/xyz.000"); 23 | myDir.mkdirs(); 24 | File f1 = new File(myDir, "aa.txt"); 25 | f1.createNewFile(); 26 | File f2 = new File(myDir, "bb.111"); 27 | f2.createNewFile(); 28 | File f3 = new File(myDir.getParentFile(), "ab.tx1"); 29 | f3.createNewFile(); 30 | 31 | IOFileFilter iof = new SimpleAntWildcardFilter(tmpDir.getAbsolutePath() + "/**/a?.tx?"); 32 | File checkDir = new File(tmpDir, "abc001234"); 33 | Collection coll = FileUtils.listFiles(checkDir, iof, HiddenFileFilter.VISIBLE); 34 | assertEquals(2, coll.size()); 35 | } 36 | 37 | @Test 38 | void test2() { 39 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("**/CVS/*"); 40 | assertEquals("(/.*)?/CVS/[^\\/]*$", filter.getPattern().toString()); 41 | } 42 | 43 | @Test 44 | void test3() { 45 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("org/apache/jakarta/**"); 46 | assertEquals("/org/apache/jakarta(/.*)?$", filter.getPattern().toString()); 47 | } 48 | 49 | @Test 50 | void test4() { 51 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("**/*.gif"); 52 | assertEquals("(/.*)?/[^\\/]*\\.gif$", filter.getPattern().toString()); 53 | } 54 | 55 | @Test 56 | void test5() { 57 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("*/.hgignore"); 58 | assertEquals("/[^\\/]*/\\.hgignore$", filter.getPattern().toString()); 59 | } 60 | 61 | @Test 62 | void test6() { 63 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("/views/**/*.cfm"); 64 | assertEquals("^/views(/.*)?/[^\\/]*\\.cfm$", filter.getPattern().toString()); 65 | } 66 | 67 | @Test 68 | void test7() { 69 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("/views/index??.cfm"); 70 | assertEquals("^/views/index..\\.cfm$", filter.getPattern().toString()); 71 | } 72 | 73 | @Test 74 | void test8() { 75 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("WEB-INF/*-context.xml"); 76 | assertEquals("/WEB-INF/[^\\/]*-context\\.xml$", filter.getPattern().toString()); 77 | } 78 | 79 | @Test 80 | void test9() { 81 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("C:/some/path/*-context.xml"); 82 | assertEquals("^C:/some/path/[^\\/]*-context\\.xml$", filter.getPattern().toString()); 83 | } 84 | 85 | @Test 86 | void test10() { 87 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("**/abc/def.txt"); 88 | assertEquals("(/.*)?/abc/def\\.txt$", filter.getPattern().toString()); 89 | } 90 | 91 | @Test 92 | void test11() { 93 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("hello/ab/**"); 94 | assertEquals("/hello/ab(/.*)?$", filter.getPattern().toString()); 95 | } 96 | 97 | @Test 98 | void test12() { 99 | SimpleAntWildcardFilter filter = new SimpleAntWildcardFilter("**/**/a/b/**/"); 100 | assertEquals("(/.*)?(/.*)?/a/b(/.*)?$", filter.getPattern().toString()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/RemoteFolderDiff.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import hudson.FilePath; 9 | import hudson.os.PosixException; 10 | import hudson.remoting.VirtualChannel; 11 | import hudson.tools.JDKInstaller.Platform; 12 | 13 | public class RemoteFolderDiff extends FolderDiff { 14 | 15 | private static final long serialVersionUID = 5823948572938475938L; 16 | protected StringBuffer buf; 17 | protected long lastBuildTime; 18 | protected long lastSuccessfulBuildTime; 19 | protected boolean verboseLogging = true; 20 | 21 | public RemoteFolderDiff() { 22 | buf = new StringBuffer(); 23 | } 24 | 25 | public long getLastBuildTime() { 26 | return lastBuildTime; 27 | } 28 | 29 | public void setLastBuildTime(long lastBuildTime) { 30 | this.lastBuildTime = lastBuildTime; 31 | } 32 | 33 | public long getLastSuccessfulBuildTime() { 34 | return lastSuccessfulBuildTime; 35 | } 36 | 37 | public void setVerboseLogging(boolean verboseLogging) { 38 | this.verboseLogging = verboseLogging; 39 | } 40 | 41 | public void setLastSuccessfulBuildTime(long lastSuccessfulBuildTime) { 42 | this.lastSuccessfulBuildTime = lastSuccessfulBuildTime; 43 | } 44 | 45 | @Override 46 | protected void log(String msg) { 47 | if (verboseLogging) 48 | buf.append(msg).append("\n"); 49 | } 50 | 51 | @Override 52 | protected void copyFile(File src, File dst) throws IOException { 53 | FilePath srcpath = new FilePath(src); 54 | FilePath dstpath = new FilePath(dst); 55 | try { 56 | changeDestinationPropertiesOnUnixSytem(dstpath, isUnix()); 57 | srcpath.copyToWithPermission(dstpath); 58 | } catch (InterruptedException e) { 59 | IOException ioe = new IOException(); 60 | ioe.initCause(e); 61 | throw ioe; 62 | } 63 | } 64 | 65 | private static void changeDestinationPropertiesOnUnixSytem(FilePath dstpath, boolean isUnix) 66 | throws IOException, InterruptedException, PosixException { 67 | // if not write-able, then we can't copy, have to set it to write-able 68 | if (isUnix && dstpath.exists()) { 69 | int mode = dstpath.mode(); 70 | // owner write-able bit = 010 000 000b = 0x80 71 | if ((mode & 0x80) == 0) { 72 | dstpath.chmod(mode | 0x80); 73 | } 74 | } 75 | } 76 | 77 | private static boolean isUnix() { 78 | boolean isUnix = false; 79 | try { 80 | isUnix = (Platform.WINDOWS != Platform.current()); 81 | } catch (Exception e) { 82 | e.printStackTrace(); 83 | } 84 | return isUnix; 85 | } 86 | 87 | public String getLog() { 88 | return buf.toString(); 89 | } 90 | 91 | public static class PollChange extends RemoteFolderDiff { 92 | 93 | private static final long serialVersionUID = 1L; 94 | 95 | @Override 96 | public Boolean invoke(File workspace, VirtualChannel channel) throws IOException { 97 | setDstPath(workspace.getAbsolutePath()); 98 | List newFiles = getNewOrModifiedFiles(lastBuildTime, true); 99 | if (newFiles.size() > 0) { 100 | return Boolean.TRUE; 101 | } 102 | if (-1 == lastSuccessfulBuildTime) { 103 | return Boolean.FALSE; 104 | } 105 | List delFiles = getFiles2Delete(true); 106 | return delFiles.size() > 0; 107 | } 108 | } 109 | 110 | public static class CheckOut extends RemoteFolderDiff> { 111 | 112 | private static final long serialVersionUID = 1L; 113 | 114 | @Override 115 | public List invoke(File workspace, VirtualChannel channel) throws IOException { 116 | setDstPath(workspace.getAbsolutePath()); 117 | List newFiles = getNewOrModifiedFiles(lastBuildTime, false); 118 | List delFiles = getFiles2Delete(false); 119 | List files = new ArrayList(); 120 | files.addAll(newFiles); 121 | files.addAll(delFiles); 122 | return files; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 5.28 8 | 9 | 10 | hudson.plugins.filesystem_scm 11 | filesystem_scm 12 | hpi 13 | Jenkins File System SCM Plugin 14 | ${changelist} 15 | Using File System as SCM, done by checking file system last modified date. 16 | https://github.com/jenkinsci/filesystem_scm-plugin 17 | 18 | 19 | 999999-SNAPSHOT 20 | jenkinsci/filesystem_scm-plugin 21 | 2.479 22 | ${jenkins.baseline}.3 23 | Max 24 | Low 25 | false 26 | 27 | 28 | 29 | 30 | samngms 31 | Sam NG 32 | samngms@yahoo.com 33 | +8 34 | 35 | 36 | kutzi 37 | Christoph Kutzinski 38 | kutzi@gmx.de 39 | +1 40 | 41 | 42 | oleg_nenashev 43 | Oleg Nenashev 44 | o.v.nenashev@gmail.com 45 | +1 46 | 47 | 48 | guymeron 49 | Guy Meron 50 | guy.meron@gmail.com 51 | +2 52 | 53 | 54 | 55 | 56 | 57 | MIT License 58 | https://www.opensource.org/licenses/mit-license.php 59 | repo 60 | 61 | 62 | 63 | 65 | 66 | 67 | repo.jenkins-ci.org 68 | https://repo.jenkins-ci.org/public/ 69 | 70 | 71 | 72 | 73 | 74 | repo.jenkins-ci.org 75 | https://repo.jenkins-ci.org/public/ 76 | 77 | 78 | 79 | 80 | 81 | org.jenkins-ci.plugins 82 | jdk-tool 83 | 83.v417146707a_3d 84 | 85 | 86 | org.jenkins-ci.plugins 87 | structs 88 | 89 | 90 | org.jenkins-ci.plugins.workflow 91 | workflow-cps 92 | test 93 | 94 | 95 | org.jenkins-ci.plugins.workflow 96 | workflow-job 97 | test 98 | 99 | 100 | org.jenkins-ci.plugins.workflow 101 | workflow-api 102 | test 103 | 104 | 105 | org.jenkins-ci.plugins.workflow 106 | workflow-support 107 | test 108 | 109 | 110 | 111 | org.jenkins-ci 112 | annotation-indexer 113 | 114 | 115 | org.apache.commons 116 | commons-lang3 117 | 3.20.0 118 | test 119 | 120 | 121 | 122 | 123 | 124 | 125 | io.jenkins.tools.bom 126 | bom-${jenkins.baseline}.x 127 | 5054.v620b_5d2b_d5e6 128 | import 129 | pom 130 | 131 | 132 | 133 | 134 | 135 | scm:git:https://github.com/${gitHubRepo}.git 136 | scm:git:https://github.com/${gitHubRepo}.git 137 | https://github.com/${gitHubRepo} 138 | ${scmTag} 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/ChangelogSet.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileNotFoundException; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.Iterator; 11 | import java.util.List; 12 | 13 | import org.apache.commons.io.IOUtils; 14 | import org.xml.sax.SAXException; 15 | 16 | import hudson.model.Run; 17 | import hudson.scm.ChangeLogSet; 18 | import hudson.scm.RepositoryBrowser; 19 | import hudson.util.XStream2; 20 | 21 | /** 22 | * FileSystem base SCM ChangelogSet 23 | *

24 | * Unlike other SCMs, there is at most ONE set of changelog when we checkout. 25 | * While multiple users may have modified some files between two builds, but we 26 | * will only be able to detect if there is any files modified (YES or NO). 27 | *

28 | * 29 | *

30 | * XML serialization is done by XStream2 31 | *

32 | * 33 | * @author Sam NG 34 | * 35 | */ 36 | public class ChangelogSet extends hudson.scm.ChangeLogSet { 37 | 38 | // I'm FileSystem SCM, basically I will only have 1 changelog 39 | // not like other SCM, e.g. SVN, there may be 2 or 3 committed changes between 40 | // builds 41 | private List logs; 42 | 43 | public ChangelogSet(Run build, List changes) { 44 | super(build, new FilesystemRepositoryBrowser()); 45 | logs = new ArrayList<>(); 46 | if (!changes.isEmpty()) { 47 | logs.add(new Changelog(this, changes)); 48 | } 49 | } 50 | 51 | @Override 52 | public String getKind() { 53 | return "fs_scm"; 54 | } 55 | 56 | @Override 57 | public boolean isEmptySet() { 58 | return logs.isEmpty(); 59 | } 60 | 61 | public Iterator iterator() { 62 | return Collections.unmodifiableList(logs).iterator(); 63 | } 64 | 65 | @Override 66 | public int hashCode() { 67 | final int prime = 31; 68 | int result = 1; 69 | result = prime * result + ((logs == null) ? 0 : logs.hashCode()); 70 | return result; 71 | } 72 | 73 | @Override 74 | public boolean equals(Object obj) { 75 | if (this == obj) 76 | return true; 77 | if (obj == null) 78 | return false; 79 | if (getClass() != obj.getClass()) 80 | return false; 81 | final ChangelogSet other = (ChangelogSet) obj; 82 | if (logs == null) { 83 | if (other.logs != null) 84 | return false; 85 | } else if (!logs.equals(other.logs)) 86 | return false; 87 | return true; 88 | } 89 | 90 | public static class XMLSerializer extends hudson.scm.ChangeLogParser { 91 | private transient XStream2 xstream; 92 | 93 | private Object readResolve() { // xstream field used to be serialized in build.xml 94 | initXStream(); 95 | return this; 96 | } 97 | 98 | public XMLSerializer() { 99 | initXStream(); 100 | } 101 | 102 | private void initXStream() { 103 | xstream = new XStream2(); 104 | xstream.alias("log", ChangelogSet.class); 105 | // xstream.addImplicitCollection(ChangelogSet.class, "changeLogSet"); 106 | xstream.aliasField("changelogset", ChangelogSet.class, "changeLogSet"); 107 | xstream.alias("changelog", Changelog.class); 108 | xstream.alias("path", Changelog.Path.class); 109 | xstream.omitField(hudson.scm.ChangeLogSet.class, "build"); 110 | // xstream.omitField(ChangelogSet.ChangeLog.class, "parent"); 111 | // xstream.omitField(ChangelogSet.Path.class, "changeLog"); 112 | } 113 | 114 | @Override 115 | public ChangeLogSet parse(Run build, RepositoryBrowser browser, File changelogFile) 116 | throws IOException, SAXException { 117 | return parse(build, changelogFile); 118 | } 119 | 120 | @SuppressWarnings("rawtypes") 121 | public ChangelogSet parse(Run build, java.io.File file) throws FileNotFoundException { 122 | FileInputStream in = null; 123 | ChangelogSet out = null; 124 | try { 125 | in = new FileInputStream(file); 126 | out = (ChangelogSet) xstream.fromXML(in); 127 | } finally { 128 | IOUtils.closeQuietly(in); 129 | } 130 | return out; 131 | } 132 | 133 | public void save(ChangelogSet changeLogSet, File changelogFile) throws FileNotFoundException { 134 | FileOutputStream out = null; 135 | try { 136 | out = new FileOutputStream(changelogFile); 137 | xstream.toXML(changeLogSet, out); 138 | } finally { 139 | IOUtils.closeQuietly(out); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/Changelog.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import hudson.scm.ChangeLogSet; 4 | import hudson.scm.EditType; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.Date; 10 | import java.util.List; 11 | import javax.annotation.CheckForNull; 12 | 13 | /** Represents a Changelog record (ChangeLogSet.Entry) in ChangelogSet 14 | * 15 | *

Author is always "Unknown"

16 | *

Date is always the checkout datetime

17 | * 18 | *@author Sam NG 19 | * 20 | */ 21 | public class Changelog extends hudson.scm.ChangeLogSet.Entry { 22 | 23 | private ChangelogSet parent; 24 | private Date date; 25 | private List paths; 26 | 27 | public Changelog() { 28 | // do nothing, only for serialization 29 | } 30 | 31 | public Changelog(ChangelogSet parent) { 32 | this.parent = parent; 33 | } 34 | 35 | public Changelog(ChangelogSet parent, List changes) { 36 | this.parent = parent; 37 | 38 | paths = new ArrayList(); 39 | for(int i=0; i getAffectedPaths() { 48 | ArrayList list = new ArrayList(); 49 | for( Path path : paths ) { 50 | list.add(path.getValue()); 51 | } 52 | return Collections.unmodifiableList(list); 53 | } 54 | 55 | @Override 56 | public Collection getAffectedFiles() { 57 | return Collections.unmodifiableList(paths); 58 | } 59 | 60 | @Override 61 | public String getMsg() { 62 | if ( 0 == paths.size() ) return "No change"; 63 | 64 | int add = 0; 65 | int del = 0; 66 | int edit = 0; 67 | for(int i=0; i 0 ) { 82 | if ( buf.length() > 0 ) buf.append(", "); 83 | buf.append(count).append(' '); 84 | if ( count > 1 ) buf.append(plural); 85 | else buf.append(singular); 86 | } 87 | return; 88 | } 89 | 90 | @Override 91 | public hudson.model.User getAuthor() { 92 | return hudson.model.User.getUnknown(); 93 | } 94 | 95 | @Override 96 | public ChangeLogSet getParent() { 97 | return parent; 98 | } 99 | 100 | @SuppressWarnings("rawtypes") 101 | @Override 102 | protected void setParent(ChangeLogSet parent) { 103 | this.parent = (ChangelogSet)parent; 104 | } 105 | 106 | @CheckForNull 107 | public Date getDate() { 108 | return date != null ? (Date)date.clone() : null; 109 | } 110 | 111 | public void setDate(Date date) { 112 | this.date = date != null ? (Date)date.clone() : null; 113 | } 114 | 115 | @Override 116 | public int hashCode() { 117 | final int prime = 31; 118 | int result = 1; 119 | result = prime * result + ((date == null) ? 0 : date.hashCode()); 120 | result = prime * result + ((paths == null) ? 0 : paths.hashCode()); 121 | return result; 122 | } 123 | 124 | @Override 125 | public boolean equals(Object obj) { 126 | if (this == obj) 127 | return true; 128 | if (obj == null) 129 | return false; 130 | if (getClass() != obj.getClass()) 131 | return false; 132 | final Changelog other = (Changelog) obj; 133 | if (date == null) { 134 | if (other.date != null) 135 | return false; 136 | } else if (!date.equals(other.date)) 137 | return false; 138 | if (paths == null) { 139 | if (other.paths != null) 140 | return false; 141 | } else if (!paths.equals(other.paths)) 142 | return false; 143 | return true; 144 | } 145 | 146 | /** A changed file in Changelog 147 | * 148 | * @author Sam NG 149 | * 150 | */ 151 | public static class Path implements hudson.scm.ChangeLogSet.AffectedFile { 152 | 153 | /** The filepath of the modified file 154 | * 155 | */ 156 | private String value; 157 | 158 | /** Either "ADD", "DELETE" or "EDIT" 159 | * 160 | */ 161 | private String action; 162 | 163 | /** The parent changelog object this child belongs to 164 | * 165 | */ 166 | private Changelog changelog; 167 | 168 | public Path() { 169 | // do nothing, only for serialization 170 | } 171 | 172 | public Path(Changelog changelog) { 173 | this.changelog = changelog; 174 | } 175 | 176 | public Path(Changelog changelog, FolderDiff.Entry entry) { 177 | this.changelog = changelog; 178 | setValue(entry.getFilename()); 179 | if ( FolderDiff.Entry.Type.NEW == entry.getType() ) setAction("ADD"); 180 | else if ( FolderDiff.Entry.Type.DELETED == entry.getType() ) setAction("DELETE"); 181 | else setAction("EDIT"); 182 | } 183 | 184 | /** 185 | * Inherited from AffectedFile 186 | */ 187 | public String getPath() { 188 | return getValue(); 189 | } 190 | 191 | public String getValue() { 192 | return value; 193 | } 194 | 195 | public void setValue(String value) { 196 | this.value = value; 197 | } 198 | 199 | public String getAction() { 200 | return action; 201 | } 202 | 203 | public void setAction(String action) { 204 | this.action = action; 205 | } 206 | 207 | public Changelog getChangelog() { 208 | return changelog; 209 | } 210 | 211 | protected void setChangelog(Changelog changelog) { 212 | this.changelog = changelog; 213 | } 214 | 215 | public hudson.scm.EditType getEditType() 216 | { 217 | if( "ADD".equalsIgnoreCase(action) ) return EditType.ADD; 218 | else if( "DELETE".equalsIgnoreCase(action) ) return EditType.DELETE; 219 | else return EditType.EDIT; 220 | } 221 | 222 | @Override 223 | public int hashCode() { 224 | final int prime = 31; 225 | int result = 1; 226 | result = prime * result 227 | + ((action == null) ? 0 : action.hashCode()); 228 | result = prime * result + ((value == null) ? 0 : value.hashCode()); 229 | return result; 230 | } 231 | 232 | @Override 233 | public boolean equals(Object obj) { 234 | if (this == obj) 235 | return true; 236 | if (obj == null) 237 | return false; 238 | if (getClass() != obj.getClass()) 239 | return false; 240 | final Path other = (Path) obj; 241 | if (action == null) { 242 | if (other.action != null) 243 | return false; 244 | } else if (!action.equals(other.action)) 245 | return false; 246 | if (value == null) { 247 | if (other.value != null) 248 | return false; 249 | } else if (!value.equals(other.value)) 250 | return false; 251 | return true; 252 | } 253 | } 254 | } -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/filesystem_scm/FolderDiffTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import hudson.plugins.filesystem_scm.FolderDiff.Entry; 4 | import org.apache.commons.io.FileUtils; 5 | import org.apache.commons.lang.SystemUtils; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.io.TempDir; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | import java.util.Set; 18 | 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.junit.jupiter.api.Assertions.assertThrows; 21 | import static org.junit.jupiter.api.Assertions.assertTrue; 22 | 23 | class FolderDiffTest { 24 | 25 | private File src; 26 | private File dst; 27 | @TempDir 28 | private File srcFolder; 29 | @TempDir 30 | private File dstFolder; 31 | private String rootFilePath; 32 | private String subfolderFilePath; 33 | private String folderFilePath; 34 | private long currentTestExecutionTime; 35 | private static final long ONE_MINUTE = 1000 * 60; 36 | 37 | @BeforeEach 38 | void setUp() throws IOException { 39 | // setup src Folder 40 | src = srcFolder; 41 | src.createNewFile(); 42 | subfolderFilePath = createFile(src, "Folder", "subFolder", "subFolderFile.txt"); 43 | folderFilePath = createFile(src, "Folder", "FolderFile.git"); 44 | rootFilePath = createFile(src, "RootFile.java"); 45 | 46 | // setup destination Folder 47 | dst = dstFolder; 48 | dst.createNewFile(); 49 | createFile(dst, subfolderFilePath); 50 | createFile(dst, folderFilePath); 51 | createFile(dst, rootFilePath); 52 | 53 | currentTestExecutionTime = System.currentTimeMillis(); 54 | } 55 | 56 | private String createFile(File root, String... strings) throws IOException { 57 | String path = TestUtils.createPlatformDependentPath(strings); 58 | File file = new File(root, path); 59 | FileUtils.touch(file); 60 | assertTrue(file.exists()); 61 | return path; 62 | } 63 | 64 | @Test 65 | void getFiles2Delete_noRemovedSrcFiles_nothing2Delete() throws IOException, InterruptedException { 66 | assertMarkAsDelete(new HashSet<>(), src, dst); 67 | } 68 | 69 | @Test 70 | void getFiles2Delete_allDestinationFolderDeleted_nothing2Delete() throws IOException, InterruptedException { 71 | FileUtils.deleteDirectory(dst); 72 | assertMarkAsDelete(new HashSet<>(), src, dst); 73 | } 74 | 75 | @Test 76 | void getFiles2Delete_aSourceFolderDeleted_markAllFilesForDeletion() 77 | throws IOException, InterruptedException { 78 | FileUtils.deleteDirectory(src); 79 | Set expected = new HashSet<>(); 80 | expected.add(new Entry(folderFilePath, FolderDiff.Entry.Type.DELETED)); 81 | expected.add(new Entry(rootFilePath, FolderDiff.Entry.Type.DELETED)); 82 | expected.add(new Entry(subfolderFilePath, FolderDiff.Entry.Type.DELETED)); 83 | assertMarkAsDelete(expected, src, dst); 84 | } 85 | 86 | @Test 87 | void getFilesNewOrModifiedFiles_noNewOrModifiedFilesLastBuildTimeNow_nothing2Add() throws IOException { 88 | assertMarkAsNewOrModified(new HashSet<>(), currentTestExecutionTime, src, dst); 89 | } 90 | 91 | @Test 92 | void getFilesNewOrModifiedFiles_DestinationFolderDeleted_AllNewFiles() throws IOException { 93 | FileUtils.deleteDirectory(dst); 94 | Set expected = new HashSet<>(); 95 | expected.add(new Entry(folderFilePath, FolderDiff.Entry.Type.NEW)); 96 | expected.add(new Entry(rootFilePath, FolderDiff.Entry.Type.NEW)); 97 | expected.add(new Entry(subfolderFilePath, FolderDiff.Entry.Type.NEW)); 98 | assertMarkAsNewOrModified(expected, 0L, src, dst); 99 | } 100 | 101 | @Test 102 | void getFilesNewOrModifiedFiles_SourceFolderDeleted_ExceptionThrown() throws IOException { 103 | FileUtils.deleteDirectory(src); 104 | assertThrows(IOException.class, () -> 105 | assertMarkAsNewOrModified(new HashSet<>(), 0L, src, dst)); 106 | } 107 | 108 | @Test 109 | void getFilesNewOrModifiedFiles_SourceFileModificationDateNewerThenLastBuildTime_AddAllModifiedFiles() 110 | throws IOException { 111 | Set expected = new HashSet<>(); 112 | expected.add(new Entry(folderFilePath, FolderDiff.Entry.Type.MODIFIED)); 113 | expected.add(new Entry(rootFilePath, FolderDiff.Entry.Type.MODIFIED)); 114 | expected.add(new Entry(subfolderFilePath, FolderDiff.Entry.Type.MODIFIED)); 115 | assertMarkAsNewOrModified(expected, currentTestExecutionTime - ONE_MINUTE, src, dst); 116 | } 117 | 118 | @Test 119 | void getFilesNewOrModifiedFiles_OneSourceFileNewerThanDestinationFile_SourceFileModified() 120 | throws IOException { 121 | Set expected = new HashSet<>(); 122 | expected.add(new Entry(folderFilePath, FolderDiff.Entry.Type.MODIFIED)); 123 | // implementation works only when times between file modification dates are at 124 | // least different by 1000 mys == 1 second 125 | // setModification Time in the future -> therefore the destination file needs to 126 | // be updated 127 | assertTrue((new File(src, folderFilePath)).setLastModified(currentTestExecutionTime + ONE_MINUTE)); 128 | assertMarkAsNewOrModified(expected, currentTestExecutionTime, src, dst); 129 | } 130 | 131 | @Test 132 | void getFilesNewOrModified_NewHiddenFileAndActOnHiddenFiles_HiddenFileIdentifiedAsNew() throws IOException { 133 | // setup 134 | String hiddenFilePath = createFile(src, "Folder", "subFolder", "._HiddenFile"); 135 | hideFile(hiddenFilePath); 136 | 137 | Set expected = new HashSet<>(); 138 | expected.add(new Entry(hiddenFilePath, FolderDiff.Entry.Type.NEW)); 139 | // execute 140 | FolderDiffFake diff = getFolderDiff(src, dst); 141 | List actualResult = diff.getNewOrModifiedFiles(currentTestExecutionTime + ONE_MINUTE, false); 142 | // check 143 | assertMarkAsNewOrModified(expected, actualResult, diff); 144 | } 145 | 146 | @Test 147 | void getFilesNewOrModified_NewHiddenFileButIgnoreHidden_NoNewFile() throws IOException { 148 | // setup 149 | String hiddenFilePathString = createFile(src, "Folder", "subFolder", "._HiddenFile"); 150 | hideFile(hiddenFilePathString); 151 | // execute 152 | FolderDiffFake diff = getFolderDiff(src, dst); 153 | diff.setIgnoreHidden(true); 154 | List actualResult = diff.getNewOrModifiedFiles(currentTestExecutionTime + ONE_MINUTE, false); 155 | // check 156 | assertMarkAsNewOrModified(new HashSet<>(), actualResult, diff); 157 | } 158 | 159 | @Test 160 | void createAndLogg_CountsEntriesCorrectly() { 161 | FolderDiffFake diff = getFolderDiff(src, dst); 162 | 163 | // Test NEW files counting 164 | FolderDiff.Entry newEntry = diff.createAndLogg("newFile.txt", FolderDiff.Entry.Type.NEW); 165 | assertEquals(1, diff.getNewCount()); 166 | assertEquals(0, diff.getModifiedCount()); 167 | assertEquals(0, diff.getDeletedCount()); 168 | assertEquals("newFile.txt", newEntry.getFilename()); 169 | assertEquals(FolderDiff.Entry.Type.NEW, newEntry.getType()); 170 | 171 | // Test MODIFIED files counting 172 | FolderDiff.Entry modifiedEntry = diff.createAndLogg("modifiedFile.txt", FolderDiff.Entry.Type.MODIFIED); 173 | assertEquals(1, diff.getNewCount()); 174 | assertEquals(1, diff.getModifiedCount()); 175 | assertEquals(0, diff.getDeletedCount()); 176 | 177 | // Testing DELETED files counting 178 | FolderDiff.Entry deletedEntry = diff.createAndLogg("deletedFile.txt", FolderDiff.Entry.Type.DELETED); 179 | assertEquals(1, diff.getNewCount()); 180 | assertEquals(1, diff.getModifiedCount()); 181 | assertEquals(1, diff.getDeletedCount()); 182 | 183 | // Testing multiple operations 184 | diff.createAndLogg("anotherNew.txt", FolderDiff.Entry.Type.NEW); 185 | diff.createAndLogg("anotherModified.txt", FolderDiff.Entry.Type.MODIFIED); 186 | 187 | assertEquals(2, diff.getNewCount()); 188 | assertEquals(2, diff.getModifiedCount()); 189 | assertEquals(1, diff.getDeletedCount()); 190 | } 191 | 192 | private void hideFile(String hiddenFilePathString) throws IOException { 193 | File hiddenFile = new File(src, hiddenFilePathString); 194 | // Windows specific code 195 | if (SystemUtils.IS_OS_WINDOWS) { 196 | Path hiddenFilePath = Paths.get(hiddenFile.getAbsolutePath()); 197 | Files.setAttribute(hiddenFilePath, "dos:hidden", true); 198 | } 199 | assertTrue(hiddenFile.isHidden()); 200 | } 201 | 202 | private void assertMarkAsNewOrModified(Set expected, List actual, 203 | FolderDiffFake diff) { 204 | assertEquals(expected, new HashSet<>(actual)); 205 | assertEquals(expected.size(), diff.copyFilePairs.size()); 206 | } 207 | 208 | private void assertMarkAsNewOrModified(Set expected, long lastBuildTime, File src, File dst) 209 | throws IOException { 210 | FolderDiffFake diff = getFolderDiff(src, dst); 211 | List actualResult = diff.getNewOrModifiedFiles(lastBuildTime, false); 212 | assertMarkAsNewOrModified(expected, actualResult, diff); 213 | } 214 | 215 | private void assertMarkAsDelete(Set expected, File src, File dst) 216 | throws InterruptedException, IOException { 217 | FolderDiffFake diff = getFolderDiff(src, dst); 218 | List result = diff.getFiles2Delete(false); 219 | assertEquals(expected, new HashSet<>(result)); 220 | assertEquals(expected.size(), diff.deleteFiles.size()); 221 | } 222 | 223 | private FolderDiffFake getFolderDiff(File src, File dst) { 224 | return new FolderDiffFake<>(src.getAbsolutePath(), dst.getAbsolutePath()); 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/FSSCM.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.IOException; 6 | import java.io.PrintStream; 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | import javax.annotation.CheckForNull; 13 | 14 | import org.apache.commons.io.filefilter.WildcardFileFilter; 15 | import org.apache.commons.lang.StringUtils; 16 | import org.jenkinsci.Symbol; 17 | import org.kohsuke.accmod.Restricted; 18 | import org.kohsuke.accmod.restrictions.NoExternalUse; 19 | import org.kohsuke.stapler.DataBoundConstructor; 20 | import org.kohsuke.stapler.QueryParameter; 21 | import org.kohsuke.stapler.StaplerRequest; 22 | 23 | import hudson.Extension; 24 | import hudson.FilePath; 25 | import hudson.Launcher; 26 | import hudson.model.Job; 27 | import hudson.model.Run; 28 | import hudson.model.TaskListener; 29 | import hudson.plugins.filesystem_scm.ChangelogSet.XMLSerializer; 30 | import hudson.scm.ChangeLogParser; 31 | import hudson.scm.PollingResult; 32 | import hudson.scm.SCM; 33 | import hudson.scm.SCMDescriptor; 34 | import hudson.scm.SCMRevisionState; 35 | import hudson.util.FormValidation; 36 | import jenkins.model.Jenkins; 37 | import net.sf.json.JSONObject; 38 | 39 | /** 40 | * {@link SCM} implementation which watches a file system folder. 41 | */ 42 | public class FSSCM extends SCM { 43 | 44 | /** 45 | * The source folder 46 | * 47 | */ 48 | private String path; 49 | /** 50 | * If true, will delete everything in workspace every time before we checkout 51 | * 52 | */ 53 | private boolean clearWorkspace; 54 | 55 | /** 56 | * If true, it will log all the file names along with their entry types. Default is true (for backward compatibility). 57 | * 58 | */ 59 | private boolean verboseLogging = true; 60 | 61 | /** 62 | * If true, will copy hidden files and folders. Default is false. 63 | * 64 | */ 65 | private transient boolean copyHidden; 66 | /** 67 | * If we have include/exclude filter, then this is true. 68 | * 69 | * @deprecated Moved to {@link FilterSettings} 70 | */ 71 | @Deprecated 72 | private transient boolean filterEnabled; 73 | /** 74 | * Is this filter a include filter or exclude filter 75 | * 76 | * @deprecated Moved to {@link FilterSettings} 77 | */ 78 | @Deprecated 79 | private transient boolean includeFilter; 80 | /** 81 | * filters, which will be passed to {@link WildcardFileFilter}. 82 | * 83 | * @deprecated Moved to {@link FilterSettings} 84 | */ 85 | @Deprecated 86 | private String[] filters; 87 | 88 | /** 89 | * Filter settings. 90 | * 91 | * @since TODO 92 | */ 93 | @CheckForNull 94 | private FilterSettings filterSettings; 95 | 96 | @DataBoundConstructor 97 | public FSSCM(String path, boolean clearWorkspace, boolean copyHidden, boolean verboseLogging, FilterSettings filterSettings) { 98 | this.path = path; 99 | this.clearWorkspace = clearWorkspace; 100 | this.copyHidden = copyHidden; 101 | this.verboseLogging = verboseLogging; 102 | this.filterSettings = filterSettings; 103 | } 104 | 105 | @Deprecated 106 | public FSSCM(String path, boolean clearWorkspace, boolean copyHidden, boolean filterEnabled, boolean includeFilter, 107 | String[] filters) { 108 | this(path, clearWorkspace, copyHidden, true, createFilterSettings(filterEnabled, includeFilter, filters)); 109 | } 110 | 111 | private static FilterSettings createFilterSettings(boolean filterEnabled, boolean includeFilter, String[] filters) { 112 | FilterSettings filterSettings; 113 | if (filterEnabled) { 114 | List selectors = new ArrayList<>(); 115 | if (null != filters) { 116 | for (String filter : filters) { 117 | // remove empty strings 118 | if (StringUtils.isNotEmpty(filter)) { 119 | selectors.add(new FilterSelector(filter)); 120 | } 121 | } 122 | } 123 | filterSettings = new FilterSettings(includeFilter, selectors); 124 | } else { 125 | filterSettings = null; 126 | } 127 | return filterSettings; 128 | } 129 | 130 | public String getPath() { 131 | return path; 132 | } 133 | 134 | public boolean isVerboseLogging() { 135 | return verboseLogging; 136 | } 137 | 138 | public void setVerboseLogging(boolean verboseLogging) { 139 | this.verboseLogging = verboseLogging; 140 | } 141 | 142 | /** 143 | * @return an array of wildcards, or null 144 | * @deprecated Use {@link #getFilterSettings()}. 145 | */ 146 | @Deprecated 147 | @CheckForNull 148 | public String[] getFilters() { 149 | if (filterSettings == null) { 150 | return new String[0]; 151 | } 152 | final List wildcards = filterSettings.getWildcards(); 153 | return wildcards.toArray(new String[wildcards.size()]); 154 | } 155 | 156 | public boolean isFilterEnabled() { 157 | return filterSettings != null; 158 | } 159 | 160 | public boolean isIncludeFilter() { 161 | return filterSettings != null && filterSettings.isIncludeFilter(); 162 | } 163 | 164 | public boolean isClearWorkspace() { 165 | return clearWorkspace; 166 | } 167 | 168 | public boolean isCopyHidden() { 169 | return copyHidden; 170 | } 171 | 172 | @CheckForNull 173 | public FilterSettings getFilterSettings() { 174 | return filterSettings; 175 | } 176 | 177 | protected Object readResolve() { 178 | if (filterEnabled && filterSettings == null) { 179 | final List selectors; 180 | if (filters != null) { 181 | selectors = new ArrayList<>(filters.length); 182 | for (String value : filters) { 183 | selectors.add(new FilterSelector(value)); 184 | } 185 | } else { 186 | selectors = Collections.emptyList(); 187 | } 188 | filterSettings = new FilterSettings(includeFilter, selectors); 189 | } 190 | return this; 191 | } 192 | 193 | @Override 194 | public ChangeLogParser createChangeLogParser() { 195 | return new ChangelogSet.XMLSerializer(); 196 | } 197 | 198 | @Override 199 | public void checkout(Run build, Launcher launcher, FilePath workspace, TaskListener listener, 200 | File changelogFile, SCMRevisionState baseline) throws IOException, InterruptedException { 201 | long start = System.currentTimeMillis(); 202 | PrintStream log = launcher.getListener().getLogger(); 203 | log.println("FSSCM.checkout " + path + " to " + workspace); 204 | 205 | AllowDeleteList allowDeleteList = new AllowDeleteList(build.getParent().getRootDir()); 206 | 207 | if (clearWorkspace) { 208 | log.println("FSSCM.clearWorkspace..."); 209 | workspace.deleteRecursive(); 210 | } 211 | 212 | // we will only delete a file if it is listed in the allowDeleteList 213 | // ie. we will only delete a file if it is copied by us 214 | if (allowDeleteList.fileExists()) { 215 | allowDeleteList.load(); 216 | } else { 217 | // watch list save file doesn't exist 218 | // we will assume all existing files are under watch 219 | // i.e. everything can be deleted 220 | if (workspace.exists()) { 221 | // if we enable clearWorkspace on the 1st jobrun, seems the workspace will be 222 | // deleted 223 | // running a RemoteListDir() on a not existing folder will throw an exception 224 | // anyway, if the folder doesn't exist, we don't need to list the files 225 | Set existingFiles = workspace.act(new RemoteListDir()); 226 | allowDeleteList.setList(existingFiles); 227 | } 228 | } 229 | 230 | RemoteFolderDiff.CheckOut callable = new RemoteFolderDiff.CheckOut(); 231 | setupRemoteFolderDiff(callable, build.getParent(), allowDeleteList.getList()); 232 | List list = workspace.act(callable); 233 | 234 | // maintain the watch list 235 | for (FolderDiff.Entry entry : list) { 236 | if (FolderDiff.Entry.Type.DELETED.equals(entry.getType())) { 237 | allowDeleteList.remove(entry.getFilename()); 238 | } else { 239 | // added or modified 240 | allowDeleteList.add(entry.getFilename()); 241 | } 242 | } 243 | allowDeleteList.save(); 244 | 245 | // raw log 246 | String str = callable.getLog(); 247 | if (str.length() > 0) 248 | log.println(str); 249 | 250 | printLogSummary(log, callable.getNewCount(), callable.getModifiedCount(), callable.getDeletedCount()); 251 | 252 | processChangelog(build, changelogFile, list); 253 | 254 | log.println("FSSCM.check completed in " + formatDuration(System.currentTimeMillis() - start)); 255 | } 256 | 257 | protected void processChangelog(Run build, File changelogFile, List list) 258 | throws FileNotFoundException { 259 | // checking for null as the @CheckForNull Annotation @asks for by SCM.checkout 260 | if (changelogFile != null) { 261 | ChangelogSet.XMLSerializer serializer = createXMLSerializer(); 262 | ChangelogSet changeLogSet = new ChangelogSet(build, list); 263 | serializer.save(changeLogSet, changelogFile); 264 | } 265 | } 266 | 267 | /** 268 | * Logs the count of files checked out. 269 | * 270 | */ 271 | private void printLogSummary(PrintStream log, int newCount, int modifiedCount, int deletedCount) { 272 | int total = newCount + modifiedCount + deletedCount; 273 | 274 | if (total > 0) { 275 | log.printf("Processed %d files (%d new, %d modified, %d deleted)%n", 276 | total, newCount, modifiedCount, deletedCount); 277 | } 278 | } 279 | 280 | private XMLSerializer createXMLSerializer() { 281 | return new ChangelogSet.XMLSerializer(); 282 | } 283 | 284 | /** 285 | * There are two things we need to check 286 | *
    287 | *
  • files created or modified since last build time, we only need to check 288 | * the source folder
  • 289 | *
  • file deleted since last build time, we have to compare source and 290 | * destination folder
  • 291 | *
292 | */ 293 | private boolean poll(Job project, Launcher launcher, FilePath workspace, TaskListener listener) 294 | throws IOException, InterruptedException { 295 | 296 | long start = System.currentTimeMillis(); 297 | 298 | PrintStream log = launcher.getListener().getLogger(); 299 | log.println("FSSCM.pollChange: " + path); 300 | 301 | AllowDeleteList allowDeleteList = new AllowDeleteList(project.getRootDir()); 302 | // we will only delete a file if it is listed in the allowDeleteList 303 | // ie. we will only delete a file if it is copied by us 304 | if (allowDeleteList.fileExists()) { 305 | allowDeleteList.load(); 306 | } else { 307 | // watch list save file doesn't exist 308 | // we will assume all existing files are under watch 309 | // ie. everything can be deleted 310 | Set existingFiles = workspace.act(new RemoteListDir()); 311 | allowDeleteList.setList(existingFiles); 312 | } 313 | 314 | RemoteFolderDiff.PollChange callable = new RemoteFolderDiff.PollChange(); 315 | setupRemoteFolderDiff(callable, project, allowDeleteList.getList()); 316 | 317 | boolean changed = workspace.act(callable); 318 | String str = callable.getLog(); 319 | if (str.length() > 0) 320 | log.println(str); 321 | log.println("FSSCM.pollChange return " + changed); 322 | 323 | log.println("FSSCM.poolChange completed in " + formatDuration(System.currentTimeMillis() - start)); 324 | return changed; 325 | } 326 | 327 | @SuppressWarnings("rawtypes") 328 | private void setupRemoteFolderDiff(RemoteFolderDiff diff, Job project, Set allowDeleteList) { 329 | Run lastBuild = project.getLastBuild(); 330 | if (null == lastBuild) { 331 | diff.setLastBuildTime(0); 332 | diff.setLastSuccessfulBuildTime(0); 333 | } else { 334 | diff.setLastBuildTime(lastBuild.getTimestamp().getTimeInMillis()); 335 | Run lastSuccessfulBuild = project.getLastSuccessfulBuild(); 336 | if (null == lastSuccessfulBuild) { 337 | diff.setLastSuccessfulBuildTime(-1); 338 | } else { 339 | diff.setLastSuccessfulBuildTime(lastSuccessfulBuild.getTimestamp().getTimeInMillis()); 340 | } 341 | } 342 | 343 | diff.setSrcPath(path); 344 | 345 | diff.setIgnoreHidden(!copyHidden); 346 | 347 | diff.setVerboseLogging(verboseLogging); 348 | 349 | if (filterSettings != null) { 350 | if (filterSettings.isIncludeFilter()) { 351 | diff.setIncludeFilter(getFilters()); 352 | } else { 353 | diff.setExcludeFilter(getFilters()); 354 | } 355 | } 356 | 357 | diff.setAllowDeleteList(allowDeleteList); 358 | } 359 | 360 | private static String formatDuration(long diff) { 361 | if (diff < 60 * 1000L) { 362 | // less than 1 minute 363 | if (diff <= 1) 364 | return diff + " millisecond"; 365 | else if (diff < 1000L) 366 | return diff + " milliseconds"; 367 | else if (diff < 2000L) 368 | return ((double) diff / 1000.0) + " second"; 369 | else 370 | return ((double) diff / 1000.0) + " seconds"; 371 | } else { 372 | return org.apache.commons.lang.time.DurationFormatUtils.formatDurationWords(diff, true, true); 373 | } 374 | } 375 | 376 | @Extension 377 | @Symbol("filesystem") 378 | public static final class DescriptorImpl extends SCMDescriptor { 379 | public DescriptorImpl() { 380 | super(FSSCM.class, null); 381 | load(); 382 | } 383 | 384 | @Override 385 | public String getDisplayName() { 386 | return "File System"; 387 | } 388 | 389 | /** 390 | * @param value 391 | * a wildcard pattern 392 | * @return a Formvalidation result, telling you whether the value is correct or 393 | * not 394 | * @deprecated Use 395 | * {@link FilterSelector.DescriptorImpl#doCheckWildcard(java.lang.String)} 396 | */ 397 | @Deprecated 398 | @Restricted(NoExternalUse.class) 399 | public FormValidation doFilterCheck(@QueryParameter final String value) { 400 | return Jenkins.getActiveInstance().getDescriptorByType(FilterSelector.DescriptorImpl.class) 401 | .doCheckWildcard(value); 402 | } 403 | 404 | @Override 405 | public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { 406 | return true; 407 | } 408 | 409 | @Override 410 | public boolean isApplicable(Job project) { 411 | // All job types are supported, the plugin does not depend on 412 | // AbstractProject/AbstractBuild anymore 413 | return true; 414 | } 415 | } 416 | 417 | @Override 418 | public SCMRevisionState calcRevisionsFromBuild(Run build, FilePath workspace, Launcher launcher, 419 | TaskListener listener) throws IOException, InterruptedException { 420 | // we cannot really calculate a sensible revision state for a filesystem folder 421 | // therefore we return NONE and simply ignore the baseline in 422 | // compareRemoteRevisionWith 423 | return SCMRevisionState.NONE; 424 | } 425 | 426 | @Override 427 | public PollingResult compareRemoteRevisionWith(Job project, Launcher launcher, FilePath workspace, 428 | TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException { 429 | if (launcher == null) { 430 | throw new IllegalArgumentException("Launcher cannot be null"); 431 | } 432 | if (poll(project, launcher, workspace, listener)) { 433 | return PollingResult.SIGNIFICANT; 434 | } else { 435 | return PollingResult.NO_CHANGES; 436 | } 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/filesystem_scm/FolderDiff.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.filesystem_scm; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.PrintWriter; 6 | import java.io.Serializable; 7 | import java.io.StringWriter; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.Iterator; 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | import org.apache.commons.io.FileUtils; 18 | import org.apache.commons.io.filefilter.AndFileFilter; 19 | import org.apache.commons.io.filefilter.HiddenFileFilter; 20 | import org.apache.commons.io.filefilter.IOFileFilter; 21 | import org.apache.commons.io.filefilter.NotFileFilter; 22 | import org.apache.commons.io.filefilter.TrueFileFilter; 23 | 24 | import hudson.plugins.filesystem_scm.FolderDiff.Entry.Type; 25 | import hudson.remoting.VirtualChannel; 26 | import jenkins.MasterToSlaveFileCallable; 27 | 28 | /** 29 | * Detect if two folders are the same or not 30 | * 31 | *

32 | * This is the core logic for detecting if we need to checkout or pollchanges 33 | *

34 | * 35 | *

36 | * Two methods to detect if the two folders are the same 37 | *

38 | *
    39 | *
  • check if there are new/modified files in the source folder
  • 40 | *
  • check if there are deleted files in the source folder
  • 41 | *
42 | * 43 | * @param 44 | * Type of the item being returned by the callable 45 | * @author Sam NG 46 | * 47 | */ 48 | public class FolderDiff extends MasterToSlaveFileCallable implements Serializable { 49 | 50 | private static final long serialVersionUID = 1L; 51 | 52 | private String srcPath; 53 | private String dstPath; 54 | private boolean ignoreHidden; 55 | private boolean filterEnabled; 56 | private boolean includeFilter; 57 | private String[] filters; 58 | private Set allowDeleteList; 59 | 60 | private int newCount = 0; 61 | private int modifiedCount = 0; 62 | private int deletedCount = 0; 63 | 64 | public FolderDiff() { 65 | filterEnabled = false; 66 | } 67 | 68 | public void setSrcPath(String srcPath) { 69 | this.srcPath = srcPath; 70 | } 71 | 72 | public void setDstPath(String dstPath) { 73 | this.dstPath = dstPath; 74 | } 75 | 76 | public void setIgnoreHidden(boolean ignoreHidden) { 77 | this.ignoreHidden = ignoreHidden; 78 | } 79 | 80 | public void setIncludeFilter(String[] filters) { 81 | filterEnabled = true; 82 | includeFilter = true; 83 | this.filters = filters; 84 | } 85 | 86 | public void setExcludeFilter(String[] filters) { 87 | filterEnabled = true; 88 | includeFilter = false; 89 | this.filters = filters; 90 | } 91 | 92 | public void setAllowDeleteList(Set allowDeleteList) { 93 | this.allowDeleteList = allowDeleteList; 94 | } 95 | 96 | public int getNewCount() { return newCount; } 97 | public int getModifiedCount() { return modifiedCount; } 98 | public int getDeletedCount() { return deletedCount; } 99 | 100 | /** 101 | * 102 | * @param time 103 | * should be the last build time, to improve performance, we will 104 | * list all files modified after "time" and check with destination 105 | * @param breakOnceFound 106 | * to improve performance, we will return once we found the 1st new 107 | * or modified file 108 | * @param testRun 109 | * is ignored 110 | * @return the list of new or modified files 111 | * @deprecated use the method without testrun, if you just want to have some 112 | * test mode, inherit this class and overwrite the copy method 113 | */ 114 | @Deprecated 115 | public List getNewOrModifiedFiles(long time, boolean breakOnceFound, boolean testRun) { 116 | List entries = new ArrayList(); 117 | try { 118 | entries = getNewOrModifiedFiles(time, breakOnceFound); 119 | } catch (IOException e) { 120 | log(e); 121 | } 122 | return entries; 123 | } 124 | 125 | /** 126 | *

127 | * For each file in the source folder 128 | *

    129 | *
  • if file is not in destination, this is a new file
  • 130 | *
  • if the destination file exists but is old, this is a modified file
  • 131 | *
132 | * 133 | *

134 | * Note: the time parameter (1st param) is basically not used in the code. On 135 | * Windows, the lastModifiedDate will not be updated when you copy a file to the 136 | * source folder, until we have a way to get the "real" lastModifiedDate on 137 | * Windows, we won't use this "time" field 138 | *

139 | * 140 | * @param time 141 | * should be the last build time, to improve performance, we will 142 | * list all files modified after "time" and check with destination 143 | * @param breakOnceFound 144 | * to improve performance, we will return once we found the 1st new 145 | * or modified file 146 | * 147 | * @return the list of new or modified files 148 | * @throws IOException 149 | * when source directory is not found or copying is not successful 150 | */ 151 | public List getNewOrModifiedFiles(long time, boolean breakOnceFound) throws IOException { 152 | File src = new File(srcPath); 153 | File dst = new File(dstPath); 154 | 155 | ArrayList list = new ArrayList(); 156 | if (src.isDirectory()) { 157 | Iterator it = (Iterator) FileUtils.iterateFiles(src, createAntPatternFileFilter(), 158 | getDirFilter()); 159 | while (it.hasNext()) { 160 | File file = it.next(); 161 | String relativeName = getRelativeName(file.getAbsolutePath(), src.getAbsolutePath()); 162 | // need to change dst to see if there is such a file 163 | File tmp = new File(dst, relativeName); 164 | boolean newOrModified = true; 165 | if (!tmp.exists()) {// new 166 | list.add(createAndLogg(relativeName, Entry.Type.NEW)); 167 | } else if (FileUtils.isFileNewer(file, time) || FileUtils.isFileNewer(file, tmp)) { // modified 168 | list.add(createAndLogg(relativeName, Entry.Type.MODIFIED)); 169 | } else { 170 | newOrModified = false; 171 | } 172 | if (newOrModified) { 173 | if (breakOnceFound) { 174 | return list; 175 | } 176 | copyFile(file, tmp); 177 | } 178 | } 179 | } else { 180 | throw new IOException(String.format("Source Directory not found! (%s)", src.getAbsolutePath())); 181 | } 182 | return list; 183 | } 184 | 185 | protected Entry createAndLogg(String relativeName, Type type) { 186 | switch (type) { 187 | case NEW -> newCount++; 188 | case MODIFIED -> modifiedCount++; 189 | case DELETED -> deletedCount++; 190 | } 191 | log(type.name() + " file: " + relativeName); 192 | return new Entry(relativeName, type); 193 | } 194 | 195 | private AndFileFilter createAntPatternFileFilter() { 196 | AndFileFilter fileFilter = new AndFileFilter(); 197 | fileFilter.addFileFilter(getDirFilter()); 198 | // AgeFileFilter is base on lastModifiedDate, but if you copy a file on Windows, 199 | // the lastModifiedDate is not changed 200 | // only the creation date is updated, so we can't use the following 201 | // AgeFileFiilter 202 | // fileFilter.addFileFilter(new AgeFileFilter(time, false /* accept newer */)); 203 | if (filterEnabled && null != filters && filters.length > 0) { 204 | for (int i = 0; i < filters.length; i++) { 205 | IOFileFilter iof = new SimpleAntWildcardFilter(filters[i]); 206 | if (includeFilter) { 207 | fileFilter.addFileFilter(iof); 208 | } else { 209 | fileFilter.addFileFilter(new NotFileFilter(iof)); 210 | } 211 | } 212 | } 213 | return fileFilter; 214 | } 215 | 216 | /** 217 | * 218 | * @param time 219 | * not used 220 | * @param breakOnceFound 221 | * to improve performance, we will return once we found the 1st new 222 | * or modified file 223 | * @param testRun 224 | * not used 225 | * @return the list of deleted files 226 | * @deprecated use getFiles2Delete instead, time never has been used anyway and 227 | * testrun is no longer supported, instead inherit from this class 228 | * and overwrite deleteFiles() for the testmode feature 229 | */ 230 | @Deprecated 231 | public List getDeletedFiles(long time, boolean breakOnceFound, boolean testRun) { 232 | List entries = new ArrayList(); 233 | try { 234 | entries = getFiles2Delete(breakOnceFound); 235 | } catch (IOException e) { 236 | log(e); 237 | } 238 | return entries; 239 | } 240 | 241 | /** 242 | *

243 | * For each file in the destination folder 244 | *

    245 | *
  • if file is not in source, and it is in the allowDeleteList, this file 246 | * will be deleted in the destination
  • 247 | *
248 | * 249 | * @param breakOnceFound 250 | * to improve performance, we will return once we found the 1st new 251 | * or modified file 252 | * 253 | * @return the list of deleted files 254 | * @throws IOException 255 | * if IO error occurs when deleting a file 256 | */ 257 | public List getFiles2Delete(boolean breakOnceFound) throws IOException { 258 | File src = new File(srcPath); 259 | File dst = new File(dstPath); 260 | 261 | IOFileFilter dirFilter = getDirFilter(); 262 | AndFileFilter fileFilter = createAntPatternFileFilter(); 263 | // this is the full list of all viewable/available source files 264 | Collection allSources = new ArrayList<>(); 265 | if (src.isDirectory()) { 266 | allSources = (Collection) FileUtils.listFiles(src, fileFilter, dirFilter); 267 | } 268 | 269 | ArrayList list = new ArrayList(); 270 | if (dst.isDirectory()) { 271 | // now get the list of all sources in workspace (destination) 272 | Iterator it = (Iterator) FileUtils.iterateFiles(dst, TrueFileFilter.TRUE, TrueFileFilter.TRUE); 273 | while (it.hasNext()) { 274 | File file = it.next(); 275 | String relativeName = getRelativeName(file.getAbsolutePath(), dst.getAbsolutePath()); 276 | File tmp = new File(src, relativeName); 277 | if (!allSources.contains(tmp) && (null == allowDeleteList || allowDeleteList.contains(relativeName))) { 278 | list.add(createAndLogg(relativeName, Type.DELETED)); 279 | if (breakOnceFound) { 280 | return list; 281 | } 282 | try { 283 | boolean deleted = deleteFile(file); 284 | if (!deleted) { 285 | log("file.delete() failed: " + file.getAbsolutePath()); 286 | } 287 | } catch (SecurityException e) { 288 | log("Can't delete " + file.getAbsolutePath(), e); 289 | } 290 | } 291 | } 292 | } 293 | return list; 294 | } 295 | 296 | private IOFileFilter getDirFilter() { 297 | return ignoreHidden ? HiddenFileFilter.VISIBLE : TrueFileFilter.TRUE; 298 | } 299 | 300 | /** 301 | * should delete the given file 302 | * 303 | * @param file 304 | * the file to delete 305 | * @return true if successful 306 | * @throws IOException 307 | * if an IOError occurs 308 | */ 309 | protected boolean deleteFile(File file) throws IOException { 310 | Path path = Paths.get(file.getAbsolutePath()); 311 | return Files.deleteIfExists(path); 312 | } 313 | 314 | /** 315 | * This function will convert e.stackTrace to String and call log(String) 316 | * 317 | * @param e 318 | * a thrown Exception which shall be logged 319 | */ 320 | protected void log(Exception e) { 321 | log(stackTraceToString(e)); 322 | } 323 | 324 | /** 325 | * This function will convert e.stackTrace to String and call log(String) 326 | * 327 | * @param msg 328 | * some message to be logged 329 | * @param e 330 | * a thrown Exception which shall be logged too 331 | */ 332 | protected void log(String msg, Exception e) { 333 | log(msg + "\n" + stackTraceToString(e)); 334 | } 335 | 336 | /** 337 | * Default log to System.out 338 | * 339 | * @param msg 340 | * some message to be logged 341 | */ 342 | protected void log(String msg) { 343 | System.out.println(msg); 344 | } 345 | 346 | /** 347 | * Convert Exception.stackTrace to String 348 | * 349 | * @param e 350 | * an Exception which shall be converted to string 351 | * 352 | * @return the exceptions stacktrace as string 353 | */ 354 | public static String stackTraceToString(Exception e) { 355 | StringWriter buf = new StringWriter(); 356 | PrintWriter writer = new PrintWriter(buf); 357 | e.printStackTrace(writer); 358 | writer.flush(); 359 | buf.flush(); 360 | return buf.toString(); 361 | } 362 | 363 | /** 364 | * Get the relative path of fileName and folderName 365 | *
    366 | *
  • fileName = c:\abc\def\foo.java
  • 367 | *
  • folderName = c:\abc
  • 368 | *
  • relativeName = def\foo.java 369 | *
370 | * This function will not handle Unix/Windows path separator conversation, but 371 | * will append a java.io.File.separator if folderName does not end with one 372 | * 373 | * @param fileName 374 | * the full path of the file, usually file.getAbsolutePath() 375 | * @param folderName 376 | * the full path of the folder, usually dir.getAbsolutePath() 377 | * @return the relativeName of fileNamae and folderName 378 | * @throws IOException 379 | * if fileName is not relative to folderName 380 | */ 381 | public static String getRelativeName(String fileName, String folderName) throws IOException { 382 | // make sure there is an end separator after folderName 383 | String sep = java.io.File.separator; 384 | if (!folderName.endsWith(sep)) 385 | folderName += sep; 386 | int x = fileName.indexOf(folderName); 387 | if (0 != x) 388 | throw new IOException(fileName + " is not inside " + folderName); 389 | String relativeName = fileName.substring(folderName.length()); 390 | return relativeName; 391 | } 392 | 393 | /** 394 | * Copy file from source to destination (default will not copy file permission) 395 | * 396 | * @param src 397 | * Source File 398 | * @param dst 399 | * Destination File 400 | * @throws IOException 401 | * when copying is not successful an exception could be thrown by 402 | * the underlying function 403 | */ 404 | protected void copyFile(File src, File dst) throws IOException { 405 | FileUtils.copyFile(src, dst); 406 | // TODO: adjust file permissions here maybe 407 | } 408 | 409 | @Override 410 | public T invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { 411 | // Just a default behavior to retain the compatibility 412 | throw new IOException("The method has not been overridden. Cannot execute"); 413 | } 414 | 415 | public static class Entry implements Serializable { 416 | 417 | private static final long serialVersionUID = 1L; 418 | 419 | private String filename; 420 | private Type type; 421 | 422 | public enum Type { 423 | MODIFIED, NEW, DELETED 424 | }; 425 | 426 | public Entry() { 427 | } 428 | 429 | public Entry(String filename, Type type) { 430 | this.filename = filename; 431 | this.type = type; 432 | } 433 | 434 | public String getFilename() { 435 | return filename; 436 | } 437 | 438 | public void setFilename(String filename) { 439 | this.filename = filename; 440 | } 441 | 442 | public Type getType() { 443 | return type; 444 | } 445 | 446 | public void setType(Type type) { 447 | this.type = type; 448 | } 449 | 450 | @Override 451 | public int hashCode() { 452 | final int prime = 31; 453 | int result = 1; 454 | result = prime * result + ((filename == null) ? 0 : filename.hashCode()); 455 | result = prime * result + ((type == null) ? 0 : type.hashCode()); 456 | return result; 457 | } 458 | 459 | @Override 460 | public boolean equals(Object obj) { 461 | if (this == obj) 462 | return true; 463 | if (obj == null) 464 | return false; 465 | if (getClass() != obj.getClass()) 466 | return false; 467 | final Entry other = (Entry) obj; 468 | if (filename == null) { 469 | if (other.filename != null) 470 | return false; 471 | } else if (!filename.equals(other.filename)) 472 | return false; 473 | if (type == null) { 474 | if (other.type != null) 475 | return false; 476 | } else if (!type.equals(other.type)) 477 | return false; 478 | return true; 479 | } 480 | } 481 | } 482 | --------------------------------------------------------------------------------