├── .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 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 | {% seo %}
15 |
16 |
17 |
18 |
32 |
33 |
34 |
35 |
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 |
--------------------------------------------------------------------------------