From ece8baeaac5e09633aea6f2ea94907c0d08e82b4 Mon Sep 17 00:00:00 2001 From: David Ashby Date: Fri, 9 Apr 2021 22:01:41 -0400 Subject: [PATCH] init --- .gitignore | 1 + go.mod | 5 ++ go.sum | 14 ++++++ main.go | 23 +++++++++ pkg/cache/cache.go | 23 +++++++++ pkg/gomod/gomod.go | 35 ++++++++++++++ pkg/gomod/modproxy.go | 105 +++++++++++++++++++++++++++++++++++++++++ pkg/libyear/libyear.go | 22 +++++++++ 8 files changed, 228 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pkg/cache/cache.go create mode 100644 pkg/gomod/gomod.go create mode 100644 pkg/gomod/modproxy.go create mode 100644 pkg/libyear/libyear.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5e3e37 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/libyear \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..57a240c --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.yetaga.in/deltamualpha/libyear + +go 1.16 + +require golang.org/x/mod v0.4.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dc5796a --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f22dbb1 --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "log" + "time" + + "git.yetaga.in/deltamualpha/libyear/pkg/gomod" + "git.yetaga.in/deltamualpha/libyear/pkg/libyear" +) + +func main() { + pairs, err := gomod.LoadAndComputePairs("go.mod") + + if err != nil { + log.Fatal(err) + } + + for dep := range pairs { + fmt.Printf("%s: %d years (newest version: %s)\n", pairs[dep].Name, libyear.Calc(pairs[dep]).Truncate(time.Hour)/time.Hour/8760, pairs[dep].Latest.Version) + } + fmt.Printf("total libyear count: %d years\n", libyear.Calc(pairs...).Truncate(time.Hour)/time.Hour/8760) +} diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 0000000..104c6a9 --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,23 @@ +package cache + +import "git.yetaga.in/deltamualpha/libyear/pkg/libyear" + +// Cache isn't concurrency-safe, but we don't care about that right now +type Cache struct { + items map[string]libyear.Info +} + +func (c *Cache) Get(k string) libyear.Info { + i, ok := c.items[k] + if ok { + return i + } + return libyear.Info{} +} + +func (c *Cache) Set(k string, v libyear.Info) { + if c.items == nil { + c.items = make(map[string]libyear.Info) + } + c.items[k] = v +} diff --git a/pkg/gomod/gomod.go b/pkg/gomod/gomod.go new file mode 100644 index 0000000..1696f04 --- /dev/null +++ b/pkg/gomod/gomod.go @@ -0,0 +1,35 @@ +package gomod + +import ( + "os" + + "git.yetaga.in/deltamualpha/libyear/pkg/libyear" + "golang.org/x/mod/modfile" +) + +func LoadAndComputePairs(filename string) ([]libyear.Pair, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + f, err := modfile.Parse(filename, b, nil) + if err != nil { + return nil, err + } + q := Queryer{} + + pairs := []libyear.Pair{} + + for v := range f.Require { + if f.Require[v].Mod.Path != "" && f.Require[v].Mod.Version != "" { + latest := q.GetLatestVersion(f.Require[v].Mod.Path) + current := q.GetVersion(f.Require[v].Mod.Path, f.Require[v].Mod.Version) + pairs = append(pairs, libyear.Pair{ + Name: f.Require[v].Mod.Path, + Current: current, + Latest: latest, + }) + } + } + return pairs, nil +} diff --git a/pkg/gomod/modproxy.go b/pkg/gomod/modproxy.go new file mode 100644 index 0000000..0c5cbc3 --- /dev/null +++ b/pkg/gomod/modproxy.go @@ -0,0 +1,105 @@ +package gomod + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strconv" + "strings" + + "git.yetaga.in/deltamualpha/libyear/pkg/cache" + "git.yetaga.in/deltamualpha/libyear/pkg/libyear" +) + +type Queryer struct { + Root string + Client http.Client + Cache cache.Cache +} + +type majorVersion struct { + raw string + version int +} + +func (q *Queryer) GetLatestVersion(module string) libyear.Info { + latestMod := q.findLatestMajorVersion(module) + return q.makeProxyRequest(latestMod + "/@latest") +} + +func (q *Queryer) GetVersion(module string, version string) libyear.Info { + if !strings.HasPrefix(version, "v") { + return libyear.Info{ + Version: version, + } + } + i := q.makeProxyRequest(module + "/@v/" + version + ".info") + if i.Version == "" { + i.Version = version + } + return i +} + +// given a module, return the "latest" major version of that module, fully defined +// i.e. github.com/foo/bar => github.com/foo/bar/v2 in the case of one version bump +// github.com/foo/baz/v2 => github.com/foo/baz/v6 in the case of a large jump +// github.com/foo/quuz/v2 => github.com/foo/quuz/v2 in the case of no jump at all! +func (q *Queryer) findLatestMajorVersion(module string) string { + for { + nextModule := incrementMajorVersion(module) + info := q.makeProxyRequest(nextModule + "/@latest") + if info.Version == "" { + return module + } + module = nextModule + } +} + +func (q *Queryer) makeProxyRequest(mod string) libyear.Info { + log.Printf("makeProxyRequest for https://proxy.golang.org/%s", mod) + if q.Root == "" { + q.Root = "https://proxy.golang.org/" + } + + u, _ := url.Parse(q.Root) + u.Path = mod + + i := q.Cache.Get(u.String()) + if i.Version != "" { + log.Printf("cache hit for https://proxy.golang.org/%s", mod) + return i + } + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return i + } + req.Header.Add("Disable-Module-Fetch", "true") + resp, err := q.Client.Do(req) + if err != nil || resp.StatusCode != 200 { + return i + } + b, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(b, &i) + q.Cache.Set(u.String(), i) + return i +} + +// parses and attempts to add an MVS version to a module +func incrementMajorVersion(mod string) string { + exp := strings.Split(mod, "/") + var module string + if exp[len(exp)-1][0] == 'v' { + n, err := strconv.Atoi(exp[len(exp)-1][1:]) + if err != nil { + module = mod + "/v2" + } else { + module = strings.Join(exp[:len(exp)-1], "/") + "/v" + fmt.Sprint(n+1) + } + } else { + module = mod + "/v2" + } + return module +} diff --git a/pkg/libyear/libyear.go b/pkg/libyear/libyear.go new file mode 100644 index 0000000..aa94fd2 --- /dev/null +++ b/pkg/libyear/libyear.go @@ -0,0 +1,22 @@ +package libyear + +import "time" + +type Info struct { + Version string // version string + Time time.Time // commit time +} + +type Pair struct { + Name string + Latest Info + Current Info +} + +func Calc(p ...Pair) time.Duration { + sum := time.Duration(0) + for i := range p { + sum = sum + p[i].Latest.Time.Sub(p[i].Current.Time) + } + return sum +}