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
This commit is contained in:
Young Xu 2023-05-24 23:40:41 +08:00
commit 72c9cc2f15
Signed by: xuthus5
GPG Key ID: A23CF9620CBB55F9
9 changed files with 477 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

15
README.md Normal file
View File

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

76
git-ls-remote.go Normal file
View File

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

17
git-ls-remote_test.go Normal file
View File

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

23
go.mod Normal file
View File

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

40
go.sum Normal file
View File

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

222
gomod.go Normal file
View File

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

71
gomod/main.go Normal file
View File

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

12
utils.go Normal file
View File

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