diff options
| author | Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> | 2018-11-26 00:23:45 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-11-26 00:23:45 +0300 |
| commit | 8d8db0b4891e2c48cde35ff4bd792ea9a187adb6 (patch) | |
| tree | 3855b12455f8862d14e5cf2f4230ef6bb9f23025 | |
| parent | Merge pull request #7 from isacikgoz/develop (diff) | |
| parent | update readme (diff) | |
| download | gitbatch-8d8db0b4891e2c48cde35ff4bd792ea9a187adb6.tar.gz | |
Merge pull request #8 from isacikgoz/develop
Develop
| -rw-r--r-- | README.md | 37 | ||||
| -rw-r--r-- | main.go | 5 | ||||
| -rw-r--r-- | pkg/app/app.go | 5 | ||||
| -rw-r--r-- | pkg/git/branch.go | 77 | ||||
| -rw-r--r-- | pkg/git/commit.go | 154 | ||||
| -rw-r--r-- | pkg/git/git-commands.go | 37 | ||||
| -rw-r--r-- | pkg/git/load.go | 5 | ||||
| -rw-r--r-- | pkg/git/model.go | 81 | ||||
| -rw-r--r-- | pkg/git/remote.go | 82 | ||||
| -rw-r--r-- | pkg/git/repository.go | 55 | ||||
| -rw-r--r-- | pkg/gui/branchview.go | 66 | ||||
| -rw-r--r-- | pkg/gui/cheatsheet.go | 40 | ||||
| -rw-r--r-- | pkg/gui/commitsview.go | 137 | ||||
| -rw-r--r-- | pkg/gui/errorview.go | 39 | ||||
| -rw-r--r-- | pkg/gui/gui-util.go | 178 | ||||
| -rw-r--r-- | pkg/gui/gui.go | 170 | ||||
| -rw-r--r-- | pkg/gui/keybindings.go | 223 | ||||
| -rw-r--r-- | pkg/gui/mainview.go | 186 | ||||
| -rw-r--r-- | pkg/gui/pullview.go | 94 | ||||
| -rw-r--r-- | pkg/gui/remotesview.go | 35 | ||||
| -rw-r--r-- | pkg/gui/scheduleview.go | 25 | ||||
| -rw-r--r-- | pkg/gui/statusview.go | 21 |
22 files changed, 1225 insertions, 527 deletions
@@ -1,17 +1,28 @@ [](https://travis-ci.com/isacikgoz/gitbatch) [](/LICENSE) [](https://goreportcard.com/report/github.com/isacikgoz/gitbatch) ## gitbatch -Aim of this simple application to make your local repositories syncrhonized with remotes easily. This is still a work in progress application. +Aim of this simple application to make your local repositories syncrhonized with remotes easily. -clone the repository: -```bash -git clone https://github.com/isacikgoz/gitbatch -``` -use it like: -```bash -go run main.go -d '/Users/ibrahim/git' -p gitbatch -``` -to get help: -```bash -go run main.go --help -``` +**Disclamier** This is still a work in progress project. + +Here is the intial look of the project: +[](https://asciinema.org/a/eYfR9eWC4VGjiAyBUE7hpaZph) + +### Use +run the command the parent of your git repositories. Or simply: +`gitbatch --help` + +## installation +installation guide will be provided after in-house tests and after implementation of the unit tests just get less headache. And maybe later I can distribute binaries from releases page. + +## Further goals +- full src-d/go-git integration +- implement modal base ux like vim (fetch/pull maybe even push) +- Resolve authentication issues +- Handle conflicts + +## Credits +[go-git](https://github.com/src-d/go-git) +[gocui](https://github.com/jroimartin/gocui) +[color](https://github.com/fatih/color) +[lazygit](https://github.com/jesseduffield/lazygit)
\ No newline at end of file @@ -25,7 +25,10 @@ func main() { log.Fatal(err) } - app.Gui.Run() + err = app.Gui.Run() + if err != nil { + log.Fatal(err) + } defer app.Close() } diff --git a/pkg/app/app.go b/pkg/app/app.go index c3575a4..74939b2 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -18,11 +18,6 @@ func Setup(directories []string) (*App, error) { } var err error - // entities, err := createRepositoryEntities(directories) - // if err != nil { - // return app, err - // } - app.Gui, err = gui.NewGui(directories) if err != nil { return app, err diff --git a/pkg/git/branch.go b/pkg/git/branch.go new file mode 100644 index 0000000..fc6cb6e --- /dev/null +++ b/pkg/git/branch.go @@ -0,0 +1,77 @@ +package git + +import ( + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" +) + +func (entity *RepoEntity) GetActiveBranch() string{ + headRef, _ := entity.Repository.Head() + return headRef.Name().Short() +} + +func (entity *RepoEntity) LocalBranches() (lbs []string, err error){ + branches, err := entity.Repository.Branches() + if err != nil { + return nil, err + } + defer branches.Close() + branches.ForEach(func(b *plumbing.Reference) error { + if b.Type() == plumbing.HashReference { + lbs = append(lbs, b.Name().Short()) + } + return nil + }) + return lbs, err +} + +func (entity *RepoEntity) NextBranch() string{ + + currentBranch := entity.GetActiveBranch() + localBranches, err := entity.LocalBranches() + if err != nil { + return currentBranch + } + + currentBranchIndex := 0 + for i, lbs := range localBranches { + if lbs == currentBranch { + currentBranchIndex = i + } + } + + if currentBranchIndex == len(localBranches)-1 { + return localBranches[0] + } + return localBranches[currentBranchIndex+1] +} + +func (entity *RepoEntity) Checkout(branchName string) error { + if branchName == entity.Branch { + return nil + } + w, err := entity.Repository.Worktree() + if err != nil { + return err + } + if err = w.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(branchName), + }); err != nil { + return err + } + entity.Branch = branchName + entity.Pushables, entity.Pullables = UpstreamDifferenceCount(entity.AbsPath) + return 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/commit.go b/pkg/git/commit.go new file mode 100644 index 0000000..500c3a9 --- /dev/null +++ b/pkg/git/commit.go @@ -0,0 +1,154 @@ +package git + +import ( + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/object" + "regexp" + "time" +) + +var ( + Hashlimit = 6 +) + +type Commit struct { + Hash string + Author string + Message string + Time time.Time +} + +func newCommit(hash, author, message string, time time.Time) (commit *Commit) { + commit = &Commit{hash, author, message, time} + return commit +} + +func lastCommit(r *git.Repository) (commit *Commit, err error) { + ref, err := r.Head() + if err != nil { + return nil, err + } + + cIter, _ := r.Log(&git.LogOptions{ + From: ref.Hash(), + Order: git.LogOrderCommitterTime, + }) + defer cIter.Close() + + c, err := cIter.Next() + if err != nil { + return nil, err + } + re := regexp.MustCompile(`\r?\n`) + commit = newCommit(re.ReplaceAllString(c.Hash.String(), " "), c.Author.Email, re.ReplaceAllString(c.Message, " "), c.Author.When) + return commit, nil +} + +func (entity *RepoEntity) NextCommit() error { + + currentCommit := entity.Commit + commits, err := entity.Commits() + if err != nil { + return err + } + + currentCommitIndex := 0 + for i, cs := range commits { + if cs.Hash == currentCommit.Hash { + currentCommitIndex = i + } + } + if currentCommitIndex == len(commits)-1 { + entity.Commit = commits[0] + return nil + } + entity.Commit = commits[currentCommitIndex+1] + return nil +} + +func (entity *RepoEntity) Commits() (commits []*Commit, err error) { + r := entity.Repository + + ref, err := r.Head() + if err != nil { + return commits, err + } + + cIter, _ := r.Log(&git.LogOptions{ + From: ref.Hash(), + Order: git.LogOrderCommitterTime, + }) + defer cIter.Close() + + // ... just iterates over the commits + err = cIter.ForEach(func(c *object.Commit) error { + re := regexp.MustCompile(`\r?\n`) + commit := newCommit(re.ReplaceAllString(c.Hash.String(), " "), c.Author.Email, re.ReplaceAllString(c.Message, " "), c.Author.When) + commits = append(commits, commit) + + return nil + }) + if err != nil { + return commits, err + } + return commits, nil +} + +func (entity *RepoEntity) Diff(hash string) (diff string, err error) { + + cms, err := entity.Commits() + if err != nil { + return "", err + } + + currentCommitIndex := 0 + for i, cs := range cms { + if cs.Hash == hash { + currentCommitIndex = i + } + } + if len(cms) -currentCommitIndex <= 1 { + return "there is no diff", nil + } + + commits, err := entity.Repository.Log(&git.LogOptions{ + From: plumbing.NewHash(cms[currentCommitIndex].Hash), + Order: git.LogOrderCommitterTime, + }) + if err != nil { + return "", err + } + + currentCommit, err := commits.Next() + if err != nil { + return "", err + } + currentTree, err := currentCommit.Tree() + if err != nil { + return diff, err + } + + prevCommit, err := commits.Next() + if err != nil { + return "", err + } + prevTree, err := prevCommit.Tree() + if err != nil { + return diff, err + } + + changes, err := prevTree.Diff(currentTree) + if err != nil { + return "", err + } + + for _, c := range changes { + patch, err := c.Patch() + if err != nil { + break + } + diff = diff + patch.String() + "\n" + } + return diff, nil +}
\ No newline at end of file diff --git a/pkg/git/git-commands.go b/pkg/git/git-commands.go index a871356..e3907d5 100644 --- a/pkg/git/git-commands.go +++ b/pkg/git/git-commands.go @@ -3,12 +3,12 @@ package git import ( "strings" "github.com/isacikgoz/gitbatch/pkg/command" - "github.com/isacikgoz/gitbatch/pkg/utils" ) // UpstreamDifferenceCount checks how many pushables/pullables there are for the // current branch +// TODO: get pull pushes to remote branch vs local branch func UpstreamDifferenceCount(repoPath string) (string, string) { args := []string{"rev-list", "@{u}..HEAD", "--count"} pushableCount, err := command.RunCommandWithOutput(repoPath, "git", args) @@ -23,27 +23,32 @@ func UpstreamDifferenceCount(repoPath string) (string, string) { return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) } -func CurrentBranchName(repoPath string) (string, error) { - args := []string{"symbolic-ref", "--short", "HEAD"} - branchName, err := command.RunCommandWithOutput(repoPath, "git", args) +func (entity *RepoEntity) FetchWithGit(remote string) error { + args := []string{"fetch", remote} + _, err := command.RunCommandWithOutput(entity.AbsPath, "git", args) if err != nil { - args = []string{"rev-parse", "--short", "HEAD"} - branchName, err = command.RunCommandWithOutput(repoPath, "git", args) - if err != nil { - return "", err - } + return err } - return utils.TrimTrailingNewline(branchName), nil + return nil } -func (entity *RepoEntity) IsClean() (bool, error) { - worktree, err := entity.Repository.Worktree() +func (entity *RepoEntity) MergeWithGit(mergeTo, mergeFrom string) error { + if err := entity.Checkout(mergeTo); err != nil { + return err + } + args := []string{"merge", mergeFrom} + _, err := command.RunCommandWithOutput(entity.AbsPath, "git", args) if err != nil { - return true, nil + return err } - status, err := worktree.Status() + return nil +} + +func (entity *RepoEntity) CheckoutWithGit(branch string) error { + args := []string{"checkout", branch} + _, err := command.RunCommandWithOutput(entity.AbsPath, "git", args) if err != nil { - return status.IsClean(), nil + return err } - return false, nil + return nil }
\ No newline at end of file diff --git a/pkg/git/load.go b/pkg/git/load.go index 5d62dcf..70a2475 100644 --- a/pkg/git/load.go +++ b/pkg/git/load.go @@ -14,9 +14,7 @@ func LoadRepositoryEntities(directories []string) (entities []*RepoEntity, err e // 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() @@ -24,7 +22,6 @@ func LoadRepositoryEntities(directories []string) (entities []*RepoEntity, err e 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() @@ -32,10 +29,8 @@ func LoadRepositoryEntities(directories []string) (entities []*RepoEntity, err e 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/model.go b/pkg/git/model.go deleted file mode 100644 index 0c14fe1..0000000 --- a/pkg/git/model.go +++ /dev/null @@ -1,81 +0,0 @@ -package git - -import ( - "github.com/fatih/color" - "github.com/isacikgoz/gitbatch/pkg/utils" - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing/object" - "regexp" -) - -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 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 - remotes = append(remotes, remoteString) - } - } - return remotes, nil -} - -func (entity *RepoEntity) GetCommits() (commits []string, err error) { - r := entity.Repository - //TODO: Handle Errors - ref, _ := r.Head() - - cIter, _ := r.Log(&git.LogOptions{ - From: ref.Hash(), - Order: git.LogOrderCommitterTime, - }) - -// ... just iterates over the commits - err = cIter.ForEach(func(c *object.Commit) error { - commitstring := utils.ColoredString(string([]rune(c.Hash.String())[:7]), color.FgGreen) + " " + c.Message - re := regexp.MustCompile(`\r?\n`) - commitstring = re.ReplaceAllString(commitstring, " ") - commits = append(commits, commitstring) - - return nil - }) - if err != nil { - return commits, err - } - return commits, nil -} - -func (entity *RepoEntity) GetStatus() (status string) { - status = "↑ " + entity.Pushables + " ↓ " + entity.Pullables + " → " + entity.Branch - re := regexp.MustCompile(`\r?\n`) - status = re.ReplaceAllString(status, " ") - return status -} - -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/remote.go b/pkg/git/remote.go new file mode 100644 index 0000000..07ba9e2 --- /dev/null +++ b/pkg/git/remote.go @@ -0,0 +1,82 @@ +package git + +import ( + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/storer" +) + +type Remote struct { + Name string + Reference *plumbing.Reference +} + +func (entity *RepoEntity) GetRemotes() (remotes []*Remote, err error) { + + r := entity.Repository + if list, err := remoteBranches(&r); err != nil { + return remotes, err + } else { + for _, r := range list { + remotes = append(remotes, r) + } + } + + return remotes, nil +} + +func (entity *RepoEntity) NextRemote() error { + + remotes, err := remoteBranches(&entity.Repository) + if err != nil { + return err + } + + currentRemoteIndex := 0 + for i, remote := range remotes { + if remote.Reference.Hash() == entity.Remote.Reference.Hash() { + currentRemoteIndex = i + } + } + // WARNING: DIDN'T CHECK THE LIFE CYCLE + if currentRemoteIndex == len(remotes)-1 { + entity.Remote = remotes[0] + } else { + entity.Remote = remotes[currentRemoteIndex+1] + } + + return nil +} + +func remoteBranches(r *git.Repository) (remotes []*Remote, err error) { + bs, err := remoteBranchesIter(r.Storer) + if err != nil { + return remotes, err + } + defer bs.Close() + err = bs.ForEach(func(b *plumbing.Reference) error { + remotes = append(remotes, &Remote{ + Name: b.Name().Short(), + Reference: b, + }) + return nil + }) + if err != nil { + return remotes, err + } + return remotes, err +} + +func remoteBranchesIter(s storer.ReferenceStorer) (storer.ReferenceIter, error) { + refs, err := s.IterReferences() + if err != nil { + return nil, err + } + + return storer.NewReferenceFilteredIter(func(ref *plumbing.Reference) bool { + if ref.Type() == plumbing.HashReference { + return ref.Name().IsRemote() + } + return false + }, refs), nil +}
\ No newline at end of file diff --git a/pkg/git/repository.go b/pkg/git/repository.go index b3abce7..29d216c 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -2,9 +2,9 @@ package git import ( "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing" "os" "time" + "strings" ) type RepoEntity struct { @@ -14,7 +14,8 @@ type RepoEntity struct { Pushables string Pullables string Branch string - Remote string + Remote *Remote + Commit *Commit Marked bool Clean bool } @@ -33,10 +34,14 @@ func InitializeRepository(directory string) (entity *RepoEntity, err error) { 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())} - + headRef, err := r.Head() + if err != nil { + return nil, err + } + branch := headRef.Name().Short() + remotes, err := remoteBranches(r) + commit, _ := lastCommit(r) + entity = &RepoEntity{fileInfo.Name(), directory, *r, pushable, pullable, branch, remotes[0], commit, false, isClean(r, fileInfo.Name())} return entity, nil } @@ -62,52 +67,32 @@ func (entity *RepoEntity) Unmark() { } func (entity *RepoEntity) Pull() error { - w, err := entity.Repository.Worktree() - if err != nil { + // TODO: Migrate this code to src-d/go-git + // 2018-11-25: tried but it fails, will investigate. + rm := entity.Remote.Reference.Name().Short() + remote := strings.Split(rm, "/")[0] + if err := entity.FetchWithGit(remote); err != nil { return err } - rf := plumbing.NewBranchReferenceName(entity.Branch) - rm := entity.Remote - err = w.Pull(&git.PullOptions{ - RemoteName: rm, - ReferenceName: rf, - }) - if err != nil { + if err := entity.MergeWithGit(entity.Branch, remote); err != nil { return err } - return nil } func (entity *RepoEntity) PullTest() error { time.Sleep(5 * time.Second) - return nil } func (entity *RepoEntity) Fetch() error { + rm := entity.Remote.Reference.Name().Short() + remote := strings.Split(rm, "/")[0] err := entity.Repository.Fetch(&git.FetchOptions{ - RemoteName: entity.Remote, + RemoteName: 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/branchview.go b/pkg/gui/branchview.go new file mode 100644 index 0000000..1ecfbf6 --- /dev/null +++ b/pkg/gui/branchview.go @@ -0,0 +1,66 @@ +package gui + +import ( + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" + "fmt" +) + +func (gui *Gui) updateBranch(g *gocui.Gui, entity *git.RepoEntity) error { + var err error + + out, err := g.View("branch") + if err != nil { + return err + } + out.Clear() + + currentindex := 0 + totalbranches := 0 + if branches, err := entity.LocalBranches(); err != nil { + return err + } else { + totalbranches = len(branches) + for i, b := range branches { + if b == entity.Branch { + currentindex = i + fmt.Fprintln(out, selectionIndicator() + b) + continue + } + fmt.Fprintln(out, tab() + b) + } + } + if err = gui.smartAnchorRelativeToLine(out, currentindex, totalbranches); err != nil { + return err + } + return nil +} + +func (gui *Gui) nextBranch(g *gocui.Gui, v *gocui.View) error { + var err error + + entity, err := gui.getSelectedRepository(g, v) + if err != nil { + return err + } + + if err = entity.Checkout(entity.NextBranch()); err != nil { + if err = gui.openErrorView(g, "Stage your changes before checkout", "You should manually manage this issue"); err != nil { + return err + } + return nil + } + + if err = gui.updateBranch(g, entity); err != nil { + return err + } + + if err = gui.updateCommits(g, entity); err != nil { + return err + } + + if err = gui.refreshMain(g); err != nil { + return err + } + return nil +}
\ No newline at end of file diff --git a/pkg/gui/cheatsheet.go b/pkg/gui/cheatsheet.go new file mode 100644 index 0000000..bf26e7f --- /dev/null +++ b/pkg/gui/cheatsheet.go @@ -0,0 +1,40 @@ +package gui + +import ( + "github.com/jroimartin/gocui" + "fmt" +) + +func (gui *Gui) openCheatSheetView(g *gocui.Gui, v *gocui.View) error { + maxX, maxY := g.Size() + v, err := g.SetView(cheatSheetViewFeature.Name, maxX/2-25, maxY/2-10, maxX/2+25, maxY/2+10) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = cheatSheetViewFeature.Title + fmt.Fprintln(v, " ") + for _, k := range gui.KeyBindings { + if k.View == mainViewFeature.Name || k.View == "" { + binding := " " + k.Display + ": " + k.Description + fmt.Fprintln(v, binding) + } + } + } + gui.updateKeyBindingsView(g, cheatSheetViewFeature.Name) + if _, err := g.SetCurrentView(cheatSheetViewFeature.Name); err != nil { + return err + } + return nil +} + +func (gui *Gui) closeCheatSheetView(g *gocui.Gui, v *gocui.View) error { + if err := g.DeleteView(v.Name()); err != nil { + return nil + } + if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { + return err + } + gui.updateKeyBindingsView(g, mainViewFeature.Name) + return nil +}
\ No newline at end of file diff --git a/pkg/gui/commitsview.go b/pkg/gui/commitsview.go index afaf1bc..9a6456b 100644 --- a/pkg/gui/commitsview.go +++ b/pkg/gui/commitsview.go @@ -4,24 +4,151 @@ import ( "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" "fmt" + "strings" + "regexp" ) func (gui *Gui) updateCommits(g *gocui.Gui, entity *git.RepoEntity) error { var err error - - out, err := g.View("commits") + out, err := g.View(commitViewFeature.Name) if err != nil { return err } out.Clear() - if list, err := entity.GetCommits(); err != nil { + totalcommits := 0 + currentindex := 0 + if commits, err := entity.Commits(); err != nil { return err } else { - for _, c := range list { - fmt.Fprintln(out, c) + totalcommits = len(commits) + for i, c := range commits { + if c.Hash == entity.Commit.Hash { + currentindex = i + fmt.Fprintln(out, selectionIndicator() + green.Sprint(c.Hash[:git.Hashlimit]) + " " + c.Message) + continue + } + fmt.Fprintln(out, tab() + cyan.Sprint(c.Hash[:git.Hashlimit]) + " " + c.Message) } } + if err = gui.smartAnchorRelativeToLine(out, currentindex, totalcommits); err != nil { + return err + } + return nil +} +func (gui *Gui) nextCommit(g *gocui.Gui, v *gocui.View) error { + var err error + entity, err := gui.getSelectedRepository(g, v) + if err != nil { + return err + } + if err = entity.NextCommit(); err != nil { + return err + } + if err = gui.updateCommits(g, entity); err != nil { + return err + } return nil +} + +func (gui *Gui) showCommitDetail(g *gocui.Gui, v *gocui.View) error { + maxX, maxY := g.Size() + v, err := g.SetView(commitdetailViewFeature.Name, 5, 3, maxX-5, maxY-3) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = commitdetailViewFeature.Title + v.Overwrite = true + v.Wrap = true + + main, _ := g.View(mainViewFeature.Name) + + entity, err := gui.getSelectedRepository(g, main) + if err != nil { + return err + } + commit := entity.Commit + commitDetail := "Hash: " + cyan.Sprint(commit.Hash) + "\n" + "Author: " + commit.Author + "\n" + commit.Time.String() + "\n" + "\n" + "\t" + commit.Message + "\n" + fmt.Fprintln(v, commitDetail) + diff, err := entity.Diff(entity.Commit.Hash) + if err != nil { + return err + } + colorized := colorizeDiff(diff) + for _, line := range colorized{ + fmt.Fprintln(v, line) + } + } + + gui.updateKeyBindingsView(g, commitdetailViewFeature.Name) + if _, err := g.SetCurrentView(commitdetailViewFeature.Name); err != nil { + return err + } + return nil +} + +func (gui *Gui) closeCommitDetailView(g *gocui.Gui, v *gocui.View) error { + if err := g.DeleteView(v.Name()); err != nil { + return nil + } + if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { + return err + } + gui.updateKeyBindingsView(g, mainViewFeature.Name) + return nil +} + +func (gui *Gui) commitCursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + _, vy := v.Size() + + // TODO: do something when it hits bottom + if err := v.SetOrigin(ox, oy+vy/2); err != nil { + return err + } + } + return nil +} + +func (gui *Gui) commitCursorUp(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + _, vy := v.Size() + + if oy-vy/2 > 0 { + if err := v.SetOrigin(ox, oy-vy/2); err != nil { + return err + } + } else if oy-vy/2 <= 0{ + if err := v.SetOrigin(0, 0); err != nil { + return err + } + } + } + return nil +} + +func colorizeDiff(original string) (colorized []string) { + colorized = strings.Split(original, "\n") + re := regexp.MustCompile(`@@ .+ @@`) + for i, line := range colorized { + if len(line) > 0 { + if line[0] == '-' { + colorized[i] = red.Sprint(line) + } else if line [0] == '+' { + colorized[i] = green.Sprint(line) + } else if re.MatchString(line) { + s := re.FindString(line) + colorized[i] = cyan.Sprint(s) + line[len(s):] + } else { + continue + } + } else { + continue + } + } + return colorized }
\ No newline at end of file diff --git a/pkg/gui/errorview.go b/pkg/gui/errorview.go new file mode 100644 index 0000000..bf3eaf9 --- /dev/null +++ b/pkg/gui/errorview.go @@ -0,0 +1,39 @@ +package gui + +import ( + "github.com/jroimartin/gocui" + "fmt" +) + +func (gui *Gui) openErrorView(g *gocui.Gui, message string, note string) error { + maxX, maxY := g.Size() + + v, err := g.SetView(errorViewFeature.Name, maxX/2-30, maxY/2-3, maxX/2+30, maxY/2+3) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = errorViewFeature.Title + v.Wrap = true + ps := red.Sprint("Note:") + " " + note + fmt.Fprintln(v, message) + fmt.Fprintln(v, "\n" + ps) + } + gui.updateKeyBindingsView(g, errorViewFeature.Name) + if _, err := g.SetCurrentView(errorViewFeature.Name); err != nil { + return err + } + return nil +} + +func (gui *Gui) closeErrorView(g *gocui.Gui, v *gocui.View) error { + + if err := g.DeleteView(v.Name()); err != nil { + return nil + } + if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { + return err + } + gui.updateKeyBindingsView(g, mainViewFeature.Name) + return nil +}
\ No newline at end of file diff --git a/pkg/gui/gui-util.go b/pkg/gui/gui-util.go index d842eb8..64d5321 100644 --- a/pkg/gui/gui-util.go +++ b/pkg/gui/gui-util.go @@ -1,149 +1,42 @@ package gui import ( - "fmt" - "sync" + "github.com/fatih/color" "github.com/isacikgoz/gitbatch/pkg/utils" "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" ) +var ( + blue = color.New(color.FgBlue) + green = color.New(color.FgGreen) + red = color.New(color.FgRed) + cyan = color.New(color.FgCyan) + orange = color.New(color.FgYellow) + white = color.New(color.FgWhite) +) func (gui *Gui) refreshViews(g *gocui.Gui, entity *git.RepoEntity) error { if err := gui.updateRemotes(g, entity); err != nil { return err } - - if err := gui.updateStatus(g, entity); err != nil { + if err := gui.updateBranch(g, entity); err != nil { return err } - if err := gui.updateCommits(g, entity); err != nil { return err } - - if err := gui.updateSchedule(g); err != nil { + if err := gui.updateSchedule(g, entity); err != nil { return err } - - return nil -} - -func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error { - if v != nil { - cx, cy := v.Cursor() - ox, oy := v.Origin() - - ly := len(gui.State.Repositories) -1 - - // if we are at the end we just return - if cy+oy == ly { - return nil - } - if err := v.SetCursor(cx, cy+1); err != nil { - - if err := v.SetOrigin(ox, oy+1); err != nil { - return err - } - } - if entity, err := gui.getSelectedRepository(g, v); err != nil { - return err - } else { - gui.refreshViews(g, entity) - } - } - return nil -} - -func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error { - if v != nil { - ox, oy := v.Origin() - cx, cy := v.Cursor() - if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { - if err := v.SetOrigin(ox, oy-1); err != nil { - return err - } - } - if entity, err := gui.getSelectedRepository(g, v); err != nil { - return err - } else { - gui.refreshViews(g, entity) - } - } return nil } -func (gui *Gui) getSelectedRepository(g *gocui.Gui, v *gocui.View) (*git.RepoEntity, error) { - var l string - var err error - var r *git.RepoEntity - - _, cy := v.Cursor() - if l, err = v.Line(cy); err != nil { - return r, err - } - - for _, sr := range gui.State.Repositories { - if l == sr.Name { - return sr, nil - } +func (gui *Gui) setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { + if _, err := g.SetCurrentView(name); err != nil { + return nil, err } - 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 + return g.SetViewOnTop(name) } // if the cursor down past the last item, move it to the last line @@ -166,22 +59,33 @@ func (gui *Gui) correctCursor(v *gocui.View) error { return nil } -func (gui *Gui) getMarkedEntities() (rs []*git.RepoEntity, err error) { - var wg sync.WaitGroup - var mu sync.Mutex +func (gui *Gui) smartAnchorRelativeToLine(v *gocui.View, currentindex, totallines int) error { - 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) + _, y := v.Size() + if currentindex >= int(0.5*float32(y)) && totallines - currentindex + int(0.5*float32(y)) >= y{ + if err := v.SetOrigin(0, currentindex - int(0.5*float32(y))); err != nil { + return err + } + } else if totallines - currentindex < y && totallines > y { + if err := v.SetOrigin(0, totallines -y ); err != nil { + return err + } + } else if totallines - currentindex <= int(0.5*float32(y)) && totallines > y -1 && currentindex > y { + if err := v.SetOrigin(0, currentindex - int(0.5*float32(y))); err != nil { + return err + } + } else { + if err := v.SetOrigin(0, 0); err != nil { + return err + } } - wg.Wait() + return nil +} + +func selectionIndicator() string { + return green.Sprint("→ ") +} - return rs, nil +func tab() string { + return green.Sprint(" ") }
\ No newline at end of file diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 55c6c18..39c2d45 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -4,25 +4,12 @@ import ( "github.com/isacikgoz/gitbatch/pkg/git" "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 - SubProcess *exec.Cmd - State guiState - Errors SentinelErrors + g *gocui.Gui + KeyBindings []*KeyBinding + State guiState } type guiState struct { @@ -30,167 +17,132 @@ type guiState struct { Directories []string } -// NewGui builds a new gui handler -func NewGui(directoies []string) (*Gui, error) { +type viewFeature struct { + Name string + Title string +} - rs, err := git.LoadRepositoryEntities(directoies) - if err != nil { - return nil, err - } +var ( + mainViewFeature = viewFeature{Name: "main", Title: " Matched Repositories "} + loadingViewFeature = viewFeature{Name: "loading", Title: " Loading in Progress "} + branchViewFeature = viewFeature{Name: "branch", Title: " Branches "} + remoteViewFeature = viewFeature{Name: "remotes", Title: " Remotes "} + commitViewFeature = viewFeature{Name: "commits", Title: " Commits "} + scheduleViewFeature = viewFeature{Name: "schedule", Title: " Schedule "} + keybindingsViewFeature = viewFeature{Name: "keybindings", Title: " Keybindings "} + pullViewFeature = viewFeature{Name: "pull", Title: " Execution Parameters "} + commitdetailViewFeature = viewFeature{Name: "commitdetail", Title: " Commit Detail "} + cheatSheetViewFeature = viewFeature{Name: "cheatsheet", Title: " Application Controls "} + errorViewFeature = viewFeature{Name: "error", Title: " Error "} +) + +func NewGui(directoies []string) (*Gui, error) { initialState := guiState{ - Repositories: rs, + Directories: directoies, } gui := &Gui{ 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() + go func(g_ui *Gui) { + maxX, maxY := g.Size() + v, err := g.SetView(loadingViewFeature.Name, maxX/2-10, maxY/2-1, maxX/2+10, maxY/2+1) + if err != nil { + if err != gocui.ErrUnknownView { + return + } + fmt.Fprintln(v, "Loading...") + } + if _, err := g.SetCurrentView(loadingViewFeature.Name); err != nil { + return + } + rs, err := git.LoadRepositoryEntities(g_ui.State.Directories) + if err != nil { + return + } + g_ui.State.Repositories = rs + gui.fillMain(g) + }(gui) + defer g.Close() + gui.g = g g.SetManagerFunc(gui.layout) + if err := gui.generateKeybindings(); err != nil { + return err + } 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() - - if v, err := g.SetView("main", 0, 0, int(0.5*float32(maxX))-1, maxY-2); err != nil { + if v, err := g.SetView(mainViewFeature.Name, 0, 0, int(0.55*float32(maxX))-1, maxY-2); err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = " Matched Repositories " + v.Title = mainViewFeature.Title v.Highlight = true v.SelBgColor = gocui.ColorWhite v.SelFgColor = gocui.ColorBlack v.Overwrite = true - for _, r := range gui.State.Repositories { - fmt.Fprintln(v, r.GetDisplayString()) - } - - if _, err = gui.setCurrentViewOnTop(g, "main"); err != nil { - return err - } } - - if v, err := g.SetView("status", int(0.5*float32(maxX)), 0, maxX-1, 2); err != nil { + if v, err := g.SetView(branchViewFeature.Name, int(0.55*float32(maxX)), 0, maxX-1, int(0.20*float32(maxY))-1); err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = " Status " + v.Title = branchViewFeature.Title v.Wrap = false v.Autoscroll = false } - - if v, err := g.SetView("remotes", int(0.5*float32(maxX)), 3, maxX-1, int(0.25*float32(maxY))); err != nil { + if v, err := g.SetView(remoteViewFeature.Name, int(0.55*float32(maxX)), int(0.20*float32(maxY)), maxX-1, int(0.40*float32(maxY))); err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = " Remotes " - v.Wrap = true - v.Autoscroll = false + v.Title = remoteViewFeature.Title + v.Wrap = false + v.Overwrite = true } - - if v, err := g.SetView("commits", int(0.5*float32(maxX)), int(0.25*float32(maxY))+1, maxX-1, int(0.75*float32(maxY))); err != nil { + if v, err := g.SetView(commitViewFeature.Name, int(0.55*float32(maxX)), int(0.40*float32(maxY))+1, maxX-1, int(0.73*float32(maxY))); err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = " Commits " + v.Title = commitViewFeature.Title v.Wrap = false v.Autoscroll = false } - - if v, err := g.SetView("schedule", int(0.5*float32(maxX)), int(0.75*float32(maxY))+1, maxX-1, maxY-2); err != nil { + if v, err := g.SetView(scheduleViewFeature.Name, int(0.55*float32(maxX)), int(0.73*float32(maxY))+1, maxX-1, maxY-2); err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = " Schedule " + v.Title = scheduleViewFeature.Title v.Wrap = true v.Autoscroll = true } - - if v, err := g.SetView("keybindings", -1, maxY-2, maxX, maxY); err != nil { + if v, err := g.SetView(keybindingsViewFeature.Name, -1, maxY-2, maxX, maxY); err != nil { if err != gocui.ErrUnknownView { return err } 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") + gui.updateKeyBindingsView(g, mainViewFeature.Name) } return nil } func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit -} - -func (gui *Gui) setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { - if _, err := g.SetCurrentView(name); err != nil { - return nil, err - } - return g.SetViewOnTop(name) -} - -func (gui *Gui) updateKeyBindingsViewForMainView(g *gocui.Gui) error { - - v, err := g.View("keybindings") - if err != nil { - 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 5f2164e..85231ba 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -2,38 +2,209 @@ package gui import ( "github.com/jroimartin/gocui" + "fmt" ) -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("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 { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowUp, gocui.ModNone, gui.cursorUp); err != nil { - return err +type KeyBinding struct { + View string + Handler func(*gocui.Gui, *gocui.View) error + Key interface{} + Modifier gocui.Modifier + Display string + Description string + Vital bool +} + +func (gui *Gui) generateKeybindings() error { + gui.KeyBindings = []*KeyBinding{ + { + View: "", + Key: gocui.KeyCtrlC, + Modifier: gocui.ModNone, + Handler: gui.quit, + Display: "ctrl + c", + Description: "Force application to quit", + Vital: false, + },{ + View: mainViewFeature.Name, + Key: 'q', + Modifier: gocui.ModNone, + Handler: gui.quit, + Display: "q", + Description: "Quit from application", + Vital: true, + },{ + View: mainViewFeature.Name, + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + Handler: gui.cursorUp, + Display: "↑", + Description: "Cursor up", + Vital: true, + },{ + View: mainViewFeature.Name, + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + Handler: gui.cursorDown, + Display: "↓", + Description: "Cursor down", + Vital: true, + },{ + View: mainViewFeature.Name, + Key: 'b', + Modifier: gocui.ModNone, + Handler: gui.nextBranch, + Display: "b", + Description: "Iterate over branches", + Vital: false, + },{ + View: mainViewFeature.Name, + Key: 'r', + Modifier: gocui.ModNone, + Handler: gui.nextRemote, + Display: "r", + Description: "Iterate over remotes", + Vital: false, + },{ + View: mainViewFeature.Name, + Key: 's', + Modifier: gocui.ModNone, + Handler: gui.nextCommit, + Display: "s", + Description: "Iterate over commits", + Vital: false, + },{ + View: mainViewFeature.Name, + Key: 'x', + Modifier: gocui.ModNone, + Handler: gui.showCommitDetail, + Display: "x", + Description: "Show commit diff", + Vital: false, + },{ + View: mainViewFeature.Name, + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.openCheatSheetView, + Display: "c", + Description: "Open cheatsheet window", + Vital: false, + },{ + View: mainViewFeature.Name, + Key: gocui.KeyEnter, + Modifier: gocui.ModNone, + Handler: gui.openPullView, + Display: "enter", + Description: "Execute jobs", + Vital: true, + },{ + View: mainViewFeature.Name, + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.markRepository, + Display: "space", + Description: "Select repository", + Vital: true, + },{ + View: mainViewFeature.Name, + Key: 'a', + Modifier: gocui.ModNone, + Handler: gui.markAllRepositories, + Display: "a", + Description: "Select all repositories", + Vital: false, + },{ + View: mainViewFeature.Name, + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.unMarkAllRepositories, + Display: "d", + Description: "Deselect all repositories", + Vital: false, + },{ + View: commitdetailViewFeature.Name, + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.closeCommitDetailView, + Display: "c", + Description: "close/cancel", + Vital: true, + },{ + View: commitdetailViewFeature.Name, + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + Handler: gui.commitCursorUp, + Display: "↑", + Description: "Page up", + Vital: true, + },{ + View: commitdetailViewFeature.Name, + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + Handler: gui.commitCursorDown, + Display: "↓", + Description: "Page down", + Vital: true, + },{ + View: cheatSheetViewFeature.Name, + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.closeCheatSheetView, + Display: "c", + Description: "close/cancel", + Vital: true, + },{ + View: pullViewFeature.Name, + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.closePullView, + Display: "c", + Description: "close/cancel", + Vital: true, + },{ + View: pullViewFeature.Name, + Key: gocui.KeyEnter, + Modifier: gocui.ModNone, + Handler: gui.executePull, + Display: "enter", + Description: "Execute", + Vital: true, + },{ + View: errorViewFeature.Name, + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.closeErrorView, + Display: "c", + Description: "close/cancel", + Vital: true, + }, } - if err := g.SetKeybinding("main", gocui.KeySpace, gocui.ModNone, gui.markRepository); err != nil { - return err + return nil +} + +func (gui *Gui) keybindings(g *gocui.Gui) error { + for _, k := range gui.KeyBindings { + if err := g.SetKeybinding(k.View, k.Key, k.Modifier, k.Handler); err != nil { + return err + } } - if err := g.SetKeybinding("main", 'a', gocui.ModNone, gui.markAllRepositories); err != nil { + return nil +} + +func (gui *Gui) updateKeyBindingsView(g *gocui.Gui, viewName string) error { + v, err := g.View(keybindingsViewFeature.Name) + if err != nil { return err } - if err := g.SetKeybinding("main", 'r', gocui.ModNone, gui.unMarkAllRepositories); err != nil { - return err + v.Clear() + v.BgColor = gocui.ColorWhite + v.FgColor = gocui.ColorBlack + v.Frame = false + for _, k := range gui.KeyBindings { + if k.View == viewName && k.Vital { + binding := " " + k.Display + ": " + k.Description + " |" + fmt.Fprint(v, binding) + } } return nil }
\ No newline at end of file diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go new file mode 100644 index 0000000..87d685e --- /dev/null +++ b/pkg/gui/mainview.go @@ -0,0 +1,186 @@ +package gui + +import ( + "fmt" + "sync" + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" + "regexp" +) + +func (gui *Gui) fillMain(g *gocui.Gui) error { + g.Update(func(g *gocui.Gui) error { + v, err := g.View(mainViewFeature.Name) + if err != nil { + return err + } + for _, r := range gui.State.Repositories { + fmt.Fprintln(v, displayString(r)) + } + err = g.DeleteView(loadingViewFeature.Name) + if err != nil { + return err + } + if _, err = gui.setCurrentViewOnTop(g, mainViewFeature.Name); err != nil { + return err + } + if entity, err := gui.getSelectedRepository(g, v); err != nil { + return err + } else { + gui.refreshViews(g, entity) + } + return nil + }) + return nil +} + +func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + cx, cy := v.Cursor() + ox, oy := v.Origin() + ly := len(gui.State.Repositories) -1 + + // if we are at the end we just return + if cy+oy == ly { + return nil + } + if err := v.SetCursor(cx, cy+1); err != nil { + + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + if entity, err := gui.getSelectedRepository(g, v); err != nil { + return err + } else { + gui.refreshViews(g, entity) + } + } + return nil +} + +func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + if entity, err := gui.getSelectedRepository(g, v); err != nil { + return err + } else { + gui.refreshViews(g, entity) + } + } + return nil +} + +func (gui *Gui) getSelectedRepository(g *gocui.Gui, v *gocui.View) (*git.RepoEntity, error) { + var l string + var err error + var r *git.RepoEntity + + _, cy := v.Cursor() + if l, err = v.Line(cy); err != nil { + return r, err + } + rg := regexp.MustCompile(` → .+ `) + ss := rg.Split(l, 5) + for _, sr := range gui.State.Repositories { + if ss[len(ss)-1] == sr.Name { + return sr, nil + } + } + 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.Clean { + if err = gui.openErrorView(g, "Stage your changes before pull", "You should manually resolve this issue"); err != nil { + return err + } + return nil + } + if !r.Marked { + r.Mark() + } else { + r.Unmark() + } + gui.refreshMain(g) + gui.updateSchedule(g, r) + } + return nil +} + +func (gui *Gui) markAllRepositories(g *gocui.Gui, v *gocui.View) error { + for _, r := range gui.State.Repositories { + if r.Clean { + r.Mark() + } + } + if err := gui.refreshMain(g); err !=nil { + return err + } + 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 + } + return nil +} + +func (gui *Gui) refreshMain(g *gocui.Gui) error { + + mainView, err := g.View(mainViewFeature.Name) + if err != nil { + return err + } + mainView.Clear() + for _, r := range gui.State.Repositories { + fmt.Fprintln(mainView, displayString(r)) + } + return nil +} + +func (gui *Gui) getMarkedEntities() (rs []*git.RepoEntity, err error) { + var wg sync.WaitGroup + var mu sync.Mutex + 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() + return rs, nil +} + +func displayString(entity *git.RepoEntity) string{ + prefix := string(blue.Sprint("↑")) + " " + entity.Pushables + " " + + string(blue.Sprint("↓")) + " " + entity.Pullables + string(red.Sprint(" → ")) + string(cyan.Sprint(entity.Branch)) + " " + if entity.Marked { + return prefix + string(green.Sprint(entity.Name)) + } else if !entity.Clean { + return prefix + string(orange.Sprint(entity.Name)) + } else { + return prefix + string(white.Sprint(entity.Name)) + } +}
\ No newline at end of file diff --git a/pkg/gui/pullview.go b/pkg/gui/pullview.go index 7523bcd..3f73124 100644 --- a/pkg/gui/pullview.go +++ b/pkg/gui/pullview.go @@ -3,68 +3,71 @@ package gui import ( "github.com/jroimartin/gocui" "fmt" + "strconv" ) 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) + v, err := g.SetView(pullViewFeature.Name, 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 + v.Title = pullViewFeature.Title + v.Wrap = true mrs, _ := gui.getMarkedEntities() + jobs := strconv.Itoa(len(mrs)) + " repositories to fetch & merge:" + fmt.Fprintln(v, jobs) for _, r := range mrs { - line := r.Name + " : " + r.GetActiveRemote() + "/" + r.Branch + " → " + r.GetActiveBranch() + line := " - " + green.Sprint(r.Name) + ": " + r.Remote.Name + green.Sprint(" → ") + r.Branch fmt.Fprintln(v, line) } + ps := red.Sprint("Note:") + " After execution you will be notified" + fmt.Fprintln(v, "\n" + ps) } - gui.updateKeyBindingsViewForPullView(g) - if _, err := g.SetCurrentView("pull"); err != nil { + gui.updateKeyBindingsView(g, pullViewFeature.Name) + if _, err := g.SetCurrentView(pullViewFeature.Name); 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) - + + if err := g.DeleteView(v.Name()); err != nil { + return nil + } + if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { + return err + } + gui.refreshMain(g) + gui.updateKeyBindingsView(g, mainViewFeature.Name) return nil } func (gui *Gui) executePull(g *gocui.Gui, v *gocui.View) error { - gui.updateKeyBindingsViewForExecution(g) + // somehow this fucntion called after this method returns, strange? + go g.Update(func(g *gocui.Gui) error { + err := updateKeyBindingsViewForExecution(g) + if err != nil { + return err + } + return nil + }) + mrs, _ := gui.getMarkedEntities() - - gui.updateKeyBindingsViewForExecution(g) for _, mr := range mrs { - - gui.updatePullViewWithExec(g) - - // here we will be waiting + // here we will be waiting mr.Pull() gui.updateCommits(g, mr) 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") +func updateKeyBindingsViewForExecution(g *gocui.Gui) error { + v, err := g.View(keybindingsViewFeature.Name) if err != nil { return err } @@ -72,35 +75,6 @@ func (gui *Gui) updateKeyBindingsViewForPullView(g *gocui.Gui) error { 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") + fmt.Fprintln(v, " Execution Completed; c: close/cancel") return nil -} - -func (gui *Gui) updatePullViewWithExec(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 - }) -} +}
\ No newline at end of file diff --git a/pkg/gui/remotesview.go b/pkg/gui/remotesview.go index fff322d..303f5ca 100644 --- a/pkg/gui/remotesview.go +++ b/pkg/gui/remotesview.go @@ -9,19 +9,48 @@ import ( func (gui *Gui) updateRemotes(g *gocui.Gui, entity *git.RepoEntity) error { var err error - out, err := g.View("remotes") + out, err := g.View(remoteViewFeature.Name) if err != nil { return err } out.Clear() + currentindex := 0 + totalRemotes := 0 if list, err := entity.GetRemotes(); err != nil { return err } else { - for _, r := range list { - fmt.Fprintln(out, r) + totalRemotes = len(list) + for i, r := range list { + if r.Reference.Hash().String() == entity.Remote.Reference.Hash().String() { + currentindex = i + fmt.Fprintln(out, selectionIndicator() + r.Name) + continue + } + fmt.Fprintln(out, tab() + r.Name) } } + if err = gui.smartAnchorRelativeToLine(out, currentindex, totalRemotes); err != nil { + return err + } + return nil +} + +func (gui *Gui) nextRemote(g *gocui.Gui, v *gocui.View) error { + var err error + + entity, err := gui.getSelectedRepository(g, v) + if err != nil { + return err + } + + if err = entity.NextRemote(); err != nil { + return err + } + + if err = gui.updateRemotes(g, entity); err != nil { + return err + } return nil }
\ No newline at end of file diff --git a/pkg/gui/scheduleview.go b/pkg/gui/scheduleview.go index 7a8a1d3..27105cd 100644 --- a/pkg/gui/scheduleview.go +++ b/pkg/gui/scheduleview.go @@ -1,26 +1,31 @@ package gui import ( + "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" "fmt" - "strconv" + "strings" ) -func (gui *Gui) updateSchedule(g *gocui.Gui) error { +func (gui *Gui) updateSchedule(g *gocui.Gui, entity *git.RepoEntity) error { var err error - out, err := g.View("schedule") + out, err := g.View(scheduleViewFeature.Name) if err != nil { return err } out.Clear() - pullJobs := 0 - for _, r := range gui.State.Repositories { - if r.Marked { - pullJobs++ - } + if entity.Marked { + s := green.Sprint("$") + " git checkout " + entity.Branch + " " + green.Sprint("✓") + fmt.Fprintln(out, s) + rm := entity.Remote.Reference.Name().Short() + remote := strings.Split(rm, "/")[0] + s = green.Sprint("$") + " git fetch " + remote + fmt.Fprintln(out, s) + s = green.Sprint("$") + " git merge " + entity.Remote.Name + fmt.Fprintln(out, s) + } else { + return nil } - jobs := strconv.Itoa(pullJobs) + " repositories to pull" - fmt.Fprintln(out, jobs) return nil }
\ No newline at end of file diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go deleted file mode 100644 index 7289f0f..0000000 --- a/pkg/gui/statusview.go +++ /dev/null @@ -1,21 +0,0 @@ -package gui - -import ( - "github.com/isacikgoz/gitbatch/pkg/git" - "github.com/jroimartin/gocui" - "fmt" -) - -func (gui *Gui) updateStatus(g *gocui.Gui, entity *git.RepoEntity) error { - var err error - - out, err := g.View("status") - if err != nil { - return err - } - out.Clear() - - fmt.Fprintln(out, entity.GetStatus()) - - return nil -}
\ No newline at end of file |
