Skip to content

Commit

Permalink
Merge pull request #18 from rallyhealth/migrate-1.9.0
Browse files Browse the repository at this point in the history
Migrate latest rally-versioning changes from 1.9.0 to open-source
  • Loading branch information
jeffmay authored Mar 11, 2020
2 parents 5290cfc + dc17626 commit 43301a1
Show file tree
Hide file tree
Showing 40 changed files with 978 additions and 147 deletions.
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ There are two sbt plugins in this one plugin library:

# Compatibility

Tested and supported for sbt versions: 0.13.17, 1.1.6, and 1.2.1
Tested and supported for sbt versions: 0.13.18 and 1.2.8. We don't currently support SBT 1.3.x because [there isn't a
version of MiMa 0.3.0 built for SBT 1.3.x, only for 1.2.x and 0.13.x](https://github.com/lightbend/mima#usage).

# Install

Expand Down Expand Up @@ -106,19 +107,19 @@ This is useful for preparing major releases with breaking changes (esp. when com

The release arg bumps the version up by a major, minor, or patch increment.
```
sbt -Drelease=major ...
sbt -Drelease=<major|minor|patch> ...
```

The release arg alters the value of `versionFromGit`, but is still bounded by `gitVersioningSnapshotLowerBound`. For example:

| versionFromGit | -Drelease | gitVersioningSnapshotLowerBound | Final Version |
| -------------- | --------- | --------------------------------- | ------------- |
| 1.0.0 | patch | | 1.0.1 |
| 1.0.0 | minor | | 1.1.0 |
| 1.0.0 | major | | 2.0.0 |
| 1.0.0 | patch | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT |
| 1.0.0 | minor | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT |
| 1.0.0 | major | 2.0.0 | 2.0.0 |
| -------------- | --------- | ------------------------------- | ------------- |
| 1.0.0 | patch | | 1.0.1 |
| 1.0.0 | minor | | 1.1.0 |
| 1.0.0 | major | | 2.0.0 |
| 1.0.0 | patch | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT |
| 1.0.0 | minor | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT |
| 1.0.0 | major | 2.0.0 | 2.0.0 |

#### Example
With most recent git tag at `v1.4.2` and a `gitVersioningSnapshotLowerBound` setting of:
Expand All @@ -136,7 +137,7 @@ $ sbt
### Notes

* The patch version is incremented if there are commits, dirty or not. But it is not incremented if there are no
commits.
commits. (I'm not clear on why this is, but it is legacy behavior.)
* The hash does **not** have a 'g' prefix like the output of `git describe`
* A build will be flagged as not clean (and will have a `-dirty-SNAPSHOT` identifier applied) if
`git status --porcelain` returns a non-empty result.
Expand Down
16 changes: 9 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ name := "sbt-git-versioning"
organizationName := "Rally Health"
organization := "com.rallyhealth.sbt"

// enable after SemVerPlugin supports sbt-plugins
//enablePlugins(SemVerPlugin)
semVerLimit := "1.0.999"
licenses := Seq("MIT" -> url("http://opensource.org/licenses/MIT"))

bintrayOrganization := Some("rallyhealth")
Expand All @@ -26,11 +23,13 @@ scalacOptions ++= {
Seq("-Xfatal-warnings", linting)
}

// to default to sbt 1.0
// sbtVersion in pluginCrossBuild := "1.1.6"
// scalaVersion := "2.12.6"
// Uncomment to default to sbt 0.13 for debugging
// sbtVersion in pluginCrossBuild := "0.13.18"
// scalaVersion := "2.10.6"

crossSbtVersions := List("0.13.17", "1.2.1")
// We don't use SBT 1.3.x because there isn't a version of MiMa 0.3.0 built for SBT 1.3.x, only for 1.2.x and 0.13.x
// https://github.com/lightbend/mima#usage
crossSbtVersions := List("0.13.18", "1.2.8")

publishMavenStyle := false

Expand All @@ -43,6 +42,9 @@ libraryDependencies ++= Seq(

// you need to enable the plugin HERE to depend on code in the plugin JAR. you can't add it as part of
// libraryDependencies nor put it in plugins.sbt
// We use MiMa 0.3.0 because it is the only version that exists for both SBT 1.2.x and 0.13.x
// Also MiMa 0.6.x has some source incompatible changes, so we'd have to fork the source to support 0.6.x and 0.3.x
// https://github.com/lightbend/mima#usage
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.3.0")
addSbtPlugin("com.dwijnand" % "sbt-compat" % "1.2.6")

Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.2.1
sbt.version=1.2.8
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ resolvers += Resolver.url(
"Rally Plugin Releases",
url("https://dl.bintray.com/rallyhealth/sbt-plugins"))(Resolver.ivyStylePatterns)

addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % "1.1.0")
addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % "1.3.0")
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")
4 changes: 2 additions & 2 deletions src/main/scala/com/rallyhealth/sbt/semver/SemVerPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.rallyhealth.sbt.semver
import com.rallyhealth.sbt.semver.level._
import com.rallyhealth.sbt.semver.level.rule._
import com.rallyhealth.sbt.semver.mima._
import com.rallyhealth.sbt.versioning.GitVersioningPlugin.autoImport.semanticVersion
import com.rallyhealth.sbt.versioning.{ReleaseVersion, SemVerReleaseType, SemanticVersion}
import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport._
import com.typesafe.tools.mima.plugin.{MimaKeys, MimaPlugin}
Expand Down Expand Up @@ -95,8 +96,7 @@ object SemVerPlugin extends AutoPlugin {

semVerEnforceAfterVersion := None,

semVerVersion := SemanticVersion.fromString(version.value).getOrElse(
throw new IllegalArgumentException(s"version=${version.value} is not a valid SemVer")),
semVerVersion := semanticVersion.value,

semVerLimit := "deprecated"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class NormalVersionBump(
case class MinorOkayForSnapshot(version: SemanticVersion) extends SemVerEnforcementLevel(
releaseType = Minor,
explanation = "SNAPSHOT rules are relaxed for convenience and may include up to minor changes. " +
"This is not part of the SemVer spec, but RallyVersioning can't do this automatically " +
"This is not part of the SemVer spec, but GitVersioningPlugin can't do this automatically " +
"without requiring you to manually bump semVerLimit every time you add a method. " +
"Minor version bumps are enforced only at release time."
)
26 changes: 26 additions & 0 deletions src/main/scala/com/rallyhealth/sbt/versioning/GitCommit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ object GitCommit {
def apply(fullHash: String, abbreviatedHashLength: Int, tags: Seq[String]): GitCommit =
GitCommit(fullHash, fullHash.take(abbreviatedHashLength), tags)

/**
* Converts a single line from a "git for-each-ref" command into a [[GitCommit]].
*
* @param logOutput Output from "git for-each-ref --sort=-v:refname refs/tags", looks like
* "5ca402250fd63e6ac3a9b51d457b89c092195098 commit refs/tags/v0.0.2"
* @param abbreviatedHashLength The length of abbreviated hashes. This is must be determined from git, and is used
* when outputting the hash.
*/
def fromGitRef(logOutput: String, abbreviatedHashLength: Int): GitCommit = {
val hash = {
val pattern = ("^(" + HashSemVerIdentifier.regex + ")").r
pattern.findFirstIn(logOutput).getOrElse(
throw new IllegalArgumentException("no hash prefix in git log: " + logOutput))
}

val tags = {
val pattern = "refs/tags/(.+)$".r
pattern.findAllMatchIn(logOutput).toSeq
.map(_.group(1).trim) // get only the version, not the whole matching string
.filter(_.nonEmpty) // drop any blanks
.sorted.reverse
}

GitCommit(hash, abbreviatedHashLength, tags)
}

/**
* Converts a single line from a "git log" command into a [[GitCommit]].
*
Expand Down
110 changes: 105 additions & 5 deletions src/main/scala/com/rallyhealth/sbt/versioning/GitDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.rallyhealth.sbt.versioning

import java.io.File

import sbt.util._
import scala.sys.process._

/**
Expand Down Expand Up @@ -71,9 +72,37 @@ trait GitDriver {
class GitDriverImpl(dir: File) extends GitDriver {

require(isGitRepo(dir), "Must be in a git repository")
require(isGitCompatible, "Must be git version 2.X.X or greater")

private class GitException(msg: String) extends Exception(msg)

// Validate that the git version is over 2.X.X
protected def isGitCompatible: Boolean = {
val outputLogger = new BufferingProcessLogger
val exitCode: Int = Process(s"""git --version""", dir) ! outputLogger
// git --version returns: git version x.y.z
val gitSemver = """git version (\d+)\.(\d+)\.(\d+).*""".r
exitCode match {
case 0 =>
val gitVersion = outputLogger.stdout.mkString("").trim.toLowerCase
gitVersion match {
case gitSemver(major, minor, patch) =>
major.toInt > 1
case _ =>
throw new GitException(
s"""Version output was not of the form 'git version x.y.z'
|version was '${gitVersion}'""".stripMargin)
}
case unexpected =>
throw new GitException(
s"""Unexpected git exit status: $unexpected
|stderr:
|${outputLogger.stderr.mkString("\n")}
|stdout:
|${outputLogger.stdout.mkString("\n")}""".stripMargin
)
}
}
private def isGitRepo(dir: File): Boolean = {
// this does NOT use runCommand() because that uses this method to check if the directory is a git directory
val outputLogger = new BufferingProcessLogger
Expand All @@ -89,7 +118,7 @@ class GitDriverImpl(dir: File) extends GitDriver {
|${outputLogger.stderr.mkString("\n")}
|stdout:
|${outputLogger.stdout.mkString("\n")}""".stripMargin
)
)
}
}

Expand All @@ -98,8 +127,18 @@ class GitDriverImpl(dir: File) extends GitDriver {

case Some(headCommit) =>

// we only care about the RELEASE commits
val releases: Seq[(GitCommit, ReleaseVersion)] = gitLog("").collect { case gc @ ReleaseVersion(rv) => (gc, rv) }
// we only care about the RELEASE commits so let's get them in order from reflog
val releaseRefs: Seq[(GitCommit, ReleaseVersion)] = gitForEachRef("").collect { case gc @ ReleaseVersion(rv) => (gc, rv) }

// We only care about the current release and previous release so let's take the top two.
// Then we want to find out what which git log commit is associated to the reflog sha.
// Note: gitForEachRef will return return reference shas and the tags associated with them.
// the reference shas are not always the same as the commit shas associated with git log.
// That is why we have to run the command here to find the correct sha
// Note: do not move git log into gitForEachRef as on long revisions it will take a LOT of time
val releases: Seq[(GitCommit, ReleaseVersion)] = releaseRefs.take(2).map(tp =>
(gitLog(s"${tp._1.fullHash} --max-count=1").head, tp._2)
)

val maybeCurrRelease = releases.headOption
val maybePrevRelease = releases.drop(1).headOption
Expand Down Expand Up @@ -138,16 +177,74 @@ class GitDriverImpl(dir: File) extends GitDriver {
output.mkString("").trim.toInt
}

/**
* Executes git rev-parse to determine the current branch/HEAD commit
*/
private def gitBranch: String = {
val cmd = s"git rev-parse --abbrev-ref HEAD"
val (exitCode, output) = runCommand(cmd, throwIfNonZero = false)
exitCode match {
// you get 128 when you run a git cmd in a dir not under git vcs
case 0 =>
val res = output.map { line =>
line
}
res.head
case 128 =>
throw new IllegalStateException(
s"Error 128: a git cmd was run in a dir that is not under git vcs or git rev-parse failed to run.")
}
}

/**
* Returns an ordered list of versions that are merged into your branch.
*/
private def gitForEachRef(arguments: String): Seq[GitCommit] = {
require(isGitRepo(dir), "Must be in a git repository")
require(isGitCompatible, "Must be git version 2.X.X or greater")

// Note: nested shell commands, piping and redirection will not work with runCommand since it is just
// invoking an OS process. You could invoke a shell and pass expressions if needed.
val cmd = s"git for-each-ref --sort=-v:refname refs/tags --merged=${gitBranch}"

/**
* Example output:
* {{{
* 686623c25b52e40fe6270ab57419551b88e89dfe tag refs/tags/v1.0.0
* fb22d49dd7d7bf5b5f130c4ff3b66667d97bc308 commit refs/tags/v0.0.3
* 5ca402250fd63e6ac3a9b51d457b89c092195098 commit refs/tags/v0.0.2
* }}}
*/

// val (exitCode, output) = runCommand(cmd, throwIfNonZero = false)
val (exitCode, output) = runCommand(cmd, throwIfNonZero = false)
exitCode match {
// you get 128 when you run 'git log' on a repository with no commits
case 0 | 128 =>
val abbreviatedHashLength = findAbbreviatedHashLength()
output map { line =>
GitCommit.fromGitRef(line, abbreviatedHashLength)
}
case ret => throw new IllegalStateException(s"Non-zero exit code when running git log: $ret")
}
}

/**
* Executes a single "git log" command.
*/
private def gitLog(arguments: String): Seq[GitCommit] = {
require(isGitRepo(dir), "Must be in a git repository")
require(isGitCompatible, "Must be git version 2.X.X or greater")

// originally this used "git describe", but that doesn't always work the way you want. its definition of "nearest"
// tag is not always what you think it means: it does NOT search backward to the root, it will search other
// branches too. See http://www.xerxesb.com/2010/12/20/git-describe-and-the-tale-of-the-wrong-commits/
val cmd = s"git log --oneline --decorate=short --first-parent --simplify-by-decoration --no-abbrev-commit $arguments"
// The old command was the following:
// git log --oneline --decorate=short --first-parent --simplify-by-decoration --no-abbrev-commit
// which has the argument --first-parent. The problem is that first-parent will hide release that are done in in another
// branch and merged into master.

val cmd = s"git log --oneline --decorate=short --simplify-by-decoration --no-abbrev-commit $arguments"
/**
* Example output:
* {{{
Expand All @@ -162,7 +259,9 @@ class GitDriverImpl(dir: File) extends GitDriver {
// you get 128 when you run 'git log' on a repository with no commits
case 0 | 128 =>
val abbreviatedHashLength = findAbbreviatedHashLength()
output.map(line => GitCommit.fromGitLog(line, abbreviatedHashLength))
output map { line =>
GitCommit.fromGitLog(line, abbreviatedHashLength)
}
case ret => throw new IllegalStateException(s"Non-zero exit code when running git log: $ret")
}
}
Expand Down Expand Up @@ -205,6 +304,7 @@ class GitDriverImpl(dir: File) extends GitDriver {
*/
private def runCommand(cmd: String, throwIfNonZero: Boolean = true): (Int, Seq[String]) = {
require(isGitRepo(dir), "Must be in a git repository")
require(isGitCompatible, "Must be git version 2.X.X or greater")
val outputLogger = new BufferingProcessLogger
val exitCode: Int = Process(cmd, dir) ! outputLogger
val result = (exitCode, outputLogger.stdout)
Expand Down
9 changes: 8 additions & 1 deletion src/main/scala/com/rallyhealth/sbt/versioning/GitState.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ case class GitWorkingState(isDirty: Boolean)
* [[Option]]s represent multiple states in one class). I learned that makes the logic harder to follow and introduces
* cyclomatic complexity (i.e. nested logic).
*/
sealed trait GitBranchState
sealed trait GitBranchState {

/** Similar to [[Predef.require()]] yet it tacks on "this" to the end of the message. */
protected def require(requirement: Boolean, message: => Any) {
if (!requirement)
throw new IllegalArgumentException(s"requirement failed: $message (from $this)")
}
}

/**
* This is used when there are two (2) commits that are tagged as [[ReleaseVersion]]s, and the HEAD commit is the
Expand Down
Loading

0 comments on commit 43301a1

Please sign in to comment.