diff --git a/README.md b/README.md index 093044e..ed60a75 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,62 @@ go install github.com/caarlos0/svu@latest Or download one from the [releases tab](https://github.com/caarlos0/svu/releases) and install manually. +## use as library + +You can use `svu` as a library without the need to install the binary. For example to use it from a magefile: + +```go +//go:build mage +// +build mage + +package main + +import ( + "github.com/caarlos0/svu/pkg/svu" + "github.com/magefile/mage/sh" + "strings" +) + +// Tag the current commit with the proper next semver. +func Version() error { + v, err := svu.Next() + if err != nil { + return err + } + return sh.RunV("git", "tag", "-a", v, "-m", strings.Replace(v, "v", "Version ", 1)) +} +``` + +### commands + +All commands are available with a function named accordingly: + +- `svu.Next()` +- `svu.Current()` +- `svu.Major()` +- `svu.Minor()` +- `svu.Patch()` +- `svu.PreRelease()` + +### options + +All flags have a matching option function to configure the previous commands beyond their default bahavior: + +- `svu.Current(svu.WithPattern("p*"))` +- `svu.Next(svu.WithPrefix("ver"))` +- `svu.Major(svu.StripPrefix())` +- `svu.Minor(svu.WithPreRelease("pre"))` +- `svu.Patch(svu.WithBuild("3"))` +- `svu.Next(svu.WithDirectory("internal"))` +- `svu.Next(svu.WithTagMode(svu.AllBranches))` or `svu.Next(svu.ForAllBranches())` +- `svu.Next(svu.WithTagMode(svu.CurrentBranch))` or `svu.Next(svu.ForCurrentBranch())` +- `svu.Next(svu.ForcePatchIncrement())` + +Or multiple options: + +- `svu.Next(svu.WithPreRelease("pre"), svu.WithBuild("3"), svu.StripPrefix())` +- `svu.PreRelease(svu.WithPreRelease("alpha.33"), svu.WithBuild("243"))` + ## stargazers over time [![Stargazers over time](https://starchart.cc/caarlos0/svu.svg)](https://starchart.cc/caarlos0/svu) diff --git a/internal/git/git.go b/internal/git/git.go index e7442e0..471119d 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -9,6 +9,11 @@ import ( "github.com/gobwas/glob" ) +const ( + AllBranchesTagMode = "all-branches" + CurrentBranchTagMode = "current-branch" +) + // copied from goreleaser // IsRepo returns true if current folder is a git repository @@ -27,7 +32,7 @@ func getAllTags(args ...string) ([]string, error) { func DescribeTag(tagMode string, pattern string) (string, error) { args := []string{} - if tagMode == "current-branch" { + if tagMode == CurrentBranchTagMode { args = []string{"--merged"} } tags, err := getAllTags(args...) diff --git a/internal/svu/svu.go b/internal/svu/svu.go index 4bcb53e..45e7ca6 100644 --- a/internal/svu/svu.go +++ b/internal/svu/svu.go @@ -1,11 +1,25 @@ package svu import ( + "fmt" "regexp" + "strconv" + "strings" + + "github.com/caarlos0/svu/internal/git" "github.com/Masterminds/semver" ) +const ( + NextCmd = "next" + MajorCmd = "major" + MinorCmd = "minor" + PatchCmd = "patch" + CurrentCmd = "current" + PreReleaseCmd = "prerelease" +) + var ( breaking = regexp.MustCompile("(?m).*BREAKING[ -]CHANGE:.*") breakingBang = regexp.MustCompile(`(?im).*(\w+)(\(.*\))?!:.*`) @@ -13,6 +27,160 @@ var ( patch = regexp.MustCompile(`(?im).*fix(\(.*\))?:.*`) ) +type Options struct { + Cmd string + Pattern string + Prefix string + StripPrefix bool + PreRelease string + Build string + Directory string + TagMode string + ForcePatchIncrement bool +} + +func Version(opts Options) (string, error) { + tag, err := git.DescribeTag(string(opts.TagMode), opts.Pattern) + if err != nil { + return "", fmt.Errorf("failed to get current tag for repo: %w", err) + } + + current, err := getCurrentVersion(tag, opts.Prefix) + if err != nil { + return "", fmt.Errorf("could not get current version from tag: '%s': %w", tag, err) + } + + result, err := nextVersion(string(opts.Cmd), current, tag, opts.PreRelease, opts.Build, opts.Directory, opts.ForcePatchIncrement) + if err != nil { + return "", fmt.Errorf("could not get next tag: '%s': %w", tag, err) + } + + if opts.StripPrefix { + return result.String(), nil + } + return opts.Prefix + result.String(), nil +} + +func nextVersion(cmd string, current *semver.Version, tag, preRelease, build, directory string, force bool) (semver.Version, error) { + if cmd == CurrentCmd { + return *current, nil + } + + if force { + c, err := current.SetMetadata("") + if err != nil { + return c, err + } + c, err = c.SetPrerelease("") + if err != nil { + return c, err + } + current = &c + } + + var result semver.Version + var err error + switch cmd { + case NextCmd, PreReleaseCmd: + result, err = findNextWithGitLog(current, tag, directory, force) + case MajorCmd: + result = current.IncMajor() + case MinorCmd: + result = current.IncMinor() + case PatchCmd: + result = current.IncPatch() + } + if err != nil { + return result, err + } + + if cmd == PreReleaseCmd { + result, err = nextPreRelease(current, &result, preRelease) + if err != nil { + return result, err + } + } else { + result, err = result.SetPrerelease(preRelease) + if err != nil { + return result, err + } + } + + result, err = result.SetMetadata(build) + if err != nil { + return result, err + } + return result, nil +} + +func nextPreRelease(current, next *semver.Version, preRelease string) (semver.Version, error) { + suffix := "" + if preRelease != "" { + // Check if the suffix already contains a version number, if it does assume the user wants to explicitly set the version so use that + splitPreRelease := strings.Split(preRelease, ".") + if len(splitPreRelease) > 1 { + if _, err := strconv.Atoi(splitPreRelease[len(splitPreRelease)-1]); err == nil { + return current.SetPrerelease(preRelease) + } + } + + suffix = preRelease + + // Check if the prerelease suffix is the same as the current prerelease + preSuffix := strings.Split(current.Prerelease(), ".")[0] + if preSuffix == preRelease { + suffix = current.Prerelease() + } + } else if current.Prerelease() != "" { + suffix = current.Prerelease() + } else { + return *current, fmt.Errorf( + "--pre-release suffix is required to calculate next pre-release version as suffix could not be determined from current version: %s", + current.String(), + ) + } + + splitSuffix := strings.Split(suffix, ".") + preReleaseName := splitSuffix[0] + preReleaseVersion := 0 + + currentWithoutPreRelease, _ := current.SetPrerelease("") + + if !next.GreaterThan(¤tWithoutPreRelease) { + preReleaseVersion = -1 + if len(splitSuffix) == 2 { + preReleaseName = splitSuffix[0] + preReleaseVersion, _ = strconv.Atoi(splitSuffix[1]) + } else if len(splitSuffix) > 2 { + preReleaseName = splitSuffix[len(splitSuffix)-1] + } + + preReleaseVersion++ + } + + return next.SetPrerelease(fmt.Sprintf("%s.%d", preReleaseName, preReleaseVersion)) +} + +func getCurrentVersion(tag, prefix string) (*semver.Version, error) { + var current *semver.Version + var err error + if tag == "" { + current, err = semver.NewVersion(strings.TrimPrefix("0.0.0", prefix)) + } else { + current, err = semver.NewVersion(strings.TrimPrefix(tag, prefix)) + } + return current, err +} + +func findNextWithGitLog(current *semver.Version, tag string, directory string, forcePatchIncrement bool) (semver.Version, error) { + log, err := git.Changelog(tag, directory) + if err != nil { + return semver.Version{}, fmt.Errorf("failed to get changelog: %w", err) + } + + return findNext(current, forcePatchIncrement, log), nil +} + func isBreaking(log string) bool { return breaking.MatchString(log) || breakingBang.MatchString(log) } @@ -25,7 +193,7 @@ func isPatch(log string) bool { return patch.MatchString(log) } -func FindNext(current *semver.Version, forcePatchIncrement bool, log string) semver.Version { +func findNext(current *semver.Version, forcePatchIncrement bool, log string) semver.Version { if isBreaking(log) { if current.Major() == 0 { return current.IncMinor() diff --git a/internal/svu/svu_test.go b/internal/svu/svu_test.go index f4fcbcb..ff8c764 100644 --- a/internal/svu/svu_test.go +++ b/internal/svu/svu_test.go @@ -1,6 +1,7 @@ package svu import ( + "reflect" "testing" "github.com/Masterminds/semver" @@ -82,19 +83,268 @@ func TestFindNext(t *testing.T) { version2 := semver.MustParse("v2.4.12") version3 := semver.MustParse("v3.4.5-beta34+ads") for expected, next := range map[string]semver.Version{ - "v0.4.5": FindNext(version0a, false, "chore: should do nothing"), - "v0.4.6": FindNext(version0a, false, "fix: inc patch"), - "v0.5.0": FindNext(version0a, false, "feat: inc minor"), - "v0.6.0": FindNext(version0b, false, "feat!: inc minor"), - "v1.2.3": FindNext(version1, false, "chore: should do nothing"), - "v1.2.4": FindNext(version1, true, "chore: is forcing patch, so should inc patch"), - "v1.3.0": FindNext(version1, false, "feat: inc major"), - "v2.0.0": FindNext(version1, true, "chore!: hashbang incs major"), - "v3.0.0": FindNext(version2, false, "feat: something\nBREAKING CHANGE: increases major"), - "v3.5.0": FindNext(version3, false, "feat: inc major"), + "v0.4.5": findNext(version0a, false, "chore: should do nothing"), + "v0.4.6": findNext(version0a, false, "fix: inc patch"), + "v0.5.0": findNext(version0a, false, "feat: inc minor"), + "v0.6.0": findNext(version0b, false, "feat!: inc minor"), + "v1.2.3": findNext(version1, false, "chore: should do nothing"), + "v1.2.4": findNext(version1, true, "chore: is forcing patch, so should inc patch"), + "v1.3.0": findNext(version1, false, "feat: inc major"), + "v2.0.0": findNext(version1, true, "chore!: hashbang incs major"), + "v3.0.0": findNext(version2, false, "feat: something\nBREAKING CHANGE: increases major"), + "v3.5.0": findNext(version3, false, "feat: inc major"), } { t.Run(expected, func(t *testing.T) { is.New(t).True(semver.MustParse(expected).Equal(&next)) // expected and next version should match }) } } + +func TestCmd(t *testing.T) { + ver := func() *semver.Version { return semver.MustParse("1.2.3-pre+123") } + t.Run(CurrentCmd, func(t *testing.T) { + cmd := CurrentCmd + t.Run("version has meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "", "", "", false) + is.NoErr(err) + is.Equal("1.2.3-pre+123", v.String()) + }) + t.Run("version is clean", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("v1.2.3"), "v1.2.3", "doesnt matter", "nope", "", true) + is.NoErr(err) + is.Equal("1.2.3", v.String()) + }) + }) + + t.Run(MinorCmd, func(t *testing.T) { + cmd := MinorCmd + t.Run("no meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "", "", "", false) + is.NoErr(err) + is.Equal("1.3.0", v.String()) + }) + t.Run("build", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "", "124", "", false) + is.NoErr(err) + is.Equal("1.3.0+124", v.String()) + }) + t.Run("prerel", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "alpha.1", "", "", false) + is.NoErr(err) + is.Equal("1.3.0-alpha.1", v.String()) + }) + t.Run("all meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "alpha.2", "125", "", false) + is.NoErr(err) + is.Equal("1.3.0-alpha.2+125", v.String()) + }) + }) + + t.Run(PatchCmd, func(t *testing.T) { + cmd := PatchCmd + t.Run("no meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("1.2.3"), "v1.2.3", "", "", "", false) + is.NoErr(err) + is.Equal("1.2.4", v.String()) + }) + t.Run("previous had meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3", "", "", "", false) + is.NoErr(err) + is.Equal("1.2.3", v.String()) + }) + t.Run("previous had meta, force", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3", "", "", "", true) + is.NoErr(err) + is.Equal("1.2.4", v.String()) + }) + t.Run("previous had meta, force, add meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3-alpha.1+1", "alpha.2", "10", "", true) + is.NoErr(err) + is.Equal("1.2.4-alpha.2+10", v.String()) + }) + t.Run("previous had meta, change it", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3-alpha.1+1", "alpha.2", "10", "", false) + is.NoErr(err) + is.Equal("1.2.3-alpha.2+10", v.String()) + }) + t.Run("build", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("1.2.3"), "v1.2.3", "", "124", "", false) + is.NoErr(err) + is.Equal("1.2.4+124", v.String()) + }) + t.Run("prerel", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("1.2.3"), "v1.2.3", "alpha.1", "", "", false) + is.NoErr(err) + is.Equal("1.2.4-alpha.1", v.String()) + }) + t.Run("all meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, semver.MustParse("1.2.3"), "v1.2.3", "alpha.2", "125", "", false) + is.NoErr(err) + is.Equal("1.2.4-alpha.2+125", v.String()) + }) + }) + + t.Run(MajorCmd, func(t *testing.T) { + cmd := MajorCmd + t.Run("no meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "", "", "", false) + is.NoErr(err) + is.Equal("2.0.0", v.String()) + }) + t.Run("build", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "", "124", "", false) + is.NoErr(err) + is.Equal("2.0.0+124", v.String()) + }) + t.Run("prerel", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "alpha.1", "", "", false) + is.NoErr(err) + is.Equal("2.0.0-alpha.1", v.String()) + }) + t.Run("all meta", func(t *testing.T) { + is := is.New(t) + v, err := nextVersion(cmd, ver(), "v1.2.3", "alpha.2", "125", "", false) + is.NoErr(err) + is.Equal("2.0.0-alpha.2+125", v.String()) + }) + }) + + t.Run("errors", func(t *testing.T) { + t.Run("invalid build", func(t *testing.T) { + is := is.New(t) + _, err := nextVersion(MinorCmd, semver.MustParse("1.2.3"), "v1.2.3", "", "+125", "", false) + is.True(err != nil) + }) + t.Run("invalid prerelease", func(t *testing.T) { + is := is.New(t) + _, err := nextVersion(MinorCmd, semver.MustParse("1.2.3"), "v1.2.3", "+aaa", "", "", false) + is.True(err != nil) + }) + }) +} + +func Test_nextPreRelease(t *testing.T) { + type args struct { + current *semver.Version + next *semver.Version + preRelease string + } + tests := []struct { + name string + args args + want semver.Version + wantErr bool + }{ + { + name: "no current suffix and no suffix supplied", + args: args{ + current: semver.MustParse("1.2.3"), + next: semver.MustParse("1.3.0"), + preRelease: "", + }, + want: *semver.MustParse("1.3.0"), + wantErr: true, + }, + { + name: "supplied suffix overrides current suffix", + args: args{ + current: semver.MustParse("1.2.3-alpha.1"), + next: semver.MustParse("1.3.0"), + preRelease: "beta", + }, + want: *semver.MustParse("1.3.0-beta.0"), + wantErr: false, + }, + { + name: "current suffix is incremented", + args: args{ + current: semver.MustParse("1.2.3-alpha.11"), + next: semver.MustParse("1.2.3"), + preRelease: "", + }, + want: *semver.MustParse("1.2.3-alpha.12"), + wantErr: false, + }, + { + name: "current suffix is incremented when supplied suffix matches current", + args: args{ + current: semver.MustParse("1.2.3-alpha.11"), + next: semver.MustParse("1.2.3"), + preRelease: "alpha", + }, + want: *semver.MustParse("1.2.3-alpha.12"), + wantErr: false, + }, + { + name: "pre release version resets if next version changes", + args: args{ + current: semver.MustParse("1.2.3-alpha.11"), + next: semver.MustParse("1.2.4"), + preRelease: "alpha", + }, + want: *semver.MustParse("1.2.4-alpha.0"), + wantErr: false, + }, + { + name: "increments a current tag that has build metadata", + args: args{ + current: semver.MustParse("1.2.3-alpha.1+build.43"), + next: semver.MustParse("1.2.3"), + preRelease: "", + }, + want: *semver.MustParse("1.2.3-alpha.2"), + wantErr: false, + }, + { + name: "don't increment if explicit pre-release is supplied", + args: args{ + current: semver.MustParse("1.2.3-alpha.1"), + next: semver.MustParse("1.2.3"), + preRelease: "alpha.10", + }, + want: *semver.MustParse("1.2.3-alpha.10"), + wantErr: false, + }, + { + name: "prerelease suffix contains a number", + args: args{ + current: semver.MustParse("1.2.3-alpha123.1"), + next: semver.MustParse("1.2.3"), + preRelease: "alpha123", + }, + want: *semver.MustParse("1.2.3-alpha123.2"), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := nextPreRelease(tt.args.current, tt.args.next, tt.args.preRelease) + if tt.wantErr { + if err == nil { + t.Errorf("nextPreRelease() error = %v, wantErr %v", err, tt.wantErr) + } + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("nextPreRelease() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/main.go b/main.go index 19dc287..d329ce5 100644 --- a/main.go +++ b/main.go @@ -4,12 +4,8 @@ import ( "fmt" "os" "runtime/debug" - "strconv" - "strings" - "github.com/Masterminds/semver" "github.com/alecthomas/kingpin" - "github.com/caarlos0/svu/internal/git" "github.com/caarlos0/svu/internal/svu" ) @@ -45,136 +41,19 @@ func main() { app.HelpFlag.Short('h') cmd := kingpin.MustParse(app.Parse(os.Args[1:])) - tag, err := git.DescribeTag(*tagMode, *pattern) - app.FatalIfError(err, "failed to get current tag for repo") - - current, err := getCurrentVersion(tag) - app.FatalIfError(err, "could not get current version from tag: '%s'", tag) - - result, err := nextVersion(cmd, current, tag, *preRelease, *build, *forcePatchIncrement) - app.FatalIfError(err, "could not get next tag: '%s'", tag) - - if *stripPrefix { - fmt.Println(result.String()) - return - } - fmt.Println(*prefix + result.String()) -} - -func nextVersion(cmd string, current *semver.Version, tag, preRelease, build string, force bool) (semver.Version, error) { - if cmd == currentCmd.FullCommand() { - return *current, nil - } - - if force { - c, err := current.SetMetadata("") - if err != nil { - return c, err - } - c, err = c.SetPrerelease("") - if err != nil { - return c, err - } - current = &c - } - - var result semver.Version - switch cmd { - case nextCmd.FullCommand(): - result = findNext(current, tag, *directory) - case majorCmd.FullCommand(): - result = current.IncMajor() - case minorCmd.FullCommand(): - result = current.IncMinor() - case patchCmd.FullCommand(): - result = current.IncPatch() - } - - var err error - if cmd == preReleaseCmd.FullCommand() { - next := findNext(current, tag, *directory) - result, err = nextPreRelease(current, &next, preRelease) - if err != nil { - return result, err - } - } else { - result, err = result.SetPrerelease(preRelease) - if err != nil { - return result, err - } - } - - result, err = result.SetMetadata(build) - if err != nil { - return result, err - } - return result, nil -} - -func nextPreRelease(current, next *semver.Version, preRelease string) (semver.Version, error) { - suffix := "" - if preRelease != "" { - // Check if the suffix already contains a version number, if it does assume the user wants to explicitly set the version so use that - splitPreRelease := strings.Split(preRelease, ".") - if len(splitPreRelease) > 1 { - if _, err := strconv.Atoi(splitPreRelease[len(splitPreRelease)-1]); err == nil { - return current.SetPrerelease(preRelease) - } - } - - suffix = preRelease - - // Check if the prerelease suffix is the same as the current prerelease - preSuffix := strings.Split(current.Prerelease(), ".")[0] - if preSuffix == preRelease { - suffix = current.Prerelease() - } - } else if current.Prerelease() != "" { - suffix = current.Prerelease() - } else { - return *current, fmt.Errorf( - "--pre-release suffix is required to calculate next pre-release version as suffix could not be determined from current version: %s", - current.String(), - ) - } - - splitSuffix := strings.Split(suffix, ".") - preReleaseName := splitSuffix[0] - preReleaseVersion := 0 - - currentWithoutPreRelease, _ := current.SetPrerelease("") - - if !next.GreaterThan(¤tWithoutPreRelease) { - preReleaseVersion = -1 - if len(splitSuffix) == 2 { - preReleaseName = splitSuffix[0] - preReleaseVersion, _ = strconv.Atoi(splitSuffix[1]) - } else if len(splitSuffix) > 2 { - preReleaseName = splitSuffix[len(splitSuffix)-1] - } - - preReleaseVersion++ - } - - return next.SetPrerelease(fmt.Sprintf("%s.%d", preReleaseName, preReleaseVersion)) -} - -func getCurrentVersion(tag string) (*semver.Version, error) { - var current *semver.Version - var err error - if tag == "" { - current, err = semver.NewVersion(strings.TrimPrefix("0.0.0", *prefix)) - } else { - current, err = semver.NewVersion(strings.TrimPrefix(tag, *prefix)) - } - return current, err -} - -func findNext(current *semver.Version, tag string, directory string) semver.Version { - log, err := git.Changelog(tag, directory) - app.FatalIfError(err, "failed to get changelog") - - return svu.FindNext(current, *forcePatchIncrement, log) + version, err := svu.Version(svu.Options{ + Cmd: cmd, + Pattern: *pattern, + Prefix: *prefix, + StripPrefix: *stripPrefix, + PreRelease: *preRelease, + Build: *build, + Directory: *directory, + TagMode: *tagMode, + ForcePatchIncrement: *forcePatchIncrement, + }) + app.FatalIfError(err, "") + fmt.Println(version) } // nolint: gochecknoglobals diff --git a/main_test.go b/main_test.go index d92f0ec..631f64e 100644 --- a/main_test.go +++ b/main_test.go @@ -1,10 +1,8 @@ package main import ( - "reflect" "testing" - "github.com/Masterminds/semver" "github.com/matryer/is" ) @@ -20,252 +18,3 @@ built at: 2021-01-02 built by: goreleaser`, buildVersion("v1.2.3", "a123cd", "2021-01-02", "goreleaser")) }) } - -func TestCmd(t *testing.T) { - ver := func() *semver.Version { return semver.MustParse("1.2.3-pre+123") } - t.Run(currentCmd.FullCommand(), func(t *testing.T) { - cmd := currentCmd.FullCommand() - t.Run("version has meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "", "", false) - is.NoErr(err) - is.Equal("1.2.3-pre+123", v.String()) - }) - t.Run("version is clean", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("v1.2.3"), "v1.2.3", "doesnt matter", "nope", true) - is.NoErr(err) - is.Equal("1.2.3", v.String()) - }) - }) - - t.Run(minorCmd.FullCommand(), func(t *testing.T) { - cmd := minorCmd.FullCommand() - t.Run("no meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "", "", false) - is.NoErr(err) - is.Equal("1.3.0", v.String()) - }) - t.Run("build", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "", "124", false) - is.NoErr(err) - is.Equal("1.3.0+124", v.String()) - }) - t.Run("prerel", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "alpha.1", "", false) - is.NoErr(err) - is.Equal("1.3.0-alpha.1", v.String()) - }) - t.Run("all meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "alpha.2", "125", false) - is.NoErr(err) - is.Equal("1.3.0-alpha.2+125", v.String()) - }) - }) - - t.Run(patchCmd.FullCommand(), func(t *testing.T) { - cmd := patchCmd.FullCommand() - t.Run("no meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("1.2.3"), "v1.2.3", "", "", false) - is.NoErr(err) - is.Equal("1.2.4", v.String()) - }) - t.Run("previous had meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3", "", "", false) - is.NoErr(err) - is.Equal("1.2.3", v.String()) - }) - t.Run("previous had meta, force", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3", "", "", true) - is.NoErr(err) - is.Equal("1.2.4", v.String()) - }) - t.Run("previous had meta, force, add meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3-alpha.1+1", "alpha.2", "10", true) - is.NoErr(err) - is.Equal("1.2.4-alpha.2+10", v.String()) - }) - t.Run("previous had meta, change it", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("1.2.3-alpha.1+1"), "v1.2.3-alpha.1+1", "alpha.2", "10", false) - is.NoErr(err) - is.Equal("1.2.3-alpha.2+10", v.String()) - }) - t.Run("build", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("1.2.3"), "v1.2.3", "", "124", false) - is.NoErr(err) - is.Equal("1.2.4+124", v.String()) - }) - t.Run("prerel", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("1.2.3"), "v1.2.3", "alpha.1", "", false) - is.NoErr(err) - is.Equal("1.2.4-alpha.1", v.String()) - }) - t.Run("all meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, semver.MustParse("1.2.3"), "v1.2.3", "alpha.2", "125", false) - is.NoErr(err) - is.Equal("1.2.4-alpha.2+125", v.String()) - }) - }) - - t.Run(majorCmd.FullCommand(), func(t *testing.T) { - cmd := majorCmd.FullCommand() - t.Run("no meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "", "", false) - is.NoErr(err) - is.Equal("2.0.0", v.String()) - }) - t.Run("build", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "", "124", false) - is.NoErr(err) - is.Equal("2.0.0+124", v.String()) - }) - t.Run("prerel", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "alpha.1", "", false) - is.NoErr(err) - is.Equal("2.0.0-alpha.1", v.String()) - }) - t.Run("all meta", func(t *testing.T) { - is := is.New(t) - v, err := nextVersion(cmd, ver(), "v1.2.3", "alpha.2", "125", false) - is.NoErr(err) - is.Equal("2.0.0-alpha.2+125", v.String()) - }) - }) - - t.Run("errors", func(t *testing.T) { - t.Run("invalid build", func(t *testing.T) { - is := is.New(t) - _, err := nextVersion(minorCmd.FullCommand(), semver.MustParse("1.2.3"), "v1.2.3", "", "+125", false) - is.True(err != nil) - }) - t.Run("invalid prerelease", func(t *testing.T) { - is := is.New(t) - _, err := nextVersion(minorCmd.FullCommand(), semver.MustParse("1.2.3"), "v1.2.3", "+aaa", "", false) - is.True(err != nil) - }) - }) -} - -func Test_nextPreRelease(t *testing.T) { - type args struct { - current *semver.Version - next *semver.Version - preRelease string - } - tests := []struct { - name string - args args - want semver.Version - wantErr bool - }{ - { - name: "no current suffix and no suffix supplied", - args: args{ - current: semver.MustParse("1.2.3"), - next: semver.MustParse("1.3.0"), - preRelease: "", - }, - want: *semver.MustParse("1.3.0"), - wantErr: true, - }, - { - name: "supplied suffix overrides current suffix", - args: args{ - current: semver.MustParse("1.2.3-alpha.1"), - next: semver.MustParse("1.3.0"), - preRelease: "beta", - }, - want: *semver.MustParse("1.3.0-beta.0"), - wantErr: false, - }, - { - name: "current suffix is incremented", - args: args{ - current: semver.MustParse("1.2.3-alpha.11"), - next: semver.MustParse("1.2.3"), - preRelease: "", - }, - want: *semver.MustParse("1.2.3-alpha.12"), - wantErr: false, - }, - { - name: "current suffix is incremented when supplied suffix matches current", - args: args{ - current: semver.MustParse("1.2.3-alpha.11"), - next: semver.MustParse("1.2.3"), - preRelease: "alpha", - }, - want: *semver.MustParse("1.2.3-alpha.12"), - wantErr: false, - }, - { - name: "pre release version resets if next version changes", - args: args{ - current: semver.MustParse("1.2.3-alpha.11"), - next: semver.MustParse("1.2.4"), - preRelease: "alpha", - }, - want: *semver.MustParse("1.2.4-alpha.0"), - wantErr: false, - }, - { - name: "increments a current tag that has build metadata", - args: args{ - current: semver.MustParse("1.2.3-alpha.1+build.43"), - next: semver.MustParse("1.2.3"), - preRelease: "", - }, - want: *semver.MustParse("1.2.3-alpha.2"), - wantErr: false, - }, - { - name: "don't increment if explicit pre-release is supplied", - args: args{ - current: semver.MustParse("1.2.3-alpha.1"), - next: semver.MustParse("1.2.3"), - preRelease: "alpha.10", - }, - want: *semver.MustParse("1.2.3-alpha.10"), - wantErr: false, - }, - { - name: "prerelease suffix contains a number", - args: args{ - current: semver.MustParse("1.2.3-alpha123.1"), - next: semver.MustParse("1.2.3"), - preRelease: "alpha123", - }, - want: *semver.MustParse("1.2.3-alpha123.2"), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := nextPreRelease(tt.args.current, tt.args.next, tt.args.preRelease) - if tt.wantErr { - if err == nil { - t.Errorf("nextPreRelease() error = %v, wantErr %v", err, tt.wantErr) - } - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("nextPreRelease() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/svu/svu.go b/pkg/svu/svu.go new file mode 100644 index 0000000..22ccfc4 --- /dev/null +++ b/pkg/svu/svu.go @@ -0,0 +1,121 @@ +package svu + +import ( + "github.com/caarlos0/svu/internal/git" + "github.com/caarlos0/svu/internal/svu" +) + +type TagMode string + +const ( + CurrentBranch TagMode = git.CurrentBranchTagMode + AllBranches TagMode = git.AllBranchesTagMode +) + +type option func(o *svu.Options) + +func Next(opts ...option) (string, error) { + return version(append(opts, cmd(svu.NextCmd))...) +} + +func Major(opts ...option) (string, error) { + return version(append(opts, cmd(svu.MajorCmd))...) +} + +func Minor(opts ...option) (string, error) { + return version(append(opts, cmd(svu.MinorCmd))...) +} + +func Patch(opts ...option) (string, error) { + return version(append(opts, cmd(svu.PatchCmd))...) +} + +func Current(opts ...option) (string, error) { + return version(append(opts, cmd(svu.CurrentCmd))...) +} + +func PreRelease(opts ...option) (string, error) { + return version(append(opts, cmd(svu.PreReleaseCmd))...) +} + +func WithPattern(pattern string) option { + return func(o *svu.Options) { + o.Pattern = pattern + } +} + +func WithPrefix(prefix string) option { + return func(o *svu.Options) { + o.Prefix = prefix + } +} + +func WithStripPrefix(stripPrefix bool) option { + return func(o *svu.Options) { + o.StripPrefix = stripPrefix + } +} + +func StripPrefix() option { + return WithStripPrefix(true) +} + +func WithPreRelease(preRelease string) option { + return func(o *svu.Options) { + o.PreRelease = preRelease + } +} + +func WithBuild(build string) option { + return func(o *svu.Options) { + o.Build = build + } +} + +func WithDirectory(directory string) option { + return func(o *svu.Options) { + o.Directory = directory + } +} + +func WithTagMode(tagMode TagMode) option { + return func(o *svu.Options) { + o.TagMode = string(tagMode) + } +} + +func ForCurrentBranch() option { + return WithTagMode(CurrentBranch) +} + +func ForAllBranches() option { + return WithTagMode(AllBranches) +} + +func WithForcePatchIncrement(forcePatchIncrement bool) option { + return func(o *svu.Options) { + o.ForcePatchIncrement = forcePatchIncrement + } +} + +func ForcePatchIncrement() option { + return WithForcePatchIncrement(true) +} + +func version(opts ...option) (string, error) { + options := &svu.Options{ + Cmd: svu.NextCmd, + Prefix: "v", + TagMode: string(CurrentBranch), + } + for _, opt := range opts { + opt(options) + } + return svu.Version(*options) +} + +func cmd(cmd string) option { + return func(o *svu.Options) { + o.Cmd = cmd + } +}