From 280bcecda806c205bd9dffc927450bf758bee223 Mon Sep 17 00:00:00 2001 From: Iain Cardnell Date: Mon, 8 Jun 2020 19:04:41 +1000 Subject: [PATCH] feat: add config command and multiple replacements in patch - New config command to display config - Info command now excludes environment variables by default - Each patch can have multiple replacements - Add all conventional commits to default CommitMessageActions --- README.md | 15 +- build.cmd | 3 +- build.sbt | 2 +- docs/config_reference.md | 23 ++- docs/installation.md | 2 +- docs/usage.md | 17 +- etc/Formula/git-mkver.rb | 4 +- etc/scoop/git-mkver.json | 6 +- etc/shell/install.sh | 2 +- git-mkver.conf | 29 +++- .../scala/net/cardnell/mkver/AppConfig.scala | 152 +++++++++++------- .../net/cardnell/mkver/CommandLineArgs.scala | 26 +-- src/main/scala/net/cardnell/mkver/Main.scala | 95 +++++------ .../scala/net/cardnell/mkver/Version.scala | 23 +-- .../scala/net/cardnell/mkver/package.scala | 2 +- .../net/cardnell/mkver/EndToEndTests.scala | 33 ++-- .../scala/net/cardnell/mkver/MainSpec.scala | 19 +-- 17 files changed, 262 insertions(+), 191 deletions(-) diff --git a/README.md b/README.md index 2fdcc48..860ac10 100644 --- a/README.md +++ b/README.md @@ -12,19 +12,16 @@ For more information head to the [project site](https://idc101.github.io/git-mkv - Branch names - Manual tagging - Next version conforms to [Semantic Versioning](https://semver.org/) scheme -- Patch the next version into the build: - - Java - - C# - - Many others, fully configurable -- Tag the current commit with the next version +- Patch the next version into source files using a configurable find and replace system +- Tag the current commit with the next version number -All of this can be configured based on the branch name so release/master branches get different -version numbers to develop or feature branches. +Works out of the box with trunk based development, GitFlow and GithubFlow. Alternatively all of this can be configured +based on the branch name so release/master branches get different version numbers to develop or feature branches. ## Installation -Download the binary for your os from the [releases](https://github.com/idc101/git-mkver/releases) page and copy to somewhere on your path. - +[Install](https://idc101.github.io/git-mkver/installation) with brew, scoop or simply download the binary for your os +from the [releases](https://github.com/idc101/git-mkver/releases) page and copy somewhere on your path. ## Usage diff --git a/build.cmd b/build.cmd index cb91e9f..f733573 100644 --- a/build.cmd +++ b/build.cmd @@ -1,9 +1,10 @@ call "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" -FOR /F %i IN ('git mkver next') DO set VERSION=%i +FOR /F %%i IN ('git mkver next') DO set VERSION=%%i call sbt assembly cd target\scala-2.12 call native-image -jar git-mkver-assembly-%VERSION%.jar --no-fallback del git-mkver.exe move git-mkver-assembly-%VERSION%.exe git-mkver.exe PowerShell -Command "Compress-Archive -Path 'git-mkver.exe' -DestinationPath 'git-mkver-windows-amd64-%VERSION%.zip'" +PowerShell -Command "Get-FileHash git-mkver-windows-amd64-%VERSION%.zip | %% Hash" cd ..\..\ diff --git a/build.sbt b/build.sbt index 1c73fc6..5e1e597 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ import Dependencies._ ThisBuild / scalaVersion := "2.12.11" -ThisBuild / version := "1.0.0" +ThisBuild / version := "1.1.0" ThisBuild / organization := "net.cardnell" lazy val root = (project in file(".")) diff --git a/docs/config_reference.md b/docs/config_reference.md index 21bcac1..add5987 100644 --- a/docs/config_reference.md +++ b/docs/config_reference.md @@ -89,16 +89,27 @@ patches: [ "**/Chart.yaml" # Chart.yaml in any subdirectory of the current working directory "Chart.yaml" # Chart.yaml the current working directory only ] - # search string, using java regular expression syntax (https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html) - find: "appVersion: .*" - # replacement string using substitutions from formats - replace: "appVersion: \"{Version}\"" + # list of replacements to apply to files + replacements: [ + { + # search string, using java regular expression syntax (https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html) + # find strings can include the special marker `{VersionRegex}` which will be replaced with the regular expression + # for a Semantic Version. + find: "appVersion: {VersionRegex}" + # replacement string using substitutions from formats + replace: "appVersion: \"{Version}\"" + } + ] } { name: Csproj filePatterns: ["**/*.csproj"] - find: ".*" - replace: "{Version}" + replacements: [ + { + find: ".*" + replace: "{Version}" + } + ] } ] # commitMessageActions configure how different commit messages will increment diff --git a/docs/installation.md b/docs/installation.md index 7502b2c..707a7ed 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -3,7 +3,7 @@ ## Linux ```bash -curl -L https://github.com/idc101/git-mkver/releases/download/v1.0.0/git-mkver-darwin-amd64-1.0.0.tar.gz | tar xvz +curl -L https://github.com/idc101/git-mkver/releases/download/v1.1.0/git-mkver-darwin-amd64-1.1.0.tar.gz | tar xvz sudo mv git-mkver /usr/local/bin ``` diff --git a/docs/usage.md b/docs/usage.md index 32534f9..51e4d5e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -68,7 +68,9 @@ $ git mkver tag The next version number will be determined based on the commit messages since the last version was tagged. The commit messages that trigger different version -increments are [configurable](config_reference) but by default they are as follows: +increments are [configurable](config_reference) but by default they are based on +the [Conventional Commit](https://www.conventionalcommits.org/) specification +as follows: - Commits containing the following will increment the _major_ version: - `major:` or `major(...):` @@ -80,6 +82,9 @@ increments are [configurable](config_reference) but by default they are as follo - `patch:` or `patch(...):` - `fix:` or `fix(...):` +Other conventional commits such as `build`, `chore`, `docs` will not increment +the version number. + All commit messages since the last tagged message are analyzed and the greatest version increment is used. For example if one commit is a minor change and one is a major change then the major version will be incremented. @@ -119,7 +124,7 @@ you can use the `patch` command. The files to be patched and the replacements ar defined in the `mkver.conf` [config](config) file. For example, suppose you have the version number in a code file: -``` +```scala object VersionInfo { val version = "1.0.0" } @@ -130,8 +135,12 @@ and you define a patch as follows in your config file: { name: Readme filePatterns: ["version.scala"] - find: "val version = \".*\"" - replace: "val version = \"{Next}\"" + replacements: [ + { + find: "val version = \"{VersionRegex}\"" + replace: "val version = \"{Next}\"" + } + ] } ``` diff --git a/etc/Formula/git-mkver.rb b/etc/Formula/git-mkver.rb index 53b14d1..2fa78c5 100644 --- a/etc/Formula/git-mkver.rb +++ b/etc/Formula/git-mkver.rb @@ -1,6 +1,6 @@ class GitMkver < Formula - MKVER_VERSION = "1.0.0".freeze - MKVER_SHA256 = "d1546df319e236d038f7c6b3b88ca155fb882b237011c2cbd7e56e9a16b6ba56".freeze + MKVER_VERSION = "1.1.0".freeze + MKVER_SHA256 = "f69fb9b97f510b05455138fc202b53aa5d3f55af471d995bbe74888aeaea28db".freeze desc "Installs git-mkver from pre-built binaries" homepage "https://idc101.github.io/git-mkver/" diff --git a/etc/scoop/git-mkver.json b/etc/scoop/git-mkver.json index 7652ecf..6959b9f 100644 --- a/etc/scoop/git-mkver.json +++ b/etc/scoop/git-mkver.json @@ -1,8 +1,8 @@ { - "version": "1.0.0", + "version": "1.1.0", "description": "Automatic Semantic Versioning for git based software development", - "url": "https://github.com/idc101/git-mkver/releases/download/v1.0.0/git-mkver-windows-amd64-1.0.0.zip", - "hash": "913cd814cf8dd2c5f933f484834c43b0ab62539b4791ce731327e0bb2fb567ac", + "url": "https://github.com/idc101/git-mkver/releases/download/v1.1.0/git-mkver-windows-amd64-1.1.0.zip", + "hash": "3F0E0E2C982A3B7C4C74B5D7CCCF4340FA6DB796337E4613C67EAF01D8B1CF60", "extract_to": "", "bin": "git-mkver.exe" } diff --git a/etc/shell/install.sh b/etc/shell/install.sh index 4656e72..65d55eb 100755 --- a/etc/shell/install.sh +++ b/etc/shell/install.sh @@ -1,5 +1,5 @@ #/bin/bash -MKVER_VERSION=1.0.0 +MKVER_VERSION=1.1.0 curl -L https://github.com/idc101/git-mkver/releases/download/v${MKVER_VERSION}/git-mkver-darwin-amd64-${MKVER_VERSION}.tar.gz -o git-mkver.tar.gz tar xvzf git-mkver.tar.gz sudo mv git-mkver /usr/local/bin diff --git a/git-mkver.conf b/git-mkver.conf index b5d845a..36c8fe4 100644 --- a/git-mkver.conf +++ b/git-mkver.conf @@ -3,14 +3,19 @@ defaults { patches: [ Sbt Installers + ScalaVersion ] } patches: [ { name: Sbt filePatterns: ["build.sbt"] - find: "version\\s+:=\\s+\".*\"" - replace: "version := \"{Next}\"" + replacements: [ + { + find: "version\\s+:=\\s+\"{VersionRegex}\"" + replace: "version := \"{Next}\"" + } + ] } { name: Installers @@ -20,7 +25,23 @@ patches: [ "etc/scoop/git-mkver.json" "etc/shell/install.sh" ] - find: "\\d+\\.\\d+\\.\\d+" - replace: "{Version}" + replacements: [ + { + find: "{VersionRegex}" + replace: "{Version}" + } + ] + } + { + name: ScalaVersion + filePatterns: [ + "src/main/scala/net/cardnell/mkver/package.scala" + ] + replacements: [ + { + find: "val GitMkverVersion = \"{VersionRegex}\"" + replace: "val GitMkverVersion = \"{Version}\"" + } + ] } ] diff --git a/src/main/scala/net/cardnell/mkver/AppConfig.scala b/src/main/scala/net/cardnell/mkver/AppConfig.scala index 5c8df6f..4573a76 100644 --- a/src/main/scala/net/cardnell/mkver/AppConfig.scala +++ b/src/main/scala/net/cardnell/mkver/AppConfig.scala @@ -2,21 +2,26 @@ package net.cardnell.mkver import cats.implicits._ import com.typesafe.config.ConfigFactory -import net.cardnell.mkver.IncrementAction.IncrementMinor -import zio.{Task, ZIO} -import zio.config.ConfigDescriptor.{string, _} +import net.cardnell.mkver.ConfigDesc._ +import net.cardnell.mkver.IncrementAction._ +import net.cardnell.mkver.VersionMode.SemVer +import zio.config.ConfigDescriptor._ import zio.config._ import zio.config.typesafe.TypesafeConfigSource +import zio.{Task, ZIO} case class Format(name: String, format: String) +case class Replacement(find: String, replace: String) +case class PatchConfig(name: String, filePatterns: List[String], replacements: List[Replacement]) +case class CommitMessageAction(pattern: String, action: IncrementAction) -object Format { - val formatDesc = ( - string("name").describe("Name of format. e.g. 'MajorMinor'") |@| - string("format").describe("Format string for this format. Can include other formats. e.g. '{x}.{y}'") - )(Format.apply, Format.unapply) -} +case class AppConfig(mode: VersionMode, + tagPrefix: Option[String], + defaults: Option[BranchConfigDefaults], + branches: Option[List[BranchConfig]], + patches: Option[List[PatchConfig]], + commitMessageActions: Option[List[CommitMessageAction]]) case class RunConfig(tag: Boolean, tagPrefix: String, @@ -48,7 +53,7 @@ case class BranchConfig(pattern: String, formats: Option[List[Format]], patches: Option[List[String]]) -object BranchConfig { +object ConfigDesc { object Defaults { val name = ".*" val tag = false @@ -56,9 +61,9 @@ object BranchConfig { val preReleaseFormat = "RC{PreReleaseNumber}" val buildMetaDataFormat = "{Branch}.{ShortHash}" val includeBuildMetaData = true - val whenNoValidCommitMessages = IncrementMinor - val patches = Nil - val formats = Nil + val whenNoValidCommitMessages = IncrementPatch + val formats: List[Format] = Nil + val patches: List[String] = Nil } def readPreReleaseFormat(value: String): Either[String, String] = @@ -68,6 +73,46 @@ object BranchConfig { Right(value) } + def readIncrementAction(value: String): Either[String, IncrementAction] = + value match { + case "Fail" => Right(Fail) + case "IncrementMajor" => Right(IncrementMajor) + case "IncrementMinor" => Right(IncrementMinor) + case "IncrementPatch" => Right(IncrementPatch) + case "NoIncrement" => Right(NoIncrement) + case _ => Left("IncrementAction must be one of Fail|IncrementMajor|IncrementMinor|IncrementPatch|NoIncrement") + } + + def readVersionMode(value: String): Either[String, VersionMode] = + value match { + case "SemVer" => Right(SemVer) + case _ => Left("VersionMode must be one of: SemVer") + } + + + val formatDesc = ( + string("name").describe("Name of format. e.g. 'MajorMinor'") |@| + string("format").describe("Format string for this format. Can include other formats. e.g. '{x}.{y}'") + )(Format.apply, Format.unapply) + + val replacementDesc = ( + string("find").describe("Regex to find in file") |@| + string("replace").describe("Replacement string. Can include version format strings (see help)") + )(Replacement.apply, Replacement.unapply) + + val patchConfigDesc = ( + string("name").describe("Name of patch, referenced from branch configs") |@| + list("filePatterns")(string).describe("Files to apply find and replace in. Supports ** and * glob patterns.") |@| + listStrict("replacements")(replacementDesc).describe("Find and replace patterns") + )(PatchConfig.apply, PatchConfig.unapply) + + val commitMessageActionDesc = ( + string("pattern").describe("Regular expression to match a commit message line") |@| + string("action") + .xmapEither(ConfigDesc.readIncrementAction, (output: IncrementAction) => Right(output.toString)) + .describe("Version Increment behaviour if a commit line matches the regex Fail|IncrementMajor|IncrementMinor|IncrementPatch|NoIncrement") + )(CommitMessageAction.apply, CommitMessageAction.unapply) + val patternDesc = string("pattern").describe("regex to match branch name on") val tagDesc = boolean("tag").describe("whether to actually tag this branch when `mkver tag` is called") val tagMessageFormatDesc = string("tagMessageFormat").describe("format to be used in the annotated git tag message") @@ -77,9 +122,9 @@ object BranchConfig { val buildMetaDataFormatDesc = string("buildMetaDataFormat").describe("format to be used for the build metadata. e.g. {BranchName}") val includeBuildMetaDataDesc = boolean("includeBuildMetaData").describe("whether the tag version includes the build metadata component") val whenNoValidCommitMessages = string("whenNoValidCommitMessages") - .xmapEither(IncrementAction.read, (output: IncrementAction) => Right(output.toString)) + .xmapEither(ConfigDesc.readIncrementAction, (output: IncrementAction) => Right(output.toString)) .describe("behaviour if no valid commit messages are found Fail|IncrementMajor|IncrementMinor|IncrementPatch|NoIncrement") - val formatsDesc = nested("formats")(list(Format.formatDesc)).describe("custom format strings") + val formatsDesc = listStrict("formats")(ConfigDesc.formatDesc).describe("custom format strings") val patchesDesc = list("patches")(string).describe("Patch configs to be applied") val branchConfigDefaultsDesc = ( @@ -89,8 +134,8 @@ object BranchConfig { buildMetaDataFormatDesc.default(Defaults.buildMetaDataFormat) |@| includeBuildMetaDataDesc.default(Defaults.includeBuildMetaData) |@| whenNoValidCommitMessages.default(Defaults.whenNoValidCommitMessages) |@| - formatsDesc.default(Defaults.formats) |@| - patchesDesc.default(Defaults.patches) + formatsDesc.default(ConfigDesc.Defaults.formats) |@| + patchesDesc.default(ConfigDesc.Defaults.patches) )(BranchConfigDefaults.apply, BranchConfigDefaults.unapply) val branchConfigDesc = ( @@ -106,57 +151,39 @@ object BranchConfig { )(BranchConfig.apply, BranchConfig.unapply) } -case class PatchConfig(name: String, filePatterns: List[String], find: String, replace: String) - -object PatchConfig { - val patchConfigDesc = ( - string("name").describe("Name of patch, referenced from branch configs") |@| - list("filePatterns")(string).describe("Files to apply find and replace in. Supports ** and * glob patterns.") |@| - string("find").describe("Regex to find in file") |@| - string("replace").describe("Replacement string. Can include version format strings (see help)") - )(PatchConfig.apply, PatchConfig.unapply) -} - -case class CommitMessageAction(pattern: String, action: IncrementAction) - -object CommitMessageAction { - val commitMessageActionDesc = ( - string("pattern").describe("Regular expression to match a commit message line") |@| - string("action") - .xmapEither(IncrementAction.read, (output: IncrementAction) => Right(output.toString)) - .describe("Version Increment behaviour if a commit line matches the regex Fail|IncrementMajor|IncrementMinor|IncrementPatch|NoIncrement") - )(CommitMessageAction.apply, CommitMessageAction.unapply) -} - -case class AppConfig(mode: VersionMode, - tagPrefix: Option[String], - defaults: Option[BranchConfigDefaults], - branches: Option[List[BranchConfig]], - patches: Option[List[PatchConfig]], - commitMessageActions: Option[List[CommitMessageAction]]) - object AppConfig { val appConfigDesc = ( - string("mode").xmapEither(VersionMode.read, (output: VersionMode) => Right(output.toString)) + string("mode").xmapEither(ConfigDesc.readVersionMode, (output: VersionMode) => Right(output.toString)) .describe("The Version Mode for this repository") .default(VersionMode.SemVer) |@| string("tagPrefix").describe("prefix for git tags").optional |@| - nested("defaults")(BranchConfig.branchConfigDefaultsDesc).optional |@| - nested("branches")(list(BranchConfig.branchConfigDesc)).optional |@| - nested("patches")(list(PatchConfig.patchConfigDesc)).optional |@| - nested("commitMessageActions")(list(CommitMessageAction.commitMessageActionDesc)).optional + nested("defaults")(ConfigDesc.branchConfigDefaultsDesc).optional |@| + listStrict("branches")(ConfigDesc.branchConfigDesc).optional |@| + listStrict("patches")(ConfigDesc.patchConfigDesc).optional |@| + listStrict("commitMessageActions")(ConfigDesc.commitMessageActionDesc).optional )(AppConfig.apply, AppConfig.unapply) - + val runConfigDesc = ( + tagDesc |@| + string("tagPrefix") |@| + tagMessageFormatDesc |@| + preReleaseFormatDesc |@| + buildMetaDataFormatDesc |@| + includeBuildMetaDataDesc |@| + listStrict("commitMessageActions")(ConfigDesc.commitMessageActionDesc) |@| + whenNoValidCommitMessages |@| + formatsDesc |@| + listStrict("patches")(ConfigDesc.patchConfigDesc) + )(RunConfig.apply, RunConfig.unapply) val defaultDefaultBranchConfig: BranchConfigDefaults = BranchConfigDefaults( - BranchConfig.Defaults.tag, - BranchConfig.Defaults.tagMessageFormat, - BranchConfig.Defaults.preReleaseFormat, - BranchConfig.Defaults.buildMetaDataFormat, - BranchConfig.Defaults.includeBuildMetaData, - BranchConfig.Defaults.whenNoValidCommitMessages, - BranchConfig.Defaults.patches, - BranchConfig.Defaults.formats + ConfigDesc.Defaults.tag, + ConfigDesc.Defaults.tagMessageFormat, + ConfigDesc.Defaults.preReleaseFormat, + ConfigDesc.Defaults.buildMetaDataFormat, + ConfigDesc.Defaults.includeBuildMetaData, + ConfigDesc.Defaults.whenNoValidCommitMessages, + ConfigDesc.Defaults.formats, + ConfigDesc.Defaults.patches ) val defaultBranchConfigs: List[BranchConfig] = List( BranchConfig("master", Some(true), None, None, None, Some(false), None, None, None), @@ -169,8 +196,11 @@ object AppConfig { CommitMessageAction("major(\\(.+\\))?:", IncrementAction.IncrementMajor), CommitMessageAction("minor(\\(.+\\))?:", IncrementAction.IncrementMinor), CommitMessageAction("patch(\\(.+\\))?:", IncrementAction.IncrementPatch), + CommitMessageAction("feature(\\(.+\\))?:", IncrementAction.IncrementMinor), CommitMessageAction("feat(\\(.+\\))?:", IncrementAction.IncrementMinor), - CommitMessageAction("fix(\\(.+\\))?:", IncrementAction.IncrementPatch) + CommitMessageAction("fix(\\(.+\\))?:", IncrementAction.IncrementPatch), + // The rest of the conventional commits + CommitMessageAction("(build|ci|chore|docs|perf|refactor|revert|style|test)(\\(.+\\))?:", IncrementAction.NoIncrement) ) def getRunConfig(configFile: Option[String], currentBranch: String): Task[RunConfig] = { diff --git a/src/main/scala/net/cardnell/mkver/CommandLineArgs.scala b/src/main/scala/net/cardnell/mkver/CommandLineArgs.scala index 9a08e2c..8275ee7 100644 --- a/src/main/scala/net/cardnell/mkver/CommandLineArgs.scala +++ b/src/main/scala/net/cardnell/mkver/CommandLineArgs.scala @@ -2,23 +2,27 @@ package net.cardnell.mkver import com.monovore.decline.{Command, Opts} import cats.implicits._ +import cats.instances.unit +import net.cardnell.mkver.CommandLineArgs.ConfigOpts object CommandLineArgs { - case class NextOpts(format: Option[String], preRelease: Boolean, prefix: Boolean) - case class TagOpts(preRelease: Boolean) - case class PatchOpts(preRelease: Boolean) - case class InfoOpts(preRelease: Boolean, includeBranchConfig: Boolean) + sealed trait AppOpts + case class NextOpts(format: Option[String], preRelease: Boolean, prefix: Boolean) extends AppOpts + case class TagOpts(preRelease: Boolean) extends AppOpts + case class PatchOpts(preRelease: Boolean) extends AppOpts + case class InfoOpts(preRelease: Boolean, includeEnv: Boolean) extends AppOpts + case object ConfigOpts extends AppOpts val configFile: Opts[Option[String]] = Opts.option[String]("config", short = "c", metavar = "file", help = "Config file to load").orNone val format: Opts[Option[String]] = Opts.option[String]("format", short = "f", metavar = "string", help = "Format string for the version number").orNone val prefix: Opts[Boolean] = Opts.flag("tag-prefix", short = "t", help = "Include the tag prefix in the output").orFalse val preRelease: Opts[Boolean] = Opts.flag("pre-release", short = "p", help = "Include the tag prefix in the output").orFalse - val includeBranchConfig: Opts[Boolean] = Opts.flag("include-branch-config", short = "i", help = "Format string for the version number").orFalse + val includeEnv: Opts[Boolean] = Opts.flag("include-env", short = "i", help = "Include environment variables").orFalse val nextOptions: Opts[NextOpts] = (format, preRelease, prefix).mapN(NextOpts.apply) val tagOptions: Opts[TagOpts] = preRelease.map(TagOpts.apply) val patchOptions: Opts[PatchOpts] = preRelease.map(PatchOpts.apply) - val infoOptions: Opts[InfoOpts] = (preRelease, includeBranchConfig).mapN(InfoOpts.apply) + val infoOptions: Opts[InfoOpts] = (preRelease, includeEnv).mapN(InfoOpts.apply) val nextCommand: Command[NextOpts] = Command("next", header = "Print the next version tag that would be used") { nextOptions @@ -36,15 +40,17 @@ object CommandLineArgs { infoOptions } - case class CommandLineOpts(configFile: Option[String], p: Product) + val configCommand: Command[AppOpts] = Command("config", header = "output final configuration to be used") { Opts(ConfigOpts) } - val commands: Opts[Product] = Opts.subcommands(nextCommand, tagCommand, patchCommand, infoCommand) + case class CommandLineOpts(configFile: Option[String], opts: AppOpts) + + val commands: Opts[AppOpts] = Opts.subcommands(nextCommand, tagCommand, patchCommand, infoCommand, configCommand) val commandLineOpts: Opts[CommandLineOpts] = (configFile, commands).mapN(CommandLineOpts.apply) val mkverCommand: Command[CommandLineOpts] = Command( - name = s"git-mkver - v${GitMkverVersion}", - header = "Uses git tags, branch names and commit messages to determine the next version of the software to release" + name = s"git mkver", + header = s"git-mkver - v${GitMkverVersion}\n\nUses git tags, branch names and commit messages to determine the next version of the software to release" ) { commandLineOpts } diff --git a/src/main/scala/net/cardnell/mkver/Main.scala b/src/main/scala/net/cardnell/mkver/Main.scala index 8940633..be2dcb5 100644 --- a/src/main/scala/net/cardnell/mkver/Main.scala +++ b/src/main/scala/net/cardnell/mkver/Main.scala @@ -1,7 +1,7 @@ package net.cardnell.mkver import net.cardnell.mkver.MkVer._ -import net.cardnell.mkver.CommandLineArgs.{CommandLineOpts, InfoOpts, NextOpts, PatchOpts, TagOpts} +import net.cardnell.mkver.CommandLineArgs.{CommandLineOpts, ConfigOpts, InfoOpts, NextOpts, PatchOpts, TagOpts} import zio._ import zio.blocking.Blocking import zio.console._ @@ -18,94 +18,99 @@ object Main extends App { .provideCustomLayer(Blocking.live >>> Git.live(None)) .fold(_ => ExitCode.failure, _ => ExitCode.success) - def appLogic(args: List[String]) = { + def appLogic(args: List[String]): ZIO[Console with Git with Blocking, Unit, Unit] = { mainImpl(args) - .flatMap(message => putStrLn(message)) .flatMapError(err => putStrLn(err.getMessage)) } - def mainImpl(args: List[String]) = { + def mainImpl(args: List[String]): ZIO[Console with Git with Blocking, Throwable, Unit] = { CommandLineArgs.mkverCommand.parse(args, sys.env) .fold( help => Task.fail(MkVerException(help.toString())), opts => run(opts)) } - def run(opts: CommandLineOpts) = { + def run(opts: CommandLineOpts): ZIO[Console with Git with Blocking, Throwable, Unit] = { for { _ <- Git.checkGitRepo() currentBranch <- Git.currentBranch() config <- AppConfig.getRunConfig(opts.configFile, currentBranch) - r <- opts.p match { + r <- opts.opts match { case nextOps@NextOpts(_, _, _) => runNext(nextOps, config, currentBranch) case tagOpts@TagOpts(_) => - runTag(tagOpts, config, currentBranch).map(_ => "") + runTag(tagOpts, config, currentBranch) case patchOpts@PatchOpts(_) => - runPatch(patchOpts, config, currentBranch).map(_ => "") + runPatch(patchOpts, config, currentBranch) case infoOpts@InfoOpts(_, _) => runInfo(infoOpts, config, currentBranch) + case ConfigOpts => + runConfig(config) } } yield r } - def runNext(nextOpts: NextOpts, config: RunConfig, currentBranch: String): RIO[Git with Blocking, String] = { - getNextVersion(config, currentBranch, nextOpts.preRelease).flatMap { nextVersion => - nextOpts.format.map { format => + def runNext(nextOpts: NextOpts, config: RunConfig, currentBranch: String): ZIO[Console with Git with Blocking, Throwable, Unit] = { + for { + nextVersion <- getNextVersion(config, currentBranch, nextOpts.preRelease) + next <- nextOpts.format.map { format => Task.effect(Formatter(nextVersion, config, nextOpts.preRelease).format(format)) }.getOrElse { formatVersion(config, nextVersion, nextOpts.prefix, nextOpts.preRelease) } - } + _ <- putStrLn(next) + } yield () } - def runTag(tagOpts: TagOpts, config: RunConfig, currentBranch: String) = { + def runTag(tagOpts: TagOpts, config: RunConfig, currentBranch: String): ZIO[Git with Blocking, Throwable, Unit] = { for { nextVersion <- getNextVersion(config, currentBranch, tagOpts.preRelease) tag <- formatVersion(config, nextVersion, formatAsTag = true, preRelease = tagOpts.preRelease) tagMessage = Formatter(nextVersion, config, tagOpts.preRelease).format(config.tagMessageFormat) - _ <- if (config.tag && nextVersion.commitCount > 0) { - Git.tag(tag, tagMessage) - } else { - RIO.unit - } + _ <- Git.tag(tag, tagMessage) when (config.tag && nextVersion.commitCount > 0) } yield () } - def runPatch(patchOpts: PatchOpts, config: RunConfig, currentBranch: String) = { + def runPatch(patchOpts: PatchOpts, config: RunConfig, currentBranch: String): ZIO[Console with Git with Blocking, Throwable, Unit] = { getNextVersion(config, currentBranch, patchOpts.preRelease).flatMap { nextVersion => ZIO.foreach(config.patches) { patch => - val regex = patch.find.r - val replacement = Formatter(nextVersion, config, patchOpts.preRelease).format(patch.replace) - ZIO.foreach(patch.filePatterns) { filePattern => - for { - cwd <- Path.currentWorkingDirectory - matches <- Files.glob(cwd, filePattern) - l <- ZIO.foreach(matches) { fileMatch => - for { - _ <- putStrLn(s"Patching file: '${fileMatch.path.toString}', new value: '$replacement'") - content <- Files.readAll(fileMatch) - newContent <- ZIO.effect(regex.replaceAllIn(content, replacement)) - p <- Files.write(fileMatch, newContent) - } yield p - } - } yield l + ZIO.foreach(patch.replacements) { findReplace => + val regex = findReplace.find + .replace("{VersionRegex}", Version.versionFullRegex).r + val replacement = Formatter(nextVersion, config, patchOpts.preRelease).format(findReplace.replace) + ZIO.foreach(patch.filePatterns) { filePattern => + for { + cwd <- Path.currentWorkingDirectory + matches <- Files.glob(cwd, filePattern) + l <- ZIO.foreach(matches) { fileMatch => + for { + _ <- putStrLn(s"Patching file: '${fileMatch.path.toString}', new value: '$replacement'") + content <- Files.readAll(fileMatch) + newContent <- ZIO.effect(regex.replaceAllIn(content, replacement)) + p <- Files.write(fileMatch, newContent) + } yield p + } + } yield l + } } } }.unit } - def runInfo(infoOpts: InfoOpts, config: RunConfig, currentBranch: String): RIO[Git with Blocking, String] = { - getNextVersion(config, currentBranch, infoOpts.preRelease).map { nextVersion => - val formatter = Formatter(nextVersion, config, infoOpts.preRelease) - val formats = formatter.formats.map { format => + def runInfo(infoOpts: InfoOpts, config: RunConfig, currentBranch: String): RIO[Console with Git with Blocking, Unit] = { + for { + nextVersion <- getNextVersion(config, currentBranch, infoOpts.preRelease) + formatter = Formatter(nextVersion, config, infoOpts.preRelease) + _ <- ZIO.foreach(formatter.formats) { format => val result = formatter.format(format.format) - s"${format.name}=$result" - }.mkString(System.lineSeparator()) - - if (infoOpts.includeBranchConfig) { - config.toString + System.lineSeparator() + System.lineSeparator() + formats - } else { - formats + putStrLn(s"${format.name}=$result") when (!format.name.startsWith("env") || infoOpts.includeEnv) } + } yield () + } + + def runConfig(config: RunConfig): RIO[Console, Unit] = { + import zio.config.typesafe._ + zio.config.write(AppConfig.runConfigDesc, config) match { + case Left(s) => RIO.fail(MkVerException(s)) + case Right(pt) => putStrLn(pt.toHoconString) } } } diff --git a/src/main/scala/net/cardnell/mkver/Version.scala b/src/main/scala/net/cardnell/mkver/Version.scala index 62561f7..3176a1d 100644 --- a/src/main/scala/net/cardnell/mkver/Version.scala +++ b/src/main/scala/net/cardnell/mkver/Version.scala @@ -7,12 +7,6 @@ sealed trait VersionMode object VersionMode { case object SemVer extends VersionMode case object YearMonth extends VersionMode - - def read(value: String): Either[String, VersionMode] = - value match { - case "SemVer" => Right(SemVer) - case _ => Left("VersionMode must be one of: ") - } } sealed trait IncrementAction @@ -23,16 +17,6 @@ object IncrementAction { case object IncrementMinor extends IncrementAction case object IncrementPatch extends IncrementAction case object NoIncrement extends IncrementAction - - def read(value: String): Either[String, IncrementAction] = - value match { - case "Fail" => Right(Fail) - case "IncrementMajor" => Right(IncrementMajor) - case "IncrementMinor" => Right(IncrementMinor) - case "IncrementPatch" => Right(IncrementPatch) - case "NoIncrement" => Right(NoIncrement) - case _ => Left("IncrementAction should be one of Fail|IncrementMajor|IncrementMinor|IncrementPatch|NoIncrement") - } } case class Version(major: Int, @@ -91,8 +75,13 @@ case class Version(major: Int, } object Version { + val versionOnlyRegex = "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" + val prereleaseRegex = "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + val metadataRegex = "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" + val versionFullRegex = s"$versionOnlyRegex$prereleaseRegex$metadataRegex" + def parseTag(input: String, prefix: String): Option[Version] = { - val version = ("^" + prefix + "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$").r + val version = ("^" + prefix + versionFullRegex + "$").r input match { case version(major, minor, patch, preRelease, buildMetaData) => diff --git a/src/main/scala/net/cardnell/mkver/package.scala b/src/main/scala/net/cardnell/mkver/package.scala index cd6a6d2..1ac86fe 100644 --- a/src/main/scala/net/cardnell/mkver/package.scala +++ b/src/main/scala/net/cardnell/mkver/package.scala @@ -5,5 +5,5 @@ import zio.Has package object mkver { type Git = Has[Git.Service] - val GitMkverVersion = "0.5.1" + val GitMkverVersion = "1.1.0" } diff --git a/src/test/scala/net/cardnell/mkver/EndToEndTests.scala b/src/test/scala/net/cardnell/mkver/EndToEndTests.scala index e8168e7..15b46ea 100644 --- a/src/test/scala/net/cardnell/mkver/EndToEndTests.scala +++ b/src/test/scala/net/cardnell/mkver/EndToEndTests.scala @@ -1,35 +1,40 @@ package net.cardnell.mkver +import net.cardnell.mkver.Main.mainImpl import zio.blocking.Blocking +import zio.console.Console import zio.test.Assertion._ import zio.test._ -import zio.{RIO, ZIO} -import Main.mainImpl +import zio.test.mock.MockConsole +import zio.{RIO, ULayer, ZIO} object EndToEndTests extends DefaultRunnableSpec { def spec = suite("trunk based semver development")( testM("no tags should return version 0.1.0") { + val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("0.1.0")) val result = test { tempDir => for { _ <- fix("code1.py", tempDir) - run <- run(tempDir, "next") + run <- run(tempDir, "next").provideCustomLayer(mockEnv) } yield run } - assertM(result)(equalTo("0.1.0")) + assertM(result)(isUnit) }, testM("master advances correctly and should return version 0.1.1") { + val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("0.1.1")) val result = test { tempDir => for { _ <- fix("code1.py", tempDir) _ <- run(tempDir, "tag") _ <- fix("code2.py", tempDir) //_ <- println(ProcessUtils.exec("git log --graph --full-history --color --oneline", Some(tempDir)).stdout) - run <- run(tempDir, "next") + run <- run(tempDir, "next").provideCustomLayer(mockEnv) } yield run } - assertM(result)(equalTo("0.1.1")) + assertM(result)(isUnit) }, testM("feature branch (+minor) and master (+major) both advance version and should return version 1.0.0") { + val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("1.0.0")) val result = test { tempDir => for { _ <- fix("code1.py", tempDir) @@ -40,12 +45,13 @@ object EndToEndTests extends DefaultRunnableSpec { _ <- major("code3.py", tempDir) _ <- merge("feature/f1", tempDir) //_ <- println(ProcessUtils.exec("git log --graph --full-history --color --oneline", Some(tempDir)).stdout) - run <- run(tempDir, "next") + run <- run(tempDir, "next").provideCustomLayer(mockEnv) } yield run } - assertM(result)(equalTo("1.0.0")) + assertM(result)(isUnit) }, testM("feature branch (+major) and master (+minor) both advance version and should return version 1.0.0") { + val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("1.0.0")) val result = test { tempDir => for { _ <- fix("code1.py", tempDir) @@ -56,12 +62,13 @@ object EndToEndTests extends DefaultRunnableSpec { _ <- feat("code3.py", tempDir) _ <- merge("feature/f1", tempDir) //_ <- println(ProcessUtils.exec("git log --graph --full-history --color --oneline", Some(tempDir)).stdout) - run <- run(tempDir, "next") + run <- run(tempDir, "next").provideCustomLayer(mockEnv) } yield run } - assertM(result)(equalTo("1.0.0")) + assertM(result)(isUnit) }, testM("feature branch 1 (+major) and feature branch 2 (+minor) both advance version and should return version 1.0.0") { + val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("1.0.0")) val result = test { tempDir => for { _ <- fix("code1.py", tempDir) @@ -78,10 +85,10 @@ object EndToEndTests extends DefaultRunnableSpec { _ <- merge ("feature/f1", tempDir) _ <- merge ("feature/f2", tempDir) //_ <- println (ProcessUtils.exec("git log --graph --full-history --color --oneline", Some(tempDir)).stdout) - run <- run(tempDir, "next") + run <- run(tempDir, "next").provideCustomLayer(mockEnv) } yield run } - assertM(result)(equalTo("1.0.0")) + assertM(result)(isUnit) } ) @@ -93,7 +100,7 @@ object EndToEndTests extends DefaultRunnableSpec { } } - def run(tempDir: File, command: String): ZIO[zio.ZEnv, Throwable, String] = { + def run(tempDir: File, command: String): ZIO[zio.ZEnv, Throwable, Unit] = { // TODO provide layer with git that has different working dir mainImpl(List(command)).provideCustomLayer(Blocking.live >>> Git.live(Some(tempDir))) } diff --git a/src/test/scala/net/cardnell/mkver/MainSpec.scala b/src/test/scala/net/cardnell/mkver/MainSpec.scala index cd6275a..9a7e16f 100644 --- a/src/test/scala/net/cardnell/mkver/MainSpec.scala +++ b/src/test/scala/net/cardnell/mkver/MainSpec.scala @@ -1,19 +1,15 @@ package net.cardnell.mkver -import zio.test.Assertion.equalTo +import zio.test.Assertion._ import zio.test.mock.Expectation._ import zio.test.mock._ import zio.test.{DefaultRunnableSpec, assertM, suite, testM} import zio.{Has, ULayer, URLayer, ZLayer} import Main.mainImpl +import zio.console.Console // TODO >> @Mockable[Git.Service] object GitMock extends Mock[Git] { -// sealed trait Tag[I, A] extends Method[Git, I, A] { -// def envBuilder: URLayer[Has[Proxy], Git] = -// GitMock.envBuilder -// } - object CurrentBranch extends Effect[Unit, Nothing, String] object FullLog extends Effect[Option[String], Nothing, String] object CommitInfoLog extends Effect[Unit, Nothing, String] @@ -36,14 +32,15 @@ object MainSpec extends DefaultRunnableSpec { def spec = suite("MainSpec")( suite("main") ( testM("next should return") { - val mockEnv: ULayer[Git] = ( + val mockEnv: ULayer[Git with Console] = ( GitMock.CheckGitRepo(unit) ++ GitMock.CurrentBranch(value("master")) ++ GitMock.CommitInfoLog(value("")) ++ - GitMock.FullLog(equalTo(None), value("")) + GitMock.FullLog(equalTo(None), value("")) ++ + MockConsole.PutStrLn(equalTo("0.1.0")) ) val result = mainImpl(List("next")).provideCustomLayer(mockEnv) - assertM(result)(equalTo("0.1.0")) + assertM(result)(isUnit) }, testM("tag should return") { val mockEnv: ULayer[Git] = ( @@ -53,9 +50,7 @@ object MainSpec extends DefaultRunnableSpec { GitMock.FullLog(equalTo(None), value("")) ) val result = mainImpl(List("tag")).provideCustomLayer(mockEnv) - assertM(result)( - equalTo("") - ) + assertM(result)(isUnit) } ) )