301 lines
7.6 KiB
Go
301 lines
7.6 KiB
Go
|
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 // Major true compare only major
|
||
|
UpMajor bool // UpMajor module up with major
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
major := semver.Major(m.Version)
|
||
|
var v string
|
||
|
switch opt.UpMajor {
|
||
|
case true:
|
||
|
v = mod.MaxVersion("", opt.Pre)
|
||
|
case false:
|
||
|
v = mod.MaxVersion(major, 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)
|
||
|
}
|
||
|
}
|
||
|
}
|