From f3d0df1bfd5f9af3a6e2e13f06c5eada1a12100a Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Mon, 8 Jun 2020 12:07:03 +0200 Subject: [PATCH] Refactor packages structure - Isolate files into their own packages - Create new printer package and interface - Refactor Repo stuct to embed the go-git *Repository directly - Simplify cmd package --- {pkg => cfg}/config.go | 2 +- {pkg => cfg}/config_test.go | 8 +- cmd/{git-get => }/main.go | 34 ++-- {pkg => git}/repo.go | 47 ++++-- pkg/helpers_test.go => git/repo_test.go | 24 +-- {pkg => git}/status.go | 18 +-- {pkg => git}/status_test.go | 2 +- path/list.go | 103 ++++++++++++ {pkg => path}/url.go | 5 +- {pkg => path}/url_test.go | 5 +- pkg/list.go | 198 ------------------------ print/print.go | 115 ++++++++++++++ {pkg => print}/tree.go | 11 +- {pkg => print}/tree_test.go | 7 +- 14 files changed, 318 insertions(+), 261 deletions(-) rename {pkg => cfg}/config.go (99%) rename {pkg => cfg}/config_test.go (97%) rename cmd/{git-get => }/main.go (50%) rename {pkg => git}/repo.go (71%) rename pkg/helpers_test.go => git/repo_test.go (93%) rename {pkg => git}/status.go (94%) rename {pkg => git}/status_test.go (99%) create mode 100644 path/list.go rename {pkg => path}/url.go (95%) rename {pkg => path}/url_test.go (98%) delete mode 100644 pkg/list.go create mode 100644 print/print.go rename {pkg => print}/tree.go (96%) rename {pkg => print}/tree_test.go (93%) diff --git a/pkg/config.go b/cfg/config.go similarity index 99% rename from pkg/config.go rename to cfg/config.go index 8c670ba..3bfc0c2 100644 --- a/pkg/config.go +++ b/cfg/config.go @@ -1,4 +1,4 @@ -package pkg +package cfg import ( "path" diff --git a/pkg/config_test.go b/cfg/config_test.go similarity index 97% rename from pkg/config_test.go rename to cfg/config_test.go index 6cbfcb7..04868ce 100644 --- a/pkg/config_test.go +++ b/cfg/config_test.go @@ -1,4 +1,4 @@ -package pkg +package cfg import ( "os" @@ -156,3 +156,9 @@ func TestConfig(t *testing.T) { checkFatal(t, err) } } + +func checkFatal(t *testing.T, err error) { + if err != nil { + t.Fatalf("%+v", err) + } +} diff --git a/cmd/git-get/main.go b/cmd/main.go similarity index 50% rename from cmd/git-get/main.go rename to cmd/main.go index 7e967c3..90fa88e 100644 --- a/cmd/git-get/main.go +++ b/cmd/main.go @@ -2,9 +2,14 @@ package main import ( "fmt" - "git-get/pkg" + "git-get/cfg" + "git-get/git" + "git-get/path" + "git-get/print" "os" + pathpkg "path" + "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -27,30 +32,37 @@ var list bool func init() { cmd.PersistentFlags().BoolVarP(&list, "list", "l", false, "Lists all repositories inside git-get root") - cmd.PersistentFlags().StringP(pkg.KeyReposRoot, "r", "", "repos root") - cmd.PersistentFlags().StringP(pkg.KeyPrivateKey, "p", "", "SSH private key path") - viper.BindPFlag(pkg.KeyReposRoot, cmd.PersistentFlags().Lookup(pkg.KeyReposRoot)) - viper.BindPFlag(pkg.KeyPrivateKey, cmd.PersistentFlags().Lookup(pkg.KeyReposRoot)) + cmd.PersistentFlags().StringP(cfg.KeyReposRoot, "r", "", "repos root") + cmd.PersistentFlags().StringP(cfg.KeyPrivateKey, "p", "", "SSH private key path") + viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) + viper.BindPFlag(cfg.KeyPrivateKey, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) } func Run(cmd *cobra.Command, args []string) { - pkg.InitConfig() + cfg.InitConfig() + root := viper.GetString(cfg.KeyReposRoot) if list { - paths, err := pkg.FindRepos() + paths, err := path.FindRepos() exitIfError(err) - repos, err := pkg.OpenAll(paths) + repos, err := path.OpenAll(paths) exitIfError(err) - pkg.PrintRepos(repos) + //tree := BuildTree(root, repos) + //fmt.Println(RenderSmartTree(tree)) + + printer := print.NewFlatPrinter() + fmt.Println(printer.Print(root, repos)) + os.Exit(0) } - url, err := pkg.ParseURL(args[0]) + url, err := path.ParseURL(args[0]) exitIfError(err) + repoPath := pathpkg.Join(root, path.URLToPath(url)) - _, err = pkg.CloneRepo(url, viper.GetString(pkg.KeyReposRoot), false) + _, err = git.CloneRepo(url, repoPath, false) exitIfError(err) } diff --git a/pkg/repo.go b/git/repo.go similarity index 71% rename from pkg/repo.go rename to git/repo.go index f4779e7..6e96365 100644 --- a/pkg/repo.go +++ b/git/repo.go @@ -1,12 +1,13 @@ -package pkg +package git import ( "fmt" + "git-get/cfg" + "io" "io/ioutil" "net/url" "os" - "path" "github.com/pkg/errors" "github.com/spf13/viper" @@ -18,18 +19,16 @@ import ( ) type Repo struct { - repo *git.Repository - path string + *git.Repository + Path string Status *RepoStatus } -func CloneRepo(url *url.URL, reposRoot string, quiet bool) (*Repo, error) { - repoPath := path.Join(reposRoot, URLToPath(url)) - +func CloneRepo(url *url.URL, path string, quiet bool) (*Repo, error) { var progress io.Writer if !quiet { progress = os.Stdout - fmt.Printf("Cloning into '%s'...\n", repoPath) + fmt.Printf("Cloning into '%s'...\n", path) } // TODO: can this be cleaner? @@ -54,12 +53,12 @@ func CloneRepo(url *url.URL, reposRoot string, quiet bool) (*Repo, error) { Tags: git.AllTags, } - repo, err := git.PlainClone(repoPath, false, opts) + repo, err := git.PlainClone(path, false, opts) if err != nil { return nil, errors.Wrap(err, "Failed cloning repo") } - return newRepo(repo, repoPath), nil + return NewRepo(repo, path), nil } func OpenRepo(repoPath string) (*Repo, error) { @@ -68,20 +67,20 @@ func OpenRepo(repoPath string) (*Repo, error) { return nil, errors.Wrap(err, "Failed opening repo") } - return newRepo(repo, repoPath), nil + return NewRepo(repo, repoPath), nil } -func newRepo(repo *git.Repository, repoPath string) *Repo { +func NewRepo(repo *git.Repository, repoPath string) *Repo { return &Repo{ - repo: repo, - path: repoPath, - Status: &RepoStatus{}, + Repository: repo, + Path: repoPath, + Status: &RepoStatus{}, } } // Fetch performs a git fetch on all remotes func (r *Repo) Fetch() error { - remotes, err := r.repo.Remotes() + remotes, err := r.Remotes() if err != nil { return errors.Wrap(err, "Failed getting remotes") } @@ -97,7 +96,7 @@ func (r *Repo) Fetch() error { } func sshKeyAuth() (transport.AuthMethod, error) { - privateKey := viper.GetString(KeyPrivateKey) + privateKey := viper.GetString(cfg.KeyPrivateKey) sshKey, err := ioutil.ReadFile(privateKey) if err != nil { return nil, errors.Wrapf(err, "Failed to open ssh private key %s", privateKey) @@ -112,3 +111,17 @@ func sshKeyAuth() (transport.AuthMethod, error) { auth := &go_git_ssh.PublicKeys{User: "git", Signer: signer} return auth, nil } + +func (r *Repo) CurrentBranchStatus() *BranchStatus { + if r.Status.CurrentBranch == StatusDetached || r.Status.CurrentBranch == StatusUnknown { + return nil + } + + for _, b := range r.Status.Branches { + if b.Name == r.Status.CurrentBranch { + return b + } + } + + return nil +} diff --git a/pkg/helpers_test.go b/git/repo_test.go similarity index 93% rename from pkg/helpers_test.go rename to git/repo_test.go index 4b10d84..aeaeb94 100644 --- a/pkg/helpers_test.go +++ b/git/repo_test.go @@ -1,6 +1,8 @@ -package pkg +package git import ( + "net/url" + "io/ioutil" "os" "testing" @@ -25,7 +27,7 @@ func newRepoEmpty(t *testing.T) *Repo { repo, err := git.PlainInit(dir, false) checkFatal(t, err) - return newRepo(repo, dir) + return NewRepo(repo, dir) } func newRepoWithUntracked(t *testing.T) *Repo { @@ -156,7 +158,7 @@ func newTempDir(t *testing.T) string { } func (r *Repo) writeFile(t *testing.T, name string, content string) { - wt, err := r.repo.Worktree() + wt, err := r.Worktree() checkFatal(t, errors.Wrap(err, "Failed getting worktree")) file, err := wt.Filesystem.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) @@ -167,7 +169,7 @@ func (r *Repo) writeFile(t *testing.T, name string, content string) { } func (r *Repo) addFile(t *testing.T, name string) { - wt, err := r.repo.Worktree() + wt, err := r.Worktree() checkFatal(t, errors.Wrap(err, "Failed getting worktree")) _, err = wt.Add(name) @@ -175,7 +177,7 @@ func (r *Repo) addFile(t *testing.T, name string) { } func (r *Repo) newCommit(t *testing.T, msg string) plumbing.Hash { - wt, err := r.repo.Worktree() + wt, err := r.Worktree() checkFatal(t, errors.Wrap(err, "Failed getting worktree")) opts := &git.CommitOptions{ @@ -192,21 +194,21 @@ func (r *Repo) newCommit(t *testing.T, msg string) plumbing.Hash { } func (r *Repo) newBranch(t *testing.T, name string) { - head, err := r.repo.Head() + head, err := r.Head() checkFatal(t, err) ref := plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), head.Hash()) - err = r.repo.Storer.SetReference(ref) + err = r.Storer.SetReference(ref) checkFatal(t, err) } func (r *Repo) clone(t *testing.T) *Repo { dir := newTempDir(t) - url, err := ParseURL("file://" + r.path) + repoURL, err := url.Parse("file://" + r.Path) checkFatal(t, err) - repo, err := CloneRepo(url, dir, true) + repo, err := CloneRepo(repoURL, dir, true) checkFatal(t, err) return repo @@ -218,7 +220,7 @@ func (r *Repo) fetch(t *testing.T) { } func (r *Repo) checkoutBranch(t *testing.T, name string) { - wt, err := r.repo.Worktree() + wt, err := r.Worktree() checkFatal(t, errors.Wrap(err, "Failed getting worktree")) opts := &git.CheckoutOptions{ @@ -229,7 +231,7 @@ func (r *Repo) checkoutBranch(t *testing.T, name string) { } func (r *Repo) checkoutHash(t *testing.T, hash plumbing.Hash) { - wt, err := r.repo.Worktree() + wt, err := r.Worktree() checkFatal(t, errors.Wrap(err, "Failed getting worktree")) opts := &git.CheckoutOptions{ diff --git a/pkg/status.go b/git/status.go similarity index 94% rename from pkg/status.go rename to git/status.go index 60aab74..2890b20 100644 --- a/pkg/status.go +++ b/git/status.go @@ -1,4 +1,4 @@ -package pkg +package git import ( "sort" @@ -39,7 +39,7 @@ type BranchStatus struct { } func (r *Repo) LoadStatus() error { - wt, err := r.repo.Worktree() + wt, err := r.Worktree() if err != nil { return errors.Wrap(err, "Failed getting worktree") } @@ -105,7 +105,7 @@ func hasUncommitted(status git.Status) bool { } func currentBranch(r *Repo) string { - head, err := r.repo.Head() + head, err := r.Head() if err != nil { return StatusUnknown } @@ -118,7 +118,7 @@ func currentBranch(r *Repo) string { } func (r *Repo) loadBranchesStatus() error { - iter, err := r.repo.Branches() + iter, err := r.Branches() if err != nil { return errors.Wrap(err, "Failed getting branches iterator") } @@ -181,7 +181,7 @@ func (r *Repo) newBranchStatus(branch string) (*BranchStatus, error) { // "remote" - name of the remote containing upstream branch (or "." if upstream is a local branch) // "merge" - full ref name of the upstream branch (eg, ref/heads/master) func (r *Repo) upstream(branch string) (string, error) { - cfg, err := r.repo.Config() + cfg, err := r.Config() if err != nil { return "", errors.Wrap(err, "Failed getting repo config") } @@ -205,22 +205,22 @@ func (r *Repo) upstream(branch string) (string, error) { } func (r *Repo) needsPullOrPush(localBranch string, upstreamBranch string) (needsPull bool, needsPush bool, err error) { - localHash, err := r.repo.ResolveRevision(plumbing.Revision(localBranch)) + localHash, err := r.ResolveRevision(plumbing.Revision(localBranch)) if err != nil { return false, false, errors.Wrapf(err, "Failed resolving revision %s", localBranch) } - upstreamHash, err := r.repo.ResolveRevision(plumbing.Revision(upstreamBranch)) + upstreamHash, err := r.ResolveRevision(plumbing.Revision(upstreamBranch)) if err != nil { return false, false, errors.Wrapf(err, "Failed resolving revision %s", upstreamBranch) } - localCommit, err := r.repo.CommitObject(*localHash) + localCommit, err := r.CommitObject(*localHash) if err != nil { return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", localHash.String()) } - upstreamCommit, err := r.repo.CommitObject(*upstreamHash) + upstreamCommit, err := r.CommitObject(*upstreamHash) if err != nil { return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", upstreamHash.String()) } diff --git a/pkg/status_test.go b/git/status_test.go similarity index 99% rename from pkg/status_test.go rename to git/status_test.go index 6890345..065a6d0 100644 --- a/pkg/status_test.go +++ b/git/status_test.go @@ -1,4 +1,4 @@ -package pkg +package git import ( "reflect" diff --git a/path/list.go b/path/list.go new file mode 100644 index 0000000..60df3cb --- /dev/null +++ b/path/list.go @@ -0,0 +1,103 @@ +package path + +import ( + "fmt" + "git-get/cfg" + "git-get/git" + "os" + "sort" + "strings" + + "github.com/karrick/godirwalk" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +// 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) { + repos = []string{} + + root := viper.GetString(cfg.KeyReposRoot) + + if _, err := os.Stat(root); err != nil { + return nil, fmt.Errorf("Repos root %s does not exist or can't be accessed", root) + } + + 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 + } + + if len(repos) == 0 { + return nil, fmt.Errorf("No git repos found in repos root %s", root) + } + + return repos, nil +} + +func WalkCb(path string, ent *godirwalk.Dirent) error { + if ent.IsDir() && ent.Name() == ".git" { + repos = append(repos, strings.TrimSuffix(path, ".git")) + 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) ([]*git.Repo, error) { + var repos []*git.Repo + reposChan := make(chan *git.Repo) + + for _, path := range paths { + go func(path string) { + repo, err := git.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 +} diff --git a/pkg/url.go b/path/url.go similarity index 95% rename from pkg/url.go rename to path/url.go index d7332a1..48cc332 100644 --- a/pkg/url.go +++ b/path/url.go @@ -1,6 +1,7 @@ -package pkg +package path import ( + "git-get/cfg" urlpkg "net/url" "path" "regexp" @@ -46,7 +47,7 @@ func ParseURL(rawURL string) (url *urlpkg.URL, err error) { // Default to configured defaultHost when host is empty if url.Host == "" { - url.Host = viper.GetString(KeyDefaultHost) + url.Host = viper.GetString(cfg.KeyDefaultHost) } // Default to https when scheme is empty diff --git a/pkg/url_test.go b/path/url_test.go similarity index 98% rename from pkg/url_test.go rename to path/url_test.go index fcd1090..e60cc35 100644 --- a/pkg/url_test.go +++ b/path/url_test.go @@ -1,6 +1,7 @@ -package pkg +package path import ( + "git-get/cfg" "testing" ) @@ -49,7 +50,7 @@ func TestURLParse(t *testing.T) { } // We need to init config first so the default values are correctly loaded - InitConfig() + cfg.InitConfig() for _, test := range tests { url, err := ParseURL(test.in) diff --git a/pkg/list.go b/pkg/list.go deleted file mode 100644 index 535142c..0000000 --- a/pkg/list.go +++ /dev/null @@ -1,198 +0,0 @@ -package pkg - -import ( - "errors" - "fmt" - "os" - "sort" - "strings" - - "github.com/go-git/go-git/v5" - "github.com/spf13/viper" - - "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) { - repos = []string{} - - root := viper.GetString(KeyReposRoot) - - if _, err := os.Stat(root); err != nil { - return nil, fmt.Errorf("Repos root %s does not exist or can't be accessed", root) - } - - 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 - } - - if len(repos) == 0 { - return nil, fmt.Errorf("No git repos found in repos root %s", root) - } - - 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 := viper.GetString(KeyReposRoot) - - tree := BuildTree(root, repos) - fmt.Println(RenderSmartTree(tree)) -} - -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" -) - -func renderWorktreeStatus(repo *Repo) string { - clean := true - var status []string - - // if current branch status can't be found it's probably a detached head - // TODO: what if current HEAD points to a tag? - if current := repo.findCurrentBranchStatus(); current == nil { - status = append(status, fmt.Sprintf(ColorYellow, repo.Status.CurrentBranch)) - } else { - status = append(status, renderBranchStatus(current)) - } - - // TODO: this is ugly - // unset clean flag to use it to render braces around worktree status and remove "ok" from branch status if it's there - if repo.Status.HasUncommittedChanges || repo.Status.HasUntrackedFiles { - clean = false - } - - if !clean { - status[len(status)-1] = strings.TrimSuffix(status[len(status)-1], StatusOk) - status = append(status, "[") - } - - if repo.Status.HasUntrackedFiles { - status = append(status, fmt.Sprintf(ColorRed, StatusUntracked)) - } - - if repo.Status.HasUncommittedChanges { - status = append(status, fmt.Sprintf(ColorRed, StatusUncommitted)) - } - - if !clean { - status = append(status, "]") - } - - return strings.Join(status, " ") -} - -func renderBranchStatus(branch *BranchStatus) string { - // ok indicates that the branch has upstream and is not ahead or behind it - ok := true - var status []string - - status = append(status, fmt.Sprintf(ColorBlue, branch.Name)) - - if branch.Upstream == "" { - ok = false - status = append(status, fmt.Sprintf(ColorYellow, StatusNoUpstream)) - } - - if branch.NeedsPull { - ok = false - status = append(status, fmt.Sprintf(ColorYellow, StatusBehind)) - } - - if branch.NeedsPush { - ok = false - status = append(status, fmt.Sprintf(ColorYellow, StatusAhead)) - } - - if ok { - status = append(status, fmt.Sprintf(ColorGreen, StatusOk)) - } - - return strings.Join(status, " ") -} - -func (r *Repo) findCurrentBranchStatus() *BranchStatus { - if r.Status.CurrentBranch == StatusDetached || r.Status.CurrentBranch == StatusUnknown { - return nil - } - - for _, b := range r.Status.Branches { - if b.Name == r.Status.CurrentBranch { - return b - } - } - - return nil -} diff --git a/print/print.go b/print/print.go new file mode 100644 index 0000000..e23b2fc --- /dev/null +++ b/print/print.go @@ -0,0 +1,115 @@ +package print + +import ( + "fmt" + "git-get/git" + "path/filepath" + "strings" +) + +type Printer interface { + Print(root string, repos []*git.Repo) string +} + +type FlatPrinter struct{} + +func NewFlatPrinter() *FlatPrinter { + return &FlatPrinter{} +} + +func (p *FlatPrinter) Print(root string, repos []*git.Repo) string { + val := root + + for _, repo := range repos { + path := strings.TrimPrefix(repo.Path, root) + path = strings.Trim(path, string(filepath.Separator)) + + val += fmt.Sprintf("\n%s %s", path, renderWorktreeStatus(repo)) + + for _, branch := range repo.Status.Branches { + // Don't print the status of the current branch. It was already printed above. + if branch.Name == repo.Status.CurrentBranch { + continue + } + + indent := strings.Repeat(" ", len(path)) + val += fmt.Sprintf("\n%s %s", indent, renderBranchStatus(branch)) + } + } + + return val +} + +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" +) + +func renderWorktreeStatus(repo *git.Repo) string { + clean := true + var status []string + + // if current branch status can't be found it's probably a detached head + // TODO: what if current HEAD points to a tag? + if current := repo.CurrentBranchStatus(); current == nil { + status = append(status, fmt.Sprintf(ColorYellow, repo.Status.CurrentBranch)) + } else { + status = append(status, renderBranchStatus(current)) + } + + // TODO: this is ugly + // unset clean flag to use it to render braces around worktree status and remove "ok" from branch status if it's there + if repo.Status.HasUncommittedChanges || repo.Status.HasUntrackedFiles { + clean = false + } + + if !clean { + status[len(status)-1] = strings.TrimSuffix(status[len(status)-1], git.StatusOk) + status = append(status, "[") + } + + if repo.Status.HasUntrackedFiles { + status = append(status, fmt.Sprintf(ColorRed, git.StatusUntracked)) + } + + if repo.Status.HasUncommittedChanges { + status = append(status, fmt.Sprintf(ColorRed, git.StatusUncommitted)) + } + + if !clean { + status = append(status, "]") + } + + return strings.Join(status, " ") +} + +func renderBranchStatus(branch *git.BranchStatus) string { + // ok indicates that the branch has upstream and is not ahead or behind it + ok := true + var status []string + + status = append(status, fmt.Sprintf(ColorBlue, branch.Name)) + + if branch.Upstream == "" { + ok = false + status = append(status, fmt.Sprintf(ColorYellow, git.StatusNoUpstream)) + } + + if branch.NeedsPull { + ok = false + status = append(status, fmt.Sprintf(ColorYellow, git.StatusBehind)) + } + + if branch.NeedsPush { + ok = false + status = append(status, fmt.Sprintf(ColorYellow, git.StatusAhead)) + } + + if ok { + status = append(status, fmt.Sprintf(ColorGreen, git.StatusOk)) + } + + return strings.Join(status, " ") +} diff --git a/pkg/tree.go b/print/tree.go similarity index 96% rename from pkg/tree.go rename to print/tree.go index e0df7e7..8fd884e 100644 --- a/pkg/tree.go +++ b/print/tree.go @@ -1,6 +1,7 @@ -package pkg +package print import ( + "git-get/git" "path/filepath" "strings" ) @@ -11,7 +12,7 @@ type Node struct { depth int // depth is a nesting depth used when rendering a tree, not an depth level of a node inside the tree parent *Node children []*Node - repo *Repo + repo *git.Repo } // Root creates a new root of a tree @@ -55,11 +56,11 @@ 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 []*git.Repo) *Node { tree := Root(root) for _, repo := range repos { - path := strings.TrimPrefix(repo.path, root) + path := strings.TrimPrefix(repo.Path, root) path = strings.Trim(path, string(filepath.Separator)) subs := strings.Split(path, string(filepath.Separator)) @@ -115,7 +116,7 @@ func RenderSmartTree(node *Node) string { // TODO: Ugly // If this is called from tests the repo will be nil and we should return just the name without the status. - if node.repo.repo == nil { + if node.repo.Repository == nil { return value } diff --git a/pkg/tree_test.go b/print/tree_test.go similarity index 93% rename from pkg/tree_test.go rename to print/tree_test.go index a31d384..0ad4dcd 100644 --- a/pkg/tree_test.go +++ b/print/tree_test.go @@ -1,7 +1,8 @@ -package pkg +package print import ( "fmt" + "git-get/git" "strings" "testing" ) @@ -90,9 +91,9 @@ gitlab.com/ } for i, test := range tests { - var repos []*Repo + var repos []*git.Repo for _, path := range test.paths { - repos = append(repos, newRepo(nil, path)) //&Repo{path: path}) + repos = append(repos, git.NewRepo(nil, path)) //&Repo{path: path}) } tree := BuildTree("root", repos)