diff options
| author | Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> | 2018-12-19 00:05:39 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-12-19 00:05:39 +0300 |
| commit | 68bcd82ca27198c31bfe06858e3ae85e48648dab (patch) | |
| tree | 3747cb46200821616a89b77a1fc142aaaff7606f | |
| parent | Merge pull request #35 from isacikgoz/develop (diff) | |
| parent | minor code style imrovements and code reduction (diff) | |
| download | gitbatch-68bcd82ca27198c31bfe06858e3ae85e48648dab.tar.gz | |
Merge pull request #36 from isacikgoz/develop
Develop
36 files changed, 836 insertions, 938 deletions
@@ -15,7 +15,7 @@ var ( ) func main() { - kingpin.Version("gitbatch version 0.1.1 (alpha)") + kingpin.Version("gitbatch version 0.1.2 (pre-release)") // parse the command line flag and options kingpin.Parse() diff --git a/pkg/git/authentication.go b/pkg/git/authentication.go index def1f99..3e834ec 100644 --- a/pkg/git/authentication.go +++ b/pkg/git/authentication.go @@ -7,7 +7,9 @@ import ( // Credentials holds user credentials to authenticate and authorize while // communicating with remote if required type Credentials struct { - User string + // User is the user id for authentication + User string + // Password is the secret information required for authetntication Password string } @@ -17,8 +19,10 @@ var ( authProtocolSSH = "ssh" ) -func (entity *RepoEntity) authProtocol(remote *Remote) (p string, err error) { - u, err := url.Parse(remote.URL[0]) +// authentication protocol 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) { + u, err := url.Parse(r.URL[0]) if err != nil { return p, err } diff --git a/pkg/git/branch.go b/pkg/git/branch.go index ace5f79..5be8e16 100644 --- a/pkg/git/branch.go +++ b/pkg/git/branch.go @@ -5,7 +5,7 @@ import ( log "github.com/sirupsen/logrus" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" - "regexp" + "strconv" "strings" ) @@ -22,33 +22,22 @@ type Branch struct { Clean bool } -// returns the active branch of the repository entity by simply getting the -// head reference and searching it from the entities branch slice -func (entity *RepoEntity) getActiveBranch() (branch *Branch) { - headRef, _ := entity.Repository.Head() - for _, lb := range entity.Branches { - if lb.Name == headRef.Name().Short() { - return lb - } - } - return nil -} - // 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 (entity *RepoEntity) loadLocalBranches() error { +func (e *RepoEntity) loadLocalBranches() error { lbs := make([]*Branch, 0) - branches, err := entity.Repository.Branches() + bs, err := e.Repository.Branches() if err != nil { log.Warn("Cannot load branches " + err.Error()) return err } - defer branches.Close() - branches.ForEach(func(b *plumbing.Reference) error { + defer bs.Close() + headRef, _ := e.Repository.Head() + bs.ForEach(func(b *plumbing.Reference) error { if b.Type() == plumbing.HashReference { var push, pull string - pushables, err := RevList(entity, RevListOptions{ + pushables, err := RevList(e, RevListOptions{ Ref1: "@{u}", Ref2: "HEAD", }) @@ -57,7 +46,7 @@ func (entity *RepoEntity) loadLocalBranches() error { } else { push = strconv.Itoa(len(pushables)) } - pullables, err := RevList(entity, RevListOptions{ + pullables, err := RevList(e, RevListOptions{ Ref1: "HEAD", Ref2: "@{u}", }) @@ -66,53 +55,53 @@ func (entity *RepoEntity) loadLocalBranches() error { } else { pull = strconv.Itoa(len(pullables)) } - clean := entity.isClean() - branch := &Branch{Name: b.Name().Short(), Reference: b, Pushables: push, Pullables: pull, Clean: clean} + 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 + } lbs = append(lbs, branch) } return nil }) - entity.Branches = lbs + e.Branches = lbs return err } // NextBranch checkouts the next branch -func (entity *RepoEntity) NextBranch() *Branch { - currentBranchIndex := entity.findCurrentBranchIndex() - if currentBranchIndex == len(entity.Branches)-1 { - return entity.Branches[0] - } - return entity.Branches[currentBranchIndex+1] +func (e *RepoEntity) NextBranch() *Branch { + return e.Branches[(e.currentBranchIndex()+1)%len(e.Branches)] } // PreviousBranch checkouts the previous branch -func (entity *RepoEntity) PreviousBranch() *Branch { - currentBranchIndex := entity.findCurrentBranchIndex() - if currentBranchIndex == 0 { - return entity.Branches[len(entity.Branches)-1] - } - return entity.Branches[currentBranchIndex-1] +func (e *RepoEntity) PreviousBranch() *Branch { + return e.Branches[(len(e.Branches)+e.currentBranchIndex()-1)%len(e.Branches)] } // returns the active branch index -func (entity *RepoEntity) findCurrentBranchIndex() int { - currentBranch := entity.Branch - currentBranchIndex := 0 - for i, lbs := range entity.Branches { - if lbs.Name == currentBranch.Name { - currentBranchIndex = i +func (e *RepoEntity) currentBranchIndex() int { + bix := 0 + for i, lbs := range e.Branches { + if lbs.Name == e.Branch.Name { + bix = i } } - return currentBranchIndex + return bix } // Checkout to given branch. If any errors occur, the method returns it instead // of returning nil -func (entity *RepoEntity) Checkout(branch *Branch) error { - if branch.Name == entity.Branch.Name { +func (e *RepoEntity) Checkout(branch *Branch) error { + if branch.Name == e.Branch.Name { return nil } - w, err := entity.Repository.Worktree() + w, err := e.Repository.Worktree() if err != nil { log.Warn("Cannot get work tree " + err.Error()) return err @@ -124,90 +113,26 @@ func (entity *RepoEntity) Checkout(branch *Branch) error { return err } - // after checking out we need to refresh some values such as; - entity.loadCommits() - entity.Commit = entity.Commits[0] - entity.Branch = branch - entity.RefreshPushPull() // make this conditional on global scale - err = entity.Remote.SyncBranches(branch.Name) - return err + err = 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 (entity *RepoEntity) isClean() bool { - status := entity.StatusWithGit() - status = helpers.TrimTrailingNewline(status) - if status != "?" { - verbose := strings.Split(status, "\n") - lastLine := verbose[len(verbose)-1] +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(lastLine, "working tree clean") || - strings.Contains(lastLine, "working directory clean") { + if strings.Contains(line, "working tree clean") || + strings.Contains(line, "working directory clean") { return true } } return false } - -// RefreshPushPull refreshes the active branchs pushable and pullable count -func (entity *RepoEntity) RefreshPushPull() { - pushables, err := RevList(entity, RevListOptions{ - Ref1: "@{u}", - Ref2: "HEAD", - }) - if err != nil { - entity.Branch.Pushables = pushables[0] - } else { - entity.Branch.Pushables = strconv.Itoa(len(pushables)) - } - pullables, err := RevList(entity, RevListOptions{ - Ref1: "HEAD", - Ref2: "@{u}", - }) - if err != nil { - entity.Branch.Pullables = pullables[0] - } else { - entity.Branch.Pullables = strconv.Itoa(len(pullables)) - } -} - -// this function creates the commit entities according to active branchs diffs -// to *its* configured upstream -func (entity *RepoEntity) pullDiffsToUpstream() ([]*Commit, error) { - remoteCommits := make([]*Commit, 0) - pullables, err := RevList(entity, RevListOptions{ - Ref1: "HEAD", - Ref2: "@{u}", - }) - if err != nil { - // possibly found nothing or no upstream set - } else { - re := regexp.MustCompile(`\r?\n`) - for _, s := range pullables { - commit := &Commit{ - Hash: s, - Author: GitShowEmail(entity.AbsPath, s), - Message: re.ReplaceAllString(GitShowBody(entity.AbsPath, s), " "), - Time: GitShowDate(entity.AbsPath, s), - CommitType: RemoteCommit, - } - remoteCommits = append(remoteCommits, commit) - } - } - return remoteCommits, nil -} - -func (entity *RepoEntity) pushDiffsToUpstream() ([]string, error) { - pushables, err := RevList(entity, RevListOptions{ - Ref1: "@{u}", - Ref2: "HEAD", - }) - if err != nil { - return make([]string, 0), nil - } - return pushables, nil -} diff --git a/pkg/git/cmd-add.go b/pkg/git/cmd-add.go index 649bad2..e7faff2 100644 --- a/pkg/git/cmd-add.go +++ b/pkg/git/cmd-add.go @@ -25,31 +25,31 @@ type AddOptions struct { } // Add is a wrapper function for "git add" command -func Add(entity *RepoEntity, file *File, option AddOptions) error { +func Add(e *RepoEntity, file *File, option AddOptions) error { addCmdMode = addCmdModeNative if option.Update || option.Force || option.DryRun { addCmdMode = addCmdModeLegacy } switch addCmdMode { case addCmdModeLegacy: - err := addWithGit(entity, file, option) + err := addWithGit(e, file, option) return err case addCmdModeNative: - err := addWithGoGit(entity, file) + err := addWithGoGit(e, file) return err } return errors.New("Unhandled add operation") } // AddAll function is the wrapper of "git add ." command -func AddAll(entity *RepoEntity, option AddOptions) error { +func AddAll(e *RepoEntity, 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) + out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { log.Warn("Error while add command") return errors.New(out + "\n" + err.Error()) @@ -57,7 +57,7 @@ func AddAll(entity *RepoEntity, option AddOptions) error { return nil } -func addWithGit(entity *RepoEntity, file *File, option AddOptions) error { +func addWithGit(e *RepoEntity, file *File, option AddOptions) error { args := make([]string, 0) args = append(args, addCommand) args = append(args, file.Name) @@ -70,7 +70,7 @@ func addWithGit(entity *RepoEntity, file *File, option AddOptions) error { if option.DryRun { args = append(args, "--dry-run") } - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { log.Warn("Error while add command") return errors.New(out + "\n" + err.Error()) @@ -78,8 +78,8 @@ func addWithGit(entity *RepoEntity, file *File, option AddOptions) error { return nil } -func addWithGoGit(entity *RepoEntity, file *File) error { - w, err := entity.Repository.Worktree() +func addWithGoGit(e *RepoEntity, file *File) error { + w, err := e.Repository.Worktree() if err != nil { return err } diff --git a/pkg/git/cmd-commit.go b/pkg/git/cmd-commit.go index 8ea5bfe..6e836ab 100644 --- a/pkg/git/cmd-commit.go +++ b/pkg/git/cmd-commit.go @@ -28,24 +28,24 @@ type CommitOptions struct { } // CommitCommand -func CommitCommand(entity *RepoEntity, options CommitOptions) (err error) { +func CommitCommand(e *RepoEntity, options CommitOptions) (err error) { // here we configure commit operation // default mode is go-git (this may be configured) commitCmdMode = commitCmdModeNative switch commitCmdMode { case commitCmdModeLegacy: - err = commitWithGit(entity, options) + err = commitWithGit(e, options) return err case commitCmdModeNative: - err = commitWithGoGit(entity, options) + err = commitWithGoGit(e, options) return err } return errors.New("Unhandled commit operation") } // commitWithGit is simply a bare git commit -m <msg> command which is flexible -func commitWithGit(entity *RepoEntity, options CommitOptions) (err error) { +func commitWithGit(e *RepoEntity, options CommitOptions) (err error) { args := make([]string, 0) args = append(args, commitCommand) args = append(args, "-m") @@ -53,18 +53,18 @@ func commitWithGit(entity *RepoEntity, options CommitOptions) (err error) { if len(options.CommitMsg) > 0 { args = append(args, options.CommitMsg) } - if err := GenericGitCommand(entity.AbsPath, args); err != nil { + if err := GenericGitCommand(e.AbsPath, args); err != nil { log.Warn("Error at git command (commit)") + e.Refresh() return err } // till this step everything should be ok - err = entity.Refresh() - return err + return e.Refresh() } // commitWithGoGit is the primary commit method -func commitWithGoGit(entity *RepoEntity, options CommitOptions) (err error) { - config, err := entity.Repository.Config() +func commitWithGoGit(e *RepoEntity, options CommitOptions) (err error) { + config, err := e.Repository.Config() if err != nil { return err } @@ -78,16 +78,16 @@ func commitWithGoGit(entity *RepoEntity, options CommitOptions) (err error) { }, } - w, err := entity.Repository.Worktree() + w, err := e.Repository.Worktree() if err != nil { return err } _, err = w.Commit(options.CommitMsg, opt) if err != nil { + e.Refresh() return err } // till this step everything should be ok - err = entity.Refresh() - return err + return e.Refresh() } diff --git a/pkg/git/cmd-config.go b/pkg/git/cmd-config.go index dfbf661..06ba74b 100644 --- a/pkg/git/cmd-config.go +++ b/pkg/git/cmd-config.go @@ -34,24 +34,24 @@ const ( ) // Config -func Config(entity *RepoEntity, options ConfigOptions) (value string, err error) { +func Config(e *RepoEntity, options ConfigOptions) (value string, err error) { // here we configure config operation // default mode is go-git (this may be configured) configCmdMode = configCmdModeLegacy switch configCmdMode { case configCmdModeLegacy: - value, err = configWithGit(entity, options) + value, err = configWithGit(e, options) return value, err case configCmdModeNative: - value, err = configWithGoGit(entity, options) + value, err = configWithGoGit(e, options) return value, err } return value, errors.New("Unhandled config operation") } // configWithGit is simply a bare git commit -m <msg> command which is flexible -func configWithGit(entity *RepoEntity, options ConfigOptions) (value string, err error) { +func configWithGit(e *RepoEntity, options ConfigOptions) (value string, err error) { args := make([]string, 0) args = append(args, configCommand) if len(string(options.Site)) > 0 { @@ -60,7 +60,7 @@ func configWithGit(entity *RepoEntity, options ConfigOptions) (value string, err args = append(args, "--get") args = append(args, options.Section+"."+options.Option) // parse options to command line arguments - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { return out, err } @@ -69,25 +69,23 @@ func configWithGit(entity *RepoEntity, options ConfigOptions) (value string, err } // commitWithGoGit is the primary commit method -func configWithGoGit(entity *RepoEntity, options ConfigOptions) (value string, err error) { +func configWithGoGit(e *RepoEntity, options ConfigOptions) (value string, err error) { // TODO: add global search - config, err := entity.Repository.Config() + config, err := e.Repository.Config() if err != nil { return value, err } - value = config.Raw.Section(options.Section).Option(options.Option) - return value, nil + return config.Raw.Section(options.Section).Option(options.Option), nil } // AddConfig -func AddConfig(entity *RepoEntity, options ConfigOptions, value string) (err error) { - err = addConfigWithGit(entity, options, value) - return err +func AddConfig(e *RepoEntity, options ConfigOptions, value string) (err error) { + return addConfigWithGit(e, options, value) } // addConfigWithGit is simply a bare git config --add <option> command which is flexible -func addConfigWithGit(entity *RepoEntity, options ConfigOptions, value string) (err error) { +func addConfigWithGit(e *RepoEntity, options ConfigOptions, value string) (err error) { args := make([]string, 0) args = append(args, configCommand) if len(string(options.Site)) > 0 { @@ -98,10 +96,10 @@ func addConfigWithGit(entity *RepoEntity, options ConfigOptions, value string) ( if len(value) > 0 { args = append(args, value) } - if err := GenericGitCommand(entity.AbsPath, args); err != nil { + if err := GenericGitCommand(e.AbsPath, args); err != nil { log.Warn("Error at git command (config)") return err } // till this step everything should be ok - return entity.Refresh() + return e.Refresh() } diff --git a/pkg/git/cmd-diff.go b/pkg/git/cmd-diff.go new file mode 100644 index 0000000..f9fcb3f --- /dev/null +++ b/pkg/git/cmd-diff.go @@ -0,0 +1,89 @@ +package git + +import ( + "errors" + + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" +) + +var ( + diffCmdMode string + + diffCommand = "diff" + diffCmdModeLegacy = "git" + diffCmdModeNative = "go-git" +) + +// 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) { + diffCmdMode = diffCmdModeNative + + switch diffCmdMode { + case diffCmdModeLegacy: + return diffWithGit(e, hash) + case diffCmdModeNative: + return diffWithGoGit(e, hash) + } + return diff, errors.New("Unhandled diff operation") +} + +func diffWithGit(e *RepoEntity, hash string) (diff string, err error) { + return diff, nil +} + +func diffWithGoGit(e *RepoEntity, hash string) (diff string, err error) { + currentCommitIndex := 0 + for i, cs := range e.Commits { + if cs.Hash == hash { + currentCommitIndex = i + } + } + if len(e.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, + }) + if err != nil { + return "", err + } + + currentCommit, err := commits.Next() + if err != nil { + return "", err + } + currentTree, err := currentCommit.Tree() + if err != nil { + return diff, err + } + + prevCommit, err := commits.Next() + if err != nil { + return "", err + } + prevTree, err := prevCommit.Tree() + if err != nil { + return diff, err + } + + changes, err := prevTree.Diff(currentTree) + if err != nil { + return "", err + } + + // here we collect the actual diff + for _, c := range changes { + patch, err := c.Patch() + if err != nil { + break + } + diff = diff + patch.String() + "\n" + } + return diff, nil +} diff --git a/pkg/git/cmd-fetch.go b/pkg/git/cmd-fetch.go index d1d6abb..e7a60f7 100644 --- a/pkg/git/cmd-fetch.go +++ b/pkg/git/cmd-fetch.go @@ -39,7 +39,7 @@ type FetchOptions struct { // 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) { +func Fetch(e *RepoEntity, options FetchOptions) (err error) { // here we configure fetch operation // default mode is go-git (this may be configured) fetchCmdMode = fetchCmdModeNative @@ -50,18 +50,18 @@ func Fetch(entity *RepoEntity, options FetchOptions) (err error) { } switch fetchCmdMode { case fetchCmdModeLegacy: - err = fetchWithGit(entity, options) + err = fetchWithGit(e, 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 entity.Branch == nil { + if e.Branch == nil { refspec = "+refs/heads/*:refs/remotes/origin/*" } else { - refspec = "+" + "refs/heads/" + entity.Branch.Name + ":" + "/refs/remotes/" + entity.Remote.Branch.Name + refspec = "+" + "refs/heads/" + e.Branch.Name + ":" + "/refs/remotes/" + e.Remote.Branch.Name } - err = fetchWithGoGit(entity, options, refspec) + err = fetchWithGoGit(e, options, refspec) return err } return nil @@ -70,7 +70,7 @@ func Fetch(entity *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(entity *RepoEntity, options FetchOptions) (err error) { +func fetchWithGit(e *RepoEntity, options FetchOptions) (err error) { args := make([]string, 0) args = append(args, fetchCommand) // parse options to command line arguments @@ -86,12 +86,13 @@ func fetchWithGit(entity *RepoEntity, options FetchOptions) (err error) { if options.DryRun { args = append(args, "--dry-run") } - if err := GenericGitCommand(entity.AbsPath, args); err != nil { + if err := GenericGitCommand(e.AbsPath, args); err != nil { log.Warn("Error at git command (fetch)") return err } + e.SetState(Success) // till this step everything should be ok - return entity.Refresh() + return e.Refresh() } // fetchWithGoGit is the primary fetch method and refspec is the main feature. @@ -100,7 +101,7 @@ func fetchWithGit(entity *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(entity *RepoEntity, options FetchOptions, refspec string) (err error) { +func fetchWithGoGit(e *RepoEntity, options FetchOptions, refspec string) (err error) { opt := &git.FetchOptions{ RemoteName: options.RemoteName, RefSpecs: []config.RefSpec{config.RefSpec(refspec)}, @@ -108,7 +109,7 @@ func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (e } // 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) + protocol, err := authProtocol(e.Remote) if err != nil { return err } @@ -122,7 +123,7 @@ func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (e } } - err = entity.Repository.Fetch(opt) + err = e.Repository.Fetch(opt) if err != nil { if err == git.NoErrAlreadyUpToDate { // Already up-to-date @@ -130,10 +131,10 @@ func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (e // 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] + rp := e.Remote.RefSpecs[0] if fetchTryCount < fetchMaxTry { fetchTryCount++ - fetchWithGoGit(entity, options, rp) + fetchWithGoGit(e, options, rp) } else { return err } @@ -145,6 +146,8 @@ func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (e return err } } + + e.SetState(Success) // till this step everything should be ok - return entity.Refresh() + return e.Refresh() } diff --git a/pkg/git/cmd-merge.go b/pkg/git/cmd-merge.go index 984bc4b..e89761d 100644 --- a/pkg/git/cmd-merge.go +++ b/pkg/git/cmd-merge.go @@ -18,7 +18,7 @@ type MergeOptions struct { // Merge incorporates changes from the named commits or branches into the // current branch -func Merge(entity *RepoEntity, options MergeOptions) error { +func Merge(e *RepoEntity, options MergeOptions) error { args := make([]string, 0) args = append(args, mergeCommand) if len(options.BranchName) > 0 { @@ -30,10 +30,10 @@ func Merge(entity *RepoEntity, options MergeOptions) error { if options.NoStat { args = append(args, "-n") } - if err := GenericGitCommand(entity.AbsPath, args); err != nil { + if err := GenericGitCommand(e.AbsPath, args); err != nil { log.Warn("Error while merging") return err } - entity.Refresh() - return nil + e.SetState(Success) + return e.Refresh() } diff --git a/pkg/git/cmd-pull.go b/pkg/git/cmd-pull.go index a493b19..2c28d3a 100644 --- a/pkg/git/cmd-pull.go +++ b/pkg/git/cmd-pull.go @@ -4,6 +4,7 @@ import ( log "github.com/sirupsen/logrus" "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" ) @@ -33,7 +34,7 @@ type PullOptions struct { } // Pull ncorporates changes from a remote repository into the current branch. -func Pull(entity *RepoEntity, options PullOptions) (err error) { +func Pull(e *RepoEntity, options PullOptions) (err error) { // here we configure pull operation // default mode is go-git (this may be configured) pullCmdMode = pullCmdModeNative @@ -41,16 +42,16 @@ func Pull(entity *RepoEntity, options PullOptions) (err error) { switch pullCmdMode { case pullCmdModeLegacy: - err = pullWithGit(entity, options) + err = pullWithGit(e, options) return err case pullCmdModeNative: - err = pullWithGoGit(entity, options) + err = pullWithGoGit(e, options) return err } return nil } -func pullWithGit(entity *RepoEntity, options PullOptions) (err error) { +func pullWithGit(e *RepoEntity, options PullOptions) (err error) { args := make([]string, 0) args = append(args, pullCommand) // parse options to command line arguments @@ -60,14 +61,15 @@ func pullWithGit(entity *RepoEntity, options PullOptions) (err error) { if options.Force { args = append(args, "-f") } - if err := GenericGitCommand(entity.AbsPath, args); err != nil { + if err := GenericGitCommand(e.AbsPath, args); err != nil { log.Warn("Error at git command (pull)") return err } - return entity.Refresh() + e.SetState(Success) + return e.Refresh() } -func pullWithGoGit(entity *RepoEntity, options PullOptions) (err error) { +func pullWithGoGit(e *RepoEntity, options PullOptions) (err error) { opt := &git.PullOptions{ RemoteName: options.RemoteName, SingleBranch: options.SingleBranch, @@ -79,7 +81,7 @@ func pullWithGoGit(entity *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 := entity.authProtocol(entity.Remote) + protocol, err := authProtocol(e.Remote) if err != nil { return err } @@ -92,13 +94,23 @@ func pullWithGoGit(entity *RepoEntity, options PullOptions) (err error) { return ErrInvalidAuthMethod } } - w, err := entity.Repository.Worktree() + w, err := e.Repository.Worktree() if err != nil { return err } err = w.Pull(opt) if err != nil { - return err + if err == git.NoErrAlreadyUpToDate { + // Already up-to-date + log.Warn(err.Error()) + } else if err == transport.ErrAuthenticationRequired { + log.Warn(err.Error()) + return ErrAuthenticationRequired + } else { + log.Warn(err.Error()) + return err + } } - return entity.Refresh() + e.SetState(Success) + return e.Refresh() } diff --git a/pkg/git/cmd-reset.go b/pkg/git/cmd-reset.go index 2e6ed91..cc5f0f9 100644 --- a/pkg/git/cmd-reset.go +++ b/pkg/git/cmd-reset.go @@ -47,12 +47,12 @@ const ( ) // Reset is the wrapper of "git reset" command -func Reset(entity *RepoEntity, file *File, option ResetOptions) error { - resetCmdMode = addCmdModeLegacy +func Reset(e *RepoEntity, file *File, option ResetOptions) error { + resetCmdMode = resetCmdModeLegacy switch resetCmdMode { case resetCmdModeLegacy: - err := resetWithGit(entity, file, option) + err := resetWithGit(e, file, option) return err case resetCmdModeNative: @@ -60,7 +60,7 @@ func Reset(entity *RepoEntity, file *File, option ResetOptions) error { return errors.New("Unhandled reset operation") } -func resetWithGit(entity *RepoEntity, file *File, option ResetOptions) error { +func resetWithGit(e *RepoEntity, file *File, option ResetOptions) error { args := make([]string, 0) args = append(args, resetCommand) @@ -69,7 +69,7 @@ func resetWithGit(entity *RepoEntity, file *File, option ResetOptions) error { if len(option.Rtype) > 0 { args = append(args, "--"+string(option.Rtype)) } - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { log.Warn("Error while reset command") return errors.New(out + "\n" + err.Error()) @@ -78,27 +78,27 @@ func resetWithGit(entity *RepoEntity, file *File, option ResetOptions) error { } // ResetAll resets the changes in a repository, should be used wise -func ResetAll(entity *RepoEntity, option ResetOptions) error { +func ResetAll(e *RepoEntity, option ResetOptions) error { resetCmdMode = addCmdModeNative switch resetCmdMode { case resetCmdModeLegacy: - err := resetAllWithGit(entity, option) + err := resetAllWithGit(e, option) return err case resetCmdModeNative: - err := resetAllWithGoGit(entity, option) + err := resetAllWithGoGit(e, option) return err } return errors.New("Unhandled reset operation") } -func resetAllWithGit(entity *RepoEntity, option ResetOptions) error { +func resetAllWithGit(e *RepoEntity, option ResetOptions) error { args := make([]string, 0) args = append(args, resetCommand) if len(option.Rtype) > 0 { args = append(args, "--"+string(option.Rtype)) } - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { log.Warn("Error while add command") return errors.New(out + "\n" + err.Error()) @@ -106,8 +106,8 @@ func resetAllWithGit(entity *RepoEntity, option ResetOptions) error { return nil } -func resetAllWithGoGit(entity *RepoEntity, option ResetOptions) error { - w, err := entity.Repository.Worktree() +func resetAllWithGoGit(e *RepoEntity, option ResetOptions) error { + w, err := e.Repository.Worktree() if err != nil { return err } diff --git a/pkg/git/cmd-rev-list.go b/pkg/git/cmd-rev-list.go index 664d69f..e68e82a 100644 --- a/pkg/git/cmd-rev-list.go +++ b/pkg/git/cmd-rev-list.go @@ -19,14 +19,14 @@ type RevListOptions struct { // 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) { +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(entity.AbsPath, args) + out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { log.Warn("Error while rev-list command") return []string{out}, err diff --git a/pkg/git/cmd-stash.go b/pkg/git/cmd-stash.go index 9d813a6..a786833 100644 --- a/pkg/git/cmd-stash.go +++ b/pkg/git/cmd-stash.go @@ -19,11 +19,11 @@ type StashedItem struct { EntityPath string } -func stashGet(entity *RepoEntity, option string) string { +func stashGet(e *RepoEntity, option string) string { args := make([]string, 0) args = append(args, stashCommand) args = append(args, option) - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { log.Warn("Error while stash command") return "?" @@ -31,9 +31,9 @@ func stashGet(entity *RepoEntity, option string) string { return out } -func (entity *RepoEntity) loadStashedItems() error { - entity.Stasheds = make([]*StashedItem, 0) - output := stashGet(entity, "list") +func (e *RepoEntity) loadStashedItems() error { + e.Stasheds = make([]*StashedItem, 0) + output := stashGet(e, "list") stashIDRegex := regexp.MustCompile(`stash@{[\d]+}:`) stashIDRegexInt := regexp.MustCompile(`[\d]+`) stashBranchRegex := regexp.MustCompile(`[\w]+: `) @@ -63,24 +63,24 @@ func (entity *RepoEntity) loadStashedItems() error { // trim hash desc := stashHashRegex.Split(trimmed, 2)[1][1:] - entity.Stasheds = append(entity.Stasheds, &StashedItem{ + e.Stasheds = append(e.Stasheds, &StashedItem{ StashID: i, BranchName: branchName, Hash: hash, Description: desc, - EntityPath: entity.AbsPath, + EntityPath: e.AbsPath, }) } return nil } // Stash is the wrapper of convetional "git stash" command -func (entity *RepoEntity) Stash() (output string, err error) { +func (e *RepoEntity) Stash() (output string, err error) { args := make([]string, 0) args = append(args, stashCommand) - output, err = GenericGitCommandWithErrorOutput(entity.AbsPath, args) - entity.Refresh() + output, err = GenericGitCommandWithErrorOutput(e.AbsPath, args) + e.Refresh() return output, err } diff --git a/pkg/git/cmd-status.go b/pkg/git/cmd-status.go index 508a27c..eec9b57 100644 --- a/pkg/git/cmd-status.go +++ b/pkg/git/cmd-status.go @@ -18,44 +18,12 @@ var ( statusCmdModeNative = "go-git" ) -// 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 byte - -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 { +func shortStatus(e *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) + out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { log.Warn("Error while status command") return "?" @@ -63,23 +31,23 @@ func shortStatus(entity *RepoEntity, option string) string { return out } -func Status(entity *RepoEntity) ([]*File, error) { +func Status(e *RepoEntity) ([]*File, error) { statusCmdMode = statusCmdModeNative switch statusCmdMode { case statusCmdModeLegacy: - return statusWithGit(entity) + return statusWithGit(e) case statusCmdModeNative: - return statusWithGoGit(entity) + return statusWithGoGit(e) } return nil, errors.New("Unhandled status operation") } // LoadFiles function simply commands a git status and collects output in a // structured way -func statusWithGit(entity *RepoEntity) ([]*File, error) { +func statusWithGit(e *RepoEntity) ([]*File, error) { files := make([]*File, 0) - output := shortStatus(entity, "--untracked-files=all") + output := shortStatus(e, "--untracked-files=all") if len(output) == 0 { return files, nil } @@ -92,7 +60,7 @@ func statusWithGit(entity *RepoEntity) ([]*File, error) { files = append(files, &File{ Name: path, - AbsPath: entity.AbsPath + string(os.PathSeparator) + path, + AbsPath: e.AbsPath + string(os.PathSeparator) + path, X: FileStatus(x), Y: FileStatus(y), }) @@ -101,9 +69,9 @@ func statusWithGit(entity *RepoEntity) ([]*File, error) { return files, nil } -func statusWithGoGit(entity *RepoEntity) ([]*File, error) { +func statusWithGoGit(e *RepoEntity) ([]*File, error) { files := make([]*File, 0) - w, err := entity.Repository.Worktree() + w, err := e.Repository.Worktree() if err != nil { return files, err } @@ -114,7 +82,7 @@ func statusWithGoGit(entity *RepoEntity) ([]*File, error) { for k, v := range s { files = append(files, &File{ Name: k, - AbsPath: entity.AbsPath + string(os.PathSeparator) + k, + AbsPath: e.AbsPath + string(os.PathSeparator) + k, X: FileStatus(v.Staging), Y: FileStatus(v.Worktree), }) @@ -122,13 +90,3 @@ func statusWithGoGit(entity *RepoEntity) ([]*File, error) { sort.Sort(filesAlphabetical(files)) 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 index b78b501..a84f489 100644 --- a/pkg/git/cmd.go +++ b/pkg/git/cmd.go @@ -72,9 +72,9 @@ func GitShowDate(repoPath, hash string) string { } // StatusWithGit returns the plaintext short status of the repo -func (entity *RepoEntity) StatusWithGit() string { +func (e *RepoEntity) StatusWithGit() string { args := []string{"status"} - status, err := helpers.RunCommandWithOutput(entity.AbsPath, "git", args) + status, err := helpers.RunCommandWithOutput(e.AbsPath, "git", args) if err != nil { return "?" } diff --git a/pkg/git/commit.go b/pkg/git/commit.go index 8694f1a..716ccba 100644 --- a/pkg/git/commit.go +++ b/pkg/git/commit.go @@ -5,7 +5,6 @@ import ( log "github.com/sirupsen/logrus" "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/object" ) @@ -34,49 +33,37 @@ const ( // NextCommit iterates over next commit of a branch // TODO: the commits entites can tied to branch instead ot the repository -func (entity *RepoEntity) NextCommit() error { - currentCommitIndex := entity.findCurrentCommitIndex() - if currentCommitIndex == len(entity.Commits)-1 { - entity.Commit = entity.Commits[0] - return nil - } - entity.Commit = entity.Commits[currentCommitIndex+1] - return nil +func (e *RepoEntity) NextCommit() { + e.Commit = e.Commits[(e.currentCommitIndex()+1)%len(e.Commits)] } // PreviousCommit iterates to opposite direction -func (entity *RepoEntity) PreviousCommit() error { - currentCommitIndex := entity.findCurrentCommitIndex() - if currentCommitIndex == 0 { - entity.Commit = entity.Commits[len(entity.Commits)-1] - return nil - } - entity.Commit = entity.Commits[currentCommitIndex-1] - return nil +func (e *RepoEntity) PreviousCommit() { + e.Commit = e.Commits[(len(e.Commits)+e.currentCommitIndex()-1)%len(e.Commits)] } // returns the active commit index -func (entity *RepoEntity) findCurrentCommitIndex() int { - currentCommitIndex := 0 - for i, cs := range entity.Commits { - if cs.Hash == entity.Commit.Hash { - currentCommitIndex = i +func (e *RepoEntity) currentCommitIndex() int { + cix := 0 + for i, c := range e.Commits { + if c.Hash == e.Commit.Hash { + cix = i } } - return currentCommitIndex + return cix } // loads the local commits by simply using git log way. ALso, gets the upstream // diff commits -func (entity *RepoEntity) loadCommits() error { - r := entity.Repository - entity.Commits = make([]*Commit, 0) +func (e *RepoEntity) loadCommits() error { + r := e.Repository + e.Commits = make([]*Commit, 0) ref, err := r.Head() if err != nil { log.Trace("Cannot get HEAD " + err.Error()) return err } - + // git log first cIter, err := r.Log(&git.LogOptions{ From: ref.Hash(), Order: git.LogOrderCommitterTime, @@ -86,19 +73,13 @@ func (entity *RepoEntity) loadCommits() error { return err } defer cIter.Close() - rmcs, err := entity.pullDiffsToUpstream() - if err != nil { - log.Trace("git rev-list failed " + err.Error()) - return err - } - for _, rmc := range rmcs { - entity.Commits = append(entity.Commits, rmc) - } - lcs, err := entity.pushDiffsToUpstream() - if err != nil { - log.Trace("git rev-list failed " + err.Error()) - return err - } + // find commits that fetched from upstream but not merged commits + rmcs, _ := e.pullDiffsToUpstream() + e.Commits = append(e.Commits, rmcs...) + + // find commits that not pushed to upstream + lcs, _ := e.pushDiffsToUpstream() + // ... just iterates over the commits err = cIter.ForEach(func(c *object.Commit) error { re := regexp.MustCompile(`\r?\n`) @@ -115,70 +96,50 @@ func (entity *RepoEntity) loadCommits() error { Time: c.Author.When.String(), CommitType: cmType, } - entity.Commits = append(entity.Commits, commit) - + e.Commits = append(e.Commits, commit) return nil }) if err != nil { return err } - // entity.Commits = commits return nil } -// Diff function returns the diff to previous commit detail of the given has -// of a specific commit -func (entity *RepoEntity) Diff(hash string) (diff string, err error) { - - currentCommitIndex := 0 - for i, cs := range entity.Commits { - if cs.Hash == hash { - currentCommitIndex = i - } - } - if len(entity.Commits)-currentCommitIndex <= 1 { - return "there is no diff", nil - } - - // maybe we dont need to log the repo again? - commits, err := entity.Repository.Log(&git.LogOptions{ - From: plumbing.NewHash(entity.Commit.Hash), - Order: git.LogOrderCommitterTime, +// this function creates the commit entities according to active branchs diffs +// to *its* configured upstream +func (e *RepoEntity) pullDiffsToUpstream() ([]*Commit, error) { + remoteCommits := make([]*Commit, 0) + pullables, err := RevList(e, RevListOptions{ + Ref1: "HEAD", + Ref2: "@{u}", }) if err != nil { - return "", err - } - - currentCommit, err := commits.Next() - if err != nil { - return "", err - } - currentTree, err := currentCommit.Tree() - if err != nil { - return diff, err - } - - prevCommit, err := commits.Next() - if err != nil { - return "", err - } - prevTree, err := prevCommit.Tree() - if err != nil { - return diff, err + // possibly found nothing or no upstream set + } else { + re := regexp.MustCompile(`\r?\n`) + for _, s := range pullables { + commit := &Commit{ + Hash: s, + Author: GitShowEmail(e.AbsPath, s), + Message: re.ReplaceAllString(GitShowBody(e.AbsPath, s), " "), + Time: GitShowDate(e.AbsPath, s), + CommitType: RemoteCommit, + } + remoteCommits = append(remoteCommits, commit) + } } + return remoteCommits, nil +} - changes, err := prevTree.Diff(currentTree) +// 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{ + Ref1: "@{u}", + Ref2: "HEAD", + }) if err != nil { - return "", err - } - - // here we collect the actual diff - for _, c := range changes { - patch, err := c.Patch() - if err != nil { - break - } - diff = diff + patch.String() + "\n" + return make([]string, 0), nil } - return diff, nil + return pushables, nil } diff --git a/pkg/git/file.go b/pkg/git/file.go new file mode 100644 index 0000000..1e086bc --- /dev/null +++ b/pkg/git/file.go @@ -0,0 +1,52 @@ +package git + +import ( + "strings" + + log "github.com/sirupsen/logrus" +) + +// 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 byte + +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 = '!' +) + +// Diff is a wrapper of "git diff" command for a file to compare with HEAD rev +func (f *File) Diff() (output string, err error) { + args := make([]string, 0) + args = append(args, "diff") + args = append(args, "HEAD") + args = append(args, f.Name) + output, err = GenericGitCommandWithErrorOutput(strings.TrimSuffix(f.AbsPath, f.Name), args) + if err != nil { + log.Warn(err) + } + return output, err +} diff --git a/pkg/git/job-queue.go b/pkg/git/job-queue.go index e95250a..3eee605 100644 --- a/pkg/git/job-queue.go +++ b/pkg/git/job-queue.go @@ -11,7 +11,7 @@ type JobQueue struct { // CreateJobQueue creates a jobqueue struct and initialize its slice then return // its pointer -func CreateJobQueue() (jobQueue *JobQueue) { +func CreateJobQueue() (jq *JobQueue) { s := make([]*Job, 0) return &JobQueue{ series: s, @@ -19,28 +19,28 @@ func CreateJobQueue() (jobQueue *JobQueue) { } // AddJob adds a job to the queue -func (jobQueue *JobQueue) AddJob(j *Job) error { - for _, job := range jobQueue.series { +func (jq *JobQueue) AddJob(j *Job) error { + for _, job := range jq.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 + jq.series = append(jq.series, nil) + copy(jq.series[1:], jq.series[0:]) + jq.series[0] = j return nil } // StartNext starts the next job in the queue -func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) { +func (jq *JobQueue) StartNext() (j *Job, finished bool, err error) { finished = false - if len(jobQueue.series) < 1 { + if len(jq.series) < 1 { finished = true return nil, finished, nil } - i := len(jobQueue.series) - 1 - lastJob := jobQueue.series[i] - jobQueue.series = jobQueue.series[:i] + i := len(jq.series) - 1 + lastJob := jq.series[i] + jq.series = jq.series[:i] if err = lastJob.start(); err != nil { return lastJob, finished, err } @@ -49,11 +49,11 @@ func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) { // RemoveFromQueue deletes the given entity and its job from the queue // TODO: it is not safe if the job has been started -func (jobQueue *JobQueue) RemoveFromQueue(entity *RepoEntity) error { +func (jq *JobQueue) RemoveFromQueue(entity *RepoEntity) error { removed := false - for i, job := range jobQueue.series { + for i, job := range jq.series { if job.Entity.RepoID == entity.RepoID { - jobQueue.series = append(jobQueue.series[:i], jobQueue.series[i+1:]...) + jq.series = append(jq.series[:i], jq.series[i+1:]...) removed = true } } @@ -66,9 +66,9 @@ func (jobQueue *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 (jobQueue *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) { +func (jq *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) { inTheQueue = false - for _, job := range jobQueue.series { + for _, job := range jq.series { if job.Entity.RepoID == entity.RepoID { inTheQueue = true jt = job.JobType diff --git a/pkg/git/job.go b/pkg/git/job.go index 7dc4126..c64086e 100644 --- a/pkg/git/job.go +++ b/pkg/git/job.go @@ -25,54 +25,47 @@ const ( ) // starts the job -func (job *Job) start() error { - job.Entity.State = Working +func (j *Job) start() error { + j.Entity.SetState(Working) // TODO: Handle errors? // TOOD: Better implementation required - switch mode := job.JobType; mode { + switch mode := j.JobType; mode { case FetchJob: var opts FetchOptions - if job.Options != nil { - opts = job.Options.(FetchOptions) + if j.Options != nil { + opts = j.Options.(FetchOptions) } else { opts = FetchOptions{ - RemoteName: job.Entity.Remote.Name, + RemoteName: j.Entity.Remote.Name, } } - if err := Fetch(job.Entity, opts); err != nil { - job.Entity.State = Fail + if err := Fetch(j.Entity, opts); err != nil { + j.Entity.SetState(Fail) return err } case PullJob: - var opts FetchOptions - if job.Options != nil { - opts = job.Options.(FetchOptions) + var opts PullOptions + if j.Options != nil { + opts = j.Options.(PullOptions) } else { - opts = FetchOptions{ - RemoteName: job.Entity.Remote.Name, + opts = PullOptions{ + RemoteName: j.Entity.Remote.Name, } } - if err := Fetch(job.Entity, opts); err != nil { - job.Entity.State = Fail + if err := Pull(j.Entity, opts); err != nil { + j.Entity.SetState(Fail) return err } - if err := Merge(job.Entity, MergeOptions{ - BranchName: job.Entity.Remote.Branch.Name, - }); err != nil { - job.Entity.State = Fail - return nil - } case MergeJob: - if err := Merge(job.Entity, MergeOptions{ - BranchName: job.Entity.Remote.Branch.Name, + if err := Merge(j.Entity, MergeOptions{ + BranchName: j.Entity.Remote.Branch.Name, }); err != nil { - job.Entity.State = Fail + j.Entity.SetState(Fail) return nil } default: - job.Entity.State = Available + j.Entity.SetState(Available) return nil } - job.Entity.State = Success return nil } diff --git a/pkg/git/remote.go b/pkg/git/remote.go index 27f627b..72c8ddd 100644 --- a/pkg/git/remote.go +++ b/pkg/git/remote.go @@ -16,48 +16,38 @@ type Remote struct { } // NextRemote iterates over next branch of a remote -func (entity *RepoEntity) NextRemote() error { - currentRemoteIndex := entity.findCurrentRemoteIndex() - if currentRemoteIndex == len(entity.Remotes)-1 { - entity.Remote = entity.Remotes[0] - } else { - entity.Remote = entity.Remotes[currentRemoteIndex+1] - } - err := entity.Remote.SyncBranches(entity.Branch.Name) - return err +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) } // PreviousRemote iterates over previous branch of a remote -func (entity *RepoEntity) PreviousRemote() error { - currentRemoteIndex := entity.findCurrentRemoteIndex() - if currentRemoteIndex == 0 { - entity.Remote = entity.Remotes[len(entity.Remotes)-1] - } else { - entity.Remote = entity.Remotes[currentRemoteIndex-1] - } - err := entity.Remote.SyncBranches(entity.Branch.Name) - return err +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) } // returns the active remote index -func (entity *RepoEntity) findCurrentRemoteIndex() int { - currentRemoteIndex := 0 - for i, remote := range entity.Remotes { - if remote.Name == entity.Remote.Name { - currentRemoteIndex = i +func (e *RepoEntity) currentRemoteIndex() int { + cix := 0 + for i, remote := range e.Remotes { + if remote.Name == e.Remote.Name { + cix = i } } - return currentRemoteIndex + return cix } // 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 (entity *RepoEntity) loadRemotes() error { - r := entity.Repository - entity.Remotes = make([]*Remote, 0) +func (e *RepoEntity) loadRemotes() error { + r := e.Repository + e.Remotes = make([]*Remote, 0) - remotes, err := r.Remotes() - for _, rm := range remotes { + rms, err := r.Remotes() + for _, rm := range rms { rfs := make([]string, 0) for _, rf := range rm.Config().Fetch { rfs = append(rfs, string(rf)) @@ -67,11 +57,11 @@ func (entity *RepoEntity) loadRemotes() error { URL: rm.Config().URLs, RefSpecs: rfs, } - remote.loadRemoteBranches(entity) + remote.loadRemoteBranches(e) if len(remote.Branches) > 0 { remote.Branch = remote.Branches[0] } - entity.Remotes = append(entity.Remotes, remote) + e.Remotes = append(e.Remotes, remote) } if err != nil { @@ -82,8 +72,8 @@ func (entity *RepoEntity) loadRemotes() error { } // SyncBranches sets the remote branch according to repository's active branch -func (remote *Remote) SyncBranches(branchName string) error { - if err := remote.switchRemoteBranch(remote.Name + "/" + branchName); err != nil { +func (r *Remote) SyncBranches(branchName string) error { + if err := r.switchRemoteBranch(r.Name + "/" + branchName); err != nil { // probably couldn't find, but its ok. } return nil diff --git a/pkg/git/remotebranch.go b/pkg/git/remotebranch.go index 8ab737f..207f5a9 100644 --- a/pkg/git/remotebranch.go +++ b/pkg/git/remotebranch.go @@ -14,66 +14,50 @@ import ( type RemoteBranch struct { Name string Reference *plumbing.Reference - Deleted bool } // NextRemoteBranch iterates to the next remote branch -func (remote *Remote) NextRemoteBranch() error { - currentRemoteIndex := remote.findCurrentRemoteBranchIndex() - if currentRemoteIndex == len(remote.Branches)-1 { - remote.Branch = remote.Branches[0] - } else { - remote.Branch = remote.Branches[currentRemoteIndex+1] - } - return nil +func (r *Remote) NextRemoteBranch(e *RepoEntity) error { + r.Branch = r.Branches[(r.currentRemoteBranchIndex()+1)%len(r.Branches)] + return e.Publish(RepositoryUpdated, nil) } // PreviousRemoteBranch iterates to the previous remote branch -func (remote *Remote) PreviousRemoteBranch() error { - currentRemoteIndex := remote.findCurrentRemoteBranchIndex() - if currentRemoteIndex == 0 { - remote.Branch = remote.Branches[len(remote.Branches)-1] - } else { - remote.Branch = remote.Branches[currentRemoteIndex-1] - } - return nil +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) } // returns the active remote branch index -func (remote *Remote) findCurrentRemoteBranchIndex() int { - currentRemoteIndex := 0 - for i, rb := range remote.Branches { - if rb.Reference.Hash() == remote.Branch.Reference.Hash() { - currentRemoteIndex = i +func (r *Remote) currentRemoteBranchIndex() int { + cix := 0 + for i, rb := range r.Branches { + if rb.Reference.Hash() == r.Branch.Reference.Hash() { + cix = i } } - return currentRemoteIndex + return cix } // search for the remote branches of the remote. It takes the go-git's repo // pointer in order to get storer struct -func (remote *Remote) loadRemoteBranches(entity *RepoEntity) error { - remote.Branches = make([]*RemoteBranch, 0) - bs, err := remoteBranchesIter(entity.Repository.Storer) +func (r *Remote) loadRemoteBranches(e *RepoEntity) error { + r.Branches = make([]*RemoteBranch, 0) + bs, err := remoteBranchesIter(e.Repository.Storer) if err != nil { log.Warn("Cannot initiate iterator " + err.Error()) return err } defer bs.Close() err = bs.ForEach(func(b *plumbing.Reference) error { - deleted := false - if strings.Split(b.Name().Short(), "/")[0] == remote.Name { - remote.Branches = append(remote.Branches, &RemoteBranch{ + if strings.Split(b.Name().Short(), "/")[0] == r.Name { + r.Branches = append(r.Branches, &RemoteBranch{ Name: b.Name().Short(), Reference: b, - Deleted: deleted, }) } return nil }) - if err != nil { - return err - } return err } @@ -95,10 +79,10 @@ func remoteBranchesIter(s storer.ReferenceStorer) (storer.ReferenceIter, error) } // switches to the given remote branch -func (remote *Remote) switchRemoteBranch(remoteBranchName string) error { - for _, rb := range remote.Branches { +func (r *Remote) switchRemoteBranch(remoteBranchName string) error { + for _, rb := range r.Branches { if rb.Name == remoteBranchName { - remote.Branch = rb + r.Branch = rb return nil } } diff --git a/pkg/git/repository.go b/pkg/git/repository.go index f37c813..28c95cd 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -3,6 +3,7 @@ package git import ( "errors" "os" + "sync" "time" "github.com/isacikgoz/gitbatch/pkg/helpers" @@ -26,126 +27,187 @@ type RepoEntity struct { Commit *Commit Commits []*Commit Stasheds []*StashedItem - State RepoState + state RepoState + + mutex *sync.RWMutex + listeners map[string][]RepositoryListener +} + +// 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 +type RepositoryEvent struct { + Name string + Data interface{} } // RepoState is the state of the repository for an operation -type RepoState uint8 +type RepoState struct { + State uint8 + Ready bool +} -const ( +var ( // Available implies repo is ready for the operation - Available RepoState = 0 + Available = RepoState{State: 0, Ready: true} // Queued means repo is queued for a operation - Queued RepoState = 1 + Queued = RepoState{State: 1, Ready: false} // Working means an operation is just started for this repository - Working RepoState = 2 + Working = RepoState{State: 2, Ready: false} // Paused is expected when a user interaction is required - Paused RepoState = 3 + Paused = RepoState{State: 3, Ready: true} // Success is the expected outcome of the operation - Success RepoState = 4 + Success = RepoState{State: 4, Ready: true} // Fail is the unexpected outcome of the operation - Fail RepoState = 5 + Fail = RepoState{State: 5, Ready: false} ) -// InitializeRepo initializes a RepoEntity struct with its belongings. -func InitializeRepo(directory string) (entity *RepoEntity, err error) { - entity, err = FastInitializeRepo(directory) +const ( + // This is the repository updated topic + RepositoryUpdated = "repository.updated" +) + +// FastInitializeRepo initializes a RepoEntity struct without its belongings. +func FastInitializeRepo(dir string) (e *RepoEntity, err error) { + f, err := os.Open(dir) if err != nil { return nil, err } - // after we intiate the struct we can fill its values - entity.loadLocalBranches() - entity.loadCommits() - // handle if there is no commit, maybe? - if len(entity.Commits) > 0 { - // select first commit - entity.Commit = entity.Commits[0] - } else { - return entity, errors.New("There is no commit for this repository: " + directory) - } - // lets load remotes this time - entity.loadRemotes() - // set the active branch to repositories HEAD - entity.Branch = entity.getActiveBranch() - if err = entity.loadStashedItems(); err != nil { - // TODO: fix here. + // get status of the file + fstat, _ := f.Stat() + r, err := git.PlainOpen(dir) + if err != nil { + return nil, err } - if len(entity.Remotes) > 0 { - // TODO: tend to take origin/master as default - entity.Remote = entity.Remotes[0] - if entity.Branch == nil { - return nil, errors.New("Unable to find a valid branch") - } - if err = entity.Remote.SyncBranches(entity.Branch.Name); err != nil { - // probably couldn't find, but its ok. - } - } else { - // if there is no remote, this project is totally useless actually - return entity, errors.New("There is no remote for this repository: " + directory) + // 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), } - return entity, nil + return e, nil } -// FastInitializeRepo initializes a RepoEntity struct without its belongings. -func FastInitializeRepo(directory string) (entity *RepoEntity, err error) { - file, err := os.Open(directory) +// InitializeRepo initializes a RepoEntity struct with its belongings. +func InitializeRepo(dir string) (e *RepoEntity, err error) { + e, err = FastInitializeRepo(dir) if err != nil { - log.WithFields(log.Fields{ - "directory": directory, - }).Trace("Cannot open as directory") return nil, err } - fileInfo, err := file.Stat() - if err != nil { - return nil, err + // need nothing extra but loading additional components + return e, e.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 { + return err } - r, err := git.PlainOpen(directory) - if err != nil { - log.WithFields(log.Fields{ - "directory": directory, - }).Trace("Cannot open directory as a git repository") - return nil, err + if err := e.loadCommits(); err != nil { + return err } - entity = &RepoEntity{RepoID: helpers.RandomString(8), - Name: fileInfo.Name(), - AbsPath: directory, - ModTime: fileInfo.ModTime(), - Repository: *r, - State: Available, + if err := e.loadRemotes(); err != nil { + return err + } + if err := e.loadStashedItems(); err != nil { + log.Warn("Cannot load stashes") } - return entity, nil + if reset { + // handle if there is no commit, maybe? + // set commit pointer for repository + if len(e.Commits) > 0 { + // select first commit + e.Commit = e.Commits[0] + } + // set remote pointer for repository + if len(e.Remotes) > 0 { + // TODO: tend to take origin/master as default + e.Remote = e.Remotes[0] + // if couldn't find, its ok. + e.Remote.SyncBranches(e.Branch.Name) + } else { + // if there is no remote, this project is totally useless actually + return errors.New("There is no remote for this repository") + } + } + return nil } // Refresh the belongings of a repositoriy, this function is called right after // fetch/pull/merge operations -func (entity *RepoEntity) Refresh() error { +func (e *RepoEntity) Refresh() error { var err error // error can be ignored since the file already exists when app is loading - if entity.Branch == nil { + // if the RepoEntity is only fast initialized, no need to refresh because + // it won't contain its belongings + if e.Branch == nil { return nil } - file, _ := os.Open(entity.AbsPath) - fileInfo, err := file.Stat() + file, _ := os.Open(e.AbsPath) + fstat, _ := file.Stat() + // re-initialize the go-git repository struct after supposed update + r, err := git.PlainOpen(e.AbsPath) if err != nil { return err } - r, err := git.PlainOpen(entity.AbsPath) - if err != nil { + e.Repository = *r + // modification date may be changed + e.ModTime = fstat.ModTime() + if err := e.loadComponents(false); err != nil { return err } - entity.Repository = *r - entity.ModTime = fileInfo.ModTime() - if err := entity.loadLocalBranches(); err != nil { - return err + // we could send an event data but we don't need for this topic + return e.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() + // add listener to the specific event topic + e.listeners[event] = append(e.listeners[event], listener) +} + +// Emit notifies listeners about the event +func (e *RepoEntity) Publish(eventName string, data interface{}) error { + e.mutex.RLock() + defer e.mutex.RUnlock() + // let's find listeners for this event topic + listeners, ok := e.listeners[eventName] + if !ok { + return nil } - entity.Branch.Clean = entity.isClean() - entity.RefreshPushPull() - if err := entity.loadCommits(); err != nil { - return err + // now notify the listeners and channel the data + for i := range listeners { + event := &RepositoryEvent{ + Name: eventName, + Data: data, + } + if err := listeners[i](event); err != nil { + return err + } } - if err := entity.loadRemotes(); err != nil { - return err + return nil +} + +// State returns the state of the repository such as queued, failed etc. +func (e *RepoEntity) State() RepoState { + return e.state +} + +// SetState sets the state of repository and sends repository updated event +func (e *RepoEntity) SetState(state RepoState) { + e.state = state + // we could send an event data but we don't need for this topic + if err := e.Publish(RepositoryUpdated, nil); err != nil { + log.Warnf("Cannot publish on %s topic.\n", RepositoryUpdated) } - err = entity.loadStashedItems() - return err } diff --git a/pkg/gui/authenticationview.go b/pkg/gui/authenticationview.go index 9c830cd..b644b6b 100644 --- a/pkg/gui/authenticationview.go +++ b/pkg/gui/authenticationview.go @@ -37,7 +37,7 @@ func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job return err } jobRequiresAuth = job - if job.Entity.State != git.Fail { + if job.Entity.State() != git.Fail { if err := jobQueue.RemoveFromQueue(job.Entity); err != nil { log.Fatal(err.Error()) return err @@ -96,7 +96,7 @@ func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error { } case git.PullJob: // we handle pull as fetch&merge so same rule applies - jobRequiresAuth.Options = git.FetchOptions{ + jobRequiresAuth.Options = git.PullOptions{ RemoteName: jobRequiresAuth.Entity.Remote.Name, Credentials: git.Credentials{ User: creduser, @@ -104,14 +104,12 @@ func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error { }, } } - jobRequiresAuth.Entity.State = git.Queued + jobRequiresAuth.Entity.SetState(git.Queued) // add this job to the last of the queue err = gui.State.Queue.AddJob(jobRequiresAuth) if err != nil { return err } - // refresh views with the updates - gui.refreshMain(g) gui.closeAuthenticationView(g, v) v_return, err := g.View(authenticationReturnView) gui.startQueue(g, v_return) diff --git a/pkg/gui/commitview.go b/pkg/gui/commitview.go index 94ef574..0810621 100644 --- a/pkg/gui/commitview.go +++ b/pkg/gui/commitview.go @@ -52,16 +52,12 @@ func (gui *Gui) openCommitMessageView(g *gocui.Gui, v *gocui.View) error { if err := gui.openCommitUserEmailView(g); err != nil { return err } - gui.updateKeyBindingsView(g, commitMessageViewFeature.Name) - if _, err := g.SetCurrentView(commitMessageViewFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(commitMessageViewFeature.Name) } // open an error view to inform user with a message and a useful note func (gui *Gui) openCommitUserNameView(g *gocui.Gui) error { - entity := gui.getSelectedRepository() + e := 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) @@ -78,7 +74,7 @@ func (gui *Gui) openCommitUserNameView(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - name, err := git.Config(entity, git.ConfigOptions{ + name, err := git.Config(e, git.ConfigOptions{ Section: "user", Option: "name", }) @@ -94,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 { - entity := gui.getSelectedRepository() + e := 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) @@ -111,7 +107,7 @@ func (gui *Gui) openCommitUserEmailView(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - email, err := git.Config(entity, git.ConfigOptions{ + email, err := git.Config(e, git.ConfigOptions{ Section: "user", Option: "email", }) @@ -127,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 { - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() // in order to read buffer of the views, first we need to find'em v_msg, err := g.View(commitMessageViewFeature.Name) v_name, err := g.View(commitUserUserViewFeature.Name) @@ -141,7 +137,7 @@ func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error { if len(email) <= 0 { return errors.New("User email needs to be provided") } - err = git.CommitCommand(entity, git.CommitOptions{ + err = git.CommitCommand(e, git.CommitOptions{ CommitMsg: msg, User: name, Email: email, @@ -149,20 +145,17 @@ func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error { if err != nil { return err } - entity.Refresh() - err = gui.closeCommitMessageView(g, v) - return err + return gui.closeCommitMessageView(g, v) } // focus to next view func (gui *Gui) nextCommitView(g *gocui.Gui, v *gocui.View) error { - err := gui.nextViewOfGroup(g, v, commitViews) - return err + return gui.nextViewOfGroup(g, v, commitViews) } // close the opened commite mesage view func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() g.Cursor = false for _, view := range commitViews { if err := g.DeleteView(view.Name); err != nil { @@ -174,13 +167,7 @@ func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { return err } } - if err := gui.refreshMain(g); err != nil { - return err - } - if err := gui.refreshViews(g, entity); err != nil { - return err - } - if err := refreshAllStatusView(g, entity, true); err != nil { + if err := refreshAllStatusView(g, e, true); err != nil { return err } return gui.closeViewCleanup(commitMesageReturnView) diff --git a/pkg/gui/diffview.go b/pkg/gui/diffview.go index 55286e9..9a47f95 100644 --- a/pkg/gui/diffview.go +++ b/pkg/gui/diffview.go @@ -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) { - entity := gui.getSelectedRepository() - commit := entity.Commit + e := gui.getSelectedRepository() + commit := e.Commit commitDetail := []string{("Hash: " + cyan.Sprint(commit.Hash) + "\n" + "Author: " + commit.Author + "\n" + commit.Time + "\n" + "\n" + "\t\t" + commit.Message + "\n")} - diff, err := entity.Diff(entity.Commit.Hash) + diff, err := git.Diff(e, e.Commit.Hash) if err != nil { return err } @@ -56,15 +56,15 @@ 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) { - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() _, cy := v.Cursor() _, oy := v.Origin() var files []*git.File switch v.Name() { case unstageViewFeature.Name: - _, files, err = populateFileLists(entity) + _, files, err = populateFileLists(e) case stageViewFeature.Name: - files, _, err = populateFileLists(entity) + files, _, err = populateFileLists(e) } if err != nil { return err @@ -95,13 +95,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) { - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() _, oy := v.Origin() _, cy := v.Cursor() - if len(entity.Stasheds) <= 0 { + if len(e.Stasheds) <= 0 { return nil } - stashedItem := entity.Stasheds[oy+cy] + stashedItem := e.Stasheds[oy+cy] output, err := stashedItem.Show() if err != nil { if err = gui.openErrorView(g, output, diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1fb94ad..2e1f2b2 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -2,6 +2,7 @@ package gui import ( "fmt" + "sync" "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" @@ -14,6 +15,7 @@ type Gui struct { g *gocui.Gui KeyBindings []*KeyBinding State guiState + mutex *sync.Mutex } // guiState struct holds the repositories, directiories, mode and queue of the @@ -81,6 +83,7 @@ func NewGui(mode string, directoies []string) (*Gui, error) { } gui := &Gui{ State: initialState, + mutex: &sync.Mutex{}, } for _, m := range modes { if string(m.ModeID) == mode { @@ -121,6 +124,10 @@ func (gui *Gui) Run() error { 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) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index eff7952..3c5db5f 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -184,7 +184,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.closeAuthenticationView, Display: "esc", - Description: "close/cancel", + Description: "Close/Cancel", Vital: true, }, { View: view.Name, @@ -214,7 +214,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.closeCommitMessageView, Display: "esc", - Description: "close/cancel", + Description: "Close/Cancel", Vital: true, }, { View: view.Name, @@ -444,7 +444,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.closeConfirmationView, Display: "esc", - Description: "close/cancel", + Description: "Close/Cancel", Vital: true, }, { View: confirmationViewFeature.Name, @@ -462,7 +462,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.closeCommitDiffView, Display: "esc", - Description: "close/cancel", + Description: "Close/Cancel", Vital: true, }, { View: diffViewFeature.Name, @@ -504,7 +504,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.closeCheatSheetView, Display: "esc", - Description: "close/cancel", + Description: "Close/Cancel", Vital: true, }, { View: cheatSheetViewFeature.Name, @@ -546,7 +546,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.closeErrorView, Display: "esc", - Description: "close/cancel", + Description: "Close/Cancel", Vital: true, }, { View: errorViewFeature.Name, diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index 389d66d..e4c0159 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -6,6 +6,7 @@ import ( "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" + log "github.com/sirupsen/logrus" ) // this is the initial function for filling the values for the main view. the @@ -16,9 +17,7 @@ func (gui *Gui) fillMain(g *gocui.Gui) error { if err != nil { return err } - for _, r := range gui.State.Repositories { - fmt.Fprintln(v, gui.displayString(r)) - } + // if there is still a loading screen we better get rid of it err = g.DeleteView(loadingViewFeature.Name) if err != nil { return err @@ -26,26 +25,39 @@ func (gui *Gui) fillMain(g *gocui.Gui) error { 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 if err = gui.sortByName(g, v); err != nil { return err } - entity := gui.getSelectedRepository() - gui.refreshViews(g, entity) return nil }) return nil } // refresh the main view and re-render the repository representations -func (gui *Gui) refreshMain(g *gocui.Gui) error { - mainView, err := g.View(mainViewFeature.Name) +func (gui *Gui) renderMain() error { + gui.mutex.Lock() + defer gui.mutex.Unlock() + + mainView, err := gui.g.View(mainViewFeature.Name) if err != nil { return err } mainView.Clear() for _, r := range gui.State.Repositories { - fmt.Fprintln(mainView, gui.displayString(r)) + fmt.Fprintln(mainView, gui.repositoryLabel(r)) } + // while refreshing, refresh sideViews for selected entity, something may + // be changed? + return gui.renderSideViews(gui.getSelectedRepository()) +} + +// listens the event -> "repository.updated" +func (gui *Gui) repositoryUpdated(event *git.RepositoryEvent) error { + gui.g.Update(func(g *gocui.Gui) error { + return gui.renderMain() + }) return nil } @@ -62,18 +74,12 @@ func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error { return nil } if err := v.SetCursor(cx, cy+1); err != nil { - if err := v.SetOrigin(ox, oy+1); err != nil { return err } } - entity := gui.getSelectedRepository() - if err := gui.refreshMain(g); err != nil { - return err - } - gui.refreshViews(g, entity) } - return nil + return gui.renderMain() } // moves the cursor upwards for the main view @@ -86,13 +92,8 @@ func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error { return err } } - entity := gui.getSelectedRepository() - if err := gui.refreshMain(g); err != nil { - return err - } - gui.refreshViews(g, entity) } - return nil + return gui.renderMain() } // returns the entity at cursors position by taking its position in the gui's @@ -103,9 +104,6 @@ func (gui *Gui) getSelectedRepository() *git.RepoEntity { v, _ := gui.g.View(mainViewFeature.Name) _, oy := v.Origin() _, cy := v.Cursor() - // if _, err := v.Line(cy); err != nil { - // return r, err - // } return gui.State.Repositories[cy+oy] } @@ -129,7 +127,7 @@ func (gui *Gui) addToQueue(entity *git.RepoEntity) error { if err != nil { return err } - entity.State = git.Queued + entity.SetState(git.Queued) return nil } @@ -139,7 +137,36 @@ func (gui *Gui) removeFromQueue(entity *git.RepoEntity) error { if err != nil { return err } - entity.State = git.Available + entity.SetState(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() + + 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 + } + // if queue is finished simply return from this goroutine + if finished { + return + } + } + }(gui, g) return nil } @@ -148,18 +175,15 @@ func (gui *Gui) removeFromQueue(entity *git.RepoEntity) 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 == git.Available || r.State == git.Success || r.State == git.Paused { + if r.State().Ready { if err := gui.addToQueue(r); err != nil { return err } - } else if r.State == git.Queued { + } else if r.State() == git.Queued { if err := gui.removeFromQueue(r); err != nil { return err } - } else { - return nil } - gui.refreshMain(g) return nil } @@ -167,7 +191,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 == git.Available || r.State == git.Success { + if r.State().Ready { if err := gui.addToQueue(r); err != nil { return err } @@ -175,7 +199,6 @@ func (gui *Gui) markAllRepositories(g *gocui.Gui, v *gocui.View) error { continue } } - gui.refreshMain(g) return nil } @@ -183,7 +206,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.State() == git.Queued { if err := gui.removeFromQueue(r); err != nil { return err } @@ -191,14 +214,13 @@ func (gui *Gui) unmarkAllRepositories(g *gocui.Gui, v *gocui.View) error { continue } } - gui.refreshMain(g) return nil } // sortByName sorts the repositories by A to Z order func (gui *Gui) sortByName(g *gocui.Gui, v *gocui.View) error { sort.Sort(git.Alphabetical(gui.State.Repositories)) - gui.refreshAfterSort(g) + gui.renderMain() return nil } @@ -206,14 +228,6 @@ func (gui *Gui) sortByName(g *gocui.Gui, v *gocui.View) error { // the top element will be the last modified func (gui *Gui) sortByMod(g *gocui.Gui, v *gocui.View) error { sort.Sort(git.LastModified(gui.State.Repositories)) - gui.refreshAfterSort(g) - return nil -} - -// utility function that refreshes main and side views after that -func (gui *Gui) refreshAfterSort(g *gocui.Gui) error { - gui.refreshMain(g) - entity := gui.getSelectedRepository() - gui.refreshViews(g, entity) + gui.renderMain() return nil } diff --git a/pkg/gui/sideviews.go b/pkg/gui/sideviews.go index 86aa3cb..6316255 100644 --- a/pkg/gui/sideviews.go +++ b/pkg/gui/sideviews.go @@ -12,28 +12,43 @@ 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 { + var err error + if err = gui.renderRemotes(e); err != nil { + return err + } + if err = gui.renderBranch(e); err != nil { + return err + } + if err = gui.renderRemoteBranches(e); err != nil { + return err + } + if err = gui.renderCommits(e); err != nil { + return err + } + return err +} + // updates the remotesview for given entity -func (gui *Gui) updateRemotes(g *gocui.Gui, entity *git.RepoEntity) error { +func (gui *Gui) renderRemotes(e *git.RepoEntity) error { var err error - out, err := g.View(remoteViewFeature.Name) + out, err := gui.g.View(remoteViewFeature.Name) if err != nil { return err } out.Clear() - currentindex := 0 - totalRemotes := len(entity.Remotes) + totalRemotes := len(e.Remotes) if totalRemotes > 0 { - for i, r := range entity.Remotes { - // TODO: maybe the text styling can be moved to textstyle.go file + for i, r := range e.Remotes { _, shortURL := trimRemoteURL(r.URL[0]) - suffix := shortURL - if r.Name == entity.Remote.Name { + if r.Name == e.Remote.Name { currentindex = i - fmt.Fprintln(out, selectionIndicator+r.Name+": "+suffix) + fmt.Fprintln(out, selectionIndicator+r.Name+": "+shortURL) continue } - fmt.Fprintln(out, tab+r.Name+": "+suffix) + fmt.Fprintln(out, tab+r.Name+": "+shortURL) } if err = gui.smartAnchorRelativeToLine(out, currentindex, totalRemotes); err != nil { return err @@ -43,27 +58,23 @@ func (gui *Gui) updateRemotes(g *gocui.Gui, entity *git.RepoEntity) error { } // updates the remotebranchview for given entity -func (gui *Gui) updateRemoteBranches(g *gocui.Gui, entity *git.RepoEntity) error { +func (gui *Gui) renderRemoteBranches(e *git.RepoEntity) error { var err error - out, err := g.View(remoteBranchViewFeature.Name) + out, err := gui.g.View(remoteBranchViewFeature.Name) if err != nil { return err } out.Clear() currentindex := 0 - trb := len(entity.Remote.Branches) + trb := len(e.Remote.Branches) if trb > 0 { - for i, r := range entity.Remote.Branches { - rName := r.Name - if r.Deleted { - rName = rName + ws + dirty - } - if r.Name == entity.Remote.Branch.Name { + for i, r := range e.Remote.Branches { + if r.Name == e.Remote.Branch.Name { currentindex = i - fmt.Fprintln(out, selectionIndicator+rName) + fmt.Fprintln(out, selectionIndicator+r.Name) continue } - fmt.Fprintln(out, tab+rName) + fmt.Fprintln(out, tab+r.Name) } if err = gui.smartAnchorRelativeToLine(out, currentindex, trb); err != nil { return err @@ -73,18 +84,17 @@ func (gui *Gui) updateRemoteBranches(g *gocui.Gui, entity *git.RepoEntity) error } // updates the branchview for given entity -func (gui *Gui) updateBranch(g *gocui.Gui, entity *git.RepoEntity) error { +func (gui *Gui) renderBranch(e *git.RepoEntity) error { var err error - out, err := g.View(branchViewFeature.Name) + out, err := gui.g.View(branchViewFeature.Name) if err != nil { return err } out.Clear() - currentindex := 0 - totalbranches := len(entity.Branches) - for i, b := range entity.Branches { - if b.Name == entity.Branch.Name { + totalbranches := len(e.Branches) + for i, b := range e.Branches { + if b.Name == e.Branch.Name { currentindex = i fmt.Fprintln(out, selectionIndicator+b.Name) continue @@ -96,31 +106,22 @@ func (gui *Gui) updateBranch(g *gocui.Gui, entity *git.RepoEntity) error { } // updates the commitsview for given entity -func (gui *Gui) updateCommits(g *gocui.Gui, entity *git.RepoEntity) error { +func (gui *Gui) renderCommits(e *git.RepoEntity) error { var err error - out, err := g.View(commitViewFeature.Name) + out, err := gui.g.View(commitViewFeature.Name) if err != nil { return err } out.Clear() - currentindex := 0 - totalcommits := len(entity.Commits) - for i, c := range entity.Commits { - var body string - if c.CommitType == git.EvenCommit { - body = cyan.Sprint(c.Hash[:hashLength]) + " " + c.Message - } else if c.CommitType == git.LocalCommit { - body = blue.Sprint(c.Hash[:hashLength]) + " " + c.Message - } else { - body = yellow.Sprint(c.Hash[:hashLength]) + " " + c.Message - } - if c.Hash == entity.Commit.Hash { + totalcommits := len(e.Commits) + for i, c := range e.Commits { + if c.Hash == e.Commit.Hash { currentindex = i - fmt.Fprintln(out, selectionIndicator+body) + fmt.Fprintln(out, selectionIndicator+commitLabel(c)) continue } - fmt.Fprintln(out, tab+body) + fmt.Fprintln(out, tab+commitLabel(c)) } if err = gui.smartAnchorRelativeToLine(out, currentindex, totalcommits); err != nil { return err @@ -128,156 +129,103 @@ func (gui *Gui) updateCommits(g *gocui.Gui, entity *git.RepoEntity) error { return err } +// cursor down variant for sideviews func (gui *Gui) sideViewsNextItem(g *gocui.Gui, v *gocui.View) error { var err error - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() switch viewName := v.Name(); viewName { case remoteBranchViewFeature.Name: - if err = entity.Remote.NextRemoteBranch(); err != nil { - return err - } - err = gui.updateRemoteBranches(g, entity) + return e.Remote.NextRemoteBranch(e) case remoteViewFeature.Name: - if err = entity.NextRemote(); err != nil { - return err - } - err = gui.remoteChangeFollowUp(g, entity) + return e.NextRemote() case branchViewFeature.Name: - if err = entity.Checkout(entity.NextBranch()); err != nil { + if err = e.Checkout(e.NextBranch()); err != nil { err = gui.openErrorView(g, err.Error(), "You should manually resolve this issue", branchViewFeature.Name) return err } - err = gui.checkoutFollowUp(g, entity) case commitViewFeature.Name: - if err = entity.NextCommit(); err != nil { - return err - } - err = gui.updateCommits(g, entity) + e.NextCommit() + return gui.renderCommits(e) } return err } +// cursor up variant for sideviews func (gui *Gui) sideViewsPreviousItem(g *gocui.Gui, v *gocui.View) error { var err error - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() switch viewName := v.Name(); viewName { case remoteBranchViewFeature.Name: - if err = entity.Remote.PreviousRemoteBranch(); err != nil { - return err - } - err = gui.updateRemoteBranches(g, entity) + return e.Remote.PreviousRemoteBranch(e) case remoteViewFeature.Name: - if err = entity.PreviousRemote(); err != nil { - return err - } - err = gui.remoteChangeFollowUp(g, entity) + return e.PreviousRemote() case branchViewFeature.Name: - if err = entity.Checkout(entity.PreviousBranch()); err != nil { + if err = e.Checkout(e.PreviousBranch()); err != nil { err = gui.openErrorView(g, err.Error(), "You should manually resolve this issue", branchViewFeature.Name) return err } - err = gui.checkoutFollowUp(g, entity) case commitViewFeature.Name: - if err = entity.PreviousCommit(); err != nil { - return err - } - err = gui.updateCommits(g, entity) + e.PreviousCommit() + return gui.renderCommits(e) } return err } // basically does fetch --prune func (gui *Gui) syncRemoteBranch(g *gocui.Gui, v *gocui.View) error { - var err error - entity := gui.getSelectedRepository() - if err = git.Fetch(entity, git.FetchOptions{ - RemoteName: entity.Remote.Name, + e := gui.getSelectedRepository() + return git.Fetch(e, git.FetchOptions{ + RemoteName: e.Remote.Name, Prune: true, - }); err != nil { - return err - } - vr, err := g.View(remoteViewFeature.Name) - if err != nil { - return err - } - // have no idea why this works.. - // some time need to fix, movement aint bad huh? - gui.sideViewsNextItem(g, vr) - gui.sideViewsPreviousItem(g, vr) - err = gui.updateRemoteBranches(g, entity) - return err + }) } -// basically does fetch --prune +// opens a confirmation view for setting default merge branch func (gui *Gui) setUpstreamToBranch(g *gocui.Gui, v *gocui.View) error { maxX, maxY := g.Size() - entity := gui.getSelectedRepository() + e := 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."+entity.Branch.Name+"."+"remote"+"="+entity.Remote.Name) - fmt.Fprintln(v, "branch."+entity.Branch.Name+"."+"merge"+"="+entity.Branch.Reference.Name().String()) + fmt.Fprintln(v, "branch."+e.Branch.Name+"."+"remote"+"="+e.Remote.Name) + fmt.Fprintln(v, "branch."+e.Branch.Name+"."+"merge"+"="+e.Branch.Reference.Name().String()) } return gui.focusToView(confirmationViewFeature.Name) } -// basically does fetch --prune +// add config for upstream merge func (gui *Gui) confirmSetUpstreamToBranch(g *gocui.Gui, v *gocui.View) error { var err error - entity := gui.getSelectedRepository() - if err = git.AddConfig(entity, git.ConfigOptions{ - Section: "branch." + entity.Branch.Name, + e := gui.getSelectedRepository() + if err = git.AddConfig(e, git.ConfigOptions{ + Section: "branch." + e.Branch.Name, Option: "remote", Site: git.ConfigSiteLocal, - }, entity.Remote.Name); err != nil { + }, e.Remote.Name); err != nil { return err } - if err = git.AddConfig(entity, git.ConfigOptions{ - Section: "branch." + entity.Branch.Name, + if err = git.AddConfig(e, git.ConfigOptions{ + Section: "branch." + e.Branch.Name, Option: "merge", Site: git.ConfigSiteLocal, - }, entity.Branch.Reference.Name().String()); err != nil { + }, e.Branch.Reference.Name().String()); err != nil { return err } - entity.Refresh() - gui.refreshMain(g) + e.Refresh() return gui.closeConfirmationView(g, v) } +// close confirmation view func (gui *Gui) closeConfirmationView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(v.Name()); err != nil { return err } return gui.closeViewCleanup(branchViewFeature.Name) } - -// after checkout a remote some refreshments needed -func (gui *Gui) remoteChangeFollowUp(g *gocui.Gui, entity *git.RepoEntity) (err error) { - if err = gui.updateRemotes(g, entity); err != nil { - return err - } - err = gui.updateRemoteBranches(g, entity) - return err -} - -// after checkout a branch some refreshments needed -func (gui *Gui) checkoutFollowUp(g *gocui.Gui, entity *git.RepoEntity) (err error) { - if err = gui.updateBranch(g, entity); err != nil { - return err - } - if err = gui.updateCommits(g, entity); err != nil { - return err - } - if err = gui.updateRemoteBranches(g, entity); err != nil { - return err - } - err = gui.refreshMain(g) - return err -} diff --git a/pkg/gui/stagedview.go b/pkg/gui/stagedview.go index 4a6beb9..a1c9737 100644 --- a/pkg/gui/stagedview.go +++ b/pkg/gui/stagedview.go @@ -18,44 +18,43 @@ func (gui *Gui) openStageView(g *gocui.Gui) error { } v.Title = stageViewFeature.Title } - entity := gui.getSelectedRepository() - if err := refreshStagedView(g, entity); err != nil { + if err := refreshStagedView(g); err != nil { return err } return gui.focusToView(stageViewFeature.Name) } func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() _, cy := v.Cursor() _, oy := v.Origin() if len(stagedFiles) <= 0 || len(stagedFiles) <= cy+oy { return nil } - if err := git.Reset(entity, stagedFiles[cy+oy], git.ResetOptions{}); err != nil { + if err := git.Reset(e, stagedFiles[cy+oy], git.ResetOptions{}); err != nil { return err } - return refreshAllStatusView(g, entity, true) + return refreshAllStatusView(g, e, true) } func (gui *Gui) resetAllChanges(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() - ref, err := entity.Repository.Head() + e := gui.getSelectedRepository() + ref, err := e.Repository.Head() if err != nil { return err } - if err := git.ResetAll(entity, git.ResetOptions{ + if err := git.ResetAll(e, git.ResetOptions{ Hash: ref.Hash().String(), Rtype: git.ResetMixed, }); err != nil { return err } - return refreshAllStatusView(g, entity, true) + return refreshAllStatusView(g, e, true) } // refresh the main view and re-render the repository representations -func refreshStagedView(g *gocui.Gui, entity *git.RepoEntity) error { +func refreshStagedView(g *gocui.Gui) error { stageView, err := g.View(stageViewFeature.Name) if err != nil { return err diff --git a/pkg/gui/stashview.go b/pkg/gui/stashview.go index 24b4f3c..81de8f0 100644 --- a/pkg/gui/stashview.go +++ b/pkg/gui/stashview.go @@ -18,15 +18,15 @@ func (gui *Gui) openStashView(g *gocui.Gui) error { } v.Title = stashViewFeature.Title } - entity := gui.getSelectedRepository() - err = refreshStashView(g, entity) + e := gui.getSelectedRepository() + err = refreshStashView(g, e) return err } // func (gui *Gui) stashChanges(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() - output, err := entity.Stash() + e := gui.getSelectedRepository() + output, err := e.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, entity, true) + err = refreshAllStatusView(g, e, true) return err } // func (gui *Gui) popStash(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() _, oy := v.Origin() _, cy := v.Cursor() - if len(entity.Stasheds) <= 0 { + if len(e.Stasheds) <= 0 { return nil } - stashedItem := entity.Stasheds[oy+cy] + stashedItem := e.Stasheds[oy+cy] output, err := stashedItem.Pop() if err != nil { if err = gui.openErrorView(g, output, @@ -55,15 +55,15 @@ func (gui *Gui) popStash(g *gocui.Gui, v *gocui.View) error { return err } } - if err := entity.Refresh(); err != nil { - return err - } - err = refreshAllStatusView(g, entity, true) + // since the pop is a func of stashed item, we need to refresh entity here + e.Refresh() + + err = refreshAllStatusView(g, e, true) return err } // refresh the main view and re-render the repository representations -func refreshStashView(g *gocui.Gui, entity *git.RepoEntity) error { +func refreshStashView(g *gocui.Gui, e *git.RepoEntity) error { stashView, err := g.View(stashViewFeature.Name) if err != nil { return err @@ -71,7 +71,7 @@ func refreshStashView(g *gocui.Gui, entity *git.RepoEntity) error { stashView.Clear() _, cy := stashView.Cursor() _, oy := stashView.Origin() - stashedItems := entity.Stasheds + stashedItems := e.Stasheds for i, stashedItem := range stashedItems { var prefix string if i == cy+oy { diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go index 82df459..cc6ef51 100644 --- a/pkg/gui/statusview.go +++ b/pkg/gui/statusview.go @@ -32,8 +32,8 @@ func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error { return nil } -func reloadFiles(entity *git.RepoEntity) (err error) { - stagedFiles, unstagedFiles, err = populateFileLists(entity) +func reloadFiles(e *git.RepoEntity) (err error) { + stagedFiles, unstagedFiles, err = populateFileLists(e) return err } @@ -67,8 +67,8 @@ func (gui *Gui) statusCursorDown(g *gocui.Gui, v *gocui.View) error { return err } } - entity := gui.getSelectedRepository() - if err := refreshStatusView(v.Name(), g, entity, false); err != nil { + e := gui.getSelectedRepository() + if err := refreshStatusView(v.Name(), g, e, false); err != nil { return err } } @@ -85,8 +85,8 @@ func (gui *Gui) statusCursorUp(g *gocui.Gui, v *gocui.View) error { return err } } - entity := gui.getSelectedRepository() - if err := refreshStatusView(v.Name(), g, entity, false); err != nil { + e := gui.getSelectedRepository() + if err := refreshStatusView(v.Name(), g, e, false); err != nil { return err } } @@ -96,13 +96,13 @@ func (gui *Gui) statusCursorUp(g *gocui.Gui, v *gocui.View) error { // header og the status layout func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error { maxX, _ := g.Size() - entity := gui.getSelectedRepository() + e := 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, entity.AbsPath) + fmt.Fprintln(v, e.AbsPath) // v.Frame = false v.Wrap = true } @@ -121,18 +121,13 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { } stagedFiles = make([]*git.File, 0) unstagedFiles = make([]*git.File, 0) - entity := gui.getSelectedRepository() - if err := gui.refreshMain(g); err != nil { - return err - } - if err := gui.refreshViews(g, entity); err != nil { - return err - } + return gui.closeViewCleanup(mainViewFeature.Name) } -func populateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) { - files, err := git.Status(entity) +// generate file lists by git status command +func populateFileLists(e *git.RepoEntity) (staged, unstaged []*git.File, err error) { + files, err := git.Status(e) if err != nil { return nil, nil, err } @@ -147,13 +142,13 @@ func populateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, er return staged, unstaged, err } -func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity, reload bool) error { +func refreshStatusView(viewName string, g *gocui.Gui, e *git.RepoEntity, reload bool) error { if reload { - reloadFiles(entity) + reloadFiles(e) } switch viewName { case stageViewFeature.Name: - if err := refreshStagedView(g, entity); err != nil { + if err := refreshStagedView(g); err != nil { return err } case unstageViewFeature.Name: @@ -161,16 +156,16 @@ func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity, re return err } case stashViewFeature.Name: - if err := refreshStashView(g, entity); err != nil { + if err := refreshStashView(g, e); err != nil { return err } } return nil } -func refreshAllStatusView(g *gocui.Gui, entity *git.RepoEntity, reload bool) error { +func refreshAllStatusView(g *gocui.Gui, e *git.RepoEntity, reload bool) error { for _, v := range statusViews { - if err := refreshStatusView(v.Name, g, entity, reload); err != nil { + if err := refreshStatusView(v.Name, g, e, reload); err != nil { return err } } diff --git a/pkg/gui/unstagedview.go b/pkg/gui/unstagedview.go index 1a33c12..dd2fe6d 100644 --- a/pkg/gui/unstagedview.go +++ b/pkg/gui/unstagedview.go @@ -23,26 +23,26 @@ func (gui *Gui) openUnStagedView(g *gocui.Gui) error { } func (gui *Gui) addChanges(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() + e := gui.getSelectedRepository() _, cy := v.Cursor() _, oy := v.Origin() if len(unstagedFiles) <= 0 || len(unstagedFiles) < cy+oy { return nil } - if err := git.Add(entity, unstagedFiles[cy+oy], git.AddOptions{}); err != nil { + if err := git.Add(e, unstagedFiles[cy+oy], git.AddOptions{}); err != nil { return err } - err := refreshAllStatusView(g, entity, true) + err := refreshAllStatusView(g, e, true) return err } func (gui *Gui) addAllChanges(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() - if err := git.AddAll(entity, git.AddOptions{}); err != nil { + e := gui.getSelectedRepository() + if err := git.AddAll(e, git.AddOptions{}); err != nil { return err } - err := refreshAllStatusView(g, entity, true) + err := refreshAllStatusView(g, e, true) return err } diff --git a/pkg/gui/util-common.go b/pkg/gui/util-common.go index f203bbf..3f6be1a 100644 --- a/pkg/gui/util-common.go +++ b/pkg/gui/util-common.go @@ -1,30 +1,11 @@ package gui import ( - "github.com/isacikgoz/gitbatch/pkg/git" "github.com/isacikgoz/gitbatch/pkg/helpers" "github.com/jroimartin/gocui" log "github.com/sirupsen/logrus" ) -// refreshes the side views of the application for given git.RepoEntity struct -func (gui *Gui) refreshViews(g *gocui.Gui, entity *git.RepoEntity) error { - var err error - if err = gui.updateRemotes(g, entity); err != nil { - return err - } - if err = gui.updateBranch(g, entity); err != nil { - return err - } - if err = gui.updateRemoteBranches(g, entity); err != nil { - return err - } - if err = gui.updateCommits(g, entity); err != nil { - return err - } - return err -} - // focus to next view func (gui *Gui) nextViewOfGroup(g *gocui.Gui, v *gocui.View, group []viewFeature) error { var focusedViewName string @@ -77,22 +58,6 @@ func (gui *Gui) previousViewOfGroup(g *gocui.Gui, v *gocui.View, group []viewFea return nil } -// siwtch the app mode -func (gui *Gui) switchMode(g *gocui.Gui, v *gocui.View) error { - for i, mode := range modes { - if mode == gui.State.Mode { - if i == len(modes)-1 { - gui.State.Mode = modes[0] - break - } - gui.State.Mode = modes[i+1] - break - } - } - gui.updateKeyBindingsView(g, mainViewFeature.Name) - return nil -} - // siwtch the app's mode to fetch func (gui *Gui) switchToFetchMode(g *gocui.Gui, v *gocui.View) error { gui.State.Mode = fetchMode diff --git a/pkg/gui/util-queuehandler.go b/pkg/gui/util-queuehandler.go deleted file mode 100644 index a814c47..0000000 --- a/pkg/gui/util-queuehandler.go +++ /dev/null @@ -1,58 +0,0 @@ -package gui - -import ( - "github.com/isacikgoz/gitbatch/pkg/git" - "github.com/jroimartin/gocui" - log "github.com/sirupsen/logrus" -) - -// this function starts the queue and updates the gui with the result of an -// 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() - // 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 - }) - - if err != nil { - if err == git.ErrAuthenticationRequired { - // pause the job, so it will be indicated to being blocking - job.Entity.State = git.Paused - err := gui_go.openAuthenticationView(g, gui_go.State.Queue, job, v.Name()) - if err != nil { - log.Warn(err.Error()) - return - } - } - return - // with not returning here, we simply ignore and continue - } - // if queue is finished simply return from this goroutine - if finished { - return - } - selectedEntity := gui_go.getSelectedRepository() - if job.Entity == selectedEntity { - gui_go.refreshViews(g, job.Entity) - } - } - }(gui, g) - return nil -} - -// flashes the keybinding view's backgroun with green color to indicate that -// the queue is started -func indicateQueueStarted(g *gocui.Gui) error { - v, err := g.View(keybindingsViewFeature.Name) - if err != nil { - return err - } - v.BgColor = gocui.ColorGreen - v.FgColor = gocui.ColorBlack - return nil -} diff --git a/pkg/gui/util-textstyle.go b/pkg/gui/util-textstyle.go index 7c36263..fbe2b64 100644 --- a/pkg/gui/util-textstyle.go +++ b/pkg/gui/util-textstyle.go @@ -24,13 +24,10 @@ var ( maxRepositoryLength = 20 hashLength = 7 - ws = " " - pushable = string(blue.Sprint("↖")) - pullable = string(blue.Sprint("↘")) - confidentArrow = string(magenta.Sprint("")) - unconfidentArrow = string(yellow.Sprint("")) - dirty = string(yellow.Sprint("✗")) - unknown = magenta.Sprint("?") + ws = " " + pushable = string(blue.Sprint("↖")) + pullable = string(blue.Sprint("↘")) + dirty = string(yellow.Sprint("✗")) queuedSymbol = "•" workingSymbol = "•" @@ -53,41 +50,41 @@ var ( // this function handles the render and representation of the repository // TODO: cleanup is required, right now it looks too complicated -func (gui *Gui) displayString(entity *git.RepoEntity) string { +func (gui *Gui) repositoryLabel(e *git.RepoEntity) string { suffix := "" prefix := "" repoName := "" - if entity.Branch.Pushables != "?" { - prefix = prefix + pushable + ws + entity.Branch.Pushables + - ws + pullable + ws + entity.Branch.Pullables + if e.Branch.Pushables != "?" { + prefix = prefix + pushable + ws + e.Branch.Pushables + + ws + pullable + ws + e.Branch.Pullables } else { - prefix = prefix + pushable + ws + yellow.Sprint(entity.Branch.Pushables) + - ws + pullable + ws + yellow.Sprint(entity.Branch.Pullables) + prefix = prefix + pushable + ws + yellow.Sprint(e.Branch.Pushables) + + ws + pullable + ws + yellow.Sprint(e.Branch.Pullables) } - selectedEntity := gui.getSelectedRepository() - if selectedEntity == entity { + se := gui.getSelectedRepository() + if se == e { prefix = prefix + selectionIndicator - repoName = green.Sprint(entity.Name) + repoName = green.Sprint(e.Name) } else { prefix = prefix + ws - repoName = entity.Name + repoName = e.Name } // some branch names can be really long, in that times I hope the first // characters are important and meaningful - branch := adjustTextLength(entity.Branch.Name, maxBranchLength) + branch := adjustTextLength(e.Branch.Name, maxBranchLength) prefix = prefix + string(cyan.Sprint(branch)) - if !entity.Branch.Clean { + if !e.Branch.Clean { prefix = prefix + ws + dirty + ws } else { prefix = prefix + ws } // rendering the satus according to repository's state - if entity.State == git.Queued { - if inQueue, ty := gui.State.Queue.IsInTheQueue(entity); inQueue { + if e.State() == git.Queued { + if inQueue, ty := gui.State.Queue.IsInTheQueue(e); inQueue { switch mode := ty; mode { case git.FetchJob: suffix = blue.Sprint(queuedSymbol) @@ -100,20 +97,35 @@ func (gui *Gui) displayString(entity *git.RepoEntity) string { } } return prefix + repoName + ws + suffix - } else if entity.State == git.Working { + } else if e.State() == git.Working { // TODO: maybe the type of the job can be written while its working? return prefix + repoName + ws + green.Sprint(workingSymbol) - } else if entity.State == git.Success { + } else if e.State() == git.Success { return prefix + repoName + ws + green.Sprint(successSymbol) - } else if entity.State == git.Paused { + } else if e.State() == git.Paused { return prefix + repoName + ws + yellow.Sprint(pauseSymbol) - } else if entity.State == git.Fail { + } else if e.State() == git.Fail { return prefix + repoName + ws + red.Sprint(failSymbol) } else { return prefix + repoName } } +func commitLabel(c *git.Commit) string { + var body string + switch c.CommitType { + case git.EvenCommit: + body = cyan.Sprint(c.Hash[:hashLength]) + " " + c.Message + case git.LocalCommit: + body = blue.Sprint(c.Hash[:hashLength]) + " " + c.Message + case git.RemoteCommit: + body = yellow.Sprint(c.Hash[:hashLength]) + " " + c.Message + default: + body = c.Hash[:hashLength] + " " + c.Message + } + return body +} + // limit the text length for visual concerns func adjustTextLength(text string, maxLength int) (adjusted string) { if len(text) > maxLength { |
