diff --git a/completers/gh_completer/cmd/action/release.go b/completers/gh_completer/cmd/action/release.go index 4db7cbe09f..217a943ffa 100644 --- a/completers/gh_completer/cmd/action/release.go +++ b/completers/gh_completer/cmd/action/release.go @@ -4,6 +4,7 @@ import ( "time" "github.com/rsteube/carapace" + "github.com/rsteube/carapace-bin/pkg/actions/number" "github.com/rsteube/carapace-bin/pkg/styles" "github.com/rsteube/carapace-bin/pkg/util" "github.com/rsteube/carapace/pkg/style" @@ -37,6 +38,22 @@ type releaseQuery struct { } } +func ActionNextReleases(cmd *cobra.Command) carapace.Action { + return carapace.ActionCallback(func(c carapace.Context) carapace.Action { + var queryResult releaseQuery + return GraphQlAction(cmd, `repository(owner: $owner, name: $repo) { releases(first: 100, orderBy: {direction: DESC, field: CREATED_AT}) { edges { node { createdAt isPrerelease name tag { name } } } } }`, &queryResult, func() carapace.Action { + releases := queryResult.Data.Repository.Releases.Edges + vals := make([]string, 0, len(releases)) + for _, release := range releases { + if release.Node.Tag.Name != "" { + vals = append(vals, release.Node.Tag.Name) + } + } + return number.ActionSemanticVersions(vals...).Style(styles.Git.Tag) + }) + }) +} + func ActionReleases(cmd *cobra.Command) carapace.Action { return carapace.ActionCallback(func(c carapace.Context) carapace.Action { var queryResult releaseQuery diff --git a/completers/gh_completer/cmd/release_create.go b/completers/gh_completer/cmd/release_create.go index ef781797f2..525d902351 100644 --- a/completers/gh_completer/cmd/release_create.go +++ b/completers/gh_completer/cmd/release_create.go @@ -38,7 +38,7 @@ func init() { }) carapace.Gen(release_createCmd).PositionalCompletion( - action.ActionReleases(release_createCmd), + action.ActionNextReleases(release_createCmd), ) carapace.Gen(release_createCmd).PositionalAnyCompletion(carapace.ActionFiles()) } diff --git a/go.mod b/go.mod index 726dd8f57b..1588d6f92c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/rsteube/carapace-spec v0.5.3 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 + golang.org/x/mod v0.7.0 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 052ba76e89..cda8ab125c 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= diff --git a/pkg/actions/number/version.go b/pkg/actions/number/version.go new file mode 100644 index 0000000000..8c33917d6f --- /dev/null +++ b/pkg/actions/number/version.go @@ -0,0 +1,72 @@ +package number + +import ( + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/rsteube/carapace" + "golang.org/x/mod/semver" +) + +// ActionSemanticVersions completes the next semantic version based on the given existing versions. +// +// v0.20.4 (next patch version) +// v0.21.0 (next minor version) +func ActionSemanticVersions(versions ...string) carapace.Action { + return carapace.ActionCallback(func(c carapace.Context) carapace.Action { + if len(versions) == 0 { + versions = append(versions, "v0.0.0") + } + + withoutPrefix := make(map[string]bool, 0) + for index, version := range versions { + switch { + case semver.IsValid(version): + // valid + case semver.IsValid("v" + version): + // support versions without 'v' prefix as well + withoutPrefix[version] = true + versions[index] = "v" + version + default: + // skip invalid + versions[index] = "" + } + } + + sort.Sort(semver.ByVersion(versions)) + canonical := semver.Canonical(versions[len(versions)-1]) + if canonical == "" { + return carapace.ActionMessage("invalid version %#v", versions[0]) + } + + prefix := "v" + if _, ok := withoutPrefix[strings.TrimPrefix(canonical, "v")]; ok { + prefix = "" + } + + r := regexp.MustCompile(`^v(?P[0-9]+)\.(?P[0-9]+)\.(?P[0-9]+)`) + vals := make([]string, 0) + if matches := r.FindStringSubmatch(canonical); matches != nil { + // valid format ensured by semver.Canonical + major, _ := strconv.Atoi(matches[1]) + minor, _ := strconv.Atoi(matches[2]) + patch, _ := strconv.Atoi(matches[3]) + + if semver.Prerelease(canonical) != "" { + vals = append(vals, + fmt.Sprintf("%v%v.%v.%v", prefix, major, minor, patch), "next version", + ) + } else { + vals = append(vals, + fmt.Sprintf("%v%v.%v.%v", prefix, major+1, 0, 0), "next major version", + fmt.Sprintf("%v%v.%v.%v", prefix, major, minor+1, 0), "next minor version", + fmt.Sprintf("%v%v.%v.%v", prefix, major, minor, patch+1), "next patch version", + ) + } + } + return carapace.ActionValuesDescribed(vals...) + }) +}