Skip to content

Commit

Permalink
Merge pull request #195 from dwijnand/cli-prep
Browse files Browse the repository at this point in the history
  • Loading branch information
dwijnand authored Jan 14, 2021
2 parents 961a143 + 145ec93 commit 5214ab8
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 311 deletions.
29 changes: 13 additions & 16 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ val dynver = project.settings(
publishSettings,
)

import com.typesafe.tools.mima.core._, ProblemFilters._
val sbtdynver = project.dependsOn(dynverP).enablePlugins(SbtPlugin).settings(
name := "sbt-dynver",

Expand All @@ -53,26 +52,24 @@ lazy val publishSettings = Def.settings(
bintrayRepository := "sbt-plugins",
)

import com.typesafe.tools.mima.core._
lazy val mimaSettings = Seq(
mimaPreviousArtifacts := Set.empty, // Set(projectID.value.withRevision("4.0.0")),
mimaPreviousArtifacts := Set(projID.value.withRevision("5.0.0-M2")),
mimaBinaryIssueFilters ++= Seq(
// Migrated from a task key to a setting key
exclude[IncompatibleResultTypeProblem]("sbtdynver.DynVerPlugin#autoImport.isVersionStable"),
// private[sbtdynver]
exclude[DirectMissingMethodProblem]("sbtdynver.GitDescribeOutput.parse"),
// Migrated from a task key to an initialise
exclude[IncompatibleResultTypeProblem]("sbtdynver.DynVerPlugin#autoImport.dynverAssertTagVersion"),
// GitDescribeOutput#Parser is private[sbtdynver]
exclude[Problem]("sbtdynver.GitDescribeOutput#Parser*"),
// lightbend/mima#388
// static method requires()sbt.Plugins in class sbtdynver.DynVerPlugin does not have a correspondent in current version
exclude[DirectMissingMethodProblem]("sbtdynver.DynVerPlugin.requires"),
ProblemFilters.exclude[Problem]("*.impl.*"), // impl is for internal implementation details
),
)

mimaPreviousArtifacts := Set.empty
publish / skip := true
lazy val projID = Def.setting {
// Using projectID something is wrong... Looks for dynver_2.12 but artifacts are name=dynver
val sbtBv = (pluginCrossBuild / sbtBinaryVersion).value
val sbv = (pluginCrossBuild / scalaBinaryVersion).value
val mid = organization.value %% moduleName.value % "0.0.0"
if (sbtPlugin.value) Defaults.sbtPluginExtra(mid, sbtBv, sbv) else mid
}

Global / cancelable := true
mimaPreviousArtifacts := Set.empty
publish / skip := true

Global / excludeLintKeys += crossSbtVersions // Used by the "^" command (PluginCrossCommand)
Global / cancelable := true
12 changes: 12 additions & 0 deletions dynver/src/main/scala/dynver/impl/NoProcessLogger.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dynver
package impl

import scala.sys.process.ProcessLogger

object NoProcessLogger extends ProcessLogger {
def info(s: => String) = ()
def out(s: => String) = ()
def error(s: => String) = ()
def err(s: => String) = ()
def buffer[T](f: => T) = f
}
142 changes: 68 additions & 74 deletions dynver/src/main/scala/sbtdynver/DynVer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,27 @@ import java.util._, regex.Pattern
import scala.{ PartialFunction => ?=> }
import scala.util._

import scala.sys.process.{ Process, ProcessLogger }
import scala.sys.process.Process

import dynver._, impl.NoProcessLogger

sealed case class GitRef(value: String)
final case class GitCommitSuffix(distance: Int, sha: String)
final case class GitDirtySuffix(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._
private def prefix = x match {
case x: GitTag => x.prefix
case _ => DynVer.tagPrefix
}
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

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 @@ -35,47 +36,48 @@ object GitRef extends (String => GitRef) {
object GitCommitSuffix extends ((Int, String) => GitCommitSuffix) {
final implicit class GitCommitSuffixOps(val x: GitCommitSuffix) extends AnyVal { import x._
def isEmpty: Boolean = distance <= 0 || sha.isEmpty

def mkString(prefix: String, infix: String, suffix: String): String =
if (sha.isEmpty) "" else prefix + distance + infix + sha + suffix
}
}

object GitDirtySuffix extends (String => GitDirtySuffix) {
final implicit class GitDirtySuffixOps(val x: GitDirtySuffix) extends AnyVal { import x._
def dropPlus: GitDirtySuffix = GitDirtySuffix(value.replaceAll("^\\+", ""))
def dropPlus: GitDirtySuffix = GitDirtySuffix(value.replaceAll("^\\+", ""))
def withSeparator(separator: String): String = dropPlus.mkString(separator, "")
def mkString(prefix: String, suffix: String): String = if (value.isEmpty) "" else prefix + value + suffix

def mkString(prefix: String, suffix: String): String =
if (value.isEmpty) "" else prefix + value + suffix
}
}

final case class GitDescribeOutput(ref: GitRef, commitSuffix: GitCommitSuffix, dirtySuffix: GitDirtySuffix) {
def version(sep: String): String = {
val dirtySuffix = this.dirtySuffix.withSeparator(sep)
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
if (isCleanAfterTag) ref.dropPrefix
else ref.dropPrefix + commitSuffix.mkString(sep, "-", "") + dirtySuffix.withSeparator(sep)
}

def sonatypeVersion(sep: String): String = if (isSnapshot) version(sep) + "-SNAPSHOT" else version(sep)
def previousVersion: String = ref.dropPrefix

def version: String = version(DynVer.separator)
def sonatypeVersion: String = sonatypeVersion(DynVer.separator)
def version: String = version(DynVer.separator)
def sonatypeVersion: String = sonatypeVersion(DynVer.separator)

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

def hasNoTags(): Boolean = !ref.isTag
def isDirty(): Boolean = dirtySuffix.value.nonEmpty
def isCleanAfterTag: Boolean = ref.isTag && commitSuffix.isEmpty && !isDirty()
def isVersionStable(): Boolean = !isDirty()
def isSnapshot(): Boolean = hasNoTags() || !commitSuffix.isEmpty || isDirty()
def isCleanAfterTag: Boolean = ref.isTag && commitSuffix.isEmpty && !isDirty()
}

object GitDescribeOutput extends ((GitRef, GitCommitSuffix, GitDirtySuffix) => GitDescribeOutput) {
private val OptWs = """[\s\n]*""" // doesn't \s include \n? why can't this call .r?
private val Distance = """\+([0-9]+)""".r
private val Sha = """([0-9a-f]{8})""".r
private val HEAD = """HEAD""".r
private val CommitSuffix = s"""($Distance-$Sha)""".r
private val CommitSuffix = s"""(?:$Distance-$Sha)""".r
private val TstampSuffix = """(\+[0-9]{8}-[0-9]{4})""".r

private[sbtdynver] final class Parser(tagPrefix: String) {
Expand All @@ -90,84 +92,86 @@ object GitDescribeOutput extends ((GitRef, GitCommitSuffix, GitDirtySuffix) => G
private val FromHead = s"""^$OptWs$HEAD$TstampSuffix$OptWs$$""".r

private[sbtdynver] def parse: String ?=> GitDescribeOutput = {
case FromTag(tag, _, dist, sha, dirty) => parseWithTag(tag, dist, sha, dirty)
case FromSha(sha, dirty) => parseWithRef(sha, dirty)
case FromHead(dirty) => parseWithRef("HEAD", dirty)
case FromTag(tag, null, null, dirty) => mk(mkTag(tag), GitCommitSuffix(0, ""), GitDirtySuffix(if (dirty == null) "" else dirty))
case FromTag(tag, dist, sha, dirty) => mk(mkTag(tag), GitCommitSuffix(dist.toInt, sha), GitDirtySuffix(if (dirty == null) "" else dirty))
case FromSha(sha, dirty) => mk(GitRef(sha), GitCommitSuffix(0, ""), GitDirtySuffix(if (dirty == null) "" else dirty))
case FromHead( dirty) => mk(GitRef("HEAD"), GitCommitSuffix(0, ""), GitDirtySuffix(dirty))
}

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(gitTag, commit, GitDirtySuffix(if (dirty eq null) "" else dirty))
}
private def mkTag(tag: String) = new GitTag(tagPrefix + tag, tagPrefix)

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

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 = versionWithSep(d, DynVer.separator)
def sonatypeVersion(d: Date): String = sonatypeVersionWithSep(d, DynVer.separator)
def getVersion(date: Date, separator: String, sonatypeSnapshots: Boolean): String =
if (sonatypeSnapshots) sonatypeVersionWithSep(date, separator) else versionWithSep(date, separator)

// overloading isn't bincompat :O
def versionWithSep(d: Date, sep: String): String = mkVersion(_.version(sep), fallback(sep, d))
def versionWithSep(d: Date, sep: String): String = mkVersion(_ .version(sep), fallback(sep, d))
def sonatypeVersionWithSep(d: Date, sep: String): String = mkVersion(_.sonatypeVersion(sep), fallback(sep, d))

def previousVersion: Option[String] = _x.map(_.previousVersion)
def isSnapshot: Boolean = _x.forall(_.isSnapshot)
def isVersionStable: Boolean = _x.exists(_.isVersionStable)
def version(d: Date): String = versionWithSep(d, DynVer.separator)
def sonatypeVersion(d: Date): String = sonatypeVersionWithSep(d, DynVer.separator)

def isDirty: Boolean = _x.fold(true)(_.isDirty())
def hasNoTags: Boolean = _x.fold(true)(_.hasNoTags())
def previousVersion: Option[String] = _x.map(_.previousVersion)

def hasNoTags: Boolean = _x.fold(true)(_.hasNoTags())
def isDirty: Boolean = _x.fold(true)(_.isDirty())

def isSnapshot: Boolean = _x.forall(_.isSnapshot)
def isVersionStable: Boolean = _x.exists(_.isVersionStable)

def assertTagVersion(version: String): Unit =
if (hasNoTags)
throw new RuntimeException(
s"Failed to derive version from git tags. Maybe run `git fetch --unshallow`? Version: $version"
)
}

private[sbtdynver] def timestamp(d: Date): String = "%1$tY%1$tm%1$td-%1$tH%1$tM" format d
private[sbtdynver] def timestamp(d: Date): String = f"$d%tY$d%tm$d%td-$d%tH$d%tM"
private[sbtdynver] def fallback(separator: String, d: Date) = s"HEAD$separator${timestamp(d)}"
}

// 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, 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, "+")

def vTagPrefix = tagPrefix == "v" // bincompat
def vTagPrefix = tagPrefix == "v"

private val TagPattern = s"$tagPrefix[0-9]*" // used by `git describe` to filter the tags
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 version(d: Date): String = getGitDescribeOutput(d).versionWithSep(d, separator)
def sonatypeVersion(d: Date): String = getGitDescribeOutput(d).sonatypeVersionWithSep(d, separator)
def previousVersion : Option[String] = getGitPreviousStableTag.previousVersion
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(separator))
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

def getDistanceToFirstCommit(): Option[Int] = {
val process = Process(s"git rev-list --count HEAD", wd)
Try(process !! impl.NoProcessLogger).toOption
.map(_.trim.toInt)
Try(process !! NoProcessLogger).toOption.map(_.trim.toInt)
}

def getGitDescribeOutput(d: Date): Option[GitDescribeOutput] = {
val process = Process(s"git describe --long --tags --abbrev=8 --match $TagPattern --always --dirty=+${timestamp(d)}", wd)
Try(process !! impl.NoProcessLogger).toOption
Try(process !! NoProcessLogger).toOption
.map(_.replaceAll("-([0-9]+)-g([0-9a-f]{8})", "+$1-$2"))
.map(parser.parse)
.flatMap(output =>
if (output.hasNoTags) getDistanceToFirstCommit().map(dist =>
output.copy(commitSuffix = output.commitSuffix.copy(distance = dist))
)
else Some(output)
)
.flatMap { out =>
if (out.hasNoTags)
getDistanceToFirstCommit().map { distance =>
GitDescribeOutput(GitRef("0.0.0"), GitCommitSuffix(distance, out.ref.value), out.dirtySuffix)
}
else Some(out)
}
}

def getGitPreviousStableTag: Option[GitDescribeOutput] = {
Expand All @@ -182,10 +186,10 @@ sealed case class DynVer(wd: Option[File], separator: String, tagPrefix: String)
}

def timestamp(d: Date): String = GitDescribeOutput.timestamp(d)
def fallback(d: Date): String = GitDescribeOutput.fallback(separator, d)
def fallback(d: Date): String = GitDescribeOutput.fallback(separator, d)

private def execAndHandleEmptyOutput(cmd: String): Option[String] = {
Try(Process(cmd, wd) !! impl.NoProcessLogger).toOption
Try(Process(cmd, wd) !! NoProcessLogger).toOption
.filter(_.trim.nonEmpty)
}

Expand All @@ -194,17 +198,7 @@ sealed case class DynVer(wd: Option[File], separator: String, tagPrefix: String)

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

object `package`

package impl {
object NoProcessLogger extends ProcessLogger {
def info(s: => String) = ()
def out(s: => String) = ()
def error(s: => String) = ()
def err(s: => String) = ()
def buffer[T](f: => T) = f
}
}
Loading

0 comments on commit 5214ab8

Please sign in to comment.