diff --git a/README.md b/README.md index dff5d02..c386a88 100644 --- a/README.md +++ b/README.md @@ -97,20 +97,19 @@ If you're publishing to Sonatype sonashots then enable `dynverSonatypeSnapshots changes). This opt-in exists because the Sonatype's snapshots repository requires all versions to end with `-SNAPSHOT`. -## Custom version string - -Sometimes you want to customise the version string. It might be for personal preference, or for compatibility with another tool or spec. - -As an example, Docker rejects tags which include `+`'s ([#5](https://github.com/dwijnand/sbt-dynver/issues/5)). +## Docker compatible version strings -A simply way to solve this is to simply post-process the value of `version in ThisBuild` (and optionally `dynver in ThisBuild`), for example by replacing '+' with '-': +The default version string format includes `+` characters, which is not compatible with docker tags. This character can be overridden by setting: ```scala -version in ThisBuild ~= (_.replace('+', '-')) - dynver in ThisBuild ~= (_.replace('+', '-')) +dynverSeparator in ThisBuild := "-" ``` -If instead you want to completely customise the string format you can use `dynverGitDescribeOutput`, `dynverCurrentDate` and `sbtdynver.DynVer`, like so: +## Custom version string + +Sometimes you want to customise the version string. It might be for personal preference, or for compatibility with another tool or spec. + +To completely customise the string format you can use `dynverGitDescribeOutput`, `dynverCurrentDate` and `sbtdynver.DynVer`, like so: ```scala def versionFmt(out: sbtdynver.GitDescribeOutput): String = { diff --git a/src/main/scala/sbtdynver/DynVerPlugin.scala b/src/main/scala/sbtdynver/DynVerPlugin.scala index 29aa1d4..b33eade 100644 --- a/src/main/scala/sbtdynver/DynVerPlugin.scala +++ b/src/main/scala/sbtdynver/DynVerPlugin.scala @@ -21,6 +21,7 @@ object DynVerPlugin extends AutoPlugin { val dynverGitDescribeOutput = settingKey[Option[GitDescribeOutput]]("The output from git describe") val dynverSonatypeSnapshots = settingKey[Boolean]("Whether to append -SNAPSHOT to snapshot versions") val dynverGitPreviousStableVersion = settingKey[Option[GitDescribeOutput]]("The last stable tag") + val dynverSeparator = settingKey[String]("The separator to use between tag and distance, and the hash and dirty timestamp") val dynverCheckVersion = taskKey[Boolean]("Checks if version and dynver match") val dynverAssertVersion = taskKey[Unit]("Asserts if version and dynver match") val dynverAssertTagVersion = taskKey[Unit]("Asserts if the version derives from git tags") @@ -35,18 +36,20 @@ object DynVerPlugin extends AutoPlugin { version := { val out = dynverGitDescribeOutput.value val date = dynverCurrentDate.value - if (dynverSonatypeSnapshots.value) out.sonatypeVersion(date) - else out.version(date) + val separator = dynverSeparator.value + if (dynverSonatypeSnapshots.value) out.sonatypeVersion(date, separator) + else out.version(date, separator) }, isSnapshot := dynverGitDescribeOutput.value.isSnapshot, isVersionStable := dynverGitDescribeOutput.value.isVersionStable, previousStableVersion := dynverGitPreviousStableVersion.value.previousVersion, dynverCurrentDate := new Date, - dynverInstance := DynVer(Some((baseDirectory in ThisBuild).value)), + dynverInstance := DynVer(Some((baseDirectory in ThisBuild).value), dynverSeparator.value), dynverGitDescribeOutput := dynverInstance.value.getGitDescribeOutput(dynverCurrentDate.value), dynverSonatypeSnapshots := false, dynverGitPreviousStableVersion := dynverInstance.value.getGitPreviousStableTag, + dynverSeparator := DynVer.separator, dynver := { val dynver = dynverInstance.value @@ -72,7 +75,9 @@ object DynVerPlugin extends AutoPlugin { final case class GitRef(value: String) final case class GitCommitSuffix(distance: Int, sha: String) -final case class GitDirtySuffix(value: String) +final case class GitDirtySuffix(suffix: String) { + def value: String = if (suffix.isEmpty) "" else s"+$suffix" +} object GitRef extends (String => GitRef) { final implicit class GitRefOps(val x: GitRef) extends AnyVal { import x._ @@ -91,26 +96,32 @@ object GitCommitSuffix extends ((Int, String) => GitCommitSuffix) { } object GitDirtySuffix extends (String => GitDirtySuffix) { - final implicit class GitDirtySuffixOps(val x: GitDirtySuffix) extends AnyVal { import x._ - def dropPlus: GitDirtySuffix = GitDirtySuffix(value.replaceAll("^\\+", "")) - def mkString(prefix: String, suffix: String): String = if (value.isEmpty) "" else prefix + value + suffix + final implicit class GitDirtySuffixOps(val x: GitDirtySuffix) extends AnyVal { + def dropPlus: GitDirtySuffix = x + def mkString(prefix: String, suffix: String): String = if (x.suffix.isEmpty) "" else prefix + x.suffix + suffix + def asSuffix(separator: String): String = mkString(separator, "") } } final case class GitDescribeOutput(ref: GitRef, commitSuffix: GitCommitSuffix, dirtySuffix: GitDirtySuffix) { - def version: String = { - if (isCleanAfterTag) ref.dropV.value + dirtySuffix.value // no commit info if clean after tag - else if (commitSuffix.sha.nonEmpty) ref.dropV.value + "+" + commitSuffix.distance + "-" + commitSuffix.sha + dirtySuffix.value - else commitSuffix.distance + "-" + ref.value + dirtySuffix.value + + def version(separator: String): String = { + val ds = dirtySuffix.asSuffix(separator) + if (isCleanAfterTag) ref.dropV.value + ds // no commit info if clean after tag + else if (commitSuffix.sha.nonEmpty) ref.dropV.value + separator + commitSuffix.distance + "-" + commitSuffix.sha + ds + else commitSuffix.distance + "-" + ref.value + ds } + def sonatypeVersion(separator: String): String = + if (isSnapshot) version(separator) + "-SNAPSHOT" else version(separator) - def sonatypeVersion: String = if (isSnapshot) version + "-SNAPSHOT" else version + def version: String = version(DynVer.separator) + def sonatypeVersion: String = sonatypeVersion(DynVer.separator) def isSnapshot(): Boolean = hasNoTags() || !commitSuffix.isEmpty || isDirty() def previousVersion: String = ref.dropV.value def isVersionStable(): Boolean = !isDirty() def hasNoTags(): Boolean = !ref.isTag - def isDirty(): Boolean = dirtySuffix.value.nonEmpty + def isDirty(): Boolean = dirtySuffix.suffix.nonEmpty def isCleanAfterTag: Boolean = ref.isTag && commitSuffix.isEmpty && !isDirty() } @@ -121,7 +132,7 @@ object GitDescribeOutput extends ((GitRef, GitCommitSuffix, GitDirtySuffix) => G private val Sha = """([0-9a-f]{8})""".r private val HEAD = """HEAD""".r private val CommitSuffix = s"""($Distance-$Sha)""".r - private val TstampSuffix = """(\+[0-9]{8}-[0-9]{4})""".r + private val TstampSuffix = """(?:\+([0-9]{8}-[0-9]{4}))""".r private val FromTag = s"""^$OptWs$Tag$CommitSuffix?$TstampSuffix?$OptWs$$""".r private val FromSha = s"""^$OptWs$Sha$TstampSuffix?$OptWs$$""".r @@ -141,8 +152,13 @@ object GitDescribeOutput extends ((GitRef, GitCommitSuffix, GitDirtySuffix) => G implicit class OptGitDescribeOutputOps(val _x: Option[GitDescribeOutput]) extends AnyVal { def mkVersion(f: GitDescribeOutput => String, fallback: => String): String = _x.fold(fallback)(f) - def version(d: Date): String = mkVersion(_.version, DynVer fallback d) - def sonatypeVersion(d: Date): String = mkVersion(_.sonatypeVersion, DynVer fallback d) + def version(d: Date, separator: String): String = + mkVersion(_.version(separator), DynVer(None, separator) fallback d) + def sonatypeVersion(d: Date, separator: String): String = + mkVersion(_.sonatypeVersion(separator), DynVer(None, separator) fallback d) + + def version(d: Date): String = version(d, DynVer.separator) + def sonatypeVersion(d: Date): String = sonatypeVersion(d, DynVer.separator) def previousVersion: Option[String] = _x.map(_.previousVersion) def isSnapshot: Boolean = _x.forall(_.isSnapshot) def isVersionStable: Boolean = _x.exists(_.isVersionStable) @@ -153,14 +169,14 @@ object GitDescribeOutput extends ((GitRef, GitCommitSuffix, GitDirtySuffix) => G } // sealed just so the companion object can extend it. Shouldn't've been a case class. -sealed case class DynVer(wd: Option[File]) { - def version(d: Date): String = getGitDescribeOutput(d) version d - def sonatypeVersion(d: Date): String = getGitDescribeOutput(d) sonatypeVersion d +sealed case class DynVer(wd: Option[File], separator: String) { + def version(d: Date): String = getGitDescribeOutput(d).version(d, separator) + def sonatypeVersion(d: Date): String = getGitDescribeOutput(d).sonatypeVersion(d, separator) def previousVersion : Option[String] = getGitPreviousStableTag.previousVersion def isSnapshot(): Boolean = getGitDescribeOutput(new Date).isSnapshot def isVersionStable(): Boolean = getGitDescribeOutput(new Date).isVersionStable - def makeDynVer(d: Date): Option[String] = getGitDescribeOutput(d) map (_.version) + def makeDynVer(d: Date): Option[String] = getGitDescribeOutput(d) map (_.version(separator)) def isDirty(): Boolean = getGitDescribeOutput(new Date).isDirty def hasNoTags(): Boolean = getGitDescribeOutput(new Date).hasNoTags @@ -195,7 +211,7 @@ sealed case class DynVer(wd: Option[File]) { } def timestamp(d: Date): String = "%1$tY%1$tm%1$td-%1$tH%1$tM" format d - def fallback(d: Date): String = s"HEAD+${timestamp(d)}" + def fallback(d: Date): String = s"HEAD$separator${timestamp(d)}" private def execAndHandleEmptyOutput(cmd: String): Option[String] = { Try(Process(cmd, wd) !! impl.NoProcessLogger).toOption @@ -203,7 +219,11 @@ sealed case class DynVer(wd: Option[File]) { } } -object DynVer extends DynVer(None) with (Option[File] => DynVer) +object DynVer extends DynVer(None, "+") + with ((Option[File], String) => DynVer) + with (Option[File] => DynVer) { + override def apply(wd: Option[File]) = apply(wd, separator) +} object `package` diff --git a/src/sbt-test/dynver/distance-separator/build.sbt b/src/sbt-test/dynver/distance-separator/build.sbt new file mode 100644 index 0000000..da65698 --- /dev/null +++ b/src/sbt-test/dynver/distance-separator/build.sbt @@ -0,0 +1,53 @@ +import scala.sys.process.stringToProcess + +dynverSeparator in ThisBuild := "-" + +def tstamp = Def.setting(sbtdynver.DynVer timestamp dynverCurrentDate.value) +def headSha = { + implicit def log2log(log: Logger): scala.sys.process.ProcessLogger = sbtLoggerToScalaSysProcessLogger(log) + Def.task("git rev-parse --short=8 HEAD".!!(streams.value.log).trim) +} + +def check(a: String, e: String) = assert(a == e, s"Version mismatch: Expected $e, Incoming $a") + +TaskKey[Unit]("checkNotAGitRepo") := check(version.value, s"HEAD-${tstamp.value}") +TaskKey[Unit]("checkNoCommits") := check(version.value, s"HEAD-${tstamp.value}") +TaskKey[Unit]("checkOnCommit") := check(version.value, s"1-${headSha.value}") +TaskKey[Unit]("checkOnCommitDirty") := check(version.value, s"1-${headSha.value}-${tstamp.value}") +TaskKey[Unit]("checkOnTag") := check(version.value, s"1.0.0") +TaskKey[Unit]("checkOnTagDirty") := check(version.value, s"1.0.0-0-${headSha.value}-${tstamp.value}") +TaskKey[Unit]("checkOnTagAndCommit") := check(version.value, s"1.0.0-1-${headSha.value}") +TaskKey[Unit]("checkOnTagAndCommitDirty") := check(version.value, s"1.0.0-1-${headSha.value}-${tstamp.value}") + +TaskKey[Unit]("gitInitSetup") := { + implicit def log2log(log: Logger): scala.sys.process.ProcessLogger = sbtLoggerToScalaSysProcessLogger(log) + "git init".!!(streams.value.log) + "git config user.email dynver@mailinator.com".!!(streams.value.log) + "git config user.name dynver".!!(streams.value.log) +} + +TaskKey[Unit]("gitAdd") := { + implicit def log2log(log: Logger): scala.sys.process.ProcessLogger = sbtLoggerToScalaSysProcessLogger(log) + "git add .".!!(streams.value.log) +} +TaskKey[Unit]("gitCommit") := { + implicit def log2log(log: Logger): scala.sys.process.ProcessLogger = sbtLoggerToScalaSysProcessLogger(log) + "git commit -am1".!!(streams.value.log) +} +TaskKey[Unit]("gitTag") := { + implicit def log2log(log: Logger): scala.sys.process.ProcessLogger = sbtLoggerToScalaSysProcessLogger(log) + "git tag -a v1.0.0 -m1.0.0".!!(streams.value.log) +} + +TaskKey[Unit]("dirty") := { + import java.nio.file._, StandardOpenOption._ + import scala.collection.JavaConverters._ + Files.write(baseDirectory.value.toPath.resolve("f.txt"), Seq("1").asJava, CREATE, APPEND) +} + +def sbtLoggerToScalaSysProcessLogger(log: Logger): scala.sys.process.ProcessLogger = + new scala.sys.process.ProcessLogger { + def buffer[T](f: => T): T = f + def err(s: => String): Unit = log info s + def out(s: => String): Unit = log error s + } diff --git a/src/sbt-test/dynver/distance-separator/project/plugins.sbt b/src/sbt-test/dynver/distance-separator/project/plugins.sbt new file mode 100644 index 0000000..66ad543 --- /dev/null +++ b/src/sbt-test/dynver/distance-separator/project/plugins.sbt @@ -0,0 +1,5 @@ +sys.props.get("plugin.version") match { + case Some(x) => addSbtPlugin("com.dwijnand" % "sbt-dynver" % x) + case _ => sys.error("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} diff --git a/src/sbt-test/dynver/distance-separator/test b/src/sbt-test/dynver/distance-separator/test new file mode 100644 index 0000000..9f22a4b --- /dev/null +++ b/src/sbt-test/dynver/distance-separator/test @@ -0,0 +1,32 @@ +> checkNotAGitRepo + +> gitInitSetup +> reload +> checkNoCommits + +> dirty +> gitAdd +> gitCommit +> reload +> checkOnCommit + +> dirty +> reload +> checkOnCommitDirty + +> gitCommit +> gitTag +> reload +> checkOnTag + +> dirty +> reload +> checkOnTagDirty + +> gitCommit +> reload +> checkOnTagAndCommit + +> dirty +> reload +> checkOnTagAndCommitDirty diff --git a/src/test/scala/sbtdynver/GH020.scala b/src/test/scala/sbtdynver/GH020.scala index 49e5178..acb91af 100644 --- a/src/test/scala/sbtdynver/GH020.scala +++ b/src/test/scala/sbtdynver/GH020.scala @@ -5,5 +5,5 @@ import org.scalacheck._, Prop._ object GH020 extends Properties("GH020") { property("Handles CF+LF (Windows)") = GitDescribeOutput.parse("v0.7.0+3-e7a84ebc+20161120-1948\r\n") ?= - GitDescribeOutput(GitRef("v0.7.0"), GitCommitSuffix(3, "e7a84ebc"), GitDirtySuffix("+20161120-1948")) + GitDescribeOutput(GitRef("v0.7.0"), GitCommitSuffix(3, "e7a84ebc"), GitDirtySuffix("20161120-1948")) } diff --git a/src/test/scala/sbtdynver/GitDescribeOutputSpec.scala b/src/test/scala/sbtdynver/GitDescribeOutputSpec.scala index ecc5c48..4b6efee 100644 --- a/src/test/scala/sbtdynver/GitDescribeOutputSpec.scala +++ b/src/test/scala/sbtdynver/GitDescribeOutputSpec.scala @@ -5,12 +5,12 @@ import org.scalacheck._, Prop._ object GitDescribeOutputSpec extends Properties("GitDescribeOutputSpec") { test("v1.0.0", "v1.0.0", 0, "", "" ) - test("v1.0.0+20140707-1030", "v1.0.0", 0, "", "+20140707-1030") + test("v1.0.0+20140707-1030", "v1.0.0", 0, "", "20140707-1030") test("v1.0.0+3-1234abcd", "v1.0.0", 3, "1234abcd", "" ) - test("v1.0.0+3-1234abcd+20140707-1030", "v1.0.0", 3, "1234abcd", "+20140707-1030") + test("v1.0.0+3-1234abcd+20140707-1030", "v1.0.0", 3, "1234abcd", "20140707-1030") test("1234abcd", "1234abcd", 0, "", "" ) - test("1234abcd+20140707-1030", "1234abcd", 0, "", "+20140707-1030") - test("HEAD+20140707-1030", "HEAD", 0, "", "+20140707-1030") + test("1234abcd+20140707-1030", "1234abcd", 0, "", "20140707-1030") + test("HEAD+20140707-1030", "HEAD", 0, "", "20140707-1030") def test(v: String, ref: String, dist: Int, sha: String, dirtySuffix: String) = { val out = GitDescribeOutput(GitRef(ref), GitCommitSuffix(dist, sha), GitDirtySuffix(dirtySuffix)) diff --git a/src/test/scala/sbtdynver/RepoStates.scala b/src/test/scala/sbtdynver/RepoStates.scala index 32cf81e..1659da5 100644 --- a/src/test/scala/sbtdynver/RepoStates.scala +++ b/src/test/scala/sbtdynver/RepoStates.scala @@ -25,7 +25,7 @@ object RepoStates { final case class State() { val dir = doto(Files.createTempDirectory(s"dynver-test-").toFile)(_.deleteOnExit()) val date = new GregorianCalendar(2016, 8, 17).getTime - val dynver = DynVer(Some(dir)) + val dynver = DynVer(Some(dir), "+") var git: Git = _ var sha: String = "undefined"