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 | - [ 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 | -  [JENKINS-40743](https://issues.jenkins-ci.org/browse/JENKINS-40743) -
12 | Make the plugin compatible with Jenkins Pipeline and other Job types
13 |
14 | -  [JENKINS-43993](https://issues.jenkins-ci.org/browse/JENKINS-43993) -
15 | Update the SCM implementation to be compatible with Stapler
16 | Databinding API
17 | -  Cleanup
18 | issues reported by FindBugs and other static analysis checks
19 | -  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 | 
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 extends Entry> 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 |
--------------------------------------------------------------------------------