You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
211 lines
4.5 KiB
Go
211 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
"golang.org/x/mod/semver"
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
func nextcmd(args []string) {
|
|
flags := flag.NewFlagSet("next", flag.ExitOnError)
|
|
flags.Parse(args)
|
|
|
|
switch flags.Arg(0) {
|
|
case "major", "minor", "patch":
|
|
default:
|
|
bail(1, "major|minor|patch only for now")
|
|
}
|
|
|
|
log_debug.Printf("reading module file go.mod")
|
|
b, err := ioutil.ReadFile("go.mod")
|
|
if err != nil {
|
|
bail(1, "unable to read modfile: %v", err)
|
|
}
|
|
|
|
log_debug.Printf("parsing module file go.mod")
|
|
f, err := modfile.Parse("go.mod", b, nil)
|
|
if err != nil {
|
|
bail(1, "unable to parse modfile: %v", err)
|
|
}
|
|
|
|
modpath := f.Module.Mod.Path
|
|
log_debug.Printf("parsed module path: %s", modpath)
|
|
|
|
u, err := url.Parse(f.Module.Mod.Path)
|
|
if err != nil {
|
|
bail(1, "module path %s is not a valid url: %v", err)
|
|
}
|
|
u.Scheme = "https"
|
|
|
|
log_debug.Printf("GET %v", u)
|
|
res, err := http.Get(u.String())
|
|
if err != nil {
|
|
bail(1, "unable to fetch module root page: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
m, err := parseModPage(res.Body)
|
|
if err != nil {
|
|
bail(1, "unable to parse module meta page: %v", err)
|
|
}
|
|
|
|
lines, err := m.fetchVersionList()
|
|
if err != nil {
|
|
bail(1, "unable to fetch version list: %v", err)
|
|
}
|
|
log_debug.Printf("%s", lines)
|
|
}
|
|
|
|
// parseModPage parses the page at a module path, looking for the appropriate
|
|
// meta tags defined by the Go module ecosystem and by mir itself
|
|
func parseModPage(r io.Reader) (*modmeta, error) {
|
|
root, err := html.Parse(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid html: %v", err)
|
|
}
|
|
|
|
var meta modmeta
|
|
if err := meta.parseTree(root); err != nil {
|
|
return nil, fmt.Errorf("parse failed: %v", err)
|
|
}
|
|
return &meta, nil
|
|
}
|
|
|
|
type modmeta struct {
|
|
path string // module path
|
|
backend string // git | hg | mod
|
|
dlRoot url.URL // download URL
|
|
}
|
|
|
|
// parseTree parses an HTML tree, evaluating each node and then descending to
|
|
// its children
|
|
func (m *modmeta) parseTree(n *html.Node) error {
|
|
m.parseNode(n)
|
|
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
if err := m.parseTree(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// attr finds the first value for a given attribute in an html attribute list
|
|
func attr(key string, attrs []html.Attribute) string {
|
|
for _, a := range attrs {
|
|
if a.Key == key {
|
|
return a.Val
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// parseNode parses a single node in an html tree, without looking at its
|
|
// descendants
|
|
func (m *modmeta) parseNode(n *html.Node) error {
|
|
if n.Type != html.ElementNode {
|
|
return nil
|
|
}
|
|
if n.Data != "meta" {
|
|
return nil
|
|
}
|
|
|
|
if attr("name", n.Attr) == "go-import" {
|
|
content := attr("content", n.Attr)
|
|
if content == "" {
|
|
return fmt.Errorf("go-import meta tag is missing content")
|
|
}
|
|
|
|
parts := strings.Fields(content)
|
|
if len(parts) != 3 {
|
|
return fmt.Errorf("go import meta tag has invalid content (not 3 parts): %q", content)
|
|
}
|
|
|
|
m.path = parts[0]
|
|
m.backend = parts[1]
|
|
u, err := url.Parse(parts[2])
|
|
if err != nil {
|
|
return fmt.Errorf("go import meta tag has invalid download url: %v", err)
|
|
}
|
|
m.dlRoot = *u
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *modmeta) listEndpoint() (*url.URL, error) {
|
|
var empty url.URL
|
|
if m.dlRoot == empty {
|
|
return nil, fmt.Errorf("dl root is empty")
|
|
}
|
|
|
|
u := url.URL{
|
|
Scheme: "https",
|
|
Host: m.dlRoot.Host,
|
|
Path: path.Join(m.dlRoot.Path, m.path, "@v", "list"),
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
func (m *modmeta) fetchVersionList() ([]string, error) {
|
|
u, err := m.listEndpoint()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to locate version list: %w", err)
|
|
}
|
|
|
|
log_debug.Printf("GET %s", u)
|
|
res, err := http.Get(u.String())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to fetch version list: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
lines, err := parseVersionLines(res.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse version list: %w", err)
|
|
}
|
|
semver.Sort(lines)
|
|
return lines, nil
|
|
}
|
|
|
|
func parseVersionLines(r io.Reader) ([]string, error) {
|
|
lines := make([]string, 0, 8)
|
|
keep := func(s string) error {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
|
|
if !semver.IsValid(s) {
|
|
return fmt.Errorf("invalid version string: %s", s)
|
|
}
|
|
lines = append(lines, s)
|
|
return nil
|
|
}
|
|
|
|
br := bufio.NewReader(r)
|
|
for {
|
|
line, err := br.ReadString('\n')
|
|
if err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return lines, keep(line)
|
|
}
|
|
return nil, fmt.Errorf("error reading version list response: %w", err)
|
|
}
|
|
if err := keep(line); err != nil {
|
|
return nil, fmt.Errorf("bad version list: %w", err)
|
|
}
|
|
}
|
|
}
|