more functionality

master
David Ashby 2 years ago
parent ece8baeaac
commit 91ff1a10d4

@ -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...)))
}

@ -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,

@ -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)

@ -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))
}

@ -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()
}
}
}

@ -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...)
}
}

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