diff --git a/build.gradle b/build.gradle index ccee100..7962566 100644 --- a/build.gradle +++ b/build.gradle @@ -41,9 +41,6 @@ group = 'com.cinnober.gradle' test { useJUnitPlatform() - retry { - maxRetries = 5 - } } // -- Maven Central -- diff --git a/readme.md b/readme.md index 35e8e6b..1b75c73 100644 --- a/readme.md +++ b/readme.md @@ -9,16 +9,17 @@ Version numbers must follow [Semantic Versioning 2.0.0](http://semver.org/spec/v In your `build.gradle` file: plugins { - id "com.cinnober.gradle.semver-git" version "2.2.0" apply false + id "com.cinnober.gradle.semver-git" version "3.1.0" + } + + semverGit { + // All of the following are optional: + nextVersion = "major", "minor" (default), "patch" or e.g. "3.0.0-rc2" + snapshotSuffix = "SNAPSHOT" (default) or a pattern, e.g. ".g-SNAPSHOT" + dirtyMarker = "-dirty" (default) replaces in snapshotSuffix + gitDescribeArgs = '--match *[0-9].[0-9]*.[0-9]*' (default) or other arguments for git describe. + prefix = 'v' if your tags are named like v3.0.0 } - // optionally: ext.nextVersion = "major", "minor" (default), "patch" or e.g. "3.0.0-rc2" - // optionally: ext.snapshotSuffix = "SNAPSHOT" (default) or a pattern, e.g. ".g-SNAPSHOT" - // optionally: ext.dirtyMarker = "-dirty" (default) replaces in snapshotSuffix - // optionally: ext.gitDescribeArgs = '--match *[0-9].[0-9]*.[0-9]*' (default) or other arguments for git describe. - // optionally: ext.semverPrefix = 'v' if your tags are named like v3.0.0 - apply plugin: 'com.cinnober.gradle.semver-git' - -Note: Use `apply false` combined with a manual apply if you want to change `nextVersion` and `snapshotSuffix`. Then everything should just work. To create a release, create an annotated git tag, e.g.: @@ -41,6 +42,29 @@ For example the snapshot suffix pattern is `.g-SNAPSHOT` which will generate the version `1.1.0-5.g5242341-SNAPSHOT`, which is a unique snapshot version. + +### Legacy config ### + +Plugin versions 3.0.0 and earlier used a different config method, +which required setting the configuration before applying the plugin: + + plugins { + id "com.cinnober.gradle.semver-git" version "2.2.0" apply false + } + // optionally: ext.nextVersion = "major", "minor" (default), "patch" or e.g. "3.0.0-rc2" + // optionally: ext.snapshotSuffix = "SNAPSHOT" (default) or a pattern, e.g. ".g-SNAPSHOT" + // optionally: ext.dirtyMarker = "-dirty" (default) replaces in snapshotSuffix + // optionally: ext.gitDescribeArgs = '--match *[0-9].[0-9]*.[0-9]*' (default) or other arguments for git describe. + // optionally: ext.semverPrefix = 'v' if your tags are named like v3.0.0 + apply plugin: 'com.cinnober.gradle.semver-git' + +Note: This configuration method requires `apply false` combined with a manual apply. + +Both legacy config and the newer `semverGit {}` config block may be +used simultaneously. Any non-null setting defined using both methods +will use the value from the `semverGit {}` block. + + ### Release ### Versions are stored as annotated tags in git. [Semantic versioning](http://semver.org) is used. diff --git a/src/main/groovy/com/cinnober/gradle/semver_git/SemverGitPlugin.groovy b/src/main/groovy/com/cinnober/gradle/semver_git/SemverGitPlugin.groovy index fdfad93..2f4ff62 100644 --- a/src/main/groovy/com/cinnober/gradle/semver_git/SemverGitPlugin.groovy +++ b/src/main/groovy/com/cinnober/gradle/semver_git/SemverGitPlugin.groovy @@ -24,8 +24,110 @@ package com.cinnober.gradle.semver_git -import org.gradle.api.Project import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.provider.Property + +interface SemverGitPluginExtension { + /** + * One of "minor" (default) or "major" to bump the respective SemVer part, + * or a literal value such as "3.0.0-rc2" to use as the next version. + */ + Property getNextVersion() + + /** + * Pattern for the suffix to append when the current commit is not version tagged. + * + *

+ * Example: "<count>.g<sha><dirty>-SNAPSHOT". + *

+ * + *

+ * Pattern may include the following placeholders: + *

+ * + *
    + *
  • <count>: Number of commits (including this) since the last version tag.
  • + *
  • <sha>: Abbreviated ID of the current commit.
  • + *
  • <dirty>: The value of {@link #getDirtyMarker() dirtyMarker} if there are unstaged changes, + * otherwise empty. Note that untracked files do not count.
  • + *
+ * + *

+ * Default: "SNAPSHOT" + *

+ */ + Property getSnapshotSuffix() + + /** + * The value to substitute for <dirty> in {@link #getSnapshotSuffix() snapshotSuffix} + * if there are unstaged changes. + * + *

+ * Note that this has no effect if {@link #getSnapshotSuffix() snapshotSuffix} + * does not include a <dirty> placeholder. + *

+ * + *

+ * Default: "-dirty" + *

+ */ + Property getDirtyMarker() + + /** + * Additional arguments for the git describe subprocess. + * + *

+ * Default: "--match *[0-9].[0-9]*.[0-9]*" + *

+ */ + Property getGitDescribeArgs() + + /** + * A prefix that may be stripped from tag names to create a version number. + * + *

+ * For example: if your tags are named like v3.0.0, set this to "v". + *

+ * + *

+ * Default: null + *

+ */ + Property getPrefix() +} + +class DeferredVersion { + private final File projectDir; + private final SemverGitPluginExtension extension; + private String version = null; + + DeferredVersion(File projectDir, SemverGitPluginExtension extension) { + this.projectDir = projectDir; + this.extension = extension; + } + + private synchronized String evaluate() { + if (version == null) { + version = SemverGitPlugin.getGitVersion( + this.extension.nextVersion.getOrNull(), + this.extension.snapshotSuffix.getOrNull(), + this.extension.dirtyMarker.getOrNull(), + this.extension.gitDescribeArgs.getOrNull(), + this.extension.prefix.getOrNull(), + this.projectDir + ) + } + return version; + } + + @Override + // Gradle uses the toString() of the version object, + // see: https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#getVersion-- + public String toString() { + return evaluate(); + } +} class SemverGitPlugin implements Plugin { @@ -113,27 +215,31 @@ class SemverGitPlugin implements Plugin { } void apply(Project project) { - def nextVersion = "minor" - def snapshotSuffix = "SNAPSHOT" - def dirtyMarker = "-dirty" - def gitDescribeArgs = '--match *[0-9].[0-9]*.[0-9]*' - String semverPrefix = null + def extension = project.extensions.create("semverGit", SemverGitPluginExtension) + extension.nextVersion.convention("minor").finalizeValueOnRead() + extension.snapshotSuffix.convention("SNAPSHOT").finalizeValueOnRead() + extension.dirtyMarker.convention("-dirty").finalizeValueOnRead() + extension.gitDescribeArgs.convention('--match *[0-9].[0-9]*.[0-9]*').finalizeValueOnRead() + extension.prefix.convention(null).finalizeValueOnRead() + if (project.ext.properties.containsKey("nextVersion")) { - nextVersion = project.ext.nextVersion + extension.nextVersion.set(project.ext.nextVersion) } if (project.ext.properties.containsKey("snapshotSuffix")) { - snapshotSuffix = project.ext.snapshotSuffix + extension.snapshotSuffix.set(project.ext.snapshotSuffix) } if (project.ext.properties.containsKey("gitDescribeArgs")) { - gitDescribeArgs = project.ext.gitDescribeArgs + extension.gitDescribeArgs.set(project.ext.gitDescribeArgs) } if (project.ext.properties.containsKey("dirtyMarker")) { - dirtyMarker = project.ext.dirtyMarker + extension.dirtyMarker.set(project.ext.dirtyMarker) } if (project.ext.properties.containsKey("semverPrefix")) { - semverPrefix = project.ext.semverPrefix + extension.prefix.set(project.ext.semverPrefix) } - project.version = getGitVersion(nextVersion, snapshotSuffix, dirtyMarker, gitDescribeArgs, semverPrefix, project.projectDir) + + project.version = new DeferredVersion(project.projectDir, extension) + project.tasks.register('showVersion') { group = 'Help' description = 'Show the project version' diff --git a/src/test/groovy/com/cinnober/gradle/semver_git/SemverGitPluginFunctionalTest.groovy b/src/test/groovy/com/cinnober/gradle/semver_git/SemverGitPluginFunctionalTest.groovy index 251c1f2..2ce4212 100644 --- a/src/test/groovy/com/cinnober/gradle/semver_git/SemverGitPluginFunctionalTest.groovy +++ b/src/test/groovy/com/cinnober/gradle/semver_git/SemverGitPluginFunctionalTest.groovy @@ -2,7 +2,6 @@ package com.cinnober.gradle.semver_git import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner -import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification import spock.lang.TempDir @@ -10,7 +9,6 @@ import spock.lang.TempDir import java.nio.file.Files import java.nio.file.Path -@Ignore class SemverGitPluginFunctionalTest extends Specification { @Shared Boolean slowIo = false @@ -38,17 +36,20 @@ class SemverGitPluginFunctionalTest extends Specification { def "Simple build"() { when: - setupPluginWithClasspath() + setupPluginWithClasspath(config) init() BuildResult result = gradleBuild("showVersion") then: result.getOutput().contains("Version: 0.1.0-SNAPSHOT") + + where: + config << configs() } def "Simple build with commit"() { when: - setupPluginWithClasspath() + setupPluginWithClasspath(config) init() commit() tag("1.0.0") @@ -57,11 +58,14 @@ class SemverGitPluginFunctionalTest extends Specification { then: result.getOutput().contains("Version: 1.1.0-SNAPSHOT") + + where: + config << configs() } def "Prefixed tag"() { when: - setupPluginWithClasspath('v') + setupPluginWithClasspath(config) init() commit() tag("v1.0.0") @@ -69,11 +73,14 @@ class SemverGitPluginFunctionalTest extends Specification { then: result.getOutput().contains("Version: 1.0.0") + + where: + config << configs(prefix: 'v') } def "Prefixed tag with commit"() { when: - setupPluginWithClasspath('v') + setupPluginWithClasspath(config) init() commit() tag("v1.0.0") @@ -82,44 +89,246 @@ class SemverGitPluginFunctionalTest extends Specification { then: result.getOutput().contains("Version: 1.1.0-SNAPSHOT") + + where: + config << configs(prefix: 'v') + } + + def "Dirty tag with default config"() { + when: + setupPluginWithClasspath(config) + init() + commit() + tag("1.0.0") + touchBuildFile() + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 1.0.0") + + where: + config << configs() + } + + def "Dirty tag with snapshotSuffix with "() { + when: + setupPluginWithClasspath(config) + init() + commit() + tag("1.0.0") + touchBuildFile() + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 1.0.0") + + where: + config << configs(snapshotSuffix: "SNAPSHOT") + } + + def "Dirty untagged commit with default config"() { + when: + setupPluginWithClasspath(config) + init() + commit() + tag("1.0.0") + commit() + touchBuildFile() + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 1.1.0-SNAPSHOT") + + where: + config << configs() + } + + def "Dirty untagged commit with snapshotSuffix with "() { + when: + setupPluginWithClasspath(config) + init() + commit() + tag("1.0.0") + commit() + touchBuildFile() + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 1.1.0-SNAPSHOT-dirty") + + where: + config << configs(snapshotSuffix: "SNAPSHOT") + } + + def "Custom nextVersion"() { + when: + setupPluginWithClasspath(config) + init() + commit() + tag("1.0.0") + commit() + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 2.0.0-SNAPSHOT") + + where: + config << configs(nextVersion: "major") + } + + def "Custom snapshotSuffix"() { + when: + setupPluginWithClasspath(config) + init() + commit() + tag("1.0.0") + commit() + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 1.1.0-volatile") + + where: + config << configs(snapshotSuffix: "volatile") + } + + def "Custom dirtyMarker"() { + when: + setupPluginWithClasspath(config) + init() + commit() + tag("1.0.0") + commit() + touchBuildFile() + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 1.1.0-SNAPSHOT_MODIFIED") + + where: + config << configs(snapshotSuffix: "SNAPSHOT", dirtyMarker: "_MODIFIED") + } + + def "Custom gitDescribeArgs"() { + when: + setupPluginWithClasspath(config) + init() + String shortCommit = commit() + tag("1.0.0") + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 1.0.0-0-g${shortCommit}") + + where: + config << configs(gitDescribeArgs: "--long") + } + + def "semverGit {} block overrides project.ext settings"() { + when: + setupPluginWithClasspath(config) + init() + commit() + tag("new-version-1.0.0") + commit() + touchBuildFile() + BuildResult result = gradleBuild("showVersion") + + then: + result.getOutput().contains("Version: 2.0.0-new_-NEW_DIRTYsnapshot") + + where: + config = """ +plugins { + id "com.cinnober.gradle.semver-git" apply false +} + +ext.nextVersion = 'minor' +ext.snapshotSuffix = 'LEGACYSNAPSHOT' +ext.dirtyMarker = '-legacydirty' +ext.gitDescribeArgs = '--match *[0-9].abc.[0-9]*.def.[0-9]*' +ext.semverPrefix = 'legacy-version-' + +apply plugin: 'com.cinnober.gradle.semver-git' + +semverGit { + nextVersion = 'major' + snapshotSuffix = 'new_snapshot' + dirtyMarker = '-NEW_DIRTY' + gitDescribeArgs = '--match *[0-9].[0-9]*.[0-9]* --long' + prefix = 'new-version-' +} +""" } def git(String argument) { - return ("git " + argument).execute(null, projectDir) + def proc = ("git " + argument).execute(null, projectDir) + proc.waitFor() + return proc } def init() { - return git("init") + git("init") + git("add .") + + // Disable GPG signing in the test directory, otherwise user might be prompted for + // manual intervention if they have this config set to true + return git("config commit.gpgSign false") } def commit() { - return git("commit --allow-empty -m empty") + git("commit --allow-empty -m empty") + return git("rev-parse --short HEAD").text.trim() } def tag(String name) { return git("tag -a $name -m tag") } - def setupPluginWithClasspath( - String prefix = null - ) { - build.write """ + def touchBuildFile() { + build << "\n// foo\n" + } + + def setupPluginWithClasspath(String config) { + build << config + } + + List configs(Map args = [:]) { + [legacyConfig(args), extensionConfig(args)] + } + + String legacyConfig(Map args) { + return """ plugins { id "com.cinnober.gradle.semver-git" apply false } -// optionally: ext.nextVersion = "major", "minor" (default), "patch" or e.g. "3.0.0-rc2" -// optionally: ext.snapshotSuffix = "SNAPSHOT" (default) or a pattern, e.g. ".g-SNAPSHOT" -// optionally: ext.dirtyMarker = "-dirty" (default) replaces in snapshotSuffix -// optionally: ext.gitDescribeArgs = '--match *[0-9].[0-9]*.[0-9]*' (default) or other arguments for git describe. -""" - if (prefix) { - build << "ext.semverPrefix = '$prefix'" - } - build << """ + +${args.nextVersion != null ? "ext.nextVersion = '${args.nextVersion}'" : ""} +${args.snapshotSuffix != null ? "ext.snapshotSuffix = '${args.snapshotSuffix}'" : ""} +${args.dirtyMarker != null ? "ext.dirtyMarker = '${args.dirtyMarker}'" : ""} +${args.gitDescribeArgs != null ? "ext.gitDescribeArgs = '${args.gitDescribeArgs}'" : ""} +${args.prefix != null ? "ext.semverPrefix = '${args.prefix}'" : ""} + apply plugin: 'com.cinnober.gradle.semver-git' """ } + String extensionConfig(Map args) { + return """ +plugins { + id "com.cinnober.gradle.semver-git" +} + +semverGit { + ${args.nextVersion != null ? "nextVersion = '${args.nextVersion}'" : ""} + ${args.snapshotSuffix != null ? "snapshotSuffix = '${args.snapshotSuffix}'" : ""} + ${args.dirtyMarker != null ? "dirtyMarker = '${args.dirtyMarker}'" : ""} + ${args.gitDescribeArgs != null ? "gitDescribeArgs = '${args.gitDescribeArgs}'" : ""} + ${args.prefix != null ? "prefix = '${args.prefix}'" : ""} +} +""" + } + BuildResult gradleBuild(String... arguments) { if (slowIo) { sleep(1000)