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

Prep for CLI introduction #195

Merged
merged 7 commits into from
Jan 14, 2021
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
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