432 lines
9.4 KiB
Go
432 lines
9.4 KiB
Go
package gomod
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/briandowns/spinner"
|
|
"github.com/fatih/color"
|
|
"github.com/google/go-github/v64/github"
|
|
"github.com/olekukonko/tablewriter"
|
|
"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,
|
|
}
|
|
|
|
whiteList = []string{"golang.org"}
|
|
)
|
|
|
|
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 GetModFile(mf string) (*modfile.File, error) {
|
|
modFileData, err := os.ReadFile(mf)
|
|
if err != nil {
|
|
logrus.Errorf("read go.mod file failed: %v", err)
|
|
return nil, err
|
|
}
|
|
modFile, err := modfile.Parse("go.mod", modFileData, nil)
|
|
if err != nil {
|
|
logrus.Errorf("parse go.mod file failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return modFile, nil
|
|
}
|
|
|
|
func ModUpgrade(upgradeIndirect bool) {
|
|
modFile, err := GetModFile("go.mod")
|
|
if err != nil {
|
|
logrus.Errorf("get 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")
|
|
}
|
|
|
|
type Module struct {
|
|
Path string // module path
|
|
Query string // version query corresponding to this version
|
|
Version string // module version
|
|
Versions []string // available module versions
|
|
Replace *Module // replaced by this module
|
|
Time *time.Time // time version was created
|
|
Update *Module // available update (with -u)
|
|
Main bool // is this the main module?
|
|
Indirect bool // module is only indirectly needed by main module
|
|
Dir string // directory holding local copy of files, if any
|
|
GoMod string // path to go.mod file describing module, if any
|
|
GoVersion string // go version used in module
|
|
Retracted []string // retraction information, if any (with -retracted or -u)
|
|
Deprecated string // deprecation message, if any (with -u)
|
|
Error *ModuleError // error loading module
|
|
Origin any // provenance of module
|
|
Reuse bool // reuse of old module info is safe
|
|
}
|
|
|
|
type ModuleError struct {
|
|
Err string // the error itself
|
|
}
|
|
|
|
func Analyzed() {
|
|
spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
|
spin.Suffix = " Analyzing for dependencies..."
|
|
spin.Start()
|
|
|
|
modules, err := analyzed("go", "list", "-m", "-json", "-mod=readonly", "all")
|
|
if err != nil {
|
|
spin.Stop()
|
|
logrus.Errorf("analyzed project dependencies failed: %v", err)
|
|
return
|
|
}
|
|
|
|
spin.Stop()
|
|
|
|
var tableRows [][]string
|
|
for _, mod := range modules {
|
|
tableRows = append(tableRows, []string{
|
|
mod.Path,
|
|
getRelation(mod),
|
|
mod.Version,
|
|
getGoVersion(mod),
|
|
getToolChains(mod),
|
|
})
|
|
}
|
|
|
|
if len(tableRows) == 0 {
|
|
awesome := color.New(color.FgHiGreen, color.Bold).Sprint("✔ Empty!")
|
|
fmt.Printf(" %s All of your dependencies are empty.\n", awesome)
|
|
} else {
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetHeader([]string{"Package", "Relation", "Version", "GoVersion", "ToolChains"})
|
|
table.EnableBorder(true)
|
|
table.AppendBulk(tableRows)
|
|
table.Render()
|
|
}
|
|
}
|
|
|
|
func getGoVersion(m *Module) string {
|
|
if m.GoVersion != "" {
|
|
return m.GoVersion
|
|
}
|
|
var mf string
|
|
if m.GoMod != "" {
|
|
mf = m.GoMod
|
|
} else if m.Dir != "" {
|
|
mf = filepath.Join(m.Dir, "go.mod")
|
|
}
|
|
if mf == "" {
|
|
return ""
|
|
}
|
|
modFile, err := GetModFile(mf)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if modFile.Go != nil {
|
|
return modFile.Go.Version
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getRelation(m *Module) string {
|
|
if m.Main {
|
|
return "main"
|
|
}
|
|
if m.Indirect {
|
|
return "indirect"
|
|
}
|
|
return "direct"
|
|
}
|
|
|
|
func getToolChains(m *Module) string {
|
|
var mf string
|
|
if m.GoMod != "" {
|
|
mf = m.GoMod
|
|
} else if m.Dir != "" {
|
|
mf = filepath.Join(m.Dir, "go.mod")
|
|
}
|
|
if mf == "" {
|
|
return ""
|
|
}
|
|
file, err := GetModFile(mf)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if file.Toolchain == nil {
|
|
return ""
|
|
}
|
|
return file.Toolchain.Name
|
|
}
|
|
|
|
func analyzed(cmd string, args ...string) ([]*Module, error) {
|
|
execBuf, err := execute(cmd, args...)
|
|
if err != nil {
|
|
logrus.Errorf("go list failed: %s, return: %v", execBuf, err)
|
|
return nil, err
|
|
}
|
|
var modules []*Module
|
|
var buf bytes.Buffer
|
|
var depth int32
|
|
for _, ch := range execBuf {
|
|
switch ch {
|
|
case '{':
|
|
depth++
|
|
buf.WriteByte(ch)
|
|
case '}':
|
|
depth--
|
|
if depth == 0 {
|
|
buf.WriteByte(ch)
|
|
var m = new(Module)
|
|
if err := json.Unmarshal(buf.Bytes(), m); err != nil {
|
|
return nil, err
|
|
}
|
|
buf.Reset()
|
|
modules = append(modules, m)
|
|
} else {
|
|
buf.WriteByte(ch)
|
|
}
|
|
default:
|
|
buf.WriteByte(ch)
|
|
}
|
|
}
|
|
return modules, nil
|
|
}
|
|
|
|
func execute(command string, args ...string) ([]byte, error) {
|
|
cmd := exec.Command(command, args...)
|
|
return cmd.CombinedOutput()
|
|
}
|
|
|
|
func UpdateList() {
|
|
spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
|
spin.Suffix = " Checking for updates..."
|
|
spin.Start()
|
|
|
|
modules, err := analyzed("go", "list", "-m", "-u", "-json", "-mod=readonly", "all")
|
|
if err != nil {
|
|
spin.Stop()
|
|
logrus.Errorf("analyzed project dependencies failed: %v", err)
|
|
return
|
|
}
|
|
|
|
spin.Stop()
|
|
|
|
var tableRows [][]string
|
|
for _, mod := range modules {
|
|
if mod.Update == nil {
|
|
continue
|
|
}
|
|
tableRows = append(tableRows, []string{
|
|
mod.Path,
|
|
getRelation(mod),
|
|
mod.Version,
|
|
mod.Update.Version,
|
|
})
|
|
}
|
|
|
|
if len(tableRows) == 0 {
|
|
awesome := color.New(color.FgHiGreen, color.Bold).Sprint("✔ Empty!")
|
|
fmt.Printf(" %s All of your dependencies are empty.\n", awesome)
|
|
} else {
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetHeader([]string{"Package", "Relation", "Current", "Latest"})
|
|
table.EnableBorder(true)
|
|
table.AppendBulk(tableRows)
|
|
table.Render()
|
|
}
|
|
}
|