diff --git a/.gitignore b/.gitignore index a391a929..2b2e8d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target project/target .idea .idea_modules +.bsp \ No newline at end of file diff --git a/README.md b/README.md index 77d50abc..b9e00276 100644 --- a/README.md +++ b/README.md @@ -109,21 +109,31 @@ A cross release behaves analogous to using the `+` command: In the section *Customizing the release process* we take a look at how to define a `ReleaseStep` to participate in a cross build. -### Convenient versioning +### Versioning Strategies -As of version 0.8, *sbt-release* comes with some strategies for computing the next snapshot version via the `releaseVersionBump` setting. These strategies are defined in `sbtrelease.Version.Bump`. By default, the `Next` strategy is used: +As of version 0.8, *sbt-release* comes with several strategies for computing the next snapshot version via the `releaseVersionBump` setting. These strategies are defined in `sbtrelease.Version.Bump`. By default, the `Next` strategy is used: * `Major`: always bumps the *major* part of the version * `Minor`: always bumps the *minor* part of the version * `Bugfix`: always bumps the *bugfix* part of the version * `Nano`: always bumps the *nano* part of the version - * `Next`: bumps the last version part (e.g. `0.17` -> `0.18`, `0.11.7` -> `0.11.8`, `3.22.3.4.91` -> `3.22.3.4.92`) + * `Next` (**default**): bumps the last version part, including the qualifier (e.g. `0.17` -> `0.18`, `0.11.7` -> `0.11.8`, `3.22.3.4.91` -> `3.22.3.4.92`, `1.0.0-RC1` -> `1.0.0-RC2`) + * `NextStable`: bumps exactly like `Next` except that any prerelease qualifier is excluded (e.g. `1.0.0-RC1` -> `1.0.0`) -Example: +Users can set their preferred versioning strategy in `build.sbt` as follows: +```sbt +releaseVersionBump := sbtrelease.Version.Bump.Major +``` + +### Default Versioning + +The default settings make use of the helper class [`Version`](https://github.com/sbt/sbt-release/blob/master/src/main/scala/Version.scala) that ships with *sbt-release*. + +`releaseVersion`: The current version in version.sbt, without the "-SNAPSHOT" ending. So, if `version.sbt` contains `1.0.0-SNAPSHOT`, the release version will be set to `1.0.0`. - releaseVersionBump := sbtrelease.Version.Bump.Major +`releaseNextVersion`: The "bumped" version according to the versioning strategy (explained above), including the `-SNAPSHOT` ending. So, if `releaseVersion` is `1.0.0`, `releaseNextVersion` will be `1.0.1-SNAPSHOT`. -### Custom versioning +### Custom Versioning *sbt-release* comes with two settings for deriving the release version and the next development version from a given version. @@ -132,20 +142,8 @@ These derived versions are used for the suggestions/defaults in the prompt and f Let's take a look at the types: ```scala -val releaseVersion : SettingKey[String => String] -val releaseNextVersion : SettingKey[String => String] -``` - -The default settings make use of the helper class [`Version`](https://github.com/sbt/sbt-release/blob/master/src/main/scala/Version.scala) that ships with *sbt-release*. - -```scala -// strip the qualifier off the input version, eg. 1.2.1-SNAPSHOT -> 1.2.1 -releaseVersion := { ver => Version(ver).map(_.withoutQualifier.string).getOrElse(versionFormatError(ver)) } - -// bump the version and append '-SNAPSHOT', eg. 1.2.1 -> 1.3.0-SNAPSHOT -releaseNextVersion := { - ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.string).getOrElse(versionFormatError(ver)) -}, +val releaseVersion : TaskKey[String => String] +val releaseNextVersion : TaskKey[String => String] ``` If you want to customize the versioning, keep the following in mind: diff --git a/src/main/scala/ReleasePlugin.scala b/src/main/scala/ReleasePlugin.scala index 2d798695..c077d9dc 100644 --- a/src/main/scala/ReleasePlugin.scala +++ b/src/main/scala/ReleasePlugin.scala @@ -1,11 +1,11 @@ package sbtrelease import java.io.Serializable - -import sbt._ -import Keys._ -import sbt.complete.DefaultParsers._ +import sbt.* +import Keys.* +import sbt.complete.DefaultParsers.* import sbt.complete.Parser +import sbtrelease.Version.Bump object ReleasePlugin extends AutoPlugin { @@ -73,7 +73,7 @@ object ReleasePlugin extends AutoPlugin { withStreams(extracted.structure, st) { str => val nv = nodeView(st, str, key :: Nil) val (newS, result) = runTask(task, st, str, extracted.structure.index.triggers, config)(nv) - (newS, processResult(result, newS.log)) + (newS, processResult2(result)) }._1 } @@ -222,11 +222,23 @@ object ReleasePlugin extends AutoPlugin { val snapshots = moduleIds.filter(m => m.isChanging || m.revision.endsWith("-SNAPSHOT")) snapshots }, - - releaseVersion := { ver => Version(ver).map(_.withoutQualifier.string).getOrElse(versionFormatError(ver)) }, + releaseVersion := { rawVersion => + Version(rawVersion).map { version => + releaseVersionBump.value match { + case Bump.Next => + if (version.isSnapshot) { + version.withoutSnapshot.unapply + } else { + expectedSnapshotVersionError(rawVersion) + } + case _ => version.withoutQualifier.unapply + } + } + .getOrElse(versionFormatError(rawVersion)) + }, releaseVersionBump := Version.Bump.default, releaseNextVersion := { - ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.string).getOrElse(versionFormatError(ver)) + ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.unapply).getOrElse(versionFormatError(ver)) }, releaseUseGlobalVersion := true, releaseCrossBuild := false, diff --git a/src/main/scala/Version.scala b/src/main/scala/Version.scala index c64482d4..05061aec 100644 --- a/src/main/scala/Version.scala +++ b/src/main/scala/Version.scala @@ -1,6 +1,7 @@ package sbtrelease -import util.control.Exception._ +import scala.util.matching.Regex +import util.control.Exception.* object Version { sealed trait Bump { @@ -8,17 +9,54 @@ object Version { } object Bump { - case object Major extends Bump { def bump = _.bumpMajor } - case object Minor extends Bump { def bump = _.bumpMinor } - case object Bugfix extends Bump { def bump = _.bumpBugfix } - case object Nano extends Bump { def bump = _.bumpNano } - case object Next extends Bump { def bump = _.bump } - val default = Next + /** + * Strategy to always bump the major version by default. Ex. 1.0.0 would be bumped to 2.0.0 + */ + case object Major extends Bump { def bump: Version => Version = _.bumpMajor } + /** + * Strategy to always bump the minor version by default. Ex. 1.0.0 would be bumped to 1.1.0 + */ + case object Minor extends Bump { def bump: Version => Version = _.bumpMinor } + /** + * Strategy to always bump the bugfix version by default. Ex. 1.0.0 would be bumped to 1.0.1 + */ + case object Bugfix extends Bump { def bump: Version => Version = _.bumpBugfix } + /** + * Strategy to always bump the nano version by default. Ex. 1.0.0.0 would be bumped to 1.0.0.1 + */ + case object Nano extends Bump { def bump: Version => Version = _.bumpNano } + + + /** + * Strategy to always increment to the next version from smallest to greatest, including prerelease versions + * Ex: + * Major: 1 becomes 2 + * Minor: 1.0 becomes 1.1 + * Bugfix: 1.0.0 becomes 1.0.1 + * Nano: 1.0.0.0 becomes 1.0.0.1 + * Qualifier with version number: 1.0-RC1 becomes 1.0-RC2 + * Qualifier without version number: 1.0-alpha becomes 1.0 + */ + case object Next extends Bump { def bump: Version => Version = _.bumpNext } + + /** + * Strategy to always increment to the next version from smallest to greatest, excluding prerelease versions + * Ex: + * Major: 1 becomes 2 + * Minor: 1.0 becomes 1.1 + * Bugfix: 1.0.0 becomes 1.0.1 + * Nano: 1.0.0.0 becomes 1.0.0.1 + * Qualifier with version number: 1.0-RC1 becomes 1.0 + * Qualifier without version number: 1.0-alpha becomes 1.0 + */ + case object NextStable extends Bump { def bump: Version => Version = _.bumpNextStable } + + val default: Bump = Next } - val VersionR = """([0-9]+)((?:\.[0-9]+)+)?([\.\-0-9a-zA-Z]*)?""".r - val PreReleaseQualifierR = """[\.-](?i:rc|m|alpha|beta)[\.-]?[0-9]*""".r + val VersionR: Regex = """([0-9]+)((?:\.[0-9]+)+)?([\.\-0-9a-zA-Z]*)?""".r + val PreReleaseQualifierR: Regex = """[\.-](?i:rc|m|alpha|beta)[\.-]?[0-9]*""".r def apply(s: String): Option[Version] = { allCatch opt { @@ -34,24 +72,52 @@ object Version { } case class Version(major: Int, subversions: Seq[Int], qualifier: Option[String]) { - def bump = { - val maybeBumpedPrerelease = qualifier.collect { - case Version.PreReleaseQualifierR() => withoutQualifier + + @deprecated("Use .bumpNext or .bumpNextStable instead") + def bump: Version = bumpNext + + def bumpNext: Version = { + val bumpedPrereleaseVersionOpt = qualifier.collect { + case rawQualifier @ Version.PreReleaseQualifierR() => + val qualifierEndsWithNumberRegex = """[0-9]*$""".r + + val opt = for { + versionNumberQualifierStr <- qualifierEndsWithNumberRegex.findFirstIn(rawQualifier) + versionNumber <- Try(versionNumberQualifierStr.toInt) + .toRight(new Exception(s"Version number not parseable to a number. Version number received: $versionNumberQualifierStr")) + .toOption + newVersionNumber = versionNumber + 1 + newQualifier = rawQualifier.replaceFirst(versionNumberQualifierStr, newVersionNumber.toString) + } yield Version(major, subversions, Some(newQualifier)) + + opt.getOrElse(this.withoutQualifier) } - def maybeBumpedLastSubversion = bumpSubversionOpt(subversions.length-1) + + bumpNextGeneric(bumpedPrereleaseVersionOpt) + } + private def bumpNextGeneric(bumpedPrereleaseVersionOpt: Option[Version]): Version = { + def maybeBumpedLastSubversion = bumpSubversionOpt(subversions.length - 1) + def bumpedMajor = copy(major = major + 1) - maybeBumpedPrerelease + bumpedPrereleaseVersionOpt .orElse(maybeBumpedLastSubversion) .getOrElse(bumpedMajor) } - def bumpMajor = copy(major = major + 1, subversions = Seq.fill(subversions.length)(0)) - def bumpMinor = maybeBumpSubversion(0) - def bumpBugfix = maybeBumpSubversion(1) - def bumpNano = maybeBumpSubversion(2) + def bumpNextStable: Version = { + val bumpedPrereleaseVersionOpt = qualifier.collect { + case Version.PreReleaseQualifierR() => withoutQualifier + } + bumpNextGeneric(bumpedPrereleaseVersionOpt) + } + + def bumpMajor: Version = copy(major = major + 1, subversions = Seq.fill(subversions.length)(0)) + def bumpMinor: Version = maybeBumpSubversion(0) + def bumpBugfix: Version = maybeBumpSubversion(1) + def bumpNano: Version = maybeBumpSubversion(2) - def maybeBumpSubversion(idx: Int) = bumpSubversionOpt(idx) getOrElse this + def maybeBumpSubversion(idx: Int): Version = bumpSubversionOpt(idx) getOrElse this private def bumpSubversionOpt(idx: Int) = { val bumped = subversions.drop(idx) @@ -64,10 +130,30 @@ case class Version(major: Int, subversions: Seq[Int], qualifier: Option[String]) def bump(bumpType: Version.Bump): Version = bumpType.bump(this) - def withoutQualifier = copy(qualifier = None) - def asSnapshot = copy(qualifier = Some("-SNAPSHOT")) + def withoutQualifier: Version = copy(qualifier = None) + def asSnapshot: Version = copy(qualifier = qualifier.map { qualifierStr => + s"$qualifierStr-SNAPSHOT" + }.orElse(Some("-SNAPSHOT"))) + + def isSnapshot: Boolean = qualifier.exists { qualifierStr => + val snapshotRegex = """(^.*)-SNAPSHOT$""".r + qualifierStr.matches(snapshotRegex.regex) + } + + def withoutSnapshot: Version = copy(qualifier = qualifier.flatMap { qualifierStr => + val snapshotRegex = """-SNAPSHOT""".r + val newQualifier = snapshotRegex.replaceFirstIn(qualifierStr, "") + if (newQualifier == qualifierStr) { + None + } else { + Some(newQualifier) + } + }) + + @deprecated("Use .unapply instead") + def string: String = unapply - def string = "" + major + mkString(subversions) + qualifier.getOrElse("") + def unapply: String = "" + major + mkString(subversions) + qualifier.getOrElse("") private def mkString(parts: Seq[Int]) = parts.map("."+_).mkString } diff --git a/src/main/scala/package.scala b/src/main/scala/package.scala index d9d65699..01216146 100644 --- a/src/main/scala/package.scala +++ b/src/main/scala/package.scala @@ -2,4 +2,6 @@ package object sbtrelease { type Versions = (String, String) def versionFormatError(version: String) = sys.error(s"Version [$version] format is not compatible with " + Version.VersionR.pattern.toString) + + def expectedSnapshotVersionError(version: String) = sys.error(s"Expected snapshot version. Received: $version") } diff --git a/src/sbt-test/sbt-release/with-defaults/build.sbt b/src/sbt-test/sbt-release/with-defaults/build.sbt index 5084d25a..78658ad8 100644 --- a/src/sbt-test/sbt-release/with-defaults/build.sbt +++ b/src/sbt-test/sbt-release/with-defaults/build.sbt @@ -1,3 +1,4 @@ +import sbt.complete.DefaultParsers._ import sbtrelease.ReleaseStateTransformations._ releaseVersionFile := file("version.sbt") @@ -13,3 +14,12 @@ releaseProcess := Seq( setNextVersion, commitNextVersion ) + +val checkContentsOfVersionSbt = inputKey[Unit]("Check that the contents of version.sbt is as expected") +val parser = Space ~> StringBasic + +checkContentsOfVersionSbt := { + val expected = parser.parsed + val versionFile = ((baseDirectory).value) / "version.sbt" + assert(IO.read(versionFile).contains(expected), s"does not contains ${expected} in ${versionFile}") +} diff --git a/src/sbt-test/sbt-release/with-defaults/test b/src/sbt-test/sbt-release/with-defaults/test index 07a0c8f9..4dc64d08 100644 --- a/src/sbt-test/sbt-release/with-defaults/test +++ b/src/sbt-test/sbt-release/with-defaults/test @@ -1,11 +1,41 @@ -$ exec git init . +# Test Suite Preparation + $ exec git init . + > update + $ exec git add . + $ exec git commit -m init + > reload -> update +# SCENARIO: When no release versions are specified in the release command + # TEST: Should fail to release if "with-defaults" is not specified + -> release -$ exec git add . -$ exec git commit -m init + # TEST: Should succeed if "with-defaults" is specified + > release with-defaults -> reload +# SCENARIO: When default bumping strategy is used + # Test Scenario Preparation + > 'release release-version 0.9.9 next-version 1.0.0-RC1-SNAPSHOT' + > reload + > checkContentsOfVersionSbt 1.0.0-RC1-SNAPSHOT + + # TEST: Snapshot version should be correctly set + > release with-defaults + > checkContentsOfVersionSbt 1.0.0-RC2-SNAPSHOT + + # TEST: Release version should be correctly set + $ exec git reset --hard HEAD~1 + > reload + > checkContentsOfVersionSbt 1.0.0-RC1 + +# SCENARIO: When NextStable bumping strategy is used + # TEST: Snapshot version should be correctly set + $ exec git reset --hard HEAD~1 + > set releaseVersionBump := sbtrelease.Version.Bump.NextStable + > release with-defaults + > checkContentsOfVersionSbt 1.0.1-SNAPSHOT + + # TEST: Release version should be correctly set + $ exec git reset --hard HEAD~1 + > reload + > checkContentsOfVersionSbt 1.0.0 --> release -> release with-defaults diff --git a/src/test/scala/VersionSpec.scala b/src/test/scala/VersionSpec.scala index a0f12f32..8a57cac1 100644 --- a/src/test/scala/VersionSpec.scala +++ b/src/test/scala/VersionSpec.scala @@ -1,5 +1,6 @@ package sbtrelease +import org.specs2.matcher.MatchResult import org.specs2.mutable.Specification object VersionSpec extends Specification { @@ -8,47 +9,82 @@ object VersionSpec extends Specification { case Some(parsed) => parsed case None => sys.error("Can't parse version " + v) } + "Next Version bumping" should { + def testBumpNext(input: String, expectedOutput: String): MatchResult[Any] = version(input).bumpNext.unapply must_== expectedOutput - "Version bumping" should { - def bump(v: String) = version(v).bump.string + def testBumpNextStable(input: String, expectedOutput: String): MatchResult[Any] = version(input).bumpNextStable.unapply must_== expectedOutput + + def testBothBumpNextStrategies(input: String, expectedOutput: String): MatchResult[Any] = { + testBumpNext(input, expectedOutput) + testBumpNextStable(input, expectedOutput) + } "bump the major version if there's only a major version" in { - bump("1") must_== "2" + testBothBumpNextStrategies("1", "2") } "bump the minor version if there's only a minor version" in { - bump("1.2") must_== "1.3" + testBothBumpNextStrategies("1.2", "1.3") } "bump the bugfix version if there's only a bugfix version" in { - bump("1.2.3") must_== "1.2.4" + testBothBumpNextStrategies("1.2.3", "1.2.4") } "bump the nano version if there's only a nano version" in { - bump("1.2.3.4") must_== "1.2.3.5" - } - "drop the qualifier if it's a pre release" in { - bump("1-rc1") must_== "1" - bump("1.2-rc1") must_== "1.2" - bump("1.2.3-rc1") must_== "1.2.3" - - bump("1-rc") must_== "1" - bump("1-RC1") must_== "1" - bump("1-M1") must_== "1" - bump("1-rc-1") must_== "1" - bump("1-rc.1") must_== "1" - bump("1-beta") must_== "1" - bump("1-beta-1") must_== "1" - bump("1-beta.1") must_== "1" - bump("1-alpha") must_== "1" - } - "not drop the qualifier if it's not a pre release" in { - bump("1.2.3-Final") must_== "1.2.4-Final" - } - "not drop the post-nano qualifier if it's not a pre release" in { - bump("1.2.3.4-Final") must_== "1.2.3.5-Final" + testBothBumpNextStrategies("1.2.3.4", "1.2.3.5") + } + "drop the qualifier if it's a pre release and there is no version number at the end" in { + testBothBumpNextStrategies("1-rc", "1") + testBothBumpNextStrategies("1.0-rc", "1.0") + testBothBumpNextStrategies("1.0.0-rc", "1.0.0") + testBothBumpNextStrategies("1.0.0.0-rc", "1.0.0.0") + testBothBumpNextStrategies("1-beta", "1") + testBothBumpNextStrategies("1-alpha", "1") + } + "when the qualifier includes a pre release with a version number at the end" >> { + "and Next is the bumping strategy" >> { + "should bump the qualifier" in { + testBumpNext("1-rc1", "1-rc2") + testBumpNext("1.2-rc1", "1.2-rc2") + testBumpNext("1.2.3-rc1", "1.2.3-rc2") + testBumpNext("1-RC1", "1-RC2") + testBumpNext("1-M1", "1-M2") + testBumpNext("1-rc-1", "1-rc-2") + testBumpNext("1-rc.1", "1-rc.2") + testBumpNext("1-beta-1", "1-beta-2") + testBumpNext("1-beta.1", "1-beta.2") + } + } + "and NextStable is the bumping strategy" >> { + "should remove the qualifier" in { + testBumpNextStable("1-rc1", "1") + testBumpNextStable("1.2-rc1", "1.2") + testBumpNextStable("1.2.3-rc1", "1.2.3") + testBumpNextStable("1-RC1", "1") + testBumpNextStable("1-M1", "1") + testBumpNextStable("1-rc-1", "1") + testBumpNextStable("1-rc.1", "1") + testBumpNextStable("1-beta-1", "1") + testBumpNextStable("1-beta.1", "1") + } + } + } + "never drop the qualifier if it's a final release" >> { + "when release is major" in { + testBothBumpNextStrategies("1-Final", "2-Final") + } + "when release is minor" in { + testBothBumpNextStrategies("1.2-Final", "1.3-Final") + } + "when release is subversion" in { + testBothBumpNextStrategies("1.2.3-Final", "1.2.4-Final") + } + "when release is nano" in { + testBothBumpNextStrategies("1.2.3.4-Final", "1.2.3.5-Final") + } } } "Major Version bumping" should { - def bumpMajor(v: String) = version(v).bumpMajor.string + def bumpMajor(v: String) = version(v).bumpMajor.unapply "bump the major version and reset other versions" in { bumpMajor("1.2.3.4.5") must_== "2.0.0.0.0" @@ -59,7 +95,7 @@ object VersionSpec extends Specification { } "Minor Version bumping" should { - def bumpMinor(v: String) = version(v).bumpMinor.string + def bumpMinor(v: String) = version(v).bumpMinor.unapply "bump the minor version" in { bumpMinor("1.2") must_== "1.3" @@ -76,7 +112,7 @@ object VersionSpec extends Specification { } "Subversion bumping" should { - def bumpSubversion(v: String)(i: Int) = version(v).maybeBumpSubversion(i).string + def bumpSubversion(v: String)(i: Int) = version(v).maybeBumpSubversion(i).unapply "bump the subversion" in { bumpSubversion("1.2")(0) must_== "1.3" @@ -91,5 +127,32 @@ object VersionSpec extends Specification { bumpSubversion("1.2.3.4.5-alpha")(2) must_== "1.2.3.5.0-alpha" } } - + "#isSnapshot" should { + "return true when -SNAPSHOT is appended with another qualifier" in { + version("1.0.0-RC1-SNAPSHOT").isSnapshot must_== true + } + "return false when -SNAPSHOT is not appended but another qualifier exists" in { + version("1.0.0-RC1").isSnapshot must_== false + } + "return false when neither -SNAPSHOT nor qualifier are appended" in { + version("1.0.0").isSnapshot must_== false + } + } + "#asSnapshot" should { + def snapshot(v: String) = version(v).asSnapshot.unapply + "include qualifier if it exists" in { + snapshot("1.0.0-RC1") must_== "1.0.0-RC1-SNAPSHOT" + } + "have no qualifier if none exists" in { + snapshot("1.0.0") must_== "1.0.0-SNAPSHOT" + } + } + "#withoutSnapshot" should { + "remove the snapshot normally" in { + version("1.0.0-SNAPSHOT").withoutSnapshot.unapply must_== "1.0.0" + } + "remove the snapshot without removing the qualifier" in { + version("1.0.0-RC1-SNAPSHOT").withoutSnapshot.unapply must_== "1.0.0-RC1" + } + } }