diff options
| author | Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> | 2018-11-22 01:16:41 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-11-22 01:16:41 +0300 |
| commit | edf7fab3c3f64085b6781b2368c84565e31121d8 (patch) | |
| tree | 5767e1d5ffa997284ca764c9f9cc5e4f531dfd09 | |
| parent | sycnhronize with dev branch (diff) | |
| parent | refactoring and added initial implementation of fetc & pull mechanisms (diff) | |
| download | gitbatch-edf7fab3c3f64085b6781b2368c84565e31121d8.tar.gz | |
Merge pull request #6 from isacikgoz/develop
Develop
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | main.go | 1 | ||||
| -rw-r--r-- | pkg/app/app.go | 26 | ||||
| -rw-r--r-- | pkg/git/git-commands.go | 14 | ||||
| -rw-r--r-- | pkg/git/load.go | 41 | ||||
| -rw-r--r-- | pkg/git/model.go (renamed from pkg/git/git.go) | 81 | ||||
| -rw-r--r-- | pkg/git/repository.go | 112 | ||||
| -rw-r--r-- | pkg/gui/gui-util.go | 95 | ||||
| -rw-r--r-- | pkg/gui/gui.go | 142 | ||||
| -rw-r--r-- | pkg/gui/keybindings.go | 17 | ||||
| -rw-r--r-- | pkg/gui/pullview.go | 105 | ||||
| -rw-r--r-- | pkg/gui/scheduleview.go | 2 |
12 files changed, 494 insertions, 143 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c11484 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +exec.go.test @@ -24,6 +24,7 @@ func main() { if err != nil { log.Fatal(err) } + app.Gui.Run() defer app.Close() diff --git a/pkg/app/app.go b/pkg/app/app.go index 83231a5..c3575a4 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -2,7 +2,6 @@ package app import ( "github.com/isacikgoz/gitbatch/pkg/gui" - "github.com/isacikgoz/gitbatch/pkg/git" "io" ) @@ -18,12 +17,13 @@ func Setup(directories []string) (*App, error) { closers: []io.Closer{}, } - entities, err := createRepositoryEntities(directories) - if err != nil { - return app, err - } + var err error + // entities, err := createRepositoryEntities(directories) + // if err != nil { + // return app, err + // } - app.Gui, err = gui.NewGui(entities) + app.Gui, err = gui.NewGui(directories) if err != nil { return app, err } @@ -39,16 +39,4 @@ func (app *App) Close() error { } } return nil -} - -func createRepositoryEntities(directories []string) (entities []*git.RepoEntity, err error) { - entities = make([]*git.RepoEntity, 0) - for _, dir := range directories { - entity, err := git.InitializeRepository(dir) - if err != nil { - continue - } - entities = append(entities, entity) - } - return entities, nil -} +}
\ No newline at end of file diff --git a/pkg/git/git-commands.go b/pkg/git/git-commands.go index 1cf6fb7..a871356 100644 --- a/pkg/git/git-commands.go +++ b/pkg/git/git-commands.go @@ -6,6 +6,7 @@ import ( "github.com/isacikgoz/gitbatch/pkg/utils" ) + // UpstreamDifferenceCount checks how many pushables/pullables there are for the // current branch func UpstreamDifferenceCount(repoPath string) (string, string) { @@ -35,5 +36,14 @@ func CurrentBranchName(repoPath string) (string, error) { return utils.TrimTrailingNewline(branchName), nil } - - +func (entity *RepoEntity) IsClean() (bool, error) { + worktree, err := entity.Repository.Worktree() + if err != nil { + return true, nil + } + status, err := worktree.Status() + if err != nil { + return status.IsClean(), nil + } + return false, nil +}
\ No newline at end of file diff --git a/pkg/git/load.go b/pkg/git/load.go new file mode 100644 index 0000000..5d62dcf --- /dev/null +++ b/pkg/git/load.go @@ -0,0 +1,41 @@ +package git + +import ( + "sync" +) + +func LoadRepositoryEntities(directories []string) (entities []*RepoEntity, err error) { + entities = make([]*RepoEntity, 0) + + var wg sync.WaitGroup + var mu sync.Mutex + + for _, dir := range directories { + // increment wait counter by one because we run a single goroutine + // below + wg.Add(1) + + go func(d string) { + + // decrement the wait counter by one, we call it in a defer so it's + // called at the end of this goroutine + defer wg.Done() + entity, err := InitializeRepository(d) + if err != nil { + return + } + + // lock so we don't get a race if multiple go routines try to add + // to the same entities + mu.Lock() + entities = append(entities, entity) + mu.Unlock() + }(dir) + } + + // wait until the wait counter is zero, this happens if all goroutines have + // finished + wg.Wait() + + return entities, nil +}
\ No newline at end of file diff --git a/pkg/git/git.go b/pkg/git/model.go index db7af8f..2c02eda 100644 --- a/pkg/git/git.go +++ b/pkg/git/model.go @@ -5,38 +5,30 @@ import ( "github.com/isacikgoz/gitbatch/pkg/utils" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing/object" - "os" "regexp" ) -type RepoEntity struct { - Name string - AbsPath string - Repository git.Repository - Pushables string - Pullables string - Branch string - Marked bool +func (entity *RepoEntity) GetRemotes() (remotes []string, err error) { + r := entity.Repository + + remotes, err = getRemotes(&r) + if err !=nil { + return nil ,err + } + return remotes, nil } -func InitializeRepository(directory string) (entity *RepoEntity, err error) { - file, err := os.Open(directory) - if err != nil { - return entity, err - } - fileInfo, err := file.Stat() - if err != nil { - return entity, err - } - r, err := git.PlainOpen(directory) - if err != nil { - return entity, err - } - pushable, pullable := UpstreamDifferenceCount(directory) - branch, err := CurrentBranchName(directory) - entity = &RepoEntity{fileInfo.Name(), directory, *r, pushable, pullable, branch, false} - - return entity, nil +func getRemotes(r *git.Repository) (remotes []string, err error) { + + if list, err := r.Remotes(); err != nil { + return remotes, err + } else { + for _, r := range list { + remoteString := r.Config().Name + " → " + r.Config().URLs[0] + remotes = append(remotes, remoteString) + } + } + return remotes, nil } func (entity *RepoEntity) GetCommits() (commits []string, err error) { @@ -64,20 +56,6 @@ func (entity *RepoEntity) GetCommits() (commits []string, err error) { return commits, nil } -func (entity *RepoEntity) GetRemotes() (remotes []string, err error) { - r := entity.Repository - - if list, err := r.Remotes(); err != nil { - return remotes, err - } else { - for _, r := range list { - remoteString := r.Config().Name + " → " + r.Config().URLs[0] - remotes = append(remotes, remoteString) - } - } - return remotes, nil -} - func (entity *RepoEntity) GetStatus() (status string) { status = "↑ " + entity.Pushables + " ↓ " + entity.Pullables + " → " + entity.Branch re := regexp.MustCompile(`\r?\n`) @@ -85,12 +63,15 @@ func (entity *RepoEntity) GetStatus() (status string) { return status } -func (entity *RepoEntity) Mark() { - entity.Marked = true -} - -func (entity *RepoEntity) UnMark() { - entity.Marked = false -} - - +func (entity *RepoEntity) GetDisplayString() string{ + if entity.Marked { + green := color.New(color.FgGreen) + return string(green.Sprint(entity.Name)) + } else if !entity.Clean { + orange := color.New(color.FgYellow) + return string(orange.Sprint(entity.Name)) + } else { + white := color.New(color.FgWhite) + return string(white.Sprint(entity.Name)) + } +}
\ No newline at end of file diff --git a/pkg/git/repository.go b/pkg/git/repository.go new file mode 100644 index 0000000..93cdbc9 --- /dev/null +++ b/pkg/git/repository.go @@ -0,0 +1,112 @@ +package git + +import ( + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" + "os" + "time" +) + +type RepoEntity struct { + Name string + AbsPath string + Repository git.Repository + Pushables string + Pullables string + Branch string + Remote string + Marked bool + Clean bool +} + +func InitializeRepository(directory string) (entity *RepoEntity, err error) { + file, err := os.Open(directory) + if err != nil { + return nil, err + } + fileInfo, err := file.Stat() + if err != nil { + return nil, err + } + r, err := git.PlainOpen(directory) + if err != nil { + return nil, err + } + pushable, pullable := UpstreamDifferenceCount(directory) + branch, err := CurrentBranchName(directory) + remotes, err := getRemotes(r) + entity = &RepoEntity{fileInfo.Name(), directory, *r, pushable, pullable, branch, remotes[0], false, isClean(r, fileInfo.Name())} + + return entity, nil +} + +func isClean(r *git.Repository, name string) bool { + w, err := r.Worktree() + if err != nil { + return false + } + // TODO: This function is incredibly slow + s, err := w.Status() + if err != nil { + return false + } + return s.IsClean() +} + +func (entity *RepoEntity) Mark() { + entity.Marked = true +} + +func (entity *RepoEntity) Unmark() { + entity.Marked = false +} + +func (entity *RepoEntity) Pull() error { + w, err := entity.Repository.Worktree() + if err != nil { + return err + } + ref := plumbing.NewBranchReferenceName(entity.Branch) + err = w.Pull(&git.PullOptions{ + RemoteName: entity.Remote, + ReferenceName: ref, + }) + if err != nil { + return err + } + + return nil +} + +func (entity *RepoEntity) PullTest() error { + time.Sleep(5 * time.Second) + + return nil +} + +func (entity *RepoEntity) Fetch() error { + err := entity.Repository.Fetch(&git.FetchOptions{ + RemoteName: entity.Remote, + }) + if err != nil { + return err + } + + return nil +} + +func (entity *RepoEntity) GetActiveBranch() string{ + headRef, _ := entity.Repository.Head() + return headRef.Name().String() +} + +func (entity *RepoEntity) GetActiveRemote() string { + if list, err := entity.Repository.Remotes(); err != nil { + return "" + } else { + for _, r := range list { + return r.Config().Name + } + } + return "" +}
\ No newline at end of file diff --git a/pkg/gui/gui-util.go b/pkg/gui/gui-util.go index 21ba9ae..d842eb8 100644 --- a/pkg/gui/gui-util.go +++ b/pkg/gui/gui-util.go @@ -1,8 +1,10 @@ package gui import ( - "github.com/isacikgoz/gitbatch/pkg/git" + "fmt" + "sync" "github.com/isacikgoz/gitbatch/pkg/utils" + "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" ) @@ -32,7 +34,7 @@ func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error { cx, cy := v.Cursor() ox, oy := v.Origin() - ly := len(gui.Repositories) -1 + ly := len(gui.State.Repositories) -1 // if we are at the end we just return if cy+oy == ly { @@ -81,7 +83,7 @@ func (gui *Gui) getSelectedRepository(g *gocui.Gui, v *gocui.View) (*git.RepoEnt return r, err } - for _, sr := range gui.Repositories { + for _, sr := range gui.State.Repositories { if l == sr.Name { return sr, nil } @@ -89,6 +91,61 @@ func (gui *Gui) getSelectedRepository(g *gocui.Gui, v *gocui.View) (*git.RepoEnt return r, err } +func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error { + + if r, err := gui.getSelectedRepository(g, v); err != nil { + return err + } else { + if err != nil { + return err + } + if r.Marked != true { + r.Mark() + } else { + r.Unmark() + } + gui.refreshMain(g) + gui.updateSchedule(g) + } + + return nil +} + +func (gui *Gui) markAllRepositories(g *gocui.Gui, v *gocui.View) error { + for _, r := range gui.State.Repositories { + r.Mark() + } + if err := gui.refreshMain(g); err !=nil { + return err + } + gui.updateSchedule(g) + return nil +} + +func (gui *Gui) unMarkAllRepositories(g *gocui.Gui, v *gocui.View) error { + for _, r := range gui.State.Repositories { + r.Unmark() + } + if err := gui.refreshMain(g); err !=nil { + return err + } + gui.updateSchedule(g) + return nil +} + +func (gui *Gui) refreshMain(g *gocui.Gui) error { + + mainView, err := g.View("main") + if err != nil { + return err + } + mainView.Clear() + for _, r := range gui.State.Repositories { + fmt.Fprintln(mainView, r.GetDisplayString()) + } + return nil +} + // if the cursor down past the last item, move it to the last line func (gui *Gui) correctCursor(v *gocui.View) error { cx, cy := v.Cursor() @@ -109,22 +166,22 @@ func (gui *Gui) correctCursor(v *gocui.View) error { return nil } -func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View { - v, _ := g.View("commits") - return v -} +func (gui *Gui) getMarkedEntities() (rs []*git.RepoEntity, err error) { + var wg sync.WaitGroup + var mu sync.Mutex -func (gui *Gui) getRemotesView(g *gocui.Gui) *gocui.View { - v, _ := g.View("remotes") - return v -} - -func (gui *Gui) getScheduleView(g *gocui.Gui) *gocui.View { - v, _ := g.View("schedule") - return v -} + for _, r := range gui.State.Repositories { + wg.Add(1) + go func(repo *git.RepoEntity){ + defer wg.Done() + if repo.Marked { + mu.Lock() + rs = append(rs, repo) + mu.Unlock() + } + }(r) + } + wg.Wait() -func (gui *Gui) getStatusView(g *gocui.Gui) *gocui.View { - v, _ := g.View("status") - return v + return rs, nil }
\ No newline at end of file diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 96e2c0a..55c6c18 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -2,28 +2,103 @@ package gui import ( "github.com/isacikgoz/gitbatch/pkg/git" - "github.com/isacikgoz/gitbatch/pkg/utils" - "github.com/fatih/color" "github.com/jroimartin/gocui" "fmt" + "time" + "os" + "os/exec" + "io/ioutil" + "log" ) +// SentinelErrors are the errors that have special meaning and need to be checked +// by calling functions. The less of these, the better +type SentinelErrors struct { + ErrSubProcess error +} + // Gui wraps the gocui Gui object which handles rendering and events type Gui struct { g *gocui.Gui - Repositories []*git.RepoEntity + SubProcess *exec.Cmd + State guiState + Errors SentinelErrors +} + +type guiState struct { + Repositories []*git.RepoEntity + Directories []string } // NewGui builds a new gui handler -func NewGui(entities []*git.RepoEntity) (*Gui, error) { +func NewGui(directoies []string) (*Gui, error) { + rs, err := git.LoadRepositoryEntities(directoies) + if err != nil { + return nil, err + } + initialState := guiState{ + Repositories: rs, + } gui := &Gui{ - Repositories: entities, + State: initialState, } return gui, nil } +// Run setup the gui with keybindings and start the mainloop +func (gui *Gui) Run() error { + + g, err := gocui.NewGui(gocui.OutputNormal) + if err != nil { + return err + } + defer g.Close() + + g.SetManagerFunc(gui.layout) + + if err := gui.keybindings(g); err != nil { + return err + } + + if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + return err + } + return nil +} + +// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration +// if the error returned from a run is a ErrSubProcess, it runs the subprocess +// otherwise it handles the error, possibly by quitting the application +func (gui *Gui) RunWithSubprocesses() { + for { + if err := gui.Run(); err != nil { + if err == gocui.ErrQuit { + break + } else if err == gui.Errors.ErrSubProcess { + gui.SubProcess.Stdin = os.Stdin + gui.SubProcess.Stdout = os.Stdout + gui.SubProcess.Stderr = os.Stderr + gui.SubProcess.Run() + gui.SubProcess.Stdout = ioutil.Discard + gui.SubProcess.Stderr = ioutil.Discard + gui.SubProcess.Stdin = nil + gui.SubProcess = nil + } else { + log.Fatal(err) + } + } + } +} + +func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) { + go func() { + for range time.Tick(interval) { + function(g) + } + }() +} func (gui *Gui) layout(g *gocui.Gui) error { maxX, maxY := g.Size() @@ -37,8 +112,8 @@ func (gui *Gui) layout(g *gocui.Gui) error { v.SelBgColor = gocui.ColorWhite v.SelFgColor = gocui.ColorBlack v.Overwrite = true - for _, r := range gui.Repositories { - fmt.Fprintln(v, r.Name) + for _, r := range gui.State.Repositories { + fmt.Fprintln(v, r.GetDisplayString()) } if _, err = gui.setCurrentViewOnTop(g, "main"); err != nil { @@ -89,39 +164,11 @@ func (gui *Gui) layout(g *gocui.Gui) error { v.BgColor = gocui.ColorWhite v.FgColor = gocui.ColorBlack v.Frame = false - fmt.Fprintln(v, "q: quit ↑ ↓: navigate space: select") + fmt.Fprintln(v, "q: quit | ↑ ↓: navigate | space: select/deselect | a: select all | r: clear selection | enter: execute") } return nil } -func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error { - - if e, err := gui.getSelectedRepository(g, v); err != nil { - return err - } else { - mainView, err := g.View("main") - if err != nil { - return err - } - if e.Marked != true { - e.Mark() - } else { - e.UnMark() - } - mainView.Clear() - for _, r := range gui.Repositories { - if r.Marked { - fmt.Fprintln(mainView, utils.ColoredString(r.Name, color.FgGreen)) - } else { - fmt.Fprintln(mainView, r.Name) - } - } - gui.updateSchedule(g) - } - - return nil -} - func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } @@ -133,24 +180,17 @@ func (gui *Gui) setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, err return g.SetViewOnTop(name) } -// Run setup the gui with keybindings and start the mainloop -func (gui *Gui) Run() error { +func (gui *Gui) updateKeyBindingsViewForMainView(g *gocui.Gui) error { - g, err := gocui.NewGui(gocui.OutputNormal) + v, err := g.View("keybindings") if err != nil { return err } - defer g.Close() - - g.SetManagerFunc(gui.layout) - - if err := gui.keybindings(g); err != nil { - return err - } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - return err - } + v.Clear() + v.BgColor = gocui.ColorWhite + v.FgColor = gocui.ColorBlack + v.Frame = false + fmt.Fprintln(v, "q: quit | ↑ ↓: navigate | space: select/deselect | a: select all | r: clear selection | enter: execute") return nil -} - +}
\ No newline at end of file diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index fcaf40a..5f2164e 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -8,7 +8,16 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, gui.quit); err != nil { return err } - if err := g.SetKeybinding("", 'q', gocui.ModNone, gui.quit); err != nil { + if err := g.SetKeybinding("main", 'q', gocui.ModNone, gui.quit); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyEnter, gocui.ModNone, gui.openPullView); err != nil { + return err + } + if err := g.SetKeybinding("pull", 'c', gocui.ModNone, gui.closePullView); err != nil { + return err + } + if err := g.SetKeybinding("pull", gocui.KeyEnter, gocui.ModNone, gui.executePull); err != nil { return err } if err := g.SetKeybinding("main", gocui.KeyArrowDown, gocui.ModNone, gui.cursorDown); err != nil { @@ -20,5 +29,11 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("main", gocui.KeySpace, gocui.ModNone, gui.markRepository); err != nil { return err } + if err := g.SetKeybinding("main", 'a', gocui.ModNone, gui.markAllRepositories); err != nil { + return err + } + if err := g.SetKeybinding("main", 'r', gocui.ModNone, gui.unMarkAllRepositories); err != nil { + return err + } return nil }
\ No newline at end of file diff --git a/pkg/gui/pullview.go b/pkg/gui/pullview.go new file mode 100644 index 0000000..90e3797 --- /dev/null +++ b/pkg/gui/pullview.go @@ -0,0 +1,105 @@ +package gui + +import ( + "github.com/jroimartin/gocui" + "fmt" +) + +func (gui *Gui) openPullView(g *gocui.Gui, v *gocui.View) error { + maxX, maxY := g.Size() + + v, err := g.SetView("pull", maxX/2-35, maxY/2-5, maxX/2+35, maxY/2+5) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = " " + "Execution Parameters" + " " + v.Wrap = false + mrs, _ := gui.getMarkedEntities() + for _, r := range mrs { + line := r.Name + " : " + r.GetActiveRemote() + "/" + r.Branch + " → " + r.GetActiveBranch() + fmt.Fprintln(v, line) + } + } + gui.updateKeyBindingsViewForPullView(g) + if _, err := g.SetCurrentView("pull"); err != nil { + return err + } + return nil +} + +func (gui *Gui) closePullView(g *gocui.Gui, v *gocui.View) error { + + if err := g.DeleteView(v.Name()); err != nil { + return nil + } + if _, err := g.SetCurrentView("main"); err != nil { + return err + } + gui.updateKeyBindingsViewForMainView(g) + + return nil +} + +func (gui *Gui) executePull(g *gocui.Gui, v *gocui.View) error { + gui.updateKeyBindingsViewForExecution(g) + mrs, _ := gui.getMarkedEntities() + + gui.updateKeyBindingsViewForExecution(g) + for _, mr := range mrs { + + go gui.counter(g) + + // here we will be waiting + mr.Pull() + mr.Unmark() + } + + gui.refreshMain(g) + gui.updateSchedule(g) + gui.closePullView(g, v) + return nil +} + +func (gui *Gui) updateKeyBindingsViewForPullView(g *gocui.Gui) error { + + v, err := g.View("keybindings") + if err != nil { + return err + } + v.Clear() + v.BgColor = gocui.ColorGreen + v.FgColor = gocui.ColorBlack + v.Frame = false + fmt.Fprintln(v, "c: cancel | ↑ ↓: navigate | enter: execute") + return nil +} + + +func (gui *Gui) updateKeyBindingsViewForExecution(g *gocui.Gui) error { + + v, err := g.View("keybindings") + if err != nil { + return err + } + v.Clear() + v.BgColor = gocui.ColorRed + v.FgColor = gocui.ColorWhite + v.Frame = false + fmt.Fprintln(v, " PULLING REPOSITORIES") + return nil +} + +func (gui *Gui) counter(g *gocui.Gui) { + + v, err := g.View("pull") + if err != nil { + return + } + + g.Update(func(g *gocui.Gui) error { + v.Clear() + fmt.Fprintln(v, "Pulling...") + return nil + }) +} diff --git a/pkg/gui/scheduleview.go b/pkg/gui/scheduleview.go index 7c7a1f2..7a8a1d3 100644 --- a/pkg/gui/scheduleview.go +++ b/pkg/gui/scheduleview.go @@ -15,7 +15,7 @@ func (gui *Gui) updateSchedule(g *gocui.Gui) error { } out.Clear() pullJobs := 0 - for _, r := range gui.Repositories { + for _, r := range gui.State.Repositories { if r.Marked { pullJobs++ } |
