mirror of
https://github.com/grdl/git-get.git
synced 2026-02-04 14:31:49 +00:00
Merge pull request #11 from grdl/pkg_structure
Improve packages structure
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"git-get/pkg/git/test"
|
||||
"git-get/pkg/run"
|
||||
"git-get/pkg/test"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -36,10 +37,11 @@ func Exists(path string) (bool, error) {
|
||||
return true, errDirectoryAccess
|
||||
}
|
||||
|
||||
// RepoFinder finds paths to git repos inside given path.
|
||||
// RepoFinder finds git repositories inside a given path.
|
||||
type RepoFinder struct {
|
||||
root string
|
||||
repos []string
|
||||
root string
|
||||
repos []*Repo
|
||||
errors []error
|
||||
}
|
||||
|
||||
// NewRepoFinder returns a RepoFinder pointed at given root path.
|
||||
@@ -49,51 +51,95 @@ func NewRepoFinder(root string) *RepoFinder {
|
||||
}
|
||||
}
|
||||
|
||||
// Find returns a sorted list of paths to git repos found inside a given root path.
|
||||
// Find finds git repositories inside a given root path.
|
||||
// It doesn't add repositories nested inside other git repos.
|
||||
// Returns error if root repo path can't be found or accessed.
|
||||
func (r *RepoFinder) Find() ([]string, error) {
|
||||
if _, err := Exists(r.root); err != nil {
|
||||
return nil, err
|
||||
func (f *RepoFinder) Find() error {
|
||||
if _, err := Exists(f.root); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
walkOpts := &godirwalk.Options{
|
||||
ErrorCallback: r.errorCb,
|
||||
Callback: r.walkCb,
|
||||
ErrorCallback: f.errorCb,
|
||||
Callback: f.walkCb,
|
||||
// Use Unsorted to improve speed because repos will be processed by goroutines in a random order anyway.
|
||||
Unsorted: true,
|
||||
}
|
||||
|
||||
err := godirwalk.Walk(r.root, walkOpts)
|
||||
err := godirwalk.Walk(f.root, walkOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
if len(r.repos) == 0 {
|
||||
return nil, fmt.Errorf("no git repos found in root path %s", r.root)
|
||||
if len(f.repos) == 0 {
|
||||
return fmt.Errorf("no git repos found in root path %s", f.root)
|
||||
}
|
||||
|
||||
return r.repos, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepoFinder) walkCb(path string, ent *godirwalk.Dirent) error {
|
||||
// LoadAll loads and returns sorted slice of statuses of all repositories found by RepoFinder.
|
||||
// If fetch equals true, it first fetches from the remote repo before loading the status.
|
||||
// Each repo is loaded concurrently in its own goroutine, with max 100 repos being loaded at the same time.
|
||||
func (f *RepoFinder) LoadAll(fetch bool) []*Status {
|
||||
var ss []*Status
|
||||
|
||||
loadedChan := make(chan *Status)
|
||||
|
||||
for _, repo := range f.repos {
|
||||
go func(repo *Repo) {
|
||||
loadedChan <- repo.LoadStatus(fetch)
|
||||
}(repo)
|
||||
}
|
||||
|
||||
for l := range loadedChan {
|
||||
ss = append(ss, l)
|
||||
|
||||
// Close the channel when all repos are loaded.
|
||||
if len(ss) == len(f.repos) {
|
||||
close(loadedChan)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the status slice by path
|
||||
sort.Slice(ss, func(i, j int) bool {
|
||||
return strings.Compare(ss[i].path, ss[j].path) < 0
|
||||
})
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
func (f *RepoFinder) walkCb(path string, ent *godirwalk.Dirent) error {
|
||||
// Do not traverse .git directories
|
||||
if ent.IsDir() && ent.Name() == ".git" {
|
||||
r.repos = append(r.repos, strings.TrimSuffix(path, ".git"))
|
||||
if ent.IsDir() && ent.Name() == dotgit {
|
||||
f.addIfOk(path)
|
||||
return errSkipNode
|
||||
}
|
||||
|
||||
// Do not traverse directories containing a .git directory
|
||||
if ent.IsDir() {
|
||||
_, err := os.Stat(filepath.Join(path, ".git"))
|
||||
_, err := os.Stat(filepath.Join(path, dotgit))
|
||||
if err == nil {
|
||||
r.repos = append(r.repos, strings.TrimSuffix(path, ".git"))
|
||||
f.addIfOk(path)
|
||||
return errSkipNode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepoFinder) errorCb(_ string, err error) godirwalk.ErrorAction {
|
||||
// addIfOk adds the found repo to the repos slice if it can be opened.
|
||||
// If repo path can't be accessed it will add an error to the errors slice.
|
||||
func (f *RepoFinder) addIfOk(path string) {
|
||||
repo, err := Open(strings.TrimSuffix(path, dotgit))
|
||||
if err != nil {
|
||||
f.errors = append(f.errors, err)
|
||||
return
|
||||
}
|
||||
|
||||
f.repos = append(f.repos, repo)
|
||||
}
|
||||
|
||||
func (f *RepoFinder) errorCb(_ string, err error) godirwalk.ErrorAction {
|
||||
// Skip .git directory and directories we don't have permissions to access
|
||||
// TODO: Will syscall.EACCES work on windows?
|
||||
if errors.Is(err, errSkipNode) || errors.Is(err, syscall.EACCES) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"git-get/pkg/test"
|
||||
"git-get/pkg/git/test"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -38,10 +38,10 @@ func TestFinder(t *testing.T) {
|
||||
root := test.reposMaker(t)
|
||||
|
||||
finder := NewRepoFinder(root)
|
||||
paths, _ := finder.Find()
|
||||
finder.Find()
|
||||
|
||||
if len(paths) != test.want {
|
||||
t.Errorf("expected %d; got %d", test.want, len(paths))
|
||||
if len(finder.repos) != test.want {
|
||||
t.Errorf("expected %d; got %d", test.want, len(finder.repos))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,24 +15,12 @@ const (
|
||||
head = "HEAD"
|
||||
)
|
||||
|
||||
// Repo represents a git repository on disk.
|
||||
type Repo interface {
|
||||
Path() string
|
||||
Branches() ([]string, error)
|
||||
CurrentBranch() (string, error)
|
||||
Fetch() error
|
||||
Remote() (string, error)
|
||||
Uncommitted() (int, error)
|
||||
Untracked() (int, error)
|
||||
Upstream(string) (string, error)
|
||||
AheadBehind(string, string) (int, int, error)
|
||||
}
|
||||
|
||||
type repo struct {
|
||||
// Repo represents a git Repository cloned or initialized on disk.
|
||||
type Repo struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// CloneOpts specify detail about repository to clone.
|
||||
// CloneOpts specify detail about Repository to clone.
|
||||
type CloneOpts struct {
|
||||
URL *url.URL
|
||||
Path string // TODO: should Path be a part of clone opts?
|
||||
@@ -41,18 +29,18 @@ type CloneOpts struct {
|
||||
}
|
||||
|
||||
// Open checks if given path can be accessed and returns a Repo instance pointing to it.
|
||||
func Open(path string) (Repo, error) {
|
||||
func Open(path string) (*Repo, error) {
|
||||
if _, err := Exists(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &repo{
|
||||
return &Repo{
|
||||
path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Clone clones repository specified with CloneOpts.
|
||||
func Clone(opts *CloneOpts) (Repo, error) {
|
||||
// Clone clones Repository specified with CloneOpts.
|
||||
func Clone(opts *CloneOpts) (*Repo, error) {
|
||||
runGit := run.Git("clone", opts.URL.String(), opts.Path)
|
||||
if opts.Branch != "" {
|
||||
runGit = run.Git("clone", "--branch", opts.Branch, "--single-branch", opts.URL.String(), opts.Path)
|
||||
@@ -69,19 +57,19 @@ func Clone(opts *CloneOpts) (Repo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo, err := Open(opts.Path)
|
||||
return repo, err
|
||||
Repo, err := Open(opts.Path)
|
||||
return Repo, err
|
||||
}
|
||||
|
||||
// Fetch preforms a git fetch on all remotes
|
||||
func (r *repo) Fetch() error {
|
||||
func (r *Repo) Fetch() error {
|
||||
err := run.Git("fetch", "--all").OnRepo(r.path).AndShutUp()
|
||||
return err
|
||||
}
|
||||
|
||||
// Uncommitted returns the number of uncommitted files in the repository.
|
||||
// Uncommitted returns the number of uncommitted files in the Repository.
|
||||
// Only tracked files are not counted.
|
||||
func (r *repo) Uncommitted() (int, error) {
|
||||
func (r *Repo) Uncommitted() (int, error) {
|
||||
out, err := run.Git("status", "--ignore-submodules", "--porcelain").OnRepo(r.path).AndCaptureLines()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -98,8 +86,8 @@ func (r *repo) Uncommitted() (int, error) {
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Untracked returns the number of untracked files in the repository.
|
||||
func (r *repo) Untracked() (int, error) {
|
||||
// Untracked returns the number of untracked files in the Repository.
|
||||
func (r *Repo) Untracked() (int, error) {
|
||||
out, err := run.Git("status", "--ignore-submodules", "--untracked-files=all", "--porcelain").OnRepo(r.path).AndCaptureLines()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -115,9 +103,9 @@ func (r *repo) Untracked() (int, error) {
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CurrentBranch returns the short name currently checked-out branch for the repository.
|
||||
// If repo is in a detached head state, it will return "HEAD".
|
||||
func (r *repo) CurrentBranch() (string, error) {
|
||||
// CurrentBranch returns the short name currently checked-out branch for the Repository.
|
||||
// If Repo is in a detached head state, it will return "HEAD".
|
||||
func (r *Repo) CurrentBranch() (string, error) {
|
||||
out, err := run.Git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD").OnRepo(r.path).AndCaptureLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -126,8 +114,8 @@ func (r *repo) CurrentBranch() (string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Branches returns a list of local branches in the repository.
|
||||
func (r *repo) Branches() ([]string, error) {
|
||||
// Branches returns a list of local branches in the Repository.
|
||||
func (r *Repo) Branches() ([]string, error) {
|
||||
out, err := run.Git("branch", "--format=%(refname:short)").OnRepo(r.path).AndCaptureLines()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -146,7 +134,7 @@ func (r *repo) Branches() ([]string, error) {
|
||||
|
||||
// Upstream returns the name of an upstream branch if a given branch is tracking one.
|
||||
// Otherwise it returns an empty string.
|
||||
func (r *repo) Upstream(branch string) (string, error) {
|
||||
func (r *Repo) Upstream(branch string) (string, error) {
|
||||
out, err := run.Git("rev-parse", "--abbrev-ref", "--symbolic-full-name", fmt.Sprintf("%s@{upstream}", branch)).OnRepo(r.path).AndCaptureLine()
|
||||
if err != nil {
|
||||
// TODO: no upstream will also throw an error.
|
||||
@@ -157,7 +145,7 @@ func (r *repo) Upstream(branch string) (string, error) {
|
||||
}
|
||||
|
||||
// AheadBehind returns the number of commits a given branch is ahead and/or behind the upstream.
|
||||
func (r *repo) AheadBehind(branch string, upstream string) (int, int, error) {
|
||||
func (r *Repo) AheadBehind(branch string, upstream string) (int, int, error) {
|
||||
out, err := run.Git("rev-list", "--left-right", "--count", fmt.Sprintf("%s...%s", branch, upstream)).OnRepo(r.path).AndCaptureLine()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
@@ -179,8 +167,8 @@ func (r *repo) AheadBehind(branch string, upstream string) (int, int, error) {
|
||||
return ahead, behind, nil
|
||||
}
|
||||
|
||||
// Remote returns URL of remote repository.
|
||||
func (r *repo) Remote() (string, error) {
|
||||
// Remote returns URL of remote Repository.
|
||||
func (r *Repo) Remote() (string, error) {
|
||||
// https://stackoverflow.com/a/16880000/1085632
|
||||
out, err := run.Git("ls-remote", "--get-url").OnRepo(r.path).AndCaptureLine()
|
||||
if err != nil {
|
||||
@@ -191,7 +179,7 @@ func (r *repo) Remote() (string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Path returns path to the repository.
|
||||
func (r *repo) Path() string {
|
||||
// Path returns path to the Repository.
|
||||
func (r *Repo) Path() string {
|
||||
return r.path
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"git-get/pkg/test"
|
||||
"git-get/pkg/git/test"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -31,7 +31,7 @@ func TestUncommitted(t *testing.T) {
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "single tracked ",
|
||||
name: "single tracked",
|
||||
repoMaker: test.RepoWithStaged,
|
||||
want: 1,
|
||||
},
|
||||
|
||||
@@ -1,67 +1,62 @@
|
||||
package pkg
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git-get/pkg/git"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Loaded represents a repository which status is Loaded from disk and stored for printing.
|
||||
type Loaded struct {
|
||||
// Status contains human readable (and printable) representation of a git repository status.
|
||||
type Status struct {
|
||||
path string
|
||||
current string
|
||||
branches map[string]string // key: branch name, value: branch status
|
||||
worktree string
|
||||
remote string
|
||||
errors []string
|
||||
errors []string // Slice of errors which occurred when loading the status.
|
||||
}
|
||||
|
||||
// Load reads status of a repository at a given path.
|
||||
func Load(path string, fetch bool) *Loaded {
|
||||
loaded := &Loaded{
|
||||
path: path,
|
||||
// LoadStatus reads status of a repository.
|
||||
// If fetch equals true, it first fetches from the remote repo before loading the status.
|
||||
// If errors occur during loading, they are stored in Status.errors slice.
|
||||
func (r *Repo) LoadStatus(fetch bool) *Status {
|
||||
status := &Status{
|
||||
path: r.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
|
||||
}
|
||||
|
||||
if fetch {
|
||||
err = repo.Fetch()
|
||||
if err != nil {
|
||||
loaded.errors = append(loaded.errors, err.Error())
|
||||
if err := r.Fetch(); err != nil {
|
||||
status.errors = append(status.errors, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
loaded.current, err = repo.CurrentBranch()
|
||||
var err error
|
||||
status.current, err = r.CurrentBranch()
|
||||
if err != nil {
|
||||
loaded.errors = append(loaded.errors, err.Error())
|
||||
status.errors = append(status.errors, err.Error())
|
||||
}
|
||||
|
||||
var errs []error
|
||||
loaded.branches, errs = loadBranches(repo)
|
||||
status.branches, errs = r.loadBranches()
|
||||
for _, err := range errs {
|
||||
loaded.errors = append(loaded.errors, err.Error())
|
||||
status.errors = append(status.errors, err.Error())
|
||||
}
|
||||
|
||||
loaded.worktree, err = loadWorkTree(repo)
|
||||
status.worktree, err = r.loadWorkTree()
|
||||
if err != nil {
|
||||
loaded.errors = append(loaded.errors, err.Error())
|
||||
status.errors = append(status.errors, err.Error())
|
||||
}
|
||||
|
||||
loaded.remote, err = repo.Remote()
|
||||
status.remote, err = r.Remote()
|
||||
if err != nil {
|
||||
loaded.errors = append(loaded.errors, err.Error())
|
||||
status.errors = append(status.errors, err.Error())
|
||||
}
|
||||
|
||||
return loaded
|
||||
return status
|
||||
}
|
||||
|
||||
func loadBranches(r git.Repo) (map[string]string, []error) {
|
||||
func (r *Repo) loadBranches() (map[string]string, []error) {
|
||||
statuses := make(map[string]string)
|
||||
errors := make([]error, 0)
|
||||
|
||||
@@ -72,7 +67,7 @@ func loadBranches(r git.Repo) (map[string]string, []error) {
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
status, err := loadBranchStatus(r, branch)
|
||||
status, err := r.loadBranchStatus(branch)
|
||||
statuses[branch] = status
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
@@ -82,7 +77,7 @@ func loadBranches(r git.Repo) (map[string]string, []error) {
|
||||
return statuses, errors
|
||||
}
|
||||
|
||||
func loadBranchStatus(r git.Repo, branch string) (string, error) {
|
||||
func (r *Repo) loadBranchStatus(branch string) (string, error) {
|
||||
upstream, err := r.Upstream(branch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -112,7 +107,7 @@ func loadBranchStatus(r git.Repo, branch string) (string, error) {
|
||||
return strings.Join(res, " "), nil
|
||||
}
|
||||
|
||||
func loadWorkTree(r git.Repo) (string, error) {
|
||||
func (r *Repo) loadWorkTree() (string, error) {
|
||||
uncommitted, err := r.Uncommitted()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -139,20 +134,20 @@ func loadWorkTree(r git.Repo) (string, error) {
|
||||
}
|
||||
|
||||
// Path returns path to a repository.
|
||||
func (r *Loaded) Path() string {
|
||||
return r.path
|
||||
func (s *Status) Path() string {
|
||||
return s.path
|
||||
}
|
||||
|
||||
// Current returns the name of currently checked out branch (or tag or detached HEAD).
|
||||
func (r *Loaded) Current() string {
|
||||
return r.current
|
||||
func (s *Status) Current() string {
|
||||
return s.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 {
|
||||
func (s *Status) Branches() []string {
|
||||
var branches []string
|
||||
for b := range r.branches {
|
||||
if b != r.current {
|
||||
for b := range s.branches {
|
||||
if b != s.current {
|
||||
branches = append(branches, b)
|
||||
}
|
||||
}
|
||||
@@ -160,21 +155,21 @@ func (r *Loaded) Branches() []string {
|
||||
}
|
||||
|
||||
// BranchStatus returns status of a given branch
|
||||
func (r *Loaded) BranchStatus(branch string) string {
|
||||
return r.branches[branch]
|
||||
func (s *Status) BranchStatus(branch string) string {
|
||||
return s.branches[branch]
|
||||
}
|
||||
|
||||
// WorkTreeStatus returns status of a worktree
|
||||
func (r *Loaded) WorkTreeStatus() string {
|
||||
return r.worktree
|
||||
func (s *Status) WorkTreeStatus() string {
|
||||
return s.worktree
|
||||
}
|
||||
|
||||
// Remote returns URL to remote repository
|
||||
func (r *Loaded) Remote() string {
|
||||
return r.remote
|
||||
func (s *Status) Remote() string {
|
||||
return s.remote
|
||||
}
|
||||
|
||||
// Errors is a slice of errors that occurred when loading repo status
|
||||
func (r *Loaded) Errors() []string {
|
||||
return r.errors
|
||||
func (s *Status) Errors() []string {
|
||||
return s.errors
|
||||
}
|
||||
44
pkg/list.go
44
pkg/list.go
@@ -5,7 +5,6 @@ import (
|
||||
"git-get/pkg/cfg"
|
||||
"git-get/pkg/git"
|
||||
"git-get/pkg/print"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -18,16 +17,15 @@ type ListCfg struct {
|
||||
|
||||
// List executes the "git list" command.
|
||||
func List(c *ListCfg) error {
|
||||
paths, err := git.NewRepoFinder(c.Root).Find()
|
||||
if err != nil {
|
||||
finder := git.NewRepoFinder(c.Root)
|
||||
if err := finder.Find(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loaded := loadAll(paths, c.Fetch)
|
||||
|
||||
printables := make([]print.Printable, len(loaded))
|
||||
for i := range loaded {
|
||||
printables[i] = loaded[i]
|
||||
statuses := finder.LoadAll(c.Fetch)
|
||||
printables := make([]print.Printable, len(statuses))
|
||||
for i := range statuses {
|
||||
printables[i] = statuses[i]
|
||||
}
|
||||
|
||||
switch c.Output {
|
||||
@@ -43,33 +41,3 @@ func List(c *ListCfg) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadAll runs a separate goroutine to open, fetch (if asked to) and load status of git repo
|
||||
func loadAll(paths []string, fetch bool) []*Loaded {
|
||||
var ll []*Loaded
|
||||
|
||||
loadedChan := make(chan *Loaded)
|
||||
|
||||
for _, path := range paths {
|
||||
go func(path string) {
|
||||
|
||||
loadedChan <- Load(path, fetch)
|
||||
}(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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user