summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIbrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>2018-12-14 02:55:04 +0300
committerGitHub <noreply@github.com>2018-12-14 02:55:04 +0300
commit25c5df6a91132a1936d58bdaecb36da51a86f4de (patch)
tree95b65edb40b0e67008ea4b6c4a938890b682854c
parentMerge pull request #32 from isacikgoz/develop (diff)
parentadded new screen cast and minor bugfixes (diff)
downloadgitbatch-25c5df6a91132a1936d58bdaecb36da51a86f4de.tar.gz
Merge pull request #33 from isacikgoz/develop
Develop
-rw-r--r--README.md25
-rw-r--r--pkg/git/authentication.go26
-rw-r--r--pkg/git/cmd-add.go (renamed from pkg/git/add.go)63
-rw-r--r--pkg/git/cmd-commit.go93
-rw-r--r--pkg/git/cmd-config.go107
-rw-r--r--pkg/git/cmd-fetch.go146
-rw-r--r--pkg/git/cmd-merge.go (renamed from pkg/git/merge.go)0
-rw-r--r--pkg/git/cmd-reset.go130
-rw-r--r--pkg/git/cmd-rev-list.go (renamed from pkg/git/rev-list.go)0
-rw-r--r--pkg/git/cmd-stash.go (renamed from pkg/git/stash.go)0
-rw-r--r--pkg/git/cmd-status.go (renamed from pkg/git/status.go)53
-rw-r--r--pkg/git/cmd.go (renamed from pkg/git/commands.go)0
-rw-r--r--pkg/git/fetch.go46
-rw-r--r--pkg/git/job-queue.go (renamed from pkg/queue/queue.go)8
-rw-r--r--pkg/git/job.go78
-rw-r--r--pkg/git/remote.go11
-rw-r--r--pkg/git/repository.go6
-rw-r--r--pkg/git/reset.go55
-rw-r--r--pkg/git/util-errors.go24
-rw-r--r--pkg/git/util-load.go (renamed from pkg/git/load.go)0
-rw-r--r--pkg/git/util-sort.go (renamed from pkg/git/repository-sort.go)39
-rw-r--r--pkg/gui/authenticationview.go176
-rw-r--r--pkg/gui/cheatsheet.go12
-rw-r--r--pkg/gui/commitview.go187
-rw-r--r--pkg/gui/diffview.go13
-rw-r--r--pkg/gui/errorview.go12
-rw-r--r--pkg/gui/gui-util.go36
-rw-r--r--pkg/gui/gui.go9
-rw-r--r--pkg/gui/keybindings.go174
-rw-r--r--pkg/gui/mainview.go14
-rw-r--r--pkg/gui/queuehandler.go17
-rw-r--r--pkg/gui/stagedview.go40
-rw-r--r--pkg/gui/stashview.go4
-rw-r--r--pkg/gui/statusview.go58
-rw-r--r--pkg/gui/textstyle.go12
-rw-r--r--pkg/gui/unstagedview.go30
-rw-r--r--pkg/queue/job.go67
37 files changed, 1373 insertions, 398 deletions
diff --git a/README.md b/README.md
index fcfef40..7e6f592 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,13 @@
[![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
-This tool has been built to make your local repositories synchronized with remotes easily. I love lazygit, with that inspiration, decided to build this project to be even more lazy; Since my daily work is tied to many repositories I often end up walking on many directories and manually pulling updates etc. To make this routine faster, I created a simple tool to handle this job. I really enjoy working on this project and I hope it will be a useful tool.
-
-**Disclaimer**
-- Unauthenticated repositories are **not supported** so using ssh is recommended if you need to authenticate to fetch/pull
- - [Connecting to GitHub with SSH](https://help.github.com/articles/connecting-to-github-with-ssh/)
- - [GitLab and SSH keys](https://docs.gitlab.com/ee/ssh/)
- - [BitBucket Set up an SSH key](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-git-728138079.html)
-- Feedbacks are welcome. For now, known issues are:
- - At very low probability app fails to load repositories, try again it will load next time (multithreading problem).
- - Colors vary to your terminal theme colors, so if the contrast is not enough on some color decisions; discussions are welcome.
+This tool is beening built to make your local repositories synchronized with remotes easily. Although the focus is batch jobs, you can still do de facto micro management of your git repositories (e.g *add/reset, stash, commit etc.*)
Here is the screencast of the app:
-[![asciicast](https://asciinema.org/a/eXgXpzZfuHxMpZqGMVODUipyc.svg)](https://asciinema.org/a/eXgXpzZfuHxMpZqGMVODUipyc)
+[![asciicast](https://asciinema.org/a/QQPVDWVxUR3bvJhIZY3c4PTuG.svg)](https://asciinema.org/a/QQPVDWVxUR3bvJhIZY3c4PTuG)
## Installation
-For now, installation requires golang compiler and minimum golang 1.10 is recommended.
+For now, installation requires golang compiler and minimum golang 1.10 is recommended. (binary distribution will be provided on minimum viable product)
- If you don't have golang installed refer to [golang.org](https://golang.org/dl/).
- You should have $GOPATH env variable set and your $PATH should include $GOPATH/bin to run app from anywhere.
@@ -42,8 +33,12 @@ For more information;
## Further goals
- add testing
- full src-d/go-git integration (*having some performance issues*)
-- resolve authentication issues
-- add commit and maybe push?
+ - fetch, config, add, reset, commit, status, diff ✔
+ - merge, rev-list, stash ✗
+- add push
+
+## Known issues
+Please refer to [Known issues page](https://github.com/isacikgoz/gitbatch/wiki/Known-issues)
## Credits
- [go-git](https://github.com/src-d/go-git) for git interface (partially)
@@ -53,3 +48,5 @@ For more information;
- [color](https://github.com/fatih/color) for colored text
- [lazygit](https://github.com/jesseduffield/lazygit) as app template and reference
- [kingpin](https://github.com/alecthomas/kingpin) for command-line flag&options
+
+I love [lazygit](https://github.com/jesseduffield/lazygit), with that inspiration, decided to build this project to be even more lazy. The rationale was; my daily work is tied to many repositories and I often end up walking on many directories and manually pulling updates etc. To make this routine faster, I created a simple tool to handle this job. I really enjoy working on this project and I hope it will be a useful tool.
diff --git a/pkg/git/authentication.go b/pkg/git/authentication.go
new file mode 100644
index 0000000..def1f99
--- /dev/null
+++ b/pkg/git/authentication.go
@@ -0,0 +1,26 @@
+package git
+
+import (
+ "net/url"
+)
+
+// Credentials holds user credentials to authenticate and authorize while
+// communicating with remote if required
+type Credentials struct {
+ User string
+ Password string
+}
+
+var (
+ authProtocolHttp = "http"
+ authProtocolHttps = "https"
+ authProtocolSSH = "ssh"
+)
+
+func (entity *RepoEntity) authProtocol(remote *Remote) (p string, err error) {
+ u, err := url.Parse(remote.URL[0])
+ if err != nil {
+ return p, err
+ }
+ return u.Scheme, err
+}
diff --git a/pkg/git/add.go b/pkg/git/cmd-add.go
index 61fd587..649bad2 100644
--- a/pkg/git/add.go
+++ b/pkg/git/cmd-add.go
@@ -2,35 +2,54 @@ package git
import (
"errors"
- "strings"
log "github.com/sirupsen/logrus"
)
-var addCommand = "add"
+var (
+ addCmdMode string
+
+ addCommand = "add"
+ addCmdModeLegacy = "git"
+ addCmdModeNative = "go-git"
+)
// AddOptions defines the rules for "git add" command
type AddOptions struct {
+ // Update
Update bool
- Force bool
+ // Force
+ Force bool
+ // DryRun
DryRun bool
}
// Add is a wrapper function for "git add" command
-func (file *File) Add(option AddOptions) error {
- args := make([]string, 0)
- args = append(args, addCommand)
- args = append(args, file.Name)
- if option.Update {
- args = append(args, "--update")
+func Add(entity *RepoEntity, file *File, option AddOptions) error {
+ addCmdMode = addCmdModeNative
+ if option.Update || option.Force || option.DryRun {
+ addCmdMode = addCmdModeLegacy
}
- if option.Force {
- args = append(args, "--force")
+ switch addCmdMode {
+ case addCmdModeLegacy:
+ err := addWithGit(entity, file, option)
+ return err
+ case addCmdModeNative:
+ err := addWithGoGit(entity, file)
+ return err
}
+ return errors.New("Unhandled add operation")
+}
+
+// AddAll function is the wrapper of "git add ." command
+func AddAll(entity *RepoEntity, option AddOptions) error {
+ args := make([]string, 0)
+ args = append(args, addCommand)
if option.DryRun {
args = append(args, "--dry-run")
}
- out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args)
+ args = append(args, ".")
+ out, err := GenericGitCommandWithOutput(entity.AbsPath, args)
if err != nil {
log.Warn("Error while add command")
return errors.New(out + "\n" + err.Error())
@@ -38,14 +57,19 @@ func (file *File) Add(option AddOptions) error {
return nil
}
-// AddAll function is the wrapper of "git add ." command
-func (entity *RepoEntity) AddAll(option AddOptions) error {
+func addWithGit(entity *RepoEntity, file *File, option AddOptions) error {
args := make([]string, 0)
args = append(args, addCommand)
+ args = append(args, file.Name)
+ if option.Update {
+ args = append(args, "--update")
+ }
+ if option.Force {
+ args = append(args, "--force")
+ }
if option.DryRun {
args = append(args, "--dry-run")
}
- args = append(args, ".")
out, err := GenericGitCommandWithOutput(entity.AbsPath, args)
if err != nil {
log.Warn("Error while add command")
@@ -53,3 +77,12 @@ func (entity *RepoEntity) AddAll(option AddOptions) error {
}
return nil
}
+
+func addWithGoGit(entity *RepoEntity, file *File) error {
+ w, err := entity.Repository.Worktree()
+ if err != nil {
+ return err
+ }
+ _, err = w.Add(file.Name)
+ return nil
+}
diff --git a/pkg/git/cmd-commit.go b/pkg/git/cmd-commit.go
new file mode 100644
index 0000000..8ea5bfe
--- /dev/null
+++ b/pkg/git/cmd-commit.go
@@ -0,0 +1,93 @@
+package git
+
+import (
+ "errors"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+)
+
+var (
+ commitCmdMode string
+
+ commitCommand = "commit"
+ commitCmdModeLegacy = "git"
+ commitCmdModeNative = "go-git"
+)
+
+// CommitOptions defines the rules for commit operation
+type CommitOptions struct {
+ // CommitMsg
+ CommitMsg string
+ // User
+ User string
+ // Email
+ Email string
+}
+
+// CommitCommand
+func CommitCommand(entity *RepoEntity, options CommitOptions) (err error) {
+ // here we configure commit operation
+ // default mode is go-git (this may be configured)
+ commitCmdMode = commitCmdModeNative
+
+ switch commitCmdMode {
+ case commitCmdModeLegacy:
+ err = commitWithGit(entity, options)
+ return err
+ case commitCmdModeNative:
+ err = commitWithGoGit(entity, options)
+ return err
+ }
+ return errors.New("Unhandled commit operation")
+}
+
+// commitWithGit is simply a bare git commit -m <msg> command which is flexible
+func commitWithGit(entity *RepoEntity, options CommitOptions) (err error) {
+ args := make([]string, 0)
+ args = append(args, commitCommand)
+ args = append(args, "-m")
+ // parse options to command line arguments
+ if len(options.CommitMsg) > 0 {
+ args = append(args, options.CommitMsg)
+ }
+ if err := GenericGitCommand(entity.AbsPath, args); err != nil {
+ log.Warn("Error at git command (commit)")
+ return err
+ }
+ // till this step everything should be ok
+ err = entity.Refresh()
+ return err
+}
+
+// commitWithGoGit is the primary commit method
+func commitWithGoGit(entity *RepoEntity, options CommitOptions) (err error) {
+ config, err := entity.Repository.Config()
+ if err != nil {
+ return err
+ }
+ name := config.Raw.Section("user").Option("name")
+ email := config.Raw.Section("user").Option("email")
+ opt := &git.CommitOptions{
+ Author: &object.Signature{
+ Name: name,
+ Email: email,
+ When: time.Now(),
+ },
+ }
+
+ w, err := entity.Repository.Worktree()
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Commit(options.CommitMsg, opt)
+ if err != nil {
+ return err
+ }
+ // till this step everything should be ok
+ err = entity.Refresh()
+ return err
+}
diff --git a/pkg/git/cmd-config.go b/pkg/git/cmd-config.go
new file mode 100644
index 0000000..541b0be
--- /dev/null
+++ b/pkg/git/cmd-config.go
@@ -0,0 +1,107 @@
+package git
+
+import (
+ "errors"
+
+ log "github.com/sirupsen/logrus"
+)
+
+var (
+ configCmdMode string
+
+ configCommand = "config"
+ configCmdModeLegacy = "git"
+ configCmdModeNative = "go-git"
+)
+
+// CommitOptions defines the rules for commit operation
+type ConfigOptions struct {
+ // Section
+ Section string
+ // Option
+ Option string
+ // Site should be Global or Local
+ Site ConfigSite
+}
+
+type ConfigSite string
+
+const (
+ // ConfigStieLocal
+ ConfigStieLocal ConfigSite = "local"
+ // ConfgiSiteGlobal
+ ConfgiSiteGlobal ConfigSite = "global"
+)
+
+// Config
+func Config(entity *RepoEntity, options ConfigOptions) (value string, err error) {
+ // here we configure config operation
+ // default mode is go-git (this may be configured)
+ configCmdMode = configCmdModeLegacy
+
+ switch configCmdMode {
+ case configCmdModeLegacy:
+ value, err = configWithGit(entity, options)
+ return value, err
+ case configCmdModeNative:
+ value, err = configWithGoGit(entity, options)
+ return value, err
+ }
+ return value, errors.New("Unhandled config operation")
+}
+
+// configWithGit is simply a bare git commit -m <msg> command which is flexible
+func configWithGit(entity *RepoEntity, options ConfigOptions) (value string, err error) {
+ args := make([]string, 0)
+ args = append(args, configCommand)
+ if len(string(options.Site)) > 0 {
+ args = append(args, "--"+string(options.Site))
+ }
+ args = append(args, "--get")
+ args = append(args, options.Section+"."+options.Option)
+ // parse options to command line arguments
+ out, err := GenericGitCommandWithOutput(entity.AbsPath, args)
+ if err != nil {
+ return out, err
+ }
+ // till this step everything should be ok
+ return out, nil
+}
+
+// commitWithGoGit is the primary commit method
+func configWithGoGit(entity *RepoEntity, options ConfigOptions) (value string, err error) {
+ // TODO: add global search
+ config, err := entity.Repository.Config()
+ if err != nil {
+ return value, err
+ }
+ value = config.Raw.Section(options.Section).Option(options.Option)
+ return value, nil
+}
+
+// AddConfig
+func AddConfig(entity *RepoEntity, options ConfigOptions, value string) (err error) {
+ err = addConfigWithGit(entity, options, value)
+ return err
+
+}
+
+// addConfigWithGit is simply a bare git commit -m <msg> command which is flexible
+func addConfigWithGit(entity *RepoEntity, options ConfigOptions, value string) (err error) {
+ args := make([]string, 0)
+ args = append(args, commitCommand)
+ if len(string(options.Site)) > 0 {
+ args = append(args, "--"+string(options.Site))
+ }
+ args = append(args, "--add")
+ args = append(args, options.Section+"."+options.Option)
+ if len(value) > 0 {
+ args = append(args, value)
+ }
+ if err := GenericGitCommand(entity.AbsPath, args); err != nil {
+ log.Warn("Error at git command (commit)")
+ return err
+ }
+ // till this step everything should be ok
+ return entity.Refresh()
+}
diff --git a/pkg/git/cmd-fetch.go b/pkg/git/cmd-fetch.go
new file mode 100644
index 0000000..8a8bf9c
--- /dev/null
+++ b/pkg/git/cmd-fetch.go
@@ -0,0 +1,146 @@
+package git
+
+import (
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/config"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
+)
+
+var (
+ fetchCmdMode string
+ fetchTryCount int
+
+ fetchCommand = "fetch"
+ fetchCmdModeLegacy = "git"
+ fetchCmdModeNative = "go-git"
+ fetchMaxTry = 1
+)
+
+// FetchOptions defines the rules for fetch operation
+type FetchOptions struct {
+ // Name of the remote to fetch from. Defaults to origin.
+ RemoteName string
+ // Credentials holds the user and pswd information
+ Credentials Credentials
+ // Before fetching, remove any remote-tracking references that no longer
+ // exist on the remote.
+ Prune bool
+ // Show what would be done, without making any changes.
+ DryRun bool
+ // Force allows the fetch to update a local branch even when the remote
+ // branch does not descend from it.
+ Force bool
+ // There should be more room for authentication, tags and progress
+}
+
+// Fetch branches refs from one or more other repositories, along with the
+// objects necessary to complete their histories
+func Fetch(entity *RepoEntity, options FetchOptions) (err error) {
+ // here we configure fetch operation
+ // default mode is go-git (this may be configured)
+ fetchCmdMode = fetchCmdModeNative
+ fetchTryCount = 0
+ // prune and dry run is not supported from go-git yet, rely on old friend
+ if options.Prune || options.DryRun {
+ fetchCmdMode = fetchCmdModeLegacy
+ }
+ switch fetchCmdMode {
+ case fetchCmdModeLegacy:
+ err = fetchWithGit(entity, options)
+ return err
+ case fetchCmdModeNative:
+ // this should be the refspec as default, let's give it a try
+ refspec := "+" + "refs/heads/" + entity.Branch.Name + ":" + "/refs/remotes/" + entity.Remote.Branch.Name
+ err = fetchWithGoGit(entity, options, refspec)
+ return err
+ }
+ return nil
+}
+
+// fetchWithGit is simply a bare git fetch <remote> command which is flexible
+// for complex operations, but on the other hand, it ties the app to another
+// tool. To avoid that, using native implementation is preferred.
+func fetchWithGit(entity *RepoEntity, options FetchOptions) (err error) {
+ args := make([]string, 0)
+ args = append(args, fetchCommand)
+ // parse options to command line arguments
+ if len(options.RemoteName) > 0 {
+ args = append(args, options.RemoteName)
+ }
+ if options.Prune {
+ args = append(args, "-p")
+ }
+ if options.Force {
+ args = append(args, "-f")
+ }
+ if options.DryRun {
+ args = append(args, "--dry-run")
+ }
+ if err := GenericGitCommand(entity.AbsPath, args); err != nil {
+ log.Warn("Error at git command (fetch)")
+ return err
+ }
+ // till this step everything should be ok
+ err = entity.Refresh()
+ return err
+}
+
+// fetchWithGoGit is the primary fetch method and refspec is the main feature.
+// RefSpec is a mapping from local branches to remote references The format of
+// the refspec is an optional +, followed by <src>:<dst>, where <src> is the
+// pattern for references on the remote side and <dst> is where those references
+// will be written locally. The + tells Git to update the reference even if it
+// isn’t a fast-forward.
+func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (err error) {
+ opt := &git.FetchOptions{
+ RemoteName: options.RemoteName,
+ RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
+ Force: options.Force,
+ }
+ // if any credential is given, let's add it to the git.FetchOptions
+ if len(options.Credentials.User) > 0 {
+ protocol, err := entity.authProtocol(entity.Remote)
+ if err != nil {
+ return err
+ }
+ if protocol == authProtocolHttp || protocol == authProtocolHttps {
+ opt.Auth = &http.BasicAuth{
+ Username: options.Credentials.User,
+ Password: options.Credentials.Password,
+ }
+ } else {
+ return ErrInvalidAuthMethod
+ }
+ }
+
+ err = entity.Repository.Fetch(opt)
+ if err != nil {
+ if err == git.NoErrAlreadyUpToDate {
+ // Already up-to-date
+ log.Warn(err.Error())
+ // TODO: submit a PR for this kind of error, this type of catch is lame
+ } else if strings.Contains(err.Error(), "couldn't find remote ref") {
+ // we dont have remote ref, so lets pull other things.. maybe it'd be useful
+ rp := entity.Remote.RefSpecs[0]
+ if fetchTryCount < fetchMaxTry {
+ fetchTryCount++
+ fetchWithGoGit(entity, options, rp)
+ } else {
+ return err
+ }
+ } else if err == transport.ErrAuthenticationRequired {
+ log.Warn(err.Error())
+ return ErrAuthenticationRequired
+ } else {
+ log.Warn(err.Error())
+ return err
+ }
+ }
+ // till this step everything should be ok
+ err = entity.Refresh()
+ return err
+}
diff --git a/pkg/git/merge.go b/pkg/git/cmd-merge.go
index 984bc4b..984bc4b 100644
--- a/pkg/git/merge.go
+++ b/pkg/git/cmd-merge.go
diff --git a/pkg/git/cmd-reset.go b/pkg/git/cmd-reset.go
new file mode 100644
index 0000000..2e6ed91
--- /dev/null
+++ b/pkg/git/cmd-reset.go
@@ -0,0 +1,130 @@
+package git
+
+import (
+ "errors"
+
+ log "github.com/sirupsen/logrus"
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+)
+
+var (
+ resetCmdMode string
+
+ resetCommand = "reset"
+ resetCmdModeLegacy = "git"
+ resetCmdModeNative = "go-git"
+)
+
+// ResetOptions defines the rules of git reset command
+type ResetOptions struct {
+ // Hash is the reference to be resetted
+ Hash string
+ // Type is the mode of a reset operation
+ Rtype ResetType
+}
+
+type ResetType string
+
+const (
+ // ResetHard Resets the index and working tree. Any changes to tracked
+ // files in the working tree since <commit> are discarded.
+ ResetHard ResetType = "hard"
+ // ResetMixed Resets the index but not the working tree (i.e., the changed
+ // files are preserved but not marked for commit) and reports what has not
+ // been updated. This is the default action.
+ ResetMixed ResetType = "mixed"
+ // ResetMerge Resets the index and updates the files in the working tree
+ // that are different between <commit> and HEAD, but keeps those which are
+ // different between the index and working tree
+ ResetMerge ResetType = "merge"
+ // ResetSoft Does not touch the index file or the working tree at all
+ // (but resets the head to <commit>
+ ResetSoft ResetType = "soft"
+ // ResetKeep Resets index entries and updates files in the working tree
+ // that are different between <commit> and HEAD
+ ResetKeep ResetType = "keep"
+)
+
+// Reset is the wrapper of "git reset" command
+func Reset(entity *RepoEntity, file *File, option ResetOptions) error {
+ resetCmdMode = addCmdModeLegacy
+
+ switch resetCmdMode {
+ case resetCmdModeLegacy:
+ err := resetWithGit(entity, file, option)
+ return err
+ case resetCmdModeNative:
+
+ }
+ return errors.New("Unhandled reset operation")
+}
+
+func resetWithGit(entity *RepoEntity, file *File, option ResetOptions) error {
+ args := make([]string, 0)
+ args = append(args, resetCommand)
+
+ args = append(args, "--")
+ args = append(args, file.Name)
+ if len(option.Rtype) > 0 {
+ args = append(args, "--"+string(option.Rtype))
+ }
+ out, err := GenericGitCommandWithOutput(entity.AbsPath, args)
+ if err != nil {
+ log.Warn("Error while reset command")
+ return errors.New(out + "\n" + err.Error())
+ }
+ return nil
+}
+
+// ResetAll resets the changes in a repository, should be used wise
+func ResetAll(entity *RepoEntity, option ResetOptions) error {
+ resetCmdMode = addCmdModeNative
+
+ switch resetCmdMode {
+ case resetCmdModeLegacy:
+ err := resetAllWithGit(entity, option)
+ return err
+ case resetCmdModeNative:
+ err := resetAllWithGoGit(entity, option)
+ return err
+ }
+ return errors.New("Unhandled reset operation")
+}
+
+func resetAllWithGit(entity *RepoEntity, option ResetOptions) error {
+ args := make([]string, 0)
+ args = append(args, resetCommand)
+ if len(option.Rtype) > 0 {
+ args = append(args, "--"+string(option.Rtype))
+ }
+ out, err := GenericGitCommandWithOutput(entity.AbsPath, args)
+ if err != nil {
+ log.Warn("Error while add command")
+ return errors.New(out + "\n" + err.Error())
+ }
+ return nil
+}
+
+func resetAllWithGoGit(entity *RepoEntity, option ResetOptions) error {
+ w, err := entity.Repository.Worktree()
+ if err != nil {
+ return err
+ }
+ var mode git.ResetMode
+ switch option.Rtype {
+ case ResetHard:
+ mode = git.HardReset
+ case ResetMixed:
+ mode = git.MixedReset
+ case ResetMerge:
+ mode = git.MergeReset
+ case ResetSoft:
+ mode = git.SoftReset
+ }
+ err = w.Reset(&git.ResetOptions{
+ Commit: plumbing.NewHash(option.Hash),
+ Mode: mode,
+ })
+ return err
+}
diff --git a/pkg/git/rev-list.go b/pkg/git/cmd-rev-list.go
index 664d69f..664d69f 100644
--- a/pkg/git/rev-list.go
+++ b/pkg/git/cmd-rev-list.go
diff --git a/pkg/git/stash.go b/pkg/git/cmd-stash.go
index 9d813a6..9d813a6 100644
--- a/pkg/git/stash.go
+++ b/pkg/git/cmd-stash.go
diff --git a/pkg/git/status.go b/pkg/git/cmd-status.go
index 4cca604..508a27c 100644
--- a/pkg/git/status.go
+++ b/pkg/git/cmd-status.go
@@ -1,14 +1,22 @@
package git
import (
+ "errors"
"os"
"regexp"
+ "sort"
"strings"
log "github.com/sirupsen/logrus"
)
-var statusCommand = "status"
+var (
+ statusCmdMode string
+
+ statusCommand = "status"
+ statusCmdModeLegacy = "git"
+ statusCmdModeNative = "go-git"
+)
// File represents the status of a file in an index or work tree
type File struct {
@@ -19,7 +27,7 @@ type File struct {
}
// FileStatus is the short representation of state of a file
-type FileStatus rune
+type FileStatus byte
var (
// StatusNotupdated says file not updated
@@ -55,9 +63,21 @@ func shortStatus(entity *RepoEntity, option string) string {
return out
}
+func Status(entity *RepoEntity) ([]*File, error) {
+ statusCmdMode = statusCmdModeNative
+
+ switch statusCmdMode {
+ case statusCmdModeLegacy:
+ return statusWithGit(entity)
+ case statusCmdModeNative:
+ return statusWithGoGit(entity)
+ }
+ return nil, errors.New("Unhandled status operation")
+}
+
// LoadFiles function simply commands a git status and collects output in a
// structured way
-func (entity *RepoEntity) LoadFiles() ([]*File, error) {
+func statusWithGit(entity *RepoEntity) ([]*File, error) {
files := make([]*File, 0)
output := shortStatus(entity, "--untracked-files=all")
if len(output) == 0 {
@@ -65,8 +85,8 @@ func (entity *RepoEntity) LoadFiles() ([]*File, error) {
}
fileslist := strings.Split(output, "\n")
for _, file := range fileslist {
- x := rune(file[0])
- y := rune(file[1])
+ x := byte(file[0])
+ y := byte(file[1])
relativePathRegex := regexp.MustCompile(`[(\w|/|.|\-)]+`)
path := relativePathRegex.FindString(file[2:])
@@ -77,6 +97,29 @@ func (entity *RepoEntity) LoadFiles() ([]*File, error) {
Y: FileStatus(y),
})
}
+ sort.Sort(filesAlphabetical(files))
+ return files, nil
+}
+
+func statusWithGoGit(entity *RepoEntity) ([]*File, error) {
+ files := make([]*File, 0)
+ w, err := entity.Repository.Worktree()
+ if err != nil {
+ return files, err
+ }
+ s, err := w.Status()
+ if err != nil {
+ return files, err
+ }
+ for k, v := range s {
+ files = append(files, &File{
+ Name: k,
+ AbsPath: entity.AbsPath + string(os.PathSeparator) + k,
+ X: FileStatus(v.Staging),
+ Y: FileStatus(v.Worktree),
+ })
+ }
+ sort.Sort(filesAlphabetical(files))
return files, nil
}
diff --git a/pkg/git/commands.go b/pkg/git/cmd.go
index b78b501..b78b501 100644
--- a/pkg/git/commands.go
+++ b/pkg/git/cmd.go
diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go
deleted file mode 100644
index 5dad021..0000000
--- a/pkg/git/fetch.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package git
-
-import (
- log "github.com/sirupsen/logrus"
-)
-
-var fetchCommand = "fetch"
-
-// FetchOptions defines the rules for fetch operation
-type FetchOptions struct {
- // Name of the remote to fetch from. Defaults to origin.
- RemoteName string
- // Before fetching, remove any remote-tracking references that no longer
- // exist on the remote.
- Prune bool
- // Show what would be done, without making any changes.
- DryRun bool
- // Force allows the fetch to update a local branch even when the remote
- // branch does not descend from it.
- Force bool
-}
-
-// Fetch branches refs from one or more other repositories, along with the
-// objects necessary to complete their histories
-func Fetch(entity *RepoEntity, options FetchOptions) error {
- args := make([]string, 0)
- args = append(args, fetchCommand)
- if len(options.RemoteName) > 0 {
- args = append(args, options.RemoteName)
- }
- if options.Prune {
- args = append(args, "-p")
- }
- if options.Force {
- args = append(args, "-f")
- }
- if options.DryRun {
- args = append(args, "--dry-run")
- }
- if err := GenericGitCommand(entity.AbsPath, args); err != nil {
- log.Warn("Error while fetching")
- return err
- }
- entity.Refresh()
- return nil
-}
diff --git a/pkg/queue/queue.go b/pkg/git/job-queue.go
index 93626f0..e95250a 100644
--- a/pkg/queue/queue.go
+++ b/pkg/git/job-queue.go
@@ -1,9 +1,7 @@
-package queue
+package git
import (
"errors"
-
- "github.com/isacikgoz/gitbatch/pkg/git"
)
// JobQueue holds the slice of Jobs
@@ -51,7 +49,7 @@ func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) {
// RemoveFromQueue deletes the given entity and its job from the queue
// TODO: it is not safe if the job has been started
-func (jobQueue *JobQueue) RemoveFromQueue(entity *git.RepoEntity) error {
+func (jobQueue *JobQueue) RemoveFromQueue(entity *RepoEntity) error {
removed := false
for i, job := range jobQueue.series {
if job.Entity.RepoID == entity.RepoID {
@@ -68,7 +66,7 @@ func (jobQueue *JobQueue) RemoveFromQueue(entity *git.RepoEntity) error {
// IsInTheQueue function; since the job and entity is not tied with its own
// struct, this function returns true if that entity is in the queue along with
// the jobs type
-func (jobQueue *JobQueue) IsInTheQueue(entity *git.RepoEntity) (inTheQueue bool, jt JobType) {
+func (jobQueue *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) {
inTheQueue = false
for _, job := range jobQueue.series {
if job.Entity.RepoID == entity.RepoID {
diff --git a/pkg/git/job.go b/pkg/git/job.go
new file mode 100644
index 0000000..7dc4126
--- /dev/null
+++ b/pkg/git/job.go
@@ -0,0 +1,78 @@
+package git
+
+import ()
+
+// Job relates the type of the operation and the entity
+type Job struct {
+ // JobType is to select operation type that will be applied to repository
+ JobType JobType
+ // Entity points to the repository that will be used for operation
+ Entity *RepoEntity
+ // Options is a placeholder for operation options
+ Options interface{}
+}
+
+// JobType is the a git operation supported
+type JobType string
+
+const (
+ // Fetch is wrapper of git fetch command
+ FetchJob JobType = "fetch"
+ // Pull is wrapper of git pull command
+ PullJob JobType = "pull"
+ // Merge is wrapper of git merge command
+ MergeJob JobType = "merge"
+)
+
+// starts the job
+func (job *Job) start() error {
+ job.Entity.State = Working
+ // TODO: Handle errors?
+ // TOOD: Better implementation required
+ switch mode := job.JobType; mode {
+ case FetchJob:
+ var opts FetchOptions
+ if job.Options != nil {
+ opts = job.Options.(FetchOptions)
+ } else {
+ opts = FetchOptions{
+ RemoteName: job.Entity.Remote.Name,
+ }
+ }
+ if err := Fetch(job.Entity, opts); err != nil {
+ job.Entity.State = Fail
+ return err
+ }
+ case PullJob:
+ var opts FetchOptions
+ if job.Options != nil {
+ opts = job.Options.(FetchOptions)
+ } else {
+ opts = FetchOptions{
+ RemoteName: job.Entity.Remote.Name,
+ }
+ }
+ if err := Fetch(job.Entity, opts); err != nil {
+ job.Entity.State = Fail
+ return err
+ }
+ if err := Merge(job.Entity, MergeOptions{
+ BranchName: job.Entity.Remote.Branch.Name,
+ }); err != nil {
+ job.Entity.State = Fail
+ return nil
+ }
+ case MergeJob:
+ if err := Merge(job.Entity, MergeOptions{
+ BranchName: job.Entity.Remote.Branch.Name,
+ }); err != nil {
+ job.Entity.State = Fail
+ return nil
+ }
+ default:
+ job.Entity.State = Available
+ return nil
+ }
+ job.Entity.State = Success
+ return nil
+}
diff --git a/pkg/git/remote.go b/pkg/git/remote.go
index 96b8aa7..27f627b 100644
--- a/pkg/git/remote.go
+++ b/pkg/git/remote.go
@@ -10,6 +10,7 @@ import (
type Remote struct {
Name string
URL []string
+ RefSpecs []string
Branch *RemoteBranch
Branches []*RemoteBranch
}
@@ -57,10 +58,14 @@ func (entity *RepoEntity) loadRemotes() error {
remotes, err := r.Remotes()
for _, rm := range remotes {
-
+ rfs := make([]string, 0)
+ for _, rf := range rm.Config().Fetch {
+ rfs = append(rfs, string(rf))
+ }
remote := &Remote{
- Name: rm.Config().Name,
- URL: rm.Config().URLs,
+ Name: rm.Config().Name,
+ URL: rm.Config().URLs,
+ RefSpecs: rfs,
}
remote.loadRemoteBranches(entity)
if len(remote.Branches) > 0 {
diff --git a/pkg/git/repository.go b/pkg/git/repository.go
index 8d0160b..ff67611 100644
--- a/pkg/git/repository.go
+++ b/pkg/git/repository.go
@@ -39,10 +39,12 @@ const (
Queued RepoState = 1
// Working means an operation is just started for this repository
Working RepoState = 2
+ // Paused is expected when a user interaction is required
+ Paused RepoState = 3
// Success is the expected outcome of the operation
- Success RepoState = 3
+ Success RepoState = 4
// Fail is the unexpected outcome of the operation
- Fail RepoState = 4
+ Fail RepoState = 5
)
// InitializeRepository initializes a RepoEntity struct with its belongings.
diff --git a/pkg/git/reset.go b/pkg/git/reset.go
deleted file mode 100644
index f93225c..0000000
--- a/pkg/git/reset.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package git
-
-import (
- "errors"
- "strings"
-
- log "github.com/sirupsen/logrus"
-)
-
-var resetCommand = "reset"
-
-// ResetOptions defines the rules of git reset command
-type ResetOptions struct {
- Hard bool
- Merge bool
- Keep bool
-}
-
-// Reset is the wrapper of "git reset" command
-func (file *File) Reset(option ResetOptions) error {
- args := make([]string, 0)
- args = append(args, resetCommand)
- args = append(args, "--")
- args = append(args, file.Name)
- if option.Hard {
- args = append(args, "--hard")
- }
- if option.Merge {
- args = append(args, "--merge")
- }
- if option.Keep {
- args = append(args, "--keep")
- }
- out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args)
- if err != nil {
- log.Warn("Error while add command")
- return errors.New(out + "\n" + err.Error())
- }
- return nil
-}
-
-// ResetAll resets the changes in a repository, should be used wise
-func (entity *RepoEntity) ResetAll(option ResetOptions) error {
- args := make([]string, 0)
- args = append(args, resetCommand)
- if option.Hard {
- args = append(args, "--hard")
- }
- out, err := GenericGitCommandWithOutput(entity.AbsPath, args)
- if err != nil {
- log.Warn("Error while add command")
- return errors.New(out + "\n" + err.Error())
- }
- return nil
-}
diff --git a/pkg/git/util-errors.go b/pkg/git/util-errors.go
new file mode 100644
index 0000000..8621cbc
--- /dev/null
+++ b/pkg/git/util-errors.go
@@ -0,0 +1,24 @@
+package git
+
+import (
+ "errors"
+)
+
+var (
+ // ErrGitCommand is thrown when git command returned an error code
+ ErrGitCommand = errors.New("Git command returned error code")
+ // ErrAuthenticationRequired is thrown when an authentication required on
+ // a remote operation
+ ErrAuthenticationRequired = errors.New("Authentication required")
+ // ErrAuthorizationFailed is thrown when authorization failed while trying
+ // to authenticate with remote
+ ErrAuthorizationFailed = errors.New("Authorization failed")
+ // ErrInvalidAuthMethod is thrown when invalid auth method is invoked
+ ErrInvalidAuthMethod = errors.New("invalid auth method")
+ // ErrAlreadyUpToDate is thrown when a repository is already up to date
+ // with its src on merge/fetch/pull
+ ErrAlreadyUpToDate = errors.New("Already up to date")
+ // ErrCouldNotFindRemoteRef is thrown when trying to fetch/pull cannot
+ // find suitable remote reference
+ ErrCouldNotFindRemoteRef = errors.New("Could not find remote ref")
+)
diff --git a/pkg/git/load.go b/pkg/git/util-load.go
index e35e1c1..e35e1c1 100644
--- a/pkg/git/load.go
+++ b/pkg/git/util-load.go
diff --git a/pkg/git/repository-sort.go b/pkg/git/util-sort.go
index de5f454..e3067bc 100644
--- a/pkg/git/repository-sort.go
+++ b/pkg/git/util-sort.go
@@ -57,3 +57,42 @@ func (s LastModified) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s LastModified) Less(i, j int) bool {
return s[i].ModTime.Unix() > s[j].ModTime.Unix()
}
+
+// filesAlphabetical slice is the re-ordered *File slice that sorted according
+// to alphabetical order (A-Z)
+type filesAlphabetical []*File
+
+// Len is the interface implementation for Alphabetical sorting function
+func (s filesAlphabetical) Len() int { return len(s) }
+
+// Swap is the interface implementation for Alphabetical sorting function
+func (s filesAlphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+// Less is the interface implementation for Alphabetical sorting function
+func (s filesAlphabetical) Less(i, j int) bool {
+ iRunes := []rune(s[i].Name)
+ jRunes := []rune(s[j].Name)
+
+ max := len(iRunes)
+ if max > len(jRunes) {
+ max = len(jRunes)
+ }
+
+ for idx := 0; idx < max; idx++ {
+ ir := iRunes[idx]
+ jr := jRunes[idx]
+
+ lir := unicode.ToLower(ir)
+ ljr := unicode.ToLower(jr)
+
+ if lir != ljr {
+ return lir < ljr
+ }
+
+ // the lowercase runes are the same, so compare the original
+ if ir != jr {
+ return ir < jr
+ }
+ }
+ return false
+}
diff --git a/pkg/gui/authenticationview.go b/pkg/gui/authenticationview.go
new file mode 100644
index 0000000..9c830cd
--- /dev/null
+++ b/pkg/gui/authenticationview.go
@@ -0,0 +1,176 @@
+package gui
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/jroimartin/gocui"
+ log "github.com/sirupsen/logrus"
+)
+
+var (
+ // this is required so we can know where we can return
+ authenticationReturnView string
+ // these views used as a label for git repository address and credential views
+ authenticationViewFeature = viewFeature{Name: "authentication", Title: " Authentication "}
+ authUserLabelFeature = viewFeature{Name: "authuserlabel", Title: " User: "}
+ authPswdLabelViewFeature = viewFeature{Name: "authpasswdlabel", Title: " Password: "}
+ // these views used as a input for the credentials
+ authUserFeature = viewFeature{Name: "authuser", Title: " User "}
+ authPasswordViewFeature = viewFeature{Name: "authpasswd", Title: " Password "}
+
+ // these are the view groups, so that we can assign common keybindings
+ authViews = []viewFeature{authUserFeature, authPasswordViewFeature}
+ authLabels = []viewFeature{authenticationViewFeature, authUserLabelFeature, authPswdLabelViewFeature}
+
+ // we can hold the job that is required to authenticate
+ jobRequiresAuth *git.Job
+)
+
+// open an auth view to get user credentials
+func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job *git.Job, returnViewName string) error {
+ maxX, maxY := g.Size()
+ // lets add this job since it is removed from the queue
+ // also it is already unsuccessfully finished
+ if err := jobQueue.AddJob(job); err != nil {
+ return err
+ }
+ jobRequiresAuth = job
+ if job.Entity.State != git.Fail {
+ if err := jobQueue.RemoveFromQueue(job.Entity); err != nil {
+ log.Fatal(err.Error())
+ return err
+ }
+ }
+ authenticationReturnView = returnViewName
+ v, err := g.SetView(authenticationViewFeature.Name, maxX/2-30, maxY/2-2, maxX/2+30, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(v, keySymbol+selectionIndicator+red.Sprint(jobRequiresAuth.Entity.Remote.URL[0]))
+ }
+ g.Cursor = true
+ if err := gui.openUserView(g); err != nil {
+ return err
+ }
+ return gui.openPasswordView(g)
+}
+
+// close the opened auth views
+func (gui *Gui) closeAuthenticationView(g *gocui.Gui, v *gocui.View) error {
+ g.Cursor = false
+ for _, vf := range authLabels {
+ if err := g.DeleteView(vf.Name); err != nil {
+ return nil
+ }
+ }
+ for _, vf := range authViews {
+ if err := g.DeleteView(vf.Name); err != nil {
+ return nil
+ }
+ }
+ return gui.closeViewCleanup(authenticationReturnView)
+}
+
+// close the opened auth views and submit the credentials
+func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error {
+ g.Cursor = false
+ // in order to read buffer of the views, first we need to find'em
+ v_user, err := g.View(authUserFeature.Name)
+ v_pswd, err := g.View(authPasswordViewFeature.Name)
+ // the return string of the views contain trailing new lines
+ re := regexp.MustCompile(`\r?\n`)
+ creduser := re.ReplaceAllString(v_user.ViewBuffer(), "")
+ credpswd := re.ReplaceAllString(v_pswd.ViewBuffer(), "")
+ // since the git ops require different types of options we better switch
+ switch mode := jobRequiresAuth.JobType; mode {
+ case git.FetchJob:
+ jobRequiresAuth.Options = git.FetchOptions{
+ RemoteName: jobRequiresAuth.Entity.Remote.Name,
+ Credentials: git.Credentials{
+ User: creduser,
+ Password: credpswd,
+ },
+ }
+ case git.PullJob:
+ // we handle pull as fetch&merge so same rule applies
+ jobRequiresAuth.Options = git.FetchOptions{
+ RemoteName: jobRequiresAuth.Entity.Remote.Name,
+ Credentials: git.Credentials{
+ User: creduser,
+ Password: credpswd,
+ },
+ }
+ }
+ jobRequiresAuth.Entity.State = git.Queued
+ // add this job to the last of the queue
+ err = gui.State.Queue.AddJob(jobRequiresAuth)
+ if err != nil {
+ return err
+ }
+ // refresh views with the updates
+ gui.refreshMain(g)
+ gui.closeAuthenticationView(g, v)
+ v_return, err := g.View(authenticationReturnView)
+ gui.startQueue(g, v_return)
+ return nil
+}
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openUserView(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+ // first, create the label for user
+ vlabel, err := g.SetView(authUserLabelFeature.Name, maxX/2-30, maxY/2-1, maxX/2-19, maxY/2+1)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(vlabel, authUserLabelFeature.Title)
+ vlabel.Frame = false
+ }
+ // second, crete the user input
+ v, err := g.SetView(authUserFeature.Name, maxX/2-18, maxY/2-1, maxX/2+29, maxY/2+1)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = authUserFeature.Title
+ v.Editable = true
+ v.Frame = false
+ }
+ return gui.focusToView(authUserFeature.Name)
+}
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openPasswordView(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+ // first, create the label for password
+ vlabel, err := g.SetView(authPswdLabelViewFeature.Name, maxX/2-30, maxY/2, maxX/2-19, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(vlabel, authPswdLabelViewFeature.Title)
+ vlabel.Frame = false
+ }
+ // second, crete the masked password input
+ v, err := g.SetView(authPasswordViewFeature.Name, maxX/2-18, maxY/2, maxX/2+29, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = authPasswordViewFeature.Title
+ v.Editable = true
+ v.Mask ^= '*'
+ v.Frame = false
+ }
+ return nil
+}
+
+// focus to next view
+func (gui *Gui) nextAuthView(g *gocui.Gui, v *gocui.View) error {
+ err := gui.nextViewOfGroup(g, v, authViews)
+ return err
+}
diff --git a/pkg/gui/cheatsheet.go b/pkg/gui/cheatsheet.go
index 43020a5..4b2f97e 100644
--- a/pkg/gui/cheatsheet.go
+++ b/pkg/gui/cheatsheet.go
@@ -24,11 +24,7 @@ func (gui *Gui) openCheatSheetView(g *gocui.Gui, v *gocui.View) error {
}
}
}
- gui.updateKeyBindingsView(g, cheatSheetViewFeature.Name)
- if _, err := g.SetCurrentView(cheatSheetViewFeature.Name); err != nil {
- return err
- }
- return nil
+ return gui.focusToView(cheatSheetViewFeature.Name)
}
// close the application controls and do the clean job
@@ -36,9 +32,5 @@ 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
+ return gui.closeViewCleanup(mainViewFeature.Name)
}
diff --git a/pkg/gui/commitview.go b/pkg/gui/commitview.go
new file mode 100644
index 0000000..94ef574
--- /dev/null
+++ b/pkg/gui/commitview.go
@@ -0,0 +1,187 @@
+package gui
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+
+ "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/jroimartin/gocui"
+)
+
+var (
+ commitFrameViewFeature = viewFeature{Name: "commitframe", Title: " Frame "}
+ commitUserNameLabelFeature = viewFeature{Name: "commitusernamelabel", Title: " Name: "}
+ commitUserEmailLabelViewFeature = viewFeature{Name: "commituseremaillabel", Title: " E-Mail: "}
+
+ // these views used as a input for the credentials
+ commitMessageViewFeature = viewFeature{Name: "commitmessage", Title: " Commit Mesage "}
+ commitUserUserViewFeature = viewFeature{Name: "commitusername", Title: " Name "}
+ commitUserEmailViewFeature = viewFeature{Name: "commituseremail", Title: " E-Mail "}
+
+ commitViews = []viewFeature{commitMessageViewFeature, commitUserUserViewFeature, commitUserEmailViewFeature}
+ commitLabelViews = []viewFeature{commitFrameViewFeature, commitUserNameLabelFeature, commitUserEmailLabelViewFeature}
+)
+
+// open the commit message views
+func (gui *Gui) openCommitMessageView(g *gocui.Gui, v *gocui.View) error {
+ maxX, maxY := g.Size()
+ commitMesageReturnView = v.Name()
+ v_frame, err := g.SetView(commitFrameViewFeature.Name, maxX/2-30, maxY/2-4, maxX/2+30, maxY/2+3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v_frame.Frame = true
+ fmt.Fprintln(v_frame, " Enter your commit message:")
+ }
+ v, err = g.SetView(commitMessageViewFeature.Name, maxX/2-29, maxY/2-3, maxX/2+29, maxY/2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Frame = false
+ v.Wrap = true
+ v.Editable = true
+ v.Editor = gocui.DefaultEditor
+ g.Cursor = true
+ }
+ if err := gui.openCommitUserNameView(g); err != nil {
+ return err
+ }
+ if err := gui.openCommitUserEmailView(g); err != nil {
+ return err
+ }
+ gui.updateKeyBindingsView(g, commitMessageViewFeature.Name)
+ if _, err := g.SetCurrentView(commitMessageViewFeature.Name); err != nil {
+ return err
+ }
+ return nil
+}
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openCommitUserNameView(g *gocui.Gui) error {
+ entity := gui.getSelectedRepository()
+ maxX, maxY := g.Size()
+ // first, create the label for user
+ vlabel, err := g.SetView(commitUserNameLabelFeature.Name, maxX/2-30, maxY/2, maxX/2-19, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(vlabel, commitUserNameLabelFeature.Title)
+ vlabel.Frame = false
+ }
+ // second, crete the user input
+ v, err := g.SetView(commitUserUserViewFeature.Name, maxX/2-18, maxY/2, maxX/2+29, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ name, err := git.Config(entity, git.ConfigOptions{
+ Section: "user",
+ Option: "name",
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(v, name)
+ v.Editable = true
+ v.Frame = false
+ }
+ return nil
+}
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openCommitUserEmailView(g *gocui.Gui) error {
+ entity := gui.getSelectedRepository()
+ maxX, maxY := g.Size()
+ // first, create the label for password
+ vlabel, err := g.SetView(commitUserEmailLabelViewFeature.Name, maxX/2-30, maxY/2+1, maxX/2-19, maxY/2+3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(vlabel, commitUserEmailLabelViewFeature.Title)
+ vlabel.Frame = false
+ }
+ // second, crete the masked password input
+ v, err := g.SetView(commitUserEmailViewFeature.Name, maxX/2-18, maxY/2+1, maxX/2+29, maxY/2+3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ email, err := git.Config(entity, git.ConfigOptions{
+ Section: "user",
+ Option: "email",
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(v, email)
+ v.Editable = true
+ v.Frame = false
+ }
+ return nil
+}
+
+// close the opened commite mesage view
+func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error {
+ entity := gui.getSelectedRepository()
+ // in order to read buffer of the views, first we need to find'em
+ v_msg, err := g.View(commitMessageViewFeature.Name)
+ v_name, err := g.View(commitUserUserViewFeature.Name)
+ v_email, err := g.View(commitUserEmailViewFeature.Name)
+ // the return string of the views contain trailing new lines
+ re := regexp.MustCompile(`\r?\n`)
+ // TODO: maybe intentionally added new lines?
+ msg := re.ReplaceAllString(v_msg.ViewBuffer(), "")
+ name := re.ReplaceAllString(v_name.ViewBuffer(), "")
+ email := re.ReplaceAllString(v_email.ViewBuffer(), "")
+ if len(email) <= 0 {
+ return errors.New("User email needs to be provided")
+ }
+ err = git.CommitCommand(entity, git.CommitOptions{
+ CommitMsg: msg,
+ User: name,
+ Email: email,
+ })
+ if err != nil {
+ return err
+ }
+ entity.Refresh()
+ err = gui.closeCommitMessageView(g, v)
+ return err
+}
+
+// focus to next view
+func (gui *Gui) nextCommitView(g *gocui.Gui, v *gocui.View) error {
+ err := gui.nextViewOfGroup(g, v, commitViews)
+ return err
+}
+
+// close the opened commite mesage view
+func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error {
+ entity := gui.getSelectedRepository()
+ g.Cursor = false
+ for _, view := range commitViews {
+ if err := g.DeleteView(view.Name); err != nil {
+ return err
+ }
+ }
+ for _, view := range commitLabelViews {
+ if err := g.DeleteView(view.Name); err != nil {
+ return err
+ }
+ }
+ if err := gui.refreshMain(g); err != nil {
+ return err
+ }
+ if err := gui.refreshViews(g, entity); err != nil {
+ return err
+ }
+ if err := refreshAllStatusView(g, entity, true); err != nil {
+ return err
+ }
+ return gui.closeViewCleanup(commitMesageReturnView)
+}
diff --git a/pkg/gui/diffview.go b/pkg/gui/diffview.go
index f99d7f2..55286e9 100644
--- a/pkg/gui/diffview.go
+++ b/pkg/gui/diffview.go
@@ -22,8 +22,7 @@ func (gui *Gui) prepareDiffView(g *gocui.Gui, v *gocui.View, display []string) (
out.Title = diffViewFeature.Title
out.Overwrite = true
out.Wrap = true
- gui.updateKeyBindingsView(g, diffViewFeature.Name)
- if _, err = g.SetCurrentView(diffViewFeature.Name); err != nil {
+ if err = gui.focusToView(diffViewFeature.Name); err != nil {
return out, err
}
for _, line := range display {
@@ -63,9 +62,9 @@ func (gui *Gui) openFileDiffView(g *gocui.Gui, v *gocui.View) (err error) {
var files []*git.File
switch v.Name() {
case unstageViewFeature.Name:
- _, files, err = generateFileLists(entity)
+ _, files, err = populateFileLists(entity)
case stageViewFeature.Name:
- files, _, err = generateFileLists(entity)
+ files, _, err = populateFileLists(entity)
}
if err != nil {
return err
@@ -126,9 +125,5 @@ func (gui *Gui) closeCommitDiffView(g *gocui.Gui, v *gocui.View) error {
if err := g.DeleteView(v.Name()); err != nil {
return nil
}
- if _, err := g.SetCurrentView(diffReturnView); err != nil {
- return err
- }
- gui.updateKeyBindingsView(g, diffReturnView)
- return nil
+ return gui.closeViewCleanup(diffReturnView)
}
diff --git a/pkg/gui/errorview.go b/pkg/gui/errorview.go
index b4be162..f679b54 100644
--- a/pkg/gui/errorview.go
+++ b/pkg/gui/errorview.go
@@ -23,11 +23,7 @@ func (gui *Gui) openErrorView(g *gocui.Gui, message, note, returnViewName string
fmt.Fprintln(v, message)
fmt.Fprintln(v, ps)
}
- gui.updateKeyBindingsView(g, errorViewFeature.Name)
- if _, err := g.SetCurrentView(errorViewFeature.Name); err != nil {
- return err
- }
- return nil
+ return gui.focusToView(errorViewFeature.Name)
}
// close the opened error view
@@ -36,9 +32,5 @@ 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(errorReturnView); err != nil {
- return err
- }
- gui.updateKeyBindingsView(g, errorReturnView)
- return nil
+ return gui.closeViewCleanup(errorReturnView)
}
diff --git a/pkg/gui/gui-util.go b/pkg/gui/gui-util.go
index bd02f7c..f203bbf 100644
--- a/pkg/gui/gui-util.go
+++ b/pkg/gui/gui-util.go
@@ -78,7 +78,6 @@ func (gui *Gui) previousViewOfGroup(g *gocui.Gui, v *gocui.View, group []viewFea
}
// siwtch the app mode
-// TODO: switching can be made with conventional iteration
func (gui *Gui) switchMode(g *gocui.Gui, v *gocui.View) error {
for i, mode := range modes {
if mode == gui.State.Mode {
@@ -94,6 +93,27 @@ func (gui *Gui) switchMode(g *gocui.Gui, v *gocui.View) error {
return nil
}
+// siwtch the app's mode to fetch
+func (gui *Gui) switchToFetchMode(g *gocui.Gui, v *gocui.View) error {
+ gui.State.Mode = fetchMode
+ gui.updateKeyBindingsView(g, mainViewFeature.Name)
+ return nil
+}
+
+// siwtch the app's mode to pull
+func (gui *Gui) switchToPullMode(g *gocui.Gui, v *gocui.View) error {
+ gui.State.Mode = pullMode
+ gui.updateKeyBindingsView(g, mainViewFeature.Name)
+ return nil
+}
+
+// siwtch the app's mode to merge
+func (gui *Gui) switchToMergeMode(g *gocui.Gui, v *gocui.View) error {
+ gui.State.Mode = mergeMode
+ gui.updateKeyBindingsView(g, mainViewFeature.Name)
+ return nil
+}
+
// bring the view on the top by its name
func (gui *Gui) setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) {
if _, err := g.SetCurrentView(name); err != nil {
@@ -193,3 +213,17 @@ func (gui *Gui) fastCursorUp(g *gocui.Gui, v *gocui.View) error {
}
return nil
}
+
+// closeViewCleanup both updates the keybidings view and focuses to returning view
+func (gui *Gui) closeViewCleanup(returningViewName string) (err error) {
+ if _, err = gui.g.SetCurrentView(returningViewName); err != nil {
+ return err
+ }
+ err = gui.updateKeyBindingsView(gui.g, returningViewName)
+ return err
+}
+
+// focus to view same as closeViewCleanup but its just a wrapper for easy reading
+func (gui *Gui) focusToView(viewName string) (err error) {
+ return gui.closeViewCleanup(viewName)
+}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 290cb88..1fb94ad 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -4,7 +4,6 @@ import (
"fmt"
"github.com/isacikgoz/gitbatch/pkg/git"
- "github.com/isacikgoz/gitbatch/pkg/queue"
"github.com/jroimartin/gocui"
log "github.com/sirupsen/logrus"
)
@@ -23,7 +22,7 @@ type guiState struct {
Repositories []*git.RepoEntity
Directories []string
Mode mode
- Queue *queue.JobQueue
+ Queue *git.JobQueue
}
// this struct encapsulates the name and title of a view. the name of a view is
@@ -78,7 +77,7 @@ func NewGui(mode string, directoies []string) (*Gui, error) {
initialState := guiState{
Directories: directoies,
Mode: fetchMode,
- Queue: queue.CreateJobQueue(),
+ Queue: git.CreateJobQueue(),
}
gui := &Gui{
State: initialState,
@@ -129,6 +128,10 @@ func (gui *Gui) Run() error {
gui.g = g
g.Highlight = true
g.SelFgColor = gocui.ColorGreen
+
+ // If InputEsc is true, when ESC sequence is in the buffer and it doesn't
+ // match any known sequence, ESC means KeyEsc.
+ g.InputEsc = true
g.SetManagerFunc(gui.layout)
if err := gui.generateKeybindings(); err != nil {
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index a9cfe63..936c7c6 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -33,93 +33,59 @@ func (gui *Gui) generateKeybindings() error {
Vital: true,
}, {
View: view.Name,
- Key: gocui.KeyTab,
- Modifier: gocui.ModNone,
- Handler: gui.switchMode,
- Display: "tab",
- Description: "Switch mode",
- Vital: true,
- }, {
- View: view.Name,
- Key: gocui.KeyArrowLeft,
+ Key: 'f',
Modifier: gocui.ModNone,
- Handler: gui.previousMainView,
- Display: "←",
- Description: "Previous Panel",
+ Handler: gui.switchToFetchMode,
+ Display: "f",
+ Description: "Fetch mode",
Vital: false,
}, {
View: view.Name,
- Key: gocui.KeyArrowRight,
+ Key: 'p',
Modifier: gocui.ModNone,
- Handler: gui.nextMainView,
- Display: "→",
- Description: "Next Panel",
+ Handler: gui.switchToPullMode,
+ Display: "p",
+ Description: "Pull mode",
Vital: false,
}, {
View: view.Name,
- Key: 'l',
+ Key: 'm',
Modifier: gocui.ModNone,
- Handler: gui.nextMainView,
- Display: "l",
- Description: "Previous Panel",
+ Handler: gui.switchToMergeMode,
+ Display: "m",
+ Description: "Merge mode",
Vital: false,
}, {
View: view.Name,
- Key: 'h',
+ Key: gocui.KeyTab,
Modifier: gocui.ModNone,
- Handler: gui.previousMainView,
- Display: "h",
+ Handler: gui.nextMainView,
+ Display: "tab",
Description: "Next Panel",
Vital: false,
},
}
- for _, binding := range mainKeybindings {
- gui.KeyBindings = append(gui.KeyBindings, binding)
- }
+ gui.KeyBindings = append(gui.KeyBindings, mainKeybindings...)
}
// Statusviews common keybindings
for _, view := range statusViews {
statusKeybindings := []*KeyBinding{
{
View: view.Name,
- Key: 'c',
+ Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.closeStatusView,
- Display: "c",
+ Display: "esc",
Description: "Close/Cancel",
Vital: true,
}, {
View: view.Name,
- Key: gocui.KeyArrowLeft,
- Modifier: gocui.ModNone,
- Handler: gui.previousStatusView,
- Display: "←",
- Description: "Previous Panel",
- Vital: false,
- }, {
- View: view.Name,
- Key: gocui.KeyArrowRight,
- Modifier: gocui.ModNone,
- Handler: gui.nextStatusView,
- Display: "→",
- Description: "Next Panel",
- Vital: false,
- }, {
- View: view.Name,
- Key: 'l',
+ Key: gocui.KeyTab,
Modifier: gocui.ModNone,
Handler: gui.nextStatusView,
- Display: "l",
- Description: "Previous Panel",
- Vital: false,
- }, {
- View: view.Name,
- Key: 'h',
- Modifier: gocui.ModNone,
- Handler: gui.previousStatusView,
- Display: "h",
+ Display: "tab",
Description: "Next Panel",
- Vital: false,
+ Vital: true,
}, {
View: view.Name,
Key: gocui.KeyArrowUp,
@@ -160,11 +126,77 @@ func (gui *Gui) generateKeybindings() error {
Display: "t",
Description: "Save to Stash",
Vital: true,
+ }, {
+ View: view.Name,
+ Key: 'm',
+ Modifier: gocui.ModNone,
+ Handler: gui.openCommitMessageView,
+ Display: "m",
+ Description: "Commit Changes",
+ Vital: true,
},
}
- for _, binding := range statusKeybindings {
- gui.KeyBindings = append(gui.KeyBindings, binding)
+ gui.KeyBindings = append(gui.KeyBindings, statusKeybindings...)
+ }
+ for _, view := range authViews {
+ authKeybindings := []*KeyBinding{
+ {
+ View: view.Name,
+ Key: gocui.KeyEsc,
+ Modifier: gocui.ModNone,
+ Handler: gui.closeAuthenticationView,
+ Display: "esc",
+ Description: "close/cancel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyTab,
+ Modifier: gocui.ModNone,
+ Handler: gui.nextAuthView,
+ Display: "tab",
+ Description: "Next Panel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyEnter,
+ Modifier: gocui.ModNone,
+ Handler: gui.submitAuthenticationView,
+ Display: "enter",
+ Description: "Submit",
+ Vital: true,
+ },
}
+ gui.KeyBindings = append(gui.KeyBindings, authKeybindings...)
+ }
+ for _, view := range commitViews {
+ commitKeybindings := []*KeyBinding{
+ {
+ View: view.Name,
+ Key: gocui.KeyEsc,
+ Modifier: gocui.ModNone,
+ Handler: gui.closeCommitMessageView,
+ Display: "esc",
+ Description: "close/cancel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyTab,
+ Modifier: gocui.ModNone,
+ Handler: gui.nextCommitView,
+ Display: "tab",
+ Description: "Next Panel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyEnter,
+ Modifier: gocui.ModNone,
+ Handler: gui.submitCommitMessageView,
+ Display: "enter",
+ Description: "Submit",
+ Vital: true,
+ },
+ }
+ gui.KeyBindings = append(gui.KeyBindings, commitKeybindings...)
}
individualKeybindings := []*KeyBinding{
// stash view
@@ -276,7 +308,7 @@ func (gui *Gui) generateKeybindings() error {
Modifier: gocui.ModNone,
Handler: gui.markRepository,
Display: "space",
- Description: "Add to queue",
+ Description: "Select",
Vital: true,
}, {
View: mainViewFeature.Name,
@@ -284,7 +316,7 @@ func (gui *Gui) generateKeybindings() error {
Modifier: gocui.ModNone,
Handler: gui.startQueue,
Display: "enter",
- Description: "Start queue",
+ Description: "Start",
Vital: true,
}, {
View: mainViewFeature.Name,
@@ -292,7 +324,7 @@ func (gui *Gui) generateKeybindings() error {
Modifier: gocui.ModNone,
Handler: gui.markAllRepositories,
Display: "ctrl + space",
- Description: "Add all to queue",
+ Description: "Select All",
Vital: false,
}, {
View: mainViewFeature.Name,
@@ -300,7 +332,7 @@ func (gui *Gui) generateKeybindings() error {
Modifier: gocui.ModNone,
Handler: gui.unmarkAllRepositories,
Display: "backspace",
- Description: "Remove all from queue",
+ Description: "Deselect All",
Vital: false,
}, {
View: mainViewFeature.Name,
@@ -320,10 +352,10 @@ func (gui *Gui) generateKeybindings() error {
Vital: false,
}, {
View: mainViewFeature.Name,
- Key: 'm',
+ Key: 'r',
Modifier: gocui.ModNone,
Handler: gui.sortByMod,
- Display: "m",
+ Display: "r",
Description: "Sort repositories by Modification date",
Vital: false,
}, {
@@ -498,10 +530,10 @@ func (gui *Gui) generateKeybindings() error {
// Diff View Controls
{
View: diffViewFeature.Name,
- Key: 'c',
+ Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.closeCommitDiffView,
- Display: "c",
+ Display: "esc",
Description: "close/cancel",
Vital: true,
}, {
@@ -540,10 +572,10 @@ func (gui *Gui) generateKeybindings() error {
// Application Controls
{
View: cheatSheetViewFeature.Name,
- Key: 'c',
+ Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.closeCheatSheetView,
- Display: "c",
+ Display: "esc",
Description: "close/cancel",
Vital: true,
}, {
@@ -582,10 +614,10 @@ func (gui *Gui) generateKeybindings() error {
// Error View
{
View: errorViewFeature.Name,
- Key: 'c',
+ Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.closeErrorView,
- Display: "c",
+ Display: "esc",
Description: "close/cancel",
Vital: true,
}, {
@@ -622,9 +654,7 @@ func (gui *Gui) generateKeybindings() error {
Vital: false,
},
}
- for _, binding := range individualKeybindings {
- gui.KeyBindings = append(gui.KeyBindings, binding)
- }
+ gui.KeyBindings = append(gui.KeyBindings, individualKeybindings...)
return nil
}
diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go
index b421775..389d66d 100644
--- a/pkg/gui/mainview.go
+++ b/pkg/gui/mainview.go
@@ -5,7 +5,6 @@ import (
"sort"
"github.com/isacikgoz/gitbatch/pkg/git"
- "github.com/isacikgoz/gitbatch/pkg/queue"
"github.com/jroimartin/gocui"
)
@@ -112,18 +111,18 @@ func (gui *Gui) getSelectedRepository() *git.RepoEntity {
// adds given entity to job queue
func (gui *Gui) addToQueue(entity *git.RepoEntity) error {
- var jt queue.JobType
+ var jt git.JobType
switch mode := gui.State.Mode.ModeID; mode {
case FetchMode:
- jt = queue.Fetch
+ jt = git.FetchJob
case PullMode:
- jt = queue.Pull
+ jt = git.PullJob
case MergeMode:
- jt = queue.Merge
+ jt = git.MergeJob
default:
return nil
}
- err := gui.State.Queue.AddJob(&queue.Job{
+ err := gui.State.Queue.AddJob(&git.Job{
JobType: jt,
Entity: entity,
})
@@ -148,7 +147,8 @@ func (gui *Gui) removeFromQueue(entity *git.RepoEntity) error {
// function does take its current state into account before adding it
func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error {
r := gui.getSelectedRepository()
- if r.State == git.Available || r.State == git.Success {
+ // maybe, failed entities may be added to queue again
+ if r.State == git.Available || r.State == git.Success || r.State == git.Paused {
if err := gui.addToQueue(r); err != nil {
return err
}
diff --git a/pkg/gui/queuehandler.go b/pkg/gui/queuehandler.go
index 3be1472..a814c47 100644
--- a/pkg/gui/queuehandler.go
+++ b/pkg/gui/queuehandler.go
@@ -1,7 +1,9 @@
package gui
import (
+ "github.com/isacikgoz/gitbatch/pkg/git"
"github.com/jroimartin/gocui"
+ log "github.com/sirupsen/logrus"
)
// this function starts the queue and updates the gui with the result of an
@@ -10,14 +12,27 @@ func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error {
go func(gui_go *Gui, g_go *gocui.Gui) {
for {
job, finished, err := gui_go.State.Queue.StartNext()
+ // for each job execution we better refresh the main
+ // it would be nice if we can also refresh side views
g_go.Update(func(gu *gocui.Gui) error {
gui_go.refreshMain(gu)
return nil
})
- defer gui.updateKeyBindingsView(g, mainViewFeature.Name)
+
if err != nil {
+ if err == git.ErrAuthenticationRequired {
+ // pause the job, so it will be indicated to being blocking
+ job.Entity.State = git.Paused
+ err := gui_go.openAuthenticationView(g, gui_go.State.Queue, job, v.Name())
+ if err != nil {
+ log.Warn(err.Error())
+ return
+ }
+ }
return
+ // with not returning here, we simply ignore and continue
}
+ // if queue is finished simply return from this goroutine
if finished {
return
}
diff --git a/pkg/gui/stagedview.go b/pkg/gui/stagedview.go
index e27d35d..4a6beb9 100644
--- a/pkg/gui/stagedview.go
+++ b/pkg/gui/stagedview.go
@@ -22,38 +22,36 @@ func (gui *Gui) openStageView(g *gocui.Gui) error {
if err := refreshStagedView(g, entity); err != nil {
return err
}
- gui.updateKeyBindingsView(g, stageViewFeature.Name)
- if _, err := g.SetCurrentView(stageViewFeature.Name); err != nil {
- return err
- }
- return nil
+ return gui.focusToView(stageViewFeature.Name)
}
func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error {
entity := gui.getSelectedRepository()
- files, _, err := generateFileLists(entity)
- if err != nil {
- return err
- }
- if len(files) <= 0 {
- return nil
- }
+
_, cy := v.Cursor()
_, oy := v.Origin()
- if err := files[cy+oy].Reset(git.ResetOptions{}); err != nil {
+ if len(stagedFiles) <= 0 || len(stagedFiles) <= cy+oy {
+ return nil
+ }
+ if err := git.Reset(entity, stagedFiles[cy+oy], git.ResetOptions{}); err != nil {
return err
}
- err = refreshAllStatusView(g, entity)
- return err
+ return refreshAllStatusView(g, entity, true)
}
func (gui *Gui) resetAllChanges(g *gocui.Gui, v *gocui.View) error {
entity := gui.getSelectedRepository()
- if err := entity.ResetAll(git.ResetOptions{}); err != nil {
+ ref, err := entity.Repository.Head()
+ if err != nil {
+ return err
+ }
+ if err := git.ResetAll(entity, git.ResetOptions{
+ Hash: ref.Hash().String(),
+ Rtype: git.ResetMixed,
+ }); err != nil {
return err
}
- err := refreshAllStatusView(g, entity)
- return err
+ return refreshAllStatusView(g, entity, true)
}
// refresh the main view and re-render the repository representations
@@ -65,11 +63,7 @@ func refreshStagedView(g *gocui.Gui, entity *git.RepoEntity) error {
stageView.Clear()
_, cy := stageView.Cursor()
_, oy := stageView.Origin()
- files, _, err := generateFileLists(entity)
- if err != nil {
- return err
- }
- for i, file := range files {
+ for i, file := range stagedFiles {
var prefix string
if i == cy+oy {
prefix = prefix + selectionIndicator
diff --git a/pkg/gui/stashview.go b/pkg/gui/stashview.go
index 54a05db..24b4f3c 100644
--- a/pkg/gui/stashview.go
+++ b/pkg/gui/stashview.go
@@ -34,7 +34,7 @@ func (gui *Gui) stashChanges(g *gocui.Gui, v *gocui.View) error {
return err
}
}
- err = refreshAllStatusView(g, entity)
+ err = refreshAllStatusView(g, entity, true)
return err
}
@@ -58,7 +58,7 @@ func (gui *Gui) popStash(g *gocui.Gui, v *gocui.View) error {
if err := entity.Refresh(); err != nil {
return err
}
- err = refreshAllStatusView(g, entity)
+ err = refreshAllStatusView(g, entity, true)
return err
}
diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go
index d6f0be1..82df459 100644
--- a/pkg/gui/statusview.go
+++ b/pkg/gui/statusview.go
@@ -9,16 +9,22 @@ import (
var (
statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "}
- // statusViewFeature = viewFeature{Name: "status", Title: " Status "}
- stageViewFeature = viewFeature{Name: "staged", Title: " Staged "}
- unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "}
- stashViewFeature = viewFeature{Name: "stash", Title: " Stash "}
+ stageViewFeature = viewFeature{Name: "staged", Title: " Staged "}
+ unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "}
+ stashViewFeature = viewFeature{Name: "stash", Title: " Stash "}
statusViews = []viewFeature{stageViewFeature, unstageViewFeature, stashViewFeature}
+
+ commitMesageReturnView string
+ stagedFiles []*git.File
+ unstagedFiles []*git.File
)
// open the status layout
func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error {
+ if err := reloadFiles(gui.getSelectedRepository()); err != nil {
+ return err
+ }
gui.openStatusHeaderView(g)
gui.openStageView(g)
gui.openUnStagedView(g)
@@ -26,6 +32,11 @@ func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error {
return nil
}
+func reloadFiles(entity *git.RepoEntity) (err error) {
+ stagedFiles, unstagedFiles, err = populateFileLists(entity)
+ return err
+}
+
// focus to next view
func (gui *Gui) nextStatusView(g *gocui.Gui, v *gocui.View) error {
err := gui.nextViewOfGroup(g, v, statusViews)
@@ -57,7 +68,7 @@ func (gui *Gui) statusCursorDown(g *gocui.Gui, v *gocui.View) error {
}
}
entity := gui.getSelectedRepository()
- if err := refreshStatusView(v.Name(), g, entity); err != nil {
+ if err := refreshStatusView(v.Name(), g, entity, false); err != nil {
return err
}
}
@@ -75,7 +86,7 @@ func (gui *Gui) statusCursorUp(g *gocui.Gui, v *gocui.View) error {
}
}
entity := gui.getSelectedRepository()
- if err := refreshStatusView(v.Name(), g, entity); err != nil {
+ if err := refreshStatusView(v.Name(), g, entity, false); err != nil {
return err
}
}
@@ -100,21 +111,16 @@ func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error {
// close the opened stat views
func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error {
- if err := g.DeleteView(stashViewFeature.Name); err != nil {
- return err
- }
- if err := g.DeleteView(unstageViewFeature.Name); err != nil {
- return err
- }
- if err := g.DeleteView(stageViewFeature.Name); err != nil {
- return err
+ for _, view := range statusViews {
+ if err := g.DeleteView(view.Name); err != nil {
+ return err
+ }
}
if err := g.DeleteView(statusHeaderViewFeature.Name); err != nil {
return err
}
- if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil {
- return err
- }
+ stagedFiles = make([]*git.File, 0)
+ unstagedFiles = make([]*git.File, 0)
entity := gui.getSelectedRepository()
if err := gui.refreshMain(g); err != nil {
return err
@@ -122,12 +128,11 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error {
if err := gui.refreshViews(g, entity); err != nil {
return err
}
- gui.updateKeyBindingsView(g, mainViewFeature.Name)
- return nil
+ return gui.closeViewCleanup(mainViewFeature.Name)
}
-func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) {
- files, err := entity.LoadFiles()
+func populateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) {
+ files, err := git.Status(entity)
if err != nil {
return nil, nil, err
}
@@ -142,14 +147,17 @@ func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, er
return staged, unstaged, err
}
-func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity) error {
+func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity, reload bool) error {
+ if reload {
+ reloadFiles(entity)
+ }
switch viewName {
case stageViewFeature.Name:
if err := refreshStagedView(g, entity); err != nil {
return err
}
case unstageViewFeature.Name:
- if err := refreshUnstagedView(g, entity); err != nil {
+ if err := refreshUnstagedView(g); err != nil {
return err
}
case stashViewFeature.Name:
@@ -160,9 +168,9 @@ func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity) er
return nil
}
-func refreshAllStatusView(g *gocui.Gui, entity *git.RepoEntity) error {
+func refreshAllStatusView(g *gocui.Gui, entity *git.RepoEntity, reload bool) error {
for _, v := range statusViews {
- if err := refreshStatusView(v.Name, g, entity); err != nil {
+ if err := refreshStatusView(v.Name, g, entity, reload); err != nil {
return err
}
}
diff --git a/pkg/gui/textstyle.go b/pkg/gui/textstyle.go
index 33f172d..7c36263 100644
--- a/pkg/gui/textstyle.go
+++ b/pkg/gui/textstyle.go
@@ -6,7 +6,6 @@ import (
"github.com/fatih/color"
"github.com/isacikgoz/gitbatch/pkg/git"
- "github.com/isacikgoz/gitbatch/pkg/queue"
)
var (
@@ -36,12 +35,15 @@ var (
queuedSymbol = "•"
workingSymbol = "•"
successSymbol = "✔"
+ pauseSymbol = "॥"
failSymbol = "✗"
fetchSymbol = "↓"
pullSymbol = "↓↳"
mergeSymbol = "↳"
+ keySymbol = ws + yellow.Sprint("🔑") + ws
+
modeSeperator = ""
keyBindingSeperator = "░"
@@ -87,11 +89,11 @@ func (gui *Gui) displayString(entity *git.RepoEntity) string {
if entity.State == git.Queued {
if inQueue, ty := gui.State.Queue.IsInTheQueue(entity); inQueue {
switch mode := ty; mode {
- case queue.Fetch:
+ case git.FetchJob:
suffix = blue.Sprint(queuedSymbol)
- case queue.Pull:
+ case git.PullJob:
suffix = magenta.Sprint(queuedSymbol)
- case queue.Merge:
+ case git.MergeJob:
suffix = cyan.Sprint(queuedSymbol)
default:
suffix = green.Sprint(queuedSymbol)
@@ -103,6 +105,8 @@ func (gui *Gui) displayString(entity *git.RepoEntity) string {
return prefix + repoName + ws + green.Sprint(workingSymbol)
} else if entity.State == git.Success {
return prefix + repoName + ws + green.Sprint(successSymbol)
+ } else if entity.State == git.Paused {
+ return prefix + repoName + ws + yellow.Sprint(pauseSymbol)
} else if entity.State == git.Fail {
return prefix + repoName + ws + red.Sprint(failSymbol)
} else {
diff --git a/pkg/gui/unstagedview.go b/pkg/gui/unstagedview.go
index ef4866f..1a33c12 100644
--- a/pkg/gui/unstagedview.go
+++ b/pkg/gui/unstagedview.go
@@ -18,40 +18,36 @@ func (gui *Gui) openUnStagedView(g *gocui.Gui) error {
}
v.Title = unstageViewFeature.Title
}
- entity := gui.getSelectedRepository()
- err = refreshUnstagedView(g, entity)
+ err = refreshUnstagedView(g)
return err
}
func (gui *Gui) addChanges(g *gocui.Gui, v *gocui.View) error {
entity := gui.getSelectedRepository()
- _, files, err := generateFileLists(entity)
- if err != nil {
- return err
- }
- if len(files) <= 0 {
- return nil
- }
+
_, cy := v.Cursor()
_, oy := v.Origin()
- if err := files[cy+oy].Add(git.AddOptions{}); err != nil {
+ if len(unstagedFiles) <= 0 || len(unstagedFiles) < cy+oy {
+ return nil
+ }
+ if err := git.Add(entity, unstagedFiles[cy+oy], git.AddOptions{}); err != nil {
return err
}
- err = refreshAllStatusView(g, entity)
+ err := refreshAllStatusView(g, entity, true)
return err
}
func (gui *Gui) addAllChanges(g *gocui.Gui, v *gocui.View) error {
entity := gui.getSelectedRepository()
- if err := entity.AddAll(git.AddOptions{}); err != nil {
+ if err := git.AddAll(entity, git.AddOptions{}); err != nil {
return err
}
- err := refreshAllStatusView(g, entity)
+ err := refreshAllStatusView(g, entity, true)
return err
}
// refresh the main view and re-render the repository representations
-func refreshUnstagedView(g *gocui.Gui, entity *git.RepoEntity) error {
+func refreshUnstagedView(g *gocui.Gui) error {
stageView, err := g.View(unstageViewFeature.Name)
if err != nil {
return err
@@ -59,11 +55,7 @@ func refreshUnstagedView(g *gocui.Gui, entity *git.RepoEntity) error {
stageView.Clear()
_, cy := stageView.Cursor()
_, oy := stageView.Origin()
- _, files, err := generateFileLists(entity)
- if err != nil {
- return err
- }
- for i, file := range files {
+ for i, file := range unstagedFiles {
var prefix string
if i == cy+oy {
prefix = prefix + selectionIndicator
diff --git a/pkg/queue/job.go b/pkg/queue/job.go
deleted file mode 100644
index 1fe5ba1..0000000
--- a/pkg/queue/job.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package queue
-
-import (
- "time"
-
- "github.com/isacikgoz/gitbatch/pkg/git"
-)
-
-// Job relates the type of the operation and the entity
-type Job struct {
- JobType JobType
- Entity *git.RepoEntity
-}
-
-// JobType is the a git operation supported
-type JobType string
-
-const (
- // Fetch is wrapper of git fetch command
- Fetch JobType = "fetch"
- // Pull is wrapper of git pull command
- Pull JobType = "pull"
- // Merge is wrapper of git merge command
- Merge JobType = "merge"
-)
-
-// starts the job
-func (job *Job) start() error {
- job.Entity.State = git.Working
- // added for testing, TODO: remove
- time.Sleep(time.Second)
- // TODO: Handle errors?
- switch mode := job.JobType; mode {
- case Fetch:
- if err := git.Fetch(job.Entity, git.FetchOptions{
- RemoteName: job.Entity.Remote.Name,
- }); err != nil {
- job.Entity.State = git.Fail
- return nil
- }
- case Pull:
- if err := git.Fetch(job.Entity, git.FetchOptions{
- RemoteName: job.Entity.Remote.Name,
- }); err != nil {
- job.Entity.State = git.Fail
- return nil
- }
- if err := git.Merge(job.Entity, git.MergeOptions{
- BranchName: job.Entity.Remote.Branch.Name,
- }); err != nil {
- job.Entity.State = git.Fail
- return nil
- }
- case Merge:
- if err := git.Merge(job.Entity, git.MergeOptions{
- BranchName: job.Entity.Remote.Branch.Name,
- }); err != nil {
- job.Entity.State = git.Fail
- return nil
- }
- default:
- job.Entity.State = git.Available
- return nil
- }
- job.Entity.State = git.Success
- return nil
-}