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

Add branch change tasks to allow for gitflow releasing #191

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,32 @@ Now let's also add steps for [posterous-sbt](https://github.com/n8han/posterous-
The `check` part of the release step is run at the start, to make sure we have everything set up to post the release notes later on.
After publishing the actual build artifacts, we also publish the release notes.

#### GitFlow
[Gitflow](http://nvie.com/posts/a-successful-git-branching-model/) is a very popular Git branching method for creating
releases. This involves creating a new release branch from the develop branch.

import ReleaseTransformations._
import sbtrelease._

// ...

releaseProcess := Seq[ReleaseStep](
checkSnapshotDependencies,
inquireVersions,
inquireBranches,
setReleaseVersion,
commitReleaseVersion,
setReleaseBranch,
pushChanges,
setNextBranch,
setNextVersion,
commitNextVersion,
pushChanges
)

`inquireBranches` will ask for a release branch name where the release version will get pushed. It will then
switch back to the current branch before committing and pushing next version.

## Credits
Thank you, [Jason](https://github.com/retronym) and [Mark](https://github.com/harrah), for your feedback and ideas.

Expand Down
57 changes: 45 additions & 12 deletions src/main/scala/ReleaseExtra.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ object ReleaseStateTransformations {

}

lazy val inquireBranches: ReleaseStep = { st: State =>
val releaseBranch = st.get(commandLineReleaseBranch).flatten.getOrElse(SimpleReader.readLine("Release branch : ") match {
case Some(input) => input.trim
case None => sys.error("No branch provided!")
})
val nextBranch = st.get(commandLineNextBranch).flatten.getOrElse(vcs(st).currentBranch)

st.put(branches, (releaseBranch, nextBranch))
}


lazy val runClean : ReleaseStep = ReleaseStep(
action = { st: State =>
Expand Down Expand Up @@ -92,6 +102,23 @@ object ReleaseStateTransformations {
), st)
}

lazy val setReleaseBranch: ReleaseStep = setBranch(_._1)
lazy val setNextBranch: ReleaseStep = setBranch(_._2)
private[sbtrelease] def setBranch(selectBranch: Branches => String): ReleaseStep = { st: State =>
val vs = st.get(branches).getOrElse(sys.error("No branches are set! Was this release part executed before inquireBranches?"))
val selected = selectBranch(vs)

st.log.info(s"Checking out $selected")
val vc = vcs(st)
val processLogger: ProcessLogger = if (vc.isInstanceOf[Git]) {
// Git outputs to standard error, so use a logger that redirects stderr to info
vc.stdErrorToStdOut(st.log)
} else st.log
vc.setBranch(selected) !! processLogger

st
}

private def vcs(st: State): Vcs = {
st.extract.get(releaseVcs).getOrElse(sys.error("Aborting release. Working directory is not a repository of a recognized VCS."))
}
Expand Down Expand Up @@ -213,9 +240,6 @@ object ReleaseStateTransformations {

lazy val pushChanges: ReleaseStep = ReleaseStep(pushChangesAction, checkUpstream)
private[sbtrelease] lazy val checkUpstream = { st: State =>
if (!vcs(st).hasUpstream) {
sys.error("No tracking branch is set up. Either configure a remote tracking branch, or remove the pushChanges release part.")
}
val defaultChoice = extractDefault(st, "n")

val log = toProcessLogger(st)
Expand Down Expand Up @@ -250,18 +274,18 @@ object ReleaseStateTransformations {

val vc = vcs(st)
if (vc.hasUpstream) {
defaultChoice orElse SimpleReader.readLine("Push changes to the remote repository (y/n)? [y] ") match {
case Yes() | Some("") =>
val processLogger: ProcessLogger = if (vc.isInstanceOf[Git]) {
// Git outputs to standard error, so use a logger that redirects stderr to info
vc.stdErrorToStdOut(log)
} else log
vc.pushChanges !! processLogger
case _ => st.log.warn("Remember to push the changes yourself!")
}
defaultChoice orElse SimpleReader.readLine("Push changes to the remote repository (y/n)? [y] ") match {
case Yes() | Some("") =>
val processLogger: ProcessLogger = if (vc.isInstanceOf[Git]) {
// Git outputs to standard error, so use a logger that redirects stderr to info
vc.stdErrorToStdOut(st.log)
} else st.log
vc.pushChanges(!vc.hasUpstream) !! processLogger
case _ => st.log.warn("Remember to push the changes yourself!")
} else {
st.log.info("Changes were NOT pushed, because no upstream branch is configured for the local branch [%s]" format vcs(st).currentBranch)
}

st
}

Expand Down Expand Up @@ -335,12 +359,21 @@ object ExtraReleaseCommands {
private lazy val inquireVersionsCommandKey = "release-inquire-versions"
lazy val inquireVersionsCommand = Command.command(inquireVersionsCommandKey)(inquireVersions)

private lazy val inquireBranchesCommandKey = "release-branches-versions"
lazy val inquireBranchesCommand = Command.command(inquireBranchesCommandKey)(inquireBranches)

private lazy val setReleaseVersionCommandKey = "release-set-release-version"
lazy val setReleaseVersionCommand = Command.command(setReleaseVersionCommandKey)(setReleaseVersion)

private lazy val setNextVersionCommandKey = "release-set-next-version"
lazy val setNextVersionCommand = Command.command(setNextVersionCommandKey)(setNextVersion)

private lazy val setReleaseBranchCommandKey = "release-set-release-branch"
lazy val setReleaseBranchCommand = Command.command(setReleaseBranchCommandKey)(setReleaseBranch)

private lazy val setNextBranchCommandKey = "release-set-next-branch"
lazy val setNextBranchCommand = Command.command(setNextBranchCommandKey)(setNextBranch)

private lazy val commitReleaseVersionCommandKey = "release-commit-release-version"
lazy val commitReleaseVersionCommand = Command.command(commitReleaseVersionCommandKey)(commitReleaseVersion)

Expand Down
13 changes: 12 additions & 1 deletion src/main/scala/ReleasePlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,11 @@ object ReleasePlugin extends AutoPlugin {
object ReleaseKeys {

val versions = AttributeKey[Versions]("releaseVersions")
val branches = AttributeKey[Branches]("releaseBranches")
val commandLineReleaseVersion = AttributeKey[Option[String]]("release-input-release-version")
val commandLineNextVersion = AttributeKey[Option[String]]("release-input-next-version")
val commandLineReleaseBranch = AttributeKey[Option[String]]("release-input-release-branch")
val commandLineNextBranch = AttributeKey[Option[String]]("release-input-next-branch")
val useDefaults = AttributeKey[Boolean]("releaseUseDefaults")
val skipTests = AttributeKey[Boolean]("releaseSkipTests")
val cross = AttributeKey[Boolean]("releaseCross")
Expand All @@ -139,6 +142,10 @@ object ReleasePlugin extends AutoPlugin {
(Space ~> token("release-version") ~> Space ~> token(StringBasic, "<release version>")) map ParseResult.ReleaseVersion
private[this] val NextVersion: Parser[ParseResult] =
(Space ~> token("next-version") ~> Space ~> token(StringBasic, "<next version>")) map ParseResult.NextVersion
private[this] val ReleaseBranch: Parser[ParseResult] =
(Space ~> token("release-branch") ~> Space ~> token(StringBasic, "<release branch>")) map ParseResult.ReleaseBranch
private[this] val NextBranch: Parser[ParseResult] =
(Space ~> token("next-branch") ~> Space ~> token(StringBasic, "<next branch>")) map ParseResult.NextBranch
private[this] val TagDefault: Parser[ParseResult] =
(Space ~> token("default-tag-exists-answer") ~> Space ~> token(StringBasic, "o|k|a|<tag-name>")) map ParseResult.TagDefault

Expand All @@ -147,13 +154,15 @@ object ReleasePlugin extends AutoPlugin {
private[this] object ParseResult {
final case class ReleaseVersion(value: String) extends ParseResult
final case class NextVersion(value: String) extends ParseResult
final case class ReleaseBranch(value: String) extends ParseResult
final case class NextBranch(value: String) extends ParseResult
final case class TagDefault(value: String) extends ParseResult
case object WithDefaults extends ParseResult
case object SkipTests extends ParseResult
case object CrossBuild extends ParseResult
}

private[this] val releaseParser: Parser[Seq[ParseResult]] = (ReleaseVersion | NextVersion | WithDefaults | SkipTests | CrossBuild | TagDefault).*
private[this] val releaseParser: Parser[Seq[ParseResult]] = (ReleaseVersion | NextVersion | ReleaseBranch | NextBranch | WithDefaults | SkipTests | CrossBuild | TagDefault).*

val releaseCommand: Command = Command(releaseCommandKey)(_ => releaseParser) { (st, args) =>
val extracted = Project.extract(st)
Expand All @@ -168,6 +177,8 @@ object ReleasePlugin extends AutoPlugin {
.put(tagDefault, args.collectFirst{case ParseResult.TagDefault(value) => value})
.put(commandLineReleaseVersion, args.collectFirst{case ParseResult.ReleaseVersion(value) => value})
.put(commandLineNextVersion, args.collectFirst{case ParseResult.NextVersion(value) => value})
.put(commandLineReleaseBranch, args.collectFirst{case ParseResult.ReleaseBranch(value) => value})
.put(commandLineNextBranch, args.collectFirst{case ParseResult.NextBranch(value) => value})

val initialChecks = releaseParts.map(_.check)

Expand Down
33 changes: 24 additions & 9 deletions src/main/scala/Vcs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ trait Vcs {
def hasUpstream: Boolean
def trackingRemote: String
def isBehindRemote: Boolean
def pushChanges: ProcessBuilder
def pushChanges(withUpstream: Boolean): ProcessBuilder
def currentBranch: String
def setBranch(branch: String): ProcessBuilder
def hasUntrackedFiles: Boolean = untrackedFiles.nonEmpty
def untrackedFiles: Seq[String]
def hasModifiedFiles: Boolean = modifiedFiles.nonEmpty
Expand Down Expand Up @@ -107,10 +108,12 @@ class Mercurial(val baseDir: File) extends Vcs with GitLike {

def isBehindRemote = cmd("incoming", "-b", ".", "-q") ! devnull == 0

def pushChanges = cmd("push", "-b", ".")
def pushChanges(withUpstream: Boolean) = cmd("push", "-b", ".")

def currentBranch = cmd("branch").!!.trim

def setBranch(branch: String) = throw sys.error("Branch switching not currently supported in hg")

// FIXME: This is utterly bogus, but I cannot find a good way...
def checkRemote(remote: String) = cmd("id", "-n")

Expand All @@ -131,13 +134,21 @@ class Git(val baseDir: File) extends Vcs with GitLike {
private lazy val trackingBranchCmd = cmd("config", "branch.%s.merge" format currentBranch)
private def trackingBranch: String = (trackingBranchCmd !!).trim.stripPrefix("refs/heads/")

private lazy val trackingRemoteCmd: ProcessBuilder = cmd("config", "branch.%s.remote" format currentBranch)
def trackingRemote: String = trackingRemoteCmd.!!.trim
private def trackingRemoteCmd(branch: String): ProcessBuilder = cmd("config", "branch.%s.remote" format branch)
def trackingRemote: String = (trackingRemoteCmd(currentBranch) !!) trim

def hasUpstream = trackingRemoteCmd ! devnull == 0 && trackingBranchCmd ! devnull == 0
def hasUpstream = trackingRemoteCmd(currentBranch) ! devnull == 0 && trackingBranchCmd ! devnull == 0

def currentBranch = cmd("symbolic-ref", "HEAD").!!.trim.stripPrefix("refs/heads/")

def setBranch(branch: String) = {
if (trackingRemoteCmd(branch) ! devnull != 0) {
val currentRemote = trackingRemote
cmd("checkout", "-b", branch).!!
cmd("config", "--add", "branch.%s.remote".format(branch), currentRemote)
} else cmd("checkout", branch)
}

def currentHash = revParse("HEAD")

private def revParse(name: String) = cmd("rev-parse", name).!!.trim
Expand Down Expand Up @@ -176,11 +187,13 @@ class Git(val baseDir: File) extends Vcs with GitLike {

def status = cmd("status", "--porcelain")

def pushChanges = pushCurrentBranch #&& pushTags
def pushChanges(setUpstream: Boolean) = pushCurrentBranch(setUpstream) #&& pushTags

private def pushCurrentBranch = {
private def pushCurrentBranch(setUpstream: Boolean) = {
val localBranch = currentBranch
cmd("push", trackingRemote, "%s:%s" format (localBranch, trackingBranch))
if (setUpstream) {
cmd ("push", "-u", trackingRemote, localBranch)
} else cmd("push", trackingRemote, "%s:%s" format (localBranch, trackingBranch))
}

private def pushTags = cmd("push", "--tags", trackingRemote)
Expand Down Expand Up @@ -217,7 +230,9 @@ class Subversion(val baseDir: File) extends Vcs {

override def currentBranch: String = workingDirSvnUrl.substring(workingDirSvnUrl.lastIndexOf("/") + 1)

override def pushChanges: ProcessBuilder = commit("push changes", false, false)
def setBranch(branch: String) = throw sys.error("Branch switching not currently supported in svn")

override def pushChanges(withUpstream: Boolean): ProcessBuilder = commit("push changes", false, false)

override def isBehindRemote: Boolean = false

Expand Down
1 change: 1 addition & 0 deletions src/main/scala/package.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package object sbtrelease {
type Versions = (String, String)
type Branches = (String, String)

def versionFormatError = sys.error("Version format is not compatible with " + Version.VersionR.pattern.toString)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ val parser = Space ~> StringBasic

checkContentsOfVersionSbt := {
val expected = parser.parsed
val versionFile = ((baseDirectory).value) / "version.sbt"
assert(IO.read(versionFile).contains(expected), s"does not contains ${expected} in ${versionFile}")
val versionFile = ((baseDirectory).value) / "version.sbt"
assert(IO.read(versionFile).contains(expected), s"does not contains ${expected} in ${versionFile}")
}


2 changes: 2 additions & 0 deletions src/sbt-test/sbt-release/gitflow/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
global/
23 changes: 23 additions & 0 deletions src/sbt-test/sbt-release/gitflow/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import ReleaseTransformations._
import sbt.complete.DefaultParsers._

releaseProcess := Seq(
checkSnapshotDependencies,
inquireVersions,
inquireBranches,
setReleaseVersion,
commitReleaseVersion,
setReleaseBranch,
setNextBranch,
setNextVersion,
commitNextVersion
)

val checkContentsOfVersionSbt = inputKey[Unit]("Check that the contents of version.sbt is as expected")
val parser = Space ~> StringBasic

checkContentsOfVersionSbt := {
val expected = parser.parsed
val versionFile = ((baseDirectory).value) / "version.sbt"
assert(IO.read(versionFile).contains(expected), s"does not contains ${expected} in ${versionFile}")
}
7 changes: 7 additions & 0 deletions src/sbt-test/sbt-release/gitflow/project/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
val pluginVersion = System.getProperty("plugin.version")
if(pluginVersion == null)
throw new RuntimeException("""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin)
else addSbtPlugin("com.github.gseitz" % "sbt-release" % pluginVersion)
}
11 changes: 11 additions & 0 deletions src/sbt-test/sbt-release/gitflow/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
$ exec git init .
$ exec git config --add branch.master.remote origin
$ exec git add .
$ exec git commit -m init

> 'release release-version 0.7.0 next-version 1.0.0-SNAPSHOT release-branch release-test next-branch master'
> checkContentsOfVersionSbt 1.0.0-SNAPSHOT
$ exec git checkout release-test
> checkContentsOfVersionSbt 0.7.0

-> release with-defaults
1 change: 1 addition & 0 deletions src/sbt-test/sbt-release/gitflow/version.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version in ThisBuild := "0.1.0-SNAPSHOT"