get next version plz
parent
474d7f53da
commit
dcb2e52e7b
@ -0,0 +1,210 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue