├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build-in-docker.sh ├── build-linux.sh ├── build-macos.sh ├── build-universal.sh ├── build-windows.ps1 ├── build.sbt ├── docs ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _includes │ └── navbar.html ├── _layouts │ └── default.html ├── branching_models.md ├── config_examples.md ├── config_reference.md ├── development.md ├── formats.md ├── index.md ├── installation.md ├── test.md └── usage.md ├── etc ├── scoop │ └── git-mkver.json └── shell │ └── install.sh ├── git-mkver.conf ├── project ├── Dependencies.scala ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ └── reference.conf └── scala │ └── net │ └── cardnell │ └── mkver │ ├── AppConfig.scala │ ├── CommandLineArgs.scala │ ├── Files.scala │ ├── Formatter.scala │ ├── Git.scala │ ├── Main.scala │ ├── MkVer.scala │ ├── Version.scala │ └── package.scala └── test ├── resources └── test-config1.conf └── scala └── net └── cardnell └── mkver ├── AppConfigSpec.scala ├── EndToEndTests.scala ├── FilesSpec.scala ├── FormatterSpec.scala ├── MainSpec.scala ├── MkVerSpec.scala └── VersionSpec.scala /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Git Mkver Build 2 | 3 | # Trigger on every master branch push and pull request 4 | on: 5 | push: 6 | branches: 7 | - master 8 | - patch-* 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build-on-windows: 15 | runs-on: windows-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 21 | # Install the .NET Core workload 22 | - name: Install .NET Core 23 | uses: actions/setup-dotnet@v3 24 | - uses: actions/setup-java@v3 25 | with: 26 | distribution: 'temurin' 27 | java-version: '20' 28 | cache: 'sbt' 29 | - uses: graalvm/setup-graalvm@v1 30 | with: 31 | distribution: 'graalvm' 32 | java-version: '20' 33 | components: 'native-image' 34 | # github-token: ${{ secrets.GITHUB_TOKEN }} 35 | native-image-job-reports: 'true' 36 | - name: Build 37 | run: .\build-windows.ps1 38 | - name: Upload binary 39 | uses: actions/upload-artifact@v3 40 | with: 41 | name: git-mkver-windows 42 | path: target\scala-2.12\git-mkver-windows-amd64-*.zip 43 | build-on-linux: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 50 | - uses: actions/setup-java@v3 51 | with: 52 | distribution: 'temurin' 53 | java-version: '20' 54 | cache: 'sbt' 55 | - uses: graalvm/setup-graalvm@v1 56 | with: 57 | distribution: 'graalvm' 58 | java-version: '20' 59 | components: 'native-image' 60 | # github-token: ${{ secrets.GITHUB_TOKEN }} 61 | native-image-job-reports: 'true' 62 | - name: Build 63 | run: ./build-linux.sh 64 | - name: Upload binary 65 | uses: actions/upload-artifact@v3 66 | with: 67 | name: git-mkver-linux 68 | path: target/git-mkver-linux-*.tar.gz 69 | build-universal: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Checkout 73 | uses: actions/checkout@v4 74 | with: 75 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 76 | - uses: actions/setup-java@v3 77 | with: 78 | distribution: 'temurin' 79 | java-version: '20' 80 | cache: 'sbt' 81 | - name: Build 82 | run: ./build-universal.sh 83 | - name: Upload binary 84 | uses: actions/upload-artifact@v3 85 | with: 86 | name: git-mkver-universal 87 | path: target/universal/git-mkver-*.zip 88 | build-on-mac-x64: 89 | runs-on: macos-latest 90 | steps: 91 | - name: Checkout 92 | uses: actions/checkout@v4 93 | with: 94 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 95 | - uses: actions/setup-java@v3 96 | with: 97 | distribution: 'temurin' 98 | java-version: '20' 99 | cache: 'sbt' 100 | - uses: graalvm/setup-graalvm@v1 101 | with: 102 | distribution: 'graalvm' 103 | java-version: '20' 104 | components: 'native-image' 105 | # github-token: ${{ secrets.GITHUB_TOKEN }} 106 | native-image-job-reports: 'true' 107 | - name: Build 108 | run: ./build-macos.sh 109 | - name: Upload binary 110 | uses: actions/upload-artifact@v3 111 | with: 112 | name: git-mkver-darwin 113 | path: target/git-mkver-darwin-*.tar.gz 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | _site/ 4 | .bsp/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN apt-get update 4 | RUN apt-get -y install curl zip unzip git gcc zlib1g-dev 5 | 6 | RUN curl -s "https://get.sdkman.io" | bash 7 | ENV SDKMAN_INIT="/root/.sdkman/bin/sdkman-init.sh" 8 | 9 | SHELL ["/bin/bash", "-c"] 10 | 11 | RUN source "$SDKMAN_INIT" && \ 12 | sdk install java 22.1.0.r17-grl && \ 13 | sdk install sbt && \ 14 | gu install native-image 15 | 16 | CMD source "$SDKMAN_INIT" && ./build.sh 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Iain Cardnell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-mkver 2 | 3 | Automatic Semantic Versioning for git based software development. 4 | 5 | For more information head to the [project site](https://idc101.github.io/git-mkver/). 6 | 7 | ## Features 8 | 9 | - Determine next version based on: 10 | - Last tagged commit 11 | - [Conventional Commits](https://www.conventionalcommits.org/) 12 | - Branch names 13 | - Manual tagging 14 | - Next version conforms to [Semantic Versioning](https://semver.org/) scheme 15 | - Patch the next version into source files using a configurable find and replace system 16 | - Tag the current commit with the next version number 17 | 18 | Works out of the box with trunk based development, GitFlow and GithubFlow. Alternatively all of this can be configured 19 | based on the branch name so release/main branches get different version numbers to develop or feature branches. 20 | 21 | ## Installation 22 | 23 | [Install](https://idc101.github.io/git-mkver/installation) with brew, scoop or simply download the binary for your os 24 | from the [releases](https://github.com/idc101/git-mkver/releases) page and copy somewhere on your path. 25 | 26 | ## Usage 27 | 28 | Start by using [Conventional Commits](https://www.conventionalcommits.org/) to indicate whether the commits contain 29 | major, minor or patch level changes. 30 | 31 | ```bash 32 | $ git commit -m "feat: added a new feature (this will increment the minor version)" 33 | ``` 34 | 35 | Then call `git mkver next` and it will tell you the next version of the software should be if you publish now. 36 | 37 | ```bash 38 | $ git mkver next 39 | v0.4.0 40 | ``` 41 | 42 | ### Tagging 43 | 44 | If you would like to publish a version, git-mkver can tag the current commit. 45 | 46 | ```bash 47 | $ git mkver tag 48 | ``` 49 | 50 | This will apply an annotated tag from the `next` command to the current commit. 51 | 52 | ### Patching versions in files 53 | 54 | If you would like to patch version numbers in files prior to building and tagging then 55 | you can use the `patch` command. The files to be patched and the replacements are 56 | defined in the `mkver.conf` config file. A large number of standard patches come 57 | pre-defined. 58 | 59 | ```bash 60 | $ git mkver patch 61 | ``` 62 | -------------------------------------------------------------------------------- /build-in-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -t git-mkver . 3 | docker run \ 4 | --rm \ 5 | -v $(pwd):/workspace \ 6 | -w /workspace \ 7 | -it git-mkver 8 | -------------------------------------------------------------------------------- /build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | sbt -error -batch "run -c git-mkver.conf patch" 5 | version=`sbt -error -batch "run -c git-mkver.conf next"` 6 | arch=`arch` 7 | 8 | sbt assembly 9 | 10 | pushd target 11 | native-image --static -H:IncludeResources='.*conf$' --no-fallback -jar scala-2.12/git-mkver-assembly-$version.jar 12 | mv git-mkver-assembly-$version git-mkver-linux-$arch-$version 13 | cp git-mkver-linux-$arch-$version git-mkver 14 | chmod +x git-mkver 15 | tar -cvzf git-mkver-linux-$arch-$version.tar.gz git-mkver 16 | rm git-mkver 17 | popd 18 | 19 | LINUX_SHA256=$(openssl dgst -sha256 target/git-mkver-linux-$arch-$version.tar.gz | cut -f2 -d' ') 20 | echo "LINUX_SHA256=$LINUX_SHA256" 21 | -------------------------------------------------------------------------------- /build-macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | sbt -error -batch "run -c git-mkver.conf patch" 5 | version=`sbt -error -batch "run -c git-mkver.conf next"` 6 | arch=`arch` 7 | 8 | sbt assembly 9 | 10 | pushd target 11 | native-image -H:IncludeResources='.*conf$' --no-fallback -jar scala-2.12/git-mkver-assembly-$version.jar 12 | mv git-mkver-assembly-$version git-mkver-darwin-$arch-$version 13 | cp git-mkver-darwin-$arch-$version git-mkver 14 | chmod +x git-mkver 15 | tar -cvzf git-mkver-darwin-$arch-$version.tar.gz git-mkver 16 | rm git-mkver 17 | popd 18 | 19 | DARWIN_SHA256=$(openssl dgst -sha256 target/git-mkver-darwin-$arch-$version.tar.gz | cut -f2 -d' ') 20 | echo "DARWIN_SHA256=$DARWIN_SHA256" 21 | -------------------------------------------------------------------------------- /build-universal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | sbt -error -batch "run -c git-mkver.conf patch" 5 | version=`sbt -error -batch "run -c git-mkver.conf next"` 6 | arch=`arch` 7 | 8 | sbt assembly 9 | 10 | # build universal 11 | sbt universal:packageBin 12 | UNIVERSAL_SHA256=$(openssl dgst -sha256 target/universal/git-mkver-$version.zip | cut -f2 -d' ') 13 | echo "UNIVERSAL_SHA256=$UNIVERSAL_SHA256" -------------------------------------------------------------------------------- /build-windows.ps1: -------------------------------------------------------------------------------- 1 | # & "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" 2 | 3 | # Get the version from the git command 4 | & sbt -error -batch "run -c git-mkver.conf patch" 5 | $VERSION = & sbt -error -batch "run -c git-mkver.conf next" 6 | 7 | & sbt assembly 8 | Set-Location -Path target\scala-2.12 9 | & native-image -jar "git-mkver-assembly-$VERSION.jar" --no-fallback 10 | Move-Item -Path "git-mkver-assembly-$VERSION.exe" -Destination git-mkver.exe 11 | Compress-Archive -Path 'git-mkver.exe' -DestinationPath 'git-mkver-windows-amd64-%VERSION%.zip' 12 | Get-FileHash git-mkver-windows-amd64-%VERSION%.zip | % Hash 13 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | 3 | scalaVersion := "2.12.11" 4 | version := "1.4.0+ci-build.2fd9c7f" 5 | organization := "net.cardnell" 6 | maintainer := "idc101@users.noreply.github.com" 7 | 8 | enablePlugins(JavaAppPackaging) 9 | 10 | lazy val root = (project in file(".")) 11 | .settings( 12 | name := "git-mkver", 13 | scalacOptions += "-Ypartial-unification", 14 | libraryDependencies += "org.typelevel" %% "cats-core" % "2.0.0", 15 | libraryDependencies += "com.monovore" %% "decline" % "1.0.0", 16 | libraryDependencies += "dev.zio" %% "zio" % "1.0.3", 17 | libraryDependencies += "dev.zio" %% "zio-process" % "0.1.0", 18 | libraryDependencies += "dev.zio" %% "zio-config" % "1.0.0-RC27", 19 | libraryDependencies += "dev.zio" %% "zio-config-typesafe" % "1.0.0-RC27", 20 | libraryDependencies += "dev.zio" %% "zio-test" % "1.0.3" % Test, 21 | libraryDependencies += "dev.zio" %% "zio-test-sbt" % "1.0.3" % Test, 22 | 23 | testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), 24 | 25 | assemblyMergeStrategy in assembly := { 26 | case PathList("META-INF", xs @ _*) => MergeStrategy.discard 27 | case x => MergeStrategy.first 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'github-pages', group: :jekyll_plugins 3 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (6.0.6.1) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 0.7, < 2) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | zeitwerk (~> 2.2, >= 2.2.2) 10 | addressable (2.8.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | coffee-script (2.4.1) 13 | coffee-script-source 14 | execjs 15 | coffee-script-source (1.11.1) 16 | colorator (1.1.0) 17 | commonmarker (0.17.13) 18 | ruby-enum (~> 0.5) 19 | concurrent-ruby (1.2.0) 20 | dnsruby (1.61.3) 21 | addressable (~> 2.5) 22 | em-websocket (0.5.1) 23 | eventmachine (>= 0.12.9) 24 | http_parser.rb (~> 0.6.0) 25 | ethon (0.12.0) 26 | ffi (>= 1.3.0) 27 | eventmachine (1.2.7) 28 | execjs (2.7.0) 29 | faraday (1.0.1) 30 | multipart-post (>= 1.2, < 3) 31 | ffi (1.12.2) 32 | forwardable-extended (2.6.0) 33 | gemoji (3.0.1) 34 | github-pages (204) 35 | github-pages-health-check (= 1.16.1) 36 | jekyll (= 3.8.5) 37 | jekyll-avatar (= 0.7.0) 38 | jekyll-coffeescript (= 1.1.1) 39 | jekyll-commonmark-ghpages (= 0.1.6) 40 | jekyll-default-layout (= 0.1.4) 41 | jekyll-feed (= 0.13.0) 42 | jekyll-gist (= 1.5.0) 43 | jekyll-github-metadata (= 2.13.0) 44 | jekyll-mentions (= 1.5.1) 45 | jekyll-optional-front-matter (= 0.3.2) 46 | jekyll-paginate (= 1.1.0) 47 | jekyll-readme-index (= 0.3.0) 48 | jekyll-redirect-from (= 0.15.0) 49 | jekyll-relative-links (= 0.6.1) 50 | jekyll-remote-theme (= 0.4.1) 51 | jekyll-sass-converter (= 1.5.2) 52 | jekyll-seo-tag (= 2.6.1) 53 | jekyll-sitemap (= 1.4.0) 54 | jekyll-swiss (= 1.0.0) 55 | jekyll-theme-architect (= 0.1.1) 56 | jekyll-theme-cayman (= 0.1.1) 57 | jekyll-theme-dinky (= 0.1.1) 58 | jekyll-theme-hacker (= 0.1.1) 59 | jekyll-theme-leap-day (= 0.1.1) 60 | jekyll-theme-merlot (= 0.1.1) 61 | jekyll-theme-midnight (= 0.1.1) 62 | jekyll-theme-minimal (= 0.1.1) 63 | jekyll-theme-modernist (= 0.1.1) 64 | jekyll-theme-primer (= 0.5.4) 65 | jekyll-theme-slate (= 0.1.1) 66 | jekyll-theme-tactile (= 0.1.1) 67 | jekyll-theme-time-machine (= 0.1.1) 68 | jekyll-titles-from-headings (= 0.5.3) 69 | jemoji (= 0.11.1) 70 | kramdown (= 1.17.0) 71 | liquid (= 4.0.3) 72 | mercenary (~> 0.3) 73 | minima (= 2.5.1) 74 | nokogiri (>= 1.10.4, < 2.0) 75 | rouge (= 3.13.0) 76 | terminal-table (~> 1.4) 77 | github-pages-health-check (1.16.1) 78 | addressable (~> 2.3) 79 | dnsruby (~> 1.60) 80 | octokit (~> 4.0) 81 | public_suffix (~> 3.0) 82 | typhoeus (~> 1.3) 83 | html-pipeline (2.12.3) 84 | activesupport (>= 2) 85 | nokogiri (>= 1.4) 86 | http_parser.rb (0.6.0) 87 | i18n (0.9.5) 88 | concurrent-ruby (~> 1.0) 89 | jekyll (3.8.5) 90 | addressable (~> 2.4) 91 | colorator (~> 1.0) 92 | em-websocket (~> 0.5) 93 | i18n (~> 0.7) 94 | jekyll-sass-converter (~> 1.0) 95 | jekyll-watch (~> 2.0) 96 | kramdown (~> 1.14) 97 | liquid (~> 4.0) 98 | mercenary (~> 0.3.3) 99 | pathutil (~> 0.9) 100 | rouge (>= 1.7, < 4) 101 | safe_yaml (~> 1.0) 102 | jekyll-avatar (0.7.0) 103 | jekyll (>= 3.0, < 5.0) 104 | jekyll-coffeescript (1.1.1) 105 | coffee-script (~> 2.2) 106 | coffee-script-source (~> 1.11.1) 107 | jekyll-commonmark (1.3.1) 108 | commonmarker (~> 0.14) 109 | jekyll (>= 3.7, < 5.0) 110 | jekyll-commonmark-ghpages (0.1.6) 111 | commonmarker (~> 0.17.6) 112 | jekyll-commonmark (~> 1.2) 113 | rouge (>= 2.0, < 4.0) 114 | jekyll-default-layout (0.1.4) 115 | jekyll (~> 3.0) 116 | jekyll-feed (0.13.0) 117 | jekyll (>= 3.7, < 5.0) 118 | jekyll-gist (1.5.0) 119 | octokit (~> 4.2) 120 | jekyll-github-metadata (2.13.0) 121 | jekyll (>= 3.4, < 5.0) 122 | octokit (~> 4.0, != 4.4.0) 123 | jekyll-mentions (1.5.1) 124 | html-pipeline (~> 2.3) 125 | jekyll (>= 3.7, < 5.0) 126 | jekyll-optional-front-matter (0.3.2) 127 | jekyll (>= 3.0, < 5.0) 128 | jekyll-paginate (1.1.0) 129 | jekyll-readme-index (0.3.0) 130 | jekyll (>= 3.0, < 5.0) 131 | jekyll-redirect-from (0.15.0) 132 | jekyll (>= 3.3, < 5.0) 133 | jekyll-relative-links (0.6.1) 134 | jekyll (>= 3.3, < 5.0) 135 | jekyll-remote-theme (0.4.1) 136 | addressable (~> 2.0) 137 | jekyll (>= 3.5, < 5.0) 138 | rubyzip (>= 1.3.0) 139 | jekyll-sass-converter (1.5.2) 140 | sass (~> 3.4) 141 | jekyll-seo-tag (2.6.1) 142 | jekyll (>= 3.3, < 5.0) 143 | jekyll-sitemap (1.4.0) 144 | jekyll (>= 3.7, < 5.0) 145 | jekyll-swiss (1.0.0) 146 | jekyll-theme-architect (0.1.1) 147 | jekyll (~> 3.5) 148 | jekyll-seo-tag (~> 2.0) 149 | jekyll-theme-cayman (0.1.1) 150 | jekyll (~> 3.5) 151 | jekyll-seo-tag (~> 2.0) 152 | jekyll-theme-dinky (0.1.1) 153 | jekyll (~> 3.5) 154 | jekyll-seo-tag (~> 2.0) 155 | jekyll-theme-hacker (0.1.1) 156 | jekyll (~> 3.5) 157 | jekyll-seo-tag (~> 2.0) 158 | jekyll-theme-leap-day (0.1.1) 159 | jekyll (~> 3.5) 160 | jekyll-seo-tag (~> 2.0) 161 | jekyll-theme-merlot (0.1.1) 162 | jekyll (~> 3.5) 163 | jekyll-seo-tag (~> 2.0) 164 | jekyll-theme-midnight (0.1.1) 165 | jekyll (~> 3.5) 166 | jekyll-seo-tag (~> 2.0) 167 | jekyll-theme-minimal (0.1.1) 168 | jekyll (~> 3.5) 169 | jekyll-seo-tag (~> 2.0) 170 | jekyll-theme-modernist (0.1.1) 171 | jekyll (~> 3.5) 172 | jekyll-seo-tag (~> 2.0) 173 | jekyll-theme-primer (0.5.4) 174 | jekyll (> 3.5, < 5.0) 175 | jekyll-github-metadata (~> 2.9) 176 | jekyll-seo-tag (~> 2.0) 177 | jekyll-theme-slate (0.1.1) 178 | jekyll (~> 3.5) 179 | jekyll-seo-tag (~> 2.0) 180 | jekyll-theme-tactile (0.1.1) 181 | jekyll (~> 3.5) 182 | jekyll-seo-tag (~> 2.0) 183 | jekyll-theme-time-machine (0.1.1) 184 | jekyll (~> 3.5) 185 | jekyll-seo-tag (~> 2.0) 186 | jekyll-titles-from-headings (0.5.3) 187 | jekyll (>= 3.3, < 5.0) 188 | jekyll-watch (2.2.1) 189 | listen (~> 3.0) 190 | jemoji (0.11.1) 191 | gemoji (~> 3.0) 192 | html-pipeline (~> 2.2) 193 | jekyll (>= 3.0, < 5.0) 194 | kramdown (1.17.0) 195 | liquid (4.0.3) 196 | listen (3.2.1) 197 | rb-fsevent (~> 0.10, >= 0.10.3) 198 | rb-inotify (~> 0.9, >= 0.9.10) 199 | mercenary (0.3.6) 200 | mini_portile2 (2.8.1) 201 | minima (2.5.1) 202 | jekyll (>= 3.5, < 5.0) 203 | jekyll-feed (~> 0.9) 204 | jekyll-seo-tag (~> 2.1) 205 | minitest (5.17.0) 206 | multipart-post (2.1.1) 207 | nokogiri (1.14.2) 208 | mini_portile2 (~> 2.8.0) 209 | racc (~> 1.4) 210 | octokit (4.18.0) 211 | faraday (>= 0.9) 212 | sawyer (~> 0.8.0, >= 0.5.3) 213 | pathutil (0.16.2) 214 | forwardable-extended (~> 2.6) 215 | public_suffix (3.1.1) 216 | racc (1.6.2) 217 | rb-fsevent (0.10.3) 218 | rb-inotify (0.10.1) 219 | ffi (~> 1.0) 220 | rouge (3.13.0) 221 | ruby-enum (0.8.0) 222 | i18n 223 | rubyzip (2.3.0) 224 | safe_yaml (1.0.5) 225 | sass (3.7.4) 226 | sass-listen (~> 4.0.0) 227 | sass-listen (4.0.0) 228 | rb-fsevent (~> 0.9, >= 0.9.4) 229 | rb-inotify (~> 0.9, >= 0.9.7) 230 | sawyer (0.8.2) 231 | addressable (>= 2.3.5) 232 | faraday (> 0.8, < 2.0) 233 | terminal-table (1.8.0) 234 | unicode-display_width (~> 1.1, >= 1.1.1) 235 | thread_safe (0.3.6) 236 | typhoeus (1.3.1) 237 | ethon (>= 0.9.0) 238 | tzinfo (1.2.11) 239 | thread_safe (~> 0.1) 240 | unicode-display_width (1.7.0) 241 | zeitwerk (2.6.6) 242 | 243 | PLATFORMS 244 | ruby 245 | 246 | DEPENDENCIES 247 | github-pages 248 | 249 | BUNDLED WITH 250 | 2.1.4 251 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect 2 | show_downloads: true 3 | github: 4 | releases_url: https://github.com/idc101/git-mkver/releases -------------------------------------------------------------------------------- /docs/_includes/navbar.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | {% seo %} 15 | 16 | 17 | 18 |
19 |
20 | 21 |

{{ site.title | default: site.github.repository_name }}

22 |
23 |

{{ site.description | default: site.github.project_tagline }}

24 | {% if site.github.is_project_page %} 25 | View project on GitHub 26 | {% endif %} 27 | {% if site.github.is_user_page %} 28 | Follow me on GitHub 29 | {% endif %} 30 |
31 |
32 | 33 |
34 |
35 |
36 | {{ content }} 37 |
38 | 39 | 56 |
57 |
58 | 59 | {% if site.google_analytics %} 60 | 68 | {% endif %} 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/branching_models.md: -------------------------------------------------------------------------------- 1 | # Branching Models 2 | 3 | Below are some popular git branching development models and how to configure them with git-mkver: 4 | - main (aka trunk) based development 5 | - Git flow 6 | - GitHub flow 7 | 8 | ### Controlling the next version number 9 | 10 | Regardless of the branching strategy, git-mkver uses the commit messages to determine the next version number. 11 | 12 | See [Usage](usage) for more details. 13 | 14 | ## main (aka trunk) based development 15 | 16 | This mode of operation works out of the box with the default configuration. 17 | 18 | Overview: 19 | 20 | - Developers commit to main or work on feature branches 21 | - All releases are done from the main branch 22 | - Only the main branch is tagged 23 | - Release Candidates are not used 24 | - Any version number not from main includes build metadata to indicate it is not an official release 25 | 26 | ### Build Server Setup 27 | 28 | The build script run by the build server would look something like: 29 | 30 | ```bash 31 | nextVer=$(git mkver next) 32 | # patch the version number into files as needed 33 | git mkver patch 34 | # build software ... 35 | # If successful: 36 | git mkver tag 37 | # Publish artifacts and push tag 38 | ``` 39 | 40 | To control the frequency of releases, include these steps only on manually triggered builds. 41 | 42 | ## Git flow 43 | 44 | [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) is a long standing and popular branching model. 45 | 46 | ## GitHub flow 47 | 48 | [GitHub Flow](https://guides.github.com/introduction/flow/) is a newer, simplified versioning model developed by GitHub. 49 | -------------------------------------------------------------------------------- /docs/config_examples.md: -------------------------------------------------------------------------------- 1 | # Config Examples 2 | 3 | Below are some examples of common configuration. 4 | 5 | ## Patching README.md 6 | 7 | I would like to update my README.md and others docs with the latest version number. 8 | 9 | ```hocon 10 | defaults { 11 | patches: [ 12 | Docs 13 | ] 14 | } 15 | patches: [ 16 | { 17 | name: Docs 18 | filePatterns: [ 19 | "README.md" 20 | "docs/installation.md" 21 | ] 22 | replacements: [ 23 | { 24 | find: "\\d+\\.\\d+\\.\\d+" 25 | replace: "{Next}" 26 | } 27 | ] 28 | } 29 | ] 30 | ``` 31 | 32 | ## I would like a different version format for docker tags 33 | 34 | Docker does not support the `+` symbol from semantics versions. Create a 35 | format for Docker tags like so: 36 | 37 | ```hocon 38 | branches: [ 39 | { 40 | pattern: "main" 41 | includeBuildMetaData: false 42 | tag: true 43 | formats: [ 44 | { name: Docker, format: "{Version}" } 45 | ] 46 | } 47 | { 48 | pattern: ".*" 49 | formats: [ 50 | { name: Docker, format: "{Version}-{BuildMetaData}" } 51 | ] 52 | } 53 | ] 54 | ``` 55 | 56 | Generate it with `git mkver next --format '{Docker}'` 57 | 58 | ## I would like to override the built-in commitMessageActions 59 | 60 | git-mkver includes a default list of commit message actions which map to 61 | [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). These can be overridden by specifying the 62 | pattern to override in the `commitMessageActions`. The pattern must match exactly one of the patterns below and then 63 | the `action` can be changed as required. 64 | 65 | The defaults are as follows: 66 | 67 | ```hcon 68 | commitMessageActions: [ 69 | # Breaking changes (major) 70 | { 71 | pattern: "BREAKING CHANGE" 72 | action: IncrementMajor 73 | } 74 | { 75 | pattern: "major(\\(.+\\))?!:" 76 | action: IncrementMajor 77 | } 78 | { 79 | pattern: "minor(\\(.+\\))?!:" 80 | action: IncrementMajor 81 | } 82 | { 83 | pattern: "patch(\\(.+\\))?!:" 84 | action: IncrementMajor 85 | } 86 | { 87 | pattern: "feature(\\(.+\\))?!:" 88 | action: IncrementMajor 89 | } 90 | { 91 | pattern: "feat(\\(.+\\))?!:" 92 | action: IncrementMajor 93 | } 94 | { 95 | pattern: "fix(\\(.+\\))?!:" 96 | action: IncrementMajor 97 | } 98 | # The rest of the conventional commits 99 | { 100 | pattern: "(build|ci|chore|docs|perf|refactor|revert|style|test)(\\(.+\\))?!:" 101 | action: IncrementMajor 102 | } 103 | { 104 | pattern: "major(\\(.+\\))?:" 105 | action: IncrementMajor 106 | } 107 | { 108 | pattern: "minor(\\(.+\\))?:" 109 | action: IncrementMinor 110 | } 111 | { 112 | pattern: "patch(\\(.+\\))?:" 113 | action: IncrementPatch 114 | } 115 | { 116 | pattern: "feature(\\(.+\\))?:" 117 | action: IncrementMinor 118 | } 119 | { 120 | pattern: "feat(\\(.+\\))?:" 121 | action: IncrementMinor 122 | } 123 | { 124 | pattern: "fix(\\(.+\\))?:" 125 | action: IncrementPatch 126 | } 127 | # The rest of the conventional commits 128 | { 129 | pattern: "(build|ci|chore|docs|perf|refactor|revert|style|test)(\\(.+\\))?:" 130 | action: NoIncrement 131 | } 132 | ``` 133 | -------------------------------------------------------------------------------- /docs/config_reference.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | git-mkver comes with a default configuration. It can be overridden by creating a custom config file. 4 | 5 | git-mkver will search for config in this order: 6 | 7 | - file specified by the `-c` or `--configFile` command line argument 8 | - file specified by the `GITMKVER_CONFIG` environment variable 9 | - `mkver.conf` in the current working directory 10 | 11 | The application uses the HOCON format. More details on the specification can be found 12 | [here](https://github.com/lightbend/config/blob/main/HOCON.md). 13 | 14 | Environment variables can be resolved in the config using the syntax `${?ENV_VAR_NAME}`. 15 | 16 | ## mkver.conf 17 | 18 | ```hocon 19 | # prefix for tags in git 20 | tagPrefix: v 21 | # defaults are used if they are not overriden by a branch config 22 | defaults { 23 | # whether to really tag the branch when `git mkver tag` is called 24 | tag: false 25 | # message for annotated version tags in git 26 | tagMessageFormat: "release {Tag}" 27 | # format tring for the pre-release. The format must end with {PreReleaseNumber} if it is used. 28 | # Examples: 29 | # * alpha 30 | # * SNAPSHOT 31 | # * RC{PreReleaseNumber} 32 | # * pre-{CommitsSinceTag} 33 | preReleaseFormat: "RC{PreReleaseNumber}" 34 | # format string to be used for the build metadata 35 | buildMetaDataFormat: "{Branch}.{ShortHash}" 36 | # whether to include the build metadata in the Semantic Version when next or tag are called 37 | includeBuildMetaData: true 38 | # action to take, if after analyzing all commit messages since the last tag 39 | # no increment instructions can be found. Options are: 40 | # * Fail - application will exit 41 | # * IncrementMajor - bump the major version 42 | # * IncrementMinor - bump the minor version 43 | # * IncrementPatch - bump the patch version 44 | # * NoIncrement - no version change will occur 45 | whenNoValidCommitMessages: IncrementMinor 46 | # list of patches to be applied when `git mkver patch` is called 47 | patches: [ 48 | HelmChart 49 | Csproj 50 | ] 51 | # list of formats 52 | formats: [ 53 | { 54 | name: Docker 55 | format: "{Version}" 56 | } 57 | { 58 | name: DockerBranch 59 | format: "{Version}.{Branch}.{ShortHash}" 60 | } 61 | ] 62 | } 63 | # branch specific overrides of the default config 64 | # name is a regular expression 65 | # branches are tried for matches in order 66 | branches: [ 67 | { 68 | pattern: "main" 69 | tag: true 70 | includeBuildMetaData: false 71 | } 72 | { 73 | pattern: ".*" 74 | tag: false 75 | formats: [ 76 | { 77 | name: Docker 78 | format: "{DockerBranch}" 79 | } 80 | ] 81 | } 82 | ] 83 | # patches control how files are updated 84 | patches: [ 85 | { 86 | # name of the patch, referenced from the branch configs 87 | name: HelmChart 88 | # files to match, can include glob wildcards 89 | filePatterns: [ 90 | "**Chart.yaml" # Chart.yaml in current working directory or any subdirectory of the current working directory 91 | "**/Chart.yaml" # Chart.yaml in any subdirectory of the current working directory 92 | "Chart.yaml" # Chart.yaml the current working directory only 93 | ] 94 | # list of replacements to apply to files 95 | replacements: [ 96 | { 97 | # search string, using java regular expression syntax (https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html) 98 | # find strings can include the special marker `{VersionRegex}` which will be replaced with the regular expression 99 | # for a Semantic Version. 100 | find: "appVersion: {VersionRegex}" 101 | # replacement string using substitutions from formats 102 | replace: "appVersion: \"{Version}\"" 103 | } 104 | ] 105 | } 106 | { 107 | name: Csproj 108 | filePatterns: ["**/*.csproj"] 109 | replacements: [ 110 | { 111 | find: ".*" 112 | replace: "{Version}" 113 | } 114 | ] 115 | } 116 | ] 117 | # commitMessageActions configure how different commit messages will increment 118 | # the version number 119 | commitMessageActions: [ 120 | { 121 | # pattern is a regular expression to occur in a single line 122 | pattern: "BREAKING CHANGE" 123 | # action is one of: 124 | # * Fail - application will exit 125 | # * IncrementMajor - bump the major version 126 | # * IncrementMinor - bump the minor version 127 | # * IncrementPatch - bump the patch version 128 | # * NoIncrement - no version change will occur 129 | action: IncrementMajor 130 | } 131 | { 132 | pattern: "major(\\(.+\\))?:" 133 | action: IncrementMajor 134 | } 135 | ] 136 | ``` -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Graal Native Image 4 | 5 | ## Windows 6 | 7 | Launch _Windows SDK 7.1 Command Prompt_ 8 | 9 | set JAVA_HOME=C:\Users\iain\Tools\graalvm-ce-java8-20.0.0\jre 10 | set PATH=%JAVA_HOME%\bin;%PATH% 11 | 12 | ``` 13 | sbt assembly 14 | native-image -jar target\scala-2.12\git-mkver-assembly-0.4.0.jar --no-fallback 15 | ``` 16 | 17 | ## MacOs 18 | 19 | ## Linux 20 | -------------------------------------------------------------------------------- /docs/formats.md: -------------------------------------------------------------------------------- 1 | # Format System 2 | 3 | git-mkver includes a powerful string formatting system for creating version strings in different styles on different 4 | branches. This is required as different software often have different restrictions on what a valid version number might be. 5 | 6 | For example git is happy with the SemVer standard for tagging but docker does not support the `+` symbol in docker tags. 7 | 8 | All replacements in format strings start with `{` and end with `}`. They are recursively replaced so that one may refer to another. 9 | 10 | ## SemVer Formats 11 | 12 | The following built in formats conform to the SemVer spec. They cannot be overriden. 13 | 14 | | Format Token | Substitution | 15 | | ------------- | ------------- | 16 | | `Version` | `{Major}.{Minor}.{Patch}` | 17 | | `VersionPreRelease` | `{Version}-{PreRelease}` | 18 | | `VersionBuildMetaData` | `{Version}+{BuildMetaData}` | 19 | | `VersionPreReleaseBuildMetaData` | `{Version}-{PreRelease}+{BuildMetaData}` | 20 | 21 | ## Built-in Formats 22 | 23 | | Format Token | Substitution | 24 | | ------------- | ------------- | 25 | | `Next` | Full Semantic Version | 26 | | `Tag` | Full Semantic Version as a tag (includes the prefix) | 27 | | `TagMessage` | Tag Message | 28 | | `Major` | Version major number | 29 | | `Minor` | Version minor number | 30 | | `Patch` | Version patch number | 31 | | `PreRelease` | Pre-release | 32 | | `BuildMetaData` | BuildMetaData | 33 | | `Branch` | Branch name | 34 | | `ShortHash` | Short Hash | 35 | | `FullHash` | Full Hash | 36 | | `CommitsSinceTag` | Number of commits since last tag | 37 | | `Tagged?` | `true` if this commit is tagged (`CommitsSinceTag` == 0), `false` otherwise | 38 | | `dd` | Day | 39 | | `mm` | Month | 40 | | `yyyy` | Year | 41 | | `Tag?` | `true` if this branch is allowed to be tagged; `false` otherwise | 42 | | `Prefix` | Tag prefix | 43 | | `env.XXXX` | Environment Variables | 44 | 45 | ### Environment Variables 46 | 47 | All environment variables are available under a set of formats prefixed with `env.`. 48 | For example `{env.BUILD_NUMBER}` would get the `BUILD_NUMBER` environment variable. 49 | This is most useful for getting information from build systems. 50 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # git mkver: Automatic Semantic Versioning 2 | 3 | ## Features 4 | 5 | - Determine next version based on: 6 | - Last tagged commit 7 | - [Conventional Commits](https://www.conventionalcommits.org/) 8 | - Branch names 9 | - Manual tagging 10 | - Next version conforms to [Semantic Versioning](https://semver.org/) scheme 11 | - Patch the next version into the build: 12 | - Java 13 | - C# 14 | - Many others, fully configurable 15 | - Tag the current commit with the next version 16 | - Works with all branching strategies: 17 | - [main/trunk based development](https://trunkbaseddevelopment.com/) 18 | - [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) 19 | - [GitHub Flow](https://guides.github.com/introduction/flow/) 20 | 21 | All of this can be configured based on the branch name so release/main 22 | branches get different version numbers to develop or feature branches. 23 | 24 | ## Getting started 25 | 26 | [Install](installation) the binary and then read through the [usage](usage). 27 | 28 | ## Related Projects 29 | 30 | * [Github Setup Action for git-mkver](https://github.com/cperezabo/setup-git-mkver) 31 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Linux 4 | 5 | ```bash 6 | curl -L https://github.com/idc101/git-mkver/releases/download/v1.4.0/git-mkver-linux-x86_64-1.4.0.tar.gz | tar xvz 7 | sudo mv git-mkver /usr/local/bin 8 | ``` 9 | 10 | ## Mac OS 11 | 12 | Install with [Homebrew](https://brew.sh): 13 | 14 | ```bash 15 | brew tap idc101/gitmkver 16 | brew install idc101/gitmkver/git-mkver 17 | ``` 18 | 19 | ## Windows 20 | 21 | Install with [scoop](https://scoop.sh): 22 | 23 | ```cmd 24 | scoop install https://raw.githubusercontent.com/idc101/git-mkver/main/etc/scoop/git-mkver.json 25 | ``` 26 | 27 | ## Manual 28 | 29 | 1. Download the binary for your os from the [releases](https://github.com/idc101/git-mkver/releases) page. 30 | 2. Move it to a directory on your path 31 | 32 | That's it :-) 33 | 34 | ## CI/Build Servers 35 | 36 | * [Github Setup Action for git-mkver](https://github.com/cperezabo/setup-git-mkver) 37 | -------------------------------------------------------------------------------- /docs/test.md: -------------------------------------------------------------------------------- 1 | ## A test 2 | 3 | 4 | 5 |
6 | 7 | 8 | 59 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Basic usage is to just call `git mkver next` and it will tell you the next 4 | version of the software if you publish now. 5 | 6 | ```bash 7 | $ git mkver next 8 | 0.4.0 9 | ``` 10 | 11 | Typical usages for `next`: 12 | - Developers can run this locally to check the what the next version is 13 | according to the commit message log 14 | - This command can be run at the beginning of an automated build to get 15 | a version number for use in built artifacts 16 | 17 | ## Tagging 18 | 19 | If you would like to publish a version mkver can tag the current commit. 20 | 21 | ```bash 22 | $ git mkver tag 23 | ``` 24 | 25 | This will apply an annotated tag from the `next` command to the current commit. 26 | 27 | This would typically be called at the end of an automated build once all tests 28 | have passed and artifacts have been successfully uploaded. This marks a successful 29 | release in the git repository, 30 | 31 | ## Pre-releases 32 | 33 | Pre-release versions are often called alpha, beta, RC (Release Candidate) or SNAPSHOT. For 34 | example: 35 | 36 | - 1.0.0-RC2 37 | - 2.5.0-alpha 38 | 39 | They denote and upcoming version that will soon be released. Pre-release versions are 40 | usually released to users for testing and therefore it is useful to tag them. 41 | 42 | It is a human decision as to when to release a pre-release version and at what point sufficient 43 | testing has been done to release a final version of that pre-release. This could be done 44 | locally by a developer and the tag pushed or there could be two build pipelines, one for 45 | pre-release versions and another for final versions. 46 | 47 | To create a pre-release the `next` and `tag` commands take a `--pre-release` (`-p`) flag which 48 | will create a version number with the pre-release. 49 | 50 | ```bash 51 | # Assuming the version is current 1.5.0 52 | # Commit the next major release 53 | $ git commit -m "major: big new changes" 54 | $ git mkver tag --pre-release 55 | 2.0.0-RC1 56 | # Found a bug... 57 | $ git commit -m "fix: bug" 58 | $ git mkver tag --pre-release 59 | 2.0.0-RC2 60 | # We're all happy make this the final version 61 | $ git mkver tag 62 | 2.0.0 63 | ``` 64 | 65 | ## Controlling the next version number 66 | 67 | ### Commit Messages 68 | 69 | The next version number will be determined based on the commit messages since 70 | the last version was tagged. The commit messages that trigger different version 71 | increments are [configurable](config_reference) but by default they are based on 72 | the [Conventional Commit](https://www.conventionalcommits.org/) specification 73 | as follows: 74 | 75 | - Commits containing the following will increment the _major_ version: 76 | - `major:` or `major(...):` 77 | - `BREAKING CHANGE` 78 | - Commits containing the following will increment the _minor_ version: 79 | - `minor:` or `minor(...):` 80 | - `feat:` or `feat(...):` 81 | - Commits containing the following will increment the _patch_ version: 82 | - `patch:` or `patch(...):` 83 | - `fix:` or `fix(...):` 84 | 85 | Other conventional commits such as `build`, `chore`, `docs` will not increment 86 | the version number. 87 | 88 | All commit messages since the last tagged message are analyzed and the greatest 89 | version increment is used. For example if one commit is a minor change and one is 90 | a major change then the major version will be incremented. 91 | 92 | ### Overrides 93 | 94 | You can explicitly set the next version by including `next-version: ` in 95 | a commit message. For example if previous commit message made an increment to the 96 | major version such that it would be `2.0.0`, number you could undo it like this: 97 | 98 | ``` 99 | next-version: 1.5.1 100 | ``` 101 | 102 | ### Branch Names 103 | 104 | Release and hotfix branches are often used for specific releases and as such will override 105 | the commit messages to set a specific version number. The following branch names will fix 106 | the version number for that branch to `1.2.3`: 107 | 108 | * `rel-1.2.3` 109 | * `rel/1.2.3` 110 | * `release-1.2.3` 111 | * `release/1.2.3` 112 | * `hotfix-1.2.3` 113 | * `hotfix/1.2.3` 114 | 115 | ## Common arguments 116 | 117 | All commands take a `-c ` or `--config ` option to set the config file to read. More details on the 118 | [Config Reference](config_reference) page. 119 | 120 | ## Patching versions in files 121 | 122 | If you would like to patch version numbers in files prior to building and tagging then 123 | you can use the `patch` command. The files to be patched and the replacements are 124 | defined in the `mkver.conf` [config](config) file. 125 | 126 | For example, suppose you have the version number in a code file: 127 | ```scala 128 | object VersionInfo { 129 | val version = "1.0.0" 130 | } 131 | ``` 132 | 133 | and you define a patch as follows in your config file: 134 | ```hocon 135 | { 136 | name: Readme 137 | filePatterns: ["version.scala"] 138 | replacements: [ 139 | { 140 | find: "val version = \"{VersionRegex}\"" 141 | replace: "val version = \"{Next}\"" 142 | } 143 | ] 144 | } 145 | ``` 146 | 147 | you could update the code automatically as follows: 148 | ```bash 149 | $ git mkver patch 150 | ``` 151 | 152 | ## Info 153 | 154 | If you want to see all format variables you can use the `info` command: 155 | 156 | ```bash 157 | $ git mkver info 158 | ``` 159 | -------------------------------------------------------------------------------- /etc/scoop/git-mkver.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.4.0", 3 | "description": "Automatic Semantic Versioning for git based software development", 4 | "url": "https://github.com/idc101/git-mkver/releases/download/v1.4.0/git-mkver-windows-amd64-1.4.0.zip", 5 | "hash": "63AD6FD8EBB1E27F2CB81B7C162CB2D0A66809C4C693402D2EC9F1496C9A2636", 6 | "extract_to": "", 7 | "bin": "git-mkver.exe" 8 | } 9 | -------------------------------------------------------------------------------- /etc/shell/install.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | MKVER_VERSION=1.4.0 3 | 4 | # Mac 5 | if [[ "$(uname)" == "Darwin" ]] 6 | then 7 | curl -L https://github.com/idc101/git-mkver/releases/download/v${MKVER_VERSION}/git-mkver-darwin-i386-${MKVER_VERSION}.tar.gz -o git-mkver.tar.gz 8 | tar xvzf git-mkver.tar.gz 9 | sudo mv git-mkver /usr/local/bin 10 | rm git-mkver.tar.gz 11 | # Linux 12 | elif [[ "$(uname)" == "Linux" ]] 13 | then 14 | curl -L https://github.com/idc101/git-mkver/releases/download/v${MKVER_VERSION}/git-mkver-linux-x86_64-${MKVER_VERSION}.tar.gz -o git-mkver.tar.gz 15 | tar xvzf git-mkver.tar.gz 16 | sudo mv git-mkver /usr/bin 17 | rm git-mkver.tar.gz 18 | fi -------------------------------------------------------------------------------- /git-mkver.conf: -------------------------------------------------------------------------------- 1 | # We won't set most of the defaults, the application defaults are fine 2 | defaults { 3 | patches: [ 4 | Sbt 5 | Installers 6 | ScalaVersion 7 | ] 8 | } 9 | patches: [ 10 | { 11 | name: Sbt 12 | filePatterns: ["build.sbt"] 13 | replacements: [ 14 | { 15 | find: "version\\s+:=\\s+\"{VersionRegex}\"" 16 | replace: "version := \"{Next}\"" 17 | } 18 | ] 19 | } 20 | { 21 | name: Installers 22 | filePatterns: [ 23 | "docs/installation.md" 24 | "etc/Formula/git-mkver.rb" 25 | "etc/scoop/git-mkver.json" 26 | "etc/shell/install.sh" 27 | ] 28 | replacements: [ 29 | { 30 | find: "{VersionRegex}" 31 | replace: "{Version}" 32 | } 33 | ] 34 | } 35 | { 36 | name: ScalaVersion 37 | filePatterns: [ 38 | "src/main/scala/net/cardnell/mkver/package.scala" 39 | ] 40 | replacements: [ 41 | { 42 | find: "val GitMkverVersion = \"{VersionRegex}\"" 43 | replace: "val GitMkverVersion = \"{Version}\"" 44 | } 45 | ] 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.1.1" 5 | } 6 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.8.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10") 2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") 3 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.4") 4 | -------------------------------------------------------------------------------- /src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | # The reference config is suitable for basic master/main based SemVer development 2 | # - releases are done from main 3 | # - no other branches will be tagged 4 | mode: SemVer 5 | tagPrefix: v 6 | defaults { 7 | tag: false 8 | tagMessageFormat: "release {Tag}" 9 | preReleaseFormat: "RC{PreReleaseNumber}" 10 | buildMetaDataFormat: "{Branch}.{ShotHash}" 11 | includeBuildMetaData: true 12 | whenNoValidCommitMessages: IncrementMinor 13 | patches: [] 14 | formats: [] 15 | } 16 | branches: [ 17 | { 18 | pattern: "master" 19 | tag: true 20 | includeBuildMetaData: false 21 | } 22 | { 23 | pattern: "main" 24 | tag: true 25 | includeBuildMetaData: false 26 | } 27 | ] 28 | commitMessageActions: [ 29 | { 30 | pattern: "BREAKING CHANGE" 31 | action: IncrementMajor 32 | } 33 | { 34 | pattern: "major(\\(.+\\))?:" 35 | action: IncrementMajor 36 | } 37 | { 38 | pattern: "minor(\\(.+\\))?:" 39 | action: IncrementMinor 40 | } 41 | { 42 | pattern: "patch(\\(.+\\))?:" 43 | action: IncrementPatch 44 | } 45 | { 46 | pattern: "feat(\\(.+\\))?:" 47 | action: IncrementMinor 48 | } 49 | { 50 | pattern: "fix(\\(.+\\))?:" 51 | action: IncrementPatch 52 | } 53 | ] 54 | patches: [] -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/AppConfig.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import cats.implicits._ 4 | import com.typesafe.config.ConfigFactory 5 | import net.cardnell.mkver.ConfigDesc._ 6 | import net.cardnell.mkver.IncrementAction._ 7 | import net.cardnell.mkver.VersionMode.SemVer 8 | import zio.config.ConfigDescriptor._ 9 | import zio.config._ 10 | import zio.config.typesafe.TypesafeConfigSource 11 | import zio.{Task, ZIO} 12 | 13 | 14 | case class Format(name: String, format: String) 15 | case class Replacement(find: String, replace: String) 16 | case class PatchConfig(name: String, filePatterns: List[String], replacements: List[Replacement]) 17 | case class CommitMessageAction(pattern: String, action: IncrementAction) 18 | 19 | case class AppConfig(mode: VersionMode, 20 | tagPrefix: Option[String], 21 | defaults: Option[BranchConfigDefaults], 22 | branches: Option[List[BranchConfig]], 23 | patches: Option[List[PatchConfig]], 24 | commitMessageActions: Option[List[CommitMessageAction]]) 25 | 26 | case class RunConfig(tag: Boolean, 27 | tagPrefix: String, 28 | tagMessageFormat: String, 29 | preReleaseFormat: String, 30 | buildMetaDataFormat: String, 31 | includeBuildMetaData: Boolean, 32 | commitMessageActions: List[CommitMessageAction], 33 | whenNoValidCommitMessages: IncrementAction, 34 | formats: List[Format], 35 | patches: List[PatchConfig]) 36 | 37 | case class BranchConfigDefaults(tag: Boolean, 38 | tagMessageFormat: String, 39 | preReleaseFormat: String, 40 | buildMetaDataFormat: String, 41 | includeBuildMetaData: Boolean, 42 | whenNoValidCommitMessages: IncrementAction, 43 | formats: List[Format], 44 | patches: List[String]) 45 | 46 | case class BranchConfig(pattern: String, 47 | tag: Option[Boolean], 48 | tagMessageFormat: Option[String], 49 | preReleaseFormat: Option[String], 50 | buildMetaDataFormat: Option[String], 51 | includeBuildMetaData: Option[Boolean], 52 | whenNoValidCommitMessages: Option[IncrementAction], 53 | formats: Option[List[Format]], 54 | patches: Option[List[String]]) 55 | 56 | object ConfigDesc { 57 | object Defaults { 58 | val name = ".*" 59 | val tag = false 60 | val tagMessageFormat = "release {Tag}" 61 | val preReleaseFormat = "RC{PreReleaseNumber}" 62 | val buildMetaDataFormat = "{Branch}.{ShortHash}" 63 | val includeBuildMetaData = true 64 | val whenNoValidCommitMessages = IncrementPatch 65 | val formats: List[Format] = Nil 66 | val patches: List[String] = Nil 67 | } 68 | 69 | def readPreReleaseFormat(value: String): Either[String, String] = 70 | if (value.contains("{PreReleaseNumber}") && !value.endsWith("{PreReleaseNumber}")) { 71 | Left("preReleaseFormat must end with {PreReleaseNumber}") 72 | } else { 73 | Right(value) 74 | } 75 | 76 | def readIncrementAction(value: String): Either[String, IncrementAction] = 77 | value match { 78 | case "Fail" => Right(Fail) 79 | case "IncrementMajor" => Right(IncrementMajor) 80 | case "IncrementMinor" => Right(IncrementMinor) 81 | case "IncrementPatch" => Right(IncrementPatch) 82 | case "NoIncrement" => Right(NoIncrement) 83 | case _ => Left("IncrementAction must be one of Fail|IncrementMajor|IncrementMinor|IncrementPatch|NoIncrement") 84 | } 85 | 86 | def readVersionMode(value: String): Either[String, VersionMode] = 87 | value match { 88 | case "SemVer" => Right(SemVer) 89 | case _ => Left("VersionMode must be one of: SemVer") 90 | } 91 | 92 | 93 | val formatDesc = ( 94 | string("name").describe("Name of format. e.g. 'MajorMinor'") |@| 95 | string("format").describe("Format string for this format. Can include other formats. e.g. '{x}.{y}'") 96 | )(Format.apply, Format.unapply) 97 | 98 | val replacementDesc = ( 99 | string("find").describe("Regex to find in file") |@| 100 | string("replace").describe("Replacement string. Can include version format strings (see help)") 101 | )(Replacement.apply, Replacement.unapply) 102 | 103 | val patchConfigDesc = ( 104 | string("name").describe("Name of patch, referenced from branch configs") |@| 105 | list("filePatterns")(string).describe("Files to apply find and replace in. Supports ** and * glob patterns.") |@| 106 | list("replacements")(replacementDesc).describe("Find and replace patterns") 107 | )(PatchConfig.apply, PatchConfig.unapply) 108 | 109 | val commitMessageActionDesc = ( 110 | string("pattern").describe("Regular expression to match a commit message line") |@| 111 | string("action") 112 | .xmapEither(ConfigDesc.readIncrementAction, (output: IncrementAction) => Right(output.toString)) 113 | .describe("Version Increment behaviour if a commit line matches the regex Fail|IncrementMajor|IncrementMinor|IncrementPatch|NoIncrement") 114 | )(CommitMessageAction.apply, CommitMessageAction.unapply) 115 | 116 | val patternDesc = string("pattern").describe("regex to match branch name on") 117 | val tagDesc = boolean("tag").describe("whether to actually tag this branch when `mkver tag` is called") 118 | val tagMessageFormatDesc = string("tagMessageFormat").describe("format to be used in the annotated git tag message") 119 | val preReleaseFormatDesc = string("preReleaseFormat") 120 | .xmapEither(readPreReleaseFormat, (v: String) => Right(v)) 121 | .describe("format to be used for the pre-release. e.g. alpha, RC-{PreReleaseNumber}, SNAPSHOT") 122 | val buildMetaDataFormatDesc = string("buildMetaDataFormat").describe("format to be used for the build metadata. e.g. {BranchName}") 123 | val includeBuildMetaDataDesc = boolean("includeBuildMetaData").describe("whether the tag version includes the build metadata component") 124 | val whenNoValidCommitMessages = string("whenNoValidCommitMessages") 125 | .xmapEither(ConfigDesc.readIncrementAction, (output: IncrementAction) => Right(output.toString)) 126 | .describe("behaviour if no valid commit messages are found Fail|IncrementMajor|IncrementMinor|IncrementPatch|NoIncrement") 127 | val formatsDesc = list("formats")(ConfigDesc.formatDesc).describe("custom format strings") 128 | val patchesDesc = list("patches")(string).describe("Patch configs to be applied") 129 | 130 | val branchConfigDefaultsDesc = ( 131 | tagDesc.default(Defaults.tag) |@| 132 | tagMessageFormatDesc.default(Defaults.tagMessageFormat) |@| 133 | preReleaseFormatDesc.default(Defaults.preReleaseFormat) |@| 134 | buildMetaDataFormatDesc.default(Defaults.buildMetaDataFormat) |@| 135 | includeBuildMetaDataDesc.default(Defaults.includeBuildMetaData) |@| 136 | whenNoValidCommitMessages.default(Defaults.whenNoValidCommitMessages) |@| 137 | formatsDesc.default(ConfigDesc.Defaults.formats) |@| 138 | patchesDesc.default(ConfigDesc.Defaults.patches) 139 | )(BranchConfigDefaults.apply, BranchConfigDefaults.unapply) 140 | 141 | val branchConfigDesc = ( 142 | patternDesc |@| 143 | tagDesc.optional |@| 144 | tagMessageFormatDesc.optional |@| 145 | preReleaseFormatDesc.optional |@| 146 | buildMetaDataFormatDesc.optional |@| 147 | includeBuildMetaDataDesc.optional |@| 148 | whenNoValidCommitMessages.optional |@| 149 | formatsDesc.optional |@| 150 | patchesDesc.optional 151 | )(BranchConfig.apply, BranchConfig.unapply) 152 | } 153 | 154 | object AppConfig { 155 | val appConfigDesc = ( 156 | string("mode").xmapEither(ConfigDesc.readVersionMode, (output: VersionMode) => Right(output.toString)) 157 | .describe("The Version Mode for this repository") 158 | .default(VersionMode.SemVer) |@| 159 | string("tagPrefix").describe("prefix for git tags").optional |@| 160 | nested("defaults")(ConfigDesc.branchConfigDefaultsDesc).optional |@| 161 | list("branches")(ConfigDesc.branchConfigDesc).optional |@| 162 | list("patches")(ConfigDesc.patchConfigDesc).optional |@| 163 | list("commitMessageActions")(ConfigDesc.commitMessageActionDesc).optional 164 | )(AppConfig.apply, AppConfig.unapply) 165 | val runConfigDesc = ( 166 | tagDesc |@| 167 | string("tagPrefix") |@| 168 | tagMessageFormatDesc |@| 169 | preReleaseFormatDesc |@| 170 | buildMetaDataFormatDesc |@| 171 | includeBuildMetaDataDesc |@| 172 | list("commitMessageActions")(ConfigDesc.commitMessageActionDesc) |@| 173 | whenNoValidCommitMessages |@| 174 | formatsDesc |@| 175 | list("patches")(ConfigDesc.patchConfigDesc) 176 | )(RunConfig.apply, RunConfig.unapply) 177 | 178 | val defaultDefaultBranchConfig: BranchConfigDefaults = BranchConfigDefaults( 179 | ConfigDesc.Defaults.tag, 180 | ConfigDesc.Defaults.tagMessageFormat, 181 | ConfigDesc.Defaults.preReleaseFormat, 182 | ConfigDesc.Defaults.buildMetaDataFormat, 183 | ConfigDesc.Defaults.includeBuildMetaData, 184 | ConfigDesc.Defaults.whenNoValidCommitMessages, 185 | ConfigDesc.Defaults.formats, 186 | ConfigDesc.Defaults.patches 187 | ) 188 | val defaultBranchConfigs: List[BranchConfig] = List( 189 | BranchConfig("master", Some(true), None, None, None, Some(false), None, None, None), 190 | BranchConfig("main", Some(true), None, None, None, Some(false), None, None, None), 191 | BranchConfig("rel[/-].*", Some(true), None, None, None, Some(false), None, None, None), 192 | BranchConfig("release[/-].*", Some(true), None, None, None, Some(false), None, None, None), 193 | BranchConfig("hotfix[/-].*", Some(true), None, None, None, Some(false), None, None, None) 194 | ) 195 | val defaultCommitMessageActions: List[CommitMessageAction] = List( 196 | // Breaking changes (major) 197 | CommitMessageAction("BREAKING CHANGE", IncrementAction.IncrementMajor), 198 | CommitMessageAction("major(\\(.+\\))?!:", IncrementAction.IncrementMajor), 199 | CommitMessageAction("minor(\\(.+\\))?!:", IncrementAction.IncrementMajor), 200 | CommitMessageAction("patch(\\(.+\\))?!:", IncrementAction.IncrementMajor), 201 | CommitMessageAction("feature(\\(.+\\))?!:", IncrementAction.IncrementMajor), 202 | CommitMessageAction("feat(\\(.+\\))?!:", IncrementAction.IncrementMajor), 203 | CommitMessageAction("fix(\\(.+\\))?!:", IncrementAction.IncrementMajor), 204 | // The rest of the conventional commits 205 | CommitMessageAction("(build|ci|chore|docs|perf|refactor|revert|style|test)(\\(.+\\))?!:", IncrementAction.IncrementMajor), 206 | CommitMessageAction("major(\\(.+\\))?:", IncrementAction.IncrementMajor), 207 | CommitMessageAction("minor(\\(.+\\))?:", IncrementAction.IncrementMinor), 208 | CommitMessageAction("patch(\\(.+\\))?:", IncrementAction.IncrementPatch), 209 | CommitMessageAction("feature(\\(.+\\))?:", IncrementAction.IncrementMinor), 210 | CommitMessageAction("feat(\\(.+\\))?:", IncrementAction.IncrementMinor), 211 | CommitMessageAction("fix(\\(.+\\))?:", IncrementAction.IncrementPatch), 212 | // The rest of the conventional commits 213 | CommitMessageAction("(build|ci|chore|docs|perf|refactor|revert|style|test)(\\(.+\\))?:", IncrementAction.NoIncrement) 214 | ) 215 | 216 | def getRunConfig(configFile: Option[String], currentBranch: String): Task[RunConfig] = { 217 | for { 218 | appConfig <- getAppConfig(configFile) 219 | defaults = appConfig.flatMap(_.defaults).getOrElse(defaultDefaultBranchConfig) 220 | branchConfig = appConfig.flatMap(_.branches).getOrElse(defaultBranchConfigs) 221 | .find { bc => currentBranch.matches(bc.pattern) } 222 | patchNames = branchConfig.flatMap(_.patches).getOrElse(defaults.patches) 223 | patchConfigs <- getPatchConfigs(appConfig.flatMap(_.patches).getOrElse(Nil), patchNames) 224 | } yield { 225 | RunConfig( 226 | tag = branchConfig.flatMap(_.tag).getOrElse(defaults.tag), 227 | tagPrefix = appConfig.flatMap(_.tagPrefix).getOrElse("v"), 228 | tagMessageFormat = branchConfig.flatMap(_.tagMessageFormat).getOrElse(defaults.tagMessageFormat), 229 | preReleaseFormat = branchConfig.flatMap(_.preReleaseFormat).getOrElse(defaults.preReleaseFormat), 230 | buildMetaDataFormat = branchConfig.flatMap(_.buildMetaDataFormat).getOrElse(defaults.buildMetaDataFormat), 231 | includeBuildMetaData = branchConfig.flatMap(_.includeBuildMetaData).getOrElse(defaults.includeBuildMetaData), 232 | commitMessageActions = mergeCommitMessageActions(defaultCommitMessageActions, appConfig.flatMap(_.commitMessageActions).getOrElse(Nil)), 233 | whenNoValidCommitMessages = branchConfig.flatMap(_.whenNoValidCommitMessages).getOrElse(defaults.whenNoValidCommitMessages), 234 | formats = mergeFormats(defaults.formats, branchConfig.flatMap(_.formats).getOrElse(Nil)), 235 | patches = patchConfigs 236 | ) 237 | } 238 | } 239 | 240 | def mergeFormats(startList: List[Format], overrides: List[Format]): List[Format] = 241 | merge(startList, overrides, (f: Format) => f.name) 242 | 243 | def mergeCommitMessageActions(startList: List[CommitMessageAction], overrides: List[CommitMessageAction]): List[CommitMessageAction] = 244 | merge(startList, overrides, (cma: CommitMessageAction) => cma.pattern) 245 | 246 | def merge[T](startList: List[T], overrides: List[T], getName: T => String): List[T] = { 247 | val startMap = startList.map( it => (getName(it), it)).toMap 248 | val overridesMap = overrides.map( it => (getName(it), it)).toMap 249 | overridesMap.values.foldLeft(startMap)((a, n) => a.+((getName(n), n))).values.toList.sortBy(getName(_)) 250 | } 251 | 252 | def getPatchConfigs(patches: List[PatchConfig], patchNames: List[String]): Task[List[PatchConfig]] = { 253 | val allPatchConfigs = patches.map(it => (it.name, it)).toMap 254 | Task.foreach(patchNames) { c => 255 | allPatchConfigs.get(c) match { 256 | case Some(p) => Task.succeed(p) 257 | case None => Task.fail(MkVerException(s"Can't find patch config named $c")) 258 | } 259 | } 260 | } 261 | 262 | def getAppConfig(configFile: Option[String]): Task[Option[AppConfig]] = { 263 | val fileIO = configFile.map { cf => 264 | for { 265 | path <- Path(cf) 266 | exists <- Files.exists(path) 267 | r <- if (exists) Task.some(cf) else Task.fail(MkVerException(s"--config $cf does not exist")) 268 | } yield r 269 | }.orElse { 270 | sys.env.get("GITMKVER_CONFIG").map { cf => 271 | for { 272 | path <- Path(cf) 273 | exists <- Files.exists(path) 274 | r <- if (exists) Task.some(cf) else Task.fail(MkVerException(s"GITMKVER_CONFIG $cf does not exist")) 275 | } yield r 276 | } 277 | }.getOrElse { 278 | for { 279 | path <- Path("mkver.conf") 280 | exists <- Files.exists(path) 281 | r <- if (exists) Task.some("mkver.conf") else Task.none 282 | } yield r 283 | } 284 | 285 | fileIO.flatMap { fileOpt => 286 | ZIO.foreach(fileOpt)(tryLoadAppConfig) 287 | } 288 | } 289 | 290 | def tryLoadAppConfig(file: String): Task[AppConfig] = { 291 | for { 292 | configSource <- TypesafeConfigSource.fromTypesafeConfig(ConfigFactory.parseFile(new java.io.File(file)).resolve()) 293 | .fold(l => Task.fail(MkVerException(l.getMessage())), r => Task.succeed(r)) 294 | appConfig <- read(AppConfig.appConfigDesc from configSource) 295 | .fold(l => Task.fail(MkVerException("Unable to parse config: " + l.prettyPrint())), r => Task.succeed(r)) 296 | } yield appConfig 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/CommandLineArgs.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import com.monovore.decline.{Command, Opts} 4 | import cats.implicits._ 5 | import cats.instances.unit 6 | import net.cardnell.mkver.CommandLineArgs.ConfigOpts 7 | 8 | object CommandLineArgs { 9 | sealed trait AppOpts 10 | case class NextOpts(format: Option[String], preRelease: Boolean, prefix: Boolean) extends AppOpts 11 | case class TagOpts(preRelease: Boolean) extends AppOpts 12 | case class PatchOpts(preRelease: Boolean) extends AppOpts 13 | case class InfoOpts(preRelease: Boolean, includeEnv: Boolean) extends AppOpts 14 | case object ConfigOpts extends AppOpts 15 | 16 | val configFile: Opts[Option[String]] = Opts.option[String]("config", short = "c", metavar = "file", help = "Config file to load").orNone 17 | val format: Opts[Option[String]] = Opts.option[String]("format", short = "f", metavar = "string", help = "Format string for the version number").orNone 18 | val prefix: Opts[Boolean] = Opts.flag("tag-prefix", short = "t", help = "Include the tag prefix in the output").orFalse 19 | val preRelease: Opts[Boolean] = Opts.flag("pre-release", short = "p", help = "Include the tag prefix in the output").orFalse 20 | val includeEnv: Opts[Boolean] = Opts.flag("include-env", short = "i", help = "Include environment variables").orFalse 21 | 22 | val nextOptions: Opts[NextOpts] = (format, preRelease, prefix).mapN(NextOpts.apply) 23 | val tagOptions: Opts[TagOpts] = preRelease.map(TagOpts.apply) 24 | val patchOptions: Opts[PatchOpts] = preRelease.map(PatchOpts.apply) 25 | val infoOptions: Opts[InfoOpts] = (preRelease, includeEnv).mapN(InfoOpts.apply) 26 | 27 | val nextCommand: Command[NextOpts] = Command("next", header = "Print the next version tag that would be used") { 28 | nextOptions 29 | } 30 | 31 | val tagCommand: Command[TagOpts] = Command("tag", header = "Git Tag the current commit with the next version tag") { 32 | tagOptions 33 | } 34 | 35 | val patchCommand: Command[PatchOpts] = Command("patch", header = "Patch version information in files with the next version tag") { 36 | patchOptions 37 | } 38 | 39 | val infoCommand: Command[InfoOpts] = Command("info", header = "output all formats and branch configuration") { 40 | infoOptions 41 | } 42 | 43 | val configCommand: Command[AppOpts] = Command("config", header = "output final configuration to be used") { Opts(ConfigOpts) } 44 | 45 | case class CommandLineOpts(configFile: Option[String], opts: AppOpts) 46 | 47 | val commands: Opts[AppOpts] = Opts.subcommands(nextCommand, tagCommand, patchCommand, infoCommand, configCommand) 48 | 49 | val commandLineOpts: Opts[CommandLineOpts] = (configFile, commands).mapN(CommandLineOpts.apply) 50 | 51 | val mkverCommand: Command[CommandLineOpts] = Command( 52 | name = s"git mkver", 53 | 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" 54 | ) { 55 | commandLineOpts 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/Files.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import java.io.{File => JFile} 4 | import java.nio.file.{FileSystems, Files => JFiles, Path => JPath, Paths => JPaths} 5 | 6 | import zio.{Task, URIO, ZIO} 7 | 8 | import scala.collection.JavaConverters._ 9 | 10 | object Files { 11 | def exists(path: Path): Task[Boolean] = { 12 | ZIO.effect(JFiles.exists(path.path)) 13 | } 14 | 15 | def glob(path: Path, glob: String): ZIO[Any, Throwable, List[Path]] = { 16 | for { 17 | fs <- ZIO.effect(FileSystems.getDefault) 18 | matcher <- ZIO.effect(fs.getPathMatcher(s"glob:$glob")) 19 | list <- ZIO.effect(JFiles.walk(path.path).iterator().asScala.filter(matcher.matches).map(Path(_)).toList) 20 | } yield list 21 | } 22 | 23 | def readAllLines(path: Path): Task[List[String]] = { 24 | ZIO.effect(JFiles.readAllLines(path.path).asScala.toList) 25 | } 26 | 27 | def readAll(path: Path): Task[String] = { 28 | ZIO.effect(scala.io.Source.fromFile(path.path.toString)).bracket(s => ZIO.effect(s.close()).ignore, { s => 29 | ZIO.effect(s.mkString) 30 | }) 31 | } 32 | 33 | def write(path: Path, lines: Iterable[String]): Task[JPath] = { 34 | ZIO.effect(JFiles.write(path.path, lines.asJava)) 35 | } 36 | 37 | def write(path: Path, content: String): Task[JPath] = { 38 | ZIO.effect(JFiles.write(path.path, content.getBytes)) 39 | } 40 | 41 | def createTempDirectory(prefix: String): ZIO[Any, Throwable, Path] = { 42 | ZIO.effect(JFiles.createTempDirectory(prefix)).map(Path(_)) 43 | } 44 | 45 | def usingTempDirectory[R, E, A](prefix: String)(f: Path => ZIO[R, E, A]): ZIO[R, Any, A] = { 46 | createTempDirectory("git-mkver") 47 | .bracket((p: Path) => ZIO.effect({ p.path.toFile.delete() }).ignore) { tempDir: Path => 48 | f(tempDir) 49 | } 50 | } 51 | 52 | def touch(path: Path): URIO[Any, Unit] = { 53 | ZIO.effect(JFiles.createFile(path.path)).ignore 54 | } 55 | } 56 | 57 | case class File(file: JFile) { 58 | 59 | } 60 | 61 | object File { 62 | def apply(path: String): ZIO[Any, Throwable, File] = { 63 | ZIO.effect(new JFile(path)).map(f => File(f)) 64 | } 65 | } 66 | 67 | case class Path(path: JPath) { 68 | def toFile: File = File(path.toFile) 69 | } 70 | 71 | object Path { 72 | def apply(first: String, more: String*): ZIO[Any, Throwable, Path] = { 73 | ZIO.effect(JPaths.get(first, more:_*)).map(p => Path(p)) 74 | } 75 | 76 | val currentWorkingDirectory: ZIO[Any, Throwable, Path] = apply("") 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/Formatter.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | object Formatter { 4 | case class Formatter(formats: List[Format]) { 5 | def format(input: String, count: Int = 0): String = { 6 | if (count > 100) { 7 | // likely an infinite loop 8 | input 9 | } else { 10 | val result = formats.sortBy(_.name.length * -1).foldLeft(input) { (s, v) => 11 | s.replace("{" + v.name + "}", v.format) 12 | } 13 | if (result == input) { 14 | // no replacements made - we are done 15 | result 16 | } else { 17 | // recursively replace 18 | format(result, count + 1) 19 | } 20 | } 21 | } 22 | } 23 | 24 | def apply(version: VersionData, runConfig: RunConfig, preRelease: Boolean): Formatter = { 25 | val versionFormat = (preRelease, runConfig.includeBuildMetaData) match { 26 | case (false, false) => "{Version}" 27 | case (true, false) => "{VersionPreRelease}" 28 | case (false, true) => "{VersionBuildMetaData}" 29 | case (true, true) => "{VersionPreReleaseBuildMetaData}" 30 | } 31 | val builtInFormats = (List( 32 | Format("Version", "{Major}.{Minor}.{Patch}"), 33 | Format("VersionPreRelease", "{Version}-{PreRelease}"), 34 | Format("VersionBuildMetaData", "{Version}+{BuildMetaData}"), 35 | Format("VersionPreReleaseBuildMetaData", "{Version}-{PreRelease}+{BuildMetaData}"), 36 | Format("BuildMetaData", runConfig.buildMetaDataFormat), 37 | Format("Next", versionFormat), 38 | Format("Tag", "{TagPrefix}{Next}"), 39 | Format("TagMessage", runConfig.tagMessageFormat), 40 | Format("PreRelease", runConfig.preReleaseFormat), 41 | Format("PreReleaseNumber", version.preReleaseNumber.map(_.toString).getOrElse("")), 42 | Format("Major", version.major.toString), 43 | Format("Minor", version.minor.toString), 44 | Format("Patch", version.patch.toString), 45 | Format("Branch", branchNameToVariable(version.branch)), 46 | Format("ShortHash", version.commitHashShort), 47 | Format("FullHash", version.commitHashFull), 48 | Format("CommitsSinceTag", version.commitCount.toString), 49 | Format("Tagged?", (version.commitCount == 0).toString), 50 | Format("dd", version.date.getDayOfMonth.formatted("%02d")), 51 | Format("mm", version.date.getMonthValue.formatted("%02d")), 52 | Format("yyyy", version.date.getYear.toString), 53 | Format("Tag?", runConfig.tag.toString), 54 | Format("TagPrefix", runConfig.tagPrefix) 55 | ) ++ envVariables() 56 | ++ azureDevOpsVariables()) 57 | Formatter(AppConfig.mergeFormats(runConfig.formats, builtInFormats)) 58 | } 59 | 60 | def azureDevOpsVariables(): List[Format] = { 61 | // These need special formatting to be useful 62 | List( 63 | envVariable("SYSTEM_PULLREQUEST_SOURCEBRANCH", "AzurePrSourceBranch") 64 | .map( f => f.copy(format = branchNameToVariable(f.format))) 65 | ).flatten 66 | } 67 | 68 | def envVariables(): List[Format] = { 69 | sys.env.map { case (name, value) => Format(s"env.$name", value) }.toList 70 | } 71 | 72 | def envVariable(name: String, formatName: String): Option[Format] = { 73 | sys.env.get(name).map { value => Format(formatName, value) } 74 | } 75 | 76 | def branchNameToVariable(branchName: String): String = { 77 | branchName 78 | .replace("refs/heads/", "") 79 | .replace("refs/", "") 80 | .replace("/", "-") 81 | .replace("_", "-") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/Git.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import net.cardnell.mkver.ProcessUtils._ 4 | import zio.blocking.Blocking 5 | import zio.process.Command 6 | import zio.{Has, Layer, RIO, Task, ZIO, ZLayer} 7 | 8 | object Git { 9 | trait Service { 10 | def currentBranch(): RIO[Blocking, String] 11 | def fullLog(fromRef: Option[String]): RIO[Blocking, String] 12 | def commitInfoLog(): RIO[Blocking, String] 13 | def tag(tag: String, tagMessage: String): RIO[Blocking, Unit] 14 | def checkGitRepo(): RIO[Blocking, Unit] 15 | } 16 | 17 | def live(workingDir: Option[File] = None): Layer[Nothing, Has[Service]] = ZLayer.succeed( 18 | new Service { 19 | val cwd: Option[File] = workingDir 20 | 21 | def currentBranch(): RIO[Blocking, String] = { 22 | if (sys.env.contains("BUILD_SOURCEBRANCH")) { 23 | // Azure Devops Pipeline 24 | RIO.succeed(sys.env("BUILD_SOURCEBRANCH") 25 | .replace("refs/heads/", "") 26 | .replace("refs/", "")) 27 | } else if (sys.env.contains("CI_COMMIT_REF_NAME")) { 28 | // Gitlab CI 29 | RIO.succeed(sys.env("CI_COMMIT_REF_NAME")) 30 | } else { 31 | // TODO better fallback if we in detached head mode like build systems do 32 | exec("git rev-parse --abbrev-ref HEAD", cwd).map(_.stdout) 33 | } 34 | } 35 | 36 | def commitInfoLog(): RIO[Blocking, String] = { 37 | exec(Array("git", "log", "--pretty=%h %H %d"), cwd).map(_.stdout) 38 | } 39 | 40 | def fullLog(fromRef: Option[String]): RIO[Blocking, String] = { 41 | val refRange = fromRef.map(r => Array(s"$r..HEAD")).getOrElse(Array()) 42 | exec(Array("git", "--no-pager", "log") ++ refRange, cwd).map(_.stdout) 43 | } 44 | 45 | def tag(tag: String, tagMessage: String): RIO[Blocking, Unit] = { 46 | exec(Array("git", "tag", "-a", "-m", tagMessage, tag), cwd).unit 47 | } 48 | 49 | def checkGitRepo(): RIO[Blocking, Unit] = { 50 | exec(s"git --no-pager show", cwd).flatMap { output => 51 | Task.fail(MkVerException(output.stdout)).when(output.exitCode != 0) 52 | } 53 | } 54 | } 55 | ) 56 | 57 | //accessor methods 58 | def currentBranch(): ZIO[Git with Blocking, Throwable, String] = 59 | ZIO.accessM(_.get.currentBranch()) 60 | 61 | def fullLog(fromRef: Option[String]): ZIO[Git with Blocking, Throwable, String] = 62 | ZIO.accessM(_.get.fullLog(fromRef)) 63 | 64 | def commitInfoLog(): ZIO[Git with Blocking, Throwable, String] = 65 | ZIO.accessM(_.get.commitInfoLog()) 66 | 67 | def tag(tag: String, tagMessage: String): RIO[Git with Blocking, Unit] = 68 | ZIO.accessM(_.get.tag(tag, tagMessage)) 69 | 70 | def checkGitRepo(): RIO[Git with Blocking, Unit] = 71 | ZIO.accessM(_.get.checkGitRepo()) 72 | } 73 | 74 | object ProcessUtils { 75 | def exec(command: String): ZIO[Blocking, MkVerException, ProcessResult] = { 76 | exec(command.split(" "), None) 77 | } 78 | 79 | def exec(command: String, dir: Option[File]): ZIO[Blocking, MkVerException, ProcessResult] = { 80 | exec(command.split(" "), dir) 81 | } 82 | 83 | def exec(command: String, dir: File): ZIO[Blocking, MkVerException, ProcessResult] = { 84 | exec(command.split(" "), Some(dir)) 85 | } 86 | 87 | def exec(commands: Array[String], dir: Option[File] = None): ZIO[Blocking, MkVerException, ProcessResult] = { 88 | val processName = commands(0) 89 | val args = commands.tail 90 | val command = Command(processName, args:_*) 91 | val process = dir.map(d => command.workingDirectory(d.file)) 92 | .getOrElse(command) 93 | val result = (for { 94 | p <- process.run 95 | lines <- p.stdout.string 96 | linesStdErr <- p.stderr.string 97 | exitCode <- p.exitCode 98 | } yield ProcessResult(lines.trim, linesStdErr.trim, exitCode.code)).mapError(ce => MkVerException(ce.toString)) 99 | 100 | result.flatMap { pr => 101 | if (pr.exitCode != 0) { 102 | ZIO.fail(MkVerException(s"Git error: ${pr.stderr}")) 103 | } else { 104 | ZIO.succeed(pr) 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/Main.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import net.cardnell.mkver.MkVer._ 4 | import net.cardnell.mkver.CommandLineArgs.{CommandLineOpts, ConfigOpts, InfoOpts, NextOpts, PatchOpts, TagOpts} 5 | import zio._ 6 | import zio.blocking.Blocking 7 | import zio.console._ 8 | 9 | case class ProcessResult(stdout: String, stderr: String, exitCode: Int) 10 | 11 | case class MkVerException(message: String) extends Exception { 12 | override def getMessage: String = message 13 | } 14 | 15 | object Main extends App { 16 | def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = 17 | appLogic(args) 18 | .provideCustomLayer(Blocking.live >>> Git.live(None)) 19 | .fold(_ => ExitCode.failure, _ => ExitCode.success) 20 | 21 | def appLogic(args: List[String]): ZIO[Console with Git with Blocking, Unit, Unit] = { 22 | mainImpl(args) 23 | .flatMapError(err => putStrLn(err.getMessage)) 24 | } 25 | 26 | def mainImpl(args: List[String]): ZIO[Console with Git with Blocking, Throwable, Unit] = { 27 | CommandLineArgs.mkverCommand.parse(args, sys.env) 28 | .fold( help => Task.fail(MkVerException(help.toString())), opts => run(opts)) 29 | } 30 | 31 | def run(opts: CommandLineOpts): ZIO[Console with Git with Blocking, Throwable, Unit] = { 32 | for { 33 | _ <- Git.checkGitRepo() 34 | currentBranch <- Git.currentBranch() 35 | config <- AppConfig.getRunConfig(opts.configFile, currentBranch) 36 | r <- opts.opts match { 37 | case nextOps@NextOpts(_, _, _) => 38 | runNext(nextOps, config, currentBranch) 39 | case tagOpts@TagOpts(_) => 40 | runTag(tagOpts, config, currentBranch) 41 | case patchOpts@PatchOpts(_) => 42 | runPatch(patchOpts, config, currentBranch) 43 | case infoOpts@InfoOpts(_, _) => 44 | runInfo(infoOpts, config, currentBranch) 45 | case ConfigOpts => 46 | runConfig(config) 47 | } 48 | } yield r 49 | } 50 | 51 | def runNext(nextOpts: NextOpts, config: RunConfig, currentBranch: String): ZIO[Console with Git with Blocking, Throwable, Unit] = { 52 | for { 53 | nextVersion <- getNextVersion(config, currentBranch, nextOpts.preRelease) 54 | next <- nextOpts.format.map { format => 55 | Task.effect(Formatter(nextVersion, config, nextOpts.preRelease).format(format)) 56 | }.getOrElse { 57 | formatVersion(config, nextVersion, nextOpts.prefix, nextOpts.preRelease) 58 | } 59 | _ <- putStrLn(next) 60 | } yield () 61 | } 62 | 63 | def runTag(tagOpts: TagOpts, config: RunConfig, currentBranch: String): ZIO[Git with Blocking, Throwable, Unit] = { 64 | for { 65 | nextVersion <- getNextVersion(config, currentBranch, tagOpts.preRelease) 66 | tag <- formatVersion(config, nextVersion, formatAsTag = true, preRelease = tagOpts.preRelease) 67 | tagMessage = Formatter(nextVersion, config, tagOpts.preRelease).format(config.tagMessageFormat) 68 | _ <- Git.tag(tag, tagMessage) when (config.tag) 69 | } yield () 70 | } 71 | 72 | def runPatch(patchOpts: PatchOpts, config: RunConfig, currentBranch: String): ZIO[Console with Git with Blocking, Throwable, Unit] = { 73 | getNextVersion(config, currentBranch, patchOpts.preRelease).flatMap { nextVersion => 74 | ZIO.foreach(config.patches) { patch => 75 | ZIO.foreach(patch.replacements) { findReplace => 76 | val regex = findReplace.find 77 | .replace("{VersionRegex}", Version.versionFullRegex).r 78 | val replacement = Formatter(nextVersion, config, patchOpts.preRelease).format(findReplace.replace) 79 | ZIO.foreach(patch.filePatterns) { filePattern => 80 | for { 81 | cwd <- Path.currentWorkingDirectory 82 | matches <- Files.glob(cwd, filePattern) 83 | l <- ZIO.foreach(matches) { fileMatch => 84 | for { 85 | _ <- putStrLn(s"Patching file: '${fileMatch.path.toString}', new value: '$replacement'") 86 | content <- Files.readAll(fileMatch) 87 | newContent <- ZIO.effect(regex.replaceAllIn(content, replacement)) 88 | p <- Files.write(fileMatch, newContent) 89 | } yield p 90 | } 91 | } yield l 92 | } 93 | } 94 | } 95 | }.unit 96 | } 97 | 98 | def runInfo(infoOpts: InfoOpts, config: RunConfig, currentBranch: String): RIO[Console with Git with Blocking, Unit] = { 99 | for { 100 | nextVersion <- getNextVersion(config, currentBranch, infoOpts.preRelease) 101 | formatter = Formatter(nextVersion, config, infoOpts.preRelease) 102 | _ <- ZIO.foreach(formatter.formats) { format => 103 | val result = formatter.format(format.format) 104 | putStrLn(s"${format.name}=$result") when (!format.name.startsWith("env") || infoOpts.includeEnv) 105 | } 106 | } yield () 107 | } 108 | 109 | def runConfig(config: RunConfig): RIO[Console, Unit] = { 110 | import zio.config.typesafe._ 111 | zio.config.write(AppConfig.runConfigDesc, config) match { 112 | case Left(s) => RIO.fail(MkVerException(s)) 113 | case Right(pt) => putStrLn(pt.toHoconString) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/MkVer.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import java.time.LocalDate 4 | 5 | import zio.blocking.Blocking 6 | import zio.{IO, RIO, Task} 7 | 8 | object MkVer { 9 | case class CommitInfo(shortHash: String, fullHash: String, commitsBeforeHead: Int, tags: List[Version]) 10 | 11 | case class LastVersion(commitHash: String, commitsBeforeHead: Int, version: Version) 12 | 13 | def getCommitInfos(prefix: String): RIO[Git with Blocking, List[CommitInfo]] = { 14 | val lineMatch = "^([0-9a-f]{5,40}) ([0-9a-f]{5,40}) *(\\((.*)\\))?$".r 15 | 16 | Git.commitInfoLog().map { log => 17 | log.linesIterator.zipWithIndex.flatMap { 18 | case (line, i) => { 19 | line match { 20 | case lineMatch(shortHash, longHash, _, names) => { 21 | val versions = Option(names).getOrElse("").split(",").toList 22 | .map(_.trim) 23 | .filter(_.startsWith("tag: ")) 24 | .map(_.replace("tag: ", "")) 25 | .flatMap(Version.parseTag(_, prefix)) 26 | Some(CommitInfo(shortHash, longHash, i, versions)) 27 | } 28 | case _ => None 29 | } 30 | } 31 | }.toList 32 | } 33 | } 34 | 35 | def getLastVersion(commitInfos: List[CommitInfo]): Option[LastVersion] = { 36 | commitInfos.find(_.tags.nonEmpty).map(ci => LastVersion(ci.fullHash, ci.commitsBeforeHead, ci.tags.max)) 37 | } 38 | 39 | def formatVersion(config: RunConfig, versionData: VersionData, formatAsTag: Boolean, preRelease: Boolean): Task[String] = { 40 | Task.effect { 41 | val formatter = Formatter(versionData, config, preRelease) 42 | if (formatAsTag) { 43 | formatter.format("{Tag}") 44 | } else { 45 | formatter.format("{Next}") 46 | } 47 | } 48 | } 49 | 50 | def getNextVersion(config: RunConfig, currentBranch: String, preRelease: Boolean): RIO[Git with Blocking, VersionData] = { 51 | for { 52 | commitInfos <- getCommitInfos(config.tagPrefix) 53 | lastVersionOpt = getLastVersion(commitInfos) 54 | bumps <- getVersionBumps(currentBranch, lastVersionOpt, config.commitMessageActions, config.whenNoValidCommitMessages) 55 | nextVersion = lastVersionOpt.map(_.version.getNextVersion(bumps, preRelease)).getOrElse(NextVersion(0, 1, 0, if (preRelease) Some(1) else None)) 56 | } yield { 57 | VersionData( 58 | major = nextVersion.major, 59 | minor = nextVersion.minor, 60 | patch = nextVersion.patch, 61 | preReleaseNumber = nextVersion.preReleaseNumber, 62 | commitCount = lastVersionOpt.map(_.commitsBeforeHead).getOrElse(commitInfos.length), 63 | branch = currentBranch, 64 | commitHashShort = commitInfos.headOption.map(_.shortHash).getOrElse(""), 65 | commitHashFull = commitInfos.headOption.map(_.fullHash).getOrElse(""), 66 | date = LocalDate.now() 67 | ) 68 | } 69 | } 70 | 71 | def getVersionBumps(currentBranch: String, 72 | lastVersion: Option[LastVersion], 73 | commitMessageActions: List[CommitMessageAction], 74 | whenNoValidCommitMessages: IncrementAction): RIO[Git with Blocking, VersionBumps] = { 75 | def logToBumps(log: String): IO[MkVerException, VersionBumps] = { 76 | val logBumps: VersionBumps = calcBumps(log.linesIterator.toList, commitMessageActions, VersionBumps()) 77 | if (logBumps.noValidCommitMessages()) { 78 | getFallbackVersionBumps(whenNoValidCommitMessages, logBumps) 79 | } else { 80 | RIO.succeed(logBumps) 81 | } 82 | } 83 | 84 | val bumps = lastVersion match { 85 | case None => Git.fullLog(None).flatMap(logToBumps) // No previous version 86 | case Some(LastVersion(_, 0, _)) => RIO.succeed(VersionBumps.none) // This commit is a version 87 | case Some(lv) => Git.fullLog(Some(lv.commitHash)).flatMap(logToBumps) 88 | } 89 | 90 | val releaseBranch = "^(hotfix|rel|release)[-/](\\d+\\.\\d+\\.\\d+)$".r 91 | val branchNameOverride = currentBranch match { 92 | case releaseBranch(_, v) => Version.parseTag(v, "") 93 | case _ => None 94 | } 95 | 96 | bumps.map(_.withBranchNameOverride(branchNameOverride)) 97 | } 98 | 99 | def getFallbackVersionBumps(whenNoValidCommitMessages: IncrementAction, logBumps: VersionBumps): IO[MkVerException, VersionBumps] = { 100 | whenNoValidCommitMessages match { 101 | case IncrementAction.Fail => IO.fail(MkVerException("No valid commit messages found describing version increment")) 102 | case IncrementAction.NoIncrement => IO.succeed(logBumps) 103 | case other => IO.succeed(logBumps.bump(other)) 104 | } 105 | } 106 | 107 | def calcBumps(lines: List[String], commitMessageActions: List[CommitMessageAction], bumps: VersionBumps): VersionBumps = { 108 | val overrideRegex = " next-version: *(\\d+\\.\\d+\\.\\d+)".r 109 | if (lines.isEmpty) { 110 | bumps 111 | } else { 112 | val line = lines.head 113 | if (line.startsWith("commit")) { 114 | calcBumps(lines.tail, commitMessageActions, bumps.bumpCommits()) 115 | } else if (line.startsWith(" ")) { 116 | // check for override text 117 | val newBumps = line match { 118 | case overrideRegex(v) => Version.parseTag(v, "").map(bumps.withCommitOverride).getOrElse(bumps) 119 | case _ => bumps 120 | } 121 | // check for bump messages 122 | val newBumps2 = commitMessageActions.flatMap { cma => 123 | if (cma.pattern.r.findFirstIn(line).nonEmpty) { 124 | Some(newBumps.bump(cma.action)) 125 | } else { 126 | None 127 | } 128 | }.headOption.getOrElse(newBumps) 129 | calcBumps(lines.tail, commitMessageActions, newBumps2) 130 | } else { 131 | calcBumps(lines.tail, commitMessageActions, bumps) 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/Version.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import java.time.LocalDate 4 | 5 | sealed trait VersionMode 6 | 7 | object VersionMode { 8 | case object SemVer extends VersionMode 9 | case object YearMonth extends VersionMode 10 | } 11 | 12 | sealed trait IncrementAction 13 | 14 | object IncrementAction { 15 | case object Fail extends IncrementAction 16 | case object IncrementMajor extends IncrementAction 17 | case object IncrementMinor extends IncrementAction 18 | case object IncrementPatch extends IncrementAction 19 | case object NoIncrement extends IncrementAction 20 | } 21 | 22 | case class Version(major: Int, 23 | minor: Int, 24 | patch: Int, 25 | preRelease: Option[String] = None, 26 | buildMetaData: Option[String] = None) { 27 | def getNextVersion(bumps: VersionBumps, bumpPreRelease: Boolean): NextVersion = { 28 | (preRelease, bumpPreRelease) match { 29 | case (Some(pr), false) => 30 | // Generating a non-pre-release version after a pre-release is always the version without pre-release 31 | // ... unless an override has been specified. 32 | bump(bumps.copy(major = false, minor = false, patch = false), None) 33 | case (Some(_), true) => 34 | // Last version was a pre-release and next version is also a pre-release 35 | val nextPreReleaseNumber = preReleaseNumber() match { 36 | case Some(lastPreReleaseNumber) => Some(lastPreReleaseNumber + 1) 37 | case _ => Some(1) 38 | } 39 | if (bumps.branchNameOverride.exists(!equalsVersionCore(_)) || 40 | bumps.commitOverride.exists(!equalsVersionCore(_))){ 41 | // there is an override to a different version - reset the pre-release number 42 | bump(bumps, Some(1)) 43 | } else { 44 | // No override 45 | NextVersion(this.major, this.minor, this.patch, nextPreReleaseNumber) 46 | } 47 | case (None, true) => 48 | // Last version was not a pre-release but this version is 49 | // Bump the version number and add in the pre-release 50 | // ??? what if there is no bump - this would mean going from a released version to a pre-release 51 | // i.e. backwards! 52 | val nextPreReleaseNumber = Some(1) 53 | bump(bumps, nextPreReleaseNumber) 54 | case (None, false) => 55 | // Not a pre-release previously and next version is not a pre-release either 56 | bump(bumps, None) 57 | } 58 | } 59 | 60 | def preReleaseNumber(): Option[Int] = { 61 | val preReleaseNumber = "^.*(\\d+)$".r 62 | preRelease match { 63 | case Some(pr) => 64 | pr match { 65 | case preReleaseNumber (lastPreReleaseNumber) => Some (lastPreReleaseNumber.toInt) 66 | case _ => Some(0) 67 | } 68 | case _ => None 69 | } 70 | } 71 | 72 | def bump(bumps: VersionBumps, newPreRelease: Option[Int]): NextVersion = { 73 | bumps match { 74 | case VersionBumps(_, _, _, _, _, Some(commitOverride)) => NextVersion(commitOverride.major, commitOverride.minor, commitOverride.patch, newPreRelease) 75 | case VersionBumps(_, _, _, _, Some(branchNameOverride), _) => NextVersion(branchNameOverride.major, branchNameOverride.minor, branchNameOverride.patch, newPreRelease) 76 | case VersionBumps(true, _, _, _, _, _) => NextVersion(this.major + 1, 0, 0, newPreRelease) 77 | case VersionBumps(false, true, _, _, _, _) => NextVersion(this.major, this.minor + 1, 0, newPreRelease) 78 | case VersionBumps(false, false, true, _, _, _) => NextVersion(this.major, this.minor, this.patch + 1, newPreRelease) 79 | case _ => NextVersion(this.major, this.minor, this.patch, newPreRelease) 80 | } 81 | } 82 | 83 | def equalsVersionCore(other: Version): Boolean = 84 | major == other.major && minor == other.minor && patch == other.patch 85 | } 86 | 87 | object Version { 88 | val versionOnlyRegex = "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" 89 | 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-]*))*))?" 90 | val metadataRegex = "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?" 91 | val versionFullRegex = s"$versionOnlyRegex$prereleaseRegex$metadataRegex" 92 | 93 | def parseTag(input: String, prefix: String): Option[Version] = { 94 | val version = ("^" + prefix + versionFullRegex + "$").r 95 | 96 | input match { 97 | case version(major, minor, patch, preRelease, buildMetaData) => 98 | Some(Version(major.toInt, minor.toInt, patch.toInt, Option(preRelease), Option(buildMetaData))) 99 | case _ => 100 | None 101 | } 102 | } 103 | 104 | implicit val versionOrdering: Ordering[Version] = Ordering.fromLessThan((l, r) => { 105 | (l.major.compare(r.major), l.minor.compare(r.minor), l.patch.compare(r.patch)) match { 106 | case (0, 0, 0) => { 107 | (l.preReleaseNumber(), r.preReleaseNumber()) match { 108 | case (None, None) => false 109 | case (Some(_), None) => true 110 | case (None, Some(_)) => false 111 | case (Some(lPreNum), Some(rPreNum)) => lPreNum < rPreNum 112 | } 113 | } 114 | case (0, 0, _) => l.patch < r.patch 115 | case (0, _, _) => l.minor < r.minor 116 | case _ => l.major < r.major 117 | } 118 | }) 119 | } 120 | 121 | case class NextVersion(major: Int, 122 | minor: Int, 123 | patch: Int, 124 | preReleaseNumber: Option[Int] = None) 125 | 126 | case class VersionBumps(major: Boolean = false, 127 | minor: Boolean = false, 128 | patch: Boolean = false, 129 | commitCount: Int = 0, 130 | branchNameOverride: Option[Version] = None, 131 | commitOverride: Option[Version] = None) { 132 | def withBranchNameOverride(version: Option[Version]): VersionBumps = 133 | this.copy(branchNameOverride = version) 134 | 135 | def withCommitOverride(version: Version): VersionBumps = 136 | this.copy(commitOverride = Some(version)) 137 | 138 | def bump(incrementAction: IncrementAction): VersionBumps = { 139 | incrementAction match { 140 | case IncrementAction.IncrementMajor => this.copy(major = true) 141 | case IncrementAction.IncrementMinor => this.copy(minor = true) 142 | case IncrementAction.IncrementPatch => this.copy(patch = true) 143 | case _ => this 144 | } 145 | } 146 | def bumpCommits(): VersionBumps = this.copy(commitCount = this.commitCount + 1) 147 | def noValidCommitMessages(): Boolean = { !major && !minor && !patch } 148 | } 149 | 150 | object VersionBumps { 151 | val none = VersionBumps() 152 | } 153 | 154 | case class VersionData(major: Int, 155 | minor: Int, 156 | patch: Int, 157 | preReleaseNumber: Option[Int], 158 | commitCount: Int, 159 | branch: String, 160 | commitHashShort: String, 161 | commitHashFull: String, 162 | date: LocalDate) 163 | -------------------------------------------------------------------------------- /src/main/scala/net/cardnell/mkver/package.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell 2 | 3 | import zio.Has 4 | 5 | package object mkver { 6 | type Git = Has[Git.Service] 7 | 8 | val GitMkverVersion = "1.4.0" 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/test-config1.conf: -------------------------------------------------------------------------------- 1 | tagPrefix: f 2 | defaults { 3 | tag: false 4 | tagMessageFormat: "release {Tag}" 5 | preReleaseFormat: "RC{PreReleaseNumber}" 6 | buildMetaDataFormat: "{Branch}.{ShotHash}" 7 | includeBuildMetaData: true 8 | whenNoValidCommitMessages: IncrementMinor 9 | patches: [] 10 | formats: [ 11 | { 12 | name: BuildMetaData 13 | format: "{Branch}.{ShortHash}" 14 | } 15 | ] 16 | } 17 | branches: [ 18 | { 19 | pattern: "main" 20 | versionFormat: Version 21 | tag: true 22 | } 23 | ] 24 | commitMessageActions: [ 25 | { 26 | pattern: "BREAKING CHANGE" 27 | action: IncrementMajor 28 | } 29 | { 30 | pattern: "major(\\(.+\\))?:" 31 | action: IncrementMajor 32 | } 33 | { 34 | pattern: "minor(\\(.+\\))?:" 35 | action: IncrementMinor 36 | } 37 | { 38 | pattern: "patch(\\(.+\\))?:" 39 | action: IncrementPatch 40 | } 41 | { 42 | pattern: "feat(\\(.+\\))?:" 43 | action: IncrementMinor 44 | } 45 | { 46 | pattern: "fix(\\(.+\\))?:" 47 | action: IncrementPatch 48 | } 49 | ] 50 | patches: [] -------------------------------------------------------------------------------- /src/test/scala/net/cardnell/mkver/AppConfigSpec.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import zio.test._ 4 | import zio.test.Assertion._ 5 | 6 | object AppConfigSpec extends DefaultRunnableSpec { 7 | def spec = suite("AppConfigSpec") ( 8 | suite("getRunConfig") ( 9 | testM("main should return main config") { 10 | assertM(AppConfig.getRunConfig(None, "main"))( 11 | hasField("tag", _.tag, equalTo(true)) 12 | ) 13 | }, 14 | testM("feat should return .* config") { 15 | assertM(AppConfig.getRunConfig(None, "feat"))( 16 | hasField("tag", (c: RunConfig) => c.tag, equalTo(false)) && 17 | hasField("buildMetaDataFormat", (c: RunConfig) => c.buildMetaDataFormat, equalTo("{Branch}.{ShortHash}")) 18 | ) 19 | } 20 | ), 21 | suite("mergeFormat")( 22 | test("should merge formats") { 23 | val f1 = Format("f1", "v1") 24 | val f2 = Format("f2", "v2") 25 | val f3 = Format("f3", "v3") 26 | val f1b = Format("f1", "v4") 27 | assert(AppConfig.mergeFormats(List(f1, f3), List(f1b, f2)))(equalTo(List(f1b, f2, f3).sortBy(_.name))) 28 | } 29 | ) 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/test/scala/net/cardnell/mkver/EndToEndTests.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import net.cardnell.mkver.Main.mainImpl 4 | import zio.blocking.Blocking 5 | import zio.console.Console 6 | import zio.test.Assertion._ 7 | import zio.test._ 8 | import zio.test.mock.MockConsole 9 | import zio.{RIO, ULayer, ZIO} 10 | 11 | object EndToEndTests extends DefaultRunnableSpec { 12 | def spec = suite("trunk based semver development")( 13 | testM("no tags should return version 0.1.0") { 14 | val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("0.1.0")) 15 | val result = test { tempDir => 16 | for { 17 | _ <- fix("code1.py", tempDir) 18 | run <- run(tempDir, "next").provideCustomLayer(mockEnv) 19 | } yield run 20 | } 21 | assertM(result)(isUnit) 22 | }, 23 | testM("main advances correctly and should return version 0.1.1") { 24 | val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("0.1.1")) 25 | val result = test { tempDir => 26 | for { 27 | _ <- fix("code1.py", tempDir) 28 | _ <- run(tempDir, "tag") 29 | _ <- fix("code2.py", tempDir) 30 | //_ <- println(ProcessUtils.exec("git log --graph --full-history --color --oneline", Some(tempDir)).stdout) 31 | run <- run(tempDir, "next").provideCustomLayer(mockEnv) 32 | } yield run 33 | } 34 | assertM(result)(isUnit) 35 | }, 36 | testM("feature branch (+minor) and main (+major) both advance version and should return version 1.0.0") { 37 | val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("1.0.0")) 38 | val result = test { tempDir => 39 | for { 40 | _ <- fix("code1.py", tempDir) 41 | _ <- run(tempDir, "tag") 42 | _ <- branch("feature/f1", tempDir) 43 | _ <- feat("code2.py", tempDir) 44 | _ <- checkout("main", tempDir) 45 | _ <- major("code3.py", tempDir) 46 | _ <- merge("feature/f1", tempDir) 47 | //_ <- println(ProcessUtils.exec("git log --graph --full-history --color --oneline", Some(tempDir)).stdout) 48 | run <- run(tempDir, "next").provideCustomLayer(mockEnv) 49 | } yield run 50 | } 51 | assertM(result)(isUnit) 52 | }, 53 | testM("feature branch (+major) and main (+minor) both advance version and should return version 1.0.0") { 54 | val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("1.0.0")) 55 | val result = test { tempDir => 56 | for { 57 | _ <- fix("code1.py", tempDir) 58 | _ <- run(tempDir, "tag") 59 | _ <- branch("feature/f1", tempDir) 60 | _ <- major("code2.py", tempDir) 61 | _ <- checkout("main", tempDir) 62 | _ <- feat("code3.py", tempDir) 63 | _ <- merge("feature/f1", tempDir) 64 | //_ <- println(ProcessUtils.exec("git log --graph --full-history --color --oneline", Some(tempDir)).stdout) 65 | run <- run(tempDir, "next").provideCustomLayer(mockEnv) 66 | } yield run 67 | } 68 | assertM(result)(isUnit) 69 | }, 70 | testM("feature branch 1 (+major) and feature branch 2 (+minor) both advance version and should return version 1.0.0") { 71 | val mockEnv: ULayer[Console] = MockConsole.PutStrLn(equalTo("1.0.0")) 72 | val result = test { tempDir => 73 | for { 74 | _ <- fix("code1.py", tempDir) 75 | _ <- run (tempDir, "tag") 76 | 77 | _ <- branch ("feature/f1", tempDir) 78 | _ <- feat ("code2.py", tempDir) 79 | _ <- checkout ("main", tempDir) 80 | 81 | _ <- branch ("feature/f2", tempDir) 82 | _ <- major ("code3.py", tempDir) 83 | _ <- checkout ("main", tempDir) 84 | 85 | _ <- merge ("feature/f1", tempDir) 86 | _ <- merge ("feature/f2", tempDir) 87 | //_ <- println (ProcessUtils.exec("git log --graph --full-history --color --oneline", Some(tempDir)).stdout) 88 | run <- run(tempDir, "next").provideCustomLayer(mockEnv) 89 | } yield run 90 | } 91 | assertM(result)(isUnit) 92 | } 93 | ) 94 | 95 | def test[R, E, A](f: File => ZIO[R, E, A]) = { 96 | Files.usingTempDirectory("git-mkver") { tempDir: Path => 97 | init(tempDir.toFile).flatMap { _ => 98 | f(tempDir.toFile) 99 | } 100 | } 101 | } 102 | 103 | def run(tempDir: File, command: String): ZIO[zio.ZEnv, Throwable, Unit] = { 104 | // TODO provide layer with git that has different working dir 105 | mainImpl(List(command)).provideCustomLayer(Blocking.live >>> Git.live(Some(tempDir))) 106 | } 107 | 108 | def init(tempDir: File): RIO[Blocking, Unit] = { 109 | for { 110 | _ <- exec(Array("git", "init", "--initial-branch=main"), Some(tempDir)) 111 | _ <- exec(Array("git", "config", "user.name", "Mona Lisa"), Some(tempDir)) 112 | _ <- exec(Array("git", "config", "user.email", "mona.lisa@email.org"), Some(tempDir)) 113 | } yield () 114 | } 115 | 116 | def fix(name: String, tempDir: File): RIO[Blocking, Unit] = { 117 | for { 118 | path <- Path(tempDir.file.toString, name) 119 | _ <- Files.touch(path) 120 | _ <- exec (Array("git", "add", "."), Some(tempDir)) 121 | _ <- exec (Array("git", "commit", "-m", s"fix: $name"), Some(tempDir)) 122 | } yield () 123 | } 124 | 125 | def feat(name: String, tempDir: File): RIO[Blocking, Unit] = { 126 | for { 127 | path <- Path(tempDir.file.toString, name) 128 | _ <- Files.touch(path) 129 | _ <- exec(Array("git", "add", "."), Some(tempDir)) 130 | _ <- exec(Array("git", "commit", "-m", s"feat: $name"), Some(tempDir)) 131 | } yield () 132 | } 133 | 134 | def major(name: String, tempDir: File): RIO[Blocking, Unit] = { 135 | for { 136 | path <- Path(tempDir.file.toString, name) 137 | _ <- Files.touch(path) 138 | _ <- exec(Array("git", "add", "."), Some(tempDir)) 139 | _ <- exec(Array("git", "commit", "-m", s"major: $name"), Some(tempDir)) 140 | } yield () 141 | } 142 | 143 | def branch(name: String, tempDir: File): RIO[Blocking, Unit] = { 144 | exec(Array("git", "checkout", "-b", name), Some(tempDir)) 145 | } 146 | 147 | def merge(name: String, tempDir: File): RIO[Blocking, Unit] = { 148 | exec(Array("git", "merge", "--no-ff", name), Some(tempDir)) 149 | } 150 | 151 | def checkout(name: String, tempDir: File): RIO[Blocking, Unit] = { 152 | exec(Array("git", "checkout", name), Some(tempDir)) 153 | } 154 | 155 | def exec(commands: Array[String], dir: Option[File] = None): RIO[Blocking, Unit] = { 156 | ProcessUtils.exec(commands, dir).flatMap { result => 157 | RIO.fail(MkVerException(result.stdout)).when(result.exitCode != 0) 158 | } 159 | } 160 | } 161 | 162 | -------------------------------------------------------------------------------- /src/test/scala/net/cardnell/mkver/FilesSpec.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import zio.test.Assertion._ 4 | import zio.test.{DefaultRunnableSpec, assertM, suite, testM} 5 | 6 | object FilesSpec extends DefaultRunnableSpec { 7 | val fs = java.io.File.separator 8 | 9 | def spec = suite("Files")( 10 | testM("format should replace variables") { 11 | assertM(Path("../").map(_.path.toFile))(equalTo(java.nio.file.Paths.get("../").toFile)) 12 | }, 13 | testM("glob specific file") { 14 | val result = for { 15 | p <- Path.currentWorkingDirectory 16 | files <- Files.glob(p, "build.sbt") 17 | list = files.map(_.path.toString) 18 | } yield list 19 | assertM(result)(equalTo(List("build.sbt"))) 20 | }, 21 | testM("glob files in sub directories") { 22 | val result = for { 23 | p <- Path.currentWorkingDirectory 24 | files <- Files.glob(p, "*.sbt") 25 | list = files.map(_.path.toString) 26 | } yield list 27 | assertM(result)(equalTo(List("build.sbt"))) 28 | }, 29 | testM("glob wildcard in directory") { 30 | val result = for { 31 | p <- Path.currentWorkingDirectory 32 | files <- Files.glob(p, "**/*.sbt") 33 | list = files.map(_.path.toString) 34 | } yield list 35 | assertM(result)(hasSameElements(List(s"project${fs}plugins.sbt"))) 36 | }, 37 | testM("glob wildcard in all directories") { 38 | val result = for { 39 | p <- Path.currentWorkingDirectory 40 | files <- Files.glob(p, "***.sbt") 41 | list = files.map(_.path.toString) 42 | } yield list 43 | assertM(result)(hasSameElements(List(s"project${fs}plugins.sbt", "build.sbt"))) 44 | }, 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/net/cardnell/mkver/FormatterSpec.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import java.time.LocalDate 4 | 5 | import zio.test.Assertion.equalTo 6 | import zio.test.{DefaultRunnableSpec, assert, suite, test} 7 | 8 | object FormatterSpec extends DefaultRunnableSpec { 9 | 10 | def spec = suite("FormatterSpec")( 11 | test("format should replace variables") { 12 | val formatter = Formatter.Formatter(List( 13 | Format("a", "1"), 14 | Format("a3", "3"), 15 | Format("b", "2"), 16 | Format("c", "{a}.{b}"), 17 | Format("r1", "{r2}") 18 | )) 19 | assert(formatter.format("hello"))(equalTo("hello")) && 20 | assert(formatter.format("{a}"))(equalTo("1")) && 21 | assert(formatter.format("{a}-{c}"))(equalTo("1-1.2")) && 22 | assert(formatter.format("{r1}"))(equalTo("{r2}")) && 23 | assert(formatter.format("{a3}"))(equalTo("3")) 24 | }, 25 | test("branchNameToVariable should sanitize name") { 26 | assert(Formatter.branchNameToVariable("refs/heads/feat/f1"))(equalTo("feat-f1")) 27 | }, 28 | test("should format default variables") { 29 | val versionData = VersionData(1,2,3, None, 4, "feature/f1", "abcd", "abcdefg", LocalDate.now()) 30 | val runConfig = RunConfig(true, "v", "release {Version}", "RC", "{Branch}", false, Nil, IncrementAction.IncrementMinor, Nil, Nil) 31 | val formatter = Formatter(versionData, runConfig, false) 32 | assert(formatter.format("{Major}"))(equalTo("1")) && 33 | assert(formatter.format("{Minor}"))(equalTo("2")) && 34 | assert(formatter.format("{Patch}"))(equalTo("3")) && 35 | assert(formatter.format("{Branch}"))(equalTo("feature-f1")) 36 | //assert(formatter.format("{env.HOME}"))(equalTo("???")) - How to make this os agnostic? 37 | }, 38 | test("Next pre-release=false buildmetadata=false") { 39 | val versionData = VersionData(1,2,3, Some(3), 4, "feature/f1", "abcd", "abcdefg", LocalDate.now()) 40 | val runConfig = RunConfig(true, "v", "release {Version}", "RC", "{Branch}", false, Nil, IncrementAction.IncrementMinor, Nil, Nil) 41 | val formatter = Formatter(versionData, runConfig, false) 42 | assert(formatter.format("{Next}"))(equalTo("1.2.3")) 43 | }, 44 | test("Next pre-release=false buildmetadata=true") { 45 | val versionData = VersionData(1,2,3, Some(3), 4, "feature/f1", "abcd", "abcdefg", LocalDate.now()) 46 | val runConfig = RunConfig(true, "v", "release {Version}", "RC", "{Branch}", true, Nil, IncrementAction.IncrementMinor, Nil, Nil) 47 | val formatter = Formatter(versionData, runConfig, false) 48 | assert(formatter.format("{Next}"))(equalTo("1.2.3+feature-f1")) 49 | }, 50 | test("Next pre-release=true buildmetadata=false") { 51 | val versionData = VersionData(1,2,3, Some(3), 4, "feature/f1", "abcd", "abcdefg", LocalDate.now()) 52 | val runConfig = RunConfig(true, "v", "release {Version}", "RC{PreReleaseNumber}", "{Branch}", false, Nil, IncrementAction.IncrementMinor, Nil, Nil) 53 | val formatter = Formatter(versionData, runConfig, true) 54 | assert(formatter.format("{Next}"))(equalTo("1.2.3-RC3")) 55 | }, 56 | test("Next pre-release=true buildmetadata=true") { 57 | val versionData = VersionData(1,2,3, Some(3), 4, "feature/f1", "abcd", "abcdefg", LocalDate.now()) 58 | val runConfig = RunConfig(true, "v", "release {Version}", "RC{PreReleaseNumber}", "{Branch}", true, Nil, IncrementAction.IncrementMinor, Nil, Nil) 59 | val formatter = Formatter(versionData, runConfig, true) 60 | assert(formatter.format("{Next}"))(equalTo("1.2.3-RC3+feature-f1")) 61 | }, 62 | test("Picks up custom formats") { 63 | val versionData = VersionData(1,2,3, None, 0, "main", "abcd", "abcdefg", LocalDate.now()) 64 | val formats = List(Format("Docker", "docker - {Version}")) 65 | val runConfig = RunConfig(true, "v", "release {Version}", "RC{PreReleaseNumber}", "{Branch}", true, Nil, IncrementAction.IncrementMinor, formats, Nil) 66 | val formatter = Formatter(versionData, runConfig, false) 67 | assert(formatter.format("{Docker}"))(equalTo("docker - 1.2.3")) 68 | } 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/test/scala/net/cardnell/mkver/MainSpec.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import zio.test.Assertion._ 4 | import zio.test.mock.Expectation._ 5 | import zio.test.mock._ 6 | import zio.test.{DefaultRunnableSpec, assertM, suite, testM} 7 | import zio.{Has, ULayer, URLayer, ZLayer} 8 | import Main.mainImpl 9 | import zio.console.Console 10 | 11 | // TODO >> @Mockable[Git.Service] 12 | object GitMock extends Mock[Git] { 13 | object CurrentBranch extends Effect[Unit, Nothing, String] 14 | object FullLog extends Effect[Option[String], Nothing, String] 15 | object CommitInfoLog extends Effect[Unit, Nothing, String] 16 | object Tag extends Effect[(String, String), Nothing, Unit] 17 | object CheckGitRepo extends Effect[Unit, Nothing, Unit] 18 | 19 | val compose: URLayer[Has[Proxy], Git] = 20 | ZLayer.fromService { proxy => 21 | new Git.Service { 22 | def currentBranch() = proxy(CurrentBranch) 23 | def fullLog(fromRef: Option[String]) = proxy(FullLog, fromRef) 24 | def commitInfoLog() = proxy(CommitInfoLog) 25 | def tag(tag: String, tagMessage: String) = proxy(Tag, tag, tagMessage) 26 | def checkGitRepo() = proxy(CheckGitRepo) 27 | } 28 | } 29 | } 30 | 31 | object MainSpec extends DefaultRunnableSpec { 32 | def spec = suite("MainSpec")( 33 | suite("main") ( 34 | testM("next should return") { 35 | val mockEnv: ULayer[Git with Console] = ( 36 | GitMock.CheckGitRepo(unit) ++ 37 | GitMock.CurrentBranch(value("main")) ++ 38 | GitMock.CommitInfoLog(value("")) ++ 39 | GitMock.FullLog(equalTo(None), value("")) ++ 40 | MockConsole.PutStrLn(equalTo("0.1.0")) 41 | ) 42 | val result = mainImpl(List("next")).provideCustomLayer(mockEnv) 43 | assertM(result)(isUnit) 44 | } 45 | ) 46 | ) 47 | 48 | // TODO stop this actually patching files! 49 | // "patch" should "return " in { 50 | // val result = new Main(fakeGit("main", "", "v0.0.0-1-gabcdef")).mainImpl(Array("patch")) 51 | // result should be(Right("")) 52 | // } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/scala/net/cardnell/mkver/MkVerSpec.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import java.time.LocalDate 4 | 5 | import net.cardnell.mkver.MkVer._ 6 | import zio.ULayer 7 | import zio.test.Assertion._ 8 | import zio.test._ 9 | import zio.test.mock.Expectation._ 10 | 11 | object MkVerSpec extends DefaultRunnableSpec { 12 | val log = """10be55f 10be55fc56c197f5e0159cfbfac22832b289182f (HEAD -> zio) 13 | |f971636 f9716367b8692ed582206951d72bc7affc150f41 (test) 14 | |298326d 298326dd43677121724e9589f390cb0279cb8708 (tag: v0.2.0, tag: v0.3.0) 15 | |2e79c27 2e79c27e2faf85ea241e1911788fd3582c5176ce 16 | |699068c 699068cdec9193878cc1fcfc44c7dd6d004621ff (tag: other) 17 | |320ed50 320ed50d79cbd585d6a28842a340d9742d9327b1 18 | |b3250df b3250df81f7ed389908a2aa89b32425a8ab8fb28 (tag: v0.1.0-RC1, tag: v0.1.0) 19 | |9ded7b1 9ded7b1edf3c066b8c15839304d0427b06cdd020 20 | |""".stripMargin 21 | 22 | val fullLog = """commit 6540bf8d6ac8ade4fc82ac8d73ba4e2739a1440a 23 | |Author: Mona Lisa 24 | |Date: Tue May 19 18:25:04 2020 +1000 25 | | 26 | | fix: code1.py""".stripMargin 27 | 28 | val commitMessageActions = AppConfig.defaultCommitMessageActions 29 | 30 | def spec = suite("MkVerSpec")( 31 | suite("calcBumps")( 32 | test("should parse correctly") { 33 | assert(calcBumps(List(" major: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(major = true))) && 34 | assert(calcBumps(List(" feat: change", " BREAKING CHANGE"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(major = true, minor = true))) && 35 | assert(calcBumps(List(" feat!: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(major = true))) && 36 | assert(calcBumps(List(" fix!: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(major = true))) && 37 | assert(calcBumps(List(" patch!: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(major = true))) && 38 | assert(calcBumps(List(" major(a component)!: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(major = true))) && 39 | assert(calcBumps(List(" feat: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(minor = true))) && 40 | assert(calcBumps(List(" fix: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(patch = true))) && 41 | assert(calcBumps(List(" patch: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(patch = true))) && 42 | assert(calcBumps(List(" major(a component): change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(major = true))) && 43 | assert(calcBumps(List(" feat(a component): change", " BREAKING CHANGE"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(major = true, minor = true))) && 44 | assert(calcBumps(List(" feat(a component): change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(minor = true))) && 45 | assert(calcBumps(List(" fix(a component): change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(patch = true))) && 46 | assert(calcBumps(List(" patch(a component): change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(patch = true))) && 47 | assert(calcBumps(List(" some random commit"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps())) && 48 | assert(calcBumps(List(" Merged PR: feat: change"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(minor = true))) && 49 | assert(calcBumps(List(" next-version: 9.4.3"), commitMessageActions, VersionBumps()))(equalTo(VersionBumps(commitOverride = Some(Version(9, 4, 3))))) 50 | }, 51 | test("should parse real log") { 52 | assert(calcBumps(fullLog.linesIterator.toList, commitMessageActions, VersionBumps()))(equalTo(VersionBumps(patch = true, commitCount = 1))) 53 | } 54 | ), 55 | suite("getLastVersion")( 56 | test("should compare correctly") { 57 | val commitInfos = List( 58 | CommitInfo("b3250df", "b3250df81f7ed389908a2aa89b32425a8ab8fb28", 0, List(Version(0, 1, 0, Some("RC1")), Version(0, 1, 0))), 59 | CommitInfo("9ded7b1", "9ded7b1edf3c066b8c15839304d0427b06cdd020", 1, List()) 60 | ) 61 | assert(getLastVersion(commitInfos))(equalTo(Some(LastVersion("b3250df81f7ed389908a2aa89b32425a8ab8fb28", 0, Version(0, 1, 0))))) 62 | } 63 | ), 64 | suite("getCommitInfos")( 65 | testM("parse commit Info Log correctly") { 66 | val mockEnv: ULayer[Git] = 67 | GitMock.CommitInfoLog(value(log)) 68 | val result = getCommitInfos("v").provideCustomLayer(mockEnv) 69 | assertM(result)(equalTo(List( 70 | CommitInfo("10be55f", "10be55fc56c197f5e0159cfbfac22832b289182f", 0, List()), 71 | CommitInfo("f971636", "f9716367b8692ed582206951d72bc7affc150f41", 1, List()), 72 | CommitInfo("298326d", "298326dd43677121724e9589f390cb0279cb8708", 2, List(Version(0, 2, 0), Version(0, 3, 0))), 73 | CommitInfo("2e79c27", "2e79c27e2faf85ea241e1911788fd3582c5176ce", 3, List()), 74 | CommitInfo("699068c", "699068cdec9193878cc1fcfc44c7dd6d004621ff", 4, List()), 75 | CommitInfo("320ed50", "320ed50d79cbd585d6a28842a340d9742d9327b1", 5, List()), 76 | CommitInfo("b3250df", "b3250df81f7ed389908a2aa89b32425a8ab8fb28", 6, List(Version(0, 1, 0, Some("RC1")), Version(0, 1, 0))), 77 | CommitInfo("9ded7b1", "9ded7b1edf3c066b8c15839304d0427b06cdd020", 7, List()), 78 | ))) 79 | } 80 | ), 81 | suite("formatVersion")( 82 | testM("should format version") { 83 | val versionData = VersionData(1,2,3, None, 4, "feature/f1", "abcd", "abcdefg", LocalDate.now()) 84 | val runConfig = RunConfig(true, "v", "release {Version}", "RC", "{Branch}", false, commitMessageActions, IncrementAction.IncrementMinor, List(Format("Version", "{Major}.{Minor}.{Patch}")), Nil) 85 | assertM(formatVersion(runConfig, versionData, true, false))(equalTo("v1.2.3")) 86 | } 87 | ), 88 | suite("getFallbackVersionBumps")( 89 | testM("should fail") { 90 | for { 91 | result <- getFallbackVersionBumps(IncrementAction.Fail, VersionBumps()).run 92 | } yield 93 | assert(result)(fails(equalTo(MkVerException("No valid commit messages found describing version increment")))) 94 | }, 95 | testM("should bump major") { 96 | assertM(getFallbackVersionBumps(IncrementAction.IncrementMajor, VersionBumps()))(equalTo(VersionBumps(major = true))) 97 | }, 98 | testM("should bump minor") { 99 | assertM(getFallbackVersionBumps(IncrementAction.IncrementMinor, VersionBumps()))(equalTo(VersionBumps(minor = true))) 100 | }, 101 | testM("should bump patch") { 102 | assertM(getFallbackVersionBumps(IncrementAction.IncrementPatch, VersionBumps()))(equalTo(VersionBumps(patch = true))) 103 | }, 104 | testM("should bump none") { 105 | assertM(getFallbackVersionBumps(IncrementAction.NoIncrement, VersionBumps()))(equalTo(VersionBumps())) 106 | } 107 | ) 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /src/test/scala/net/cardnell/mkver/VersionSpec.scala: -------------------------------------------------------------------------------- 1 | package net.cardnell.mkver 2 | 3 | import zio.test.Assertion.equalTo 4 | import zio.test.{DefaultRunnableSpec, assert, suite, test} 5 | 6 | object VersionSpec extends DefaultRunnableSpec { 7 | def spec = suite("VersionSpec")( 8 | suite("getNextVersion")( 9 | test("should return correctly") { 10 | val vb = VersionBumps(true, true, true, 0) 11 | assert(Version(1,2,3,Some("RC4")).getNextVersion(vb, false))(equalTo(NextVersion(1,2,3,None))) && 12 | assert(Version(1,2,3,Some("RC4")).getNextVersion(vb, true))(equalTo(NextVersion(1,2,3,Some(5)))) && 13 | assert(Version(1,2,3,Some("RC")).getNextVersion(vb, true))(equalTo(NextVersion(1,2,3,Some(1)))) && 14 | assert(Version(1,2,3,None).getNextVersion(vb, true))(equalTo(NextVersion(2,0,0,Some(1)))) && 15 | assert(Version(1,2,3,None).getNextVersion(vb, false))(equalTo(NextVersion(2,0,0,None))) 16 | }, 17 | test("should return correctly with commit override") { 18 | val vb = VersionBumps(true, true, true, 0, None, Some(Version(3,4,5))) 19 | assert(Version(1,2,3,Some("RC4")).getNextVersion(vb, false))(equalTo(NextVersion(3,4,5,None))) && 20 | assert(Version(1,2,3,Some("RC4")).getNextVersion(vb, true))(equalTo(NextVersion(3,4,5,Some(1)))) && 21 | assert(Version(1,2,3,Some("RC")).getNextVersion(vb, true))(equalTo(NextVersion(3,4,5,Some(1)))) && 22 | assert(Version(1,2,3,None).getNextVersion(vb, true))(equalTo(NextVersion(3,4,5,Some(1)))) && 23 | assert(Version(1,2,3,None).getNextVersion(vb, false))(equalTo(NextVersion(3,4,5,None))) 24 | }, 25 | test("should return correctly with branch override") { 26 | val vb = VersionBumps(true, true, true, 0, Some(Version(3,4,5)), None) 27 | assert(Version(1,2,3,Some("RC4")).getNextVersion(vb, false))(equalTo(NextVersion(3,4,5,None))) && 28 | assert(Version(1,2,3,Some("RC4")).getNextVersion(vb, true))(equalTo(NextVersion(3,4,5,Some(1)))) && 29 | assert(Version(1,2,3,Some("RC")).getNextVersion(vb, true))(equalTo(NextVersion(3,4,5,Some(1)))) && 30 | assert(Version(1,2,3,None).getNextVersion(vb, true))(equalTo(NextVersion(3,4,5,Some(1)))) && 31 | assert(Version(1,2,3,None).getNextVersion(vb, false))(equalTo(NextVersion(3,4,5,None))) 32 | } 33 | ), 34 | suite("bump")( 35 | test("should bump correctly") { 36 | assert(Version(1,0,0).bump(VersionBumps(true, true, true, 0, Some(Version(3,0,0)), None), Some(6)))(equalTo(NextVersion(3,0,0,Some(6)))) && 37 | assert(Version(1,0,0).bump(VersionBumps(true, true, true, 0, None, Some(Version(4,0,0))), Some(6)))(equalTo(NextVersion(4,0,0,Some(6)))) && 38 | assert(Version(1,0,0).bump(VersionBumps(true, true, true, 0, Some(Version(3,0,0)), Some(Version(4,0,0))), Some(6)))(equalTo(NextVersion(4,0,0,Some(6)))) && 39 | assert(Version(1,0,0).bump(VersionBumps(true, true, true, 0), Some(6)))(equalTo(NextVersion(2,0,0,Some(6)))) && 40 | assert(Version(1,0,0).bump(VersionBumps(false, true, true, 0), Some(6)))(equalTo(NextVersion(1,1,0,Some(6)))) && 41 | assert(Version(1,0,0).bump(VersionBumps(false, false, true, 0), Some(6)))(equalTo(NextVersion(1,0,1,Some(6)))) && 42 | assert(Version(1,0,0).bump(VersionBumps(false, false, false, 0), Some(6)))(equalTo(NextVersion(1,0,0,Some(6)))) && 43 | assert(Version(1,2,3).bump(VersionBumps(true, true, true, 0), Some(6)))(equalTo(NextVersion(2,0,0,Some(6)))) && 44 | assert(Version(1,2,3).bump(VersionBumps(false, true, true, 0), Some(6)))(equalTo(NextVersion(1,3,0,Some(6)))) && 45 | assert(Version(1,2,3).bump(VersionBumps(false, false, true, 0), Some(6)))(equalTo(NextVersion(1,2,4,Some(6)))) && 46 | assert(Version(1,2,3).bump(VersionBumps(false, false, false, 0), Some(6)))(equalTo(NextVersion(1,2,3,Some(6)))) 47 | } 48 | ), 49 | suite("parseTag")( 50 | test("should parse Version correctly") { 51 | assert(Version.parseTag("v10.5.3", "v"))(equalTo(Some(Version(10, 5, 3, None, None)))) 52 | }, 53 | test("should parse VersionPreRelease correctly") { 54 | assert(Version.parseTag("v10.5.3-pre", "v"))(equalTo(Some(Version(10, 5, 3, Some("pre"), None)))) 55 | }, 56 | test("should parse VersionPreReleaseBuildMetaData correctly") { 57 | assert(Version.parseTag("v10.5.3-pre+build", "v"))(equalTo(Some(Version(10, 5, 3, Some("pre"), Some("build"))))) 58 | }, 59 | test("should parse VersionBuildMetaData correctly") { 60 | assert(Version.parseTag("v10.5.3+build", "v"))(equalTo(Some(Version(10, 5, 3, None, Some("build"))))) 61 | }, 62 | test("should parse VersionPreReleaseBuildMetaData with hyphen in PreRelease correctly") { 63 | assert(Version.parseTag("v10.5.3-pre-3+build", "v"))(equalTo(Some(Version(10, 5, 3, Some("pre-3"), Some("build"))))) 64 | }, 65 | test("should parse VersionPreReleaseBuildMetaData with hyphen in PreRelease and BuildMetaData correctly") { 66 | assert(Version.parseTag("v10.5.3-pre-3+build-1", "v"))(equalTo(Some(Version(10, 5, 3, Some("pre-3"), Some("build-1"))))) 67 | }, 68 | test("should parse VersionBuildMetaData with hyphen in BuildMetaData correctly") { 69 | assert(Version.parseTag("v10.5.3+build-1", "v"))(equalTo(Some(Version(10, 5, 3, None, Some("build-1"))))) 70 | } 71 | ), 72 | suite("ordering")( 73 | test("less than") { 74 | assert(Version.versionOrdering.lt(Version(0, 2, 3, None, None), Version(1, 2, 3, None, None)))(equalTo(true)) 75 | assert(Version.versionOrdering.lt(Version(1, 1, 3, None, None), Version(1, 2, 3, None, None)))(equalTo(true)) 76 | assert(Version.versionOrdering.lt(Version(1, 2, 2, None, None), Version(1, 2, 3, None, None)))(equalTo(true)) 77 | }, 78 | test("equal") { 79 | assert(Version.versionOrdering.lt(Version(1, 2, 3, None, None), Version(1, 2, 3, None, None)))(equalTo(false)) 80 | }, 81 | test("greater than") { 82 | assert(Version.versionOrdering.lt(Version(2, 2, 3, None, None), Version(1, 2, 3, None, None)))(equalTo(false)) 83 | assert(Version.versionOrdering.lt(Version(1, 3, 3, None, None), Version(1, 2, 3, None, None)))(equalTo(false)) 84 | assert(Version.versionOrdering.lt(Version(1, 2, 4, None, None), Version(1, 2, 3, None, None)))(equalTo(false)) 85 | }, 86 | test("pre-release vs non pre-release less than") { 87 | assert(Version.versionOrdering.lt(Version(1, 2, 3, Some("RC1"), None), Version(1, 2, 3, None, None)))(equalTo(true)) 88 | assert(Version.versionOrdering.lt(Version(1, 2, 3, None, None), Version(1, 2, 3, Some("RC1"), None)))(equalTo(false)) 89 | }, 90 | test("pre-release vs pre-release less than") { 91 | assert(Version.versionOrdering.lt(Version(1, 2, 3, Some("RC1"), None), Version(1, 2, 3, Some("RC2"), None)))(equalTo(true)) 92 | assert(Version.versionOrdering.lt(Version(1, 2, 3, Some("RC2"), None), Version(1, 2, 3, Some("RC1"), None)))(equalTo(false)) 93 | }, 94 | ) 95 | ) 96 | } 97 | --------------------------------------------------------------------------------