Completed a fully functional plugin for SBT 1.9.0+.
This commit is contained in:
parent
fee52a97bf
commit
5f94372a45
7 changed files with 335 additions and 49 deletions
69
build.sbt
69
build.sbt
|
@ -1,5 +1,5 @@
|
||||||
ThisBuild / version := "0.1.0-SNAPSHOT"
|
ThisBuild / organizationName := "garrity software"
|
||||||
ThisBuild / organization := "gs"
|
ThisBuild / organization := "gs"
|
||||||
|
|
||||||
ThisBuild / homepage := Some(
|
ThisBuild / homepage := Some(
|
||||||
url("https://git.garrity.co/garrity-software/gs-semver-sbt-plugin")
|
url("https://git.garrity.co/garrity-software/gs-semver-sbt-plugin")
|
||||||
|
@ -9,12 +9,71 @@ ThisBuild / licenses := Seq(
|
||||||
"Apache-2.0" -> url("http://www.apache.org/license/LICENSE-2.0")
|
"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)
|
.enablePlugins(SbtPlugin)
|
||||||
.settings(
|
.settings(
|
||||||
name := "gs-semver-sbt-plugin",
|
name := ProjectName,
|
||||||
|
version := SelectedVersion,
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit" % "6.8.0.202311291450-r"
|
"com.lihaoyi" %% "os-lib" % "0.9.3"
|
||||||
),
|
),
|
||||||
pluginCrossBuild / sbtVersion := {
|
pluginCrossBuild / sbtVersion := {
|
||||||
scalaBinaryVersion.value match {
|
scalaBinaryVersion.value match {
|
||||||
|
|
|
@ -1,38 +1,40 @@
|
||||||
package gs
|
package gs
|
||||||
|
|
||||||
import java.io.File
|
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
|
import scala.util.Try
|
||||||
|
|
||||||
object Git {
|
object Git {
|
||||||
|
|
||||||
private lazy val repo: Repository = {
|
/** Get the latest tag on this repository and attempt to parse it as a
|
||||||
val builder = new FileRepositoryBuilder()
|
* [[SemVer]]. This value is retrieved using the `git` command. If for some
|
||||||
builder.setGitDir(new File(".")).findGitDir().build()
|
* 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")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
65
src/main/scala/gs/PluginProperties.scala
Normal file
65
src/main/scala/gs/PluginProperties.scala
Normal 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)
|
||||||
|
|
||||||
|
}
|
25
src/main/scala/gs/ReleaseType.scala
Normal file
25
src/main/scala/gs/ReleaseType.scala
Normal 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'")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,23 +2,76 @@ package gs
|
||||||
|
|
||||||
import scala.util.matching.Regex
|
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(
|
case class SemVer(
|
||||||
major: Int,
|
major: Int,
|
||||||
minor: Int,
|
minor: Int,
|
||||||
patch: Int
|
patch: Int
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/** Render this SemVer as: `Major.Minor.Patch`
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The `Major.Minor.Patch` representation of this SemVer.
|
||||||
|
*/
|
||||||
override def toString(): String =
|
override def toString(): String =
|
||||||
s"$major.$minor.$patch"
|
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)
|
def incrementPatch(): SemVer = copy(patch = this.patch + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
object SemVer {
|
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
|
"^(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] =
|
def parse(candidate: String): Option[SemVer] =
|
||||||
candidate match {
|
candidate match {
|
||||||
case SemVerPattern(major, minor, patch) =>
|
case SemVerPattern(major, minor, patch) =>
|
||||||
|
|
|
@ -2,26 +2,62 @@ package gs
|
||||||
|
|
||||||
import sbt._
|
import sbt._
|
||||||
|
|
||||||
|
/** Defines all setting and task keys for the GS SemVer SBT Plugin.
|
||||||
|
*/
|
||||||
object SemVerKeys {
|
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
|
* 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.
|
* 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."
|
"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
|
/** SBT Setting for the **selected** project version.
|
||||||
* non-committed changes.
|
|
||||||
*
|
*
|
||||||
* This value is automatically derived by taking the latest version, and
|
* Use this value to set your project version:
|
||||||
* either incrementing the revision by 1 or just using 0.1.0 if the project
|
*
|
||||||
* does not have any Git tags.
|
* {{{
|
||||||
|
* 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](
|
lazy val semVerSelected = settingKey[String](
|
||||||
"Revision level increment of the latest version, or 0.1.0 if no tags exist."
|
"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."
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package gs
|
package gs
|
||||||
|
|
||||||
import Keys._
|
|
||||||
import sbt._
|
import sbt._
|
||||||
|
|
||||||
object SemVerPlugin extends AutoPlugin {
|
object SemVerPlugin extends AutoPlugin {
|
||||||
|
@ -10,9 +9,56 @@ object SemVerPlugin extends AutoPlugin {
|
||||||
|
|
||||||
import autoImport._
|
import autoImport._
|
||||||
|
|
||||||
/* override lazy val globalSettings: Seq[Setting[_]] = Seq( helloGreeting :=
|
// Perform all version calculations and expose as a variable.
|
||||||
* "hi", )
|
lazy val semVerDefaults: Seq[Setting[_]] = {
|
||||||
*
|
// The latest semantic version relevant to this project, calculated by
|
||||||
* override lazy val projectSettings: Seq[Setting[_]] = Seq( hello := { val s
|
// looking at the latest Git tag.
|
||||||
* = streams.value val g = helloGreeting.value s.log.info(g) } ) */
|
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}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue