summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIbrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>2019-01-04 18:22:50 +0300
committerGitHub <noreply@github.com>2019-01-04 18:22:50 +0300
commit8a7a258de22432c5927ccfd2c9d5c41fea275b19 (patch)
tree1bd6738d5016aecbc944f04f0391197541cbd7c3
parentMinor checks and changes throughout files (#53) (diff)
parentcleanup before version increase (diff)
downloadgitbatch-8a7a258de22432c5927ccfd2c9d5c41fea275b19.tar.gz
Merge pull request #54 from isacikgoz/develop
Develop (0.3.0)
-rw-r--r--README.md9
-rw-r--r--app/app.go (renamed from pkg/app/app.go)12
-rw-r--r--app/config.go (renamed from pkg/app/config.go)0
-rw-r--r--app/files.go (renamed from pkg/app/files.go)47
-rw-r--r--app/quick.go (renamed from pkg/app/quick.go)9
-rw-r--r--core/command/add.go (renamed from pkg/git/cmd-add.go)21
-rw-r--r--core/command/cmd.go (renamed from pkg/helpers/command.go)51
-rw-r--r--core/command/commit.go (renamed from pkg/git/cmd-commit.go)31
-rw-r--r--core/command/config.go (renamed from pkg/git/cmd-config.go)29
-rw-r--r--core/command/diff.go (renamed from pkg/git/cmd-diff.go)25
-rw-r--r--core/command/fetch.go (renamed from pkg/git/cmd-fetch.go)65
-rw-r--r--core/command/file.go (renamed from pkg/git/file.go)42
-rw-r--r--core/command/merge.go (renamed from pkg/git/cmd-merge.go)16
-rw-r--r--core/command/pull.go (renamed from pkg/git/cmd-pull.go)71
-rw-r--r--core/command/reset.go (renamed from pkg/git/cmd-reset.go)39
-rw-r--r--core/command/status.go (renamed from pkg/git/cmd-status.go)26
-rw-r--r--core/errors/errors.go63
-rw-r--r--core/git/authentication.go (renamed from pkg/git/authentication.go)12
-rw-r--r--core/git/branch.go209
-rw-r--r--core/git/commit.go (renamed from pkg/git/commit.go)88
-rw-r--r--core/git/random.go (renamed from pkg/helpers/utils.go)20
-rw-r--r--core/git/random_test.go (renamed from pkg/helpers/utils_test.go)2
-rw-r--r--core/git/remote.go (renamed from pkg/git/remote.go)34
-rw-r--r--core/git/remotebranch.go (renamed from pkg/git/remotebranch.go)34
-rw-r--r--core/git/repository.go (renamed from pkg/git/repository.go)162
-rw-r--r--core/git/sort.go (renamed from pkg/git/util-sort.go)26
-rw-r--r--core/git/stash.go (renamed from pkg/git/cmd-stash.go)73
-rw-r--r--core/job/job.go79
-rw-r--r--core/job/queue.go (renamed from pkg/git/job-queue.go)65
-rw-r--r--core/load/load.go100
-rw-r--r--gui/authenticationview.go (renamed from pkg/gui/authenticationview.go)39
-rw-r--r--gui/commitview.go (renamed from pkg/gui/commitview.go)18
-rw-r--r--gui/controlsview.go (renamed from pkg/gui/controlsview.go)0
-rw-r--r--gui/diffview.go (renamed from pkg/gui/diffview.go)26
-rw-r--r--gui/errorview.go (renamed from pkg/gui/errorview.go)0
-rw-r--r--gui/extensions.go (renamed from pkg/gui/util-common.go)11
-rw-r--r--gui/gui.go (renamed from pkg/gui/gui.go)96
-rw-r--r--gui/keybindings.go (renamed from pkg/gui/keybindings.go)60
-rw-r--r--gui/mainview.go (renamed from pkg/gui/mainview.go)188
-rw-r--r--gui/sideviews.go (renamed from pkg/gui/sideviews.go)115
-rw-r--r--gui/stagedview.go (renamed from pkg/gui/stagedview.go)18
-rw-r--r--gui/stashview.go (renamed from pkg/gui/stashview.go)26
-rw-r--r--gui/statusview.go (renamed from pkg/gui/statusview.go)58
-rw-r--r--gui/text-renderer.go (renamed from pkg/gui/util-textstyle.go)61
-rw-r--r--gui/unstagedview.go (renamed from pkg/gui/unstagedview.go)14
-rw-r--r--main.go28
-rw-r--r--pkg/git/branch.go157
-rw-r--r--pkg/git/cmd-rev-list.go42
-rw-r--r--pkg/git/cmd.go82
-rw-r--r--pkg/git/job.go71
-rw-r--r--pkg/git/util-errors.go24
-rw-r--r--pkg/git/util-load.go48
52 files changed, 1502 insertions, 1140 deletions
diff --git a/README.md b/README.md
index 12f4cb8..bb19d78 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
[![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 is being 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.*)
+I like to use polyrepos. I (*was*) 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. 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/QQPVDWVxUR3bvJhIZY3c4PTuG.svg)](https://asciinema.org/a/QQPVDWVxUR3bvJhIZY3c4PTuG)
+[![asciicast](https://asciinema.org/a/AiH2y2gwr8sLce40epnIQxRAH.svg)](https://asciinema.org/a/AiH2y2gwr8sLce40epnIQxRAH)
## Installation
To install with go, run the following command;
@@ -19,7 +19,7 @@ run the `gitbatch` command from the parent of your git repositories. For start-u
For more information see the [wiki pages](https://github.com/isacikgoz/gitbatch/wiki)
## Further goals
-- add testing
+- **add testing**
- add push
- full src-d/go-git integration (*having some performance issues in such cases*)
- fetch, config, add, reset, commit, status and diff commands are supported but not fully utilized, still using git sometimes
@@ -34,7 +34,6 @@ Please refer to [Known issues page](https://github.com/isacikgoz/gitbatch/wiki/K
- [logrus](https://github.com/sirupsen/logrus) for logging
- [viper](https://github.com/spf13/viper) for configuration management
- [color](https://github.com/fatih/color) for colored text
-- [lazygit](https://github.com/jesseduffield/lazygit) as app template and reference
+- [lazygit](https://github.com/jesseduffield/lazygit) for inspiration
- [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/app/app.go b/app/app.go
index 9c9e9a7..b8eebc8 100644
--- a/pkg/app/app.go
+++ b/app/app.go
@@ -3,7 +3,7 @@ package app
import (
"os"
- "github.com/isacikgoz/gitbatch/pkg/gui"
+ "github.com/isacikgoz/gitbatch/gui"
log "github.com/sirupsen/logrus"
)
@@ -45,18 +45,18 @@ func Setup(setupConfig *SetupConfig) (*App, error) {
x := appConfig.Mode == "fetch"
y := appConfig.Mode == "pull"
if x == y {
- log.Fatal("Unrecognized quick mode: " + appConfig.Mode)
+ log.Error("Unrecognized quick mode: " + appConfig.Mode)
+ os.Exit(1)
}
quick(directories, appConfig.Depth, appConfig.Mode)
- log.Fatal("Finished")
+ os.Exit(0)
}
// create a gui.Gui struct and set it as App's gui
app.Gui, err = gui.NewGui(appConfig.Mode, directories)
if err != nil {
- // the error types and handling is not considered yer
- log.Error(err)
- return app, err
+ // the error types and handling is not considered yet
+ return nil, err
}
// hopefull everything went smooth as butter
log.Trace("App configuration completed")
diff --git a/pkg/app/config.go b/app/config.go
index 57c2e6e..57c2e6e 100644
--- a/pkg/app/config.go
+++ b/app/config.go
diff --git a/pkg/app/files.go b/app/files.go
index 25e5074..1ec05cd 100644
--- a/pkg/app/files.go
+++ b/app/files.go
@@ -4,20 +4,20 @@ import (
"io/ioutil"
"os"
"path/filepath"
- "strings"
log "github.com/sirupsen/logrus"
)
// generateDirectories returns poosible git repositories to pipe into git pkg's
// load function
-func generateDirectories(directories []string, depth int) (gitDirectories []string) {
+func generateDirectories(dirs []string, depth int) []string {
+ gitDirs := make([]string, 0)
for i := 0; i <= depth; i++ {
- nonrepos, repos := walkRecursive(directories, gitDirectories)
- directories = nonrepos
- gitDirectories = repos
+ nonrepos, repos := walkRecursive(dirs, gitDirs)
+ dirs = nonrepos
+ gitDirs = repos
}
- return gitDirectories
+ return gitDirs
}
// returns given values, first search directories and second stands for possible
@@ -33,7 +33,7 @@ func walkRecursive(search, appendant []string) ([]string, []string) {
if err != nil {
log.WithFields(log.Fields{
"directory": search[i],
- }).Trace("Can't read directory")
+ }).WithError(err).Trace("Can't read directory")
continue
}
// since we started to search let's get rid of it and remove from search
@@ -49,14 +49,16 @@ func walkRecursive(search, appendant []string) ([]string, []string) {
// seperateDirectories is to find all the files in given path. This method
// does not check if the given file is a valid git repositories
-func seperateDirectories(directory string) (directories, gitDirectories []string, err error) {
+func seperateDirectories(directory string) ([]string, []string, error) {
+ dirs := make([]string, 0)
+ gitDirs := make([]string, 0)
files, err := ioutil.ReadDir(directory)
// can we read the directory?
if err != nil {
log.WithFields(log.Fields{
"directory": directory,
}).Trace("Can't read directory")
- return directories, gitDirectories, nil
+ return nil, nil, nil
}
for _, f := range files {
repo := directory + string(os.PathSeparator) + f.Name()
@@ -66,38 +68,25 @@ func seperateDirectories(directory string) (directories, gitDirectories []string
log.WithFields(log.Fields{
"file": file,
"directory": repo,
- }).Trace("Failed to open file in the directory")
+ }).WithError(err).Trace("Failed to open file in the directory")
+ file.Close()
continue
}
dir, err := filepath.Abs(file.Name())
if err != nil {
- return nil, nil, err
+ file.Close()
+ continue
}
// with this approach, we ignore submodule or sub repositoreis in a git repository
ff, err := os.Open(dir + string(os.PathSeparator) + ".git")
if err != nil {
- directories = append(directories, dir)
+ dirs = append(dirs, dir)
} else {
- gitDirectories = append(gitDirectories, dir)
+ gitDirs = append(gitDirs, dir)
}
ff.Close()
file.Close()
}
- return directories, gitDirectories, nil
-}
-
-// takes a fileInfo slice and returns it with the ones matches with the
-// pattern string
-func filterDirectories(files []os.FileInfo, pattern string) []os.FileInfo {
- var filteredRepos []os.FileInfo
- for _, f := range files {
- // it is just a simple filter
- if strings.Contains(f.Name(), pattern) && f.Name() != ".git" {
- filteredRepos = append(filteredRepos, f)
- } else {
- continue
- }
- }
- return filteredRepos
+ return dirs, gitDirs, nil
}
diff --git a/pkg/app/quick.go b/app/quick.go
index 488e1d6..005c033 100644
--- a/pkg/app/quick.go
+++ b/app/quick.go
@@ -5,7 +5,8 @@ import (
"sync"
"time"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/isacikgoz/gitbatch/core/git"
)
func quick(directories []string, depth int, mode string) {
@@ -35,12 +36,14 @@ func operate(directory, mode string) error {
}
switch mode {
case "fetch":
- return git.Fetch(r, git.FetchOptions{
+ return command.Fetch(r, command.FetchOptions{
RemoteName: "origin",
+ Progress: true,
})
case "pull":
- return git.Pull(r, git.PullOptions{
+ return command.Pull(r, command.PullOptions{
RemoteName: "origin",
+ Progress: true,
})
}
return nil
diff --git a/pkg/git/cmd-add.go b/core/command/add.go
index e7faff2..5ad8774 100644
--- a/pkg/git/cmd-add.go
+++ b/core/command/add.go
@@ -1,8 +1,9 @@
-package git
+package command
import (
"errors"
+ "github.com/isacikgoz/gitbatch/core/git"
log "github.com/sirupsen/logrus"
)
@@ -25,31 +26,31 @@ type AddOptions struct {
}
// Add is a wrapper function for "git add" command
-func Add(e *RepoEntity, file *File, option AddOptions) error {
+func Add(r *git.Repository, file *File, option AddOptions) error {
addCmdMode = addCmdModeNative
if option.Update || option.Force || option.DryRun {
addCmdMode = addCmdModeLegacy
}
switch addCmdMode {
case addCmdModeLegacy:
- err := addWithGit(e, file, option)
+ err := addWithGit(r, file, option)
return err
case addCmdModeNative:
- err := addWithGoGit(e, file)
+ err := addWithGoGit(r, file)
return err
}
return errors.New("Unhandled add operation")
}
// AddAll function is the wrapper of "git add ." command
-func AddAll(e *RepoEntity, option AddOptions) error {
+func AddAll(r *git.Repository, option AddOptions) error {
args := make([]string, 0)
args = append(args, addCommand)
if option.DryRun {
args = append(args, "--dry-run")
}
args = append(args, ".")
- out, err := GenericGitCommandWithOutput(e.AbsPath, args)
+ out, err := GenericGitCommandWithOutput(r.AbsPath, args)
if err != nil {
log.Warn("Error while add command")
return errors.New(out + "\n" + err.Error())
@@ -57,7 +58,7 @@ func AddAll(e *RepoEntity, option AddOptions) error {
return nil
}
-func addWithGit(e *RepoEntity, file *File, option AddOptions) error {
+func addWithGit(r *git.Repository, file *File, option AddOptions) error {
args := make([]string, 0)
args = append(args, addCommand)
args = append(args, file.Name)
@@ -70,7 +71,7 @@ func addWithGit(e *RepoEntity, file *File, option AddOptions) error {
if option.DryRun {
args = append(args, "--dry-run")
}
- out, err := GenericGitCommandWithOutput(e.AbsPath, args)
+ out, err := GenericGitCommandWithOutput(r.AbsPath, args)
if err != nil {
log.Warn("Error while add command")
return errors.New(out + "\n" + err.Error())
@@ -78,8 +79,8 @@ func addWithGit(e *RepoEntity, file *File, option AddOptions) error {
return nil
}
-func addWithGoGit(e *RepoEntity, file *File) error {
- w, err := e.Repository.Worktree()
+func addWithGoGit(r *git.Repository, file *File) error {
+ w, err := r.Repo.Worktree()
if err != nil {
return err
}
diff --git a/pkg/helpers/command.go b/core/command/cmd.go
index b861812..536495b 100644
--- a/pkg/helpers/command.go
+++ b/core/command/cmd.go
@@ -1,8 +1,9 @@
-package helpers
+package command
import (
"log"
"os/exec"
+ "strings"
"syscall"
)
@@ -14,7 +15,7 @@ func RunCommandWithOutput(dir string, command string, args []string) (string, er
if dir != "" {
cmd.Dir = dir
}
- output, err := cmd.Output()
+ output, err := cmd.CombinedOutput()
return string(output), err
}
@@ -49,3 +50,49 @@ func GetCommandStatus(dir string, command string, args []string) (int, error) {
}
return -1, err
}
+
+// TrimTrailingNewline removes the trailing new line form a string. this method
+// is used mostly on outputs of a command
+func TrimTrailingNewline(str string) string {
+ if strings.HasSuffix(str, "\n") {
+ return str[:len(str)-1]
+ }
+ return str
+}
+
+// GenericGitCommand runs any git command without expecting output
+func GenericGitCommand(repoPath string, args []string) error {
+ _, err := RunCommandWithOutput(repoPath, "git", args)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// GenericGitCommandWithOutput runs any git command with returning output
+func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) {
+ out, err := RunCommandWithOutput(repoPath, "git", args)
+ if err != nil {
+ return out, err
+ }
+ return TrimTrailingNewline(out), nil
+}
+
+// GenericGitCommandWithErrorOutput runs any git command with returning output
+func GenericGitCommandWithErrorOutput(repoPath string, args []string) (string, error) {
+ out, err := RunCommandWithOutput(repoPath, "git", args)
+ if err != nil {
+ return TrimTrailingNewline(out), err
+ }
+ return TrimTrailingNewline(out), nil
+}
+
+// GitShow is conventional git show command without any argument
+func GitShow(repoPath, hash string) string {
+ args := []string{"show", hash}
+ diff, err := RunCommandWithOutput(repoPath, "git", args)
+ if err != nil {
+ return "?"
+ }
+ return diff
+}
diff --git a/pkg/git/cmd-commit.go b/core/command/commit.go
index 23c1de1..bf81a25 100644
--- a/pkg/git/cmd-commit.go
+++ b/core/command/commit.go
@@ -1,11 +1,12 @@
-package git
+package command
import (
"errors"
"time"
+ "github.com/isacikgoz/gitbatch/core/git"
log "github.com/sirupsen/logrus"
- "gopkg.in/src-d/go-git.v4"
+ gogit "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
@@ -28,22 +29,22 @@ type CommitOptions struct {
}
// CommitCommand defines which commit command to use.
-func CommitCommand(e *RepoEntity, options CommitOptions) (err error) {
+func CommitCommand(r *git.Repository, options CommitOptions) (err error) {
// here we configure commit operation
// default mode is go-git (this may be configured)
commitCmdMode = commitCmdModeNative
switch commitCmdMode {
case commitCmdModeLegacy:
- return commitWithGit(e, options)
+ return commitWithGit(r, options)
case commitCmdModeNative:
- return commitWithGoGit(e, options)
+ return commitWithGoGit(r, options)
}
return errors.New("Unhandled commit operation")
}
// commitWithGit is simply a bare git commit -m <msg> command which is flexible
-func commitWithGit(e *RepoEntity, options CommitOptions) (err error) {
+func commitWithGit(r *git.Repository, options CommitOptions) (err error) {
args := make([]string, 0)
args = append(args, commitCommand)
args = append(args, "-m")
@@ -51,24 +52,24 @@ func commitWithGit(e *RepoEntity, options CommitOptions) (err error) {
if len(options.CommitMsg) > 0 {
args = append(args, options.CommitMsg)
}
- if err := GenericGitCommand(e.AbsPath, args); err != nil {
+ if err := GenericGitCommand(r.AbsPath, args); err != nil {
log.Warn("Error at git command (commit)")
- e.Refresh()
+ r.Refresh()
return err
}
// till this step everything should be ok
- return e.Refresh()
+ return r.Refresh()
}
// commitWithGoGit is the primary commit method
-func commitWithGoGit(e *RepoEntity, options CommitOptions) (err error) {
- config, err := e.Repository.Config()
+func commitWithGoGit(r *git.Repository, options CommitOptions) (err error) {
+ config, err := r.Repo.Config()
if err != nil {
return err
}
name := config.Raw.Section("user").Option("name")
email := config.Raw.Section("user").Option("email")
- opt := &git.CommitOptions{
+ opt := &gogit.CommitOptions{
Author: &object.Signature{
Name: name,
Email: email,
@@ -76,16 +77,16 @@ func commitWithGoGit(e *RepoEntity, options CommitOptions) (err error) {
},
}
- w, err := e.Repository.Worktree()
+ w, err := r.Repo.Worktree()
if err != nil {
return err
}
_, err = w.Commit(options.CommitMsg, opt)
if err != nil {
- e.Refresh()
+ r.Refresh()
return err
}
// till this step everything should be ok
- return e.Refresh()
+ return r.Refresh()
}
diff --git a/pkg/git/cmd-config.go b/core/command/config.go
index e4bacf0..35eff23 100644
--- a/pkg/git/cmd-config.go
+++ b/core/command/config.go
@@ -1,8 +1,9 @@
-package git
+package command
import (
"errors"
+ "github.com/isacikgoz/gitbatch/core/git"
log "github.com/sirupsen/logrus"
)
@@ -35,23 +36,23 @@ const (
ConfgiSiteGlobal ConfigSite = "global"
)
-// Config
-func Config(e *RepoEntity, options ConfigOptions) (value string, err error) {
+// Config adds or reads config of a repository
+func Config(r *git.Repository, 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:
- return configWithGit(e, options)
+ return configWithGit(r, options)
case configCmdModeNative:
- return configWithGoGit(e, options)
+ return configWithGoGit(r, options)
}
return value, errors.New("Unhandled config operation")
}
// configWithGit is simply a bare git commit -m <msg> command which is flexible
-func configWithGit(e *RepoEntity, options ConfigOptions) (value string, err error) {
+func configWithGit(r *git.Repository, options ConfigOptions) (value string, err error) {
args := make([]string, 0)
args = append(args, configCommand)
if len(string(options.Site)) > 0 {
@@ -60,7 +61,7 @@ func configWithGit(e *RepoEntity, options ConfigOptions) (value string, err erro
args = append(args, "--get")
args = append(args, options.Section+"."+options.Option)
// parse options to command line arguments
- out, err := GenericGitCommandWithOutput(e.AbsPath, args)
+ out, err := GenericGitCommandWithOutput(r.AbsPath, args)
if err != nil {
return out, err
}
@@ -69,9 +70,9 @@ func configWithGit(e *RepoEntity, options ConfigOptions) (value string, err erro
}
// commitWithGoGit is the primary commit method
-func configWithGoGit(e *RepoEntity, options ConfigOptions) (value string, err error) {
+func configWithGoGit(r *git.Repository, options ConfigOptions) (value string, err error) {
// TODO: add global search
- config, err := e.Repository.Config()
+ config, err := r.Repo.Config()
if err != nil {
return value, err
}
@@ -79,13 +80,13 @@ func configWithGoGit(e *RepoEntity, options ConfigOptions) (value string, err er
}
// AddConfig adds an entry on the ConfigOptions field.
-func AddConfig(e *RepoEntity, options ConfigOptions, value string) (err error) {
- return addConfigWithGit(e, options, value)
+func AddConfig(r *git.Repository, options ConfigOptions, value string) (err error) {
+ return addConfigWithGit(r, options, value)
}
// addConfigWithGit is simply a bare git config --add <option> command which is flexible
-func addConfigWithGit(e *RepoEntity, options ConfigOptions, value string) (err error) {
+func addConfigWithGit(r *git.Repository, options ConfigOptions, value string) (err error) {
args := make([]string, 0)
args = append(args, configCommand)
if len(string(options.Site)) > 0 {
@@ -96,10 +97,10 @@ func addConfigWithGit(e *RepoEntity, options ConfigOptions, value string) (err e
if len(value) > 0 {
args = append(args, value)
}
- if err := GenericGitCommand(e.AbsPath, args); err != nil {
+ if err := GenericGitCommand(r.AbsPath, args); err != nil {
log.Warn("Error at git command (config)")
return err
}
// till this step everything should be ok
- return e.Refresh()
+ return r.Refresh()
}
diff --git a/pkg/git/cmd-diff.go b/core/command/diff.go
index f9fcb3f..ef48336 100644
--- a/pkg/git/cmd-diff.go
+++ b/core/command/diff.go
@@ -1,9 +1,10 @@
-package git
+package command
import (
"errors"
- "gopkg.in/src-d/go-git.v4"
+ "github.com/isacikgoz/gitbatch/core/git"
+ gogit "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
)
@@ -18,37 +19,37 @@ var (
// Diff is a wrapper function for "git diff" command
// Diff function returns the diff to previous commit detail of the given has
// of a specific commit
-func Diff(e *RepoEntity, hash string) (diff string, err error) {
+func Diff(r *git.Repository, hash string) (diff string, err error) {
diffCmdMode = diffCmdModeNative
switch diffCmdMode {
case diffCmdModeLegacy:
- return diffWithGit(e, hash)
+ return diffWithGit(r, hash)
case diffCmdModeNative:
- return diffWithGoGit(e, hash)
+ return diffWithGoGit(r, hash)
}
return diff, errors.New("Unhandled diff operation")
}
-func diffWithGit(e *RepoEntity, hash string) (diff string, err error) {
+func diffWithGit(r *git.Repository, hash string) (diff string, err error) {
return diff, nil
}
-func diffWithGoGit(e *RepoEntity, hash string) (diff string, err error) {
+func diffWithGoGit(r *git.Repository, hash string) (diff string, err error) {
currentCommitIndex := 0
- for i, cs := range e.Commits {
+ for i, cs := range r.Commits {
if cs.Hash == hash {
currentCommitIndex = i
}
}
- if len(e.Commits)-currentCommitIndex <= 1 {
+ if len(r.Commits)-currentCommitIndex <= 1 {
return "there is no diff", nil
}
// maybe we dont need to log the repo again?
- commits, err := e.Repository.Log(&git.LogOptions{
- From: plumbing.NewHash(e.Commit.Hash),
- Order: git.LogOrderCommitterTime,
+ commits, err := r.Repo.Log(&gogit.LogOptions{
+ From: plumbing.NewHash(r.State.Commit.Hash),
+ Order: gogit.LogOrderCommitterTime,
})
if err != nil {
return "", err
diff --git a/pkg/git/cmd-fetch.go b/core/command/fetch.go
index 0830105..cc827bf 100644
--- a/pkg/git/cmd-fetch.go
+++ b/core/command/fetch.go
@@ -1,10 +1,13 @@
-package git
+package command
import (
+ "os"
"strings"
+ gerr "github.com/isacikgoz/gitbatch/core/errors"
+ "github.com/isacikgoz/gitbatch/core/git"
log "github.com/sirupsen/logrus"
- "gopkg.in/src-d/go-git.v4"
+ gogit "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"
@@ -25,12 +28,14 @@ type FetchOptions struct {
// Name of the remote to fetch from. Defaults to origin.
RemoteName string
// Credentials holds the user and pswd information
- Credentials Credentials
+ Credentials git.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
+ // Process logs the output to stdout
+ Progress bool
// Force allows the fetch to update a local branch even when the remote
// branch does not descend from it.
Force bool
@@ -39,7 +44,7 @@ type FetchOptions struct {
// Fetch branches refs from one or more other repositories, along with the
// objects necessary to complete their histories
-func Fetch(e *RepoEntity, options FetchOptions) (err error) {
+func Fetch(r *git.Repository, options FetchOptions) (err error) {
// here we configure fetch operation
// default mode is go-git (this may be configured)
fetchCmdMode = fetchCmdModeNative
@@ -50,18 +55,18 @@ func Fetch(e *RepoEntity, options FetchOptions) (err error) {
}
switch fetchCmdMode {
case fetchCmdModeLegacy:
- err = fetchWithGit(e, options)
+ err = fetchWithGit(r, options)
return err
case fetchCmdModeNative:
// this should be the refspec as default, let's give it a try
// TODO: Fix for quick mode, maybe better read config file
var refspec string
- if e.Branch == nil {
+ if r.State.Branch == nil {
refspec = "+refs/heads/*:refs/remotes/origin/*"
} else {
- refspec = "+" + "refs/heads/" + e.Branch.Name + ":" + "/refs/remotes/" + e.Remote.Branch.Name
+ refspec = "+" + "refs/heads/" + r.State.Branch.Name + ":" + "/refs/remotes/" + r.State.Remote.Branch.Name
}
- err = fetchWithGoGit(e, options, refspec)
+ err = fetchWithGoGit(r, options, refspec)
return err
}
return nil
@@ -70,7 +75,7 @@ func Fetch(e *RepoEntity, options FetchOptions) (err error) {
// 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(e *RepoEntity, options FetchOptions) (err error) {
+func fetchWithGit(r *git.Repository, options FetchOptions) (err error) {
args := make([]string, 0)
args = append(args, fetchCommand)
// parse options to command line arguments
@@ -86,13 +91,12 @@ func fetchWithGit(e *RepoEntity, options FetchOptions) (err error) {
if options.DryRun {
args = append(args, "--dry-run")
}
- if err := GenericGitCommand(e.AbsPath, args); err != nil {
- log.Warn("Error at git command (fetch)")
- return err
+ if out, err := GenericGitCommandWithOutput(r.AbsPath, args); err != nil {
+ return gerr.ParseGitError(out, err)
}
- e.SetState(Success)
+ r.SetWorkStatus(git.Success)
// till this step everything should be ok
- return e.Refresh()
+ return r.Refresh()
}
// fetchWithGoGit is the primary fetch method and refspec is the main feature.
@@ -101,52 +105,59 @@ func fetchWithGit(e *RepoEntity, options FetchOptions) (err error) {
// 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(e *RepoEntity, options FetchOptions, refspec string) (err error) {
- opt := &git.FetchOptions{
+func fetchWithGoGit(r *git.Repository, options FetchOptions, refspec string) (err error) {
+ opt := &gogit.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 := authProtocol(e.Remote)
+ protocol, err := git.AuthProtocol(r.State.Remote)
if err != nil {
return err
}
- if protocol == authProtocolHTTP || protocol == authProtocolHTTPS {
+ if protocol == git.AuthProtocolHTTP || protocol == git.AuthProtocolHTTPS {
opt.Auth = &http.BasicAuth{
Username: options.Credentials.User,
Password: options.Credentials.Password,
}
} else {
- return ErrInvalidAuthMethod
+ return gerr.ErrInvalidAuthMethod
}
}
+ if options.Progress {
+ opt.Progress = os.Stdout
+ }
- if err := e.Repository.Fetch(opt); err != nil {
- if err == git.NoErrAlreadyUpToDate {
+ if err := r.Repo.Fetch(opt); err != nil {
+ if err == gogit.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 := e.Remote.RefSpecs[0]
+ rp := r.State.Remote.RefSpecs[0]
if fetchTryCount < fetchMaxTry {
fetchTryCount++
- fetchWithGoGit(e, options, rp)
+ fetchWithGoGit(r, options, rp)
} else {
return err
}
+ // TODO: submit a PR for this kind of error, this type of catch is lame
+ } else if strings.Contains(err.Error(), "SSH_AUTH_SOCK") {
+ // The env variable SSH_AUTH_SOCK is not defined, maybe git can handle this
+ return fetchWithGit(r, options)
} else if err == transport.ErrAuthenticationRequired {
log.Warn(err.Error())
- return ErrAuthenticationRequired
+ return gerr.ErrAuthenticationRequired
} else {
log.Warn(err.Error())
- return err
+ return fetchWithGit(r, options)
}
}
- e.SetState(Success)
+ r.SetWorkStatus(git.Success)
// till this step everything should be ok
- return e.Refresh()
+ return r.Refresh()
}
diff --git a/pkg/git/file.go b/core/command/file.go
index 1e086bc..ebfbdcf 100644
--- a/pkg/git/file.go
+++ b/core/command/file.go
@@ -1,7 +1,8 @@
-package git
+package command
import (
"strings"
+ "unicode"
log "github.com/sirupsen/logrus"
)
@@ -50,3 +51,42 @@ func (f *File) Diff() (output string, err error) {
}
return output, err
}
+
+// 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/git/cmd-merge.go b/core/command/merge.go
index e89761d..0552403 100644
--- a/pkg/git/cmd-merge.go
+++ b/core/command/merge.go
@@ -1,7 +1,8 @@
-package git
+package command
import (
- log "github.com/sirupsen/logrus"
+ gerr "github.com/isacikgoz/gitbatch/core/errors"
+ "github.com/isacikgoz/gitbatch/core/git"
)
var mergeCommand = "merge"
@@ -18,7 +19,7 @@ type MergeOptions struct {
// Merge incorporates changes from the named commits or branches into the
// current branch
-func Merge(e *RepoEntity, options MergeOptions) error {
+func Merge(r *git.Repository, options MergeOptions) error {
args := make([]string, 0)
args = append(args, mergeCommand)
if len(options.BranchName) > 0 {
@@ -30,10 +31,9 @@ func Merge(e *RepoEntity, options MergeOptions) error {
if options.NoStat {
args = append(args, "-n")
}
- if err := GenericGitCommand(e.AbsPath, args); err != nil {
- log.Warn("Error while merging")
- return err
+ if out, err := GenericGitCommandWithOutput(r.AbsPath, args); err != nil {
+ return gerr.ParseGitError(out, err)
}
- e.SetState(Success)
- return e.Refresh()
+ r.SetWorkStatus(git.Success)
+ return r.Refresh()
}
diff --git a/pkg/git/cmd-pull.go b/core/command/pull.go
index f620925..5644417 100644
--- a/pkg/git/cmd-pull.go
+++ b/core/command/pull.go
@@ -1,11 +1,17 @@
-package git
+package command
import (
+ "os"
+ "strings"
+
+ gerr "github.com/isacikgoz/gitbatch/core/errors"
+ "github.com/isacikgoz/gitbatch/core/git"
log "github.com/sirupsen/logrus"
- "gopkg.in/src-d/go-git.v4"
+ gogit "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
)
var (
@@ -27,14 +33,16 @@ type PullOptions struct {
// Fetch only ReferenceName if true.
SingleBranch bool
// Credentials holds the user and pswd information
- Credentials Credentials
+ Credentials git.Credentials
+ // Process logs the output to stdout
+ Progress bool
// Force allows the pull to update a local branch even when the remote
// branch does not descend from it.
Force bool
}
// Pull ncorporates changes from a remote repository into the current branch.
-func Pull(e *RepoEntity, options PullOptions) (err error) {
+func Pull(r *git.Repository, options PullOptions) (err error) {
// here we configure pull operation
// default mode is go-git (this may be configured)
pullCmdMode = pullCmdModeNative
@@ -42,16 +50,16 @@ func Pull(e *RepoEntity, options PullOptions) (err error) {
switch pullCmdMode {
case pullCmdModeLegacy:
- err = pullWithGit(e, options)
+ err = pullWithGit(r, options)
return err
case pullCmdModeNative:
- err = pullWithGoGit(e, options)
+ err = pullWithGoGit(r, options)
return err
}
return nil
}
-func pullWithGit(e *RepoEntity, options PullOptions) (err error) {
+func pullWithGit(r *git.Repository, options PullOptions) (err error) {
args := make([]string, 0)
args = append(args, pullCommand)
// parse options to command line arguments
@@ -61,16 +69,15 @@ func pullWithGit(e *RepoEntity, options PullOptions) (err error) {
if options.Force {
args = append(args, "-f")
}
- if err := GenericGitCommand(e.AbsPath, args); err != nil {
- log.Warn("Error at git command (pull)")
- return err
+ if out, err := GenericGitCommandWithOutput(r.AbsPath, args); err != nil {
+ return gerr.ParseGitError(out, err)
}
- e.SetState(Success)
- return e.Refresh()
+ r.SetWorkStatus(git.Success)
+ return r.Refresh()
}
-func pullWithGoGit(e *RepoEntity, options PullOptions) (err error) {
- opt := &git.PullOptions{
+func pullWithGoGit(r *git.Repository, options PullOptions) (err error) {
+ opt := &gogit.PullOptions{
RemoteName: options.RemoteName,
SingleBranch: options.SingleBranch,
Force: options.Force,
@@ -81,36 +88,54 @@ func pullWithGoGit(e *RepoEntity, options PullOptions) (err error) {
}
// if any credential is given, let's add it to the git.PullOptions
if len(options.Credentials.User) > 0 {
- protocol, err := authProtocol(e.Remote)
+ protocol, err := git.AuthProtocol(r.State.Remote)
if err != nil {
return err
}
- if protocol == authProtocolHTTP || protocol == authProtocolHTTPS {
+ if protocol == git.AuthProtocolHTTP || protocol == git.AuthProtocolHTTPS {
opt.Auth = &http.BasicAuth{
Username: options.Credentials.User,
Password: options.Credentials.Password,
}
} else {
- return ErrInvalidAuthMethod
+ return gerr.ErrInvalidAuthMethod
}
}
- w, err := e.Repository.Worktree()
+ if options.Progress {
+ opt.Progress = os.Stdout
+ }
+ w, err := r.Repo.Worktree()
if err != nil {
return err
}
if err = w.Pull(opt); err != nil {
- if err == git.NoErrAlreadyUpToDate {
+ if err == gogit.NoErrAlreadyUpToDate {
+ // log.Error("error: " + err.Error())
// 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 err == memory.ErrRefHasChanged && pullTryCount < pullMaxTry {
+ pullTryCount++
+ log.Error("trying to fetch")
+ if err := Fetch(r, FetchOptions{
+ RemoteName: options.RemoteName,
+ }); err != nil {
+ return err
+ }
+ return Pull(r, options)
+ } else if strings.Contains(err.Error(), "SSH_AUTH_SOCK") {
+ // The env variable SSH_AUTH_SOCK is not defined, maybe git can handle this
+ return pullWithGit(r, options)
} else if err == transport.ErrAuthenticationRequired {
log.Warn(err.Error())
- return ErrAuthenticationRequired
+ return gerr.ErrAuthenticationRequired
} else {
log.Warn(err.Error())
- return err
+ return pullWithGit(r, options)
}
}
- e.SetState(Success)
- return e.Refresh()
+
+ r.SetWorkStatus(git.Success)
+ return r.Refresh()
}
diff --git a/pkg/git/cmd-reset.go b/core/command/reset.go
index bea436c..d97d82f 100644
--- a/pkg/git/cmd-reset.go
+++ b/core/command/reset.go
@@ -1,10 +1,11 @@
-package git
+package command
import (
"errors"
+ "github.com/isacikgoz/gitbatch/core/git"
log "github.com/sirupsen/logrus"
- "gopkg.in/src-d/go-git.v4"
+ gogit "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
)
@@ -52,12 +53,12 @@ const (
)
// Reset is the wrapper of "git reset" command
-func Reset(e *RepoEntity, file *File, option ResetOptions) error {
+func Reset(r *git.Repository, file *File, option ResetOptions) error {
resetCmdMode = resetCmdModeLegacy
switch resetCmdMode {
case resetCmdModeLegacy:
- err := resetWithGit(e, file, option)
+ err := resetWithGit(r, file, option)
return err
case resetCmdModeNative:
@@ -65,7 +66,7 @@ func Reset(e *RepoEntity, file *File, option ResetOptions) error {
return errors.New("Unhandled reset operation")
}
-func resetWithGit(e *RepoEntity, file *File, option ResetOptions) error {
+func resetWithGit(r *git.Repository, file *File, option ResetOptions) error {
args := make([]string, 0)
args = append(args, resetCommand)
@@ -74,7 +75,7 @@ func resetWithGit(e *RepoEntity, file *File, option ResetOptions) error {
if len(option.Rtype) > 0 {
args = append(args, "--"+string(option.Rtype))
}
- out, err := GenericGitCommandWithOutput(e.AbsPath, args)
+ out, err := GenericGitCommandWithOutput(r.AbsPath, args)
if err != nil {
log.Warn("Error while reset command")
return errors.New(out + "\n" + err.Error())
@@ -83,27 +84,27 @@ func resetWithGit(e *RepoEntity, file *File, option ResetOptions) error {
}
// ResetAll resets the changes in a repository, should be used wise
-func ResetAll(e *RepoEntity, option ResetOptions) error {
+func ResetAll(r *git.Repository, option ResetOptions) error {
resetCmdMode = addCmdModeNative
switch resetCmdMode {
case resetCmdModeLegacy:
- err := resetAllWithGit(e, option)
+ err := resetAllWithGit(r, option)
return err
case resetCmdModeNative:
- err := resetAllWithGoGit(e, option)
+ err := resetAllWithGoGit(r, option)
return err
}
return errors.New("Unhandled reset operation")
}
-func resetAllWithGit(e *RepoEntity, option ResetOptions) error {
+func resetAllWithGit(r *git.Repository, 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(e.AbsPath, args)
+ out, err := GenericGitCommandWithOutput(r.AbsPath, args)
if err != nil {
log.Warn("Error while add command")
return errors.New(out + "\n" + err.Error())
@@ -111,23 +112,23 @@ func resetAllWithGit(e *RepoEntity, option ResetOptions) error {
return nil
}
-func resetAllWithGoGit(e *RepoEntity, option ResetOptions) error {
- w, err := e.Repository.Worktree()
+func resetAllWithGoGit(r *git.Repository, option ResetOptions) error {
+ w, err := r.Repo.Worktree()
if err != nil {
return err
}
- var mode git.ResetMode
+ var mode gogit.ResetMode
switch option.Rtype {
case ResetHard:
- mode = git.HardReset
+ mode = gogit.HardReset
case ResetMixed:
- mode = git.MixedReset
+ mode = gogit.MixedReset
case ResetMerge:
- mode = git.MergeReset
+ mode = gogit.MergeReset
case ResetSoft:
- mode = git.SoftReset
+ mode = gogit.SoftReset
}
- err = w.Reset(&git.ResetOptions{
+ err = w.Reset(&gogit.ResetOptions{
Commit: plumbing.NewHash(option.Hash),
Mode: mode,
})
diff --git a/pkg/git/cmd-status.go b/core/command/status.go
index eec9b57..f5947d6 100644
--- a/pkg/git/cmd-status.go
+++ b/core/command/status.go
@@ -1,4 +1,4 @@
-package git
+package command
import (
"errors"
@@ -7,6 +7,7 @@ import (
"sort"
"strings"
+ "github.com/isacikgoz/gitbatch/core/git"
log "github.com/sirupsen/logrus"
)
@@ -18,12 +19,12 @@ var (
statusCmdModeNative = "go-git"
)
-func shortStatus(e *RepoEntity, option string) string {
+func shortStatus(r *git.Repository, option string) string {
args := make([]string, 0)
args = append(args, statusCommand)
args = append(args, option)
args = append(args, "--short")
- out, err := GenericGitCommandWithOutput(e.AbsPath, args)
+ out, err := GenericGitCommandWithOutput(r.AbsPath, args)
if err != nil {
log.Warn("Error while status command")
return "?"
@@ -31,23 +32,24 @@ func shortStatus(e *RepoEntity, option string) string {
return out
}
-func Status(e *RepoEntity) ([]*File, error) {
+// Status returns the dirty files
+func Status(r *git.Repository) ([]*File, error) {
statusCmdMode = statusCmdModeNative
switch statusCmdMode {
case statusCmdModeLegacy:
- return statusWithGit(e)
+ return statusWithGit(r)
case statusCmdModeNative:
- return statusWithGoGit(e)
+ return statusWithGoGit(r)
}
return nil, errors.New("Unhandled status operation")
}
// LoadFiles function simply commands a git status and collects output in a
// structured way
-func statusWithGit(e *RepoEntity) ([]*File, error) {
+func statusWithGit(r *git.Repository) ([]*File, error) {
files := make([]*File, 0)
- output := shortStatus(e, "--untracked-files=all")
+ output := shortStatus(r, "--untracked-files=all")
if len(output) == 0 {
return files, nil
}
@@ -60,7 +62,7 @@ func statusWithGit(e *RepoEntity) ([]*File, error) {
files = append(files, &File{
Name: path,
- AbsPath: e.AbsPath + string(os.PathSeparator) + path,
+ AbsPath: r.AbsPath + string(os.PathSeparator) + path,
X: FileStatus(x),
Y: FileStatus(y),
})
@@ -69,9 +71,9 @@ func statusWithGit(e *RepoEntity) ([]*File, error) {
return files, nil
}
-func statusWithGoGit(e *RepoEntity) ([]*File, error) {
+func statusWithGoGit(r *git.Repository) ([]*File, error) {
files := make([]*File, 0)
- w, err := e.Repository.Worktree()
+ w, err := r.Repo.Worktree()
if err != nil {
return files, err
}
@@ -82,7 +84,7 @@ func statusWithGoGit(e *RepoEntity) ([]*File, error) {
for k, v := range s {
files = append(files, &File{
Name: k,
- AbsPath: e.AbsPath + string(os.PathSeparator) + k,
+ AbsPath: r.AbsPath + string(os.PathSeparator) + k,
X: FileStatus(v.Staging),
Y: FileStatus(v.Worktree),
})
diff --git a/core/errors/errors.go b/core/errors/errors.go
new file mode 100644
index 0000000..f922617
--- /dev/null
+++ b/core/errors/errors.go
@@ -0,0 +1,63 @@
+package errors
+
+import (
+ "errors"
+ "strings"
+)
+
+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")
+ // ErrMergeAbortedTryCommit indicates that the repositort is not clean and
+ // some changes may conflict with the merge
+ ErrMergeAbortedTryCommit = errors.New("stash/commit changes. aborted")
+ // ErrRemoteBranchNotSpecified means that default remote branch is not set
+ // for the current branch. can be setted with "git config --local --add
+ // branch.<your branch name>.remote=<your remote name> "
+ ErrRemoteBranchNotSpecified = errors.New("upstream not set")
+ // ErrRemoteNotFound is thrown when the remote is not reachable. It may be
+ // caused by the deletion of the remote or coneectivty problems
+ ErrRemoteNotFound = errors.New("remote not found")
+ // ErrConflictAfterMerge is thrown when a conflict occurs at merging two
+ // references
+ ErrConflictAfterMerge = errors.New("conflict while merging")
+ // ErrUnmergedFiles possibly occurs after a conflict
+ ErrUnmergedFiles = errors.New("unmerged files detected")
+ // ErrReferenceBroken thrown when unable to resolve reference
+ ErrReferenceBroken = errors.New("unable to resolve reference")
+ // ErrUnclassified is unconsidered error type
+ ErrUnclassified = errors.New("unclassified error")
+)
+
+// ParseGitError takes git output as an input and tries to find some meaningful
+// errors can be used by the app
+func ParseGitError(out string, err error) error {
+ if strings.Contains(out, "error: Your local changes to the following files would be overwritten by merge") {
+ return ErrMergeAbortedTryCommit
+ } else if strings.Contains(out, "ERROR: Repository not found") {
+ return ErrRemoteNotFound
+ } else if strings.Contains(out, "for your current branch, you must specify a branch on the command line") {
+ return ErrRemoteBranchNotSpecified
+ } else if strings.Contains(out, "Automatic merge failed; fix conflicts and then commit the result") {
+ return ErrConflictAfterMerge
+ } else if strings.Contains(out, "error: Pulling is not possible because you have unmerged files.") {
+ return ErrUnmergedFiles
+ } else if strings.Contains(out, "unable to resolve reference") {
+ return ErrReferenceBroken
+ }
+ return ErrUnclassified
+}
diff --git a/pkg/git/authentication.go b/core/git/authentication.go
index a33816b..77c8edb 100644
--- a/pkg/git/authentication.go
+++ b/core/git/authentication.go
@@ -13,15 +13,15 @@ type Credentials struct {
Password string
}
-var (
- authProtocolHTTP = "http"
- authProtocolHTTPS = "https"
- authProtocolSSH = "ssh"
+const (
+ AuthProtocolHTTP = "http"
+ AuthProtocolHTTPS = "https"
+ AuthProtocolSSH = "ssh"
)
-// authentication protocol returns the type of protocol for given remote's URL
+// AuthProtocol returns the type of protocol for given remote's URL
// various auth protocols require different kind of authentication
-func authProtocol(r *Remote) (p string, err error) {
+func AuthProtocol(r *Remote) (p string, err error) {
u, err := url.Parse(r.URL[0])
if err != nil {
return p, err
diff --git a/core/git/branch.go b/core/git/branch.go
new file mode 100644
index 0000000..e2ae793
--- /dev/null
+++ b/core/git/branch.go
@@ -0,0 +1,209 @@
+package git
+
+import (
+ "os/exec"
+ "strconv"
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+ git "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+)
+
+// Branch is the wrapper of go-git's Reference struct. In addition to that, it
+// also holds name of the branch, pullable and pushable commit count from the
+// branchs' upstream. It also tracks if the repository has unstaged or uncommit-
+// ed changes
+type Branch struct {
+ Name string
+ Reference *plumbing.Reference
+ Pushables string
+ Pullables string
+ Clean bool
+}
+
+var (
+ revlistCommand = "rev-list"
+ hashLength = 40
+)
+
+// search for branches in go-git way. It is useful to do so that checkout and
+// checkout error handling can be handled by code rather than struggling with
+// git cammand and its output
+func (r *Repository) loadLocalBranches() error {
+ lbs := make([]*Branch, 0)
+ bs, err := r.Repo.Branches()
+ if err != nil {
+ log.Warn("Cannot load branches " + err.Error())
+ return err
+ }
+ defer bs.Close()
+ headRef, err := r.Repo.Head()
+ if err != nil {
+ return err
+ }
+ var branchFound bool
+ bs.ForEach(func(b *plumbing.Reference) error {
+ if b.Type() != plumbing.HashReference {
+ return nil
+ }
+
+ var push, pull string
+ pushables, err := RevList(r, RevListOptions{
+ Ref1: "@{u}",
+ Ref2: "HEAD",
+ })
+ if err != nil {
+ push = "?"
+ } else {
+ push = strconv.Itoa(len(pushables))
+ }
+ pullables, err := RevList(r, RevListOptions{
+ Ref1: "HEAD",
+ Ref2: "@{u}",
+ })
+ if err != nil {
+ pull = "?"
+ } else {
+ pull = strconv.Itoa(len(pullables))
+ }
+ clean := r.isClean()
+ branch := &Branch{
+ Name: b.Name().Short(),
+ Reference: b,
+ Pushables: push,
+ Pullables: pull,
+ Clean: clean,
+ }
+ if b.Name() == headRef.Name() {
+ r.State.Branch = branch
+ branchFound = true
+ }
+ lbs = append(lbs, branch)
+
+ return nil
+ })
+ if !branchFound {
+ branch := &Branch{
+ Name: headRef.Hash().String(),
+ Reference: headRef,
+ Pushables: "?",
+ Pullables: "?",
+ Clean: r.isClean(),
+ }
+ lbs = append(lbs, branch)
+ r.State.Branch = branch
+ }
+ r.Branches = lbs
+ return err
+}
+
+// NextBranch checkouts the next branch
+func (r *Repository) NextBranch() *Branch {
+ return r.Branches[(r.currentBranchIndex()+1)%len(r.Branches)]
+}
+
+// PreviousBranch checkouts the previous branch
+func (r *Repository) PreviousBranch() *Branch {
+ return r.Branches[(len(r.Branches)+r.currentBranchIndex()-1)%len(r.Branches)]
+}
+
+// returns the active branch index
+func (r *Repository) currentBranchIndex() int {
+ bix := 0
+ for i, lbs := range r.Branches {
+ if lbs.Name == r.State.Branch.Name {
+ bix = i
+ }
+ }
+ return bix
+}
+
+// Checkout to given branch. If any errors occur, the method returns it instead
+// of returning nil
+func (r *Repository) Checkout(b *Branch) error {
+ if b.Name == r.State.Branch.Name {
+ return nil
+ }
+
+ w, err := r.Repo.Worktree()
+ if err != nil {
+ log.Warn("Cannot get work tree " + err.Error())
+ return err
+ }
+ if err = w.Checkout(&git.CheckoutOptions{
+ Branch: b.Reference.Name(),
+ }); err != nil {
+ log.Warn("Cannot checkout " + err.Error())
+ return err
+ }
+
+ // make this conditional on global scale
+ // we don't care if this function returns an error
+ r.State.Remote.SyncBranches(b.Name)
+
+ return r.Refresh()
+}
+
+// checking the branch if it has any changes from its head revision. Initially
+// I implemented this with go-git but it was incredibly slow and there is also
+// an issue about it: https://github.com/src-d/go-git/issues/844
+func (r *Repository) isClean() bool {
+ args := []string{"status"}
+ cmd := exec.Command("git", args...)
+ cmd.Dir = r.AbsPath
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return false
+ }
+ s := string(out)
+ if strings.HasSuffix(s, "\n") {
+ s = s[:len(s)-1]
+ }
+ if len(s) >= 0 {
+ vs := strings.Split(s, "\n")
+ line := vs[len(vs)-1]
+ // earlier versions of git returns "working directory clean" instead of
+ //"working tree clean" message
+ if strings.Contains(line, "working tree clean") ||
+ strings.Contains(line, "working directory clean") {
+ return true
+ }
+ }
+ return false
+}
+
+// RevListOptions defines the rules of rev-list func
+type RevListOptions struct {
+ // Ref1 is the first reference hash to link
+ Ref1 string
+ // Ref2 is the second reference hash to link
+ Ref2 string
+}
+
+// RevList returns the commit hashes that are links from the given commit(s).
+// The output is given in reverse chronological order by default.
+func RevList(r *Repository, options RevListOptions) ([]string, error) {
+ args := make([]string, 0)
+ args = append(args, revlistCommand)
+ if len(options.Ref1) > 0 && len(options.Ref2) > 0 {
+ arg1 := options.Ref1 + ".." + options.Ref2
+ args = append(args, arg1)
+ }
+ cmd := exec.Command("git", args...)
+ cmd.Dir = r.AbsPath
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return nil, err
+ }
+ s := string(out)
+
+ hashes := strings.Split(s, "\n")
+ for _, hash := range hashes {
+ if len(hash) != hashLength {
+ return make([]string, 0), nil
+ }
+ break
+ }
+ return hashes, nil
+}
diff --git a/pkg/git/commit.go b/core/git/commit.go
index f78bb64..71076b8 100644
--- a/pkg/git/commit.go
+++ b/core/git/commit.go
@@ -1,10 +1,11 @@
package git
import (
+ "os/exec"
"regexp"
log "github.com/sirupsen/logrus"
- "gopkg.in/src-d/go-git.v4"
+ git "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
@@ -33,20 +34,20 @@ const (
// NextCommit iterates over next commit of a branch
// TODO: the commits entites can tied to branch instead ot the repository
-func (e *RepoEntity) NextCommit() {
- e.Commit = e.Commits[(e.currentCommitIndex()+1)%len(e.Commits)]
+func (r *Repository) NextCommit() {
+ r.State.Commit = r.Commits[(r.currentCommitIndex()+1)%len(r.Commits)]
}
// PreviousCommit iterates to opposite direction
-func (e *RepoEntity) PreviousCommit() {
- e.Commit = e.Commits[(len(e.Commits)+e.currentCommitIndex()-1)%len(e.Commits)]
+func (r *Repository) PreviousCommit() {
+ r.State.Commit = r.Commits[(len(r.Commits)+r.currentCommitIndex()-1)%len(r.Commits)]
}
// returns the active commit index
-func (e *RepoEntity) currentCommitIndex() int {
+func (r *Repository) currentCommitIndex() int {
cix := 0
- for i, c := range e.Commits {
- if c.Hash == e.Commit.Hash {
+ for i, c := range r.Commits {
+ if c.Hash == r.State.Commit.Hash {
cix = i
}
}
@@ -55,16 +56,16 @@ func (e *RepoEntity) currentCommitIndex() int {
// loads the local commits by simply using git log way. ALso, gets the upstream
// diff commits
-func (e *RepoEntity) loadCommits() error {
- r := e.Repository
- e.Commits = make([]*Commit, 0)
- ref, err := r.Head()
+func (r *Repository) loadCommits() error {
+ rp := r.Repo
+ r.Commits = make([]*Commit, 0)
+ ref, err := rp.Head()
if err != nil {
log.Trace("Cannot get HEAD " + err.Error())
return err
}
// git log first
- cIter, err := r.Log(&git.LogOptions{
+ cIter, err := rp.Log(&git.LogOptions{
From: ref.Hash(),
Order: git.LogOrderCommitterTime,
})
@@ -74,11 +75,11 @@ func (e *RepoEntity) loadCommits() error {
}
defer cIter.Close()
// find commits that fetched from upstream but not merged commits
- rmcs, _ := e.pullDiffsToUpstream()
- e.Commits = append(e.Commits, rmcs...)
+ rmcs, _ := r.pullDiffsToUpstream()
+ r.Commits = append(r.Commits, rmcs...)
// find commits that not pushed to upstream
- lcs, _ := e.pushDiffsToUpstream()
+ lcs, _ := r.pushDiffsToUpstream()
// ... just iterates over the commits
err = cIter.ForEach(func(c *object.Commit) error {
@@ -96,7 +97,7 @@ func (e *RepoEntity) loadCommits() error {
Time: c.Author.When.String(),
CommitType: cmType,
}
- e.Commits = append(e.Commits, commit)
+ r.Commits = append(r.Commits, commit)
return nil
})
return err
@@ -104,9 +105,9 @@ func (e *RepoEntity) loadCommits() error {
// this function creates the commit entities according to active branchs diffs
// to *its* configured upstream
-func (e *RepoEntity) pullDiffsToUpstream() ([]*Commit, error) {
+func (r *Repository) pullDiffsToUpstream() ([]*Commit, error) {
remoteCommits := make([]*Commit, 0)
- pullables, err := RevList(e, RevListOptions{
+ pullables, err := RevList(r, RevListOptions{
Ref1: "HEAD",
Ref2: "@{u}",
})
@@ -115,11 +116,14 @@ func (e *RepoEntity) pullDiffsToUpstream() ([]*Commit, error) {
} else {
re := regexp.MustCompile(`\r?\n`)
for _, s := range pullables {
+ if len(s) < hashLength {
+ continue
+ }
commit := &Commit{
Hash: s,
- Author: GitShowEmail(e.AbsPath, s),
- Message: re.ReplaceAllString(GitShowBody(e.AbsPath, s), " "),
- Time: GitShowDate(e.AbsPath, s),
+ Author: gitShowEmail(r.AbsPath, s),
+ Message: re.ReplaceAllString(gitShowBody(r.AbsPath, s), " "),
+ Time: gitShowDate(r.AbsPath, s),
CommitType: RemoteCommit,
}
remoteCommits = append(remoteCommits, commit)
@@ -130,8 +134,8 @@ func (e *RepoEntity) pullDiffsToUpstream() ([]*Commit, error) {
// this function returns the hashes of the commits that are not pushed to the
// upstream of the specific branch
-func (e *RepoEntity) pushDiffsToUpstream() ([]string, error) {
- pushables, err := RevList(e, RevListOptions{
+func (r *Repository) pushDiffsToUpstream() ([]string, error) {
+ pushables, err := RevList(r, RevListOptions{
Ref1: "@{u}",
Ref2: "HEAD",
})
@@ -140,3 +144,39 @@ func (e *RepoEntity) pushDiffsToUpstream() ([]string, error) {
}
return pushables, nil
}
+
+// gitShowEmail gets author's e-mail with git show command
+func gitShowEmail(repoPath, hash string) string {
+ args := []string{"show", "--quiet", "--pretty=format:%ae", hash}
+ cmd := exec.Command("git", args...)
+ cmd.Dir = repoPath
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return ""
+ }
+ return string(out)
+}
+
+// gitShowBody gets body of the commit with git show
+func gitShowBody(repoPath, hash string) string {
+ args := []string{"show", "--quiet", "--pretty=format:%B", hash}
+ cmd := exec.Command("git", args...)
+ cmd.Dir = repoPath
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return ""
+ }
+ return string(out)
+}
+
+// gitShowDate gets commit's date with git show as string
+func gitShowDate(repoPath, hash string) string {
+ args := []string{"show", "--quiet", "--pretty=format:%ai", hash}
+ cmd := exec.Command("git", args...)
+ cmd.Dir = repoPath
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return ""
+ }
+ return string(out)
+}
diff --git a/pkg/helpers/utils.go b/core/git/random.go
index 35de2c3..c388613 100644
--- a/pkg/helpers/utils.go
+++ b/core/git/random.go
@@ -1,31 +1,13 @@
-package helpers
+package git
import (
"math/rand"
- "strings"
"time"
)
var characterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
-// TrimTrailingNewline removes the trailing new line form a string. this method
-// is used mostly on outputs of a command
-func TrimTrailingNewline(str string) string {
- if strings.HasSuffix(str, "\n") {
- return str[:len(str)-1]
- }
- return str
-}
-
-// Min finds the minimum value of two int
-func Min(x, y int) int {
- if x < y {
- return x
- }
- return y
-}
-
// RandomString generates a random string of n length
func RandomString(n int) string {
b := make([]rune, n)
diff --git a/pkg/helpers/utils_test.go b/core/git/random_test.go
index a6a56a4..2eeb00c 100644
--- a/pkg/helpers/utils_test.go
+++ b/core/git/random_test.go
@@ -1,4 +1,4 @@
-package helpers
+package git
import "testing"
diff --git a/pkg/git/remote.go b/core/git/remote.go
index 72c8ddd..4fb3070 100644
--- a/pkg/git/remote.go
+++ b/core/git/remote.go
@@ -16,24 +16,24 @@ type Remote struct {
}
// NextRemote iterates over next branch of a remote
-func (e *RepoEntity) NextRemote() error {
- e.Remote = e.Remotes[(e.currentRemoteIndex()+1)%len(e.Remotes)]
- e.Remote.SyncBranches(e.Branch.Name)
- return e.Publish(RepositoryUpdated, nil)
+func (r *Repository) NextRemote() error {
+ r.State.Remote = r.Remotes[(r.currentRemoteIndex()+1)%len(r.Remotes)]
+ r.State.Remote.SyncBranches(r.State.Branch.Name)
+ return r.Publish(RepositoryUpdated, nil)
}
// PreviousRemote iterates over previous branch of a remote
-func (e *RepoEntity) PreviousRemote() error {
- e.Remote = e.Remotes[(len(e.Remotes)+e.currentRemoteIndex()-1)%len(e.Remotes)]
- e.Remote.SyncBranches(e.Branch.Name)
- return e.Publish(RepositoryUpdated, nil)
+func (r *Repository) PreviousRemote() error {
+ r.State.Remote = r.Remotes[(len(r.Remotes)+r.currentRemoteIndex()-1)%len(r.Remotes)]
+ r.State.Remote.SyncBranches(r.State.Branch.Name)
+ return r.Publish(RepositoryUpdated, nil)
}
// returns the active remote index
-func (e *RepoEntity) currentRemoteIndex() int {
+func (r *Repository) currentRemoteIndex() int {
cix := 0
- for i, remote := range e.Remotes {
- if remote.Name == e.Remote.Name {
+ for i, remote := range r.Remotes {
+ if remote.Name == r.State.Remote.Name {
cix = i
}
}
@@ -42,11 +42,11 @@ func (e *RepoEntity) currentRemoteIndex() int {
// search for remotes in go-git way. It is the short way to get remotes but it
// does not give any insght about remote branches
-func (e *RepoEntity) loadRemotes() error {
- r := e.Repository
- e.Remotes = make([]*Remote, 0)
+func (r *Repository) loadRemotes() error {
+ rp := r.Repo
+ r.Remotes = make([]*Remote, 0)
- rms, err := r.Remotes()
+ rms, err := rp.Remotes()
for _, rm := range rms {
rfs := make([]string, 0)
for _, rf := range rm.Config().Fetch {
@@ -57,11 +57,11 @@ func (e *RepoEntity) loadRemotes() error {
URL: rm.Config().URLs,
RefSpecs: rfs,
}
- remote.loadRemoteBranches(e)
+ remote.loadRemoteBranches(r)
if len(remote.Branches) > 0 {
remote.Branch = remote.Branches[0]
}
- e.Remotes = append(e.Remotes, remote)
+ r.Remotes = append(r.Remotes, remote)
}
if err != nil {
diff --git a/pkg/git/remotebranch.go b/core/git/remotebranch.go
index 337b28b..19f4ec2 100644
--- a/pkg/git/remotebranch.go
+++ b/core/git/remotebranch.go
@@ -17,22 +17,22 @@ type RemoteBranch struct {
}
// NextRemoteBranch iterates to the next remote branch
-func (r *Remote) NextRemoteBranch(e *RepoEntity) error {
- r.Branch = r.Branches[(r.currentRemoteBranchIndex()+1)%len(r.Branches)]
- return e.Publish(RepositoryUpdated, nil)
+func (rm *Remote) NextRemoteBranch(r *Repository) error {
+ rm.Branch = rm.Branches[(rm.currentRemoteBranchIndex()+1)%len(rm.Branches)]
+ return r.Publish(RepositoryUpdated, nil)
}
// PreviousRemoteBranch iterates to the previous remote branch
-func (r *Remote) PreviousRemoteBranch(e *RepoEntity) error {
- r.Branch = r.Branches[(len(r.Branches)+r.currentRemoteBranchIndex()-1)%len(r.Branches)]
- return e.Publish(RepositoryUpdated, nil)
+func (rm *Remote) PreviousRemoteBranch(r *Repository) error {
+ rm.Branch = rm.Branches[(len(rm.Branches)+rm.currentRemoteBranchIndex()-1)%len(rm.Branches)]
+ return r.Publish(RepositoryUpdated, nil)
}
// returns the active remote branch index
-func (r *Remote) currentRemoteBranchIndex() int {
+func (rm *Remote) currentRemoteBranchIndex() int {
cix := 0
- for i, rb := range r.Branches {
- if rb.Reference.Hash() == r.Branch.Reference.Hash() {
+ for i, rb := range rm.Branches {
+ if rb.Reference.Hash() == rm.Branch.Reference.Hash() {
cix = i
}
}
@@ -41,17 +41,17 @@ func (r *Remote) currentRemoteBranchIndex() int {
// search for the remote branches of the remote. It takes the go-git's repo
// pointer in order to get storer struct
-func (r *Remote) loadRemoteBranches(e *RepoEntity) error {
- r.Branches = make([]*RemoteBranch, 0)
- bs, err := remoteBranchesIter(e.Repository.Storer)
+func (rm *Remote) loadRemoteBranches(r *Repository) error {
+ rm.Branches = make([]*RemoteBranch, 0)
+ bs, err := remoteBranchesIter(r.Repo.Storer)
if err != nil {
log.Warn("Cannot initiate iterator " + err.Error())
return err
}
defer bs.Close()
err = bs.ForEach(func(b *plumbing.Reference) error {
- if strings.Split(b.Name().Short(), "/")[0] == r.Name {
- r.Branches = append(r.Branches, &RemoteBranch{
+ if strings.Split(b.Name().Short(), "/")[0] == rm.Name {
+ rm.Branches = append(rm.Branches, &RemoteBranch{
Name: b.Name().Short(),
Reference: b,
})
@@ -79,10 +79,10 @@ func remoteBranchesIter(s storer.ReferenceStorer) (storer.ReferenceIter, error)
}
// switches to the given remote branch
-func (r *Remote) switchRemoteBranch(remoteBranchName string) error {
- for _, rb := range r.Branches {
+func (rm *Remote) switchRemoteBranch(remoteBranchName string) error {
+ for _, rb := range rm.Branches {
if rb.Name == remoteBranchName {
- r.Branch = rb
+ rm.Branch = rb
return nil
}
}
diff --git a/pkg/git/repository.go b/core/git/repository.go
index 0e57482..ba990fd 100644
--- a/pkg/git/repository.go
+++ b/core/git/repository.go
@@ -6,62 +6,67 @@ import (
"sync"
"time"
- "github.com/isacikgoz/gitbatch/pkg/helpers"
log "github.com/sirupsen/logrus"
- "gopkg.in/src-d/go-git.v4"
+ git "gopkg.in/src-d/go-git.v4"
)
-// RepoEntity is the main entity of the application. The repository name is
+// Repository is the main entity of the application. The repository name is
// actually the name of its folder in the host's filesystem. It holds the go-git
// repository entity along with critic entites such as remote/branches and commits
-type RepoEntity struct {
- RepoID string
- Name string
- AbsPath string
- ModTime time.Time
- Repository git.Repository
- Branch *Branch
- Branches []*Branch
- Remote *Remote
- Remotes []*Remote
- Commit *Commit
- Commits []*Commit
- Stasheds []*StashedItem
- state RepoState
+type Repository struct {
+ RepoID string
+ Name string
+ AbsPath string
+ ModTime time.Time
+ Repo git.Repository
+ Branches []*Branch
+ Remotes []*Remote
+ Commits []*Commit
+ Stasheds []*StashedItem
+ State *RepositoryState
mutex *sync.RWMutex
listeners map[string][]RepositoryListener
}
+// RepositoryState is the current pointers of a repository
+type RepositoryState struct {
+ workStatus WorkStatus
+ Branch *Branch
+ Remote *Remote
+ Commit *Commit
+ Message string
+}
+
// RepositoryListener is a type for listeners
type RepositoryListener func(event *RepositoryEvent) error
// RepositoryEvent is used to transfer event-related data.
-// It is passed to listeners when Emit() is called
+// It is passed to listeners when Publish() is called
type RepositoryEvent struct {
Name string
Data interface{}
}
-// RepoState is the state of the repository for an operation
-type RepoState struct {
- State uint8
- Ready bool
+// WorkStatus is the state of the repository for an operation
+type WorkStatus struct {
+ Status uint8
+ Ready bool
}
var (
// Available implies repo is ready for the operation
- Available = RepoState{State: 0, Ready: true}
+ Available = WorkStatus{Status: 0, Ready: true}
// Queued means repo is queued for a operation
- Queued = RepoState{State: 1, Ready: false}
+ Queued = WorkStatus{Status: 1, Ready: false}
// Working means an operation is just started for this repository
- Working = RepoState{State: 2, Ready: false}
+ Working = WorkStatus{Status: 2, Ready: false}
// Paused is expected when a user interaction is required
- Paused = RepoState{State: 3, Ready: true}
+ Paused = WorkStatus{Status: 3, Ready: true}
// Success is the expected outcome of the operation
- Success = RepoState{State: 4, Ready: true}
+ Success = WorkStatus{Status: 4, Ready: true}
// Fail is the unexpected outcome of the operation
- Fail = RepoState{State: 5, Ready: false}
+ Fail = WorkStatus{Status: 5, Ready: false}
)
const (
@@ -69,69 +74,72 @@ const (
RepositoryUpdated = "repository.updated"
)
-// FastInitializeRepo initializes a RepoEntity struct without its belongings.
-func FastInitializeRepo(dir string) (e *RepoEntity, err error) {
+// FastInitializeRepo initializes a Repository struct without its belongings.
+func FastInitializeRepo(dir string) (r *Repository, err error) {
f, err := os.Open(dir)
if err != nil {
return nil, err
}
// get status of the file
fstat, _ := f.Stat()
- r, err := git.PlainOpen(dir)
+ rp, err := git.PlainOpen(dir)
if err != nil {
return nil, err
}
- // initialize RepoEntity with minimum viable fields
- e = &RepoEntity{RepoID: helpers.RandomString(8),
- Name: fstat.Name(),
- AbsPath: dir,
- ModTime: fstat.ModTime(),
- Repository: *r,
- state: Available,
- mutex: &sync.RWMutex{},
- listeners: make(map[string][]RepositoryListener),
+ // initialize Repository with minimum viable fields
+ r = &Repository{RepoID: RandomString(8),
+ Name: fstat.Name(),
+ AbsPath: dir,
+ ModTime: fstat.ModTime(),
+ Repo: *rp,
+ State: &RepositoryState{
+ workStatus: Available,
+ Message: "",
+ },
+ mutex: &sync.RWMutex{},
+ listeners: make(map[string][]RepositoryListener),
}
- return e, nil
+ return r, nil
}
-// InitializeRepo initializes a RepoEntity struct with its belongings.
-func InitializeRepo(dir string) (e *RepoEntity, err error) {
- e, err = FastInitializeRepo(dir)
+// InitializeRepo initializes a Repository struct with its belongings.
+func InitializeRepo(dir string) (r *Repository, err error) {
+ r, err = FastInitializeRepo(dir)
if err != nil {
return nil, err
}
// need nothing extra but loading additional components
- return e, e.loadComponents(true)
+ return r, r.loadComponents(true)
}
// loadComponents initializes the fields of a repository such as branches,
// remotes, commits etc. If reset, reload commit, remote pointers too
-func (e *RepoEntity) loadComponents(reset bool) error {
- if err := e.loadLocalBranches(); err != nil {
+func (r *Repository) loadComponents(reset bool) error {
+ if err := r.loadLocalBranches(); err != nil {
return err
}
- if err := e.loadCommits(); err != nil {
+ if err := r.loadCommits(); err != nil {
return err
}
- if err := e.loadRemotes(); err != nil {
+ if err := r.loadRemotes(); err != nil {
return err
}
- if err := e.loadStashedItems(); err != nil {
+ if err := r.loadStashedItems(); err != nil {
log.Warn("Cannot load stashes")
}
if reset {
// handle if there is no commit, maybe?
// set commit pointer for repository
- if len(e.Commits) > 0 {
+ if len(r.Commits) > 0 {
// select first commit
- e.Commit = e.Commits[0]
+ r.State.Commit = r.Commits[0]
}
// set remote pointer for repository
- if len(e.Remotes) > 0 {
+ if len(r.Remotes) > 0 {
// TODO: tend to take origin/master as default
- e.Remote = e.Remotes[0]
+ r.State.Remote = r.Remotes[0]
// if couldn't find, its ok.
- e.Remote.SyncBranches(e.Branch.Name)
+ r.State.Remote.SyncBranches(r.State.Branch.Name)
} else {
// if there is no remote, this project is totally useless actually
return errors.New("There is no remote for this repository")
@@ -142,46 +150,46 @@ func (e *RepoEntity) loadComponents(reset bool) error {
// Refresh the belongings of a repositoriy, this function is called right after
// fetch/pull/merge operations
-func (e *RepoEntity) Refresh() error {
+func (r *Repository) Refresh() error {
var err error
// error can be ignored since the file already exists when app is loading
- // if the RepoEntity is only fast initialized, no need to refresh because
+ // if the Repository is only fast initialized, no need to refresh because
// it won't contain its belongings
- if e.Branch == nil {
+ if r.State.Branch == nil {
return nil
}
- file, _ := os.Open(e.AbsPath)
+ file, _ := os.Open(r.AbsPath)
fstat, _ := file.Stat()
// re-initialize the go-git repository struct after supposed update
- r, err := git.PlainOpen(e.AbsPath)
+ rp, err := git.PlainOpen(r.AbsPath)
if err != nil {
return err
}
- e.Repository = *r
+ r.Repo = *rp
// modification date may be changed
- e.ModTime = fstat.ModTime()
- if err := e.loadComponents(false); err != nil {
+ r.ModTime = fstat.ModTime()
+ if err := r.loadComponents(false); err != nil {
return err
}
// we could send an event data but we don't need for this topic
- return e.Publish(RepositoryUpdated, nil)
+ return r.Publish(RepositoryUpdated, nil)
}
// On adds new listener.
// listener is a callback function that will be called when event emits
-func (e *RepoEntity) On(event string, listener RepositoryListener) {
- e.mutex.Lock()
- defer e.mutex.Unlock()
+func (r *Repository) On(event string, listener RepositoryListener) {
+ r.mutex.Lock()
+ defer r.mutex.Unlock()
// add listener to the specific event topic
- e.listeners[event] = append(e.listeners[event], listener)
+ r.listeners[event] = append(r.listeners[event], listener)
}
// Publish publishes the data to a certain event by its name.
-func (e *RepoEntity) Publish(eventName string, data interface{}) error {
- e.mutex.RLock()
- defer e.mutex.RUnlock()
+func (r *Repository) Publish(eventName string, data interface{}) error {
+ r.mutex.RLock()
+ defer r.mutex.RUnlock()
// let's find listeners for this event topic
- listeners, ok := e.listeners[eventName]
+ listeners, ok := r.listeners[eventName]
if !ok {
return nil
}
@@ -199,15 +207,15 @@ func (e *RepoEntity) Publish(eventName string, data interface{}) error {
}
// State returns the state of the repository such as queued, failed etc.
-func (e *RepoEntity) State() RepoState {
- return e.state
+func (r *Repository) WorkStatus() WorkStatus {
+ return r.State.workStatus
}
// SetState sets the state of repository and sends repository updated event
-func (e *RepoEntity) SetState(state RepoState) {
- e.state = state
+func (r *Repository) SetWorkStatus(ws WorkStatus) {
+ r.State.workStatus = ws
// we could send an event data but we don't need for this topic
- if err := e.Publish(RepositoryUpdated, nil); err != nil {
+ if err := r.Publish(RepositoryUpdated, nil); err != nil {
log.Warnf("Cannot publish on %s topic.\n", RepositoryUpdated)
}
}
diff --git a/pkg/git/util-sort.go b/core/git/sort.go
index e3067bc..179368a 100644
--- a/pkg/git/util-sort.go
+++ b/core/git/sort.go
@@ -4,9 +4,9 @@ import (
"unicode"
)
-// Alphabetical slice is the re-ordered *RepoEntity slice that sorted according
+// Alphabetical slice is the re-ordered *Repository slice that sorted according
// to alphabetical order (A-Z)
-type Alphabetical []*RepoEntity
+type Alphabetical []*Repository
// Len is the interface implementation for Alphabetical sorting function
func (s Alphabetical) Len() int { return len(s) }
@@ -43,9 +43,9 @@ func (s Alphabetical) Less(i, j int) bool {
return false
}
-// LastModified slice is the re-ordered *RepoEntity slice that sorted according
+// LastModified slice is the re-ordered *Repository slice that sorted according
// to last modified date of the repository directory
-type LastModified []*RepoEntity
+type LastModified []*Repository
// Len is the interface implementation for LastModified sorting function
func (s LastModified) Len() int { return len(s) }
@@ -58,20 +58,10 @@ 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)
+func Less(ri, rj *Repository) bool {
+ iRunes := []rune(ri.Name)
+ jRunes := []rune(rj.Name)
max := len(iRunes)
if max > len(jRunes) {
@@ -95,4 +85,4 @@ func (s filesAlphabetical) Less(i, j int) bool {
}
}
return false
-}
+} \ No newline at end of file
diff --git a/pkg/git/cmd-stash.go b/core/git/stash.go
index d2708ec..1391aed 100644
--- a/pkg/git/cmd-stash.go
+++ b/core/git/stash.go
@@ -1,6 +1,7 @@
package git
import (
+ "os/exec"
"regexp"
"strconv"
"strings"
@@ -19,21 +20,9 @@ type StashedItem struct {
EntityPath string
}
-func stashGet(e *RepoEntity, option string) string {
- args := make([]string, 0)
- args = append(args, stashCommand)
- args = append(args, option)
- out, err := GenericGitCommandWithOutput(e.AbsPath, args)
- if err != nil {
- log.Warn("Error while stash command")
- return "?"
- }
- return out
-}
-
-func (e *RepoEntity) loadStashedItems() error {
- e.Stasheds = make([]*StashedItem, 0)
- output := stashGet(e, "list")
+func (r *Repository) loadStashedItems() error {
+ r.Stasheds = make([]*StashedItem, 0)
+ output := stashGet(r, "list")
stashIDRegex := regexp.MustCompile(`stash@{[\d]+}:`)
stashIDRegexInt := regexp.MustCompile(`[\d]+`)
stashBranchRegex := regexp.MustCompile(`^(.*?): `)
@@ -75,44 +64,64 @@ func (e *RepoEntity) loadStashedItems() error {
}
// trim hash
- e.Stasheds = append(e.Stasheds, &StashedItem{
+ r.Stasheds = append(r.Stasheds, &StashedItem{
StashID: i,
BranchName: branchName,
Hash: hash,
Description: desc,
- EntityPath: e.AbsPath,
+ EntityPath: r.AbsPath,
})
}
return nil
}
-// Stash is the wrapper of convetional "git stash" command
-func (e *RepoEntity) Stash() (output string, err error) {
+func stashGet(r *Repository, option string) string {
args := make([]string, 0)
- args = append(args, stashCommand)
-
- output, err = GenericGitCommandWithErrorOutput(e.AbsPath, args)
- e.Refresh()
- return output, err
+ args = append(args, "stash")
+ args = append(args, option)
+ cmd := exec.Command("git", args...)
+ cmd.Dir = r.AbsPath
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return "?"
+ }
+ return string(output)
}
// Pop is the wrapper of "git stash pop" command that used for a file
-func (stashedItem *StashedItem) Pop() (output string, err error) {
+func (stashedItem *StashedItem) Pop() (string, error) {
args := make([]string, 0)
- args = append(args, stashCommand)
+ args = append(args, "stash")
args = append(args, "pop")
args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}")
- output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args)
- return output, err
+ cmd := exec.Command("git", args...)
+ cmd.Dir = stashedItem.EntityPath
+ output, err := cmd.CombinedOutput()
+ return string(output), err
}
// Show is the wrapper of "git stash show -p " command
-func (stashedItem *StashedItem) Show() (output string, err error) {
+func (stashedItem *StashedItem) Show() (string, error) {
args := make([]string, 0)
- args = append(args, stashCommand)
+ args = append(args, "stash")
args = append(args, "show")
args = append(args, "-p")
args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}")
- output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args)
- return output, err
+ cmd := exec.Command("git", args...)
+ cmd.Dir = stashedItem.EntityPath
+ output, err := cmd.CombinedOutput()
+
+ return string(output), err
+}
+
+// Stash is the wrapper of convetional "git stash" command
+func (r *Repository) Stash() (string, error) {
+ args := make([]string, 0)
+ args = append(args, "stash")
+
+ cmd := exec.Command("git", args...)
+ cmd.Dir = r.AbsPath
+ output, err := cmd.CombinedOutput()
+ r.Refresh()
+ return string(output), err
}
diff --git a/core/job/job.go b/core/job/job.go
new file mode 100644
index 0000000..9cfc0ed
--- /dev/null
+++ b/core/job/job.go
@@ -0,0 +1,79 @@
+package job
+
+import (
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/isacikgoz/gitbatch/core/git"
+)
+
+// 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
+ // Repository points to the repository that will be used for operation
+ Repository *git.Repository
+ // Options is a placeholder for operation options
+ Options interface{}
+}
+
+// JobType is the a git operation supported
+type JobType string
+
+const (
+ // FetchJob is wrapper of git fetch command
+ FetchJob JobType = "fetch"
+
+ // PullJob is wrapper of git pull command
+ PullJob JobType = "pull"
+
+ // MergeJob is wrapper of git merge command
+ MergeJob JobType = "merge"
+)
+
+// starts the job
+func (j *Job) start() error {
+ j.Repository.SetWorkStatus(git.Working)
+ // TODO: Handle errors?
+ // TOOD: Better implementation required
+ switch mode := j.JobType; mode {
+ case FetchJob:
+ var opts command.FetchOptions
+ if j.Options != nil {
+ opts = j.Options.(command.FetchOptions)
+ } else {
+ opts = command.FetchOptions{
+ RemoteName: j.Repository.State.Remote.Name,
+ }
+ }
+ if err := command.Fetch(j.Repository, opts); err != nil {
+ j.Repository.SetWorkStatus(git.Fail)
+ j.Repository.State.Message = err.Error()
+ return err
+ }
+ case PullJob:
+ var opts command.PullOptions
+ if j.Options != nil {
+ opts = j.Options.(command.PullOptions)
+ } else {
+ opts = command.PullOptions{
+ RemoteName: j.Repository.State.Remote.Name,
+ }
+ }
+ if err := command.Pull(j.Repository, opts); err != nil {
+ j.Repository.SetWorkStatus(git.Fail)
+ j.Repository.State.Message = err.Error()
+ return err
+ }
+ case MergeJob:
+ if err := command.Merge(j.Repository, command.MergeOptions{
+ BranchName: j.Repository.State.Remote.Branch.Name,
+ }); err != nil {
+ j.Repository.SetWorkStatus(git.Fail)
+ j.Repository.State.Message = err.Error()
+ return nil
+ }
+ default:
+ j.Repository.SetWorkStatus(git.Available)
+ return nil
+ }
+ return nil
+}
diff --git a/pkg/git/job-queue.go b/core/job/queue.go
index 3eee605..e337064 100644
--- a/pkg/git/job-queue.go
+++ b/core/job/queue.go
@@ -1,7 +1,14 @@
-package git
+package job
import (
+ "context"
"errors"
+ "runtime"
+ "sync"
+
+ "github.com/isacikgoz/gitbatch/core/git"
+ log "github.com/sirupsen/logrus"
+ "golang.org/x/sync/semaphore"
)
// JobQueue holds the slice of Jobs
@@ -21,7 +28,7 @@ func CreateJobQueue() (jq *JobQueue) {
// AddJob adds a job to the queue
func (jq *JobQueue) AddJob(j *Job) error {
for _, job := range jq.series {
- if job.Entity.RepoID == j.Entity.RepoID && job.JobType == j.JobType {
+ if job.Repository.RepoID == j.Repository.RepoID && job.JobType == j.JobType {
return errors.New("Same job already is in the queue")
}
}
@@ -49,10 +56,10 @@ func (jq *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 (jq *JobQueue) RemoveFromQueue(entity *RepoEntity) error {
+func (jq *JobQueue) RemoveFromQueue(r *git.Repository) error {
removed := false
for i, job := range jq.series {
- if job.Entity.RepoID == entity.RepoID {
+ if job.Repository.RepoID == r.RepoID {
jq.series = append(jq.series[:i], jq.series[i+1:]...)
removed = true
}
@@ -66,13 +73,55 @@ func (jq *JobQueue) RemoveFromQueue(entity *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 (jq *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) {
+func (jq *JobQueue) IsInTheQueue(r *git.Repository) (inTheQueue bool, j *Job) {
inTheQueue = false
for _, job := range jq.series {
- if job.Entity.RepoID == entity.RepoID {
+ if job.Repository.RepoID == r.RepoID {
inTheQueue = true
- jt = job.JobType
+ j = job
+ }
+ }
+ return inTheQueue, j
+}
+
+// StartJobsAsync start he jobs in the queue asynchronously
+func (jq *JobQueue) StartJobsAsync() map[*Job]error {
+
+ ctx := context.TODO()
+
+ var (
+ maxWorkers = runtime.GOMAXPROCS(0)
+ sem = semaphore.NewWeighted(int64(maxWorkers))
+ fails = make(map[*Job]error)
+ )
+
+ var mx sync.Mutex
+ for range jq.series {
+
+ if err := sem.Acquire(ctx, 1); err != nil {
+ log.Errorf("Failed to acquire semaphore: %v", err)
+ break
}
+
+ go func() {
+
+ defer sem.Release(1)
+ j, _, err := jq.StartNext()
+ if err != nil {
+ mx.Lock()
+ fails[j] = err
+ mx.Unlock()
+ }
+ }()
}
- return inTheQueue, jt
+
+ // Acquire all of the tokens to wait for any remaining workers to finish.
+ //
+ // If you are already waiting for the workers by some other means (such as an
+ // errgroup.Group), you can omit this final Acquire call.
+ if err := sem.Acquire(ctx, int64(maxWorkers)); err != nil {
+ log.Errorf("Failed to acquire semaphore: %v", err)
+ }
+
+ return fails
}
diff --git a/core/load/load.go b/core/load/load.go
new file mode 100644
index 0000000..c568517
--- /dev/null
+++ b/core/load/load.go
@@ -0,0 +1,100 @@
+package load
+
+import (
+ log "github.com/sirupsen/logrus"
+
+ "context"
+ "errors"
+ "runtime"
+ "sync"
+
+ "github.com/isacikgoz/gitbatch/core/git"
+ "golang.org/x/sync/semaphore"
+)
+
+// AsyncAdd is interface to caller
+type AsyncAdd func(r *git.Repository)
+
+// SyncLoad initializes the go-git's repository obejcts with given
+// slice of paths. since this job is done parallel, the order of the directories
+// is not kept
+func SyncLoad(directories []string) (entities []*git.Repository, err error) {
+ entities = make([]*git.Repository, 0)
+
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+
+ for _, dir := range directories {
+ // increment wait counter by one because we run a single goroutine
+ // below
+ wg.Add(1)
+ go func(d string) {
+ // decrement the wait counter by one, we call it in a defer so it's
+ // called at the end of this goroutine
+ defer wg.Done()
+ entity, err := git.InitializeRepo(d)
+ if err != nil {
+ log.WithFields(log.Fields{
+ "directory": d,
+ }).Trace("Cannot load git repository.")
+ return
+ }
+ // lock so we don't get a race if multiple go routines try to add
+ // to the same entities
+ mu.Lock()
+ entities = append(entities, entity)
+ mu.Unlock()
+ }(dir)
+ }
+ // wait until the wait counter is zero, this happens if all goroutines have
+ // finished
+ wg.Wait()
+ if len(entities) == 0 {
+ return entities, errors.New("There are no git repositories at given path(s)")
+ }
+ return entities, nil
+}
+
+// AsyncLoad asynchronously adds to AsyncAdd function
+func AsyncLoad(directories []string, add AsyncAdd, d chan bool) error {
+ ctx := context.TODO()
+
+ var (
+ maxWorkers = runtime.GOMAXPROCS(0)
+ sem = semaphore.NewWeighted(int64(maxWorkers))
+ )
+
+ var mx sync.Mutex
+
+ // Compute the output using up to maxWorkers goroutines at a time.
+ for _, dir := range directories {
+ if err := sem.Acquire(ctx, 1); err != nil {
+ log.Errorf("Failed to acquire semaphore: %v", err)
+ break
+ }
+
+ go func(d string) {
+
+ defer sem.Release(1)
+ entity, err := git.InitializeRepo(d)
+ if err != nil {
+ log.WithFields(log.Fields{
+ "directory": d,
+ }).Trace("Cannot load git repository.")
+ return
+ }
+ // lock so we don't get a race if multiple go routines try to add
+ // to the same entities
+ mx.Lock()
+ add(entity)
+ mx.Unlock()
+ }(dir)
+ }
+ // Acquire all of the tokens to wait for any remaining workers to finish.
+ if err := sem.Acquire(ctx, int64(maxWorkers)); err != nil {
+ return err
+ }
+ d <- true
+ sem = nil
+ return nil
+}
diff --git a/pkg/gui/authenticationview.go b/gui/authenticationview.go
index a274425..2faa3b7 100644
--- a/pkg/gui/authenticationview.go
+++ b/gui/authenticationview.go
@@ -4,7 +4,9 @@ import (
"fmt"
"regexp"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/isacikgoz/gitbatch/core/job"
"github.com/jroimartin/gocui"
log "github.com/sirupsen/logrus"
)
@@ -27,11 +29,11 @@ var (
authLabels = []viewFeature{authenticationViewFeature, authUserLabelFeature, authPswdLabelViewFeature}
// we can hold the job that is required to authenticate
- jobRequiresAuth *git.Job
+ jobRequiresAuth *job.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 {
+func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *job.JobQueue, job *job.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
@@ -39,8 +41,8 @@ func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job
return err
}
jobRequiresAuth = job
- if job.Entity.State() != git.Fail {
- if err := jobQueue.RemoveFromQueue(job.Entity); err != nil {
+ if job.Repository.WorkStatus() != git.Fail {
+ if err := jobQueue.RemoveFromQueue(job.Repository); err != nil {
log.Fatal(err.Error())
return err
}
@@ -51,7 +53,7 @@ func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job
if err != gocui.ErrUnknownView {
return err
}
- fmt.Fprintln(v, keySymbol+selectionIndicator+red.Sprint(jobRequiresAuth.Entity.Remote.URL[0]))
+ fmt.Fprintln(v, keySymbol+selectionIndicator+red.Sprint(jobRequiresAuth.Repository.State.Remote.URL[0]))
}
g.Cursor = true
if err := gui.openUserView(g); err != nil {
@@ -100,41 +102,32 @@ func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error {
// 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,
+ case job.FetchJob:
+ jobRequiresAuth.Options = command.FetchOptions{
+ RemoteName: jobRequiresAuth.Repository.State.Remote.Name,
Credentials: git.Credentials{
User: creduser,
Password: credpswd,
},
}
- case git.PullJob:
+ case job.PullJob:
// we handle pull as fetch&merge so same rule applies
- jobRequiresAuth.Options = git.PullOptions{
- RemoteName: jobRequiresAuth.Entity.Remote.Name,
+ jobRequiresAuth.Options = command.PullOptions{
+ RemoteName: jobRequiresAuth.Repository.State.Remote.Name,
Credentials: git.Credentials{
User: creduser,
Password: credpswd,
},
}
}
- jobRequiresAuth.Entity.SetState(git.Queued)
+ jobRequiresAuth.Repository.SetWorkStatus(git.Queued)
// add this job to the last of the queue
if err := gui.State.Queue.AddJob(jobRequiresAuth); err != nil {
return err
}
- if err := gui.closeAuthenticationView(g, v); err != nil {
- return err // should return??
- }
-
- vReturn, err := g.View(authenticationReturnView)
- if err != nil {
- return err // should return??
- }
-
- return gui.startQueue(g, vReturn)
+ return gui.closeAuthenticationView(g, v)
}
// open an error view to inform user with a message and a useful note
diff --git a/pkg/gui/commitview.go b/gui/commitview.go
index 3359037..7acc5ca 100644
--- a/pkg/gui/commitview.go
+++ b/gui/commitview.go
@@ -5,7 +5,7 @@ import (
"fmt"
"regexp"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/command"
"github.com/jroimartin/gocui"
)
@@ -57,7 +57,7 @@ func (gui *Gui) openCommitMessageView(g *gocui.Gui, v *gocui.View) error {
// open an error view to inform user with a message and a useful note
func (gui *Gui) openCommitUserNameView(g *gocui.Gui) error {
- e := gui.getSelectedRepository()
+ r := 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)
@@ -74,7 +74,7 @@ func (gui *Gui) openCommitUserNameView(g *gocui.Gui) error {
if err != gocui.ErrUnknownView {
return err
}
- name, err := git.Config(e, git.ConfigOptions{
+ name, err := command.Config(r, command.ConfigOptions{
Section: "user",
Option: "name",
})
@@ -90,7 +90,7 @@ func (gui *Gui) openCommitUserNameView(g *gocui.Gui) error {
// open an error view to inform user with a message and a useful note
func (gui *Gui) openCommitUserEmailView(g *gocui.Gui) error {
- e := gui.getSelectedRepository()
+ r := 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)
@@ -107,7 +107,7 @@ func (gui *Gui) openCommitUserEmailView(g *gocui.Gui) error {
if err != gocui.ErrUnknownView {
return err
}
- email, err := git.Config(e, git.ConfigOptions{
+ email, err := command.Config(r, command.ConfigOptions{
Section: "user",
Option: "email",
})
@@ -123,7 +123,7 @@ func (gui *Gui) openCommitUserEmailView(g *gocui.Gui) error {
// close the opened commite mesage view
func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
// in order to read buffer of the views, first we need to find'em
vMsg, err := g.View(commitMessageViewFeature.Name)
@@ -151,7 +151,7 @@ func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error {
return errors.New("User email needs to be provided")
}
- err = git.CommitCommand(e, git.CommitOptions{
+ err = command.CommitCommand(r, command.CommitOptions{
CommitMsg: msg,
User: name,
Email: email,
@@ -170,7 +170,7 @@ func (gui *Gui) nextCommitView(g *gocui.Gui, v *gocui.View) error {
// close the opened commite mesage view
func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
g.Cursor = false
for _, view := range commitViews {
if err := g.DeleteView(view.Name); err != nil {
@@ -182,7 +182,7 @@ func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error {
return err
}
}
- if err := refreshAllStatusView(g, e, true); err != nil {
+ if err := refreshAllStatusView(g, r, true); err != nil {
return err
}
return gui.closeViewCleanup(commitMesageReturnView)
diff --git a/pkg/gui/controlsview.go b/gui/controlsview.go
index 4b2f97e..4b2f97e 100644
--- a/pkg/gui/controlsview.go
+++ b/gui/controlsview.go
diff --git a/pkg/gui/diffview.go b/gui/diffview.go
index 9a47f95..9f49456 100644
--- a/pkg/gui/diffview.go
+++ b/gui/diffview.go
@@ -3,7 +3,7 @@ package gui
import (
"fmt"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/command"
"github.com/jroimartin/gocui"
)
@@ -34,11 +34,11 @@ func (gui *Gui) prepareDiffView(g *gocui.Gui, v *gocui.View, display []string) (
// open diff view for the selcted commit
// called from commitview, so initial view is commitview
func (gui *Gui) openCommitDiffView(g *gocui.Gui, v *gocui.View) (err error) {
- e := gui.getSelectedRepository()
- commit := e.Commit
+ r := gui.getSelectedRepository()
+ commit := r.State.Commit
commitDetail := []string{("Hash: " + cyan.Sprint(commit.Hash) + "\n" + "Author: " + commit.Author +
"\n" + commit.Time + "\n" + "\n" + "\t\t" + commit.Message + "\n")}
- diff, err := git.Diff(e, e.Commit.Hash)
+ diff, err := command.Diff(r, r.State.Commit.Hash)
if err != nil {
return err
}
@@ -56,19 +56,17 @@ func (gui *Gui) openCommitDiffView(g *gocui.Gui, v *gocui.View) (err error) {
// called from status, so initial view may be stagedview or unstaged view
func (gui *Gui) openFileDiffView(g *gocui.Gui, v *gocui.View) (err error) {
- e := gui.getSelectedRepository()
+
_, cy := v.Cursor()
_, oy := v.Origin()
- var files []*git.File
+ var files []*command.File
switch v.Name() {
case unstageViewFeature.Name:
- _, files, err = populateFileLists(e)
+ files = unstagedFiles
case stageViewFeature.Name:
- files, _, err = populateFileLists(e)
- }
- if err != nil {
- return err
+ files = stagedFiles
}
+
if len(files) <= 0 {
return nil
}
@@ -95,13 +93,13 @@ func (gui *Gui) openFileDiffView(g *gocui.Gui, v *gocui.View) (err error) {
// called from stashview, so initial view is stashview
func (gui *Gui) showStash(g *gocui.Gui, v *gocui.View) (err error) {
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
_, oy := v.Origin()
_, cy := v.Cursor()
- if len(e.Stasheds) <= 0 {
+ if len(r.Stasheds) <= 0 {
return nil
}
- stashedItem := e.Stasheds[oy+cy]
+ stashedItem := r.Stasheds[oy+cy]
output, err := stashedItem.Show()
if err != nil {
if err = gui.openErrorView(g, output,
diff --git a/pkg/gui/errorview.go b/gui/errorview.go
index f679b54..f679b54 100644
--- a/pkg/gui/errorview.go
+++ b/gui/errorview.go
diff --git a/pkg/gui/util-common.go b/gui/extensions.go
index 92c49b9..b25848c 100644
--- a/pkg/gui/util-common.go
+++ b/gui/extensions.go
@@ -1,7 +1,6 @@
package gui
import (
- "github.com/isacikgoz/gitbatch/pkg/helpers"
"github.com/jroimartin/gocui"
log "github.com/sirupsen/logrus"
)
@@ -94,7 +93,7 @@ func (gui *Gui) correctCursor(v *gocui.View) error {
if oy+cy <= ly {
return nil
}
- newCy := helpers.Min(ly, maxY)
+ newCy := min(ly, maxY)
if err := v.SetCursor(cx, newCy); err != nil {
return err
}
@@ -102,6 +101,14 @@ func (gui *Gui) correctCursor(v *gocui.View) error {
return err
}
+// min finds the minimum value of two int
+func min(x, y int) int {
+ if x < y {
+ return x
+ }
+ return y
+}
+
// this function handles the iteration of a side view and set its origin point
// so that the selected line can be in the middle of the view
func (gui *Gui) smartAnchorRelativeToLine(v *gocui.View, currentindex, totallines int) error {
diff --git a/pkg/gui/gui.go b/gui/gui.go
index 607c6f8..767bf16 100644
--- a/pkg/gui/gui.go
+++ b/gui/gui.go
@@ -2,9 +2,12 @@ package gui
import (
"fmt"
+ "sort"
"sync"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/isacikgoz/gitbatch/core/job"
+ "github.com/isacikgoz/gitbatch/core/load"
"github.com/jroimartin/gocui"
log "github.com/sirupsen/logrus"
)
@@ -21,10 +24,11 @@ type Gui struct {
// guiState struct holds the repositories, directiories, mode and queue of the
// gui object. These values are not static
type guiState struct {
- Repositories []*git.RepoEntity
- Directories []string
- Mode mode
- Queue *git.JobQueue
+ Repositories []*git.Repository
+ Directories []string
+ Mode mode
+ Queue *job.JobQueue
+ FailoverQueue *job.JobQueue
}
// this struct encapsulates the name and title of a view. the name of a view is
@@ -72,14 +76,17 @@ var (
mainViews = []viewFeature{mainViewFeature, remoteViewFeature, remoteBranchViewFeature, branchViewFeature, commitViewFeature}
modes = []mode{fetchMode, pullMode, mergeMode}
+
+ loaded = make(chan bool)
)
// NewGui creates a Gui opject and fill it's state related entites
func NewGui(mode string, directoies []string) (*Gui, error) {
initialState := guiState{
- Directories: directoies,
- Mode: fetchMode,
- Queue: git.CreateJobQueue(),
+ Directories: directoies,
+ Mode: fetchMode,
+ Queue: job.CreateJobQueue(),
+ FailoverQueue: job.CreateJobQueue(),
}
gui := &Gui{
State: initialState,
@@ -106,40 +113,11 @@ func (gui *Gui) Run() error {
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)
- // start an async view apart from this loop to show loading screen
- go func(g_ui *Gui) {
- maxX, maxY := g.Size()
- // TODO: view size can be handled in a more smart way
- 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 {
- log.Warn("Loading view cannot be created.")
- return
- }
- fmt.Fprintln(v, "Loading...")
- }
- if _, err := g.SetCurrentView(loadingViewFeature.Name); err != nil {
- log.Warn("Loading view cannot be focused.")
- return
- }
- rs, err := git.LoadRepositoryEntities(g_ui.State.Directories)
- if err != nil {
- g.Close()
- log.Fatal(err)
- return
- }
- g_ui.State.Repositories = rs
- // add gui's repositoryUpdated func as an observer to repositories
- for _, repo := range rs {
- repo.On(git.RepositoryUpdated, gui.repositoryUpdated)
- }
- gui.fillMain(g)
- }(gui)
+ // load repositories in background asynchronously
+ go load.AsyncLoad(gui.State.Directories, gui.loadRepository, loaded)
if err := gui.generateKeybindings(); err != nil {
log.Error("Keybindings could not be created.")
@@ -156,6 +134,43 @@ func (gui *Gui) Run() error {
return nil
}
+func (gui *Gui) loadRepository(r *git.Repository) {
+ rs := gui.State.Repositories
+
+ // insertion sort implementation
+ index := sort.Search(len(rs), func(i int) bool { return git.Less(r, rs[i]) })
+ rs = append(rs, &git.Repository{})
+ copy(rs[index+1:], rs[index:])
+ rs[index] = r
+ // add listener
+ r.On(git.RepositoryUpdated, gui.repositoryUpdated)
+ // update gui
+ gui.repositoryUpdated(nil)
+ gui.renderTitle()
+ // take pointer back
+ gui.State.Repositories = rs
+ go func() {
+ if <-loaded {
+ v, err := gui.g.View(mainViewFeature.Name)
+ if err != nil {
+ log.Warn(err.Error())
+ return
+ }
+ v.Title = mainViewFeature.Title + fmt.Sprintf("(%d) ", len(gui.State.Repositories))
+ }
+ }()
+}
+
+func (gui *Gui) renderTitle() error {
+ v, err := gui.g.View(mainViewFeature.Name)
+ if err != nil {
+ log.Warn(err.Error())
+ return err
+ }
+ v.Title = mainViewFeature.Title + fmt.Sprintf("(%d/%d) ", len(gui.State.Repositories), len(gui.State.Directories))
+ return nil
+}
+
// set the layout and create views with their default size, name etc. values
// TODO: window sizes can be handled better
func (gui *Gui) layout(g *gocui.Gui) error {
@@ -166,6 +181,9 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
v.Title = mainViewFeature.Title
v.Overwrite = true
+ if _, err := gui.setCurrentViewOnTop(g, mainViewFeature.Name); err != nil {
+ return err
+ }
}
if v, err := g.SetView(remoteViewFeature.Name, int(0.55*float32(maxX)), 0, maxX-1, int(0.10*float32(maxY))); err != nil {
if err != gocui.ErrUnknownView {
diff --git a/pkg/gui/keybindings.go b/gui/keybindings.go
index 3c5db5f..668fe90 100644
--- a/pkg/gui/keybindings.go
+++ b/gui/keybindings.go
@@ -110,10 +110,10 @@ func (gui *Gui) generateKeybindings() error {
statusKeybindings := []*KeyBinding{
{
View: view.Name,
- Key: gocui.KeyEsc,
+ Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.closeStatusView,
- Display: "esc",
+ Display: "q",
Description: "Close/Cancel",
Vital: true,
}, {
@@ -310,6 +310,14 @@ func (gui *Gui) generateKeybindings() error {
// Main view controls
{
View: mainViewFeature.Name,
+ Key: 'u',
+ Modifier: gocui.ModNone,
+ Handler: gui.submitCredentials,
+ Display: "u",
+ Description: "Submit Credentials",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.cursorUp,
@@ -318,6 +326,38 @@ func (gui *Gui) generateKeybindings() error {
Vital: false,
}, {
View: mainViewFeature.Name,
+ Key: gocui.KeyPgup,
+ Modifier: gocui.ModNone,
+ Handler: gui.pageUp,
+ Display: "page up",
+ Description: "Page up",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyHome,
+ Modifier: gocui.ModNone,
+ Handler: gui.cursorTop,
+ Display: "home",
+ Description: "Home",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyPgdn,
+ Modifier: gocui.ModNone,
+ Handler: gui.pageDown,
+ Display: "page down",
+ Description: "Page Down",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyEnd,
+ Modifier: gocui.ModNone,
+ Handler: gui.cursorEnd,
+ Display: "end",
+ Description: "End",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.cursorDown,
@@ -440,10 +480,10 @@ func (gui *Gui) generateKeybindings() error {
// upstream confirmation
{
View: confirmationViewFeature.Name,
- Key: gocui.KeyEsc,
+ Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.closeConfirmationView,
- Display: "esc",
+ Display: "q",
Description: "Close/Cancel",
Vital: true,
}, {
@@ -458,10 +498,10 @@ func (gui *Gui) generateKeybindings() error {
// Diff View Controls
{
View: diffViewFeature.Name,
- Key: gocui.KeyEsc,
+ Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.closeCommitDiffView,
- Display: "esc",
+ Display: "q",
Description: "Close/Cancel",
Vital: true,
}, {
@@ -500,10 +540,10 @@ func (gui *Gui) generateKeybindings() error {
// Application Controls
{
View: cheatSheetViewFeature.Name,
- Key: gocui.KeyEsc,
+ Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.closeCheatSheetView,
- Display: "esc",
+ Display: "q",
Description: "Close/Cancel",
Vital: true,
}, {
@@ -542,10 +582,10 @@ func (gui *Gui) generateKeybindings() error {
// Error View
{
View: errorViewFeature.Name,
- Key: gocui.KeyEsc,
+ Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.closeErrorView,
- Display: "esc",
+ Display: "q",
Description: "Close/Cancel",
Vital: true,
}, {
diff --git a/pkg/gui/mainview.go b/gui/mainview.go
index d07a742..6cdaf61 100644
--- a/pkg/gui/mainview.go
+++ b/gui/mainview.go
@@ -4,35 +4,13 @@ import (
"fmt"
"sort"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ gerr "github.com/isacikgoz/gitbatch/core/errors"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/isacikgoz/gitbatch/core/job"
"github.com/jroimartin/gocui"
log "github.com/sirupsen/logrus"
)
-// this is the initial function for filling the values for the main view. the
-// function waits a separate routine to fill the gui's repository slice
-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
- }
-
- // if there is still a loading screen we better get rid of it
- if err := g.DeleteView(loadingViewFeature.Name); err != nil {
- return err
- }
- if _, err := gui.setCurrentViewOnTop(g, mainViewFeature.Name); err != nil {
- return err
- }
-
- // Sort by name is default behavior as expected, so it handles initial
- // rendering of the main view
- return gui.sortByName(g, v)
- })
- return nil
-}
-
// refresh the main view and re-render the repository representations
func (gui *Gui) renderMain() error {
gui.mutex.Lock()
@@ -94,11 +72,97 @@ func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
return gui.renderMain()
}
+// moves cursor to the top
+func (gui *Gui) cursorTop(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, _ := v.Origin()
+ cx, _ := v.Cursor()
+ if err := v.SetOrigin(ox, 0); err != nil {
+ return err
+ }
+ if err := v.SetCursor(cx, 0); err != nil {
+ return err
+ }
+ }
+ return gui.renderMain()
+}
+
+// moves cursor to the end
+func (gui *Gui) cursorEnd(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, _ := v.Origin()
+ cx, _ := v.Cursor()
+ _, vy := v.Size()
+ lr := len(gui.State.Repositories)
+ if lr <= vy {
+ if err := v.SetCursor(cx, lr-1); err != nil {
+ return err
+ }
+ return gui.renderMain()
+ }
+ if err := v.SetOrigin(ox, lr-vy); err != nil {
+ return err
+ }
+ if err := v.SetCursor(cx, vy-1); err != nil {
+ return err
+ }
+ }
+ return gui.renderMain()
+}
+
+// moves cursor down for a page size
+func (gui *Gui) pageDown(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, oy := v.Origin()
+ cx, _ := v.Cursor()
+ _, vy := v.Size()
+ lr := len(gui.State.Repositories)
+ if lr < vy {
+ return nil
+ }
+ if oy+vy >= lr-vy {
+ if err := v.SetOrigin(ox, lr-vy); err != nil {
+ return err
+ }
+ } else if err := v.SetOrigin(ox, oy+vy); err != nil {
+ return err
+ }
+ if err := v.SetCursor(cx, 0); err != nil {
+ return err
+ }
+ }
+ return gui.renderMain()
+}
+
+// moves cursor up for a page size
+func (gui *Gui) pageUp(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, oy := v.Origin()
+ cx, cy := v.Cursor()
+ _, vy := v.Size()
+ if oy == 0 || oy+cy < vy {
+ if err := v.SetOrigin(ox, 0); err != nil {
+ return err
+ }
+ } else if oy <= vy {
+ if err := v.SetOrigin(ox, oy+cy-vy); err != nil {
+ return err
+ }
+ } else if err := v.SetOrigin(ox, oy-vy); err != nil {
+ return err
+ }
+ if err := v.SetCursor(cx, 0); err != nil {
+ return err
+ }
+ }
+ return gui.renderMain()
+}
+
// returns the entity at cursors position by taking its position in the gui's
// slice of repositories. Since it is not a %100 percent safe methodology it may
// rrequire a better implementation or the slice's order must be synchronized
// with the views lines
-func (gui *Gui) getSelectedRepository() *git.RepoEntity {
+func (gui *Gui) getSelectedRepository() *git.Repository {
if len(gui.State.Repositories) == 0 {
return nil
}
@@ -109,65 +173,69 @@ func (gui *Gui) getSelectedRepository() *git.RepoEntity {
}
// adds given entity to job queue
-func (gui *Gui) addToQueue(entity *git.RepoEntity) error {
- var jt git.JobType
+func (gui *Gui) addToQueue(r *git.Repository) error {
+ var jt job.JobType
switch mode := gui.State.Mode.ModeID; mode {
case FetchMode:
- jt = git.FetchJob
+ jt = job.FetchJob
case PullMode:
- jt = git.PullJob
+ jt = job.PullJob
case MergeMode:
- jt = git.MergeJob
+ jt = job.MergeJob
default:
return nil
}
- err := gui.State.Queue.AddJob(&git.Job{
- JobType: jt,
- Entity: entity,
+ err := gui.State.Queue.AddJob(&job.Job{
+ JobType: jt,
+ Repository: r,
})
if err != nil {
return err
}
- entity.SetState(git.Queued)
+ r.SetWorkStatus(git.Queued)
return nil
}
// removes given entity from job queue
-func (gui *Gui) removeFromQueue(entity *git.RepoEntity) error {
- err := gui.State.Queue.RemoveFromQueue(entity)
+func (gui *Gui) removeFromQueue(r *git.Repository) error {
+ err := gui.State.Queue.RemoveFromQueue(r)
if err != nil {
return err
}
- entity.SetState(git.Available)
+ r.SetWorkStatus(git.Available)
return nil
}
// this function starts the queue and updates the gui with the result of an
// operation
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()
+ go func(gui_go *Gui) {
+ fails := gui_go.State.Queue.StartJobsAsync()
+ gui_go.State.Queue = job.CreateJobQueue()
+ for j, err := range fails {
+ if err == gerr.ErrAuthenticationRequired {
+ j.Repository.SetWorkStatus(git.Paused)
+ gui_go.State.FailoverQueue.AddJob(j)
+ }
+ }
+ }(gui)
+ return nil
+}
+func (gui *Gui) submitCredentials(g *gocui.Gui, v *gocui.View) error {
+ if is, j := gui.State.FailoverQueue.IsInTheQueue(gui.getSelectedRepository()); is {
+ if j.Repository.WorkStatus() == git.Paused {
+ gui.State.FailoverQueue.RemoveFromQueue(j.Repository)
+ err := gui.openAuthenticationView(g, gui.State.Queue, j, v.Name())
if err != nil {
- if err == git.ErrAuthenticationRequired {
- // pause the job, so it will be indicated to being blocking
- job.Entity.SetState(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
+ log.Warn(err.Error())
+ return err
}
- // if queue is finished simply return from this goroutine
- if finished {
- return
+ if isnt, _ := gui.State.Queue.IsInTheQueue(j.Repository); !isnt {
+ gui.State.FailoverQueue.AddJob(j)
}
}
- }(gui, g)
+ }
return nil
}
@@ -176,11 +244,11 @@ func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error {
r := gui.getSelectedRepository()
// maybe, failed entities may be added to queue again
- if r.State().Ready {
+ if r.WorkStatus().Ready {
if err := gui.addToQueue(r); err != nil {
return err
}
- } else if r.State() == git.Queued {
+ } else if r.WorkStatus() == git.Queued {
if err := gui.removeFromQueue(r); err != nil {
return err
}
@@ -192,7 +260,7 @@ func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error {
// current state into account before adding it
func (gui *Gui) markAllRepositories(g *gocui.Gui, v *gocui.View) error {
for _, r := range gui.State.Repositories {
- if r.State().Ready {
+ if r.WorkStatus().Ready {
if err := gui.addToQueue(r); err != nil {
return err
}
@@ -207,7 +275,7 @@ func (gui *Gui) markAllRepositories(g *gocui.Gui, v *gocui.View) error {
// current state into account before removing it
func (gui *Gui) unmarkAllRepositories(g *gocui.Gui, v *gocui.View) error {
for _, r := range gui.State.Repositories {
- if r.State() == git.Queued {
+ if r.WorkStatus() == git.Queued {
if err := gui.removeFromQueue(r); err != nil {
return err
}
diff --git a/pkg/gui/sideviews.go b/gui/sideviews.go
index fcd7f13..1a910cf 100644
--- a/pkg/gui/sideviews.go
+++ b/gui/sideviews.go
@@ -3,7 +3,8 @@ package gui
import (
"fmt"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/isacikgoz/gitbatch/core/git"
"github.com/jroimartin/gocui"
)
@@ -12,29 +13,29 @@ var (
sideViews = []viewFeature{remoteViewFeature, remoteBranchViewFeature, branchViewFeature, commitViewFeature}
)
-// refreshes the side views of the application for given git.RepoEntity struct
-func (gui *Gui) renderSideViews(e *git.RepoEntity) error {
- if e == nil {
+// refreshes the side views of the application for given repository.Repository struct
+func (gui *Gui) renderSideViews(r *git.Repository) error {
+ if r == nil {
return nil
}
- if err := gui.renderRemotes(e); err != nil {
+ if err := gui.renderRemotes(r); err != nil {
return err
}
- if err := gui.renderBranch(e); err != nil {
+ if err := gui.renderBranch(r); err != nil {
return err
}
- if err := gui.renderRemoteBranches(e); err != nil {
+ if err := gui.renderRemoteBranches(r); err != nil {
return err
}
- if err := gui.renderCommits(e); err != nil {
+ if err := gui.renderCommits(r); err != nil {
return err
}
return nil
}
// updates the remotesview for given entity
-func (gui *Gui) renderRemotes(e *git.RepoEntity) error {
+func (gui *Gui) renderRemotes(r *git.Repository) error {
var err error
out, err := gui.g.View(remoteViewFeature.Name)
if err != nil {
@@ -42,16 +43,16 @@ func (gui *Gui) renderRemotes(e *git.RepoEntity) error {
}
out.Clear()
currentindex := 0
- totalRemotes := len(e.Remotes)
+ totalRemotes := len(r.Remotes)
if totalRemotes > 0 {
- for i, r := range e.Remotes {
- _, shortURL := trimRemoteURL(r.URL[0])
- if r.Name == e.Remote.Name {
+ for i, rm := range r.Remotes {
+ _, shortURL := trimRemoteURL(rm.URL[0])
+ if rm.Name == r.State.Remote.Name {
currentindex = i
- fmt.Fprintln(out, selectionIndicator+r.Name+": "+shortURL)
+ fmt.Fprintln(out, selectionIndicator+rm.Name+": "+shortURL)
continue
}
- fmt.Fprintln(out, tab+r.Name+": "+shortURL)
+ fmt.Fprintln(out, tab+rm.Name+": "+shortURL)
}
if err = gui.smartAnchorRelativeToLine(out, currentindex, totalRemotes); err != nil {
return err
@@ -61,7 +62,7 @@ func (gui *Gui) renderRemotes(e *git.RepoEntity) error {
}
// updates the remotebranchview for given entity
-func (gui *Gui) renderRemoteBranches(e *git.RepoEntity) error {
+func (gui *Gui) renderRemoteBranches(r *git.Repository) error {
var err error
out, err := gui.g.View(remoteBranchViewFeature.Name)
if err != nil {
@@ -69,15 +70,15 @@ func (gui *Gui) renderRemoteBranches(e *git.RepoEntity) error {
}
out.Clear()
currentindex := 0
- trb := len(e.Remote.Branches)
+ trb := len(r.State.Remote.Branches)
if trb > 0 {
- for i, r := range e.Remote.Branches {
- if r.Name == e.Remote.Branch.Name {
+ for i, rm := range r.State.Remote.Branches {
+ if rm.Name == r.State.Remote.Branch.Name {
currentindex = i
- fmt.Fprintln(out, selectionIndicator+r.Name)
+ fmt.Fprintln(out, selectionIndicator+rm.Name)
continue
}
- fmt.Fprintln(out, tab+r.Name)
+ fmt.Fprintln(out, tab+rm.Name)
}
if err = gui.smartAnchorRelativeToLine(out, currentindex, trb); err != nil {
return err
@@ -87,7 +88,7 @@ func (gui *Gui) renderRemoteBranches(e *git.RepoEntity) error {
}
// updates the branchview for given entity
-func (gui *Gui) renderBranch(e *git.RepoEntity) error {
+func (gui *Gui) renderBranch(r *git.Repository) error {
var err error
out, err := gui.g.View(branchViewFeature.Name)
if err != nil {
@@ -95,9 +96,9 @@ func (gui *Gui) renderBranch(e *git.RepoEntity) error {
}
out.Clear()
currentindex := 0
- totalbranches := len(e.Branches)
- for i, b := range e.Branches {
- if b.Name == e.Branch.Name {
+ totalbranches := len(r.Branches)
+ for i, b := range r.Branches {
+ if b.Name == r.State.Branch.Name {
currentindex = i
fmt.Fprintln(out, selectionIndicator+b.Name)
continue
@@ -109,7 +110,7 @@ func (gui *Gui) renderBranch(e *git.RepoEntity) error {
}
// updates the commitsview for given entity
-func (gui *Gui) renderCommits(e *git.RepoEntity) error {
+func (gui *Gui) renderCommits(r *git.Repository) error {
var err error
out, err := gui.g.View(commitViewFeature.Name)
if err != nil {
@@ -117,9 +118,9 @@ func (gui *Gui) renderCommits(e *git.RepoEntity) error {
}
out.Clear()
currentindex := 0
- totalcommits := len(e.Commits)
- for i, c := range e.Commits {
- if c.Hash == e.Commit.Hash {
+ totalcommits := len(r.Commits)
+ for i, c := range r.Commits {
+ if c.Hash == r.State.Commit.Hash {
currentindex = i
fmt.Fprintln(out, selectionIndicator+commitLabel(c))
continue
@@ -132,22 +133,22 @@ func (gui *Gui) renderCommits(e *git.RepoEntity) error {
// cursor down variant for sideviews
func (gui *Gui) sideViewsNextItem(g *gocui.Gui, v *gocui.View) error {
var err error
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
switch viewName := v.Name(); viewName {
case remoteBranchViewFeature.Name:
- return e.Remote.NextRemoteBranch(e)
+ return r.State.Remote.NextRemoteBranch(r)
case remoteViewFeature.Name:
- return e.NextRemote()
+ return r.NextRemote()
case branchViewFeature.Name:
- if err = e.Checkout(e.NextBranch()); err != nil {
+ if err = r.Checkout(r.NextBranch()); err != nil {
err = gui.openErrorView(g, err.Error(),
"You should manually resolve this issue",
branchViewFeature.Name)
return err
}
case commitViewFeature.Name:
- e.NextCommit()
- return gui.renderCommits(e)
+ r.NextCommit()
+ return gui.renderCommits(r)
}
return err
}
@@ -155,31 +156,31 @@ func (gui *Gui) sideViewsNextItem(g *gocui.Gui, v *gocui.View) error {
// cursor up variant for sideviews
func (gui *Gui) sideViewsPreviousItem(g *gocui.Gui, v *gocui.View) error {
var err error
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
switch viewName := v.Name(); viewName {
case remoteBranchViewFeature.Name:
- return e.Remote.PreviousRemoteBranch(e)
+ return r.State.Remote.PreviousRemoteBranch(r)
case remoteViewFeature.Name:
- return e.PreviousRemote()
+ return r.PreviousRemote()
case branchViewFeature.Name:
- if err = e.Checkout(e.PreviousBranch()); err != nil {
+ if err = r.Checkout(r.PreviousBranch()); err != nil {
err = gui.openErrorView(g, err.Error(),
"You should manually resolve this issue",
branchViewFeature.Name)
return err
}
case commitViewFeature.Name:
- e.PreviousCommit()
- return gui.renderCommits(e)
+ r.PreviousCommit()
+ return gui.renderCommits(r)
}
return err
}
// basically does fetch --prune
func (gui *Gui) syncRemoteBranch(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
- return git.Fetch(e, git.FetchOptions{
- RemoteName: e.Remote.Name,
+ r := gui.getSelectedRepository()
+ return command.Fetch(r, command.FetchOptions{
+ RemoteName: r.State.Remote.Name,
Prune: true,
})
}
@@ -188,14 +189,14 @@ func (gui *Gui) syncRemoteBranch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) setUpstreamToBranch(g *gocui.Gui, v *gocui.View) error {
maxX, maxY := g.Size()
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
v, err := g.SetView(confirmationViewFeature.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, "branch."+e.Branch.Name+"."+"remote"+"="+e.Remote.Name)
- fmt.Fprintln(v, "branch."+e.Branch.Name+"."+"merge"+"="+e.Branch.Reference.Name().String())
+ fmt.Fprintln(v, "branch."+r.State.Branch.Name+"."+"remote"+"="+r.State.Remote.Name)
+ fmt.Fprintln(v, "branch."+r.State.Branch.Name+"."+"merge"+"="+r.State.Branch.Reference.Name().String())
}
return gui.focusToView(confirmationViewFeature.Name)
}
@@ -203,22 +204,22 @@ func (gui *Gui) setUpstreamToBranch(g *gocui.Gui, v *gocui.View) error {
// add config for upstream merge
func (gui *Gui) confirmSetUpstreamToBranch(g *gocui.Gui, v *gocui.View) error {
var err error
- e := gui.getSelectedRepository()
- if err = git.AddConfig(e, git.ConfigOptions{
- Section: "branch." + e.Branch.Name,
+ r := gui.getSelectedRepository()
+ if err = command.AddConfig(r, command.ConfigOptions{
+ Section: "branch." + r.State.Branch.Name,
Option: "remote",
- Site: git.ConfigSiteLocal,
- }, e.Remote.Name); err != nil {
+ Site: command.ConfigSiteLocal,
+ }, r.State.Remote.Name); err != nil {
return err
}
- if err = git.AddConfig(e, git.ConfigOptions{
- Section: "branch." + e.Branch.Name,
+ if err = command.AddConfig(r, command.ConfigOptions{
+ Section: "branch." + r.State.Branch.Name,
Option: "merge",
- Site: git.ConfigSiteLocal,
- }, e.Branch.Reference.Name().String()); err != nil {
+ Site: command.ConfigSiteLocal,
+ }, r.State.Branch.Reference.Name().String()); err != nil {
return err
}
- e.Refresh()
+ r.Refresh()
return gui.closeConfirmationView(g, v)
}
diff --git a/pkg/gui/stagedview.go b/gui/stagedview.go
index a1c9737..95b474a 100644
--- a/pkg/gui/stagedview.go
+++ b/gui/stagedview.go
@@ -3,7 +3,7 @@ package gui
import (
"fmt"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/command"
"github.com/jroimartin/gocui"
)
@@ -25,32 +25,32 @@ func (gui *Gui) openStageView(g *gocui.Gui) error {
}
func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
_, cy := v.Cursor()
_, oy := v.Origin()
if len(stagedFiles) <= 0 || len(stagedFiles) <= cy+oy {
return nil
}
- if err := git.Reset(e, stagedFiles[cy+oy], git.ResetOptions{}); err != nil {
+ if err := command.Reset(r, stagedFiles[cy+oy], command.ResetOptions{}); err != nil {
return err
}
- return refreshAllStatusView(g, e, true)
+ return refreshAllStatusView(g, r, true)
}
func (gui *Gui) resetAllChanges(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
- ref, err := e.Repository.Head()
+ r := gui.getSelectedRepository()
+ ref, err := r.Repo.Head()
if err != nil {
return err
}
- if err := git.ResetAll(e, git.ResetOptions{
+ if err := command.ResetAll(r, command.ResetOptions{
Hash: ref.Hash().String(),
- Rtype: git.ResetMixed,
+ Rtype: command.ResetMixed,
}); err != nil {
return err
}
- return refreshAllStatusView(g, e, true)
+ return refreshAllStatusView(g, r, true)
}
// refresh the main view and re-render the repository representations
diff --git a/pkg/gui/stashview.go b/gui/stashview.go
index 0afc983..f1ecc48 100644
--- a/pkg/gui/stashview.go
+++ b/gui/stashview.go
@@ -3,7 +3,7 @@ package gui
import (
"fmt"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/git"
"github.com/jroimartin/gocui"
)
@@ -18,15 +18,15 @@ func (gui *Gui) openStashView(g *gocui.Gui) error {
}
v.Title = stashViewFeature.Title
}
- e := gui.getSelectedRepository()
- err = refreshStashView(g, e)
+ r := gui.getSelectedRepository()
+ err = refreshStashView(g, r)
return err
}
//
func (gui *Gui) stashChanges(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
- output, err := e.Stash()
+ r := gui.getSelectedRepository()
+ output, err := r.Stash()
if err != nil {
if err = gui.openErrorView(g, output,
"You should manually resolve this issue",
@@ -34,19 +34,19 @@ func (gui *Gui) stashChanges(g *gocui.Gui, v *gocui.View) error {
return err
}
}
- err = refreshAllStatusView(g, e, true)
+ err = refreshAllStatusView(g, r, true)
return err
}
//
func (gui *Gui) popStash(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
_, oy := v.Origin()
_, cy := v.Cursor()
- if len(e.Stasheds) <= 0 {
+ if len(r.Stasheds) <= 0 {
return nil
}
- stashedItem := e.Stasheds[oy+cy]
+ stashedItem := r.Stasheds[oy+cy]
output, err := stashedItem.Pop()
if err != nil {
if err = gui.openErrorView(g, output,
@@ -56,15 +56,15 @@ func (gui *Gui) popStash(g *gocui.Gui, v *gocui.View) error {
}
}
// since the pop is a func of stashed item, we need to refresh entity here
- if err := e.Refresh(); err != nil {
+ if err := r.Refresh(); err != nil {
return err
}
- return refreshAllStatusView(g, e, true)
+ return refreshAllStatusView(g, r, true)
}
// refresh the main view and re-render the repository representations
-func refreshStashView(g *gocui.Gui, e *git.RepoEntity) error {
+func refreshStashView(g *gocui.Gui, r *git.Repository) error {
stashView, err := g.View(stashViewFeature.Name)
if err != nil {
return err
@@ -72,7 +72,7 @@ func refreshStashView(g *gocui.Gui, e *git.RepoEntity) error {
stashView.Clear()
_, cy := stashView.Cursor()
_, oy := stashView.Origin()
- stashedItems := e.Stasheds
+ stashedItems := r.Stasheds
for i, stashedItem := range stashedItems {
var prefix string
if i == cy+oy {
diff --git a/pkg/gui/statusview.go b/gui/statusview.go
index b66d49f..7b92e6a 100644
--- a/pkg/gui/statusview.go
+++ b/gui/statusview.go
@@ -3,7 +3,8 @@ package gui
import (
"fmt"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/isacikgoz/gitbatch/core/git"
"github.com/jroimartin/gocui"
)
@@ -16,13 +17,13 @@ var (
statusViews = []viewFeature{stageViewFeature, unstageViewFeature, stashViewFeature}
commitMesageReturnView string
- stagedFiles []*git.File
- unstagedFiles []*git.File
+ stagedFiles []*command.File
+ unstagedFiles []*command.File
)
// open the status layout
func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error {
- if err := reloadFiles(gui.getSelectedRepository()); err != nil {
+ if err := populateFileLists(gui.getSelectedRepository()); err != nil {
return err
}
gui.openStatusHeaderView(g)
@@ -32,11 +33,6 @@ func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error {
return nil
}
-func reloadFiles(e *git.RepoEntity) error {
- _, _, err := populateFileLists(e)
- return err
-}
-
// focus to next view
func (gui *Gui) nextStatusView(g *gocui.Gui, v *gocui.View) error {
return gui.nextViewOfGroup(g, v, statusViews)
@@ -68,8 +64,8 @@ func (gui *Gui) statusCursorDown(g *gocui.Gui, v *gocui.View) error {
return err
}
}
- e := gui.getSelectedRepository()
- return refreshStatusView(v.Name(), g, e, false)
+ r := gui.getSelectedRepository()
+ return refreshStatusView(v.Name(), g, r, false)
}
// moves the cursor upwards for the main view
@@ -85,20 +81,20 @@ func (gui *Gui) statusCursorUp(g *gocui.Gui, v *gocui.View) error {
return err
}
}
- e := gui.getSelectedRepository()
- return refreshStatusView(v.Name(), g, e, false)
+ r := gui.getSelectedRepository()
+ return refreshStatusView(v.Name(), g, r, false)
}
// header og the status layout
func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error {
maxX, _ := g.Size()
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
v, err := g.SetView(statusHeaderViewFeature.Name, 6, 2, maxX-6, 4)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
- fmt.Fprintln(v, e.AbsPath)
+ fmt.Fprintln(v, r.AbsPath)
// v.Frame = false
v.Wrap = true
}
@@ -115,32 +111,34 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error {
if err := g.DeleteView(statusHeaderViewFeature.Name); err != nil {
return err
}
- stagedFiles = make([]*git.File, 0)
- unstagedFiles = make([]*git.File, 0)
+ stagedFiles = make([]*command.File, 0)
+ unstagedFiles = make([]*command.File, 0)
return gui.closeViewCleanup(mainViewFeature.Name)
}
// generate file lists by git status command
-func populateFileLists(e *git.RepoEntity) (staged, unstaged []*git.File, err error) {
- files, err := git.Status(e)
+func populateFileLists(r *git.Repository) error {
+ files, err := command.Status(r)
if err != nil {
- return nil, nil, err
+ return err
}
+ stagedFiles = make([]*command.File, 0)
+ unstagedFiles = make([]*command.File, 0)
for _, file := range files {
- if file.X != git.StatusNotupdated && file.X != git.StatusUntracked && file.X != git.StatusIgnored && file.X != git.StatusUpdated {
- staged = append(staged, file)
+ if file.X != command.StatusNotupdated && file.X != command.StatusUntracked && file.X != command.StatusIgnored && file.X != command.StatusUpdated {
+ stagedFiles = append(stagedFiles, file)
}
- if file.Y != git.StatusNotupdated {
- unstaged = append(unstaged, file)
+ if file.Y != command.StatusNotupdated {
+ unstagedFiles = append(unstagedFiles, file)
}
}
- return staged, unstaged, err
+ return err
}
-func refreshStatusView(viewName string, g *gocui.Gui, e *git.RepoEntity, reload bool) error {
+func refreshStatusView(viewName string, g *gocui.Gui, r *git.Repository, reload bool) error {
if reload {
- reloadFiles(e)
+ populateFileLists(r)
}
var err error
switch viewName {
@@ -149,14 +147,14 @@ func refreshStatusView(viewName string, g *gocui.Gui, e *git.RepoEntity, reload
case unstageViewFeature.Name:
err = refreshUnstagedView(g)
case stashViewFeature.Name:
- err = refreshStashView(g, e)
+ err = refreshStashView(g, r)
}
return err
}
-func refreshAllStatusView(g *gocui.Gui, e *git.RepoEntity, reload bool) error {
+func refreshAllStatusView(g *gocui.Gui, r *git.Repository, reload bool) error {
for _, v := range statusViews {
- if err := refreshStatusView(v.Name, g, e, reload); err != nil {
+ if err := refreshStatusView(v.Name, g, r, reload); err != nil {
return err
}
}
diff --git a/pkg/gui/util-textstyle.go b/gui/text-renderer.go
index 95c8de7..d699acf 100644
--- a/pkg/gui/util-textstyle.go
+++ b/gui/text-renderer.go
@@ -5,7 +5,8 @@ import (
"strings"
"github.com/fatih/color"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/isacikgoz/gitbatch/core/job"
)
var (
@@ -50,32 +51,33 @@ var (
// this function handles the render and representation of the repository
// TODO: cleanup is required, right now it looks too complicated
-func (gui *Gui) repositoryLabel(e *git.RepoEntity) string {
+func (gui *Gui) repositoryLabel(r *git.Repository) string {
var prefix string
- if e.Branch.Pushables != "?" {
- prefix = prefix + pushable + ws + e.Branch.Pushables +
- ws + pullable + ws + e.Branch.Pullables
+ b := r.State.Branch
+ if b.Pushables != "?" {
+ prefix = prefix + pushable + ws + b.Pushables +
+ ws + pullable + ws + b.Pullables
} else {
- prefix = prefix + pushable + ws + yellow.Sprint(e.Branch.Pushables) +
- ws + pullable + ws + yellow.Sprint(e.Branch.Pullables)
+ prefix = prefix + pushable + ws + yellow.Sprint(b.Pushables) +
+ ws + pullable + ws + yellow.Sprint(b.Pullables)
}
var repoName string
- se := gui.getSelectedRepository()
- if se == e {
+ sr := gui.getSelectedRepository()
+ if sr == r {
prefix = prefix + selectionIndicator
- repoName = green.Sprint(e.Name)
+ repoName = green.Sprint(r.Name)
} else {
prefix = prefix + ws
- repoName = e.Name
+ repoName = r.Name
}
// some branch names can be really long, in that times I hope the first
// characters are important and meaningful
- branch := adjustTextLength(e.Branch.Name, maxBranchLength)
+ branch := adjustTextLength(b.Name, maxBranchLength)
prefix = prefix + string(cyan.Sprint(branch))
- if !e.Branch.Clean {
+ if !b.Clean {
prefix = prefix + ws + dirty + ws
} else {
prefix = prefix + ws
@@ -83,32 +85,31 @@ func (gui *Gui) repositoryLabel(e *git.RepoEntity) string {
var suffix string
// rendering the satus according to repository's state
- if e.State() == git.Queued {
- if inQueue, ty := gui.State.Queue.IsInTheQueue(e); inQueue {
- switch mode := ty; mode {
- case git.FetchJob:
+ if r.WorkStatus() == git.Queued {
+ if inQueue, j := gui.State.Queue.IsInTheQueue(r); inQueue {
+ switch mode := j.JobType; mode {
+ case job.FetchJob:
suffix = blue.Sprint(queuedSymbol)
- case git.PullJob:
+ case job.PullJob:
suffix = magenta.Sprint(queuedSymbol)
- case git.MergeJob:
+ case job.MergeJob:
suffix = cyan.Sprint(queuedSymbol)
default:
suffix = green.Sprint(queuedSymbol)
}
}
return prefix + repoName + ws + suffix
- } else if e.State() == git.Working {
+ } else if r.WorkStatus() == git.Working {
// TODO: maybe the type of the job can be written while its working?
return prefix + repoName + ws + green.Sprint(workingSymbol)
- } else if e.State() == git.Success {
+ } else if r.WorkStatus() == git.Success {
return prefix + repoName + ws + green.Sprint(successSymbol)
- } else if e.State() == git.Paused {
- return prefix + repoName + ws + yellow.Sprint(pauseSymbol)
- } else if e.State() == git.Fail {
- return prefix + repoName + ws + red.Sprint(failSymbol)
- } else {
- return prefix + repoName
+ } else if r.WorkStatus() == git.Paused {
+ return prefix + repoName + ws + yellow.Sprint("authentication required (u)")
+ } else if r.WorkStatus() == git.Fail {
+ return prefix + repoName + ws + red.Sprint(failSymbol) + ws + red.Sprint(r.State.Message)
}
+ return prefix + repoName
}
func commitLabel(c *git.Commit) string {
@@ -119,7 +120,11 @@ func commitLabel(c *git.Commit) string {
case git.LocalCommit:
body = blue.Sprint(c.Hash[:hashLength]) + " " + c.Message
case git.RemoteCommit:
- body = yellow.Sprint(c.Hash[:hashLength]) + " " + c.Message
+ if len(c.Hash) > hashLength {
+ body = yellow.Sprint(c.Hash[:hashLength]) + " " + c.Message
+ } else {
+ body = yellow.Sprint(c.Hash[:len(c.Hash)]) + " " + c.Message
+ }
default:
body = c.Hash[:hashLength] + " " + c.Message
}
diff --git a/pkg/gui/unstagedview.go b/gui/unstagedview.go
index 8d152c9..f5b6e43 100644
--- a/pkg/gui/unstagedview.go
+++ b/gui/unstagedview.go
@@ -3,7 +3,7 @@ package gui
import (
"fmt"
- "github.com/isacikgoz/gitbatch/pkg/git"
+ "github.com/isacikgoz/gitbatch/core/command"
"github.com/jroimartin/gocui"
)
@@ -23,27 +23,27 @@ func (gui *Gui) openUnStagedView(g *gocui.Gui) error {
}
func (gui *Gui) addChanges(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
+ r := gui.getSelectedRepository()
_, cy := v.Cursor()
_, oy := v.Origin()
if len(unstagedFiles) <= 0 || len(unstagedFiles) < cy+oy {
return nil
}
- if err := git.Add(e, unstagedFiles[cy+oy], git.AddOptions{}); err != nil {
+ if err := command.Add(r, unstagedFiles[cy+oy], command.AddOptions{}); err != nil {
return err
}
- return refreshAllStatusView(g, e, true)
+ return refreshAllStatusView(g, r, true)
}
func (gui *Gui) addAllChanges(g *gocui.Gui, v *gocui.View) error {
- e := gui.getSelectedRepository()
- if err := git.AddAll(e, git.AddOptions{}); err != nil {
+ r := gui.getSelectedRepository()
+ if err := command.AddAll(r, command.AddOptions{}); err != nil {
return err
}
- return refreshAllStatusView(g, e, true)
+ return refreshAllStatusView(g, r, true)
}
// refresh the main view and re-render the repository representations
diff --git a/main.go b/main.go
index 7ef48b8..ebe6583 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,11 @@
package main
import (
- "github.com/isacikgoz/gitbatch/pkg/app"
+ "os"
+
+ "github.com/isacikgoz/gitbatch/app"
log "github.com/sirupsen/logrus"
- "gopkg.in/alecthomas/kingpin.v2"
+ kingpin "gopkg.in/alecthomas/kingpin.v2"
)
var (
@@ -15,10 +17,19 @@ var (
)
func main() {
- kingpin.Version("gitbatch version 0.2.1")
+ kingpin.Version("gitbatch version 0.3.0")
// parse the command line flag and options
kingpin.Parse()
+ if err := run(); err != nil {
+ log.WithFields(log.Fields{
+ "error": err.Error(),
+ }).Error("Application quitted with an unhandled error.")
+ os.Exit(1)
+ }
+}
+
+func run() error {
// set the app
app, err := app.Setup(&app.SetupConfig{
Directories: *dirs,
@@ -28,15 +39,12 @@ func main() {
Mode: *mode,
})
if err != nil {
- log.Fatal(err)
- }
-
- // execute the app and wait its routine
- err = app.Gui.Run()
- if err != nil {
- log.Fatal(err)
+ return err
}
// good citizens always clean up their mess
defer app.Close()
+
+ // execute the app and wait its routine
+ return app.Gui.Run()
}
diff --git a/pkg/git/branch.go b/pkg/git/branch.go
deleted file mode 100644
index d3b2425..0000000
--- a/pkg/git/branch.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package git
-
-import (
- "strconv"
- "strings"
-
- "github.com/isacikgoz/gitbatch/pkg/helpers"
- log "github.com/sirupsen/logrus"
- "gopkg.in/src-d/go-git.v4"
- "gopkg.in/src-d/go-git.v4/plumbing"
-)
-
-// Branch is the wrapper of go-git's Reference struct. In addition to that, it
-// also holds name of the branch, pullable and pushable commit count from the
-// branchs' upstream. It also tracks if the repository has unstaged or uncommit-
-// ed changes
-type Branch struct {
- Name string
- Reference *plumbing.Reference
- Pushables string
- Pullables string
- Clean bool
-}
-
-// search for branches in go-git way. It is useful to do so that checkout and
-// checkout error handling can be handled by code rather than struggling with
-// git cammand and its output
-func (e *RepoEntity) loadLocalBranches() error {
- lbs := make([]*Branch, 0)
- bs, err := e.Repository.Branches()
- if err != nil {
- log.Warn("Cannot load branches " + err.Error())
- return err
- }
- defer bs.Close()
- headRef, err := e.Repository.Head()
- if err != nil {
- return err
- }
- var branchFound bool
- bs.ForEach(func(b *plumbing.Reference) error {
- if b.Type() == plumbing.HashReference {
- var push, pull string
- pushables, err := RevList(e, RevListOptions{
- Ref1: "@{u}",
- Ref2: "HEAD",
- })
- if err != nil {
- push = pushables[0]
- } else {
- push = strconv.Itoa(len(pushables))
- }
- pullables, err := RevList(e, RevListOptions{
- Ref1: "HEAD",
- Ref2: "@{u}",
- })
- if err != nil {
- pull = pullables[0]
- } else {
- pull = strconv.Itoa(len(pullables))
- }
- clean := e.isClean()
- branch := &Branch{
- Name: b.Name().Short(),
- Reference: b,
- Pushables: push,
- Pullables: pull,
- Clean: clean,
- }
- if b.Name() == headRef.Name() {
- e.Branch = branch
- branchFound = true
- }
- lbs = append(lbs, branch)
- }
- return nil
- })
- if !branchFound {
- branch := &Branch{
- Name: headRef.Hash().String(),
- Reference: headRef,
- Pushables: "?",
- Pullables: "?",
- Clean: e.isClean(),
- }
- lbs = append(lbs, branch)
- e.Branch = branch
- }
- e.Branches = lbs
- return err
-}
-
-// NextBranch checkouts the next branch
-func (e *RepoEntity) NextBranch() *Branch {
- return e.Branches[(e.currentBranchIndex()+1)%len(e.Branches)]
-}
-
-// PreviousBranch checkouts the previous branch
-func (e *RepoEntity) PreviousBranch() *Branch {
- return e.Branches[(len(e.Branches)+e.currentBranchIndex()-1)%len(e.Branches)]
-}
-
-// returns the active branch index
-func (e *RepoEntity) currentBranchIndex() int {
- bix := 0
- for i, lbs := range e.Branches {
- if lbs.Name == e.Branch.Name {
- bix = i
- }
- }
- return bix
-}
-
-// Checkout to given branch. If any errors occur, the method returns it instead
-// of returning nil
-func (e *RepoEntity) Checkout(branch *Branch) error {
- if branch.Name == e.Branch.Name {
- return nil
- }
-
- w, err := e.Repository.Worktree()
- if err != nil {
- log.Warn("Cannot get work tree " + err.Error())
- return err
- }
- if err = w.Checkout(&git.CheckoutOptions{
- Branch: branch.Reference.Name(),
- }); err != nil {
- log.Warn("Cannot checkout " + err.Error())
- return err
- }
-
- // make this conditional on global scale
- // we don't care if this function returns an error
- e.Remote.SyncBranches(branch.Name)
-
- return e.Refresh()
-}
-
-// checking the branch if it has any changes from its head revision. Initially
-// I implemented this with go-git but it was incredibly slow and there is also
-// an issue about it: https://github.com/src-d/go-git/issues/844
-func (e *RepoEntity) isClean() bool {
- s := e.StatusWithGit()
- s = helpers.TrimTrailingNewline(s)
- if s != "?" {
- vs := strings.Split(s, "\n")
- line := vs[len(vs)-1]
- // earlier versions of git returns "working directory clean" instead of
- //"working tree clean" message
- if strings.Contains(line, "working tree clean") ||
- strings.Contains(line, "working directory clean") {
- return true
- }
- }
- return false
-}
diff --git a/pkg/git/cmd-rev-list.go b/pkg/git/cmd-rev-list.go
deleted file mode 100644
index e68e82a..0000000
--- a/pkg/git/cmd-rev-list.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package git
-
-import (
- "strings"
-
- log "github.com/sirupsen/logrus"
-)
-
-var revlistCommand = "rev-list"
-var hashLength = 40
-
-// RevListOptions defines the rules of rev-list func
-type RevListOptions struct {
- // Ref1 is the first reference hash to link
- Ref1 string
- // Ref2 is the second reference hash to link
- Ref2 string
-}
-
-// RevList returns the commit hashes that are links from the given commit(s).
-// The output is given in reverse chronological order by default.
-func RevList(e *RepoEntity, options RevListOptions) ([]string, error) {
- args := make([]string, 0)
- args = append(args, revlistCommand)
- if len(options.Ref1) > 0 && len(options.Ref2) > 0 {
- arg1 := options.Ref1 + ".." + options.Ref2
- args = append(args, arg1)
- }
- out, err := GenericGitCommandWithOutput(e.AbsPath, args)
- if err != nil {
- log.Warn("Error while rev-list command")
- return []string{out}, err
- }
- hashes := strings.Split(out, "\n")
- for _, hash := range hashes {
- if len(hash) != hashLength {
- return make([]string, 0), nil
- }
- break
- }
- return hashes, nil
-}
diff --git a/pkg/git/cmd.go b/pkg/git/cmd.go
deleted file mode 100644
index a84f489..0000000
--- a/pkg/git/cmd.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package git
-
-import (
- "github.com/isacikgoz/gitbatch/pkg/helpers"
-)
-
-// GenericGitCommand runs any git command without expecting output
-func GenericGitCommand(repoPath string, args []string) error {
- _, err := helpers.RunCommandWithOutput(repoPath, "git", args)
- if err != nil {
- return err
- }
- return nil
-}
-
-// GenericGitCommandWithOutput runs any git command with returning output
-func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) {
- out, err := helpers.RunCommandWithOutput(repoPath, "git", args)
- if err != nil {
- return "?", err
- }
- return helpers.TrimTrailingNewline(out), nil
-}
-
-// GenericGitCommandWithErrorOutput runs any git command with returning output
-func GenericGitCommandWithErrorOutput(repoPath string, args []string) (string, error) {
- out, err := helpers.RunCommandWithOutput(repoPath, "git", args)
- if err != nil {
- return helpers.TrimTrailingNewline(out), err
- }
- return helpers.TrimTrailingNewline(out), nil
-}
-
-// GitShow is conventional git show command without any argument
-func GitShow(repoPath, hash string) string {
- args := []string{"show", hash}
- diff, err := helpers.RunCommandWithOutput(repoPath, "git", args)
- if err != nil {
- return "?"
- }
- return diff
-}
-
-// GitShowEmail gets author's e-mail with git show command
-func GitShowEmail(repoPath, hash string) string {
- args := []string{"show", "--quiet", "--pretty=format:%ae", hash}
- diff, err := helpers.RunCommandWithOutput(repoPath, "git", args)
- if err != nil {
- return "?"
- }
- return diff
-}
-
-// GitShowBody gets body of the commit with git show
-func GitShowBody(repoPath, hash string) string {
- args := []string{"show", "--quiet", "--pretty=format:%B", hash}
- diff, err := helpers.RunCommandWithOutput(repoPath, "git", args)
- if err != nil {
- return err.Error()
- }
- return diff
-}
-
-// GitShowDate gets commit's date with git show as string
-func GitShowDate(repoPath, hash string) string {
- args := []string{"show", "--quiet", "--pretty=format:%ai", hash}
- diff, err := helpers.RunCommandWithOutput(repoPath, "git", args)
- if err != nil {
- return "?"
- }
- return diff
-}
-
-// StatusWithGit returns the plaintext short status of the repo
-func (e *RepoEntity) StatusWithGit() string {
- args := []string{"status"}
- status, err := helpers.RunCommandWithOutput(e.AbsPath, "git", args)
- if err != nil {
- return "?"
- }
- return status
-}
diff --git a/pkg/git/job.go b/pkg/git/job.go
deleted file mode 100644
index 85c28f3..0000000
--- a/pkg/git/job.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package git
-
-// 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 (
- // FetchJob is wrapper of git fetch command
- FetchJob JobType = "fetch"
-
- // PullJob is wrapper of git pull command
- PullJob JobType = "pull"
-
- // MergeJob is wrapper of git merge command
- MergeJob JobType = "merge"
-)
-
-// starts the job
-func (j *Job) start() error {
- j.Entity.SetState(Working)
- // TODO: Handle errors?
- // TOOD: Better implementation required
- switch mode := j.JobType; mode {
- case FetchJob:
- var opts FetchOptions
- if j.Options != nil {
- opts = j.Options.(FetchOptions)
- } else {
- opts = FetchOptions{
- RemoteName: j.Entity.Remote.Name,
- }
- }
- if err := Fetch(j.Entity, opts); err != nil {
- j.Entity.SetState(Fail)
- return err
- }
- case PullJob:
- var opts PullOptions
- if j.Options != nil {
- opts = j.Options.(PullOptions)
- } else {
- opts = PullOptions{
- RemoteName: j.Entity.Remote.Name,
- }
- }
- if err := Pull(j.Entity, opts); err != nil {
- j.Entity.SetState(Fail)
- return err
- }
- case MergeJob:
- if err := Merge(j.Entity, MergeOptions{
- BranchName: j.Entity.Remote.Branch.Name,
- }); err != nil {
- j.Entity.SetState(Fail)
- return nil
- }
- default:
- j.Entity.SetState(Available)
- return nil
- }
- return nil
-}
diff --git a/pkg/git/util-errors.go b/pkg/git/util-errors.go
deleted file mode 100644
index 8621cbc..0000000
--- a/pkg/git/util-errors.go
+++ /dev/null
@@ -1,24 +0,0 @@
-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/util-load.go b/pkg/git/util-load.go
deleted file mode 100644
index bf19de8..0000000
--- a/pkg/git/util-load.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package git
-
-import (
- log "github.com/sirupsen/logrus"
-
- "errors"
- "sync"
-)
-
-// LoadRepositoryEntities initializes the go-git's repository obejcts with given
-// slice of paths. since this job is done parallel, the order of the directories
-// is not kept
-func LoadRepositoryEntities(directories []string) (entities []*RepoEntity, err error) {
- entities = make([]*RepoEntity, 0)
-
- var wg sync.WaitGroup
- var mu sync.Mutex
-
- for _, dir := range directories {
- // increment wait counter by one because we run a single goroutine
- // below
- wg.Add(1)
- go func(d string) {
- // decrement the wait counter by one, we call it in a defer so it's
- // called at the end of this goroutine
- defer wg.Done()
- entity, err := InitializeRepo(d)
- if err != nil {
- log.WithFields(log.Fields{
- "directory": d,
- }).Trace("Cannot load git repository.")
- return
- }
- // lock so we don't get a race if multiple go routines try to add
- // to the same entities
- mu.Lock()
- entities = append(entities, entity)
- mu.Unlock()
- }(dir)
- }
- // wait until the wait counter is zero, this happens if all goroutines have
- // finished
- wg.Wait()
- if len(entities) == 0 {
- return entities, errors.New("There are no git repositories at given path(s)")
- }
- return entities, nil
-}