├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── angularcommit ├── angular.go └── angular_test.go ├── go.mod ├── go.sum ├── inspectgit ├── inspectgit.go └── inspectgit_test.go └── semrel ├── semrel.go └── semrel_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | vendor 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.12.x" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Juhani Ränkimies 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 | # go-semrel 2 | 3 | **Automate releases** with [semantic versioning](https://semver.org/) and 4 | [AngularJS commit conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message) 5 | 6 | [![Build Status](https://travis-ci.org/juranki/go-semrel.svg?branch=master)](https://travis-ci.org/juranki/go-semrel) 7 | [![GoDoc](https://godoc.org/github.com/juranki/go-semrel?status.svg)](https://godoc.org/github.com/juranki/go-semrel) 8 | 9 | 10 | This library is used in [go-semrel-gitlab](https://juhani.gitlab.io/go-semrel-gitlab/) 11 | to 12 | 13 | - determine next version and 14 | - extract information for release note 15 | 16 | -------------------------------------------------------------------------------- /angularcommit/angular.go: -------------------------------------------------------------------------------- 1 | // Package angularcommit analyzes angular-style commit messages 2 | // 3 | // https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message 4 | // https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# 5 | package angularcommit 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/juranki/go-semrel/semrel" 14 | ) 15 | 16 | var ( 17 | fullAngularHead = regexp.MustCompile(`^\s*([a-zA-Z]+)\s*\(([^\)]+)\):\s*([^\n]*)`) 18 | minimalAngularHead = regexp.MustCompile(`^\s*([a-zA-Z]+):\s*([^\n]*)`) 19 | // DefaultOptions for angular commit Analyzer 20 | DefaultOptions = &Options{ 21 | ChoreTypes: []string{"chore", "docs", "test"}, 22 | FixTypes: []string{"fix", "refactor", "perf", "style"}, 23 | FeatureTypes: []string{"feat"}, 24 | BreakingChangeMarkers: []string{ 25 | `BREAKING\s+CHANGE:`, 26 | `BREAKING\s+CHANGE`, 27 | `BREAKING:`, 28 | }, 29 | } 30 | ) 31 | 32 | // Options control how angular commit analyzer behaves 33 | type Options struct { 34 | ChoreTypes []string 35 | FixTypes []string 36 | FeatureTypes []string 37 | BreakingChangeMarkers []string 38 | } 39 | 40 | // Analyzer is a semrel.Analyzer instance that parses commits 41 | // according to angularjs commit conventions 42 | type Analyzer struct { 43 | options *Options 44 | } 45 | 46 | // NewWithOptions initializes Analyzer with options provided 47 | func NewWithOptions(options *Options) *Analyzer { 48 | return &Analyzer{ 49 | options: options, 50 | } 51 | } 52 | 53 | // New initializes Analyzer with DefaultOptions 54 | func New() *Analyzer { 55 | return &Analyzer{} 56 | } 57 | 58 | // Lint checks if message is fomatted according to rules specified in analyzer. 59 | // Currently only checs the format of head line and that type is found. 60 | func (analyzer *Analyzer) Lint(message string) []error { 61 | options := analyzer.options 62 | if options == nil { 63 | options = DefaultOptions 64 | } 65 | ac := parseAngularHead(message) 66 | if !ac.isAngular { 67 | return []error{errors.New("invalid message head")} 68 | } 69 | for _, t := range options.ChoreTypes { 70 | if ac.CommitType == t { 71 | return []error{} 72 | } 73 | } 74 | for _, t := range options.FeatureTypes { 75 | if ac.CommitType == t { 76 | return []error{} 77 | } 78 | } 79 | for _, t := range options.FixTypes { 80 | if ac.CommitType == t { 81 | return []error{} 82 | } 83 | } 84 | return []error{errors.New("invalid type")} 85 | } 86 | 87 | // Analyze implements semrel.Analyzer interface for angularcommit.Analyzer 88 | func (analyzer *Analyzer) Analyze(commit *semrel.Commit) ([]semrel.Change, error) { 89 | options := analyzer.options 90 | if analyzer.options == nil { 91 | options = DefaultOptions 92 | } 93 | changes := []semrel.Change{} 94 | message := commit.Msg 95 | ac := parseAngularHead(message) 96 | ac.BreakingMessage = parseAngularBreakingChange(message, options.BreakingChangeMarkers) 97 | ac.commit = *commit 98 | ac.options = options 99 | ac.Hash = commit.SHA 100 | if len(ac.Category()) > 0 { 101 | changes = append(changes, ac) 102 | } 103 | return changes, nil 104 | } 105 | 106 | // Change captures commit message analysis 107 | type Change struct { 108 | isAngular bool 109 | CommitType string 110 | Scope string 111 | Subject string 112 | BreakingMessage string 113 | Hash string 114 | commit semrel.Commit 115 | options *Options 116 | } 117 | 118 | // Category implements semrel.Change interface 119 | func (commit *Change) Category() string { 120 | var categoryMap = map[semrel.BumpLevel]string{ 121 | semrel.NoBump: "other", 122 | semrel.BumpMajor: "breaking", 123 | semrel.BumpMinor: "feature", 124 | semrel.BumpPatch: "fix", 125 | } 126 | return categoryMap[commit.BumpLevel()] 127 | } 128 | 129 | // BumpLevel implements semrel.Change interface 130 | func (commit *Change) BumpLevel() semrel.BumpLevel { 131 | if len(commit.BreakingMessage) > 0 { 132 | return semrel.BumpMajor 133 | } 134 | for _, fType := range commit.options.FeatureTypes { 135 | if fType == commit.CommitType { 136 | return semrel.BumpMinor 137 | } 138 | } 139 | for _, fType := range commit.options.FixTypes { 140 | if fType == commit.CommitType { 141 | return semrel.BumpPatch 142 | } 143 | } 144 | return semrel.NoBump 145 | } 146 | 147 | // PreReleased implements semrel.Change interface 148 | func (commit *Change) PreReleased() bool { 149 | return commit.commit.PreReleased 150 | } 151 | 152 | func parseAngularHead(text string) *Change { 153 | t := strings.Replace(text, "\r", "", -1) 154 | if match := fullAngularHead.FindStringSubmatch(t); len(match) > 0 { 155 | return &Change{ 156 | isAngular: true, 157 | CommitType: strings.ToLower(strings.Trim(match[1], " \t\n")), 158 | Scope: strings.ToLower(strings.Trim(match[2], " \t\n")), 159 | Subject: strings.Trim(match[3], " \t\n"), 160 | } 161 | } 162 | if match := minimalAngularHead.FindStringSubmatch(text); len(match) > 0 { 163 | return &Change{ 164 | isAngular: true, 165 | CommitType: strings.ToLower(strings.Trim(match[1], " \t\n")), 166 | Subject: strings.Trim(match[2], " \t\n"), 167 | } 168 | } 169 | return &Change{ 170 | isAngular: false, 171 | Subject: strings.Trim(strings.Split(text, "\n")[0], " \n\t"), 172 | } 173 | } 174 | 175 | func parseAngularBreakingChange(text string, markers []string) string { 176 | for _, marker := range markers { 177 | re, err := regexp.Compile(`(?ms)` + marker + `\s+(.*)`) 178 | if err != nil { 179 | fmt.Printf("WARNING: unable to compile regular expression for marker '%s'\n", marker) 180 | continue 181 | } 182 | if match := re.FindStringSubmatch(text); len(match) > 0 { 183 | return strings.Trim(match[1], " \n\t") 184 | } 185 | 186 | } 187 | return "" 188 | } 189 | -------------------------------------------------------------------------------- /angularcommit/angular_test.go: -------------------------------------------------------------------------------- 1 | package angularcommit 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestAngularHead(t *testing.T) { 9 | cases := []struct { 10 | msg string 11 | isAngular bool 12 | commitType string 13 | scope string 14 | subject string 15 | }{ 16 | {"fix(testing): angular head", true, "fix", "testing", "angular head"}, 17 | {"fix(testing): angular head ", true, "fix", "testing", "angular head"}, 18 | {"Fix(Testing): Angular head ", true, "fix", "testing", "Angular head"}, 19 | {" fix (testing): angular head", true, "fix", "testing", "angular head"}, 20 | {" fix ( testing ): angular head", true, "fix", "testing", "angular head"}, 21 | {"fix: angular head", true, "fix", "", "angular head"}, 22 | {"Fix: Angular head", true, "fix", "", "Angular head"}, 23 | {"angular head", false, "", "", "angular head"}, 24 | {"trailing newline\n", false, "", "", "trailing newline"}, 25 | {" trim\n", false, "", "", "trim"}, 26 | {" fix:trailing newline\nasdf", true, "fix", "", "trailing newline"}, 27 | } 28 | for _, c := range cases { 29 | ah := parseAngularHead(c.msg) 30 | if ah.isAngular != c.isAngular { 31 | t.Errorf("'%s': got isAngular=%t, want %t\n", c.msg, ah.isAngular, c.isAngular) 32 | } 33 | if ah.CommitType != c.commitType { 34 | t.Errorf("'%s': got type '%s', want '%s'\n", c.msg, ah.CommitType, c.commitType) 35 | } 36 | if ah.Scope != c.scope { 37 | t.Errorf("'%s': got scope '%s', want '%s'\n", c.msg, ah.Scope, c.scope) 38 | } 39 | if ah.Subject != c.subject { 40 | t.Errorf("'%s': got subject '%s', want '%s'\n", c.msg, ah.Subject, c.subject) 41 | } 42 | } 43 | } 44 | 45 | func TestBreakingChange(t *testing.T) { 46 | markers := []string{"break:", "break"} 47 | cases := []struct { 48 | msg string 49 | result string 50 | }{ 51 | {"foo\n\nbreak message", "message"}, 52 | {"foo\n\nbreak: message\nsecondline\n ", "message\nsecondline"}, 53 | {"foo\n\nbrek: message\nsecondline\n ", ""}, 54 | } 55 | for _, c := range cases { 56 | result := parseAngularBreakingChange(c.msg, markers) 57 | if result != c.result { 58 | t.Errorf("got %s, want %s\n", result, c.result) 59 | } 60 | } 61 | } 62 | 63 | func TestAnalyzer_Lint(t *testing.T) { 64 | analyzer := New() 65 | tests := []struct { 66 | name string 67 | message string 68 | want []string 69 | }{ 70 | {"simple ok", "chore: test", []string{}}, 71 | {"no type", "test", []string{"invalid message head"}}, 72 | {"invalid type", "foo: test", []string{"invalid type"}}, 73 | {"with scope", "test(test): test", []string{}}, 74 | } 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | errs := analyzer.Lint(tt.message) 78 | got := make([]string, len(errs)) 79 | for i, s := range errs { 80 | got[i] = s.Error() 81 | } 82 | if !reflect.DeepEqual(got, tt.want) { 83 | t.Errorf("Analyzer.Lint() = %v, want %v", got, tt.want) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/juranki/go-semrel 2 | 3 | require ( 4 | github.com/blang/semver v3.5.1+incompatible 5 | github.com/google/go-cmp v0.3.1 // indirect 6 | github.com/pkg/errors v0.8.1 7 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect 8 | gopkg.in/src-d/go-billy.v4 v4.3.2 9 | gopkg.in/src-d/go-git.v4 v4.13.1 10 | ) 11 | -------------------------------------------------------------------------------- /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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 6 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 7 | github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= 8 | github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 9 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 14 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 15 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 16 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 17 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 18 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 19 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 20 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 21 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 23 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 24 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 25 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 26 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 27 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 28 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 31 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 32 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 33 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 34 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 35 | github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= 36 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 37 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 38 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 42 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 43 | github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= 44 | github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 46 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 47 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 48 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 49 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 50 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 51 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 52 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 53 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 54 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 55 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 56 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 57 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= 58 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 59 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 61 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 65 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 67 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 68 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 69 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 70 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 71 | golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 72 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 73 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= 75 | gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 76 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= 77 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 78 | gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= 79 | gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= 80 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 81 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 82 | -------------------------------------------------------------------------------- /inspectgit/inspectgit.go: -------------------------------------------------------------------------------- 1 | // Package inspectgit collects version tags and unreleased commits from 2 | // a git repository 3 | package inspectgit 4 | 5 | import ( 6 | "io" 7 | "sort" 8 | "strings" 9 | "time" 10 | 11 | "github.com/blang/semver" 12 | "github.com/juranki/go-semrel/semrel" 13 | "github.com/pkg/errors" 14 | git "gopkg.in/src-d/go-git.v4" 15 | "gopkg.in/src-d/go-git.v4/plumbing" 16 | "gopkg.in/src-d/go-git.v4/plumbing/object" 17 | ) 18 | 19 | // VCSData returns current version and list of unreleased changes 20 | // 21 | // Open repository at `path` and traverse parents of `HEAD` to find 22 | // the tag that represents previous release and the commits that haven't 23 | // been released yet. 24 | func VCSData(path string) (*semrel.VCSData, error) { 25 | return VCSDataWithPrefix(path, "") 26 | } 27 | 28 | // VCSDataWithPrefix returns current version and list of unreleased changes 29 | // 30 | // The same as VCSData, but allows prefix before version, when searching earlier 31 | // releases. Versions without the prefix are still recognized. 32 | func VCSDataWithPrefix(path string, prefix string) (*semrel.VCSData, error) { 33 | 34 | r, err := git.PlainOpen(path) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | versions, err := getVersions(r, prefix) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | data, err := getUnreleasedCommits(r, versions) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | t, err := getHeadTime(r) 50 | if err != nil { 51 | return nil, err 52 | } 53 | data.Time = *t 54 | 55 | return data, nil 56 | } 57 | 58 | func getHeadTime(r *git.Repository) (*time.Time, error) { 59 | h, err := r.Head() 60 | if err != nil { 61 | return nil, errors.Wrap(err, "get HEAD") 62 | } 63 | hCommit, err := r.CommitObject(h.Hash()) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return &hCommit.Author.When, nil 69 | } 70 | 71 | // Search semantic versions from tags, including pre-releases 72 | // prefix is removed from the tag before trying to parse semantic version 73 | func getVersions(r *git.Repository, prefix string) (map[string]semver.Version, error) { 74 | versions := make(map[string]semver.Version) 75 | 76 | addIfSemVer := func(sha string, version string) { 77 | s := strings.TrimPrefix(version, prefix) 78 | sv, err := semver.ParseTolerant(s) 79 | if err == nil { 80 | prevV, prevExists := versions[sha] 81 | if prevExists && prevV.GT(sv) { 82 | return 83 | } 84 | versions[sha] = sv 85 | } 86 | } 87 | 88 | tagRefs, err := r.Tags() 89 | if err != nil { 90 | return nil, err 91 | } 92 | err = tagRefs.ForEach(func(t *plumbing.Reference) error { 93 | addIfSemVer(t.Hash().String(), t.Name().Short()) 94 | return nil 95 | }) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | tagObjects, err := r.TagObjects() 101 | if err != nil { 102 | return nil, err 103 | } 104 | err = tagObjects.ForEach(func(t *object.Tag) error { 105 | addIfSemVer(t.Target.String(), t.Name) 106 | return nil 107 | }) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | return versions, nil 113 | } 114 | 115 | func getUnreleasedCommits(r *git.Repository, versions map[string]semver.Version) (*semrel.VCSData, error) { 116 | var traverse func(*object.Commit, bool, bool) error 117 | currVersion := semver.MustParse("0.0.0") 118 | cache := newCache() 119 | traverse = func(c *object.Commit, isNew bool, isPreReleased bool) error { 120 | unReleased := isNew 121 | preReleased := isPreReleased 122 | tag, hasTag := versions[c.Hash.String()] 123 | if hasTag { 124 | if len(tag.Pre) > 0 || len(tag.Build) > 0 { 125 | preReleased = true 126 | } else if isNew { 127 | unReleased = false 128 | if tag.GT(currVersion) { 129 | currVersion = tag 130 | } 131 | } 132 | } 133 | if !cache.add(c, unReleased, preReleased) { 134 | return nil 135 | } 136 | parents := c.Parents() 137 | defer parents.Close() 138 | for { 139 | cc, err := parents.Next() 140 | if err == io.EOF { 141 | return nil 142 | } 143 | if err != nil { 144 | return err 145 | } 146 | // fmt.Println(c.NumParents(), c.Hash, " -> ", cc.Hash) 147 | traverse(cc, unReleased, preReleased) 148 | } 149 | } 150 | h, err := r.Head() 151 | if err != nil { 152 | return nil, errors.Wrap(err, "get HEAD") 153 | } 154 | hCommit, err := r.CommitObject(h.Hash()) 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | err = traverse(hCommit, true, false) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | newCommits := cache.newCommits() 165 | sort.Sort(semrel.ByTime(newCommits)) 166 | 167 | return &semrel.VCSData{ 168 | CurrentVersion: currVersion, 169 | UnreleasedCommits: newCommits, 170 | }, nil 171 | } 172 | 173 | type commitCacheEntry struct { 174 | isNew bool 175 | commit semrel.Commit 176 | } 177 | 178 | type commitCache struct { 179 | commits map[string]*commitCacheEntry 180 | } 181 | 182 | func newCache() *commitCache { 183 | return &commitCache{ 184 | commits: map[string]*commitCacheEntry{}, 185 | } 186 | } 187 | 188 | func (cache *commitCache) newCommits() []semrel.Commit { 189 | rv := []semrel.Commit{} 190 | for _, entry := range cache.commits { 191 | if entry.isNew { 192 | rv = append(rv, entry.commit) 193 | } 194 | } 195 | return rv 196 | } 197 | 198 | func (cache *commitCache) add(commit *object.Commit, isNew bool, isPreReleased bool) bool { 199 | isMerge := false 200 | if commit.NumParents() > 1 { 201 | isMerge = true 202 | } 203 | entry, hasEntry := cache.commits[commit.Hash.String()] 204 | if !hasEntry { 205 | cache.commits[commit.Hash.String()] = &commitCacheEntry{ 206 | isNew: isNew, 207 | commit: semrel.Commit{ 208 | Msg: commit.Message, 209 | SHA: commit.Hash.String(), 210 | Time: commit.Author.When, 211 | PreReleased: isPreReleased, 212 | IsMerge: isMerge, 213 | }, 214 | } 215 | return true 216 | } 217 | if isPreReleased { 218 | entry.commit.PreReleased = true 219 | } 220 | if !entry.isNew || entry.isNew == isNew { 221 | return false 222 | } 223 | entry.isNew = isNew 224 | return true 225 | } 226 | -------------------------------------------------------------------------------- /inspectgit/inspectgit_test.go: -------------------------------------------------------------------------------- 1 | package inspectgit 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | 9 | "gopkg.in/src-d/go-billy.v4/memfs" 10 | git "gopkg.in/src-d/go-git.v4" 11 | "gopkg.in/src-d/go-git.v4/plumbing" 12 | "gopkg.in/src-d/go-git.v4/plumbing/object" 13 | "gopkg.in/src-d/go-git.v4/storage/memory" 14 | ) 15 | 16 | func setupRepo(t *testing.T) (*git.Repository, *git.Worktree) { 17 | t.Helper() 18 | r, err := git.Init(memory.NewStorage(), memfs.New()) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | w, err := r.Worktree() 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | return r, w 27 | } 28 | 29 | func commit(t *testing.T, w *git.Worktree, msg string) plumbing.Hash { 30 | t.Helper() 31 | hash, err := w.Commit(msg, &git.CommitOptions{ 32 | Author: &object.Signature{ 33 | Name: "a", 34 | Email: "a@b", 35 | When: time.Now(), 36 | }, 37 | }) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | return hash 42 | } 43 | func merge(t *testing.T, w *git.Worktree, msg string, parents []plumbing.Hash) plumbing.Hash { 44 | t.Helper() 45 | hash, err := w.Commit(msg, &git.CommitOptions{ 46 | Author: &object.Signature{ 47 | Name: "a", 48 | Email: "a@b", 49 | When: time.Now(), 50 | }, 51 | Parents: parents, 52 | }) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | return hash 57 | } 58 | 59 | func tag(t *testing.T, r *git.Repository, hash plumbing.Hash, version string) { 60 | t.Helper() 61 | n := plumbing.ReferenceName(fmt.Sprintf("refs/tags/%s", version)) 62 | tag := plumbing.NewHashReference(n, hash) 63 | err := r.Storer.SetReference(tag) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | } 68 | 69 | func checkReleaseData(t *testing.T, r *git.Repository, n int, version string) { 70 | t.Helper() 71 | vs, err := getVersions(r, "") 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | vcsData, err := getUnreleasedCommits(r, vs) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | if len(vcsData.UnreleasedCommits) != n { 80 | t.Logf("commits: %+v\n", vcsData.UnreleasedCommits) 81 | t.Logf("versions: %+v\n", vs) 82 | t.Logf("version: %+v\n", vcsData.CurrentVersion) 83 | t.Errorf("got %d commits, want %d", len(vcsData.UnreleasedCommits), n) 84 | } 85 | if vcsData.CurrentVersion.String() != version { 86 | t.Logf("commits: %+v\n", vcsData.UnreleasedCommits) 87 | t.Logf("versions: %+v\n", vs) 88 | t.Logf("version: %+v\n", vcsData.CurrentVersion) 89 | t.Errorf("got %s, want %s", vcsData.CurrentVersion.String(), version) 90 | } 91 | } 92 | 93 | func TestGetVersions(t *testing.T) { 94 | r, w := setupRepo(t) 95 | 96 | checkVersionCount := func(n int) { 97 | vs, err := getVersions(r, "") 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | if len(vs) != n { 102 | t.Errorf("got %d versions, want %d", len(vs), n) 103 | } 104 | } 105 | 106 | checkVersionCount(0) 107 | 108 | hash := commit(t, w, "initial") 109 | tag(t, r, hash, "v2.3.4") 110 | 111 | checkVersionCount(1) 112 | } 113 | 114 | func TestGetVersionsWPrefix(t *testing.T) { 115 | r, w := setupRepo(t) 116 | 117 | checkVersionCount := func(n int) { 118 | vs, err := getVersions(r, "releases/") 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | if len(vs) != n { 123 | t.Errorf("got %d versions, want %d", len(vs), n) 124 | } 125 | } 126 | 127 | checkVersionCount(0) 128 | 129 | hash := commit(t, w, "initial") 130 | tag(t, r, hash, "releases/2.3.4") 131 | 132 | checkVersionCount(1) 133 | } 134 | 135 | func TestGetUnreleasedCommits(t *testing.T) { 136 | r, w := setupRepo(t) 137 | 138 | commit(t, w, "initial") 139 | checkReleaseData(t, r, 1, "0.0.0") 140 | hash := commit(t, w, "1") 141 | checkReleaseData(t, r, 2, "0.0.0") 142 | tag(t, r, hash, "v1.0.0") 143 | checkReleaseData(t, r, 0, "1.0.0") 144 | commit(t, w, "2") 145 | checkReleaseData(t, r, 1, "1.0.0") 146 | } 147 | 148 | func TestIgnorePreRelease(t *testing.T) { 149 | r, w := setupRepo(t) 150 | 151 | commit(t, w, "initial") 152 | checkReleaseData(t, r, 1, "0.0.0") 153 | hash := commit(t, w, "1") 154 | checkReleaseData(t, r, 2, "0.0.0") 155 | tag(t, r, hash, "v1.0.0-pre") 156 | checkReleaseData(t, r, 2, "0.0.0") 157 | commit(t, w, "2") 158 | checkReleaseData(t, r, 3, "0.0.0") 159 | } 160 | 161 | func TestMultipleTagsOnCommit(t *testing.T) { 162 | r, w := setupRepo(t) 163 | 164 | commit(t, w, "initial") 165 | checkReleaseData(t, r, 1, "0.0.0") 166 | hash := commit(t, w, "1") 167 | checkReleaseData(t, r, 2, "0.0.0") 168 | tag(t, r, hash, "v1.0.0") 169 | tag(t, r, hash, "v1.0.1") 170 | checkReleaseData(t, r, 0, "1.0.1") 171 | hash = commit(t, w, "2") 172 | tag(t, r, hash, "v2.0.0") 173 | tag(t, r, hash, "v2.0.1") 174 | checkReleaseData(t, r, 0, "2.0.1") 175 | } 176 | 177 | func TestMerge(t *testing.T) { 178 | r, w := setupRepo(t) 179 | 180 | commit(t, w, "initial") 181 | a1 := commit(t, w, "a1") 182 | a2 := commit(t, w, "a2") 183 | checkReleaseData(t, r, 3, "0.0.0") 184 | tag(t, r, a2, "v1.0.0") 185 | checkReleaseData(t, r, 0, "1.0.0") 186 | a3 := commit(t, w, "a3") 187 | checkReleaseData(t, r, 1, "1.0.0") 188 | 189 | err := w.Checkout(&git.CheckoutOptions{ 190 | Hash: a1, 191 | Branch: "refs/heads/b", 192 | Create: true, 193 | Force: true, 194 | }) 195 | if err != nil { 196 | t.Error(err) 197 | } 198 | checkReleaseData(t, r, 2, "0.0.0") 199 | f, err := w.Filesystem.Create("foo") 200 | if err != nil { 201 | t.Error(err) 202 | } 203 | f.Write([]byte("hello")) 204 | f.Close() 205 | w.Add("foo") 206 | commit(t, w, "b1") 207 | // fmt.Printf("%+v\n", head) 208 | checkReleaseData(t, r, 3, "0.0.0") 209 | commit(t, w, "b2") 210 | b3 := commit(t, w, "b3") 211 | checkReleaseData(t, r, 5, "0.0.0") 212 | merge(t, w, "merge", []plumbing.Hash{b3, a3}) 213 | checkReleaseData(t, r, 5, "1.0.0") 214 | } 215 | -------------------------------------------------------------------------------- /semrel/semrel.go: -------------------------------------------------------------------------------- 1 | // Package semrel processes version control data using an analyser 2 | // function, to produce data for a release note 3 | package semrel 4 | 5 | import ( 6 | "fmt" 7 | "time" 8 | 9 | "github.com/blang/semver" 10 | ) 11 | 12 | // BumpLevel of the release and/or individual commit 13 | type BumpLevel int 14 | 15 | // BumpLevel values 16 | const ( 17 | NoBump BumpLevel = iota 18 | BumpPatch = iota 19 | BumpMinor = iota 20 | BumpMajor = iota 21 | ) 22 | 23 | // ChangeAnalyzer analyzes a commit message and returns 0 or more entries to release note 24 | type ChangeAnalyzer interface { 25 | Analyze(commit *Commit) ([]Change, error) 26 | } 27 | 28 | // VCSData contains data collected from version control system 29 | type VCSData struct { 30 | CurrentVersion semver.Version 31 | UnreleasedCommits []Commit 32 | // Time of the commit being released 33 | Time time.Time 34 | } 35 | 36 | // Commit contains VCS commit data 37 | type Commit struct { 38 | Msg string 39 | SHA string 40 | Time time.Time 41 | PreReleased bool 42 | IsMerge bool 43 | } 44 | 45 | // ByTime implements sort.Interface for []Commit based on Time(). 46 | type ByTime []Commit 47 | 48 | func (a ByTime) Len() int { return len(a) } 49 | func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 50 | func (a ByTime) Less(i, j int) bool { return a[i].Time.Before(a[j].Time) } 51 | 52 | // Change captures ChangeAnalyzer results 53 | type Change interface { 54 | Category() string 55 | BumpLevel() BumpLevel 56 | PreReleased() bool 57 | } 58 | 59 | // ReleaseData contains information for next release 60 | type ReleaseData struct { 61 | CurrentVersion semver.Version 62 | NextVersion semver.Version 63 | BumpLevel BumpLevel 64 | Changes map[string][]Change 65 | // Time of the commit being released 66 | Time time.Time 67 | } 68 | 69 | // Release processes the release data 70 | func Release(input *VCSData, analyzer ChangeAnalyzer) (*ReleaseData, error) { 71 | output := &ReleaseData{ 72 | CurrentVersion: input.CurrentVersion, 73 | NextVersion: input.CurrentVersion, 74 | BumpLevel: NoBump, 75 | Changes: map[string][]Change{}, 76 | Time: input.Time, 77 | } 78 | for _, commit := range input.UnreleasedCommits { 79 | changes, err := analyzer.Analyze(&commit) 80 | if err != nil { 81 | return nil, err 82 | } 83 | for _, change := range changes { 84 | if category, catOK := output.Changes[change.Category()]; catOK { 85 | output.Changes[change.Category()] = append(category, change) 86 | } else { 87 | output.Changes[change.Category()] = []Change{change} 88 | } 89 | if change.BumpLevel() > output.BumpLevel { 90 | output.BumpLevel = change.BumpLevel() 91 | } 92 | } 93 | } 94 | output.NextVersion = bump(output.CurrentVersion, output.BumpLevel) 95 | return output, nil 96 | } 97 | 98 | func bump(curr semver.Version, bumpLevel BumpLevel) semver.Version { 99 | var major uint64 100 | var minor uint64 101 | var patch uint64 102 | if bumpLevel == NoBump { 103 | return semver.MustParse(curr.String()) 104 | } 105 | if bumpLevel == BumpMajor && curr.Major > 0 { 106 | major = curr.Major + 1 107 | } 108 | if bumpLevel == BumpMinor || (curr.Major == 0 && bumpLevel == BumpMajor) { 109 | major = curr.Major 110 | minor = curr.Minor + 1 111 | } 112 | if bumpLevel == BumpPatch { 113 | major = curr.Major 114 | minor = curr.Minor 115 | patch = curr.Patch + 1 116 | } 117 | return semver.MustParse(fmt.Sprintf("%d.%d.%d", major, minor, patch)) 118 | } 119 | -------------------------------------------------------------------------------- /semrel/semrel_test.go: -------------------------------------------------------------------------------- 1 | package semrel 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/blang/semver" 10 | ) 11 | 12 | // implement Change interface for BumpLevel 13 | func (change BumpLevel) Category() string { return fmt.Sprintf("%d", int(change)) } 14 | func (change BumpLevel) BumpLevel() BumpLevel { return change } 15 | func (change BumpLevel) PreReleased() bool { return false } 16 | 17 | type analyzer struct{} 18 | 19 | func (a analyzer) Analyze(commit *Commit) ([]Change, error) { 20 | msg := commit.Msg 21 | if strings.HasPrefix(msg, "fix") { 22 | return []Change{BumpLevel(BumpPatch)}, nil 23 | } 24 | if strings.HasPrefix(msg, "feat") { 25 | return []Change{BumpLevel(BumpMinor)}, nil 26 | } 27 | if strings.HasPrefix(msg, "break") { 28 | return []Change{BumpLevel(BumpMajor)}, nil 29 | } 30 | if strings.HasPrefix(msg, "fail") { 31 | return nil, fmt.Errorf("fail") 32 | } 33 | return []Change{}, nil 34 | } 35 | 36 | var dummyAnalyzer = analyzer{} 37 | 38 | func TestNoChangesBump(t *testing.T) { 39 | input := &VCSData{ 40 | CurrentVersion: semver.MustParse("0.1.0"), 41 | UnreleasedCommits: []Commit{ 42 | {"aaa", "", time.Now(), false, false}, 43 | }, 44 | } 45 | output, err := Release(input, dummyAnalyzer) 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | fmt.Print(output) 50 | if output.NextVersion.String() != "0.1.0" { 51 | t.Error(output.NextVersion.String()) 52 | } 53 | } 54 | 55 | func TestBump(t *testing.T) { 56 | data := []struct { 57 | origVersion string 58 | bumpLevel BumpLevel 59 | newVersion string 60 | }{ 61 | {"0.0.0", NoBump, "0.0.0"}, 62 | {"0.0.0", BumpPatch, "0.0.1"}, 63 | {"0.2.1", BumpMinor, "0.3.0"}, 64 | {"0.2.1", BumpMajor, "0.3.0"}, 65 | {"1.2.1", BumpMajor, "2.0.0"}, 66 | } 67 | for _, d := range data { 68 | bumpedVersion := bump(semver.MustParse(d.origVersion), d.bumpLevel).String() 69 | if bumpedVersion != d.newVersion { 70 | t.Errorf("with %+v, got %s, want %s", d, bumpedVersion, d.newVersion) 71 | } 72 | } 73 | } 74 | 75 | func TestRelease1(t *testing.T) { 76 | input := &VCSData{ 77 | CurrentVersion: semver.MustParse("0.0.0"), 78 | UnreleasedCommits: []Commit{ 79 | {"fix", "", time.Now(), false, false}, 80 | }, 81 | } 82 | output, err := Release(input, dummyAnalyzer) 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | if output.NextVersion.String() != "0.0.1" { 87 | t.Errorf("got %s, want 0.0.1", output.NextVersion.String()) 88 | } 89 | if len(output.Changes["1"]) != 1 { 90 | t.Errorf("got %d, want 1 fix", len(output.Changes["1"])) 91 | } 92 | } 93 | 94 | func TestRelease2(t *testing.T) { 95 | input := &VCSData{ 96 | CurrentVersion: semver.MustParse("1.2.3"), 97 | UnreleasedCommits: []Commit{ 98 | {"fix", "", time.Now(), false, false}, 99 | {"fix", "", time.Now(), false, false}, 100 | {"feat", "", time.Now(), false, false}, 101 | {"break", "", time.Now(), false, false}, 102 | }, 103 | } 104 | output, err := Release(input, dummyAnalyzer) 105 | if err != nil { 106 | t.Error(err) 107 | } 108 | if output.NextVersion.String() != "2.0.0" { 109 | t.Errorf("got %s, want 2.0.0", output.NextVersion.String()) 110 | } 111 | if len(output.Changes["1"]) != 2 { 112 | t.Errorf("got %d, want 2 fixes", len(output.Changes["1"])) 113 | } 114 | if len(output.Changes["2"]) != 1 { 115 | t.Errorf("got %d, want 1 features", len(output.Changes["2"])) 116 | } 117 | if len(output.Changes["3"]) != 1 { 118 | t.Errorf("got %d, want expected 1 breaking change", len(output.Changes["3"])) 119 | } 120 | } 121 | 122 | func TestRelease3(t *testing.T) { 123 | input := &VCSData{ 124 | CurrentVersion: semver.MustParse("1.2.3"), 125 | UnreleasedCommits: []Commit{ 126 | {"fix", "", time.Now(), false, false}, 127 | {"fix", "", time.Now(), false, false}, 128 | {"fail", "", time.Now(), false, false}, 129 | {"break", "", time.Now(), false, false}, 130 | }, 131 | } 132 | _, err := Release(input, dummyAnalyzer) 133 | if err == nil || err.Error() != "fail" { 134 | t.Errorf("got %+v, want error", err) 135 | } 136 | } 137 | --------------------------------------------------------------------------------