blang / semver Goto Github PK
View Code? Open in Web Editor NEWSemantic Versioning (semver) library written in golang
License: MIT License
Semantic Versioning (semver) library written in golang
License: MIT License
return only finalize version i.e. major, minor and patch number and discarding prerelease and build number.
π Hi there!
tl;dr - I forked your (very good!) library. Are you interested in upstreaming the changes?
I'm in the unenviable position of needing to migrate a Node server that uses node-semver to golang without breaking any user who is currently relying on the logic in the Node library. Currently there is no equivalent semver library for Go that supports the range of inputs that the Node library does. Some examples:
~>1.2.x
^1.2.0
^10.14.1 || ^8.15.0
8 - 10
~7.x || ~8.x || ~9.x
~10.1.x
1.* || >=2.0.* <2.2.*
etc
To that end, I basically translated the (regex heavy) logic from the Node library directly and added it as a pre-processing step that normalizes the above input into the >=1.2.3 <2.0.0
type ranges that your library expects.
Changes here: master...jmorrell:master
I'm happy to either clean up these changes and upstream them into your library, or fork and support as a separate semver
library for those who need these other features and something that is compatible with Node's semver
library. Or I can maintain my fork separately if you are not interested.
Downsides:
Benchmark after changes:
$ go test -v -bench .
...
goos: darwin
goarch: amd64
pkg: github.com/jmorrell/semver
BenchmarkRangeParseSimple-8 1000 1332107 ns/op 1155055 B/op 9163 allocs/op
BenchmarkRangeParseAverage-8 1000 1325482 ns/op 1163610 B/op 9283 allocs/op
BenchmarkRangeParseComplex-8 300 3933910 ns/op 3485886 B/op 27792 allocs/op
BenchmarkRangeMatchSimple-8 100000000 12.7 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchAverage-8 50000000 26.7 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchComplex-8 20000000 73.2 ns/op 0 B/op 0 allocs/op
BenchmarkParseSimple-8 10000000 162 ns/op 48 B/op 1 allocs/op
BenchmarkParseComplex-8 2000000 826 ns/op 256 B/op 7 allocs/op
BenchmarkParseAverage-8 3000000 537 ns/op 163 B/op 4 allocs/op
BenchmarkParseTolerantAverage-8 5000000 356 ns/op 101 B/op 3 allocs/op
BenchmarkStringSimple-8 30000000 38.1 ns/op 5 B/op 1 allocs/op
BenchmarkStringLarger-8 20000000 83.0 ns/op 32 B/op 2 allocs/op
BenchmarkStringComplex-8 10000000 127 ns/op 80 B/op 3 allocs/op
BenchmarkStringAverage-8 20000000 107 ns/op 47 B/op 2 allocs/op
BenchmarkValidateSimple-8 500000000 3.41 ns/op 0 B/op 0 allocs/op
BenchmarkValidateComplex-8 5000000 259 ns/op 0 B/op 0 allocs/op
BenchmarkValidateAverage-8 10000000 141 ns/op 0 B/op 0 allocs/op
BenchmarkCompareSimple-8 300000000 4.22 ns/op 0 B/op 0 allocs/op
BenchmarkCompareComplex-8 100000000 16.4 ns/op 0 B/op 0 allocs/op
BenchmarkCompareAverage-8 100000000 22.1 ns/op 0 B/op 0 allocs/op
BenchmarkSort-8 10000000 184 ns/op 256 B/op 2 allocs/op
Your repo on master on the same machine:
$ go test -v -bench .
...
goos: darwin
goarch: amd64
pkg: github.com/blang/semver
BenchmarkRangeParseSimple-8 3000000 553 ns/op 240 B/op 7 allocs/op
BenchmarkRangeParseAverage-8 1000000 1058 ns/op 480 B/op 13 allocs/op
BenchmarkRangeParseComplex-8 500000 3309 ns/op 1792 B/op 39 allocs/op
BenchmarkRangeMatchSimple-8 100000000 12.0 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchAverage-8 50000000 26.1 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchComplex-8 20000000 75.3 ns/op 0 B/op 0 allocs/op
BenchmarkParseSimple-8 10000000 172 ns/op 48 B/op 1 allocs/op
BenchmarkParseComplex-8 2000000 762 ns/op 256 B/op 7 allocs/op
BenchmarkParseAverage-8 3000000 561 ns/op 163 B/op 4 allocs/op
BenchmarkParseTolerantAverage-8 5000000 359 ns/op 101 B/op 3 allocs/op
BenchmarkStringSimple-8 50000000 37.7 ns/op 5 B/op 1 allocs/op
BenchmarkStringLarger-8 20000000 79.3 ns/op 32 B/op 2 allocs/op
BenchmarkStringComplex-8 10000000 120 ns/op 80 B/op 3 allocs/op
BenchmarkStringAverage-8 20000000 103 ns/op 47 B/op 2 allocs/op
BenchmarkValidateSimple-8 500000000 3.38 ns/op 0 B/op 0 allocs/op
BenchmarkValidateComplex-8 10000000 191 ns/op 0 B/op 0 allocs/op
BenchmarkValidateAverage-8 20000000 113 ns/op 0 B/op 0 allocs/op
BenchmarkCompareSimple-8 300000000 4.21 ns/op 0 B/op 0 allocs/op
BenchmarkCompareComplex-8 100000000 17.5 ns/op 0 B/op 0 allocs/op
BenchmarkCompareAverage-8 100000000 21.7 ns/op 0 B/op 0 allocs/op
BenchmarkSort-8 10000000 175 ns/op 256 B/op 2 allocs/op
What is the motivation behind stopping pre-releases from being incremented? I read the SemVer spec and couldn't find this rule, could you point out where you've seen this constraint?
// IncrementMinor increments the minor version
func (v *Version) IncrementMinor() error {
//-----
if v.Major == 0 { // why?
return fmt.Errorf("Minor version can not be incremented for %q", v.String())
}
//-----
v.Minor += 1
v.Patch = 0
return nil
}
Would you mind me submitting a PR to remove it?
I'm trying to implement a range check that allows any major or minor version change, but never a major change. So that anything starting with 1
but never anything starting with 2
or above.
I tried expressing that as >= 1.x
, >= 1.x < 2.x
, >= 1.x < 2.x.x
, and >= 1.x < 2.0.0
, but found that it will allow 2.x prereleases, e.g. 2.0.0-v2-5
.
package main
import (
"fmt"
"github.com/blang/semver/v4"
)
func main() {
v, _ := semver.Parse("2.0.0-v2-5")
r, _ := semver.ParseRange(">= 1.x")
fmt.Println(r(v))
r, _ = semver.ParseRange(">= 1.x < 2.x")
fmt.Println(r(v))
r, _ = semver.ParseRange(">= 1.x < 2.x.x")
fmt.Println(r(v))
r, _ = semver.ParseRange(">= 1.x < 2.0.0")
fmt.Println(r(v))
}
This prints true
four times. I know that 2.0.0-v2-5
is "less than" 2.0.0
because it is a prerelease, but this seems to be making it caught by 1.x
as well.
In this fork, I added support for the following:
{[][]string{{"~1.2.1"}}, [][]string{{"<1.3.0", ">=1.2.1"}}},
{[][]string{{"^1.2.1"}}, [][]string{{"<2.0.0", ">=1.2.1"}}},
{[][]string{{"~>1.2.x"}}, [][]string{{">=1.2.0"}}},
{[][]string{{"~>1.x"}}, [][]string{{">=1.0.0"}}},
{[][]string{{"1.*"}}, [][]string{{">=1.0.0", "<2.0.0"}}},
{[][]string{{"1.2.*"}}, [][]string{{">=1.2.0", "<1.3.0"}}},
{[][]string{{"*"}}, [][]string{{">=0.0.0"}}},
{[][]string{{"8.0.0 - 10.0.0"}}, [][]string{{"<10.0.0", ">=8.0.0"}}},
{[][]string{{"8 - 10"}}, [][]string{{"<10.0.0", ">=8.0.0"}}},
{[][]string{{"8 - 10.1"}}, [][]string{{"<10.1.0", ">=8.0.0"}}},
{[][]string{{" 8 "}}, [][]string{{"8.0.0"}}},
{[][]string{{" 800000 "}}, [][]string{{"800000.0.0"}}},
{[][]string{{" ~7.x "}}, [][]string{{"<8.0.0", ">=7.0.0"}}},
{[][]string{{" ~7.0.x "}}, [][]string{{"<7.1.0", ">=7.0.0"}}},
{[][]string{{" ~* "}}, [][]string{{">=0.0.0"}}},
It does not allocate more memory. But, it's about 8% slower. Do you want me submit a PR?
goos: darwin
goarch: amd64
pkg: github.com/Jarred-Sumner/semver/v4
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
BenchmarkRangeParseSimple-16 2402451 493.4 ns/op 224 B/op 7 allocs/op
BenchmarkRangeParseAverage-16 1235457 967.9 ns/op 456 B/op 13 allocs/op
BenchmarkRangeParseComplex-16 392412 3129 ns/op 1736 B/op 39 allocs/op
BenchmarkRangeMatchSimple-16 100000000 11.32 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchAverage-16 48316423 25.28 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchComplex-16 17318359 70.28 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchNPM-16 27442455 43.70 ns/op 0 B/op 0 allocs/op
BenchmarkParseSimple-16 8453367 139.5 ns/op 48 B/op 1 allocs/op
BenchmarkParseComplex-16 1795279 687.0 ns/op 256 B/op 7 allocs/op
BenchmarkParseAverage-16 2672629 466.3 ns/op 163 B/op 4 allocs/op
BenchmarkParseTolerantAverage-16 3093957 394.1 ns/op 132 B/op 4 allocs/op
BenchmarkStringSimple-16 40966444 30.76 ns/op 5 B/op 1 allocs/op
BenchmarkStringLarger-16 17750650 65.63 ns/op 32 B/op 2 allocs/op
BenchmarkStringComplex-16 12103804 98.85 ns/op 80 B/op 3 allocs/op
BenchmarkStringAverage-16 13836576 89.55 ns/op 45 B/op 2 allocs/op
BenchmarkValidateSimple-16 441872518 2.589 ns/op 0 B/op 0 allocs/op
BenchmarkValidateComplex-16 7224438 151.2 ns/op 0 B/op 0 allocs/op
BenchmarkValidateAverage-16 13011160 88.88 ns/op 0 B/op 0 allocs/op
BenchmarkCompareSimple-16 309173464 3.999 ns/op 0 B/op 0 allocs/op
BenchmarkCompareComplex-16 82620462 12.37 ns/op 0 B/op 0 allocs/op
BenchmarkCompareAverage-16 68264864 17.24 ns/op 0 B/op 0 allocs/op
BenchmarkSort-16 7555494 160.2 ns/op 248 B/op 2 allocs/op
PASS
ok github.com/Jarred-Sumner/semver/v4 31.099s
Fair warning that this is my ~4th day of writing go.
After #47 I noticed that the following (which semver.ParseTolerant had previously accepted) now no longer parses:
semver.Parse("4.1.0-0.nightly") is returned as 4.1.0-0.nightly
but after this change
semver.ParseTolerant("4.1.0-0.nightly") errors with strconv.ParseUint: parsing "": invalid syntax
I think the fix is that the "remove leading zeroes" part of that change needs a more accurate check than len(p) >1
- probably needs to be looking to the first dot, not the whole string.
Given the version 0.1.0-x
, ParseRange
returns the following error:
Could not get version from string: "<"
Replacing the x with another character fixes the problem (e.g. 0.1.0-y
).
This appears to be related to x
being used to denote wildcards as implemented in #27:
Line 330 in 4487282
This can be reproduced with the following test:
func TestExpandWildcardVersion(t *testing.T) {
tests := []struct {
i [][]string
o [][]string
}{
{[][]string{{"foox"}}, nil},
// FAILING TEST
{[][]string{{"0.1.0-x"}}, [][]string{{"0.1.0-x"}}},
{[][]string{{">=1.2.x"}}, [][]string{{">=1.2.0"}}},
{[][]string{{"<=1.2.x"}}, [][]string{{"<1.3.0"}}},
{[][]string{{">1.2.x"}}, [][]string{{">=1.3.0"}}},
{[][]string{{"<1.2.x"}}, [][]string{{"<1.2.0"}}},
{[][]string{{"!=1.2.x"}}, [][]string{{"<1.2.0", ">=1.3.0"}}},
{[][]string{{">=1.x"}}, [][]string{{">=1.0.0"}}},
{[][]string{{"<=1.x"}}, [][]string{{"<2.0.0"}}},
{[][]string{{">1.x"}}, [][]string{{">=2.0.0"}}},
{[][]string{{"<1.x"}}, [][]string{{"<1.0.0"}}},
{[][]string{{"!=1.x"}}, [][]string{{"<1.0.0", ">=2.0.0"}}},
{[][]string{{"1.2.x"}}, [][]string{{">=1.2.0", "<1.3.0"}}},
{[][]string{{"1.x"}}, [][]string{{">=1.0.0", "<2.0.0"}}},
}
for _, tc := range tests {
o, _ := expandWildcardVersion(tc.i)
if !reflect.DeepEqual(tc.o, o) {
t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.o, o)
}
}
}
Result of this test is: Invalid for case [["0.1.0-x"]]: Expected [["0.1.0-x"]], got: [[">=0.1.0-x" "<"]]
At the very least, it would be helpful to call out this corner case in the docs if fixing this would be challenging due to breaking changes.
This string explicitly checks for major.minor.patch version. In certain cases there could be 0.1
, 1.0
or even 2
version formats.
v3.5.1 is the last tagged version that doesnβt include a go.mod file
The newer has a go.mod file but is invalid since the module needs to have a /v3 suffix then
see golang/go#31944 (comment) for more
Thanks for reworking this to be value oriented. I revisited my project where I was using this and forgot I had reported the whole pointer thing and wondered "Why did I use pointers everywhere". Then I came here and checked the issues to see that I had raised the issue with you and was pleased to see that v2
was now available. That was perfect since I was rewriting my code to use values anyway.
However, one minor thing I noticed. You still have a function called New
. The Golang convention here is to use Make
for functions that return values and New
for ones that return pointers. So I'd suggest you change New
to Make
. You could include New
as well...but it should return a pointer. But I'm not how useful that would be for someone since the whole API is now value based. But the value versions of the methods should work with the pointers (just not the other way around). So it shouldn't hurt to have both New
and Make
. I could submit a PR if you want.
Thanks again!
I notice that lots of methods in your API (e.g., Validate
, Compare
) take pointer arguments. But I don't see why they should. None of these operations actually mutate the object. So it should be fine to simply pass a value.
The reason I see this as an issue is twofold. First, it gives the impression (when looking at the API), that these things mutate the objects. But more important for me, it means I need to have pointers available in my code which keeps the door open for inadvertent mutation in my code as well.
Is there a reason to use the pointers in the API like you have?
I can't find a peer with current version pinned and,when I fetch the repo and pin it locally, I get a different hash.
It would be great if you could do this:
r := semver.ParseRange("1.x")
v := semver.Parse("1.0.0")
fmt.Println(r.Contains(v) == true) // true
Did you consider allowing versions like "1.0"? I personally did not know that semver requires X.Y.Z format.
Hi! Thank you for a great package!
There is a caveat though: the package at its v3 major revision but it doesn't have /v3
suffix in its module name.
IMO you should either to drop all versions history or give it a proper name, i.e. github.com/blang/semver/v3
Thanks for this project, I'm sorry if I missed this in the godocs, but is there a way to get a string representation of a range? For example I have a range that is equal to >=1.0.0 AND <= 3.0.0, would like to get that as a string to show to users. Thanks!
A common (and extremely useful) semver constraint is the ^x.x.x
form. While this can also be expressed using a range (eg ^1.2.3
can be expressed as >= 1.2.3 <2.0.0
). Similarly (but less useful), ~1.2
can be expressed as >= 1.2.0 <1.3.0
.
See https://semver.npmjs.com/.
Are there any plans to add this? If not, do you have any pointers on how to best to approach this?
I does work for
but is broken for
And so on ( comparing a 2 digit rc vs a 1 digit rc )
I think I have found a bug in the constraint check for .0
patch prereleases.
An example for this can be found in the Go Playground https://go.dev/play/p/LShjC5F37TT or in the main.go:
package main
import (
"fmt"
"github.com/blang/semver/v4"
)
func main() {
constraint := "0.49.x"
versionConstraint, err := semver.ParseRange(constraint)
if err != nil {
return
}
versions := []string{
"0.49.0-alpha",
"0.49.1-alpha",
"0.49.0",
"0.49.1",
}
for _, ver := range versions {
parsedVersion, err := semver.Parse(ver)
if err != nil {
return
}
fmt.Printf("%v becomes:\n %#v \n", ver, parsedVersion)
fmt.Printf(" Major: %d\n", parsedVersion.Major)
fmt.Printf(" Minor: %d\n", parsedVersion.Minor)
fmt.Printf(" Patch: %d\n", parsedVersion.Patch)
fmt.Printf(" Pre: %s\n", parsedVersion.Pre)
fmt.Printf(" Build: %s\n", parsedVersion.Build)
didMatch := "false"
if versionConstraint(parsedVersion) {
didMatch = "true"
}
fmt.Printf(" => constraint %v matched for %v: %v \n\n", constraint, ver, didMatch)
}
}
The output of this is:
0.49.0-alpha becomes:
semver.Version{Major:0x0, Minor:0x31, Patch:0x0, Pre:[]semver.PRVersion{semver.PRVersion{VersionStr:"alpha", VersionNum:0x0, IsNum:false}}, Build:[]string(nil)}
Major: 0
Minor: 49
Patch: 0
Pre: [alpha]
Build: []
=> constraint 0.49.x matched for 0.49.0-alpha: false // β this seems to be a bug
0.49.1-alpha becomes:
semver.Version{Major:0x0, Minor:0x31, Patch:0x1, Pre:[]semver.PRVersion{semver.PRVersion{VersionStr:"alpha", VersionNum:0x0, IsNum:false}}, Build:[]string(nil)}
Major: 0
Minor: 49
Patch: 1
Pre: [alpha]
Build: []
=> constraint 0.49.x matched for 0.49.1-alpha: true // β
as expected
0.49.0 becomes:
semver.Version{Major:0x0, Minor:0x31, Patch:0x0, Pre:[]semver.PRVersion(nil), Build:[]string(nil)}
Major: 0
Minor: 49
Patch: 0
Pre: []
Build: []
=> constraint 0.49.x matched for 0.49.0: true // β
as expected
0.49.1 becomes:
semver.Version{Major:0x0, Minor:0x31, Patch:0x1, Pre:[]semver.PRVersion(nil), Build:[]string(nil)}
Major: 0
Minor: 49
Patch: 1
Pre: []
Build: []
=> constraint 0.49.x matched for 0.49.1: true // β
as expected
According to this SemVer check, that's incorrect: https://jubianchi.github.io/semver-check/#/0.49.x/0.49.0-alpha.
Guessing this might have to do with the v
prefix, which should be disregarded.
Could you use the next names (in uppercase)?
eq
Returns the boolean truth of arg1 == arg2
ne
Returns the boolean truth of arg1 != arg2
lt
Returns the boolean truth of arg1 < arg2
le
Returns the boolean truth of arg1 <= arg2
gt
Returns the boolean truth of arg1 > arg2
ge
Returns the boolean truth of arg1 >= arg2
Got from http://golang.org/pkg/text/template/
according to https://stackoverflow.com/questions/5101591/what-does-mean-in-a-gem-file
~> 2.0.0 means ">= 2.0.0 and < 2.1" in version numbers.
seems like it is wildly known, is there any plan to support this syntax?
I was trying to figure out what might be causing some unexpected behavior in this package. To be honest, I'm not sure if this is something I'm doing wrong or not.
I added some extra code to range_test.go, which I think shows the issue:
{">1.2.2 <1.2.4", []tv{
{"1.2.1", false},
{"1.2.1-pl1", false},
{"1.2.2", false},
{"1.2.2-pl1", false},
{"1.2.3", true},
{"1.2.3-pl1", true},
{"1.2.4", false},
{"1.2.4-pl1", false},
}},
{">=1.2.2 <1.2.4", []tv{
{"1.2.1", false},
{"1.2.1-pl1", false},
{"1.2.2", true},
{"1.2.2-pl1", true},
{"1.2.3", true},
{"1.2.3-pl1", true},
{"1.2.4", false},
{"1.2.4-pl1", false},
}},
The issue is that versions with -pl1 seem to return different results then those without.
The output of the above is:
--- FAIL: TestParseRange (0.00s)
range_test.go:514: Invalid for case ">1.2.2 <1.2.4" matching "1.2.4-pl1": Expected false, got: true
range_test.go:514: Invalid for case ">=1.2.2 <1.2.4" matching "1.2.2-pl1": Expected true, got: false
range_test.go:514: Invalid for case ">=1.2.2 <1.2.4" matching "1.2.4-pl1": Expected false, got: true
FAIL
exit status 1
FAIL github.com/blang/semver 0.007s
Am I doing something wrong here?
Passing >=1.6.5 <2.0.0 || >= 2.1.7
(note the space before 2.1.7
) to ParseRange
will make the library return an error.
Can we have a ParseRangeTolerant
function that would be more tolerant to this errors?
Hey @blang
I would like to help you maintain this library by reviewing PR and fixing issues. Is this appreciated?
If I have a range >=4.3.0
and I pass a version with release tag such as 4.3.0-20191231
, the result will be False. However, if I use range >4.3.0-0
, then the result will be True. This is a bit of confusing behavior because from my perspective 4.3.0-20191231
should be a part of the range >=4.3.0
. I wonder if someone can explains this behavior a bit for me.
Here is the code if someone wants to test it out on Playground:
package main
import (
"fmt"
"github.com/blang/semver"
)
func main() {
ra, _ := semver.ParseRange(">=4.3.0")
if ra(semver.MustParse("4.3.0-201912000000")) {
fmt.Print("True")
} else {
fmt.Print("False")
}
}
Thanks so much in advance.
Might also drop 1.4 - 1.7.
Why the result is false
? I think 201912060615 < 201912111117. How can I make it return true
.?
package main
import (
"fmt"
"github.com/blang/semver"
)
func main() {
inRange, err := semver.ParseRange(">=4.3.0 <4.3.0-201912111117")
if err != nil {
panic(err)
}
version := semver.MustParse("4.3.0-201912060615")
// Check if the version meets the constraints. The a variable will be true.
fmt.Printf("within range: %t", inRange(version))
}
Fully tested (Coverage >99%)
But it seems coverage is around 87% according to batch in readme.
Please, don't get me wrong, it's just to increase awareness and we can make it great again. If you wish, I can even give a hand.
Using semver v4
e, err := semver.ParseTolerant("1.15-alpine3.14")
if err != nil {
fmt.Println(err)
}
returns error Invalid character(s) found in minor number "15-alpine3"
I really like your Tool.
Are you planning on releasing it as bin?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.