1313// limitations under the License.
1414
1515// Package semver provides functionality for parsing, comparing, and manipulating
16- // semantic version strings according to the SemVer 2.0.0 spec.
16+ // semantic version strings according to the SemVer 1.0.0 and 2.0.0 spec.
1717package semver
1818
1919import (
@@ -31,7 +31,8 @@ type Version struct {
3131 // Prerelease is the non-numeric part of the prerelease string (e.g., "alpha", "beta").
3232 Prerelease string
3333 // PrereleaseSeparator is the separator between the pre-release string and
34- // its version (e.g., ".").
34+ // its version (e.g., "."). SemVer 1.0.0 versions do not have a prerelease
35+ // separator.
3536 PrereleaseSeparator string
3637 // PrereleaseNumber is the numeric part of the pre-release segment of the
3738 // version string (e.g., the 1 in "alpha.1"). Zero is a valid pre-release
@@ -41,22 +42,25 @@ type Version struct {
4142}
4243
4344// semverV1PrereleaseNumberRegexp extracts the prerelease number, if present, in
44- // the prerelease portion of the SemVer 1.0.0 version string.
45+ // the prerelease portion of the SemVer 1.0.0 version string. For example, a
46+ // version string like "1.2.3-alpha01" is a SemVer 1.0.0. compliant, numbered
47+ // prerelease - https://semver.org/spec/v1.0.0.html#spec-item-4.
4548var semverV1PrereleaseNumberRegexp = regexp .MustCompile (`^(.*?)(\d+)$` )
4649
47- // Parse parses a version string into a Version struct.
48- func Parse (versionString string ) (* Version , error ) {
50+ // Parse deconstructs the SemVer 1.0.0 or 2.0.0 version string into a Version
51+ // struct.
52+ func Parse (versionString string ) (Version , error ) {
4953 // Our client versions must not have a "v" prefix.
5054 if strings .HasPrefix (versionString , "v" ) {
51- return nil , fmt .Errorf ("invalid version format: %s" , versionString )
55+ return Version {} , fmt .Errorf ("invalid version format: %s" , versionString )
5256 }
5357
5458 // Prepend "v" internally so that we can use various [semver] APIs.
5559 // Then canonicalize it to zero-fill any missing version segments.
5660 // Strips build metadata if present - we do not use build metadata suffixes.
5761 vPrefixedVersion := "v" + versionString
5862 if ! semver .IsValid (vPrefixedVersion ) {
59- return nil , fmt .Errorf ("invalid version format: %s" , versionString )
63+ return Version {} , fmt .Errorf ("invalid version format: %s" , versionString )
6064 }
6165 vPrefixedVersion = semver .Canonical (vPrefixedVersion )
6266
@@ -68,22 +72,22 @@ func Parse(versionString string) (*Version, error) {
6872 versionCore = strings .TrimSuffix (versionCore , prerelease )
6973 vParts := strings .Split (versionCore , "." )
7074
71- v := & Version {}
75+ var v Version
7276 var err error
7377
7478 v .Major , err = strconv .Atoi (vParts [0 ])
7579 if err != nil {
76- return nil , fmt .Errorf ("invalid major version: %w" , err )
80+ return Version {} , fmt .Errorf ("invalid major version: %w" , err )
7781 }
7882
7983 v .Minor , err = strconv .Atoi (vParts [1 ])
8084 if err != nil {
81- return nil , fmt .Errorf ("invalid minor version: %w" , err )
85+ return Version {} , fmt .Errorf ("invalid minor version: %w" , err )
8286 }
8387
8488 v .Patch , err = strconv .Atoi (vParts [2 ])
8589 if err != nil {
86- return nil , fmt .Errorf ("invalid patch version: %w" , err )
90+ return Version {} , fmt .Errorf ("invalid patch version: %w" , err )
8791 }
8892
8993 if prerelease == "" {
@@ -106,7 +110,7 @@ func Parse(versionString string) (*Version, error) {
106110 if numStr != "" {
107111 num , err := strconv .Atoi (numStr )
108112 if err != nil {
109- return nil , fmt .Errorf ("invalid prerelease number: %w" , err )
113+ return Version {} , fmt .Errorf ("invalid prerelease number: %w" , err )
110114 }
111115 v .PrereleaseNumber = & num
112116 }
@@ -115,7 +119,7 @@ func Parse(versionString string) (*Version, error) {
115119}
116120
117121// String formats a Version struct into a string.
118- func (v * Version ) String () string {
122+ func (v Version ) String () string {
119123 version := fmt .Sprintf ("%d.%d.%d" , v .Major , v .Minor , v .Patch )
120124 if v .Prerelease != "" {
121125 version += "-" + v .Prerelease
@@ -126,44 +130,6 @@ func (v *Version) String() string {
126130 return version
127131}
128132
129- // incrementPrerelease increments the pre-release version number, or appends
130- // one if it doesn't exist.
131- func (v * Version ) incrementPrerelease () {
132- if v .PrereleaseNumber == nil {
133- v .PrereleaseSeparator = "."
134- // Initialize a new int pointer set to 0. Fallthrough to increment to 1.
135- // We prefer the first prerelease to use 1 instead of 0.
136- v .PrereleaseNumber = new (int )
137- }
138- * v .PrereleaseNumber ++
139- }
140-
141- func (v * Version ) bump (highestChange ChangeLevel ) {
142- if v .Prerelease != "" {
143- // Only bump the prerelease version number.
144- v .incrementPrerelease ()
145- return
146- }
147-
148- // Bump the version core.
149- // Breaking changes and feat result in minor bump for pre-1.0.0 versions.
150- if (v .Major == 0 && highestChange == Major ) || highestChange == Minor {
151- v .Minor ++
152- v .Patch = 0
153- return
154- }
155- if highestChange == Patch {
156- v .Patch ++
157- return
158- }
159- if highestChange == Major {
160- v .Major ++
161- v .Minor = 0
162- v .Patch = 0
163- return
164- }
165- }
166-
167133// MaxVersion returns the largest semantic version string among the provided version strings.
168134func MaxVersion (versionStrings ... string ) string {
169135 if len (versionStrings ) == 0 {
@@ -214,18 +180,73 @@ func (c ChangeLevel) String() string {
214180 return [... ]string {"none" , "patch" , "minor" , "major" }[c ]
215181}
216182
217- // DeriveNext calculates the next version based on the highest change type and current version.
218- func DeriveNext (highestChange ChangeLevel , currentVersion string ) (string , error ) {
183+ // DeriveNextOptions contains options for controlling SemVer version derivation.
184+ type DeriveNextOptions struct {
185+ // BumpVersionCore forces the version bump to occur in the version core,
186+ // as opposed to the prerelease number, if one was present. If true, and
187+ // the version has a prerelease number, that number will be reset to 1.
188+ //
189+ // Default behavior is to prefer bumping the prerelease number or adding one
190+ // when the version is a prerelease without a number.
191+ BumpVersionCore bool
192+ }
193+
194+ // DeriveNext determines the appropriate SemVer version bump based on the
195+ // provided [ChangeLevel] and the provided [DeriveNextOptions].
196+ func (o DeriveNextOptions ) DeriveNext (highestChange ChangeLevel , currentVersion string ) (string , error ) {
219197 if highestChange == None {
220198 return currentVersion , nil
221199 }
222200
223- currentSemVer , err := Parse (currentVersion )
201+ version , err := Parse (currentVersion )
224202 if err != nil {
225203 return "" , fmt .Errorf ("failed to parse current version: %w" , err )
226204 }
227205
228- currentSemVer .bump (highestChange )
206+ // Only bump the prerelease version number.
207+ if version .Prerelease != "" && ! o .BumpVersionCore {
208+ // Append prerelease number if there isn't one.
209+ if version .PrereleaseNumber == nil {
210+ version .PrereleaseSeparator = "."
211+
212+ // Initialize a new int pointer set to 0. Fallthrough to increment
213+ // to 1. We prefer the first prerelease to use 1 instead of 0.
214+ version .PrereleaseNumber = new (int )
215+ }
216+
217+ * version .PrereleaseNumber ++
218+ return version .String (), nil
219+ }
220+
221+ // Reset prerelease number, if present, then fallthrough to bump version core.
222+ if version .PrereleaseNumber != nil && o .BumpVersionCore {
223+ * version .PrereleaseNumber = 1
224+ }
229225
230- return currentSemVer .String (), nil
226+ // Breaking changes result in a minor bump for pre-1.0.0 versions.
227+ if highestChange == Major && version .Major == 0 {
228+ highestChange = Minor
229+ }
230+
231+ // Bump the version core.
232+ switch highestChange {
233+ case Major :
234+ version .Major ++
235+ version .Minor = 0
236+ version .Patch = 0
237+ case Minor :
238+ version .Minor ++
239+ version .Patch = 0
240+ case Patch :
241+ version .Patch ++
242+ }
243+
244+ return version .String (), nil
245+ }
246+
247+ // DeriveNext calculates the next version based on the highest change type and
248+ // current version using the default [DeriveNextOptions]. This is a convenience
249+ // method.
250+ func DeriveNext (highestChange ChangeLevel , currentVersion string ) (string , error ) {
251+ return DeriveNextOptions {}.DeriveNext (highestChange , currentVersion )
231252}
0 commit comments