Skip to content

Instantly share code, notes, and snippets.

@icholy
Created August 4, 2020 16:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save icholy/aef930d7c5e3f500e922cb5c92ad4a31 to your computer and use it in GitHub Desktop.
Save icholy/aef930d7c5e3f500e922cb5c92ad4a31 to your computer and use it in GitHub Desktop.
package main
import (
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/andybalholm/cascadia"
"github.com/hashicorp/go-version"
"golang.org/x/net/html"
"golang.org/x/tools/go/packages"
)
func main() {
cfg := packages.Config{
Mode: packages.NeedModule,
Dir: ".",
}
pkgs, err := packages.Load(&cfg, "all")
if err != nil {
log.Fatal(err)
}
seen := map[string]bool{}
for _, pkg := range pkgs {
if mod := pkg.Module; mod != nil && !mod.Indirect && !mod.Main && !seen[mod.Path] {
seen[mod.Path] = true
vv, err := versions(mod.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s: %v\n", mod.Path, err)
continue
}
latest, err := highest(vv)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s: %v\n", mod.Path, err)
continue
}
if mod.Version != latest {
fmt.Printf("%s %s [latest %s]\n", mod.Path, mod.Version, latest)
}
}
}
}
// parse all the versions from the pkg.go.dev versions tab
func parse(r io.Reader) ([]string, error) {
var versions []string
doc, err := html.Parse(r)
if err != nil {
return nil, err
}
sel := cascadia.MustCompile(".Versions-item>a")
for _, node := range cascadia.QueryAll(doc, sel) {
walk(node, func(n *html.Node) {
if n.Type == html.TextNode {
versions = append(versions, n.Data)
}
})
}
return versions, nil
}
// walk the node using depth first
func walk(n *html.Node, f func(*html.Node)) {
f(n)
if n.FirstChild != nil {
for c := n.FirstChild; c != nil; c = c.NextSibling {
walk(c, f)
}
}
}
// get all the versions for a module
func versions(modpath string) ([]string, error) {
url := fmt.Sprintf("https://pkg.go.dev/%s?tab=versions", modpath)
res, err := http.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status: %s", res.Status)
}
return parse(res.Body)
}
// highest returns the highest semver version string
func highest(versions []string) (string, error) {
if len(versions) == 0 {
return "", errors.New("no versions")
}
var newest string
var newestVer *version.Version
for _, s := range versions {
v, err := version.NewVersion(s)
if err != nil {
return "", err
}
if newestVer == nil || v.GreaterThan(newestVer) {
newestVer = v
newest = s
}
}
return newest, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment