6
0
mirror of https://github.com/grdl/git-get.git synced 2026-02-04 18:34:51 +00:00

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
This commit is contained in:
Grzegorz Dlugoszewski
2020-06-08 12:07:03 +02:00
parent 29c21cb78d
commit f3d0df1bfd
14 changed files with 318 additions and 261 deletions

View File

@@ -1,4 +1,4 @@
package pkg package cfg
import ( import (
"path" "path"

View File

@@ -1,4 +1,4 @@
package pkg package cfg
import ( import (
"os" "os"
@@ -156,3 +156,9 @@ func TestConfig(t *testing.T) {
checkFatal(t, err) checkFatal(t, err)
} }
} }
func checkFatal(t *testing.T, err error) {
if err != nil {
t.Fatalf("%+v", err)
}
}

View File

@@ -2,9 +2,14 @@ package main
import ( import (
"fmt" "fmt"
"git-get/pkg" "git-get/cfg"
"git-get/git"
"git-get/path"
"git-get/print"
"os" "os"
pathpkg "path"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@@ -27,30 +32,37 @@ var list bool
func init() { func init() {
cmd.PersistentFlags().BoolVarP(&list, "list", "l", false, "Lists all repositories inside git-get root") 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(cfg.KeyReposRoot, "r", "", "repos root")
cmd.PersistentFlags().StringP(pkg.KeyPrivateKey, "p", "", "SSH private key path") cmd.PersistentFlags().StringP(cfg.KeyPrivateKey, "p", "", "SSH private key path")
viper.BindPFlag(pkg.KeyReposRoot, cmd.PersistentFlags().Lookup(pkg.KeyReposRoot)) viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot))
viper.BindPFlag(pkg.KeyPrivateKey, cmd.PersistentFlags().Lookup(pkg.KeyReposRoot)) viper.BindPFlag(cfg.KeyPrivateKey, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot))
} }
func Run(cmd *cobra.Command, args []string) { func Run(cmd *cobra.Command, args []string) {
pkg.InitConfig() cfg.InitConfig()
root := viper.GetString(cfg.KeyReposRoot)
if list { if list {
paths, err := pkg.FindRepos() paths, err := path.FindRepos()
exitIfError(err) exitIfError(err)
repos, err := pkg.OpenAll(paths) repos, err := path.OpenAll(paths)
exitIfError(err) 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) os.Exit(0)
} }
url, err := pkg.ParseURL(args[0]) url, err := path.ParseURL(args[0])
exitIfError(err) 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) exitIfError(err)
} }

View File

@@ -1,12 +1,13 @@
package pkg package git
import ( import (
"fmt" "fmt"
"git-get/cfg"
"io" "io"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os" "os"
"path"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -18,18 +19,16 @@ import (
) )
type Repo struct { type Repo struct {
repo *git.Repository *git.Repository
path string Path string
Status *RepoStatus Status *RepoStatus
} }
func CloneRepo(url *url.URL, reposRoot string, quiet bool) (*Repo, error) { func CloneRepo(url *url.URL, path string, quiet bool) (*Repo, error) {
repoPath := path.Join(reposRoot, URLToPath(url))
var progress io.Writer var progress io.Writer
if !quiet { if !quiet {
progress = os.Stdout progress = os.Stdout
fmt.Printf("Cloning into '%s'...\n", repoPath) fmt.Printf("Cloning into '%s'...\n", path)
} }
// TODO: can this be cleaner? // TODO: can this be cleaner?
@@ -54,12 +53,12 @@ func CloneRepo(url *url.URL, reposRoot string, quiet bool) (*Repo, error) {
Tags: git.AllTags, Tags: git.AllTags,
} }
repo, err := git.PlainClone(repoPath, false, opts) repo, err := git.PlainClone(path, false, opts)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Failed cloning repo") return nil, errors.Wrap(err, "Failed cloning repo")
} }
return newRepo(repo, repoPath), nil return NewRepo(repo, path), nil
} }
func OpenRepo(repoPath string) (*Repo, error) { 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 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{ return &Repo{
repo: repo, Repository: repo,
path: repoPath, Path: repoPath,
Status: &RepoStatus{}, Status: &RepoStatus{},
} }
} }
// Fetch performs a git fetch on all remotes // Fetch performs a git fetch on all remotes
func (r *Repo) Fetch() error { func (r *Repo) Fetch() error {
remotes, err := r.repo.Remotes() remotes, err := r.Remotes()
if err != nil { if err != nil {
return errors.Wrap(err, "Failed getting remotes") return errors.Wrap(err, "Failed getting remotes")
} }
@@ -97,7 +96,7 @@ func (r *Repo) Fetch() error {
} }
func sshKeyAuth() (transport.AuthMethod, error) { func sshKeyAuth() (transport.AuthMethod, error) {
privateKey := viper.GetString(KeyPrivateKey) privateKey := viper.GetString(cfg.KeyPrivateKey)
sshKey, err := ioutil.ReadFile(privateKey) sshKey, err := ioutil.ReadFile(privateKey)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "Failed to open ssh private key %s", privateKey) 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} auth := &go_git_ssh.PublicKeys{User: "git", Signer: signer}
return auth, nil 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
}

View File

@@ -1,6 +1,8 @@
package pkg package git
import ( import (
"net/url"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
@@ -25,7 +27,7 @@ func newRepoEmpty(t *testing.T) *Repo {
repo, err := git.PlainInit(dir, false) repo, err := git.PlainInit(dir, false)
checkFatal(t, err) checkFatal(t, err)
return newRepo(repo, dir) return NewRepo(repo, dir)
} }
func newRepoWithUntracked(t *testing.T) *Repo { 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) { 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")) checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
file, err := wt.Filesystem.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 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) { 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")) checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
_, err = wt.Add(name) _, 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 { 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")) checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
opts := &git.CommitOptions{ 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) { func (r *Repo) newBranch(t *testing.T, name string) {
head, err := r.repo.Head() head, err := r.Head()
checkFatal(t, err) checkFatal(t, err)
ref := plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), head.Hash()) ref := plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), head.Hash())
err = r.repo.Storer.SetReference(ref) err = r.Storer.SetReference(ref)
checkFatal(t, err) checkFatal(t, err)
} }
func (r *Repo) clone(t *testing.T) *Repo { func (r *Repo) clone(t *testing.T) *Repo {
dir := newTempDir(t) dir := newTempDir(t)
url, err := ParseURL("file://" + r.path) repoURL, err := url.Parse("file://" + r.Path)
checkFatal(t, err) checkFatal(t, err)
repo, err := CloneRepo(url, dir, true) repo, err := CloneRepo(repoURL, dir, true)
checkFatal(t, err) checkFatal(t, err)
return repo return repo
@@ -218,7 +220,7 @@ func (r *Repo) fetch(t *testing.T) {
} }
func (r *Repo) checkoutBranch(t *testing.T, name string) { 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")) checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
opts := &git.CheckoutOptions{ 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) { 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")) checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
opts := &git.CheckoutOptions{ opts := &git.CheckoutOptions{

View File

@@ -1,4 +1,4 @@
package pkg package git
import ( import (
"sort" "sort"
@@ -39,7 +39,7 @@ type BranchStatus struct {
} }
func (r *Repo) LoadStatus() error { func (r *Repo) LoadStatus() error {
wt, err := r.repo.Worktree() wt, err := r.Worktree()
if err != nil { if err != nil {
return errors.Wrap(err, "Failed getting worktree") return errors.Wrap(err, "Failed getting worktree")
} }
@@ -105,7 +105,7 @@ func hasUncommitted(status git.Status) bool {
} }
func currentBranch(r *Repo) string { func currentBranch(r *Repo) string {
head, err := r.repo.Head() head, err := r.Head()
if err != nil { if err != nil {
return StatusUnknown return StatusUnknown
} }
@@ -118,7 +118,7 @@ func currentBranch(r *Repo) string {
} }
func (r *Repo) loadBranchesStatus() error { func (r *Repo) loadBranchesStatus() error {
iter, err := r.repo.Branches() iter, err := r.Branches()
if err != nil { if err != nil {
return errors.Wrap(err, "Failed getting branches iterator") 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) // "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) // "merge" - full ref name of the upstream branch (eg, ref/heads/master)
func (r *Repo) upstream(branch string) (string, error) { func (r *Repo) upstream(branch string) (string, error) {
cfg, err := r.repo.Config() cfg, err := r.Config()
if err != nil { if err != nil {
return "", errors.Wrap(err, "Failed getting repo config") 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) { 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 { if err != nil {
return false, false, errors.Wrapf(err, "Failed resolving revision %s", localBranch) 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 { if err != nil {
return false, false, errors.Wrapf(err, "Failed resolving revision %s", upstreamBranch) 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 { if err != nil {
return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", localHash.String()) 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 { if err != nil {
return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", upstreamHash.String()) return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", upstreamHash.String())
} }

View File

@@ -1,4 +1,4 @@
package pkg package git
import ( import (
"reflect" "reflect"

103
path/list.go Normal file
View File

@@ -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
}

View File

@@ -1,6 +1,7 @@
package pkg package path
import ( import (
"git-get/cfg"
urlpkg "net/url" urlpkg "net/url"
"path" "path"
"regexp" "regexp"
@@ -46,7 +47,7 @@ func ParseURL(rawURL string) (url *urlpkg.URL, err error) {
// Default to configured defaultHost when host is empty // Default to configured defaultHost when host is empty
if url.Host == "" { if url.Host == "" {
url.Host = viper.GetString(KeyDefaultHost) url.Host = viper.GetString(cfg.KeyDefaultHost)
} }
// Default to https when scheme is empty // Default to https when scheme is empty

View File

@@ -1,6 +1,7 @@
package pkg package path
import ( import (
"git-get/cfg"
"testing" "testing"
) )
@@ -49,7 +50,7 @@ func TestURLParse(t *testing.T) {
} }
// We need to init config first so the default values are correctly loaded // We need to init config first so the default values are correctly loaded
InitConfig() cfg.InitConfig()
for _, test := range tests { for _, test := range tests {
url, err := ParseURL(test.in) url, err := ParseURL(test.in)

View File

@@ -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
}

115
print/print.go Normal file
View File

@@ -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, " ")
}

View File

@@ -1,6 +1,7 @@
package pkg package print
import ( import (
"git-get/git"
"path/filepath" "path/filepath"
"strings" "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 depth int // depth is a nesting depth used when rendering a tree, not an depth level of a node inside the tree
parent *Node parent *Node
children []*Node children []*Node
repo *Repo repo *git.Repo
} }
// Root creates a new root of a tree // 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. // BuildTree builds a directory tree of paths to repositories.
// Each node represents a directory in the repo path. // Each node represents a directory in the repo path.
// Each leaf (final node) contains a pointer to the repo. // 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) tree := Root(root)
for _, repo := range repos { for _, repo := range repos {
path := strings.TrimPrefix(repo.path, root) path := strings.TrimPrefix(repo.Path, root)
path = strings.Trim(path, string(filepath.Separator)) path = strings.Trim(path, string(filepath.Separator))
subs := strings.Split(path, string(filepath.Separator)) subs := strings.Split(path, string(filepath.Separator))
@@ -115,7 +116,7 @@ func RenderSmartTree(node *Node) string {
// TODO: Ugly // TODO: Ugly
// If this is called from tests the repo will be nil and we should return just the name without the status. // 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 return value
} }

View File

@@ -1,7 +1,8 @@
package pkg package print
import ( import (
"fmt" "fmt"
"git-get/git"
"strings" "strings"
"testing" "testing"
) )
@@ -90,9 +91,9 @@ gitlab.com/
} }
for i, test := range tests { for i, test := range tests {
var repos []*Repo var repos []*git.Repo
for _, path := range test.paths { 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) tree := BuildTree("root", repos)