Completed a fully functional plugin for SBT 1.9.0+.

This commit is contained in:
Pat Garrity 2024-01-07 22:22:36 -06:00
parent fee52a97bf
commit 5f94372a45
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
7 changed files with 335 additions and 49 deletions

View file

@ -1,4 +1,4 @@
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organizationName := "garrity software"
ThisBuild / organization := "gs"
ThisBuild / homepage := Some(
@ -9,12 +9,71 @@ ThisBuild / licenses := Seq(
"Apache-2.0" -> url("http://www.apache.org/license/LICENSE-2.0")
)
lazy val root = Project("gs-semver-sbt-plugin", file("."))
externalResolvers := Seq(
"Garrity Software Releases" at "https://maven.garrity.co/releases"
)
val ProjectName: String = "gs-semver-sbt-plugin"
val Description: String = "SBT 1.9.0+ plugin for Git-based semantic versioning."
def getProperty[A](
name: String,
conv: String => A
): Option[A] =
Option(System.getProperty(name)).map(conv)
val VersionProperty: String = "version"
val ReleaseProperty: String = "release"
lazy val InputVersion: Option[String] =
getProperty(VersionProperty, identity)
lazy val IsRelease: Boolean =
getProperty(ReleaseProperty, _.toBoolean).getOrElse(false)
lazy val Modifier: String =
if (IsRelease) "" else "-SNAPSHOT"
val DefaultVersion: String = "0.1.0-SNAPSHOT"
lazy val SelectedVersion: String =
InputVersion
.map(v => s"$v$Modifier")
.getOrElse(DefaultVersion)
lazy val publishSettings = Seq(
publishMavenStyle := true,
Test / publishArtifact := false,
pomIncludeRepository := Function.const(false),
scmInfo := Some(
ScmInfo(
url(s"https://git.garrity.co/garrity-software/$ProjectName"),
s"git@git.garrity.co:garrity-software/$ProjectName.git"
)
),
description := Description,
licenses := List(
"Apache 2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0.html")
),
homepage := Some(
url(s"https://git.garrity.co/garrity-software/$ProjectName")
),
publishTo := {
val repo = "https://maven.garrity.co/"
if (!IsRelease)
Some("Garrity Software Maven" at repo + "snapshots")
else
Some("Garrity Software Maven" at repo + "releases")
}
)
lazy val root = Project(ProjectName, file("."))
.enablePlugins(SbtPlugin)
.settings(
name := "gs-semver-sbt-plugin",
name := ProjectName,
version := SelectedVersion,
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit" % "6.8.0.202311291450-r"
"com.lihaoyi" %% "os-lib" % "0.9.3"
),
pluginCrossBuild / sbtVersion := {
scalaBinaryVersion.value match {

View file

@ -1,38 +1,40 @@
package gs
import java.io.File
import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
import scala.collection.JavaConverters._
import scala.util.Try
object Git {
private lazy val repo: Repository = {
val builder = new FileRepositoryBuilder()
builder.setGitDir(new File(".")).findGitDir().build()
/** Get the latest tag on this repository and attempt to parse it as a
* [[SemVer]]. This value is retrieved using the `git` command. If for some
* reason the call fails, there are no tags, or the tag value cannot be
* parsed, the default [[SemVer]] (`0.1.0`) is used.
*
* This function assumes the current directory is a Git repository. The
* command executed is: `git describe --tags --abbrev=0`
*
* @return
* The latest semantic version according to the current Git repository.
*/
def getLatestSemVer(): SemVer = {
// Suppress all error output. Suppress exceptions for the failed process.
// Capture the standard output and parse it if the command succeeds.
val result = os
.proc("git", "describe", "--tags", "--abbrev=0")
.call(
stderr = os.ProcessOutput(
(
_,
_
) => ()
),
check = false
)
if (result.exitCode != 0)
SemVer.DefaultVersion
else
SemVer.parse(result.out.text()).getOrElse(SemVer.DefaultVersion)
}
def getLatestSemVer(): Option[SemVer] = {
val walk = new RevWalk(repo)
val result = repo
.getRefDatabase()
.getRefsByPrefix(Constants.R_TAGS)
.asScala
.map(ref => walk.parseTag(ref.getObjectId()))
.sortBy(_.getTaggerIdent().getWhenAsInstant())
.headOption
.map(_.getTagName())
.map(normalizeTagName)
.flatMap(SemVer.parse)
walk.dispose()
result
}
private def normalizeTagName(tag: String): String =
tag.trim().stripPrefix("v")
}

View file

@ -0,0 +1,65 @@
package gs
import sbt._
/** Helper for managing all SBT command line properties (`-Dproperty=value`).
*/
object PluginProperties {
/** Helper to extract the value from `-Dproperty=value`.
*
* @param name
* The property name.
* @param conv
* The conversion function to the output type.
* @return
* The converted value, or `None` if no value exists.
*/
def getProperty[A](
name: String,
conv: String => A
): Option[A] =
Option(System.getProperty(name)).map(conv)
/** Use one of the following to calculate an incremented release version:
*
* - `sbt -Drelease=major`
* - `sbt -Drelease=minor`
* - `sbt -Drelease=patch`
*/
val ReleaseProperty: String = "release"
/** If true, or no release type is set, this build is treated as a
* pre-release, and the suffix `-SNAPSHOT` is appended to the version.
*/
val SnapshotProperty: String = "snapshot"
/** The value of `-Drelease=<value>`, parsed and validated. Influences the
* final calculated [[SemVer]].
*
* @return
* The version passed as input to SBT.
*/
def getReleaseType(): Option[ReleaseType] =
getProperty(
ReleaseProperty,
raw =>
ReleaseType.parse(raw) match {
case scala.util.Success(value) => value
case scala.util.Failure(e) => throw e
}
)
/** The value of `-Dsnapshot=<value>`, parsed and validated. If parsing
* fails or no value is present, `false` is returned. Influences whether
* the pre-release suffix (`-SNAPSHOT`) is appended to the version.
*
* @return
* `true` if `-Dsnapshot=true` is specified at the command line, `false`
* otherwise.
*/
def isSnapshot(): Boolean =
getProperty(SnapshotProperty, raw => raw.trim().equalsIgnoreCase("true"))
.getOrElse(false)
}

View file

@ -0,0 +1,25 @@
package gs
import scala.util.Failure
import scala.util.Success
import scala.util.Try
sealed abstract class ReleaseType(val name: String)
object ReleaseType {
case object Major extends ReleaseType("major")
case object Minor extends ReleaseType("minor")
case object Patch extends ReleaseType("patch")
def parse(candidate: String): Try[ReleaseType] =
candidate.trim().toLowerCase() match {
case Major.name => Success(Major)
case Minor.name => Success(Minor)
case Patch.name => Success(Patch)
case _ =>
Failure(
new IllegalArgumentException(s"Invalid release type: '$candidate'")
)
}
}

View file

@ -2,23 +2,76 @@ package gs
import scala.util.matching.Regex
/** Simplified representation of SemVer. Only supports major, minor and patch
* versions. Not intended to be used as a generalized SemVer implementation.
*
* @param major
* The major version.
* @param minor
* The minor version.
* @param patch
* The patch version.
*/
case class SemVer(
major: Int,
minor: Int,
patch: Int
) {
/** Render this SemVer as: `Major.Minor.Patch`
*
* @return
* The `Major.Minor.Patch` representation of this SemVer.
*/
override def toString(): String =
s"$major.$minor.$patch"
/** Render this SemVer as: `Major.Minor.Patch`, and append a suffix,
* `-SNAPSHOT` if a snapshot rendition is requested.
*
* @param isSnapshot
* Requests a snapshot rendition if true. Uses the `-SNAPSHOT` pre-release
* suffix.
* @return
* The string rendition of this version.
*/
def render(isSnapshot: Boolean): String = {
val version = toString()
if (isSnapshot) version + "-SNAPSHOT" else version
}
/** @return
* Copy of this SemVer with the major version incremented by 1.
*/
def incrementMajor(): SemVer = copy(major = this.major + 1)
/** @return
* Copy of this SemVer with the minor version incremented by 1.
*/
def incrementMinor(): SemVer = copy(minor = this.minor + 1)
/** @return
* Copy of this SemVer with the patch version incremented by 1.
*/
def incrementPatch(): SemVer = copy(patch = this.patch + 1)
}
object SemVer {
val SemVerPattern: Regex =
/** Version 0.1.0
*/
val DefaultVersion: SemVer = SemVer(0, 1, 0)
private val SemVerPattern: Regex =
"^(0|(?:[1-9][0-9]*))\\.(0|(?:[1-9][0-9]*))\\.(0|(?:[1-9][0-9]*))$".r
/** Attempt to parse the input string as a [[SemVer]].
*
* @param candidate
* The candidate string to parse.
* @return
* The parsed [[SemVer]], or `None` if parsing failed.
*/
def parse(candidate: String): Option[SemVer] =
candidate match {
case SemVerPattern(major, minor, patch) =>

View file

@ -2,26 +2,62 @@ package gs
import sbt._
/** Defines all setting and task keys for the GS SemVer SBT Plugin.
*/
object SemVerKeys {
/** SBT Setting Key for the latest known project version.
/** SBT Setting for the latest known project version (before this build).
*
* For setting your project version, please use the `semVerSelected` setting.
*
* This value is automatically derived from Git by looking at the most recent
* tag. If the project does not yet have any tags, the value 0.1.0 is used.
*/
lazy val latestVersion = settingKey[String](
lazy val semVerCurrent = settingKey[String](
"Latest Git-tagged project version, or 0.1.0 if no tags exist."
)
/** SBT Setting Key for the working project version - used if there are any
* non-committed changes.
/** SBT Setting for the **selected** project version.
*
* This value is automatically derived by taking the latest version, and
* either incrementing the revision by 1 or just using 0.1.0 if the project
* does not have any Git tags.
* Use this value to set your project version:
*
* {{{
* version := semVerSelected.value
* }}}
*
* This value should be used as the version for all project artifacts. It is
* automatically derived based on the `semVerCurrent` setting, the value of
* the `release` property, and the value of the `snapshot` property:
*
* Consider the example current version of `1.2.3`:
*
* - `-Drelease=major`: `semVerSelected = 2.2.3`
* - `-Drelease=major`, `-Dsnapshot=true`: `semVerSelected =
* 2.2.3-SNAPSHOT`
* - `-Drelease=minor`: `semVerSelected = 1.3.3`
* - `-Drelease=minor`, `-Dsnapshot=true`: `semVerSelected =
* 1.3.3-SNAPSHOT`
* - `-Drelease=patch`: `semVerSelected = 1.2.4`
* - `-Drelease=patch`, `-Dsnapshot=true`: `semVerSelected =
* 1.2.4-SNAPSHOT`
* - `release` not set: `semVerSelected = 1.2.4-SNAPSHOT`
*/
lazy val workingVersion = settingKey[String](
"Revision level increment of the latest version, or 0.1.0 if no tags exist."
lazy val semVerSelected = settingKey[String](
"Version selected for the current build. Depends on the release type and current version."
)
/** SBT Setting for the selected major version. This value is always the major
* version part of the `semVerSelected` value.
*/
lazy val semVerMajor = settingKey[Int](
"Major version selected for the current build. Depends on the release type and current version."
)
/** Task which emits the current and selected versions at the informational
* log level.
*/
lazy val semVerInfo = taskKey[Unit](
"Dump the calculated version information."
)
}

View file

@ -1,6 +1,5 @@
package gs
import Keys._
import sbt._
object SemVerPlugin extends AutoPlugin {
@ -10,9 +9,56 @@ object SemVerPlugin extends AutoPlugin {
import autoImport._
/* override lazy val globalSettings: Seq[Setting[_]] = Seq( helloGreeting :=
* "hi", )
*
* override lazy val projectSettings: Seq[Setting[_]] = Seq( hello := { val s
* = streams.value val g = helloGreeting.value s.log.info(g) } ) */
// Perform all version calculations and expose as a variable.
lazy val semVerDefaults: Seq[Setting[_]] = {
// The latest semantic version relevant to this project, calculated by
// looking at the latest Git tag.
val latestSemVer =
Git.getLatestSemVer()
// The selected release type (if any), determined by looking at the SBT
// command line and checking if `-Drelease=<release-type>` was specified.
val releaseType =
PluginProperties.getReleaseType()
// See whether this is a pre-release build or not. If it is (if this value
// is true), the pre-release suffix `-SNAPSHOT` will be appended to the
// calculated version.
val isSnapshot =
PluginProperties.isSnapshot() || releaseType.isEmpty
// Calculate the appropriate semantic version for this build of the project.
val selectedSemVer = releaseType match {
case Some(ReleaseType.Major) => latestSemVer.incrementMajor()
case Some(ReleaseType.Minor) => latestSemVer.incrementMinor()
case Some(ReleaseType.Patch) => latestSemVer.incrementPatch()
case None =>
// Note: this doesn't play nicely with repositories that have
// pre-release versions before 0.1.0.
if (latestSemVer == SemVer.DefaultVersion)
latestSemVer
else
latestSemVer.incrementPatch()
}
// Expose the relevant values as setting keys.
Seq(
semVerCurrent := latestSemVer.toString(),
semVerSelected := selectedSemVer.render(isSnapshot),
semVerMajor := selectedSemVer.major
)
}
// Automatically exposed globally.
override lazy val globalSettings: Seq[Setting[_]] = semVerDefaults
// Add the custom task.
override lazy val buildSettings: Seq[Setting[_]] = Seq(
semVerInfo := {
val log = Keys.streams.value.log
log.info(s"[SemVer] Current: ${semVerCurrent.value}")
log.info(s"[SemVer] Selected: ${semVerSelected.value}")
}
)
}