diff --git a/domain/settings.go b/domain/settings.go index 2f9c9ee..453f0d5 100644 --- a/domain/settings.go +++ b/domain/settings.go @@ -1,11 +1,12 @@ package domain type Settings struct { - Cache *bool `json:"cache,omitempty"` - Finder Finder `json:"finder"` - Icons Icons `json:"icons"` - Theme Theme `json:"theme"` - Verbose bool `json:"verbose,omitempty"` + Cache *bool `json:"cache,omitempty"` + Finder Finder `json:"finder"` + Icons Icons `json:"icons"` + Theme Theme `json:"theme"` + Verbose bool `json:"verbose,omitempty"` + WorkerCount int `json:"workerCount,omitempty"` } type Finder struct { diff --git a/internal/cli/clone/handler.go b/internal/cli/clone/handler.go index 49156ba..94a2969 100644 --- a/internal/cli/clone/handler.go +++ b/internal/cli/clone/handler.go @@ -3,7 +3,9 @@ package clone import ( "fmt" "os" + "sync" + "github.com/charmbracelet/lipgloss" log "github.com/sirupsen/logrus" "github.com/rafi/gits/domain" @@ -24,7 +26,9 @@ func ExecClone(args []string, deps types.RuntimeCLI) error { if repo != nil { // Clone a single repository. - return cloneRepo(project, *repo, deps) + resp := cloneRepo(project, *repo, deps) + fmt.Println(resp) + return err } // Clone all project's repositories. @@ -35,25 +39,54 @@ func ExecClone(args []string, deps types.RuntimeCLI) error { return nil } +type CloneResponse struct { + output string + title lipgloss.Style + error error + errorStyle lipgloss.Style +} + +func (r CloneResponse) String() string { + if r.error != nil { + return fmt.Sprintf("%s %s", r.title, r.errorStyle.Render(r.error.Error())) + } + + return fmt.Sprintf( + "%s %s", + r.title.Render(), + r.output, + ) +} + func cloneProjectRepos(project domain.Project, deps types.RuntimeCLI) []error { fmt.Println(cli.ProjectTitleWithBullet(project, deps.Theme)) errList := make([]error, 0) + maxLen := cli.GetMaxLen(project) + if project.Clone != nil && !*project.Clone { log.Warn("Skipping clone due to config") return nil } - if project.Path == "" { - log.Warn("Skipping clone due to missing path") - return nil - } - for _, repo := range project.Repos { - err := cloneRepo(project, repo, deps) - if err != nil { - errList = append(errList, err) + var wg sync.WaitGroup + for idx, repo := range project.Repos { + wg.Add(1) + go func() { + defer wg.Done() + resp := cloneRepo(project, repo, deps) + resp.title.Width(maxLen) + fmt.Println(resp) + if resp.error != nil { + errList = append(errList, resp.error) + } + }() + if idx > 0 && idx%deps.Settings.WorkerCount == 0 { + wg.Wait() } } + wg.Wait() + for _, subProject := range project.SubProjects { fmt.Println() errs := cloneProjectRepos(subProject, deps) @@ -62,28 +95,28 @@ func cloneProjectRepos(project domain.Project, deps types.RuntimeCLI) []error { return errList } -func cloneRepo(project domain.Project, repo domain.Repository, deps types.RuntimeCLI) error { - maxLen := cli.GetMaxLen(project) - repoTitle := cli.RepoTitle(project, repo, deps.HomeDir, deps.Theme). - Width(maxLen). - Render() - - fmt.Printf("%s ", repoTitle) - defer fmt.Println() +func cloneRepo(project domain.Project, repo domain.Repository, deps types.RuntimeCLI) CloneResponse { + resp := CloneResponse{ + title: cli.RepoTitle(repo, project.AbsPath, deps.HomeDir, deps.Theme), + errorStyle: deps.Theme.Error, + } if repo.State == domain.RepoStateError { - return cli.AbortOnRepoState(repo, deps.Theme.Error) + resp.error = cli.AbortOnRepoState(repo, deps.Theme.Error) + return resp } if _, err := os.Stat(repo.AbsPath); !os.IsNotExist(err) { repoPath := cli.Path(repo.AbsPath, deps.HomeDir) - return types.NewWarning("already cloned at %s", repoPath) + resp.error = types.NewWarning("already cloned at %s", repoPath) + return resp } - out, err := deps.Git.Clone(repo.Src, repo.AbsPath) - fmt.Print(deps.Theme.GitOutput.Render(out)) + var err error + resp.output, err = deps.Git.Clone(repo.Src, repo.AbsPath) if err != nil { - fmt.Print(deps.Theme.Error.Render(err.Error())) - return cli.RepoError(err, repo) + resp.error = cli.RepoError(err, repo) + return resp } - return nil + resp.output = deps.Theme.GitOutput.Render(resp.output) + return resp } diff --git a/internal/cli/config/file.go b/internal/cli/config/file.go index 0af104f..30636b8 100644 --- a/internal/cli/config/file.go +++ b/internal/cli/config/file.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "github.com/charmbracelet/lipgloss" "github.com/knadh/koanf/parsers/json" @@ -129,6 +130,10 @@ func (f *File) loadConfig(filePath string) error { return fmt.Errorf("unable to parse config file: %w", err) } + if f.Settings.WorkerCount == 0 { + f.Settings.WorkerCount = max(runtime.NumCPU()/2, 2) + } + // Set never/always color toggle. switch f.Color { case ColorOptionNever.String(): diff --git a/internal/cli/fetch/handler.go b/internal/cli/fetch/handler.go index 13be733..14fc696 100644 --- a/internal/cli/fetch/handler.go +++ b/internal/cli/fetch/handler.go @@ -2,6 +2,9 @@ package fetch import ( "fmt" + "sync" + + "github.com/charmbracelet/lipgloss" "github.com/rafi/gits/domain" "github.com/rafi/gits/internal/cli" @@ -21,7 +24,9 @@ func ExecFetch(args []string, deps types.RuntimeCLI) error { if repo != nil { // Fetch a single repository. - return fetchRepo(project, *repo, deps) + resp := fetchRepo(project, *repo, deps) + fmt.Println(resp) + return err } // Fetch all project's repositories. @@ -32,16 +37,49 @@ func ExecFetch(args []string, deps types.RuntimeCLI) error { return nil } +type FetchResponse struct { + repoPath string + output string + title lipgloss.Style + error error + errorStyle lipgloss.Style +} + +func (r FetchResponse) String() string { + if r.error != nil { + return fmt.Sprintf("%s %s", r.title, r.errorStyle.Render(r.error.Error())) + } + + if r.title.Value() == r.repoPath { + return fmt.Sprintf("%s %s", r.title, r.output) + } + return fmt.Sprintf("%s %s %s", r.title, r.repoPath, r.output) +} + func fetchProjectRepos(project domain.Project, deps types.RuntimeCLI) []error { fmt.Println(cli.ProjectTitleWithBullet(project, deps.Theme)) errList := make([]error, 0) - for _, repo := range project.Repos { - err := fetchRepo(project, repo, deps) - if err != nil { - errList = append(errList, err) + maxLen := cli.GetMaxLen(project) + + var wg sync.WaitGroup + for idx, repo := range project.Repos { + wg.Add(1) + go func() { + defer wg.Done() + resp := fetchRepo(project, repo, deps) + resp.title.Width(maxLen) + fmt.Println(resp) + if resp.error != nil { + errList = append(errList, resp.error) + } + }() + if idx > 0 && idx%deps.Settings.WorkerCount == 0 { + wg.Wait() } } + wg.Wait() + for _, subProject := range project.SubProjects { fmt.Println() errs := fetchProjectRepos(subProject, deps) @@ -50,29 +88,25 @@ func fetchProjectRepos(project domain.Project, deps types.RuntimeCLI) []error { return errList } -func fetchRepo(project domain.Project, repo domain.Repository, deps types.RuntimeCLI) error { - maxLen := cli.GetMaxLen(project) - repoTitle := cli.RepoTitle(project, repo, deps.HomeDir, deps.Theme). - Width(maxLen) - - repoPath := cli.Path(repo.AbsPath, deps.HomeDir) - if repoTitle.Value() == repoPath { - fmt.Printf("%s ", repoTitle) - } else { - fmt.Printf("%s %s ", repoTitle, repoPath) +func fetchRepo(project domain.Project, repo domain.Repository, deps types.RuntimeCLI) FetchResponse { + resp := FetchResponse{ + title: cli.RepoTitle(repo, project.AbsPath, deps.HomeDir, deps.Theme), + repoPath: cli.Path(repo.AbsPath, deps.HomeDir), + errorStyle: deps.Theme.Error, } - defer fmt.Println() // Abort if repository is not cloned or has errors. if repo.State != domain.RepoStateOK { - return cli.AbortOnRepoState(repo, deps.Theme.Error) + resp.error = cli.AbortOnRepoState(repo, deps.Theme.Error) + return resp } - out, err := deps.Git.Fetch(repo.AbsPath) - fmt.Print(deps.Theme.GitOutput.Render(out)) + var err error + resp.output, err = deps.Git.Fetch(repo.AbsPath) if err != nil { - fmt.Print(deps.Theme.Error.Render(err.Error())) - return cli.RepoError(err, repo) + resp.error = cli.RepoError(err, repo) + return resp } - return nil + resp.output = deps.Theme.GitOutput.Render(resp.output) + return resp } diff --git a/internal/cli/pull/handler.go b/internal/cli/pull/handler.go index 9d27ae4..15c1af1 100644 --- a/internal/cli/pull/handler.go +++ b/internal/cli/pull/handler.go @@ -2,6 +2,9 @@ package pull import ( "fmt" + "sync" + + "github.com/charmbracelet/lipgloss" "github.com/rafi/gits/domain" "github.com/rafi/gits/internal/cli" @@ -21,7 +24,9 @@ func ExecPull(args []string, deps types.RuntimeCLI) error { if repo != nil { // Pull a single repository. - return pullRepo(project, *repo, deps) + resp := pullRepo(project, *repo, deps) + fmt.Println(resp) + return resp.error } // Pull all project's repositories. @@ -32,16 +37,53 @@ func ExecPull(args []string, deps types.RuntimeCLI) error { return nil } +type PullResponse struct { + currentBranch string + upstream string + output string + title lipgloss.Style + error error + errorStyle lipgloss.Style +} + +func (r PullResponse) String() string { + if r.error != nil { + return fmt.Sprintf("%s %s", r.title, r.errorStyle.Render(r.error.Error())) + } + + return fmt.Sprintf( + "%s [%s <- %s] %s", + r.title.Render(), + r.currentBranch, + r.upstream, + r.output, + ) +} + func pullProjectRepos(project domain.Project, deps types.RuntimeCLI) []error { fmt.Println(cli.ProjectTitleWithBullet(project, deps.Theme)) errList := make([]error, 0) - for _, repo := range project.Repos { - err := pullRepo(project, repo, deps) - if err != nil { - errList = append(errList, err) + maxLen := cli.GetMaxLen(project) + + var wg sync.WaitGroup + for idx, repo := range project.Repos { + wg.Add(1) + go func() { + defer wg.Done() + resp := pullRepo(project, repo, deps) + resp.title.Width(maxLen) + fmt.Println(resp) + if resp.error != nil { + errList = append(errList, resp.error) + } + }() + if idx > 0 && idx%deps.Settings.WorkerCount == 0 { + wg.Wait() } } + wg.Wait() + for _, subProject := range project.SubProjects { fmt.Println() errs := pullProjectRepos(subProject, deps) @@ -50,41 +92,42 @@ func pullProjectRepos(project domain.Project, deps types.RuntimeCLI) []error { return errList } -func pullRepo(project domain.Project, repo domain.Repository, deps types.RuntimeCLI) error { - maxLen := cli.GetMaxLen(project) - repoTitle := cli.RepoTitle(project, repo, deps.HomeDir, deps.Theme). - Width(maxLen) - - fmt.Printf("%s ", repoTitle) - defer fmt.Println() +func pullRepo(project domain.Project, repo domain.Repository, deps types.RuntimeCLI) PullResponse { + resp := PullResponse{ + title: cli.RepoTitle(repo, project.AbsPath, deps.HomeDir, deps.Theme), + errorStyle: deps.Theme.Error, + } // Abort if repository is not cloned or has errors. if repo.State != domain.RepoStateOK { - return cli.AbortOnRepoState(repo, deps.Theme.Error) + resp.error = cli.AbortOnRepoState(repo, deps.Theme.Error) + return resp } gitRepo, err := deps.Git.Open(repo.AbsPath) if err != nil { - return cli.RepoError(err, repo) + resp.error = cli.RepoError(err, repo) + return resp } - currentBranch, err := gitRepo.CurrentBranch() + resp.currentBranch, err = gitRepo.CurrentBranch() if err != nil { - return cli.RepoError(err, repo) + resp.error = cli.RepoError(err, repo) + return resp } - upstream, err := gitRepo.GetUpstream(currentBranch) + upstream, err := gitRepo.GetUpstream(resp.currentBranch) if err != nil { - return cli.RepoError(err, repo) + resp.error = cli.RepoError(err, repo) + return resp } + resp.upstream = upstream.Short() - fmt.Printf("[%s <- %s] ", currentBranch, upstream.Short()) - - out, err := deps.Git.Pull(repo.AbsPath) - fmt.Print(deps.Theme.GitOutput.Render(out)) + resp.output, err = deps.Git.Pull(repo.AbsPath) if err != nil { - fmt.Print(deps.Theme.Error.Render(err.Error())) - return cli.RepoError(err, repo) + resp.error = cli.RepoError(err, repo) + return resp } - return nil + resp.output = deps.Theme.GitOutput.Render(resp.output) + return resp }