summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIbrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>2018-11-26 00:23:45 +0300
committerGitHub <noreply@github.com>2018-11-26 00:23:45 +0300
commit8d8db0b4891e2c48cde35ff4bd792ea9a187adb6 (patch)
tree3855b12455f8862d14e5cf2f4230ef6bb9f23025
parentMerge pull request #7 from isacikgoz/develop (diff)
parentupdate readme (diff)
downloadgitbatch-8d8db0b4891e2c48cde35ff4bd792ea9a187adb6.tar.gz
Merge pull request #8 from isacikgoz/develop
Develop
-rw-r--r--README.md37
-rw-r--r--main.go5
-rw-r--r--pkg/app/app.go5
-rw-r--r--pkg/git/branch.go77
-rw-r--r--pkg/git/commit.go154
-rw-r--r--pkg/git/git-commands.go37
-rw-r--r--pkg/git/load.go5
-rw-r--r--pkg/git/model.go81
-rw-r--r--pkg/git/remote.go82
-rw-r--r--pkg/git/repository.go55
-rw-r--r--pkg/gui/branchview.go66
-rw-r--r--pkg/gui/cheatsheet.go40
-rw-r--r--pkg/gui/commitsview.go137
-rw-r--r--pkg/gui/errorview.go39
-rw-r--r--pkg/gui/gui-util.go178
-rw-r--r--pkg/gui/gui.go170
-rw-r--r--pkg/gui/keybindings.go223
-rw-r--r--pkg/gui/mainview.go186
-rw-r--r--pkg/gui/pullview.go94
-rw-r--r--pkg/gui/remotesview.go35
-rw-r--r--pkg/gui/scheduleview.go25
-rw-r--r--pkg/gui/statusview.go21
22 files changed, 1225 insertions, 527 deletions
diff --git a/README.md b/README.md
index 0578837..558b330 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,28 @@
[![Build Status](https://travis-ci.com/isacikgoz/gitbatch.svg?branch=master)](https://travis-ci.com/isacikgoz/gitbatch) [![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/isacikgoz/gitbatch)](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:
+[![asciicast](https://asciinema.org/a/eYfR9eWC4VGjiAyBUE7hpaZph.svg)](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
diff --git a/main.go b/main.go
index 4a716d3..bc2b79f 100644
--- a/main.go
+++ b/main.go
@@ -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