diff --git a/go.mod b/go.mod index 340c0f5..9844d7c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.14 require ( github.com/go-git/go-git/v5 v5.1.0 + github.com/karrick/godirwalk v1.15.6 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.0.0 + github.com/xlab/treeprint v1.0.0 ) diff --git a/go.sum b/go.sum index 939db20..1cfde32 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/karrick/godirwalk v1.15.6 h1:Yf2mmR8TJy+8Fa0SuQVto5SYap6IF7lNVX4Jdl8G1qA= +github.com/karrick/godirwalk v1.15.6/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -143,6 +145,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v1.0.0 h1:J0TkWtiuYgtdlrkkrDLISYBQ92M+X5m4LrIIMKrbDTs= +github.com/xlab/treeprint v1.0.0/go.mod h1:IoImgRak9i3zJyuxOKUP1v4UZd1tMoKkq/Cimt1uhCg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -185,6 +189,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/pkg/list.go b/pkg/list.go new file mode 100644 index 0000000..68501d5 --- /dev/null +++ b/pkg/list.go @@ -0,0 +1,161 @@ +package pkg + +import ( + "errors" + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/xlab/treeprint" + + "github.com/go-git/go-git/v5" + + "github.com/karrick/godirwalk" +) + +// skipNode is used as an error indicating that .git directory has been found. +// It's handled by ErrorsCallback to tell the WalkCallback to skip this dir. +var skipNode = errors.New(".git directory found, skipping this node") + +var repos []string + +func FindRepos() ([]string, error) { + root := Cfg.ReposRoot() + + walkOpts := &godirwalk.Options{ + ErrorCallback: ErrorCb, + Callback: WalkCb, + // Use Unsorted to improve speed because repos will be processed by goroutines in a random order anyway. + Unsorted: true, + } + + err := godirwalk.Walk(root, walkOpts) + if err != nil { + return nil, err + } + + return repos, nil +} + +func WalkCb(path string, ent *godirwalk.Dirent) error { + if ent.IsDir() && ent.Name() == git.GitDirName { + repos = append(repos, strings.TrimSuffix(path, git.GitDirName)) + return skipNode + } + return nil +} + +func ErrorCb(_ string, err error) godirwalk.ErrorAction { + if errors.Is(err, skipNode) { + return godirwalk.SkipNode + } + return godirwalk.Halt +} + +func OpenAll(paths []string) ([]*Repo, error) { + var repos []*Repo + reposChan := make(chan *Repo) + + for _, path := range paths { + go func(path string) { + repo, err := OpenRepo(path) + + if err != nil { + // TODO handle error + fmt.Println(err) + } + + err = repo.LoadStatus() + if err != nil { + // TODO handle error + fmt.Println(err) + } + // when error happened we just sent a nil + reposChan <- repo + }(path) + } + + for repo := range reposChan { + repos = append(repos, repo) + + // TODO: is this the right way to close the channel? What if we have non-unique paths? + if len(repos) == len(paths) { + close(reposChan) + } + } + + // sort the final array to make printing easier + sort.Slice(repos, func(i, j int) bool { + return strings.Compare(repos[i].path, repos[j].path) < 0 + }) + + return repos, nil +} + +func PrintRepos(repos []*Repo) { + root := Cfg.ReposRoot() + + seg := make([][]string, len(repos)) + + t := treeprint.New() + t.SetValue(root) + + for i, repo := range repos { + path := strings.TrimPrefix(repo.path, root) + path = strings.Trim(path, string(filepath.Separator)) + subpaths := strings.Split(path, string(filepath.Separator)) + + seg[i] = make([]string, len(subpaths)) + + //t.AddBranch(fmt.Sprintf("\033[1;31m%s\033[0m", path)) + + branch := t + for j, sub := range subpaths { + seg[i][j] = sub + + if i > 0 && seg[i][j] == seg[i-1][j] { + branch = branch.FindLastNode() + continue + } + + value := seg[i][j] + + // if this is the last segment, it means that's the name of the repository and we need to print its status + if j == len(seg[i])-1 { + value = value + PrintRepoStatus(repo) + } + + branch = branch.AddBranch(value) + } + } + + fmt.Println(t.String()) +} + +const ( + ColorRed = "\033[1;31m%s\033[0m" + ColorGreen = "\033[0;32m%s\033[0m" + ColorBlue = "\033[1;34m%s\033[0m" + ColorYellow = "\033[1;33m%s\033[0m" +) + +const ( + StatusOk = "ok" + StatusUncommitted = "uncommitted" + StatusUntracked = "untracked" +) + +func PrintRepoStatus(repo *Repo) string { + status := fmt.Sprintf(ColorGreen, StatusOk) + + if repo.Status.HasUntrackedFiles { + status = fmt.Sprintf(ColorRed, StatusUntracked) + } + + if repo.Status.HasUncommittedChanges { + status = fmt.Sprintf(ColorRed, StatusUncommitted) + } + + return " " + status +} diff --git a/pkg/list_test.go b/pkg/list_test.go new file mode 100644 index 0000000..6158162 --- /dev/null +++ b/pkg/list_test.go @@ -0,0 +1,17 @@ +package pkg + +import ( + "os" + "testing" +) + +func TestList(t *testing.T) { + _ = os.Setenv(EnvReposRoot, "/home/gru/workspace") + + paths, err := FindRepos() + checkFatal(t, err) + + repos, _ := OpenAll(paths) + + PrintRepos(repos) +}