├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── main.yml ├── .gitignore ├── cmd ├── doc.go ├── version_test.go ├── gsemver_test.go ├── docs.go ├── version.go ├── gsemver.go ├── bump.go └── bump_test.go ├── pkg ├── git │ ├── doc.go │ ├── tag.go │ ├── hash.go │ ├── commit.go │ ├── signature_test.go │ ├── hash_test.go │ └── signature.go ├── version │ ├── doc.go │ ├── error_test.go │ ├── bump_strategy_example_test.go │ ├── error.go │ ├── git_repo.go │ ├── bump_strategy_type_test.go │ ├── context.go │ ├── bump_strategy_type.go │ ├── bump_branches_strategy_test.go │ ├── bump_branches_strategy.go │ ├── version.go │ ├── version_test.go │ ├── bump_strategy.go │ └── bump_strategy_test.go └── gsemver │ └── version │ └── version.go ├── internal ├── utils │ ├── regexp.go │ ├── array_equal.go │ └── template.go ├── release │ └── main.go ├── git │ ├── factory.go │ ├── commit_parser.go │ └── git_repo_impl.go ├── version │ └── version.go ├── log │ └── log.go └── command │ ├── command_test.go │ └── command.go ├── test ├── data │ └── gsemver-test-config.yaml └── integration │ ├── utils.go │ └── gsemver_bump_auto_conventionalcommits_test.go ├── main.go ├── scripts └── coverage.sh ├── .chglog ├── config.yml └── CHANGELOG.tpl.md ├── .golangci.yml ├── docs └── cmd │ ├── gsemver.md │ ├── gsemver_completion_powershell.md │ ├── gsemver_completion_fish.md │ ├── gsemver_completion.md │ ├── gsemver_completion_bash.md │ ├── gsemver_completion_zsh.md │ ├── gsemver_version.md │ └── gsemver_bump.md ├── LICENSE ├── .goreleaser.yml ├── go.mod ├── CONTRIBUTING.md ├── Makefile ├── CODE_OF_CONDUCT.md ├── go.sum └── README.md /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build/ 3 | .DS_Store 4 | pkg/**/mock/ 5 | codecov* -------------------------------------------------------------------------------- /cmd/doc.go: -------------------------------------------------------------------------------- 1 | // Package cmd contains the gsemver CLI implementation 2 | package cmd 3 | -------------------------------------------------------------------------------- /pkg/git/doc.go: -------------------------------------------------------------------------------- 1 | // Package git contains implementations of Git objects that are useful to compute the version. 2 | package git 3 | -------------------------------------------------------------------------------- /pkg/version/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package version contains SemVer compatible Version implementation and the bump strategies 3 | 4 | See the spec: https://semver.org/spec/v2.0.0.html 5 | */ 6 | package version 7 | -------------------------------------------------------------------------------- /internal/utils/regexp.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | // RegexpToString converts *regexp.Regexp instance to string 8 | func RegexpToString(r *regexp.Regexp) string { 9 | if r != nil { 10 | return r.String() 11 | } 12 | return "" 13 | } 14 | -------------------------------------------------------------------------------- /test/data/gsemver-test-config.yaml: -------------------------------------------------------------------------------- 1 | majorPattern: "majorPatternConfig" 2 | minorPattern: "minorPatternConfig" 3 | bumpStrategies: 4 | - branchesPattern: "releaseBranchesPattern" 5 | strategy: "AUTO" 6 | - branchesPattern: "all" 7 | strategy: "AUTO" 8 | buildMetadataTemplate: "myBuildMetadataTemplate" -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | app "github.com/arnaud-deprez/gsemver/cmd" 8 | ) 9 | 10 | // Entrypoint for gsemver command 11 | func main() { 12 | if err := app.Run(); err != nil { 13 | fmt.Fprintf(os.Stderr, "%v\n", err) 14 | os.Exit(1) 15 | } 16 | os.Exit(0) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/git/tag.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | // Tag is data of git-tag 4 | type Tag struct { 5 | // Hash of the tag. 6 | Hash Hash 7 | // Name of the tag. 8 | Name string 9 | // Tagger is the one who created the tag. 10 | Tagger Signature 11 | // Message is an arbitrary text message. 12 | Message string 13 | } 14 | -------------------------------------------------------------------------------- /pkg/git/hash.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | // Hash of commit 4 | type Hash string 5 | 6 | // String return the string representation 7 | func (h Hash) String() string { 8 | return string(h) 9 | } 10 | 11 | // Short convert the 7 first bytes of the Hash to String 12 | func (h Hash) Short() Hash { 13 | return h[:7] 14 | } 15 | -------------------------------------------------------------------------------- /internal/utils/array_equal.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // ArrayStringEqual returns true if 2 arrays of string are equals 4 | func ArrayStringEqual(a, b []string) bool { 5 | if len(a) != len(b) { 6 | return false 7 | } 8 | for i, v := range a { 9 | if v != b[i] { 10 | return false 11 | } 12 | } 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | covermode=${COVERMODE:-atomic} 5 | coverdir=build/coverage 6 | profile="${coverdir}/cover.out" 7 | 8 | mkdir -p $coverdir 9 | 10 | go test -race -coverprofile="${profile}" -covermode="$covermode" ./... 11 | 12 | go tool cover -func "${profile}" 13 | go tool cover -html "${profile}" -------------------------------------------------------------------------------- /pkg/version/error_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ExampleError_Error() { 8 | err := newError("Error 1 occurred") 9 | fmt.Println(err) 10 | err = newErrorC(newError("Error 3"), "Error 2 occurred") 11 | fmt.Println(err) 12 | // Output: 13 | // Error 1 occurred 14 | // Error 2 occurred caused by: Error 3 15 | } 16 | -------------------------------------------------------------------------------- /pkg/git/commit.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | // Commit data 4 | type Commit struct { 5 | // Hash of the commit object. 6 | Hash Hash 7 | // Author is the original author of the commit. 8 | Author Signature 9 | // Committer is the one performing the commit. 10 | // It might be different from Author. 11 | Committer Signature 12 | // Message is the commit message, contains arbitrary text. 13 | Message string 14 | } 15 | -------------------------------------------------------------------------------- /pkg/git/signature_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func ExampleSignature_GoString() { 9 | s := &Signature{Name: "Arnaud Deprez", Email: "arnaudeprez@gmail.com", When: time.Date(2019, time.August, 12, 22, 47, 0, 0, time.UTC)} 10 | fmt.Printf("%#v\n", s) 11 | // Output: git.Signature{Name: "Arnaud Deprez", Email: "arnaudeprez@gmail.com", When: "2019-08-12 22:47:00 +0000 UTC"} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/version/bump_strategy_example_test.go: -------------------------------------------------------------------------------- 1 | package version_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/arnaud-deprez/gsemver/internal/git" 7 | "github.com/arnaud-deprez/gsemver/pkg/version" 8 | ) 9 | 10 | func ExampleBumpStrategy_Bump() { 11 | gitRepo := git.NewVersionGitRepo("dir") 12 | bumpStrategy := version.NewConventionalCommitBumpStrategy(gitRepo) 13 | v, err := bumpStrategy.Bump() 14 | if err != nil { 15 | panic(err) 16 | } 17 | fmt.Println(v.String()) 18 | // Use v like you want 19 | } 20 | -------------------------------------------------------------------------------- /pkg/git/hash_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHashString(t *testing.T) { 11 | hashValue := "12345678901234567890" 12 | hash := Hash(hashValue) 13 | fmt.Println(hash[:]) 14 | assert.Equal(t, hashValue, hash.String()) 15 | } 16 | 17 | func TestHashShortString(t *testing.T) { 18 | hashValue := "12345678901234567890" 19 | hash := Hash(hashValue) 20 | fmt.Println(hash[:]) 21 | assert.Equal(t, "1234567", hash.Short().String()) 22 | } 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /internal/release/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/arnaud-deprez/gsemver/internal/git" 8 | "github.com/arnaud-deprez/gsemver/internal/log" 9 | "github.com/arnaud-deprez/gsemver/pkg/version" 10 | ) 11 | 12 | // Use to compte the next version of gsemver itself 13 | func main() { 14 | gitRepo := git.NewDefaultVersionGitRepo() 15 | bumper := version.NewConventionalCommitBumpStrategy(gitRepo) 16 | version, err := bumper.Bump() 17 | 18 | if err != nil { 19 | log.Error("Cannot bump version caused by: %v", err) 20 | os.Exit(1) 21 | } 22 | fmt.Println(version) 23 | } 24 | -------------------------------------------------------------------------------- /internal/utils/template.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "text/template" 5 | 6 | "github.com/Masterminds/sprig/v3" 7 | ) 8 | 9 | // NewTemplate create a new Template with sprig functions 10 | func NewTemplate(value string) *template.Template { 11 | if value == "" { 12 | return nil 13 | } 14 | return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(value)) //nolint:typecheck 15 | } 16 | 17 | // TemplateToString converts *template.Template instance to string 18 | func TemplateToString(t *template.Template) string { 19 | if t != nil { 20 | return t.Root.String() 21 | } 22 | return "" 23 | } 24 | -------------------------------------------------------------------------------- /pkg/git/signature.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Signature is used to identify who and when created a commit or tag. 9 | type Signature struct { 10 | // Name represents a person name. It is an arbitrary string. 11 | Name string 12 | // Email is an email, but it cannot be assumed to be well-formed. 13 | Email string 14 | // When is the timestamp of the signature. 15 | When time.Time 16 | } 17 | 18 | // GoString makes Signature satisfy the GoStringer interface. 19 | func (s Signature) GoString() string { 20 | return fmt.Sprintf("git.Signature{Name: %q, Email: %q, When: %q}", s.Name, s.Email, s.When) 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ### Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ### Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ### Additional context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /internal/git/factory.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/arnaud-deprez/gsemver/internal/log" 7 | "github.com/arnaud-deprez/gsemver/pkg/version" 8 | ) 9 | 10 | // NewVersionGitRepo creates a version.GitRepo instance for a directory 11 | func NewVersionGitRepo(dir string) version.GitRepo { 12 | return &gitRepoCLI{ 13 | dir: dir, 14 | commitParser: &commitParser{logFormat: logFormat}, 15 | } 16 | } 17 | 18 | // NewDefaultVersionGitRepo creates a version.GitRepo instance with current working dir 19 | func NewDefaultVersionGitRepo() version.GitRepo { 20 | dir, err := os.Getwd() 21 | if err != nil { 22 | log.Error("Unable to retrieve working directory: %v", err) 23 | os.Exit(1) 24 | } 25 | 26 | return NewVersionGitRepo(dir) 27 | } 28 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/arnaud-deprez/gsemver 6 | options: 7 | commits: 8 | filters: 9 | Type: 10 | - feat 11 | - fix 12 | - perf 13 | - refactor 14 | - chore 15 | - docs 16 | commit_groups: 17 | title_maps: 18 | feat: Features 19 | fix: Bug Fixes 20 | perf: Performance Improvements 21 | refactor: Code Refactoring 22 | chore: Chores 23 | docs: Documentation 24 | header: 25 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" 26 | pattern_maps: 27 | - Type 28 | - Scope 29 | - Subject 30 | notes: 31 | keywords: 32 | - BREAKING CHANGE -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ### To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Create this git repository history 16 | * git init 17 | * do while you get history 18 | * touch change1 19 | * git add . 20 | * git commit -m "\" 21 | 2. Then run `gsemver ...` 22 | 3. See error 23 | 24 | ### Expected behavior 25 | A clear and concise description of what you expected to happen. 26 | 27 | ### Screenshots 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | ### Version: 31 | - output of `git version` [e.g. 2.22.0] 32 | - gsemver version ? 33 | -------------------------------------------------------------------------------- /pkg/version/error.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "fmt" 4 | 5 | // Error is a typical error representation that can happen during the version bump process 6 | type Error struct { 7 | message string 8 | cause error 9 | } 10 | 11 | // Error formats VersionError into a string 12 | func (e Error) Error() string { 13 | if e.cause == nil { 14 | return e.message 15 | } 16 | return fmt.Sprintf("%s caused by: %v", e.message, e.cause) 17 | } 18 | 19 | // NewError create an error based on a format error message 20 | func newError(format string, args ...interface{}) Error { 21 | return newErrorC(nil, format, args...) 22 | } 23 | 24 | // NewErrorC create an error based on a cause error and a format error message 25 | func newErrorC(cause error, format string, args ...interface{}) Error { 26 | return Error{message: fmt.Sprintf(format, args...), cause: cause} 27 | } 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - dupl 6 | - gocyclo 7 | - govet 8 | - ineffassign 9 | - misspell 10 | - nakedret 11 | - revive 12 | - staticcheck 13 | - unused 14 | settings: 15 | dupl: 16 | threshold: 400 17 | gocyclo: 18 | min-complexity: 15 19 | exclusions: 20 | generated: lax 21 | presets: 22 | - comments 23 | - common-false-positives 24 | - legacy 25 | - std-error-handling 26 | paths: 27 | - third_party$ 28 | - builtin$ 29 | - examples$ 30 | formatters: 31 | enable: 32 | - gofmt 33 | settings: 34 | gofmt: 35 | simplify: true 36 | goimports: 37 | local-prefixes: 38 | - github.com/arnaud-deprez/gsemver 39 | exclusions: 40 | generated: lax 41 | paths: 42 | - third_party$ 43 | - builtin$ 44 | - examples$ 45 | -------------------------------------------------------------------------------- /docs/cmd/gsemver.md: -------------------------------------------------------------------------------- 1 | ## gsemver 2 | 3 | CLI to manage semver compliant version from your git tags 4 | 5 | ### Synopsis 6 | 7 | Simple CLI to manage semver compliant version from your git tags 8 | 9 | 10 | ``` 11 | gsemver [flags] 12 | ``` 13 | 14 | ### Options 15 | 16 | ``` 17 | -c, --config string config file (default is .gsemver.yaml) 18 | -h, --help help for gsemver 19 | --log-level string Sets the logging level (fatal, error, warning, info, debug, trace) (default "info") 20 | -v, --verbose Enables verbose output by setting log level to debug. This is a shortland to --log-level debug. 21 | ``` 22 | 23 | ### SEE ALSO 24 | 25 | * [gsemver bump](gsemver_bump.md) - Bump to next version 26 | * [gsemver completion](gsemver_completion.md) - Generate the autocompletion script for the specified shell 27 | * [gsemver version](gsemver_version.md) - Print the CLI version information 28 | 29 | -------------------------------------------------------------------------------- /pkg/version/git_repo.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "github.com/arnaud-deprez/gsemver/pkg/git" 5 | ) 6 | 7 | // GitRepo defines common git actions used by gsemver 8 | // 9 | //go:generate mockgen -destination mock/git_repo.go github.com/arnaud-deprez/gsemver/pkg/version GitRepo 10 | type GitRepo interface { 11 | // FetchTags fetches the tags from remote 12 | FetchTags() error 13 | // GetCommits return the list of commits between 2 revisions. 14 | // If no revision is provided, it does from beginning to HEAD 15 | GetCommits(from string, to string) ([]git.Commit, error) 16 | // CountCommits counts the number of commits between 2 revisions. 17 | CountCommits(from string, to string) (int, error) 18 | // GetLastRelativeTag gives the last ancestor tag from HEAD 19 | GetLastRelativeTag(rev string) (git.Tag, error) 20 | // GetCurrentBranch gives the current branch from HEAD 21 | GetCurrentBranch() (string, error) 22 | } 23 | -------------------------------------------------------------------------------- /cmd/version_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | shellquote "github.com/kballard/go-shellquote" 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/arnaud-deprez/gsemver/internal/version" 13 | ) 14 | 15 | func TestVersion(t *testing.T) { 16 | testCases := []struct { 17 | args, expected string 18 | }{ 19 | {"version", fmt.Sprintf("%#v\n", version.Get())}, 20 | {"version --short", fmt.Sprintf("%s\n", version.GetVersion())}, 21 | } 22 | 23 | for id, tc := range testCases { 24 | t.Run(fmt.Sprintf("TestVersion-%d", id), func(t *testing.T) { 25 | assert := assert.New(t) 26 | out, errOut := new(bytes.Buffer), new(bytes.Buffer) 27 | root := newRootCommand(os.Stdin, out, errOut) 28 | 29 | args, err := shellquote.Split(tc.args) 30 | assert.NoError(err) 31 | _, err = executeCommand(root, args...) 32 | assert.NoError(err) 33 | assert.Equal(tc.expected, out.String()) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/gsemver/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package version represents the current version of the project. 3 | 4 | It is about the version of the tool and not the semver version implementation used by this tool. 5 | */ 6 | package version 7 | 8 | // BuildInfo describes the compile time information. 9 | type BuildInfo struct { 10 | // Version is the current semver. 11 | Version string `json:"version,omitempty"` 12 | // GitCommit is the git sha1. 13 | GitCommit string `json:"gitCommit,omitempty"` 14 | // GitTreeState is the state of the git tree. 15 | // It is either clean or dirty. 16 | GitTreeState string `json:"gitTreeState,omitempty"` 17 | // BuildDate is the build date. 18 | BuildDate string `json:"buildDate,omitempty"` 19 | // GoVersion is the version of the Go compiler used. 20 | GoVersion string `json:"goVersion,omitempty"` 21 | // Compiler is the go compiler that built gsemver. 22 | Compiler string `json:"compiler,omitempty"` 23 | // Platform is the OS on which it is running. 24 | Platform string `json:"platform,omitempty"` 25 | } 26 | -------------------------------------------------------------------------------- /cmd/gsemver_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // func emptyRun(*cobra.Command, ...string) {} 13 | 14 | func executeCommand(root *cobra.Command, args ...string) (output string, err error) { 15 | _, output, err = executeCommandC(root, args...) 16 | return output, err 17 | } 18 | 19 | func executeCommandC(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { 20 | buf := new(bytes.Buffer) 21 | root.SetOut(buf) 22 | root.SetArgs(args) 23 | c, err = root.ExecuteC() 24 | return c, buf.String(), err 25 | } 26 | 27 | func TestConfigFile(t *testing.T) { 28 | assert := assert.New(t) 29 | for _, opt := range []string{"--config", "-c"} { 30 | t.Run(opt, func(_ *testing.T) { 31 | _, err := executeCommand(newDefaultRootCommand(), opt, "../test/data/gsemver-test-config.yaml") 32 | assert.NoError(err) 33 | assert.Equal("../test/data/gsemver-test-config.yaml", viper.ConfigFileUsed()) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | gversion "github.com/arnaud-deprez/gsemver/pkg/gsemver/version" 8 | ) 9 | 10 | var ( 11 | // version is the current version of the gsemver. 12 | // Update this whenever making a new release. 13 | // The version is in string format and follow the semver 2 spec (Major.Minor.Patch[-Prerelease][+BuildMetadata]) 14 | version = "0.1.0" 15 | 16 | // gitCommit is the git sha1 17 | gitCommit = "" 18 | // gitTreeState is the state of the git tree 19 | gitTreeState = "" 20 | // buildDate 21 | buildDate = "" 22 | ) 23 | 24 | // GetVersion returns the version 25 | func GetVersion() string { 26 | return version 27 | } 28 | 29 | // Get returns build info 30 | func Get() gversion.BuildInfo { 31 | v := gversion.BuildInfo{ 32 | Version: version, 33 | GitCommit: gitCommit, 34 | GitTreeState: gitTreeState, 35 | BuildDate: buildDate, 36 | GoVersion: runtime.Version(), 37 | Compiler: runtime.Compiler, 38 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 39 | } 40 | 41 | return v 42 | } 43 | -------------------------------------------------------------------------------- /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{- range .Versions }} 2 | {{- if .Tag.Previous }} 3 | ## [{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}) 4 | {{- else }} 5 | ## [{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/releases/tag/{{ .Tag.Name }}) 6 | {{- end }} ({{ datetime "2006-01-02" .Tag.Date }}) 7 | {{ range .CommitGroups }} 8 | ### {{ .Title }} 9 | {{ range .Commits }} 10 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} ([{{ .Hash.Short }}]({{ $.Info.RepositoryURL }}/commits/{{ .Hash.Long }}) 11 | {{- $lenRefs := len .Refs -}} 12 | {{- if gt $lenRefs 0 }}, closes 13 | {{- range $idx, $ref := .Refs }} 14 | {{- if $idx }},{{ end }} [#{{ $ref.Ref }}]({{ $.Info.RepositoryURL }}/issues/{{ $ref.Ref }}) 15 | {{- end }} 16 | {{- end }}) 17 | {{- end }} 18 | {{ end -}} 19 | 20 | {{- if .RevertCommits }} 21 | ### Reverts 22 | {{ range .RevertCommits }} 23 | - {{ .Revert.Header }} 24 | {{- end }} 25 | {{ end -}} 26 | 27 | {{- if .NoteGroups }} 28 | {{ range .NoteGroups -}} 29 | ### {{ .Title }} 30 | {{ range .Notes }} 31 | - {{ .Body }} 32 | {{- end }} 33 | {{- end }} 34 | {{- end }} 35 | {{- end }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Arnaud Deprez 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 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | version: 2 4 | dist: build/dist 5 | before: 6 | hooks: 7 | - make clean 8 | - make build 9 | builds: 10 | - main: main.go 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | goarch: 16 | - "386" 17 | - amd64 18 | - arm 19 | - arm64 20 | env: 21 | - CGO_ENABLED=0 22 | ldflags: 23 | - -s -w -X "github.com/arnaud-deprez/gsemver/internal/version.version={{ .Version }}" -X "github.com/arnaud-deprez/gsemver/internal/version.gitCommit={{ .FullCommit }}" -X "github.com/arnaud-deprez/gsemver/internal/version.gitTreeState={{ .Env.GIT_DIRTY }}" -X "github.com/arnaud-deprez/gsemver/internal/version.buildDate={{ .Date }}" 24 | archives: 25 | - format_overrides: 26 | - goos: windows 27 | formats: 28 | - zip 29 | checksum: 30 | name_template: "checksums.txt" 31 | snapshot: 32 | version_template: "v{{ .Version }}" 33 | changelog: 34 | sort: asc 35 | filters: 36 | exclude: 37 | - "^docs:" 38 | - "^test:" 39 | -------------------------------------------------------------------------------- /docs/cmd/gsemver_completion_powershell.md: -------------------------------------------------------------------------------- 1 | ## gsemver completion powershell 2 | 3 | Generate the autocompletion script for powershell 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for powershell. 8 | 9 | To load completions in your current shell session: 10 | 11 | gsemver completion powershell | Out-String | Invoke-Expression 12 | 13 | To load completions for every new session, add the output of the above command 14 | to your powershell profile. 15 | 16 | 17 | ``` 18 | gsemver completion powershell [flags] 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | -h, --help help for powershell 25 | --no-descriptions disable completion descriptions 26 | ``` 27 | 28 | ### Options inherited from parent commands 29 | 30 | ``` 31 | -c, --config string config file (default is .gsemver.yaml) 32 | --log-level string Sets the logging level (fatal, error, warning, info, debug, trace) (default "info") 33 | -v, --verbose Enables verbose output by setting log level to debug. This is a shortland to --log-level debug. 34 | ``` 35 | 36 | ### SEE ALSO 37 | 38 | * [gsemver completion](gsemver_completion.md) - Generate the autocompletion script for the specified shell 39 | 40 | -------------------------------------------------------------------------------- /docs/cmd/gsemver_completion_fish.md: -------------------------------------------------------------------------------- 1 | ## gsemver completion fish 2 | 3 | Generate the autocompletion script for fish 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the fish shell. 8 | 9 | To load completions in your current shell session: 10 | 11 | gsemver completion fish | source 12 | 13 | To load completions for every new session, execute once: 14 | 15 | gsemver completion fish > ~/.config/fish/completions/gsemver.fish 16 | 17 | You will need to start a new shell for this setup to take effect. 18 | 19 | 20 | ``` 21 | gsemver completion fish [flags] 22 | ``` 23 | 24 | ### Options 25 | 26 | ``` 27 | -h, --help help for fish 28 | --no-descriptions disable completion descriptions 29 | ``` 30 | 31 | ### Options inherited from parent commands 32 | 33 | ``` 34 | -c, --config string config file (default is .gsemver.yaml) 35 | --log-level string Sets the logging level (fatal, error, warning, info, debug, trace) (default "info") 36 | -v, --verbose Enables verbose output by setting log level to debug. This is a shortland to --log-level debug. 37 | ``` 38 | 39 | ### SEE ALSO 40 | 41 | * [gsemver completion](gsemver_completion.md) - Generate the autocompletion script for the specified shell 42 | 43 | -------------------------------------------------------------------------------- /pkg/version/bump_strategy_type_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMarshallBumpStrategyType(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | testData := []struct { 14 | value BumpStrategyType 15 | expected string 16 | }{ 17 | {PATCH, `"PATCH"`}, 18 | {MINOR, `"MINOR"`}, 19 | {MAJOR, `"MAJOR"`}, 20 | {AUTO, `"AUTO"`}, 21 | } 22 | 23 | for _, tc := range testData { 24 | bytes, err := json.Marshal(tc.value) 25 | assert.Nil(err) 26 | assert.Equal(tc.expected, string(bytes)) 27 | } 28 | } 29 | 30 | func TestUnMarshallBumpStrategyType(t *testing.T) { 31 | assert := assert.New(t) 32 | 33 | testData := []struct { 34 | value string 35 | expected BumpStrategyType 36 | }{ 37 | {`"PATCH"`, PATCH}, 38 | {`"MINOR"`, MINOR}, 39 | {`"MAJOR"`, MAJOR}, 40 | {`"AUTO"`, AUTO}, 41 | {`"patch"`, PATCH}, 42 | {`"minor"`, MINOR}, 43 | {`"major"`, MAJOR}, 44 | {`"auto"`, AUTO}, 45 | {`"foo"`, AUTO}, // fallback to AUTO if unknown value 46 | } 47 | 48 | for _, tc := range testData { 49 | var value BumpStrategyType 50 | err := json.Unmarshal([]byte(tc.value), &value) 51 | assert.Nil(err) 52 | assert.Equal(tc.expected, value) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/cmd/gsemver_completion.md: -------------------------------------------------------------------------------- 1 | ## gsemver completion 2 | 3 | Generate the autocompletion script for the specified shell 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for gsemver for the specified shell. 8 | See each sub-command's help for details on how to use the generated script. 9 | 10 | 11 | ### Options 12 | 13 | ``` 14 | -h, --help help for completion 15 | ``` 16 | 17 | ### Options inherited from parent commands 18 | 19 | ``` 20 | -c, --config string config file (default is .gsemver.yaml) 21 | --log-level string Sets the logging level (fatal, error, warning, info, debug, trace) (default "info") 22 | -v, --verbose Enables verbose output by setting log level to debug. This is a shortland to --log-level debug. 23 | ``` 24 | 25 | ### SEE ALSO 26 | 27 | * [gsemver](gsemver.md) - CLI to manage semver compliant version from your git tags 28 | * [gsemver completion bash](gsemver_completion_bash.md) - Generate the autocompletion script for bash 29 | * [gsemver completion fish](gsemver_completion_fish.md) - Generate the autocompletion script for fish 30 | * [gsemver completion powershell](gsemver_completion_powershell.md) - Generate the autocompletion script for powershell 31 | * [gsemver completion zsh](gsemver_completion_zsh.md) - Generate the autocompletion script for zsh 32 | 33 | -------------------------------------------------------------------------------- /pkg/version/context.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "strings" 5 | "text/template" 6 | 7 | "github.com/arnaud-deprez/gsemver/internal/log" 8 | "github.com/arnaud-deprez/gsemver/pkg/git" 9 | ) 10 | 11 | // NewContext returns a new Context 12 | func NewContext(branch string, lastVersion *Version, lastTag *git.Tag, commits []git.Commit) *Context { 13 | return &Context{ 14 | Branch: branch, 15 | LastVersion: lastVersion, 16 | LastTag: lastTag, 17 | Commits: commits, 18 | } 19 | } 20 | 21 | // Context represents the context data used to compute the next version. 22 | // This context is also used as template data. 23 | type Context struct { 24 | // Branch is the current branch name 25 | Branch string 26 | // LastVersion is a semver version representation of the last git tag 27 | LastVersion *Version 28 | // LastTag is the last git tag 29 | LastTag *git.Tag 30 | // Commits is the list of commits from the previous tag until now 31 | Commits []git.Commit 32 | } 33 | 34 | // EvalTemplate evaluates the given template against the current context 35 | func (c *Context) EvalTemplate(template *template.Template) string { 36 | if c == nil || template == nil { 37 | return "" 38 | } 39 | var sb strings.Builder 40 | err := template.Execute(&sb, c) 41 | if err != nil { 42 | // Stop the program 43 | log.Fatal("TemplateContext: fails to evaluate template caused by %v", err) 44 | } 45 | 46 | return sb.String() 47 | } 48 | -------------------------------------------------------------------------------- /pkg/version/bump_strategy_type.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | // BumpStrategyType represents the bump SemVer strategy to use to bump the version 9 | type BumpStrategyType int 10 | 11 | const ( 12 | // PATCH means to bump the patch number 13 | PATCH BumpStrategyType = iota 14 | // MINOR means to bump the minor number 15 | MINOR 16 | // MAJOR means to bump the patch number 17 | MAJOR 18 | // AUTO means to apply the automatic strategy based on commit history 19 | AUTO 20 | ) 21 | 22 | var bumpStrategyToString = []string{"PATCH", "MINOR", "MAJOR", "AUTO"} 23 | 24 | // ParseBumpStrategyType converts string value to BumpStrategy 25 | func ParseBumpStrategyType(value string) BumpStrategyType { 26 | switch strings.ToLower(value) { 27 | case "major": 28 | return MAJOR 29 | case "minor": 30 | return MINOR 31 | case "patch": 32 | return PATCH 33 | default: 34 | return AUTO 35 | } 36 | } 37 | 38 | func (b BumpStrategyType) String() string { 39 | return bumpStrategyToString[b] 40 | } 41 | 42 | // UnmarshalJSON implements unmarshall for encoding/json 43 | func (b *BumpStrategyType) UnmarshalJSON(bs []byte) error { 44 | var s string 45 | if err := json.Unmarshal(bs, &s); err != nil { 46 | return err 47 | } 48 | *b = ParseBumpStrategyType(s) 49 | return nil 50 | } 51 | 52 | // MarshalJSON implements marshall for encoding/json 53 | func (b BumpStrategyType) MarshalJSON() ([]byte, error) { 54 | return json.Marshal(b.String()) 55 | } 56 | -------------------------------------------------------------------------------- /test/integration/utils.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/arnaud-deprez/gsemver/internal/command" 11 | ) 12 | 13 | // GitRepoPath is the git repo path used for integration tests 14 | const GitRepoPath = "./build/git-tmp" 15 | 16 | func execInDir(t *testing.T, dir, cmd string) string { 17 | out, err := command.New(cmd).InDir(dir).Run() 18 | assert.NoError(t, err) 19 | return out 20 | } 21 | 22 | func execInGitRepo(t *testing.T, cmd string) string { 23 | return execInDir(t, GitRepoPath, cmd) 24 | } 25 | 26 | func createTag(t *testing.T, tag string) { 27 | execInGitRepo(t, fmt.Sprintf(`git tag -fa v%s -m "Release %s"`, tag, tag)) 28 | } 29 | 30 | func commit(t *testing.T, msg string) { 31 | execInGitRepo(t, "git add --all") 32 | execInGitRepo(t, fmt.Sprintf(`git commit -am "%s"`, msg)) 33 | } 34 | 35 | func merge(t *testing.T, from, to string) { 36 | execInGitRepo(t, "git checkout "+to) 37 | execInGitRepo(t, fmt.Sprintf(`git merge --no-ff -m "Merge from %s" %s`, from, from)) 38 | } 39 | 40 | func mergePullRequest(t *testing.T, from, to string) { 41 | merge(t, from, to) 42 | execInGitRepo(t, "git branch -d "+from) 43 | } 44 | 45 | func appendToFile(t *testing.T, file, content string) { 46 | f, err := os.OpenFile(GitRepoPath+"/"+file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 47 | assert.NoError(t, err) 48 | defer f.Close() 49 | _, err = f.WriteString(content + "\n") 50 | assert.NoError(t, err) 51 | } 52 | -------------------------------------------------------------------------------- /docs/cmd/gsemver_completion_bash.md: -------------------------------------------------------------------------------- 1 | ## gsemver completion bash 2 | 3 | Generate the autocompletion script for bash 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the bash shell. 8 | 9 | This script depends on the 'bash-completion' package. 10 | If it is not installed already, you can install it via your OS's package manager. 11 | 12 | To load completions in your current shell session: 13 | 14 | source <(gsemver completion bash) 15 | 16 | To load completions for every new session, execute once: 17 | 18 | #### Linux: 19 | 20 | gsemver completion bash > /etc/bash_completion.d/gsemver 21 | 22 | #### macOS: 23 | 24 | gsemver completion bash > $(brew --prefix)/etc/bash_completion.d/gsemver 25 | 26 | You will need to start a new shell for this setup to take effect. 27 | 28 | 29 | ``` 30 | gsemver completion bash 31 | ``` 32 | 33 | ### Options 34 | 35 | ``` 36 | -h, --help help for bash 37 | --no-descriptions disable completion descriptions 38 | ``` 39 | 40 | ### Options inherited from parent commands 41 | 42 | ``` 43 | -c, --config string config file (default is .gsemver.yaml) 44 | --log-level string Sets the logging level (fatal, error, warning, info, debug, trace) (default "info") 45 | -v, --verbose Enables verbose output by setting log level to debug. This is a shortland to --log-level debug. 46 | ``` 47 | 48 | ### SEE ALSO 49 | 50 | * [gsemver completion](gsemver_completion.md) - Generate the autocompletion script for the specified shell 51 | 52 | -------------------------------------------------------------------------------- /docs/cmd/gsemver_completion_zsh.md: -------------------------------------------------------------------------------- 1 | ## gsemver completion zsh 2 | 3 | Generate the autocompletion script for zsh 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the zsh shell. 8 | 9 | If shell completion is not already enabled in your environment you will need 10 | to enable it. You can execute the following once: 11 | 12 | echo "autoload -U compinit; compinit" >> ~/.zshrc 13 | 14 | To load completions in your current shell session: 15 | 16 | source <(gsemver completion zsh) 17 | 18 | To load completions for every new session, execute once: 19 | 20 | #### Linux: 21 | 22 | gsemver completion zsh > "${fpath[1]}/_gsemver" 23 | 24 | #### macOS: 25 | 26 | gsemver completion zsh > $(brew --prefix)/share/zsh/site-functions/_gsemver 27 | 28 | You will need to start a new shell for this setup to take effect. 29 | 30 | 31 | ``` 32 | gsemver completion zsh [flags] 33 | ``` 34 | 35 | ### Options 36 | 37 | ``` 38 | -h, --help help for zsh 39 | --no-descriptions disable completion descriptions 40 | ``` 41 | 42 | ### Options inherited from parent commands 43 | 44 | ``` 45 | -c, --config string config file (default is .gsemver.yaml) 46 | --log-level string Sets the logging level (fatal, error, warning, info, debug, trace) (default "info") 47 | -v, --verbose Enables verbose output by setting log level to debug. This is a shortland to --log-level debug. 48 | ``` 49 | 50 | ### SEE ALSO 51 | 52 | * [gsemver completion](gsemver_completion.md) - Generate the autocompletion script for the specified shell 53 | 54 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 3 | 4 | ### What this PR does / why we need it: 5 | 6 | 7 | ### Special notes for your reviewer 8 | 9 | ### How Has This Been Tested? 10 | 11 | 12 | 13 | 14 | ### Types of changes 15 | 16 | - [ ] Bug fix (non-breaking change which fixes an issue) 17 | - [ ] New feature (non-breaking change which adds functionality) 18 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 19 | 20 | ### Checklist: 21 | 22 | 23 | - [ ] My code follows the code style of this project. 24 | - [ ] My change requires a change to the documentation. 25 | - [ ] I have updated the documentation accordingly. 26 | - [ ] I have added tests to cover my changes. 27 | - [ ] All new and existing tests passed. 28 | 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/arnaud-deprez/gsemver 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/Masterminds/sprig/v3 v3.3.0 9 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 10 | github.com/pkg/errors v0.9.1 11 | github.com/sirupsen/logrus v1.9.3 12 | github.com/spf13/cobra v1.9.1 13 | github.com/spf13/viper v1.20.1 14 | github.com/stretchr/testify v1.10.0 15 | go.uber.org/mock v0.5.1 16 | ) 17 | 18 | require ( 19 | dario.cat/mergo v1.0.1 // indirect 20 | github.com/Masterminds/goutils v1.1.1 // indirect 21 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 22 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/fsnotify/fsnotify v1.9.0 // indirect 25 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/huandu/xstrings v1.5.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/mitchellh/copystructure v1.2.0 // indirect 30 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 31 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 34 | github.com/sagikazarmark/locafero v0.9.0 // indirect 35 | github.com/shopspring/decimal v1.4.0 // indirect 36 | github.com/sourcegraph/conc v0.3.0 // indirect 37 | github.com/spf13/afero v1.14.0 // indirect 38 | github.com/spf13/cast v1.7.1 // indirect 39 | github.com/spf13/pflag v1.0.6 // indirect 40 | github.com/subosito/gotenv v1.6.0 // indirect 41 | go.uber.org/multierr v1.11.0 // indirect 42 | golang.org/x/crypto v0.37.0 // indirect 43 | golang.org/x/sys v0.32.0 // indirect 44 | golang.org/x/text v0.24.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /docs/cmd/gsemver_version.md: -------------------------------------------------------------------------------- 1 | ## gsemver version 2 | 3 | Print the CLI version information 4 | 5 | ### Synopsis 6 | 7 | 8 | Show the version for gsemver. 9 | 10 | This will print a representation the version of gsemver. 11 | The output will look something like this: 12 | 13 | version.BuildInfo{Version:"0.1.0", GitCommit:"acfe51b15f9a1f12d47a20f88c29e5364916ae57", GitTreeState:"clean", BuildDate:"2019-07-02T07:44:00Z", GoVersion:"go1.12.6", Compiler:"gc", Platform:"darwin/amd64"} 14 | 15 | - Version is the semantic version of the release. 16 | - GitCommit is the SHA for the commit that this version was built from. 17 | - GitTreeState is "clean" if there are no local code changes when this binary was 18 | built, and "dirty" if the binary was built from locally modified code. 19 | - BuildDate is the build date in ISO-8601 format at UTC. 20 | - GoVersion is the go version with which it has been built. 21 | - Compiler is the go compiler with which it has been built. 22 | - Platform is the current OS platform on which it is running and for which it has been built. 23 | 24 | 25 | ``` 26 | gsemver version [flags] 27 | ``` 28 | 29 | ### Examples 30 | 31 | ``` 32 | 33 | # Print version of gsemver 34 | $ gsemver version 35 | 36 | ``` 37 | 38 | ### Options 39 | 40 | ``` 41 | -h, --help help for version 42 | --short print the version number 43 | ``` 44 | 45 | ### Options inherited from parent commands 46 | 47 | ``` 48 | -c, --config string config file (default is .gsemver.yaml) 49 | --log-level string Sets the logging level (fatal, error, warning, info, debug, trace) (default "info") 50 | -v, --verbose Enables verbose output by setting log level to debug. This is a shortland to --log-level debug. 51 | ``` 52 | 53 | ### SEE ALSO 54 | 55 | * [gsemver](gsemver.md) - CLI to manage semver compliant version from your git tags 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | env: 14 | GIT_COMMIT: ${{ github.sha }} 15 | steps: 16 | - name: Setup Git 17 | run: | 18 | git config --global user.name "${{ github.actor }}" 19 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 20 | 21 | - name: Extract Branch Name in GIT_BRANCH env 22 | run: | 23 | echo "GITHUB_HEAD_REF=${GITHUB_HEAD_REF} & GITHUB_REF=${GITHUB_REF}" 24 | if [ -n "$GITHUB_HEAD_REF" ] 25 | then 26 | echo "GIT_BRANCH=${GITHUB_HEAD_REF#refs/heads/}" >> $GITHUB_ENV 27 | else 28 | echo "GIT_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV 29 | fi 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: ^1.24 35 | id: go 36 | 37 | - name: Check out code into the Go module directory 38 | uses: actions/checkout@v4 39 | 40 | - name: Unshallow git repo 41 | run: git fetch --prune --unshallow 42 | 43 | - name: Build 44 | run: make --environment-overrides test-release 45 | 46 | - name: Test 47 | run: make --environment-overrides test-integration 48 | 49 | - uses: codecov/codecov-action@v5 50 | with: 51 | fail_ci_if_error: true # optional (default = false) 52 | token: ${{ secrets.CODECOV_TOKEN }} # required if not using the GitHub app 53 | 54 | - name: Release 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | run: make --environment-overrides release 58 | if: github.event_name != 'pull_request' && (github.ref_name == 'main' || startsWith(github.ref_name, 'release/')) 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing `gsemver` 2 | 3 | Thank you for contributing `gsemver` :tada: 4 | 5 | ## Issue templates 6 | 7 | Please use issue/PR templates for bugs or feature request which are inserted automatically. 8 | 9 | If you have a question or need to raise another kind of issue, then choose custom. 10 | 11 | Once you have raised an issue, you can also submit a Pull Request with a resolution. 12 | 13 | ## Commit Message Format 14 | 15 | A format influenced by [Conventional commits](https://www.conventionalcommits.org). 16 | 17 | ``` 18 | : 19 | 20 | [body] 21 | 22 | [footer] 23 | ``` 24 | 25 | ### Type 26 | 27 | Must be one of the following: 28 | 29 | * **docs:** Documention only changes 30 | * **ci:** Changes to our CI configuration files and scripts 31 | * **chore:** Updating Makefile etc, no production code changes 32 | * **feat:** A new feature 33 | * **fix:** A bug fix 34 | * **perf:** A code change that improves performance 35 | * **refactor:** A code change that neither fixes a bug nor adds a feature 36 | * **style:** Changes that do not affect the meaning of the code 37 | * **test:** Adding missing tests or correcting existing tests 38 | 39 | ### Footer 40 | 41 | The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any. 42 | 43 | The **footer** should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit **Closes**. 44 | 45 | **Breaking Changes** must start with the word `BREAKING CHANGE:` followed by a space and a description of it. 46 | 47 | ## Development 48 | 49 | 1. Fork (https://github.com/arnaud-deprez/gsemver) :tada: 50 | 1. Create a feature branch :coffee: 51 | 1. Run test suite with the `$ make test test-integration` command and confirm that it passes :zap: 52 | 1. Ensure the doc is up to date with your changes with the `$ make docs`command :+1: 53 | 1. Commit your changes :memo: 54 | 1. Rebase your local changes against the `main` branch and squash your commits if necessary :bulb: 55 | 1. Create new Pull Request :love_letter: 56 | 57 | Bugs, feature requests and comments are more than welcome in the [issues](https://github.com/arnaud-deprez/gsemver/issues). -------------------------------------------------------------------------------- /internal/git/commit_parser.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "github.com/arnaud-deprez/gsemver/pkg/git" 9 | ) 10 | 11 | var ( 12 | // constants 13 | separator = "-->8--" 14 | delimiter = "$_$" 15 | 16 | // fields 17 | hashField = "HASH" 18 | authorField = "AUTHOR" 19 | committerField = "COMMITTER" 20 | messageField = "MESSAGE" 21 | // subjectField = "SUBJECT" 22 | // bodyField = "BODY" 23 | 24 | // formats 25 | hashFormat = hashField + ":%H" 26 | authorFormat = authorField + ":%an\t%ae\t%at" 27 | committerFormat = committerField + ":%cn\t%ce\t%ct" 28 | messageFormat = messageField + ":%B" 29 | 30 | // log 31 | logFormat = separator + strings.Join([]string{ 32 | hashFormat, 33 | authorFormat, 34 | committerFormat, 35 | messageFormat, 36 | }, delimiter) 37 | ) 38 | 39 | type commitParser struct { 40 | logFormat string 41 | } 42 | 43 | func (p *commitParser) Parse(out string) []git.Commit { 44 | if p == nil { 45 | p = &commitParser{ 46 | logFormat: logFormat, 47 | } 48 | } 49 | 50 | lines := strings.Split(out, separator) 51 | lines = lines[1:] 52 | commits := make([]git.Commit, len(lines)) 53 | 54 | for i, line := range lines { 55 | commit := p.parseCommit(line) 56 | commits[i] = *commit 57 | } 58 | 59 | return commits 60 | } 61 | 62 | func (p *commitParser) parseCommit(input string) *git.Commit { 63 | commit := &git.Commit{} 64 | tokens := strings.Split(input, delimiter) 65 | 66 | for _, token := range tokens { 67 | firstSep := strings.Index(token, ":") 68 | field := token[0:firstSep] 69 | value := strings.TrimSpace(token[firstSep+1:]) 70 | 71 | switch field { 72 | case hashField: 73 | commit.Hash = git.Hash(value) 74 | case authorField: 75 | commit.Author = p.parseSignature(value) 76 | case committerField: 77 | commit.Committer = p.parseSignature(value) 78 | case messageField: 79 | commit.Message = value 80 | } 81 | } 82 | 83 | return commit 84 | } 85 | 86 | func (p *commitParser) parseSignature(input string) git.Signature { 87 | arr := strings.Split(input, "\t") 88 | ts, err := strconv.Atoi(arr[2]) 89 | if err != nil { 90 | ts = 0 91 | } 92 | 93 | return git.Signature{ 94 | Name: arr[0], 95 | Email: arr[1], 96 | When: time.Unix(int64(ts), 0), 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /cmd/docs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/cobra/doc" 9 | 10 | "github.com/arnaud-deprez/gsemver/internal/log" 11 | ) 12 | 13 | const ( 14 | docsDesc = ` 15 | Generate documentation files for gsemver. 16 | This command can generate documentation for gsemver in the following formats: 17 | - Markdown 18 | - Man pages 19 | It can also generate bash autocompletions. 20 | ` 21 | docsExample = ` 22 | # For Markdown documentation: 23 | gsemver docs markdown -dir docs/ 24 | 25 | # For Man page format: 26 | gsemver docs man -dir docs/ 27 | 28 | # For bash completion: 29 | gsemver docs bash -dir docs/ 30 | ` 31 | ) 32 | 33 | type docsOptions struct { 34 | *globalOptions 35 | dest string 36 | docTypeString string 37 | topCmd *cobra.Command 38 | } 39 | 40 | func newDocsCommands(globalOpts *globalOptions) *cobra.Command { 41 | o := &docsOptions{ 42 | globalOptions: globalOpts, 43 | } 44 | 45 | cmd := &cobra.Command{ 46 | Use: "docs", 47 | Short: "Generate documentation as markdown or man pages", 48 | Long: docsDesc, 49 | Example: docsExample, 50 | Hidden: true, 51 | ValidArgs: []string{"markdown", "man", "bash"}, 52 | Args: cobra.RangeArgs(0, 1), 53 | RunE: func(cmd *cobra.Command, args []string) error { 54 | o.topCmd = cmd.Root() 55 | if len(args) == 0 { 56 | o.docTypeString = "mardown" 57 | } else { 58 | o.docTypeString = args[0] 59 | } 60 | return o.run() 61 | }, 62 | } 63 | 64 | f := cmd.Flags() 65 | f.StringVar(&o.dest, "dir", "./", "directory to which documentation is written") 66 | 67 | return cmd 68 | } 69 | 70 | func (o *docsOptions) run() error { 71 | log.Debug("Run docs command with configuration: %#v", o) 72 | 73 | o.topCmd.DisableAutoGenTag = true 74 | switch o.docTypeString { 75 | case "markdown", "mdown", "md": 76 | return doc.GenMarkdownTree(o.topCmd, o.dest) 77 | case "man": 78 | manHdr := &doc.GenManHeader{Title: "gsemver", Section: "1"} 79 | return doc.GenManTree(o.topCmd, manHdr, o.dest) 80 | case "bash": 81 | return o.topCmd.GenBashCompletionFile(filepath.Join(o.dest, "completions.bash")) 82 | default: 83 | return errors.Errorf("unknown doc type %q. Try 'markdown' or 'man'", o.docTypeString) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/version/bump_branches_strategy_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBumpBranchesStrategyEncodingJson(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | testData := []struct { 15 | jsonVal string 16 | objVal *BumpBranchesStrategy 17 | }{ 18 | { 19 | `{"branchesPattern":"master","preRelease":false,"preReleaseOverwrite":false,"strategy":"AUTO"}`, 20 | NewDefaultBumpBranchesStrategy("master"), 21 | }, 22 | { 23 | `{"branchesPattern":"milestone-1.2","preRelease":true,"preReleaseTemplate":"{{.Branch}}-foo","preReleaseOverwrite":false,"strategy":"AUTO"}`, 24 | NewPreReleaseBumpBranchesStrategy("milestone-1.2", "{{.Branch}}-foo", false), 25 | }, 26 | { 27 | `{"branchesPattern":".*","preRelease":true,"preReleaseOverwrite":true,"buildMetadataTemplate":"{{.Branch}}.{{.Commits | len}}","strategy":"AUTO"}`, 28 | NewBumpAllBranchesStrategy(AUTO, true, "", true, "{{.Branch}}.{{.Commits | len}}"), 29 | }, 30 | } 31 | 32 | for idx, tc := range testData { 33 | t.Run(fmt.Sprintf("Case %d Marshal", idx), func(_ *testing.T) { 34 | out, err := json.Marshal(tc.objVal) 35 | assert.NoError(err) 36 | assert.JSONEq(tc.jsonVal, string(out)) 37 | }) 38 | 39 | t.Run(fmt.Sprintf("Case %d Unmarshal", idx), func(_ *testing.T) { 40 | var out BumpBranchesStrategy 41 | err := json.Unmarshal([]byte(tc.jsonVal), &out) 42 | assert.NoError(err) 43 | 44 | if tc.objVal.BranchesPattern != nil { 45 | assert.Equal(tc.objVal.BranchesPattern.String(), out.BranchesPattern.String()) 46 | } 47 | if tc.objVal.PreReleaseTemplate != nil { 48 | assert.Equal(tc.objVal.PreReleaseTemplate.Root.String(), out.PreReleaseTemplate.Root.String()) 49 | } 50 | assert.Equal(tc.objVal.PreReleaseOverwrite, out.PreReleaseOverwrite) 51 | if tc.objVal.BuildMetadataTemplate != nil { 52 | assert.Equal(tc.objVal.BuildMetadataTemplate.Root.String(), out.BuildMetadataTemplate.Root.String()) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func ExampleBumpBranchesStrategy_GoString() { 59 | s := NewBumpBranchesStrategy(AUTO, ".*", true, "foo", true, "bar") 60 | fmt.Printf("%#v\n", s) 61 | // Output: version.BumpBranchesStrategy{Strategy: AUTO, BranchesPattern: ®exp.Regexp{expr: ".*"}, PreRelease: true, PreReleaseTemplate: &template.Template{text: "foo"}, PreReleaseOverwrite: true, BuildMetadataTemplate: &template.Template{text: "bar"}} 62 | } 63 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/arnaud-deprez/gsemver/internal/log" 9 | "github.com/arnaud-deprez/gsemver/internal/version" 10 | ) 11 | 12 | const ( 13 | versionDesc = ` 14 | Show the version for gsemver. 15 | 16 | This will print a representation the version of gsemver. 17 | The output will look something like this: 18 | 19 | version.BuildInfo{Version:"0.1.0", GitCommit:"acfe51b15f9a1f12d47a20f88c29e5364916ae57", GitTreeState:"clean", BuildDate:"2019-07-02T07:44:00Z", GoVersion:"go1.12.6", Compiler:"gc", Platform:"darwin/amd64"} 20 | 21 | - Version is the semantic version of the release. 22 | - GitCommit is the SHA for the commit that this version was built from. 23 | - GitTreeState is "clean" if there are no local code changes when this binary was 24 | built, and "dirty" if the binary was built from locally modified code. 25 | - BuildDate is the build date in ISO-8601 format at UTC. 26 | - GoVersion is the go version with which it has been built. 27 | - Compiler is the go compiler with which it has been built. 28 | - Platform is the current OS platform on which it is running and for which it has been built. 29 | ` 30 | versionExample = ` 31 | # Print version of gsemver 32 | $ gsemver version 33 | ` 34 | ) 35 | 36 | // newVersionCommands create the version commands 37 | func newVersionCommands(globalOpts *globalOptions) *cobra.Command { 38 | options := &versionOptions{ 39 | globalOptions: globalOpts, 40 | } 41 | 42 | cmd := &cobra.Command{ 43 | Use: "version", 44 | Short: "Print the CLI version information", 45 | Long: versionDesc, 46 | Example: versionExample, 47 | Args: cobra.ExactArgs(0), 48 | RunE: func(_ *cobra.Command, _ []string) error { 49 | return options.run() 50 | }, 51 | } 52 | 53 | options.addVersionFlags(cmd) 54 | 55 | return cmd 56 | } 57 | 58 | type versionOptions struct { 59 | *globalOptions 60 | short bool 61 | } 62 | 63 | func (o *versionOptions) addVersionFlags(cmd *cobra.Command) { 64 | cmd.Flags().BoolVar(&o.short, "short", false, "print the version number") 65 | 66 | o.Cmd = cmd 67 | } 68 | 69 | func (o *versionOptions) run() error { 70 | log.Debug("Run version command with configuration: %#v", o) 71 | 72 | fmt.Fprintln(o.ioStreams.Out, formatVersion(o.short)) 73 | return nil 74 | } 75 | 76 | func formatVersion(short bool) string { 77 | if short { 78 | return version.GetVersion() 79 | } 80 | return fmt.Sprintf("%#v", version.Get()) 81 | } 82 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func init() { 11 | f := &log.TextFormatter{} 12 | f.ForceColors = true 13 | log.SetFormatter(f) 14 | } 15 | 16 | // Level type 17 | type Level uint32 18 | 19 | // These are the different logging levels. You can set the logging level to log 20 | // on your instance of logger, obtained with `logrus.New()`. 21 | const ( 22 | // FatalLevel level, highest level of severity. Logs and then calls `logger.Exit(1)`. It will exit even if the 23 | // logging level is set to Panic. 24 | FatalLevel Level = iota + 1 25 | // ErrorLevel level. Logs. Used for errors that should definitely be noted. 26 | // Commonly used for hooks to send errors to an error tracking service. 27 | ErrorLevel 28 | // WarnLevel level. Non-critical entries that deserve eyes. 29 | WarnLevel 30 | // InfoLevel level. General operational entries about what's going on inside the 31 | // application. 32 | InfoLevel 33 | // DebugLevel level. Usually only enabled when debugging. Very verbose logging. 34 | DebugLevel 35 | // TraceLevel level. Designates finer-grained informational events than the Debug. 36 | TraceLevel 37 | ) 38 | 39 | // Trace log formatted message at trace level 40 | func Trace(format string, args ...interface{}) { 41 | log.Tracef(format, args...) 42 | } 43 | 44 | // Debug log formatted message at debug level 45 | func Debug(format string, args ...interface{}) { 46 | log.Debugf(format, args...) 47 | } 48 | 49 | // Info log formatted message at info level 50 | func Info(format string, args ...interface{}) { 51 | log.Infof(format, args...) 52 | } 53 | 54 | // Warn log formatted message at info level 55 | func Warn(format string, args ...interface{}) { 56 | log.Warnf(format, args...) 57 | } 58 | 59 | // Error log formatted message at error level 60 | func Error(format string, args ...interface{}) { 61 | log.Errorf(format, args...) 62 | } 63 | 64 | // Fatal log formatted message at fatal level 65 | // Calls os.Exit(1) after logging 66 | func Fatal(format string, args ...interface{}) { 67 | log.Errorf(format, args...) 68 | os.Exit(1) 69 | } 70 | 71 | // SetLevel sets the standard logger level. 72 | func SetLevel(level Level) { 73 | log.SetLevel(log.Level(level)) 74 | } 75 | 76 | // SetLevelS sets the standard logger level from string. 77 | // Level can be: trace, debug, info, error or fatal 78 | func SetLevelS(level string) { 79 | l, err := log.ParseLevel(strings.ToLower(level)) 80 | if err != nil { 81 | Fatal("Cannot configure logger caused by %v", err) 82 | } 83 | log.SetLevel(l) 84 | } 85 | 86 | // IsLevelEnabled checks if the log level of the standard logger is greater than the level param 87 | func IsLevelEnabled(level Level) bool { 88 | return log.IsLevelEnabled(log.Level(level)) 89 | } 90 | -------------------------------------------------------------------------------- /internal/command/command_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestNew(t *testing.T) { 15 | execCmd := New("vim --noplugin") 16 | assert.Equal(t, "vim", execCmd.Name) 17 | assert.Equal(t, 1, len(execCmd.Args)) 18 | assert.Equal(t, "--noplugin", execCmd.Args[0]) 19 | } 20 | 21 | func TestNewWithVarArgs(t *testing.T) { 22 | execCmd := NewWithVarArgs("vim", "--noplugin") 23 | assert.Equal(t, "vim", execCmd.Name) 24 | assert.Equal(t, 1, len(execCmd.Args)) 25 | assert.Equal(t, "--noplugin", execCmd.Args[0]) 26 | } 27 | 28 | func TestWithDir(t *testing.T) { 29 | execCmd := New("git") 30 | path, _ := filepath.Abs("") 31 | execCmd.InDir(path) 32 | assert.Equal(t, path, execCmd.Dir) 33 | } 34 | 35 | func TestWithArg(t *testing.T) { 36 | execCmd := New("git") 37 | execCmd.WithArg("command").WithArg("--amend").WithArg("-m").WithArg(`""`) 38 | assert.Equal(t, "git", execCmd.Name) 39 | assert.Equal(t, 4, len(execCmd.Args)) 40 | } 41 | 42 | func TestWithArgs(t *testing.T) { 43 | execCmd := New("git") 44 | //WithArgs reset the array 45 | execCmd.WithArg("command").WithArgs("--amend", "-m") 46 | assert.Equal(t, "git", execCmd.Name) 47 | assert.Equal(t, 2, len(execCmd.Args)) 48 | } 49 | 50 | func TestWithEnvVariable(t *testing.T) { 51 | assert := assert.New(t) 52 | execCmd := New("echo foo") 53 | execCmd.WithEnvVariable("FOO", "BAR") 54 | assert.Equal("echo", execCmd.Name) 55 | execCmd.Run() 56 | assert.False(execCmd.DidError()) 57 | assert.Contains(execCmd.Env, "FOO") 58 | assert.Equal("BAR", execCmd.Env["FOO"]) 59 | } 60 | 61 | func TestWithEnv(t *testing.T) { 62 | assert := assert.New(t) 63 | execCmd := New("echo foo") 64 | env := map[string]string{} 65 | env["TEST"] = "POC" 66 | //WithEnv reset the map 67 | execCmd.WithEnvVariable("FOO", "BAR").WithEnv(env) 68 | assert.Equal("echo", execCmd.Name) 69 | execCmd.Run() 70 | assert.False(execCmd.DidError()) 71 | assert.Equal(env, execCmd.Env) 72 | } 73 | 74 | func TestEcho(t *testing.T) { 75 | t.Parallel() 76 | assert := assert.New(t) 77 | cmd := New("echo foo") 78 | out, err := cmd.Run() 79 | assert.Nil(err) 80 | assert.False(cmd.DidError()) 81 | assert.Equal("foo", out) 82 | } 83 | 84 | func TestEchoWithStream(t *testing.T) { 85 | t.Parallel() 86 | assert := assert.New(t) 87 | cmd := New("echo foo") 88 | var out, err bytes.Buffer 89 | cmd.Out = &out 90 | cmd.Err = &err 91 | cmd.Run() 92 | assert.False(cmd.DidError()) 93 | assert.Equal("", strings.TrimSpace(err.String())) 94 | assert.Equal("foo", strings.TrimSpace(out.String())) 95 | } 96 | 97 | func TestUnknownCommand(t *testing.T) { 98 | t.Parallel() 99 | assert := assert.New(t) 100 | cmd := New("unknownCommand") 101 | _, err := cmd.Run() 102 | assert.NotNil(err) 103 | assert.True(cmd.DidError()) 104 | _, ok := cmd.Error().(Error) 105 | assert.True(ok) 106 | } 107 | 108 | func TestCommandTimedOut(t *testing.T) { 109 | t.Parallel() 110 | assert := assert.New(t) 111 | cmd := New("sleep 2").WithTimeout(500 * time.Millisecond) 112 | out, err := cmd.Run() 113 | assert.NotNil(err) 114 | assert.True(cmd.DidError()) 115 | assert.Equal(fmt.Sprintf("Failed to run '%s' command in directory '%s', output: '%s' caused by: 'Command timed out after %.2f seconds'", cmd.String(), cmd.Dir, out, cmd.Timeout.Seconds()), err.Error()) 116 | _, ok := cmd.Error().(Error) 117 | assert.True(ok) 118 | } 119 | -------------------------------------------------------------------------------- /internal/git/git_repo_impl.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/arnaud-deprez/gsemver/internal/command" 10 | "github.com/arnaud-deprez/gsemver/internal/log" 11 | "github.com/arnaud-deprez/gsemver/pkg/git" 12 | "github.com/arnaud-deprez/gsemver/pkg/version" 13 | ) 14 | 15 | const ( 16 | gitRepoBranchEnv = "GIT_BRANCH" 17 | ) 18 | 19 | type gitRepoCLI struct { 20 | version.GitRepo 21 | dir string 22 | commitParser *commitParser 23 | } 24 | 25 | // FetchTags implements version.GitRepo.FetchTags 26 | func (g *gitRepoCLI) FetchTags() error { 27 | _, err := gitCmd(g). 28 | WithArgs( 29 | "fetch", 30 | "--tags", 31 | ).Run() 32 | return err 33 | } 34 | 35 | // GetCommits implements version.GitRepo.Getcommits 36 | func (g *gitRepoCLI) GetCommits(from string, to string) ([]git.Commit, error) { 37 | rev := parseRev(from, to) 38 | out, err := gitCmd(g). 39 | WithArgs( 40 | "log", 41 | rev, 42 | "--no-decorate", 43 | "--pretty="+g.commitParser.logFormat, 44 | ).Run() 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return g.commitParser.Parse(out), nil 51 | } 52 | 53 | // CountCommits implements version.GitRepo.CountCommits 54 | func (g *gitRepoCLI) CountCommits(from string, to string) (int, error) { 55 | rev := parseRev(from, to) 56 | cmd := gitCmd(g).WithArgs("rev-list", "--ancestry-path", "--count", rev) 57 | out, err := cmd.Run() 58 | if err != nil { 59 | return -1, err 60 | } 61 | count, err := strconv.Atoi(out) 62 | if err != nil { 63 | return -1, err 64 | } 65 | return count, err 66 | } 67 | 68 | // GetLastRelativeTag - use git describe to retrieve the last relative tag 69 | func (g *gitRepoCLI) GetLastRelativeTag(rev string) (git.Tag, error) { 70 | cmd := gitCmd(g).WithArgs("describe", "--tags", "--abbrev=0", "--match", "*[0-9]*.[0-9]*.[0-9]*", "--first-parent", rev) 71 | out, err := cmd.Run() 72 | if err != nil { 73 | return git.Tag{}, err 74 | } 75 | return git.Tag{Name: strings.TrimSpace(out)}, nil 76 | } 77 | 78 | // GetCurrentBranch - use git symbolic-ref to retrieve the current branch name 79 | func (g *gitRepoCLI) GetCurrentBranch() (string, error) { 80 | branch, err := gitCmd(g). 81 | WithArgs("symbolic-ref", "--short", "HEAD"). 82 | Run() 83 | 84 | // Then it is probably because we are in detached mode in CI server. 85 | if err != nil { 86 | // Most of the time during CI build, the build occurred in a detached HEAD state. 87 | // And so we can retrieve the current branch name from environment variable. 88 | branchFromEnv := getCurrentBranchFromEnv() 89 | if branchFromEnv == "" { 90 | return "", fmt.Errorf("unable to retrieve branch name from `git symbolic-ref HEAD` nor %s environment variable", gitRepoBranchEnv) 91 | } 92 | return branchFromEnv, nil 93 | } 94 | 95 | return branch, nil 96 | } 97 | 98 | func getCurrentBranchFromEnv() string { 99 | // We will use CI GIT_BRANCH environment variable. 100 | // This need to be mapped with real environment variable from your CI server. 101 | // TODO: eventually add support for most CI environment variable out of the box. 102 | log.Trace("GitRepo: retrieve branch name from %s env variable", gitRepoBranchEnv) 103 | return strings.TrimSpace(os.Getenv(gitRepoBranchEnv)) 104 | } 105 | 106 | func gitCmd(g *gitRepoCLI) *command.Command { 107 | return command.New("git").InDir(g.dir) 108 | } 109 | 110 | func parseRev(from string, to string) string { 111 | if to == "" { 112 | to = "HEAD" 113 | } 114 | if from == "" { 115 | return to 116 | } 117 | return fmt.Sprintf("%s..%s", from, to) 118 | } 119 | -------------------------------------------------------------------------------- /cmd/gsemver.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | 11 | log "github.com/arnaud-deprez/gsemver/internal/log" 12 | ) 13 | 14 | const ( 15 | optionConfig = "config" 16 | optionVerbose = "verbose" 17 | optionLogLevel = "log-level" 18 | ) 19 | 20 | var ( 21 | globalUsage = `Simple CLI to manage semver compliant version from your git tags 22 | ` 23 | globalOpts *globalOptions 24 | ) 25 | 26 | // ioStreams provides the standard names for iostreams. This is useful for embedding and for unit testing. 27 | // Inconsistent and different names make it hard to read and review code 28 | type ioStreams struct { 29 | // In think, os.Stdin 30 | In io.Reader 31 | // Out think, os.Stdout 32 | Out io.Writer 33 | // ErrOut think, os.Stderr 34 | ErrOut io.Writer 35 | } 36 | 37 | // newIOStreams creates a IOStreams 38 | func newIOStreams(in io.Reader, out, err io.Writer) *ioStreams { 39 | return &ioStreams{ 40 | In: in, 41 | Out: out, 42 | ErrOut: err, 43 | } 44 | } 45 | 46 | // globalOptions provides the global options of the CLI 47 | type globalOptions struct { 48 | // Cmd is the current *cobra.Command 49 | Cmd *cobra.Command 50 | // Args contains all the non options args for the command 51 | Args []string 52 | // CurrentDir is the directory from where the command has been executed. 53 | CurrentDir string 54 | // Verbose enables verbose output 55 | Verbose bool 56 | // LogLevel sets the log level (panic, fatal, error, warning, info, debug) 57 | LogLevel string 58 | // ConfigFile sets the config file to use 59 | ConfigFile string 60 | // ioStreams contains the input, output and error stream 61 | ioStreams *ioStreams 62 | } 63 | 64 | // newDefaultRootCommand creates the `gsemver` command with default arguments 65 | func newDefaultRootCommand() *cobra.Command { 66 | return newRootCommand(os.Stdin, os.Stdout, os.Stderr) 67 | } 68 | 69 | // newRootCommand creates the `gsemver` command with args 70 | func newRootCommand(in io.Reader, out, errout io.Writer) *cobra.Command { 71 | cmds := &cobra.Command{ 72 | Use: "gsemver", 73 | Short: "CLI to manage semver compliant version from your git tags", 74 | Long: globalUsage, 75 | Run: runHelp, 76 | } 77 | 78 | // create global configuration 79 | globalOpts = &globalOptions{ioStreams: newIOStreams(in, out, errout)} 80 | // initialize configuration 81 | cobra.OnInitialize(initConfig) 82 | // commonOpts holds the global flags that will be shared/inherited by all sub-commands created bellow 83 | globalOpts.addGlobalFlags(cmds) 84 | 85 | cmds.AddCommand( 86 | newBumpCommands(globalOpts), 87 | newVersionCommands(globalOpts), 88 | // Hidden documentation generator command: 'helm docs' 89 | newDocsCommands(globalOpts), 90 | ) 91 | return cmds 92 | } 93 | 94 | func initConfig() { 95 | if globalOpts.ConfigFile != "" { 96 | // Use config file from the flag. 97 | viper.SetConfigFile(globalOpts.ConfigFile) 98 | } else { 99 | // Find home directory. 100 | home, err := os.UserHomeDir() 101 | if err != nil { 102 | log.Fatal("Unable to home directory: %v", err) 103 | } 104 | 105 | // Search config in home directory with name ".cobra" (without extension). 106 | viper.AddConfigPath(".") 107 | viper.AddConfigPath(home) 108 | viper.SetConfigName(".gsemver") 109 | } 110 | 111 | viper.AutomaticEnv() 112 | 113 | if err := viper.ReadInConfig(); err == nil { 114 | log.Debug("Using config file: %s", viper.ConfigFileUsed()) 115 | } 116 | } 117 | 118 | // addGlobalFlags adds the common flags to the given command 119 | func (o *globalOptions) addGlobalFlags(cmd *cobra.Command) { 120 | cmd.PersistentFlags().StringVarP(&o.ConfigFile, optionConfig, "c", "", "config file (default is .gsemver.yaml)") 121 | cmd.PersistentFlags().BoolVarP(&o.Verbose, optionVerbose, "v", false, "Enables verbose output by setting log level to debug. This is a shortland to --log-level debug.") 122 | cmd.PersistentFlags().StringVarP(&o.LogLevel, optionLogLevel, "", "info", "Sets the logging level (fatal, error, warning, info, debug, trace)") 123 | 124 | dir, err := os.Getwd() 125 | if err != nil { 126 | log.Fatal("Unable to retrieve working directory: %v", err) 127 | } 128 | 129 | o.CurrentDir = dir 130 | o.Cmd = cmd 131 | } 132 | 133 | func (o *globalOptions) configureLogger() { 134 | if o.Verbose && strings.ToLower(o.LogLevel) != "trace" { 135 | log.SetLevel(log.DebugLevel) 136 | } else { 137 | log.SetLevelS(o.LogLevel) 138 | } 139 | } 140 | 141 | func runHelp(cmd *cobra.Command, _ []string) { 142 | cmd.Help() 143 | } 144 | 145 | // Run runs the command 146 | func Run() error { 147 | cmd := newDefaultRootCommand() 148 | return cmd.Execute() 149 | } 150 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILDDIR := $(CURDIR)/build 2 | BINDIR := $(BUILDDIR)/bin 3 | DIST_DIRS := find * -type d -exec 4 | BINNAME ?= gsemver 5 | 6 | GO_NOMOD := GO111MODULE=off go 7 | GOPATH := $(shell go env GOPATH) 8 | MOCKGEN := $(GOPATH)/bin/mockgen 9 | GOIMPORTS := $(GOPATH)/bin/goimports 10 | GOLANGCI_LINT := $(GOPATH)/bin/golangci-lint 11 | GIT_CHGLOG := $(GOPATH)/bin/git-chglog 12 | 13 | # go option 14 | PKG := ./... 15 | TAGS := 16 | TESTS := . 17 | TESTFLAGS := 18 | LDFLAGS := -w -s 19 | GOFLAGS := 20 | SRC := $(shell find . -type f -name '*.go' -print) 21 | 22 | # Required for globs to work correctly 23 | SHELL := /bin/bash 24 | 25 | # use gsemver to retrieve version 26 | GIT_BRANCH ?= $(shell git symbolic-ref --short HEAD) 27 | GIT_COMMIT ?= $(shell git rev-parse HEAD) 28 | VERSION = $(shell go run internal/release/main.go) 29 | GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") 30 | BUILD_DATE = $(shell date -u +%Y-%m-%dT%H:%M:%SZ) 31 | LAST_TAG = $(shell git describe --tags --abbrev=0 --first-parent --match v[0-9]*.[0-9]*.[0-9]* $(GIT_COMMIT)~ || echo "") 32 | 33 | LDFLAGS += -X github.com/arnaud-deprez/gsemver/internal/version.version=v$(VERSION) 34 | LDFLAGS += -X github.com/arnaud-deprez/gsemver/internal/version.gitCommit=$(GIT_COMMIT) 35 | LDFLAGS += -X github.com/arnaud-deprez/gsemver/internal/version.gitTreeState=$(GIT_DIRTY) 36 | LDFLAGS += -X github.com/arnaud-deprez/gsemver/internal/version.buildDate=$(BUILD_DATE) 37 | 38 | 39 | .PHONY: all 40 | all: build docs release 41 | 42 | # ------------------------------------------------------------------------------ 43 | # dependencies 44 | $(MOCKGEN): 45 | go install go.uber.org/mock/mockgen@v0.5.1 46 | 47 | $(GOLANGCI_LINT): 48 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(GOPATH)/bin v2.0.2 49 | 50 | $(GOIMPORTS): 51 | go install golang.org/x/tools/cmd/goimports@latest 52 | 53 | $(GIT_CHGLOG): 54 | go install github.com/git-chglog/git-chglog/cmd/git-chglog@latest 55 | 56 | # ------------------------------------------------------------------------------ 57 | # build 58 | 59 | build: download-dependencies generate $(BINDIR)/$(BINNAME) docs 60 | 61 | download-dependencies: 62 | go mod download 63 | 64 | .PHONY: generate 65 | generate: download-dependencies $(MOCKGEN) 66 | go generate ./... 67 | 68 | .PHONY: build 69 | $(BINDIR)/$(BINNAME): generate $(SRC) 70 | go build $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(BINDIR)/$(BINNAME) github.com/arnaud-deprez/gsemver 71 | 72 | .PHONY: download-dependencies docs 73 | docs: $(BINDIR)/$(BINNAME) 74 | mkdir -p docs/cmd 75 | $(BINDIR)/$(BINNAME) docs markdown --dir docs/cmd 76 | 77 | # ------------------------------------------------------------------------------ 78 | # test 79 | 80 | .PHONY: test 81 | test: build 82 | test: TESTFLAGS += -race -v 83 | test: test-style 84 | test: test-coverage 85 | 86 | .PHONY: test-coverage 87 | test-coverage: 88 | @echo 89 | @echo "==> Running unit tests with coverage <==" 90 | scripts/coverage.sh 91 | 92 | .PHONY: test-style 93 | test-style: $(GOLANGCI_LINT) 94 | $(GOLANGCI_LINT) run 95 | 96 | .PHONY: test-unit 97 | test-unit: test 98 | @echo 99 | @echo "==> Running unit tests <==" 100 | go test $(GOFLAGS) -run $(TESTS) $(PKG) -short $(TESTFLAGS) 101 | 102 | .PHONY: test-integration 103 | test-integration: test 104 | @echo 105 | @echo "==> Running integration tests <==" 106 | go test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) -failfast 107 | 108 | # .PHONY: verify-docs 109 | # verify-docs: build 110 | # @scripts/verify-docs.sh 111 | 112 | .PHONY: format 113 | format: $(GOIMPORTS) generate 114 | go list -f '{{.Dir}}' ./... | xargs $(GOIMPORTS) -w -local github.com/arnaud-deprez/gsemver 115 | 116 | # ------------------------------------------------------------------------------ 117 | # release 118 | 119 | .PHONY: test-release 120 | test-release: $(GIT_CHGLOG) 121 | @echo "Test release $(VERSION) on $(GIT_BRANCH), last version was $(LAST_TAG)" 122 | # Because of https://github.com/git-chglog/git-chglog/issues/45, it will generate changelog for both LAST_TAG and VERSION 123 | export GIT_DIRTY=$(GIT_DIRTY) && curl -sL https://git.io/goreleaser | bash -s -- release --config=./.goreleaser.yml --snapshot --skip=publish --verbose --clean --release-notes <($(GIT_CHGLOG) --next-tag v$(VERSION) $(strip $(LAST_TAG))..) 124 | 125 | .PHONY: release 126 | release: $(GIT_CHGLOG) 127 | @echo "Release $(VERSION) on $(GIT_BRANCH), last version was $(LAST_TAG)" 128 | git tag -am "Release v$(VERSION) by ci script" v$(VERSION) 129 | # This is a bit weird: https://github.com/git-chglog/git-chglog/issues/45 130 | export GIT_DIRTY=$(GIT_DIRTY) && curl -sL https://git.io/goreleaser | bash -s -- release --config=./.goreleaser.yml --clean --release-notes <($(GIT_CHGLOG) v$(VERSION)) 131 | 132 | # ------------------------------------------------------------------------------ 133 | # clean 134 | 135 | .PHONY: clean 136 | clean: 137 | @rm -rf $(BUILDDIR) 138 | -------------------------------------------------------------------------------- /docs/cmd/gsemver_bump.md: -------------------------------------------------------------------------------- 1 | ## gsemver bump 2 | 3 | Bump to next version 4 | 5 | ### Synopsis 6 | 7 | 8 | This will compute and print the next semver compatible version of your project based on commits logs, tags and current branch. 9 | 10 | The version will look like ..[-][+] where: 11 | - X is the Major number 12 | - Y is the Minor number 13 | - Z is the Patch number 14 | - pre-release is the pre-release identifiers (optional) 15 | - metadata is the build metadata identifiers (optional) 16 | 17 | More info on the semver spec https://semver.org/spec/v2.0.0.html. 18 | 19 | It can work in 2 fashions, the automatic or manual. 20 | 21 | Automatic way assumes: 22 | - your previous tags are semver compatible. 23 | - you follow some conventions in your commit and ideally https://www.conventionalcommits.org 24 | - you follow some branch convention for your releases (eg. a release should be done on main, master or release/* branches) 25 | 26 | Base on this information, it is able to compute the next version. 27 | 28 | The manual way is less restrictive and just assumes your previous tags are semver compatible. 29 | 30 | 31 | ``` 32 | gsemver bump [strategy] [flags] 33 | ``` 34 | 35 | ### Examples 36 | 37 | ``` 38 | 39 | # To bump automatically: 40 | gsemver bump 41 | 42 | # Or more explicitly 43 | gsemver bump auto 44 | 45 | # To bump manually the major number: 46 | gsemver bump major 47 | 48 | # To bump manually the minor number: 49 | gsemver bump minor 50 | 51 | # To bump manually the patch number: 52 | gsemver bump patch 53 | 54 | # To use a pre-release version 55 | gsemver bump --pre-release alpha 56 | # Or with go-template 57 | gsemver bump --pre-release "alpha-{{.Branch}}" 58 | 59 | # To use a pre-release version without indexation (maven like SNAPSHOT) 60 | gsemver bump minor --pre-release SNAPSHOT --pre-release-overwrite true 61 | 62 | # To use version with build metadata 63 | gsemver bump --build-metadata "issue-1.build.1" 64 | # Or with go-template 65 | gsemver bump --build-metadata "{{(.Commits | first).Hash.Short}}" 66 | 67 | # To use bump auto with one or many branch strategies 68 | gsemver bump --branch-strategy='{"branchesPattern":"^miletone-1.1$","preReleaseTemplate":"beta"}' --branch-strategy='{"branchesPattern":"^miletone-2.0$","preReleaseTemplate":"alpha"}' 69 | 70 | ``` 71 | 72 | ### Options 73 | 74 | ``` 75 | --branch-strategy stringArray Use branch-strategy will set a strategy for a set of branches. 76 | The strategy is defined in json and looks like {"branchesPattern":"^milestone-.*$", "preReleaseTemplate":"alpha"} for example. 77 | This will use pre-release alpha version for every milestone-* branches. 78 | You can find all available options https://godoc.org/github.com/arnaud-deprez/gsemver/pkg/version#BumpBranchesStrategy 79 | --build-metadata string Use build metadata template which will give something like X.Y.Z+. 80 | You can also use go-template expression with context https://godoc.org/github.com/arnaud-deprez/gsemver/pkg/version#Context and http://masterminds.github.io/sprig functions. 81 | This flag cannot be used with --pre-release* flags and take precedence over them. 82 | -h, --help help for bump 83 | --major-pattern string Use major-pattern option to define your regular expression to match a breaking change commit message 84 | --minor-pattern string Use major-pattern option to define your regular expression to match a minor change commit message 85 | --pre-release string Use pre-release template version such as 'alpha' which will give a version like 'X.Y.Z-alpha.N'. 86 | If pre-release flag is present but does not contain template value, it will give a version like 'X.Y.Z-N' where 'N' is the next pre-release increment for the version 'X.Y.Z'. 87 | You can also use go-template expression with context https://godoc.org/github.com/arnaud-deprez/gsemver/pkg/version#Context and http://masterminds.github.io/sprig functions. 88 | This flag is not taken into account if --build-metadata is set. 89 | --pre-release-overwrite X.Y.Z-SNAPSHOT Use pre-release overwrite option to remove the pre-release identifier suffix which will give a version like X.Y.Z-SNAPSHOT if pre-release=SNAPSHOT 90 | ``` 91 | 92 | ### Options inherited from parent commands 93 | 94 | ``` 95 | -c, --config string config file (default is .gsemver.yaml) 96 | --log-level string Sets the logging level (fatal, error, warning, info, debug, trace) (default "info") 97 | -v, --verbose Enables verbose output by setting log level to debug. This is a shortland to --log-level debug. 98 | ``` 99 | 100 | ### SEE ALSO 101 | 102 | * [gsemver](gsemver.md) - CLI to manage semver compliant version from your git tags 103 | 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [discussions](https://github.com/arnaud-deprez/gsemver/discussions). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /pkg/version/bump_branches_strategy.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/arnaud-deprez/gsemver/internal/utils" 11 | ) 12 | 13 | // NewBumpBranchesStrategy creates a new BumpBranchesStrategy 14 | func NewBumpBranchesStrategy(strategy BumpStrategyType, pattern string, preRelease bool, preReleaseTemplate string, preReleaseOverwrite bool, buildMetadataTemplate string) *BumpBranchesStrategy { 15 | return &BumpBranchesStrategy{ 16 | Strategy: strategy, 17 | BranchesPattern: regexp.MustCompile(pattern), 18 | PreRelease: preRelease, 19 | PreReleaseTemplate: utils.NewTemplate(preReleaseTemplate), 20 | PreReleaseOverwrite: preReleaseOverwrite, 21 | BuildMetadataTemplate: utils.NewTemplate(buildMetadataTemplate), 22 | } 23 | } 24 | 25 | // NewBumpAllBranchesStrategy creates a new BumpBranchesStrategy that matches all branches. 26 | func NewBumpAllBranchesStrategy(strategy BumpStrategyType, preRelease bool, preReleaseTemplate string, preReleaseOverwrite bool, buildMetadataTemplate string) *BumpBranchesStrategy { 27 | return NewBumpBranchesStrategy(strategy, ".*", preRelease, preReleaseTemplate, preReleaseOverwrite, buildMetadataTemplate) 28 | } 29 | 30 | // NewDefaultBumpBranchesStrategy creates a new BumpBranchesStrategy for pre-release version strategy. 31 | func NewDefaultBumpBranchesStrategy(pattern string) *BumpBranchesStrategy { 32 | return NewBumpBranchesStrategy(AUTO, pattern, false, "", false, "") 33 | } 34 | 35 | // NewPreReleaseBumpBranchesStrategy creates a new BumpBranchesStrategy for pre-release version strategy. 36 | func NewPreReleaseBumpBranchesStrategy(pattern string, preReleaseTemplate string, preReleaseOverwrite bool) *BumpBranchesStrategy { 37 | return NewBumpBranchesStrategy(AUTO, pattern, true, preReleaseTemplate, preReleaseOverwrite, "") 38 | } 39 | 40 | // NewBuildBumpBranchesStrategy creates a new BumpBranchesStrategy for build version strategy. 41 | func NewBuildBumpBranchesStrategy(pattern string, buildMetadataTemplate string) *BumpBranchesStrategy { 42 | return NewBumpBranchesStrategy(AUTO, pattern, false, "", false, buildMetadataTemplate) 43 | } 44 | 45 | // BumpBranchesStrategy allows you to configure the bump strategy option for a matching set of branches. 46 | type BumpBranchesStrategy struct { 47 | // Strategy defines the strategy to use to bump the version. 48 | // It can be automatic (AUTO) or manual (MAJOR, MINOR, PATCH) 49 | Strategy BumpStrategyType `json:"strategy"` 50 | // BranchesPattern is the regex used to match against the current branch 51 | BranchesPattern *regexp.Regexp `json:"branchesPattern,omitempty"` 52 | // PreRelease defines if the bump strategy should generate a pre-release version 53 | PreRelease bool `json:"preRelease"` 54 | // PreReleaseTemplate defines the pre-release template for the next version 55 | // It can be alpha, beta, or a go-template expression 56 | PreReleaseTemplate *template.Template `json:"preReleaseTemplate,omitempty"` 57 | // PreReleaseOverwrite defines if a pre-release can be overwritten 58 | // If true, it will not append an index to the next version 59 | // If false, it will append an incremented index based on the previous same version of same class if any and 0 otherwise 60 | PreReleaseOverwrite bool `json:"preReleaseOverwrite"` 61 | // BuildMetadataTemplate defines the build metadata for the next version. 62 | // It can be a static value but it will usually be a go-template expression to guarantee uniqueness of each built version. 63 | BuildMetadataTemplate *template.Template `json:"buildMetadataTemplate,omitempty"` 64 | } 65 | 66 | // createVersionBumperFrom is an implementation for BumpBranchStrategy 67 | func (s *BumpBranchesStrategy) createVersionBumperFrom(bumper versionBumper, ctx *Context) versionBumper { 68 | return func(v Version) Version { 69 | // build-metadata and pre-release are exclusives 70 | if s != nil && s.BuildMetadataTemplate != nil { 71 | return v.WithBuildMetadata(ctx.EvalTemplate(s.BuildMetadataTemplate)) 72 | } 73 | if s != nil && s.PreRelease { 74 | return v.BumpPreRelease(ctx.EvalTemplate(s.PreReleaseTemplate), s.PreReleaseOverwrite, bumper) 75 | } 76 | return bumper(v) 77 | } 78 | } 79 | 80 | // GoString makes BumpBranchesStrategy satisfy the GoStringer interface. 81 | func (s BumpBranchesStrategy) GoString() string { 82 | var sb strings.Builder 83 | sb.WriteString("version.BumpBranchesStrategy{") 84 | sb.WriteString(fmt.Sprintf("Strategy: %v, ", s.Strategy)) 85 | sb.WriteString(fmt.Sprintf("BranchesPattern: ®exp.Regexp{expr: %q}, ", s.BranchesPattern)) 86 | sb.WriteString(fmt.Sprintf("PreRelease: %v, PreReleaseTemplate: &template.Template{text: %q}, PreReleaseOverwrite: %v, ", s.PreRelease, utils.TemplateToString(s.PreReleaseTemplate), s.PreReleaseOverwrite)) 87 | sb.WriteString(fmt.Sprintf("BuildMetadataTemplate: &template.Template{text: %q}", utils.TemplateToString(s.BuildMetadataTemplate))) 88 | sb.WriteString("}") 89 | return sb.String() 90 | } 91 | 92 | // MarshalJSON implements json encoding 93 | func (s *BumpBranchesStrategy) MarshalJSON() ([]byte, error) { 94 | type Alias BumpBranchesStrategy 95 | return json.Marshal(&struct { 96 | BranchesPattern string `json:"branchesPattern,omitempty"` 97 | PreReleaseTemplate string `json:"preReleaseTemplate,omitempty"` 98 | BuildMetadataTemplate string `json:"buildMetadataTemplate,omitempty"` 99 | *Alias 100 | }{ 101 | BranchesPattern: utils.RegexpToString(s.BranchesPattern), 102 | PreReleaseTemplate: utils.TemplateToString(s.PreReleaseTemplate), 103 | BuildMetadataTemplate: utils.TemplateToString(s.BuildMetadataTemplate), 104 | Alias: (*Alias)(s), 105 | }) 106 | } 107 | 108 | // UnmarshalJSON implements json decoding 109 | func (s *BumpBranchesStrategy) UnmarshalJSON(data []byte) error { 110 | type Alias BumpBranchesStrategy 111 | aux := struct { 112 | BranchesPattern string `json:"branchesPattern,omitempty"` 113 | PreReleaseTemplate string `json:"preReleaseTemplate,omitempty"` 114 | BuildMetadataTemplate string `json:"buildMetadataTemplate,omitempty"` 115 | *Alias 116 | }{ 117 | Alias: (*Alias)(s), 118 | } 119 | if err := json.Unmarshal(data, &aux); err != nil { 120 | return err 121 | } 122 | s.BranchesPattern = regexp.MustCompile(aux.BranchesPattern) 123 | s.PreReleaseTemplate = utils.NewTemplate(aux.PreReleaseTemplate) 124 | s.BuildMetadataTemplate = utils.NewTemplate(aux.BuildMetadataTemplate) 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /internal/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "time" 11 | 12 | shellquote "github.com/kballard/go-shellquote" 13 | 14 | "github.com/arnaud-deprez/gsemver/internal/log" 15 | ) 16 | 17 | // Command is a struct containing the details of an external command to be executed 18 | type Command struct { 19 | Name string 20 | Args []string 21 | Dir string 22 | In io.Reader 23 | Out io.Writer 24 | Err io.Writer 25 | Env map[string]string 26 | Timeout time.Duration 27 | _error error 28 | } 29 | 30 | // String method returns a string representation of the Command 31 | func (c Command) String() string { 32 | var builder strings.Builder 33 | builder.WriteString(c.Name) 34 | for _, arg := range c.Args { 35 | builder.WriteString(" ") 36 | builder.WriteString(arg) 37 | } 38 | return builder.String() 39 | } 40 | 41 | // Error is the error object encapsulating an error from a Command 42 | type Error struct { 43 | Command Command 44 | Output string 45 | cause error 46 | } 47 | 48 | func (c Error) Error() string { 49 | var sb strings.Builder 50 | fmt.Fprintf(&sb, "Failed to run '%s %s' command in directory '%s', output: '%s'", 51 | c.Command.Name, strings.Join(c.Command.SanitisedArgs(), " "), c.Command.Dir, c.Output) 52 | if c.cause != nil { 53 | fmt.Fprintf(&sb, " caused by: '%s'", c.cause.Error()) 54 | } 55 | return sb.String() 56 | } 57 | 58 | // New construct new command based on string 59 | func New(cmd string) *Command { 60 | cmds, err := shellquote.Split(cmd) 61 | if err != nil { 62 | log.Fatal("Failed to parse command %s due to %s", cmd, err) 63 | } 64 | 65 | return NewWithVarArgs(cmds...) 66 | } 67 | 68 | // NewWithVarArgs construct new command based on a string array 69 | func NewWithVarArgs(cmd ...string) *Command { 70 | if len(cmd) == 0 { 71 | log.Fatal("Cannot instantiate an empty command!") 72 | } 73 | return &Command{Name: cmd[0], Args: cmd[1:]} 74 | } 75 | 76 | // InDir Setter method for Dir to enable use of interface instead of Command struct 77 | func (c *Command) InDir(dir string) *Command { 78 | c.Dir = dir 79 | return c 80 | } 81 | 82 | // WithArg sets an argument into the args 83 | func (c *Command) WithArg(arg string) *Command { 84 | c.Args = append(c.Args, arg) 85 | return c 86 | } 87 | 88 | // WithArgs Setter method for Args to enable use of interface instead of Command struct 89 | func (c *Command) WithArgs(args ...string) *Command { 90 | c.Args = args 91 | return c 92 | } 93 | 94 | // SanitisedArgs sanitises any password arguments before printing the error string. 95 | // The actual sensitive argument is still present in the Command object 96 | func (c *Command) SanitisedArgs() []string { 97 | sanitisedArgs := make([]string, len(c.Args)) 98 | copy(sanitisedArgs, c.Args) 99 | for i, arg := range sanitisedArgs { 100 | if strings.Contains(strings.ToLower(arg), "password") && i < len(sanitisedArgs)-1 { 101 | // sanitise the subsequent argument to any 'password' fields 102 | sanitisedArgs[i+1] = "*****" 103 | } 104 | } 105 | return sanitisedArgs 106 | } 107 | 108 | // WithTimeout Setter method for Timeout to enable use of interface instead of Command struct 109 | func (c *Command) WithTimeout(timeout time.Duration) *Command { 110 | c.Timeout = timeout 111 | return c 112 | } 113 | 114 | // getTimeout private getter method that returns the current timeout duration or 3 minutes by default 115 | func (c *Command) getOrDefaultTimeout() time.Duration { 116 | // configure timeout, default is 3 minutes 117 | if c.Timeout == 0 { 118 | c.Timeout = 3 * time.Minute 119 | } 120 | return c.Timeout 121 | } 122 | 123 | // WithEnv Setter method for Env to enable use of interface instead of Command struct 124 | func (c *Command) WithEnv(env map[string]string) *Command { 125 | c.Env = env 126 | return c 127 | } 128 | 129 | // WithEnvVariable sets an environment variable into the environment 130 | func (c *Command) WithEnvVariable(name string, value string) *Command { 131 | if c.Env == nil { 132 | c.Env = map[string]string{} 133 | } 134 | c.Env[name] = value 135 | return c 136 | } 137 | 138 | // DidError returns a boolean if any error occurred in any execution of the command 139 | func (c *Command) DidError() bool { 140 | return c._error != nil 141 | } 142 | 143 | // Error returns the last error 144 | func (c *Command) Error() error { 145 | return c._error 146 | } 147 | 148 | // Run Execute the command without retrying on failure and block waiting for return values 149 | func (c *Command) Run() (string, error) { 150 | ctx, cancel := context.WithTimeout(context.Background(), c.getOrDefaultTimeout()) 151 | // The cancel should be deferred so resources are cleaned up 152 | defer cancel() 153 | 154 | r, e := c.RunWithContext(&ctx) 155 | 156 | // We want to check the context error to see if the timeout was executed. 157 | // The error returned by cmd.Output() will be OS specific based on what 158 | // happens when a process is killed. 159 | if ctx.Err() == context.DeadlineExceeded { 160 | err := Error{ 161 | Command: *c, 162 | cause: fmt.Errorf("Command timed out after %.2f seconds", c.Timeout.Seconds()), 163 | } 164 | c._error = err 165 | return "", err 166 | } 167 | 168 | if e != nil { 169 | c._error = e 170 | } 171 | return r, e 172 | } 173 | 174 | // RunWithContext private method executes the command and wait for the result 175 | func (c *Command) RunWithContext(ctx *context.Context) (string, error) { 176 | log.Trace("Command: run %q", c.String()) 177 | e := exec.CommandContext(*ctx, c.Name, c.Args...) 178 | if c.Dir != "" { 179 | e.Dir = c.Dir 180 | } 181 | // merge env in e *Cmd 182 | if len(c.Env) > 0 { 183 | m := map[string]string{} 184 | environ := os.Environ() 185 | for _, kv := range environ { 186 | paths := strings.SplitN(kv, "=", 2) 187 | if len(paths) == 2 { 188 | m[paths[0]] = paths[1] 189 | } 190 | } 191 | for k, v := range c.Env { 192 | m[k] = v 193 | } 194 | envVars := []string{} 195 | for k, v := range m { 196 | envVars = append(envVars, k+"="+v) 197 | } 198 | e.Env = envVars 199 | } 200 | 201 | if c.Out != nil { 202 | e.Stdout = c.Out 203 | } 204 | 205 | if c.Err != nil { 206 | e.Stderr = c.Err 207 | } 208 | 209 | // zero value of string is "" 210 | var text string 211 | var err error 212 | 213 | if c.Out != nil { 214 | err := e.Run() 215 | if err != nil { 216 | return text, Error{ 217 | Command: *c, 218 | cause: err, 219 | } 220 | } 221 | } else { 222 | data, err := e.CombinedOutput() 223 | output := string(data) 224 | text = strings.TrimSpace(output) 225 | if err != nil { 226 | return text, Error{ 227 | Command: *c, 228 | Output: text, 229 | cause: err, 230 | } 231 | } 232 | } 233 | 234 | return text, err 235 | } 236 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/arnaud-deprez/gsemver/internal/utils" 10 | ) 11 | 12 | var ( 13 | /* const */ versionRegex = regexp.MustCompile(`^v?([0-9]+)\.([0-9]+)\.([0-9]+)` + 14 | `(?:-([0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?` + 15 | `(?:\+([0-9A-Za-z\-]+(?:\.[0-9A-Za-z\-]+)*))?$`) 16 | /* const */ zeroVersion = Version{} 17 | /* const */ versionBumperIdentity = func(v Version) Version { return v } 18 | ) 19 | 20 | // NewVersion creates a new Version from a string representation 21 | func NewVersion(value string) (Version, error) { 22 | if value == "" { 23 | return zeroVersion, nil 24 | } 25 | 26 | m := versionRegex.FindStringSubmatch(value) 27 | if m == nil { 28 | return zeroVersion, newError("'%s' is not a semver compatible version", value) 29 | } 30 | 31 | major, _ := strconv.Atoi(m[1]) 32 | minor, _ := strconv.Atoi(m[2]) 33 | patch, _ := strconv.Atoi(m[3]) 34 | 35 | return Version{ 36 | Major: major, 37 | Minor: minor, 38 | Patch: patch, 39 | PreRelease: m[4], 40 | BuildMetadata: m[5], 41 | }, nil 42 | } 43 | 44 | // Version object to represent a SemVer version 45 | type Version struct { 46 | // Major represents the major (aka X) number in a semver version 47 | Major int `json:"major"` 48 | // Minor represents the minor (aka Y) number in a semver version 49 | Minor int `json:"minor"` 50 | // Patch represents the patch (aka Z) number in a semver version 51 | Patch int `json:"patch"` 52 | // PreRelease represents the optional pre-release information in a semver version 53 | PreRelease string `json:"preRelease,omitempty"` 54 | // BuildMetadata represents the optional build metadata in a semver version 55 | BuildMetadata string `json:"buildMetadata,omitempty"` 56 | } 57 | 58 | // String returns a string representation of a Version object. 59 | // The format is: major.minor.patch[-pre_release_identifiers][+build_metadata] 60 | func (v Version) String() string { 61 | var sb strings.Builder 62 | fmt.Fprintf(&sb, "%d.%d.%d", v.Major, v.Minor, v.Patch) 63 | if v.PreRelease != "" { 64 | sb.WriteString("-") 65 | sb.WriteString(v.PreRelease) 66 | } 67 | if v.BuildMetadata != "" { 68 | sb.WriteString("+") 69 | sb.WriteString(v.BuildMetadata) 70 | } 71 | return sb.String() 72 | } 73 | 74 | // IsUnstable returns true if the version is an early stage version. eg. 0.Y.Z 75 | func (v *Version) IsUnstable() bool { 76 | return v.Major == 0 77 | } 78 | 79 | // BumpMajor bump the major number of the version 80 | func (v Version) BumpMajor() Version { 81 | next := v 82 | // according to https://semver.org/#spec-item-11 83 | // Pre-release versions have a lower precedence than the associated normal version. 84 | // Build metadata SHOULD be ignored when determining version precedence. 85 | next.PreRelease = "" 86 | next.BuildMetadata = "" 87 | if v.PreRelease == "" || v.Minor != 0 || v.Patch != 0 { 88 | next.Major++ 89 | next.Minor = 0 90 | next.Patch = 0 91 | } 92 | 93 | return next 94 | } 95 | 96 | // BumpMinor bumps the minor number of the version 97 | func (v Version) BumpMinor() Version { 98 | next := v 99 | // according to https://semver.org/#spec-item-11 100 | // Pre-release versions have a lower precedence than the associated normal version. 101 | // Build metadata SHOULD be ignored when determining version precedence. 102 | next.PreRelease = "" 103 | next.BuildMetadata = "" 104 | if v.PreRelease == "" || v.Patch != 0 { 105 | next.Minor++ 106 | next.Patch = 0 107 | } 108 | return next 109 | } 110 | 111 | // BumpPatch bumps the patch number of the version 112 | func (v Version) BumpPatch() Version { 113 | next := v 114 | // according to https://semver.org/#spec-item-11 115 | // Pre-release versions have a lower precedence than the associated normal version. 116 | // Build metadata SHOULD be ignored when determining version precedence. 117 | next.PreRelease = "" 118 | next.BuildMetadata = "" 119 | if v.PreRelease == "" { 120 | next.Patch++ 121 | } 122 | return next 123 | } 124 | 125 | // BumpPreRelease bumps the pre-release identifiers 126 | func (v Version) BumpPreRelease(preRelease string, overwrite bool, semverBumper func(Version) Version) Version { 127 | // if no preRelease and overwrite, then it should return the current version. 128 | if preRelease == "" && overwrite { 129 | return v 130 | } 131 | 132 | next := v 133 | 134 | if semverBumper == nil { 135 | // by default bump minor if this is not yet a pre-release 136 | semverBumper = Version.BumpMinor 137 | } 138 | // extract desired identifiers 139 | desiredIdentifiers := extractIdentifiers(preRelease) 140 | 141 | if !v.IsPreRelease() { 142 | // bump MAJOR, MINOR or PATCH if it's not yet a pre-release 143 | next = semverBumper(v) 144 | } 145 | 146 | if overwrite { 147 | next.PreRelease = preRelease 148 | return next 149 | } 150 | 151 | if v.IsPreRelease() { 152 | if inc, err := v.GetPreReleaseIncrement(); err == nil && v.PreReleaseIdentifiersEqual(preRelease) { 153 | next.PreRelease = strings.Join(append(desiredIdentifiers, strconv.Itoa(inc+1)), ".") 154 | return next 155 | } 156 | // TODO: eventually compare if pre-release name is >= v.PreRelease 157 | } 158 | next.PreRelease = strings.Join(append(desiredIdentifiers, strconv.Itoa(0)), ".") 159 | return next 160 | } 161 | 162 | // IsPreRelease returns true if it's a pre-release version. eg 1.1.0-alpha.1 163 | func (v Version) IsPreRelease() bool { 164 | return v.PreRelease != "" 165 | } 166 | 167 | // GetPreReleaseIncrement returns the current pre-release increment or an error if there is not. 168 | func (v Version) GetPreReleaseIncrement() (int, error) { 169 | if !v.IsPreRelease() { 170 | return -1, newError("%#v is not a pre-release version", v) 171 | } 172 | currentIdentifiers := extractIdentifiers(v.PreRelease) 173 | return strconv.Atoi(currentIdentifiers[len(currentIdentifiers)-1]) 174 | } 175 | 176 | // PreReleaseIdentifiersEqual returns true if the version has the same pre-release identifiers. 177 | // The parameter identifiers is a string where identifiers are separated by . 178 | func (v Version) PreReleaseIdentifiersEqual(identifiers string) bool { 179 | if v.PreRelease == identifiers { 180 | return true 181 | } 182 | currentIdentifiers := extractIdentifiers(v.PreRelease) 183 | desiredIdentifiers := extractIdentifiers(identifiers) 184 | 185 | return utils.ArrayStringEqual(currentIdentifiers, desiredIdentifiers) || 186 | (len(currentIdentifiers) > 0 && utils.ArrayStringEqual(currentIdentifiers[:len(currentIdentifiers)-1], desiredIdentifiers)) 187 | } 188 | 189 | // WithBuildMetadata return a new Version with build metadata 190 | func (v Version) WithBuildMetadata(metadata string) Version { 191 | next := v 192 | next.BuildMetadata = metadata 193 | return next 194 | } 195 | 196 | func extractIdentifiers(value string) []string { 197 | if value == "" { 198 | // set empty array if preRelease is empty 199 | return []string{} 200 | } 201 | return strings.Split(value, ".") 202 | } 203 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 4 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 6 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 7 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 8 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 15 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 16 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 17 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 18 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 19 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 21 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 23 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 25 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 26 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 27 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 28 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 29 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 30 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 31 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 34 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 35 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 36 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 37 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 38 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 39 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 40 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 41 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 45 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 46 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 47 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 48 | github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= 49 | github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= 50 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 51 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 52 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 53 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 54 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 55 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 56 | github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= 57 | github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 58 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 59 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 60 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 61 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 62 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 63 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 64 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 65 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 66 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 67 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 68 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 69 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 70 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 71 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 72 | go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= 73 | go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 74 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 75 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 76 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 77 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 78 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 80 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 81 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 82 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 85 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 88 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | -------------------------------------------------------------------------------- /pkg/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPreReleaseIdentifiersEqual(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | testData := []struct { 14 | version string 15 | identifiers string 16 | expected bool 17 | }{ 18 | {"0.1.0", "", true}, 19 | {"v1.0.0-alpha.0", "alpha", true}, 20 | {"v1.0.0-alpha.0", "alpha.0", true}, 21 | {"v1.0.0-SNAPSHOT", "SNAPSHOT", true}, 22 | } 23 | 24 | for _, tc := range testData { 25 | v, _ := NewVersion(tc.version) 26 | assert.Equal(tc.expected, v.PreReleaseIdentifiersEqual(tc.identifiers)) 27 | } 28 | } 29 | 30 | func TestGetPreReleaseIncrement(t *testing.T) { 31 | assert := assert.New(t) 32 | 33 | testData := []struct { 34 | version string 35 | expected int 36 | expectedError bool 37 | }{ 38 | {"0.1.0", -1, true}, 39 | {"v1.0.0-alpha.0", 0, false}, 40 | {"v1.0.0-alpha.1", 1, false}, 41 | } 42 | 43 | for _, tc := range testData { 44 | v, _ := NewVersion(tc.version) 45 | inc, err := v.GetPreReleaseIncrement() 46 | if tc.expectedError { 47 | assert.Error(err) 48 | } else { 49 | assert.NoError(err) 50 | assert.Equal(tc.expected, inc) 51 | } 52 | } 53 | } 54 | 55 | func TestVersionBumpMajor(t *testing.T) { 56 | assert := assert.New(t) 57 | testData := []struct { 58 | data string 59 | expected string 60 | }{ 61 | {"0.1.0", "1.0.0"}, 62 | {"v1.0.0-alpha.0", "1.0.0"}, // pre-release < release 63 | {"1.1.0-alpha.0", "2.0.0"}, // but not if we want to bump major on a minor pre-release 64 | {"v1.1.1-alpha.0", "2.0.0"}, // but not if we want to bump major on a patch pre-release 65 | {"v1.0.1-alpha.0", "2.0.0"}, 66 | } 67 | for _, tc := range testData { 68 | v1, _ := NewVersion(tc.data) 69 | expected, _ := NewVersion(tc.expected) 70 | actual := v1.BumpMajor() 71 | assert.Equal(expected, actual) 72 | } 73 | } 74 | 75 | func ExampleVersion_BumpMajor() { 76 | v := Version{Major: 1} // 1.0.0 77 | v2 := v.BumpMajor() 78 | fmt.Println(v2.String()) 79 | // Output: 2.0.0 80 | } 81 | 82 | func TestVersionBumpMinor(t *testing.T) { 83 | assert := assert.New(t) 84 | testData := []struct { 85 | data string 86 | expected string 87 | }{ 88 | {"0.1.0", "0.2.0"}, 89 | {"1.0.1", "1.1.0"}, 90 | {"v1.1.0-alpha.2", "1.1.0"}, // pre-release < release 91 | {"v1.0.1-alpha.2", "1.1.0"}, // but not if we want to bump minor on a patch pre-release 92 | {"1.1.1-alpha.2", "1.2.0"}, // same 93 | } 94 | for _, tc := range testData { 95 | v1, _ := NewVersion(tc.data) 96 | expected, _ := NewVersion(tc.expected) 97 | actual := v1.BumpMinor() 98 | assert.Equal(expected, actual) 99 | } 100 | } 101 | 102 | func ExampleVersion_BumpMinor() { 103 | v := Version{Major: 1} // 1.0.0 104 | v2 := v.BumpMinor() 105 | fmt.Println(v2.String()) 106 | // Output: 1.1.0 107 | } 108 | 109 | func TestVersionBumpPatch(t *testing.T) { 110 | assert := assert.New(t) 111 | testData := []struct { 112 | data string 113 | expected string 114 | }{ 115 | {"0.1.0", "0.1.1"}, 116 | {"0.1.0-alpha.0", "0.1.0"}, // pre-release < release 117 | } 118 | for _, tc := range testData { 119 | v1, _ := NewVersion(tc.data) 120 | expected, _ := NewVersion(tc.expected) 121 | actual := v1.BumpPatch() 122 | assert.Equal(expected, actual) 123 | } 124 | } 125 | 126 | func ExampleVersion_BumpPatch() { 127 | v := Version{Major: 1} // 1.0.0 128 | v2 := v.BumpPatch() 129 | fmt.Println(v2.String()) 130 | // Output: 1.0.1 131 | } 132 | 133 | func TestNewVersion(t *testing.T) { 134 | assert := assert.New(t) 135 | testData := []struct { 136 | version string 137 | err bool 138 | }{ 139 | {"1.2.3", false}, 140 | {"v1.2.3", false}, 141 | {"1.0", true}, 142 | {"v1.0", true}, 143 | {"1", true}, 144 | {"v1", true}, 145 | {"1.2.beta", true}, 146 | {"v1.2.beta", true}, 147 | {"foo", true}, 148 | {"1.2-5", true}, 149 | {"v1.2-5", true}, 150 | {"1.2-beta.5", true}, 151 | {"v1.2-beta.5", true}, 152 | {"\n1.2", true}, 153 | {"\nv1.2", true}, 154 | {"1.2.0-x.Y.0+metadata", false}, 155 | {"v1.2.0-x.Y.0+metadata", false}, 156 | {"1.2.0-x.Y.0+metadata-width-hypen", false}, 157 | {"v1.2.0-x.Y.0+metadata-width-hypen", false}, 158 | {"1.2.3-rc1-with-hypen", false}, 159 | {"v1.2.3-rc1-with-hypen", false}, 160 | {"1.2.3.4", true}, 161 | {"v1.2.3.4", true}, 162 | {"1.2.2147483648", false}, 163 | {"1.2147483648.3", false}, 164 | {"2147483648.3.0", false}, 165 | } 166 | 167 | for _, tc := range testData { 168 | _, err := NewVersion(tc.version) 169 | if tc.err && err == nil { 170 | assert.Fail("expected error for version: %s", tc.version) 171 | } else if !tc.err && err != nil { 172 | assert.Fail("error for version %s: %s", tc.version, err) 173 | } 174 | } 175 | } 176 | 177 | func ExampleNewVersion() { 178 | NewVersion("1.2.3") 179 | NewVersion("v1.2.3") // with v prefix 180 | NewVersion("2.3.5-beta") // pre-release overwritable 181 | NewVersion("2.3.5-beta.5") // pre-release with index 182 | NewVersion("2.3.5+metadata") // build-metadata 183 | } 184 | 185 | func TestBumpPreRelease(t *testing.T) { 186 | t.Parallel() 187 | assert := assert.New(t) 188 | 189 | testData := []struct { 190 | version string 191 | preRelease string 192 | overridePreRelease bool 193 | expected string 194 | }{ 195 | {"1.0.0", "", false, "1.1.0-0"}, 196 | {"1.0.0-0", "", false, "1.0.0-1"}, 197 | {"1.0.0", "alpha", false, "1.1.0-alpha.0"}, 198 | {"1.1.0-alpha.0", "alpha", false, "1.1.0-alpha.1"}, 199 | {"1.1.0-alpha.1", "beta", false, "1.1.0-beta.0"}, 200 | {"1.0.0", "", true, "1.0.0"}, 201 | {"1.0.0", "SNAPSHOT", true, "1.1.0-SNAPSHOT"}, 202 | {"1.1.0-SNAPSHOT", "SNAPSHOT", true, "1.1.0-SNAPSHOT"}, 203 | } 204 | 205 | for _, tc := range testData { 206 | version, err := NewVersion(tc.version) 207 | assert.Nil(err) 208 | actual := version.BumpPreRelease(tc.preRelease, tc.overridePreRelease, nil) 209 | assert.Equal(tc.expected, actual.String()) 210 | } 211 | } 212 | 213 | func ExampleVersion_BumpPreRelease() { 214 | v1 := Version{Major: 1} // 1.0.0 215 | // Parameters: pre-release, pre-release overwrite, versionBumper (default to Version.BumpMinor) 216 | v2 := v1.BumpPreRelease("alpha", false, nil) 217 | // The current version is not a pre-release, so it will use the versionBumper to first bump the minor (default) and then set the pre-release to alpha.0 218 | fmt.Println(v2.String()) 219 | // However if the current is already a pre-release of the same class (alpha here), then it just increments the pre-release id 220 | v3 := v2.BumpPreRelease("alpha", false, nil) 221 | fmt.Println(v3.String()) 222 | // Output: 223 | // 1.1.0-alpha.0 224 | // 1.1.0-alpha.1 225 | } 226 | 227 | func ExampleVersion_BumpPreRelease_overwrite() { 228 | v1 := Version{Major: 1} // 1.0.0 229 | // If you don't want to have pre-release index, you can pass true for pre-release overwrite as parameter 230 | v2 := v1.BumpPreRelease("alpha", true, nil) 231 | fmt.Println(v2.String()) 232 | // But then it means your version can be overwritten if you perform again the same operation 233 | v3 := v2.BumpPreRelease("alpha", true, nil) 234 | fmt.Println(v3.String()) 235 | // Output: 236 | // 1.1.0-alpha 237 | // 1.1.0-alpha 238 | } 239 | 240 | func ExampleVersion_BumpPreRelease_versionBumper() { 241 | v1 := Version{Major: 1} // 1.0.0 242 | // It is also possible to overwrite the default version bumper 243 | v2 := v1.BumpPreRelease("alpha", false, Version.BumpMajor) 244 | fmt.Println(v2.String()) 245 | // But if the previous is already a pre-release of the same class (alpha here), then it will not be used 246 | v3 := v2.BumpPreRelease("alpha", false, Version.BumpMajor) 247 | fmt.Println(v3.String()) 248 | // Output: 249 | // 2.0.0-alpha.0 250 | // 2.0.0-alpha.1 251 | } 252 | 253 | func TestWithBuildMetadata(t *testing.T) { 254 | t.Parallel() 255 | assert := assert.New(t) 256 | 257 | testData := []struct { 258 | version string 259 | buildMetadata string 260 | expected string 261 | }{ 262 | {"1.0.0", "build.8", "1.0.0+build.8"}, 263 | {"1.0.0", "3.abcdkd", "1.0.0+3.abcdkd"}, 264 | } 265 | 266 | for _, tc := range testData { 267 | version, err := NewVersion(tc.version) 268 | assert.Nil(err) 269 | actual := version.WithBuildMetadata(tc.buildMetadata) 270 | assert.Equal(tc.expected, actual.String()) 271 | } 272 | } 273 | 274 | func ExampleVersion_WithBuildMetadata() { 275 | v := Version{Major: 1} // 1.0.0 276 | v2 := v.WithBuildMetadata("build.1") // this simply set the build metadata to the version 277 | fmt.Println(v2.String()) 278 | // Output: 1.0.0+build.1 279 | } 280 | -------------------------------------------------------------------------------- /pkg/version/bump_strategy.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/arnaud-deprez/gsemver/internal/log" 9 | ) 10 | 11 | const ( 12 | // DefaultMajorPattern defines default regular expression to match a commit message with a major change. 13 | DefaultMajorPattern = `(?:^.+\!:.+|(?m)^BREAKING CHANGE:.+$)` 14 | // DefaultMinorPattern defines default regular expression to match a commit message with a minor change. 15 | DefaultMinorPattern = `^(?:feat|chore|build|ci|refactor|perf)(?:\(.+\))?:.+` 16 | // DefaultReleaseBranchesPattern defines default regular expression to match release branches 17 | DefaultReleaseBranchesPattern = `^(main|master|release/.*)$` 18 | // DefaultPreRelease defines default pre-release activation for non release branches 19 | DefaultPreRelease = false 20 | // DefaultPreReleaseTemplate defines default pre-release go template for non release branches 21 | DefaultPreReleaseTemplate = "" 22 | // DefaultPreReleaseOverwrite defines default pre-release overwrite activation for non release branches 23 | DefaultPreReleaseOverwrite = false 24 | // DefaultBuildMetadataTemplate defines default go template used for non release branches strategy 25 | DefaultBuildMetadataTemplate = `{{.Commits | len}}.{{(.Commits | first).Hash.Short}}` 26 | ) 27 | 28 | var ( 29 | // strategyVersionBumperMap defined the link between BumpStrategyType and versionBumper. 30 | // Note that AUTO BumpStrategyType versionBumper is dynamically computed and therefore cannot be part of this static links 31 | /* const */ strategyVersionBumperMap = map[BumpStrategyType]versionBumper{ 32 | MAJOR: Version.BumpMajor, 33 | MINOR: Version.BumpMinor, 34 | PATCH: Version.BumpPatch, 35 | } 36 | ) 37 | 38 | // versionBumper type helper for the bump process 39 | type versionBumper func(Version) Version 40 | 41 | // BumpStrategy allows you to configure the bump strategy 42 | type BumpStrategy struct { 43 | // MajorPattern is the regex used to detect if a commit contains a breaking/major change 44 | // See RegexMinor for more details 45 | MajorPattern *regexp.Regexp `json:"majorPattern,omitempty"` 46 | // MinorPattern is the regex used to detect if a commit contains a minor change 47 | // If no commit match RegexMajor or RegexMinor, the change is considered as a patch 48 | MinorPattern *regexp.Regexp `json:"minorPattern,omitempty"` 49 | // BumpStrategies is a list of bump strategies for matching branches 50 | BumpStrategies []BumpBranchesStrategy `json:"bumpStrategies,omitempty"` 51 | // gitRepo is an implementation of GitRepo 52 | gitRepo GitRepo 53 | } 54 | 55 | /* 56 | NewConventionalCommitBumpStrategy create a BumpStrategy following https://www.conventionalcommits.org 57 | 58 | The strategy configuration is: 59 | 60 | MajorPattern: (?:^.+\!:.+|(?m)^BREAKING CHANGE:.+$) 61 | MinorPattern: ^(?:feat|chore|build|ci|refactor|perf)(?:\(.+\))?:.+ 62 | BumpBranchesStrategies: [ 63 | { 64 | Strategy: AUTO 65 | BranchesPattern: ^(main|master|release/.*)$ 66 | PreRelease: false 67 | PreReleaseTemplate: "" 68 | PreReleaseOverwrite: false 69 | BuildMetadataTemplate: "" 70 | }, 71 | { 72 | Strategy: AUTO 73 | BranchesPattern: .* 74 | PreRelease: false 75 | PreReleaseTemplate: "" 76 | PreReleaseOverwrite: false 77 | BuildMetadataTemplate: "{{.Commits | len}}.{{(.Commits | first).Hash.Short}}" 78 | } 79 | ] 80 | */ 81 | func NewConventionalCommitBumpStrategy(gitRepo GitRepo) *BumpStrategy { 82 | return &BumpStrategy{ 83 | BumpStrategies: []BumpBranchesStrategy{ 84 | *NewDefaultBumpBranchesStrategy(DefaultReleaseBranchesPattern), 85 | *NewBuildBumpBranchesStrategy(".*", DefaultBuildMetadataTemplate), 86 | }, 87 | MajorPattern: regexp.MustCompile(DefaultMajorPattern), 88 | MinorPattern: regexp.MustCompile(DefaultMinorPattern), 89 | gitRepo: gitRepo, 90 | } 91 | } 92 | 93 | // GoString makes BumpStrategy satisfy the GoStringer interface. 94 | func (o BumpStrategy) GoString() string { 95 | var sb strings.Builder 96 | sb.WriteString("version.BumpStrategy{") 97 | sb.WriteString(fmt.Sprintf("MajorPattern: ®exp.Regexp{expr: %q}, MinorPattern: ®exp.Regexp{expr: %q}, ", o.MajorPattern, o.MinorPattern)) 98 | sb.WriteString(fmt.Sprintf("BumpBranchesStrategies: %#v", o.BumpStrategies)) 99 | sb.WriteString("}") 100 | return sb.String() 101 | } 102 | 103 | // SetGitRepository configures the git repository to use for the strategy 104 | func (o *BumpStrategy) SetGitRepository(gitRepo GitRepo) { 105 | o.gitRepo = gitRepo 106 | } 107 | 108 | // Bump performs the version bumping based on the strategy 109 | func (o *BumpStrategy) Bump() (Version, error) { 110 | log.Debug("BumpStrategy: bump with configuration: %#v", o) 111 | 112 | // Make sure we have the tags 113 | err := o.gitRepo.FetchTags() 114 | if err != nil { 115 | return zeroVersion, newErrorC(err, "Cannot fetch tags") 116 | } 117 | 118 | // This assumes we used annotated tags for the release. Annotated tag are created with: git tag -a -m "" 119 | // Annotated tags adds timestamp, author and message to a tag compared to lightweight tag which does not contain any of these information. 120 | // Thanks to that git describe will only show the more recent annotated tag if many annotated tags are on the same commit. 121 | // However if you use lightweight tags there are many on the same commit, it just takes the first one. 122 | lastTag, err := o.gitRepo.GetLastRelativeTag("HEAD") 123 | if err != nil { 124 | // just log for debug but the program can continue 125 | log.Debug("%v", newErrorC(err, "Unable to get last relative tag")) 126 | } 127 | 128 | // Parse the last version from the tag name 129 | lastVersion, err := NewVersion(extractVersionFromTag(lastTag.Name)) 130 | if err != nil { 131 | return zeroVersion, err 132 | } 133 | 134 | currentBranch, err := o.gitRepo.GetCurrentBranch() 135 | if err != nil { 136 | return zeroVersion, newErrorC(err, "Cannot get current branch name") 137 | } 138 | 139 | // Check if describe is a tag, if so return the version that matches this tag 140 | commits, cErr := o.gitRepo.GetCommits(lastTag.Name, "HEAD") 141 | if cErr != nil { 142 | // Oops 143 | return zeroVersion, err 144 | } 145 | 146 | context := NewContext(currentBranch, &lastVersion, &lastTag, commits) 147 | 148 | log.Debug("BumpStrategy: look for appropriate version bumper with %#v, lastVersion=%v, branch=%v", lastTag, lastVersion, currentBranch) 149 | versionBumper := o.computeVersionBumper(context) 150 | 151 | // Bump the version 152 | return versionBumper(lastVersion), nil 153 | } 154 | 155 | func extractVersionFromTag(tagName string) string { 156 | return tagName[strings.LastIndex(tagName, "/")+1:] 157 | } 158 | 159 | // computeAutoVersionBumper computes what bump strategy to apply 160 | func (o *BumpStrategy) computeVersionBumper(context *Context) versionBumper { 161 | for _, it := range o.BumpStrategies { 162 | if it.BranchesPattern.MatchString(context.Branch) { 163 | if log.IsLevelEnabled(log.DebugLevel) { 164 | log.Debug("BumpStrategy: will use bump %s", strings.ToUpper(it.Strategy.String())) 165 | } 166 | 167 | // find the correct bumper 168 | if val, ok := strategyVersionBumperMap[it.Strategy]; ok { 169 | return it.createVersionBumperFrom(val, context) 170 | } else if it.Strategy == AUTO { 171 | return o.computeSemverBumperFromCommits(&it, context) 172 | } 173 | } 174 | } 175 | 176 | log.Debug("BumpStrategy: not matching strategy found in %#v. versionBumperIdentity will be used", o.BumpStrategies) 177 | return versionBumperIdentity 178 | } 179 | 180 | func (o *BumpStrategy) computeSemverBumperFromCommits(bbs *BumpBranchesStrategy, context *Context) versionBumper { 181 | if len(context.Commits) == 0 { 182 | log.Debug("BumpStrategy: will not use identity bump strategy because there is not commit") 183 | return versionBumperIdentity 184 | } 185 | 186 | strategy := PATCH 187 | bumper := Version.BumpPatch 188 | for _, commit := range context.Commits { 189 | if o.MajorPattern.MatchString(commit.Message) { 190 | if context.LastVersion.IsUnstable() { 191 | log.Trace("BumpStrategy: detects a MAJOR change at %#v however the last version is unstable so it will use bump MINOR strategy", commit) 192 | return bbs.createVersionBumperFrom(Version.BumpMinor, context) 193 | } 194 | log.Debug("BumpStrategy: detects a MAJOR change at %#v", commit) 195 | return bbs.createVersionBumperFrom(Version.BumpMajor, context) 196 | } 197 | if o.MinorPattern.MatchString(commit.Message) { 198 | strategy = MINOR 199 | log.Trace("BumpStrategy: detects a MINOR change at %#v", commit) 200 | bumper = Version.BumpMinor 201 | } 202 | } 203 | 204 | log.Debug("BumpStrategy: will use bump %s strategy", strategy) 205 | return bbs.createVersionBumperFrom(bumper, context) 206 | } 207 | -------------------------------------------------------------------------------- /test/integration/gsemver_bump_auto_conventionalcommits_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "regexp" 7 | "runtime" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | 13 | "github.com/arnaud-deprez/gsemver/internal/git" 14 | "github.com/arnaud-deprez/gsemver/pkg/version" 15 | ) 16 | 17 | const ( 18 | README = "README.md" 19 | ) 20 | 21 | var ( 22 | bumper *version.BumpStrategy 23 | ) 24 | 25 | func beforeAll(t *testing.T) { 26 | t.Log("BeforeAll: initializing git repo at", GitRepoPath) 27 | assert.NoError(t, os.RemoveAll(GitRepoPath)) 28 | os.MkdirAll(GitRepoPath, 0755) 29 | execInGitRepo(t, "git init") 30 | execInGitRepo(t, "git branch -m main") 31 | execInGitRepo(t, "git status") 32 | gitRepo := git.NewVersionGitRepo(GitRepoPath) 33 | bumper = version.NewConventionalCommitBumpStrategy(gitRepo) 34 | } 35 | 36 | func afterAll(t *testing.T) { 37 | out := execInGitRepo(t, "git log --oneline --decorate --graph --all") 38 | appendToFile(t, "git.log", out) 39 | } 40 | 41 | func beforeEach(_ *testing.T) {} 42 | 43 | func afterEach(t *testing.T) { 44 | v, err := bumper.Bump() 45 | assert.NoError(t, err) 46 | if v.String() != "0.0.0" { 47 | execInGitRepo(t, "git checkout main") 48 | } 49 | } 50 | 51 | func TestSuite(t *testing.T) { 52 | if testing.Short() { 53 | t.Skip() 54 | } 55 | 56 | beforeAll(t) 57 | 58 | tests := []func(t *testing.T){ 59 | testFirstVersionWithoutCommit, 60 | testWithFirstFeatureCommit, 61 | testWithFirstFixCommit, 62 | testCreateFeaturePullRequest, 63 | testCreateFeature2PullRequest, 64 | testCreateFixPullRequestOnMain, 65 | testCreateReleaseBranchWithFix, 66 | testMergeDirectlyReleaseBranchShouldHaveSameVersionOnMain, 67 | testCreateFeature3PullRequest, 68 | testMergeReleaseBranch, 69 | testCreateFixPullRequestInReleaseBranch, 70 | testMerge2ReleaseBranch, 71 | } 72 | 73 | for _, tf := range tests { 74 | code := t.Run(runtime.FuncForPC(reflect.ValueOf(tf).Pointer()).Name(), func(t *testing.T) { 75 | beforeEach(t) 76 | tf(t) 77 | afterEach(t) 78 | }) 79 | assert.True(t, code, "Test failed: %s", runtime.FuncForPC(reflect.ValueOf(tf).Pointer()).Name()) 80 | } 81 | 82 | afterAll(t) 83 | } 84 | 85 | func testFirstVersionWithoutCommit(t *testing.T) { 86 | assert := assert.New(t) 87 | v, err := bumper.Bump() 88 | assert.NoError(err) 89 | assert.Equal("0.0.0", v.String()) 90 | } 91 | 92 | func testWithFirstFeatureCommit(t *testing.T) { 93 | assert := assert.New(t) 94 | appendToFile(t, README, "First feature") 95 | commit(t, "feat: add README.md") 96 | 97 | v, err := bumper.Bump() 98 | assert.NoError(err) 99 | assert.Equal("0.1.0", v.String()) 100 | 101 | createTag(t, v.String()) 102 | v2, err := bumper.Bump() 103 | assert.NoError(err) 104 | assert.Equal(v, v2) 105 | } 106 | 107 | func testWithFirstFixCommit(t *testing.T) { 108 | assert := assert.New(t) 109 | appendToFile(t, README, "First fix") 110 | commit(t, "fix(doc): fix documentation") 111 | 112 | v, err := bumper.Bump() 113 | assert.NoError(err) 114 | assert.Equal("0.1.1", v.String()) 115 | 116 | createTag(t, v.String()) 117 | v2, err := bumper.Bump() 118 | assert.NoError(err) 119 | assert.Equal(v, v2) 120 | } 121 | 122 | func testCreateFeaturePullRequest(t *testing.T) { 123 | assert := assert.New(t) 124 | branch := "feature/awesome-1" 125 | execInGitRepo(t, "git checkout -b "+branch) 126 | appendToFile(t, README, "Awesome feature with breaking change") 127 | commit(t, `feat: my awesome change 128 | 129 | BREAKING CHANGE: this is a breaking change but should not bump major as it is a development release`) 130 | 131 | v, err := bumper.Bump() 132 | assert.NoError(err) 133 | assert.Regexp(regexp.MustCompile(`0.1.1\+1\..*`), v.String()) 134 | 135 | mergePullRequest(t, branch, "main") 136 | 137 | v, err = bumper.Bump() 138 | assert.NoError(err) 139 | assert.Equal("0.2.0", v.String()) 140 | createTag(t, v.String()) 141 | 142 | time.Sleep(1 * time.Second) 143 | createTag(t, "1.0.0") 144 | 145 | v, err = bumper.Bump() 146 | assert.NoError(err) 147 | assert.Equal("1.0.0", v.String()) 148 | } 149 | 150 | func testCreateFeature2PullRequest(t *testing.T) { 151 | assert := assert.New(t) 152 | branch := "feature/awesome-2" 153 | execInGitRepo(t, "git checkout -b "+branch) 154 | appendToFile(t, README, "Awesome 2nd feature") 155 | commit(t, `feat: my awesome 2nd change`) 156 | 157 | v, err := bumper.Bump() 158 | assert.NoError(err) 159 | assert.Regexp(regexp.MustCompile(`1.0.0\+1\..*`), v.String()) 160 | 161 | mergePullRequest(t, branch, "main") 162 | v, err = bumper.Bump() 163 | assert.NoError(err) 164 | assert.Equal("1.1.0", v.String()) 165 | createTag(t, v.String()) 166 | } 167 | 168 | func testCreateFixPullRequestOnMain(t *testing.T) { 169 | assert := assert.New(t) 170 | branch := "bug/fix-1" 171 | execInGitRepo(t, "git checkout -b "+branch) 172 | appendToFile(t, README, "Bug fix on main") 173 | commit(t, `fix: my bug fix on main`) 174 | 175 | v, err := bumper.Bump() 176 | assert.NoError(err) 177 | assert.Regexp(regexp.MustCompile(`1.1.0\+1\..*`), v.String()) 178 | 179 | mergePullRequest(t, branch, "main") 180 | v, err = bumper.Bump() 181 | assert.NoError(err) 182 | assert.Equal("1.1.1", v.String()) 183 | createTag(t, v.String()) 184 | } 185 | 186 | func testCreateReleaseBranchWithFix(t *testing.T) { 187 | assert := assert.New(t) 188 | releaseBranch := "release/1.1.x" 189 | execInGitRepo(t, "git checkout -b "+releaseBranch) 190 | 191 | v, err := bumper.Bump() 192 | assert.NoError(err) 193 | assert.Equal("1.1.1", v.String()) 194 | 195 | branch := "fix/fix-2" 196 | execInGitRepo(t, "git checkout -b "+branch) 197 | appendToFile(t, "README-1.1.x.md", "Bug fix 2 on "+releaseBranch) 198 | commit(t, `fix: my bug fix 2 on `+releaseBranch) 199 | 200 | v, err = bumper.Bump() 201 | assert.NoError(err) 202 | assert.Regexp(regexp.MustCompile(`1.1.1\+1\..*`), v.String()) 203 | 204 | mergePullRequest(t, branch, releaseBranch) 205 | 206 | v, err = bumper.Bump() 207 | assert.NoError(err) 208 | assert.Equal("1.1.2", v.String()) 209 | createTag(t, v.String()) 210 | } 211 | 212 | func testMergeDirectlyReleaseBranchShouldHaveSameVersionOnMain(t *testing.T) { 213 | assert := assert.New(t) 214 | 215 | // to merge into release branch, we should first perform the merge in a working branch 216 | branch := "feature/merge-direct-release-1.1.x" 217 | execInGitRepo(t, "git checkout -b "+branch) 218 | merge(t, "release/1.1.x", branch) 219 | mergePullRequest(t, branch, "main") 220 | 221 | v, err := bumper.Bump() 222 | assert.NoError(err) 223 | // We should have the same version as semantically nothing is different from release and main branch 224 | assert.Equal(`1.1.2`, v.String()) 225 | 226 | // revert this change 227 | execInGitRepo(t, "git reset --hard v1.1.1") 228 | } 229 | 230 | func testCreateFeature3PullRequest(t *testing.T) { 231 | assert := assert.New(t) 232 | branch := "feature/awesome-3" 233 | execInGitRepo(t, "git checkout -b "+branch) 234 | appendToFile(t, README, "Awesome 3rd feature") 235 | commit(t, `feat: my awesome 3rd change`) 236 | 237 | v, err := bumper.Bump() 238 | assert.NoError(err) 239 | assert.Regexp(regexp.MustCompile(`1.1.1\+1\..*`), v.String()) 240 | 241 | mergePullRequest(t, branch, "main") 242 | v, err = bumper.Bump() 243 | assert.NoError(err) 244 | assert.Equal("1.2.0", v.String()) 245 | createTag(t, v.String()) 246 | } 247 | 248 | func testMergeReleaseBranch(t *testing.T) { 249 | assert := assert.New(t) 250 | 251 | // to merge into release branch, we should first perform the merge in a working branch 252 | branch := "feature/merge-release-1.1.x" 253 | execInGitRepo(t, "git checkout -b "+branch) 254 | merge(t, "release/1.1.x", branch) 255 | mergePullRequest(t, branch, "main") 256 | 257 | v, err := bumper.Bump() 258 | assert.NoError(err) 259 | assert.Equal(`1.2.1`, v.String()) 260 | createTag(t, v.String()) 261 | } 262 | 263 | func testCreateFixPullRequestInReleaseBranch(t *testing.T) { 264 | assert := assert.New(t) 265 | releaseBranch := "release/1.1.x" 266 | execInGitRepo(t, "git checkout "+releaseBranch) 267 | 268 | branch := "fix/fix-3" 269 | execInGitRepo(t, "git checkout -b "+branch) 270 | appendToFile(t, "README-1.1.x.md", "Bug fix 3 on "+releaseBranch) 271 | commit(t, `fix: my bug fix 3 on `+releaseBranch) 272 | 273 | v, err := bumper.Bump() 274 | assert.NoError(err) 275 | assert.Regexp(regexp.MustCompile(`1.1.2\+1\..*`), v.String()) 276 | 277 | mergePullRequest(t, branch, releaseBranch) 278 | 279 | v, err = bumper.Bump() 280 | assert.NoError(err) 281 | assert.Equal("1.1.3", v.String()) 282 | createTag(t, v.String()) 283 | } 284 | 285 | func testMerge2ReleaseBranch(t *testing.T) { 286 | assert := assert.New(t) 287 | // to merge into release branch, we should first perform the merge in a working branch 288 | branch := "feature/merge2-release-1.1.x" 289 | execInGitRepo(t, "git checkout -b "+branch) 290 | merge(t, "release/1.1.x", branch) 291 | mergePullRequest(t, branch, "main") 292 | 293 | v, err := bumper.Bump() 294 | assert.NoError(err) 295 | assert.Equal(`1.2.2`, v.String()) 296 | createTag(t, v.String()) 297 | } 298 | -------------------------------------------------------------------------------- /cmd/bump.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | 12 | "github.com/arnaud-deprez/gsemver/internal/git" 13 | "github.com/arnaud-deprez/gsemver/internal/log" 14 | "github.com/arnaud-deprez/gsemver/internal/utils" 15 | "github.com/arnaud-deprez/gsemver/pkg/version" 16 | ) 17 | 18 | const ( 19 | bumpDesc = ` 20 | This will compute and print the next semver compatible version of your project based on commits logs, tags and current branch. 21 | 22 | The version will look like ..[-][+] where: 23 | - X is the Major number 24 | - Y is the Minor number 25 | - Z is the Patch number 26 | - pre-release is the pre-release identifiers (optional) 27 | - metadata is the build metadata identifiers (optional) 28 | 29 | More info on the semver spec https://semver.org/spec/v2.0.0.html. 30 | 31 | It can work in 2 fashions, the automatic or manual. 32 | 33 | Automatic way assumes: 34 | - your previous tags are semver compatible. 35 | - you follow some conventions in your commit and ideally https://www.conventionalcommits.org 36 | - you follow some branch convention for your releases (eg. a release should be done on main, master or release/* branches) 37 | 38 | Base on this information, it is able to compute the next version. 39 | 40 | The manual way is less restrictive and just assumes your previous tags are semver compatible. 41 | ` 42 | bumpExample = ` 43 | # To bump automatically: 44 | gsemver bump 45 | 46 | # Or more explicitly 47 | gsemver bump auto 48 | 49 | # To bump manually the major number: 50 | gsemver bump major 51 | 52 | # To bump manually the minor number: 53 | gsemver bump minor 54 | 55 | # To bump manually the patch number: 56 | gsemver bump patch 57 | 58 | # To use a pre-release version 59 | gsemver bump --pre-release alpha 60 | # Or with go-template 61 | gsemver bump --pre-release "alpha-{{.Branch}}" 62 | 63 | # To use a pre-release version without indexation (maven like SNAPSHOT) 64 | gsemver bump minor --pre-release SNAPSHOT --pre-release-overwrite true 65 | 66 | # To use version with build metadata 67 | gsemver bump --build-metadata "issue-1.build.1" 68 | # Or with go-template 69 | gsemver bump --build-metadata "{{(.Commits | first).Hash.Short}}" 70 | 71 | # To use bump auto with one or many branch strategies 72 | gsemver bump --branch-strategy='{"branchesPattern":"^miletone-1.1$","preReleaseTemplate":"beta"}' --branch-strategy='{"branchesPattern":"^miletone-2.0$","preReleaseTemplate":"alpha"}' 73 | ` 74 | preReleaseTemplateDesc = `Use pre-release template version such as 'alpha' which will give a version like 'X.Y.Z-alpha.N'. 75 | If pre-release flag is present but does not contain template value, it will give a version like 'X.Y.Z-N' where 'N' is the next pre-release increment for the version 'X.Y.Z'. 76 | You can also use go-template expression with context https://godoc.org/github.com/arnaud-deprez/gsemver/pkg/version#Context and http://masterminds.github.io/sprig functions. 77 | This flag is not taken into account if --build-metadata is set.` 78 | 79 | buildMetadataTemplateDesc = `Use build metadata template which will give something like X.Y.Z+. 80 | You can also use go-template expression with context https://godoc.org/github.com/arnaud-deprez/gsemver/pkg/version#Context and http://masterminds.github.io/sprig functions. 81 | This flag cannot be used with --pre-release* flags and take precedence over them.` 82 | 83 | branchStrategyDesc = `Use branch-strategy will set a strategy for a set of branches. 84 | The strategy is defined in json and looks like {"branchesPattern":"^milestone-.*$", "preReleaseTemplate":"alpha"} for example. 85 | This will use pre-release alpha version for every milestone-* branches. 86 | You can find all available options https://godoc.org/github.com/arnaud-deprez/gsemver/pkg/version#BumpBranchesStrategy` 87 | ) 88 | 89 | // newBumpCommands create the bump command with its subcommands 90 | func newBumpCommands(globalOpts *globalOptions) *cobra.Command { 91 | return newBumpCommandsWithRun(globalOpts, run) 92 | } 93 | 94 | // newBumpCommandsWithRun create the bump commands from bumpOptions with run function. 95 | // it is used for internal usage only and for test purpose. 96 | func newBumpCommandsWithRun(globalOpts *globalOptions, run func(o *bumpOptions) error) *cobra.Command { 97 | options := &bumpOptions{ 98 | globalOptions: globalOpts, 99 | } 100 | 101 | cmd := &cobra.Command{ 102 | Use: "bump [strategy]", 103 | Short: "Bump to next version", 104 | Long: bumpDesc, 105 | Example: bumpExample, 106 | ValidArgs: []string{"auto", "major", "minor", "patch"}, 107 | Args: cobra.RangeArgs(0, 1), 108 | RunE: func(cmd *cobra.Command, args []string) error { 109 | options.configureLogger() 110 | 111 | options.Cmd = cmd 112 | options.Args = args 113 | if len(args) == 0 { 114 | options.Bump = "auto" 115 | } else { 116 | options.Bump = args[0] 117 | } 118 | 119 | options.PreRelease = cmd.Flags().Changed("pre-release") 120 | 121 | return run(options) 122 | }, 123 | } 124 | 125 | options.addBumpFlags(cmd) 126 | 127 | return cmd 128 | } 129 | 130 | type config struct { 131 | MajorPattern string 132 | MinorPattern string 133 | BumpStrategies []struct { 134 | Strategy string 135 | BranchesPattern string 136 | PreRelease bool 137 | PreReleaseTemplate string 138 | PreReleaseOverwrite bool 139 | BuildMetadataTemplate string 140 | } 141 | } 142 | 143 | func (c *config) createBumpStrategy() *version.BumpStrategy { 144 | ret := version.BumpStrategy{BumpStrategies: []version.BumpBranchesStrategy{}} 145 | ret.MajorPattern = regexp.MustCompile(c.MajorPattern) 146 | ret.MinorPattern = regexp.MustCompile(c.MinorPattern) 147 | for _, it := range c.BumpStrategies { 148 | s := version.BumpBranchesStrategy{ 149 | Strategy: version.ParseBumpStrategyType(it.Strategy), 150 | BranchesPattern: regexp.MustCompile(it.BranchesPattern), 151 | PreRelease: it.PreRelease, 152 | PreReleaseTemplate: utils.NewTemplate(it.PreReleaseTemplate), 153 | PreReleaseOverwrite: it.PreReleaseOverwrite, 154 | BuildMetadataTemplate: utils.NewTemplate(it.BuildMetadataTemplate), 155 | } 156 | ret.BumpStrategies = append(ret.BumpStrategies, s) 157 | } 158 | return &ret 159 | } 160 | 161 | // BumpOptions type to represent the available options for the bump commands 162 | // It extends GlobalOptions. 163 | type bumpOptions struct { 164 | *globalOptions 165 | viperConfig config 166 | // Bump is mapped to pkg/version/BumpStrategyOptions#Strategy 167 | Bump string 168 | // PreRelease is mapped to pkg/version/BumpStrategyOptions#PreRelease 169 | // It is set to true only if explicitly set by the user 170 | PreRelease bool 171 | // PreReleaseTemplate is mapped to pkg/version/BumpStrategyOptions#PreReleaseTemplate 172 | PreReleaseTemplate string 173 | // PreReleaseOverwrite is mapped to pkg/version/BumpStrategyOptions#PreReleaseOverwrite 174 | PreReleaseOverwrite bool 175 | // BuildMetadataTemplate is mapped to pkg/version/BumpStrategyOptions#BuildMetadataTemplate 176 | BuildMetadataTemplate string 177 | // BranchStrategies is mapped to pkg/version/BumpStrategyOptions#BranchStrategies 178 | BranchStrategies []string 179 | } 180 | 181 | func (o *bumpOptions) addBumpFlags(cmd *cobra.Command) { 182 | cmd.Flags().String("major-pattern", "", "Use major-pattern option to define your regular expression to match a breaking change commit message") 183 | cmd.Flags().String("minor-pattern", "", "Use major-pattern option to define your regular expression to match a minor change commit message") 184 | cmd.Flags().StringVar(&o.PreReleaseTemplate, "pre-release", "", preReleaseTemplateDesc) 185 | cmd.Flags().BoolVar(&o.PreReleaseOverwrite, "pre-release-overwrite", false, "Use pre-release overwrite option to remove the pre-release identifier suffix which will give a version like `X.Y.Z-SNAPSHOT` if pre-release=SNAPSHOT") 186 | cmd.Flags().StringVar(&o.BuildMetadataTemplate, "build-metadata", "", buildMetadataTemplateDesc) 187 | cmd.Flags().StringArrayVar(&o.BranchStrategies, "branch-strategy", []string{}, branchStrategyDesc) 188 | 189 | viper.BindPFlag("majorPattern", cmd.Flags().Lookup("major-pattern")) 190 | viper.BindPFlag("minorPattern", cmd.Flags().Lookup("minor-pattern")) 191 | 192 | viper.SetDefault("majorPattern", version.DefaultMajorPattern) 193 | viper.SetDefault("minorPattern", version.DefaultMinorPattern) 194 | viper.SetDefault("bumpStrategies", []interface{}{ 195 | map[string]interface{}{ 196 | "strategy": "AUTO", 197 | "branchesPattern": version.DefaultReleaseBranchesPattern, 198 | }, 199 | map[string]interface{}{ 200 | "strategy": "AUTO", 201 | "branchesPattern": ".*", 202 | "buildMetadataTemplate": version.DefaultBuildMetadataTemplate, 203 | }, 204 | }) 205 | 206 | o.Cmd = cmd 207 | } 208 | 209 | func (o *bumpOptions) hasDefaultCommandSettings() bool { 210 | return strings.ToLower(o.Bump) != "auto" || o.Cmd.Flags().Changed("pre-release") || o.Cmd.Flags().Changed("pre-release-overwrite") || o.Cmd.Flags().Changed("build-metadata") 211 | } 212 | 213 | func (o *bumpOptions) createBumpStrategy() *version.BumpStrategy { 214 | viper.Unmarshal(&o.viperConfig) 215 | ret := o.viperConfig.createBumpStrategy() 216 | ret.SetGitRepository(git.NewVersionGitRepo(o.CurrentDir)) 217 | 218 | for id, s := range o.BranchStrategies { 219 | if id == 0 { 220 | // reset branch strategy 221 | ret.BumpStrategies = []version.BumpBranchesStrategy{} 222 | } 223 | var b version.BumpBranchesStrategy 224 | json.Unmarshal([]byte(s), &b) 225 | ret.BumpStrategies = append(ret.BumpStrategies, b) 226 | } 227 | 228 | if o.hasDefaultCommandSettings() { 229 | // configure default BumpBranchesStrategy 230 | defaultStrategy := *version.NewBumpAllBranchesStrategy(version.ParseBumpStrategyType(o.Bump), o.PreRelease, o.PreReleaseTemplate, o.PreReleaseOverwrite, o.BuildMetadataTemplate) 231 | ret.BumpStrategies = []version.BumpBranchesStrategy{defaultStrategy} 232 | } 233 | 234 | return ret 235 | } 236 | 237 | func run(o *bumpOptions) error { 238 | log.Debug("Run bump command with configuration: %#v", o) 239 | 240 | version, err := o.createBumpStrategy().Bump() 241 | if err != nil { 242 | return err 243 | } 244 | fmt.Fprintf(o.ioStreams.Out, "%v", version) 245 | return nil 246 | } 247 | -------------------------------------------------------------------------------- /cmd/bump_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "regexp" 7 | "testing" 8 | 9 | shellquote "github.com/kballard/go-shellquote" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "github.com/stretchr/testify/assert" 13 | 14 | "github.com/arnaud-deprez/gsemver/internal/utils" 15 | "github.com/arnaud-deprez/gsemver/pkg/version" 16 | ) 17 | 18 | func TestBumpNoFlag(t *testing.T) { 19 | testData := []struct { 20 | args string 21 | expectedStrategy version.BumpStrategyType 22 | expectedBumpBranchesStrategy []version.BumpBranchesStrategy 23 | }{ 24 | {"major", version.MAJOR, []version.BumpBranchesStrategy{*version.NewBumpAllBranchesStrategy(version.MAJOR, false, "", false, "")}}, 25 | {"minor", version.MINOR, []version.BumpBranchesStrategy{*version.NewBumpAllBranchesStrategy(version.MINOR, false, "", false, "")}}, 26 | {"patch", version.PATCH, []version.BumpBranchesStrategy{*version.NewBumpAllBranchesStrategy(version.PATCH, false, "", false, "")}}, 27 | {"auto", version.AUTO, []version.BumpBranchesStrategy{ 28 | *version.NewDefaultBumpBranchesStrategy(version.DefaultReleaseBranchesPattern), 29 | *version.NewBuildBumpBranchesStrategy(".*", version.DefaultBuildMetadataTemplate), 30 | }}, 31 | {"", version.AUTO, []version.BumpBranchesStrategy{ 32 | *version.NewDefaultBumpBranchesStrategy(version.DefaultReleaseBranchesPattern), 33 | *version.NewBuildBumpBranchesStrategy(".*", version.DefaultBuildMetadataTemplate), 34 | }}, 35 | } 36 | 37 | for _, tc := range testData { 38 | t.Run(tc.args, func(t *testing.T) { 39 | assert := assert.New(t) 40 | out, errOut := new(bytes.Buffer), new(bytes.Buffer) 41 | globalOpts := &globalOptions{ 42 | ioStreams: newIOStreams(os.Stdin, out, errOut), 43 | } 44 | 45 | args, err := shellquote.Split(tc.args) 46 | assert.NoError(err) 47 | root := newBumpCommandsWithRun(globalOpts, func(o *bumpOptions) error { 48 | s := o.createBumpStrategy() 49 | 50 | assert.Equal(version.DefaultMajorPattern, utils.RegexpToString(s.MajorPattern)) 51 | assert.Equal(version.DefaultMinorPattern, utils.RegexpToString(s.MinorPattern)) 52 | assert.Equal(len(tc.expectedBumpBranchesStrategy), len(s.BumpStrategies)) 53 | 54 | for i := range tc.expectedBumpBranchesStrategy { 55 | assert.Equal(tc.expectedBumpBranchesStrategy[i].GoString(), s.BumpStrategies[i].GoString()) 56 | } 57 | 58 | return nil 59 | }) 60 | globalOpts.addGlobalFlags(root) 61 | 62 | _, err = executeCommand(root, args...) 63 | assert.NoError(err) 64 | }) 65 | } 66 | } 67 | 68 | func TestBumpChangePattern(t *testing.T) { 69 | testData := []struct { 70 | args string 71 | expectedMajorPattern string 72 | expectedMinorPattern string 73 | }{ 74 | {`--major-pattern 'foo'`, "foo", version.DefaultMinorPattern}, 75 | {`--minor-pattern 'bar'`, version.DefaultMajorPattern, "bar"}, 76 | {`--major-pattern 'foo' --minor-pattern 'bar'`, "foo", "bar"}, 77 | } 78 | 79 | for _, tc := range testData { 80 | t.Run(tc.args, func(t *testing.T) { 81 | assert := assert.New(t) 82 | out, errOut := new(bytes.Buffer), new(bytes.Buffer) 83 | globalOpts := &globalOptions{ 84 | ioStreams: newIOStreams(os.Stdin, out, errOut), 85 | } 86 | 87 | args, err := shellquote.Split(tc.args) 88 | assert.NoError(err) 89 | root := newBumpCommandsWithRun(globalOpts, func(o *bumpOptions) error { 90 | s := o.createBumpStrategy() 91 | 92 | assert.Equal(tc.expectedMajorPattern, utils.RegexpToString(s.MajorPattern)) 93 | assert.Equal(tc.expectedMinorPattern, utils.RegexpToString(s.MinorPattern)) 94 | return nil 95 | }) 96 | globalOpts.addGlobalFlags(root) 97 | 98 | _, err = executeCommand(root, args...) 99 | assert.NoError(err) 100 | }) 101 | } 102 | } 103 | 104 | func TestBumpPreRelease(t *testing.T) { 105 | testData := []struct { 106 | args string 107 | expectedPreRelease bool 108 | expectedPreReleaseTemplate string 109 | expectedPreReleaseOverwrite bool 110 | }{ 111 | // TODO: would be nice to make this works {`--pre-release`, true, "", false}, 112 | {`--pre-release ""`, true, "", false}, 113 | {`--pre-release alpha`, true, "alpha", false}, 114 | {`--pre-release SNAPSHOT --pre-release-overwrite`, true, "SNAPSHOT", true}, 115 | // TODO: and this {`--pre-release --pre-release-overwrite`, true, "", true}, 116 | {`--pre-release '' --pre-release-overwrite`, true, "", true}, 117 | } 118 | 119 | for _, tc := range testData { 120 | t.Run(tc.args, func(t *testing.T) { 121 | assert := assert.New(t) 122 | out, errOut := new(bytes.Buffer), new(bytes.Buffer) 123 | globalOpts := &globalOptions{ 124 | ioStreams: newIOStreams(os.Stdin, out, errOut), 125 | } 126 | 127 | args, err := shellquote.Split(tc.args) 128 | assert.NoError(err) 129 | root := newBumpCommandsWithRun(globalOpts, func(o *bumpOptions) error { 130 | s := o.createBumpStrategy() 131 | 132 | assert.Len(s.BumpStrategies, 1) 133 | assert.Equal(".*", utils.RegexpToString(s.BumpStrategies[0].BranchesPattern)) 134 | assert.Equal(tc.expectedPreRelease, s.BumpStrategies[0].PreRelease) 135 | assert.Equal(tc.expectedPreReleaseTemplate, utils.TemplateToString(s.BumpStrategies[0].PreReleaseTemplate)) 136 | assert.Equal(tc.expectedPreReleaseOverwrite, s.BumpStrategies[0].PreReleaseOverwrite) 137 | assert.Equal("", utils.TemplateToString(s.BumpStrategies[0].BuildMetadataTemplate)) 138 | 139 | return nil 140 | }) 141 | globalOpts.addGlobalFlags(root) 142 | 143 | _, err = executeCommand(root, args...) 144 | assert.NoError(err) 145 | }) 146 | } 147 | } 148 | 149 | func TestBumpBuildMetadata(t *testing.T) { 150 | testData := []struct { 151 | args string 152 | expectedBumpBranchesStrategy []version.BumpBranchesStrategy 153 | }{ 154 | {``, []version.BumpBranchesStrategy{ 155 | *version.NewDefaultBumpBranchesStrategy(version.DefaultReleaseBranchesPattern), 156 | *version.NewBuildBumpBranchesStrategy(".*", version.DefaultBuildMetadataTemplate), 157 | }}, 158 | {`--build-metadata ""`, []version.BumpBranchesStrategy{ 159 | *version.NewBumpAllBranchesStrategy(version.AUTO, false, "", false, ""), 160 | }}, 161 | {`--build-metadata "{{.Branch}}.{{(.Commits | first).Hash.Short}}"`, []version.BumpBranchesStrategy{ 162 | *version.NewBumpAllBranchesStrategy(version.AUTO, false, "", false, "{{.Branch}}.{{(.Commits | first).Hash.Short}}"), 163 | }}, 164 | } 165 | 166 | for _, tc := range testData { 167 | t.Run(tc.args, func(t *testing.T) { 168 | assert := assert.New(t) 169 | out, errOut := new(bytes.Buffer), new(bytes.Buffer) 170 | globalOpts := &globalOptions{ 171 | ioStreams: newIOStreams(os.Stdin, out, errOut), 172 | } 173 | 174 | args, err := shellquote.Split(tc.args) 175 | assert.NoError(err) 176 | root := newBumpCommandsWithRun(globalOpts, func(o *bumpOptions) error { 177 | s := o.createBumpStrategy() 178 | 179 | assert.Equal(len(tc.expectedBumpBranchesStrategy), len(s.BumpStrategies)) 180 | for i := range tc.expectedBumpBranchesStrategy { 181 | assert.Equal(tc.expectedBumpBranchesStrategy[i].GoString(), s.BumpStrategies[i].GoString()) 182 | } 183 | 184 | return nil 185 | }) 186 | globalOpts.addGlobalFlags(root) 187 | 188 | _, err = executeCommand(root, args...) 189 | assert.NoError(err) 190 | }) 191 | } 192 | } 193 | 194 | func TestBumpBranchStrategy(t *testing.T) { 195 | testData := []struct { 196 | args string 197 | expectedBranchPattern string 198 | expectedPreRelease bool 199 | expectedPreReleaseTemplate string 200 | expectedPreReleaseOverwrite bool 201 | expectedBuildMetadata string 202 | }{ 203 | {``, `^(main|master|release/.*)$`, false, "", false, ""}, 204 | {`--branch-strategy '{"branchesPattern":".*","preRelease":true,"preReleaseTemplate":"foo","preReleaseOverwrite":true,"buildMetadataTemplate":"bar"}'`, `.*`, true, "foo", true, "bar"}, 205 | } 206 | 207 | for _, tc := range testData { 208 | t.Run(tc.args, func(t *testing.T) { 209 | assert := assert.New(t) 210 | out, errOut := new(bytes.Buffer), new(bytes.Buffer) 211 | globalOpts := &globalOptions{ 212 | ioStreams: newIOStreams(os.Stdin, out, errOut), 213 | } 214 | 215 | args, err := shellquote.Split(tc.args) 216 | assert.NoError(err) 217 | root := newBumpCommandsWithRun(globalOpts, func(o *bumpOptions) error { 218 | s := o.createBumpStrategy() 219 | 220 | size := 1 221 | if tc.args == "" { 222 | size = 2 223 | } 224 | assert.Len(s.BumpStrategies, size) 225 | assert.Equal(tc.expectedBranchPattern, utils.RegexpToString(s.BumpStrategies[0].BranchesPattern)) 226 | assert.Equal(tc.expectedPreRelease, s.BumpStrategies[0].PreRelease) 227 | assert.Equal(tc.expectedPreReleaseTemplate, utils.TemplateToString(s.BumpStrategies[0].PreReleaseTemplate)) 228 | assert.Equal(tc.expectedPreReleaseOverwrite, s.BumpStrategies[0].PreReleaseOverwrite) 229 | assert.Equal(tc.expectedBuildMetadata, utils.TemplateToString(s.BumpStrategies[0].BuildMetadataTemplate)) 230 | 231 | return nil 232 | }) 233 | globalOpts.addGlobalFlags(root) 234 | 235 | _, err = executeCommand(root, args...) 236 | assert.NoError(err) 237 | }) 238 | } 239 | } 240 | 241 | func TestWithConfiguration(t *testing.T) { 242 | assert := assert.New(t) 243 | 244 | out, errOut := new(bytes.Buffer), new(bytes.Buffer) 245 | globalOpts := &globalOptions{ 246 | ioStreams: newIOStreams(os.Stdin, out, errOut), 247 | } 248 | 249 | //args, err := shellquote.Split(tc.args) 250 | // assert.NoError(err) 251 | cmd := newBumpCommandsWithRun(globalOpts, func(o *bumpOptions) error { 252 | s := o.createBumpStrategy() 253 | 254 | assert.Equal("majorPatternConfig", s.MajorPattern.String(), "majorPattern does not match") 255 | assert.Equal("minorPatternConfig", s.MinorPattern.String(), "minorPattern does not match") 256 | expectedBumpBranchesStrategy := []version.BumpBranchesStrategy{ 257 | { 258 | Strategy: version.AUTO, 259 | BranchesPattern: regexp.MustCompile("releaseBranchesPattern"), 260 | }, 261 | { 262 | Strategy: version.AUTO, 263 | BranchesPattern: regexp.MustCompile("all"), 264 | BuildMetadataTemplate: utils.NewTemplate("myBuildMetadataTemplate"), 265 | }, 266 | } 267 | assert.Equal(len(expectedBumpBranchesStrategy), len(s.BumpStrategies)) 268 | for i := range expectedBumpBranchesStrategy { 269 | assert.Equal(expectedBumpBranchesStrategy[i].GoString(), s.BumpStrategies[i].GoString()) 270 | } 271 | 272 | return nil 273 | }) 274 | globalOpts.addGlobalFlags(cmd) 275 | 276 | cobra.OnInitialize(func() { 277 | viper.SetConfigType("yaml") 278 | 279 | var yamlConfig = []byte(` 280 | majorPattern: "majorPatternConfig" 281 | minorPattern: "minorPatternConfig" 282 | bumpStrategies: 283 | - branchesPattern: "releaseBranchesPattern" 284 | strategy: "AUTO" 285 | - branchesPattern: "all" 286 | strategy: "AUTO" 287 | buildMetadataTemplate: "myBuildMetadataTemplate" 288 | `) 289 | err := viper.ReadConfig(bytes.NewBuffer(yamlConfig)) 290 | assert.NoError(err, "Cannot read configuration") 291 | }) 292 | 293 | _, err := executeCommand(cmd) 294 | assert.NoError(err) 295 | } 296 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gsemver 2 | 3 | gsemver is a command line tool developed in [Go (Golang)](https://golang.org/) that uses git commit convention to automate the generation of your next version compliant with [semver 2.0.0 spec](https://semver.org/spec/v2.0.0.html). 4 | 5 | [![Build Status](https://github.com/arnaud-deprez/gsemver/workflows/Go/badge.svg)](https://github.com/arnaud-deprez/gsemver/actions/) 6 | [![GoDoc](https://godoc.org/github.com/arnaud-deprez/gsemver?status.svg)](https://godoc.org/github.com/arnaud-deprez/gsemver) 7 | [![Downloads](https://img.shields.io/github/downloads/arnaud-deprez/gsemver/total.svg)](https://github.com/arnaud-deprez/gsemver/releases) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/arnaud-deprez/gsemver)](https://goreportcard.com/report/github.com/arnaud-deprez/gsemver) 9 | [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/arnaud-deprez/gsemver/blob/main/LICENSE) 10 | [![codecov](https://codecov.io/gh/arnaud-deprez/gsemver/branch/main/graph/badge.svg)](https://codecov.io/gh/arnaud-deprez/gsemver) 11 | [![GitHub release](https://img.shields.io/github/release/arnaud-deprez/gsemver.svg)](https://github.com/arnaud-deprez/gsemver/releases) 12 | 13 | ## Table of Contents 14 | 15 | - [gsemver](#gsemver) 16 | - [Table of Contents](#table-of-contents) 17 | - [Motivations](#motivations) 18 | - [Thanks](#thanks) 19 | - [Getting Started](#getting-started) 20 | - [Installation](#installation) 21 | - [Go users](#go-users) 22 | - [Manual](#manual) 23 | - [Test Installation](#test-installation) 24 | - [Usage](#usage) 25 | - [Pre-requisites](#pre-requisites) 26 | - [CLI](#cli) 27 | - [Automatic version bump](#automatic-version-bump) 28 | - [Manual version bump](#manual-version-bump) 29 | - [Configuration file](#configuration-file) 30 | - [API](#api) 31 | - [Contributing](#contributing) 32 | - [Feedback](#feedback) 33 | - [License](#license) 34 | 35 | ## Motivations 36 | 37 | Why yet another git version tool ? 38 | 39 | When you try to implement DevOps pipeline for applications and libraries from different horizons (java, go, javascript, etc.), you always need to deal with versions from the moment you want to release your application/library to the deployment in production. 40 | 41 | As DevOps is all about automation, you need a way to automate the generation of your next version. 42 | 43 | Then, you have 2 choices: 44 | 45 | 1. you can use no human meaningful information: 46 | * forever increment a number 47 | * use git commit hash 48 | * use build number injected by your CI server 49 | * etc. 50 | 2. you can use a human meaningful convention such as [semver](https://semver.org/spec/v2.0.0.html). 51 | 52 | The first option is easy and does not required any tool. 53 | 54 | However some tools/tech require you to use a [semver](https://semver.org/spec/v2.0.0.html) compatible format version (eg. [go modules](https://github.com/golang/go/wiki/Modules), [helm](https://helm.sh/), etc.). 55 | You can still decide to always bump the major, minor or patch number but then your version is not meaningful in you are just doing a hack to be compliant with the spec format but not with spec semantic. 56 | 57 | So for the second option, in order to provide human meaningful information by following the spec semantic, you need to rely on some conventions. 58 | 59 | You can find some git convention such as: 60 | 61 | * [conventional commits](https://www.conventionalcommits.org): generalization of angular commit convention to other projects 62 | * [angular commit convention](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#-commit-message-guidelines) 63 | * [gitflow](https://datasift.github.io/gitflow/IntroducingGitFlow.html) 64 | 65 | Then I looked for existing tools and here is a non exhaustive list of what I've found so far: 66 | 67 | * [GitVersion](https://gitversion.readthedocs.io/en/latest/): tool written in .NET. 68 | * [semantic-release](https://github.com/semantic-release/semantic-release): tool for npm 69 | * [standard-version](https://github.com/conventional-changelog/standard-version): tool for npm 70 | * [jgitver](https://github.com/jgitver/jgitver): CLI running on java, maven and gradle plugins. 71 | * [hartym/git-semver](https://github.com/hartym/git-semver): git plugin written in python. 72 | * [markchalloner/git-semver](https://github.com/markchalloner/git-semver): another git plugin written in bash 73 | * [semver-maven-plugin](https://github.com/sidohaakma/semver-maven-plugin) 74 | 75 | All these tools have at least one of these problems: 76 | 77 | * They rely on a runtime environment (nodejs, python, java). But what if I want to build an application on another runtime ? On a VM, this is probably not a big deal but in a container where we try keep them as small as possible, this can be annoying. 78 | * They are not designed to automatically generate a new version based on a convention. Instead, you have to specify what number you want to bump (major, minor, patch) and/or what type of version you want to generate (alpha, beta, build, etc.) 79 | * They manage the full release lifecycle and so they are tightly coupled to some build tools like `npm`, `maven` or `gradle`. 80 | 81 | I've found some libraries written in [go](https://golang.org/) but they don't deal with git commits/tags convention: 82 | 83 | * [hashicorp/go-version](https://github.com/hashicorp/go-version) 84 | * [coreos/go-semver](https://github.com/coreos/go-semver) 85 | * [Masterminds/semver](https://github.com/Masterminds/semver) 86 | * [blang/semver](https://github.com/blang/semver) 87 | 88 | I needed a tool to generate the next release semver compatible version based on previous git tag that I could use on every type of application/library and so that is not relying on a specific runtime environment. 89 | 90 | That's why I decided to build this tool using [go](https://golang.org/) with inspirations and credits from the tools I've found. 91 | 92 | ## Thanks 93 | 94 | Thank you all for the inspirations! 95 | 96 | I'd like also to thanks 2 projects that are used in combination with gsemver to better automate the release of this tool: 97 | 98 | * [conventional commits](https://www.conventionalcommits.org) a commit convention I've decided to adopt in all my commits. 99 | * [git-chglog](https://github.com/git-chglog/git-chglog) is a customizable CHANGELOG generator implemented in go based on commits log. 100 | * [GoGeleaser](https://goreleaser.com) is a release automation tool for Go projects. 101 | 102 | With these 3 tools and `gsemver`, it gets easier to automate the release your projects. 103 | 104 | ## Getting Started 105 | 106 | ### Installation 107 | 108 | Please install `gsemver` in a way that matches your environment. 109 | 110 | #### Go users 111 | 112 | ```sh 113 | go install github.com/arnaud-deprez/gsemver@latest 114 | ``` 115 | 116 | #### Manual 117 | 118 | For a manual installation, you can download binary from [release page](https://github.com/arnaud-deprez/gsemver/releases) and place it in directory registered in your `$PATH` environment variable. 119 | 120 | ### Test Installation 121 | 122 | You can check with the following command whether the `gsemver` command was included in a valid `$PATH`. 123 | 124 | ```bash 125 | $ gsemver version 126 | # output the gsemver version 127 | ``` 128 | 129 | ## Usage 130 | 131 | ### Pre-requisites 132 | 133 | Most of CI server uses - by default - [shallow git clone](https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---depthltdepthgt) when cloning your git repository. 134 | 135 | When performing such a clone, the local copy of your git repository will contain a _truncated history_ and most probably will be _detached from HEAD_. 136 | 137 | As `gsemver` is currently using `git describe` to compute the next version, it means you should use **annotated tag** instead of _lightweight tag_ to release your code (see [lightweight vs annotated tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging#:~:text=Git%20supports%20two%20types%20of,objects%20in%20the%20Git%20database.)). 138 | Likewise, it also needs to have access to at least to the last parent annotated tag. 139 | For these reasons, `gsemver` will execute `git fetch --tags` before computing the next version. 140 | 141 | As `gsemver` also needs to know the current branch and it tries to retrieve it with `git symbolic-ref HEAD` command. 142 | However most of CI server execute the build in _detached from HEAD_ state and then it becomes hard in git to retrieve the branch from where the build has been triggered. 143 | Fortunately, most of CI server injects the branch name in an environment variable. 144 | That's why `gsemver` allows you to use the `GIT_BRANCH` environment variable as a backup solution. 145 | 146 | ### CLI 147 | 148 | #### Automatic version bump 149 | 150 | ```sh 151 | gsemver bump 152 | ``` 153 | 154 | This will use the git commits convention to generate the next version. 155 | 156 | The only current supported convention is [conventional commits](https://www.conventionalcommits.org). 157 | It also uses by default `main`, `master` and `release/*` branches by default as release branches and it generates version with build metadata for any branch that does not match. 158 | This is a current limitation but the [roadmap](https://github.com/arnaud-deprez/gsemver/issues/4) is to make more configurable. 159 | 160 | The [conventional commits integration tests](test/integration/gsemver_bump_auto_conventionalcommits_test.go) shows you in depth how version is generated. 161 | For a more comprehension view, here an example of the logs graph these tests generate: 162 | 163 | ```git 164 | * 34385d9 (HEAD -> main, tag: v1.2.2) Merge from feature/merge2-release-1.1.x 165 | |\ 166 | | * b884197 Merge from release/1.1.x 167 | | |\ 168 | |/ / 169 | | * 869c83f (tag: v1.1.3, release/1.1.x) Merge from fix/fix-3 170 | | |\ 171 | | | * 22eabaf fix: my bug fix 3 on release/1.1.x 172 | | |/ 173 | * | 704fde4 (tag: v1.2.1) Merge from feature/merge-release-1.1.x 174 | |\ \ 175 | | * \ 61b6a7c Merge from release/1.1.x 176 | | |\ \ 177 | |/ / / 178 | | | _ 179 | | * f2d9b5e (tag: v1.1.2) Merge from fix/fix-2 180 | | |\ 181 | | | * f95ccbe fix: my bug fix 2 on release/1.1.x 182 | | |/ 183 | * | 99a3662 (tag: v1.2.0) Merge from feature/awesome-3 184 | |\ \ 185 | | |/ 186 | |/| 187 | | * cc6c1ed feat: my awesome 3rd change 188 | |/ 189 | * 145cbff (tag: v1.1.1) Merge from bug/fix-1 190 | |\ 191 | | * 681a11b fix: my bug fix on main 192 | |/ 193 | * e9e7644 (tag: v1.1.0) Merge from feature/awesome-2 194 | |\ 195 | | * f30042e feat: my awesome 2nd change 196 | |/ 197 | * fba50a2 (tag: v1.0.0, tag: v0.2.0) Merge from feature/awesome-1 198 | |\ 199 | | * bf05218 feat: my awesome change 200 | |/ 201 | * c619bff (tag: v0.1.1) fix(doc): fix documentation 202 | * 128a5d9 (tag: v0.1.0) feat: add README.md 203 | ``` 204 | 205 | #### Manual version bump 206 | 207 | ```sh 208 | gsemver bump major 209 | gsemver bump minor 210 | gsemver bump patch 211 | ``` 212 | 213 | All the CLI options are documented [here](docs/cmd/gsemver.md). 214 | 215 | --- 216 | **NOTE** 217 | 218 | When you specify a CLI option for the bump command, it overrides the whole configuration if defined. See bellow. 219 | 220 | --- 221 | 222 | #### Go module tags 223 | 224 | Since v0.8.0, it can extract the version from a [go module tag](https://github.com/golang/go/wiki/Modules#publishing-a-release). 225 | 226 | **Example:** if your last tag is `foo/v1.2.0`, it will use `v1.2.0` to calculate the next version and return a version in the form of `vX.Y.Z` without the module prefix. 227 | 228 | #### Configuration file 229 | 230 | You can also use a configuration file to define your own rules. 231 | By default it will look for a file in `.gsemver.yaml` or then in `$HOME/.gsemver.yaml` but you can specify your own configuration file thanks to the `--config` (or `-c`) option: 232 | 233 | ```sh 234 | gsemver --config my-config.yaml 235 | # or 236 | gsemver -c my-config.yaml 237 | ``` 238 | 239 | The configuration file format looks like: 240 | 241 | ```yaml 242 | majorPattern: "(?:^.+\!:.*$|(?m)^BREAKING CHANGE:.*$)" 243 | minorPattern: "^(?:feat|chore|build|ci|refactor|perf)(?:\(.+\))?:.*$" 244 | bumpStrategies: 245 | - branchesPattern: "^(main|master|release/.*)$" 246 | strategy: "AUTO" 247 | preRelease: false 248 | preReleaseTemplate: 249 | preReleaseOverwrite: false 250 | buildMetadataTemplate: 251 | - branchesPattern: ".*" 252 | strategy: "AUTO" 253 | preRelease: false 254 | preReleaseTemplate: 255 | preReleaseOverwrite: false 256 | buildMetadataTemplate: "{{.Commits | len}}.{{(.Commits | first).Hash.Short}}" 257 | ``` 258 | 259 | This is the default configuration used for Conventional Commits. You can adapt the configuration to your needs. 260 | The `bumpStrategies` are applied in order until one matches the `branchesPattern` regular expression with the current branch. 261 | This allows you to define your strategies based on your own git flow. 262 | 263 | ### API 264 | 265 | For the API usage, you can check the [godoc](https://godoc.org/github.com/arnaud-deprez/gsemver) where there are some examples. 266 | 267 | You can also check [version bumper release](internal/release/main.go) which is used to release gsemver itself. 268 | 269 | ## Contributing 270 | 271 | We are always welcoming your contribution :clap: 272 | 273 | But to make everyone's work easier, please read the [CONTRIBUTING guide](CONTRIBUTING.md) first. 274 | 275 | ### Feedback 276 | 277 | I would like to make `gsemver` a better tool and take more scenario into account and eventually non conventional commits log. 278 | 279 | Therefore, your feedback is very useful. 280 | I am very happy to hear your opinions on Issues and PR :heart: 281 | 282 | ## License 283 | 284 | [MIT © Arnaud Deprez](./LICENSE) 285 | -------------------------------------------------------------------------------- /pkg/version/bump_strategy_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "go.uber.org/mock/gomock" 10 | 11 | "github.com/arnaud-deprez/gsemver/pkg/git" 12 | mock_version "github.com/arnaud-deprez/gsemver/pkg/version/mock" 13 | ) 14 | 15 | func TestBumpVersionStrategyWithoutTag(t *testing.T) { 16 | assert := assert.New(t) 17 | 18 | // mock 19 | ctrl := gomock.NewController(t) 20 | defer ctrl.Finish() 21 | 22 | testData := []struct { 23 | strategy BumpStrategyType 24 | branch string 25 | preRelease bool 26 | preReleaseTemplate string 27 | preReleaseOverwrite bool 28 | buildMetadataTemplate string 29 | expected string 30 | }{ 31 | {MAJOR, "dummy", false, "", false, "", "1.0.0"}, 32 | {MINOR, "dummy", false, "", false, "", "0.1.0"}, 33 | {PATCH, "dummy", false, "", false, "", "0.0.1"}, 34 | {MAJOR, "dummy", true, "", false, "", "1.0.0-0"}, 35 | {MAJOR, "dummy", true, "alpha", false, "", "1.0.0-alpha.0"}, 36 | {MINOR, "dummy", true, "SNAPSHOT", true, "", "0.1.0-SNAPSHOT"}, 37 | {0, "dummy", false, "", false, "build.8", "0.0.0+build.8"}, 38 | {AUTO, "master", false, "", false, "", "0.1.0"}, 39 | {AUTO, "master", true, "", false, "", "0.1.0-0"}, 40 | {AUTO, "master", true, "alpha", false, "", "0.1.0-alpha.0"}, 41 | {AUTO, "master", false, "", false, "build.1", "0.0.0+build.1"}, 42 | {AUTO, "main", false, "", false, "", "0.1.0"}, 43 | {AUTO, "main", true, "", false, "", "0.1.0-0"}, 44 | {AUTO, "main", true, "alpha", false, "", "0.1.0-alpha.0"}, 45 | {AUTO, "main", false, "", false, "build.1", "0.0.0+build.1"}, 46 | {AUTO, "feature/test", false, "", false, "{{ .Commits | len }}.{{ (.Commits | first).Hash.Short }}", "0.0.0+1.1234567"}, 47 | } 48 | 49 | for idx, tc := range testData { 50 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 51 | gitRepo := mock_version.NewMockGitRepo(ctrl) 52 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 53 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{}, nil) 54 | // no commit so it should return the same version 55 | gitRepo.EXPECT().GetCommits("", "HEAD").Times(1).Return([]git.Commit{ 56 | { 57 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 58 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 59 | Hash: git.Hash("1234567890"), 60 | Message: `feat: init import`, 61 | }, 62 | }, nil) 63 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 64 | 65 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 66 | strategy.BumpStrategies = []BumpBranchesStrategy{*NewBumpAllBranchesStrategy(tc.strategy, tc.preRelease, tc.preReleaseTemplate, tc.preReleaseOverwrite, tc.buildMetadataTemplate)} 67 | version, err := strategy.Bump() 68 | assert.Nil(err) 69 | assert.Equal(tc.expected, version.String()) 70 | }) 71 | } 72 | } 73 | 74 | func TestBumpVersionStrategyNoDeltaCommit(t *testing.T) { 75 | assert := assert.New(t) 76 | 77 | // mock 78 | ctrl := gomock.NewController(t) 79 | defer ctrl.Finish() 80 | 81 | testData := []struct { 82 | from string 83 | strategy BumpStrategyType 84 | branch string 85 | preRelease bool 86 | preReleaseTemplate string 87 | preReleaseOverwrite bool 88 | buildMetadata string 89 | expected string 90 | }{ 91 | {"v1.1.0-alpha.0", MAJOR, "dummy", false, "", false, "", "2.0.0"}, 92 | {"v1.1.0", PATCH, "dummy", false, "", false, "", "1.1.1"}, 93 | {"v1.2.0", MINOR, "dummy", false, "", false, "", "1.3.0"}, 94 | {"v1.2.0", MINOR, "dummy", true, "alpha", false, "", "1.3.0-alpha.0"}, 95 | {"1.2.0", MAJOR, "dummy", true, "SNAPSHOT", true, "", "2.0.0-SNAPSHOT"}, 96 | {"v1.2.0", MAJOR, "dummy", true, "SNAPSHOT", true, "", "2.0.0-SNAPSHOT"}, 97 | // in AUTO the version should not change 98 | {"1.2.0", AUTO, "master", false, "", false, "", "1.2.0"}, 99 | {"v1.2.0", AUTO, "master", false, "", false, "", "1.2.0"}, 100 | {"1.2.0", AUTO, "main", false, "", false, "", "1.2.0"}, 101 | {"v1.2.0", AUTO, "main", false, "", false, "", "1.2.0"}, 102 | {"v1.2.0", AUTO, "feature/test", false, "", false, "", "1.2.0"}, 103 | {"v1.2.0", AUTO, "master", true, "", false, "", "1.2.0"}, 104 | {"v1.2.0", AUTO, "master", true, "alpha", false, "", "1.2.0"}, 105 | {"v1.2.0", AUTO, "main", true, "", false, "", "1.2.0"}, 106 | {"v1.2.0", AUTO, "main", true, "alpha", false, "", "1.2.0"}, 107 | {"v1.2.0", AUTO, "feature/test", true, "alpha", false, "", "1.2.0"}, 108 | {"v1.2.0", AUTO, "master", true, "SNAPSHOT", true, "", "1.2.0"}, 109 | {"v1.2.0", AUTO, "main", true, "SNAPSHOT", true, "", "1.2.0"}, 110 | {"v1.2.0", AUTO, "feature/test", true, "SNAPSHOT", true, "", "1.2.0"}, 111 | {"v1.2.0", AUTO, "master", false, "", false, "build.1", "1.2.0"}, 112 | {"v1.2.0", AUTO, "main", false, "", false, "build.1", "1.2.0"}, 113 | {"v1.2.0", AUTO, "feature/test", false, "", false, "build.1", "1.2.0"}, 114 | {"arnaud-deprez/gsemver/1.2.0", AUTO, "feature/test", false, "", false, "build.1", "1.2.0"}, 115 | {"arnaud-deprez/gsemver/v1.2.0", AUTO, "feature/test", false, "", false, "build.1", "1.2.0"}, 116 | } 117 | 118 | for idx, tc := range testData { 119 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 120 | gitRepo := mock_version.NewMockGitRepo(ctrl) 121 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 122 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 123 | // no commit so it should return the same version 124 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{}, nil) 125 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 126 | 127 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 128 | strategy.BumpStrategies = []BumpBranchesStrategy{*NewBumpAllBranchesStrategy(tc.strategy, tc.preRelease, tc.preReleaseTemplate, tc.preReleaseOverwrite, tc.buildMetadata)} 129 | version, err := strategy.Bump() 130 | 131 | assert.Nil(err) 132 | assert.Equal(tc.expected, version.String()) 133 | }) 134 | } 135 | } 136 | 137 | func TestBumpVersionStrategyMajor(t *testing.T) { 138 | assert := assert.New(t) 139 | 140 | // mock 141 | ctrl := gomock.NewController(t) 142 | defer ctrl.Finish() 143 | 144 | gitRepo := mock_version.NewMockGitRepo(ctrl) 145 | from := "v0.1.0" 146 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 147 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: from}, nil) 148 | gitRepo.EXPECT().GetCommits(from, "HEAD").Times(1).Return([]git.Commit{ 149 | { 150 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 151 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 152 | Hash: git.Hash("1234567890"), 153 | Message: `This is not relevant`, 154 | }, 155 | }, nil) 156 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return("dummy", nil) 157 | 158 | strategy := &BumpStrategy{BumpStrategies: []BumpBranchesStrategy{*NewBumpAllBranchesStrategy(MAJOR, false, "", false, "")}, gitRepo: gitRepo} 159 | version, err := strategy.Bump() 160 | 161 | assert.Nil(err) 162 | assert.Equal("1.0.0", version.String()) 163 | } 164 | 165 | func TestBumpVersionStrategyMinor(t *testing.T) { 166 | assert := assert.New(t) 167 | 168 | // mock 169 | ctrl := gomock.NewController(t) 170 | defer ctrl.Finish() 171 | 172 | gitRepo := mock_version.NewMockGitRepo(ctrl) 173 | from := "v0.1.0" 174 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 175 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: from}, nil) 176 | gitRepo.EXPECT().GetCommits(from, "HEAD").Times(1).Return([]git.Commit{ 177 | { 178 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 179 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 180 | Hash: git.Hash("1234567890"), 181 | Message: `This is not relevant`, 182 | }, 183 | }, nil) 184 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return("dummy", nil) 185 | 186 | strategy := &BumpStrategy{BumpStrategies: []BumpBranchesStrategy{*NewBumpAllBranchesStrategy(MINOR, false, "", false, "")}, gitRepo: gitRepo} 187 | version, err := strategy.Bump() 188 | 189 | assert.Nil(err) 190 | assert.Equal("0.2.0", version.String()) 191 | } 192 | 193 | func TestBumpVersionStrategyPatch(t *testing.T) { 194 | assert := assert.New(t) 195 | 196 | // mock 197 | ctrl := gomock.NewController(t) 198 | defer ctrl.Finish() 199 | 200 | gitRepo := mock_version.NewMockGitRepo(ctrl) 201 | from := "v0.1.0" 202 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 203 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: from}, nil) 204 | gitRepo.EXPECT().GetCommits(from, "HEAD").Times(1).Return([]git.Commit{ 205 | { 206 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 207 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 208 | Hash: git.Hash("1234567890"), 209 | Message: `This is not relevant`, 210 | }, 211 | }, nil) 212 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return("dummy", nil) 213 | 214 | strategy := &BumpStrategy{BumpStrategies: []BumpBranchesStrategy{*NewBumpAllBranchesStrategy(PATCH, false, "", false, "")}, gitRepo: gitRepo} 215 | version, err := strategy.Bump() 216 | 217 | assert.Nil(err) 218 | assert.Equal("0.1.1", version.String()) 219 | } 220 | 221 | func TestBumpVersionStrategyAutoBreakingChangeOnInitialDevelopmentRelease(t *testing.T) { 222 | assert := assert.New(t) 223 | 224 | // mock 225 | ctrl := gomock.NewController(t) 226 | defer ctrl.Finish() 227 | 228 | testData := []struct { 229 | from string 230 | branch string 231 | expected string 232 | }{ 233 | {"v0.1.0", "master", "0.2.0"}, 234 | {"v0.1.0", "main", "0.2.0"}, 235 | {"v0.1.0", "feature/test", "0.1.0+1.1234567"}, 236 | {"foo/bar/0.1.0", "feature/test", "0.1.0+1.1234567"}, 237 | {"foo/bar/v0.1.0", "feature/test", "0.1.0+1.1234567"}, 238 | } 239 | 240 | for idx, tc := range testData { 241 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 242 | gitRepo := mock_version.NewMockGitRepo(ctrl) 243 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 244 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 245 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{ 246 | { 247 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 248 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 249 | Hash: git.Hash("1234567890"), 250 | Message: `feat(version): add auto bump strategies 251 | 252 | BREAKING CHANGE: replace next option by bump for more convenience 253 | `, 254 | }, 255 | }, nil) 256 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 257 | 258 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 259 | version, err := strategy.Bump() 260 | 261 | assert.Nil(err) 262 | assert.Equal(tc.expected, version.String()) 263 | }) 264 | } 265 | } 266 | 267 | func TestBumpVersionStrategyAutoBreakingChangeOnInitialDevelopmentReleaseShortForm(t *testing.T) { 268 | assert := assert.New(t) 269 | 270 | // mock 271 | ctrl := gomock.NewController(t) 272 | defer ctrl.Finish() 273 | 274 | testData := []struct { 275 | from string 276 | branch string 277 | expected string 278 | }{ 279 | {"v0.1.0", "master", "0.2.0"}, 280 | {"v0.1.0", "main", "0.2.0"}, 281 | {"foo/bar/v0.1.0", "main", "0.2.0"}, 282 | {"v0.1.0", "feature/test", "0.1.0+1.1234567"}, 283 | {"foo/bar/v0.1.0", "feature/test", "0.1.0+1.1234567"}, 284 | } 285 | 286 | for idx, tc := range testData { 287 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 288 | gitRepo := mock_version.NewMockGitRepo(ctrl) 289 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 290 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 291 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{ 292 | { 293 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 294 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 295 | Hash: git.Hash("1234567890"), 296 | Message: `feat(version)!: add auto bump strategies`, 297 | }, 298 | }, nil) 299 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 300 | 301 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 302 | version, err := strategy.Bump() 303 | 304 | assert.Nil(err) 305 | assert.Equal(tc.expected, version.String()) 306 | }) 307 | } 308 | } 309 | 310 | func TestBumpVersionStrategyAutoBreakingChange(t *testing.T) { 311 | assert := assert.New(t) 312 | 313 | // mock 314 | ctrl := gomock.NewController(t) 315 | defer ctrl.Finish() 316 | 317 | testData := []struct { 318 | from string 319 | branch string 320 | expected string 321 | }{ 322 | {"v1.1.0", "master", "2.0.0"}, 323 | {"v1.1.0", "main", "2.0.0"}, 324 | {"foo/bar/v1.1.0", "main", "2.0.0"}, 325 | {"v1.1.0", "feature/test", "1.1.0+2.1234567"}, 326 | {"foo/bar/v1.1.0", "feature/test", "1.1.0+2.1234567"}, 327 | } 328 | 329 | for idx, tc := range testData { 330 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 331 | gitRepo := mock_version.NewMockGitRepo(ctrl) 332 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 333 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 334 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{ 335 | { 336 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 337 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 338 | Hash: git.Hash("1234567890"), 339 | Message: `feat(version): add auto bump strategies 340 | 341 | BREAKING CHANGE: replace next option by bump for more convenience 342 | `, 343 | }, 344 | { 345 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 346 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 347 | Hash: git.Hash("1234567890"), 348 | Message: `feat(version): add pre-release option`, 349 | }, 350 | }, nil) 351 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 352 | 353 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 354 | version, err := strategy.Bump() 355 | 356 | assert.Nil(err) 357 | assert.Equal(tc.expected, version.String()) 358 | }) 359 | } 360 | } 361 | 362 | func TestBumpVersionStrategyAutoBreakingChangeShortForm(t *testing.T) { 363 | assert := assert.New(t) 364 | 365 | // mock 366 | ctrl := gomock.NewController(t) 367 | defer ctrl.Finish() 368 | 369 | testData := []struct { 370 | from string 371 | branch string 372 | expected string 373 | }{ 374 | {"v1.1.0", "master", "2.0.0"}, 375 | {"v1.1.0", "main", "2.0.0"}, 376 | {"foo/bar/v1.1.0", "main", "2.0.0"}, 377 | {"v1.1.0", "feature/test", "1.1.0+2.1234567"}, 378 | {"foo/bar/v1.1.0", "feature/test", "1.1.0+2.1234567"}, 379 | } 380 | 381 | for idx, tc := range testData { 382 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 383 | gitRepo := mock_version.NewMockGitRepo(ctrl) 384 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 385 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 386 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{ 387 | { 388 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 389 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 390 | Hash: git.Hash("1234567890"), 391 | Message: `fix(version)!: add auto bump strategies`, 392 | }, 393 | { 394 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 395 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 396 | Hash: git.Hash("1234567890"), 397 | Message: `feat(version): add pre-release option`, 398 | }, 399 | }, nil) 400 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 401 | 402 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 403 | version, err := strategy.Bump() 404 | 405 | assert.Nil(err) 406 | assert.Equal(tc.expected, version.String()) 407 | }) 408 | } 409 | } 410 | 411 | func TestBumpVersionStrategyAutoWithNewFeature(t *testing.T) { 412 | assert := assert.New(t) 413 | 414 | // mock 415 | ctrl := gomock.NewController(t) 416 | defer ctrl.Finish() 417 | 418 | testData := []struct { 419 | from string 420 | branch string 421 | expected string 422 | }{ 423 | {"v1.1.0", "master", "1.2.0"}, 424 | {"v1.1.0", "main", "1.2.0"}, 425 | {"foo/bar/v1.1.0", "main", "1.2.0"}, 426 | {"v1.1.0", "feature/test", "1.1.0+2.1234567"}, 427 | {"foo/bar/v1.1.0", "feature/test", "1.1.0+2.1234567"}, 428 | } 429 | 430 | for idx, tc := range testData { 431 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 432 | gitRepo := mock_version.NewMockGitRepo(ctrl) 433 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 434 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 435 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{ 436 | { 437 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 438 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 439 | Hash: git.Hash("1234567890"), 440 | Message: `fix: this should not be a patch anyway`, 441 | }, 442 | { 443 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 444 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 445 | Hash: git.Hash("1234567890"), 446 | Message: `feat(version): add pre-release option`, 447 | }, 448 | }, nil) 449 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 450 | 451 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 452 | version, err := strategy.Bump() 453 | 454 | assert.Nil(err) 455 | assert.Equal(tc.expected, version.String()) 456 | }) 457 | } 458 | } 459 | 460 | func TestBumpVersionStrategyAutoWithNewFeatureAndBody(t *testing.T) { 461 | assert := assert.New(t) 462 | 463 | // mock 464 | ctrl := gomock.NewController(t) 465 | defer ctrl.Finish() 466 | 467 | testData := []struct { 468 | from string 469 | branch string 470 | expected string 471 | }{ 472 | {"v1.1.0", "master", "1.2.0"}, 473 | {"v1.1.0", "main", "1.2.0"}, 474 | {"foo/bar/v1.1.0", "main", "1.2.0"}, 475 | {"v1.1.0", "feature/test", "1.1.0+2.1234567"}, 476 | {"foo/bar/v1.1.0", "feature/test", "1.1.0+2.1234567"}, 477 | } 478 | 479 | for idx, tc := range testData { 480 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 481 | gitRepo := mock_version.NewMockGitRepo(ctrl) 482 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 483 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 484 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{ 485 | { 486 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 487 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 488 | Hash: git.Hash("1234567890"), 489 | Message: `fix(version): this should not be a patch anyway 490 | 491 | Closes #123`, 492 | }, 493 | { 494 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 495 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 496 | Hash: git.Hash("1234567890"), 497 | Message: `feat(version): add pre-release option 498 | 499 | Closes #123`, 500 | }, 501 | }, nil) 502 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 503 | 504 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 505 | version, err := strategy.Bump() 506 | 507 | assert.Nil(err) 508 | assert.Equal(tc.expected, version.String()) 509 | }) 510 | } 511 | } 512 | 513 | func TestBumpVersionStrategyAutoWithPatch(t *testing.T) { 514 | assert := assert.New(t) 515 | 516 | // mock 517 | ctrl := gomock.NewController(t) 518 | defer ctrl.Finish() 519 | 520 | testData := []struct { 521 | from string 522 | branch string 523 | expected string 524 | }{ 525 | {"v1.1.0", "master", "1.1.1"}, 526 | {"v1.1.0", "main", "1.1.1"}, 527 | {"foo/bar/v1.1.0", "main", "1.1.1"}, 528 | {"v1.1.0", "feature/test", "1.1.0+1.1234567"}, 529 | {"foo/bar/v1.1.0", "feature/test", "1.1.0+1.1234567"}, 530 | {"v1.1.0", "release/1.1.x", "1.1.1"}, 531 | {"foo/bar/v1.1.0", "release/1.1.x", "1.1.1"}, 532 | } 533 | 534 | for idx, tc := range testData { 535 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 536 | gitRepo := mock_version.NewMockGitRepo(ctrl) 537 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 538 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 539 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{ 540 | { 541 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 542 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 543 | Hash: git.Hash("1234567890"), 544 | Message: `fix: typo error`, 545 | }, 546 | }, nil) 547 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 548 | 549 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 550 | version, err := strategy.Bump() 551 | 552 | assert.Nil(err) 553 | assert.Equal(tc.expected, version.String()) 554 | }) 555 | } 556 | } 557 | 558 | func TestBumpVersionStrategyAutoWithPreReleaseStrategyAndNewFeature(t *testing.T) { 559 | assert := assert.New(t) 560 | 561 | // mock 562 | ctrl := gomock.NewController(t) 563 | defer ctrl.Finish() 564 | 565 | testData := []struct { 566 | from string 567 | branch string 568 | preRelease bool 569 | preReleaseTemplate string 570 | expected string 571 | }{ 572 | {"v1.1.0", "master", true, "alpha", "1.2.0"}, 573 | {"v1.1.0", "main", true, "alpha", "1.2.0"}, 574 | {"v1.1.0", "milestone-1.2", true, "alpha", "1.2.0-alpha.0"}, 575 | {"foo/bar/v1.1.0", "milestone-1.2", true, "alpha", "1.2.0-alpha.0"}, 576 | {"v1.2.0-alpha.0", "milestone-1.2", true, "alpha", "1.2.0-alpha.1"}, 577 | {"foo/bar/v1.2.0-alpha.0", "milestone-1.2", true, "alpha", "1.2.0-alpha.1"}, 578 | {"v1.1.0", "feature/test", true, "alpha", "1.1.0+1.1234567"}, 579 | {"v1.1.0-alpha.0", "feature/test", true, "alpha", "1.1.0-alpha.0+1.1234567"}, 580 | {"foo/bar/v1.1.0-alpha.0", "feature/test", true, "alpha", "1.1.0-alpha.0+1.1234567"}, 581 | } 582 | 583 | for idx, tc := range testData { 584 | t.Run(fmt.Sprintf("Case %d", idx), func(_ *testing.T) { 585 | gitRepo := mock_version.NewMockGitRepo(ctrl) 586 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 587 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: tc.from}, nil) 588 | gitRepo.EXPECT().GetCommits(tc.from, "HEAD").Times(1).Return([]git.Commit{ 589 | { 590 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 591 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 592 | Hash: git.Hash("1234567890"), 593 | Message: `feat(version): add pre-release option`, 594 | }, 595 | }, nil) 596 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return(tc.branch, nil) 597 | 598 | strategy := &BumpStrategy{ 599 | gitRepo: gitRepo, 600 | BumpStrategies: []BumpBranchesStrategy{ 601 | *NewDefaultBumpBranchesStrategy(DefaultReleaseBranchesPattern), 602 | *NewBumpBranchesStrategy(AUTO, "milestone-1.2", tc.preRelease, tc.preReleaseTemplate, false, ""), 603 | *NewBumpAllBranchesStrategy(AUTO, DefaultPreRelease, DefaultPreReleaseTemplate, DefaultPreReleaseOverwrite, DefaultBuildMetadataTemplate), 604 | }, 605 | MajorPattern: regexp.MustCompile(DefaultMajorPattern), 606 | MinorPattern: regexp.MustCompile(DefaultMinorPattern), 607 | } 608 | version, err := strategy.Bump() 609 | 610 | assert.Nil(err) 611 | assert.Equal(tc.expected, version.String()) 612 | }) 613 | } 614 | } 615 | 616 | func TestBumpVersionStrategyAutoWithPreReleaseMavenLike(t *testing.T) { 617 | assert := assert.New(t) 618 | 619 | // mock 620 | ctrl := gomock.NewController(t) 621 | defer ctrl.Finish() 622 | 623 | gitRepo := mock_version.NewMockGitRepo(ctrl) 624 | from := "v1.0.0" 625 | gitRepo.EXPECT().FetchTags().Times(1).Return(nil) 626 | gitRepo.EXPECT().GetLastRelativeTag("HEAD").Times(1).Return(git.Tag{Name: from}, nil) 627 | gitRepo.EXPECT().GetCommits(from, "HEAD").Times(1).Return([]git.Commit{ 628 | { 629 | Author: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 630 | Committer: git.Signature{Name: "Arnaud Deprez", Email: "xxx@example.com"}, 631 | Hash: git.Hash("1234567890"), 632 | Message: `feat(version): add pre-release option`, 633 | }, 634 | }, nil) 635 | gitRepo.EXPECT().GetCurrentBranch().Times(1).Return("feature/xyz", nil) 636 | 637 | strategy := NewConventionalCommitBumpStrategy(gitRepo) 638 | strategy.BumpStrategies = []BumpBranchesStrategy{*NewBumpAllBranchesStrategy(AUTO, true, "SNAPSHOT", true, "")} 639 | version, err := strategy.Bump() 640 | 641 | assert.Nil(err) 642 | assert.Equal("1.1.0-SNAPSHOT", version.String()) 643 | } 644 | 645 | func TestSetGitRepository(t *testing.T) { 646 | assert := assert.New(t) 647 | s := &BumpStrategy{} 648 | // mock 649 | ctrl := gomock.NewController(t) 650 | defer ctrl.Finish() 651 | 652 | gitRepo := mock_version.NewMockGitRepo(ctrl) 653 | s.SetGitRepository(gitRepo) 654 | 655 | assert.Equal(gitRepo, s.gitRepo) 656 | } 657 | func ExampleBumpStrategy_GoString() { 658 | gitRepo := mock_version.NewMockGitRepo(nil) 659 | s := NewConventionalCommitBumpStrategy(gitRepo) 660 | fmt.Printf("%#v\n", s) 661 | // Output: version.BumpStrategy{MajorPattern: ®exp.Regexp{expr: "(?:^.+\\!:.+|(?m)^BREAKING CHANGE:.+$)"}, MinorPattern: ®exp.Regexp{expr: "^(?:feat|chore|build|ci|refactor|perf)(?:\\(.+\\))?:.+"}, BumpBranchesStrategies: []version.BumpBranchesStrategy{version.BumpBranchesStrategy{Strategy: AUTO, BranchesPattern: ®exp.Regexp{expr: "^(main|master|release/.*)$"}, PreRelease: false, PreReleaseTemplate: &template.Template{text: ""}, PreReleaseOverwrite: false, BuildMetadataTemplate: &template.Template{text: ""}}, version.BumpBranchesStrategy{Strategy: AUTO, BranchesPattern: ®exp.Regexp{expr: ".*"}, PreRelease: false, PreReleaseTemplate: &template.Template{text: ""}, PreReleaseOverwrite: false, BuildMetadataTemplate: &template.Template{text: "{{.Commits | len}}.{{(.Commits | first).Hash.Short}}"}}}} 662 | } 663 | --------------------------------------------------------------------------------