From 539c3beb90a5a7daf46f0f07d3ae98f79a09db11 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Thu, 25 Jun 2020 12:53:27 +0200 Subject: [PATCH] Load repos status simultaneously with goroutines Also, refactor printer interface. --- README.md | 2 +- pkg/io/io.go | 3 - pkg/list.go | 68 +++++++++--------- pkg/load.go | 173 +++++++++++++++++++++++++++++++++++++++++++++ pkg/print/dump.go | 21 ++---- pkg/print/flat.go | 44 ++++++------ pkg/print/print.go | 159 ++++++----------------------------------- pkg/print/tree.go | 38 +++++----- 8 files changed, 274 insertions(+), 234 deletions(-) create mode 100644 pkg/load.go diff --git a/README.md b/README.md index 89e2f85..2a2ef59 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ https://github.com/grdl/testsite master Dump file format is simply: - Each repo URL on a separate line. -- Each URL can have a suffix with a branch or tag name to check out after cloning. Without that suffix, repository HEAD is used (usually it's `master`). +- Each URL can have a space-separated suffix with a branch or tag name to check out after cloning. Without that suffix, repository HEAD is cloned (usually it's `master`). Example dump file content: ``` diff --git a/pkg/io/io.go b/pkg/io/io.go index 8ac17b1..df81c92 100644 --- a/pkg/io/io.go +++ b/pkg/io/io.go @@ -5,7 +5,6 @@ import ( "fmt" "io/ioutil" "os" - "sort" "strings" "syscall" @@ -98,8 +97,6 @@ func (r *RepoFinder) Find() ([]string, error) { return nil, fmt.Errorf("no git repos found in root path %s", r.root) } - sort.Strings(r.repos) - return r.repos, nil } diff --git a/pkg/list.go b/pkg/list.go index a8aec8e..ec3c43e 100644 --- a/pkg/list.go +++ b/pkg/list.go @@ -3,14 +3,12 @@ package pkg import ( "fmt" "git-get/pkg/cfg" - "git-get/pkg/git" "git-get/pkg/io" "git-get/pkg/print" + "sort" "strings" ) -var repos []string - // ListCfg provides configuration for the List command. type ListCfg struct { Fetch bool @@ -25,50 +23,52 @@ func List(c *ListCfg) error { return err } - // TODO: we should open, fetch and read status of each repo in separate goroutine - var repos []git.Repo - for _, path := range paths { - repo, err := git.Open(path) - if err != nil { - // TODO: how should we handle it? - continue - } + loaded := loadAll(paths) - if c.Fetch { - err := repo.Fetch() - if err != nil { - // TODO: handle error - } - } - - repos = append(repos, *repo) + printables := make([]print.Printable, len(loaded)) + for i := range loaded { + printables[i] = loaded[i] } switch c.Output { case cfg.OutFlat: - printables := make([]print.Repo, len(repos)) - for i := range repos { - printables[i] = &repos[i] - } fmt.Println(print.NewFlatPrinter().Print(printables)) - case cfg.OutTree: - printables := make([]print.Repo, len(repos)) - for i := range repos { - printables[i] = &repos[i] - } fmt.Println(print.NewTreePrinter().Print(c.Root, printables)) - case cfg.OutDump: - printables := make([]print.DumpRepo, len(repos)) - for i := range repos { - printables[i] = &repos[i] - } fmt.Println(print.NewDumpPrinter().Print(printables)) - default: return fmt.Errorf("invalid --out flag; allowed values: [%s]", strings.Join(cfg.AllowedOut, ", ")) } return nil } + +// loadAll runs a separate goroutine to open, fetch (if asked to) and load status of git repo +func loadAll(paths []string) []*Loaded { + var ll []*Loaded + + loadedChan := make(chan *Loaded) + + for _, path := range paths { + go func(path string) { + loadedChan <- Load(path) + }(path) + } + + for l := range loadedChan { + ll = append(ll, l) + + // Close the channell when loaded all paths + if len(ll) == len(paths) { + close(loadedChan) + } + } + + // sort the loaded slice by path + sort.Slice(ll, func(i, j int) bool { + return strings.Compare(ll[i].path, ll[j].path) < 0 + }) + + return ll +} diff --git a/pkg/load.go b/pkg/load.go new file mode 100644 index 0000000..e90cd8d --- /dev/null +++ b/pkg/load.go @@ -0,0 +1,173 @@ +package pkg + +import ( + "fmt" + "git-get/pkg/git" + "strings" +) + +// Loaded represents a repository which status is Loaded from disk and stored for printing. +type Loaded struct { + path string + current string + branches map[string]string // key: branch name, value: branch status + worktree string + remote string + errors []string +} + +// Load reads status of a repository at a given path. +func Load(path string) *Loaded { + loaded := &Loaded{ + path: path, + branches: make(map[string]string), + errors: make([]string, 0), + } + + repo, err := git.Open(path) + if err != nil { + loaded.errors = append(loaded.errors, err.Error()) + return loaded + } + + loaded.current, err = repo.CurrentBranch() + if err != nil { + loaded.errors = append(loaded.errors, err.Error()) + } + + var errs []error + loaded.branches, errs = loadBranches(repo) + for _, err := range errs { + loaded.errors = append(loaded.errors, err.Error()) + } + + loaded.worktree, err = loadWorkTree(repo) + if err != nil { + loaded.errors = append(loaded.errors, err.Error()) + } + + loaded.remote, err = repo.Remote() + if err != nil { + loaded.errors = append(loaded.errors, err.Error()) + } + + return loaded +} + +func loadBranches(r *git.Repo) (map[string]string, []error) { + statuses := make(map[string]string) + errors := make([]error, 0) + + branches, err := r.Branches() + if err != nil { + errors = append(errors, err) + return statuses, errors + } + + for _, branch := range branches { + status, err := loadBranchStatus(r, branch) + statuses[branch] = status + if err != nil { + errors = append(errors, err) + } + } + + return statuses, errors +} + +func loadBranchStatus(r *git.Repo, branch string) (string, error) { + upstream, err := r.Upstream(branch) + if err != nil { + return "", err + } + + if upstream == "" { + return "no upstream", nil + } + + ahead, behind, err := r.AheadBehind(branch, upstream) + if err != nil { + return "", err + } + + if ahead == 0 && behind == 0 { + return "", nil + } + + var res []string + if ahead != 0 { + res = append(res, fmt.Sprintf("%d ahead", ahead)) + } + if behind != 0 { + res = append(res, fmt.Sprintf("%d behind", behind)) + } + + return strings.Join(res, " "), nil +} + +func loadWorkTree(r *git.Repo) (string, error) { + uncommitted, err := r.Uncommitted() + if err != nil { + return "", err + } + + untracked, err := r.Untracked() + if err != nil { + return "", err + } + + if uncommitted == 0 && untracked == 0 { + return "", nil + } + + var res []string + if uncommitted != 0 { + res = append(res, fmt.Sprintf("%d uncommitted", uncommitted)) + } + if untracked != 0 { + res = append(res, fmt.Sprintf("%d untracked", untracked)) + } + + return strings.Join(res, " "), nil +} + +// Path returns path to a repository. +func (r *Loaded) Path() string { + return r.path +} + +// Current returns the name of currently checked out branch (or tag or detached HEAD). +func (r *Loaded) Current() string { + return r.current +} + +// Branches returns a list of all branches names except the currently checked out one. Use Current() to get its name. +func (r *Loaded) Branches() []string { + var branches []string + for b := range r.branches { + if b != r.current { + branches = append(branches, b) + } + } + return branches +} + +// BranchStatus returns status of a given branch +func (r *Loaded) BranchStatus(branch string) string { + return r.branches[branch] +} + +// WorkTreeStatus returns status of a worktree +func (r *Loaded) WorkTreeStatus() string { + return r.worktree +} + +// Remote returns URL to remote repository +func (r *Loaded) Remote() string { + return r.remote +} + +// Errors is a slice of errors that occurred when loading repo status +func (r *Loaded) Errors() []string { + return r.errors +} diff --git a/pkg/print/dump.go b/pkg/print/dump.go index 106c983..63ca454 100644 --- a/pkg/print/dump.go +++ b/pkg/print/dump.go @@ -4,13 +4,6 @@ import ( "strings" ) -// DumpRepo is a git repository printable into a dump file. -type DumpRepo interface { - Path() string - Remote() (string, error) - CurrentBranch() (string, error) -} - // DumpPrinter prints a list of repos in a dump file format. type DumpPrinter struct{} @@ -21,20 +14,14 @@ func NewDumpPrinter() *DumpPrinter { // Print generates a list of repos URLs. Each line contains a URL and, if applicable, a currently checked out branch name. // It's a way to dump all repositories managed by git-get and is supposed to be consumed by `git get --dump`. -func (p *DumpPrinter) Print(repos []DumpRepo) string { +func (p *DumpPrinter) Print(repos []Printable) string { var str strings.Builder for i, r := range repos { - url, err := r.Remote() - if err != nil { - continue - // TODO: handle error? - } + str.WriteString(r.Remote()) - str.WriteString(url) - - current, err := r.CurrentBranch() - if err != nil || current != detached { + // TODO: if head is detached maybe we should get the revision it points to in case it's a tag + if current := r.Current(); current != "" && current != head { str.WriteString(" " + current) } diff --git a/pkg/print/flat.go b/pkg/print/flat.go index 7599e17..ca1fb66 100644 --- a/pkg/print/flat.go +++ b/pkg/print/flat.go @@ -2,6 +2,7 @@ package print import ( "fmt" + "os" "strings" ) @@ -14,37 +15,38 @@ func NewFlatPrinter() *FlatPrinter { } // Print generates a flat list of repositories and their statuses - each repo in new line with full path. -func (p *FlatPrinter) Print(repos []Repo) string { +func (p *FlatPrinter) Print(repos []Printable) string { var str strings.Builder - for _, r := range repos { - str.WriteString(fmt.Sprintf("\n%s %s", r.Path(), printCurrentBranchLine(r))) + for i, r := range repos { + str.WriteString(strings.TrimSuffix(r.Path(), string(os.PathSeparator))) + str.WriteString(" " + blue(r.Current())) - branches, err := r.Branches() - if err != nil { - str.WriteString(printErr(err)) - continue + current := r.BranchStatus(r.Current()) + worktree := r.WorkTreeStatus() + + if worktree != "" { + worktree = fmt.Sprintf("[ %s ]", worktree) } - current, err := r.CurrentBranch() - if err != nil { - str.WriteString(printErr(err)) - continue + if worktree == "" && current == "" { + str.WriteString(" " + green("ok")) + } else { + str.WriteString(" " + strings.Join([]string{yellow(current), red(worktree)}, " ")) } - for _, branch := range branches { - // Don't print the status of the current branch. It was already printed above. - if branch == current { - continue + for _, branch := range r.Branches() { + status := r.BranchStatus(branch) + if status == "" { + status = green("ok") } - status, err := printBranchStatus(r, branch) - if err != nil { - status = printErr(err) - } + indent := strings.Repeat(" ", len(r.Path())-1) + str.WriteString(fmt.Sprintf("\n%s %s %s", indent, blue(branch), yellow(status))) + } - indent := strings.Repeat(" ", len(r.Path())) - str.WriteString(fmt.Sprintf("\n%s %s %s", indent, printBranchName(branch), status)) + if i < len(repos)-1 { + str.WriteString("\n") } } diff --git a/pkg/print/print.go b/pkg/print/print.go index 38e06a5..18ee341 100644 --- a/pkg/print/print.go +++ b/pkg/print/print.go @@ -1,148 +1,35 @@ package print -import ( - "fmt" - "strings" +import "fmt" + +const ( + head = "HEAD" ) +// Printable represents a repository which status can be printed +type Printable interface { + Path() string + Current() string + Branches() []string + BranchStatus(string) string + WorkTreeStatus() string + Remote() string + Errors() []string +} + // TODO: not sure if this works on windows. See https://github.com/mattn/go-colorable -const ( - colorRed = "\033[1;31m%s\033[0m" - colorGreen = "\033[1;32m%s\033[0m" - colorBlue = "\033[1;34m%s\033[0m" - colorYellow = "\033[1;33m%s\033[0m" -) - -const ( - untracked = "untracked" - uncommitted = "uncommitted" - ahead = "ahead" - behind = "behind" - noUpstream = "no upstream" - ok = "ok" - detached = "detached" - head = "HEAD" -) - -// Repo is a git repository -// TODO: maybe branch should be a separate interface -type Repo interface { - Path() string - Branches() ([]string, error) - CurrentBranch() (string, error) - Upstream(branch string) (string, error) - AheadBehind(branch string, upstream string) (int, int, error) - Uncommitted() (int, error) - Untracked() (int, error) +func red(str string) string { + return fmt.Sprintf("\033[1;31m%s\033[0m", str) } -// // Printer provides a way to print a list of repos and their statuses -// type Printer interface { -// Print(root string, repos []Repo) string -// } - -// prints status of currently checked out branch and the work tree. -// The format is: branch_name branch_status [ worktree_status ] -// Eg: master 1 head 2 behind [ 1 uncommitted ] -func printCurrentBranchLine(r Repo) string { - var res []string - - current, err := r.CurrentBranch() - if err != nil { - return printErr(err) - } - - // if current head is detached don't print its status - if current == head { - return fmt.Sprintf(colorYellow, detached) - } - - status, err := printBranchStatus(r, current) - if err != nil { - return printErr(err) - } - - worktree, err := printWorkTreeStatus(r) - if err != nil { - return printErr(err) - } - - res = append(res, printBranchName(current)) - - // if worktree is not clean and branch is ok then it shouldn't be ok - if worktree != "" && strings.Contains(status, ok) { - res = append(res, worktree) - } else { - res = append(res, status) - res = append(res, worktree) - } - - return strings.Join(res, " ") +func green(str string) string { + return fmt.Sprintf("\033[1;32m%s\033[0m", str) } -func printBranchName(branch string) string { - return fmt.Sprintf(colorBlue, branch) +func blue(str string) string { + return fmt.Sprintf("\033[1;34m%s\033[0m", str) } -func printBranchStatus(r Repo, branch string) (string, error) { - var res []string - upstream, err := r.Upstream(branch) - if err != nil { - return "", err - } - - if upstream == "" { - return fmt.Sprintf(colorYellow, noUpstream), nil - } - - a, b, err := r.AheadBehind(branch, upstream) - if err != nil { - return printErr(err), nil - } - - if a == 0 && b == 0 { - return fmt.Sprintf(colorGreen, ok), nil - } - - if a != 0 { - res = append(res, fmt.Sprintf(colorYellow, fmt.Sprintf("%d %s", a, ahead))) - } - if b != 0 { - res = append(res, fmt.Sprintf(colorYellow, fmt.Sprintf("%d %s", b, behind))) - } - - return strings.Join(res, " "), nil -} - -func printWorkTreeStatus(r Repo) (string, error) { - uc, err := r.Uncommitted() - if err != nil { - return "", err - } - - ut, err := r.Untracked() - if err != nil { - return "", err - } - - if uc == 0 && ut == 0 { - return "", nil - } - - var res []string - res = append(res, "[") - if uc != 0 { - res = append(res, fmt.Sprintf(colorRed, fmt.Sprintf("%d %s", uc, uncommitted))) - } - if ut != 0 { - res = append(res, fmt.Sprintf(colorRed, fmt.Sprintf("%d %s", ut, untracked))) - } - - res = append(res, "]") - - return strings.Join(res, " "), nil -} - -func printErr(err error) string { - return fmt.Sprintf(colorRed, err.Error()) +func yellow(str string) string { + return fmt.Sprintf("\033[1;33m%s\033[0m", str) } diff --git a/pkg/print/tree.go b/pkg/print/tree.go index 976af92..d484b22 100644 --- a/pkg/print/tree.go +++ b/pkg/print/tree.go @@ -18,7 +18,7 @@ func NewTreePrinter() *TreePrinter { } // Print generates a tree view of repos and their statuses. -func (p *TreePrinter) Print(root string, repos []Repo) string { +func (p *TreePrinter) Print(root string, repos []Printable) string { if len(repos) == 0 { return fmt.Sprintf("There are no git repos under %s", root) } @@ -37,7 +37,7 @@ type Node struct { val string parent *Node children []*Node - repo Repo + repo Printable } // Root creates a new root of a tree. @@ -81,7 +81,7 @@ func (n *Node) GetChild(val string) *Node { // buildTree builds a directory tree of paths to repositories. // Each node represents a directory in the repo path. // Each leaf (final node) contains a pointer to the repo. -func buildTree(root string, repos []Repo) *Node { +func buildTree(root string, repos []Printable) *Node { tree := Root(root) for _, r := range repos { @@ -115,32 +115,26 @@ func buildTree(root string, repos []Repo) *Node { func (p *TreePrinter) printTree(node *Node, tp treeprint.Tree) { if node.children == nil { r := node.repo - tp.SetValue(node.val + " " + printCurrentBranchLine(r)) + current := r.BranchStatus(r.Current()) + worktree := r.WorkTreeStatus() - branches, err := r.Branches() - if err != nil { - tp.AddNode(printErr(err)) - return + if worktree != "" { + worktree = fmt.Sprintf("[ %s ]", worktree) } - current, err := r.CurrentBranch() - if err != nil { - tp.AddNode(printErr(err)) - return + if worktree == "" && current == "" { + tp.SetValue(node.val + " " + blue(r.Current()) + " " + green("ok")) + } else { + tp.SetValue(node.val + " " + blue(r.Current()) + " " + strings.Join([]string{yellow(current), red(worktree)}, " ")) } - for _, branch := range branches { - // Don't print the status of the current branch. It was already printed above. - if branch == current { - continue + for _, branch := range r.Branches() { + status := r.BranchStatus(branch) + if status == "" { + status = green("ok") } - status, err := printBranchStatus(r, branch) - if err != nil { - tp.AddNode(printErr(err)) - continue - } - tp.AddNode(printBranchName(branch) + " " + status) + tp.AddNode(blue(branch) + " " + yellow(status)) } }