commit 72c9cc2f15a378c243ef6d9a5a3efc58ec5bbf9f Author: Young Xu Date: Wed May 24 23:40:41 2023 +0800 first commit docs: readme fix: upgrade with commit id feat: log to logrus upgrade: lormatter feat: strong search go module version fix: github repo no version fix: ignore google.golang.org upgrade: github api first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5430313 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +## gomod + +> go.mod file manager + +### install + +```shell +go install gitter.top/apps/gomod/...@latest +``` + +### usage + +- [ ] `gomod` show go.mod available updates +- [x] `gomod u` upgrade go.mod +- [ ] `gomod a` analyzed project dependencies diff --git a/git-ls-remote.go b/git-ls-remote.go new file mode 100644 index 0000000..da42f39 --- /dev/null +++ b/git-ls-remote.go @@ -0,0 +1,76 @@ +package gomod + +import ( + "fmt" + "os/exec" + "regexp" + "strings" + + "github.com/sirupsen/logrus" +) + +var ( + lsRemote _lsRemote + + tagRegexp = regexp.MustCompile(`refs/tags/(.*)`) + headRegexp = regexp.MustCompile(`refs/heads/(master|main)`) +) + +type _lsRemote struct { + url string + output string +} + +func (lr *_lsRemote) setUrl(url string) *_lsRemote { + url = strings.ReplaceAll(url, "https://", "") + url = strings.ReplaceAll(url, "http://", "") + if v := strings.Split(url, "/"); len(v) > 3 { + url = strings.Join(v[:3], "/") + } + if !strings.HasPrefix(url, "https") { + url = "https://" + url + } + lr.url = url + return lr +} + +func (lr *_lsRemote) command() error { + cmd := exec.Command("git", "ls-remote", "--heads", "--tags", "--sort=v:refname", lr.url) + output, err := cmd.CombinedOutput() + if err != nil { + logrus.WithField("cmd", cmd.String()).Errorf("run command failed: %v", err) + return err + } + lr.output = string(output) + return nil +} + +func (lr *_lsRemote) tagOrCommitID() (string, error) { + if err := lr.command(); err != nil { + return "", err + } + lr.output = strings.Trim(lr.output, "\n ") + commits := strings.Split(lr.output, "\n") + if len(commits) == 0 { + return "", fmt.Errorf("get commit log empty") + } + commit := commits[len(commits)-1] + + if strings.Contains(commit, "refs/heads/") { + matches := headRegexp.FindStringSubmatch(commit) + if len(matches) != 2 { + return "", fmt.Errorf("%s no match rule: %s", commit, headRegexp.String()) + } + return strings.Trim(strings.ReplaceAll(commit, matches[0], ""), " \n\t\r"), nil + } + + if strings.Contains(commit, "refs/tags/") { + matches := tagRegexp.FindStringSubmatch(commit) + if len(matches) != 2 { + return "", fmt.Errorf("%s no match rule: %s", commit, tagRegexp.String()) + } + return matches[1], nil + } + + return "", fmt.Errorf("%s no match heads and tags rule", commit) +} diff --git a/git-ls-remote_test.go b/git-ls-remote_test.go new file mode 100644 index 0000000..b993047 --- /dev/null +++ b/git-ls-remote_test.go @@ -0,0 +1,17 @@ +package gomod + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalUrlGetTag(t *testing.T) { + output, err := lsRemote.setUrl("https://gitter.top/coco/bootstrap").tagOrCommitID() + assert.NoError(t, err) + t.Log(output) + + output, err = lsRemote.setUrl("https://github.com/spf13/cobra").tagOrCommitID() + assert.NoError(t, err) + t.Log(output) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cf80b76 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module gitter.top/apps/gomod + +go 1.20 + +require ( + github.com/google/go-github/v64 v64.0.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.9.0 + gitter.top/common/lormatter v0.0.1 + golang.org/x/mod v0.20.0 + golang.org/x/net v0.28.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.24.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..086b488 --- /dev/null +++ b/go.sum @@ -0,0 +1,40 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg= +github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gitter.top/common/lormatter v0.0.1 h1:RwNmDsgXl6gjU1KBgaVdDgG+dYDLjhM76TQu/C6bArY= +gitter.top/common/lormatter v0.0.1/go.mod h1:P3v4TVOF52RRh41UgcABmDWyK8ZRo6ekbG/uln8PH+w= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gomod.go b/gomod.go new file mode 100644 index 0000000..86e1409 --- /dev/null +++ b/gomod.go @@ -0,0 +1,222 @@ +package gomod + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strings" + "time" + + "github.com/google/go-github/v64/github" + "github.com/sirupsen/logrus" + "golang.org/x/mod/modfile" + "golang.org/x/net/html" +) + +const ( + defaultVersion = "latest" +) + +var ( + httpClient = &http.Client{ + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + Timeout: time.Second * 5, + } +) + +func goGet(u, v string) error { + versionUrl := u + "@" + v + cmd := exec.Command("go", "get", "-u", versionUrl) + if _, err := cmd.CombinedOutput(); err != nil { + return err + } + return nil +} + +type upgrade interface { + upgrade() error +} + +type githubRepo struct { + client *github.Client + url string + owner string + repo string + semV string +} + +func newGithubRepo(url string) *githubRepo { + return &githubRepo{ + client: github.NewClient(&http.Client{ + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + Timeout: time.Second * 3, + }), + url: url, + } +} + +func (g *githubRepo) parse() error { + ss := strings.Split(g.url, "/") + if len(ss) < 3 { + return fmt.Errorf("invalid github url: %s", g.url) + } + g.owner = ss[1] + g.repo = ss[2] + if len(ss) == 4 { + g.semV = ss[3] + } + + return nil +} + +func (g *githubRepo) getVersion() string { + ctx := context.Background() + release, _, err := g.client.Repositories.GetLatestRelease(ctx, g.owner, g.repo) + if err == nil { + return *release.TagName + } + // get latest commit id + commits, _, err := g.client.Repositories.ListCommits(ctx, g.owner, g.repo, &github.CommitsListOptions{ + ListOptions: github.ListOptions{ + Page: 0, + PerPage: 1, + }, + }) + if err != nil { + return defaultVersion + } + if len(commits) == 0 { + return defaultVersion + } + return (*commits[0].SHA)[:8] +} + +func (g *githubRepo) upgrade() error { + if err := g.parse(); err != nil { + return err + } + v := g.getVersion() + if err := goGet(g.url, v); err != nil { + return err + } + logrus.WithField("url", g.url+"@"+v).Infof("upgrade success") + return nil +} + +type repo struct { + dependency string + originRepo string +} + +func newRepo(dep string) *repo { + return &repo{ + dependency: dep, + } +} + +func (r *repo) extraGoImportMetadata(reader io.Reader) (string, error) { + tokenizer := html.NewTokenizer(reader) + for { + tt := tokenizer.Next() + switch tt { + case html.ErrorToken: + err := tokenizer.Err() + if errors.Is(err, io.EOF) { + return "", errors.New("go-import meta attr not found") + } + return "", errors.New("read dependency metadata failed: " + err.Error()) + case html.StartTagToken, html.SelfClosingTagToken: + t := tokenizer.Token() + if t.Data != "meta" { + continue + } + for i, attribute := range t.Attr { + if attribute.Key == "name" && attribute.Val == "go-import" && i+1 < len(t.Attr) { + return t.Attr[i+1].Val, nil + } + } + default: + continue + } + } +} + +func (r *repo) upgrade() error { + u := "https://" + r.dependency + "?go-get=1" + request, err := http.NewRequest("GET", u, nil) + if err != nil { + return err + } + response, err := httpClient.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + metadata, err := r.extraGoImportMetadata(response.Body) + if err != nil { + return err + } + ss := strings.Split(metadata, " ") + if len(ss) != 3 { + return errors.New("go-import metadata invalid: " + metadata) + } + r.originRepo = ss[2] + + v, err := lsRemote.setUrl(r.originRepo).tagOrCommitID() + if err != nil { + return err + } + if err := goGet(r.dependency, v); err != nil { + return err + } + logrus.WithField("url", r.dependency+"@"+v).Infof("upgrade success") + return nil +} + +func ModUpgrade(upgradeIndirect bool) { + modFileData, err := os.ReadFile("go.mod") + if err != nil { + logrus.Errorf("read go.mod file failed: %v", err) + return + } + modFile, err := modfile.Parse("go.mod", modFileData, nil) + if err != nil { + logrus.Errorf("parse go.mod file failed: %v", err) + return + } + for _, mod := range modFile.Require { + if mod.Indirect && !upgradeIndirect { + continue + } + var repo upgrade + if strings.Contains(mod.Mod.Path, "github.com") { + repo = newGithubRepo(mod.Mod.Path) + } else { + repo = newRepo(mod.Mod.Path) + } + if err := repo.upgrade(); err != nil { + logrus.WithField("url", mod.Mod.Path).Errorf("upgrade failed: %v. (starting fallback)", err) + fallback(mod.Mod.Path) + continue + } + } + + tidy() +} + +func tidy() { + cmd := exec.Command("go", "mod", "tidy") + _ = cmd.Run() +} + +func fallback(repo string) { + if err := goGet(repo, defaultVersion); err != nil { + logrus.WithField("url", repo+"@"+defaultVersion).Errorf("upgrade failed: %v", err) + return + } + logrus.WithField("url", repo+"@"+defaultVersion).Infof("upgrade success") +} diff --git a/gomod/main.go b/gomod/main.go new file mode 100644 index 0000000..ab117fb --- /dev/null +++ b/gomod/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "os" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gitter.top/common/lormatter" + + "gitter.top/apps/gomod" +) + +var ( + upgradeIndirect bool + mod *cobra.Command +) + +func init() { + formatter := &lormatter.Formatter{ShowField: true} + logrus.SetFormatter(formatter) + logrus.SetReportCaller(true) + + mod = &cobra.Command{ + Use: "gomod", + Short: "go mod manager", + Example: "gomod", + PreRun: func(cmd *cobra.Command, args []string) { + _, err := os.Stat("go.mod") + if os.IsNotExist(err) { + logrus.Fatalf("go.mod file not found on this directory") + } else if err != nil { + logrus.Fatalf("check go.mod file failed: %v", err) + } + }, + Run: func(cmd *cobra.Command, args []string) { + + }, + } + mod.AddCommand(upgrade()) + mod.AddCommand(analyzed()) +} + +func upgrade() *cobra.Command { + cmd := &cobra.Command{ + Use: "upgrade", + Short: "update project dependencies to latest", + Aliases: []string{"u"}, + Run: func(cmd *cobra.Command, args []string) { + gomod.ModUpgrade(upgradeIndirect) + }, + } + cmd.Flags().BoolVarP(&upgradeIndirect, "indirect", "i", false, "upgrade indirect dependency") + return cmd +} + +func analyzed() *cobra.Command { + return &cobra.Command{ + Use: "analyzed", + Short: "analyzed project dependencies", + Aliases: []string{"a"}, + Run: func(cmd *cobra.Command, args []string) { + + }, + } +} + +func main() { + if err := mod.Execute(); err != nil { + logrus.Errorf("execute command failed: %v", err) + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..c69581a --- /dev/null +++ b/utils.go @@ -0,0 +1,12 @@ +package gomod + +import "strings" + +func ElemIn(elems []string, target string) bool { + for _, elem := range elems { + if strings.Contains(target, elem) { + return true + } + } + return false +}