blob: f0f67c193cd016b891b2962739f5f5b2b4bc74e5 [file] [log] [blame]
// 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 modload
import (
"fmt"
pathpkg "path"
"strings"
"sync"
"cmd/go/internal/modfetch"
"cmd/go/internal/modfetch/codehost"
"cmd/go/internal/module"
"cmd/go/internal/search"
"cmd/go/internal/semver"
"cmd/go/internal/str"
)
// Query looks up a revision of a given module given a version query string.
// The module must be a complete module path.
// 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.
// 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 or tag, denoting that commit.
//
// If the allowed function is non-nil, Query excludes any versions for which allowed returns false.
//
// If path is the path of the main module and the query is "latest",
// Query returns Target.Version as the version.
func Query(path, query string, allowed func(module.Version) bool) (*modfetch.RevInfo, error) {
if allowed == nil {
allowed = func(module.Version) bool { return true }
}
// Parse query to detect parse errors (and possibly handle query)
// before any network I/O.
badVersion := func(v string) (*modfetch.RevInfo, error) {
return nil, fmt.Errorf("invalid semantic version %q in range %q", v, query)
}
var ok func(module.Version) bool
var prefix string
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)
}
prefix = query + "."
default:
// Direct lookup of semantic version or commit identifier.
//
// If the identifier is not a canonical semver tag — including if it's a
// semver tag with a +metadata suffix — then modfetch.Stat will populate
// info.Version with a suitable pseudo-version.
info, err := modfetch.Stat(path, query)
if err != nil {
queryErr := err
// The full query doesn't correspond to a tag. If it is a semantic version
// with a +metadata suffix, see if there is a tag without that suffix:
// semantic versioning defines them to be equivalent.
if vers := module.CanonicalVersion(query); vers != "" && vers != query {
info, err = modfetch.Stat(path, vers)
}
if err != nil {
return nil, queryErr
}
}
if !allowed(module.Version{Path: path, Version: info.Version}) {
return nil, fmt.Errorf("%s@%s excluded", path, info.Version)
}
return info, nil
}
if path == Target.Path {
if query != "latest" {
return nil, fmt.Errorf("can't query specific version (%q) for the main module (%s)", query, path)
}
if !allowed(Target) {
return nil, fmt.Errorf("internal error: main module version is not allowed")
}
return &modfetch.RevInfo{Version: Target.Version}, nil
}
if str.HasPathPrefix(path, "std") || str.HasPathPrefix(path, "cmd") {
return nil, fmt.Errorf("explicit requirement on standard-library module %s not allowed", path)
}
// Load versions and execute query.
repo, err := modfetch.Lookup(path)
if err != nil {
return nil, err
}
versions, err := repo.Versions(prefix)
if err != nil {
return nil, err
}
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, &NoMatchingVersionError{query: 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 && semver.Prerelease(v) == ""
}
type QueryResult struct {
Mod module.Version
Rev *modfetch.RevInfo
Packages []string
}
// QueryPackage looks up the module(s) containing path at a revision matching
// query. The results are sorted by module path length in descending order.
//
// If the package is in the main module, QueryPackage considers only the main
// module and only the version "latest", without checking for other possible
// modules.
func QueryPackage(path, query string, allowed func(module.Version) bool) ([]QueryResult, error) {
if search.IsMetaPackage(path) || strings.Contains(path, "...") {
return nil, fmt.Errorf("pattern %s is not an importable package", path)
}
return QueryPattern(path, query, allowed)
}
// QueryPattern looks up the module(s) containing at least one package matching
// the given pattern at the given version. The results are sorted by module path
// length in descending order.
//
// QueryPattern queries modules with package paths up to the first "..."
// in the pattern. For the pattern "example.com/a/b.../c", QueryPattern would
// consider prefixes of "example.com/a". If multiple modules have versions
// that match the query and packages that match the pattern, QueryPattern
// picks the one with the longest module path.
//
// If any matching package is in the main module, QueryPattern considers only
// the main module and only the version "latest", without checking for other
// possible modules.
func QueryPattern(pattern string, query string, allowed func(module.Version) bool) ([]QueryResult, error) {
base := pattern
var match func(m module.Version, root string, isLocal bool) (pkgs []string)
if i := strings.Index(pattern, "..."); i >= 0 {
base = pathpkg.Dir(pattern[:i+3])
match = func(m module.Version, root string, isLocal bool) []string {
return matchPackages(pattern, anyTags, false, []module.Version{m})
}
} else {
match = func(m module.Version, root string, isLocal bool) []string {
prefix := m.Path
if m == Target {
prefix = targetPrefix
}
if _, ok := dirInModule(pattern, prefix, root, isLocal); ok {
return []string{pattern}
} else {
return nil
}
}
}
if HasModRoot() {
pkgs := match(Target, modRoot, true)
if len(pkgs) > 0 {
if query != "latest" {
return nil, fmt.Errorf("can't query specific version for package %s in the main module (%s)", pattern, Target.Path)
}
if !allowed(Target) {
return nil, fmt.Errorf("internal error: package %s is in the main module (%s), but version is not allowed", pattern, Target.Path)
}
return []QueryResult{{
Mod: Target,
Rev: &modfetch.RevInfo{Version: Target.Version},
Packages: pkgs,
}}, nil
}
}
// If the path we're attempting is not in the module cache and we don't have a
// fetch result cached either, we'll end up making a (potentially slow)
// request to the proxy or (often even slower) the origin server.
// To minimize latency, execute all of those requests in parallel.
type result struct {
QueryResult
err error
}
results := make([]result, strings.Count(base, "/")+1) // by descending path length
i, p := 0, base
var wg sync.WaitGroup
wg.Add(len(results))
for {
go func(p string, r *result) (err error) {
defer func() {
r.err = err
wg.Done()
}()
r.Mod.Path = p
if HasModRoot() && p == Target.Path {
r.Mod.Version = Target.Version
r.Rev = &modfetch.RevInfo{Version: Target.Version}
// We already know (from above) that Target does not contain any
// packages matching pattern, so leave r.Packages empty.
} else {
r.Rev, err = Query(p, query, allowed)
if err != nil {
return err
}
r.Mod.Version = r.Rev.Version
root, isLocal, err := fetch(r.Mod)
if err != nil {
return err
}
r.Packages = match(r.Mod, root, isLocal)
}
if len(r.Packages) == 0 {
return &packageNotInModuleError{
mod: r.Mod,
query: query,
pattern: pattern,
}
}
return nil
}(p, &results[i])
j := strings.LastIndexByte(p, '/')
if i++; i == len(results) {
if j >= 0 {
panic("undercounted slashes")
}
break
}
if j < 0 {
panic("overcounted slashes")
}
p = p[:j]
}
wg.Wait()
// Classify the results. In case of failure, identify the error that the user
// is most likely to find helpful.
var (
successes []QueryResult
mostUseful result
)
for _, r := range results {
if r.err == nil {
successes = append(successes, r.QueryResult)
continue
}
switch mostUseful.err.(type) {
case nil:
mostUseful = r
continue
case *packageNotInModuleError:
// Any other error is more useful than one that reports that the main
// module does not contain the requested packages.
if mostUseful.Mod.Path == Target.Path {
mostUseful = r
continue
}
}
switch r.err.(type) {
case *codehost.VCSError:
// A VCSError means that we've located a repository, but couldn't look
// inside it for packages. That's a very strong signal, and should
// override any others.
return nil, r.err
case *packageNotInModuleError:
if r.Mod.Path == Target.Path {
// Don't override a potentially-useful error for some other module with
// a trivial error for the main module.
continue
}
// A module with an appropriate prefix exists at the requested version,
// but it does not contain the requested package(s).
if _, worsePath := mostUseful.err.(*packageNotInModuleError); !worsePath {
mostUseful = r
}
case *NoMatchingVersionError:
// A module with an appropriate prefix exists, but not at the requested
// version.
_, worseError := mostUseful.err.(*packageNotInModuleError)
_, worsePath := mostUseful.err.(*NoMatchingVersionError)
if !(worseError || worsePath) {
mostUseful = r
}
}
}
// TODO(#26232): If len(successes) == 0 and some of the errors are 4xx HTTP
// codes, have the auth package recheck the failed paths.
// If we obtain new credentials for any of them, re-run the above loop.
if len(successes) == 0 {
// All of the possible module paths either did not exist at the requested
// version, or did not contain the requested package(s).
return nil, mostUseful.err
}
// At least one module at the requested version contained the requested
// package(s). Any remaining errors only describe the non-existence of
// alternatives, so ignore them.
return successes, nil
}
// A NoMatchingVersionError indicates that Query found a module at the requested
// path, but not at any versions satisfying the query string and allow-function.
type NoMatchingVersionError struct {
query string
}
func (e *NoMatchingVersionError) Error() string {
return fmt.Sprintf("no matching versions for query %q", e.query)
}
// A packageNotInModuleError indicates that QueryPattern found a candidate
// module at the requested version, but that module did not contain any packages
// matching the requested pattern.
type packageNotInModuleError struct {
mod module.Version
query string
pattern string
}
func (e *packageNotInModuleError) Error() string {
found := ""
if e.query != e.mod.Version {
found = fmt.Sprintf(" (%s)", e.mod.Version)
}
if strings.Contains(e.pattern, "...") {
return fmt.Sprintf("module %s@%s%s found, but does not contain packages matching %s", e.mod.Path, e.query, found, e.pattern)
}
return fmt.Sprintf("module %s@%s%s found, but does not contain package %s", e.mod.Path, e.query, found, e.pattern)
}
// ModuleHasRootPackage returns whether module m contains a package m.Path.
func ModuleHasRootPackage(m module.Version) (bool, error) {
root, isLocal, err := fetch(m)
if err != nil {
return false, err
}
_, ok := dirInModule(m.Path, m.Path, root, isLocal)
return ok, nil
}