├── .gitignore ├── demo ├── demo0.png ├── demo1.png └── demo2.png ├── cmd_test.go ├── cmd.go ├── colors.go ├── commits.go ├── imports.go ├── vcs.go ├── package.go ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | gofresh 2 | *.swp 3 | *.exe -------------------------------------------------------------------------------- /demo/demo0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divan/gofresh/HEAD/demo/demo0.png -------------------------------------------------------------------------------- /demo/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divan/gofresh/HEAD/demo/demo1.png -------------------------------------------------------------------------------- /demo/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divan/gofresh/HEAD/demo/demo2.png -------------------------------------------------------------------------------- /cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestCmd(t *testing.T) { 6 | data := []byte(`line1 7 | line2 8 | line3`) 9 | 10 | lines := bytes2strings(data) 11 | if len(lines) != 3 { 12 | t.Fatal("Expecting 3 lines, but have", len(lines)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | ) 7 | 8 | func Run(dir, command string, args ...string) ([]string, error) { 9 | cmd := exec.Command(command, args...) 10 | cmd.Dir = dir 11 | out, err := cmd.Output() 12 | if err != nil { 13 | return nil, err 14 | } 15 | return bytes2strings(out), nil 16 | } 17 | 18 | func bytes2strings(data []byte) []string { 19 | isNewline := func(r rune) bool { 20 | return r == '\n' 21 | } 22 | return strings.FieldsFunc(string(data), isNewline) 23 | } 24 | -------------------------------------------------------------------------------- /colors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fatih/color" 5 | "github.com/mattn/go-colorable" 6 | ) 7 | 8 | var ( 9 | stdout = colorable.NewColorableStdout() 10 | 11 | bold = color.New(color.FgWhite).Add(color.Bold).SprintfFunc() 12 | red = color.New(color.FgRed).SprintfFunc() 13 | redBold = color.New(color.FgRed).Add(color.Bold).SprintfFunc() 14 | green = color.New(color.FgGreen).Add(color.Bold).SprintfFunc() 15 | cyan = color.New(color.FgCyan).SprintfFunc() 16 | yellow = color.New(color.FgYellow).Add(color.Bold).SprintfFunc() 17 | ) 18 | -------------------------------------------------------------------------------- /commits.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // MaxCommits limits number of commits to show. 6 | const MaxCommits = 3 7 | 8 | type Commits []string 9 | 10 | func (commits Commits) String() string { 11 | if len(commits) == 0 { 12 | return "" 13 | } 14 | 15 | count := len(commits) 16 | 17 | Max := MaxCommits 18 | if *expand { 19 | Max = count 20 | } 21 | isLimited := count > Max 22 | 23 | limit := Max 24 | if !isLimited { 25 | limit = count 26 | } 27 | 28 | var out string 29 | for _, commit := range commits[:limit] { 30 | str := cyan(fmt.Sprintf(" %s\n", commit)) 31 | out = fmt.Sprintf("%s%s", out, str) 32 | } 33 | 34 | if isLimited { 35 | more := yellow(fmt.Sprintf("and %d more...\n", count-Max)) 36 | out = fmt.Sprintf("%s%s", out, more) 37 | } 38 | 39 | return out 40 | } 41 | -------------------------------------------------------------------------------- /imports.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/parser" 5 | "go/token" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // Imports returns list of packages imported by 11 | // all sources found in dir. 12 | func Imports(dir string) ([]string, error) { 13 | fset := token.NewFileSet() 14 | 15 | // Find all packages in current dir 16 | pkgs, err := parser.ParseDir(fset, dir, nil, 0) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // Iterate over each package, each file 22 | // and add imported packages to map 23 | imports := make(map[string]struct{}) 24 | for _, pkg := range pkgs { 25 | for _, file := range pkg.Files { 26 | for _, impt := range file.Imports { 27 | path := strings.Trim(impt.Path.Value, `"`) 28 | imports[path] = struct{}{} 29 | } 30 | } 31 | } 32 | 33 | // Convert map to slice and sort 34 | var ret []string 35 | for name := range imports { 36 | ret = append(ret, name) 37 | } 38 | sort.Strings(ret) 39 | return ret, nil 40 | } 41 | -------------------------------------------------------------------------------- /vcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import () 4 | 5 | // VCS represents Version Control System. 6 | type VCS interface { 7 | Update() error 8 | Commits() []string 9 | } 10 | 11 | // Git implements VCS interface for Git. 12 | type Git struct { 13 | Dir string 14 | } 15 | 16 | // NewGit creates new Git object. 17 | func NewGit(dir string) *Git { 18 | return &Git{ 19 | Dir: dir, 20 | } 21 | } 22 | 23 | // Update updates info from the remote. 24 | func (git *Git) Update() error { 25 | _, err := Run(git.Dir, "git", "fetch", "origin") 26 | return err 27 | } 28 | 29 | // Commits returns new commits in master branch. 30 | func (git *Git) Commits() []string { 31 | out, err := Run(git.Dir, "git", "log", "HEAD..origin/master", "--oneline") 32 | if err != nil { 33 | return nil 34 | } 35 | return out 36 | } 37 | 38 | // Hg implements VCS interface for Mercurial. 39 | type Hg struct { 40 | Dir string 41 | } 42 | 43 | // NewHg creates new Hg object. 44 | func NewHg(dir string) *Hg { 45 | return &Hg{ 46 | Dir: dir, 47 | } 48 | } 49 | 50 | // Update updates info from the remote. 51 | func (hg *Hg) Update() error { 52 | return nil 53 | } 54 | 55 | // Commits returns new commits in master branch. 56 | func (hg *Hg) Commits() []string { 57 | out, err := Run(hg.Dir, "hg", "incoming", "-n", "-q", "--template", "{node|short} {desc|strip|firstline}\n") 58 | if err != nil { 59 | return nil 60 | } 61 | return out 62 | } 63 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "golang.org/x/tools/go/vcs" 9 | ) 10 | 11 | // Package represents single Go package/repo in Gopath. 12 | type Package struct { 13 | Name string 14 | Dir string 15 | Repo *vcs.RepoRoot 16 | 17 | Commits Commits 18 | } 19 | 20 | // Packages is an obvious type, but I prefer to have golint happy. 21 | type Packages []*Package 22 | 23 | var emojiRune = '✅' 24 | 25 | // NewPackage returns new package. 26 | func NewPackage(name, gopath string) (*Package, error) { 27 | dir := filepath.Join(gopath, "src", name) 28 | repo, err := vcs.RepoRootForImportPath(name, false) 29 | if err != nil { 30 | // it's ok, silently discard errors here 31 | return nil, err 32 | } 33 | return &Package{ 34 | Name: name, 35 | Dir: dir, 36 | 37 | Repo: repo, 38 | }, nil 39 | } 40 | 41 | // Refresh updates package info about new commits. 42 | // 43 | // It typically require internet connection to check 44 | // remote side. 45 | func (p *Package) Refresh() error { 46 | var vcs VCS 47 | switch p.Repo.VCS.Name { 48 | case "Git": 49 | vcs = NewGit(p.Dir) 50 | case "Mercurial": 51 | vcs = NewHg(p.Dir) 52 | default: 53 | return fmt.Errorf("unknown VCS") 54 | } 55 | 56 | if err := vcs.Update(); err != nil { 57 | return err 58 | } 59 | 60 | p.Commits = vcs.Commits() 61 | return nil 62 | } 63 | 64 | // IsOutdated returns true if package has updates on remote. 65 | func (p *Package) IsOutdated() bool { 66 | return len(p.Commits) > 0 67 | } 68 | 69 | // String implements Stringer for Package. 70 | func (p *Package) String() string { 71 | count := len(p.Commits) 72 | out := fmt.Sprintf("%s [%c %d]\n", green(p.Name), emojiRune, count) 73 | out = fmt.Sprintf("%s%s", out, p.Commits) 74 | return out 75 | } 76 | 77 | // UpdateCmd returns command used to update package. 78 | func (p Package) UpdateCmd(force bool) []string { 79 | if force { 80 | return []string{"go", "get", "-u", "-f", p.Name} 81 | } 82 | return []string{"go", "get", "-u", p.Name} 83 | } 84 | 85 | // Update updates package to the latest revision. 86 | func (p *Package) Update(force bool) (err error) { 87 | if force { 88 | _, err = Run(p.Dir, "go", "get", "-u", "-f", p.Name) 89 | return 90 | } 91 | _, err = Run(p.Dir, "go", "get", "-u", p.Name) 92 | return 93 | } 94 | 95 | // Outdated filters only outdated packages. 96 | func (pkgs Packages) Outdated() Packages { 97 | var outdated Packages 98 | for _, pkg := range pkgs { 99 | if pkg.IsOutdated() { 100 | outdated = append(outdated, pkg) 101 | } 102 | } 103 | return outdated 104 | } 105 | 106 | func init() { 107 | // No legacy is so rich as honesty. (: 108 | if os.Getenv("TRUTH_MODE") != "" { 109 | emojiRune = '🐞' 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoFresh 2 | 3 | [![Build Status](https://drone.io/github.com/divan/gofresh/status.png)](https://drone.io/github.com/divan/gofresh/latest) 4 | 5 | Keep your Go package dependencies fresh. Console tool for checking and updating package dependencies (imports). 6 | 7 | ## Introduction 8 | 9 | GoFresh checks if there are any updates for imports in your package. It doesn't update packages, unless asked explicitly. 10 | 11 | ## Demo 12 | 13 | gofresh 14 | 15 | ## Installation 16 | 17 | Just run go get: 18 | 19 | go get github.com/divan/gofresh 20 | 21 | ## Usage 22 | 23 | Simply invoke **gofresh** inside a directory containing Go source files and it will tell you if you have any updates for your imports. 24 | 25 | gofresh 26 | 27 | To check a package in your $GOPATH, you can specify the exact package by name: 28 | 29 | gofresh golang.org/x/tools/go/vcs 30 | 31 | By default, it shows first 3 commits, but you can expand commits list using -expand flag. See -help for more details. 32 | 33 | $ gofresh -h 34 | gofresh [-options] 35 | gofresh [-options] [package(s)] 36 | Options: 37 | -dry-run 38 | Dry run 39 | -expand 40 | Expand list of commits 41 | -f Use force while updating packages 42 | -update 43 | Update all packages 44 | 45 | Using -update flag you can update automatically all packages. 46 | 47 | If you want to update them manually, use following flags to see the commands to invoke: 48 | 49 | gofresh -update -dry-run 50 | 51 | gofresh 52 | 53 | ## Workflow 54 | 55 | Typically, you simply invoke **gofresh** in your package dir to see what dependencies has been changed. Then you might want to see all new commits for the specific package and update it manually or update all automatically: 56 | 57 | $ cd src/github.com/myusername/myproject/ 58 | $ gofresh 59 | $ gofresh -expand github.com/howeyc/fsnotify 60 | $ gofresh -update 61 | 62 | gofresh 63 | 64 | ## Using with vendoring tools 65 | 66 | If you use vendoring tool, such as Godep, your workflow doesn't change much: 67 | 68 | * First, use gofresh to inspect updates and update if needed 69 | * Second, run ```godep save``` or similar to update vendor dir from your GOPATH 70 | * Commit update 71 | 72 | ## Issues 73 | 74 | * No support for Bazaar & SVN *(how to check new commits on them?)* 75 | * Missing/failed repositories are not reported (no way to identify error from vcs.RepoRootForImportPath) 76 | * Subpackages from the same repo will be checked all anyway (TODO: optimize) 77 | 78 | ## Alternatives 79 | 80 | * [Go-Package-Store](https://github.com/shurcooL/Go-Package-Store) - displays updates for the Go packages in your GOPATH and shows with a nice Web UI. 81 | 82 | ## License 83 | 84 | This program is under [WTFPL license](http://www.wtfpl.net) 85 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | var ( 13 | update = flag.Bool("update", false, "Update all packages") 14 | force = flag.Bool("f", false, "Use force while updating packages") 15 | dryRun = flag.Bool("dry-run", false, "Dry run") 16 | expand = flag.Bool("expand", false, "Expand list of commits") 17 | ) 18 | 19 | func main() { 20 | flag.Usage = Usage 21 | flag.Parse() 22 | 23 | var packages []string 24 | 25 | // In case package name(s) were specified, check only them 26 | byName := len(flag.Args()) != 0 27 | if byName { 28 | packages = flag.Args() 29 | fmt.Fprintf(stdout, "Checking %d packages for updates...\n", len(packages)) 30 | } else { 31 | // otherwise, find imports for current package and 32 | // subpackages 33 | var err error 34 | packages, err = Imports(".") 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | fmt.Fprintf(stdout, "Found %d imports, checking for updates...\n", len(packages)) 40 | } 41 | 42 | var ( 43 | wg sync.WaitGroup 44 | pkgs Packages 45 | ch = make(chan *Package) 46 | failed bool 47 | ) 48 | 49 | go func() { 50 | for pkg := range ch { 51 | pkgs = append(pkgs, pkg) 52 | } 53 | }() 54 | 55 | gopath := GOPATH() 56 | for _, name := range packages { 57 | wg.Add(1) 58 | go func(name string) { 59 | defer wg.Done() 60 | pkg, err := NewPackage(name, gopath) 61 | if err != nil { 62 | // There always will be error, when processing imports from 63 | // source, like 'fmt', 'net/http', etc. 64 | // But for explicitly specified packages by name, we should 65 | // show user an error. 66 | if byName { 67 | failed = true 68 | fmt.Fprintf(stdout, "%s: %s\n", red(name), redBold(err.Error())) 69 | } 70 | return 71 | } 72 | err = pkg.Refresh() 73 | if err != nil { 74 | failed = true 75 | fmt.Fprintf(stdout, "%s: %s\n", red(name), redBold(err.Error())) 76 | return 77 | } 78 | 79 | ch <- pkg 80 | 81 | }(name) 82 | } 83 | wg.Wait() 84 | close(ch) 85 | 86 | outdated := pkgs.Outdated() 87 | 88 | // Update, if requested 89 | if *update { 90 | for _, pkg := range outdated { 91 | cmdline := strings.Join(pkg.UpdateCmd(*force), " ") 92 | fmt.Fprintln(stdout, green(cmdline)) 93 | if !*dryRun { 94 | err := pkg.Update(*force) 95 | if err != nil { 96 | fmt.Fprintf(stdout, "%s: %s\n", red(pkg.Name), redBold(err.Error())) 97 | failed = true 98 | continue 99 | } 100 | } 101 | } 102 | 103 | // TODO: check again? 104 | outdated = Packages{} 105 | } 106 | 107 | upToDate := len(outdated) == 0 108 | if upToDate && !failed { 109 | fmt.Fprintln(stdout, "Everything is up to date.") 110 | return 111 | } else if upToDate && failed { 112 | fmt.Fprintln(stdout, "There were some errors, check incomplete or wrong usage.") 113 | return 114 | } 115 | 116 | for _, pkg := range outdated { 117 | fmt.Print(pkg) 118 | } 119 | fmt.Fprintf(stdout, green("---\nYou have %d packages out of date\n", len(outdated))) 120 | fmt.Fprintln(stdout, "To update all packages automatically, run", bold("gofresh -update")) 121 | } 122 | 123 | func Usage() { 124 | fmt.Fprintf(os.Stderr, "gofresh [-options]\n") 125 | fmt.Fprintf(os.Stderr, "gofresh [-options] [package(s)]\n") 126 | fmt.Fprintf(os.Stderr, "Options:\n") 127 | flag.PrintDefaults() 128 | } 129 | 130 | // GOPATH returns GOPATH to be used for package update checks. 131 | // 132 | // In case there are many dirs in GOPATH, only the first will be used. 133 | // TODO: add multiple dirs support? someone use it w/o vendoring tools? 134 | func GOPATH() string { 135 | path := os.Getenv("GOPATH") 136 | fields := strings.Split(path, string(os.PathListSeparator)) 137 | return fields[0] 138 | } 139 | --------------------------------------------------------------------------------