Skip to content

Commit

Permalink
perf: clone, fetch and pull in parallel
Browse files Browse the repository at this point in the history
  • Loading branch information
rafi committed Jul 2, 2024
1 parent 4306ec4 commit 3452ff8
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 76 deletions.
11 changes: 6 additions & 5 deletions domain/settings.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
81 changes: 57 additions & 24 deletions internal/cli/clone/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package clone
import (
"fmt"
"os"
"sync"

"github.com/charmbracelet/lipgloss"
log "github.com/sirupsen/logrus"

"github.com/rafi/gits/domain"
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
}
5 changes: 5 additions & 0 deletions internal/cli/config/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"

"github.com/charmbracelet/lipgloss"
"github.com/knadh/koanf/parsers/json"
Expand Down Expand Up @@ -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():
Expand Down
78 changes: 56 additions & 22 deletions internal/cli/fetch/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package fetch

import (
"fmt"
"sync"

"github.com/charmbracelet/lipgloss"

"github.com/rafi/gits/domain"
"github.com/rafi/gits/internal/cli"
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
}
Loading

0 comments on commit 3452ff8

Please sign in to comment.