release-tag 0.2.5__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ package completion
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+
7
+ "github.com/spf13/cobra"
8
+ )
9
+
10
+ func AddCompletionCmd(cmd *cobra.Command) *cobra.Command {
11
+ var completionCmd = &cobra.Command{
12
+ Use: "completion [bash|zsh|fish|powershell]",
13
+ Short: "Generate completion script",
14
+ Long: fmt.Sprintf(`To load completions:
15
+
16
+ Bash:
17
+
18
+ $ source <(%[1]s completion bash)
19
+
20
+ # To load completions for each session, execute once:
21
+ # Linux:
22
+ $ %[1]s completion bash > /etc/bash_completion.d/%[1]s
23
+ # macOS:
24
+ $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
25
+
26
+ Zsh:
27
+
28
+ # If shell completion is not already enabled in your environment,
29
+ # you will need to enable it. You can execute the following once:
30
+
31
+ $ echo "autoload -U compinit; compinit" >> ~/.zshrc
32
+
33
+ # To load completions for each session, execute once:
34
+ $ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
35
+
36
+ # You will need to start a new shell for this setup to take effect.
37
+
38
+ fish:
39
+
40
+ $ %[1]s completion fish | source
41
+
42
+ # To load completions for each session, execute once:
43
+ $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
44
+
45
+ PowerShell:
46
+
47
+ PS> %[1]s completion powershell | Out-String | Invoke-Expression
48
+
49
+ # To load completions for every new session, run:
50
+ PS> %[1]s completion powershell > %[1]s.ps1
51
+ # and source this file from your PowerShell profile.
52
+ `, cmd.Root().Name()),
53
+ DisableFlagsInUseLine: true,
54
+ ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
55
+ Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
56
+ Run: func(cmd *cobra.Command, args []string) {
57
+ var err error
58
+ switch args[0] {
59
+ case "bash":
60
+ err = cmd.Root().GenBashCompletion(os.Stdout)
61
+ case "zsh":
62
+ err = cmd.Root().GenZshCompletion(os.Stdout)
63
+ case "fish":
64
+ err = cmd.Root().GenFishCompletion(os.Stdout, true)
65
+ case "powershell":
66
+ err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
67
+ }
68
+ if err != nil {
69
+ fmt.Fprintf(os.Stderr, "Error generating completion: %v\n", err)
70
+ os.Exit(1)
71
+ }
72
+ },
73
+ }
74
+ return completionCmd
75
+ }
git/git.go ADDED
@@ -0,0 +1,123 @@
1
+ package git
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "os/exec"
7
+ "strings"
8
+
9
+ "github.com/jmelahman/tag/semver"
10
+ )
11
+
12
+ func GetLatestSemverTag() (string, error) {
13
+ cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
14
+ output, err := cmd.Output()
15
+ if err != nil {
16
+ return "v0.0.0", nil
17
+ }
18
+
19
+ tagsAt, err := ListTagsAt(strings.TrimSpace(string(output)))
20
+ if err != nil {
21
+ return "v0.0.0", nil
22
+ }
23
+
24
+ var largestTag string
25
+ var largestVersion *semver.Version
26
+
27
+ for _, tag := range tagsAt {
28
+ if largestTag == "" {
29
+ largestTag = tag
30
+ }
31
+
32
+ version, err := semver.ParseSemver(tag)
33
+ if err != nil {
34
+ continue
35
+ }
36
+
37
+ if largestVersion == nil {
38
+ largestTag = tag
39
+ largestVersion = version
40
+ continue
41
+ }
42
+
43
+ if semver.CompareSemver(version, largestVersion) {
44
+ largestTag = tag
45
+ largestVersion = version
46
+ }
47
+ }
48
+
49
+ return largestTag, nil
50
+ }
51
+
52
+ // List all git tags
53
+ func ListTags() ([]string, error) {
54
+ cmd := exec.Command("git", "tag", "-l", "v[0-9]*.[0-9]*.[0-9]*")
55
+ cmd.Stderr = os.Stderr
56
+ tagsOutput, err := cmd.Output()
57
+ if err != nil {
58
+ return nil, err
59
+ }
60
+
61
+ tagList := strings.Split(strings.TrimSpace(string(tagsOutput)), "\n")
62
+
63
+ return tagList, nil
64
+ }
65
+
66
+ func ListTagsAt(ref string) ([]string, error) {
67
+ cmd := exec.Command("git", "tag", "--points-at", ref)
68
+ cmd.Stderr = os.Stderr
69
+ tagsOutput, err := cmd.Output()
70
+ if err != nil {
71
+ return nil, err
72
+ }
73
+
74
+ tagList := strings.Split(strings.TrimSpace(string(tagsOutput)), "\n")
75
+
76
+ return tagList, nil
77
+ }
78
+
79
+ func TagExists(tag string) (bool, error) {
80
+ cmd := exec.Command("git", "show-ref", "--tags", "--quiet", tag)
81
+ if err := cmd.Run(); err != nil {
82
+ if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 {
83
+ return false, nil
84
+ }
85
+ return false, err
86
+ }
87
+ return true, nil
88
+ }
89
+
90
+ func CreateAndPushTag(tag string, remote string) error {
91
+ cmd := exec.Command("git", "tag", tag)
92
+ cmd.Stderr = os.Stderr
93
+ if err := cmd.Run(); err != nil {
94
+ return fmt.Errorf("failed to create tag: %w", err)
95
+ }
96
+
97
+ cmd = exec.Command("git", "push", "--quiet", remote, tag)
98
+ cmd.Stderr = os.Stderr
99
+ if err := cmd.Run(); err != nil {
100
+ return fmt.Errorf("failed to push tag to %s: %w", remote, err)
101
+ }
102
+
103
+ return nil
104
+ }
105
+
106
+ func FetchSemverTags(remote string) error {
107
+ cmd := exec.Command("git", "fetch", "--quiet", "--prune", remote, "refs/tags/v*:refs/tags/v*")
108
+ cmd.Stderr = os.Stderr
109
+ if err := cmd.Run(); err != nil {
110
+ return fmt.Errorf("failed to fetch tags from %s: %w", remote, err)
111
+ }
112
+ return nil
113
+ }
114
+
115
+ func IsHEADAlreadyTagged() (bool, error) {
116
+ cmd := exec.Command("git", "tag", "--points-at", "HEAD")
117
+ cmd.Stderr = os.Stderr
118
+ output, err := cmd.Output()
119
+ if err != nil {
120
+ return false, fmt.Errorf("failed to check tags for HEAD: %w", err)
121
+ }
122
+ return len(strings.TrimSpace(string(output))) > 0, nil
123
+ }
go.mod ADDED
@@ -0,0 +1,16 @@
1
+ module github.com/jmelahman/tag
2
+
3
+ go 1.23.4
4
+
5
+ require (
6
+ github.com/spf13/cobra v1.8.1
7
+ github.com/stretchr/testify v1.10.0
8
+ )
9
+
10
+ require (
11
+ github.com/davecgh/go-spew v1.1.1 // indirect
12
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
13
+ github.com/pmezard/go-difflib v1.0.0 // indirect
14
+ github.com/spf13/pflag v1.0.5 // indirect
15
+ gopkg.in/yaml.v3 v3.0.1 // indirect
16
+ )
go.sum ADDED
@@ -0,0 +1,18 @@
1
+ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4
+ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
5
+ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
6
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8
+ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
9
+ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
10
+ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
11
+ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
12
+ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
13
+ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
14
+ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
15
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
16
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
18
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
main.go ADDED
@@ -0,0 +1,127 @@
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "fmt"
6
+ "os"
7
+ "strings"
8
+
9
+ "github.com/jmelahman/tag/completion"
10
+ "github.com/jmelahman/tag/git"
11
+ "github.com/jmelahman/tag/semver"
12
+ "github.com/spf13/cobra"
13
+ )
14
+
15
+ var (
16
+ version = "dev"
17
+ commit = "none"
18
+ )
19
+
20
+ func main() {
21
+ var major, minor, patch, push bool
22
+ var metadata, suffix, remote string
23
+
24
+ rootCmd := &cobra.Command{
25
+ Use: "tag",
26
+ Short: "Calculate the next semantic version tag",
27
+ Version: fmt.Sprintf("%s\ncommit %s", version, commit),
28
+ PreRunE: func(cmd *cobra.Command, args []string) error {
29
+ // Validate that only one version increment flag is set
30
+ incrementFlags := []bool{major, minor, patch}
31
+ var setFlags int
32
+ for _, flag := range incrementFlags {
33
+ if flag {
34
+ setFlags++
35
+ }
36
+ }
37
+ if setFlags > 1 {
38
+ return fmt.Errorf("only one version increment flag (--major, --minor, or --patch) can be used at a time")
39
+ }
40
+ return nil
41
+ },
42
+ Run: func(cmd *cobra.Command, args []string) {
43
+ if err := git.FetchSemverTags(remote); err != nil {
44
+ fmt.Printf("Error fetching tags: %v\n", err)
45
+ os.Exit(1)
46
+ }
47
+
48
+ // Check if HEAD is already tagged
49
+ alreadyTagged, err := git.IsHEADAlreadyTagged()
50
+ if err != nil {
51
+ fmt.Printf("Error checking tags: %v\n", err)
52
+ os.Exit(1)
53
+ }
54
+ if alreadyTagged {
55
+ fmt.Println("Error: Current HEAD is already tagged")
56
+ os.Exit(1)
57
+ }
58
+
59
+ latestTag, err := git.GetLatestSemverTag()
60
+ if err != nil {
61
+ fmt.Printf("Error: %v\n", err)
62
+ os.Exit(1)
63
+ }
64
+
65
+ allTags, err := git.ListTags()
66
+ if err != nil {
67
+ fmt.Printf("Error: %v\n", err)
68
+ os.Exit(1)
69
+ }
70
+
71
+ nextVersion, err := semver.CalculateNextVersion(latestTag, allTags, major, minor, patch, suffix)
72
+ if err != nil {
73
+ fmt.Printf("Error: %v\n", err)
74
+ os.Exit(1)
75
+ }
76
+
77
+ if metadata != "" {
78
+ nextVersion = fmt.Sprint(nextVersion, "+", metadata)
79
+ }
80
+
81
+ tagExists, err := git.TagExists(nextVersion)
82
+ if err != nil {
83
+ fmt.Printf("Error: %v\n", err)
84
+ os.Exit(1)
85
+ }
86
+
87
+ if tagExists {
88
+ fmt.Printf("Next tag '%s' already exists.\n", nextVersion)
89
+ os.Exit(1)
90
+ }
91
+
92
+ if !push {
93
+ reader := bufio.NewReader(os.Stdin)
94
+ fmt.Printf("Push tag '%s' to %s? (y/N): ", nextVersion, remote)
95
+ response, _ := reader.ReadString('\n')
96
+ response = strings.TrimSpace(strings.ToLower(response))
97
+
98
+ if response == "" || response == "y" || response == "yes" {
99
+ push = true
100
+ }
101
+ }
102
+
103
+ if push {
104
+ if err := git.CreateAndPushTag(nextVersion, remote); err != nil {
105
+ fmt.Printf("Error: %v\n", err)
106
+ os.Exit(1)
107
+ }
108
+ fmt.Printf("Tag '%s' created and pushed to %s.\n", nextVersion, remote)
109
+ }
110
+ },
111
+ }
112
+
113
+ rootCmd.Flags().BoolVar(&major, "major", false, "increment the major version")
114
+ rootCmd.Flags().BoolVar(&minor, "minor", false, "increment the minor version")
115
+ rootCmd.Flags().BoolVar(&patch, "patch", false, "increment the patch version")
116
+ rootCmd.Flags().BoolVar(&push, "push", false, "create and push the tag to remote")
117
+ rootCmd.Flags().StringVar(&suffix, "suffix", "", "set the pre-release suffix (e.g., rc, alpha, beta)")
118
+ rootCmd.Flags().StringVar(&metadata, "metadata", "", "set the build metadata")
119
+ rootCmd.Flags().StringVar(&remote, "remote", "origin", "remote repository to push tag to")
120
+
121
+ rootCmd.AddCommand(completion.AddCompletionCmd(rootCmd))
122
+
123
+ if err := rootCmd.Execute(); err != nil {
124
+ fmt.Printf("Error: %v\n", err)
125
+ os.Exit(1)
126
+ }
127
+ }
Binary file
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: release-tag
3
+ Version: 0.2.5
4
+ Summary: Automatically create semantic version git tags
5
+ Project-URL: Repository, https://github.com/jmelahman/tag
6
+ Author-email: Jamison Lahman <jamison@lahman.dev>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2024 Jamison Lahman
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Keywords: git,git tags,release,releasing,semver,tagging
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Operating System :: OS Independent
32
+ Classifier: Programming Language :: Go
33
+ Requires-Python: >=3.6
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Tag Release
37
+
38
+ [![Test status](https://github.com/jmelahman/tag/actions/workflows/test.yml/badge.svg)](https://github.com/jmelahman/tag/actions)
39
+ [![Deploy Status](https://github.com/jmelahman/tag/actions/workflows/release.yml/badge.svg)](https://github.com/jmelahman/tag/actions)
40
+ [![Go Reference](https://pkg.go.dev/badge/github.com/jmelahman/tag.svg)](https://pkg.go.dev/github.com/jmelahman/tag)
41
+ [![Arch User Repsoitory](https://img.shields.io/aur/version/release-tag)](https://aur.archlinux.org/packages/release-tag)
42
+ [![PyPI](https://img.shields.io/pypi/v/release-tag.svg)]()
43
+ [![Go Report Card](https://goreportcard.com/badge/github.com/jmelahman/tag)](https://goreportcard.com/report/github.com/jmelahman/tag)
44
+
45
+ Automatically create [semantic version](https://semver.org/) git tags.
46
+
47
+ ```text
48
+ $ tag
49
+ Push tag 'v1.0.1' to origin? (y/N): y
50
+ Tag 'v1.0.1' created and pushed to origin.
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ By default, `tag` will increment the smallest digit following [SemVer precedence](https://semver.org/#semantic-versioning-specification-semver).
56
+ Incrementing a specific is achieved by passing the respective flag: `--major`, `--minor`, `--patch`.
57
+
58
+ Tags can be automatically pushed to a remote repository by passing `--push`.
59
+
60
+ `tag` supports [pre-release](https://semver.org/#spec-item-9) versions.
61
+ Creating a pre-release tag is achieved by the using the `--suffix` flag.
62
+ For example, `--suffix="alpha"` will create a tag like `v1.0.0-alpha`.
63
+ If the previous tag was for a pre-release, that suffix is preferred.
64
+ This behavior can be overridden by passing `--patch` or `--suffix=""`.
65
+ Only incrementing the trailing pre-release identifier is currently supported.
66
+
67
+ `tag` authoritatively discourages duplicate tags for a single commit.
68
+
69
+ ### Autocomplete
70
+
71
+ `tag` provides autocomplete for `bash`, `fish`, `powershell` and `zsh` shells.
72
+ For example, to enable autocomplete for the `bash` shell,
73
+
74
+ ```shell
75
+ tag completion bash | sudo tee /etc/bash_completion.d/tag > /dev/null
76
+ ```
77
+
78
+ _Note: bash completion requires the [bash-completion](https://github.com/scop/bash-completion/) package be installed._
79
+
80
+ For more information, see `tag completion <shell> --help` for your respective `<shell>`.
81
+
82
+ ## Install
83
+
84
+ **AUR:**
85
+
86
+ `tag` is available from the [Arch User Repository](https://aur.archlinux.org/packages/release-tag).
87
+
88
+ ```shell
89
+ yay -S release-tag
90
+ ```
91
+
92
+ **pip:**
93
+
94
+ `tag` is available as a [pypi package](https://pypi.org/project/release-tag/).
95
+
96
+ ```shell
97
+ pip install release-tag
98
+ ```
99
+
100
+ **go:**
101
+
102
+ ```shell
103
+ go install github.com/jmelahman/tag@latest
104
+ ```
105
+
106
+ **github:**
107
+
108
+ Prebuilt packages are available from [Github Releases](https://github.com/jmelahman/tag/releases).
@@ -0,0 +1,12 @@
1
+ go.mod,sha256=yqrrzLAqT2bxEcjq-XEp7hRrtrLVgyozVrUUSo-UE9Y,371
2
+ go.sum,sha256=NhTyKeYVMD29_XicuWpg9bSSxCALHSPAHkAtC8AZO9I,1592
3
+ main.go,sha256=wrz-whOruIWj-1lRCyDNqS2DNHRf8o99hsW1l62MHGs,3377
4
+ completion/completion.go,sha256=5d7jq9-sQATbG2jqEnhmLvoBBKpkP0HrCs2B9EeTwDM,2055
5
+ git/git.go,sha256=LKx7fmBwa2jgRkZJwUfDeelke0ryTwPeQ2isfCBuB0E,2733
6
+ semver/semver.go,sha256=LQgpwqqne2NV312U8_6AMQvS-VSh6TD2Dg6AtuDmYOA,3046
7
+ semver/semver_test.go,sha256=XosZIXKOZeK1x6r9W1GUBdghXXKYtFeXD1YjwlHq8kU,5793
8
+ release_tag-0.2.5.data/scripts/tag,sha256=-Oqz4POzs3jNRktwBILkiVrMpav5-zS-iMihgerH75Q,4009472
9
+ release_tag-0.2.5.dist-info/METADATA,sha256=NUM0ZWh-xy_aGV79N5X_8ZuKz7TL_T0eebJbiCj2DzU,4378
10
+ release_tag-0.2.5.dist-info/WHEEL,sha256=Vhj7RpPOKCXcsr4rUs7q4pISuo5nT6VsOuaQwH9GILY,94
11
+ release_tag-0.2.5.dist-info/licenses/LICENSE,sha256=P_te03YIieIzk4EnP3-blVrdJGbhityi_YcHwxaL7gk,1071
12
+ release_tag-0.2.5.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jamison Lahman
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.
semver/semver.go ADDED
@@ -0,0 +1,127 @@
1
+ package semver
2
+
3
+ import (
4
+ "fmt"
5
+ "regexp"
6
+ "strconv"
7
+ )
8
+
9
+ type Version struct {
10
+ Major int
11
+ Minor int
12
+ Patch int
13
+ PreRelease string
14
+ PreReleaseNum int
15
+ }
16
+
17
+ func ParseSemver(tag string) (*Version, error) {
18
+ re := regexp.MustCompile(`v(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z]+)(?:\.(\d+))?)?`)
19
+ matches := re.FindStringSubmatch(tag)
20
+ if matches == nil {
21
+ return nil, fmt.Errorf("invalid semver tag: %s", tag)
22
+ }
23
+
24
+ major, _ := strconv.Atoi(matches[1])
25
+ minor, _ := strconv.Atoi(matches[2])
26
+ patch, _ := strconv.Atoi(matches[3])
27
+ preRelease := matches[4]
28
+ preReleaseNum := 0
29
+ if len(matches) > 5 && matches[5] != "" {
30
+ preReleaseNum, _ = strconv.Atoi(matches[5])
31
+ }
32
+
33
+ return &Version{
34
+ Major: major,
35
+ Minor: minor,
36
+ Patch: patch,
37
+ PreRelease: preRelease,
38
+ PreReleaseNum: preReleaseNum,
39
+ }, nil
40
+ }
41
+
42
+ func CompareSemver(v1, v2 *Version) bool {
43
+ if v1.Major > v2.Major {
44
+ return true
45
+ }
46
+ if v1.Major < v2.Major {
47
+ return false
48
+ }
49
+ if v1.Minor > v2.Minor {
50
+ return true
51
+ }
52
+ if v1.Minor < v2.Minor {
53
+ return false
54
+ }
55
+ if v1.Patch > v2.Patch {
56
+ return true
57
+ }
58
+ if v1.Patch < v2.Patch {
59
+ return false
60
+ }
61
+
62
+ // Handle pre-release versions
63
+ if v1.PreRelease == "" && v2.PreRelease != "" {
64
+ return true
65
+ }
66
+ if v1.PreRelease != "" && v2.PreRelease == "" {
67
+ return false
68
+ }
69
+ if v1.PreRelease == v2.PreRelease {
70
+ return v1.PreReleaseNum > v2.PreReleaseNum
71
+ }
72
+ return false
73
+ }
74
+
75
+ func CalculateNextVersion(tag string, allTags []string, incMajor, incMinor, incPatch bool, suffix string) (string, error) {
76
+ // Parse the current version
77
+ version, err := ParseSemver(tag)
78
+ if err != nil {
79
+ return "", err
80
+ }
81
+
82
+ // Increment version based on flags
83
+ if incMajor {
84
+ version.Major++
85
+ version.Minor = 0
86
+ version.Patch = 0
87
+ version.PreReleaseNum = 0
88
+ } else if incMinor {
89
+ version.Minor++
90
+ version.Patch = 0
91
+ version.PreReleaseNum = 0
92
+ } else if incPatch {
93
+ version.Patch++
94
+ version.PreReleaseNum = 0
95
+ } else if suffix == "" && version.PreRelease == "" {
96
+ version.Patch++
97
+ } else if suffix != "" && version.PreRelease != suffix {
98
+ // Find the largest PreReleaseNum for the given suffix
99
+ largestPreReleaseNum := 0
100
+ for _, existingTag := range allTags {
101
+ existingVersion, err := ParseSemver(existingTag)
102
+ if err == nil && existingVersion.PreRelease == suffix {
103
+ if existingVersion.PreReleaseNum > largestPreReleaseNum {
104
+ largestPreReleaseNum = existingVersion.PreReleaseNum
105
+ }
106
+ }
107
+ }
108
+ if largestPreReleaseNum > 0 {
109
+ version.PreReleaseNum = largestPreReleaseNum + 1
110
+ }
111
+ } else {
112
+ version.PreReleaseNum++
113
+ }
114
+
115
+ if suffix != "" {
116
+ version.PreRelease = suffix
117
+ }
118
+
119
+ // Construct the version string
120
+ if version.PreRelease != "" {
121
+ if version.PreReleaseNum > 0 {
122
+ return fmt.Sprintf("v%d.%d.%d-%s.%d", version.Major, version.Minor, version.Patch, version.PreRelease, version.PreReleaseNum), nil
123
+ }
124
+ return fmt.Sprintf("v%d.%d.%d-%s", version.Major, version.Minor, version.Patch, version.PreRelease), nil
125
+ }
126
+ return fmt.Sprintf("v%d.%d.%d", version.Major, version.Minor, version.Patch), nil
127
+ }
semver/semver_test.go ADDED
@@ -0,0 +1,261 @@
1
+ package semver
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/stretchr/testify/assert"
7
+ )
8
+
9
+ func TestParseSemver(t *testing.T) {
10
+ testCases := []struct {
11
+ name string
12
+ tag string
13
+ expectedVer *Version
14
+ expectError bool
15
+ }{
16
+ {
17
+ name: "Standard version",
18
+ tag: "v1.2.3",
19
+ expectedVer: &Version{
20
+ Major: 1,
21
+ Minor: 2,
22
+ Patch: 3,
23
+ },
24
+ },
25
+ {
26
+ name: "Pre-release version",
27
+ tag: "v1.2.3-rc",
28
+ expectedVer: &Version{
29
+ Major: 1,
30
+ Minor: 2,
31
+ Patch: 3,
32
+ PreRelease: "rc",
33
+ PreReleaseNum: 0,
34
+ },
35
+ },
36
+ {
37
+ name: "Pre-release version",
38
+ tag: "v1.2.3-rc.1",
39
+ expectedVer: &Version{
40
+ Major: 1,
41
+ Minor: 2,
42
+ Patch: 3,
43
+ PreRelease: "rc",
44
+ PreReleaseNum: 1,
45
+ },
46
+ },
47
+ {
48
+ name: "Build metadata version",
49
+ tag: "v1.2.3+21AF26D3",
50
+ expectedVer: &Version{
51
+ Major: 1,
52
+ Minor: 2,
53
+ Patch: 3,
54
+ PreRelease: "",
55
+ PreReleaseNum: 0,
56
+ },
57
+ },
58
+ {
59
+ name: "Invalid version",
60
+ tag: "invalid-tag",
61
+ expectError: true,
62
+ },
63
+ }
64
+
65
+ for _, tc := range testCases {
66
+ t.Run(tc.name, func(t *testing.T) {
67
+ version, err := ParseSemver(tc.tag)
68
+
69
+ if tc.expectError {
70
+ assert.Error(t, err)
71
+ } else {
72
+ assert.NoError(t, err)
73
+ assert.Equal(t, tc.expectedVer, version)
74
+ }
75
+ })
76
+ }
77
+ }
78
+
79
+ func TestCompareSemver(t *testing.T) {
80
+ testCases := []struct {
81
+ name string
82
+ v1 *Version
83
+ v2 *Version
84
+ expected bool
85
+ }{
86
+ {
87
+ name: "v1 major version higher",
88
+ v1: &Version{Major: 2, Minor: 0, Patch: 0},
89
+ v2: &Version{Major: 1, Minor: 9, Patch: 9},
90
+ expected: true,
91
+ },
92
+ {
93
+ name: "v1 minor version higher",
94
+ v1: &Version{Major: 1, Minor: 2, Patch: 0},
95
+ v2: &Version{Major: 1, Minor: 1, Patch: 9},
96
+ expected: true,
97
+ },
98
+ {
99
+ name: "v1 patch version higher",
100
+ v1: &Version{Major: 1, Minor: 1, Patch: 2},
101
+ v2: &Version{Major: 1, Minor: 1, Patch: 1},
102
+ expected: true,
103
+ },
104
+ {
105
+ name: "v1 pre-release version higher",
106
+ v1: &Version{Major: 1, Minor: 1, Patch: 1, PreRelease: "rc", PreReleaseNum: 2},
107
+ v2: &Version{Major: 1, Minor: 1, Patch: 1, PreRelease: "rc", PreReleaseNum: 1},
108
+ expected: true,
109
+ },
110
+ {
111
+ name: "Identical versions",
112
+ v1: &Version{Major: 1, Minor: 1, Patch: 1},
113
+ v2: &Version{Major: 1, Minor: 1, Patch: 1},
114
+ expected: false,
115
+ },
116
+ }
117
+
118
+ for _, tc := range testCases {
119
+ t.Run(tc.name, func(t *testing.T) {
120
+ result := CompareSemver(tc.v1, tc.v2)
121
+ assert.Equal(t, tc.expected, result)
122
+ })
123
+ }
124
+ }
125
+
126
+ func TestCalculateNextVersion(t *testing.T) {
127
+ testCases := []struct {
128
+ name string
129
+ currentTag string
130
+ incMajor bool
131
+ incMinor bool
132
+ incPatch bool
133
+ suffix string
134
+ allTags []string
135
+ expectedTag string
136
+ expectError bool
137
+ }{
138
+ {
139
+ name: "Invalid tag format",
140
+ currentTag: "invalid-tag",
141
+ expectError: true,
142
+ },
143
+ {
144
+ name: "Next tag already exists error",
145
+ currentTag: "v1.2.3",
146
+ allTags: []string{"v1.2.3", "v1.3.0"},
147
+ expectedTag: "v1.2.4",
148
+ },
149
+ {
150
+ name: "Patch increment",
151
+ currentTag: "v1.2.3",
152
+ expectedTag: "v1.2.4",
153
+ },
154
+ {
155
+ name: "Patch increment with incPatch",
156
+ currentTag: "v1.2.3",
157
+ incPatch: true,
158
+ expectedTag: "v1.2.4",
159
+ },
160
+ {
161
+ name: "Patch increment with suffix",
162
+ currentTag: "v1.2.3",
163
+ incPatch: true,
164
+ suffix: "alpha",
165
+ expectedTag: "v1.2.4-alpha",
166
+ },
167
+ {
168
+ name: "Minor increment",
169
+ currentTag: "v1.2.3",
170
+ incMinor: true,
171
+ expectedTag: "v1.3.0",
172
+ },
173
+ {
174
+ name: "Minor increment with suffix",
175
+ currentTag: "v1.2.3",
176
+ incMinor: true,
177
+ suffix: "alpha",
178
+ expectedTag: "v1.3.0-alpha",
179
+ },
180
+ {
181
+ name: "Major increment",
182
+ currentTag: "v1.2.3",
183
+ incMajor: true,
184
+ expectedTag: "v2.0.0",
185
+ },
186
+ {
187
+ name: "Major increment with suffix",
188
+ currentTag: "v1.2.3",
189
+ incMajor: true,
190
+ suffix: "alpha",
191
+ expectedTag: "v2.0.0-alpha",
192
+ },
193
+ {
194
+ name: "Pre-release increment with pre-release version",
195
+ currentTag: "v1.2.3-rc.1",
196
+ suffix: "rc",
197
+ expectedTag: "v1.2.3-rc.2",
198
+ },
199
+ {
200
+ name: "Pre-release increment with patch version",
201
+ currentTag: "v1.2.3-rc.1",
202
+ incPatch: true,
203
+ expectedTag: "v1.2.4-rc",
204
+ },
205
+ {
206
+ name: "Pre-release increment with minor version",
207
+ currentTag: "v1.2.3-rc.1",
208
+ incMinor: true,
209
+ expectedTag: "v1.3.0-rc",
210
+ },
211
+ {
212
+ name: "Pre-release increment with major version",
213
+ currentTag: "v1.2.3-rc.1",
214
+ incMajor: true,
215
+ expectedTag: "v2.0.0-rc",
216
+ },
217
+ {
218
+ name: "Pre-release without number",
219
+ currentTag: "v1.2.3-rc",
220
+ expectedTag: "v1.2.3-rc.1",
221
+ },
222
+ {
223
+ name: "Add suffix to version",
224
+ currentTag: "v1.2.3",
225
+ suffix: "beta",
226
+ expectedTag: "v1.2.3-beta",
227
+ },
228
+ {
229
+ name: "Add suffix to pre-release version",
230
+ currentTag: "v1.1.1-beta",
231
+ suffix: "beta",
232
+ expectedTag: "v1.1.1-beta.1",
233
+ },
234
+ {
235
+ name: "Override existing pre-release with suffix",
236
+ currentTag: "v1.2.3-rc.1",
237
+ suffix: "beta",
238
+ expectedTag: "v1.2.3-beta.1",
239
+ },
240
+ {
241
+ name: "Override existing pre-release with suffix",
242
+ currentTag: "v1.2.3-rc.1",
243
+ suffix: "beta",
244
+ allTags: []string{"v1.2.3-rc.1", "v1.2.3-beta.2"},
245
+ expectedTag: "v1.2.3-beta.3",
246
+ },
247
+ }
248
+
249
+ for _, tc := range testCases {
250
+ t.Run(tc.name, func(t *testing.T) {
251
+ nextVersion, err := CalculateNextVersion(tc.currentTag, tc.allTags, tc.incMajor, tc.incMinor, tc.incPatch, tc.suffix)
252
+
253
+ if tc.expectError {
254
+ assert.Error(t, err)
255
+ } else {
256
+ assert.NoError(t, err)
257
+ assert.Equal(t, tc.expectedTag, nextVersion)
258
+ }
259
+ })
260
+ }
261
+ }