From a4a29171fb11c98432ead8e72bc0948a549d9979 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Thu, 13 Dec 2018 02:12:03 +0300 Subject: login implementation stablized, some renaming, some refactoring and updated readme --- README.md | 19 ++---- pkg/git/add.go | 55 --------------- pkg/git/cmd-add.go | 55 +++++++++++++++ pkg/git/cmd-fetch.go | 146 ++++++++++++++++++++++++++++++++++++++++ pkg/git/cmd-merge.go | 39 +++++++++++ pkg/git/cmd-reset.go | 55 +++++++++++++++ pkg/git/cmd-rev-list.go | 42 ++++++++++++ pkg/git/cmd-stash.go | 106 +++++++++++++++++++++++++++++ pkg/git/cmd-status.go | 91 +++++++++++++++++++++++++ pkg/git/cmd.go | 82 ++++++++++++++++++++++ pkg/git/commands.go | 82 ---------------------- pkg/git/errors.go | 24 ------- pkg/git/fetch.go | 142 -------------------------------------- pkg/git/job-queue.go | 78 +++++++++++++++++++++ pkg/git/job.go | 26 ++++--- pkg/git/load.go | 48 ------------- pkg/git/merge.go | 39 ----------- pkg/git/queue.go | 78 --------------------- pkg/git/repository-sort.go | 59 ---------------- pkg/git/repository.go | 6 +- pkg/git/reset.go | 55 --------------- pkg/git/rev-list.go | 42 ------------ pkg/git/stash.go | 106 ----------------------------- pkg/git/status.go | 91 ------------------------- pkg/git/util-errors.go | 24 +++++++ pkg/git/util-load.go | 48 +++++++++++++ pkg/git/util-repository-sort.go | 59 ++++++++++++++++ pkg/gui/authenticationview.go | 81 ++++++++++++---------- pkg/gui/cheatsheet.go | 12 +--- pkg/gui/diffview.go | 9 +-- pkg/gui/errorview.go | 12 +--- pkg/gui/gui-util.go | 14 ++++ pkg/gui/keybindings.go | 4 +- pkg/gui/mainview.go | 3 +- pkg/gui/queuehandler.go | 8 ++- pkg/gui/stagedview.go | 6 +- pkg/gui/statusview.go | 27 ++------ pkg/gui/textstyle.go | 5 ++ 38 files changed, 942 insertions(+), 936 deletions(-) delete mode 100644 pkg/git/add.go create mode 100644 pkg/git/cmd-add.go create mode 100644 pkg/git/cmd-fetch.go create mode 100644 pkg/git/cmd-merge.go create mode 100644 pkg/git/cmd-reset.go create mode 100644 pkg/git/cmd-rev-list.go create mode 100644 pkg/git/cmd-stash.go create mode 100644 pkg/git/cmd-status.go create mode 100644 pkg/git/cmd.go delete mode 100644 pkg/git/commands.go delete mode 100644 pkg/git/errors.go delete mode 100644 pkg/git/fetch.go create mode 100644 pkg/git/job-queue.go delete mode 100644 pkg/git/load.go delete mode 100644 pkg/git/merge.go delete mode 100644 pkg/git/queue.go delete mode 100644 pkg/git/repository-sort.go delete mode 100644 pkg/git/reset.go delete mode 100644 pkg/git/rev-list.go delete mode 100644 pkg/git/stash.go delete mode 100644 pkg/git/status.go create mode 100644 pkg/git/util-errors.go create mode 100644 pkg/git/util-load.go create mode 100644 pkg/git/util-repository-sort.go diff --git a/README.md b/README.md index fcfef40..7719c88 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@ [![Build Status](https://travis-ci.com/isacikgoz/gitbatch.svg?branch=master)](https://travis-ci.com/isacikgoz/gitbatch) [![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/isacikgoz/gitbatch)](https://goreportcard.com/report/github.com/isacikgoz/gitbatch) ## gitbatch -This tool has been built to make your local repositories synchronized with remotes easily. I love lazygit, with that inspiration, decided to build this project to be even more lazy; Since my daily work is tied to many repositories I often end up walking on many directories and manually pulling updates etc. To make this routine faster, I created a simple tool to handle this job. I really enjoy working on this project and I hope it will be a useful tool. - -**Disclaimer** -- Unauthenticated repositories are **not supported** so using ssh is recommended if you need to authenticate to fetch/pull - - [Connecting to GitHub with SSH](https://help.github.com/articles/connecting-to-github-with-ssh/) - - [GitLab and SSH keys](https://docs.gitlab.com/ee/ssh/) - - [BitBucket Set up an SSH key](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-git-728138079.html) -- Feedbacks are welcome. For now, known issues are: - - At very low probability app fails to load repositories, try again it will load next time (multithreading problem). - - Colors vary to your terminal theme colors, so if the contrast is not enough on some color decisions; discussions are welcome. +This tool is beening built to make your local repositories synchronized with remotes easily. Although the focus is batch jobs, you can still do de facto micro management of your git repositories (e.g *add/reset, stash, commit etc.*) Here is the screencast of the app: [![asciicast](https://asciinema.org/a/eXgXpzZfuHxMpZqGMVODUipyc.svg)](https://asciinema.org/a/eXgXpzZfuHxMpZqGMVODUipyc) ## Installation -For now, installation requires golang compiler and minimum golang 1.10 is recommended. +For now, installation requires golang compiler and minimum golang 1.10 is recommended. (binary distribution will be provided on minimum viable product) - If you don't have golang installed refer to [golang.org](https://golang.org/dl/). - You should have $GOPATH env variable set and your $PATH should include $GOPATH/bin to run app from anywhere. @@ -42,9 +33,11 @@ For more information; ## Further goals - add testing - full src-d/go-git integration (*having some performance issues*) -- resolve authentication issues - add commit and maybe push? +## Known issues +Please refer to [Known issues page](https://github.com/isacikgoz/gitbatch/wiki/Known-issues) + ## Credits - [go-git](https://github.com/src-d/go-git) for git interface (partially) - [gocui](https://github.com/jroimartin/gocui) for user interface @@ -53,3 +46,5 @@ For more information; - [color](https://github.com/fatih/color) for colored text - [lazygit](https://github.com/jesseduffield/lazygit) as app template and reference - [kingpin](https://github.com/alecthomas/kingpin) for command-line flag&options + +I love [lazygit](https://github.com/jesseduffield/lazygit), with that inspiration, decided to build this project to be even more lazy. The rationale was; my daily work is tied to many repositories and I often end up walking on many directories and manually pulling updates etc. To make this routine faster, I created a simple tool to handle this job. I really enjoy working on this project and I hope it will be a useful tool. diff --git a/pkg/git/add.go b/pkg/git/add.go deleted file mode 100644 index 61fd587..0000000 --- a/pkg/git/add.go +++ /dev/null @@ -1,55 +0,0 @@ -package git - -import ( - "errors" - "strings" - - log "github.com/sirupsen/logrus" -) - -var addCommand = "add" - -// AddOptions defines the rules for "git add" command -type AddOptions struct { - Update bool - Force bool - DryRun bool -} - -// Add is a wrapper function for "git add" command -func (file *File) Add(option AddOptions) error { - args := make([]string, 0) - args = append(args, addCommand) - args = append(args, file.Name) - if option.Update { - args = append(args, "--update") - } - if option.Force { - args = append(args, "--force") - } - if option.DryRun { - args = append(args, "--dry-run") - } - out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) - if err != nil { - log.Warn("Error while add command") - return errors.New(out + "\n" + err.Error()) - } - return nil -} - -// AddAll function is the wrapper of "git add ." command -func (entity *RepoEntity) AddAll(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(entity.AbsPath, args) - if err != nil { - log.Warn("Error while add command") - return errors.New(out + "\n" + err.Error()) - } - return nil -} diff --git a/pkg/git/cmd-add.go b/pkg/git/cmd-add.go new file mode 100644 index 0000000..61fd587 --- /dev/null +++ b/pkg/git/cmd-add.go @@ -0,0 +1,55 @@ +package git + +import ( + "errors" + "strings" + + log "github.com/sirupsen/logrus" +) + +var addCommand = "add" + +// AddOptions defines the rules for "git add" command +type AddOptions struct { + Update bool + Force bool + DryRun bool +} + +// Add is a wrapper function for "git add" command +func (file *File) Add(option AddOptions) error { + args := make([]string, 0) + args = append(args, addCommand) + args = append(args, file.Name) + if option.Update { + args = append(args, "--update") + } + if option.Force { + args = append(args, "--force") + } + if option.DryRun { + args = append(args, "--dry-run") + } + out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} + +// AddAll function is the wrapper of "git add ." command +func (entity *RepoEntity) AddAll(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(entity.AbsPath, args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} diff --git a/pkg/git/cmd-fetch.go b/pkg/git/cmd-fetch.go new file mode 100644 index 0000000..8a8bf9c --- /dev/null +++ b/pkg/git/cmd-fetch.go @@ -0,0 +1,146 @@ +package git + +import ( + "strings" + + log "github.com/sirupsen/logrus" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/config" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/http" +) + +var ( + fetchCmdMode string + fetchTryCount int + + fetchCommand = "fetch" + fetchCmdModeLegacy = "git" + fetchCmdModeNative = "go-git" + fetchMaxTry = 1 +) + +// FetchOptions defines the rules for fetch operation +type FetchOptions struct { + // Name of the remote to fetch from. Defaults to origin. + RemoteName string + // Credentials holds the user and pswd information + Credentials Credentials + // Before fetching, remove any remote-tracking references that no longer + // exist on the remote. + Prune bool + // Show what would be done, without making any changes. + DryRun bool + // Force allows the fetch to update a local branch even when the remote + // branch does not descend from it. + Force bool + // There should be more room for authentication, tags and progress +} + +// Fetch branches refs from one or more other repositories, along with the +// objects necessary to complete their histories +func Fetch(entity *RepoEntity, options FetchOptions) (err error) { + // here we configure fetch operation + // default mode is go-git (this may be configured) + fetchCmdMode = fetchCmdModeNative + fetchTryCount = 0 + // prune and dry run is not supported from go-git yet, rely on old friend + if options.Prune || options.DryRun { + fetchCmdMode = fetchCmdModeLegacy + } + switch fetchCmdMode { + case fetchCmdModeLegacy: + err = fetchWithGit(entity, options) + return err + case fetchCmdModeNative: + // this should be the refspec as default, let's give it a try + refspec := "+" + "refs/heads/" + entity.Branch.Name + ":" + "/refs/remotes/" + entity.Remote.Branch.Name + err = fetchWithGoGit(entity, options, refspec) + return err + } + return nil +} + +// fetchWithGit is simply a bare git fetch command which is flexible +// for complex operations, but on the other hand, it ties the app to another +// tool. To avoid that, using native implementation is preferred. +func fetchWithGit(entity *RepoEntity, options FetchOptions) (err error) { + args := make([]string, 0) + args = append(args, fetchCommand) + // parse options to command line arguments + if len(options.RemoteName) > 0 { + args = append(args, options.RemoteName) + } + if options.Prune { + args = append(args, "-p") + } + if options.Force { + args = append(args, "-f") + } + if options.DryRun { + args = append(args, "--dry-run") + } + if err := GenericGitCommand(entity.AbsPath, args); err != nil { + log.Warn("Error at git command (fetch)") + return err + } + // till this step everything should be ok + err = entity.Refresh() + return err +} + +// fetchWithGoGit is the primary fetch method and refspec is the main feature. +// RefSpec is a mapping from local branches to remote references The format of +// the refspec is an optional +, followed by :, where is the +// pattern for references on the remote side and is where those references +// will be written locally. The + tells Git to update the reference even if it +// isn’t a fast-forward. +func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (err error) { + opt := &git.FetchOptions{ + RemoteName: options.RemoteName, + RefSpecs: []config.RefSpec{config.RefSpec(refspec)}, + Force: options.Force, + } + // if any credential is given, let's add it to the git.FetchOptions + if len(options.Credentials.User) > 0 { + protocol, err := entity.authProtocol(entity.Remote) + if err != nil { + return err + } + if protocol == authProtocolHttp || protocol == authProtocolHttps { + opt.Auth = &http.BasicAuth{ + Username: options.Credentials.User, + Password: options.Credentials.Password, + } + } else { + return ErrInvalidAuthMethod + } + } + + err = entity.Repository.Fetch(opt) + if err != nil { + if err == git.NoErrAlreadyUpToDate { + // Already up-to-date + log.Warn(err.Error()) + // TODO: submit a PR for this kind of error, this type of catch is lame + } else if strings.Contains(err.Error(), "couldn't find remote ref") { + // we dont have remote ref, so lets pull other things.. maybe it'd be useful + rp := entity.Remote.RefSpecs[0] + if fetchTryCount < fetchMaxTry { + fetchTryCount++ + fetchWithGoGit(entity, options, rp) + } else { + return err + } + } else if err == transport.ErrAuthenticationRequired { + log.Warn(err.Error()) + return ErrAuthenticationRequired + } else { + log.Warn(err.Error()) + return err + } + } + // till this step everything should be ok + err = entity.Refresh() + return err +} diff --git a/pkg/git/cmd-merge.go b/pkg/git/cmd-merge.go new file mode 100644 index 0000000..984bc4b --- /dev/null +++ b/pkg/git/cmd-merge.go @@ -0,0 +1,39 @@ +package git + +import ( + log "github.com/sirupsen/logrus" +) + +var mergeCommand = "merge" + +// MergeOptions defines the rules of a merge operation +type MergeOptions struct { + // Name of the branch to merge with. + BranchName string + // Be verbose. + Verbose bool + // With true do not show a diffstat at the end of the merge. + NoStat bool +} + +// Merge incorporates changes from the named commits or branches into the +// current branch +func Merge(entity *RepoEntity, options MergeOptions) error { + args := make([]string, 0) + args = append(args, mergeCommand) + if len(options.BranchName) > 0 { + args = append(args, options.BranchName) + } + if options.Verbose { + args = append(args, "-v") + } + if options.NoStat { + args = append(args, "-n") + } + if err := GenericGitCommand(entity.AbsPath, args); err != nil { + log.Warn("Error while merging") + return err + } + entity.Refresh() + return nil +} diff --git a/pkg/git/cmd-reset.go b/pkg/git/cmd-reset.go new file mode 100644 index 0000000..f93225c --- /dev/null +++ b/pkg/git/cmd-reset.go @@ -0,0 +1,55 @@ +package git + +import ( + "errors" + "strings" + + log "github.com/sirupsen/logrus" +) + +var resetCommand = "reset" + +// ResetOptions defines the rules of git reset command +type ResetOptions struct { + Hard bool + Merge bool + Keep bool +} + +// Reset is the wrapper of "git reset" command +func (file *File) Reset(option ResetOptions) error { + args := make([]string, 0) + args = append(args, resetCommand) + args = append(args, "--") + args = append(args, file.Name) + if option.Hard { + args = append(args, "--hard") + } + if option.Merge { + args = append(args, "--merge") + } + if option.Keep { + args = append(args, "--keep") + } + out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} + +// ResetAll resets the changes in a repository, should be used wise +func (entity *RepoEntity) ResetAll(option ResetOptions) error { + args := make([]string, 0) + args = append(args, resetCommand) + if option.Hard { + args = append(args, "--hard") + } + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} diff --git a/pkg/git/cmd-rev-list.go b/pkg/git/cmd-rev-list.go new file mode 100644 index 0000000..664d69f --- /dev/null +++ b/pkg/git/cmd-rev-list.go @@ -0,0 +1,42 @@ +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(entity *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(entity.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-stash.go b/pkg/git/cmd-stash.go new file mode 100644 index 0000000..9d813a6 --- /dev/null +++ b/pkg/git/cmd-stash.go @@ -0,0 +1,106 @@ +package git + +import ( + "regexp" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +var stashCommand = "stash" + +// StashedItem holds the required fields for a stashed change +type StashedItem struct { + StashID int + BranchName string + Hash string + Description string + EntityPath string +} + +func stashGet(entity *RepoEntity, option string) string { + args := make([]string, 0) + args = append(args, stashCommand) + args = append(args, option) + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while stash command") + return "?" + } + return out +} + +func (entity *RepoEntity) loadStashedItems() error { + entity.Stasheds = make([]*StashedItem, 0) + output := stashGet(entity, "list") + stashIDRegex := regexp.MustCompile(`stash@{[\d]+}:`) + stashIDRegexInt := regexp.MustCompile(`[\d]+`) + stashBranchRegex := regexp.MustCompile(`[\w]+: `) + stashHashRegex := regexp.MustCompile(`[\w]{7}`) + + stashlist := strings.Split(output, "\n") + for _, stashitem := range stashlist { + // find id + id := stashIDRegexInt.FindString(stashIDRegex.FindString(stashitem)) + i, err := strconv.Atoi(id) + if err != nil { + // probably something isn't right let's continue over this iteration + log.Trace("cannot initiate stashed item") + continue + } + // trim id section + trimmed := stashIDRegex.Split(stashitem, 2)[1] + + // find branch + stashBranchRegexMatch := stashBranchRegex.FindString(trimmed) + branchName := stashBranchRegexMatch[:len(stashBranchRegexMatch)-2] + + // trim branch section + trimmed = stashBranchRegex.Split(trimmed, 2)[1] + hash := stashHashRegex.FindString(trimmed) + + // trim hash + desc := stashHashRegex.Split(trimmed, 2)[1][1:] + + entity.Stasheds = append(entity.Stasheds, &StashedItem{ + StashID: i, + BranchName: branchName, + Hash: hash, + Description: desc, + EntityPath: entity.AbsPath, + }) + } + return nil +} + +// Stash is the wrapper of convetional "git stash" command +func (entity *RepoEntity) Stash() (output string, err error) { + args := make([]string, 0) + args = append(args, stashCommand) + + output, err = GenericGitCommandWithErrorOutput(entity.AbsPath, args) + entity.Refresh() + return output, err +} + +// Pop is the wrapper of "git stash pop" command that used for a file +func (stashedItem *StashedItem) Pop() (output string, err error) { + args := make([]string, 0) + args = append(args, stashCommand) + args = append(args, "pop") + args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}") + output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args) + return output, err +} + +// Show is the wrapper of "git stash show -p " command +func (stashedItem *StashedItem) Show() (output string, err error) { + args := make([]string, 0) + args = append(args, stashCommand) + 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 +} diff --git a/pkg/git/cmd-status.go b/pkg/git/cmd-status.go new file mode 100644 index 0000000..4cca604 --- /dev/null +++ b/pkg/git/cmd-status.go @@ -0,0 +1,91 @@ +package git + +import ( + "os" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" +) + +var statusCommand = "status" + +// File represents the status of a file in an index or work tree +type File struct { + Name string + AbsPath string + X FileStatus + Y FileStatus +} + +// FileStatus is the short representation of state of a file +type FileStatus rune + +var ( + // StatusNotupdated says file not updated + StatusNotupdated FileStatus = ' ' + // StatusModified says file is modifed + StatusModified FileStatus = 'M' + // StatusAdded says file is added to index + StatusAdded FileStatus = 'A' + // StatusDeleted says file is deleted + StatusDeleted FileStatus = 'D' + // StatusRenamed says file is renamed + StatusRenamed FileStatus = 'R' + // StatusCopied says file is copied + StatusCopied FileStatus = 'C' + // StatusUpdated says file is updated + StatusUpdated FileStatus = 'U' + // StatusUntracked says file is untraced + StatusUntracked FileStatus = '?' + // StatusIgnored says file is ignored + StatusIgnored FileStatus = '!' +) + +func shortStatus(entity *RepoEntity, option string) string { + args := make([]string, 0) + args = append(args, statusCommand) + args = append(args, option) + args = append(args, "--short") + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while status command") + return "?" + } + return out +} + +// LoadFiles function simply commands a git status and collects output in a +// structured way +func (entity *RepoEntity) LoadFiles() ([]*File, error) { + files := make([]*File, 0) + output := shortStatus(entity, "--untracked-files=all") + if len(output) == 0 { + return files, nil + } + fileslist := strings.Split(output, "\n") + for _, file := range fileslist { + x := rune(file[0]) + y := rune(file[1]) + relativePathRegex := regexp.MustCompile(`[(\w|/|.|\-)]+`) + path := relativePathRegex.FindString(file[2:]) + + files = append(files, &File{ + Name: path, + AbsPath: entity.AbsPath + string(os.PathSeparator) + path, + X: FileStatus(x), + Y: FileStatus(y), + }) + } + return files, nil +} + +// Diff is a wrapper of "git diff" command for a file to compare with HEAD rev +func (file *File) Diff() (output string, err error) { + args := make([]string, 0) + args = append(args, "diff") + args = append(args, "HEAD") + args = append(args, file.Name) + output, err = GenericGitCommandWithErrorOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + return output, err +} diff --git a/pkg/git/cmd.go b/pkg/git/cmd.go new file mode 100644 index 0000000..b78b501 --- /dev/null +++ b/pkg/git/cmd.go @@ -0,0 +1,82 @@ +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 (entity *RepoEntity) StatusWithGit() string { + args := []string{"status"} + status, err := helpers.RunCommandWithOutput(entity.AbsPath, "git", args) + if err != nil { + return "?" + } + return status +} diff --git a/pkg/git/commands.go b/pkg/git/commands.go deleted file mode 100644 index b78b501..0000000 --- a/pkg/git/commands.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 (entity *RepoEntity) StatusWithGit() string { - args := []string{"status"} - status, err := helpers.RunCommandWithOutput(entity.AbsPath, "git", args) - if err != nil { - return "?" - } - return status -} diff --git a/pkg/git/errors.go b/pkg/git/errors.go deleted file mode 100644 index 8621cbc..0000000 --- a/pkg/git/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/fetch.go b/pkg/git/fetch.go deleted file mode 100644 index 39a0277..0000000 --- a/pkg/git/fetch.go +++ /dev/null @@ -1,142 +0,0 @@ -package git - -import ( - "strings" - - log "github.com/sirupsen/logrus" - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/config" - "gopkg.in/src-d/go-git.v4/plumbing/transport" - "gopkg.in/src-d/go-git.v4/plumbing/transport/http" -) - -var ( - fetchCmdMode string - fetchTryCount int - - fetchCommand = "fetch" - fetchCmdModeLegacy = "git" - fetchCmdModeNative = "go-git" - fetchMaxTry = 1 -) - -// FetchOptions defines the rules for fetch operation -type FetchOptions struct { - // Name of the remote to fetch from. Defaults to origin. - RemoteName string - // Credentials holds the user and pswd information - Credentials Credentials - // Before fetching, remove any remote-tracking references that no longer - // exist on the remote. - Prune bool - // Show what would be done, without making any changes. - DryRun bool - // Force allows the fetch to update a local branch even when the remote - // branch does not descend from it. - Force bool - // There should be more room for authentication, tags and progress -} - -// Fetch branches refs from one or more other repositories, along with the -// objects necessary to complete their histories -func Fetch(entity *RepoEntity, options FetchOptions) (err error) { - // here we configure fetch operation - // default mode is go-git (this may be configured) - fetchCmdMode = fetchCmdModeNative - fetchTryCount = 0 - // prune and dry run is not supported from go-git yet, rely on old friend - if options.Prune || options.DryRun { - fetchCmdMode = fetchCmdModeLegacy - } - switch fetchCmdMode { - case fetchCmdModeLegacy: - err = fetchWithGit(entity.AbsPath, options) - return err - case fetchCmdModeNative: - // this should be the refspec as default, let's give it a try - refspec := "+" + "refs/heads/" + entity.Branch.Name + ":" + "/refs/remotes/" + entity.Remote.Branch.Name - err = fetchWithGoGit(entity, options, refspec) - return err - } - // till this step everything should be ok - err = entity.Refresh() - return err -} - -// fetchWithGit is simply a bare git fetch 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(abspath string, options FetchOptions) (err error) { - args := make([]string, 0) - args = append(args, fetchCommand) - // parse options to command line arguments - if len(options.RemoteName) > 0 { - args = append(args, options.RemoteName) - } - if options.Prune { - args = append(args, "-p") - } - if options.Force { - args = append(args, "-f") - } - if options.DryRun { - args = append(args, "--dry-run") - } - if err := GenericGitCommand(abspath, args); err != nil { - log.Warn("Error at git command (fetch)") - } - return err -} - -// fetchWithGoGit is the primary fetch method and refspec is the main feature. -// RefSpec is a mapping from local branches to remote references The format of -// the refspec is an optional +, followed by :, where is the -// pattern for references on the remote side and is where those references -// will be written locally. The + tells Git to update the reference even if it -// isn’t a fast-forward. -func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (err error) { - opt := &git.FetchOptions{ - RemoteName: options.RemoteName, - RefSpecs: []config.RefSpec{config.RefSpec(refspec)}, - Force: options.Force, - } - if len(options.Credentials.User) > 0 { - protocol, err := entity.authProtocol(entity.Remote) - if err != nil { - return err - } - if protocol == authProtocolHttp || protocol == authProtocolHttps { - opt.Auth = &http.BasicAuth{ - Username: options.Credentials.User, - Password: options.Credentials.Password, - } - } else { - return ErrInvalidAuthMethod - } - } - - err = entity.Repository.Fetch(opt) - if err != nil { - if err == git.NoErrAlreadyUpToDate { - // Already up-to-date - log.Warn(err.Error()) - // TODO: submit a PR for this kind of error, this type of catch is lame - } else if strings.Contains(err.Error(), "couldn't find remote ref") { - // we dont have remote ref, so lets pull other things.. maybe it'd be useful - rp := entity.Remote.RefSpecs[0] - if fetchTryCount < fetchMaxTry { - fetchTryCount++ - fetchWithGoGit(entity, options, rp) - } else { - return err - } - } else if err == transport.ErrAuthenticationRequired { - log.Warn(err.Error()) - return ErrAuthenticationRequired - } else { - log.Warn(err.Error()) - return err - } - } - return nil -} diff --git a/pkg/git/job-queue.go b/pkg/git/job-queue.go new file mode 100644 index 0000000..e95250a --- /dev/null +++ b/pkg/git/job-queue.go @@ -0,0 +1,78 @@ +package git + +import ( + "errors" +) + +// JobQueue holds the slice of Jobs +type JobQueue struct { + series []*Job +} + +// CreateJobQueue creates a jobqueue struct and initialize its slice then return +// its pointer +func CreateJobQueue() (jobQueue *JobQueue) { + s := make([]*Job, 0) + return &JobQueue{ + series: s, + } +} + +// AddJob adds a job to the queue +func (jobQueue *JobQueue) AddJob(j *Job) error { + for _, job := range jobQueue.series { + if job.Entity.RepoID == j.Entity.RepoID && job.JobType == j.JobType { + return errors.New("Same job already is in the queue") + } + } + jobQueue.series = append(jobQueue.series, nil) + copy(jobQueue.series[1:], jobQueue.series[0:]) + jobQueue.series[0] = j + return nil +} + +// StartNext starts the next job in the queue +func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) { + finished = false + if len(jobQueue.series) < 1 { + finished = true + return nil, finished, nil + } + i := len(jobQueue.series) - 1 + lastJob := jobQueue.series[i] + jobQueue.series = jobQueue.series[:i] + if err = lastJob.start(); err != nil { + return lastJob, finished, err + } + return lastJob, finished, nil +} + +// RemoveFromQueue deletes the given entity and its job from the queue +// TODO: it is not safe if the job has been started +func (jobQueue *JobQueue) RemoveFromQueue(entity *RepoEntity) error { + removed := false + for i, job := range jobQueue.series { + if job.Entity.RepoID == entity.RepoID { + jobQueue.series = append(jobQueue.series[:i], jobQueue.series[i+1:]...) + removed = true + } + } + if !removed { + return errors.New("There is no job with given repoID") + } + return nil +} + +// IsInTheQueue function; since the job and entity is not tied with its own +// struct, this function returns true if that entity is in the queue along with +// the jobs type +func (jobQueue *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) { + inTheQueue = false + for _, job := range jobQueue.series { + if job.Entity.RepoID == entity.RepoID { + inTheQueue = true + jt = job.JobType + } + } + return inTheQueue, jt +} diff --git a/pkg/git/job.go b/pkg/git/job.go index 13ef566..7dc4126 100644 --- a/pkg/git/job.go +++ b/pkg/git/job.go @@ -1,13 +1,14 @@ package git -import ( - "time" -) +import () // Job relates the type of the operation and the entity type Job struct { + // JobType is to select operation type that will be applied to repository JobType JobType - Entity *RepoEntity + // Entity points to the repository that will be used for operation + Entity *RepoEntity + // Options is a placeholder for operation options Options interface{} } @@ -26,9 +27,8 @@ const ( // starts the job func (job *Job) start() error { job.Entity.State = Working - // added for testing, TODO: remove - time.Sleep(time.Second) // TODO: Handle errors? + // TOOD: Better implementation required switch mode := job.JobType; mode { case FetchJob: var opts FetchOptions @@ -44,11 +44,17 @@ func (job *Job) start() error { return err } case PullJob: - if err := Fetch(job.Entity, FetchOptions{ - RemoteName: job.Entity.Remote.Name, - }); err != nil { + var opts FetchOptions + if job.Options != nil { + opts = job.Options.(FetchOptions) + } else { + opts = FetchOptions{ + RemoteName: job.Entity.Remote.Name, + } + } + if err := Fetch(job.Entity, opts); err != nil { job.Entity.State = Fail - return nil + return err } if err := Merge(job.Entity, MergeOptions{ BranchName: job.Entity.Remote.Branch.Name, diff --git a/pkg/git/load.go b/pkg/git/load.go deleted file mode 100644 index e35e1c1..0000000 --- a/pkg/git/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 := InitializeRepository(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 -} diff --git a/pkg/git/merge.go b/pkg/git/merge.go deleted file mode 100644 index 984bc4b..0000000 --- a/pkg/git/merge.go +++ /dev/null @@ -1,39 +0,0 @@ -package git - -import ( - log "github.com/sirupsen/logrus" -) - -var mergeCommand = "merge" - -// MergeOptions defines the rules of a merge operation -type MergeOptions struct { - // Name of the branch to merge with. - BranchName string - // Be verbose. - Verbose bool - // With true do not show a diffstat at the end of the merge. - NoStat bool -} - -// Merge incorporates changes from the named commits or branches into the -// current branch -func Merge(entity *RepoEntity, options MergeOptions) error { - args := make([]string, 0) - args = append(args, mergeCommand) - if len(options.BranchName) > 0 { - args = append(args, options.BranchName) - } - if options.Verbose { - args = append(args, "-v") - } - if options.NoStat { - args = append(args, "-n") - } - if err := GenericGitCommand(entity.AbsPath, args); err != nil { - log.Warn("Error while merging") - return err - } - entity.Refresh() - return nil -} diff --git a/pkg/git/queue.go b/pkg/git/queue.go deleted file mode 100644 index e95250a..0000000 --- a/pkg/git/queue.go +++ /dev/null @@ -1,78 +0,0 @@ -package git - -import ( - "errors" -) - -// JobQueue holds the slice of Jobs -type JobQueue struct { - series []*Job -} - -// CreateJobQueue creates a jobqueue struct and initialize its slice then return -// its pointer -func CreateJobQueue() (jobQueue *JobQueue) { - s := make([]*Job, 0) - return &JobQueue{ - series: s, - } -} - -// AddJob adds a job to the queue -func (jobQueue *JobQueue) AddJob(j *Job) error { - for _, job := range jobQueue.series { - if job.Entity.RepoID == j.Entity.RepoID && job.JobType == j.JobType { - return errors.New("Same job already is in the queue") - } - } - jobQueue.series = append(jobQueue.series, nil) - copy(jobQueue.series[1:], jobQueue.series[0:]) - jobQueue.series[0] = j - return nil -} - -// StartNext starts the next job in the queue -func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) { - finished = false - if len(jobQueue.series) < 1 { - finished = true - return nil, finished, nil - } - i := len(jobQueue.series) - 1 - lastJob := jobQueue.series[i] - jobQueue.series = jobQueue.series[:i] - if err = lastJob.start(); err != nil { - return lastJob, finished, err - } - return lastJob, finished, nil -} - -// RemoveFromQueue deletes the given entity and its job from the queue -// TODO: it is not safe if the job has been started -func (jobQueue *JobQueue) RemoveFromQueue(entity *RepoEntity) error { - removed := false - for i, job := range jobQueue.series { - if job.Entity.RepoID == entity.RepoID { - jobQueue.series = append(jobQueue.series[:i], jobQueue.series[i+1:]...) - removed = true - } - } - if !removed { - return errors.New("There is no job with given repoID") - } - return nil -} - -// IsInTheQueue function; since the job and entity is not tied with its own -// struct, this function returns true if that entity is in the queue along with -// the jobs type -func (jobQueue *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) { - inTheQueue = false - for _, job := range jobQueue.series { - if job.Entity.RepoID == entity.RepoID { - inTheQueue = true - jt = job.JobType - } - } - return inTheQueue, jt -} diff --git a/pkg/git/repository-sort.go b/pkg/git/repository-sort.go deleted file mode 100644 index de5f454..0000000 --- a/pkg/git/repository-sort.go +++ /dev/null @@ -1,59 +0,0 @@ -package git - -import ( - "unicode" -) - -// Alphabetical slice is the re-ordered *RepoEntity slice that sorted according -// to alphabetical order (A-Z) -type Alphabetical []*RepoEntity - -// Len is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Len() int { return len(s) } - -// Swap is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less is the interface implementation for Alphabetical sorting function -func (s Alphabetical) 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 -} - -// LastModified slice is the re-ordered *RepoEntity slice that sorted according -// to last modified date of the repository directory -type LastModified []*RepoEntity - -// Len is the interface implementation for LastModified sorting function -func (s LastModified) Len() int { return len(s) } - -// Swap is the interface implementation for LastModified sorting function -func (s LastModified) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less is the interface implementation for LastModified sorting function -func (s LastModified) Less(i, j int) bool { - return s[i].ModTime.Unix() > s[j].ModTime.Unix() -} diff --git a/pkg/git/repository.go b/pkg/git/repository.go index 8d0160b..ff67611 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -39,10 +39,12 @@ const ( Queued RepoState = 1 // Working means an operation is just started for this repository Working RepoState = 2 + // Paused is expected when a user interaction is required + Paused RepoState = 3 // Success is the expected outcome of the operation - Success RepoState = 3 + Success RepoState = 4 // Fail is the unexpected outcome of the operation - Fail RepoState = 4 + Fail RepoState = 5 ) // InitializeRepository initializes a RepoEntity struct with its belongings. diff --git a/pkg/git/reset.go b/pkg/git/reset.go deleted file mode 100644 index f93225c..0000000 --- a/pkg/git/reset.go +++ /dev/null @@ -1,55 +0,0 @@ -package git - -import ( - "errors" - "strings" - - log "github.com/sirupsen/logrus" -) - -var resetCommand = "reset" - -// ResetOptions defines the rules of git reset command -type ResetOptions struct { - Hard bool - Merge bool - Keep bool -} - -// Reset is the wrapper of "git reset" command -func (file *File) Reset(option ResetOptions) error { - args := make([]string, 0) - args = append(args, resetCommand) - args = append(args, "--") - args = append(args, file.Name) - if option.Hard { - args = append(args, "--hard") - } - if option.Merge { - args = append(args, "--merge") - } - if option.Keep { - args = append(args, "--keep") - } - out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) - if err != nil { - log.Warn("Error while add command") - return errors.New(out + "\n" + err.Error()) - } - return nil -} - -// ResetAll resets the changes in a repository, should be used wise -func (entity *RepoEntity) ResetAll(option ResetOptions) error { - args := make([]string, 0) - args = append(args, resetCommand) - if option.Hard { - args = append(args, "--hard") - } - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) - if err != nil { - log.Warn("Error while add command") - return errors.New(out + "\n" + err.Error()) - } - return nil -} diff --git a/pkg/git/rev-list.go b/pkg/git/rev-list.go deleted file mode 100644 index 664d69f..0000000 --- a/pkg/git/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(entity *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(entity.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/stash.go b/pkg/git/stash.go deleted file mode 100644 index 9d813a6..0000000 --- a/pkg/git/stash.go +++ /dev/null @@ -1,106 +0,0 @@ -package git - -import ( - "regexp" - "strconv" - "strings" - - log "github.com/sirupsen/logrus" -) - -var stashCommand = "stash" - -// StashedItem holds the required fields for a stashed change -type StashedItem struct { - StashID int - BranchName string - Hash string - Description string - EntityPath string -} - -func stashGet(entity *RepoEntity, option string) string { - args := make([]string, 0) - args = append(args, stashCommand) - args = append(args, option) - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) - if err != nil { - log.Warn("Error while stash command") - return "?" - } - return out -} - -func (entity *RepoEntity) loadStashedItems() error { - entity.Stasheds = make([]*StashedItem, 0) - output := stashGet(entity, "list") - stashIDRegex := regexp.MustCompile(`stash@{[\d]+}:`) - stashIDRegexInt := regexp.MustCompile(`[\d]+`) - stashBranchRegex := regexp.MustCompile(`[\w]+: `) - stashHashRegex := regexp.MustCompile(`[\w]{7}`) - - stashlist := strings.Split(output, "\n") - for _, stashitem := range stashlist { - // find id - id := stashIDRegexInt.FindString(stashIDRegex.FindString(stashitem)) - i, err := strconv.Atoi(id) - if err != nil { - // probably something isn't right let's continue over this iteration - log.Trace("cannot initiate stashed item") - continue - } - // trim id section - trimmed := stashIDRegex.Split(stashitem, 2)[1] - - // find branch - stashBranchRegexMatch := stashBranchRegex.FindString(trimmed) - branchName := stashBranchRegexMatch[:len(stashBranchRegexMatch)-2] - - // trim branch section - trimmed = stashBranchRegex.Split(trimmed, 2)[1] - hash := stashHashRegex.FindString(trimmed) - - // trim hash - desc := stashHashRegex.Split(trimmed, 2)[1][1:] - - entity.Stasheds = append(entity.Stasheds, &StashedItem{ - StashID: i, - BranchName: branchName, - Hash: hash, - Description: desc, - EntityPath: entity.AbsPath, - }) - } - return nil -} - -// Stash is the wrapper of convetional "git stash" command -func (entity *RepoEntity) Stash() (output string, err error) { - args := make([]string, 0) - args = append(args, stashCommand) - - output, err = GenericGitCommandWithErrorOutput(entity.AbsPath, args) - entity.Refresh() - return output, err -} - -// Pop is the wrapper of "git stash pop" command that used for a file -func (stashedItem *StashedItem) Pop() (output string, err error) { - args := make([]string, 0) - args = append(args, stashCommand) - args = append(args, "pop") - args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}") - output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args) - return output, err -} - -// Show is the wrapper of "git stash show -p " command -func (stashedItem *StashedItem) Show() (output string, err error) { - args := make([]string, 0) - args = append(args, stashCommand) - 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 -} diff --git a/pkg/git/status.go b/pkg/git/status.go deleted file mode 100644 index 4cca604..0000000 --- a/pkg/git/status.go +++ /dev/null @@ -1,91 +0,0 @@ -package git - -import ( - "os" - "regexp" - "strings" - - log "github.com/sirupsen/logrus" -) - -var statusCommand = "status" - -// File represents the status of a file in an index or work tree -type File struct { - Name string - AbsPath string - X FileStatus - Y FileStatus -} - -// FileStatus is the short representation of state of a file -type FileStatus rune - -var ( - // StatusNotupdated says file not updated - StatusNotupdated FileStatus = ' ' - // StatusModified says file is modifed - StatusModified FileStatus = 'M' - // StatusAdded says file is added to index - StatusAdded FileStatus = 'A' - // StatusDeleted says file is deleted - StatusDeleted FileStatus = 'D' - // StatusRenamed says file is renamed - StatusRenamed FileStatus = 'R' - // StatusCopied says file is copied - StatusCopied FileStatus = 'C' - // StatusUpdated says file is updated - StatusUpdated FileStatus = 'U' - // StatusUntracked says file is untraced - StatusUntracked FileStatus = '?' - // StatusIgnored says file is ignored - StatusIgnored FileStatus = '!' -) - -func shortStatus(entity *RepoEntity, option string) string { - args := make([]string, 0) - args = append(args, statusCommand) - args = append(args, option) - args = append(args, "--short") - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) - if err != nil { - log.Warn("Error while status command") - return "?" - } - return out -} - -// LoadFiles function simply commands a git status and collects output in a -// structured way -func (entity *RepoEntity) LoadFiles() ([]*File, error) { - files := make([]*File, 0) - output := shortStatus(entity, "--untracked-files=all") - if len(output) == 0 { - return files, nil - } - fileslist := strings.Split(output, "\n") - for _, file := range fileslist { - x := rune(file[0]) - y := rune(file[1]) - relativePathRegex := regexp.MustCompile(`[(\w|/|.|\-)]+`) - path := relativePathRegex.FindString(file[2:]) - - files = append(files, &File{ - Name: path, - AbsPath: entity.AbsPath + string(os.PathSeparator) + path, - X: FileStatus(x), - Y: FileStatus(y), - }) - } - return files, nil -} - -// Diff is a wrapper of "git diff" command for a file to compare with HEAD rev -func (file *File) Diff() (output string, err error) { - args := make([]string, 0) - args = append(args, "diff") - args = append(args, "HEAD") - args = append(args, file.Name) - output, err = GenericGitCommandWithErrorOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) - return output, err -} diff --git a/pkg/git/util-errors.go b/pkg/git/util-errors.go new file mode 100644 index 0000000..8621cbc --- /dev/null +++ b/pkg/git/util-errors.go @@ -0,0 +1,24 @@ +package git + +import ( + "errors" +) + +var ( + // ErrGitCommand is thrown when git command returned an error code + ErrGitCommand = errors.New("Git command returned error code") + // ErrAuthenticationRequired is thrown when an authentication required on + // a remote operation + ErrAuthenticationRequired = errors.New("Authentication required") + // ErrAuthorizationFailed is thrown when authorization failed while trying + // to authenticate with remote + ErrAuthorizationFailed = errors.New("Authorization failed") + // ErrInvalidAuthMethod is thrown when invalid auth method is invoked + ErrInvalidAuthMethod = errors.New("invalid auth method") + // ErrAlreadyUpToDate is thrown when a repository is already up to date + // with its src on merge/fetch/pull + ErrAlreadyUpToDate = errors.New("Already up to date") + // ErrCouldNotFindRemoteRef is thrown when trying to fetch/pull cannot + // find suitable remote reference + ErrCouldNotFindRemoteRef = errors.New("Could not find remote ref") +) diff --git a/pkg/git/util-load.go b/pkg/git/util-load.go new file mode 100644 index 0000000..e35e1c1 --- /dev/null +++ b/pkg/git/util-load.go @@ -0,0 +1,48 @@ +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 := InitializeRepository(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 +} diff --git a/pkg/git/util-repository-sort.go b/pkg/git/util-repository-sort.go new file mode 100644 index 0000000..de5f454 --- /dev/null +++ b/pkg/git/util-repository-sort.go @@ -0,0 +1,59 @@ +package git + +import ( + "unicode" +) + +// Alphabetical slice is the re-ordered *RepoEntity slice that sorted according +// to alphabetical order (A-Z) +type Alphabetical []*RepoEntity + +// Len is the interface implementation for Alphabetical sorting function +func (s Alphabetical) Len() int { return len(s) } + +// Swap is the interface implementation for Alphabetical sorting function +func (s Alphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less is the interface implementation for Alphabetical sorting function +func (s Alphabetical) 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 +} + +// LastModified slice is the re-ordered *RepoEntity slice that sorted according +// to last modified date of the repository directory +type LastModified []*RepoEntity + +// Len is the interface implementation for LastModified sorting function +func (s LastModified) Len() int { return len(s) } + +// Swap is the interface implementation for LastModified sorting function +func (s LastModified) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less is the interface implementation for LastModified sorting function +func (s LastModified) Less(i, j int) bool { + return s[i].ModTime.Unix() > s[j].ModTime.Unix() +} diff --git a/pkg/gui/authenticationview.go b/pkg/gui/authenticationview.go index 240b46c..9c830cd 100644 --- a/pkg/gui/authenticationview.go +++ b/pkg/gui/authenticationview.go @@ -2,30 +2,40 @@ package gui import ( "fmt" + "regexp" + "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" log "github.com/sirupsen/logrus" ) var ( - authenticationReturnView string + // this is required so we can know where we can return + authenticationReturnView string + // these views used as a label for git repository address and credential views authenticationViewFeature = viewFeature{Name: "authentication", Title: " Authentication "} - authUserFeature = viewFeature{Name: "authuser", Title: " User "} - authPasswordViewFeature = viewFeature{Name: "authpasswd", Title: " Password "} authUserLabelFeature = viewFeature{Name: "authuserlabel", Title: " User: "} authPswdLabelViewFeature = viewFeature{Name: "authpasswdlabel", Title: " Password: "} + // these views used as a input for the credentials + authUserFeature = viewFeature{Name: "authuser", Title: " User "} + authPasswordViewFeature = viewFeature{Name: "authpasswd", Title: " Password "} + // these are the view groups, so that we can assign common keybindings authViews = []viewFeature{authUserFeature, authPasswordViewFeature} authLabels = []viewFeature{authenticationViewFeature, authUserLabelFeature, authPswdLabelViewFeature} + // we can hold the job that is required to authenticate jobRequiresAuth *git.Job ) -// open an error view to inform user with a message and a useful note +// open an auth view to get user credentials func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job *git.Job, returnViewName string) error { maxX, maxY := g.Size() - // lets remove this job from the queue so that it won't block anything + // lets add this job since it is removed from the queue // also it is already unsuccessfully finished + if err := jobQueue.AddJob(job); err != nil { + return err + } jobRequiresAuth = job if job.Entity.State != git.Fail { if err := jobQueue.RemoveFromQueue(job.Entity); err != nil { @@ -33,26 +43,22 @@ func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job return err } } - authenticationReturnView = returnViewName v, err := g.SetView(authenticationViewFeature.Name, maxX/2-30, maxY/2-2, maxX/2+30, maxY/2+2) if err != nil { if err != gocui.ErrUnknownView { return err } - fmt.Fprintln(v, " Enter credentials for: "+red.Sprint(job.Entity.Remote.URL[0])) + fmt.Fprintln(v, keySymbol+selectionIndicator+red.Sprint(jobRequiresAuth.Entity.Remote.URL[0])) } g.Cursor = true if err := gui.openUserView(g); err != nil { - return nil - } - if err := gui.openPasswordView(g); err != nil { - return nil + return err } - return nil + return gui.openPasswordView(g) } -// close the opened error view +// close the opened auth views func (gui *Gui) closeAuthenticationView(g *gocui.Gui, v *gocui.View) error { g.Cursor = false for _, vf := range authLabels { @@ -65,21 +71,20 @@ func (gui *Gui) closeAuthenticationView(g *gocui.Gui, v *gocui.View) error { return nil } } - if _, err := g.SetCurrentView(authenticationReturnView); err != nil { - return err - } - gui.updateKeyBindingsView(g, authenticationReturnView) - return nil + return gui.closeViewCleanup(authenticationReturnView) } -// close the opened error view +// close the opened auth views and submit the credentials func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error { g.Cursor = false + // in order to read buffer of the views, first we need to find'em v_user, err := g.View(authUserFeature.Name) v_pswd, err := g.View(authPasswordViewFeature.Name) - creduser := v_user.ViewBuffer() - credpswd := v_pswd.ViewBuffer() - // Maybe pause implementation can be added + // the return string of the views contain trailing new lines + re := regexp.MustCompile(`\r?\n`) + creduser := re.ReplaceAllString(v_user.ViewBuffer(), "") + credpswd := re.ReplaceAllString(v_pswd.ViewBuffer(), "") + // since the git ops require different types of options we better switch switch mode := jobRequiresAuth.JobType; mode { case git.FetchJob: jobRequiresAuth.Options = git.FetchOptions{ @@ -89,21 +94,34 @@ func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error { Password: credpswd, }, } + case git.PullJob: + // we handle pull as fetch&merge so same rule applies + jobRequiresAuth.Options = git.FetchOptions{ + RemoteName: jobRequiresAuth.Entity.Remote.Name, + Credentials: git.Credentials{ + User: creduser, + Password: credpswd, + }, + } } + jobRequiresAuth.Entity.State = git.Queued + // add this job to the last of the queue err = gui.State.Queue.AddJob(jobRequiresAuth) if err != nil { return err } - jobRequiresAuth.Entity.State = git.Queued + // refresh views with the updates gui.refreshMain(g) - gui.refreshViews(g, jobRequiresAuth.Entity) gui.closeAuthenticationView(g, v) + v_return, err := g.View(authenticationReturnView) + gui.startQueue(g, v_return) return nil } // open an error view to inform user with a message and a useful note func (gui *Gui) openUserView(g *gocui.Gui) error { maxX, maxY := g.Size() + // first, create the label for user vlabel, err := g.SetView(authUserLabelFeature.Name, maxX/2-30, maxY/2-1, maxX/2-19, maxY/2+1) if err != nil { if err != gocui.ErrUnknownView { @@ -112,6 +130,7 @@ func (gui *Gui) openUserView(g *gocui.Gui) error { fmt.Fprintln(vlabel, authUserLabelFeature.Title) vlabel.Frame = false } + // second, crete the user input v, err := g.SetView(authUserFeature.Name, maxX/2-18, maxY/2-1, maxX/2+29, maxY/2+1) if err != nil { if err != gocui.ErrUnknownView { @@ -121,16 +140,13 @@ func (gui *Gui) openUserView(g *gocui.Gui) error { v.Editable = true v.Frame = false } - gui.updateKeyBindingsView(g, authUserFeature.Name) - if _, err := g.SetCurrentView(authUserFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(authUserFeature.Name) } // open an error view to inform user with a message and a useful note func (gui *Gui) openPasswordView(g *gocui.Gui) error { maxX, maxY := g.Size() + // first, create the label for password vlabel, err := g.SetView(authPswdLabelViewFeature.Name, maxX/2-30, maxY/2, maxX/2-19, maxY/2+2) if err != nil { if err != gocui.ErrUnknownView { @@ -139,6 +155,7 @@ func (gui *Gui) openPasswordView(g *gocui.Gui) error { fmt.Fprintln(vlabel, authPswdLabelViewFeature.Title) vlabel.Frame = false } + // second, crete the masked password input v, err := g.SetView(authPasswordViewFeature.Name, maxX/2-18, maxY/2, maxX/2+29, maxY/2+2) if err != nil { if err != gocui.ErrUnknownView { @@ -157,9 +174,3 @@ func (gui *Gui) nextAuthView(g *gocui.Gui, v *gocui.View) error { err := gui.nextViewOfGroup(g, v, authViews) return err } - -// focus to previous view -func (gui *Gui) previousAuthView(g *gocui.Gui, v *gocui.View) error { - err := gui.previousViewOfGroup(g, v, authViews) - return err -} diff --git a/pkg/gui/cheatsheet.go b/pkg/gui/cheatsheet.go index 43020a5..4b2f97e 100644 --- a/pkg/gui/cheatsheet.go +++ b/pkg/gui/cheatsheet.go @@ -24,11 +24,7 @@ func (gui *Gui) openCheatSheetView(g *gocui.Gui, v *gocui.View) error { } } } - gui.updateKeyBindingsView(g, cheatSheetViewFeature.Name) - if _, err := g.SetCurrentView(cheatSheetViewFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(cheatSheetViewFeature.Name) } // close the application controls and do the clean job @@ -36,9 +32,5 @@ func (gui *Gui) closeCheatSheetView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(v.Name()); err != nil { return nil } - if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { - return err - } - gui.updateKeyBindingsView(g, mainViewFeature.Name) - return nil + return gui.closeViewCleanup(mainViewFeature.Name) } diff --git a/pkg/gui/diffview.go b/pkg/gui/diffview.go index f99d7f2..04c9e10 100644 --- a/pkg/gui/diffview.go +++ b/pkg/gui/diffview.go @@ -22,8 +22,7 @@ func (gui *Gui) prepareDiffView(g *gocui.Gui, v *gocui.View, display []string) ( out.Title = diffViewFeature.Title out.Overwrite = true out.Wrap = true - gui.updateKeyBindingsView(g, diffViewFeature.Name) - if _, err = g.SetCurrentView(diffViewFeature.Name); err != nil { + if err = gui.focusToView(diffViewFeature.Name); err != nil { return out, err } for _, line := range display { @@ -126,9 +125,5 @@ func (gui *Gui) closeCommitDiffView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(v.Name()); err != nil { return nil } - if _, err := g.SetCurrentView(diffReturnView); err != nil { - return err - } - gui.updateKeyBindingsView(g, diffReturnView) - return nil + return gui.closeViewCleanup(diffReturnView) } diff --git a/pkg/gui/errorview.go b/pkg/gui/errorview.go index b4be162..f679b54 100644 --- a/pkg/gui/errorview.go +++ b/pkg/gui/errorview.go @@ -23,11 +23,7 @@ func (gui *Gui) openErrorView(g *gocui.Gui, message, note, returnViewName string fmt.Fprintln(v, message) fmt.Fprintln(v, ps) } - gui.updateKeyBindingsView(g, errorViewFeature.Name) - if _, err := g.SetCurrentView(errorViewFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(errorViewFeature.Name) } // close the opened error view @@ -36,9 +32,5 @@ func (gui *Gui) closeErrorView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(v.Name()); err != nil { return nil } - if _, err := g.SetCurrentView(errorReturnView); err != nil { - return err - } - gui.updateKeyBindingsView(g, errorReturnView) - return nil + return gui.closeViewCleanup(errorReturnView) } diff --git a/pkg/gui/gui-util.go b/pkg/gui/gui-util.go index bd02f7c..343e293 100644 --- a/pkg/gui/gui-util.go +++ b/pkg/gui/gui-util.go @@ -193,3 +193,17 @@ func (gui *Gui) fastCursorUp(g *gocui.Gui, v *gocui.View) error { } return nil } + +// closeViewCleanup both updates the keybidings view and focuses to returning view +func (gui *Gui) closeViewCleanup(returningViewName string) (err error) { + if _, err = gui.g.SetCurrentView(returningViewName); err != nil { + return err + } + err = gui.updateKeyBindingsView(gui.g, returningViewName) + return err +} + +// focus to view same as closeViewCleanup but its just a wrapper for easy reading +func (gui *Gui) focusToView(viewName string) (err error) { + return gui.closeViewCleanup(viewName) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 9b23fa2..1d4dd6d 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -191,7 +191,7 @@ func (gui *Gui) generateKeybindings() error { Handler: gui.nextAuthView, Display: "tab", Description: "Next Panel", - Vital: false, + Vital: true, }, { View: view.Name, Key: gocui.KeyEnter, @@ -199,7 +199,7 @@ func (gui *Gui) generateKeybindings() error { Handler: gui.submitAuthenticationView, Display: "enter", Description: "Submit", - Vital: false, + Vital: true, }, } for _, binding := range authKeybindings { diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index fe8931b..389d66d 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -147,7 +147,8 @@ func (gui *Gui) removeFromQueue(entity *git.RepoEntity) error { // function does take its current state into account before adding it func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error { r := gui.getSelectedRepository() - if r.State == git.Available || r.State == git.Success { + // maybe, failed entities may be added to queue again + if r.State == git.Available || r.State == git.Success || r.State == git.Paused { if err := gui.addToQueue(r); err != nil { return err } diff --git a/pkg/gui/queuehandler.go b/pkg/gui/queuehandler.go index cec6f8b..a814c47 100644 --- a/pkg/gui/queuehandler.go +++ b/pkg/gui/queuehandler.go @@ -12,13 +12,17 @@ func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error { go func(gui_go *Gui, g_go *gocui.Gui) { for { job, finished, err := gui_go.State.Queue.StartNext() + // for each job execution we better refresh the main + // it would be nice if we can also refresh side views g_go.Update(func(gu *gocui.Gui) error { gui_go.refreshMain(gu) return nil }) - defer gui.updateKeyBindingsView(g, mainViewFeature.Name) + if err != nil { if err == git.ErrAuthenticationRequired { + // pause the job, so it will be indicated to being blocking + job.Entity.State = git.Paused err := gui_go.openAuthenticationView(g, gui_go.State.Queue, job, v.Name()) if err != nil { log.Warn(err.Error()) @@ -26,7 +30,9 @@ func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error { } } return + // with not returning here, we simply ignore and continue } + // if queue is finished simply return from this goroutine if finished { return } diff --git a/pkg/gui/stagedview.go b/pkg/gui/stagedview.go index e27d35d..f12c06d 100644 --- a/pkg/gui/stagedview.go +++ b/pkg/gui/stagedview.go @@ -22,11 +22,7 @@ func (gui *Gui) openStageView(g *gocui.Gui) error { if err := refreshStagedView(g, entity); err != nil { return err } - gui.updateKeyBindingsView(g, stageViewFeature.Name) - if _, err := g.SetCurrentView(stageViewFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(stageViewFeature.Name) } func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go index 494e07a..b241ed6 100644 --- a/pkg/gui/statusview.go +++ b/pkg/gui/statusview.go @@ -11,8 +11,7 @@ import ( ) var ( - statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "} - // statusViewFeature = viewFeature{Name: "status", Title: " Status "} + statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "} stageViewFeature = viewFeature{Name: "staged", Title: " Staged "} unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "} stashViewFeature = viewFeature{Name: "stash", Title: " Stash "} @@ -105,21 +104,14 @@ func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error { // close the opened stat views func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { - if err := g.DeleteView(stashViewFeature.Name); err != nil { - return err - } - if err := g.DeleteView(unstageViewFeature.Name); err != nil { - return err - } - if err := g.DeleteView(stageViewFeature.Name); err != nil { - return err + for _, view := range statusViews { + if err := g.DeleteView(view.Name); err != nil { + return err + } } if err := g.DeleteView(statusHeaderViewFeature.Name); err != nil { return err } - if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { - return err - } entity := gui.getSelectedRepository() if err := gui.refreshMain(g); err != nil { return err @@ -127,8 +119,7 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { if err := gui.refreshViews(g, entity); err != nil { return err } - gui.updateKeyBindingsView(g, mainViewFeature.Name) - return nil + return gui.closeViewCleanup(mainViewFeature.Name) } func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) { @@ -228,9 +219,6 @@ func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(commitMessageViewFeature.Name); err != nil { return err } - if _, err := g.SetCurrentView(commitMesageReturnView); err != nil { - return err - } if err := gui.refreshMain(g); err != nil { return err } @@ -240,6 +228,5 @@ func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { if err := refreshAllStatusView(g, entity); err != nil { return err } - gui.updateKeyBindingsView(g, commitMesageReturnView) - return nil + return gui.closeViewCleanup(commitMesageReturnView) } diff --git a/pkg/gui/textstyle.go b/pkg/gui/textstyle.go index efbbceb..7c36263 100644 --- a/pkg/gui/textstyle.go +++ b/pkg/gui/textstyle.go @@ -35,12 +35,15 @@ var ( queuedSymbol = "•" workingSymbol = "•" successSymbol = "✔" + pauseSymbol = "॥" failSymbol = "✗" fetchSymbol = "↓" pullSymbol = "↓↳" mergeSymbol = "↳" + keySymbol = ws + yellow.Sprint("🔑") + ws + modeSeperator = "" keyBindingSeperator = "░" @@ -102,6 +105,8 @@ func (gui *Gui) displayString(entity *git.RepoEntity) string { return prefix + repoName + ws + green.Sprint(workingSymbol) } else if entity.State == git.Success { return prefix + repoName + ws + green.Sprint(successSymbol) + } else if entity.State == git.Paused { + return prefix + repoName + ws + yellow.Sprint(pauseSymbol) } else if entity.State == git.Fail { return prefix + repoName + ws + red.Sprint(failSymbol) } else { -- cgit v1.2.3