github.com/gernest/nezuko@v0.1.2/internal/modload/query.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modload
     6  
     7  import (
     8  	"fmt"
     9  	"github.com/gernest/nezuko/internal/modfetch"
    10  	"github.com/gernest/nezuko/internal/modfetch/codehost"
    11  	"github.com/gernest/nezuko/internal/module"
    12  	"github.com/gernest/nezuko/internal/semver"
    13  	pathpkg "path"
    14  	"strings"
    15  )
    16  
    17  // Query looks up a revision of a given module given a version query string.
    18  // The module must be a complete module path.
    19  // The version must take one of the following forms:
    20  //
    21  //	- the literal string "latest", denoting the latest available, allowed tagged version,
    22  //	  with non-prereleases preferred over prereleases.
    23  //	  If there are no tagged versions in the repo, latest returns the most recent commit.
    24  //	- v1, denoting the latest available tagged version v1.x.x.
    25  //	- v1.2, denoting the latest available tagged version v1.2.x.
    26  //	- v1.2.3, a semantic version string denoting that tagged version.
    27  //	- <v1.2.3, <=v1.2.3, >v1.2.3, >=v1.2.3,
    28  //	   denoting the version closest to the target and satisfying the given operator,
    29  //	   with non-prereleases preferred over prereleases.
    30  //	- a repository commit identifier, denoting that commit.
    31  //
    32  // If the allowed function is non-nil, Query excludes any versions for which allowed returns false.
    33  //
    34  // If path is the path of the main module and the query is "latest",
    35  // Query returns Target.Version as the version.
    36  func Query(path, query string, allowed func(module.Version) bool) (*modfetch.RevInfo, error) {
    37  	if allowed == nil {
    38  		allowed = func(module.Version) bool { return true }
    39  	}
    40  
    41  	// Parse query to detect parse errors (and possibly handle query)
    42  	// before any network I/O.
    43  	badVersion := func(v string) (*modfetch.RevInfo, error) {
    44  		return nil, fmt.Errorf("invalid semantic version %q in range %q", v, query)
    45  	}
    46  	var ok func(module.Version) bool
    47  	var prefix string
    48  	var preferOlder bool
    49  	switch {
    50  	case query == "latest":
    51  		ok = allowed
    52  
    53  	case strings.HasPrefix(query, "<="):
    54  		v := query[len("<="):]
    55  		if !semver.IsValid(v) {
    56  			return badVersion(v)
    57  		}
    58  		if isSemverPrefix(v) {
    59  			// Refuse to say whether <=v1.2 allows v1.2.3 (remember, @v1.2 might mean v1.2.3).
    60  			return nil, fmt.Errorf("ambiguous semantic version %q in range %q", v, query)
    61  		}
    62  		ok = func(m module.Version) bool {
    63  			return semver.Compare(m.Version, v) <= 0 && allowed(m)
    64  		}
    65  
    66  	case strings.HasPrefix(query, "<"):
    67  		v := query[len("<"):]
    68  		if !semver.IsValid(v) {
    69  			return badVersion(v)
    70  		}
    71  		ok = func(m module.Version) bool {
    72  			return semver.Compare(m.Version, v) < 0 && allowed(m)
    73  		}
    74  
    75  	case strings.HasPrefix(query, ">="):
    76  		v := query[len(">="):]
    77  		if !semver.IsValid(v) {
    78  			return badVersion(v)
    79  		}
    80  		ok = func(m module.Version) bool {
    81  			return semver.Compare(m.Version, v) >= 0 && allowed(m)
    82  		}
    83  		preferOlder = true
    84  
    85  	case strings.HasPrefix(query, ">"):
    86  		v := query[len(">"):]
    87  		if !semver.IsValid(v) {
    88  			return badVersion(v)
    89  		}
    90  		if isSemverPrefix(v) {
    91  			// Refuse to say whether >v1.2 allows v1.2.3 (remember, @v1.2 might mean v1.2.3).
    92  			return nil, fmt.Errorf("ambiguous semantic version %q in range %q", v, query)
    93  		}
    94  		ok = func(m module.Version) bool {
    95  			return semver.Compare(m.Version, v) > 0 && allowed(m)
    96  		}
    97  		preferOlder = true
    98  
    99  	case semver.IsValid(query) && isSemverPrefix(query):
   100  		ok = func(m module.Version) bool {
   101  			return matchSemverPrefix(query, m.Version) && allowed(m)
   102  		}
   103  		prefix = query + "."
   104  
   105  	case semver.IsValid(query):
   106  		vers := module.CanonicalVersion(query)
   107  		if !allowed(module.Version{Path: path, Version: vers}) {
   108  			return nil, fmt.Errorf("%s@%s excluded", path, vers)
   109  		}
   110  		return modfetch.Stat(path, vers)
   111  
   112  	default:
   113  		// Direct lookup of semantic version or commit identifier.
   114  		info, err := modfetch.Stat(path, query)
   115  		if err != nil {
   116  			return nil, err
   117  		}
   118  		if !allowed(module.Version{Path: path, Version: info.Version}) {
   119  			return nil, fmt.Errorf("%s@%s excluded", path, info.Version)
   120  		}
   121  		return info, nil
   122  	}
   123  
   124  	if path == Target.Path {
   125  		if query != "latest" {
   126  			return nil, fmt.Errorf("can't query specific version (%q) for the main module (%s)", query, path)
   127  		}
   128  		if !allowed(Target) {
   129  			return nil, fmt.Errorf("internal error: main module version is not allowed")
   130  		}
   131  		return &modfetch.RevInfo{Version: Target.Version}, nil
   132  	}
   133  
   134  	// Load versions and execute query.
   135  	repo, err := modfetch.Lookup(path)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	versions, err := repo.Versions(prefix)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	if preferOlder {
   145  		for _, v := range versions {
   146  			if semver.Prerelease(v) == "" && ok(module.Version{Path: path, Version: v}) {
   147  				return repo.Stat(v)
   148  			}
   149  		}
   150  		for _, v := range versions {
   151  			if semver.Prerelease(v) != "" && ok(module.Version{Path: path, Version: v}) {
   152  				return repo.Stat(v)
   153  			}
   154  		}
   155  	} else {
   156  		for i := len(versions) - 1; i >= 0; i-- {
   157  			v := versions[i]
   158  			if semver.Prerelease(v) == "" && ok(module.Version{Path: path, Version: v}) {
   159  				return repo.Stat(v)
   160  			}
   161  		}
   162  		for i := len(versions) - 1; i >= 0; i-- {
   163  			v := versions[i]
   164  			if semver.Prerelease(v) != "" && ok(module.Version{Path: path, Version: v}) {
   165  				return repo.Stat(v)
   166  			}
   167  		}
   168  	}
   169  
   170  	if query == "latest" {
   171  		// Special case for "latest": if no tags match, use latest commit in repo,
   172  		// provided it is not excluded.
   173  		if info, err := repo.Latest(); err == nil && allowed(module.Version{Path: path, Version: info.Version}) {
   174  			return info, nil
   175  		}
   176  	}
   177  
   178  	return nil, fmt.Errorf("no matching versions for query %q", query)
   179  }
   180  
   181  // isSemverPrefix reports whether v is a semantic version prefix: v1 or  v1.2 (not v1.2.3).
   182  // The caller is assumed to have checked that semver.IsValid(v) is true.
   183  func isSemverPrefix(v string) bool {
   184  	dots := 0
   185  	for i := 0; i < len(v); i++ {
   186  		switch v[i] {
   187  		case '-', '+':
   188  			return false
   189  		case '.':
   190  			dots++
   191  			if dots >= 2 {
   192  				return false
   193  			}
   194  		}
   195  	}
   196  	return true
   197  }
   198  
   199  // matchSemverPrefix reports whether the shortened semantic version p
   200  // matches the full-width (non-shortened) semantic version v.
   201  func matchSemverPrefix(p, v string) bool {
   202  	return len(v) > len(p) && v[len(p)] == '.' && v[:len(p)] == p
   203  }
   204  
   205  // QueryPackage looks up a revision of a module containing path.
   206  //
   207  // If multiple modules with revisions matching the query provide the requested
   208  // package, QueryPackage picks the one with the longest module path.
   209  //
   210  // If the path is in the main module and the query is "latest",
   211  // QueryPackage returns Target as the version.
   212  func QueryPackage(path, query string, allowed func(module.Version) bool) (module.Version, *modfetch.RevInfo, error) {
   213  	if HasModRoot() {
   214  		if _, ok := dirInModule(path, Target.Path, modRoot, true); ok {
   215  			if query != "latest" {
   216  				return module.Version{}, nil, fmt.Errorf("can't query specific version (%q) for package %s in the main module (%s)", query, path, Target.Path)
   217  			}
   218  			if !allowed(Target) {
   219  				return module.Version{}, nil, fmt.Errorf("internal error: package %s is in the main module (%s), but version is not allowed", path, Target.Path)
   220  			}
   221  			return Target, &modfetch.RevInfo{Version: Target.Version}, nil
   222  		}
   223  	}
   224  
   225  	finalErr := errMissing
   226  	for p := path; p != "." && p != "/"; p = pathpkg.Dir(p) {
   227  		info, err := Query(p, query, allowed)
   228  		if err != nil {
   229  			if _, ok := err.(*codehost.VCSError); ok {
   230  				// A VCSError means we know where to find the code,
   231  				// we just can't. Abort search.
   232  				return module.Version{}, nil, err
   233  			}
   234  			if finalErr == errMissing {
   235  				finalErr = err
   236  			}
   237  			continue
   238  		}
   239  		m := module.Version{Path: p, Version: info.Version}
   240  		root, isLocal, err := fetch(m)
   241  		if err != nil {
   242  			return module.Version{}, nil, err
   243  		}
   244  		_, ok := dirInModule(path, m.Path, root, isLocal)
   245  		if ok {
   246  			return m, info, nil
   247  		}
   248  	}
   249  
   250  	return module.Version{}, nil, finalErr
   251  }