From dc17626cfa1cc25af2901efcecc8343be4035273 Mon Sep 17 00:00:00 2001 From: Jeff May Date: Mon, 9 Mar 2020 12:59:54 -0700 Subject: [PATCH] Migrate latest rally-versioning changes from 1.9.0 to open-source --- README.md | 21 ++-- build.sbt | 16 +-- project/build.properties | 2 +- project/plugins.sbt | 2 +- .../rallyhealth/sbt/semver/SemVerPlugin.scala | 4 +- .../semver/level/rule/VersionDiffRule.scala | 2 +- .../sbt/versioning/GitCommit.scala | 26 +++++ .../sbt/versioning/GitDriver.scala | 110 +++++++++++++++++- .../rallyhealth/sbt/versioning/GitState.scala | 9 +- .../sbt/versioning/GitVersioningPlugin.scala | 48 ++++++-- .../LowerBoundedSemanticVersion.scala | 8 +- .../sbt/versioning/SemanticVersion.scala | 16 ++- src/sbt-test-disabled/semver/log/build.sbt | 4 - src/sbt-test-disabled/semver/log/test | 2 +- src/sbt-test/versioning/Readme.md | 92 +++++++++++++++ src/sbt-test/versioning/branch/.gitignore | 24 ++++ src/sbt-test/versioning/branch/build.sbt | 38 ++++++ .../versioning/branch/project/plugins.sbt | 9 ++ src/sbt-test/versioning/branch/test | 42 +++++++ .../versioning/generateVersion/.gitignore | 24 ++++ .../versioning/generateVersion/build.sbt | 54 +++++++++ .../generateVersion/project/plugins.sbt | 9 ++ src/sbt-test/versioning/generateVersion/test | 27 +++++ src/sbt-test/versioning/hotfix/.gitignore | 24 ++++ src/sbt-test/versioning/hotfix/build.sbt | 43 +++++++ .../versioning/hotfix/project/plugins.sbt | 9 ++ src/sbt-test/versioning/hotfix/test | 50 ++++++++ src/sbt-test/versioning/lowerBound/test | 29 +++-- src/sbt-test/versioning/reflog/.gitignore | 24 ++++ src/sbt-test/versioning/reflog/README.md | 78 +++++++++++++ src/sbt-test/versioning/reflog/build.sbt | 43 +++++++ .../versioning/reflog/project/plugins.sbt | 9 ++ src/sbt-test/versioning/reflog/test | 25 ++++ src/sbt-test/versioning/release/test | 9 +- .../resources/scriptedOutput-example1.txt | 2 +- .../rallyhealth/sbt/util/NullSbtLogger.scala | 2 +- .../LowerBoundedSemanticVersionSpec.scala | 24 ++-- .../ReleaseableSemanticVersionSpec.scala | 76 ++++++++++++ .../versioning/SemVerReleaseTypeSpec.scala | 79 +++---------- .../sbt/versioning/SemanticVersionSpec.scala | 10 +- 40 files changed, 978 insertions(+), 147 deletions(-) create mode 100644 src/sbt-test/versioning/Readme.md create mode 100644 src/sbt-test/versioning/branch/.gitignore create mode 100644 src/sbt-test/versioning/branch/build.sbt create mode 100644 src/sbt-test/versioning/branch/project/plugins.sbt create mode 100644 src/sbt-test/versioning/branch/test create mode 100644 src/sbt-test/versioning/generateVersion/.gitignore create mode 100644 src/sbt-test/versioning/generateVersion/build.sbt create mode 100644 src/sbt-test/versioning/generateVersion/project/plugins.sbt create mode 100644 src/sbt-test/versioning/generateVersion/test create mode 100644 src/sbt-test/versioning/hotfix/.gitignore create mode 100644 src/sbt-test/versioning/hotfix/build.sbt create mode 100644 src/sbt-test/versioning/hotfix/project/plugins.sbt create mode 100644 src/sbt-test/versioning/hotfix/test create mode 100644 src/sbt-test/versioning/reflog/.gitignore create mode 100644 src/sbt-test/versioning/reflog/README.md create mode 100644 src/sbt-test/versioning/reflog/build.sbt create mode 100644 src/sbt-test/versioning/reflog/project/plugins.sbt create mode 100644 src/sbt-test/versioning/reflog/test create mode 100644 src/test/scala/com/rallyhealth/sbt/versioning/ReleaseableSemanticVersionSpec.scala diff --git a/README.md b/README.md index cba2b02..474da61 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ There are two sbt plugins in this one plugin library: # Compatibility -Tested and supported for sbt versions: 0.13.17, 1.1.6, and 1.2.1 +Tested and supported for sbt versions: 0.13.18 and 1.2.8. We don't currently support SBT 1.3.x because [there isn't a +version of MiMa 0.3.0 built for SBT 1.3.x, only for 1.2.x and 0.13.x](https://github.com/lightbend/mima#usage). # Install @@ -106,19 +107,19 @@ This is useful for preparing major releases with breaking changes (esp. when com The release arg bumps the version up by a major, minor, or patch increment. ``` -sbt -Drelease=major ... +sbt -Drelease= ... ``` The release arg alters the value of `versionFromGit`, but is still bounded by `gitVersioningSnapshotLowerBound`. For example: | versionFromGit | -Drelease | gitVersioningSnapshotLowerBound | Final Version | -| -------------- | --------- | --------------------------------- | ------------- | -| 1.0.0 | patch | | 1.0.1 | -| 1.0.0 | minor | | 1.1.0 | -| 1.0.0 | major | | 2.0.0 | -| 1.0.0 | patch | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT | -| 1.0.0 | minor | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT | -| 1.0.0 | major | 2.0.0 | 2.0.0 | +| -------------- | --------- | ------------------------------- | ------------- | +| 1.0.0 | patch | | 1.0.1 | +| 1.0.0 | minor | | 1.1.0 | +| 1.0.0 | major | | 2.0.0 | +| 1.0.0 | patch | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT | +| 1.0.0 | minor | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT | +| 1.0.0 | major | 2.0.0 | 2.0.0 | #### Example With most recent git tag at `v1.4.2` and a `gitVersioningSnapshotLowerBound` setting of: @@ -136,7 +137,7 @@ $ sbt ### Notes * The patch version is incremented if there are commits, dirty or not. But it is not incremented if there are no -commits. +commits. (I'm not clear on why this is, but it is legacy behavior.) * The hash does **not** have a 'g' prefix like the output of `git describe` * A build will be flagged as not clean (and will have a `-dirty-SNAPSHOT` identifier applied) if `git status --porcelain` returns a non-empty result. diff --git a/build.sbt b/build.sbt index 1017f55..ae0d745 100644 --- a/build.sbt +++ b/build.sbt @@ -4,9 +4,6 @@ name := "sbt-git-versioning" organizationName := "Rally Health" organization := "com.rallyhealth.sbt" -// enable after SemVerPlugin supports sbt-plugins -//enablePlugins(SemVerPlugin) -semVerLimit := "1.0.999" licenses := Seq("MIT" -> url("http://opensource.org/licenses/MIT")) bintrayOrganization := Some("rallyhealth") @@ -26,11 +23,13 @@ scalacOptions ++= { Seq("-Xfatal-warnings", linting) } -// to default to sbt 1.0 -// sbtVersion in pluginCrossBuild := "1.1.6" -// scalaVersion := "2.12.6" +// Uncomment to default to sbt 0.13 for debugging +// sbtVersion in pluginCrossBuild := "0.13.18" +// scalaVersion := "2.10.6" -crossSbtVersions := List("0.13.17", "1.2.1") +// We don't use SBT 1.3.x because there isn't a version of MiMa 0.3.0 built for SBT 1.3.x, only for 1.2.x and 0.13.x +// https://github.com/lightbend/mima#usage +crossSbtVersions := List("0.13.18", "1.2.8") publishMavenStyle := false @@ -43,6 +42,9 @@ libraryDependencies ++= Seq( // you need to enable the plugin HERE to depend on code in the plugin JAR. you can't add it as part of // libraryDependencies nor put it in plugins.sbt +// We use MiMa 0.3.0 because it is the only version that exists for both SBT 1.2.x and 0.13.x +// Also MiMa 0.6.x has some source incompatible changes, so we'd have to fork the source to support 0.6.x and 0.3.x +// https://github.com/lightbend/mima#usage addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.3.0") addSbtPlugin("com.dwijnand" % "sbt-compat" % "1.2.6") diff --git a/project/build.properties b/project/build.properties index 5620cc5..c0bab04 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.1 +sbt.version=1.2.8 diff --git a/project/plugins.sbt b/project/plugins.sbt index fd2df6f..840c810 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,5 +2,5 @@ resolvers += Resolver.url( "Rally Plugin Releases", url("https://dl.bintray.com/rallyhealth/sbt-plugins"))(Resolver.ivyStylePatterns) -addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % "1.1.0") +addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % "1.3.0") addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") diff --git a/src/main/scala/com/rallyhealth/sbt/semver/SemVerPlugin.scala b/src/main/scala/com/rallyhealth/sbt/semver/SemVerPlugin.scala index ae245ea..4700eba 100644 --- a/src/main/scala/com/rallyhealth/sbt/semver/SemVerPlugin.scala +++ b/src/main/scala/com/rallyhealth/sbt/semver/SemVerPlugin.scala @@ -3,6 +3,7 @@ package com.rallyhealth.sbt.semver import com.rallyhealth.sbt.semver.level._ import com.rallyhealth.sbt.semver.level.rule._ import com.rallyhealth.sbt.semver.mima._ +import com.rallyhealth.sbt.versioning.GitVersioningPlugin.autoImport.semanticVersion import com.rallyhealth.sbt.versioning.{ReleaseVersion, SemVerReleaseType, SemanticVersion} import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport._ import com.typesafe.tools.mima.plugin.{MimaKeys, MimaPlugin} @@ -95,8 +96,7 @@ object SemVerPlugin extends AutoPlugin { semVerEnforceAfterVersion := None, - semVerVersion := SemanticVersion.fromString(version.value).getOrElse( - throw new IllegalArgumentException(s"version=${version.value} is not a valid SemVer")), + semVerVersion := semanticVersion.value, semVerLimit := "deprecated" ) diff --git a/src/main/scala/com/rallyhealth/sbt/semver/level/rule/VersionDiffRule.scala b/src/main/scala/com/rallyhealth/sbt/semver/level/rule/VersionDiffRule.scala index ed0f1d8..2a6a800 100644 --- a/src/main/scala/com/rallyhealth/sbt/semver/level/rule/VersionDiffRule.scala +++ b/src/main/scala/com/rallyhealth/sbt/semver/level/rule/VersionDiffRule.scala @@ -43,7 +43,7 @@ class NormalVersionBump( case class MinorOkayForSnapshot(version: SemanticVersion) extends SemVerEnforcementLevel( releaseType = Minor, explanation = "SNAPSHOT rules are relaxed for convenience and may include up to minor changes. " + - "This is not part of the SemVer spec, but RallyVersioning can't do this automatically " + + "This is not part of the SemVer spec, but GitVersioningPlugin can't do this automatically " + "without requiring you to manually bump semVerLimit every time you add a method. " + "Minor version bumps are enforced only at release time." ) diff --git a/src/main/scala/com/rallyhealth/sbt/versioning/GitCommit.scala b/src/main/scala/com/rallyhealth/sbt/versioning/GitCommit.scala index 18fa0d9..10d74ba 100644 --- a/src/main/scala/com/rallyhealth/sbt/versioning/GitCommit.scala +++ b/src/main/scala/com/rallyhealth/sbt/versioning/GitCommit.scala @@ -24,6 +24,32 @@ object GitCommit { def apply(fullHash: String, abbreviatedHashLength: Int, tags: Seq[String]): GitCommit = GitCommit(fullHash, fullHash.take(abbreviatedHashLength), tags) + /** + * Converts a single line from a "git for-each-ref" command into a [[GitCommit]]. + * + * @param logOutput Output from "git for-each-ref --sort=-v:refname refs/tags", looks like + * "5ca402250fd63e6ac3a9b51d457b89c092195098 commit refs/tags/v0.0.2" + * @param abbreviatedHashLength The length of abbreviated hashes. This is must be determined from git, and is used + * when outputting the hash. + */ + def fromGitRef(logOutput: String, abbreviatedHashLength: Int): GitCommit = { + val hash = { + val pattern = ("^(" + HashSemVerIdentifier.regex + ")").r + pattern.findFirstIn(logOutput).getOrElse( + throw new IllegalArgumentException("no hash prefix in git log: " + logOutput)) + } + + val tags = { + val pattern = "refs/tags/(.+)$".r + pattern.findAllMatchIn(logOutput).toSeq + .map(_.group(1).trim) // get only the version, not the whole matching string + .filter(_.nonEmpty) // drop any blanks + .sorted.reverse + } + + GitCommit(hash, abbreviatedHashLength, tags) + } + /** * Converts a single line from a "git log" command into a [[GitCommit]]. * diff --git a/src/main/scala/com/rallyhealth/sbt/versioning/GitDriver.scala b/src/main/scala/com/rallyhealth/sbt/versioning/GitDriver.scala index 0176333..b87d075 100644 --- a/src/main/scala/com/rallyhealth/sbt/versioning/GitDriver.scala +++ b/src/main/scala/com/rallyhealth/sbt/versioning/GitDriver.scala @@ -2,6 +2,7 @@ package com.rallyhealth.sbt.versioning import java.io.File +import sbt.util._ import scala.sys.process._ /** @@ -71,9 +72,37 @@ trait GitDriver { class GitDriverImpl(dir: File) extends GitDriver { require(isGitRepo(dir), "Must be in a git repository") + require(isGitCompatible, "Must be git version 2.X.X or greater") private class GitException(msg: String) extends Exception(msg) + // Validate that the git version is over 2.X.X + protected def isGitCompatible: Boolean = { + val outputLogger = new BufferingProcessLogger + val exitCode: Int = Process(s"""git --version""", dir) ! outputLogger + // git --version returns: git version x.y.z + val gitSemver = """git version (\d+)\.(\d+)\.(\d+).*""".r + exitCode match { + case 0 => + val gitVersion = outputLogger.stdout.mkString("").trim.toLowerCase + gitVersion match { + case gitSemver(major, minor, patch) => + major.toInt > 1 + case _ => + throw new GitException( + s"""Version output was not of the form 'git version x.y.z' + |version was '${gitVersion}'""".stripMargin) + } + case unexpected => + throw new GitException( + s"""Unexpected git exit status: $unexpected + |stderr: + |${outputLogger.stderr.mkString("\n")} + |stdout: + |${outputLogger.stdout.mkString("\n")}""".stripMargin + ) + } + } private def isGitRepo(dir: File): Boolean = { // this does NOT use runCommand() because that uses this method to check if the directory is a git directory val outputLogger = new BufferingProcessLogger @@ -89,7 +118,7 @@ class GitDriverImpl(dir: File) extends GitDriver { |${outputLogger.stderr.mkString("\n")} |stdout: |${outputLogger.stdout.mkString("\n")}""".stripMargin - ) + ) } } @@ -98,8 +127,18 @@ class GitDriverImpl(dir: File) extends GitDriver { case Some(headCommit) => - // we only care about the RELEASE commits - val releases: Seq[(GitCommit, ReleaseVersion)] = gitLog("").collect { case gc @ ReleaseVersion(rv) => (gc, rv) } + // we only care about the RELEASE commits so let's get them in order from reflog + val releaseRefs: Seq[(GitCommit, ReleaseVersion)] = gitForEachRef("").collect { case gc @ ReleaseVersion(rv) => (gc, rv) } + + // We only care about the current release and previous release so let's take the top two. + // Then we want to find out what which git log commit is associated to the reflog sha. + // Note: gitForEachRef will return return reference shas and the tags associated with them. + // the reference shas are not always the same as the commit shas associated with git log. + // That is why we have to run the command here to find the correct sha + // Note: do not move git log into gitForEachRef as on long revisions it will take a LOT of time + val releases: Seq[(GitCommit, ReleaseVersion)] = releaseRefs.take(2).map(tp => + (gitLog(s"${tp._1.fullHash} --max-count=1").head, tp._2) + ) val maybeCurrRelease = releases.headOption val maybePrevRelease = releases.drop(1).headOption @@ -138,16 +177,74 @@ class GitDriverImpl(dir: File) extends GitDriver { output.mkString("").trim.toInt } + /** + * Executes git rev-parse to determine the current branch/HEAD commit + */ + private def gitBranch: String = { + val cmd = s"git rev-parse --abbrev-ref HEAD" + val (exitCode, output) = runCommand(cmd, throwIfNonZero = false) + exitCode match { + // you get 128 when you run a git cmd in a dir not under git vcs + case 0 => + val res = output.map { line => + line + } + res.head + case 128 => + throw new IllegalStateException( + s"Error 128: a git cmd was run in a dir that is not under git vcs or git rev-parse failed to run.") + } + } + + /** + * Returns an ordered list of versions that are merged into your branch. + */ + private def gitForEachRef(arguments: String): Seq[GitCommit] = { + require(isGitRepo(dir), "Must be in a git repository") + require(isGitCompatible, "Must be git version 2.X.X or greater") + + // Note: nested shell commands, piping and redirection will not work with runCommand since it is just + // invoking an OS process. You could invoke a shell and pass expressions if needed. + val cmd = s"git for-each-ref --sort=-v:refname refs/tags --merged=${gitBranch}" + + /** + * Example output: + * {{{ + * 686623c25b52e40fe6270ab57419551b88e89dfe tag refs/tags/v1.0.0 + * fb22d49dd7d7bf5b5f130c4ff3b66667d97bc308 commit refs/tags/v0.0.3 + * 5ca402250fd63e6ac3a9b51d457b89c092195098 commit refs/tags/v0.0.2 + * }}} + */ + + // val (exitCode, output) = runCommand(cmd, throwIfNonZero = false) + val (exitCode, output) = runCommand(cmd, throwIfNonZero = false) + exitCode match { + // you get 128 when you run 'git log' on a repository with no commits + case 0 | 128 => + val abbreviatedHashLength = findAbbreviatedHashLength() + output map { line => + GitCommit.fromGitRef(line, abbreviatedHashLength) + } + case ret => throw new IllegalStateException(s"Non-zero exit code when running git log: $ret") + } + } + /** * Executes a single "git log" command. */ private def gitLog(arguments: String): Seq[GitCommit] = { require(isGitRepo(dir), "Must be in a git repository") + require(isGitCompatible, "Must be git version 2.X.X or greater") // originally this used "git describe", but that doesn't always work the way you want. its definition of "nearest" // tag is not always what you think it means: it does NOT search backward to the root, it will search other // branches too. See http://www.xerxesb.com/2010/12/20/git-describe-and-the-tale-of-the-wrong-commits/ - val cmd = s"git log --oneline --decorate=short --first-parent --simplify-by-decoration --no-abbrev-commit $arguments" + // The old command was the following: + // git log --oneline --decorate=short --first-parent --simplify-by-decoration --no-abbrev-commit + // which has the argument --first-parent. The problem is that first-parent will hide release that are done in in another + // branch and merged into master. + + val cmd = s"git log --oneline --decorate=short --simplify-by-decoration --no-abbrev-commit $arguments" /** * Example output: * {{{ @@ -162,7 +259,9 @@ class GitDriverImpl(dir: File) extends GitDriver { // you get 128 when you run 'git log' on a repository with no commits case 0 | 128 => val abbreviatedHashLength = findAbbreviatedHashLength() - output.map(line => GitCommit.fromGitLog(line, abbreviatedHashLength)) + output map { line => + GitCommit.fromGitLog(line, abbreviatedHashLength) + } case ret => throw new IllegalStateException(s"Non-zero exit code when running git log: $ret") } } @@ -205,6 +304,7 @@ class GitDriverImpl(dir: File) extends GitDriver { */ private def runCommand(cmd: String, throwIfNonZero: Boolean = true): (Int, Seq[String]) = { require(isGitRepo(dir), "Must be in a git repository") + require(isGitCompatible, "Must be git version 2.X.X or greater") val outputLogger = new BufferingProcessLogger val exitCode: Int = Process(cmd, dir) ! outputLogger val result = (exitCode, outputLogger.stdout) diff --git a/src/main/scala/com/rallyhealth/sbt/versioning/GitState.scala b/src/main/scala/com/rallyhealth/sbt/versioning/GitState.scala index c16c795..f0186c5 100644 --- a/src/main/scala/com/rallyhealth/sbt/versioning/GitState.scala +++ b/src/main/scala/com/rallyhealth/sbt/versioning/GitState.scala @@ -30,7 +30,14 @@ case class GitWorkingState(isDirty: Boolean) * [[Option]]s represent multiple states in one class). I learned that makes the logic harder to follow and introduces * cyclomatic complexity (i.e. nested logic). */ -sealed trait GitBranchState +sealed trait GitBranchState { + + /** Similar to [[Predef.require()]] yet it tacks on "this" to the end of the message. */ + protected def require(requirement: Boolean, message: => Any) { + if (!requirement) + throw new IllegalArgumentException(s"requirement failed: $message (from $this)") + } +} /** * This is used when there are two (2) commits that are tagged as [[ReleaseVersion]]s, and the HEAD commit is the diff --git a/src/main/scala/com/rallyhealth/sbt/versioning/GitVersioningPlugin.scala b/src/main/scala/com/rallyhealth/sbt/versioning/GitVersioningPlugin.scala index a4ed45d..df2853e 100644 --- a/src/main/scala/com/rallyhealth/sbt/versioning/GitVersioningPlugin.scala +++ b/src/main/scala/com/rallyhealth/sbt/versioning/GitVersioningPlugin.scala @@ -48,11 +48,15 @@ object GitVersioningPlugin extends AutoPlugin { lazy val versionFromGit: SettingKey[SemanticVersion] = settingKey[SemanticVersion]( "`versionFromGit` is The version as determined by git history") + lazy val semanticVersion: SettingKey[SemanticVersion] = settingKey[SemanticVersion]( + "The typed representation of `version`" + ) + /** * The sbt ecosystem relies on the value of the version setting key as the * source of truth for the version we're currently building. * - * RallyVersioning is based on git tags which are often incremented to later versions after CI builds complete. + * GitVersioningPlugin is based on git tags which are often incremented to later versions after CI builds complete. * * For example, given a most recent tag of v1.5.0, [[GitVersioningPlugin]] at CI time * will determine the version to be something like v1.5.1-4-aabcdef-SNAPSHOT. @@ -88,6 +92,10 @@ object GitVersioningPlugin extends AutoPlugin { "`versionOverride` overrides the automatically determined `version`. This is set (often by a system property)" + " to the version you want to release before executing `publish` or `publishLocal`") + lazy val generateVersion: TaskKey[File] = TaskKey[File]( + "generate-version", + "Writes the version to a rally-version.properties file.") + lazy val ignoreDirty: SettingKey[Boolean] = settingKey[Boolean]( "Forces clean builds, i.e. doesn't add '-dirty' to the version.") @@ -135,7 +143,7 @@ object GitVersioningPlugin extends AutoPlugin { autoFetchTimeout := 15, // seconds autoFetchResult := { - implicit val logger: Logger = ConsoleLogger() + implicit val logger: Logger = sLog.value if (autoFetch.value) { logger.info("Fetching the most up-to-date tags from git remotes") GitFetcher.fetchRemotes(autoFetchRemotes.value, autoFetchTimeout.value.seconds) @@ -179,7 +187,9 @@ object GitVersioningPlugin extends AutoPlugin { ignoreDirty := false, - version := { + version := semanticVersion.value.toString, + + semanticVersion := { import SemVerReleaseType._ val log = ConsoleLogger() // A setting cannot depend on a task (like streams.value) @@ -213,16 +223,14 @@ object GitVersioningPlugin extends AutoPlugin { applyAllTransforms(versionFromGit.value) } - val version = verOverride.getOrElse(boundedVersionFromGit).toString + val version = verOverride.getOrElse(boundedVersionFromGit) log.info(s"GitVersioningPlugin set version=$version") version }, isCleanRelease := { // version must be semver, see version's definition - val isDirty = SemanticVersion.fromString(version.value) - .getOrElse(throw new IllegalArgumentException(s"cannot parse version=${version.value}")) - .isDirty + val isDirty = semanticVersion.value.isDirty ConsoleLogger().info(s"GitVersioningPlugin set isCleanRelease=${!isDirty}") @@ -235,6 +243,9 @@ object GitVersioningPlugin extends AutoPlugin { versionOverride.value.foreach { verOverride => log.info(s"Version as determined by system property (version.override): $verOverride") } + gitVersioningMaybeRelease.value.foreach { relType => + log.info(s"Release type: $relType") + } log.success(s"Successfully determined version: ${versionOverride.value.getOrElse(versionFromGit.value)}") }, @@ -246,4 +257,27 @@ object GitVersioningPlugin extends AutoPlugin { commands ++= Seq(printVersionCommand, writeVersionCommand) ) + /** + * Generates a rally-version.properties file in the managedResources directory suitable for serving version requests during compile. + */ + override def projectSettings: Seq[Def.Setting[_]] = Seq( + generateVersion := { + val log = streams.value.log + val props = new java.util.Properties() + props.setProperty("Version", version.value) + props.setProperty("Name", name.value) + props.setProperty("Created", System.currentTimeMillis().toString) + + val file = (resourceManaged in Compile).value / "rally-version.properties" + log.info(s"Writing version file $file") + IO.write(props, "generated by sbt", file) + file + }, + + /** + * Trigger generating the version file on resources. + */ + resourceGenerators in Compile += generateVersion.map(Seq(_)).taskValue + ) + } diff --git a/src/main/scala/com/rallyhealth/sbt/versioning/LowerBoundedSemanticVersion.scala b/src/main/scala/com/rallyhealth/sbt/versioning/LowerBoundedSemanticVersion.scala index 19c1e4a..0ca84c0 100644 --- a/src/main/scala/com/rallyhealth/sbt/versioning/LowerBoundedSemanticVersion.scala +++ b/src/main/scala/com/rallyhealth/sbt/versioning/LowerBoundedSemanticVersion.scala @@ -17,7 +17,8 @@ object LowerBoundedSemanticVersion { /** * A new [[SemanticVersion]] increased up to the provided lower bound or else the original [[version]]. * - * @param hash Used to fill in the hash if this returns a new [[SemanticVersion]] + * @param bound Used to set the stable portion of the generated lower bound + * @param hashAndCount Used to fill in the hash and count of the lower bound * @return A new [[SemanticVersion]] increased up to the provided lower bound, or else the original [[version]]. */ def lowerBound(bound: LowerBound, hashAndCount: HashAndCount): SemanticVersion = { @@ -31,7 +32,10 @@ object LowerBoundedSemanticVersion { hashAndCount.count ) - if (lowerBoundedVersion > version) lowerBoundedVersion else version + if (lowerBoundedVersion > version) { + require(version.isSnapshot, s"gitVersioningSnapshotLowerBound=$bound is higher than release=$version. Refusing to do a release build.") + lowerBoundedVersion + } else version } } diff --git a/src/main/scala/com/rallyhealth/sbt/versioning/SemanticVersion.scala b/src/main/scala/com/rallyhealth/sbt/versioning/SemanticVersion.scala index 5d398cb..2904d58 100644 --- a/src/main/scala/com/rallyhealth/sbt/versioning/SemanticVersion.scala +++ b/src/main/scala/com/rallyhealth/sbt/versioning/SemanticVersion.scala @@ -29,6 +29,10 @@ sealed trait SemanticVersion extends Ordered[SemanticVersion] { */ def versionIdentifiers: SemVerIdentifierList + def isRelease: Boolean + + def isSnapshot: Boolean = !isRelease + override lazy val toString: String = { val tagStr = identifiers.toString s"$major.$minor.$patch" + (if (tagStr.isEmpty) "" else SemVerIdentifierList.separatorChar + tagStr) @@ -170,6 +174,8 @@ case class ReleaseVersion( } override def toRelease: ReleaseVersion = this + + override def isRelease: Boolean = true } object ReleaseVersion { @@ -271,12 +277,14 @@ case class SnapshotVersion( 0 case thatRelease: ReleaseVersion => - val superDelta = toRelease.compare(thatRelease) + // Compare only major.minor.patch ignoring "dirty" as a tie-breaker. + val majorMinorPatchDelta = toRelease.copy(isDirty = false).compare(thatRelease.copy(isDirty = false)) + // snapshots for this release are considered LESS than the release, see http://semver.org/#spec-item-11 - if (superDelta == 0) + if (majorMinorPatchDelta == 0) -1 else - superDelta + majorMinorPatchDelta } /** @@ -314,6 +322,8 @@ case class SnapshotVersion( def undoNextVersion(): SnapshotVersion = copy(patch = patch - 1) override def toRelease: ReleaseVersion = ReleaseVersion(major, minor, patch, versionIdentifiers, isDirty) + + override def isRelease: Boolean = false } object SnapshotVersion { diff --git a/src/sbt-test-disabled/semver/log/build.sbt b/src/sbt-test-disabled/semver/log/build.sbt index f586d0a..0ad3e4b 100644 --- a/src/sbt-test-disabled/semver/log/build.sbt +++ b/src/sbt-test-disabled/semver/log/build.sbt @@ -6,10 +6,6 @@ organization := "com.rallyhealth.test.scripted" scalacOptions ++= Seq("-Xfatal-warnings", "-Xlint") -credentials += Credentials(Path.userHome / ".ivy2" / ".credentials") - -resolvers += "Artifactory Libs Releases" at "https://artifacts.werally.in/artifactory/libs-release" - libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.1" % "test" publish := {} diff --git a/src/sbt-test-disabled/semver/log/test b/src/sbt-test-disabled/semver/log/test index d354821..21b608d 100644 --- a/src/sbt-test-disabled/semver/log/test +++ b/src/sbt-test-disabled/semver/log/test @@ -19,7 +19,7 @@ # absence of the log output for "xxx" is a good way to be sure "xxx" didn't execute unintentionally. # Notes: -# - 'version' must be defined as 'version in ThisBuild' since it is defined by RallyVersioning in the +# - 'version' must be defined as 'version in ThisBuild' since it is defined by GitVersioningPlugin in the # buildSettings scope ({.}/version) as opposed to the project settings scope. # - The "reload" task is should be run before tasks many tasks to ensure that any settings changed by the # previous tests are removed. Sometimes, when all tests change the same setting (or no settings) "reload" diff --git a/src/sbt-test/versioning/Readme.md b/src/sbt-test/versioning/Readme.md new file mode 100644 index 0000000..21e9ab2 --- /dev/null +++ b/src/sbt-test/versioning/Readme.md @@ -0,0 +1,92 @@ +## Hotfix information + +A hotfix branch is created off of commit D (latest tag). Work begins on the hot fix and a new commit is added to the repo. +This commit is tagged as a new minor release if you are following this model: http://www.xerxesb.com/2010/12/20/git-describe-and-the-tale-of-the-wrong-commits/ +The hotfix is then merged back into the main branch. + + +Diagram: +``` + A < Merge Commit + | \ + | B tag: hotfix 0.1.1 + C tag: 0.2.0 + | + D tag: 0.1.0 +``` + + +## Release Branch Information: + +A release branch is created to run all releases then merged back into master. +For example if you are releasing version 0.2.0 +Then you would create a new branch off of commit C and pull your commits into that release branch. Then run a release +and back merge your changes into master. + +Diagram: +``` + A < Merge Commit + | \ + | B tag: 0.2.0 + C < Merge Commit + | \ + | D tag: 0.1.0 + E init tag: 0.0.1 +``` + + +## Reference Diagrams: + +## HotFixes: +#### Nested: +``` +A < Merge Commit +| \ +| B tag: hotfix 0.1.1 +C tag: 0.2.0 +| +D tag: 0.1.0 +``` + +#### Flat: +``` +A Merge +B tag 0.1.1 +C tag 0.2.0 +D tag: 0.1.0 +``` + +#### Flat First Parent: +``` +A Merge +C tag 0.2.0 +D tag: 0.1.0 +``` + +## Release Branch: +#### Nested: +``` +A < Merge Commit +| \ +| B tag: 0.2.0 +C < Merge Commit +| \ +| D tag: 0.1.0 +E init tag: 0.0.1 +``` + +#### Flat: +``` +A Merge +B tag 0.2.0 +C Merge +D tag: 0.1.0 +E init tag: 0.0.1 +``` + +#### Flat First Parent: +``` +A Merge +C Merge +D init tag: 0.0.1 +``` diff --git a/src/sbt-test/versioning/branch/.gitignore b/src/sbt-test/versioning/branch/.gitignore new file mode 100644 index 0000000..15f47b2 --- /dev/null +++ b/src/sbt-test/versioning/branch/.gitignore @@ -0,0 +1,24 @@ +# Created by .ignore support plugin (hsz.mobi) +### Scala template +*.class +*.log + +# sbt specific +.cache +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +global/ +pending* + +# Scala-IDE specific +.scala_dependencies +.worksheet + +# RallyScriptedPlugin +scriptedOutput-* diff --git a/src/sbt-test/versioning/branch/build.sbt b/src/sbt-test/versioning/branch/build.sbt new file mode 100644 index 0000000..cc1b9dc --- /dev/null +++ b/src/sbt-test/versioning/branch/build.sbt @@ -0,0 +1,38 @@ +import sbt.complete.DefaultParsers._ + +organization := "com.rallyhealth.test.scripted" + +scalaVersion := "2.11.12" + +scalacOptions ++= Seq("-Xfatal-warnings", "-Xlint") + +publish := {} + +logLevel := sbt.Level.Info + +lazy val assertVersion = inputKey[Unit]("Checks that the version matches the expected value.") +lazy val setRelease = inputKey[Unit]("Sets the release property to the specified value") +lazy val clearRelease = inputKey[Unit]("Removes the release property") + +/** + * Checks that the [[version]] matches the provided pattern. The pattern looks like + * {{{ + * 0.0.1-1--SNAPSHOT + * }}} + * where `` is replaced with `[0-9a-f]{7,}` before comparing. + */ +assertVersion := { + // Replace the hash with a stable placeholder so we can do assertions against it. + val actual = version.value.replaceAll("[0-9a-f]{7,}", "") + val expected = spaceDelimited("").parsed.head + + assert(expected == actual, s"expected: $expected actual: $actual") +} +setRelease := { + val value = spaceDelimited("").parsed.head + sys.props += "release" -> value +} + +clearRelease := { + sys.props -= "release" +} diff --git a/src/sbt-test/versioning/branch/project/plugins.sbt b/src/sbt-test/versioning/branch/project/plugins.sbt new file mode 100644 index 0000000..31095cf --- /dev/null +++ b/src/sbt-test/versioning/branch/project/plugins.sbt @@ -0,0 +1,9 @@ +{ + val pluginVersion = System.getProperty("plugin.version") + if (pluginVersion == null) + throw new RuntimeException( + """|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin + ) + else addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % pluginVersion) +} diff --git a/src/sbt-test/versioning/branch/test b/src/sbt-test/versioning/branch/test new file mode 100644 index 0000000..6e81f67 --- /dev/null +++ b/src/sbt-test/versioning/branch/test @@ -0,0 +1,42 @@ +############################################################################################################ +# A release branch is created to run all releases then merged back into master +# see readme + +# Init the repo +$ exec git init +$ touch bar.txt +$ exec git add . +$ exec git commit -am 'Initial commit.' +$ exec git tag v0.0.2 -m '' + +# do a little work +$ exec awk 'BEGIN{ a="fubar"; {print a >> "foo.txt"; system("git add ."); system("git commit -am \"letter: "a"\"")} }' + +# Fake a initial release +> setRelease major +> reload +> assertVersion 1.0.0 +> clearRelease +$ exec git tag v1.0.0 -m '' + +# Now create some commits +$ exec git checkout -b rc +$ exec touch fubar.txt +$ exec git add . +$ exec git commit -am 'fubar.txt' + +# do a bunch of commits to the file in a new branch +$ exec awk 'BEGIN{ split("fubar",a,""); for(i=1;i<=length(a);i++) {print a[i] > "fubar.txt"; system("git commit -am \"letter: "a[i]"\"")} }' + +# Fake a new release off the release branch +> setRelease major +> reload +> assertVersion 2.0.0 +> clearRelease +$ exec git tag v2.0.0 -m '' + +# Now fake merging the PR +$ exec git checkout master +$ exec git merge --no-ff rc -m "Merge branch 'rc' via fake PR" +> reload +> assertVersion 2.0.1-1--SNAPSHOT diff --git a/src/sbt-test/versioning/generateVersion/.gitignore b/src/sbt-test/versioning/generateVersion/.gitignore new file mode 100644 index 0000000..15f47b2 --- /dev/null +++ b/src/sbt-test/versioning/generateVersion/.gitignore @@ -0,0 +1,24 @@ +# Created by .ignore support plugin (hsz.mobi) +### Scala template +*.class +*.log + +# sbt specific +.cache +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +global/ +pending* + +# Scala-IDE specific +.scala_dependencies +.worksheet + +# RallyScriptedPlugin +scriptedOutput-* diff --git a/src/sbt-test/versioning/generateVersion/build.sbt b/src/sbt-test/versioning/generateVersion/build.sbt new file mode 100644 index 0000000..c2ef59d --- /dev/null +++ b/src/sbt-test/versioning/generateVersion/build.sbt @@ -0,0 +1,54 @@ +import java.io.FileInputStream + +import sbt.Keys.version +import sbt.complete.DefaultParsers.spaceDelimited + +import scala.collection.JavaConverters._ + +organization := "com.rallyhealth.sbt.scripted" + +scalaVersion := "2.11.12" + +scalacOptions ++= Seq("-Xfatal-warnings", "-Xlint") + +publish := {} + +logLevel := sbt.Level.Info + +lazy val assertGenerateVersion = inputKey[Unit]("Checks the file generated by 'generateVersion' task") + +/** + * Asserts that the file created by 'generateVersion' exists and has the expected content. + * + * #param versionRegex The version must match this regex + */ +assertGenerateVersion := { + + val generatedFile: File = com.rallyhealth.sbt.versioning.GitVersioningPlugin.autoImport.generateVersion.value + + assert(generatedFile.exists(), s"generatedFile=$generatedFile does not exist") + assert(generatedFile.getName == "rally-version.properties", s"generatedFile=$generatedFile name is different") + + val properties = new java.util.Properties() + properties.load(new FileInputStream(generatedFile)) + + assert( + properties.size() == 3, + s"generatedFile=$generatedFile does not contains 3 values: " + properties.keys().asScala.map(_.toString).mkString(", ")) + + val generatedVersion = properties.getProperty("Version") + + assert(generatedVersion == version.value, s"generatedFile.Version=$generatedVersion != sbt.version=${version.value}") + + val versionRegex = spaceDelimited("").parsed.head + assert( + generatedVersion.matches(versionRegex), + s"generatedFile.Version=$generatedVersion does not match regex=$versionRegex") + + assert( + properties.getProperty("Name") == name.value, + s"generatedFile.Name=${properties.getProperty("Name")} != sbt.name=${name.value}") + assert( + properties.getProperty("Created").toLong + 10000 >= System.currentTimeMillis(), + s"generatedFile.Created=${properties.getProperty("Created")} is too old") +} diff --git a/src/sbt-test/versioning/generateVersion/project/plugins.sbt b/src/sbt-test/versioning/generateVersion/project/plugins.sbt new file mode 100644 index 0000000..29ba2a2 --- /dev/null +++ b/src/sbt-test/versioning/generateVersion/project/plugins.sbt @@ -0,0 +1,9 @@ +{ + val pluginVersion = System.getProperty("plugin.version") + if (pluginVersion == null) + throw new RuntimeException( + """|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin + ) + else addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % pluginVersion) +} diff --git a/src/sbt-test/versioning/generateVersion/test b/src/sbt-test/versioning/generateVersion/test new file mode 100644 index 0000000..9f8e816 --- /dev/null +++ b/src/sbt-test/versioning/generateVersion/test @@ -0,0 +1,27 @@ +############################################################################################################ +# Test generateVersion + +# Init the repo +$ exec git init + +# No prior commits +$ exec git add . +> reload +> assertGenerateVersion 0\.0\.1-dirty-SNAPSHOT + +# Initial commit +$ exec git commit -m "Initial commit" +> reload +> 'assertGenerateVersion 0\.0\.1-1-[0-9a-f]{7,}-SNAPSHOT' + +# Dirty +$ touch file1.txt +$ exec git add file1.txt +> reload +> 'assertGenerateVersion 0\.0\.1-1-[0-9a-f]{7,}-dirty-SNAPSHOT' + +# Release commit +$ exec git commit -m "Initial commit" +$ exec git tag v1.0.0 +> reload +> assertGenerateVersion 1\.0\.0 diff --git a/src/sbt-test/versioning/hotfix/.gitignore b/src/sbt-test/versioning/hotfix/.gitignore new file mode 100644 index 0000000..15f47b2 --- /dev/null +++ b/src/sbt-test/versioning/hotfix/.gitignore @@ -0,0 +1,24 @@ +# Created by .ignore support plugin (hsz.mobi) +### Scala template +*.class +*.log + +# sbt specific +.cache +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +global/ +pending* + +# Scala-IDE specific +.scala_dependencies +.worksheet + +# RallyScriptedPlugin +scriptedOutput-* diff --git a/src/sbt-test/versioning/hotfix/build.sbt b/src/sbt-test/versioning/hotfix/build.sbt new file mode 100644 index 0000000..eb9362e --- /dev/null +++ b/src/sbt-test/versioning/hotfix/build.sbt @@ -0,0 +1,43 @@ +import sbt.complete.DefaultParsers._ + +organization := "com.rallyhealth.test.scripted" + +scalaVersion := "2.11.12" + +scalacOptions ++= Seq("-Xfatal-warnings", "-Xlint") + +publish := {} + +logLevel := sbt.Level.Info + +lazy val assertVersion = inputKey[Unit]("Checks that the version matches the expected value.") +lazy val setRelease = inputKey[Unit]("Sets the release property to the specified value") +lazy val clearRelease = inputKey[Unit]("Removes the release property") +lazy val printVer = inputKey[Unit]("prints ver") + +/** + * Checks that the [[version]] matches the provided pattern. The pattern looks like + * {{{ + * 0.0.1-1--SNAPSHOT + * }}} + * where `` is replaced with `[0-9a-f]{7,}` before comparing. + */ +assertVersion := { + // Replace the hash with a stable placeholder so we can do assertions against it. + val actual = version.value.replaceAll("[0-9a-f]{7,}", "") + val expected = spaceDelimited("").parsed.head + + assert(expected == actual, s"expected: $expected actual: $actual") +} + +printVer := { + println(s"version: ${version.value}") +} +setRelease := { + val value = spaceDelimited("").parsed.head + sys.props += "release" -> value +} + +clearRelease := { + sys.props -= "release" +} diff --git a/src/sbt-test/versioning/hotfix/project/plugins.sbt b/src/sbt-test/versioning/hotfix/project/plugins.sbt new file mode 100644 index 0000000..31095cf --- /dev/null +++ b/src/sbt-test/versioning/hotfix/project/plugins.sbt @@ -0,0 +1,9 @@ +{ + val pluginVersion = System.getProperty("plugin.version") + if (pluginVersion == null) + throw new RuntimeException( + """|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin + ) + else addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % pluginVersion) +} diff --git a/src/sbt-test/versioning/hotfix/test b/src/sbt-test/versioning/hotfix/test new file mode 100644 index 0000000..65be095 --- /dev/null +++ b/src/sbt-test/versioning/hotfix/test @@ -0,0 +1,50 @@ +############################################################################################################ +# Tests creating a hotfix on a separate branch +# and somone needs to cut a release to push to integration. +# see readme. + +# Init the repo +$ exec git init +$ touch bar.txt +$ exec git add . +$ exec git commit -am 'Initial commit.' +> setRelease patch +> reload +> assertVersion 0.0.1 +> clearRelease +$ exec git tag v0.0.1 + +# do a lot of work with commits for each change +$ exec awk 'BEGIN{ split("fubar",a,""); for(i=1;i<=length(a);i++) {print a[i] >> "bar.txt"; system("git add ."); system("git commit -am \"letter: "a[i]"\"")} }' + +# Someone is preparing a release and needs a stable artifact for integration +> setRelease major +> reload +> assertVersion 1.0.0 +> clearRelease +$ exec git tag v1.0.0 + +# Uh oh we need to hotfix v0.0.1 checkout hotfix branch +$ exec git checkout -b hotfix v0.0.1 + +# do a little work +$ exec awk 'BEGIN{ a="fubar"; {print a >> "foo.txt"; system("git add \."); system("git commit -am \"letter: "a"\"")} }' + +# Fake a new release +> setRelease patch +> printVersion +> printVer +> reload +> printVersion +> printVer +> assertVersion 0.0.2 +> clearRelease +$ exec git tag v0.0.2 -m 'v0.0.2' + +# checkout master tonight +$ exec git checkout master + +# merge hotfix into master with PR +$ exec git merge --no-ff hotfix -m "Merge branch 'hotfix' via fake PR" +> reload +> assertVersion 1.0.1-1--SNAPSHOT diff --git a/src/sbt-test/versioning/lowerBound/test b/src/sbt-test/versioning/lowerBound/test index 9bec9b0..407cf5e 100644 --- a/src/sbt-test/versioning/lowerBound/test +++ b/src/sbt-test/versioning/lowerBound/test @@ -34,15 +34,9 @@ $ exec git tag v1.0.0 > 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.0.0"' > assertVersion 1.0.0 -> 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.0.1"' -> assertVersion 1.0.1-0--SNAPSHOT - -> 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.2.0"' -> assertVersion 1.2.0-0--SNAPSHOT - -> 'set gitVersioningSnapshotLowerBound in ThisBuild := "2.2.2"' -> assertVersion 2.2.2-0--SNAPSHOT +-> 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.0.1"' +# Dirty release, but still a release (i.e. v1.0.0-dirty) $ touch dirty.txt $ exec git add dirty.txt > reload @@ -53,11 +47,22 @@ $ exec git add dirty.txt > 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.0.0"' > assertVersion 1.0.0-dirty-SNAPSHOT +-> 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.0.1"' + +$ exec git commit -am "We're a 1.0.1-snapshot now." +> reload + +> 'set gitVersioningSnapshotLowerBound in ThisBuild := "0.0.1"' +> assertVersion 1.0.1-1--SNAPSHOT + +> 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.0.0"' +> assertVersion 1.0.1-1--SNAPSHOT + > 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.0.1"' -> assertVersion 1.0.1-0--dirty-SNAPSHOT +> assertVersion 1.0.1-1--SNAPSHOT -> 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.2.0"' -> assertVersion 1.2.0-0--dirty-SNAPSHOT +> 'set gitVersioningSnapshotLowerBound in ThisBuild := "1.1.0"' +> assertVersion 1.1.0-1--SNAPSHOT > 'set gitVersioningSnapshotLowerBound in ThisBuild := "2.2.2"' -> assertVersion 2.2.2-0--dirty-SNAPSHOT +> assertVersion 2.2.2-1--SNAPSHOT diff --git a/src/sbt-test/versioning/reflog/.gitignore b/src/sbt-test/versioning/reflog/.gitignore new file mode 100644 index 0000000..15f47b2 --- /dev/null +++ b/src/sbt-test/versioning/reflog/.gitignore @@ -0,0 +1,24 @@ +# Created by .ignore support plugin (hsz.mobi) +### Scala template +*.class +*.log + +# sbt specific +.cache +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +global/ +pending* + +# Scala-IDE specific +.scala_dependencies +.worksheet + +# RallyScriptedPlugin +scriptedOutput-* diff --git a/src/sbt-test/versioning/reflog/README.md b/src/sbt-test/versioning/reflog/README.md new file mode 100644 index 0000000..b31b2d4 --- /dev/null +++ b/src/sbt-test/versioning/reflog/README.md @@ -0,0 +1,78 @@ +# Links + +* Plugin Home: https://github.com/sbt/sbt/tree/0.13/scripted/plugin +* Code +** Plugin: https://github.com/sbt/sbt/blob/0.13/scripted/plugin/src/main/scala/sbt/ScriptedPlugin.scala +** Scripted: https://github.com/sbt/sbt/blob/0.13/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala +** Scripted's dependencies: https://github.com/sbt/sbt-zero-thirteen/tree/0.13/scripted/base/src/main/scala/xsbt/test +* Blog post from the author of sbt-assembly: http://eed3si9n.com/testing-sbt-plugins +* Examples +** From xsbt-web-plugin: https://github.com/earldouglas/xsbt-web-plugin/tree/master/src/sbt-test +** From sbt-assembly: https://github.com/sbt/sbt-assembly/tree/master/src/sbt-test/sbt-assembly + +# Usage + +## All tests + +```bash +4 sbt +sbt$ scripted +``` + +## Specific tests + +```bash +sbt$ scripted [group]/[test] +``` + +For example to run all the tests in "versioning": + +```bash +sbt$ scripted versioning/* +``` + +For example to run the "fromGit" tests in "versioning": + +```bash +sbt$ scripted versioning/fromGit +``` + +# Notes + +* When running tests you will see "[error]" statements. These do not (always) mean errors in the test; they are errors + in the *code under test*, which is often what is being tested. The final "[success]" or "[error]" is really the only + the only one that matters +* You cannot have anything but directories within a [group] -- any files will be treated as directories and it will + bail out with an error. +* Code in the scripted tests won't be compiled until *after* the plugin is published. So try to avoid being lazy and + letting the compiler catch your dumb errors because it takes a while for the compiler to get the point where it can + check your code +* IntelliJ does not really understand what's going on with scripted tests. It can see Scala files and gives you syntax + highlighting but it can't tell there's a "project" so referencing a class from another file will show errors. Just + don't trust IntelliJ too much. +* If you add a new check task you *must* parse the arguments even if you don't use them, i.e. you must add +```val cmd: Seq[String] = spaceDelimited("...").parsed``` to your check task. +* I think you can add a [file named "disabled"](https://github.com/sbt/sbt/blob/1.0.x/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala#L40) + to the [test] directory to disable the test. +* You must put all your tests in a file named "test". It is [not configurable](https://github.com/sbt/sbt/blob/0.13/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala#L19) +* You can make a separate test file called "[pending](https://github.com/sbt/sbt/blob/0.13/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala#L20)". + I think "pending" tests are like a scratch area so you can run a few tests in isolation before moving them to the main + "test" file. + ** Their value is somewhat dubious -- Their value is dubious. I've found it useful when working with large + "test" scripts. You can copy out a subsection of "test" into "pending", get "pending" passing, and copy it back into + test". +* A "pending" test will always fail -- even if all the tests pass. The final failure is cryptic message "Mark as + passing to remove this failure." which I think means to copy all your passing tests to "test". +* Pending tests will always create a failure at the end. I guess so you can never mistakenly merge them? (There's no + comments; I'm forced to speculate.)* You cannot have a comment line with no comment, e.g. ```# ``` is an invalid comment line. +* Many of the built-in "]" commands require quotes around the entire command if there are any spaces +* Be careful with escaping and spaces: +** Sometimes you have to double escape things, e.g. if you want to '\' you need to '\\' +** An extra or unexpected spaces will break the parser, .e.g ```] '; task1 ; task2'``` is fine but +```] ' ; task1 ; task2'``` (space between the first ' and ;) will fail. +* If you have a syntax error in your script then the script will fail as soon as you try running it. If this happens + with a "pending" script you won't get an error telling you what the problem is, it just fails mysteriously. ```] last + scripted``` won't tell you anything more either. +* You can combine commands into single line using ";" as long as you start with a ";" +* It is best to use semver/log's "grepLog" command with triple quotes. +* Scripted tests ALWAYS run at 'Info' logging levels. [You can't change it.](https://github.com/sbt/sbt-zero-thirteen/blob/0.13/util/log/src/main/scala/sbt/BasicLogger.scala#L9) diff --git a/src/sbt-test/versioning/reflog/build.sbt b/src/sbt-test/versioning/reflog/build.sbt new file mode 100644 index 0000000..eb9362e --- /dev/null +++ b/src/sbt-test/versioning/reflog/build.sbt @@ -0,0 +1,43 @@ +import sbt.complete.DefaultParsers._ + +organization := "com.rallyhealth.test.scripted" + +scalaVersion := "2.11.12" + +scalacOptions ++= Seq("-Xfatal-warnings", "-Xlint") + +publish := {} + +logLevel := sbt.Level.Info + +lazy val assertVersion = inputKey[Unit]("Checks that the version matches the expected value.") +lazy val setRelease = inputKey[Unit]("Sets the release property to the specified value") +lazy val clearRelease = inputKey[Unit]("Removes the release property") +lazy val printVer = inputKey[Unit]("prints ver") + +/** + * Checks that the [[version]] matches the provided pattern. The pattern looks like + * {{{ + * 0.0.1-1--SNAPSHOT + * }}} + * where `` is replaced with `[0-9a-f]{7,}` before comparing. + */ +assertVersion := { + // Replace the hash with a stable placeholder so we can do assertions against it. + val actual = version.value.replaceAll("[0-9a-f]{7,}", "") + val expected = spaceDelimited("").parsed.head + + assert(expected == actual, s"expected: $expected actual: $actual") +} + +printVer := { + println(s"version: ${version.value}") +} +setRelease := { + val value = spaceDelimited("").parsed.head + sys.props += "release" -> value +} + +clearRelease := { + sys.props -= "release" +} diff --git a/src/sbt-test/versioning/reflog/project/plugins.sbt b/src/sbt-test/versioning/reflog/project/plugins.sbt new file mode 100644 index 0000000..31095cf --- /dev/null +++ b/src/sbt-test/versioning/reflog/project/plugins.sbt @@ -0,0 +1,9 @@ +{ + val pluginVersion = System.getProperty("plugin.version") + if (pluginVersion == null) + throw new RuntimeException( + """|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin + ) + else addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % pluginVersion) +} diff --git a/src/sbt-test/versioning/reflog/test b/src/sbt-test/versioning/reflog/test new file mode 100644 index 0000000..8b368dd --- /dev/null +++ b/src/sbt-test/versioning/reflog/test @@ -0,0 +1,25 @@ +############################################################################################################ +# Tests to validate that the shas returned from reflog are converted to git log commits + +# Init the repo +$ exec git init +$ touch bar.txt +$ exec git add . +$ exec git commit -am 'Initial commit.' +> setRelease patch +> reload +> assertVersion 0.0.1 +> clearRelease +$ exec git tag v0.0.1 + +# do a lot of work with commits for each change +$ exec awk 'BEGIN{ split("fubar",a,""); for(i=1;i<=length(a);i++) {print a[i] >> "bar.txt"; system("git add ."); system("git commit -am \"letter: "a[i]"\"")} }' + +# Someone is preparing a release and needs a stable artifact for integration +> setRelease major +> reload +> assertVersion 1.0.0 +> clearRelease +$ exec git tag -m "v1.0.0" -m "this is how we tag on the build server" "v1.0.0" +# we want it to successfully reload before it was failing on reload +> reload diff --git a/src/sbt-test/versioning/release/test b/src/sbt-test/versioning/release/test index 7f80f3a..1a91bc7 100644 --- a/src/sbt-test/versioning/release/test +++ b/src/sbt-test/versioning/release/test @@ -29,12 +29,6 @@ $ exec git tag v1.2.3 > clearRelease > reload > assertVersion 1.2.3 - -# Work around lack of https://github.com/AudaxHealthInc/sbt-git-versioning/pull/64 by adding a commit. -$ exec touch whatever.txt -$ exec git add . -$ exec git commit -am 'Commit message.' - > setRelease major > reload @@ -47,5 +41,4 @@ $ exec git commit -am 'Commit message.' > 'set gitVersioningSnapshotLowerBound in ThisBuild := "2.0.0"' > assertVersion 2.0.0 -> 'set gitVersioningSnapshotLowerBound in ThisBuild := "2.0.1"' -> assertVersion 2.0.1-1--SNAPSHOT +-> 'set gitVersioningSnapshotLowerBound in ThisBuild := "2.0.1"' diff --git a/src/test/resources/scriptedOutput-example1.txt b/src/test/resources/scriptedOutput-example1.txt index f80d019..7998d08 100644 --- a/src/test/resources/scriptedOutput-example1.txt +++ b/src/test/resources/scriptedOutput-example1.txt @@ -258,7 +258,7 @@ Initialized empty Git repository in /private/var/folders/vp/p4d6nc0d7bnctv6j0lgr [info]  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [info]  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [info]  at java.lang.Thread.run(Thread.java:745) -[info] [error] ({.}/*:semVerVersionLimit) java.lang.IllegalArgumentException: 'semVerLimit' is not set in your 'build.sbt': 'semVerLimit' is a threshold version, it is a version you do NOT want to exceed with these changes. If non-empty the `semVerCheck` will ensure that your changes do not require a release EQUAL or GREATER than this version. (no default value) +[info] [error] ({.}/*:semVerLimit) java.lang.IllegalArgumentException: 'semVerLimit' is not set in your 'build.sbt': 'semVerLimit' is a threshold version, it is a version you do NOT want to exceed with these changes. If non-empty the `semVerCheck` will ensure that your changes do not require a release EQUAL or GREATER than this version. (no default value) [info] [error] (*:update) sbt.ResolveException: unresolved dependency: com.rallyhealth#sbt-git-versioning;1.0.0-m1-4-bd0d236-dirty-SNAPSHOT: not found [info] [error] Total time: 0 s, completed Nov 27, 2016 10:26:23 PM [info] java.lang.AssertionError: assertion failed: Could not find '[error]' in log: diff --git a/src/test/scala/com/rallyhealth/sbt/util/NullSbtLogger.scala b/src/test/scala/com/rallyhealth/sbt/util/NullSbtLogger.scala index f0d23a7..b99804c 100644 --- a/src/test/scala/com/rallyhealth/sbt/util/NullSbtLogger.scala +++ b/src/test/scala/com/rallyhealth/sbt/util/NullSbtLogger.scala @@ -3,7 +3,7 @@ package com.rallyhealth.sbt.util import sbt.{ControlEvent, Level, LogEvent} /** - * No-op logger. + * Based on [[scala.tools.nsc.io.NullLogger]], which is inaccessible for some reason. */ object NullSbtLogger extends sbt.BasicLogger { diff --git a/src/test/scala/com/rallyhealth/sbt/versioning/LowerBoundedSemanticVersionSpec.scala b/src/test/scala/com/rallyhealth/sbt/versioning/LowerBoundedSemanticVersionSpec.scala index 1dc8892..892aa6e 100644 --- a/src/test/scala/com/rallyhealth/sbt/versioning/LowerBoundedSemanticVersionSpec.scala +++ b/src/test/scala/com/rallyhealth/sbt/versioning/LowerBoundedSemanticVersionSpec.scala @@ -1,11 +1,11 @@ package com.rallyhealth.sbt.versioning import com.rallyhealth.sbt.versioning.LowerBoundedSemanticVersion._ -import org.scalatest.FunSpec +import org.scalatest.{FunSpec, Matchers} import scala.language.implicitConversions -class LowerBoundedSemanticVersionSpec extends FunSpec { +class LowerBoundedSemanticVersionSpec extends FunSpec with Matchers { private val hash1 = HashSemVerIdentifier("0123abc") // for these tests we need a full hash, not an abbreviation @@ -32,15 +32,9 @@ class LowerBoundedSemanticVersionSpec extends FunSpec { it("higher bound") { val version = ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = false) val bound = LowerBound(1, 2, 4) - val result = version.lowerBound(bound, hashAndCount1) - assert(result === SnapshotVersion(1, 2, 4, SemVerIdentifierList.empty, false, hashAndCount1, 1)) - } - - it("identifiers") { - val version = ReleaseVersion(1, 0, 0, Seq("rc.0"), isDirty = false) - val bound = LowerBound(2, 0, 0) - val result = version.lowerBound(bound, hashAndCount1) - assert(result === SnapshotVersion(2, 0, 0, SemVerIdentifierList.empty, false, hashAndCount1, 1)) + an[IllegalArgumentException] shouldBe thrownBy { + version.lowerBound(bound, hashAndCount1) + } } } @@ -80,6 +74,14 @@ class LowerBoundedSemanticVersionSpec extends FunSpec { val result = version.lowerBound(bound, hashAndCount1) assert(result.toString === s"2.0.0-1-$hash1-SNAPSHOT") } + + it("identifiers") { + val version = SnapshotVersion(1, 0, 0, Seq("rc.0"), isDirty = false, hashAndCount1, 1) + val bound = LowerBound(2, 0, 0) + val result = version.lowerBound(bound, hashAndCount1) + assert(result === SnapshotVersion(2, 0, 0, SemVerIdentifierList.empty, false, hashAndCount1, 1)) + } + } } diff --git a/src/test/scala/com/rallyhealth/sbt/versioning/ReleaseableSemanticVersionSpec.scala b/src/test/scala/com/rallyhealth/sbt/versioning/ReleaseableSemanticVersionSpec.scala new file mode 100644 index 0000000..ff7a8a6 --- /dev/null +++ b/src/test/scala/com/rallyhealth/sbt/versioning/ReleaseableSemanticVersionSpec.scala @@ -0,0 +1,76 @@ +package com.rallyhealth.sbt.versioning + +import com.rallyhealth.sbt.versioning.SemVerReleaseType.{Major, Minor, Patch, ReleaseableSemanticVersion} +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.{FunSpec, Matchers} + +class ReleaseableSemanticVersionSpec + extends FunSpec + with Matchers + with TypeCheckedTripleEquals { + + private val testCases = Seq( + TestCase( + name = "Clean snapshots should be bumped, for everything but patch", + version = SnapshotVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = false, "0123abc", 1), + expectedReleases = Seq( + Patch -> ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = false), + Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = false), + Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = false) + ) + ), + TestCase( + name = "Dirty snapshots should stay dirty", + version = SnapshotVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = true, "0123abc", 1), + expectedReleases = Seq( + Patch -> ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = true), + Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = true), + Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = true) + ) + ), + TestCase( + name = "Snapshot identifiers should be stripped", + version = SnapshotVersion(1, 2, 3, Seq("identifier"), isDirty = true, "0123abc", 1), + expectedReleases = Seq( + Patch -> ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = true), + Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = true), + Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = true) + ) + ), + TestCase( + name = "Release versions should be bumped", + version = ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = false), + expectedReleases = Seq( + Patch -> ReleaseVersion(1, 2, 4, SemVerIdentifierList.empty, isDirty = false), + Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = false), + Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = false) + ) + ), + TestCase( + name = "Release identifiers should be stripped", + version = ReleaseVersion(1, 2, 3, Seq("identifier"), isDirty = false), + expectedReleases = Seq( + Patch -> ReleaseVersion(1, 2, 4, SemVerIdentifierList.empty, isDirty = false), + Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = false), + Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = false) + ) + ) + ) + + for (tc <- testCases) { + describe(s"${tc.name} for ${tc.version.toString}") { + for ((releaseType, expected) <- tc.expectedReleases) { + it(s"$releaseType -> $expected") { + val actual = tc.version.release(releaseType) + assert(actual === expected) + } + } + } + } +} + +case class TestCase( + name: String, + version: SemanticVersion, + expectedReleases: Seq[(SemVerReleaseType, SemanticVersion)] +) diff --git a/src/test/scala/com/rallyhealth/sbt/versioning/SemVerReleaseTypeSpec.scala b/src/test/scala/com/rallyhealth/sbt/versioning/SemVerReleaseTypeSpec.scala index b7b98be..1111ceb 100644 --- a/src/test/scala/com/rallyhealth/sbt/versioning/SemVerReleaseTypeSpec.scala +++ b/src/test/scala/com/rallyhealth/sbt/versioning/SemVerReleaseTypeSpec.scala @@ -1,76 +1,25 @@ package com.rallyhealth.sbt.versioning -import com.rallyhealth.sbt.versioning.SemVerReleaseType.{Major, Minor, Patch, ReleaseableSemanticVersion} import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.{FunSpec, Matchers} +import org.scalatest.{Matchers, WordSpec} class SemVerReleaseTypeSpec - extends FunSpec + extends WordSpec with Matchers with TypeCheckedTripleEquals { - private val testCases = Seq( - TestCase( - name = "Clean snapshots should be bumped, for everything but patch", - version = SnapshotVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = false, "0123abc", 1), - expectedReleases = Seq( - Patch -> ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = false), - Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = false), - Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = false) - ) - ), - TestCase( - name = "Dirty snapshots should stay dirty", - version = SnapshotVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = true, "0123abc", 1), - expectedReleases = Seq( - Patch -> ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = true), - Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = true), - Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = true) - ) - ), - TestCase( - name = "Snapshot identifiers should be stripped", - version = SnapshotVersion(1, 2, 3, Seq("identifier"), isDirty = true, "0123abc", 1), - expectedReleases = Seq( - Patch -> ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = true), - Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = true), - Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = true) - ) - ), - TestCase( - name = "Release versions should be bumped", - version = ReleaseVersion(1, 2, 3, SemVerIdentifierList.empty, isDirty = false), - expectedReleases = Seq( - Patch -> ReleaseVersion(1, 2, 4, SemVerIdentifierList.empty, isDirty = false), - Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = false), - Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = false) - ) - ), - TestCase( - name = "Release identifiers should be stripped", - version = ReleaseVersion(1, 2, 3, Seq("identifier"), isDirty = false), - expectedReleases = Seq( - Patch -> ReleaseVersion(1, 2, 4, SemVerIdentifierList.empty, isDirty = false), - Minor -> ReleaseVersion(1, 3, 0, SemVerIdentifierList.empty, isDirty = false), - Major -> ReleaseVersion(2, 0, 0, SemVerIdentifierList.empty, isDirty = false) - ) - ) - ) + "Major > Minor > Patch" in { + assert(SemVerReleaseType.Major === SemVerReleaseType.Major) + assert(SemVerReleaseType.Major > SemVerReleaseType.Minor) + assert(SemVerReleaseType.Major > SemVerReleaseType.Patch) - for (tc <- testCases) { - describe(s"${tc.name} for ${tc.version.toString}") { - for ((releaseType, expected) <- tc.expectedReleases) { - it(s"$releaseType -> $expected") { - val actual = tc.version.release(releaseType) - assert(actual === expected) - } - } - } + assert(SemVerReleaseType.Minor < SemVerReleaseType.Major) + assert(SemVerReleaseType.Minor === SemVerReleaseType.Minor) + assert(SemVerReleaseType.Minor > SemVerReleaseType.Patch) + + assert(SemVerReleaseType.Patch < SemVerReleaseType.Major) + assert(SemVerReleaseType.Patch < SemVerReleaseType.Minor) + assert(SemVerReleaseType.Patch === SemVerReleaseType.Patch) } -} -case class TestCase( - name: String, - version: SemanticVersion, - expectedReleases: Seq[(SemVerReleaseType, SemanticVersion)] -) +} diff --git a/src/test/scala/com/rallyhealth/sbt/versioning/SemanticVersionSpec.scala b/src/test/scala/com/rallyhealth/sbt/versioning/SemanticVersionSpec.scala index 0cb110c..e5dfbf2 100644 --- a/src/test/scala/com/rallyhealth/sbt/versioning/SemanticVersionSpec.scala +++ b/src/test/scala/com/rallyhealth/sbt/versioning/SemanticVersionSpec.scala @@ -1,16 +1,12 @@ package com.rallyhealth.sbt.versioning -import com.rallyhealth.sbt.util.NullSbtLogger import com.rallyhealth.sbt.versioning.SemanticVersion._ import org.scalatest.FunSpec -import sbt.Logger import scala.util.Random class SemanticVersionSpec extends FunSpec { - implicit val logger: Logger = NullSbtLogger - describe("ReleaseVersion") { describe("dirty is false") { @@ -170,6 +166,12 @@ class SemanticVersionSpec extends FunSpec { assert(version.identifiers.values.map(_.toString) === Seq("rc1", "beta", "99", "1234567", "dirty", "SNAPSHOT")) assert(SnapshotVersion.regex.pattern.matcher(version.toString).matches, version.toString) } + + it("dirty snapshot vs release") { + val current = SemanticVersion.fromString("1.0.0-1-f650aaa-dirty-SNAPSHOT").get + val enforceAfterVersion = SemanticVersion.fromString("1.0.0").get + assert(current <= enforceAfterVersion) + } } describe("nextVersion()") {