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 }