mirror of https://github.com/sbt/sbt.git
Cleanup & tweak VersionNumber
This commit is contained in:
parent
c6b2b626c8
commit
0f93849214
|
|
@ -5,38 +5,31 @@ final class VersionNumber private[sbt] (
|
|||
val tags: Seq[String],
|
||||
val extras: Seq[String]
|
||||
) {
|
||||
|
||||
def _1: Option[Long] = get(0)
|
||||
def _2: Option[Long] = get(1)
|
||||
def _3: Option[Long] = get(2)
|
||||
def _4: Option[Long] = get(3)
|
||||
def get(idx: Int): Option[Long] =
|
||||
if (size <= idx) None
|
||||
else Some(numbers(idx))
|
||||
def get(idx: Int): Option[Long] = numbers lift idx
|
||||
def size: Int = numbers.size
|
||||
|
||||
/** The vector of version numbers from more to less specific from this version number. */
|
||||
lazy val cascadingVersions: Vector[VersionNumber] =
|
||||
(Vector(this) ++
|
||||
(numbers.inits filter (_.length >= 2) map (VersionNumber(_, Nil, Nil)))).distinct
|
||||
(Vector(this) ++ (numbers.inits filter (_.size >= 2) map (VersionNumber(_, Nil, Nil)))).distinct
|
||||
|
||||
private[this] val versionStr: String =
|
||||
numbers.mkString(".") +
|
||||
(tags match {
|
||||
case Seq() => ""
|
||||
case ts => "-" + ts.mkString("-")
|
||||
}) +
|
||||
extras.mkString("")
|
||||
override def toString: String = versionStr
|
||||
override def hashCode: Int =
|
||||
numbers.hashCode * 41 * 41 +
|
||||
tags.hashCode * 41 +
|
||||
extras.hashCode
|
||||
override def equals(o: Any): Boolean =
|
||||
o match {
|
||||
case v: VersionNumber =>
|
||||
(this.numbers == v.numbers) && (this.tags == v.tags) && (this.extras == v.extras)
|
||||
case _ => false
|
||||
}
|
||||
override val toString: String =
|
||||
numbers.mkString(".") + mkString1(tags, "-", "-", "") + extras.mkString("")
|
||||
|
||||
override def hashCode: Int = numbers.## * 41 * 41 + tags.## * 41 + extras.##
|
||||
|
||||
override def equals(that: Any): Boolean = that match {
|
||||
case v: VersionNumber => (numbers == v.numbers) && (tags == v.tags) && (extras == v.extras)
|
||||
case _ => false
|
||||
}
|
||||
|
||||
/** A variant of mkString that returns the empty string if the sequence is empty. */
|
||||
private[this] def mkString1[A](xs: Seq[A], start: String, sep: String, end: String): String =
|
||||
if (xs.isEmpty) "" else xs.mkString(start, sep, end)
|
||||
}
|
||||
|
||||
object VersionNumber {
|
||||
|
|
@ -44,120 +37,156 @@ object VersionNumber {
|
|||
/**
|
||||
* @param numbers numbers delimited by a dot.
|
||||
* @param tags string prefixed by a dash.
|
||||
* @param any other strings at the end.
|
||||
* @param extras strings at the end.
|
||||
*/
|
||||
def apply(numbers: Seq[Long], tags: Seq[String], extras: Seq[String]): VersionNumber =
|
||||
new VersionNumber(numbers, tags, extras)
|
||||
def apply(v: String): VersionNumber =
|
||||
unapply(v) match {
|
||||
|
||||
def apply(s: String): VersionNumber =
|
||||
unapply(s) match {
|
||||
case Some((ns, ts, es)) => VersionNumber(ns, ts, es)
|
||||
case _ => sys.error(s"Invalid version number: $v")
|
||||
case _ => throw new IllegalArgumentException(s"Invalid version number: $s")
|
||||
}
|
||||
|
||||
def unapply(v: VersionNumber): Option[(Seq[Long], Seq[String], Seq[String])] =
|
||||
Some((v.numbers, v.tags, v.extras))
|
||||
|
||||
def unapply(v: String): Option[(Seq[Long], Seq[String], Seq[String])] = {
|
||||
def splitDot(s: String): Vector[Long] =
|
||||
Option(s) match {
|
||||
case Some(x) => x.split('.').toVector.filterNot(_ == "").map(_.toLong)
|
||||
case _ => Vector()
|
||||
}
|
||||
def splitDash(s: String): Vector[String] =
|
||||
Option(s) match {
|
||||
case Some(x) => x.split('-').toVector.filterNot(_ == "")
|
||||
case _ => Vector()
|
||||
}
|
||||
def splitPlus(s: String): Vector[String] =
|
||||
Option(s) match {
|
||||
case Some(x) => x.split('+').toVector.filterNot(_ == "").map("+" + _)
|
||||
case _ => Vector()
|
||||
}
|
||||
def unapply(s: String): Option[(Seq[Long], Seq[String], Seq[String])] = {
|
||||
|
||||
// null safe, empty string safe
|
||||
def splitOn[A](s: String, sep: Char): Vector[String] =
|
||||
if (s eq null) Vector()
|
||||
else s.split(sep).filterNot(_ == "").toVector
|
||||
|
||||
def splitDot(s: String) = splitOn(s, '.') map (_.toLong)
|
||||
def splitDash(s: String) = splitOn(s, '-')
|
||||
def splitPlus(s: String) = splitOn(s, '+') map ("+" + _)
|
||||
|
||||
val TaggedVersion = """(\d{1,14})([\.\d{1,14}]*)((?:-\w+)*)((?:\+.+)*)""".r
|
||||
val NonSpaceString = """(\S+)""".r
|
||||
v match {
|
||||
|
||||
s match {
|
||||
case TaggedVersion(m, ns, ts, es) =>
|
||||
Some((Vector(m.toLong) ++ splitDot(ns), splitDash(ts), splitPlus(es)))
|
||||
val numbers = Vector(m.toLong) ++ splitDot(ns)
|
||||
val tags = splitDash(ts)
|
||||
val extras = splitPlus(es)
|
||||
Some((numbers, tags, extras))
|
||||
case "" => None
|
||||
case NonSpaceString(s) => Some((Vector(), Vector(), Vector(s)))
|
||||
case NonSpaceString(s) => Some((Vector.empty, Vector.empty, Vector(s)))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict. Checks everythig.
|
||||
*/
|
||||
/** Strict. Checks everything. */
|
||||
object Strict extends VersionNumberCompatibility {
|
||||
def name: String = "Strict"
|
||||
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean = v1 == v2
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic versioning. See http://semver.org/spec/v2.0.0.html
|
||||
*/
|
||||
/** Semantic Versioning. See http://semver.org/spec/v2.0.0.html */
|
||||
object SemVer extends VersionNumberCompatibility {
|
||||
def name: String = "Semantic Versioning"
|
||||
|
||||
/* Quotes of parts of the rules in the SemVer Spec relevant to compatibility checking:
|
||||
*
|
||||
* Rule 2:
|
||||
* > A normal version number MUST take the form X.Y.Z
|
||||
*
|
||||
* Rule 4:
|
||||
* > Major version zero (0.y.z) is for initial development. Anything may change at any time.
|
||||
*
|
||||
* Rule 6:
|
||||
* > Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced.
|
||||
*
|
||||
* Rule 7:
|
||||
* > Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced.
|
||||
*
|
||||
* Rule 8:
|
||||
* > Major version X (X.y.z | X > 0) MUST be incremented if any backwards incompatible changes are introduced.
|
||||
*
|
||||
* Rule 9:
|
||||
* > A pre-release version MAY be denoted by appending a hyphen and a series of
|
||||
* > dot separated identifiers immediately following the patch version.
|
||||
* > Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
|
||||
* > Identifiers MUST NOT be empty.
|
||||
* > Numeric identifiers MUST NOT include leading zeroes.
|
||||
* > Pre-release versions have a lower precedence than the associated normal version.
|
||||
* > A pre-release version indicates that the version is unstable and might not satisfy the
|
||||
* > intended compatibility requirements as denoted by its associated normal version.
|
||||
* > Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.
|
||||
*
|
||||
* Rule 10:
|
||||
* > Build metadata MAY be denoted by appending a plus sign and a series of
|
||||
* > dot separated identifiers immediately following the patch or pre-release version.
|
||||
* > Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
|
||||
* > Identifiers MUST NOT be empty.
|
||||
* > Build metadata SHOULD be ignored when determining version precedence.
|
||||
* > Thus two versions that differ only in the build metadata, have the same precedence.
|
||||
* > Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.
|
||||
*
|
||||
* Rule 10 means that build metadata is never considered for compatibility
|
||||
* we'll enforce this immediately by dropping them from both versions
|
||||
* Rule 2 we enforce with custom extractors.
|
||||
* Rule 4 we enforce by matching x = 0 & fully equals checking the two versions
|
||||
* Rule 6, 7 & 8 means version compatibility is determined by comparing the two X values
|
||||
* Rule 9 means pre-release versions are fully equals checked
|
||||
*/
|
||||
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
doIsCompat(v1, v2) || doIsCompat(v2, v1)
|
||||
doIsCompat(dropBuildMetadata(v1), dropBuildMetadata(v2))
|
||||
|
||||
private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
(v1, v2) match {
|
||||
case (v1, v2)
|
||||
if (v1.size >= 2) && (v2.size >= 2) => // A normal version number MUST take the form X.Y.Z
|
||||
(
|
||||
v1._1.get,
|
||||
v1._2.get,
|
||||
v1._3.getOrElse(0L),
|
||||
v1.tags,
|
||||
v2._1.get,
|
||||
v2._2.get,
|
||||
v2._3.getOrElse(0L),
|
||||
v2.tags
|
||||
) match {
|
||||
case (0L, _, _, _, 0L, _, _, _) =>
|
||||
// Major version zero (0.y.z) is for initial development. Anything may change at any time. The public API should not be considered stable.
|
||||
equalsIgnoreExtra(v1, v2)
|
||||
case (_, 0, 0, ts1, _, 0, 0, ts2) if ts1.nonEmpty || ts2.nonEmpty =>
|
||||
// A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers
|
||||
equalsIgnoreExtra(v1, v2)
|
||||
case (x1, _, _, _, x2, _, _, _) =>
|
||||
// Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced.
|
||||
// Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced
|
||||
x1 == x2
|
||||
case _ => equalsIgnoreExtra(v1, v2)
|
||||
}
|
||||
case _ => false
|
||||
case (NormalVersion(0, _, _), NormalVersion(0, _, _)) => v1 == v2 // R4
|
||||
case (NormalVersion(x1, _, _), NormalVersion(x2, _, _)) => x1 == x2 // R6, R7 & R8
|
||||
case (PrereleaseVersion(_, _, _), PrereleaseVersion(_, _, _)) => v1 == v2 // R9
|
||||
case _ => false
|
||||
}
|
||||
// Build metadata SHOULD be ignored when determining version precedence.
|
||||
private[this] def equalsIgnoreExtra(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
(v1.numbers == v2.numbers) && (v1.tags == v2.tags)
|
||||
|
||||
// SemVer Spec Rule 10 (above)
|
||||
private[VersionNumber] def dropBuildMetadata(v: VersionNumber) =
|
||||
if (v.extras.isEmpty) v else VersionNumber(v.numbers, v.tags, Nil)
|
||||
|
||||
// SemVer Spec Rule 9 (above)
|
||||
private[VersionNumber] def isPrerelease(v: VersionNumber): Boolean = v.tags.nonEmpty
|
||||
|
||||
// An extractor for SemVer's "normal version number" - SemVer Spec Rule 2 & Rule 9 (above)
|
||||
private[VersionNumber] object NormalVersion {
|
||||
def unapply(v: VersionNumber): Option[(Long, Long, Long)] =
|
||||
PartialFunction.condOpt(v.numbers) {
|
||||
// NOTE! We allow the z to be missing, because of legacy like commons-io 1.3
|
||||
case Seq(x, y, _*) if !isPrerelease(v) => (x, y, v._3 getOrElse 0)
|
||||
}
|
||||
}
|
||||
|
||||
// An extractor for SemVer's "pre-release versions" - SemVer Spec Rule 2 & Rule 9 (above)
|
||||
private[VersionNumber] object PrereleaseVersion {
|
||||
def unapply(v: VersionNumber): Option[(Long, Long, Long)] =
|
||||
PartialFunction.condOpt(v.numbers) {
|
||||
// NOTE! We allow the z to be missing, because of legacy like commons-io 1.3
|
||||
case Seq(x, y, _*) if isPrerelease(v) => (x, y, v._3 getOrElse 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A variant of SemVar that seems to be common among the Scala libraries.
|
||||
/** A variant of SemVar that seems to be common among the Scala libraries.
|
||||
* The second segment (y in x.y.z) increments breaks the binary compatibility even when x > 0.
|
||||
* Also API comatibility is expected even when the first segment is zero.
|
||||
* Also API compatibility is expected even when the first segment is zero.
|
||||
*/
|
||||
object SecondSegment extends VersionNumberCompatibility {
|
||||
import SemVer._
|
||||
|
||||
def name: String = "Second Segment Variant"
|
||||
|
||||
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
doIsCompat(v1, v2) || doIsCompat(v2, v1)
|
||||
private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
doIsCompat(dropBuildMetadata(v1), dropBuildMetadata(v2))
|
||||
|
||||
private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean = {
|
||||
(v1, v2) match {
|
||||
case (v1, v2)
|
||||
if (v1.size >= 3) && (v2.size >= 3) => // A normal version number MUST take the form X.Y.Z
|
||||
(v1._1.get, v1._2.get, v1._3.get, v1.tags, v2._1.get, v2._2.get, v2._3.get, v2.tags) match {
|
||||
case (x1 @ _, y1 @ _, 0, ts1, x2 @ _, y2 @ _, 0, ts2) if ts1.nonEmpty || ts2.nonEmpty =>
|
||||
// A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers
|
||||
equalsIgnoreExtra(v1, v2)
|
||||
case (x1, y1, _, _, x2, y2, _, _) =>
|
||||
// Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible changes are introduced.
|
||||
(x1 == x2) && (y1 == y2)
|
||||
case _ => equalsIgnoreExtra(v1, v2)
|
||||
}
|
||||
case _ => false
|
||||
case (NormalVersion(x1, y1, _), NormalVersion(x2, y2, _)) => (x1 == x2) && (y1 == y2)
|
||||
case (PrereleaseVersion(_, _, _), PrereleaseVersion(_, _, _)) => v1 == v2 // R2 & R9
|
||||
case _ => false
|
||||
}
|
||||
// Build metadata SHOULD be ignored when determining version precedence.
|
||||
private[this] def equalsIgnoreExtra(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
(v1.numbers == v2.numbers) && (v1.tags == v2.tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
|
|||
|
||||
assertIsNotCompatibleWith(v, "0.12.0-RC1", SecondSegment)
|
||||
assertIsCompatibleWith(v, "0.12.1", SecondSegment)
|
||||
assertIsCompatibleWith(v, "0.12.1-M1", SecondSegment)
|
||||
assertIsNotCompatibleWith(v, "0.12.1-M1", SecondSegment)
|
||||
}
|
||||
|
||||
version("0.1.0-SNAPSHOT") { v =>
|
||||
|
|
@ -85,7 +85,7 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
|
|||
version("2.10.4-20140115-000117-b3a-sources") { v =>
|
||||
assertParsesTo(v, Seq(2, 10, 4), Seq("20140115", "000117", "b3a", "sources"), Seq())
|
||||
assertCascadesTo(v, Seq("2.10.4-20140115-000117-b3a-sources", "2.10.4", "2.10"))
|
||||
assertIsCompatibleWith(v, "2.0.0", SemVer)
|
||||
assertIsNotCompatibleWith(v, "2.0.0", SemVer)
|
||||
assertIsNotCompatibleWith(v, "2.0.0", SecondSegment)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue