├── .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 | [](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------