292
internal/modules/modproxy.go
Normal file
292
internal/modules/modproxy.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/semver"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// Module contains the module path and versions
|
||||
type Module struct {
|
||||
Path string
|
||||
Versions []string
|
||||
}
|
||||
|
||||
// MaxVersion returns the latest version.
|
||||
// If there are no versions, the empty string is returned.
|
||||
// Prefix can be used to filter the versions based on a prefix.
|
||||
// If pre is false, pre-release versions will are excluded.
|
||||
func (m *Module) MaxVersion(prefix string, pre bool) string {
|
||||
var max string
|
||||
for _, v := range m.Versions {
|
||||
if !semver.IsValid(v) || !strings.HasPrefix(v, prefix) {
|
||||
continue
|
||||
}
|
||||
if !pre && semver.Prerelease(v) != "" {
|
||||
continue
|
||||
}
|
||||
max = MaxVersion(v, max)
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// IsNewerVersion returns true if newversion is greater than oldversion in terms of semver.
|
||||
// If major is true, then newversion must be a major version ahead of oldversion to be considered newer.
|
||||
func IsNewerVersion(oldversion, newversion string, major bool) bool {
|
||||
if major {
|
||||
return semver.Compare(semver.Major(oldversion), semver.Major(newversion)) < 0
|
||||
}
|
||||
return semver.Compare(oldversion, newversion) < 0
|
||||
}
|
||||
|
||||
// MaxVersion returns the larger of two versions according to semantic version precedence.
|
||||
// Incompatible versions are considered lower than non-incompatible ones.
|
||||
// Invalid versions are considered lower than valid ones.
|
||||
// If both versions are invalid, the empty string is returned.
|
||||
func MaxVersion(v, w string) string {
|
||||
// sort by validity
|
||||
vValid := semver.IsValid(v)
|
||||
wValid := semver.IsValid(w)
|
||||
if !vValid && !wValid {
|
||||
return ""
|
||||
}
|
||||
if vValid != wValid {
|
||||
if vValid {
|
||||
return v
|
||||
}
|
||||
return w
|
||||
}
|
||||
// sort by compatibility
|
||||
vIncompatible := strings.HasSuffix(semver.Build(v), "+incompatible")
|
||||
wIncompatible := strings.HasSuffix(semver.Build(w), "+incompatible")
|
||||
if vIncompatible != wIncompatible {
|
||||
if wIncompatible {
|
||||
return v
|
||||
}
|
||||
return w
|
||||
}
|
||||
// sort by semver
|
||||
if semver.Compare(v, w) == 1 {
|
||||
return v
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// NextMajor returns the next major version after the provided version
|
||||
func NextMajor(version string) (string, error) {
|
||||
major, err := strconv.Atoi(strings.TrimPrefix(semver.Major(version), "v"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
major++
|
||||
return fmt.Sprintf("v%d", major), nil
|
||||
}
|
||||
|
||||
// WithMajorPath returns the module path for the provided version
|
||||
func (m *Module) WithMajorPath(version string) string {
|
||||
prefix := ModPrefix(m.Path)
|
||||
return JoinPath(prefix, version, "")
|
||||
}
|
||||
|
||||
// NextMajorPath returns the module path of the next major version
|
||||
func (m *Module) NextMajorPath() (string, bool) {
|
||||
latest := m.MaxVersion("", true)
|
||||
if latest == "" {
|
||||
return "", false
|
||||
}
|
||||
if semver.Major(latest) == "v0" {
|
||||
return "", false
|
||||
}
|
||||
next, err := NextMajor(latest)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return m.WithMajorPath(next), true
|
||||
}
|
||||
|
||||
// Query the module proxy for all versions of a module.
|
||||
// If the module does not exist, the second return parameter will be false
|
||||
// cached sets the Disable-Module-Fetch: true header
|
||||
func Query(modpath string, cached bool) (*Module, bool, error) {
|
||||
escaped, err := module.EscapePath(modpath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
url := fmt.Sprintf("https://proxy.golang.org/%s/@v/list", escaped)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "GoMajor/1.0")
|
||||
if cached {
|
||||
req.Header.Set("Disable-Module-Fetch", "true")
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
if res.StatusCode == http.StatusNotFound && bytes.HasPrefix(body, []byte("not found:")) {
|
||||
return nil, false, nil
|
||||
}
|
||||
msg := string(body)
|
||||
if msg == "" {
|
||||
msg = res.Status
|
||||
}
|
||||
return nil, false, fmt.Errorf("proxy: %s", msg)
|
||||
}
|
||||
var mod Module
|
||||
mod.Path = modpath
|
||||
sc := bufio.NewScanner(res.Body)
|
||||
for sc.Scan() {
|
||||
mod.Versions = append(mod.Versions, sc.Text())
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &mod, true, nil
|
||||
}
|
||||
|
||||
// Latest finds the latest major version of a module
|
||||
// cached sets the Disable-Module-Fetch: true header
|
||||
func Latest(modpath string, cached bool) (*Module, error) {
|
||||
latest, ok, err := Query(modpath, cached)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("module not found: %s", modpath)
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
nextpath, ok := latest.NextMajorPath()
|
||||
if !ok {
|
||||
return latest, nil
|
||||
}
|
||||
next, ok, err := Query(nextpath, cached)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
// handle the case where a project switched to modules
|
||||
// without incrementing the major version
|
||||
version := latest.MaxVersion("", true)
|
||||
if semver.Build(version) == "+incompatible" {
|
||||
nextpath = latest.WithMajorPath(semver.Major(version))
|
||||
if nextpath != latest.Path {
|
||||
next, ok, err = Query(nextpath, cached)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return latest, nil
|
||||
}
|
||||
latest = next
|
||||
}
|
||||
return nil, fmt.Errorf("request limit exceeded")
|
||||
}
|
||||
|
||||
// QueryPackage tries to find the module path for the provided package path
|
||||
// it does so by repeatedly chopping off the last path element and trying to
|
||||
// use it as a path.
|
||||
func QueryPackage(pkgpath string, cached bool) (*Module, error) {
|
||||
prefix := pkgpath
|
||||
for prefix != "" {
|
||||
if module.CheckPath(prefix) == nil {
|
||||
mod, ok, err := Query(prefix, cached)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
modprefix := ModPrefix(mod.Path)
|
||||
if modpath, pkgdir, ok := SplitPath(modprefix, pkgpath); ok && modpath != mod.Path {
|
||||
if major, ok := ModMajor(modpath); ok {
|
||||
if v := mod.MaxVersion(major, false); v != "" {
|
||||
spec := JoinPath(modprefix, "", pkgdir) + "@" + v
|
||||
return nil, fmt.Errorf("%s doesn't support import versioning; use %s", major, spec)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find module for package: %s", pkgpath)
|
||||
}
|
||||
}
|
||||
return mod, nil
|
||||
}
|
||||
}
|
||||
remaining, last := path.Split(prefix)
|
||||
if last == "" {
|
||||
break
|
||||
}
|
||||
prefix = strings.TrimSuffix(remaining, "/")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find module for package: %s", pkgpath)
|
||||
}
|
||||
|
||||
// Update reports a newer version of a module.
|
||||
// The Err field will be set if an error occured.
|
||||
type Update struct {
|
||||
Err error
|
||||
Module module.Version
|
||||
Version string
|
||||
}
|
||||
|
||||
// UpdateOptions specifies a set of modules to check for updates.
|
||||
// The OnUpdate callback will be invoked with any updates found.
|
||||
type UpdateOptions struct {
|
||||
OnUpdate func(Update)
|
||||
Modules []module.Version
|
||||
Pre bool
|
||||
Cached bool
|
||||
Major bool
|
||||
}
|
||||
|
||||
// Updates finds updates for a set of specified modules.
|
||||
func Updates(opt UpdateOptions) {
|
||||
ch := make(chan Update)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
private := os.Getenv("GOPRIVATE")
|
||||
var group errgroup.Group
|
||||
if opt.Cached {
|
||||
group.SetLimit(3)
|
||||
} else {
|
||||
group.SetLimit(1)
|
||||
}
|
||||
for _, m := range opt.Modules {
|
||||
m := m
|
||||
if module.MatchPrefixPatterns(private, m.Path) {
|
||||
continue
|
||||
}
|
||||
group.Go(func() error {
|
||||
mod, err := Latest(m.Path, opt.Cached)
|
||||
if err != nil {
|
||||
ch <- Update{Module: m, Err: err}
|
||||
return nil
|
||||
}
|
||||
v := mod.MaxVersion("", opt.Pre)
|
||||
if IsNewerVersion(m.Version, v, opt.Major) {
|
||||
ch <- Update{Module: m, Version: v}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
_ = group.Wait()
|
||||
}()
|
||||
for u := range ch {
|
||||
if opt.OnUpdate != nil {
|
||||
opt.OnUpdate(u)
|
||||
}
|
||||
}
|
||||
}
|
115
internal/modules/packages.go
Normal file
115
internal/modules/packages.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// ModPrefix returns the module path with no SIV
|
||||
func ModPrefix(modpath string) string {
|
||||
prefix, _, ok := module.SplitPathVersion(modpath)
|
||||
if !ok {
|
||||
prefix = modpath
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
// ModMajor returns the major version in vN format
|
||||
func ModMajor(modpath string) (string, bool) {
|
||||
_, major, ok := module.SplitPathVersion(modpath)
|
||||
if ok {
|
||||
major = strings.TrimPrefix(major, "/")
|
||||
major = strings.TrimPrefix(major, ".")
|
||||
}
|
||||
return major, ok
|
||||
}
|
||||
|
||||
// SplitPath splits the package path into the module path and the package subdirectory.
|
||||
// It requires the a module path prefix to figure this out.
|
||||
func SplitPath(modprefix, pkgpath string) (modpath, pkgdir string, ok bool) {
|
||||
if !strings.HasPrefix(pkgpath, modprefix) {
|
||||
return "", "", false
|
||||
}
|
||||
modpathlen := len(modprefix)
|
||||
if rest := pkgpath[modpathlen:]; len(rest) > 0 && rest[0] != '/' && rest[0] != '.' {
|
||||
return "", "", false
|
||||
}
|
||||
if strings.HasPrefix(pkgpath[modpathlen:], "/") {
|
||||
modpathlen++
|
||||
}
|
||||
if idx := strings.Index(pkgpath[modpathlen:], "/"); idx >= 0 {
|
||||
modpathlen += idx
|
||||
} else {
|
||||
modpathlen = len(pkgpath)
|
||||
}
|
||||
modpath = modprefix
|
||||
if major, ok := ModMajor(pkgpath[:modpathlen]); ok {
|
||||
modpath = JoinPath(modprefix, major, "")
|
||||
}
|
||||
pkgdir = strings.TrimPrefix(pkgpath[len(modpath):], "/")
|
||||
return modpath, pkgdir, true
|
||||
}
|
||||
|
||||
// SplitSpec splits the path/to/package@query format strings
|
||||
func SplitSpec(spec string) (path, query string) {
|
||||
parts := strings.SplitN(spec, "@", 2)
|
||||
if len(parts) == 2 {
|
||||
path = parts[0]
|
||||
query = parts[1]
|
||||
} else {
|
||||
path = spec
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// JoinPath creates a full package path given a module prefix, version, and package directory.
|
||||
func JoinPath(modprefix, version, pkgdir string) string {
|
||||
version = strings.TrimPrefix(version, ".")
|
||||
version = strings.TrimPrefix(version, "/")
|
||||
major := semver.Major(version)
|
||||
pkgpath := modprefix
|
||||
switch {
|
||||
case strings.HasPrefix(pkgpath, "gopkg.in"):
|
||||
pkgpath += "." + major
|
||||
case major != "" && major != "v0" && major != "v1" && !strings.Contains(version, "+incompatible"):
|
||||
if !strings.HasSuffix(pkgpath, "/") {
|
||||
pkgpath += "/"
|
||||
}
|
||||
pkgpath += major
|
||||
}
|
||||
if pkgdir != "" {
|
||||
pkgpath += "/" + pkgdir
|
||||
}
|
||||
return pkgpath
|
||||
}
|
||||
|
||||
// FindModFile recursively searches up the directory structure until it
|
||||
// finds the go.mod, reaches the root of the directory tree, or encounters
|
||||
// an error.
|
||||
func FindModFile(dir string) (string, error) {
|
||||
var err error
|
||||
dir, err = filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
name := filepath.Join(dir, "go.mod")
|
||||
_, err := os.Stat(name)
|
||||
if err == nil {
|
||||
return name, nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("cannot find go.mod")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user