Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow arbitrary prefixes for tags #158

Merged
merged 4 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ or change the value of `dynverVTagPrefix in ThisBuild` to remove the requirement

dynverVTagPrefix in ThisBuild := false

or, more generally, use `dynverTagPrefix in ThisBuild` to fully customising tag prefixes, for example:

dynverTagPrefix in ThisBuild := "foo-" // our tags have the format foo-<version>, e.g. foo-1.2.3

## Tasks

* `dynver`: Returns the dynamic version of your project, inferred from the git metadata
Expand Down Expand Up @@ -135,8 +139,8 @@ To completely customise the string format you can use `dynverGitDescribeOutput`,
```scala
def versionFmt(out: sbtdynver.GitDescribeOutput): String = {
val dirtySuffix = out.dirtySuffix.dropPlus.mkString("-", "")
if (out.isCleanAfterTag) out.ref.dropV.value + dirtySuffix // no commit info if clean after tag
else out.ref.dropV.value + out.commitSuffix.mkString("-", "-", "") + dirtySuffix
if (out.isCleanAfterTag) out.ref.dropPrefix + dirtySuffix // no commit info if clean after tag
else out.ref.dropPrefix + out.commitSuffix.mkString("-", "-", "") + dirtySuffix
}

def fallbackVersion(d: java.util.Date): String = s"HEAD-${sbtdynver.DynVer timestamp d}"
Expand Down
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ mimaBinaryIssueFilters ++= Seq(
// private[sbtdynver]
exclude[DirectMissingMethodProblem]("sbtdynver.GitDescribeOutput.parse"),
// Migrated from a task key to an initialise
exclude[IncompatibleResultTypeProblem]("sbtdynver.DynVerPlugin#autoImport.dynverAssertTagVersion")
exclude[IncompatibleResultTypeProblem]("sbtdynver.DynVerPlugin#autoImport.dynverAssertTagVersion"),
// GitDescribeOutput#Parser is private[sbtdynver]
exclude[Problem]("sbtdynver.GitDescribeOutput#Parser*"),
)

// TaskKey[Unit]("verify") := Def.sequential(test in Test, scripted.toTask(""), mimaReportBinaryIssues).value
Expand Down
80 changes: 56 additions & 24 deletions src/main/scala/sbtdynver/DynVerPlugin.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package sbtdynver

import java.util._
import java.util._, regex.Pattern

import scala.{ PartialFunction => ?=> }
import scala.util._
Expand All @@ -22,6 +22,7 @@ object DynVerPlugin extends AutoPlugin {
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 dynverTagPrefix = settingKey[String]("The prefix to use when matching the version tag")
val dynverVTagPrefix = settingKey[Boolean]("Whether or not tags have a 'v' prefix")
val dynverCheckVersion = taskKey[Boolean]("Checks if version and dynver match")
val dynverAssertVersion = taskKey[Unit]("Asserts if version and dynver match")
Expand Down Expand Up @@ -53,13 +54,20 @@ object DynVerPlugin extends AutoPlugin {
isVersionStable := dynverGitDescribeOutput.value.isVersionStable,
previousStableVersion := dynverGitPreviousStableVersion.value.previousVersion,

dynverInstance := {
val tagPrefix = dynverTagPrefix.value
val vTagPrefix = dynverVTagPrefix.value
assert(vTagPrefix ^ tagPrefix != "v", s"Incoherence: dynverTagPrefix=$tagPrefix vs dynverVTagPrefix=$vTagPrefix")
DynVer(Some(buildBase.value), dynverSeparator.value, tagPrefix)
},

dynverCurrentDate := new Date,
dynverInstance := DynVer(Some(buildBase.value), dynverSeparator.value, dynverVTagPrefix.value),
dynverGitDescribeOutput := dynverInstance.value.getGitDescribeOutput(dynverCurrentDate.value),
dynverSonatypeSnapshots := false,
dynverGitPreviousStableVersion := dynverInstance.value.getGitPreviousStableTag,
dynverSeparator := DynVer.separator,
dynverVTagPrefix := DynVer.vTagPrefix,
dynverTagPrefix := DynVer.tagPrefix,
dynverVTagPrefix := dynverTagPrefix.value == "v",

dynver := {
val dynver = dynverInstance.value
Expand All @@ -78,15 +86,27 @@ object DynVerPlugin extends AutoPlugin {
private val buildBase = baseDirectory in ThisBuild
}

final case class GitRef(value: String)
sealed case class GitRef(value: String)
final case class GitCommitSuffix(distance: Int, sha: String)
final case class GitDirtySuffix(value: String)

private final class GitTag(value: String, val prefix: String) extends GitRef(value) {
override def toString = s"GitTag($value, prefix=$prefix)"
}

object GitRef extends (String => GitRef) {
final implicit class GitRefOps(val x: GitRef) extends AnyVal { import x._
def isTag: Boolean = value startsWith "v"
def dropV: GitRef = GitRef(value.replaceAll("^v", ""))
private def prefix = x match {
case x: GitTag => x.prefix
case _ => DynVer.tagPrefix
}

def isTag: Boolean = value.startsWith(prefix)
def dropPrefix: String = value.stripPrefix(prefix)
def mkString(prefix: String, suffix: String): String = if (value.isEmpty) "" else prefix + value + suffix

@deprecated("Generalised to all prefixes, use dropPrefix (note it returns just the string)", "4.1.0")
def dropV: GitRef = if (value.startsWith("v")) GitRef(value.stripPrefix("v")) else x
}
}

Expand All @@ -109,8 +129,8 @@ object GitDirtySuffix extends (String => GitDirtySuffix) {
final case class GitDescribeOutput(ref: GitRef, commitSuffix: GitCommitSuffix, dirtySuffix: GitDirtySuffix) {
def version(sep: String): String = {
val dirtySuffix = this.dirtySuffix.withSeparator(sep)
if (isCleanAfterTag) ref.dropV.value + dirtySuffix // no commit info if clean after tag
else if (commitSuffix.sha.nonEmpty) ref.dropV.value + sep + commitSuffix.distance + "-" + commitSuffix.sha + dirtySuffix
if (isCleanAfterTag) ref.dropPrefix + dirtySuffix // no commit info if clean after tag
else if (commitSuffix.sha.nonEmpty) ref.dropPrefix + sep + commitSuffix.distance + "-" + commitSuffix.sha + dirtySuffix
else "0.0.0" + sep + commitSuffix.distance + "-" + ref.value + dirtySuffix
}

Expand All @@ -120,7 +140,7 @@ final case class GitDescribeOutput(ref: GitRef, commitSuffix: GitCommitSuffix, d
def sonatypeVersion: String = sonatypeVersion(DynVer.separator)

def isSnapshot(): Boolean = hasNoTags() || !commitSuffix.isEmpty || isDirty()
def previousVersion: String = ref.dropV.value
def previousVersion: String = ref.dropPrefix
def isVersionStable(): Boolean = !isDirty()

def hasNoTags(): Boolean = !ref.isTag
Expand All @@ -136,25 +156,33 @@ object GitDescribeOutput extends ((GitRef, GitCommitSuffix, GitDirtySuffix) => G
private val CommitSuffix = s"""($Distance-$Sha)""".r
private val TstampSuffix = """(\+[0-9]{8}-[0-9]{4})""".r

private[sbtdynver] final class Parser(vTagPrefix: Boolean) {
private val Tag = (if (vTagPrefix) """(v[0-9][^+]*?)""" else """([0-9]+\.[^+]*?)""").r
private[sbtdynver] final class Parser(tagPrefix: String) {
private val tagBody = tagPrefix match {
case "" => """([0-9]+\.[^+]*?)""" // Use a dot to distinguish tags for SHAs...
case _ => """([0-9]+[^+]*?)""" // ... but not when there's a prefix (e.g. v2 is a tag)
}
private val Tag = s"${Pattern.quote(tagPrefix)}$tagBody".r // quote the prefix so it doesn't interact

private val FromTag = s"""^$OptWs$Tag$CommitSuffix?$TstampSuffix?$OptWs$$""".r
private val FromSha = s"""^$OptWs$Sha$TstampSuffix?$OptWs$$""".r
private val FromHead = s"""^$OptWs$HEAD$TstampSuffix$OptWs$$""".r

private[sbtdynver] def parse: String ?=> GitDescribeOutput = {
case FromTag(tag, _, dist, sha, dirty) => parse0(v(tag), dist, sha, dirty)
case FromSha(sha, dirty) => parse0( sha, "0", "", dirty)
case FromHead(dirty) => parse0("HEAD", "0", "", dirty)
case FromTag(tag, _, dist, sha, dirty) => parseWithTag(tag, dist, sha, dirty)
case FromSha(sha, dirty) => parseWithRef(sha, dirty)
case FromHead(dirty) => parseWithRef("HEAD", dirty)
}

// If tags aren't v-prefixed, add the v back, so the rest of dynver works (e.g. GitRef#isTag)
private def v(s: String) = if (vTagPrefix) s else s"v$s"

private def parse0(ref: String, dist: String, sha: String, dirty: String) = {
private def parseWithTag(tag: String, dist: String, sha: String, dirty: String) = {
// the "value" of the GitTag is the entire string section, with the prefix
// but also keep the, user-customisable, prefix so dropPrefix knows what to drop
val gitTag = new GitTag(tagPrefix + tag, tagPrefix)
val commit = if (dist == null || sha == null) GitCommitSuffix(0, "") else GitCommitSuffix(dist.toInt, sha)
GitDescribeOutput(GitRef(ref), commit, GitDirtySuffix(if (dirty eq null) "" else dirty))
GitDescribeOutput(gitTag, commit, GitDirtySuffix(if (dirty eq null) "" else dirty))
}

private def parseWithRef(ref: String, dirty: String) = {
GitDescribeOutput(GitRef(ref), GitCommitSuffix(0, ""), GitDirtySuffix(if (dirty eq null) "" else dirty))
}
}

Expand All @@ -181,12 +209,15 @@ 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], separator: String, vTagPrefix: Boolean) {
sealed case class DynVer(wd: Option[File], separator: String, tagPrefix: String) {
private def this(wd: Option[File], separator: String, vTagPrefix: Boolean) = this(wd, separator, if (vTagPrefix) "v" else "")
private def this(wd: Option[File], separator: String) = this(wd, separator, true)
private def this(wd: Option[File]) = this(wd, "+")

private val TagPattern = if (vTagPrefix) "v[0-9]*" else "[0-9]*"
private[sbtdynver] val parser = new GitDescribeOutput.Parser(vTagPrefix)
def vTagPrefix = tagPrefix == "v" // bincompat

private val TagPattern = s"$tagPrefix[0-9]*" // used by `git describe` to filter the tags
private[sbtdynver] val parser = new GitDescribeOutput.Parser(tagPrefix) // .. then parsed back

def version(d: Date): String = getGitDescribeOutput(d).versionWithSep(d, separator)
def sonatypeVersion(d: Date): String = getGitDescribeOutput(d).sonatypeVersionWithSep(d, separator)
Expand Down Expand Up @@ -236,11 +267,12 @@ sealed case class DynVer(wd: Option[File], separator: String, vTagPrefix: Boolea
.filter(_.trim.nonEmpty)
}

def copy(wd: Option[File] = wd): DynVer = new DynVer(wd, separator)
def copy(wd: Option[File] = wd): DynVer = new DynVer(wd, separator, tagPrefix)
}

object DynVer extends DynVer(None) with (Option[File] => DynVer) {
override def apply(wd: Option[File]) = apply(wd, separator, vTagPrefix)
override def apply(wd: Option[File]) = new DynVer(wd)
def apply(wd: Option[File], separator: String, vTagPrefix: Boolean) = new DynVer(wd, separator, vTagPrefix) // bincompat
}

object `package`
Expand Down
54 changes: 54 additions & 0 deletions src/sbt-test/dynver/multi-project/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import scala.sys.process.stringToProcess

dynverTagPrefix in ThisBuild := "bar-v"

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]("checkOnTagFoo") := check(version.value, s"0.0.0+1-${headSha.value}")
TaskKey[Unit]("checkOnTagBar") := check(version.value, "2.0.0")
TaskKey[Unit]("checkOnTagBarDirty") := check(version.value, s"2.0.0+0-${headSha.value}+${tstamp.value}")
TaskKey[Unit]("checkOnTagBarAndCommit") := check(version.value, s"2.0.0+1-${headSha.value}")
TaskKey[Unit]("checkOnTagBarAndCommitDirty") := check(version.value, s"2.0.0+1-${headSha.value}+${tstamp.value}")

import sbt.complete.DefaultParsers._

def exec(cmd: String, streams: TaskStreams): String = {
import scala.sys.process._
implicit def log2log(log: Logger): ProcessLogger = sbtLoggerToScalaSysProcessLogger(log)
cmd !! streams.log
}

val dirParser = Def setting fileParser(baseDirectory.value)
val tagParser = Def setting (Space ~> StringBasic)

InputKey[Unit]("gitInitSetup") := {
exec("git init .", streams.value)
exec("git config user.email [email protected]", streams.value)
exec("git config user.name dynver", streams.value)
IO.writeLines(baseDirectory.value / ".gitignore", Seq("target/"))
}
InputKey[Unit]("gitAdd") := exec("git add .", streams.value)
InputKey[Unit]("gitCommit") := exec("git commit -am1", streams.value)
InputKey[Unit]("gitTag") := {
val tag = tagParser.value.parsed
exec(s"git tag -a $tag -m '$tag'", streams.value)
}

InputKey[Unit]("dirty") := {
import java.nio.file._, StandardOpenOption._
import scala.collection.JavaConverters._
Files.write(dirParser.value.parsed.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
}
5 changes: 5 additions & 0 deletions src/sbt-test/dynver/multi-project/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -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)
}
28 changes: 28 additions & 0 deletions src/sbt-test/dynver/multi-project/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
> gitInitSetup
> dirty .
> gitAdd
> gitCommit
> gitTag foo-v1.0.0

> reload
> checkOnTagFoo

> dirty .
> gitAdd
> gitCommit
> gitTag bar-v2.0.0

> reload
> checkOnTagBar

> dirty .
> reload
> checkOnTagBarDirty

> gitCommit
> reload
> checkOnTagBarAndCommit

> dirty .
> reload
> checkOnTagBarAndCommitDirty
12 changes: 6 additions & 6 deletions src/test/scala/sbtdynver/RepoStates.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@ import scala.collection.JavaConverters._
import org.eclipse.jgit.api._
import org.eclipse.jgit.merge.MergeStrategy

object RepoStates extends RepoStates(vTagPrefix = true)
object RepoStates extends RepoStates(tagPrefix = "v")

sealed class RepoStates(vTagPrefix: Boolean) {
sealed class RepoStates(tagPrefix: String) {
def notAGitRepo() = new State()
def noCommits() = notAGitRepo().init()
def onCommit() = noCommits().commit().commit().commit()
def onCommitDirty() = onCommit().dirty()
def onTag(n: String = "1.0.0") = onCommit().tag(vOptPrefix(n))
def onTag(n: String = "1.0.0") = onCommit().tag(optPrefix(n))
def onTagDirty() = onTag().dirty()
def onTagAndCommit() = onTag().commit()
def onTagAndCommitDirty() = onTagAndCommit().dirty()
def onTagAndSecondCommit() = onTagAndCommitDirty().commit()
def onSecondTag(n: String = "2.0.0") = onTagAndSecondCommit().tag(vOptPrefix(n))
def onSecondTag(n: String = "2.0.0") = onTagAndSecondCommit().tag(optPrefix(n))

private def vOptPrefix(s: String) = if (s.startsWith("v")) s else if (vTagPrefix) s"v$s" else s
private def optPrefix(s: String) = if (s.startsWith(tagPrefix)) s else s"$tagPrefix$s"

final 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), DynVer.separator, vTagPrefix)
val dynver = DynVer(Some(dir), DynVer.separator, tagPrefix)

var git: Git = _
var sha: String = "undefined"
Expand Down
18 changes: 14 additions & 4 deletions src/test/scala/sbtdynver/TagPatternSpec.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package sbtdynver

import org.scalacheck._, Prop._
import org.scalacheck._, Prop._, util.Pretty, Pretty.pretty

object TagPatternVersionSpec extends Properties("TagPatternVersionSpec") {
val repoStates = new RepoStates(vTagPrefix = false)
val repoStates = new RepoStates(tagPrefix = "")
import repoStates._

property("not a git repo") = notAGitRepo().version() ?= "HEAD+20160917-0000"
property("no commits") = noCommits().version() ?= "HEAD+20160917-0000"
property("on commit, w/o local changes") = onCommit().version() ?= "0.0.0+3-1234abcd"
property("on commit with local changes") = onCommitDirty().version() ?= "0.0.0+3-1234abcd+20160917-0000"
property("on commit, w/o local changes") = onCommit().version() ??= "0.0.0+3-1234abcd"
property("on commit with local changes") = onCommitDirty().version() ??= "0.0.0+3-1234abcd+20160917-0000"
property("on tag 1.0.0, w/o local changes") = onTag().version() ?= "1.0.0"
property("on tag 1.0.0 with local changes") = onTagDirty().version() ?= "1.0.0+0-1234abcd+20160917-0000"
property("on tag 1.0.0 and 1 commit, w/o local changes") = onTagAndCommit().version() ?= "1.0.0+1-1234abcd"
property("on tag 1.0.0 and 1 commit with local changes") = onTagAndCommitDirty().version() ?= "1.0.0+1-1234abcd+20160917-0000"

implicit class LazyAnyOps[A](x: => A)(implicit ev: A => Pretty) {
def ??=(y: A) = {
if (x == y) passed // the standard "?=" uses "proved" while we want to run multiple times
else falsified :| s"Expected ${pretty(y)} but got ${pretty(x)}"
}
}

override def overrideParameters(p: Test.Parameters) =
p.withMinSuccessfulTests(3) // .. but not 100 times!
}