Skip to content

Commit dc17626

Browse files
committed
Migrate latest rally-versioning changes from 1.9.0 to open-source
1 parent 5290cfc commit dc17626

40 files changed

+978
-147
lines changed

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ There are two sbt plugins in this one plugin library:
77

88
# Compatibility
99

10-
Tested and supported for sbt versions: 0.13.17, 1.1.6, and 1.2.1
10+
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
11+
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).
1112

1213
# Install
1314

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

107108
The release arg bumps the version up by a major, minor, or patch increment.
108109
```
109-
sbt -Drelease=major ...
110+
sbt -Drelease=<major|minor|patch> ...
110111
```
111112

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

114115
| versionFromGit | -Drelease | gitVersioningSnapshotLowerBound | Final Version |
115-
| -------------- | --------- | --------------------------------- | ------------- |
116-
| 1.0.0 | patch | | 1.0.1 |
117-
| 1.0.0 | minor | | 1.1.0 |
118-
| 1.0.0 | major | | 2.0.0 |
119-
| 1.0.0 | patch | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT |
120-
| 1.0.0 | minor | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT |
121-
| 1.0.0 | major | 2.0.0 | 2.0.0 |
116+
| -------------- | --------- | ------------------------------- | ------------- |
117+
| 1.0.0 | patch | | 1.0.1 |
118+
| 1.0.0 | minor | | 1.1.0 |
119+
| 1.0.0 | major | | 2.0.0 |
120+
| 1.0.0 | patch | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT |
121+
| 1.0.0 | minor | 2.0.0 | 2.0.0-n-0123abc-SNAPSHOT |
122+
| 1.0.0 | major | 2.0.0 | 2.0.0 |
122123

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

138139
* The patch version is incremented if there are commits, dirty or not. But it is not incremented if there are no
139-
commits.
140+
commits. (I'm not clear on why this is, but it is legacy behavior.)
140141
* The hash does **not** have a 'g' prefix like the output of `git describe`
141142
* A build will be flagged as not clean (and will have a `-dirty-SNAPSHOT` identifier applied) if
142143
`git status --porcelain` returns a non-empty result.

build.sbt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ name := "sbt-git-versioning"
44
organizationName := "Rally Health"
55
organization := "com.rallyhealth.sbt"
66

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

129
bintrayOrganization := Some("rallyhealth")
@@ -26,11 +23,13 @@ scalacOptions ++= {
2623
Seq("-Xfatal-warnings", linting)
2724
}
2825

29-
// to default to sbt 1.0
30-
// sbtVersion in pluginCrossBuild := "1.1.6"
31-
// scalaVersion := "2.12.6"
26+
// Uncomment to default to sbt 0.13 for debugging
27+
// sbtVersion in pluginCrossBuild := "0.13.18"
28+
// scalaVersion := "2.10.6"
3229

33-
crossSbtVersions := List("0.13.17", "1.2.1")
30+
// 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
31+
// https://github.com/lightbend/mima#usage
32+
crossSbtVersions := List("0.13.18", "1.2.8")
3433

3534
publishMavenStyle := false
3635

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

4443
// you need to enable the plugin HERE to depend on code in the plugin JAR. you can't add it as part of
4544
// libraryDependencies nor put it in plugins.sbt
45+
// We use MiMa 0.3.0 because it is the only version that exists for both SBT 1.2.x and 0.13.x
46+
// 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
47+
// https://github.com/lightbend/mima#usage
4648
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.3.0")
4749
addSbtPlugin("com.dwijnand" % "sbt-compat" % "1.2.6")
4850

project/build.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.2.1
1+
sbt.version=1.2.8

project/plugins.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ resolvers += Resolver.url(
22
"Rally Plugin Releases",
33
url("https://dl.bintray.com/rallyhealth/sbt-plugins"))(Resolver.ivyStylePatterns)
44

5-
addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % "1.1.0")
5+
addSbtPlugin("com.rallyhealth.sbt" % "sbt-git-versioning" % "1.3.0")
66
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")

src/main/scala/com/rallyhealth/sbt/semver/SemVerPlugin.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.rallyhealth.sbt.semver
33
import com.rallyhealth.sbt.semver.level._
44
import com.rallyhealth.sbt.semver.level.rule._
55
import com.rallyhealth.sbt.semver.mima._
6+
import com.rallyhealth.sbt.versioning.GitVersioningPlugin.autoImport.semanticVersion
67
import com.rallyhealth.sbt.versioning.{ReleaseVersion, SemVerReleaseType, SemanticVersion}
78
import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport._
89
import com.typesafe.tools.mima.plugin.{MimaKeys, MimaPlugin}
@@ -95,8 +96,7 @@ object SemVerPlugin extends AutoPlugin {
9596

9697
semVerEnforceAfterVersion := None,
9798

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

101101
semVerLimit := "deprecated"
102102
)

src/main/scala/com/rallyhealth/sbt/semver/level/rule/VersionDiffRule.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class NormalVersionBump(
4343
case class MinorOkayForSnapshot(version: SemanticVersion) extends SemVerEnforcementLevel(
4444
releaseType = Minor,
4545
explanation = "SNAPSHOT rules are relaxed for convenience and may include up to minor changes. " +
46-
"This is not part of the SemVer spec, but RallyVersioning can't do this automatically " +
46+
"This is not part of the SemVer spec, but GitVersioningPlugin can't do this automatically " +
4747
"without requiring you to manually bump semVerLimit every time you add a method. " +
4848
"Minor version bumps are enforced only at release time."
4949
)

src/main/scala/com/rallyhealth/sbt/versioning/GitCommit.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,32 @@ object GitCommit {
2424
def apply(fullHash: String, abbreviatedHashLength: Int, tags: Seq[String]): GitCommit =
2525
GitCommit(fullHash, fullHash.take(abbreviatedHashLength), tags)
2626

27+
/**
28+
* Converts a single line from a "git for-each-ref" command into a [[GitCommit]].
29+
*
30+
* @param logOutput Output from "git for-each-ref --sort=-v:refname refs/tags", looks like
31+
* "5ca402250fd63e6ac3a9b51d457b89c092195098 commit refs/tags/v0.0.2"
32+
* @param abbreviatedHashLength The length of abbreviated hashes. This is must be determined from git, and is used
33+
* when outputting the hash.
34+
*/
35+
def fromGitRef(logOutput: String, abbreviatedHashLength: Int): GitCommit = {
36+
val hash = {
37+
val pattern = ("^(" + HashSemVerIdentifier.regex + ")").r
38+
pattern.findFirstIn(logOutput).getOrElse(
39+
throw new IllegalArgumentException("no hash prefix in git log: " + logOutput))
40+
}
41+
42+
val tags = {
43+
val pattern = "refs/tags/(.+)$".r
44+
pattern.findAllMatchIn(logOutput).toSeq
45+
.map(_.group(1).trim) // get only the version, not the whole matching string
46+
.filter(_.nonEmpty) // drop any blanks
47+
.sorted.reverse
48+
}
49+
50+
GitCommit(hash, abbreviatedHashLength, tags)
51+
}
52+
2753
/**
2854
* Converts a single line from a "git log" command into a [[GitCommit]].
2955
*

src/main/scala/com/rallyhealth/sbt/versioning/GitDriver.scala

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.rallyhealth.sbt.versioning
22

33
import java.io.File
44

5+
import sbt.util._
56
import scala.sys.process._
67

78
/**
@@ -71,9 +72,37 @@ trait GitDriver {
7172
class GitDriverImpl(dir: File) extends GitDriver {
7273

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

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

79+
// Validate that the git version is over 2.X.X
80+
protected def isGitCompatible: Boolean = {
81+
val outputLogger = new BufferingProcessLogger
82+
val exitCode: Int = Process(s"""git --version""", dir) ! outputLogger
83+
// git --version returns: git version x.y.z
84+
val gitSemver = """git version (\d+)\.(\d+)\.(\d+).*""".r
85+
exitCode match {
86+
case 0 =>
87+
val gitVersion = outputLogger.stdout.mkString("").trim.toLowerCase
88+
gitVersion match {
89+
case gitSemver(major, minor, patch) =>
90+
major.toInt > 1
91+
case _ =>
92+
throw new GitException(
93+
s"""Version output was not of the form 'git version x.y.z'
94+
|version was '${gitVersion}'""".stripMargin)
95+
}
96+
case unexpected =>
97+
throw new GitException(
98+
s"""Unexpected git exit status: $unexpected
99+
|stderr:
100+
|${outputLogger.stderr.mkString("\n")}
101+
|stdout:
102+
|${outputLogger.stdout.mkString("\n")}""".stripMargin
103+
)
104+
}
105+
}
77106
private def isGitRepo(dir: File): Boolean = {
78107
// this does NOT use runCommand() because that uses this method to check if the directory is a git directory
79108
val outputLogger = new BufferingProcessLogger
@@ -89,7 +118,7 @@ class GitDriverImpl(dir: File) extends GitDriver {
89118
|${outputLogger.stderr.mkString("\n")}
90119
|stdout:
91120
|${outputLogger.stdout.mkString("\n")}""".stripMargin
92-
)
121+
)
93122
}
94123
}
95124

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

99128
case Some(headCommit) =>
100129

101-
// we only care about the RELEASE commits
102-
val releases: Seq[(GitCommit, ReleaseVersion)] = gitLog("").collect { case gc @ ReleaseVersion(rv) => (gc, rv) }
130+
// we only care about the RELEASE commits so let's get them in order from reflog
131+
val releaseRefs: Seq[(GitCommit, ReleaseVersion)] = gitForEachRef("").collect { case gc @ ReleaseVersion(rv) => (gc, rv) }
132+
133+
// We only care about the current release and previous release so let's take the top two.
134+
// Then we want to find out what which git log commit is associated to the reflog sha.
135+
// Note: gitForEachRef will return return reference shas and the tags associated with them.
136+
// the reference shas are not always the same as the commit shas associated with git log.
137+
// That is why we have to run the command here to find the correct sha
138+
// Note: do not move git log into gitForEachRef as on long revisions it will take a LOT of time
139+
val releases: Seq[(GitCommit, ReleaseVersion)] = releaseRefs.take(2).map(tp =>
140+
(gitLog(s"${tp._1.fullHash} --max-count=1").head, tp._2)
141+
)
103142

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

180+
/**
181+
* Executes git rev-parse to determine the current branch/HEAD commit
182+
*/
183+
private def gitBranch: String = {
184+
val cmd = s"git rev-parse --abbrev-ref HEAD"
185+
val (exitCode, output) = runCommand(cmd, throwIfNonZero = false)
186+
exitCode match {
187+
// you get 128 when you run a git cmd in a dir not under git vcs
188+
case 0 =>
189+
val res = output.map { line =>
190+
line
191+
}
192+
res.head
193+
case 128 =>
194+
throw new IllegalStateException(
195+
s"Error 128: a git cmd was run in a dir that is not under git vcs or git rev-parse failed to run.")
196+
}
197+
}
198+
199+
/**
200+
* Returns an ordered list of versions that are merged into your branch.
201+
*/
202+
private def gitForEachRef(arguments: String): Seq[GitCommit] = {
203+
require(isGitRepo(dir), "Must be in a git repository")
204+
require(isGitCompatible, "Must be git version 2.X.X or greater")
205+
206+
// Note: nested shell commands, piping and redirection will not work with runCommand since it is just
207+
// invoking an OS process. You could invoke a shell and pass expressions if needed.
208+
val cmd = s"git for-each-ref --sort=-v:refname refs/tags --merged=${gitBranch}"
209+
210+
/**
211+
* Example output:
212+
* {{{
213+
* 686623c25b52e40fe6270ab57419551b88e89dfe tag refs/tags/v1.0.0
214+
* fb22d49dd7d7bf5b5f130c4ff3b66667d97bc308 commit refs/tags/v0.0.3
215+
* 5ca402250fd63e6ac3a9b51d457b89c092195098 commit refs/tags/v0.0.2
216+
* }}}
217+
*/
218+
219+
// val (exitCode, output) = runCommand(cmd, throwIfNonZero = false)
220+
val (exitCode, output) = runCommand(cmd, throwIfNonZero = false)
221+
exitCode match {
222+
// you get 128 when you run 'git log' on a repository with no commits
223+
case 0 | 128 =>
224+
val abbreviatedHashLength = findAbbreviatedHashLength()
225+
output map { line =>
226+
GitCommit.fromGitRef(line, abbreviatedHashLength)
227+
}
228+
case ret => throw new IllegalStateException(s"Non-zero exit code when running git log: $ret")
229+
}
230+
}
231+
141232
/**
142233
* Executes a single "git log" command.
143234
*/
144235
private def gitLog(arguments: String): Seq[GitCommit] = {
145236
require(isGitRepo(dir), "Must be in a git repository")
237+
require(isGitCompatible, "Must be git version 2.X.X or greater")
146238

147239
// originally this used "git describe", but that doesn't always work the way you want. its definition of "nearest"
148240
// tag is not always what you think it means: it does NOT search backward to the root, it will search other
149241
// branches too. See http://www.xerxesb.com/2010/12/20/git-describe-and-the-tale-of-the-wrong-commits/
150-
val cmd = s"git log --oneline --decorate=short --first-parent --simplify-by-decoration --no-abbrev-commit $arguments"
242+
// The old command was the following:
243+
// git log --oneline --decorate=short --first-parent --simplify-by-decoration --no-abbrev-commit
244+
// which has the argument --first-parent. The problem is that first-parent will hide release that are done in in another
245+
// branch and merged into master.
246+
247+
val cmd = s"git log --oneline --decorate=short --simplify-by-decoration --no-abbrev-commit $arguments"
151248
/**
152249
* Example output:
153250
* {{{
@@ -162,7 +259,9 @@ class GitDriverImpl(dir: File) extends GitDriver {
162259
// you get 128 when you run 'git log' on a repository with no commits
163260
case 0 | 128 =>
164261
val abbreviatedHashLength = findAbbreviatedHashLength()
165-
output.map(line => GitCommit.fromGitLog(line, abbreviatedHashLength))
262+
output map { line =>
263+
GitCommit.fromGitLog(line, abbreviatedHashLength)
264+
}
166265
case ret => throw new IllegalStateException(s"Non-zero exit code when running git log: $ret")
167266
}
168267
}
@@ -205,6 +304,7 @@ class GitDriverImpl(dir: File) extends GitDriver {
205304
*/
206305
private def runCommand(cmd: String, throwIfNonZero: Boolean = true): (Int, Seq[String]) = {
207306
require(isGitRepo(dir), "Must be in a git repository")
307+
require(isGitCompatible, "Must be git version 2.X.X or greater")
208308
val outputLogger = new BufferingProcessLogger
209309
val exitCode: Int = Process(cmd, dir) ! outputLogger
210310
val result = (exitCode, outputLogger.stdout)

src/main/scala/com/rallyhealth/sbt/versioning/GitState.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,14 @@ case class GitWorkingState(isDirty: Boolean)
3030
* [[Option]]s represent multiple states in one class). I learned that makes the logic harder to follow and introduces
3131
* cyclomatic complexity (i.e. nested logic).
3232
*/
33-
sealed trait GitBranchState
33+
sealed trait GitBranchState {
34+
35+
/** Similar to [[Predef.require()]] yet it tacks on "this" to the end of the message. */
36+
protected def require(requirement: Boolean, message: => Any) {
37+
if (!requirement)
38+
throw new IllegalArgumentException(s"requirement failed: $message (from $this)")
39+
}
40+
}
3441

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

0 commit comments

Comments
 (0)