├── Makefile ├── README.md ├── example.gif ├── go.mod ├── go.sum ├── go_test.mod ├── main.go ├── misc.go ├── mod.go ├── version.go └── version_test.go /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | go build -ldflags "-X main.FileName=go_test.mod" && ./gomod-check 4 | rm ./gomod-check -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gomod-check 2 | Check go mod dependencies via terminal ui 3 | 4 | ![](https://raw.githubusercontent.com/brianvoe/gomod-check/master/example.gif) 5 | 6 | ## Installation 7 | ```bash 8 | go get github.com/brianvoe/gomod-check 9 | ``` 10 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianvoe/gomod-check/b735867a6165d44e36055b378f50d3b9c324da6d/example.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/brianvoe/gomod-check 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/abiosoft/ishell v2.0.0+incompatible 7 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect 8 | github.com/chzyer/logex v1.1.10 // indirect 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect 10 | github.com/fatih/color v1.9.0 11 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect 12 | github.com/stretchr/testify v1.5.1 // indirect 13 | golang.org/x/mod v0.2.0 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= 2 | github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= 3 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= 4 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= 5 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 6 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 9 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 12 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 13 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= 14 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= 15 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 16 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 17 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 18 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= 19 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 24 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 27 | golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= 28 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 29 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 30 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 31 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 34 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 36 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 38 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 39 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 40 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= 41 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 42 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 45 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 46 | -------------------------------------------------------------------------------- /go_test.mod: -------------------------------------------------------------------------------- 1 | module github.com/brianvoe/gomod-check 2 | 3 | go 1.13 4 | 5 | require ( 6 | cloud.google.com/go/storage v1.1.1 7 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect 8 | github.com/DATA-DOG/go-sqlmock v1.3.3 // indirect 9 | github.com/DataDog/datadog-go v2.3.0+incompatible 10 | github.com/PumpkinSeed/incrmntr v0.8.2 11 | github.com/PumpkinSeed/ptr v0.2.0 12 | github.com/PumpkinSeed/reflection v0.1.0 13 | github.com/brianvoe/devlog v0.2.1 14 | github.com/brianvoe/gofakeit/v4 v4.2.1 15 | github.com/brianvoe/sjwt v0.5.1 16 | github.com/couchbase/gocb v1.6.3 17 | github.com/dgraph-io/badger v1.6.0 18 | github.com/dgryski/go-farm v0.0.0-20191112170834-c2139c5d712b // indirect 19 | github.com/dutchcoders/goftp v0.0.0-20170301105846-ed59a591ce14 20 | github.com/ericlagergren/decimal v0.0.0-20190912144844-2c3e3e1ef942 // indirect 21 | github.com/fluidpay/dough v0.2.1 22 | github.com/friendsofgo/errors v0.9.2 23 | github.com/go-chi/chi v4.0.2+incompatible 24 | github.com/go-chi/cors v1.0.0 25 | github.com/go-redis/redis v6.15.6+incompatible 26 | github.com/go-sql-driver/mysql v1.4.1 27 | github.com/gocarina/gocsv v0.0.0-20190927101021-3ecffd272576 28 | github.com/gorilla/schema v1.1.0 29 | github.com/hashicorp/vault/api v1.0.4 30 | github.com/jarcoal/httpmock v1.0.4 31 | github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a 32 | github.com/jmoiron/sqlx v1.2.0 33 | github.com/liip/sheriff v0.0.0-20190308094614-91aa83a45a3d 34 | github.com/onsi/ginkgo v1.10.3 // indirect 35 | github.com/onsi/gomega v1.7.1 // indirect 36 | github.com/patrickmn/go-cache v2.1.0+incompatible 37 | github.com/pelletier/go-toml v1.6.0 // indirect 38 | github.com/pkg/errors v0.8.1 39 | github.com/pkg/sftp v1.10.1 40 | github.com/pquerna/otp v1.2.0 41 | github.com/robfig/cron/v3 v3.0.0 42 | github.com/rs/xid v1.2.1 43 | github.com/segmentio/ksuid v1.0.2 44 | github.com/sendgrid/rest v2.4.1+incompatible // indirect 45 | github.com/sendgrid/sendgrid-go v3.5.0+incompatible 46 | github.com/spf13/afero v1.2.2 // indirect 47 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 48 | github.com/spf13/pflag v1.0.5 // indirect 49 | github.com/spf13/viper v1.5.0 50 | github.com/valyala/fasttemplate v1.1.0 51 | github.com/volatiletech/null v8.0.0+incompatible 52 | github.com/volatiletech/sqlboiler v3.6.0+incompatible 53 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 54 | golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 55 | golang.org/x/sys v0.0.0-20191120130536-6bfc516c8699 // indirect 56 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect 57 | google.golang.org/api v0.11.0 58 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 59 | gopkg.in/couchbase/gocbcore.v7 v7.1.14 // indirect 60 | gopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4 // indirect 61 | gopkg.in/couchbaselabs/gojcbmock.v1 v1.0.3 // indirect 62 | gopkg.in/yaml.v2 v2.2.5 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "sort" 7 | 8 | "github.com/abiosoft/ishell" 9 | "github.com/fatih/color" 10 | 11 | "golang.org/x/mod/modfile" 12 | ) 13 | 14 | // FileName is the main file used for parsing 15 | var FileName = "go.mod" 16 | 17 | func main() { 18 | // Read in go mod file 19 | dat, err := ioutil.ReadFile("./" + FileName) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // Pares file 25 | file, err := modfile.Parse(FileName, dat, nil) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | // Colors 31 | red := color.New(color.FgRed).SprintFunc() 32 | yellow := color.New(color.FgYellow).SprintFunc() 33 | green := color.New(color.FgGreen).SprintFunc() 34 | blue := color.New(color.FgBlue).SprintFunc() 35 | 36 | var mods Mods 37 | urlLength := 0 38 | curVersionLength := 0 39 | latestVersionLength := 0 40 | 41 | // Loop through required mods and exclude indirect ones 42 | for _, r := range file.Require { 43 | if r.Indirect { 44 | continue 45 | } 46 | 47 | // Check if mod is on the most current version 48 | mod, err := NewMod(r.Mod.Path, r.Mod.Version) 49 | if err != nil { 50 | continue 51 | } 52 | 53 | // Do nothing if mod is current 54 | if mod.Status == "current" { 55 | continue 56 | } 57 | 58 | // Check url length for option padding 59 | if urlLength < len(mod.Path) { 60 | urlLength = len(mod.Path) 61 | } 62 | if curVersionLength < len(mod.CurrentVersion.cleanString()) { 63 | curVersionLength = len(mod.CurrentVersion.cleanString()) 64 | } 65 | if latestVersionLength < len(mod.AvailableVersions[0].cleanString()) { 66 | latestVersionLength = len(mod.AvailableVersions[0].cleanString()) 67 | } 68 | 69 | mods = append(mods, mod) 70 | } 71 | 72 | // Sort mods by status 73 | sort.Sort(mods) 74 | 75 | // Create options 76 | var options []string 77 | hasIncompatible := false 78 | for _, m := range mods { 79 | urlStr := strPadding(m.Path, urlLength) + " " 80 | versionStr := strPadding(m.CurrentVersion.cleanString(), curVersionLength) + " -> " + strPadding(m.AvailableVersions[0].cleanString(), latestVersionLength) 81 | if m.AvailableVersions[0].incompatible { 82 | versionStr += " " + blue("I") 83 | hasIncompatible = true 84 | } 85 | if m.Status == "major" { 86 | options = append(options, red(urlStr)+versionStr) 87 | } else if m.Status == "minor" { 88 | options = append(options, yellow(urlStr)+versionStr) 89 | } else if m.Status == "patch" { 90 | options = append(options, green(urlStr)+versionStr) 91 | } 92 | } 93 | 94 | if len(options) == 0 { 95 | fmt.Println(green("You are all up to date!!!")) 96 | return 97 | } 98 | 99 | shell := ishell.New() 100 | 101 | shell.AddCmd(&ishell.Cmd{ 102 | Name: "checklist", 103 | Help: "checklist prompt", 104 | Func: func(c *ishell.Context) { 105 | text := "Hit space to select modules you want to update. Ctrl + c to cancel\n" 106 | text += green("Patch") + " " + yellow("Minor") + " " + red("Major") + " " 107 | if hasIncompatible { 108 | text += blue("I = Incompatible") 109 | } 110 | choices := c.Checklist(options, text, nil) 111 | 112 | if len(choices) > 0 && choices[0] != -1 { 113 | c.ClearScreen() 114 | c.Println(green("Modules that were updated!!!")) 115 | for _, i := range choices { 116 | err := file.AddRequire(mods[i].Path, mods[i].AvailableVersions[0].original) 117 | if err != nil { 118 | c.Err(err) 119 | } 120 | c.Println(options[i]) 121 | } 122 | file.Cleanup() 123 | dat, err := file.Format() 124 | if err != nil { 125 | c.Err(err) 126 | } 127 | 128 | // Write back to file 129 | err = ioutil.WriteFile("./"+FileName, dat, 644) 130 | if err != nil { 131 | c.Err(err) 132 | } 133 | } 134 | 135 | shell.Close() 136 | }, 137 | }) 138 | 139 | shell.Process("checklist") 140 | shell.Run() 141 | } 142 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func strPadding(str string, length int) string { 4 | curLength := len(str) 5 | if curLength == length { 6 | return str 7 | } 8 | 9 | difference := length - curLength 10 | for i := 0; i < difference; i++ { 11 | str += " " 12 | } 13 | 14 | return str 15 | } 16 | -------------------------------------------------------------------------------- /mod.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "errors" 4 | 5 | // Mods is an alias to an array of mods 6 | type Mods []*Mod 7 | 8 | // Mod is the main struct containing path, current version and possible available versions 9 | type Mod struct { 10 | Path string 11 | CurrentVersion *version 12 | AvailableVersions versions 13 | Status string 14 | } 15 | 16 | func (v Mods) Len() int { return len(v) } 17 | func (v Mods) Less(i, j int) bool { return v[i].compare(v[j]) > 0 } 18 | func (v Mods) Swap(i, j int) { v[i], v[j] = v[j], v[i] } 19 | 20 | func (m *Mod) compare(m2 *Mod) int { 21 | if statusInt(m.Status) < statusInt(m2.Status) { 22 | return -1 23 | } 24 | if statusInt(m.Status) > statusInt(m2.Status) { 25 | return 1 26 | } 27 | 28 | return 0 29 | } 30 | 31 | func statusInt(status string) int { 32 | if status == "major" { 33 | return 0 34 | } else if status == "minor" { 35 | return 1 36 | } else if status == "patch" { 37 | return 2 38 | } 39 | 40 | return 0 41 | } 42 | 43 | // NewMod will parse the current version and get all possible versions for this package 44 | func NewMod(path string, version string) (*Mod, error) { 45 | // Parse current version 46 | current, err := parseVersion(version) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // Get all available versions 52 | vs := getProxyVersions(path, true) 53 | 54 | // Lets make sure their is a latest version to get 55 | if len(vs) == 0 { 56 | return nil, errors.New("No latest version") 57 | } 58 | 59 | latest := vs[0] 60 | 61 | // Check if current version is up to date 62 | status := "" 63 | compare := current.compare(latest) 64 | if compare >= 0 { 65 | status = "current" 66 | } else if current.major < latest.major { 67 | status = "major" 68 | } else if current.minor < latest.minor { 69 | status = "minor" 70 | } else if current.patch < latest.patch { 71 | status = "patch" 72 | } 73 | 74 | return &Mod{ 75 | Path: path, 76 | CurrentVersion: current, 77 | AvailableVersions: vs, 78 | Status: status, 79 | }, nil 80 | } 81 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "regexp" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | var allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789" 15 | var versionRegex = regexp.MustCompile(`^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?$`) 16 | 17 | type versions []*version 18 | 19 | func (v versions) Len() int { return len(v) } 20 | func (v versions) Less(i, j int) bool { return v[i].compare(v[j]) > 0 } 21 | func (v versions) Swap(i, j int) { v[i], v[j] = v[j], v[i] } 22 | 23 | // version represents a single semantic version. 24 | type version struct { 25 | major, minor, patch uint64 26 | prerelease string 27 | metadata string 28 | incompatible bool 29 | original string 30 | } 31 | 32 | func (v *version) cleanString() string { 33 | return fmt.Sprintf("v%d.%d.%d", v.major, v.minor, v.patch) 34 | } 35 | 36 | func getProxyVersions(url string, skipPrelease bool) versions { 37 | resp, err := http.Get(fmt.Sprintf("https://proxy.golang.org/%s/@v/list", url)) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | defer resp.Body.Close() 43 | 44 | body, err := ioutil.ReadAll(resp.Body) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | var vs versions 50 | versionsString := strings.Split(string(body), "\n") 51 | for _, vss := range versionsString { 52 | v, err := parseVersion(vss) 53 | if err != nil { 54 | // If has error parsing skip it 55 | continue 56 | } 57 | if skipPrelease && v.prerelease != "" { 58 | continue 59 | } 60 | vs = append(vs, v) 61 | } 62 | sort.Sort(vs) 63 | 64 | return vs 65 | } 66 | 67 | // parseVersion parses a given version mod 68 | func parseVersion(v string) (*version, error) { 69 | m := versionRegex.FindStringSubmatch(v) 70 | if m == nil { 71 | return nil, errors.New("Invalid Semantic Version") 72 | } 73 | 74 | var err error 75 | 76 | // Regex breakdown 77 | major := m[1] 78 | minor := m[2] 79 | patch := m[3] 80 | prerelease := m[5] 81 | metadata := m[8] 82 | 83 | sv := &version{ 84 | prerelease: prerelease, 85 | metadata: metadata, 86 | original: v, 87 | incompatible: strings.Contains(v, "incompatible"), 88 | } 89 | 90 | // Major check 91 | sv.major, err = strconv.ParseUint(major, 10, 64) 92 | if err != nil { 93 | return nil, fmt.Errorf("Error parsing version segment: %s", err) 94 | } 95 | 96 | // Minor check 97 | if minor != "" { 98 | sv.minor, err = strconv.ParseUint(strings.TrimPrefix(minor, "."), 10, 64) 99 | if err != nil { 100 | return nil, fmt.Errorf("Error parsing version segment: %s", err) 101 | } 102 | } else { 103 | sv.minor = 0 104 | } 105 | 106 | // Patch check 107 | if patch != "" { 108 | sv.patch, err = strconv.ParseUint(strings.TrimPrefix(patch, "."), 10, 64) 109 | if err != nil { 110 | return nil, fmt.Errorf("Error parsing version segment: %s", err) 111 | } 112 | } else { 113 | sv.patch = 0 114 | } 115 | 116 | // Prerelease check 117 | if sv.prerelease != "" { 118 | if err = validatePrerelease(sv.prerelease); err != nil { 119 | return nil, err 120 | } 121 | } 122 | 123 | // Metadata check 124 | if sv.metadata != "" { 125 | if err = validateMetadata(sv.metadata); err != nil { 126 | return nil, err 127 | } 128 | } 129 | 130 | return sv, nil 131 | } 132 | 133 | func (v *version) compare(v2 *version) int { 134 | if d := compareSegment(v.major, v2.major); d != 0 { 135 | return d 136 | } 137 | if d := compareSegment(v.minor, v2.minor); d != 0 { 138 | return d 139 | } 140 | if d := compareSegment(v.patch, v2.patch); d != 0 { 141 | return d 142 | } 143 | 144 | // Major, minor, and patch are the same lets check prerelease 145 | if v.prerelease == "" && v2.prerelease == "" { 146 | return 0 147 | } 148 | if v.prerelease == "" { 149 | return 1 150 | } 151 | if v2.prerelease == "" { 152 | return -1 153 | } 154 | 155 | return 0 156 | } 157 | 158 | func compareSegment(v, o uint64) int { 159 | if v < o { 160 | return -1 161 | } 162 | if v > o { 163 | return 1 164 | } 165 | 166 | return 0 167 | } 168 | 169 | // Like strings.ContainsAny but does an only instead of any. 170 | func containsOnly(s string, comp string) bool { 171 | return strings.IndexFunc(s, func(r rune) bool { 172 | return !strings.ContainsRune(comp, r) 173 | }) == -1 174 | } 175 | 176 | // validatePrerelease loops through values to check for valid characters 177 | func validatePrerelease(p string) error { 178 | eparts := strings.Split(p, ".") 179 | for _, p := range eparts { 180 | if containsOnly(p, "0123456789") { 181 | if len(p) > 1 && p[0] == '0' { 182 | return errors.New("Version segment starts with 0") 183 | } 184 | } else if !containsOnly(p, allowed) { 185 | return errors.New("Invalid Prerelease string") 186 | } 187 | } 188 | return nil 189 | } 190 | 191 | // validateMetadata loops through values to check for valid characters 192 | func validateMetadata(m string) error { 193 | eparts := strings.Split(m, ".") 194 | for _, p := range eparts { 195 | if !containsOnly(p, allowed) { 196 | return errors.New("Invalid Metadata string") 197 | } 198 | } 199 | return nil 200 | } 201 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | ) 7 | 8 | func TestParseVersion(t *testing.T) { 9 | v, err := parseVersion("v3.2.1") 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | 14 | if v.major != 3 { 15 | t.Fatal("major doesnt equal 3") 16 | } 17 | 18 | if v.minor != 2 { 19 | t.Fatal("minor doesnt equal 2") 20 | } 21 | 22 | if v.patch != 1 { 23 | t.Fatal("patch doesnt equal 1") 24 | } 25 | } 26 | 27 | func TestSortVersion(t *testing.T) { 28 | vs := []string{"v4.2.1", "v0.5.7", "v2.15.99"} 29 | 30 | var versionsArray versions 31 | for _, v := range vs { 32 | ver, err := parseVersion(v) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | versionsArray = append(versionsArray, ver) 37 | } 38 | 39 | sort.Sort(versionsArray) 40 | 41 | if versionsArray[0].original != "v0.5.7" { 42 | t.Fatal("0 Not in order got:", versionsArray[0].original) 43 | } 44 | if versionsArray[1].original != "v2.15.99" { 45 | t.Fatal("1 Not in order got:", versionsArray[1].original) 46 | } 47 | if versionsArray[2].original != "v4.2.1" { 48 | t.Fatal("2 Not in order got:", versionsArray[2].original) 49 | } 50 | } 51 | --------------------------------------------------------------------------------