├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── build ├── debian │ └── control.template └── snap │ └── snapcraft.yaml ├── cmd └── gogitver │ ├── cmd │ └── root.go │ └── main.go ├── go.mod ├── go.sum └── pkg └── git ├── branchWalker.go ├── git.go ├── git_test.go ├── settings.go └── settings_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - uses: actions/checkout@v1 9 | with: 10 | ref: ${{ github.ref }} 11 | 12 | - name: Prepare repository 13 | run: git checkout "${GITHUB_REF:11}" 14 | 15 | - name: Install Go 16 | uses: actions/setup-go@v1 17 | with: 18 | go-version: 1.13.1 19 | 20 | - name: test 21 | run: make test 22 | 23 | release: 24 | runs-on: ubuntu-latest 25 | needs: test 26 | if: github.ref == 'refs/heads/master' 27 | steps: 28 | - uses: actions/checkout@v1 29 | with: 30 | ref: ${{ github.ref }} 31 | 32 | - name: Prepare repository 33 | run: git checkout "${GITHUB_REF:11}" 34 | 35 | - uses: syncromatics/gogitver-action@v0.0.2 36 | id: gogitver 37 | 38 | - name: Create Release 39 | id: create_release 40 | uses: actions/create-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | tag_name: ${{ steps.gogitver.outputs.version }} 45 | release_name: Release ${{ steps.gogitver.outputs.version }} 46 | draft: false 47 | prerelease: false 48 | 49 | - name: Install Go 50 | uses: actions/setup-go@v1 51 | with: 52 | go-version: 1.13.1 53 | 54 | - name: build 55 | run: make package 56 | 57 | - uses: actions/upload-release-asset@v1.0.1 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | with: 61 | upload_url: ${{ steps.create_release.outputs.upload_url }} 62 | asset_path: ./artifacts/linux.tar.gz 63 | asset_name: linux.tar.gz 64 | asset_content_type: application/gzip 65 | 66 | - uses: actions/upload-release-asset@v1.0.1 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | upload_url: ${{ steps.create_release.outputs.upload_url }} 71 | asset_path: ./artifacts/windows.zip 72 | asset_name: windows.zip 73 | asset_content_type: application/zip 74 | 75 | - uses: actions/upload-release-asset@v1.0.1 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | with: 79 | upload_url: ${{ steps.create_release.outputs.upload_url }} 80 | asset_path: ./artifacts/arm.tar.gz 81 | asset_name: arm.tar.gz 82 | asset_content_type: application/gzip 83 | 84 | - uses: actions/upload-release-asset@v1.0.1 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | with: 88 | upload_url: ${{ steps.create_release.outputs.upload_url }} 89 | asset_path: ./artifacts/darwin.tar.gz 90 | asset_name: darwin.tar.gz 91 | asset_content_type: application/gzip 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | artifacts/ 15 | .vscode/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joshua Benjamin 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_PATH := ./artifacts 2 | LINUX_BUILD_PATH = $(BUILD_PATH)/linux/gogitver 3 | LINUX_ARM_BUILD_PATH = $(BUILD_PATH)/arm/gogitver 4 | WINDOWS_BUILD_PATH = $(BUILD_PATH)/windows/gogitver.exe 5 | MAC_BUILD_PATH = $(BUILD_PATH)/darwin/gogitver 6 | 7 | .PHONY: clean 8 | clean: 9 | rm -Rf ./artifacts 10 | 11 | .PHONY: test 12 | test: 13 | go test -v ./... 14 | 15 | .PHONY: build 16 | build: clean test 17 | mkdir -p artifacts/linux artifacts/arm artifacts/windows artifacts/darwin 18 | GOOS=linux GOARCH=amd64 go build -o $(LINUX_BUILD_PATH) cmd/gogitver/main.go 19 | GOOS=linux GOARCH=arm go build -o $(LINUX_ARM_BUILD_PATH) cmd/gogitver/main.go 20 | GOOS=darwin GOARCH=amd64 go build -o $(MAC_BUILD_PATH) cmd/gogitver/main.go 21 | GOOS=windows GOARCH=amd64 go build -o $(WINDOWS_BUILD_PATH) cmd/gogitver/main.go 22 | 23 | .PHONY: build-debian-package 24 | build-debian-package: build 25 | mkdir -p artifacts/debian/DEBIAN 26 | mkdir -p artifacts/debian/usr/local/bin/ 27 | cat ./build/debian/control.template | envsubst > ./artifacts/debian/DEBIAN/control 28 | cp $(LINUX_BUILD_PATH) artifacts/debian/usr/local/bin/gogitver 29 | dpkg-deb --build ./artifacts/debian ./artifacts/gogitver_amd64.deb 30 | rm -R ./artifacts/debian 31 | 32 | .PHONY: build-snap 33 | build-snap: build 34 | mkdir -p artifacts/snap/snap 35 | mkdir -p artifacts/snap/source 36 | cp $(LINUX_BUILD_PATH) artifacts/snap/source/gogitver 37 | cat ./build/snap/snapcraft.yaml | envsubst > ./artifacts/snap/snap/snapcraft.yaml 38 | cd ./artifacts/snap && snapcraft clean gogitver -s pull 39 | cd ./artifacts/snap && snapcraft 40 | mv ./artifacts/snap/gogitver*.snap ./artifacts 41 | rm -R ./artifacts/snap 42 | 43 | package: build 44 | cd $(BUILD_PATH)/darwin && tar -zcvf ../darwin.tar.gz * 45 | cd $(BUILD_PATH)/linux && tar -zcvf ../linux.tar.gz * 46 | cd $(BUILD_PATH)/arm && tar -zcvf ../arm.tar.gz * 47 | cd $(BUILD_PATH)/windows && zip -r ../windows.zip * 48 | rm -R $(BUILD_PATH)/darwin $(BUILD_PATH)/linux $(BUILD_PATH)/arm $(BUILD_PATH)/windows 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gogitver 2 | 3 | gogitver is a tool to determine the semantic version of a project based on keywords used in the commit history. This project draws a lot of inspiration from [GitVersion](https://github.com/GitTools/GitVersion) but with the benefit of go's single binary executable. With the work done by go-git the binary produced can run on Linux, Windows, and Mac. 4 | 5 | ## Getting Started 6 | 7 | ### Installing 8 | 9 | #### Snap 10 | ```snap install gogitver``` 11 | 12 | #### Binaries 13 | 14 | To install download the latest release from the [releases](https://github.com/syncromatics/gogitver/releases) page for your machine architecture and place the binary in your path. You can then run the executable while in the path of your project and it should output the current version. You can then use this version to tag container images, helm charts, etc. 15 | 16 | ### Usage 17 | 18 | To get this most out of this tool you should be adding keywords to your git commits. 19 | 20 | Example: 21 | ```git commit -m "(+semver: breaking) this change adds a breaking change to the public api"``` 22 | 23 | When gogitver sees this commit in the git history it will bump the major version. 24 | 25 | The default regex patterns gogitver will use are: 26 | 27 | * Major: ```\+semver:\s?(breaking|major)``` 28 | * Minor: ```\+semver:\s?(feature|minor)``` 29 | * Patch: ```\+semver:\s?(fix|patch)``` 30 | 31 | However you can override these by providing a settings file ```.gogitver.yaml``` that looks like: 32 | 33 | ```yaml 34 | major-version-bump-message: '(major|breaking)\(.*\)' 35 | minor-version-bump-message: '(feat|feature|minor)\(.*\)' 36 | patch-version-bump-message: '(patch|fix)\(.*\)' 37 | ``` 38 | 39 | You can also override the name and location of this file by providing the settings flag ```gotgitver --settings=./anotherfile.yaml``` 40 | 41 | ## Development 42 | 43 | ### Requirements 44 | 45 | This project requires at least [Go](https://golang.org/dl/) 1.11 because it makes use of go modules for dependencies. 46 | 47 | ### Building 48 | 49 | To build the project simply run ```make build``` which will generate the binaries and put them in the artifacts folder. 50 | 51 | ## Built With 52 | 53 | * [go-git](https://github.com/src-d/go-git) - The git interface 54 | 55 | ## License 56 | 57 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 58 | 59 | ## Acknowledgments 60 | 61 | * [go-git](https://github.com/src-d/go-git) for allowing interactions with git to be easy and without dependencies 62 | * [GitVersion](https://github.com/GitTools/GitVersion) for the inspiration 63 | * [Visual Studio Code](https://code.visualstudio.com/) for just being an all around great editor 64 | -------------------------------------------------------------------------------- /build/debian/control.template: -------------------------------------------------------------------------------- 1 | Package: gogitver 2 | Version: ${VERSION} 3 | Maintainer: Joshua Benjamin 4 | Architecture: amd64 5 | Description: gogitver is a tool to determine the semantic version of a project based on keywords used in the commit history. 6 | -------------------------------------------------------------------------------- /build/snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: gogitver 2 | version: '${VERSION}' 3 | summary: Provides semantic versioning through git history 4 | description: | 5 | gogitver is a tool to determine the semantic version of a 6 | project based on keywords used in the commit history. gogitver 7 | draws a lot of inspiration from GitVersion but with the benefit 8 | of go's single binary executable. With the work done by go-git 9 | the binary produced can run on Linux, Windows, and Mac. 10 | 11 | grade: stable 12 | confinement: strict 13 | 14 | architectures: 15 | - build-on: amd64 16 | 17 | parts: 18 | gogitver: 19 | source: ./source 20 | plugin: dump 21 | 22 | apps: 23 | gogitver: 24 | command: gogitver 25 | plugs: [home] 26 | -------------------------------------------------------------------------------- /cmd/gogitver/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/syncromatics/gogitver/pkg/git" 10 | "github.com/pkg/errors" 11 | "github.com/spf13/cobra" 12 | 13 | gogit "gopkg.in/src-d/go-git.v4" 14 | ) 15 | 16 | var rootCmd = &cobra.Command{ 17 | Use: "gogitver", 18 | Short: "gogitver is a semver generator that uses git history", 19 | Long: ``, 20 | Run: runRoot, 21 | } 22 | 23 | var prereleaseCmd = &cobra.Command{ 24 | Use: "label", 25 | Short: "Gets the prerelease label, if any", 26 | Long: ``, 27 | Run: runPrerelease, 28 | } 29 | 30 | func init() { 31 | var cmds = [2]*cobra.Command{rootCmd, prereleaseCmd} 32 | for _, cmd := range cmds { 33 | cmd.Flags().String("path", ".", "the path to the git repository") 34 | cmd.Flags().String("settings", "./.gogitver.yaml", "the file that contains the settings") 35 | cmd.Flags().Bool("trim-branch-prefix", false, "Trim branch prefixes feature/ and hotfix/ from prerelease label") 36 | cmd.Flags().BoolP("verbose", "v", false, "Show information about how the version was calculated") 37 | } 38 | 39 | rootCmd.Flags().Bool("forbid-behind-master", false, "error if the current branch's calculated version is behind the calculated version of refs/heads/master") 40 | 41 | rootCmd.AddCommand(prereleaseCmd) 42 | } 43 | 44 | // Execute gogitver 45 | func Execute() { 46 | if err := rootCmd.Execute(); err != nil { 47 | fmt.Println(err) 48 | os.Exit(1) 49 | } 50 | } 51 | 52 | func getRepoAndSettings(cmd *cobra.Command) (*gogit.Repository, *git.Settings) { 53 | f := cmd.Flag("path") 54 | sf := cmd.Flag("settings") 55 | 56 | var s *git.Settings 57 | _, err := os.Stat(sf.Value.String()) 58 | if sf.Changed || err == nil { 59 | r, err := os.Open(sf.Value.String()) 60 | if err != nil { 61 | panic(errors.Wrap(err, "cannot open settings file")) 62 | } 63 | 64 | s, err = git.GetSettingsFromFile(r) 65 | if err != nil { 66 | panic(err) 67 | } 68 | } else { 69 | s = git.GetDefaultSettings() 70 | } 71 | 72 | r, err := gogit.PlainOpen(f.Value.String()) 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | return r, s 78 | } 79 | 80 | func getBoolFromFlag(cmd *cobra.Command, flagName string) bool { 81 | result, err := strconv.ParseBool(cmd.Flag(flagName).Value.String()) 82 | if err != nil { 83 | result = false 84 | } 85 | return result 86 | } 87 | 88 | func getBranchSettings(cmd *cobra.Command) *git.BranchSettings { 89 | fbm := getBoolFromFlag(cmd, "forbid-behind-master") 90 | trimPrefix := getBoolFromFlag(cmd, "trim-branch-prefix") 91 | return &git.BranchSettings{ 92 | ForbidBehindMaster: fbm, 93 | TrimBranchPrefix: trimPrefix, 94 | } 95 | } 96 | 97 | func runRoot(cmd *cobra.Command, args []string) { 98 | r, s := getRepoAndSettings(cmd) 99 | v := getBoolFromFlag(cmd, "verbose") 100 | 101 | if v { 102 | log.SetFlags(0) 103 | } 104 | 105 | branchSettings := getBranchSettings(cmd) 106 | version, err := git.GetCurrentVersion(r, s, branchSettings, v) 107 | if err != nil { 108 | panic(err) 109 | } 110 | 111 | fmt.Println(version) 112 | } 113 | 114 | func runPrerelease(cmd *cobra.Command, args []string) { 115 | r, s := getRepoAndSettings(cmd) 116 | trimPrefix := getBoolFromFlag(cmd, "trim-branch-prefix") 117 | branchSettings := &git.BranchSettings{ 118 | TrimBranchPrefix: trimPrefix, 119 | } 120 | 121 | label, err := git.GetPrereleaseLabel(r, s, branchSettings) 122 | if err != nil { 123 | panic(err) 124 | } 125 | 126 | if label == "master" { 127 | label = "" 128 | } 129 | 130 | fmt.Println(label) 131 | } 132 | -------------------------------------------------------------------------------- /cmd/gogitver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/syncromatics/gogitver/cmd/gogitver/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syncromatics/gogitver 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/coreos/go-semver v0.2.0 7 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 8 | github.com/pkg/errors v0.8.0 9 | github.com/spf13/cobra v0.0.3 10 | github.com/spf13/pflag v1.0.1 // indirect 11 | github.com/stretchr/testify v1.2.2 12 | gopkg.in/src-d/go-billy.v4 v4.2.1 13 | gopkg.in/src-d/go-git.v4 v4.7.1 14 | gopkg.in/yaml.v2 v2.2.1 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 2 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 3 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 4 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 5 | github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= 6 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo= 10 | github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 11 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 12 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 13 | github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw= 14 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 15 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 16 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 17 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 18 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 19 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 20 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 21 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 22 | github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8= 23 | github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 24 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 25 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= 30 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 31 | github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= 32 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 33 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 34 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 38 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 39 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 40 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 41 | github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= 42 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 43 | github.com/src-d/gcfg v1.3.0 h1:2BEDr8r0I0b8h/fOqwtxCEiq2HJu8n2JGZJQFGXWLjg= 44 | github.com/src-d/gcfg v1.3.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 45 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 46 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 47 | github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro= 48 | github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= 49 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= 50 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 51 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 52 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 53 | golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9 h1:lkiLiLBHGoH3XnqSLUIaBsilGMUjI+Uy2Xu2JLUtTas= 54 | golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 55 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 56 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 59 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/src-d/go-billy.v4 v4.2.1 h1:omN5CrMrMcQ+4I8bJ0wEhOBPanIRWzFC953IiXKdYzo= 61 | gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= 62 | gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs= 63 | gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 64 | gopkg.in/src-d/go-git.v4 v4.7.1 h1:phAV/kNULxfYEvyInGdPuq3U2MtPpJdgmtOUF3cghkQ= 65 | gopkg.in/src-d/go-git.v4 v4.7.1/go.mod h1:xrJH/YX8uSWewT6evfocf8qsivF18JgCN7/IMitOptY= 66 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 67 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 68 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 69 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 70 | -------------------------------------------------------------------------------- /pkg/git/branchWalker.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "log" 5 | "regexp" 6 | "strings" 7 | 8 | "gopkg.in/src-d/go-git.v4/plumbing" 9 | 10 | "github.com/coreos/go-semver/semver" 11 | "github.com/pkg/errors" 12 | "gopkg.in/src-d/go-git.v4" 13 | "gopkg.in/src-d/go-git.v4/plumbing/object" 14 | ) 15 | 16 | type branchWalker struct { 17 | repository *git.Repository 18 | head *object.Commit 19 | tagMap map[string]string 20 | settings *Settings 21 | isMaster bool 22 | endHash string 23 | verbose bool 24 | 25 | visited map[string]bool 26 | commitsToReconcile map[string]*gitVersion 27 | } 28 | 29 | type versionHolder struct { 30 | versionMap []*gitVersion 31 | } 32 | 33 | func newBranchWalker(repository *git.Repository, head *object.Commit, tagMap map[string]string, settings *Settings, isMaster bool, endHash string, verbose bool) *branchWalker { 34 | return &branchWalker{ 35 | repository: repository, 36 | head: head, 37 | settings: settings, 38 | tagMap: tagMap, 39 | isMaster: isMaster, 40 | endHash: endHash, 41 | visited: make(map[string]bool), 42 | commitsToReconcile: make(map[string]*gitVersion), 43 | verbose: verbose, 44 | } 45 | } 46 | 47 | func (b *branchWalker) GetVersion() (*semver.Version, error) { 48 | versionMap, err := b.GetVersionMap() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | var baseVersion *semver.Version 54 | index := len(versionMap) - 1 55 | v := versionMap[index] 56 | if v.IsSolid { 57 | baseVersion = v.Name 58 | index-- 59 | } else { 60 | baseVersion, err = semver.NewVersion("0.0.0") 61 | if err != nil { 62 | return nil, err 63 | } 64 | } 65 | 66 | if index < 0 { 67 | return baseVersion, nil 68 | } 69 | 70 | if b.verbose { 71 | log.Printf("[%s] %s", v.Commit, baseVersion.String()) 72 | } 73 | 74 | for ; index >= 0; index-- { 75 | v := versionMap[index] 76 | switch { 77 | case v.MajorBump: 78 | baseVersion.BumpMajor() 79 | case v.MinorBump: 80 | baseVersion.BumpMinor() 81 | case v.PatchBump: 82 | baseVersion.BumpPatch() 83 | default: // every commit in master has at least a patch bump 84 | baseVersion.BumpPatch() 85 | } 86 | if b.verbose { 87 | log.Printf("[%s] %s", v.Commit, baseVersion.String()) 88 | } 89 | } 90 | 91 | return baseVersion, nil 92 | } 93 | 94 | func (b *branchWalker) GetVersionMap() ([]*gitVersion, error) { 95 | versionMap := versionHolder{ 96 | versionMap: []*gitVersion{}, 97 | } 98 | 99 | err := b.walkVersion(b.head, &versionMap, false) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | if b.isMaster { 105 | for hash, version := range b.commitsToReconcile { 106 | err = b.reconcileCommit(hash, version) 107 | if err != nil { 108 | return nil, err 109 | } 110 | } 111 | } 112 | 113 | return versionMap.versionMap, nil 114 | } 115 | 116 | func (b *branchWalker) walkVersion(ref *object.Commit, version *versionHolder, tilVisited bool) error { 117 | if _, visited := b.visited[ref.Hash.String()]; tilVisited && visited { 118 | return nil 119 | } 120 | 121 | b.visited[ref.Hash.String()] = true 122 | 123 | tag, ok := b.tagMap[ref.Hash.String()] 124 | if ok { 125 | tagVersion, err := parseTag(tag) 126 | if err != nil { 127 | return err 128 | } 129 | version.versionMap = append(version.versionMap, &gitVersion{IsSolid: true, Name: tagVersion, Commit: ref.Hash.String()}) 130 | return nil 131 | } 132 | 133 | parents := ref.NumParents() 134 | if parents > 1 { 135 | versionToReconcile := gitVersion{IsSolid: false, Commit: ref.Hash.String()} 136 | version.versionMap = append(version.versionMap, &versionToReconcile) 137 | 138 | b.commitsToReconcile[ref.Hash.String()] = &versionToReconcile 139 | return b.checkWalkParent(ref, version, tilVisited) 140 | } 141 | 142 | matched, err := regexp.MatchString(b.settings.MajorPattern, ref.Message) 143 | if err != nil { 144 | return err 145 | } 146 | if matched { 147 | version.versionMap = append(version.versionMap, &gitVersion{IsSolid: false, MajorBump: true, Commit: ref.Hash.String()}) 148 | return b.checkWalkParent(ref, version, tilVisited) 149 | } 150 | 151 | matched, err = regexp.MatchString(b.settings.MinorPattern, ref.Message) 152 | if err != nil { 153 | return err 154 | } 155 | if matched { 156 | version.versionMap = append(version.versionMap, &gitVersion{IsSolid: false, MinorBump: true, Commit: ref.Hash.String()}) 157 | return b.checkWalkParent(ref, version, tilVisited) 158 | } 159 | 160 | matched, err = regexp.MatchString(b.settings.PatchPattern, ref.Message) 161 | if err != nil { 162 | return err 163 | } 164 | if matched { 165 | version.versionMap = append(version.versionMap, &gitVersion{IsSolid: false, PatchBump: true, Commit: ref.Hash.String()}) 166 | return b.checkWalkParent(ref, version, tilVisited) 167 | } 168 | 169 | version.versionMap = append(version.versionMap, &gitVersion{IsSolid: false, Commit: ref.Hash.String()}) 170 | return b.checkWalkParent(ref, version, tilVisited) 171 | } 172 | 173 | func (b *branchWalker) checkWalkParent(ref *object.Commit, version *versionHolder, tilVisited bool) error { 174 | parents := ref.NumParents() 175 | if parents == 0 { 176 | return nil 177 | } 178 | 179 | parent, err := ref.Parent(0) 180 | if err != nil { 181 | return nil 182 | } 183 | 184 | if parent.Hash.String() == b.endHash { 185 | return nil 186 | } 187 | 188 | return b.walkVersion(parent, version, tilVisited) 189 | } 190 | 191 | func (b *branchWalker) reconcileCommit(hash string, version *gitVersion) error { 192 | commit, err := b.repository.CommitObject(plumbing.NewHash(hash)) 193 | if err != nil { 194 | return errors.Wrap(err, "failed to get commit in reconcile") 195 | } 196 | 197 | numParents := commit.NumParents() 198 | if numParents <= 1 { 199 | return nil 200 | } 201 | 202 | versionMap := versionHolder{ 203 | versionMap: []*gitVersion{}, 204 | } 205 | for i := 1; i < numParents; i++ { 206 | parentToWalk, err := commit.Parent(i) 207 | if err != nil { 208 | return errors.Wrap(err, "failed to get parent in reconcile") 209 | } 210 | 211 | err = b.walkVersion(parentToWalk, &versionMap, true) 212 | if err != nil { 213 | return err 214 | } 215 | } 216 | 217 | var hasMajor, hasMinor bool 218 | for _, bump := range versionMap.versionMap { 219 | if bump.MajorBump { 220 | hasMajor = true 221 | } 222 | if bump.MinorBump { 223 | hasMinor = true 224 | } 225 | } 226 | 227 | if hasMajor { 228 | version.MajorBump = true 229 | } else if hasMinor { 230 | version.MinorBump = true 231 | } else { 232 | version.PatchBump = true 233 | } 234 | 235 | return nil 236 | } 237 | 238 | func parseTag(tag string) (*semver.Version, error) { // TODO: Support ignoring invalid semver tags 239 | trimmedTag := strings.TrimPrefix(tag, "v") 240 | version, err := semver.NewVersion(trimmedTag) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | return version, nil 246 | } 247 | -------------------------------------------------------------------------------- /pkg/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/coreos/go-semver/semver" 11 | "github.com/pkg/errors" 12 | git "gopkg.in/src-d/go-git.v4" 13 | "gopkg.in/src-d/go-git.v4/plumbing" 14 | "gopkg.in/src-d/go-git.v4/plumbing/object" 15 | ) 16 | 17 | // BranchSettings contains flags that determine how branches are handled when calculating versions. 18 | type BranchSettings struct { 19 | ForbidBehindMaster bool 20 | TrimBranchPrefix bool 21 | IgnoreEnvVars bool 22 | } 23 | 24 | type gitVersion struct { 25 | IsSolid bool 26 | Name *semver.Version 27 | 28 | MajorBump bool 29 | MinorBump bool 30 | PatchBump bool 31 | Commit string 32 | } 33 | 34 | // GetCurrentVersion returns the current version 35 | func GetCurrentVersion(r *git.Repository, settings *Settings, branchSettings *BranchSettings, verbose bool) (version string, err error) { 36 | tag, ok := os.LookupEnv("TRAVIS_TAG") 37 | if !branchSettings.IgnoreEnvVars && ok && tag != "" { // If this is a tagged build in travis shortcircuit here 38 | version, err := parseTag(tag) 39 | if err != nil { 40 | return "", err 41 | } 42 | if verbose { 43 | log.Printf("Version determined using TRAVIS_TAG") 44 | } 45 | return version.String(), err 46 | } 47 | 48 | tagMap := make(map[string]string) 49 | 50 | // lightweight tags 51 | ltags, err := r.Tags() 52 | if err != nil { 53 | return "", errors.Wrap(err, "get tags failed") 54 | } 55 | 56 | err = ltags.ForEach(func(ref *plumbing.Reference) error { 57 | name := ref.Name().String() 58 | tag := strings.Replace(name, "refs/tags/", "", -1) 59 | if verbose { 60 | log.Printf("Found lightweight tag %s for ref %s", tag, name) 61 | } 62 | tagMap[ref.Hash().String()] = tag 63 | return nil 64 | }) 65 | 66 | // annotated tags 67 | tags, err := r.TagObjects() 68 | if err != nil { 69 | return "", errors.Wrap(err, "get tag objects failed") 70 | } 71 | 72 | err = tags.ForEach(func(ref *object.Tag) error { 73 | c, err := ref.Commit() 74 | if err != nil { 75 | return errors.Wrap(err, "get commit failed") 76 | } 77 | if verbose { 78 | log.Printf("Found tag %s", ref.Name) 79 | } 80 | tagMap[c.Hash.String()] = ref.Name 81 | return nil 82 | }) 83 | if err != nil { 84 | return "", errors.Wrap(err, "GetCurrentVersion failed") 85 | } 86 | 87 | h, err := r.Head() 88 | if err != nil { 89 | return "", errors.Wrap(err, "GetCurrentVersion failed") 90 | } 91 | 92 | v, err := getVersion(r, h, tagMap, branchSettings, settings, verbose) 93 | if err != nil { 94 | return "", errors.Wrap(err, "GetCurrentVersion failed") 95 | } 96 | 97 | return v.String(), nil 98 | } 99 | 100 | // GetPrereleaseLabel returns the prerelease label for the current branch 101 | func GetPrereleaseLabel(r *git.Repository, settings *Settings, branchSettings *BranchSettings) (result string, err error) { 102 | h, err := r.Head() 103 | if err != nil { 104 | return "", errors.Wrap(err, "GetCurrentVersion failed") 105 | } 106 | return getCurrentBranch(r, h, branchSettings) 107 | } 108 | 109 | func getVersion(r *git.Repository, h *plumbing.Reference, tagMap map[string]string, branchSettings *BranchSettings, settings *Settings, verbose bool) (version *semver.Version, err error) { 110 | currentBranch, err := getCurrentBranch(r, h, branchSettings) 111 | if err != nil { 112 | return nil, errors.Wrap(err, "getVersion failed") 113 | } 114 | if verbose { 115 | log.Printf("Current branch is %s", currentBranch) 116 | } 117 | 118 | masterHead, err := r.Reference("refs/heads/master", false) 119 | if err != nil { 120 | masterHead, err = r.Reference("refs/remotes/origin/master", false) // TODO: This needs test coverage 121 | if err != nil { 122 | return nil, errors.Wrap(err, "failed to get master branch at 'refs/heads/master, 'refs/remotes/origin/master'") 123 | } 124 | } 125 | 126 | masterCommit, err := r.CommitObject(masterHead.Hash()) 127 | if err != nil { 128 | return nil, errors.Wrap(err, "failed to get master commit from reference") 129 | } 130 | 131 | masterWalker := newBranchWalker(r, masterCommit, tagMap, settings, true, "", verbose) 132 | masterVersion, err := masterWalker.GetVersion() 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | if h.Hash() == masterHead.Hash() { 138 | return masterVersion, nil 139 | } 140 | 141 | c, err := r.CommitObject(h.Hash()) 142 | if err != nil { 143 | return nil, errors.Wrap(err, "getVersion failed") 144 | } 145 | 146 | walker := newBranchWalker(r, c, tagMap, settings, false, masterHead.Hash().String(), verbose) 147 | versionMap, err := walker.GetVersionMap() 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | var baseVersion *semver.Version 153 | index := len(versionMap) - 1 154 | if index == -1 { 155 | return nil, errors.Errorf("Cannot determine version in branch") 156 | } 157 | 158 | if versionMap[index].IsSolid { 159 | baseVersion = versionMap[index].Name 160 | index-- 161 | } else { 162 | baseVersion = masterVersion 163 | } 164 | 165 | if index < 0 { 166 | return baseVersion, nil 167 | } 168 | 169 | for ; index >= 0; index-- { 170 | v := versionMap[index] 171 | switch { 172 | case v.MajorBump: 173 | baseVersion.BumpMajor() 174 | case v.MinorBump: 175 | baseVersion.BumpMinor() 176 | case v.PatchBump: 177 | baseVersion.BumpPatch() 178 | } 179 | } 180 | 181 | shortHash := h.Hash().String()[:4] 182 | prerelease := fmt.Sprintf("%s-%d-%s", currentBranch, len(versionMap)-1, shortHash) 183 | baseVersion.PreRelease = semver.PreRelease(prerelease) 184 | 185 | if branchSettings.ForbidBehindMaster && baseVersion.LessThan(*masterVersion) { 186 | return nil, errors.Errorf("Branch has calculated version '%s' whose version is less than master '%s'", baseVersion, masterVersion) 187 | } 188 | 189 | return baseVersion, nil 190 | } 191 | 192 | func getCurrentBranch(r *git.Repository, h *plumbing.Reference, branchSettings *BranchSettings) (name string, err error) { 193 | branchName := "" 194 | 195 | if !branchSettings.IgnoreEnvVars { 196 | name, ok := os.LookupEnv("TRAVIS_PULL_REQUEST_BRANCH") // Travis 197 | if ok { 198 | branchName, err := cleanseBranchName(name, branchSettings.TrimBranchPrefix) 199 | if err != nil { 200 | return "", err 201 | } 202 | return branchName, nil 203 | } 204 | 205 | name, ok = os.LookupEnv("TRAVIS_BRANCH") 206 | if ok { 207 | branchName, err := cleanseBranchName(name, branchSettings.TrimBranchPrefix) 208 | if err != nil { 209 | return "", err 210 | } 211 | return branchName, nil 212 | } 213 | 214 | name, ok = os.LookupEnv("CI_COMMIT_REF_NAME") // GitLab 215 | if ok { 216 | branchName, err := cleanseBranchName(name, branchSettings.TrimBranchPrefix) 217 | if err != nil { 218 | return "", err 219 | } 220 | return branchName, nil 221 | } 222 | } 223 | 224 | refs, err := r.References() 225 | if err != nil { 226 | return "", err 227 | } 228 | err = refs.ForEach(func(r *plumbing.Reference) error { 229 | if r.Name().IsBranch() && r.Hash() == h.Hash() { 230 | branchName = r.Name().Short() 231 | } 232 | return nil 233 | }) 234 | if err != nil { 235 | return "", err 236 | } 237 | 238 | if branchName == "" { 239 | return "", fmt.Errorf("Cannot determine branch") 240 | } 241 | 242 | branch, err := cleanseBranchName(branchName, branchSettings.TrimBranchPrefix) 243 | if err != nil { 244 | return "", err 245 | } 246 | return branch, nil 247 | } 248 | 249 | func cleanseBranchName(name string, trimPrefix bool) (string, error) { 250 | reg, err := regexp.Compile("[^a-zA-Z0-9]+") 251 | if err != nil { 252 | return "", err 253 | } 254 | 255 | branchName := reg.ReplaceAllString(name, "-") 256 | if !trimPrefix { 257 | return branchName, nil 258 | } 259 | 260 | reg, err = regexp.Compile("^(feature|hotfix)-") 261 | if err != nil { 262 | return "", err 263 | } 264 | 265 | branchName = reg.ReplaceAllString(branchName, "") 266 | return branchName, nil 267 | } 268 | -------------------------------------------------------------------------------- /pkg/git/git_test.go: -------------------------------------------------------------------------------- 1 | package git_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "gopkg.in/src-d/go-billy.v4/memfs" 12 | "gopkg.in/src-d/go-billy.v4/util" 13 | git "gopkg.in/src-d/go-git.v4" 14 | "gopkg.in/src-d/go-git.v4/plumbing" 15 | "gopkg.in/src-d/go-git.v4/plumbing/object" 16 | "gopkg.in/src-d/go-git.v4/storage/memory" 17 | 18 | igit "github.com/syncromatics/gogitver/pkg/git" 19 | ) 20 | 21 | func Test_ShouldCalculateVersionFromCommitsInMaster(t *testing.T) { 22 | // Arrange 23 | repository, worktree := initRepository(t) 24 | 25 | commitMultiple(t, worktree, 26 | "(+semver: breaking) This is a major commit\n", 27 | "(+semver: major) This is also a major commit\n", 28 | "(+semver: feature) This is a minor commit\n", 29 | "(+semver: minor) This is also a minor commit\n", 30 | "(+semver: fix) This is a patch commit\n", 31 | "(+semver: patch) This is also a patch commit\n", 32 | "This is also a patch commit\n", 33 | ) 34 | 35 | settings := igit.GetDefaultSettings() 36 | branchSettings := &igit.BranchSettings{ 37 | IgnoreEnvVars: true, 38 | } 39 | 40 | // Act 41 | version, err := igit.GetCurrentVersion(repository, settings, branchSettings, false) 42 | assert.Nil(t, err) 43 | 44 | // Assert 45 | assert.Equal(t, "2.2.3", version) 46 | } 47 | 48 | func Test_ShouldCalculateVersionFromCommitsInBranch(t *testing.T) { 49 | // Arrange 50 | repository, worktree := initRepository(t) 51 | 52 | commitMultiple(t, worktree, "Initial commit") 53 | 54 | err := worktree.Checkout(&git.CheckoutOptions{ 55 | Create: true, 56 | Branch: plumbing.ReferenceName("refs/heads/a-branch"), 57 | }) 58 | assert.Nil(t, err) 59 | 60 | hash := commitMultiple(t, worktree, 61 | "(+semver: major)\n", 62 | "(+semver: minor)\n", 63 | "(+semver: patch)\n", 64 | "some text\n", 65 | ) 66 | 67 | settings := igit.GetDefaultSettings() 68 | branchSettings := &igit.BranchSettings{ 69 | IgnoreEnvVars: true, 70 | } 71 | 72 | // Act 73 | version, err := igit.GetCurrentVersion(repository, settings, branchSettings, false) 74 | assert.Nil(t, err) 75 | 76 | // Assert 77 | shortHash := hash.String()[0:4] 78 | expected := fmt.Sprintf("1.1.1-a-branch-3-%s", shortHash) 79 | assert.Equal(t, expected, version) 80 | } 81 | 82 | func Test_ShouldCalculateVersionFromCommitsInMasterWithMergeCommits(t *testing.T) { 83 | // Arrange 84 | repository, worktree := initRepository(t) 85 | 86 | masterHash := commitMultiple(t, worktree, "Initial commit") 87 | 88 | err := worktree.Checkout(&git.CheckoutOptions{ 89 | Create: true, 90 | Branch: plumbing.ReferenceName("refs/heads/a-branch"), 91 | }) 92 | assert.Nil(t, err) 93 | 94 | branchHash := commitMultiple(t, worktree, 95 | "(+semver: major)\n", 96 | "(+semver: minor)\n", 97 | "(+semver: patch)\n", 98 | "some text\n", 99 | ) 100 | 101 | err = worktree.Checkout(&git.CheckoutOptions{ 102 | Branch: plumbing.ReferenceName("refs/heads/master"), 103 | }) 104 | assert.Nil(t, err) 105 | 106 | masterHash, err = worktree.Commit("merged a-branch\n", &git.CommitOptions{ 107 | Author: defaultSignature(), 108 | Parents: []plumbing.Hash{ 109 | masterHash, 110 | branchHash, 111 | }, 112 | }) 113 | assert.Nil(t, err) 114 | 115 | err = worktree.Checkout(&git.CheckoutOptions{ 116 | Create: true, 117 | Branch: plumbing.ReferenceName("refs/heads/another-branch"), 118 | }) 119 | assert.Nil(t, err) 120 | 121 | branchHash = commitMultiple(t, worktree, 122 | "(+semver: minor)\n", 123 | "(+semver: patch)\n", 124 | ) 125 | 126 | err = worktree.Checkout(&git.CheckoutOptions{ 127 | Branch: plumbing.ReferenceName("refs/heads/master"), 128 | }) 129 | assert.Nil(t, err) 130 | 131 | masterHash, err = worktree.Commit("merged another-branch\n", &git.CommitOptions{ 132 | Author: defaultSignature(), 133 | Parents: []plumbing.Hash{ 134 | masterHash, 135 | branchHash, 136 | }, 137 | }) 138 | assert.Nil(t, err) 139 | 140 | err = worktree.Checkout(&git.CheckoutOptions{ 141 | Create: true, 142 | Branch: plumbing.ReferenceName("refs/heads/yet-another-branch"), 143 | }) 144 | assert.Nil(t, err) 145 | 146 | branchHash = commitMultiple(t, worktree, 147 | "(+semver: patch)\n", 148 | "(+semver: patch)\n", 149 | "(+semver: patch)\n", 150 | ) 151 | 152 | err = worktree.Checkout(&git.CheckoutOptions{ 153 | Branch: plumbing.ReferenceName("refs/heads/master"), 154 | }) 155 | assert.Nil(t, err) 156 | 157 | _, err = worktree.Commit("merged yet-another-branch\n", &git.CommitOptions{ 158 | Author: defaultSignature(), 159 | Parents: []plumbing.Hash{ 160 | masterHash, 161 | branchHash, 162 | }, 163 | }) 164 | assert.Nil(t, err) 165 | 166 | settings := igit.GetDefaultSettings() 167 | branchSettings := &igit.BranchSettings{ 168 | IgnoreEnvVars: true, 169 | } 170 | 171 | // Act 172 | version, err := igit.GetCurrentVersion(repository, settings, branchSettings, false) 173 | assert.Nil(t, err) 174 | 175 | // Assert 176 | assert.Equal(t, "1.1.1", version) 177 | } 178 | 179 | func Test_ShouldCalculateVersionFromLightweightTag(t *testing.T) { 180 | // Arrange 181 | repository, worktree := initRepository(t) 182 | 183 | hash := commitMultiple(t, worktree, 184 | "(+semver: breaking)\n", 185 | "(+semver: breaking)\n", 186 | ) 187 | 188 | ref := plumbing.NewHashReference(plumbing.ReferenceName("refs/tags/v1.2.3"), hash) 189 | err := repository.Storer.SetReference(ref) 190 | assert.Nil(t, err) 191 | 192 | commitMultiple(t, worktree, 193 | "(+semver: minor)\n", 194 | "(+semver: patch)\n", 195 | ) 196 | 197 | settings := igit.GetDefaultSettings() 198 | branchSettings := &igit.BranchSettings{ 199 | IgnoreEnvVars: true, 200 | } 201 | 202 | // Act 203 | version, err := igit.GetCurrentVersion(repository, settings, branchSettings, false) 204 | assert.Nil(t, err) 205 | 206 | // Assert 207 | assert.Equal(t, "1.3.1", version) 208 | } 209 | 210 | func Test_ShouldFailToCalculateVersionFromImproperlyNamedLightweightTag(t *testing.T) { // TODO: Submit a PR that allows this condition to exist; ignore unparsable tags 211 | // Arrange 212 | repository, worktree := initRepository(t) 213 | 214 | hash := commitMultiple(t, worktree, "Initial commit") 215 | 216 | ref := plumbing.NewHashReference(plumbing.ReferenceName("refs/tags/an-arbitrary-tag-name"), hash) 217 | err := repository.Storer.SetReference(ref) 218 | assert.Nil(t, err) 219 | 220 | settings := igit.GetDefaultSettings() 221 | branchSettings := &igit.BranchSettings{ 222 | IgnoreEnvVars: true, 223 | } 224 | 225 | // Act 226 | _, err = igit.GetCurrentVersion(repository, settings, branchSettings, false) 227 | 228 | // Assert 229 | assert.NotNil(t, err) 230 | } 231 | 232 | func Test_ShouldCalculateVersionFromAnnotatedTag(t *testing.T) { 233 | // Arrange 234 | repository, worktree := initRepository(t) 235 | 236 | hash := commitMultiple(t, worktree, 237 | "(+semver: breaking)\n", 238 | "(+semver: breaking)\n", 239 | ) 240 | 241 | commitObj, err := object.GetObject(repository.Storer, hash) 242 | tag := object.Tag{ 243 | Name: "5.6.7", 244 | Message: "not important", 245 | TargetType: commitObj.Type(), 246 | Target: hash, 247 | } 248 | tagObj := repository.Storer.NewEncodedObject() 249 | err = tag.Encode(tagObj) 250 | assert.Nil(t, err) 251 | 252 | target, err := repository.Storer.SetEncodedObject(tagObj) 253 | assert.Nil(t, err) 254 | 255 | ref := plumbing.NewHashReference(plumbing.ReferenceName("refs/tags/5.6.7"), target) 256 | err = repository.Storer.SetReference(ref) 257 | assert.Nil(t, err) 258 | 259 | commitMultiple(t, worktree, 260 | "(+semver: minor)\n", 261 | "(+semver: patch)\n", 262 | ) 263 | 264 | settings := igit.GetDefaultSettings() 265 | branchSettings := &igit.BranchSettings{ 266 | IgnoreEnvVars: true, 267 | } 268 | 269 | // Act 270 | version, err := igit.GetCurrentVersion(repository, settings, branchSettings, false) 271 | assert.Nil(t, err) 272 | 273 | // Assert 274 | assert.Equal(t, "5.7.1", version) 275 | } 276 | 277 | func Test_ShouldCalculateVersionFromTravisTag(t *testing.T) { 278 | // Arrange 279 | repository, worktree := initRepository(t) 280 | 281 | commitMultiple(t, worktree, "Initial commit") 282 | 283 | settings := igit.GetDefaultSettings() 284 | branchSettings := &igit.BranchSettings{} 285 | os.Setenv("TRAVIS_TAG", "v1.2.3") 286 | 287 | // Act 288 | version, err := igit.GetCurrentVersion(repository, settings, branchSettings, false) 289 | assert.Nil(t, err) 290 | 291 | // Assert 292 | assert.Equal(t, "1.2.3", version) 293 | } 294 | 295 | func initRepository(t *testing.T) (*git.Repository, *git.Worktree) { 296 | fs := memfs.New() 297 | storage := memory.NewStorage() 298 | 299 | repository, err := git.Init(storage, fs) 300 | assert.Nil(t, err) 301 | 302 | worktree, err := repository.Worktree() 303 | assert.Nil(t, err) 304 | 305 | return repository, worktree 306 | } 307 | 308 | func commitMultiple(t *testing.T, worktree *git.Worktree, messages ...string) plumbing.Hash { 309 | var hash plumbing.Hash 310 | var err error 311 | for _, msg := range messages { 312 | hash, err = worktree.Commit(msg, &git.CommitOptions{Author: defaultSignature()}) 313 | assert.Nil(t, err) 314 | } 315 | 316 | return hash 317 | } 318 | 319 | func TestTrimBranchPrefix(t *testing.T) { 320 | r := getSingleBranchCommit("feature/should-be-trimmed", t) 321 | s := igit.GetDefaultSettings() 322 | label, err := igit.GetPrereleaseLabel(r, s, &igit.BranchSettings{ 323 | IgnoreEnvVars: true, 324 | TrimBranchPrefix: true, 325 | }) 326 | if err != nil { 327 | t.Error(err) 328 | t.FailNow() 329 | } 330 | 331 | assert.Equal(t, "should-be-trimmed", label) 332 | } 333 | 334 | func TestCleanseBranchName(t *testing.T) { 335 | r := getSingleBranchCommit("author's-branch", t) 336 | s := igit.GetDefaultSettings() 337 | label, err := igit.GetPrereleaseLabel(r, s, &igit.BranchSettings{ 338 | IgnoreEnvVars: true, 339 | }) 340 | if err != nil { 341 | t.Error(err) 342 | t.FailNow() 343 | } 344 | 345 | assert.Equal(t, "author-s-branch", label) 346 | } 347 | 348 | func getSingleBranchCommit(branchName string, t *testing.T) *git.Repository { 349 | fs := memfs.New() 350 | storage := memory.NewStorage() 351 | 352 | r, err := git.Init(storage, fs) 353 | if err != nil { 354 | t.Error(err) 355 | t.FailNow() 356 | } 357 | 358 | w, err := r.Worktree() 359 | if err != nil { 360 | t.Error(err) 361 | t.FailNow() 362 | } 363 | 364 | util.WriteFile(fs, "foo", []byte("foo"), 0644) 365 | _, err = w.Add("foo") 366 | if err != nil { 367 | t.Error(err) 368 | t.FailNow() 369 | } 370 | _, err = w.Commit("foo\n", &git.CommitOptions{Author: defaultSignature()}) 371 | if err != nil { 372 | t.Error(err) 373 | t.FailNow() 374 | } 375 | 376 | ref := fmt.Sprintf("refs/heads/%s", branchName) 377 | b := plumbing.ReferenceName(ref) 378 | w.Checkout(&git.CheckoutOptions{ 379 | Create: true, 380 | Force: false, 381 | Branch: b, 382 | }) 383 | 384 | util.WriteFile(fs, "foo2", []byte("foo"), 0644) 385 | _, err = w.Add("foo2") 386 | if err != nil { 387 | t.Error(err) 388 | t.FailNow() 389 | } 390 | _, err = w.Commit("(+semver: major) This is a major commit\n", &git.CommitOptions{Author: defaultSignature()}) 391 | if err != nil { 392 | t.Error(err) 393 | t.FailNow() 394 | } 395 | 396 | return r 397 | } 398 | 399 | func defaultSignature() *object.Signature { 400 | when, _ := time.Parse(object.DateFormat, "Thu May 04 00:03:43 2017 +0200") 401 | return &object.Signature{ 402 | Name: "foo", 403 | Email: "foo@foo.foo", 404 | When: when, 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /pkg/git/settings.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | 7 | "github.com/pkg/errors" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | // Settings provides the regex patterns used for version bumping 12 | type Settings struct { 13 | MajorPattern string `yaml:"major-version-bump-message"` 14 | MinorPattern string `yaml:"minor-version-bump-message"` 15 | PatchPattern string `yaml:"patch-version-bump-message"` 16 | } 17 | 18 | // GetSettingsFromFile provides a settings object by parsing the yaml from the file provided 19 | func GetSettingsFromFile(file io.Reader) (*Settings, error) { 20 | s := Settings{} 21 | 22 | fileBytes, err := ioutil.ReadAll(file) 23 | if err != nil { 24 | return nil, errors.Wrap(err, "read bytes from file failed") 25 | } 26 | 27 | err = yaml.Unmarshal(fileBytes, &s) 28 | if err != nil { 29 | return nil, errors.Wrap(err, "read yaml from file failed") 30 | } 31 | 32 | return &s, nil 33 | } 34 | 35 | // GetDefaultSettings returns the default settings 36 | func GetDefaultSettings() *Settings { 37 | return &Settings{ 38 | MajorPattern: "\\+semver:\\s?(breaking|major)", 39 | MinorPattern: "\\+semver:\\s?(feature|minor)", 40 | PatchPattern: "\\+semver:\\s?(fix|patch)", 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/git/settings_test.go: -------------------------------------------------------------------------------- 1 | package git_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/syncromatics/gogitver/pkg/git" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSettingsParse(t *testing.T) { 12 | testString := ` 13 | major-version-bump-message: '\+semver:\s?(breaking|major)' 14 | minor-version-bump-message: '\+semver:\s?(feature|minor)' 15 | patch-version-bump-message: '\+semver:\s?(fix|patch)' 16 | ` 17 | 18 | b := []byte(testString) 19 | r := bytes.NewReader(b) 20 | 21 | s, err := git.GetSettingsFromFile(r) 22 | if err != nil { 23 | t.Error(err) 24 | t.FailNow() 25 | } 26 | 27 | assert.Equal(t, "\\+semver:\\s?(breaking|major)", s.MajorPattern) 28 | 29 | assert.Equal(t, "\\+semver:\\s?(feature|minor)", s.MinorPattern) 30 | 31 | assert.Equal(t, "\\+semver:\\s?(fix|patch)", s.PatchPattern) 32 | } 33 | --------------------------------------------------------------------------------