more functionality

This commit is contained in:
David 2021-04-10 22:14:45 -04:00
parent ece8baeaac
commit 91ff1a10d4
7 changed files with 200 additions and 15 deletions

45
main.go
View File

@ -1,23 +1,56 @@
package main
import (
"flag"
"fmt"
"log"
"time"
"os"
"git.yetaga.in/deltamualpha/libyear/pkg/gomod"
"git.yetaga.in/deltamualpha/libyear/pkg/libyear"
"git.yetaga.in/deltamualpha/libyear/pkg/logger"
)
func main() {
pairs, err := gomod.LoadAndComputePairs("go.mod")
quiet := flag.Bool("q", false, "quiet; reduces logging and outputs only out-of-date dependencies")
verbose := flag.Bool("v", false, "verbose; outputs diagnostic information")
indirect := flag.Bool("indirect", false, "include indirect dependencies in calculation (off by default)")
flag.Parse()
l := logger.NewLogger(*quiet, *verbose, os.Stdout, os.Stderr)
g := gomod.GoMod{
IncludeIndirect: *indirect,
ProxyLoader: gomod.Queryer{Logger: l},
Logger: l,
}
pairs, err := g.LoadAndComputePairs("go.mod")
if err != nil {
log.Fatal(err)
l.Error(err)
os.Exit(255)
}
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)
if pairs[dep].Current.Time == pairs[dep].Latest.Time {
if !*quiet {
fmt.Printf(
"%s: %s year(s) (up to date: %s)\n",
pairs[dep].Name,
libyear.DecimalYear(libyear.Calc(pairs[dep])),
pairs[dep].Latest.Version,
)
}
} else {
fmt.Printf(
"%s: %s year(s) (current: %s, newest: %s)\n",
pairs[dep].Name,
libyear.DecimalYear(libyear.Calc(pairs[dep])),
pairs[dep].Current.Version,
pairs[dep].Latest.Version,
)
}
}
fmt.Printf("total libyear count: %d years\n", libyear.Calc(pairs...).Truncate(time.Hour)/time.Hour/8760)
fmt.Printf("total libyear count: %s year(s)\n", libyear.DecimalYear(libyear.Calc(pairs...)))
}

View File

@ -7,7 +7,27 @@ import (
"golang.org/x/mod/modfile"
)
func LoadAndComputePairs(filename string) ([]libyear.Pair, error) {
type logger interface {
Logf(f string, s ...interface{})
Debugf(f string, s ...interface{})
}
type GoMod struct {
IncludeIndirect bool
ProxyLoader Queryer
Logger logger
}
func isReplaced(module string, replaces []*modfile.Replace) bool {
for i := range replaces {
if module == replaces[i].Old.Path {
return true
}
}
return false
}
func (g *GoMod) LoadAndComputePairs(filename string) ([]libyear.Pair, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
@ -16,14 +36,20 @@ func LoadAndComputePairs(filename string) ([]libyear.Pair, error) {
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)
if isReplaced(f.Require[v].Mod.Path, f.Replace) {
g.Logger.Logf("%s is replaced, skipping...\n", f.Require[v].Mod.Path)
continue
}
if !g.IncludeIndirect && f.Require[v].Indirect {
g.Logger.Logf("%s is indirect, skipping...\n", f.Require[v].Mod.Path)
continue
}
latest := g.ProxyLoader.GetLatestVersion(f.Require[v].Mod.Path)
current := g.ProxyLoader.GetVersion(f.Require[v].Mod.Path, f.Require[v].Mod.Version)
pairs = append(pairs, libyear.Pair{
Name: f.Require[v].Mod.Path,
Current: current,

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
@ -18,6 +17,7 @@ type Queryer struct {
Root string
Client http.Client
Cache cache.Cache
Logger logger
}
type majorVersion struct {
@ -59,17 +59,17 @@ func (q *Queryer) findLatestMajorVersion(module string) string {
}
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/"
}
q.Logger.Debugf("makeProxyRequest for %s/%s\n", q.Root, mod)
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)
q.Logger.Debugf("cache hit for https://proxy.golang.org/%s\n", mod)
return i
}
req, err := http.NewRequest("GET", u.String(), nil)

View File

@ -1,6 +1,9 @@
package libyear
import "time"
import (
"fmt"
"time"
)
type Info struct {
Version string // version string
@ -13,6 +16,8 @@ type Pair struct {
Current Info
}
// TODO: sum can only represent ~290 years before overflowing, but we don't actually need nanosecond precision!
// Probably worth switching to hour-based summing here.
func Calc(p ...Pair) time.Duration {
sum := time.Duration(0)
for i := range p {
@ -20,3 +25,7 @@ func Calc(p ...Pair) time.Duration {
}
return sum
}
func DecimalYear(d time.Duration) string {
return fmt.Sprintf("%.2f", float64(d.Truncate(time.Hour)/time.Hour)/float64(8760))
}

View File

@ -0,0 +1,23 @@
package libyear
import (
"testing"
"time"
)
func TestDecimalYear(t *testing.T) {
values := map[time.Duration]string{
time.Hour: "0.00",
time.Hour * 8760: "1.00",
time.Hour * 4380: "0.50",
time.Hour * 12264: "1.40",
time.Hour * 27520: "3.14",
time.Hour * 1000000: "114.16",
}
for d, s := range values {
if r := DecimalYear(d); r != s {
t.Logf("Expected %s, got %s for %d", s, r, d)
t.Fail()
}
}
}

61
pkg/logger/logger.go Normal file
View File

@ -0,0 +1,61 @@
package logger
import (
"fmt"
"io"
"os"
)
type Log struct {
quiet bool
debug bool
errOut io.Writer
stdOut io.Writer
}
func NewLogger(quiet, debug bool, stdOut, errOut io.Writer) *Log {
if stdOut == nil {
stdOut = os.Stdout
}
if errOut == nil {
errOut = os.Stderr
}
return &Log{
quiet: quiet,
debug: debug,
stdOut: stdOut,
errOut: errOut,
}
}
func (l *Log) Error(s ...interface{}) {
fmt.Fprint(l.errOut, s...)
}
func (l *Log) Errorf(f string, s ...interface{}) {
fmt.Fprint(l.errOut, s...)
}
func (l *Log) Log(s ...interface{}) {
if !l.quiet {
fmt.Fprint(l.stdOut, s...)
}
}
func (l *Log) Logf(f string, s ...interface{}) {
if !l.quiet {
fmt.Fprintf(l.stdOut, f, s...)
}
}
func (l *Log) Debug(s ...interface{}) {
if l.debug {
fmt.Fprint(l.errOut, s...)
}
}
func (l *Log) Debugf(f string, s ...interface{}) {
if l.debug {
fmt.Fprintf(l.errOut, f, s...)
}
}

33
readme.md Normal file
View File

@ -0,0 +1,33 @@
# libyear
A **simple** measure of software dependency freshness. It is a **single number** telling you how up-to-date your dependencies are.
[libyear.com](https://libyear.com/)
## Usage
`libyear` only supports projects using go modules. It ignores dependencies that are replaced in a go.mod file.
`-q`: quiet mode; reduce logging and output only dependencies that are not at their latest version
`-v`: verbose mode; output diagnostic information
`-indirect`: include dependencies marked `// indirect` in `go.mod` in the calculation (excluded by default)
## Example output
When run in a directory that contains a `go.mod` file:
```bash
$ libyear
github.com/pkg/errors: 1.03 year(s) (current: v0.8.1, newest: v0.9.1)
github.com/stretchr/testify: 1.51 year(s) (current: v1.4.0, newest: v1.7.0)
go.uber.org/atomic: 0.00 year(s) (up to date: v1.7.0)
go.uber.org/multierr: 0.00 year(s) (up to date: v1.6.0)
gopkg.in/yaml.v2: 2.01 year(s) (current: v2.2.2, newest: v2.4.0)
total libyear count: 4.55 year(s)
```
## References
J. Cox, E. Bouwers, M. van Eekelen and J. Visser, Measuring Dependency Freshness in Software Systems. In Proceedings of the 37th International Conference on Software Engineering (ICSE 2015), May 2015 <https://ericbouwers.github.io/papers/icse15.pdf>