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 2 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
54 changes: 46 additions & 8 deletions dynver/src/main/scala/sbtdynver/DynVer.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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 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,6 +172,43 @@ 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(hybridAlphaNumericSort).lastOption
} yield {
out.copy(ref = highestTag)
}).orElse(Some(out))
}

private def hybridAlphaNumericSort(a: GitRef, b: GitRef): Boolean = {
val a1 = a.dropPrefix
val b1 = b.dropPrefix
@tailrec
def loop(a: String, b: String) : Boolean =
if(a.isEmpty) true
else if (b.isEmpty) false
else if (a.head.isDigit && b.head.isDigit) {
val (aNum, aRest) = a.span(_.isDigit)
val (bNum, bRest) = b.span(_.isDigit)
if (aNum.toInt == bNum.toInt) loop(aRest, bRest)
else aNum.toInt < bNum.toInt
} else if (a.head.isDigit) true
else if (b.head.isDigit) false
else if (a.head == b.head) loop(a.tail, b.tail)
else a.head < b.head
loop(a1, b1)
}

def getGitPreviousStableTag: Option[GitDescribeOutput] = {
Expand All @@ -180,9 +217,10 @@ sealed case class DynVer(wd: Option[File], separator: String, tagPrefix: String)
// 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 Down
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