get next version plz

master
Jordan Orelli 3 years ago
parent 474d7f53da
commit dcb2e52e7b

@ -6,5 +6,6 @@ require golang.org/x/mod v0.5.1
require ( require (
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect
) )

@ -2,6 +2,8 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+Wr
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

@ -5,13 +5,17 @@ import (
_ "embed" _ "embed"
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/signal" "os/signal"
) )
var log_error = log.New(os.Stderr, "", 0) var (
var log_info = log.New(os.Stdout, "", 0) log_error = log.New(os.Stderr, "", 0)
log_info = log.New(os.Stdout, "", 0)
log_debug = log.New(io.Discard, "", 0)
)
func bail(status int, t string, args ...interface{}) { func bail(status int, t string, args ...interface{}) {
if status != 0 { if status != 0 {
@ -39,17 +43,40 @@ func sigCancel(ctx context.Context) context.Context {
} }
func main() { func main() {
var (
quiet bool
verbose bool
)
sigCancel(context.Background()) sigCancel(context.Background())
root := flag.NewFlagSet("", flag.ExitOnError) root := flag.NewFlagSet("", flag.ExitOnError)
root.BoolVar(&quiet, "q", false, "suppress non-error output")
root.BoolVar(&verbose, "v", false, "show additional debug output")
root.Parse(os.Args[1:]) root.Parse(os.Args[1:])
if quiet {
log_info = log.New(io.Discard, "", 0)
}
if !quiet && verbose {
log_debug = log.New(os.Stdout, "", 0)
}
rest := root.Args()[1:]
switch root.Arg(0) { switch root.Arg(0) {
case "serve": case "serve":
serve(root.Args()[1:]) serve(rest)
case "zip": case "zip":
zipcmd(root.Args()[1:]) zipcmd(rest)
case "pwhash": case "pwhash":
pwhashcmd(root.Args()[1:]) pwhashcmd(rest)
case "next":
nextcmd(rest)
// mir next major
// mir next minor
// mir next patch
// mir next pre fartstorm
default: default:
bail(0, usage) bail(0, usage)
} }

@ -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…
Cancel
Save