1212// See the License for the specific language governing permissions and
1313// limitations under the License.
1414
15- // Package legacysemver provides functionality for parsing, comparing, and manipulating
15+ // Package semver provides functionality for parsing, comparing, and manipulating
1616// semantic version strings according to the SemVer 2.0.0 spec.
17- package legacysemver
17+ package semver
1818
1919import (
2020 "fmt"
@@ -32,15 +32,22 @@ type Version struct {
3232 // PrereleaseSeparator is the separator between the pre-release string and
3333 // its version (e.g., ".").
3434 PrereleaseSeparator string
35- // PrereleaseNumber is the numeric part of the pre-release string (e.g., "1", "21").
36- PrereleaseNumber string
35+ // PrereleaseNumber is the numeric part of the pre-release segment of the
36+ // version string (e.g., the 1 in "alpha.1"). Zero is a valid pre-release
37+ // number. If there is no numeric part in the pre-release segment, this
38+ // field is nil.
39+ PrereleaseNumber * int
3740}
3841
3942// semverRegex defines format for semantic version.
4043// Regex from https://semver.org/, with buildmetadata part removed.
4144// It uses named capture groups for major, minor, patch, and prerelease.
4245var semverRegex = regexp .MustCompile (`^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$` )
4346
47+ // unsegmentedPrereleaseRegexp extracts the prerelease number, if present, in the
48+ // prerelease portion of the SemVer version parsed by [semverRegex].
49+ var unsegmentedPrereleaseRegexp = regexp .MustCompile (`^(.*?)(\d+)$` )
50+
4451// Parse parses a version string into a Version struct.
4552func Parse (versionString string ) (* Version , error ) {
4653 matches := semverRegex .FindStringSubmatch (versionString )
@@ -78,13 +85,21 @@ func Parse(versionString string) (*Version, error) {
7885 if i := strings .LastIndex (prerelease , "." ); i != - 1 {
7986 v .Prerelease = prerelease [:i ]
8087 v .PrereleaseSeparator = "."
81- v .PrereleaseNumber = prerelease [i + 1 :]
88+ num , err := strconv .Atoi (prerelease [i + 1 :])
89+ if err != nil {
90+ return nil , fmt .Errorf ("invalid prerelease number: %w" , err )
91+ }
92+ v .PrereleaseNumber = & num
8293 } else {
83- re := regexp .MustCompile (`^(.*?)(\d+)$` )
84- matches := re .FindStringSubmatch (prerelease )
94+ matches := unsegmentedPrereleaseRegexp .FindStringSubmatch (prerelease )
8595 if len (matches ) == 3 {
8696 v .Prerelease = matches [1 ]
87- v .PrereleaseNumber = matches [2 ]
97+ num , err := strconv .Atoi (matches [2 ])
98+ if err != nil {
99+ // This should not happen if the regex is correct.
100+ return nil , fmt .Errorf ("invalid prerelease number: %w" , err )
101+ }
102+ v .PrereleaseNumber = & num
88103 } else {
89104 v .Prerelease = prerelease
90105 }
@@ -130,39 +145,73 @@ func (v *Version) Compare(other *Version) int {
130145 return 1
131146 }
132147 // prerelease number (e.g. "alpha1" vs "alpha2")
133- if v .PrereleaseNumber < other .PrereleaseNumber {
148+ // Note: Lack of prerelease number is considered lower precedence than
149+ // the same prerelease when it has a number - https://semver.org/#spec-item-11.
150+ if v .PrereleaseNumber == nil && other .PrereleaseNumber != nil {
134151 return - 1
135152 }
136- if v .PrereleaseNumber > other .PrereleaseNumber {
153+ if v .PrereleaseNumber != nil && other .PrereleaseNumber == nil {
137154 return 1
138155 }
156+ if v .PrereleaseNumber != nil && other .PrereleaseNumber != nil {
157+ if * v .PrereleaseNumber < * other .PrereleaseNumber {
158+ return - 1
159+ }
160+ if * v .PrereleaseNumber > * other .PrereleaseNumber {
161+ return 1
162+ }
163+ }
139164 return 0
140165}
141166
142167// String formats a Version struct into a string.
143168func (v * Version ) String () string {
144169 version := fmt .Sprintf ("%d.%d.%d" , v .Major , v .Minor , v .Patch )
145170 if v .Prerelease != "" {
146- version += "-" + v .Prerelease + v .PrereleaseSeparator + v .PrereleaseNumber
171+ version += "-" + v .Prerelease
172+ if v .PrereleaseNumber != nil {
173+ version += v .PrereleaseSeparator + strconv .Itoa (* v .PrereleaseNumber )
174+ }
147175 }
148176 return version
149177}
150178
151179// incrementPrerelease increments the pre-release version number, or appends
152180// one if it doesn't exist.
153- func (v * Version ) incrementPrerelease () error {
154- if v .PrereleaseNumber == "" {
181+ func (v * Version ) incrementPrerelease () {
182+ if v .PrereleaseNumber == nil {
155183 v .PrereleaseSeparator = "."
156- v .PrereleaseNumber = "1"
157- return nil
184+ // Initialize a new int pointer set to 0. Fallthrough to increment to 1.
185+ // We prefer the first prerelease to use 1 instead of 0.
186+ v .PrereleaseNumber = new (int )
158187 }
159- num , err := strconv .Atoi (v .PrereleaseNumber )
160- if err != nil {
161- // This should not happen if Parse is correct.
162- return fmt .Errorf ("invalid prerelease version: %w" , err )
188+ * v .PrereleaseNumber ++
189+ }
190+
191+ func (v * Version ) bump (highestChange ChangeLevel ) {
192+ if v .Prerelease != "" {
193+ // Only bump the prerelease version number.
194+ v .incrementPrerelease ()
195+ return
196+ }
197+
198+ // Bump the version core.
199+ // Breaking changes and feat result in minor bump for pre-1.0.0 versions.
200+ if (v .Major == 0 && highestChange == Major ) || highestChange == Minor {
201+ v .Minor ++
202+ v .Patch = 0
203+ return
204+ }
205+ if highestChange == Patch {
206+ v .Patch ++
207+ return
208+ }
209+ if highestChange == Major {
210+ v .Major ++
211+ v .Minor = 0
212+ v .Patch = 0
213+ return
163214 }
164- v .PrereleaseNumber = strconv .Itoa (num + 1 )
165- return nil
166215}
167216
168217// MaxVersion returns the largest semantic version string among the provided version strings.
@@ -218,36 +267,7 @@ func DeriveNext(highestChange ChangeLevel, currentVersion string) (string, error
218267 return "" , fmt .Errorf ("failed to parse current version: %w" , err )
219268 }
220269
221- // Handle prerelease versions
222- if currentSemVer .Prerelease != "" {
223- if err := currentSemVer .incrementPrerelease (); err != nil {
224- return "" , err
225- }
226- return currentSemVer .String (), nil
227- }
228-
229- // Handle standard versions
230- if currentSemVer .Major == 0 {
231- // breaking change and feat result in minor bump for pre-1.0.0
232- if highestChange == Major || highestChange == Minor {
233- currentSemVer .Minor ++
234- currentSemVer .Patch = 0
235- } else {
236- currentSemVer .Patch ++
237- }
238- } else {
239- switch highestChange {
240- case Major :
241- currentSemVer .Major ++
242- currentSemVer .Minor = 0
243- currentSemVer .Patch = 0
244- case Minor :
245- currentSemVer .Minor ++
246- currentSemVer .Patch = 0
247- case Patch :
248- currentSemVer .Patch ++
249- }
250- }
270+ currentSemVer .bump (highestChange )
251271
252272 return currentSemVer .String (), nil
253273}
0 commit comments