cmd/go/internal/modfetch: clean up Query
Clean up module query notation to make it easier to explain.
Apply a suggestion from Bryan in the previous CL to apply
the preference for releases (not pre-releases) to all queries,
not just "latest".
Change "v1" to mean "the latest v1.x.x" not "v1.0.0".
Change "v1.2" to mean "the latest v1.2.x" not "v1.2.0".
There was a TODO about checking for a tag/branch named v1,
but using the latest tagged v1.x.x release seems better than
taking the last commit (possibly untagged) from a branch
anyway.
Add <= and >= operators instead of misintepreting them
as < and > with malformed semantic versions.
Given that "v1" and "v1.2" now essentially address a wide range
of values, disallow ambiguous <=v1, <=v1.2, >v1, and >v1.2.
Add test.
Change-Id: I97a39c5a468ee710a19b587c46d1821f03d76acd
Reviewed-on: https://go-review.googlesource.com/122257
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/vendor/cmd/go/internal/list/list.go b/vendor/cmd/go/internal/list/list.go
index 855d0d6..788542a 100644
--- a/vendor/cmd/go/internal/list/list.go
+++ b/vendor/cmd/go/internal/list/list.go
@@ -358,10 +358,6 @@
// TODO(rsc): Could make this mean something with -m.
base.Fatalf("go list -deps cannot be used with -m")
}
- if *listE {
- // TODO(rsc): Could make this mean something with -m.
- base.Fatalf("go list -e cannot be used with -m")
- }
if *listExport {
base.Fatalf("go list -export cannot be used with -m")
}
@@ -375,6 +371,13 @@
vgo.LoadBuildList()
mods := vgo.ListModules(args, *listU, *listVersions)
+ if !*listE {
+ for _, m := range mods {
+ if m.Error != nil {
+ base.Fatalf("go list -m %s: %v", m.Path, m.Error.Err)
+ }
+ }
+ }
for _, m := range mods {
do(m)
}
diff --git a/vendor/cmd/go/internal/modfetch/query.go b/vendor/cmd/go/internal/modfetch/query.go
index 5902d5e..9d3a6f0 100644
--- a/vendor/cmd/go/internal/modfetch/query.go
+++ b/vendor/cmd/go/internal/modfetch/query.go
@@ -16,86 +16,171 @@
// The version must take one of the following forms:
//
// - the literal string "latest", denoting the latest available, allowed tagged version,
-// with non-prereleases preferred over prereleases
-// - v1.2.3, a semantic version string
-// - v1 or v1.2, an abbreviated semantic version string completed by adding zeroes (v1.0.0 or v1.2.0)
-// - >v1.2.3, denoting the earliest available version after v1.2.3 (including prereleases)
-// - <v1.2.3, denoting the latest available version before v1.2.3 (including prereleases)
-// - a repository commit identifier, denoting that version
+// with non-prereleases preferred over prereleases.
+// If there are no tagged versions in the repo, latest returns the most recent commit.
+// - v1, denoting the latest available tagged version v1.x.x.
+// - v1.2, denoting the latest available tagged version v1.2.x.
+// - v1.2.3, a semantic version string denoting that tagged version.
+// - <v1.2.3, <=v1.2.3, >v1.2.3, >=v1.2.3,
+// denoting the version closest to the target and satisfying the given operator,
+// with non-prereleases preferred over prereleases.
+// - a repository commit identifier, denoting that commit.
//
// If the allowed function is non-nil, Query excludes any versions for which allowed returns false.
//
-func Query(path, vers string, allowed func(module.Version) bool) (*RevInfo, error) {
+func Query(path, query string, allowed func(module.Version) bool) (*RevInfo, error) {
if allowed == nil {
allowed = func(module.Version) bool { return true }
}
- if semver.IsValid(vers) {
- // TODO: This turns query for "v2" into Stat "v2.0.0",
- // but probably it should allow checking for a branch named "v2".
- vers = semver.Canonical(vers)
+
+ // Parse query to detect parse errors (and possibly handle query)
+ // before any network I/O.
+ badVersion := func(v string) (*RevInfo, error) {
+ return nil, fmt.Errorf("invalid semantic version %q in range %q", v, query)
+ }
+ var ok func(module.Version) bool
+ var preferOlder bool
+ switch {
+ case query == "latest":
+ ok = allowed
+
+ case strings.HasPrefix(query, "<="):
+ v := query[len("<="):]
+ if !semver.IsValid(v) {
+ return badVersion(v)
+ }
+ if isSemverPrefix(v) {
+ // Refuse to say whether <=v1.2 allows v1.2.3 (remember, @v1.2 might mean v1.2.3).
+ return nil, fmt.Errorf("ambiguous semantic version %q in range %q", v, query)
+ }
+ ok = func(m module.Version) bool {
+ return semver.Compare(m.Version, v) <= 0 && allowed(m)
+ }
+
+ case strings.HasPrefix(query, "<"):
+ v := query[len("<"):]
+ if !semver.IsValid(v) {
+ return badVersion(v)
+ }
+ ok = func(m module.Version) bool {
+ return semver.Compare(m.Version, v) < 0 && allowed(m)
+ }
+
+ case strings.HasPrefix(query, ">="):
+ v := query[len(">="):]
+ if !semver.IsValid(v) {
+ return badVersion(v)
+ }
+ ok = func(m module.Version) bool {
+ return semver.Compare(m.Version, v) >= 0 && allowed(m)
+ }
+ preferOlder = true
+
+ case strings.HasPrefix(query, ">"):
+ v := query[len(">"):]
+ if !semver.IsValid(v) {
+ return badVersion(v)
+ }
+ if isSemverPrefix(v) {
+ // Refuse to say whether >v1.2 allows v1.2.3 (remember, @v1.2 might mean v1.2.3).
+ return nil, fmt.Errorf("ambiguous semantic version %q in range %q", v, query)
+ }
+ ok = func(m module.Version) bool {
+ return semver.Compare(m.Version, v) > 0 && allowed(m)
+ }
+ preferOlder = true
+
+ case semver.IsValid(query) && isSemverPrefix(query):
+ ok = func(m module.Version) bool {
+ return matchSemverPrefix(query, m.Version) && allowed(m)
+ }
+
+ case semver.IsValid(query):
+ vers := semver.Canonical(query)
if !allowed(module.Version{Path: path, Version: vers}) {
return nil, fmt.Errorf("%s@%s excluded", path, vers)
}
+ return Stat(path, vers)
- // Fast path that avoids network overhead of Lookup (resolving path to repo host),
- // if we already have this stat information cached on disk.
- info, err := Stat(path, vers)
- if err == nil {
- return info, nil
+ default:
+ // Direct lookup of semantic version or commit identifier.
+ info, err := Stat(path, query)
+ if err != nil {
+ return nil, err
}
+ if !allowed(module.Version{Path: path, Version: info.Version}) {
+ return nil, fmt.Errorf("%s@%s excluded", path, info.Version)
+ }
+ return info, nil
}
+ // Load versions and execute query.
repo, err := Lookup(path)
if err != nil {
return nil, err
}
-
- if semver.IsValid(vers) {
- return repo.Stat(vers)
- }
- if strings.HasPrefix(vers, ">") || strings.HasPrefix(vers, "<") || vers == "latest" {
- var op string
- if vers != "latest" {
- if !semver.IsValid(vers[1:]) {
- return nil, fmt.Errorf("invalid semantic version in range %s", vers)
- }
- op, vers = vers[:1], vers[1:]
- }
- versions, err := repo.Versions("")
- if err != nil {
- return nil, err
- }
- if len(versions) == 0 && vers == "latest" {
- return repo.Latest()
- }
- if vers == "latest" {
- // Prefer a proper (non-prerelease) release.
- for i := len(versions) - 1; i >= 0; i-- {
- if semver.Prerelease(versions[i]) == "" && allowed(module.Version{Path: path, Version: versions[i]}) {
- return repo.Stat(versions[i])
- }
- }
- // Fall back to pre-releases if that's all we have.
- for i := len(versions) - 1; i >= 0; i-- {
- if semver.Prerelease(versions[i]) != "" && allowed(module.Version{Path: path, Version: versions[i]}) {
- return repo.Stat(versions[i])
- }
- }
- } else if op == "<" {
- for i := len(versions) - 1; i >= 0; i-- {
- if semver.Compare(versions[i], vers) < 0 && allowed(module.Version{Path: path, Version: versions[i]}) {
- return repo.Stat(versions[i])
- }
- }
- } else {
- for i := 0; i < len(versions); i++ {
- if semver.Compare(versions[i], vers) > 0 && allowed(module.Version{Path: path, Version: versions[i]}) {
- return repo.Stat(versions[i])
- }
- }
- }
- return nil, fmt.Errorf("no matching versions for %s%s", op, vers)
+ versions, err := repo.Versions("")
+ if err != nil {
+ return nil, err
}
- return repo.Stat(vers)
+ if preferOlder {
+ for _, v := range versions {
+ if semver.Prerelease(v) == "" && ok(module.Version{Path: path, Version: v}) {
+ return repo.Stat(v)
+ }
+ }
+ for _, v := range versions {
+ if semver.Prerelease(v) != "" && ok(module.Version{Path: path, Version: v}) {
+ return repo.Stat(v)
+ }
+ }
+ } else {
+ for i := len(versions) - 1; i >= 0; i-- {
+ v := versions[i]
+ if semver.Prerelease(v) == "" && ok(module.Version{Path: path, Version: v}) {
+ return repo.Stat(v)
+ }
+ }
+ for i := len(versions) - 1; i >= 0; i-- {
+ v := versions[i]
+ if semver.Prerelease(v) != "" && ok(module.Version{Path: path, Version: v}) {
+ return repo.Stat(v)
+ }
+ }
+ }
+
+ if query == "latest" {
+ // Special case for "latest": if no tags match, use latest commit in repo,
+ // provided it is not excluded.
+ if info, err := repo.Latest(); err == nil && allowed(module.Version{Path: path, Version: info.Version}) {
+ return info, nil
+ }
+ }
+
+ return nil, fmt.Errorf("no matching versions for query %q", query)
+}
+
+// isSemverPrefix reports whether v is a semantic version prefix: v1 or v1.2 (not v1.2.3).
+// The caller is assumed to have checked that semver.IsValid(v) is true.
+func isSemverPrefix(v string) bool {
+ dots := 0
+ for i := 0; i < len(v); i++ {
+ switch v[i] {
+ case '-', '+':
+ return false
+ case '.':
+ dots++
+ if dots >= 2 {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+// matchSemverPrefix reports whether the shortened semantic version p
+// matches the full-width (non-shortened) semantic version v.
+func matchSemverPrefix(p, v string) bool {
+ return len(v) > len(p) && v[len(p)] == '.' && v[:len(p)] == p
}
diff --git a/vendor/cmd/go/internal/modfetch/query_test.go b/vendor/cmd/go/internal/modfetch/query_test.go
new file mode 100644
index 0000000..eabdf75
--- /dev/null
+++ b/vendor/cmd/go/internal/modfetch/query_test.go
@@ -0,0 +1,129 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package modfetch
+
+import (
+ "cmd/go/internal/module"
+ "internal/testenv"
+ "path"
+ "strings"
+ "testing"
+)
+
+var (
+ queryRepo = "vcs-test.golang.org/git/querytest.git"
+ queryRepoV2 = queryRepo + "/v2"
+ queryRepoV3 = queryRepo + "/v3"
+
+ // Empty version list (no semver tags), not actually empty.
+ emptyRepo = "vcs-test.golang.org/git/emptytest.git"
+)
+
+var queryTests = []struct {
+ path string
+ query string
+ allow string
+ vers string
+ err string
+}{
+ /*
+ git init
+ echo module vcs-test.golang.org/git/querytest.git >go.mod
+ git add go.mod
+ git commit -m v1 go.mod
+ git tag start
+ for i in v0.0.0-pre1 v0.0.0 v0.0.1 v0.0.2 v0.0.3 v0.1.0 v0.1.1 v0.1.2 v0.3.0 v1.0.0 v1.1.0 v1.9.0 v1.9.9 v1.9.10-pre1; do
+ echo before $i >status
+ git add status
+ git commit -m "before $i" status
+ echo at $i >status
+ git commit -m "at $i" status
+ git tag $i
+ done
+ git tag favorite v0.0.3
+
+ git branch v2 start
+ git checkout v2
+ echo module vcs-test.golang.org/git/querytest.git/v2 >go.mod
+ git commit -m v2 go.mod
+ for i in v2.0.0 v2.1.0 v2.2.0 v2.5.5; do
+ echo before $i >status
+ git add status
+ git commit -m "before $i" status
+ echo at $i >status
+ git commit -m "at $i" status
+ git tag $i
+ done
+ echo after v2.5.5 >status
+ git commit -m 'after v2.5.5' status
+ git checkout master
+ zip -r ../querytest.zip
+ gsutil cp ../querytest.zip gs://vcs-test/git/querytest.zip
+ curl 'https://vcs-test.golang.org/git/querytest?go-get=1'
+ */
+ {path: queryRepo, query: "<v0.0.0", vers: "v0.0.0-pre1"},
+ {path: queryRepo, query: "<v0.0.0-pre1", err: `no matching versions for query "<v0.0.0-pre1"`},
+ {path: queryRepo, query: "<=v0.0.0", vers: "v0.0.0"},
+ {path: queryRepo, query: ">v0.0.0", vers: "v0.0.1"},
+ {path: queryRepo, query: ">=v0.0.0", vers: "v0.0.0"},
+ {path: queryRepo, query: "v0.0.1", vers: "v0.0.1"},
+ {path: queryRepo, query: "v0.0.1+foo", vers: "v0.0.1"},
+ {path: queryRepo, query: "v0.0.99", err: `unknown revision v0.0.99`},
+ {path: queryRepo, query: "v0", vers: "v0.3.0"},
+ {path: queryRepo, query: "v0.1", vers: "v0.1.2"},
+ {path: queryRepo, query: "v0.2", err: `no matching versions for query "v0.2"`},
+ {path: queryRepo, query: "v0.0", vers: "v0.0.3"},
+ {path: queryRepo, query: "latest", vers: "v1.9.9"},
+ {path: queryRepo, query: "latest", allow: "NOMATCH", err: `no matching versions for query "latest"`},
+ {path: queryRepo, query: ">v1.9.9", vers: "v1.9.10-pre1"},
+ {path: queryRepo, query: ">v1.10.0", err: `no matching versions for query ">v1.10.0"`},
+ {path: queryRepo, query: ">=v1.10.0", err: `no matching versions for query ">=v1.10.0"`},
+ {path: queryRepo, query: "6cf84eb", vers: "v0.0.0-20180704023347-6cf84ebaea54"},
+ {path: queryRepo, query: "start", vers: "v0.0.0-20180704023101-5e9e31667ddf"},
+ {path: queryRepo, query: "7a1b6bf", vers: "v0.1.0"},
+
+ {path: queryRepoV2, query: "<v0.0.0", err: `no matching versions for query "<v0.0.0"`},
+ {path: queryRepoV2, query: "<=v0.0.0", err: `no matching versions for query "<=v0.0.0"`},
+ {path: queryRepoV2, query: ">v0.0.0", vers: "v2.0.0"},
+ {path: queryRepoV2, query: ">=v0.0.0", vers: "v2.0.0"},
+ {path: queryRepoV2, query: "v0.0.1+foo", vers: "v2.0.0-20180704023347-179bc86b1be3"},
+ {path: queryRepoV2, query: "latest", vers: "v2.5.5"},
+
+ {path: queryRepoV3, query: "latest", vers: "v3.0.0-20180704024501-e0cf3de987e6"},
+
+ {path: emptyRepo, query: "latest", vers: "v0.0.0-20180704023549-7bb914627242"},
+ {path: emptyRepo, query: ">v0.0.0", err: `no matching versions for query ">v0.0.0"`},
+ {path: emptyRepo, query: "<v10.0.0", err: `no matching versions for query "<v10.0.0"`},
+}
+
+func TestQuery(t *testing.T) {
+ testenv.MustHaveExternalNetwork(t)
+
+ for _, tt := range queryTests {
+ allow := tt.allow
+ if allow == "" {
+ allow = "*"
+ }
+ allowed := func(m module.Version) bool {
+ ok, _ := path.Match(allow, m.Version)
+ return ok
+ }
+ t.Run(strings.Replace(tt.path, "/", "_", -1)+"/"+tt.query+"/"+allow, func(t *testing.T) {
+ info, err := Query(tt.path, tt.query, allowed)
+ if tt.err != "" {
+ if err != nil && err.Error() == tt.err {
+ return
+ }
+ t.Fatalf("Query(%q, %q, %v): %v, want error %q", tt.path, tt.query, allow, err, tt.err)
+ }
+ if err != nil {
+ t.Fatalf("Query(%q, %q, %v): %v", tt.path, tt.query, allow, err)
+ }
+ if info.Version != tt.vers {
+ t.Errorf("Query(%q, %q, %v) = %v, want %v", tt.path, tt.query, allow, info.Version, tt.vers)
+ }
+ })
+ }
+}
diff --git a/vendor/cmd/go/internal/vgo/list.go b/vendor/cmd/go/internal/vgo/list.go
index 7470625..761eae1 100644
--- a/vendor/cmd/go/internal/vgo/list.go
+++ b/vendor/cmd/go/internal/vgo/list.go
@@ -10,7 +10,9 @@
"strings"
"cmd/go/internal/base"
+ "cmd/go/internal/modfetch"
"cmd/go/internal/modinfo"
+ "cmd/go/internal/module"
"cmd/go/internal/par"
"cmd/go/internal/search"
)
@@ -53,9 +55,20 @@
if search.IsRelativePath(arg) {
base.Fatalf("vgo: cannot use relative path %s to specify module", arg)
}
- if strings.Contains(arg, "@") {
- // TODO(rsc): Add support for 'go list -m golang.org/x/text@v0.3.0'
- base.Fatalf("vgo: list path@version not implemented")
+ if i := strings.Index(arg, "@"); i >= 0 {
+ info, err := modfetch.Query(arg[:i], arg[i+1:], nil)
+ if err != nil {
+ mods = append(mods, &modinfo.ModulePublic{
+ Path: arg[:i],
+ Version: arg[i+1:],
+ Error: &modinfo.ModuleError{
+ Err: err.Error(),
+ },
+ })
+ continue
+ }
+ mods = append(mods, moduleInfo(module.Version{Path: arg[:i], Version: info.Version}, false))
+ continue
}
// Module path or pattern.
diff --git a/vendor/cmd/go/vgo_test.go b/vendor/cmd/go/vgo_test.go
index 30ef894..fec216f 100644
--- a/vendor/cmd/go/vgo_test.go
+++ b/vendor/cmd/go/vgo_test.go
@@ -501,6 +501,17 @@
if tg.getStdout() != want {
t.Errorf("vgo list versions:\nhave:\n%s\nwant:\n%s", tg.getStdout(), want)
}
+
+ tg.run("-vgo", "list", "-m", "rsc.io/quote@>v1.5.2")
+ tg.grepStdout(`v1.5.3-pre1`, "expected to find v1.5.3-pre1")
+ tg.run("-vgo", "list", "-m", "rsc.io/quote@<v1.5.4")
+ tg.grepStdout(`v1.5.2$`, "expected to find v1.5.2 (NOT v1.5.3-pre1)")
+
+ tg.runFail("-vgo", "list", "-m", "rsc.io/quote@>v1.5.3")
+ tg.grepStderr(`go list -m rsc.io/quote: no matching versions for query ">v1.5.3"`, "expected no matching version")
+
+ tg.run("-vgo", "list", "-m", "-e", "-f={{.Error.Err}}", "rsc.io/quote@>v1.5.3")
+ tg.grepStdout(`no matching versions for query ">v1.5.3"`, "expected no matching version")
}
func TestVgoBadDomain(t *testing.T) {
@@ -773,8 +784,9 @@
tg.grepStderr("finding github.com/gorilla/mux v1.6.1", "find version 1.6.1")
tg.must(ioutil.WriteFile(tg.path("x/go.mod"), gomod, 0666))
- tg.runFail("-vgo", "get", "github.com/gorilla/mux@v1.6")
- tg.grepStderr("github.com/gorilla/mux@v1.6.0 excluded", "print version excluded")
+ tg.run("-vgo", "get", "github.com/gorilla/mux@>=v1.6")
+ tg.run("-vgo", "list", "-m", "...mux")
+ tg.grepStdout("github.com/gorilla/mux v1.6.[1-9]", "expected version 1.6.1 or later")
}
func TestRequireExcluded(t *testing.T) {