@@ -2,6 +2,7 @@ package com.rallyhealth.sbt.versioning
22
33import java .io .File
44
5+ import sbt .util ._
56import scala .sys .process ._
67
78/**
@@ -71,9 +72,37 @@ trait GitDriver {
7172class 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)
0 commit comments