├── settings.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── examples ├── buildscript.gradle ├── info │ └── build.gradle ├── checkout │ └── build.gradle ├── README.md ├── version │ └── build.gradle └── patch │ ├── build.gradle │ └── sample.patch ├── .gitignore ├── .travis.yml ├── docs ├── SvnUpdate.md ├── SvnDelete.md ├── SvnApplyPatch.md ├── SvnCleanup.md ├── SvnCreatePatch.md ├── ApplyPlugin.md ├── SvnRevert.md ├── SvnExport.md ├── SvnVersion.md ├── SvnCheckout.md ├── SvnAdd.md ├── SvnCommit.md ├── SvnBranch.md ├── SvnTag.md ├── SvnInfo.md └── GeneralConfig.md ├── src ├── main │ └── groovy │ │ └── at │ │ └── bxm │ │ └── gradleplugins │ │ └── svntools │ │ ├── internal │ │ ├── SvnProxy.groovy │ │ ├── SvnBaseTask.groovy │ │ ├── SvnPath.groovy │ │ ├── SimpleAuthenticationManager.groovy │ │ ├── SvnCopy.groovy │ │ └── SvnSupport.groovy │ │ ├── api │ │ ├── SvnDepth.groovy │ │ ├── SvnData.groovy │ │ └── SvnVersionData.groovy │ │ ├── tasks │ │ ├── SvnTag.groovy │ │ ├── SvnBranch.groovy │ │ ├── SvnInfo.groovy │ │ ├── SvnApplyPatch.groovy │ │ ├── SvnVersion.groovy │ │ ├── SvnCleanup.groovy │ │ ├── SvnUpdate.groovy │ │ ├── SvnDelete.groovy │ │ ├── SvnRevert.groovy │ │ ├── SvnExport.groovy │ │ ├── SvnAdd.groovy │ │ ├── SvnCreatePatch.groovy │ │ ├── SvnCommit.groovy │ │ └── SvnCheckout.groovy │ │ ├── SvnToolsPlugin.groovy │ │ └── SvnToolsPluginExtension.groovy └── test │ └── groovy │ └── at │ └── bxm │ └── gradleplugins │ └── svntools │ ├── tasks │ ├── SvnApplyPatchTest.groovy │ ├── SvnCleanupTest.groovy │ ├── SvnUpdateTest.groovy │ ├── SvnRevertTest.groovy │ ├── SvnExportTest.groovy │ ├── SvnCreatePatchTest.groovy │ ├── SvnWorkspaceTestSupport.groovy │ ├── SvnTagDirty.groovy │ ├── SvnTagRemoteTest.groovy │ ├── SvnVersionTest2.groovy │ ├── SvnAddTest.groovy │ ├── SvnBranchTest.groovy │ ├── SvnDeleteTest.groovy │ ├── SvnTagTest.groovy │ ├── SvnVersionTest.groovy │ ├── SvnCommitTest.groovy │ ├── SvnInfoTest.groovy │ └── SvnCheckoutTest.groovy │ ├── SvnToolsPluginTest.groovy │ ├── TestSvnStatusHandler.groovy │ ├── internal │ └── SvnPathTest.groovy │ ├── SvnToolsPluginExtensionTest.groovy │ └── SvnTestSupport.groovy ├── LICENSE ├── release.md ├── README.md ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "gradle-svntools-plugin" 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version = 3.1 2 | releaseNotes = Kotlin-DSL fix (#49) 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martoe/gradle-svntools-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/buildscript.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | mavenLocal() 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath("at.bxm.gradleplugins:gradle-svntools-plugin:latest.integration") 7 | } 8 | -------------------------------------------------------------------------------- /examples/info/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply from: "../buildscript.gradle", to: buildscript 3 | } 4 | apply plugin: "at.bxm.svntools" 5 | 6 | task printRevision << { 7 | println "Current revision is $svntools.info.revisionNumber" 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-bin.zip 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # IDEA IDE files 6 | /*.iml 7 | /*.ipr 8 | /*.iws 9 | /*.idea 10 | 11 | # Eclipse IDE files 12 | /bin/ 13 | .classpath 14 | .project 15 | .settings/ 16 | 17 | /.subversion/ 18 | /examples/workspace/ 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: groovy 2 | before_cache: 3 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 4 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 5 | cache: 6 | directories: 7 | - $HOME/.gradle/caches/ 8 | - $HOME/.gradle/wrapper/ 9 | after_success: 10 | - ./gradlew jacocoTestReport coveralls 11 | -------------------------------------------------------------------------------- /examples/checkout/build.gradle: -------------------------------------------------------------------------------- 1 | import at.bxm.gradleplugins.svntools.tasks.SvnCheckout 2 | 3 | buildscript { 4 | apply from: "../buildscript.gradle", to: buildscript 5 | } 6 | apply plugin: "at.bxm.svntools" 7 | 8 | task clean(type: Delete) { 9 | delete "$project.projectDir/../workspace" 10 | } 11 | 12 | task checkoutSvntools(type: SvnCheckout, dependsOn: clean) { 13 | svnUrl = "https://github.com/martoe/gradle-svntools-plugin/trunk" 14 | workspaceDir = "$project.projectDir/../workspace" 15 | } 16 | -------------------------------------------------------------------------------- /docs/SvnUpdate.md: -------------------------------------------------------------------------------- 1 | ## [SvnUpdate](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnUpdate.groovy) task 2 | 3 | Updates an SVN workspace. 4 | 5 | ### Configuration 6 | 7 | Property | Description | Default value 8 | ------------- | ----------- | ------------- 9 | workspaceDir | The local workspace that should be updated. | `$project.projectDir` 10 | revision | The revision number to be checked out | `HEAD` 11 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 12 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 13 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/internal/SvnProxy.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.internal 2 | 3 | import groovy.transform.ToString 4 | 5 | @ToString(includePackage = false, excludes = "password") 6 | class SvnProxy { 7 | String host 8 | int port 9 | String username 10 | char[] password 11 | String nonProxyHosts 12 | { 13 | host = System.getProperty("http.proxyHost") 14 | port = (System.getProperty("http.proxyPort") ?: -1) as int 15 | username = System.getProperty("http.proxyUser") 16 | password = System.getProperty("http.proxyPassword") ? System.getProperty("http.proxyPassword").chars : null 17 | nonProxyHosts = System.getProperty("http.nonProxyHosts") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/SvnDelete.md: -------------------------------------------------------------------------------- 1 | ## [SvnDelete](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnDelete.groovy) task 2 | 3 | Deletes files and directories (within an SVN working copy) and schedules them to be removed from version control. 4 | 5 | ### Configuration 6 | 7 | Property | Description | Default value 8 | ------------------ | ----------- | ------------- 9 | delete | Files and directories to be deleted by this task | 10 | ignoreErrors | Continue the build if the specified paths conflict with the WC status (can't delete) | `false` 11 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 12 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 13 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnDepth.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.api 2 | /** Checkout/update depth constants, see http://svnbook.red-bean.com/en/1.7/svn.advanced.sparsedirs.html */ 3 | enum SvnDepth { 4 | 5 | /** Include only the immediate target of the operation, not any of its file or directory children. */ 6 | EMPTY, 7 | /** Include the immediate target of the operation and any of its immediate file children. */ 8 | FILES, 9 | /** Include the immediate target of the operation and any of its immediate file or directory children. The directory children will themselves be empty. */ 10 | IMMEDIATES, 11 | /** Include the immediate target, its file and directory children, its children's children, and so on to full recursion. */ 12 | INFINITY 13 | } 14 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnTag.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnCopy 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.tasks.Internal 6 | 7 | /** Creates an SVN tag based on a local SVN workspace. */ 8 | class SvnTag extends SvnCopy { 9 | 10 | /** Name of the new SVN tag (required, no default) */ 11 | @Internal String tagName 12 | 13 | @Override 14 | String getDestinationPath() { 15 | if (!tagName) { 16 | throw new InvalidUserDataException("tagName missing") 17 | } 18 | if (!isValidName(tagName)) { 19 | throw new InvalidUserDataException("tagName contains invalid chars: $tagName") 20 | } 21 | return "tags/$tagName" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Sample Gradle scripts 2 | 3 | To use a local snapshot version instead of the latest release, execute `gradlew clean publishToMavenLocal` first 4 | 5 | 6 | ## SVN Checkout 7 | 8 | Checking out the gradle-svntools-plugin sources: 9 | 10 | gradlew -b examples/checkout/build.gradle checkoutSvntools 11 | 12 | [Sources](checkout/) 13 | 14 | ## SVN Version 15 | 16 | Print some status information for a checked-out workspace: 17 | 18 | gradlew -b examples/version/build.gradle printVersion 19 | 20 | [Sources](version/) 21 | 22 | ## SVN Patches 23 | 24 | Creating a patch file from a modified workspace: 25 | 26 | gradlew -b examples/patch/build.gradle createPatch 27 | 28 | Applying a patch file to a workspace: 29 | 30 | gradlew -b examples/patch/build.gradle applyPatch 31 | 32 | [Sources](patch/) 33 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnBranch.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnCopy 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.tasks.Internal 6 | 7 | /** Creates an SVN branch based on a local SVN workspace. */ 8 | class SvnBranch extends SvnCopy { 9 | 10 | /** Name of the new SVN branch (required, no default) */ 11 | @Internal String branchName 12 | 13 | @Override 14 | String getDestinationPath() { 15 | if (!branchName) { 16 | throw new InvalidUserDataException("branchName missing") 17 | } 18 | if (!isValidName(branchName)) { 19 | throw new InvalidUserDataException("branchName contains invalid chars: $branchName") 20 | } 21 | return "branches/$branchName" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/version/build.gradle: -------------------------------------------------------------------------------- 1 | import at.bxm.gradleplugins.svntools.tasks.SvnVersion 2 | 3 | buildscript { 4 | apply from: "../buildscript.gradle", to: buildscript 5 | } 6 | apply plugin: "at.bxm.svntools" 7 | 8 | task printVersionForProject(description: "Using the implicit 'svntools.version' object - only works if the project is part of an SVN workspace") << { 9 | println "'svnversion' output: $svntools.version" 10 | println svntools.version.modified ? "working copy is dirty" : "working copy is clean" 11 | } 12 | 13 | task printVersion(type: SvnVersion, description: "Using the SvnVersion task for advanced configuration") { 14 | sourcePath = "$project.projectDir/../workspace" 15 | doLast { 16 | println "'svnversion' output: $svnVersion" 17 | println svnVersion.modified ? "working copy is dirty" : "working copy is clean" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/SvnApplyPatch.md: -------------------------------------------------------------------------------- 1 | ## [SvnApplyPatch](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnApplyPatch.groovy) task 2 | 3 | Applies a patch file onto a local workspace directory (without committing the changes). 4 | 5 | ### Configuration 6 | 7 | Property | Description | Default value 8 | ------------- | ----------- | ------------- 9 | patchFile | The name of the patch file, relative to the project directory (required) | 10 | dir | The base directory to apply the patch (must be part of a local SVN workspace) | `$project.projectDir` 11 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 12 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 13 | 14 | ### Example 15 | 16 | See [patch examples](../examples/README.md#svn-patches) 17 | -------------------------------------------------------------------------------- /docs/SvnCleanup.md: -------------------------------------------------------------------------------- 1 | ## [SvnCleanup](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCleanup.groovy) task 2 | 3 | Recursively cleans up the working copy, removing locks and resuming unfinished operations. 4 | 5 | ### Configuration 6 | 7 | Property | Description | Default value 8 | ------------------ | ----------- | ------------- 9 | cleanup | Directories to be recursively cleaned up by this task | 10 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 11 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 12 | 13 | ### Example 14 | 15 | apply plugin: "at.bxm.svntools" 16 | 17 | task svnCleanup(type: at.bxm.gradleplugins.svntools.tasks.SvnCleanup) { 18 | cleanup "$project.projectDir/my-working-copy" 19 | } 20 | -------------------------------------------------------------------------------- /docs/SvnCreatePatch.md: -------------------------------------------------------------------------------- 1 | ## [SvnCreatePatch](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCreatePatch.groovy) task 2 | 3 | Creates a patch file that contains all modifications of a local workspace directory (including subdirectories). 4 | 5 | ### Configuration 6 | 7 | Property | Description | Default value 8 | ------------- | ----------- | ------------- 9 | source | Local workspace files and directories with modifications that shall be saved to a patch file | `$project.projectDir` 10 | patchFile | The name of the target patch file, relative to the project directory (required). If it exists it will be overwritten | 11 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 12 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 13 | 14 | ### Example 15 | 16 | See [patch examples](../examples/README.md#svn-patches) 17 | -------------------------------------------------------------------------------- /docs/ApplyPlugin.md: -------------------------------------------------------------------------------- 1 | ## Applying the plugin 2 | 3 | (see also the [Gradle plugin portal](https://plugins.gradle.org/plugin/at.bxm.svntools) page) 4 | 5 | Binaries are hosted at [Bintray](https://bintray.com/martoe/gradle-plugins/gradle-svntools-plugin/) and are available at the [jcenter Maven repo](https://bintray.com/bintray/jcenter/). 6 | 7 | ### Using the [Gradle plugins DSL](https://www.gradle.org/docs/current/userguide/plugins.html#sec:plugins_block) (Gradle 2.1 and above) 8 | 9 | plugins { 10 | id "at.bxm.svntools" version "latest.release" 11 | } 12 | 13 | ### Using an [external dependency](https://www.gradle.org/docs/current/userguide/organizing_build_logic.html#sec:external_dependencies) 14 | 15 | buildscript { 16 | repositories { 17 | jcenter() 18 | } 19 | dependencies { 20 | classpath "at.bxm.gradleplugins:gradle-svntools-plugin:latest.release" 21 | } 22 | } 23 | apply plugin: "at.bxm.svntools" 24 | -------------------------------------------------------------------------------- /docs/SvnRevert.md: -------------------------------------------------------------------------------- 1 | ## [SvnCommit](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnRevert.groovy) task 2 | 3 | Reverts all local changes of the given files or directories (which must be part of an SVN working copy). 4 | Items that are not under version control will be ignored. 5 | 6 | ### Configuration 7 | 8 | Property | Description | Default value 9 | ------------- | ----------- | ------------- 10 | revert | A list of local workspace files or directories that should be reverted | `project.projectDir` 11 | recursive | Also revert items in subdirectories | `false` 12 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 13 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 14 | 15 | ### Example 16 | 17 | apply plugin: "at.bxm.svntools" 18 | 19 | task revertSources(type: at.bxm.gradleplugins.svntools.tasks.SvnRevert) { 20 | revert "src/main/java", "src/main/resources" 21 | recursive = true 22 | } 23 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnData.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.api 2 | 3 | import groovy.transform.ToString 4 | 5 | /** Provides information about a SVN workspace */ 6 | @ToString(includePackage = false, includeNames = true, ignoreNulls = true) 7 | class SvnData { 8 | static final long UNKNOWN_REVISION = -1 9 | long revisionNumber = UNKNOWN_REVISION 10 | java.util.Date committedDate 11 | String committedAuthor 12 | /** The complete SVN URL of the checked-out project */ 13 | String url 14 | /** The root URL of the SVN repository */ 15 | String repositoryRootUrl 16 | /** 17 | * Either "trunk", the name of the branch (i.e. the path segment succeeding the "branches" segment), or the name of 18 | * the tag (i.e. the path segment succeeding the "tags" segment) 19 | */ 20 | String name 21 | /** If the SVN URL refers to a trunk (i.e. it contains a "trunk" path segment) */ 22 | boolean trunk 23 | /** If the SVN URL refers to a branch (i.e. it contains a "branches" path segment) */ 24 | boolean branch 25 | /** If the SVN URL refers to a tag (i.e. it contains a "tags" path segment) */ 26 | boolean tag 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Martin Ehrnhoefer 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnInfo.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import at.bxm.gradleplugins.svntools.internal.SvnSupport 5 | import org.gradle.api.PathValidation 6 | import org.gradle.api.tasks.Internal 7 | import org.gradle.api.tasks.TaskAction 8 | 9 | /** Provides information about the current SVN workspace */ 10 | class SvnInfo extends SvnBaseTask { 11 | 12 | /** Source path for reading the SVN metadata (default: {@code project.projectDir}) */ 13 | @Internal sourcePath 14 | /** The name of the project extra property that will receive the resulting {@link at.bxm.gradleplugins.svntools.api.SvnData} object (default: {@code svnData}) */ 15 | @Internal String targetPropertyName 16 | /** Continue the build if the specified path doesn't contain SVN data (default: {@code false}) */ 17 | @Internal boolean ignoreErrors 18 | 19 | @TaskAction 20 | def run() { 21 | def srcPath = sourcePath != null ? project.file(sourcePath, PathValidation.EXISTS) : project.projectDir 22 | def result = SvnSupport.createSvnData(srcPath, getUsername(), getPassword(), proxy, ignoreErrors) 23 | project.ext.set(targetPropertyName ?: "svnData", result) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/SvnExport.md: -------------------------------------------------------------------------------- 1 | ## [SvnExport](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnExport.groovy) task 2 | 3 | Exports parts of an SVN repository to a local directory. 4 | Different from the [SvnCheckout](SvnCheckout.md) task, the target directory will receive the versioned files only, but no SVN metadata. 5 | 6 | ### Configuration 7 | 8 | Property | Description | Default value 9 | ------------ | ----------- | ------------- 10 | svnUrl | The remote repository URL (required) | 11 | targetDir | The target directory for export (required). If it doesn't exist it will be created. If it exists it must be empty | 12 | revision | The revision number to be exported | `HEAD` 13 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 14 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 15 | 16 | ### Example 17 | 18 | This Gradle script performs an export from a local SVN repository into `build/export`: 19 | 20 | apply plugin: "at.bxm.svntools" 21 | 22 | task export(type: at.bxm.gradleplugins.svntools.tasks.SvnExport) { 23 | svnUrl = "file:///home/user/svn/repo/myproject/trunk" 24 | targetDir = "$project.buildDir/export" 25 | } 26 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnApplyPatchTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import org.gradle.api.Project 5 | 6 | class SvnApplyPatchTest extends SvnTestSupport { 7 | 8 | File workspace 9 | Project project 10 | SvnApplyPatch task 11 | 12 | def setup() { 13 | createLocalRepo() 14 | workspace = checkoutTrunk() 15 | project = projectWithPlugin() 16 | task = project.task(type: SvnApplyPatch, "applyPatch") as SvnApplyPatch 17 | } 18 | 19 | def "single file"() { 20 | given: "patch for a single file" 21 | def patchFile = new File(tempDir, "myPatch.txt") 22 | patchFile.text = """Index: test.txt 23 | =================================================================== 24 | --- test.txt\t(revision 1) 25 | +++ test.txt\t(working copy) 26 | @@ -0,0 +1 @@ 27 | +changed 28 | \\ No newline at end of file 29 | """ 30 | 31 | when: "running the SvnApplyPatch task" 32 | task.patchFile = patchFile 33 | task.dir = workspace 34 | task.run() 35 | 36 | then: "patch has been applied, but not committed" 37 | def patchedFile = new File(workspace, "test.txt") 38 | patchedFile.text == "changed" 39 | getRevision(patchedFile) == 1 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnVersionData.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.api 2 | 3 | /** Output of the {@link at.bxm.gradleplugins.svntools.tasks.SvnVersion} task */ 4 | class SvnVersionData { 5 | 6 | long minRevisionNumber = SvnData.UNKNOWN_REVISION 7 | long maxRevisionNumber = SvnData.UNKNOWN_REVISION 8 | /** "true" if the working copy contains at least one file with modified content */ 9 | boolean modified 10 | /** "true" if parts of the working copy have been switched */ 11 | boolean switched 12 | /** "true" if the working copy is sparsely populated (i.e. "depth" is not "infinity") */ 13 | boolean sparse 14 | 15 | boolean isMixedRevision() { 16 | return minRevisionNumber != maxRevisionNumber 17 | } 18 | 19 | @Override 20 | String toString() { 21 | if (minRevisionNumber == SvnData.UNKNOWN_REVISION) { 22 | return "exported" // see bottom of http://svnbook.red-bean.com/en/1.6/svn.ref.svnversion.re.html 23 | } 24 | def result = mixedRevision ? "$minRevisionNumber:$maxRevisionNumber" : "$minRevisionNumber" 25 | if (modified) { 26 | result += "M" 27 | } 28 | if (switched) { 29 | result += "S" 30 | } 31 | if (sparse) { 32 | result += "P" 33 | } 34 | return result 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/SvnVersion.md: -------------------------------------------------------------------------------- 1 | ## [SvnVersion](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnVersion.groovy) task 2 | 3 | Creates a [SvnVersionData](../src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnVersionData.groovy) object (see [General Configuration](GeneralConfig.md)) that contains information about an SVN working copy (similar to the [svnversion](http://svnbook.red-bean.com/en/1.7/svn.ref.svnversion.re.html) command). 4 | The object is added as an "extra property" to the Gradle project and may be accessed with `$project.svnVersion`. 5 | 6 | ### Configuration 7 | 8 | Property | Description | Default value 9 | ------------------ | ----------- | ------------- 10 | sourcePath | Local SVN working copy directory | `$project.projectDir` 11 | targetPropertyName | The name of the project extra property that will receive the resulting SvnVersionData object | `svnVersion` 12 | ignoreErrors | Continue the build if the specified path is no SVN working copy | `false` 13 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 14 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 15 | 16 | ### Example 17 | 18 | See [version examples](../examples/README.md#svn-version) 19 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | Release process steps, to be automated... 2 | 3 | 1. checkout "master" and merge "develop" 4 | ``` 5 | git checkout master 6 | git merge --no-ff develop 7 | ``` 8 | 9 | 2. remove "-SNAPSHOT" and set release-notes in gradle.properties 10 | 11 | 3. `gradlew clean publishPlugins -PbintrayUser=*** -PbintrayApiKey=*** -Pgradle.publish.key=*** -Pgradle.publish.secret=***` 12 | 13 | 4. commit and push gradle.properties 14 | ``` 15 | git commit -am "Release 3.0" 16 | git push 17 | ``` 18 | 19 | 5. [create Github release](https://github.com/martoe/gradle-svntools-plugin/releases/new) 20 | * Tag version: "v0.0" for feature releases, "v0.0.0" for bugfix releases, @master 21 | * Release title: same as "Tag version" 22 | * Description: from `gradle.properties` 23 | 24 | 6. [close current milestone](https://github.com/martoe/gradle-svntools-plugin/milestones) and all assigned issues 25 | 26 | 6. [create next milestone](https://github.com/martoe/gradle-svntools-plugin/milestones/new) 27 | 28 | 6. checkout "develop" and rebase against "master" 29 | ``` 30 | git checkout develop 31 | git rebase master 32 | ``` 33 | 34 | 7. bump version and add "-SNAPSHOT" in gradle.properties (and reset release-notes) 35 | 36 | 8. commit and push gradle.properties 37 | ``` 38 | git commit -am "Post-release" 39 | git push 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnApplyPatch.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.tasks.Internal 6 | import org.gradle.api.tasks.TaskAction 7 | import org.tmatesoft.svn.core.SVNException 8 | 9 | import static org.gradle.api.PathValidation.* 10 | 11 | /** 12 | * Applies a patch file onto a local workspace directory (without committing the changes). 13 | */ 14 | class SvnApplyPatch extends SvnBaseTask { 15 | 16 | /** The name of the patch file, relative to the project directory (required) */ 17 | @Internal patchFile 18 | /** The base directory to apply the patch, must be part of a local SVN workspace (default: `$project.projectDir`) */ 19 | @Internal dir 20 | 21 | @TaskAction 22 | def run() { 23 | def sourceFile = project.file(patchFile, FILE) 24 | def targetDir = dir ? project.file(dir, DIRECTORY) : project.projectDir 25 | logger.debug("Applying patch {} to {}", sourceFile, targetDir.absolutePath) 26 | try { 27 | createSvnClientManager().diffClient.doPatch(sourceFile, targetDir, false, 0) 28 | } catch (SVNException e) { 29 | throw new InvalidUserDataException("svn-patch failed for $sourceFile\n" + e.message, e) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/SvnToolsPluginTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools 2 | 3 | import at.bxm.gradleplugins.svntools.tasks.* 4 | 5 | class SvnToolsPluginTest extends SvnTestSupport { 6 | 7 | def "apply plugin"() { 8 | when: "applying the plugin" 9 | def project = projectWithPlugin() 10 | 11 | then: "extension is defined" 12 | project.extensions.getByType(SvnToolsPluginExtension) 13 | } 14 | 15 | def "Tasks are available by name"(String name, Class taskClass) { 16 | expect: 17 | def project = projectWithPlugin() 18 | def task = project.task(type: project."${name}", name.toLowerCase()) 19 | task in taskClass 20 | 21 | where: 22 | name | taskClass 23 | 'SvnAdd' | SvnAdd.class 24 | 'SvnApplyPatch' | SvnApplyPatch.class 25 | 'SvnBranch' | SvnBranch.class 26 | 'SvnCheckout' | SvnCheckout.class 27 | 'SvnCleanup' | SvnCleanup.class 28 | 'SvnCommit' | SvnCommit.class 29 | 'SvnCreatePatch' | SvnCreatePatch.class 30 | 'SvnDelete' | SvnDelete.class 31 | 'SvnExport' | SvnExport.class 32 | 'SvnInfo' | SvnInfo.class 33 | 'SvnRevert' | SvnRevert.class 34 | 'SvnTag' | SvnTag.class 35 | 'SvnUpdate' | SvnUpdate.class 36 | 'SvnVersion' | SvnVersion.class 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnVersion.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.api.SvnVersionData 4 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 5 | import at.bxm.gradleplugins.svntools.internal.SvnSupport 6 | import org.gradle.api.PathValidation 7 | import org.gradle.api.tasks.Internal 8 | import org.gradle.api.tasks.TaskAction 9 | 10 | /** Provides information similar to the "svnversion" command */ 11 | class SvnVersion extends SvnBaseTask { 12 | 13 | /** Local SVN working copy directory (default: {@code project.projectDir}) */ 14 | @Internal sourcePath 15 | /** The name of the project extra property that will receive the resulting {@link SvnVersionData} object (default: {@code svnVersion}) */ 16 | @Internal String targetPropertyName 17 | /** Continue the build if the specified path is no SVN working copy */ 18 | @Internal boolean ignoreErrors 19 | 20 | @TaskAction 21 | def run() { 22 | def srcPath = sourcePath != null ? project.file(sourcePath, PathValidation.EXISTS) : project.projectDir 23 | def result = SvnSupport.createSvnVersionData(srcPath, getUsername(), getPassword(), proxy, ignoreErrors) 24 | project.ext.set(targetPropertyName ?: "svnVersion", result) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/SvnCheckout.md: -------------------------------------------------------------------------------- 1 | ## [SvnCheckout](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCheckout.groovy) task 2 | 3 | Creates an SVN workspace by checking out an SVN URL to a local directory. Also supports updating an existing workspace. 4 | If you just need the versioned files without SVN metadata, use the [SvnExport](SvnExport.md) task. 5 | 6 | ### Configuration 7 | 8 | Property | Description | Default value 9 | ------------ | ----------- | ------------- 10 | svnUrl | The remote repository URL (required) | 11 | workspaceDir | The target directory for checkout (required). If it doesn't exist it will be created. If it exists (and the `update` flag is not set) it must be empty | 12 | revision | The revision number to be checked out | `HEAD` 13 | depth | The checkout depth - either `empty`, `files`, `immediates`, or `infinity`. See [SvnDepth](../src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnDepth.groovy) | `infinity` 14 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 15 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 16 | update | If "workspaceDir" already contains checked-out data, update it instead of performing a fresh checkout | `false` 17 | 18 | ### Example 19 | 20 | See [checkout examples](../examples/README.md#svn-checkout) 21 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCleanup.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import at.bxm.gradleplugins.svntools.internal.SvnSupport 5 | import org.gradle.api.InvalidUserDataException 6 | import org.gradle.api.tasks.Internal 7 | import org.gradle.api.tasks.TaskAction 8 | import org.tmatesoft.svn.core.SVNException 9 | 10 | /** Performs a "svn cleanup" on a working-copy directory */ 11 | class SvnCleanup extends SvnBaseTask { 12 | 13 | @Internal final cleanup = [] 14 | 15 | /** To specify directories to be cleaned up */ 16 | void setCleanup(target) { 17 | cleanup.clear() 18 | cleanup << target 19 | } 20 | 21 | void cleanup(Object... targets) { 22 | targets.each { 23 | cleanup << it 24 | } 25 | } 26 | 27 | @TaskAction 28 | def run() { 29 | def wcClient = SvnSupport.createSvnClientManager(username, password, proxy).WCClient 30 | project.files(cleanup).each { file -> 31 | if (file.isDirectory()) { 32 | try { 33 | wcClient.doCleanup(file) 34 | } catch (SVNException e) { 35 | throw new InvalidUserDataException("svn-cleanup failed for $file.absolutePath\n" + e.message, e) 36 | } 37 | } else { 38 | throw new InvalidUserDataException("Not a directory: $file") 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCleanupTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | 5 | class SvnCleanupTest extends SvnWorkspaceTestSupport { 6 | 7 | def "cleanup two directories"() { 8 | given: "an SVN working copy" 9 | 10 | when: "running the SvnCleanup task" 11 | def task = taskWithType(SvnCleanup) 12 | task.cleanup(workspace, new File(workspace, "dir")) 13 | task.run() 14 | 15 | then: "no error" 16 | } 17 | 18 | def "cleanup a file"() { 19 | given: 20 | def workspaceFile = existingFile("test.txt") 21 | 22 | when: "running the SvnCleanup task" 23 | def task = taskWithType(SvnCleanup) 24 | task.cleanup = workspaceFile 25 | task.run() 26 | 27 | then: "exception" 28 | def e = thrown InvalidUserDataException 29 | e.message =~ "Not a directory: .*" 30 | } 31 | 32 | def "cleanup a non-working-copy directory"() { 33 | given: 34 | def unversioned = new File(workspace, "newDir") 35 | unversioned.mkdir() 36 | 37 | when: "running the SvnCleanup task" 38 | def task = taskWithType(SvnCleanup) 39 | task.cleanup(unversioned) 40 | task.run() 41 | 42 | then: "exception" 43 | def e = thrown InvalidUserDataException 44 | def msg = e.message.readLines() 45 | msg[0] =~ "svn-cleanup failed for .*" 46 | msg[1] =~ "svn: E155007: .* is not a working copy directory" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/internal/SvnBaseTask.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.internal 2 | 3 | import at.bxm.gradleplugins.svntools.SvnToolsPluginExtension 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.tasks.Internal 6 | import org.tmatesoft.svn.core.wc.SVNClientManager 7 | 8 | abstract class SvnBaseTask extends DefaultTask { 9 | 10 | /** The SVN username - leave empty if no authentication is required (default: {@code project.svntools.username}) */ 11 | @Internal String username 12 | /** The SVN password - leave empty if no authentication is required (default: {@code project.svntools.password}) */ 13 | private char[] password 14 | 15 | SVNClientManager createSvnClientManager() { 16 | return SvnSupport.createSvnClientManager(getUsername(), getPassword(), proxy) 17 | } 18 | 19 | String getUsername() { 20 | return username ?: project.extensions.getByType(SvnToolsPluginExtension).username 21 | } 22 | 23 | @Internal 24 | Object getPassword() { 25 | return password ?: project.extensions.getByType(SvnToolsPluginExtension).password 26 | } 27 | 28 | void setPassword(Object password) { 29 | if (password != null) { 30 | this.password = password instanceof char[] ?: password.toString().chars 31 | } else { 32 | this.password = null 33 | } 34 | } 35 | 36 | @Internal 37 | SvnProxy getProxy() { 38 | return project.extensions.getByType(SvnToolsPluginExtension).proxy 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/SvnAdd.md: -------------------------------------------------------------------------------- 1 | ## [SvnAdd](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnAdd.groovy) task 2 | 3 | Schedules files (within an SVN working copy) to be added to version control. 4 | 5 | ### Configuration 6 | 7 | Property | Description | Default value 8 | ------------------ | ----------- | ------------- 9 | add | Files and directories to be added by this task | 10 | recursive | Also add items in subdirectories | `false` 11 | ignoreErrors | Continue the build if the specified paths conflict with the WC status (can't add) | `false` 12 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 13 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 14 | 15 | 16 | ### Example 17 | 18 | This Gradle script creates a `svn.properties` file that contains the SVN URL and revision of the buildfile, and adds it to the JAR artifact: 19 | 20 | apply plugin: "at.bxm.svntools" 21 | 22 | version = "1.0-SNAPSHOT" 23 | String previousVersion = "0.1" 24 | 25 | task createChangelog() { 26 | file("changelog_${project.version}.txt").text = "list of changes for this release..." 27 | 28 | file("changelog_${previousVersion}.txt") 29 | } 30 | 31 | task addChangelog(type: at.bxm.gradleplugins.svntools.tasks.SvnAdd, dependsOn: createChangelog) { 32 | add "changelog_${project.version}.txt" 33 | } 34 | -------------------------------------------------------------------------------- /docs/SvnCommit.md: -------------------------------------------------------------------------------- 1 | ## [SvnCommit](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCommit.groovy) task 2 | 3 | Commits a list of files (and directories). All files and directories must be part of the same SVN workspace. 4 | 5 | ### Configuration 6 | 7 | Property | Description | Default value 8 | ------------- | ----------- | ------------- 9 | source | A list of files and directories that should be committed. If these are not under version control already, they will be added first. If this list is empty of the files contain no modifications, no commit will be executed | 10 | recursive | Also commit items in subdirectories | `false` 11 | commitMessage | A commit message (optional) | 12 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 13 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 14 | 15 | ### Example 16 | 17 | This Gradle script commits a changelog file to SVN: 18 | 19 | apply plugin: "at.bxm.svntools" 20 | 21 | version = "1.0-SNAPSHOT" 22 | 23 | task createChangelog() { 24 | project.ext.changelog = file("changelog_${project.version}.txt") 25 | project.ext.changelog.text = "list of changes for this release..." 26 | } 27 | 28 | task commitChangelog(type: at.bxm.gradleplugins.svntools.tasks.SvnCommit, dependsOn: createChangelog) { 29 | source << project.ext.changelog 30 | commitMessage = "Changelog added" 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/SvnToolsPlugin.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools 2 | 3 | import at.bxm.gradleplugins.svntools.tasks.* 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | 7 | class SvnToolsPlugin implements Plugin { 8 | 9 | @Override 10 | void apply(Project project) { 11 | project.extensions.create("svntools", SvnToolsPluginExtension, project) 12 | project.extensions.extraProperties.set("SvnAdd", SvnAdd.class) 13 | project.extensions.extraProperties.set("SvnApplyPatch", SvnApplyPatch.class) 14 | project.extensions.extraProperties.set("SvnBranch", SvnBranch.class) 15 | project.extensions.extraProperties.set("SvnCheckout", SvnCheckout.class) 16 | project.extensions.extraProperties.set("SvnCleanup", SvnCleanup.class) 17 | project.extensions.extraProperties.set("SvnCommit", SvnCommit.class) 18 | project.extensions.extraProperties.set("SvnCreatePatch", SvnCreatePatch.class) 19 | project.extensions.extraProperties.set("SvnDelete", SvnDelete.class) 20 | project.extensions.extraProperties.set("SvnExport", SvnExport.class) 21 | project.extensions.extraProperties.set("SvnInfo", SvnInfo.class) 22 | project.extensions.extraProperties.set("SvnRevert", SvnRevert.class) 23 | project.extensions.extraProperties.set("SvnTag", SvnTag.class) 24 | project.extensions.extraProperties.set("SvnUpdate", SvnUpdate.class) 25 | project.extensions.extraProperties.set("SvnVersion", SvnVersion.class) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnUpdateTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.Project 6 | 7 | class SvnUpdateTest extends SvnTestSupport { 8 | 9 | File workspace 10 | Project project 11 | SvnUpdate task 12 | 13 | def setup() { 14 | createLocalRepo() 15 | addFile("trunk/revision2.txt") 16 | workspace = checkoutTrunk() 17 | addFile("trunk/revision3.txt") 18 | project = projectWithPlugin() 19 | task = project.task(type: SvnUpdate, "task") as SvnUpdate 20 | task.workspaceDir = workspace 21 | // now, the server is at rev3 and the workspace is at rev2 22 | } 23 | 24 | def "happy path"() { 25 | when: "updating to HEAD" 26 | task.run() 27 | 28 | then: "update succeeded" 29 | getRevision(workspace) == 3 30 | } 31 | 32 | def "update to revision"() { 33 | when: "updating to revision" 34 | task.revision = 1 35 | task.run() 36 | 37 | then: "update succeeded" 38 | getRevision(workspace) == 1 39 | } 40 | 41 | def "update into a non-repo dir"() { 42 | given: "a non-repo dir" 43 | def workspaceDir = new File(tempDir, "myNewWorkspace") 44 | workspaceDir.mkdir() 45 | 46 | when: "updating" 47 | task.workspaceDir = workspaceDir 48 | task.run() 49 | 50 | then: 51 | def e = thrown InvalidUserDataException 52 | e.message =~ ".* E155007: None of the targets are working copies" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnUpdate.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.PathValidation 6 | import org.gradle.api.tasks.Internal 7 | import org.gradle.api.tasks.TaskAction 8 | import org.tmatesoft.svn.core.SVNDepth 9 | import org.tmatesoft.svn.core.SVNException 10 | 11 | import static at.bxm.gradleplugins.svntools.internal.SvnSupport.* 12 | 13 | class SvnUpdate extends SvnBaseTask { 14 | 15 | /** Local workspace that should be updated (default: {@code project.projectDir}) */ 16 | @Internal workspaceDir 17 | /** The target revision number (optional, defaults to HEAD) */ 18 | @Internal Long revision 19 | 20 | @TaskAction 21 | run() { 22 | def rev = revisionFrom(revision) 23 | def dir = workspaceDir ? project.file(workspaceDir, PathValidation.DIRECTORY) : project.projectDir 24 | try { 25 | def targetRev = createSvnClientManager().updateClient.doUpdate(dir, rev, SVNDepth.INFINITY, false, false) 26 | if (targetRev < 0) { 27 | // this has been working for svnkit-1.8.12 28 | // svnkit-1.8.15 throws an exception instead ("E155007: None of the targets are working copies") 29 | throw new InvalidUserDataException("workspaceDir $dir.absolutePath is no SVN workspace") 30 | } 31 | } catch (SVNException e) { 32 | throw new InvalidUserDataException("svn-update failed for $dir.absolutePath\n" + e.message, e) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnDelete.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import at.bxm.gradleplugins.svntools.internal.SvnSupport 5 | import org.gradle.api.InvalidUserDataException 6 | import org.gradle.api.tasks.Internal 7 | import org.gradle.api.tasks.TaskAction 8 | import org.tmatesoft.svn.core.SVNException 9 | 10 | /** Schedules files (within an SVN working copy) to be removed from version control (and deleted locally) */ 11 | class SvnDelete extends SvnBaseTask { 12 | 13 | @Internal final delete = [] 14 | /** Continue the build if the specified paths conflict with the WC status (can't delete) (default: {@code false}) */ 15 | @Internal boolean ignoreErrors 16 | 17 | /** To specify files to be scheduled for deletion */ 18 | void setDelete(target) { 19 | delete.clear() 20 | delete << target 21 | } 22 | 23 | void delete(Object... targets) { 24 | targets.each { 25 | delete << it 26 | } 27 | } 28 | 29 | @TaskAction 30 | def run() { 31 | def wcClient = SvnSupport.createSvnClientManager(username, password, proxy).WCClient 32 | project.files(delete).each { file -> 33 | try { 34 | wcClient.doDelete(file, ignoreErrors, false) 35 | } catch (SVNException e) { 36 | if (ignoreErrors) { 37 | logger.warn "Could not execute svn-delete on $file.absolutePath ($e.message)" 38 | } else { 39 | throw new InvalidUserDataException("Could not execute svn-delete on $file.absolutePath ($e.message)", e) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/patch/build.gradle: -------------------------------------------------------------------------------- 1 | import at.bxm.gradleplugins.svntools.tasks.SvnApplyPatch 2 | import at.bxm.gradleplugins.svntools.tasks.SvnCreatePatch 3 | import at.bxm.gradleplugins.svntools.tasks.SvnRevert 4 | import at.bxm.gradleplugins.svntools.tasks.SvnVersion 5 | 6 | buildscript { 7 | apply from: "../buildscript.gradle", to: buildscript 8 | } 9 | apply plugin: "at.bxm.svntools" 10 | 11 | task createPatch(type: SvnCreatePatch, description: "Make some modification to a workspace, and then create a patch file") { 12 | source "$project.projectDir/../workspace" 13 | patchFile = "$project.buildDir/sample.patch" 14 | doFirst { 15 | // perform some modifications 16 | def readmeFile = file("$project.projectDir/../workspace/README.md") 17 | def readme = readmeFile.text 18 | readmeFile.text = readme.replace("SVN", "Subversion") 19 | } 20 | } 21 | 22 | task applyPatch(type: SvnApplyPatch, description: "Checking out a project and applying a patch file") { 23 | patchFile = "$project.projectDir/sample.patch" 24 | dir = "$project.projectDir/../workspace" 25 | } 26 | 27 | // a helper task to clean up after 'createPatch' 28 | task revertAfterCreatePatch(type: SvnRevert) { 29 | revert "$project.projectDir/../workspace" 30 | recursive = true 31 | } 32 | createPatch.finalizedBy revertAfterCreatePatch 33 | 34 | // a demo task that tells us at the end of the build if the workspace is clean or dirty 35 | task printStatus(type: SvnVersion) { 36 | sourcePath = "$project.projectDir/../workspace" 37 | doLast { 38 | println "svnversion: $svnVersion - working copy is " + (svnVersion.modified ? "dirty" : "clean") 39 | } 40 | } 41 | [createPatch, applyPatch, revertAfterCreatePatch]*.finalizedBy printStatus 42 | 43 | -------------------------------------------------------------------------------- /docs/SvnBranch.md: -------------------------------------------------------------------------------- 1 | ## [SvnBranch](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnBranch.groovy) task 2 | 3 | Creates an SVN branch based on a local SVN workspace or a remote repository. 4 | This task requires the [standard SVN directory layout](http://svnbook.red-bean.com/en/1.7/svn.branchmerge.maint.html#svn.branchmerge.maint.layout) (`[module]/trunk`, `[module]/branches/[branch]`, `[module]/tags/[tag]`). 5 | 6 | ### Configuration 7 | 8 | Property | Description | Default value 9 | --------------- | ----------- | ------------- 10 | svnUrl | The repository URL that should be branched. This must point to either a trunk, a branch, or a tag.
Branches are always created from `HEAD`.
If this property is missing, the local workspace will be branched instead (see `workspaceDir` below). | 11 | workspaceDir | The local workspace that should be branched.
Will only be used if the `svnUrl` property is missing. | `$project.projectDir` 12 | branchName | The name of the new SVN branch (required) | 13 | replaceExisting | If the branch already exists, delete it first | `false` 14 | localChanges | If the workspace contains changes, commit those changes to the new branch | `false` 15 | specialChars | Set to `true` if the target name contains arbitrary chars (as supported by the current SVN server and operation system). If `false`, only a reduced subset of chars (a-z, A-Z, 0-9, "_", "-", ".", and "/") is allowed | `false` 16 | commitMessage | A commit message (optional) | 17 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 18 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 19 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnRevert.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.file.FileCollection 6 | import org.gradle.api.tasks.Internal 7 | import org.gradle.api.tasks.TaskAction 8 | import org.tmatesoft.svn.core.SVNException 9 | 10 | import static org.tmatesoft.svn.core.SVNDepth.* 11 | 12 | /** 13 | * Reverts all local changes of the given files or directories (which must be part of an SVN working copy). 14 | * Items that are not under version control will be ignored. 15 | * 16 | * Future improvements: 17 | * - optionally delete unversionized files 18 | */ 19 | class SvnRevert extends SvnBaseTask { 20 | 21 | /** Local workspace files or directories that should be reverted (default: {@code project.projectDir}) */ 22 | @Internal final revert = [] 23 | /** Also revert items in subdirectories (default: {@code false}) */ 24 | @Internal boolean recursive 25 | 26 | void setRevert(target) { 27 | revert.clear() 28 | revert << target 29 | } 30 | 31 | void revert(Object... targets) { 32 | targets.each { 33 | revert << it 34 | } 35 | } 36 | 37 | private FileCollection targetFiles() { 38 | return project.files(revert ? revert : project.projectDir); 39 | } 40 | 41 | @TaskAction 42 | def run() { 43 | def depth = recursive ? INFINITY : EMPTY 44 | def targets = targetFiles().files 45 | logger.debug("Reverting with depth {}: {}", depth, targets) 46 | try { 47 | createSvnClientManager().WCClient.doRevert(targets as File[], depth, null) 48 | } catch (SVNException e) { 49 | throw new InvalidUserDataException("svn-revert failed for $targets\n" + e.message, e) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnRevertTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | 5 | class SvnRevertTest extends SvnWorkspaceTestSupport { 6 | 7 | def "reverting a single file"() { 8 | given: 9 | def file = existingFile("test.txt") 10 | file.text = "changed" 11 | 12 | when: "running the SvnRevert task" 13 | def task = taskWithType(SvnRevert) 14 | task.revert file 15 | task.run() 16 | 17 | then: "file reverted" 18 | file.text == "" 19 | } 20 | 21 | def "reverting a directory non-recursively"() { 22 | given: 23 | def file = existingFile("test.txt") 24 | file.text = "changed" 25 | 26 | when: "running the SvnRevert task on the base directory only" 27 | def task = taskWithType(SvnRevert) 28 | task.revert = workspace 29 | task.run() 30 | 31 | then: "file not reverted" 32 | file.text == "changed" 33 | } 34 | 35 | def "reverting a directory recursively"() { 36 | given: 37 | def file = existingFile("test.txt") 38 | file.text = "changed" 39 | 40 | when: "running the SvnRevert task on the base directory recursively" 41 | def task = taskWithType(SvnRevert) 42 | task.revert << workspace 43 | task.recursive = true 44 | task.run() 45 | 46 | then: "file reverted" 47 | file.text == "" 48 | } 49 | 50 | def "reverting without a workspace"() { 51 | when: "running the SvnRevert task outside a working copy" 52 | def task = taskWithType(SvnRevert) 53 | task.revert = tempDir 54 | task.run() 55 | 56 | then: "exception" 57 | def exception = thrown InvalidUserDataException 58 | exception.message.readLines().size() == 2 59 | exception.message.readLines()[1] =~ "svn: E155007: .* is not a working copy" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnExportTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import org.gradle.api.Project 5 | import org.gradle.api.InvalidUserDataException 6 | 7 | class SvnExportTest extends SvnTestSupport { 8 | 9 | File workspace 10 | Project project 11 | SvnExport task 12 | 13 | def setup() { 14 | createLocalRepo() 15 | project = projectWithPlugin() 16 | task = project.task(type: SvnExport, "export") as SvnExport 17 | } 18 | 19 | def "happy path"() { 20 | given: "nonexistent targetDir" 21 | def targetDir = new File("$tempDir.absolutePath/myTargetDir") 22 | assert !targetDir.exists() 23 | 24 | when: "running the SvnExport task" 25 | task.svnUrl = localRepoUrl.appendPath("trunk", false) 26 | task.targetDir = targetDir 27 | task.run() 28 | 29 | then: "targetDir exists" 30 | targetDir.exists() 31 | targetDir.list().sort() as List == ["dir", "test.txt"] 32 | } 33 | 34 | def "invalid remote URL"() { 35 | when: "running the SvnExport task" 36 | task.svnUrl = "$localRepoUrl/blah" 37 | task.targetDir = new File(tempDir, "targetDir") 38 | task.run() 39 | 40 | then: 41 | def e = thrown InvalidUserDataException 42 | e.message =~ "svn-export failed for .*" 43 | } 44 | 45 | def "non-empty targetDir"() { 46 | given: "a non-empty targetDir" 47 | def targetDir = new File(tempDir, "myTargetDir") 48 | targetDir.mkdirs() 49 | new File(targetDir, "someFile.txt").text = "placeholder" 50 | 51 | when: "running the SvnExport task" 52 | task.svnUrl = "$localRepoUrl/trunk" 53 | task.targetDir = targetDir 54 | task.run() 55 | 56 | then: 57 | def e = thrown InvalidUserDataException 58 | e.message =~ ".* must be an empty directory" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCreatePatchTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | 5 | class SvnCreatePatchTest extends SvnWorkspaceTestSupport { 6 | 7 | def "single file"() { 8 | given: 9 | def file = existingFile("test.txt") 10 | file.text = "changed" 11 | 12 | when: "running the SvnCreatePatch task" 13 | def task = taskWithType(SvnCreatePatch) 14 | task.source(file) 15 | task.patchFile = "myPatch.txt" 16 | task.run() 17 | 18 | then: "patchfile contains one added line" 19 | def patchFile = project.file(task.patchFile) 20 | patchFile.exists() == true 21 | patchFile.text.readLines().size() == 7 22 | patchFile.text.readLines()[4] == "@@ -0,0 +1 @@" 23 | patchFile.text.readLines()[5] == "+changed" 24 | } 25 | 26 | def "file outside a workspace"() { 27 | given: 28 | def file = new File(tempDir, "file-outside-workspace.txt") 29 | file.text = "blah" 30 | 31 | when: "running the SvnCreatePatch task" 32 | def task = taskWithType(SvnCreatePatch) 33 | task.source = file 34 | task.patchFile = "myPatch.txt" 35 | task.run() 36 | 37 | then: "useful error message" 38 | def exception = thrown InvalidUserDataException 39 | exception.message.readLines().size() == 2 40 | exception.message.readLines()[1] == "Invalid source file or directory" 41 | } 42 | 43 | def "patchfile already exists"() { 44 | given: 45 | def file = existingFile("test.txt") 46 | file.text = "changed" 47 | def patchFile = newFile("myPatch.txt") 48 | 49 | when: "running the SvnCreatePatch task" 50 | def task = taskWithType(SvnCreatePatch) 51 | task.source(file) 52 | task.patchFile = patchFile 53 | task.run() 54 | 55 | then: "patchfile is overwritten" 56 | patchFile.text.readLines().size() == 7 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnWorkspaceTestSupport.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import at.bxm.gradleplugins.svntools.TestSvnStatusHandler 5 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 6 | import org.gradle.api.Project 7 | import org.tmatesoft.svn.core.SVNDepth 8 | import org.tmatesoft.svn.core.wc.SVNRevision 9 | 10 | abstract class SvnWorkspaceTestSupport extends SvnTestSupport { 11 | 12 | File workspace 13 | Project project 14 | 15 | def setup() { 16 | createLocalRepo() 17 | workspace = checkoutTrunk() 18 | project = projectWithPlugin() 19 | } 20 | 21 | protected T taskWithType(Class taskClass) { 22 | return project.task(type: taskClass, taskClass.simpleName.toLowerCase()) 23 | } 24 | 25 | File newFile(String name, File path = workspace) { 26 | def file = new File(path, name) 27 | assert !file.exists(), "$file.absolutePath already exist" 28 | file.parentFile.mkdirs() 29 | file.text = "new file" 30 | return file 31 | } 32 | 33 | File existingFile(String name) { 34 | def file = new File(workspace, name) 35 | assert file.exists(), "$file.absolutePath doesn't exist" 36 | assert file.file, "$file.absolutePath isn't a file" 37 | return file 38 | } 39 | 40 | File existingDir(String name) { 41 | def file = new File(workspace, name) 42 | assert file.exists(), "$file.absolutePath doesn't exist" 43 | assert file.directory, "$file.absolutePath isn't a directory" 44 | return file 45 | } 46 | 47 | TestSvnStatusHandler status(File rootPath = workspace) { 48 | def statusHandler = new TestSvnStatusHandler() 49 | clientManager.statusClient.doStatus(rootPath, SVNRevision.UNDEFINED, SVNDepth.INFINITY, false, true, false, false, statusHandler, null) 50 | return statusHandler 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnTagDirty.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import org.gradle.api.Project 5 | 6 | /** 7 | * Do all the tests for SvnTag plus some extra ... 8 | */ 9 | class SvnTagDirty extends SvnTestSupport { 10 | 11 | File workspace 12 | Project project 13 | SvnTag task 14 | 15 | def setup() { 16 | createLocalRepo() 17 | project = projectWithPlugin() 18 | task = project.task(type: SvnTag, "tagging") as SvnTag 19 | } 20 | 21 | def "tag on dirty workspace" () { 22 | given: "a dirty workspace" 23 | workspace = checkoutTrunk() 24 | def textFile = new File(workspace, "test.txt") 25 | textFile.write("Dirty !") 26 | 27 | when: "running the SvnTag task" 28 | task.tagName = "dirty-tag" 29 | task.localChanges = true 30 | task.workspaceDir = workspace 31 | task.run() 32 | 33 | then: "tag contains changed file" 34 | workspace.deleteDir() 35 | def tag = checkoutLocalRepo("tags/dirty-tag") 36 | def branchFile = new File(tag, "test.txt") 37 | branchFile.text == "Dirty !" 38 | tag.deleteDir() 39 | def cleanWorkspace = checkoutTrunk() 40 | def cleanFile = new File(cleanWorkspace, "test.txt") 41 | cleanFile.text == "" 42 | } 43 | 44 | def "clean tag on dirty workspace" () { 45 | given: "a dirty workspace" 46 | workspace = checkoutTrunk() 47 | def textFile = new File(workspace, "test.txt") 48 | textFile.write("Dirty !") 49 | 50 | when: "running the SvnTag task without local changes" 51 | task.tagName = "clean-tag" 52 | task.workspaceDir = workspace 53 | task.run() 54 | 55 | then: "tag does not contain changed file" 56 | workspace.deleteDir() 57 | def tag = checkoutLocalRepo("tags/clean-tag") 58 | def branchFile = new File(tag, "test.txt") 59 | branchFile.text == "" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnExport.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.tasks.Internal 6 | import org.gradle.api.tasks.TaskAction 7 | import org.tmatesoft.svn.core.SVNDepth 8 | import org.tmatesoft.svn.core.SVNException 9 | import org.tmatesoft.svn.core.SVNURL 10 | import org.tmatesoft.svn.core.wc.SVNRevision 11 | 12 | import static at.bxm.gradleplugins.svntools.internal.SvnSupport.* 13 | 14 | class SvnExport extends SvnBaseTask { 15 | 16 | /** The remote SVN URL to be exported */ 17 | @Internal String svnUrl 18 | /** The target directory for export (required). If it doesn't exist it will be created. If it exists it must be empty. */ 19 | @Internal targetDir 20 | /** The revision number to be exported (optional, defaults to HEAD) */ 21 | @Internal Long revision 22 | 23 | @TaskAction 24 | def run() { 25 | def rev = revisionFrom(revision) 26 | def repoUrl 27 | try { 28 | repoUrl = SVNURL.parseURIEncoded(svnUrl) 29 | } catch (SVNException e) { 30 | throw new InvalidUserDataException("Invalid svnUrl value: $svnUrl", e) 31 | } 32 | if (!targetDir) { 33 | throw new InvalidUserDataException("targetDir must be specified") 34 | } 35 | def dir = targetDir instanceof File ? targetDir : targetDir.toString() as File 36 | if (dir.exists()) { 37 | if (!dir.isDirectory()) { 38 | throw new InvalidUserDataException("targetDir $dir.absolutePath must be a directory") 39 | } 40 | if (dir.list()) { 41 | throw new InvalidUserDataException("targetDir $dir.absolutePath must be an empty directory") 42 | } 43 | } 44 | try { 45 | createSvnClientManager().updateClient.doExport(repoUrl, dir, SVNRevision.UNDEFINED, rev, null, true, SVNDepth.INFINITY) 46 | } catch (SVNException e) { 47 | throw new InvalidUserDataException("svn-export failed for $svnUrl\n" + e.message, e) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/SvnTag.md: -------------------------------------------------------------------------------- 1 | ## [SvnTag](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnTag.groovy) task 2 | 3 | Creates an SVN tag based on a local SVN workspace or a remote repository. 4 | This task requires the [standard SVN directory layout](http://svnbook.red-bean.com/en/1.7/svn.branchmerge.maint.html#svn.branchmerge.maint.layout) (`[module]/trunk`, `[module]/branches/[branch]`, `[module]/tags/[tag]`). 5 | 6 | ### Configuration 7 | 8 | Property | Description | Default value 9 | --------------- | ----------- | ------------- 10 | svnUrl | The repository URL that should be tagged. This must point to either a trunk, a branch, or a tag.
Tags are always created from `HEAD`.
If this property is missing, the local workspace will be tagged instead (see `workspaceDir` below). | 11 | workspaceDir | The local workspace that should be tagged.
Will only be used if the `svnUrl` property is missing. | `$project.projectDir` 12 | tagName | The name of the new SVN tag (required) | 13 | replaceExisting | If the tag already exists, delete it first | `false` 14 | localChanges | If the workspace contains changes, commit those changes to the new tag | `false` 15 | specialChars | Set to `true` if the target name contains arbitrary chars (as supported by the current SVN server and operation system). If `false`, only a reduced subset of chars (a-z, A-Z, 0-9, "_", "-", ".", and "/") is allowed | `false` 16 | commitMessage | A commit message (optional) | 17 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 18 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 19 | 20 | ### Example 21 | 22 | The `release` task creates an SVN tag using the current version number: 23 | 24 | apply plugin: "at.bxm.svntools" 25 | 26 | version = "1.0-SNAPSHOT" 27 | 28 | task svnTag(type: at.bxm.gradleplugins.svntools.tasks.SvnTag) { 29 | tagName = "v$project.version" 30 | commitMessage = "Release version $project.version" 31 | } 32 | 33 | task release(dependsOn: svnTag) 34 | -------------------------------------------------------------------------------- /docs/SvnInfo.md: -------------------------------------------------------------------------------- 1 | ## [SvnInfo](../src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnInfo.groovy) task 2 | 3 | Creates a [SvnData](../src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnData.groovy) object (see [General Configuration](GeneralConfig.md)) that contains information about a file or directory 4 | within an SVN workspace. 5 | The object is added as an "extra property" to the Gradle project and may be accessed with `$project.svnData`. 6 | This task requires the [standard SVN directory layout](http://svnbook.red-bean.com/en/1.7/svn.branchmerge.maint.html#svn.branchmerge.maint.layout) (`[module]/trunk`, `[module]/branches/[branch]`, `[module]/tags/[tag]`). 7 | 8 | ### Configuration 9 | 10 | Property | Description | Default value 11 | ------------------ | ----------- | ------------- 12 | sourcePath | Source path for reading the SVN metadata | `$project.projectDir` 13 | targetPropertyName | The name of the project extra property that will receive the resulting SvnData object | `svnData` 14 | ignoreErrors | Continue the build if the specified path doesn't contain SVN data | `false` 15 | username | The SVN username - leave empty if no authentication is required | `$project.svntools.username` 16 | password | The SVN password - leave empty if no authentication is required | `$project.svntools.password` 17 | 18 | ### Example 19 | 20 | This Gradle script creates a `svn.properties` file that contains the SVN URL and revision of the buildfile, and adds it to the JAR artifact: 21 | 22 | apply plugin: "java" 23 | apply plugin: "at.bxm.svntools" 24 | 25 | task svnStatus(type: at.bxm.gradleplugins.svntools.tasks.SvnInfo) { 26 | sourcePath = project.buildFile 27 | doLast { 28 | def props = new Properties() 29 | props.setProperty("url", project.svnData.url) 30 | props.setProperty("revision", project.svnData.revisionNumber as String) 31 | file("$project.buildDir/svn.properties").withWriter { props.store(it, null) } 32 | } 33 | } 34 | 35 | jar { 36 | dependsOn svnStatus 37 | from(project.buildDir, { include "svn.properties" }) 38 | } 39 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnAdd.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import at.bxm.gradleplugins.svntools.internal.SvnSupport 5 | import org.gradle.api.InvalidUserDataException 6 | import org.gradle.api.tasks.Internal 7 | import org.gradle.api.tasks.TaskAction 8 | import org.tmatesoft.svn.core.SVNDepth 9 | import org.tmatesoft.svn.core.SVNException 10 | 11 | /** Schedules files (within an SVN working copy) to be added to version control */ 12 | class SvnAdd extends SvnBaseTask { 13 | 14 | /** Local files or directories (within a workspace) that should be added */ 15 | @Internal final add = [] 16 | /** Also add items in subdirectories (default: {@code false}) */ 17 | @Internal boolean recursive 18 | /** Continue the build if the specified paths conflict with the WC status (can't add) (default: {@code false}) */ 19 | @Internal boolean ignoreErrors 20 | 21 | /** To specify files to be scheduled for addition */ 22 | void setAdd(target) { 23 | add.clear() 24 | add << target 25 | } 26 | 27 | void add(Object... targets) { 28 | targets.each { 29 | add << it 30 | } 31 | } 32 | 33 | @TaskAction 34 | def run() { 35 | SVNDepth svnDepth = recursive ? SVNDepth.INFINITY : SVNDepth.EMPTY 36 | File[] files = project.files(add).getFiles().toArray() 37 | if (!files) { 38 | if (ignoreErrors) { 39 | logger.warn("No files found for adding") 40 | } else { 41 | throw new InvalidUserDataException("No files to add specified") 42 | } 43 | } else { 44 | try { 45 | SvnSupport.createSvnClientManager(username, password, proxy).WCClient.doAdd( 46 | files, ignoreErrors, false, true, svnDepth, false, false, true) 47 | } catch (SVNException e) { 48 | if (ignoreErrors) { 49 | logger.warn "Could not execute svn-add on ${files*.absolutePath} ($e.message)" 50 | } else { 51 | throw new InvalidUserDataException("Could not execute svn-add on ${files*.absolutePath} ($e.message)", e) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/TestSvnStatusHandler.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools 2 | 3 | import groovy.util.logging.Log 4 | import org.tmatesoft.svn.core.SVNException 5 | import org.tmatesoft.svn.core.wc.ISVNStatusHandler 6 | import org.tmatesoft.svn.core.wc.SVNStatus 7 | 8 | import static org.tmatesoft.svn.core.SVNNodeKind.* 9 | import static org.tmatesoft.svn.core.wc.SVNStatusType.* 10 | 11 | @Log 12 | class TestSvnStatusHandler implements ISVNStatusHandler { 13 | 14 | final added = new TreeSet() 15 | final deleted = new TreeSet() 16 | final unversioned = new TreeSet() 17 | 18 | def getAdded() { 19 | return added as List 20 | } 21 | 22 | def getDeleted() { 23 | return deleted as List 24 | } 25 | def getUnversioned() { 26 | return unversioned as List 27 | } 28 | 29 | @Override 30 | void handleStatus(SVNStatus status) throws SVNException { 31 | switch (status.kind) { 32 | case DIR: 33 | switch (status.contentsStatus) { 34 | case STATUS_NORMAL: 35 | switch (status.nodeStatus) { 36 | case STATUS_ADDED: 37 | added << status.file 38 | return 39 | case STATUS_NORMAL: // unchanged 40 | return 41 | } 42 | } 43 | case FILE: 44 | switch (status.contentsStatus) { 45 | case STATUS_MODIFIED: 46 | switch (status.nodeStatus) { 47 | case STATUS_ADDED: 48 | added << status.file 49 | return 50 | } 51 | case STATUS_NORMAL: 52 | switch (status.nodeStatus) { 53 | case STATUS_DELETED: 54 | deleted << status.file 55 | return 56 | case STATUS_NORMAL: // unchanged 57 | return 58 | } 59 | } 60 | case UNKNOWN: 61 | switch (status.contentsStatus) { 62 | case STATUS_NONE: 63 | switch (status.nodeStatus) { 64 | case STATUS_UNVERSIONED: 65 | unversioned << status.file 66 | return 67 | } 68 | } 69 | } 70 | log.warning("unknown $status.file.absolutePath: kind=$status.kind, contents=$status.contentsStatus, node=$status.nodeStatus") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/internal/SvnPathTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.internal 2 | 3 | import org.tmatesoft.svn.core.SVNURL 4 | import spock.lang.Specification 5 | 6 | class SvnPathTest extends Specification { 7 | 8 | def "parse trunk"() { 9 | when: 10 | def path = SvnPath.parse(new SVNURL("http://localhost/repo/mymodule/trunk", false)) 11 | then: 12 | path.moduleBasePath == "/repo/mymodule" 13 | path.trunk == true 14 | path.path == "/" 15 | } 16 | 17 | def "parse trunk with file"() { 18 | when: 19 | def path = SvnPath.parse(new SVNURL("http://localhost/repo/mymodule/trunk/path/to/file.txt", false)) 20 | then: 21 | path.moduleBasePath == "/repo/mymodule" 22 | path.trunk == true 23 | path.path == "/path/to/file.txt" 24 | } 25 | 26 | def "parse branch"() { 27 | when: 28 | def path = SvnPath.parse(new SVNURL("http://localhost/repo/mymodule/branches/mybranch", false)) 29 | then: 30 | path.moduleBasePath == "/repo/mymodule" 31 | path.branch == true 32 | path.branchName == "mybranch" 33 | path.path == "/" 34 | } 35 | 36 | def "parse branch with file"() { 37 | when: 38 | def path = SvnPath.parse(new SVNURL("http://localhost/repo/mymodule/branches/mybranch/path/to/file.txt", false)) 39 | then: 40 | path.moduleBasePath == "/repo/mymodule" 41 | path.branch == true 42 | path.branchName == "mybranch" 43 | path.path == "/path/to/file.txt" 44 | } 45 | def "parse tag"() { 46 | when: 47 | def path = SvnPath.parse(new SVNURL("http://localhost/repo/mymodule/tags/mytag", false)) 48 | then: 49 | path.moduleBasePath == "/repo/mymodule" 50 | path.tag == true 51 | path.tagName == "mytag" 52 | path.path == "/" 53 | } 54 | 55 | def "parse tag with file"() { 56 | when: 57 | def path = SvnPath.parse(new SVNURL("http://localhost/repo/mymodule/tags/mytag/path/to/file.txt", false)) 58 | then: 59 | path.moduleBasePath == "/repo/mymodule" 60 | path.tag == true 61 | path.tagName == "mytag" 62 | path.path == "/path/to/file.txt" 63 | } 64 | 65 | def "invalid path"() { 66 | when: 67 | SvnPath.parse(new SVNURL("http://localhost/repo/mymodule/invalid/path", false)) 68 | then: 69 | thrown MalformedURLException 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/internal/SvnPath.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.internal 2 | 3 | import groovy.transform.ToString 4 | import org.tmatesoft.svn.core.SVNURL 5 | 6 | /** Split an SVN URL into its semantic parts (trunk, branch, or tag) */ 7 | @ToString(includePackage = false, includeNames = true, ignoreNulls = true) 8 | class SvnPath { 9 | 10 | final String moduleBasePath 11 | final String branchName 12 | final String tagName 13 | final String path 14 | 15 | private SvnPath(String moduleBasePath, String branchName, String tagName, String path) { 16 | this.moduleBasePath = moduleBasePath 17 | this.branchName = branchName 18 | this.tagName = tagName 19 | this.path = path 20 | } 21 | 22 | boolean isTrunk() { 23 | !branchName && !tagName 24 | } 25 | 26 | boolean isBranch() { 27 | branchName 28 | } 29 | 30 | boolean isTag() { 31 | tagName 32 | } 33 | 34 | boolean isRootPath() { 35 | path == "/" 36 | } 37 | 38 | static SvnPath parse(SVNURL url) throws MalformedURLException { 39 | if (url.path.endsWith("/trunk")) { 40 | return new SvnPath(url.path[0..-7], null, null, "/") 41 | } 42 | def matcher = url.path =~ '^(.+)/trunk(/.+)$' 43 | if (matcher) { 44 | return new SvnPath(matcher[0][1] as String, null, null, matcher[0][2] as String) 45 | } 46 | matcher = url.path =~ '^(.+)/branches/(.+)$' 47 | if (matcher) { 48 | def branchPath = matcher[0][2] as String 49 | def endOfBranchName = branchPath.indexOf "/" 50 | if (endOfBranchName > 0) { 51 | return new SvnPath(matcher[0][1] as String, branchPath[0..endOfBranchName - 1], null, branchPath[endOfBranchName..-1]) 52 | } else { 53 | return new SvnPath(matcher[0][1] as String, branchPath, null, "/") 54 | } 55 | } 56 | matcher = url.path =~ '^(.+)/tags/(.+)$' 57 | if (matcher) { 58 | def tagPath = matcher[0][2] as String 59 | def endOfTagName = tagPath.indexOf "/" 60 | if (endOfTagName > 0) { 61 | return new SvnPath(matcher[0][1] as String, null, tagPath[0..endOfTagName - 1], tagPath[endOfTagName..-1]) 62 | } else { 63 | return new SvnPath(matcher[0][1] as String, null, tagPath, "/") 64 | } 65 | } 66 | throw new MalformedURLException(url.path) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnTagRemoteTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import org.gradle.api.Project 5 | import org.gradle.api.InvalidUserDataException 6 | 7 | class SvnTagRemoteTest extends SvnTestSupport { 8 | 9 | Project project 10 | SvnTag task 11 | 12 | def setup() { 13 | createLocalRepo() 14 | project = projectWithPlugin() 15 | task = project.task(type: SvnTag, "tagging") as SvnTag 16 | } 17 | 18 | def "remote tag from trunk"() { 19 | when: "running the SvnTag task without workspace" 20 | task.svnUrl = localRepoUrl.appendPath("trunk", false) 21 | task.tagName = "my-tag" 22 | task.run() 23 | 24 | then: "tag exists" 25 | def workspace = checkoutLocalRepo("tags/my-tag") 26 | getRevision(workspace) == 2 27 | } 28 | 29 | def "remote tag without trunk-branches-tags structure"() { 30 | when: "running the SvnTag task from module root" 31 | task.svnUrl = localRepoUrl 32 | task.tagName = "my-tag" 33 | task.run() 34 | 35 | then: "exception" 36 | thrown MalformedURLException 37 | } 38 | 39 | def "both remote and local source is set"() { 40 | given: "a workspace" 41 | def workspace = checkoutTrunk() 42 | 43 | when: "running the SvnTag task with local _and_ remote source" 44 | task.workspaceDir = workspace 45 | task.svnUrl = localRepoUrl 46 | task.tagName = "my-tag" 47 | task.run() 48 | 49 | then: "exception" 50 | def e = thrown InvalidUserDataException 51 | e.message == "Either 'svnUrl' or 'workspaceDir' may be set" 52 | } 53 | 54 | def "remote tag with invalid url"() { 55 | when: "running the SvnTag task with an invalid url" 56 | task.svnUrl = "blah" 57 | task.tagName = "my-tag" 58 | task.run() 59 | 60 | then: "exception" 61 | def e = thrown InvalidUserDataException 62 | e.message == "Invalid svnUrl value: blah" 63 | } 64 | 65 | def "remote tag with non-existing url"() { 66 | when: "running the SvnTag task with a non-existing url" 67 | task.svnUrl = "http://localhost:7654/foo/bar/trunk/" 68 | task.tagName = "my-tag" 69 | task.run() 70 | 71 | then: "exception" 72 | def e = thrown InvalidUserDataException 73 | e.message =~ "^svn-copy failed for /foo/bar/tags/my-tag.*" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnVersionTest2.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import at.bxm.gradleplugins.svntools.api.SvnVersionData 5 | import org.tmatesoft.svn.core.SVNDepth 6 | 7 | class SvnVersionTest2 extends SvnTestSupport { 8 | 9 | def "sparse working copy"() { 10 | given: "a sparsely populated SVN working copy" 11 | createLocalRepo() 12 | def workspace = checkoutLocalRepo("/", SVNDepth.IMMEDIATES) 13 | 14 | when: "running the SvnVersion task" 15 | def project = projectWithPlugin() 16 | def task = project.task(type: SvnVersion, "version") as SvnVersion 17 | task.sourcePath = workspace 18 | task.run() 19 | 20 | then: "SVN version contains a sparse working copy" 21 | def version = project.ext.svnVersion as SvnVersionData 22 | version != null 23 | version as String == "1P" 24 | version.mixedRevision == false 25 | version.minRevisionNumber == 1 26 | version.maxRevisionNumber == 1 27 | version.modified == false 28 | version.switched == false 29 | version.sparse == true 30 | } 31 | 32 | def "no working copy"() { 33 | when: "running the SvnVersion task without working copy" 34 | def project = projectWithPlugin() 35 | def task = project.task(type: SvnVersion, "version") as SvnVersion 36 | task.sourcePath = tempDir 37 | task.ignoreErrors = true 38 | task.run() 39 | 40 | then: "SVN version contains a sparse working copy" 41 | def version = project.ext.svnVersion as SvnVersionData 42 | version != null 43 | version as String == "exported" 44 | } 45 | 46 | def "externals"() { // see https://github.com/martoe/gradle-svntools-plugin/issues/23 47 | given: "an SVN workspace with an external definition" 48 | createLocalRepoWithExternals() 49 | def workspace = checkoutLocalRepo("/") 50 | 51 | when: "running the SvnVersion task" 52 | def project = projectWithPlugin() 53 | def task = project.task(type: SvnVersion, "version") as SvnVersion 54 | task.sourcePath = workspace 55 | task.targetPropertyName = "myVersion" 56 | task.run() 57 | 58 | then: "SVN version contains no modification" 59 | def version = project.ext.myVersion as SvnVersionData 60 | version != null 61 | version as String == "1:2" // mixed because the external contains rev.1 62 | version.modified == false 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gradle-svntools-plugin 2 | 3 | A [Gradle](https://www.gradle.org) plugin (based on [SVNKit](http://svnkit.com/)) that provides various [Subversion](http://svnbook.red-bean.com/)-related tasks. 4 | 5 | The svntools-plugin can interact with existing SVN workspaces as well as create new workspaces (by performing a svn-checkout). It can interact with any SVN working copy format; no additional SVN client is required. 6 | 7 | Please report bugs and feature requests at the [Github issue page](https://github.com/martoe/gradle-svntools-plugin/issues). 8 | 9 | ## Version compatibility 10 | 11 | Plugin version | Gradle version | Java version 12 | -------------- | -------------- | ------------- 13 | [up to 1.7](../gradle2/README.md) | 2.0 - 2.14 | 6 and above 14 | 2.x | 3.0 - 5.x | 7 and above 15 | 3.0 and above | 6.0 and above | 11 and above 16 | 17 | 18 | ## Usage 19 | 20 | * [Applying the plugin](docs/ApplyPlugin.md) 21 | * [General Configuration](docs/GeneralConfig.md) 22 | * [Examples](examples/) 23 | 24 | ## Available tasks 25 | 26 | * [SvnAdd](docs/SvnAdd.md): schedules files within a working copy to be added to SVN 27 | * [SvnApplyPatch](docs/SvnApplyPatch.md): applies a patch file 28 | * [SvnBranch](docs/SvnBranch.md): creates an SVN branch 29 | * [SvnCheckout](docs/SvnCheckout.md): creates a local working copy of an SVN repository 30 | * [SvnCleanup](docs/SvnCleanup.md): cleans up a working copy 31 | * [SvnCommit](docs/SvnCommit.md): commits modifications of a local working copy 32 | * [SvnCreatePatch](docs/SvnCreatePatch.md): creates a patch file based on modifications of a local working copy 33 | * [SvnDelete](docs/SvnDelete.md): schedules files within a working copy to be deleted from SVN 34 | * [SvnExport](docs/SvnExport.md): exports parts of an SVN repository to a local directory 35 | * [SvnInfo](docs/SvnInfo.md): information about a local working copy file 36 | * [SvnRevert](docs/SvnRevert.md): reverts modifications of a local working copy 37 | * [SvnTag](docs/SvnTag.md): creates an SVN tag 38 | * [SvnUpdate](docs/SvnUpdate.md): updates a local working copy 39 | * [SvnVersion](docs/SvnVersion.md): summarize the local revision(s) of a working copy. 40 | 41 | [![Build Status](https://api.travis-ci.org/martoe/gradle-svntools-plugin.svg?branch=develop)](https://travis-ci.org/martoe/gradle-svntools-plugin) 42 | [![Coverage Status](https://coveralls.io/repos/github/martoe/gradle-svntools-plugin/badge.svg?branch=develop)](https://coveralls.io/github/martoe/gradle-svntools-plugin?branch=develop) 43 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnAddTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | 5 | class SvnAddTest extends SvnWorkspaceTestSupport { 6 | 7 | def "adding a file"() { 8 | given: "a new file to be added to SVN" 9 | def newFile = newFile("newfile.txt") 10 | 11 | when: "running the SvnAdd task" 12 | def task = taskWithType(SvnAdd) 13 | task.add(newFile) 14 | task.run() 15 | 16 | then: "one file added" 17 | status().added == [newFile] 18 | } 19 | 20 | def "adding a directory without contents"() { 21 | given: "a directory to be added to SVN" 22 | def newFile = newFile("newDir/newfile.txt") 23 | def newDir = newFile.parentFile 24 | 25 | when: "running the SvnAdd task" 26 | def task = taskWithType(SvnAdd) 27 | task.add = newDir 28 | task.run() 29 | 30 | then: "just the directory added" 31 | def status = status() 32 | status.added == [newDir] 33 | status.unversioned == [newFile] 34 | } 35 | 36 | def "adding a directory with contents"() { 37 | given: "a directory to be added to SVN" 38 | def file = newFile("newDir/newfile.txt") 39 | def dir = file.parentFile 40 | def subFile = newFile("newDir/newDir2/newfile2.txt") 41 | def subDir = subFile.parentFile 42 | 43 | when: "running the SvnAdd task" 44 | def task = taskWithType(SvnAdd) 45 | task.add = dir 46 | task.recursive = true 47 | task.run() 48 | 49 | then: "everything added" 50 | def status = status() 51 | status.added == [dir, subDir, subFile, file] 52 | status.unversioned == [] 53 | } 54 | 55 | def "no file to add - strict"() { 56 | when: "running the SvnAdd task without a file" 57 | def task = taskWithType(SvnAdd) 58 | task.run() 59 | 60 | then: "exception" 61 | def exception = thrown InvalidUserDataException 62 | exception.message.readLines()[0] == "No files to add specified" 63 | } 64 | 65 | def "no file to add - lenient"() { 66 | when: "running the SvnAdd task without a file" 67 | def task = taskWithType(SvnAdd) 68 | task.ignoreErrors = true 69 | task.run() 70 | 71 | then: "nothing has happened" 72 | status().added == [] 73 | } 74 | 75 | def "adding a nonexisting file"() { 76 | when: 77 | def task = taskWithType(SvnAdd) 78 | task.add(new File(workspace, "invalid.file")) 79 | task.run() 80 | 81 | then: "exception" 82 | def exception = thrown InvalidUserDataException 83 | exception.cause.message.readLines()[0] =~ ".*svn: E155010: .* not found.*" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/SvnToolsPluginExtension.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools 2 | 3 | import at.bxm.gradleplugins.svntools.api.SvnData 4 | import at.bxm.gradleplugins.svntools.api.SvnVersionData 5 | import at.bxm.gradleplugins.svntools.internal.SvnProxy 6 | import at.bxm.gradleplugins.svntools.internal.SvnSupport 7 | import org.gradle.api.InvalidUserDataException 8 | import org.gradle.api.Project 9 | import org.tmatesoft.svn.core.SVNException 10 | import org.tmatesoft.svn.core.SVNURL 11 | 12 | /** Holds configuration values shared by all SVN tasks and provides SVN information about the current workspace */ 13 | class SvnToolsPluginExtension { 14 | 15 | private final Project project 16 | String username 17 | private char[] password 18 | final SvnProxy proxy = new SvnProxy() 19 | SvnData info 20 | SvnVersionData version 21 | 22 | SvnToolsPluginExtension(Project project) { 23 | this.project = project 24 | } 25 | 26 | Object getPassword() { 27 | return password 28 | } 29 | 30 | void setPassword(Object password) { 31 | if (password != null) { 32 | this.password = password instanceof char[] ?: password.toString().chars 33 | } else { 34 | this.password = null 35 | } 36 | } 37 | 38 | /** @return svn-info for the project directory (cached) */ 39 | SvnData getInfo() { 40 | if (!info) { 41 | info = SvnSupport.createSvnData(project.projectDir, username, password, proxy, true) 42 | } 43 | return info 44 | } 45 | 46 | /** Convenience method for receiving SVN status data for an arbitrary path (https://github.com/martoe/gradle-svntools-plugin/issues/21) */ 47 | SvnData getInfo(File file) { 48 | return SvnSupport.createSvnData(file, username, password, proxy, false) 49 | } 50 | 51 | /** @return svn-version for the project directory (cached) */ 52 | SvnVersionData getVersion() { 53 | if (!version) { 54 | version = SvnSupport.createSvnVersionData(project.projectDir, username, password, proxy, true) 55 | } 56 | return version 57 | } 58 | 59 | /** 60 | * Retrieves SVN status data for a path within a remote repository 61 | * @param repoUrl An SVN repository URL 62 | * @param filePath An optional path within the repository 63 | */ 64 | SvnData getRemoteInfo(String repoUrl, String filePath = null) { 65 | try { 66 | SVNURL url = SVNURL.parseURIEncoded(repoUrl) 67 | return SvnSupport.createSvnData(url, filePath ?: "/", username, password, proxy, false) 68 | } catch (SVNException e) { 69 | throw new InvalidUserDataException("Invalid svnUrl value: $repoUrl", e) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnBranchTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | 5 | class SvnBranchTest extends SvnWorkspaceTestSupport { 6 | 7 | def "branch from trunk"() { 8 | given: "a trunk workspace" 9 | 10 | when: "running the task" 11 | def task = taskWithType(SvnBranch) 12 | task.workspaceDir = workspace 13 | task.branchName = "branch" 14 | task.run() 15 | 16 | then: "branch exists" 17 | switchLocalRepo("branches/branch") 18 | getRevision(workspace) == 2 19 | } 20 | 21 | def "branch from a branch"() { 22 | given: "a branch workspace" 23 | switchLocalRepo("branches/test-branch") 24 | 25 | when: "running the task" 26 | def task = taskWithType(SvnBranch) 27 | task.workspaceDir = workspace 28 | task.branchName = "v2.0" 29 | task.run() 30 | 31 | then: "branch exists" 32 | switchLocalRepo("branches/v2.0") 33 | getRevision(workspace) == 2 34 | } 35 | 36 | def "branch already exists"() { 37 | given: "a trunk workspace" 38 | 39 | when: "creating an existing branch" 40 | def task = taskWithType(SvnBranch) 41 | task.workspaceDir = workspace 42 | task.branchName = "test-branch" 43 | task.run() 44 | 45 | then: 46 | def e = thrown InvalidUserDataException 47 | e.message =~ /.* already exists/ 48 | } 49 | 50 | def "branch already exists - replace"() { 51 | given: "a trunk workspace" 52 | 53 | when: "creating an existing branch" 54 | def task = taskWithType(SvnBranch) 55 | task.workspaceDir = workspace 56 | task.branchName = "test-branch" 57 | task.replaceExisting = true 58 | task.run() 59 | 60 | then: "two more commits (delete and copy)" 61 | switchLocalRepo("branches/test-branch") 62 | getRevision(workspace) == 3 63 | } 64 | 65 | def "no branch name"() { 66 | when: "running the task without branch name" 67 | def task = taskWithType(SvnBranch) 68 | task.workspaceDir = workspace 69 | task.run() 70 | 71 | then: "exception" 72 | def exception = thrown InvalidUserDataException 73 | exception.message.readLines()[0] == "branchName missing" 74 | } 75 | 76 | def "invalid branch name"() { 77 | when: "running the task with an invalid branch name" 78 | def task = taskWithType(SvnBranch) 79 | task.workspaceDir = workspace 80 | task.branchName = ":invalid:" 81 | task.run() 82 | 83 | then: "exception" 84 | def exception = thrown InvalidUserDataException 85 | exception.message.readLines()[0] == "branchName contains invalid chars: :invalid:" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnDeleteTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | 5 | class SvnDeleteTest extends SvnWorkspaceTestSupport { 6 | 7 | def "deleting a file"() { 8 | given: 9 | def existingFile = existingFile("test.txt") 10 | 11 | when: "running the SvnDelete task" 12 | def task = taskWithType(SvnDelete) 13 | task.delete(existingFile) 14 | task.run() 15 | 16 | then: "file deleted" 17 | status().deleted == [existingFile] 18 | } 19 | 20 | def "deleting a non-empty directory"() { 21 | given: 22 | def dir = existingDir("dir") 23 | def file = new File(dir, "test.txt") 24 | 25 | when: "running the SvnDelete task" 26 | def task = taskWithType(SvnDelete) 27 | task.delete(dir) 28 | task.run() 29 | 30 | then: "dir deleted" 31 | status().deleted == [dir, file] 32 | } 33 | 34 | def "deleting a dirty directory"() { 35 | given: 36 | def dir = existingDir("dir") 37 | new File(dir, "unversioned.txt").text = "x" 38 | 39 | when: "running the SvnDelete task" 40 | def task = taskWithType(SvnDelete) 41 | task.delete(dir) 42 | task.run() 43 | 44 | then: "exception" 45 | def exception = thrown InvalidUserDataException 46 | exception.cause.message.readLines()[0] =~ "svn: E200005: .* is not under version control" 47 | } 48 | 49 | def "force deletion a dirty directory"() { 50 | given: 51 | def dir = existingDir("dir") 52 | def file = new File(dir, "test.txt") 53 | new File(dir, "unversioned.txt").text = "x" 54 | 55 | when: "running the SvnDelete task" 56 | def task = taskWithType(SvnDelete) 57 | task.ignoreErrors = true 58 | task.delete(dir) 59 | task.run() 60 | 61 | then: "dir deleted" 62 | status().deleted == [dir, file] 63 | } 64 | 65 | def "deleting a nonexisting file"() { 66 | given: 67 | def file = new File(workspace, "nonexisting.txt") 68 | 69 | when: "running the SvnDelete task" 70 | def task = taskWithType(SvnDelete) 71 | task.delete = file 72 | task.run() 73 | 74 | then: "exception" 75 | def exception = thrown InvalidUserDataException 76 | exception.cause.message.readLines()[0] =~ "svn: E125001: .* does not exist" 77 | } 78 | 79 | def "force deletion of a nonexisting file"() { 80 | given: 81 | def file = new File(workspace, "nonexisting.txt") 82 | 83 | when: "running the SvnDelete task" 84 | def task = taskWithType(SvnDelete) 85 | task.delete = file 86 | task.ignoreErrors = true 87 | task.run() 88 | 89 | then: "no error" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCreatePatch.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.file.FileCollection 6 | import org.gradle.api.tasks.Internal 7 | import org.gradle.api.tasks.TaskAction 8 | import org.tmatesoft.svn.core.SVNException 9 | 10 | import static org.tmatesoft.svn.core.SVNDepth.* 11 | import static org.tmatesoft.svn.core.wc.SVNRevision.* 12 | 13 | /** 14 | * Creates a patch file that contains all modifications of local workspace files and directories (including subdirectories). 15 | * 16 | * Corresponds to a `svn diff > filename.patch` 17 | */ 18 | class SvnCreatePatch extends SvnBaseTask { 19 | 20 | /** Local workspace files and directories with modifications that shall be saved to a patch file (default: {@code project.projectDir}) */ 21 | @Internal final source = [] 22 | /** The name of the target patch file (required). If it exists it will be overwritten */ 23 | @Internal patchFile 24 | 25 | void setSource(path) { 26 | source.clear() 27 | source << path 28 | } 29 | 30 | void source(Object... paths) { 31 | paths.each { 32 | source << it 33 | } 34 | } 35 | 36 | private FileCollection sourceFiles() { 37 | return project.files(source ? source : project.projectDir); 38 | } 39 | 40 | @TaskAction 41 | def run() { 42 | def sources = sourceFiles().files 43 | def targetFile = project.file(patchFile) 44 | if (targetFile.exists()) { 45 | if (targetFile.file) { 46 | if (!targetFile.delete()) { 47 | throw new InvalidUserDataException("Could not delete $targetFile.absolutePath") 48 | } 49 | } else { 50 | throw new InvalidUserDataException("$targetFile.absolutePath is a directory") 51 | } 52 | } 53 | logger.debug("Writing diffs of {} to {}", sources, targetFile.absolutePath) 54 | try { 55 | targetFile.withOutputStream { 56 | createSvnClientManager().diffClient.doDiff(sources as File[], BASE, WORKING, UNDEFINED, INFINITY, true, it, null) 57 | } 58 | } catch (SVNException e) { 59 | final msg 60 | if (e.errorMessage?.errorCode?.code == 200007) { 61 | // misleading error message "svn: E200007: Runner for 'org.tmatesoft.svn.core.wc2.SvnDiff' command have not been found; probably not yet implement in this API" 62 | msg = "Invalid source file or directory" 63 | } else { 64 | msg = e.message 65 | } 66 | throw new InvalidUserDataException("svn-patch failed for $targetFile.absolutePath\n$msg", e) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/internal/SimpleAuthenticationManager.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.internal 2 | 3 | import groovy.util.logging.Log 4 | import org.tmatesoft.svn.core.SVNException 5 | import org.tmatesoft.svn.core.SVNURL 6 | import org.tmatesoft.svn.core.auth.BasicAuthenticationManager 7 | import org.tmatesoft.svn.core.auth.ISVNProxyManager 8 | import org.tmatesoft.svn.core.auth.SVNAuthentication 9 | import org.tmatesoft.svn.core.auth.SVNPasswordAuthentication 10 | import org.tmatesoft.svn.core.auth.SVNSSHAuthentication 11 | import org.tmatesoft.svn.core.auth.SVNSSLAuthentication 12 | import org.tmatesoft.svn.core.auth.SVNUserNameAuthentication 13 | import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions 14 | 15 | /** 16 | * Adds "nonProxyHosts" support to the BasicAuthenticationManager 17 | */ 18 | @Log 19 | class SimpleAuthenticationManager extends BasicAuthenticationManager { 20 | 21 | private String nonProxyHosts 22 | 23 | SimpleAuthenticationManager(String username, char[] password) { 24 | super([ 25 | // the first three arguments are copied from BasicAuthenticationManager.newInstance(String, char[]) 26 | SVNPasswordAuthentication.newInstance(username, password, false, null, false), 27 | SVNSSHAuthentication.newInstance(username, password, -1, false, null, false), 28 | SVNUserNameAuthentication.newInstance(username, false, null, false), 29 | // the forth argument is needed when accessing a repo via https 30 | SVNSSLAuthentication.newInstance(null as File, "dummy".toCharArray(), false, null, false) 31 | ] as SVNAuthentication[]) 32 | } 33 | 34 | void setProxy(String proxyHost, int proxyPort, String proxyUserName, char[] proxyPassword, String nonProxyHosts) { 35 | super.setProxy(proxyHost, proxyPort, proxyUserName, (char[])proxyPassword) 36 | this.nonProxyHosts = nonProxyHosts 37 | } 38 | 39 | @Override 40 | ISVNProxyManager getProxyManager(SVNURL url) throws SVNException { 41 | if (!proxyHost) { 42 | log.fine("No proxy configured for $url") 43 | return null 44 | } else if (hostExceptedFromProxy(url.host)) { 45 | log.fine("Bypassing proxy for $url") 46 | return null 47 | } else { 48 | log.fine("Using proxy for $url") 49 | return this 50 | } 51 | } 52 | 53 | /** @see org.tmatesoft.svn.core.internal.wc.DefaultSVNHostOptions */ 54 | private boolean hostExceptedFromProxy(String host) { 55 | if (nonProxyHosts) { 56 | for (def exceptions = new StringTokenizer(nonProxyHosts, "|"); exceptions.hasMoreTokens();) { 57 | def exception = exceptions.nextToken().trim() 58 | if (DefaultSVNOptions.matches(exception, host)) { 59 | return true 60 | } 61 | } 62 | } 63 | return false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/patch/sample.patch: -------------------------------------------------------------------------------- 1 | Index: README.md 2 | =================================================================== 3 | --- README.md (revision 225) 4 | +++ README.md (working copy) 5 | @@ -1,21 +1,21 @@ 6 | # gradle-svntools-plugin 7 | 8 | -A [Gradle](https://www.gradle.org) plugin (based on [SVNKit](http://svnkit.com/)) that provides various [Subversion](http://svnbook.red-bean.com/)-related tasks. 9 | +A [Gradle](https://www.gradle.org) plugin (based on [SubersionKit](http://svnkit.com/)) that provides various [Subversion](http://svnbook.red-bean.com/)-related tasks. 10 | 11 | -Here is a very short build script that prints out the SVN revision: 12 | +Here is a very short build script that prints out the Subersion revision: 13 | 14 | apply plugin: "at.bxm.svntools" 15 | task info << { 16 | println "Current revision is $svntools.info.revisionNumber" 17 | } 18 | 19 | -The svntools-plugin can interact with existing SVN workspaces as well as create new workspaces (by performing a svn-checkout). It can interact with any SVN working copy format; no additional SVN client is required. 20 | +The svntools-plugin can interact with existing Subersion workspaces as well as create new workspaces (by performing a svn-checkout). It can interact with any Subersion working copy format; no additional Subersion client is required. 21 | 22 | Please report bugs and feature requests at the [Github issue page](https://github.com/martoe/gradle-svntools-plugin/issues). 23 | 24 | ## Use-case examples 25 | 26 | -* Add the SVN revision to the MANIFEST file when publishing artifacts 27 | +* Add the Subersion revision to the MANIFEST file when publishing artifacts 28 | * Create a tag as part of an automated release process 29 | * Commit files that have been changed during the build process (e.g. bumped version numbers) 30 | 31 | @@ -29,14 +29,14 @@ 32 | 33 | * [SvnInfo](docs/SvnInfo.md): information about a workspace file 34 | * [SvnVersion](docs/SvnVersion.md): summarize the local revision(s) of a working copy. 35 | -* [SvnCheckout](docs/SvnCheckout.md): creates a local workspace of an SVN repository 36 | -* [SvnUpdate](docs/SvnUpdate.md): updates an SVN workspace 37 | +* [SvnCheckout](docs/SvnCheckout.md): creates a local workspace of an Subersion repository 38 | +* [SvnUpdate](docs/SvnUpdate.md): updates an Subersion workspace 39 | * [SvnCommit](docs/SvnCommit.md): commits workspace modifications 40 | * [SvnRevert](docs/SvnRevert.md): reverts workspace modifications 41 | * [SvnCreatePatch](docs/SvnCreatePatch.md): creates a patch file based on workspace modifications 42 | * [SvnApplyPatch](docs/SvnApplyPatch.md): applies a patch file 43 | -* [SvnBranch](docs/SvnBranch.md): creates an SVN branch 44 | -* [SvnTag](docs/SvnTag.md): creates an SVN tag 45 | -* [SvnExport](docs/SvnExport.md): exports parts of an SVN repository to a local directory 46 | +* [SvnBranch](docs/SvnBranch.md): creates an Subersion branch 47 | +* [SvnTag](docs/SvnTag.md): creates an Subersion tag 48 | +* [SvnExport](docs/SvnExport.md): exports parts of an Subersion repository to a local directory 49 | 50 | [![Build Status](https://travis-ci.org/martoe/gradle-svntools-plugin.png)](https://travis-ci.org/martoe/gradle-svntools-plugin) 51 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCommit.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.tasks.Internal 6 | import org.gradle.api.tasks.TaskAction 7 | import org.tmatesoft.svn.core.SVNDepth 8 | import org.tmatesoft.svn.core.SVNException 9 | import org.tmatesoft.svn.core.wc.SVNInfo 10 | import org.tmatesoft.svn.core.wc.SVNRevision 11 | 12 | /** Commits a list of files (and directories) within the current SVN workspace. */ 13 | class SvnCommit extends SvnBaseTask { 14 | 15 | /** 16 | * A list of files and directories that should be committed. 17 | * If these are not under version control already, they will be added first. 18 | * If this list is empty of the files contain no modifications, no commit will be executed. 19 | */ 20 | @Internal source = [] 21 | /** Also commit items in subdirectories (default: {@code false}) */ 22 | @Internal boolean recursive 23 | /** An optional commit message. */ 24 | @Internal String commitMessage 25 | 26 | @TaskAction 27 | run() { 28 | if (!source) { 29 | logger.warn "No files to commit" 30 | return 31 | } 32 | def addedFiles = [] 33 | def clientManager = createSvnClientManager() 34 | SVNDepth svnDepth = recursive ? SVNDepth.INFINITY : SVNDepth.EMPTY 35 | source.each { 36 | def file = project.file(it) 37 | SVNInfo status 38 | try { 39 | status = clientManager.WCClient.doInfo(file, SVNRevision.WORKING) 40 | } catch (SVNException e) { 41 | status = null // not under version control yet 42 | } 43 | if (status?.schedule == "delete") { 44 | logger.debug("File {} is already scheduled for delete", file.absolutePath) 45 | } else if (!file.exists()) { 46 | throw new InvalidUserDataException("File " + file.absolutePath + " doesn't exist") 47 | } else if (status?.schedule == "add") { 48 | logger.debug("File {} is already scheduled for add", file.absolutePath) 49 | } else { // status?.schedule == null 50 | logger.debug("Adding {} to SVN", file.absolutePath) 51 | try { 52 | clientManager.WCClient.doAdd(file, true, false, false, svnDepth, false, false) 53 | } catch (SVNException e) { 54 | throw new InvalidUserDataException("svn-add failed for $file.absolutePath\n" + e.message, e) 55 | } 56 | } 57 | addedFiles << file 58 | } 59 | logger.info "Committing $addedFiles" 60 | try { 61 | def committed = clientManager.commitClient.doCommit(addedFiles as File[], false, commitMessage, null, null, false, true, svnDepth) 62 | if (committed.errorMessage) { 63 | if (committed.errorMessage.warning) { 64 | logger.warn "Commit completed with warning: $committed" 65 | } else { 66 | throw new InvalidUserDataException("svn-commit failed: $committed") 67 | } 68 | } else { 69 | logger.info "svn-commit successful: $committed" 70 | } 71 | } catch (SVNException e) { 72 | throw new InvalidUserDataException("svn-commit failed for $addedFiles\n" + e.message, e) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCheckout.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.api.SvnDepth 4 | import at.bxm.gradleplugins.svntools.internal.SvnBaseTask 5 | import org.gradle.api.InvalidUserDataException 6 | import org.gradle.api.tasks.Internal 7 | import org.gradle.api.tasks.TaskAction 8 | import org.tmatesoft.svn.core.SVNDepth 9 | import org.tmatesoft.svn.core.SVNException 10 | import org.tmatesoft.svn.core.SVNURL 11 | import org.tmatesoft.svn.core.wc.SVNRevision 12 | 13 | import static at.bxm.gradleplugins.svntools.internal.SvnSupport.* 14 | 15 | class SvnCheckout extends SvnBaseTask { 16 | 17 | /** The remote SVN URL to be checked out */ 18 | @Internal String svnUrl 19 | /** The target directory for checkout (required). If it doesn't exist it will be created. If it exists it must be empty. */ 20 | @Internal workspaceDir 21 | /** The revision number to be checked out (optional, defaults to HEAD) */ 22 | @Internal Long revision 23 | /** 24 | * The checkout depth (optional, defaults to INFINITY) 25 | * @see SvnDepth 26 | */ 27 | @Internal depth 28 | /** If {@code true}, an "svn update" is performed if the {@link #workspaceDir} already contains checked-out data. */ 29 | @Internal update 30 | 31 | @TaskAction 32 | def run() { 33 | def rev = revisionFrom(revision) 34 | def repoUrl 35 | try { 36 | repoUrl = SVNURL.parseURIEncoded(svnUrl) 37 | } catch (SVNException e) { 38 | throw new InvalidUserDataException("Invalid svnUrl value: $svnUrl", e) 39 | } 40 | if (!workspaceDir) { 41 | throw new InvalidUserDataException("workspaceDir must be specified") 42 | } 43 | def dir = workspaceDir instanceof File ? workspaceDir : workspaceDir.toString() as File 44 | def performUpdate = false 45 | if (dir.exists()) { 46 | if (!dir.isDirectory()) { 47 | throw new InvalidUserDataException("workspaceDir $dir.absolutePath must be a directory") 48 | } 49 | if (dir.list()) { 50 | if (!update) { 51 | throw new InvalidUserDataException("workspaceDir $dir.absolutePath must be an empty directory") 52 | } 53 | def result = createSvnData(dir, getUsername(), getPassword(), proxy, true) 54 | if (!result.url) { 55 | throw new InvalidUserDataException("workspaceDir $dir.absolutePath must be either an empty directory or an SVN workspace") 56 | } 57 | if (result.url != repoUrl.toString()) { 58 | throw new InvalidUserDataException("SVN location of $dir.absolutePath is invalid: $result.url") 59 | } 60 | performUpdate = true 61 | } 62 | } 63 | try { 64 | if (performUpdate) { 65 | createSvnClientManager().updateClient.doUpdate(dir, rev, parseDepth(depth), false, false) 66 | } else { 67 | createSvnClientManager().updateClient.doCheckout(repoUrl, dir, SVNRevision.UNDEFINED, rev, parseDepth(depth), false) 68 | } 69 | } catch (SVNException e) { 70 | throw new InvalidUserDataException((performUpdate ? "svn-update" : "svn-checkout") + " failed for $svnUrl\n" + e.message, e) 71 | } 72 | } 73 | 74 | private static SVNDepth parseDepth(depth) { 75 | if (depth) { 76 | switch (depth.toString().toLowerCase()) { 77 | case "empty": return SVNDepth.EMPTY 78 | case "files": return SVNDepth.FILES 79 | case "immediates": return SVNDepth.IMMEDIATES 80 | case "infinity": return SVNDepth.INFINITY 81 | default: 82 | throw new InvalidUserDataException("Invalid depth value: $depth") 83 | } 84 | } 85 | return SVNDepth.INFINITY 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnTagTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | import org.tmatesoft.svn.core.io.SVNRepositoryFactory 5 | 6 | class SvnTagTest extends SvnWorkspaceTestSupport { 7 | 8 | def "no tagName"() { 9 | given: "a workspace" 10 | 11 | when: "running the SvnTag task" 12 | def task = taskWithType(SvnTag) 13 | task.workspaceDir = workspace 14 | task.run() 15 | 16 | then: "exception" 17 | def e = thrown InvalidUserDataException 18 | e.message == "tagName missing" 19 | } 20 | 21 | def "tag from trunk"() { 22 | given: "a trunk workspace" 23 | 24 | when: "running the SvnTag task" 25 | def task = taskWithType(SvnTag) 26 | task.workspaceDir = workspace 27 | task.tagName = "my-tag" 28 | task.run() 29 | 30 | then: "tag exists" 31 | switchLocalRepo("tags/my-tag") 32 | getRevision(workspace) == 2 33 | } 34 | 35 | def "tag from a branch"() { 36 | given: "a branch workspace" 37 | switchLocalRepo("branches/test-branch") 38 | 39 | when: "running the SvnTag task" 40 | def task = taskWithType(SvnTag) 41 | task.workspaceDir = workspace 42 | task.tagName = "my.tag" 43 | task.run() 44 | 45 | then: "tag exists" 46 | switchLocalRepo("tags/my.tag") 47 | getRevision(workspace) == 2 48 | } 49 | 50 | def "tag from a tag"() { 51 | given: "a tag workspace" 52 | switchLocalRepo("tags/test-tag") 53 | 54 | when: "running the SvnTag task" 55 | def task = taskWithType(SvnTag) 56 | task.workspaceDir = workspace 57 | task.tagName = "myTag" 58 | task.run() 59 | 60 | then: "tag exists" 61 | switchLocalRepo("tags/myTag") 62 | getRevision(workspace) == 2 63 | } 64 | 65 | def "invalid tagName"() { 66 | given: "a workspace" 67 | 68 | when: "running the SvnTag task" 69 | def task = taskWithType(SvnTag) 70 | task.workspaceDir = workspace 71 | task.tagName = param 72 | task.run() 73 | 74 | then: "exception" 75 | def e = thrown InvalidUserDataException 76 | e.message == "tagName contains invalid chars: $param" as String 77 | 78 | where: 79 | param << ["blank ", "backslash\\"] 80 | } 81 | 82 | def "tagName with special chars"() { 83 | given: "a workspace" 84 | 85 | when: "running the SvnTag task" 86 | def task = taskWithType(SvnTag) 87 | task.workspaceDir = workspace 88 | task.tagName = param 89 | task.specialChars = true 90 | task.run() 91 | 92 | then: "tag exists" 93 | switchLocalRepo("tags/$param") 94 | getRevision(workspace) == 2 95 | 96 | where: 97 | param << ["blank ", "backslash\\"] 98 | } 99 | 100 | def "tag into subdirectory"() { 101 | given: "a repo with a 'tags' subdirectory" 102 | def repo = SVNRepositoryFactory.create(localRepoUrl) 103 | def editor = repo.getCommitEditor("creating a new file", null) 104 | editor.openRoot(-1) 105 | editor.addDir("tags/tag-subdir", null, -1) 106 | editor.closeDir() 107 | editor.closeEdit() 108 | updateLocalRepo() 109 | 110 | when: "running the SvnTag task on that subdirectory" 111 | def task = taskWithType(SvnTag) 112 | task.workspaceDir = workspace 113 | task.tagName = "tag-subdir/new-tag" 114 | task.run() 115 | 116 | then: "tag exists" 117 | switchLocalRepo("tags/tag-subdir/new-tag") 118 | getRevision(workspace) == 3 119 | } 120 | 121 | def "tag into non-existing subdirectory"() { 122 | given: "a trunk workspace" 123 | 124 | when: "running the SvnTag task no a non-existing subdirectory" 125 | def task = taskWithType(SvnTag) 126 | task.workspaceDir = workspace 127 | task.tagName = "tag-subdir/new-tag" 128 | task.run() 129 | 130 | then: "tag exists" 131 | switchLocalRepo("tags/tag-subdir/new-tag") 132 | getRevision(workspace) == 2 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/SvnToolsPluginExtensionTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | 5 | class SvnToolsPluginExtensionTest extends SvnTestSupport { 6 | 7 | def "access svnData at trunk"() { 8 | given: "an SVN workspace at trunk" 9 | createLocalRepo() 10 | def workspace = checkoutTrunk() 11 | 12 | expect: "valid SvnInfo object" 13 | def svnData = projectWithPlugin(workspace).extensions.getByType(SvnToolsPluginExtension).info 14 | svnData != null 15 | svnData.trunk 16 | svnData.name == "trunk" 17 | !svnData.branch 18 | !svnData.tag 19 | svnData.revisionNumber == 1 20 | svnData.committedDate != null 21 | svnData.committedAuthor == "username" 22 | } 23 | 24 | def "access svnData outside a workspace"() { 25 | given: "no SVN workspace" 26 | 27 | expect: "invalid SvnInfo object" 28 | def svnData = projectWithPlugin().extensions.getByType(SvnToolsPluginExtension).info 29 | svnData != null 30 | !svnData.trunk 31 | !svnData.branch 32 | !svnData.tag 33 | svnData.revisionNumber == -1 34 | } 35 | 36 | def "access svnData for an arbitrary file"() { 37 | given: "an SVN workspace at trunk" 38 | createLocalRepo() 39 | def workspace = checkoutTrunk() 40 | 41 | when: "reading SvnData for a file" 42 | def svnData = projectWithPlugin(workspace).extensions.getByType(SvnToolsPluginExtension).getInfo(new File(workspace, "test.txt")) 43 | 44 | then: "data returned" 45 | svnData != null 46 | svnData.trunk 47 | svnData.name == "trunk" 48 | !svnData.branch 49 | !svnData.tag 50 | svnData.revisionNumber == 1 51 | } 52 | 53 | def "access svnData for a non-existing file"() { 54 | given: "an SVN workspace at trunk" 55 | createLocalRepo() 56 | def workspace = checkoutTrunk() 57 | 58 | when: "reading SvnData for a non-existing file" 59 | projectWithPlugin(workspace).extensions.getByType(SvnToolsPluginExtension).getInfo(new File(workspace, "missing.txt")) 60 | 61 | then: "error" 62 | thrown(InvalidUserDataException) 63 | } 64 | 65 | def "access svnData for a remote repository"() { 66 | given: "an SVN repo" 67 | def url = createLocalRepo() as String 68 | 69 | when: "reading SvnData for the repository" 70 | def svnData = projectWithPlugin().extensions.getByType(SvnToolsPluginExtension).getRemoteInfo(url) 71 | 72 | then: "data returned" 73 | svnData != null 74 | svnData.name == null 75 | !svnData.trunk 76 | !svnData.branch 77 | !svnData.tag 78 | svnData.revisionNumber == 1 79 | } 80 | 81 | def "access svnData for a remote file"() { 82 | given: "an SVN repo" 83 | def url = createLocalRepo() as String 84 | 85 | when: "reading SvnData for a remote file" 86 | def svnData = projectWithPlugin().extensions.getByType(SvnToolsPluginExtension).getRemoteInfo(url, "trunk/test.txt") 87 | 88 | then: "data returned" 89 | svnData != null 90 | svnData.trunk 91 | svnData.name == "trunk" 92 | !svnData.branch 93 | !svnData.tag 94 | svnData.revisionNumber == 1 95 | } 96 | 97 | def "access svnData for a non-existing remote file"() { 98 | given: "an SVN repo" 99 | def url = createLocalRepo() as String 100 | 101 | when: "reading SvnData for a non-existing remote file" 102 | def svnData = projectWithPlugin().extensions.getByType(SvnToolsPluginExtension).getRemoteInfo(url, "trunk/invalid.txt") 103 | 104 | then: "error" 105 | thrown(InvalidUserDataException) 106 | } 107 | 108 | def "access svnVersion at trunk"() { 109 | given: "an SVN workspace at trunk" 110 | createLocalRepo() 111 | def workspace = checkoutTrunk() 112 | 113 | expect: "valid SvnVersion object" 114 | def version = projectWithPlugin(workspace).extensions.getByType(SvnToolsPluginExtension).version 115 | version != null 116 | version as String == "1" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnVersionTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.api.SvnVersionData 4 | 5 | import static org.tmatesoft.svn.core.SVNDepth.* 6 | 7 | class SvnVersionTest extends SvnWorkspaceTestSupport { 8 | 9 | def "single revision"() { 10 | given: "an SVN workspace at a single revision" 11 | 12 | when: "running the SvnVersion task" 13 | def task = taskWithType(SvnVersion) 14 | task.sourcePath = workspace 15 | task.targetPropertyName = "myVersion" 16 | task.run() 17 | 18 | then: "SVN version contains the single revision" 19 | def version = project.ext.myVersion as SvnVersionData 20 | version != null 21 | version as String == "1" 22 | version.mixedRevision == false 23 | version.minRevisionNumber == 1 24 | version.maxRevisionNumber == 1 25 | version.modified == false 26 | version.sparse == false 27 | } 28 | 29 | def "mixed revision"() { 30 | given: "an SVN workspace with mixed revision" 31 | switchLocalRepo("/") 32 | addFile("trunk/newfile.txt") 33 | addFile("trunk/newfile2.txt") 34 | update("trunk") 35 | 36 | when: "running the SvnVersion task" 37 | def task = taskWithType(SvnVersion) 38 | task.sourcePath = workspace 39 | task.run() 40 | 41 | then: "SVN version contains mixed revision" 42 | def version = project.ext.svnVersion as SvnVersionData 43 | version != null 44 | version as String == "1:3" 45 | version.mixedRevision == true 46 | version.minRevisionNumber == 1 47 | version.maxRevisionNumber == 3 48 | version.modified == false 49 | } 50 | 51 | def "workspace modification"() { 52 | given: "an SVN workspace at a single revision" 53 | switchLocalRepo("/") 54 | new File(workspace, "trunk/test.txt").text = "modified content" 55 | 56 | when: "running the SvnVersion task" 57 | def task = taskWithType(SvnVersion) 58 | task.sourcePath = workspace 59 | task.targetPropertyName = "myVersion" 60 | task.run() 61 | 62 | then: "SVN version contains the single revision" 63 | def version = project.ext.myVersion as SvnVersionData 64 | version != null 65 | version as String == "1M" 66 | version.mixedRevision == false 67 | version.minRevisionNumber == 1 68 | version.maxRevisionNumber == 1 69 | version.modified == true 70 | } 71 | 72 | def "switched workspace"() { 73 | given: "a switched SVN workspace" 74 | switchLocalRepo("/") 75 | switchLocalRepo("trunk", "branches") 76 | 77 | when: "running the SvnVersion task" 78 | def task = taskWithType(SvnVersion) 79 | task.sourcePath = workspace 80 | task.run() 81 | 82 | then: "SVN version contains a switched workspace" 83 | def version = project.ext.svnVersion as SvnVersionData 84 | version != null 85 | version as String == "1S" 86 | version.mixedRevision == false 87 | version.minRevisionNumber == 1 88 | version.maxRevisionNumber == 1 89 | version.modified == false 90 | version.switched == true 91 | } 92 | 93 | def "unversioned file"() { 94 | given: "an SVN workspace with an unversioned file" 95 | newFile("newfile.txt") 96 | 97 | when: "running the SvnVersion task" 98 | def task = taskWithType(SvnVersion) 99 | task.sourcePath = workspace 100 | task.run() 101 | 102 | then: "no modification" 103 | project.ext.svnVersion as String == "1" 104 | } 105 | 106 | def "added dir"() { 107 | given: "an SVN workspace with an added dir" 108 | clientManager.WCClient.doAdd(newFile("newdir/newfile.txt").parentFile, false, false, false, EMPTY, false, false) 109 | 110 | when: "running the SvnVersion task" 111 | def task = taskWithType(SvnVersion) 112 | task.sourcePath = workspace 113 | task.run() 114 | 115 | then: "modification" 116 | project.ext.svnVersion as String == "1M" 117 | } 118 | 119 | def "locally deleted file"() { 120 | given: "an SVN workspace with a deleted file" 121 | existingFile("test.txt").delete() 122 | 123 | when: "running the SvnVersion task" 124 | def task = taskWithType(SvnVersion) 125 | task.sourcePath = workspace 126 | task.run() 127 | 128 | then: "modification" 129 | project.ext.svnVersion as String == "1M" 130 | } 131 | 132 | def "file marked for deletion"() { 133 | given: "an SVN workspace with a deleted file" 134 | clientManager.WCClient.doDelete(existingFile("test.txt"), false, false, false) 135 | 136 | when: "running the SvnVersion task" 137 | def task = taskWithType(SvnVersion) 138 | task.sourcePath = workspace 139 | task.run() 140 | 141 | then: "modification" 142 | project.ext.svnVersion as String == "1M" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/internal/SvnCopy.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.internal 2 | 3 | import org.gradle.api.InvalidUserDataException 4 | import org.gradle.api.PathValidation 5 | import org.gradle.api.tasks.Internal 6 | import org.gradle.api.tasks.TaskAction 7 | import org.tmatesoft.svn.core.SVNException 8 | import org.tmatesoft.svn.core.SVNNodeKind 9 | import org.tmatesoft.svn.core.SVNURL 10 | import org.tmatesoft.svn.core.io.SVNRepository 11 | import org.tmatesoft.svn.core.wc.SVNClientManager 12 | import org.tmatesoft.svn.core.wc.SVNCopySource 13 | import org.tmatesoft.svn.core.wc.SVNInfo 14 | import org.tmatesoft.svn.core.wc.SVNRevision 15 | 16 | abstract class SvnCopy extends SvnBaseTask { 17 | 18 | /** The remote SVN URL to be copied from. Optional - fallback to {@link #workspaceDir} if missing. */ 19 | @Internal String svnUrl 20 | /** Local workspace that should be copied (default: {@code project.projectDir}) */ 21 | @Internal workspaceDir 22 | /** An optional commit message. */ 23 | @Internal String commitMessage 24 | /** If {@code true}, the target will be removed first if it already exists */ 25 | @Internal boolean replaceExisting 26 | /** If local changes should be committed to the copy target. Only used it not svnUrl is provided. */ 27 | @Internal boolean localChanges 28 | /** 29 | * Set to {@code true} if the target name contains arbitrary chars (as supported by the current SVN server and operation system). 30 | * If {@code false} (default), only a reduced subset of chars (a-z, A-Z, 0-9, "_", "-", ".", and "/") is allowed 31 | */ 32 | @Internal boolean specialChars 33 | 34 | @Internal 35 | abstract String getDestinationPath() 36 | 37 | @TaskAction 38 | def run() { 39 | def clientManager = createSvnClientManager() 40 | def sourceInfo = svnUrl ? fromRemote() : fromWorkspace(clientManager) 41 | def copySource = sourceInfo.copySource 42 | def sourceUrl = sourceInfo.url 43 | def basePath = SvnPath.parse(sourceUrl).moduleBasePath 44 | def fullDestPath = "$basePath/$destinationPath" 45 | try { 46 | if (replaceExisting) { 47 | def repo = SvnSupport.remoteRepository(sourceUrl.setPath(basePath, false), username, password, proxy) 48 | if (existsInRepo(repo, destinationPath)) { 49 | deleteFromRepo(repo, destinationPath) 50 | } 51 | } 52 | def destUrl = sourceUrl.setPath(fullDestPath, false) 53 | logger.info "Copying $sourceUrl at revision $copySource.revision to $destUrl" 54 | def copied = clientManager.copyClient.doCopy([copySource] as SVNCopySource[], destUrl, false, true, true, commitMessage, null); 55 | if (copied.errorMessage) { 56 | if (copied.errorMessage.warning) { 57 | logger.warn "svn-copy completed with warning: $copied" 58 | } else { 59 | throw new InvalidUserDataException("svn-copy failed: $copied") 60 | } 61 | } else { 62 | logger.info "svn-copy successful: $copied" 63 | } 64 | } catch (SVNException e) { 65 | throw new InvalidUserDataException("svn-copy failed for $fullDestPath\n$e.message", e) 66 | } 67 | } 68 | 69 | private def fromWorkspace(SVNClientManager clientManager) { 70 | def workspace = workspaceDir ? project.file(workspaceDir, PathValidation.DIRECTORY) : project.projectDir 71 | try { 72 | SVNInfo info = clientManager.WCClient.doInfo(workspace, SVNRevision.WORKING) 73 | if (localChanges) { 74 | return [copySource: new SVNCopySource(SVNRevision.WORKING, SVNRevision.WORKING, workspace), url: info.URL] 75 | } else { 76 | return [copySource: new SVNCopySource(info.revision, info.revision, info.URL), url: info.URL] 77 | } 78 | } catch (SVNException e) { 79 | throw new InvalidUserDataException("svn-info failed for $workspace.absolutePath\n" + e.message, e) 80 | } 81 | } 82 | 83 | private def fromRemote() { 84 | if (workspaceDir) { 85 | throw new InvalidUserDataException("Either 'svnUrl' or 'workspaceDir' may be set") 86 | } 87 | try { 88 | SVNURL url = SVNURL.parseURIEncoded(svnUrl) 89 | return [copySource: new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, url), url: url] 90 | } catch (SVNException e) { 91 | throw new InvalidUserDataException("Invalid svnUrl value: $svnUrl", e) 92 | } 93 | } 94 | 95 | private static boolean existsInRepo(SVNRepository repo, String path) { 96 | return repo.checkPath(path, -1) != SVNNodeKind.NONE 97 | } 98 | 99 | private static void deleteFromRepo(SVNRepository repo, String path) { 100 | def editor = repo.getCommitEditor("creating a new file", null) 101 | editor.openRoot(-1) 102 | editor.deleteEntry(path, -1) 103 | editor.closeEdit() 104 | } 105 | 106 | boolean isValidName(String svnPath) { 107 | specialChars || svnPath =~ '^[a-zA-Z0-9_\\-./]+$' 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/SvnTestSupport.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools 2 | 3 | import groovy.util.logging.Log 4 | import org.gradle.api.Project 5 | import org.gradle.testfixtures.ProjectBuilder 6 | import org.tmatesoft.svn.core.SVNDepth 7 | import org.tmatesoft.svn.core.SVNProperty 8 | import org.tmatesoft.svn.core.SVNPropertyValue 9 | import org.tmatesoft.svn.core.SVNURL 10 | import org.tmatesoft.svn.core.io.ISVNEditor 11 | import org.tmatesoft.svn.core.io.SVNRepositoryFactory 12 | import org.tmatesoft.svn.core.wc.SVNClientManager 13 | import org.tmatesoft.svn.core.wc.SVNRevision 14 | import spock.lang.Specification 15 | 16 | import static org.tmatesoft.svn.core.wc.SVNWCUtil.* 17 | 18 | @Log 19 | abstract class SvnTestSupport extends Specification { 20 | 21 | File tempDir 22 | SVNClientManager clientManager 23 | SVNURL localRepoUrl 24 | 25 | Project projectWithPlugin(File projectDir = null) { 26 | def projectBuilder = ProjectBuilder.builder() 27 | if (projectDir) { 28 | projectBuilder.withProjectDir(projectDir) 29 | } 30 | def project = projectBuilder.build() 31 | project.apply plugin: "at.bxm.svntools" 32 | return project 33 | } 34 | 35 | SVNURL createLocalRepo(String baseDir = "repo") { 36 | def localRepoDir = new File(tempDir, baseDir) 37 | localRepoUrl = SVNRepositoryFactory.createLocalRepository(localRepoDir, true, false) 38 | def repo = SVNRepositoryFactory.create(localRepoUrl) 39 | repo.authenticationManager = createDefaultAuthenticationManager("username", "password".chars) 40 | def editor = repo.getCommitEditor("creating a new file", null) 41 | editor.openRoot(-1) 42 | editor.addDir("trunk", null, -1) 43 | editor.addDir("trunk/dir", null, -1) 44 | addEmptyFile(editor, "trunk/test.txt") 45 | addEmptyFile(editor, "trunk/dir/test.txt") 46 | editor.addDir("branches", null, -1) 47 | editor.addDir("branches/test-branch", null, -1) 48 | addEmptyFile(editor, "branches/test-branch/test.txt") 49 | editor.addDir("tags", null, -1) 50 | editor.addDir("tags/test-tag", null, -1) 51 | addEmptyFile(editor, "tags/test-tag/test.txt") 52 | editor.closeDir() 53 | editor.closeEdit() 54 | return localRepoUrl 55 | } 56 | 57 | /** Creates a local repo that uses another local repo (also created here) as external definition */ 58 | SVNURL createLocalRepoWithExternals() { 59 | def externalRepo = createLocalRepo("external") 60 | def repoWithExternals = createLocalRepo() 61 | 62 | def repo = SVNRepositoryFactory.create(repoWithExternals) 63 | def editor = repo.getCommitEditor("adding externals", null) 64 | editor.openRoot(-1) 65 | editor.openDir("trunk", -1) 66 | editor.changeDirProperty(SVNProperty.EXTERNALS, SVNPropertyValue.create("ext $externalRepo")) 67 | editor.closeEdit() 68 | return repoWithExternals 69 | } 70 | 71 | void addFile(String path) { 72 | def repo = SVNRepositoryFactory.create(localRepoUrl) 73 | def editor = repo.getCommitEditor("creating a new file", null) 74 | editor.openRoot(-1) 75 | addEmptyFile(editor, path) 76 | editor.closeDir() 77 | editor.closeEdit() 78 | } 79 | 80 | private static ISVNEditor addEmptyFile(ISVNEditor editor, String path) { 81 | editor.addFile path, null, -1 82 | editor.applyTextDelta path, null 83 | editor.textDeltaEnd path 84 | editor.closeFile path, null 85 | editor 86 | } 87 | 88 | File checkoutTrunk() { 89 | return checkoutLocalRepo("trunk") 90 | } 91 | 92 | File checkoutBranch() { 93 | return checkoutLocalRepo("branches/test-branch") 94 | } 95 | 96 | File checkoutTag() { 97 | return checkoutLocalRepo("tags/test-tag") 98 | } 99 | 100 | File checkoutLocalRepo(String path, SVNDepth depth = SVNDepth.INFINITY) { 101 | def workspaceDir = new File(tempDir, "workspace") 102 | clientManager.updateClient.doCheckout(localRepoUrl.appendPath(path, false), workspaceDir, SVNRevision.UNDEFINED, SVNRevision.HEAD, depth, false) 103 | return workspaceDir 104 | } 105 | 106 | void switchLocalRepo(String remotePath, String localPath = null) { 107 | clientManager.updateClient.doSwitch(new File(tempDir, "workspace/" + (localPath ?: "")), localRepoUrl.appendPath(remotePath, false), SVNRevision.UNDEFINED, SVNRevision.HEAD, SVNDepth.INFINITY, false, false) 108 | } 109 | 110 | void updateLocalRepo() { 111 | update("/") 112 | } 113 | 114 | void update(String path) { 115 | clientManager.updateClient.doUpdate(new File(tempDir, "workspace/$path"), SVNRevision.HEAD, SVNDepth.INFINITY, false, false) 116 | } 117 | 118 | long getRevision(file) { 119 | clientManager.WCClient.doInfo(file as File, SVNRevision.WORKING).revision.number 120 | } 121 | 122 | def setup() { 123 | tempDir = new File(System.getProperty("java.io.tmpdir") + "/svntest-" + System.currentTimeMillis()) 124 | clientManager = SVNClientManager.newInstance() 125 | assert tempDir.mkdir(), "Could not create directory $tempDir.absolutePath" 126 | } 127 | 128 | def cleanup() { 129 | if (!tempDir.deleteDir()) { 130 | log.warning("Could not delete directory $tempDir.absolutePath") 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCommitTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import org.gradle.api.InvalidUserDataException 5 | import org.gradle.api.Project 6 | import org.tmatesoft.svn.core.SVNDepth 7 | 8 | class SvnCommitTest extends SvnTestSupport { 9 | 10 | File workspace 11 | Project project 12 | SvnCommit task 13 | 14 | def setup() { 15 | createLocalRepo() 16 | workspace = checkoutTrunk() 17 | project = projectWithPlugin() 18 | task = project.task(type: SvnCommit, "commit") as SvnCommit 19 | } 20 | 21 | def "commit a new file"() { 22 | given: 23 | def newFile = newFile("newfile.txt") 24 | 25 | when: "running the SvnCommit task" 26 | task.source << newFile 27 | task.run() 28 | 29 | then: "file committed, new revision" 30 | getRevision(newFile) == 2 31 | } 32 | 33 | def "commit an added file"() { 34 | given: 35 | def newFile = newFile("newfile.txt") 36 | clientManager.WCClient.doAdd(newFile, false, false, false, SVNDepth.INFINITY, false, false) 37 | 38 | when: "running the SvnCommit task" 39 | task.source << newFile 40 | task.run() 41 | 42 | then: "file committed, new revision" 43 | getRevision(newFile) == 2 44 | } 45 | 46 | def "commit an existing file"() { 47 | given: 48 | def file = existingFile("test.txt") 49 | file.text = "new file" 50 | 51 | when: "running the SvnCommit task" 52 | task.source << file 53 | task.run() 54 | 55 | then: "file committed, new revision" 56 | getRevision(file) == 2 57 | } 58 | 59 | def "empty commit"() { 60 | when: "running the SvnCommit task" 61 | task.run() 62 | 63 | then: "no new revision" 64 | getRevision(workspace) == 1 65 | } 66 | 67 | def "commit without changes"() { 68 | given: 69 | def file = existingFile("test.txt") 70 | 71 | when: "running the SvnCommit task" 72 | task.source << file 73 | task.run() 74 | 75 | then: "no new revision" 76 | getRevision(file) == 1 77 | } 78 | 79 | def "commit a new file inside an unversioned directory"() { 80 | given: 81 | def newFile = newFile("newfile.txt", new File(workspace, "newdir")) 82 | 83 | when: "running the SvnCommit task" 84 | task.source << newFile 85 | task.run() 86 | 87 | then: 88 | thrown InvalidUserDataException 89 | } 90 | 91 | def "commit a new file inside a new directory"() { 92 | given: 93 | def newDir = new File(workspace, "newdir") 94 | def newFile = newFile("newfile.txt", newDir) 95 | 96 | when: "running the SvnCommit task" 97 | task.source << newDir << newFile 98 | task.run() 99 | 100 | then: "file committed, new revision" 101 | getRevision(newFile) == 2 102 | } 103 | 104 | def "commit a new directory recursively"() { 105 | given: 106 | def newDir = new File(workspace, "newdir") 107 | def newFile = newFile("newfile.txt", newDir) 108 | 109 | when: "running the SvnCommit task" 110 | task.source << newDir 111 | task.recursive = true 112 | task.run() 113 | 114 | then: "directory and file committed, new revision" 115 | getRevision(newDir) == 2 116 | getRevision(newFile) == 2 117 | } 118 | 119 | def "commit a changed file in a subdirectory"() { 120 | given: "changed file in a subdirectory" 121 | def unchangedDir = new File(workspace, "dir") 122 | def changedFile = new File(unchangedDir, "test.txt") 123 | changedFile.text = "changed" 124 | 125 | when: "running the SvnCommit task on the directory" 126 | task.source = [unchangedDir] 127 | task.recursive = true 128 | task.run() 129 | 130 | then: "file committed, new revision" 131 | getRevision(unchangedDir) == 1 132 | getRevision(changedFile) == 2 133 | } 134 | 135 | def "commit a file outside of the workspace"() { 136 | given: 137 | def newValidFile = newFile("newfile.txt") 138 | def newInvalidFile = newFile("newfile.txt", "$workspace.absolutePath/.." as File) 139 | 140 | when: "running the SvnCommit task" 141 | task.source << newValidFile 142 | task.source << newInvalidFile 143 | task.run() 144 | 145 | then: "file committed, new revision" 146 | def e = thrown InvalidUserDataException 147 | e.message =~ "svn-add failed for .*" 148 | } 149 | 150 | def "commit a deleted directory"(boolean deleteLocally) { 151 | given: "directory marked for deletion" 152 | def deleteDir = new File(workspace, "dir") 153 | clientManager.WCClient.doDelete(deleteDir, false, deleteLocally, false) 154 | 155 | when: "running the SvnCommit task" 156 | task.source << deleteDir 157 | task.run() 158 | 159 | then: "dir deleted, new revision" 160 | workspace.deleteDir() 161 | checkoutTrunk() 162 | deleteDir.exists() == false 163 | getRevision(workspace) == 2 164 | 165 | where: 166 | deleteLocally | _ 167 | true | _ 168 | false | _ 169 | } 170 | 171 | private File newFile(String name, File path = workspace) { 172 | def file = new File(path, name) 173 | assert !file.exists(), "$file.absolutePath already exist" 174 | path.mkdirs() 175 | file.text = "new file" 176 | return file 177 | } 178 | 179 | private File existingFile(String name) { 180 | def file = new File(workspace, name) 181 | assert file.exists(), "$file.absolutePath doesn't exist" 182 | return file 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then 166 | cd "$(dirname "$0")" 167 | fi 168 | 169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 170 | -------------------------------------------------------------------------------- /docs/GeneralConfig.md: -------------------------------------------------------------------------------- 1 | ## General Configuration 2 | 3 | The `svntools` block (implemented by [SvnToolsPluginExtension](../src/main/groovy/at/bxm/gradleplugins/svntools/SvnToolsPluginExtension.groovy)) can be used to 4 | 5 | * specify default values for some configuration properties: 6 | * **username**: The SVN username - leave empty if no authentication is required. [See below](#svn-credentials) 7 | * **password**: The SVN password - leave empty if no authentication is required. [See below](#svn-credentials) 8 | * adjust proxy server settings (see below) 9 | * access information about the current SVN workspace root (i.e. the project's root directory), wrapped by an [SvnData](../src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnData.groovy) object: 10 | * **info.revisionNumber** The SVN revision number 11 | * **info.committedDate** The last committed Date (java.util.Date) 12 | * **info.committedAuthor** The last committer 13 | * **info.url** The complete SVN URL of the checked-out project 14 | * **info.repositoryRootUrl** The root URL of the SVN repository 15 | * **info.name** Either "trunk", the name of the current branch, or the name of the current tag 16 | * **info.trunk** "true" if the SVN URL refers to a trunk 17 | * **info.branch** "true" if the SVN URL refers to a branch 18 | * **info.tag** "true" if the SVN URL refers to a tag 19 | * access information about an arbitrary path within an SVN workspace, wrapped by an [SvnData](../src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnData.groovy) object: **getInfo("path/file.ext")** (see example below) 20 | * access information about a path within a remote SVN repository, wrapped by an [SvnData](../src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnData.groovy) object: **getRemoteInfo("repoUrl","path/file.ext")** (see example below) 21 | * summarize the local revision(s) of a working copy, wrapped by an [SvnVersionData](../src/main/groovy/at/bxm/gradleplugins/svntools/api/SvnVersionData.groovy) object: 22 | * **version** [svnversion](http://svnbook.red-bean.com/en/1.7/svn.ref.svnversion.re.html) output 23 | * **version.mixedRevision** "true" if the working copy contains mixed revisions 24 | * **version.minRevisionNumber** The smallest SVN revision within the working copy 25 | * **version.maxRevisionNumber** The greatest SVN revision within the working copy 26 | * **version.modified** "true" if the working copy contains local modifications 27 | * **version.sparse** "true" if the working copy is sparsely populated (i.e. "depth" is not "infinity") 28 | * **version.switched** "true" if the parts of the working copy have been switched 29 | 30 | Note: The `svntools.info` and `svntools.version` objects assume that the current Gradle project has been checked out from SVN. To retrieve information about other SVN files or workspaces, use the [SvnInfo](SvnInfo.md) resp. [SvnVersion](SvnVersion.md) tasks. 31 | 32 | #### SVN credentials 33 | 34 | It isn't a good idea to include usernames and passwords in the build script. Instead, add them to the `~/gradle.properties` file: 35 | 36 | svnUsername = foo 37 | svnPassword = bar 38 | 39 | Now, these values can be used within the build script like regular variables: 40 | 41 | task svnStatus(type: at.bxm.gradleplugins.svntools.tasks.SvnInfo) { 42 | username = svnUsername // credentials for the current task only 43 | password = svnPassword 44 | } 45 | 46 | svntools { 47 | username = svnUsername // credentials for all tasks 48 | password = svnPassword 49 | } 50 | 51 | Instead of `username = svnUsername`, write `username = findProperty("svnUsername")` to avoid an error if that value doesn't exist. 52 | 53 | ### Example 54 | 55 | apply plugin: "at.bxm.svntools" 56 | 57 | svntools { 58 | username = "john" 59 | password = "secret" 60 | } 61 | 62 | task info << { 63 | println "Current revision is $svntools.info.revisionNumber" 64 | } 65 | 66 | task specialInfo << { 67 | println "Current revision of 'readme.txt' is " + svntools.getInfo(file("readme.txt")) 68 | } 69 | 70 | task remoteInfo << { 71 | try { 72 | println "Remote revision of 'readme.txt' is " + svntools.getRemoteInfo("https://svn.apache.org/repos/asf/subversion", "trunk/readme.txt") 73 | } catch (Exception e) { 74 | println "Remote file is not available: $e.message" 75 | } 76 | } 77 | 78 | ## Using a Proxy Server 79 | 80 | If a proxy server is needed for connecting to the remote SVN repository, it can be configured in one of the following ways (ordered by precedence): 81 | 82 | ### Specify the proxy settings in `build.gradle` 83 | 84 | svntools { 85 | proxy.host = "[hostname]" 86 | proxy.port = [portnumber] 87 | proxy.username = "[username]" 88 | proxy.password = "[password]" 89 | proxy.nonProxyHosts = "127.0.0.1|localhost|companysvn" 90 | } 91 | 92 | This way, the proxy settings are only applied to the svn-tools-plugin and are ignored by Gradle. 93 | 94 | ### Specify the proxy settings at the command line 95 | 96 | gradlew -Dhttp.proxyHost=[hostname] \ 97 | -Dhttp.proxyPort=[portnumber] \ 98 | -Dhttp.proxyUser=[username] \ 99 | -Dhttp.proxyPassword=[password] \ 100 | -Dhttp.nonProxyHosts=[list_of_hostnames] \ 101 | [taskname] 102 | 103 | Now all tasks of the current Gradle execution are using the proxy server (e.g. dependencies are downloaded through the proxy server) 104 | 105 | ### Specify the proxy settings in `~/gradle.properties` 106 | 107 | systemProp.http.proxyHost = [hostname] 108 | systemProp.http.proxyPort = [portnumber] 109 | systemProp.http.proxyUser = [username] 110 | systemProp.http.proxyPassword = [password] 111 | systemProp.http.nonProxyHosts = [list_of_hostnames] 112 | 113 | Every Gradle execution will use the proxy server. 114 | 115 | See [Java networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html#Proxies) 116 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnInfoTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import at.bxm.gradleplugins.svntools.api.SvnData 5 | import org.gradle.api.InvalidUserDataException 6 | 7 | class SvnInfoTest extends SvnTestSupport { 8 | 9 | def "execute at trunk"() { 10 | given: "an SVN workspace at trunk" 11 | createLocalRepo() 12 | def workspace = checkoutTrunk() 13 | 14 | when: "running the SvnInfo task" 15 | def project = projectWithPlugin() 16 | def task = project.task(type: SvnInfo, "info") as SvnInfo 17 | task.sourcePath = workspace 18 | task.run() 19 | 20 | then: "SVN data are available" 21 | def svnData = project.ext.svnData as SvnData 22 | svnData != null 23 | svnData.trunk 24 | svnData.name == "trunk" 25 | !svnData.branch 26 | !svnData.tag 27 | svnData.revisionNumber == 1 28 | } 29 | 30 | def "execute at a branch"() { 31 | given: "an SVN workspace at a branch" 32 | createLocalRepo() 33 | def workspace = checkoutBranch() 34 | 35 | when: "running the SvnInfo task" 36 | def project = projectWithPlugin() 37 | def task = project.task(type: SvnInfo, "info") as SvnInfo 38 | task.sourcePath = workspace 39 | task.run() 40 | 41 | then: "SVN data are available" 42 | def svnData = project.ext.svnData as SvnData 43 | svnData != null 44 | !svnData.trunk 45 | svnData.branch 46 | svnData.name == "test-branch" 47 | !svnData.tag 48 | svnData.revisionNumber == 1 49 | } 50 | 51 | def "execute at a tag"() { 52 | given: "an SVN workspace at a tag" 53 | createLocalRepo() 54 | def workspace = checkoutTag() 55 | 56 | when: "running the SvnInfo task" 57 | def project = projectWithPlugin() 58 | def task = project.task(type: SvnInfo, "info") as SvnInfo 59 | task.sourcePath = workspace 60 | task.run() 61 | 62 | then: "SVN data are available" 63 | def svnData = project.ext.svnData as SvnData 64 | svnData != null 65 | !svnData.trunk 66 | !svnData.branch 67 | svnData.tag 68 | svnData.name == "test-tag" 69 | svnData.revisionNumber == 1 70 | } 71 | 72 | def "use custom property name"() { 73 | given: "an SVN workspace" 74 | createLocalRepo() 75 | def workspace = checkoutTrunk() 76 | 77 | when: "running the SvnInfo task" 78 | def project = projectWithPlugin() 79 | def task = project.task(type: SvnInfo, "info") as SvnInfo 80 | task.sourcePath = workspace 81 | task.targetPropertyName = "myProp" 82 | task.run() 83 | 84 | then: "SVN data are available with the right name" 85 | project.hasProperty("myProp") 86 | !project.hasProperty("svnData") 87 | } 88 | 89 | def "execute on a single file at trunk"() { 90 | given: "an SVN workspace at trunk" 91 | createLocalRepo() 92 | def workspace = checkoutTrunk() 93 | 94 | when: "running the SvnInfo task" 95 | def project = projectWithPlugin() 96 | def task = project.task(type: SvnInfo, "info") as SvnInfo 97 | task.sourcePath = new File(workspace, "test.txt") 98 | task.run() 99 | 100 | then: "SVN data are available" 101 | project.ext.svnData.name == "trunk" 102 | } 103 | 104 | def "execute on a single file at a branch"() { 105 | given: "an SVN workspace at a branch" 106 | createLocalRepo() 107 | def workspace = checkoutBranch() 108 | 109 | when: "running the SvnInfo task" 110 | def project = projectWithPlugin() 111 | def task = project.task(type: SvnInfo, "info") as SvnInfo 112 | task.sourcePath = new File(workspace, "test.txt") 113 | task.run() 114 | 115 | then: "SVN data are available" 116 | project.ext.svnData.name == "test-branch" 117 | } 118 | 119 | def "execute on a single file at a tag"() { 120 | given: "an SVN workspace at a tag" 121 | createLocalRepo() 122 | def workspace = checkoutTag() 123 | 124 | when: "running the SvnInfo task" 125 | def project = projectWithPlugin() 126 | def task = project.task(type: SvnInfo, "info") as SvnInfo 127 | task.sourcePath = new File(workspace, "test.txt") 128 | task.run() 129 | 130 | then: "SVN data are available" 131 | project.ext.svnData.name == "test-tag" 132 | } 133 | 134 | def "execute outside of a workspace"() { 135 | given: "no SVN workspace" 136 | 137 | when: "running the SvnInfo task" 138 | def project = projectWithPlugin() 139 | def task = project.task(type: SvnInfo, "info") as SvnInfo 140 | task.sourcePath = tempDir 141 | task.run() 142 | 143 | then: "SVN data are available" 144 | thrown InvalidUserDataException 145 | } 146 | 147 | // https://github.com/martoe/gradle-svntools-plugin/issues/11 148 | def "different revisions on branches"() { 149 | given: "an SVN repo at revision 1" 150 | createLocalRepo() 151 | def project = projectWithPlugin() 152 | def workspace = checkoutTrunk() 153 | 154 | when: "checking out trunk" 155 | def task = project.task(type: SvnInfo, "info1") as SvnInfo 156 | task.sourcePath = workspace 157 | task.run() 158 | 159 | then: "trunk is at revision 1" 160 | project.ext.svnData.revisionNumber == 1 161 | 162 | when: "committing to trunk" 163 | addFile("trunk/somefile.txt") 164 | updateLocalRepo() 165 | task = project.task(type: SvnInfo, "info2") as SvnInfo 166 | task.sourcePath = workspace 167 | task.run() 168 | 169 | then: "trunk is at revision 2" 170 | project.ext.svnData.revisionNumber == 2 171 | 172 | when: "switching to root" 173 | switchLocalRepo("/") 174 | task = project.task(type: SvnInfo, "info3") as SvnInfo 175 | task.sourcePath = workspace 176 | task.run() 177 | 178 | then: "root is at revision 2" 179 | project.ext.svnData.revisionNumber == 2 180 | 181 | when: "switching to branch" 182 | switchLocalRepo("branches/test-branch") 183 | task = project.task(type: SvnInfo, "info4") as SvnInfo 184 | task.sourcePath = workspace 185 | task.run() 186 | 187 | then: "branch is still at revision 1" 188 | project.ext.svnData.revisionNumber == 1 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/test/groovy/at/bxm/gradleplugins/svntools/tasks/SvnCheckoutTest.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.tasks 2 | 3 | import at.bxm.gradleplugins.svntools.SvnTestSupport 4 | import at.bxm.gradleplugins.svntools.SvnToolsPluginExtension 5 | import at.bxm.gradleplugins.svntools.api.SvnDepth 6 | import org.gradle.api.InvalidUserDataException 7 | import org.gradle.api.Project 8 | import spock.lang.Ignore 9 | 10 | class SvnCheckoutTest extends SvnTestSupport { 11 | 12 | File workspace 13 | Project project 14 | SvnCheckout task 15 | 16 | def setup() { 17 | createLocalRepo() 18 | project = projectWithPlugin() 19 | task = project.task(type: SvnCheckout, "checkout") as SvnCheckout 20 | } 21 | 22 | def "happy path"() { 23 | given: "nonexistent workspace" 24 | def workspaceDir = "$tempDir.absolutePath/myNewWorkspace" 25 | assert !new File(workspaceDir).exists() 26 | 27 | when: "running the SvnCheckout task" 28 | task.svnUrl = localRepoUrl 29 | task.workspaceDir = workspaceDir 30 | task.run() 31 | 32 | then: "local workspace exists and contains files" 33 | getRevision(workspaceDir) == 1 34 | childrenOf(workspaceDir).size() == 3 35 | } 36 | 37 | def "invalid remote URL"() { 38 | when: "running the SvnCheckout task" 39 | task.svnUrl = "$localRepoUrl/blah" 40 | task.workspaceDir = new File(tempDir, "workspace") 41 | task.run() 42 | 43 | then: 44 | def e = thrown InvalidUserDataException 45 | e.message =~ "svn-checkout failed for .*" 46 | } 47 | 48 | def "checkout using a non-empty dir"() { 49 | given: "a non-empty workspace" 50 | def workspaceDir = new File(tempDir, "myNewWorkspace") 51 | workspaceDir.mkdirs() 52 | new File(workspaceDir, "test.txt").text = "placeholder" 53 | 54 | when: "running the SvnCheckout task" 55 | task.svnUrl = "$localRepoUrl/trunk" 56 | task.workspaceDir = workspaceDir 57 | task.run() 58 | 59 | then: 60 | def e = thrown InvalidUserDataException 61 | e.message =~ ".* must be an empty directory" 62 | } 63 | 64 | def "no target dir"() { 65 | when: "running the SvnCheckout task without target dir" 66 | task.svnUrl = localRepoUrl 67 | task.run() 68 | 69 | then: 70 | def e = thrown InvalidUserDataException 71 | e.message == "workspaceDir must be specified" 72 | } 73 | 74 | def "checkout with proxy"() { 75 | given: "nonexistent workspace" 76 | def workspaceDir = "$tempDir.absolutePath/myNewWorkspace" 77 | assert !new File(workspaceDir).exists() 78 | project.extensions.findByType(SvnToolsPluginExtension).proxy.host = "localhost" 79 | project.extensions.findByType(SvnToolsPluginExtension).proxy.port = 9999 80 | 81 | when: "running the SvnCheckout task" 82 | task.svnUrl = localRepoUrl 83 | task.workspaceDir = workspaceDir 84 | task.run() 85 | 86 | then: "local workspace exists" 87 | getRevision(workspaceDir) == 1 88 | } 89 | 90 | def "checkout into a existing workspace"() { 91 | given: "an SVN workspace" 92 | def workspaceDir = checkoutTrunk() 93 | 94 | when: "running the SvnCheckout task" 95 | task.svnUrl = "$localRepoUrl/trunk" 96 | task.workspaceDir = workspaceDir 97 | task.update = true 98 | task.run() 99 | 100 | then: "local workspace exists" 101 | getRevision(workspaceDir) == 1 102 | } 103 | 104 | def "checkout into a existing workspace without update-flag"() { 105 | given: "an SVN workspace" 106 | def workspaceDir = checkoutTrunk() 107 | 108 | when: "running the SvnCheckout task" 109 | task.svnUrl = "$localRepoUrl/trunk" 110 | task.workspaceDir = workspaceDir 111 | task.run() 112 | 113 | then: 114 | def e = thrown InvalidUserDataException 115 | e.message =~ ".* must be an empty directory" 116 | } 117 | 118 | def "checkout into a wrong workspace"() { 119 | given: "an SVN workspace" 120 | def workspaceDir = checkoutBranch() 121 | 122 | when: "running the SvnCheckout task for a different location" 123 | task.svnUrl = "$localRepoUrl/trunk" 124 | task.workspaceDir = workspaceDir 125 | task.update = true 126 | task.run() 127 | 128 | then: 129 | def e = thrown InvalidUserDataException 130 | e.message =~ "SVN location of .* is invalid: .*" 131 | } 132 | 133 | def "checkout depth=empty"() { 134 | given: "nonexistent workspace" 135 | def workspaceDir = "$tempDir.absolutePath/myNewWorkspace" 136 | assert !new File(workspaceDir).exists() 137 | 138 | when: "running the SvnCheckout task with depth=empty" 139 | task.svnUrl = localRepoUrl.appendPath("trunk", false) 140 | task.workspaceDir = workspaceDir 141 | task.depth = "empty" 142 | task.run() 143 | 144 | then: "local workspace is empty" 145 | childrenOf(workspaceDir).size() == 0 146 | } 147 | 148 | def "checkout depth=files"() { 149 | given: "nonexistent workspace" 150 | def workspaceDir = "$tempDir.absolutePath/myNewWorkspace" 151 | assert !new File(workspaceDir).exists() 152 | 153 | when: "running the SvnCheckout task with depth=files" 154 | task.svnUrl = localRepoUrl.appendPath("trunk", false) 155 | task.workspaceDir = workspaceDir 156 | task.depth = "Files" 157 | task.run() 158 | 159 | then: "local workspace contains one file" 160 | def ws = childrenOf(workspaceDir) 161 | ws.size() == 1 162 | ws[0].isFile() 163 | } 164 | 165 | def "checkout depth=immediates"() { 166 | given: "nonexistent workspace" 167 | def workspaceDir = "$tempDir.absolutePath/myNewWorkspace" 168 | assert !new File(workspaceDir).exists() 169 | 170 | when: "running the SvnCheckout task with depth=immediates" 171 | task.svnUrl = localRepoUrl.appendPath("trunk", false) 172 | task.workspaceDir = workspaceDir 173 | task.depth = SvnDepth.IMMEDIATES 174 | task.run() 175 | 176 | then: "local workspace contains one file and one empty dir" 177 | def ws = childrenOf(workspaceDir) 178 | ws.findAll({ it.isFile() }).size() == 1 179 | ws.findAll({ it.isDirectory() }).size() == 1 180 | childrenOf(ws.find({ it.isDirectory() })).size() == 0 181 | } 182 | 183 | @Ignore("expensive test with remote dependencies") 184 | def "remote repo"() { 185 | given: "nonexistent workspace" 186 | def workspaceDir = "$tempDir.absolutePath/myNewWorkspace" 187 | assert !new File(workspaceDir).exists() 188 | 189 | when: "running the SvnCheckout task" 190 | task.svnUrl = "https://svn.svnkit.com/repos/svnkit/trunk/svnkit-osgi" 191 | task.workspaceDir = workspaceDir 192 | task.run() 193 | 194 | then: "local workspace exists and contains files" 195 | getRevision(workspaceDir) > 10000 196 | childrenOf(workspaceDir).size() > 2 197 | } 198 | 199 | private static List childrenOf(parentFile) { 200 | (parentFile as File).listFiles().findAll { it.name != ".svn" } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/groovy/at/bxm/gradleplugins/svntools/internal/SvnSupport.groovy: -------------------------------------------------------------------------------- 1 | package at.bxm.gradleplugins.svntools.internal 2 | 3 | import at.bxm.gradleplugins.svntools.api.SvnData 4 | import at.bxm.gradleplugins.svntools.api.SvnVersionData 5 | import groovy.util.logging.Log 6 | import org.gradle.api.InvalidUserDataException 7 | import org.tmatesoft.svn.core.SVNDepth 8 | import org.tmatesoft.svn.core.SVNURL 9 | import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager 10 | import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions 11 | import org.tmatesoft.svn.core.io.SVNRepository 12 | import org.tmatesoft.svn.core.io.SVNRepositoryFactory 13 | import org.tmatesoft.svn.core.wc.ISVNStatusHandler 14 | import org.tmatesoft.svn.core.wc.SVNClientManager 15 | import org.tmatesoft.svn.core.wc.SVNRevision 16 | import org.tmatesoft.svn.core.wc.SVNStatus 17 | import org.tmatesoft.svn.core.wc.SVNStatusType 18 | 19 | @Log 20 | class SvnSupport { 21 | 22 | static SVNClientManager createSvnClientManager(String username, char[] password, SvnProxy proxy) { 23 | return SVNClientManager.newInstance( 24 | // create a local SVN config dir to make sure we don't reuse existing credentials: 25 | new DefaultSVNOptions(new File(".subversion"), true), 26 | createAuthenticationManager(username, password, proxy)) 27 | } 28 | 29 | static ISVNAuthenticationManager createAuthenticationManager(String username, char[] password, SvnProxy proxy) { 30 | def authManager = new SimpleAuthenticationManager(username, password) 31 | if (proxy?.host) { 32 | log.info "Using proxy $proxy" 33 | authManager.setProxy(proxy.host, proxy.port, proxy.username, (char[])proxy.password, proxy.nonProxyHosts) 34 | } 35 | return authManager 36 | } 37 | 38 | static SvnData createSvnData(File srcPath, String username, char[] password, SvnProxy proxy, boolean ignoreErrors) { 39 | def result = new SvnData() 40 | try { 41 | def info = createSvnClientManager(username, password, proxy).WCClient.doInfo srcPath, SVNRevision.WORKING 42 | result.revisionNumber = info.committedRevision.number 43 | result.committedDate = info.committedDate 44 | result.committedAuthor = info.author 45 | result.url = info.URL 46 | result.repositoryRootUrl = info.repositoryRootURL 47 | try { 48 | def svnPath = SvnPath.parse info.URL 49 | if (svnPath.trunk) { 50 | result.trunk = true 51 | result.name = "trunk" 52 | log.info "Working copy is on trunk at revision $result.revisionNumber" 53 | } else if (svnPath.branch) { 54 | result.branch = true 55 | result.name = svnPath.branchName 56 | log.info "Working copy is on branch $result.branch at revision $result.revisionNumber" 57 | } else if (svnPath.tag) { 58 | result.tag = true 59 | result.name = svnPath.tagName 60 | log.info "Working copy is on tag $result.tag at revision $result.revisionNumber" 61 | } 62 | } catch (MalformedURLException e) { 63 | log.warning "Working copy must be a trunk, branches or tags folder: $e.message" 64 | } 65 | } catch (Exception e) { 66 | if (ignoreErrors) { 67 | log.warning "Could not execute svn-info on $srcPath.absolutePath ($e.message)" 68 | } else { 69 | throw new InvalidUserDataException("Could not execute svn-info on $srcPath.absolutePath ($e.message)", e) 70 | } 71 | } 72 | return result 73 | } 74 | 75 | static SvnData createSvnData(SVNURL svnUrl, String path, String username, char[] password, SvnProxy proxy, boolean ignoreErrors) { 76 | def result = new SvnData() 77 | try { 78 | def info = remoteRepository(svnUrl, username, password, proxy).info(path, -1) 79 | result.revisionNumber = info.revision 80 | result.committedDate = info.date 81 | result.committedAuthor = info.author 82 | result.url = info.URL 83 | result.repositoryRootUrl = info.repositoryRoot 84 | try { 85 | def svnPath = SvnPath.parse info.URL 86 | if (svnPath.trunk) { 87 | result.trunk = true 88 | result.name = "trunk" 89 | log.info "Remote URL is on trunk at revision $result.revisionNumber" 90 | } else if (svnPath.branch) { 91 | result.branch = true 92 | result.name = svnPath.branchName 93 | log.info "Remote URL is on branch $result.branch at revision $result.revisionNumber" 94 | } else if (svnPath.tag) { 95 | result.tag = true 96 | result.name = svnPath.tagName 97 | log.info "Remote URL is on tag $result.tag at revision $result.revisionNumber" 98 | } 99 | } catch (MalformedURLException e) { 100 | log.info "Remote URL is neither a trunk, nor branch, nor tag: $e.message" 101 | } 102 | } catch (Exception e) { 103 | if (ignoreErrors) { 104 | log.warning "Could not execute svn-info on $svnUrl ($e.message)" 105 | } else { 106 | throw new InvalidUserDataException("Could not execute svn-info on $svnUrl ($e.message)", e) 107 | } 108 | } 109 | return result 110 | } 111 | 112 | static SvnVersionData createSvnVersionData(File srcPath, String username, char[] password, SvnProxy proxy, boolean ignoreErrors) { 113 | def versionHandler = new VersionHandler() 114 | try { 115 | createSvnClientManager(username, password, proxy).statusClient.doStatus( 116 | srcPath, SVNRevision.UNDEFINED, SVNDepth.INFINITY, false, true, false, false, versionHandler, null) 117 | } catch (Exception e) { 118 | if (ignoreErrors) { 119 | log.warning "Could not execute svnversion on $srcPath.absolutePath ($e.message)" 120 | } else { 121 | throw new InvalidUserDataException("Could not execute svnversion on $srcPath.absolutePath ($e.message)", e) 122 | } 123 | } 124 | return versionHandler.version 125 | } 126 | 127 | static SVNRepository remoteRepository(SVNURL repositoryUrl, String username, char[] password, SvnProxy proxy) { 128 | def repo = SVNRepositoryFactory.create(repositoryUrl) 129 | repo.authenticationManager = createAuthenticationManager(username, password, proxy) 130 | return repo 131 | } 132 | 133 | static SVNRevision revisionFrom(Long value) { 134 | return value != null && value >= 0 ? SVNRevision.create(value) : SVNRevision.HEAD 135 | } 136 | 137 | static class VersionHandler implements ISVNStatusHandler { 138 | final version = new SvnVersionData() 139 | 140 | @Override 141 | void handleStatus(SVNStatus status) { 142 | version.minRevisionNumber = version.minRevisionNumber == SvnData.UNKNOWN_REVISION ? status.revision.number : Math.min(version.minRevisionNumber, status.revision.number) 143 | version.maxRevisionNumber = version.maxRevisionNumber == SvnData.UNKNOWN_REVISION ? status.revision.number : Math.max(version.maxRevisionNumber, status.revision.number) 144 | if (status.contentsStatus == SVNStatusType.STATUS_NONE) { 145 | log.info("no status for $status.file - probably an external definition") 146 | } else { 147 | if (status.contentsStatus != SVNStatusType.STATUS_NORMAL) { 148 | // TODO use "combinedNodeAndContentsStatus" instead? 149 | log.info("$status.repositoryRelativePath has status $status.contentsStatus - workspace is dirty") 150 | version.modified = true 151 | } else if (status.nodeStatus != SVNStatusType.STATUS_NORMAL) { 152 | // e.g. deleted 153 | log.info("$status.repositoryRelativePath has nodeStatus $status.nodeStatus - workspace is dirty") 154 | version.modified = true 155 | } 156 | // TODO also check "propertiesStatus"? 157 | version.switched |= status.switched 158 | version.sparse |= !status.depth.recursive 159 | } 160 | } 161 | } 162 | } 163 | --------------------------------------------------------------------------------