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

✨ Select highest tag when there are sibling tags #279

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ To use this feature with the Migration Manager [MiMa](https://github.com/lightbe
mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% moduleName.value % _).toSet
```

#### Multiple tags on the same commit

In case the last know tag has sibling tags that points at the same commit, the highest tag is used.
jprudent marked this conversation as resolved.
Show resolved Hide resolved
For example, for the following tags:
```
v1.0.0
v10.1 // this tag is the highest selected tag
v2.0.0
```


## Tag Requirements

In order to be recognized by sbt-dynver, by default tags must begin with the lowercase letter 'v' followed by a digit.
Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ val dynverLib = LocalProject("dynver")
val dynver = project.settings(
libraryDependencies += "org.eclipse.jgit" % "org.eclipse.jgit" % "5.13.2.202306221912-r" % Test,
libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.17.0" % Test,
libraryDependencies += "org.scala-sbt" % "sbt" % sbtVersion.value % "provided",
jprudent marked this conversation as resolved.
Show resolved Hide resolved
resolvers += Resolver.sbtPluginRepo("releases"), // for prev artifacts, not repo1 b/c of mergly publishing
publishSettings,
crossScalaVersions ++= Seq(scala2_13, scala3),
Expand Down
53 changes: 45 additions & 8 deletions dynver/src/main/scala/sbtdynver/DynVer.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package sbtdynver

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

import scala.PartialFunction
import java.util._
import regex.Pattern
import scala.util._

import scala.sys.process.Process
import dynver._
import impl.NoProcessLogger
import sbt.librarymanagement.SemanticSelector
import sbt.librarymanagement.VersionNumber

import dynver._, impl.NoProcessLogger
import scala.annotation.tailrec

sealed case class GitRef(value: String)
final case class GitCommitSuffix(distance: Int, sha: String)
Expand Down Expand Up @@ -172,17 +174,37 @@ sealed case class DynVer(wd: Option[File], separator: String, tagPrefix: String)
}
else Some(out)
}
.flatMap(selectFromMultipleTags)

}

private def selectFromMultipleTags(out: GitDescribeOutput): Option[GitDescribeOutput] = {
def exec(cmd: String) = Try(Process(cmd, wd) !! NoProcessLogger).toOption

// if out.ref is an annotated tag `git tag --list --points-at ${out.ref.value}` returns tags on that tag object
// and not tags on the commit object. So we need to find the commit object SHA the ref points to.
(for {
// find the commit object SHA of the current commit
commitHash <- exec(s"git rev-parse ${out.ref.value}^{commit}")
// find all the tags that points at the commit object
allTags <- exec(s"git tag --list --points-at $commitHash")
highestTag <- allTags.split("\n").toList.map(GitRef(_)).filter(_.isTag).sortWith(DynVer.versionCompare).lastOption
} yield {
out.copy(ref = highestTag)
}).orElse(Some(out))
}


def getGitPreviousStableTag: Option[GitDescribeOutput] = {
for {
// Find the parent of the current commit. The "^1" instructs it to show only the first parent,
// as merge commits can have multiple parents
parentHash <- execAndHandleEmptyOutput("git --no-pager log --pretty=%H -n 1 HEAD^1")
// Find the closest tag of the parent commit
tag <- execAndHandleEmptyOutput(s"git describe --tags --abbrev=0 --match $TagPattern --always $parentHash")
out <- PartialFunction.condOpt(tag)(parser.parse)
} yield out
closestTag <- execAndHandleEmptyOutput(s"git describe --tags --abbrev=0 --match $TagPattern --always $parentHash")
outOfClosestTag <- PartialFunction.condOpt(closestTag)(parser.parse)
highestTag <- selectFromMultipleTags(outOfClosestTag)
} yield highestTag
}

def timestamp(d: Date): String = GitDescribeOutput.timestamp(d)
Expand All @@ -197,6 +219,21 @@ 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)

/**
* Compare 2 versions based on semantic versioning (or approaching semver)
* @param a
* @param b
* @return true if a is semantically before b
*/
private[sbtdynver] def versionCompare(a: GitRef, b: GitRef): Boolean = {
val v1 = VersionNumber(a.dropPrefix)
// in case there are missing version numbers, fill the blank with 0
// this is to support version numbering not exactly semver, like 1.2-alpha
val v1fixed = VersionNumber(v1.numbers.padTo(3, 0L), v1.tags, v1.extras)
val v2 = VersionNumber(b.dropPrefix)
SemanticSelector(s">${v1fixed.toString}").matches(v2)
}
}

object `package`
21 changes: 21 additions & 0 deletions dynver/src/test/scala/sbtdynver/DynVerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ object VersionSpec extends Properties("VersionSpec") {
property("on tag v1.0.0 and 1 commit, w/o local changes") = onTag.commit .version ?= "1.0.0+1-1234abcd"
property("on tag v1.0.0 and 1 commit with local changes") = onTag.commit.dirty .version ?= "1.0.0+1-1234abcd+20160917-0000"
property("on tag v2") = onTag.commit.tag("2").version ?= "2" // #7, didn't match
// for multiple tags, the highest tag is used
property("on multiple tags v1.0.0 and v2.0.0, w/o local changes") = onMultipleTags.version ?= "2.0.0"
property("on multiple tags v1.0.0 and v2.0.0, with local changes") = onMultipleTags.dirty.version ?= "2.0.0+0-1234abcd+20160917-0000"
property("on multiple tags v1.0.0 and v2.0.0, with local changes") = onMultipleTags.dirty.version ?= "2.0.0+0-1234abcd+20160917-0000"
property("on multiple tags v1.0.0 and v2.0.0, and 1 commit, w/o local changes") = onMultipleTags.commit.version ?= "2.0.0+1-1234abcd"
property("on multiple tags v1.0.0 and v2.0.0, and 1 commit with local changes") = onMultipleTags.commit.dirty.version ?= "2.0.0+1-1234abcd+20160917-0000"
property("on multiple tags v1.0.0 and v2.0.0, numerical sort") = onMultipleTags.tag("10.3").tag("10.20").version ?= "10.20" // 1.0.0 < 2.0.0 < 10.1 < 10.20
property("on multiple tags v1.0.0 and v2.0.0, then tag v2") = onMultipleTags.commit.tag("2").version ?= "2" // #previous tags didn't match
}

object PreviousVersionSpec extends Properties("PreviousVersionSpec") {
Expand All @@ -27,6 +35,12 @@ object PreviousVersionSpec extends Properties("PreviousVersionSpec") {
property("on tag v1.0.0 and 1 commit with local changes") = onTag.commit.dirty .previousVersion ?= Some("1.0.0")
property("on tag v2.0.0, w/o local changes") = onTag.commit.tag("2.0.0").previousVersion ?= Some("1.0.0")

property("on multiple tags v1.0.0 and v2.0.0, w/o local changes") = onMultipleTags.previousVersion ?= None
property("on multiple tags v1.0.0 and v2.0.0, with local changes") = onMultipleTags.dirty.previousVersion ?= None
property("on multiple tags v1.0.0 and v2.0.0, and 1 commit, w/o local changes") = onMultipleTags.commit.previousVersion ?= Some("2.0.0")
property("on multiple tags v1.0.0 and v2.0.0, and 1 commit with local changes") = onMultipleTags.commit.dirty.previousVersion ?= Some("2.0.0")
property("on tag v3.0.0 with previous multiple tags v1.0.0 and v2.0.0") = onMultipleTags.commit.tag("3.0.0").previousVersion ?= Some("2.0.0")

property("w/ merge commits") = onBranch2x.checkout("master").merge("2.x") .previousVersion ?= Some("1.0.0")
property("w/ merge commits + tag") = onBranch2Tag .previousVersion ?= Some("1.0.0")
property("on multiple branches") = onMultiBranch.checkout("2.x") .previousVersion ?= Some("2.0.0")
Expand All @@ -44,12 +58,16 @@ object IsSnapshotSpec extends Properties("IsSnapshotSpec") {
property("on tag v1.0.0 and 1 commit, w/o local changes") = onTag.commit .isSnapshot ?= true
property("on tag v1.0.0 and 1 commit with local changes") = onTag.commit.dirty .isSnapshot ?= true
property("on tag v2") = onTag.commit.tag("2").isSnapshot ?= false
property("on multiple tags, w/o local changes") = onMultipleTags.isSnapshot ?= false
property("on multiple tags and 1 commit, w/o local changes") = onMultipleTags.commit.isSnapshot ?= true
property("on multiple tags v1.0.0 and 1 commit with local changes") = onMultipleTags.commit.dirty.isSnapshot ?= true
}

object SonatypeSnapshotSpec extends Properties("SonatypeSnapshotSpec") {
property("on tag v1.0.0 with local changes") = onTag.dirty .sonatypeVersion ?= "1.0.0+0-1234abcd+20160917-0000-SNAPSHOT"
property("on tag v1.0.0 and 1 commit, w/o local changes S") = onTag.commit .sonatypeVersion ?= "1.0.0+1-1234abcd-SNAPSHOT"
property("on tag v1.0.0, w/o local changes") = onTag .sonatypeVersion ?= "1.0.0"
property("on multiple tags, w/o local changes") = onMultipleTags .sonatypeVersion ?= "2.0.0"
property("on tag v2") = onTag.commit.tag("2").sonatypeVersion ?= "2"
}

Expand All @@ -63,4 +81,7 @@ object isVersionStableSpec extends Properties("IsVersionStableSpec") {
property("on tag v1.0.0 and 1 commit, w/o local changes") = onTag.commit .isVersionStable ?= true
property("on tag v1.0.0 and 1 commit with local changes") = onTag.commit.dirty .isVersionStable ?= false
property("on tag v2") = onTag.commit.tag("2").isVersionStable ?= true
property("on multiple tags, w/o local changes") = onMultipleTags.isVersionStable ?= true
property("on multiple tags and 1 commit w/o local changes") = onMultipleTags.commit.isVersionStable ?= true
property("on multiple tags with local changes") = onMultipleTags.dirty.isVersionStable ?= false
}
1 change: 1 addition & 0 deletions dynver/src/test/scala/sbtdynver/RepoStates.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ sealed class RepoStates(tagPrefix: String) {
def noCommits = notAGitRepo.init
def onCommit = noCommits.commit.commit.commit
def onTag = onCommit.tag("1.0.0")
def onMultipleTags = onTag.tag("2.0.0")
def onBranch2x = onTag.branch("2.x").commit
def onBranch2Tag = onBranch2x.tag("2.0.0")
def onMultiBranch = onBranch2Tag.commit.tag("2.1.0").checkout("master").commit.tag("1.1.0")
Expand Down
26 changes: 26 additions & 0 deletions dynver/src/test/scala/sbtdynver/VersionCompareSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package sbtdynver

import org.scalacheck.Gen
import org.scalacheck.Prop.forAll
import org.scalacheck.Prop.propBoolean
import org.scalacheck.Properties

object VersionCompareSpec extends Properties("VersionCompareSpec") {
property("major version is higher than minors") = forAll(Gen.posNum[Int]){ (i:Int) =>
DynVer.versionCompare(GitRef(s"v${i-1}.$i.$i"), GitRef(s"$i.${i+1}.${i+1}")) &&
DynVer.versionCompare(GitRef(s"${i-1}.$i.$i"), GitRef(s"v$i.${i+1}.${i+1}")) &&
DynVer.versionCompare(GitRef(s"v$i.$i.$i"), GitRef(s"$i.${i+1}.${i+1}")) &&
DynVer.versionCompare(GitRef(s"$i.$i.$i"), GitRef(s"v$i.${i+1}.${i+1}"))
}

property("not really semver versions are supported") = DynVer.versionCompare(GitRef(s"9"), GitRef(s"v10"))

property("not really semver versions that are not even in length are supported") =
forAll{ (i: Int) => DynVer.versionCompare(GitRef(s"9"), GitRef(s"v10.$i")) }

property("not really versions with qualifier are supported") = DynVer.versionCompare(GitRef(s"9-SNAPSHOT"), GitRef(s"v10"))

property("qualifiers are supported") = forAll(Gen.alphaNumStr){ (qualifier:String) =>
qualifier.nonEmpty ==> DynVer.versionCompare(GitRef(s"9.2.2-$qualifier"), GitRef(s"v10"))
}
}
Loading