From 9d7b6fcc32a07d16826ac92fbde69b8ae3234882 Mon Sep 17 00:00:00 2001 From: İbrahim Serdar Açıkgöz Date: Tue, 11 Dec 2018 18:20:01 +0300 Subject: added initial commit implementation. --- pkg/gui/gui.go | 4 +++ pkg/gui/keybindings.go | 26 ++++++++++++++++ pkg/gui/statusview.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 290cb88..1eb7969 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -129,6 +129,10 @@ func (gui *Gui) Run() error { gui.g = g g.Highlight = true g.SelFgColor = gocui.ColorGreen + + // If InputEsc is true, when ESC sequence is in the buffer and it doesn't + // match any known sequence, ESC means KeyEsc. + g.InputEsc = true g.SetManagerFunc(gui.layout) if err := gui.generateKeybindings(); err != nil { diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index a9cfe63..77bd0a2 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -160,6 +160,14 @@ func (gui *Gui) generateKeybindings() error { Display: "t", Description: "Save to Stash", Vital: true, + }, { + View: view.Name, + Key: 'm', + Modifier: gocui.ModNone, + Handler: gui.openCommitMessageView, + Display: "m", + Description: "Commit Changes", + Vital: true, }, } for _, binding := range statusKeybindings { @@ -237,6 +245,24 @@ func (gui *Gui) generateKeybindings() error { Description: "Show diff", Vital: true, }, + // commit message view + { + View: commitMessageViewFeature.Name, + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.closeCommitMessageView, + Display: "esc", + Description: "Close / Cancel", + Vital: true, + }, { + View: commitMessageViewFeature.Name, + Key: gocui.KeyEnter, //TODO: enter should be replaced with a better option + Modifier: gocui.ModNone, + Handler: gui.submitCommitMessageView, + Display: "ctrl + enter", + Description: "Submit", + Vital: true, + }, // Main view controls { View: mainViewFeature.Name, diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go index d6f0be1..494e07a 100644 --- a/pkg/gui/statusview.go +++ b/pkg/gui/statusview.go @@ -2,19 +2,24 @@ package gui import ( "fmt" + "time" "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" + ggt "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing/object" ) var ( statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "} // statusViewFeature = viewFeature{Name: "status", Title: " Status "} - stageViewFeature = viewFeature{Name: "staged", Title: " Staged "} - unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "} - stashViewFeature = viewFeature{Name: "stash", Title: " Stash "} + stageViewFeature = viewFeature{Name: "staged", Title: " Staged "} + unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "} + stashViewFeature = viewFeature{Name: "stash", Title: " Stash "} + commitMessageViewFeature = viewFeature{Name: "commitmessage", Title: " Commit Mesage "} - statusViews = []viewFeature{stageViewFeature, unstageViewFeature, stashViewFeature} + statusViews = []viewFeature{stageViewFeature, unstageViewFeature, stashViewFeature} + commitMesageReturnView string ) // open the status layout @@ -168,3 +173,73 @@ func refreshAllStatusView(g *gocui.Gui, entity *git.RepoEntity) error { } return nil } + +// open the commit message views +func (gui *Gui) openCommitMessageView(g *gocui.Gui, v *gocui.View) error { + maxX, maxY := g.Size() + commitMesageReturnView = v.Name() + v, err := g.SetView(commitMessageViewFeature.Name, maxX/2-30, maxY/2-3, maxX/2+30, maxY/2+3) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = commitMessageViewFeature.Title + v.Wrap = true + v.Editable = true + v.Editor = gocui.DefaultEditor + v.Highlight = true + g.Cursor = true + } + gui.updateKeyBindingsView(g, commitMessageViewFeature.Name) + if _, err := g.SetCurrentView(commitMessageViewFeature.Name); err != nil { + return err + } + return nil +} + +// close the opened commite mesage view +func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + w, err := entity.Repository.Worktree() + if err != nil { + return err + } + // WIP: This better be removed to git pkg + // TODO: read config and get name & e-mail + _, err = w.Commit(v.ViewBuffer(), &ggt.CommitOptions{ + Author: &object.Signature{ + Name: "İbrahim Serdar Açıkgöz", + Email: "serdaracikgoz86@gmail.com", + When: time.Now(), + }, + }) + if err != nil { + return err + } + entity.Refresh() + err = gui.closeCommitMessageView(g, v) + return err +} + +// close the opened commite mesage view +func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + g.Cursor = false + if err := g.DeleteView(commitMessageViewFeature.Name); err != nil { + return err + } + if _, err := g.SetCurrentView(commitMesageReturnView); err != nil { + return err + } + if err := gui.refreshMain(g); err != nil { + return err + } + if err := gui.refreshViews(g, entity); err != nil { + return err + } + if err := refreshAllStatusView(g, entity); err != nil { + return err + } + gui.updateKeyBindingsView(g, commitMesageReturnView) + return nil +} -- cgit v1.2.3 From d5eefa3d792a37ba104bfba36359ad1d5dea4677 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 12 Dec 2018 02:33:54 +0300 Subject: migrate fetch operation to go-git impl. for default --- pkg/git/fetch.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++---- pkg/git/remote.go | 11 +++++-- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go index 5dad021..d0757bb 100644 --- a/pkg/git/fetch.go +++ b/pkg/git/fetch.go @@ -1,10 +1,22 @@ package git import ( + "strings" + log "github.com/sirupsen/logrus" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/config" ) -var fetchCommand = "fetch" +var ( + fetchCmdMode string + fetchTryCount int + + fetchCommand = "fetch" + fetchCmdModeLegacy = "git" + fetchCmdModeNative = "go-git" + fetchMaxTry = 1 +) // FetchOptions defines the rules for fetch operation type FetchOptions struct { @@ -18,13 +30,42 @@ type FetchOptions struct { // Force allows the fetch to update a local branch even when the remote // branch does not descend from it. Force bool + // There should be more room for authentication, tags and progress } // Fetch branches refs from one or more other repositories, along with the // objects necessary to complete their histories -func Fetch(entity *RepoEntity, options FetchOptions) error { +func Fetch(entity *RepoEntity, options FetchOptions) (err error) { + // here we configure fetch operation + // default mode is go-git (this may be configured) + fetchCmdMode = fetchCmdModeNative + fetchTryCount = 0 + // prune and dry run is not supported from go-git yet, rely on old friend + if options.Prune || options.DryRun { + fetchCmdMode = fetchCmdModeLegacy + } + switch fetchCmdMode { + case fetchCmdModeLegacy: + err = fetchWithGit(entity.AbsPath, options) + return err + case fetchCmdModeNative: + // this should be the refspec as default, let's give it a try + refspec := "+" + "refs/heads/" + entity.Branch.Name + ":" + "/refs/remotes/" + entity.Remote.Branch.Name + err = fetchWithGoGit(entity, options, refspec) + return err + } + // till this step everything should be ok + err = entity.Refresh() + return err +} + +// fetchWithGit is simply a bare git fetch command which is flexible +// for complex operations, but on the other hand, it ties the app to another +// tool. To avoid that, using native implementation is preferred. +func fetchWithGit(abspath string, options FetchOptions) (err error) { args := make([]string, 0) args = append(args, fetchCommand) + // parse options to command line arguments if len(options.RemoteName) > 0 { args = append(args, options.RemoteName) } @@ -37,10 +78,44 @@ func Fetch(entity *RepoEntity, options FetchOptions) error { if options.DryRun { args = append(args, "--dry-run") } - if err := GenericGitCommand(entity.AbsPath, args); err != nil { - log.Warn("Error while fetching") - return err + if err := GenericGitCommand(abspath, args); err != nil { + log.Warn("Error at git command (fetch)") + } + return err +} + +// fetchWithGoGit is the primary fetch method and refspec is the main feature. +// RefSpec is a mapping from local branches to remote references The format of +// the refspec is an optional +, followed by :, where is the +// pattern for references on the remote side and is where those references +// will be written locally. The + tells Git to update the reference even if it +// isn’t a fast-forward. +func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (err error) { + opt := &git.FetchOptions{ + RemoteName: options.RemoteName, + RefSpecs: []config.RefSpec{config.RefSpec(refspec)}, + Force: options.Force, + } + err = entity.Repository.Fetch(opt) + if err != nil { + if err == git.NoErrAlreadyUpToDate { + // Already up-to-date + log.Warn(err.Error()) + // TODO: submit a PR for this kind of error, this type of catch is lame + } else if strings.Contains(err.Error(), "couldn't find remote ref") { + // we dont have remote ref, so lets pull other things.. maybe it'd be useful + rp := entity.Remote.RefSpecs[0] + if fetchTryCount < fetchMaxTry { + fetchTryCount++ + fetchWithGoGit(entity, options, rp) + } else { + return err + } + // TODO: handle authentication exception + } else { + log.Warn(err.Error()) + return err + } } - entity.Refresh() return nil } diff --git a/pkg/git/remote.go b/pkg/git/remote.go index 96b8aa7..27f627b 100644 --- a/pkg/git/remote.go +++ b/pkg/git/remote.go @@ -10,6 +10,7 @@ import ( type Remote struct { Name string URL []string + RefSpecs []string Branch *RemoteBranch Branches []*RemoteBranch } @@ -57,10 +58,14 @@ func (entity *RepoEntity) loadRemotes() error { remotes, err := r.Remotes() for _, rm := range remotes { - + rfs := make([]string, 0) + for _, rf := range rm.Config().Fetch { + rfs = append(rfs, string(rf)) + } remote := &Remote{ - Name: rm.Config().Name, - URL: rm.Config().URLs, + Name: rm.Config().Name, + URL: rm.Config().URLs, + RefSpecs: rfs, } remote.loadRemoteBranches(entity) if len(remote.Branches) > 0 { -- cgit v1.2.3 From a1abe10a125e1b74c38726f02565140b1a440b95 Mon Sep 17 00:00:00 2001 From: İbrahim Serdar Açıkgöz Date: Wed, 12 Dec 2018 18:38:02 +0300 Subject: implementing authentication for fetch operation --- pkg/git/authentication.go | 26 +++++++ pkg/git/errors.go | 24 ++++++ pkg/git/fetch.go | 23 +++++- pkg/git/job.go | 72 ++++++++++++++++++ pkg/git/queue.go | 78 ++++++++++++++++++++ pkg/gui/authenticationview.go | 165 ++++++++++++++++++++++++++++++++++++++++++ pkg/gui/gui.go | 5 +- pkg/gui/keybindings.go | 32 ++++++++ pkg/gui/mainview.go | 11 ++- pkg/gui/queuehandler.go | 9 +++ pkg/gui/textstyle.go | 7 +- pkg/queue/job.go | 67 ----------------- pkg/queue/queue.go | 80 -------------------- 13 files changed, 438 insertions(+), 161 deletions(-) create mode 100644 pkg/git/authentication.go create mode 100644 pkg/git/errors.go create mode 100644 pkg/git/job.go create mode 100644 pkg/git/queue.go create mode 100644 pkg/gui/authenticationview.go delete mode 100644 pkg/queue/job.go delete mode 100644 pkg/queue/queue.go diff --git a/pkg/git/authentication.go b/pkg/git/authentication.go new file mode 100644 index 0000000..def1f99 --- /dev/null +++ b/pkg/git/authentication.go @@ -0,0 +1,26 @@ +package git + +import ( + "net/url" +) + +// Credentials holds user credentials to authenticate and authorize while +// communicating with remote if required +type Credentials struct { + User string + Password string +} + +var ( + authProtocolHttp = "http" + authProtocolHttps = "https" + authProtocolSSH = "ssh" +) + +func (entity *RepoEntity) authProtocol(remote *Remote) (p string, err error) { + u, err := url.Parse(remote.URL[0]) + if err != nil { + return p, err + } + return u.Scheme, err +} diff --git a/pkg/git/errors.go b/pkg/git/errors.go new file mode 100644 index 0000000..8621cbc --- /dev/null +++ b/pkg/git/errors.go @@ -0,0 +1,24 @@ +package git + +import ( + "errors" +) + +var ( + // ErrGitCommand is thrown when git command returned an error code + ErrGitCommand = errors.New("Git command returned error code") + // ErrAuthenticationRequired is thrown when an authentication required on + // a remote operation + ErrAuthenticationRequired = errors.New("Authentication required") + // ErrAuthorizationFailed is thrown when authorization failed while trying + // to authenticate with remote + ErrAuthorizationFailed = errors.New("Authorization failed") + // ErrInvalidAuthMethod is thrown when invalid auth method is invoked + ErrInvalidAuthMethod = errors.New("invalid auth method") + // ErrAlreadyUpToDate is thrown when a repository is already up to date + // with its src on merge/fetch/pull + ErrAlreadyUpToDate = errors.New("Already up to date") + // ErrCouldNotFindRemoteRef is thrown when trying to fetch/pull cannot + // find suitable remote reference + ErrCouldNotFindRemoteRef = errors.New("Could not find remote ref") +) diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go index d0757bb..39a0277 100644 --- a/pkg/git/fetch.go +++ b/pkg/git/fetch.go @@ -6,6 +6,8 @@ import ( log "github.com/sirupsen/logrus" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/config" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/http" ) var ( @@ -22,6 +24,8 @@ var ( type FetchOptions struct { // Name of the remote to fetch from. Defaults to origin. RemoteName string + // Credentials holds the user and pswd information + Credentials Credentials // Before fetching, remove any remote-tracking references that no longer // exist on the remote. Prune bool @@ -96,6 +100,21 @@ func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (e RefSpecs: []config.RefSpec{config.RefSpec(refspec)}, Force: options.Force, } + if len(options.Credentials.User) > 0 { + protocol, err := entity.authProtocol(entity.Remote) + if err != nil { + return err + } + if protocol == authProtocolHttp || protocol == authProtocolHttps { + opt.Auth = &http.BasicAuth{ + Username: options.Credentials.User, + Password: options.Credentials.Password, + } + } else { + return ErrInvalidAuthMethod + } + } + err = entity.Repository.Fetch(opt) if err != nil { if err == git.NoErrAlreadyUpToDate { @@ -111,7 +130,9 @@ func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (e } else { return err } - // TODO: handle authentication exception + } else if err == transport.ErrAuthenticationRequired { + log.Warn(err.Error()) + return ErrAuthenticationRequired } else { log.Warn(err.Error()) return err diff --git a/pkg/git/job.go b/pkg/git/job.go new file mode 100644 index 0000000..13ef566 --- /dev/null +++ b/pkg/git/job.go @@ -0,0 +1,72 @@ +package git + +import ( + "time" +) + +// Job relates the type of the operation and the entity +type Job struct { + JobType JobType + Entity *RepoEntity + Options interface{} +} + +// JobType is the a git operation supported +type JobType string + +const ( + // Fetch is wrapper of git fetch command + FetchJob JobType = "fetch" + // Pull is wrapper of git pull command + PullJob JobType = "pull" + // Merge is wrapper of git merge command + MergeJob JobType = "merge" +) + +// starts the job +func (job *Job) start() error { + job.Entity.State = Working + // added for testing, TODO: remove + time.Sleep(time.Second) + // TODO: Handle errors? + switch mode := job.JobType; mode { + case FetchJob: + var opts FetchOptions + if job.Options != nil { + opts = job.Options.(FetchOptions) + } else { + opts = FetchOptions{ + RemoteName: job.Entity.Remote.Name, + } + } + if err := Fetch(job.Entity, opts); err != nil { + job.Entity.State = Fail + return err + } + case PullJob: + if err := Fetch(job.Entity, FetchOptions{ + RemoteName: job.Entity.Remote.Name, + }); err != nil { + job.Entity.State = Fail + return nil + } + if err := Merge(job.Entity, MergeOptions{ + BranchName: job.Entity.Remote.Branch.Name, + }); err != nil { + job.Entity.State = Fail + return nil + } + case MergeJob: + if err := Merge(job.Entity, MergeOptions{ + BranchName: job.Entity.Remote.Branch.Name, + }); err != nil { + job.Entity.State = Fail + return nil + } + default: + job.Entity.State = Available + return nil + } + job.Entity.State = Success + return nil +} diff --git a/pkg/git/queue.go b/pkg/git/queue.go new file mode 100644 index 0000000..e95250a --- /dev/null +++ b/pkg/git/queue.go @@ -0,0 +1,78 @@ +package git + +import ( + "errors" +) + +// JobQueue holds the slice of Jobs +type JobQueue struct { + series []*Job +} + +// CreateJobQueue creates a jobqueue struct and initialize its slice then return +// its pointer +func CreateJobQueue() (jobQueue *JobQueue) { + s := make([]*Job, 0) + return &JobQueue{ + series: s, + } +} + +// AddJob adds a job to the queue +func (jobQueue *JobQueue) AddJob(j *Job) error { + for _, job := range jobQueue.series { + if job.Entity.RepoID == j.Entity.RepoID && job.JobType == j.JobType { + return errors.New("Same job already is in the queue") + } + } + jobQueue.series = append(jobQueue.series, nil) + copy(jobQueue.series[1:], jobQueue.series[0:]) + jobQueue.series[0] = j + return nil +} + +// StartNext starts the next job in the queue +func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) { + finished = false + if len(jobQueue.series) < 1 { + finished = true + return nil, finished, nil + } + i := len(jobQueue.series) - 1 + lastJob := jobQueue.series[i] + jobQueue.series = jobQueue.series[:i] + if err = lastJob.start(); err != nil { + return lastJob, finished, err + } + return lastJob, finished, nil +} + +// RemoveFromQueue deletes the given entity and its job from the queue +// TODO: it is not safe if the job has been started +func (jobQueue *JobQueue) RemoveFromQueue(entity *RepoEntity) error { + removed := false + for i, job := range jobQueue.series { + if job.Entity.RepoID == entity.RepoID { + jobQueue.series = append(jobQueue.series[:i], jobQueue.series[i+1:]...) + removed = true + } + } + if !removed { + return errors.New("There is no job with given repoID") + } + return nil +} + +// IsInTheQueue function; since the job and entity is not tied with its own +// struct, this function returns true if that entity is in the queue along with +// the jobs type +func (jobQueue *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) { + inTheQueue = false + for _, job := range jobQueue.series { + if job.Entity.RepoID == entity.RepoID { + inTheQueue = true + jt = job.JobType + } + } + return inTheQueue, jt +} diff --git a/pkg/gui/authenticationview.go b/pkg/gui/authenticationview.go new file mode 100644 index 0000000..240b46c --- /dev/null +++ b/pkg/gui/authenticationview.go @@ -0,0 +1,165 @@ +package gui + +import ( + "fmt" + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" + log "github.com/sirupsen/logrus" +) + +var ( + authenticationReturnView string + authenticationViewFeature = viewFeature{Name: "authentication", Title: " Authentication "} + authUserFeature = viewFeature{Name: "authuser", Title: " User "} + authPasswordViewFeature = viewFeature{Name: "authpasswd", Title: " Password "} + authUserLabelFeature = viewFeature{Name: "authuserlabel", Title: " User: "} + authPswdLabelViewFeature = viewFeature{Name: "authpasswdlabel", Title: " Password: "} + + authViews = []viewFeature{authUserFeature, authPasswordViewFeature} + authLabels = []viewFeature{authenticationViewFeature, authUserLabelFeature, authPswdLabelViewFeature} + + jobRequiresAuth *git.Job +) + +// open an error view to inform user with a message and a useful note +func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job *git.Job, returnViewName string) error { + maxX, maxY := g.Size() + // lets remove this job from the queue so that it won't block anything + // also it is already unsuccessfully finished + jobRequiresAuth = job + if job.Entity.State != git.Fail { + if err := jobQueue.RemoveFromQueue(job.Entity); err != nil { + log.Fatal(err.Error()) + return err + } + } + + authenticationReturnView = returnViewName + v, err := g.SetView(authenticationViewFeature.Name, maxX/2-30, maxY/2-2, maxX/2+30, maxY/2+2) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + fmt.Fprintln(v, " Enter credentials for: "+red.Sprint(job.Entity.Remote.URL[0])) + } + g.Cursor = true + if err := gui.openUserView(g); err != nil { + return nil + } + if err := gui.openPasswordView(g); err != nil { + return nil + } + return nil +} + +// close the opened error view +func (gui *Gui) closeAuthenticationView(g *gocui.Gui, v *gocui.View) error { + g.Cursor = false + for _, vf := range authLabels { + if err := g.DeleteView(vf.Name); err != nil { + return nil + } + } + for _, vf := range authViews { + if err := g.DeleteView(vf.Name); err != nil { + return nil + } + } + if _, err := g.SetCurrentView(authenticationReturnView); err != nil { + return err + } + gui.updateKeyBindingsView(g, authenticationReturnView) + return nil +} + +// close the opened error view +func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error { + g.Cursor = false + v_user, err := g.View(authUserFeature.Name) + v_pswd, err := g.View(authPasswordViewFeature.Name) + creduser := v_user.ViewBuffer() + credpswd := v_pswd.ViewBuffer() + // Maybe pause implementation can be added + switch mode := jobRequiresAuth.JobType; mode { + case git.FetchJob: + jobRequiresAuth.Options = git.FetchOptions{ + RemoteName: jobRequiresAuth.Entity.Remote.Name, + Credentials: git.Credentials{ + User: creduser, + Password: credpswd, + }, + } + } + err = gui.State.Queue.AddJob(jobRequiresAuth) + if err != nil { + return err + } + jobRequiresAuth.Entity.State = git.Queued + gui.refreshMain(g) + gui.refreshViews(g, jobRequiresAuth.Entity) + gui.closeAuthenticationView(g, v) + return nil +} + +// open an error view to inform user with a message and a useful note +func (gui *Gui) openUserView(g *gocui.Gui) error { + maxX, maxY := g.Size() + vlabel, err := g.SetView(authUserLabelFeature.Name, maxX/2-30, maxY/2-1, maxX/2-19, maxY/2+1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + fmt.Fprintln(vlabel, authUserLabelFeature.Title) + vlabel.Frame = false + } + v, err := g.SetView(authUserFeature.Name, maxX/2-18, maxY/2-1, maxX/2+29, maxY/2+1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = authUserFeature.Title + v.Editable = true + v.Frame = false + } + gui.updateKeyBindingsView(g, authUserFeature.Name) + if _, err := g.SetCurrentView(authUserFeature.Name); err != nil { + return err + } + return nil +} + +// open an error view to inform user with a message and a useful note +func (gui *Gui) openPasswordView(g *gocui.Gui) error { + maxX, maxY := g.Size() + vlabel, err := g.SetView(authPswdLabelViewFeature.Name, maxX/2-30, maxY/2, maxX/2-19, maxY/2+2) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + fmt.Fprintln(vlabel, authPswdLabelViewFeature.Title) + vlabel.Frame = false + } + v, err := g.SetView(authPasswordViewFeature.Name, maxX/2-18, maxY/2, maxX/2+29, maxY/2+2) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = authPasswordViewFeature.Title + v.Editable = true + v.Mask ^= '*' + v.Frame = false + } + return nil +} + +// focus to next view +func (gui *Gui) nextAuthView(g *gocui.Gui, v *gocui.View) error { + err := gui.nextViewOfGroup(g, v, authViews) + return err +} + +// focus to previous view +func (gui *Gui) previousAuthView(g *gocui.Gui, v *gocui.View) error { + err := gui.previousViewOfGroup(g, v, authViews) + return err +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1eb7969..1fb94ad 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/isacikgoz/gitbatch/pkg/git" - "github.com/isacikgoz/gitbatch/pkg/queue" "github.com/jroimartin/gocui" log "github.com/sirupsen/logrus" ) @@ -23,7 +22,7 @@ type guiState struct { Repositories []*git.RepoEntity Directories []string Mode mode - Queue *queue.JobQueue + Queue *git.JobQueue } // this struct encapsulates the name and title of a view. the name of a view is @@ -78,7 +77,7 @@ func NewGui(mode string, directoies []string) (*Gui, error) { initialState := guiState{ Directories: directoies, Mode: fetchMode, - Queue: queue.CreateJobQueue(), + Queue: git.CreateJobQueue(), } gui := &Gui{ State: initialState, diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 77bd0a2..9b23fa2 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -174,6 +174,38 @@ func (gui *Gui) generateKeybindings() error { gui.KeyBindings = append(gui.KeyBindings, binding) } } + for _, view := range authViews { + authKeybindings := []*KeyBinding{ + { + View: view.Name, + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.closeAuthenticationView, + Display: "esc", + Description: "close/cancel", + Vital: true, + }, { + View: view.Name, + Key: gocui.KeyTab, + Modifier: gocui.ModNone, + Handler: gui.nextAuthView, + Display: "tab", + Description: "Next Panel", + Vital: false, + }, { + View: view.Name, + Key: gocui.KeyEnter, + Modifier: gocui.ModNone, + Handler: gui.submitAuthenticationView, + Display: "enter", + Description: "Submit", + Vital: false, + }, + } + for _, binding := range authKeybindings { + gui.KeyBindings = append(gui.KeyBindings, binding) + } + } individualKeybindings := []*KeyBinding{ // stash view { diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index b421775..fe8931b 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -5,7 +5,6 @@ import ( "sort" "github.com/isacikgoz/gitbatch/pkg/git" - "github.com/isacikgoz/gitbatch/pkg/queue" "github.com/jroimartin/gocui" ) @@ -112,18 +111,18 @@ func (gui *Gui) getSelectedRepository() *git.RepoEntity { // adds given entity to job queue func (gui *Gui) addToQueue(entity *git.RepoEntity) error { - var jt queue.JobType + var jt git.JobType switch mode := gui.State.Mode.ModeID; mode { case FetchMode: - jt = queue.Fetch + jt = git.FetchJob case PullMode: - jt = queue.Pull + jt = git.PullJob case MergeMode: - jt = queue.Merge + jt = git.MergeJob default: return nil } - err := gui.State.Queue.AddJob(&queue.Job{ + err := gui.State.Queue.AddJob(&git.Job{ JobType: jt, Entity: entity, }) diff --git a/pkg/gui/queuehandler.go b/pkg/gui/queuehandler.go index 3be1472..cec6f8b 100644 --- a/pkg/gui/queuehandler.go +++ b/pkg/gui/queuehandler.go @@ -1,7 +1,9 @@ package gui import ( + "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" + log "github.com/sirupsen/logrus" ) // this function starts the queue and updates the gui with the result of an @@ -16,6 +18,13 @@ func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error { }) defer gui.updateKeyBindingsView(g, mainViewFeature.Name) if err != nil { + if err == git.ErrAuthenticationRequired { + err := gui_go.openAuthenticationView(g, gui_go.State.Queue, job, v.Name()) + if err != nil { + log.Warn(err.Error()) + return + } + } return } if finished { diff --git a/pkg/gui/textstyle.go b/pkg/gui/textstyle.go index 33f172d..efbbceb 100644 --- a/pkg/gui/textstyle.go +++ b/pkg/gui/textstyle.go @@ -6,7 +6,6 @@ import ( "github.com/fatih/color" "github.com/isacikgoz/gitbatch/pkg/git" - "github.com/isacikgoz/gitbatch/pkg/queue" ) var ( @@ -87,11 +86,11 @@ func (gui *Gui) displayString(entity *git.RepoEntity) string { if entity.State == git.Queued { if inQueue, ty := gui.State.Queue.IsInTheQueue(entity); inQueue { switch mode := ty; mode { - case queue.Fetch: + case git.FetchJob: suffix = blue.Sprint(queuedSymbol) - case queue.Pull: + case git.PullJob: suffix = magenta.Sprint(queuedSymbol) - case queue.Merge: + case git.MergeJob: suffix = cyan.Sprint(queuedSymbol) default: suffix = green.Sprint(queuedSymbol) diff --git a/pkg/queue/job.go b/pkg/queue/job.go deleted file mode 100644 index 1fe5ba1..0000000 --- a/pkg/queue/job.go +++ /dev/null @@ -1,67 +0,0 @@ -package queue - -import ( - "time" - - "github.com/isacikgoz/gitbatch/pkg/git" -) - -// Job relates the type of the operation and the entity -type Job struct { - JobType JobType - Entity *git.RepoEntity -} - -// JobType is the a git operation supported -type JobType string - -const ( - // Fetch is wrapper of git fetch command - Fetch JobType = "fetch" - // Pull is wrapper of git pull command - Pull JobType = "pull" - // Merge is wrapper of git merge command - Merge JobType = "merge" -) - -// starts the job -func (job *Job) start() error { - job.Entity.State = git.Working - // added for testing, TODO: remove - time.Sleep(time.Second) - // TODO: Handle errors? - switch mode := job.JobType; mode { - case Fetch: - if err := git.Fetch(job.Entity, git.FetchOptions{ - RemoteName: job.Entity.Remote.Name, - }); err != nil { - job.Entity.State = git.Fail - return nil - } - case Pull: - if err := git.Fetch(job.Entity, git.FetchOptions{ - RemoteName: job.Entity.Remote.Name, - }); err != nil { - job.Entity.State = git.Fail - return nil - } - if err := git.Merge(job.Entity, git.MergeOptions{ - BranchName: job.Entity.Remote.Branch.Name, - }); err != nil { - job.Entity.State = git.Fail - return nil - } - case Merge: - if err := git.Merge(job.Entity, git.MergeOptions{ - BranchName: job.Entity.Remote.Branch.Name, - }); err != nil { - job.Entity.State = git.Fail - return nil - } - default: - job.Entity.State = git.Available - return nil - } - job.Entity.State = git.Success - return nil -} diff --git a/pkg/queue/queue.go b/pkg/queue/queue.go deleted file mode 100644 index 93626f0..0000000 --- a/pkg/queue/queue.go +++ /dev/null @@ -1,80 +0,0 @@ -package queue - -import ( - "errors" - - "github.com/isacikgoz/gitbatch/pkg/git" -) - -// JobQueue holds the slice of Jobs -type JobQueue struct { - series []*Job -} - -// CreateJobQueue creates a jobqueue struct and initialize its slice then return -// its pointer -func CreateJobQueue() (jobQueue *JobQueue) { - s := make([]*Job, 0) - return &JobQueue{ - series: s, - } -} - -// AddJob adds a job to the queue -func (jobQueue *JobQueue) AddJob(j *Job) error { - for _, job := range jobQueue.series { - if job.Entity.RepoID == j.Entity.RepoID && job.JobType == j.JobType { - return errors.New("Same job already is in the queue") - } - } - jobQueue.series = append(jobQueue.series, nil) - copy(jobQueue.series[1:], jobQueue.series[0:]) - jobQueue.series[0] = j - return nil -} - -// StartNext starts the next job in the queue -func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) { - finished = false - if len(jobQueue.series) < 1 { - finished = true - return nil, finished, nil - } - i := len(jobQueue.series) - 1 - lastJob := jobQueue.series[i] - jobQueue.series = jobQueue.series[:i] - if err = lastJob.start(); err != nil { - return lastJob, finished, err - } - return lastJob, finished, nil -} - -// RemoveFromQueue deletes the given entity and its job from the queue -// TODO: it is not safe if the job has been started -func (jobQueue *JobQueue) RemoveFromQueue(entity *git.RepoEntity) error { - removed := false - for i, job := range jobQueue.series { - if job.Entity.RepoID == entity.RepoID { - jobQueue.series = append(jobQueue.series[:i], jobQueue.series[i+1:]...) - removed = true - } - } - if !removed { - return errors.New("There is no job with given repoID") - } - return nil -} - -// IsInTheQueue function; since the job and entity is not tied with its own -// struct, this function returns true if that entity is in the queue along with -// the jobs type -func (jobQueue *JobQueue) IsInTheQueue(entity *git.RepoEntity) (inTheQueue bool, jt JobType) { - inTheQueue = false - for _, job := range jobQueue.series { - if job.Entity.RepoID == entity.RepoID { - inTheQueue = true - jt = job.JobType - } - } - return inTheQueue, jt -} -- cgit v1.2.3 From a4a29171fb11c98432ead8e72bc0948a549d9979 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Thu, 13 Dec 2018 02:12:03 +0300 Subject: login implementation stablized, some renaming, some refactoring and updated readme --- README.md | 19 ++---- pkg/git/add.go | 55 --------------- pkg/git/cmd-add.go | 55 +++++++++++++++ pkg/git/cmd-fetch.go | 146 ++++++++++++++++++++++++++++++++++++++++ pkg/git/cmd-merge.go | 39 +++++++++++ pkg/git/cmd-reset.go | 55 +++++++++++++++ pkg/git/cmd-rev-list.go | 42 ++++++++++++ pkg/git/cmd-stash.go | 106 +++++++++++++++++++++++++++++ pkg/git/cmd-status.go | 91 +++++++++++++++++++++++++ pkg/git/cmd.go | 82 ++++++++++++++++++++++ pkg/git/commands.go | 82 ---------------------- pkg/git/errors.go | 24 ------- pkg/git/fetch.go | 142 -------------------------------------- pkg/git/job-queue.go | 78 +++++++++++++++++++++ pkg/git/job.go | 26 ++++--- pkg/git/load.go | 48 ------------- pkg/git/merge.go | 39 ----------- pkg/git/queue.go | 78 --------------------- pkg/git/repository-sort.go | 59 ---------------- pkg/git/repository.go | 6 +- pkg/git/reset.go | 55 --------------- pkg/git/rev-list.go | 42 ------------ pkg/git/stash.go | 106 ----------------------------- pkg/git/status.go | 91 ------------------------- pkg/git/util-errors.go | 24 +++++++ pkg/git/util-load.go | 48 +++++++++++++ pkg/git/util-repository-sort.go | 59 ++++++++++++++++ pkg/gui/authenticationview.go | 81 ++++++++++++---------- pkg/gui/cheatsheet.go | 12 +--- pkg/gui/diffview.go | 9 +-- pkg/gui/errorview.go | 12 +--- pkg/gui/gui-util.go | 14 ++++ pkg/gui/keybindings.go | 4 +- pkg/gui/mainview.go | 3 +- pkg/gui/queuehandler.go | 8 ++- pkg/gui/stagedview.go | 6 +- pkg/gui/statusview.go | 27 ++------ pkg/gui/textstyle.go | 5 ++ 38 files changed, 942 insertions(+), 936 deletions(-) delete mode 100644 pkg/git/add.go create mode 100644 pkg/git/cmd-add.go create mode 100644 pkg/git/cmd-fetch.go create mode 100644 pkg/git/cmd-merge.go create mode 100644 pkg/git/cmd-reset.go create mode 100644 pkg/git/cmd-rev-list.go create mode 100644 pkg/git/cmd-stash.go create mode 100644 pkg/git/cmd-status.go create mode 100644 pkg/git/cmd.go delete mode 100644 pkg/git/commands.go delete mode 100644 pkg/git/errors.go delete mode 100644 pkg/git/fetch.go create mode 100644 pkg/git/job-queue.go delete mode 100644 pkg/git/load.go delete mode 100644 pkg/git/merge.go delete mode 100644 pkg/git/queue.go delete mode 100644 pkg/git/repository-sort.go delete mode 100644 pkg/git/reset.go delete mode 100644 pkg/git/rev-list.go delete mode 100644 pkg/git/stash.go delete mode 100644 pkg/git/status.go create mode 100644 pkg/git/util-errors.go create mode 100644 pkg/git/util-load.go create mode 100644 pkg/git/util-repository-sort.go diff --git a/README.md b/README.md index fcfef40..7719c88 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@ [![Build Status](https://travis-ci.com/isacikgoz/gitbatch.svg?branch=master)](https://travis-ci.com/isacikgoz/gitbatch) [![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/isacikgoz/gitbatch)](https://goreportcard.com/report/github.com/isacikgoz/gitbatch) ## gitbatch -This tool has been built to make your local repositories synchronized with remotes easily. I love lazygit, with that inspiration, decided to build this project to be even more lazy; Since my daily work is tied to many repositories I often end up walking on many directories and manually pulling updates etc. To make this routine faster, I created a simple tool to handle this job. I really enjoy working on this project and I hope it will be a useful tool. - -**Disclaimer** -- Unauthenticated repositories are **not supported** so using ssh is recommended if you need to authenticate to fetch/pull - - [Connecting to GitHub with SSH](https://help.github.com/articles/connecting-to-github-with-ssh/) - - [GitLab and SSH keys](https://docs.gitlab.com/ee/ssh/) - - [BitBucket Set up an SSH key](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-git-728138079.html) -- Feedbacks are welcome. For now, known issues are: - - At very low probability app fails to load repositories, try again it will load next time (multithreading problem). - - Colors vary to your terminal theme colors, so if the contrast is not enough on some color decisions; discussions are welcome. +This tool is beening built to make your local repositories synchronized with remotes easily. Although the focus is batch jobs, you can still do de facto micro management of your git repositories (e.g *add/reset, stash, commit etc.*) Here is the screencast of the app: [![asciicast](https://asciinema.org/a/eXgXpzZfuHxMpZqGMVODUipyc.svg)](https://asciinema.org/a/eXgXpzZfuHxMpZqGMVODUipyc) ## Installation -For now, installation requires golang compiler and minimum golang 1.10 is recommended. +For now, installation requires golang compiler and minimum golang 1.10 is recommended. (binary distribution will be provided on minimum viable product) - If you don't have golang installed refer to [golang.org](https://golang.org/dl/). - You should have $GOPATH env variable set and your $PATH should include $GOPATH/bin to run app from anywhere. @@ -42,9 +33,11 @@ For more information; ## Further goals - add testing - full src-d/go-git integration (*having some performance issues*) -- resolve authentication issues - add commit and maybe push? +## Known issues +Please refer to [Known issues page](https://github.com/isacikgoz/gitbatch/wiki/Known-issues) + ## Credits - [go-git](https://github.com/src-d/go-git) for git interface (partially) - [gocui](https://github.com/jroimartin/gocui) for user interface @@ -53,3 +46,5 @@ For more information; - [color](https://github.com/fatih/color) for colored text - [lazygit](https://github.com/jesseduffield/lazygit) as app template and reference - [kingpin](https://github.com/alecthomas/kingpin) for command-line flag&options + +I love [lazygit](https://github.com/jesseduffield/lazygit), with that inspiration, decided to build this project to be even more lazy. The rationale was; my daily work is tied to many repositories and I often end up walking on many directories and manually pulling updates etc. To make this routine faster, I created a simple tool to handle this job. I really enjoy working on this project and I hope it will be a useful tool. diff --git a/pkg/git/add.go b/pkg/git/add.go deleted file mode 100644 index 61fd587..0000000 --- a/pkg/git/add.go +++ /dev/null @@ -1,55 +0,0 @@ -package git - -import ( - "errors" - "strings" - - log "github.com/sirupsen/logrus" -) - -var addCommand = "add" - -// AddOptions defines the rules for "git add" command -type AddOptions struct { - Update bool - Force bool - DryRun bool -} - -// Add is a wrapper function for "git add" command -func (file *File) Add(option AddOptions) error { - args := make([]string, 0) - args = append(args, addCommand) - args = append(args, file.Name) - if option.Update { - args = append(args, "--update") - } - if option.Force { - args = append(args, "--force") - } - if option.DryRun { - args = append(args, "--dry-run") - } - out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) - if err != nil { - log.Warn("Error while add command") - return errors.New(out + "\n" + err.Error()) - } - return nil -} - -// AddAll function is the wrapper of "git add ." command -func (entity *RepoEntity) AddAll(option AddOptions) error { - args := make([]string, 0) - args = append(args, addCommand) - if option.DryRun { - args = append(args, "--dry-run") - } - args = append(args, ".") - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) - if err != nil { - log.Warn("Error while add command") - return errors.New(out + "\n" + err.Error()) - } - return nil -} diff --git a/pkg/git/cmd-add.go b/pkg/git/cmd-add.go new file mode 100644 index 0000000..61fd587 --- /dev/null +++ b/pkg/git/cmd-add.go @@ -0,0 +1,55 @@ +package git + +import ( + "errors" + "strings" + + log "github.com/sirupsen/logrus" +) + +var addCommand = "add" + +// AddOptions defines the rules for "git add" command +type AddOptions struct { + Update bool + Force bool + DryRun bool +} + +// Add is a wrapper function for "git add" command +func (file *File) Add(option AddOptions) error { + args := make([]string, 0) + args = append(args, addCommand) + args = append(args, file.Name) + if option.Update { + args = append(args, "--update") + } + if option.Force { + args = append(args, "--force") + } + if option.DryRun { + args = append(args, "--dry-run") + } + out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} + +// AddAll function is the wrapper of "git add ." command +func (entity *RepoEntity) AddAll(option AddOptions) error { + args := make([]string, 0) + args = append(args, addCommand) + if option.DryRun { + args = append(args, "--dry-run") + } + args = append(args, ".") + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} diff --git a/pkg/git/cmd-fetch.go b/pkg/git/cmd-fetch.go new file mode 100644 index 0000000..8a8bf9c --- /dev/null +++ b/pkg/git/cmd-fetch.go @@ -0,0 +1,146 @@ +package git + +import ( + "strings" + + log "github.com/sirupsen/logrus" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/config" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/http" +) + +var ( + fetchCmdMode string + fetchTryCount int + + fetchCommand = "fetch" + fetchCmdModeLegacy = "git" + fetchCmdModeNative = "go-git" + fetchMaxTry = 1 +) + +// FetchOptions defines the rules for fetch operation +type FetchOptions struct { + // Name of the remote to fetch from. Defaults to origin. + RemoteName string + // Credentials holds the user and pswd information + Credentials Credentials + // Before fetching, remove any remote-tracking references that no longer + // exist on the remote. + Prune bool + // Show what would be done, without making any changes. + DryRun bool + // Force allows the fetch to update a local branch even when the remote + // branch does not descend from it. + Force bool + // There should be more room for authentication, tags and progress +} + +// Fetch branches refs from one or more other repositories, along with the +// objects necessary to complete their histories +func Fetch(entity *RepoEntity, options FetchOptions) (err error) { + // here we configure fetch operation + // default mode is go-git (this may be configured) + fetchCmdMode = fetchCmdModeNative + fetchTryCount = 0 + // prune and dry run is not supported from go-git yet, rely on old friend + if options.Prune || options.DryRun { + fetchCmdMode = fetchCmdModeLegacy + } + switch fetchCmdMode { + case fetchCmdModeLegacy: + err = fetchWithGit(entity, options) + return err + case fetchCmdModeNative: + // this should be the refspec as default, let's give it a try + refspec := "+" + "refs/heads/" + entity.Branch.Name + ":" + "/refs/remotes/" + entity.Remote.Branch.Name + err = fetchWithGoGit(entity, options, refspec) + return err + } + return nil +} + +// fetchWithGit is simply a bare git fetch command which is flexible +// for complex operations, but on the other hand, it ties the app to another +// tool. To avoid that, using native implementation is preferred. +func fetchWithGit(entity *RepoEntity, options FetchOptions) (err error) { + args := make([]string, 0) + args = append(args, fetchCommand) + // parse options to command line arguments + if len(options.RemoteName) > 0 { + args = append(args, options.RemoteName) + } + if options.Prune { + args = append(args, "-p") + } + if options.Force { + args = append(args, "-f") + } + if options.DryRun { + args = append(args, "--dry-run") + } + if err := GenericGitCommand(entity.AbsPath, args); err != nil { + log.Warn("Error at git command (fetch)") + return err + } + // till this step everything should be ok + err = entity.Refresh() + return err +} + +// fetchWithGoGit is the primary fetch method and refspec is the main feature. +// RefSpec is a mapping from local branches to remote references The format of +// the refspec is an optional +, followed by :, where is the +// pattern for references on the remote side and is where those references +// will be written locally. The + tells Git to update the reference even if it +// isn’t a fast-forward. +func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (err error) { + opt := &git.FetchOptions{ + RemoteName: options.RemoteName, + RefSpecs: []config.RefSpec{config.RefSpec(refspec)}, + Force: options.Force, + } + // if any credential is given, let's add it to the git.FetchOptions + if len(options.Credentials.User) > 0 { + protocol, err := entity.authProtocol(entity.Remote) + if err != nil { + return err + } + if protocol == authProtocolHttp || protocol == authProtocolHttps { + opt.Auth = &http.BasicAuth{ + Username: options.Credentials.User, + Password: options.Credentials.Password, + } + } else { + return ErrInvalidAuthMethod + } + } + + err = entity.Repository.Fetch(opt) + if err != nil { + if err == git.NoErrAlreadyUpToDate { + // Already up-to-date + log.Warn(err.Error()) + // TODO: submit a PR for this kind of error, this type of catch is lame + } else if strings.Contains(err.Error(), "couldn't find remote ref") { + // we dont have remote ref, so lets pull other things.. maybe it'd be useful + rp := entity.Remote.RefSpecs[0] + if fetchTryCount < fetchMaxTry { + fetchTryCount++ + fetchWithGoGit(entity, options, rp) + } else { + return err + } + } else if err == transport.ErrAuthenticationRequired { + log.Warn(err.Error()) + return ErrAuthenticationRequired + } else { + log.Warn(err.Error()) + return err + } + } + // till this step everything should be ok + err = entity.Refresh() + return err +} diff --git a/pkg/git/cmd-merge.go b/pkg/git/cmd-merge.go new file mode 100644 index 0000000..984bc4b --- /dev/null +++ b/pkg/git/cmd-merge.go @@ -0,0 +1,39 @@ +package git + +import ( + log "github.com/sirupsen/logrus" +) + +var mergeCommand = "merge" + +// MergeOptions defines the rules of a merge operation +type MergeOptions struct { + // Name of the branch to merge with. + BranchName string + // Be verbose. + Verbose bool + // With true do not show a diffstat at the end of the merge. + NoStat bool +} + +// Merge incorporates changes from the named commits or branches into the +// current branch +func Merge(entity *RepoEntity, options MergeOptions) error { + args := make([]string, 0) + args = append(args, mergeCommand) + if len(options.BranchName) > 0 { + args = append(args, options.BranchName) + } + if options.Verbose { + args = append(args, "-v") + } + if options.NoStat { + args = append(args, "-n") + } + if err := GenericGitCommand(entity.AbsPath, args); err != nil { + log.Warn("Error while merging") + return err + } + entity.Refresh() + return nil +} diff --git a/pkg/git/cmd-reset.go b/pkg/git/cmd-reset.go new file mode 100644 index 0000000..f93225c --- /dev/null +++ b/pkg/git/cmd-reset.go @@ -0,0 +1,55 @@ +package git + +import ( + "errors" + "strings" + + log "github.com/sirupsen/logrus" +) + +var resetCommand = "reset" + +// ResetOptions defines the rules of git reset command +type ResetOptions struct { + Hard bool + Merge bool + Keep bool +} + +// Reset is the wrapper of "git reset" command +func (file *File) Reset(option ResetOptions) error { + args := make([]string, 0) + args = append(args, resetCommand) + args = append(args, "--") + args = append(args, file.Name) + if option.Hard { + args = append(args, "--hard") + } + if option.Merge { + args = append(args, "--merge") + } + if option.Keep { + args = append(args, "--keep") + } + out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} + +// ResetAll resets the changes in a repository, should be used wise +func (entity *RepoEntity) ResetAll(option ResetOptions) error { + args := make([]string, 0) + args = append(args, resetCommand) + if option.Hard { + args = append(args, "--hard") + } + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} diff --git a/pkg/git/cmd-rev-list.go b/pkg/git/cmd-rev-list.go new file mode 100644 index 0000000..664d69f --- /dev/null +++ b/pkg/git/cmd-rev-list.go @@ -0,0 +1,42 @@ +package git + +import ( + "strings" + + log "github.com/sirupsen/logrus" +) + +var revlistCommand = "rev-list" +var hashLength = 40 + +// RevListOptions defines the rules of rev-list func +type RevListOptions struct { + // Ref1 is the first reference hash to link + Ref1 string + // Ref2 is the second reference hash to link + Ref2 string +} + +// RevList returns the commit hashes that are links from the given commit(s). +// The output is given in reverse chronological order by default. +func RevList(entity *RepoEntity, options RevListOptions) ([]string, error) { + args := make([]string, 0) + args = append(args, revlistCommand) + if len(options.Ref1) > 0 && len(options.Ref2) > 0 { + arg1 := options.Ref1 + ".." + options.Ref2 + args = append(args, arg1) + } + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while rev-list command") + return []string{out}, err + } + hashes := strings.Split(out, "\n") + for _, hash := range hashes { + if len(hash) != hashLength { + return make([]string, 0), nil + } + break + } + return hashes, nil +} diff --git a/pkg/git/cmd-stash.go b/pkg/git/cmd-stash.go new file mode 100644 index 0000000..9d813a6 --- /dev/null +++ b/pkg/git/cmd-stash.go @@ -0,0 +1,106 @@ +package git + +import ( + "regexp" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +var stashCommand = "stash" + +// StashedItem holds the required fields for a stashed change +type StashedItem struct { + StashID int + BranchName string + Hash string + Description string + EntityPath string +} + +func stashGet(entity *RepoEntity, option string) string { + args := make([]string, 0) + args = append(args, stashCommand) + args = append(args, option) + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while stash command") + return "?" + } + return out +} + +func (entity *RepoEntity) loadStashedItems() error { + entity.Stasheds = make([]*StashedItem, 0) + output := stashGet(entity, "list") + stashIDRegex := regexp.MustCompile(`stash@{[\d]+}:`) + stashIDRegexInt := regexp.MustCompile(`[\d]+`) + stashBranchRegex := regexp.MustCompile(`[\w]+: `) + stashHashRegex := regexp.MustCompile(`[\w]{7}`) + + stashlist := strings.Split(output, "\n") + for _, stashitem := range stashlist { + // find id + id := stashIDRegexInt.FindString(stashIDRegex.FindString(stashitem)) + i, err := strconv.Atoi(id) + if err != nil { + // probably something isn't right let's continue over this iteration + log.Trace("cannot initiate stashed item") + continue + } + // trim id section + trimmed := stashIDRegex.Split(stashitem, 2)[1] + + // find branch + stashBranchRegexMatch := stashBranchRegex.FindString(trimmed) + branchName := stashBranchRegexMatch[:len(stashBranchRegexMatch)-2] + + // trim branch section + trimmed = stashBranchRegex.Split(trimmed, 2)[1] + hash := stashHashRegex.FindString(trimmed) + + // trim hash + desc := stashHashRegex.Split(trimmed, 2)[1][1:] + + entity.Stasheds = append(entity.Stasheds, &StashedItem{ + StashID: i, + BranchName: branchName, + Hash: hash, + Description: desc, + EntityPath: entity.AbsPath, + }) + } + return nil +} + +// Stash is the wrapper of convetional "git stash" command +func (entity *RepoEntity) Stash() (output string, err error) { + args := make([]string, 0) + args = append(args, stashCommand) + + output, err = GenericGitCommandWithErrorOutput(entity.AbsPath, args) + entity.Refresh() + return output, err +} + +// Pop is the wrapper of "git stash pop" command that used for a file +func (stashedItem *StashedItem) Pop() (output string, err error) { + args := make([]string, 0) + args = append(args, stashCommand) + args = append(args, "pop") + args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}") + output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args) + return output, err +} + +// Show is the wrapper of "git stash show -p " command +func (stashedItem *StashedItem) Show() (output string, err error) { + args := make([]string, 0) + args = append(args, stashCommand) + args = append(args, "show") + args = append(args, "-p") + args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}") + output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args) + return output, err +} diff --git a/pkg/git/cmd-status.go b/pkg/git/cmd-status.go new file mode 100644 index 0000000..4cca604 --- /dev/null +++ b/pkg/git/cmd-status.go @@ -0,0 +1,91 @@ +package git + +import ( + "os" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" +) + +var statusCommand = "status" + +// File represents the status of a file in an index or work tree +type File struct { + Name string + AbsPath string + X FileStatus + Y FileStatus +} + +// FileStatus is the short representation of state of a file +type FileStatus rune + +var ( + // StatusNotupdated says file not updated + StatusNotupdated FileStatus = ' ' + // StatusModified says file is modifed + StatusModified FileStatus = 'M' + // StatusAdded says file is added to index + StatusAdded FileStatus = 'A' + // StatusDeleted says file is deleted + StatusDeleted FileStatus = 'D' + // StatusRenamed says file is renamed + StatusRenamed FileStatus = 'R' + // StatusCopied says file is copied + StatusCopied FileStatus = 'C' + // StatusUpdated says file is updated + StatusUpdated FileStatus = 'U' + // StatusUntracked says file is untraced + StatusUntracked FileStatus = '?' + // StatusIgnored says file is ignored + StatusIgnored FileStatus = '!' +) + +func shortStatus(entity *RepoEntity, option string) string { + args := make([]string, 0) + args = append(args, statusCommand) + args = append(args, option) + args = append(args, "--short") + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while status command") + return "?" + } + return out +} + +// LoadFiles function simply commands a git status and collects output in a +// structured way +func (entity *RepoEntity) LoadFiles() ([]*File, error) { + files := make([]*File, 0) + output := shortStatus(entity, "--untracked-files=all") + if len(output) == 0 { + return files, nil + } + fileslist := strings.Split(output, "\n") + for _, file := range fileslist { + x := rune(file[0]) + y := rune(file[1]) + relativePathRegex := regexp.MustCompile(`[(\w|/|.|\-)]+`) + path := relativePathRegex.FindString(file[2:]) + + files = append(files, &File{ + Name: path, + AbsPath: entity.AbsPath + string(os.PathSeparator) + path, + X: FileStatus(x), + Y: FileStatus(y), + }) + } + return files, nil +} + +// Diff is a wrapper of "git diff" command for a file to compare with HEAD rev +func (file *File) Diff() (output string, err error) { + args := make([]string, 0) + args = append(args, "diff") + args = append(args, "HEAD") + args = append(args, file.Name) + output, err = GenericGitCommandWithErrorOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + return output, err +} diff --git a/pkg/git/cmd.go b/pkg/git/cmd.go new file mode 100644 index 0000000..b78b501 --- /dev/null +++ b/pkg/git/cmd.go @@ -0,0 +1,82 @@ +package git + +import ( + "github.com/isacikgoz/gitbatch/pkg/helpers" +) + +// GenericGitCommand runs any git command without expecting output +func GenericGitCommand(repoPath string, args []string) error { + _, err := helpers.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return err + } + return nil +} + +// GenericGitCommandWithOutput runs any git command with returning output +func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) { + out, err := helpers.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return "?", err + } + return helpers.TrimTrailingNewline(out), nil +} + +// GenericGitCommandWithErrorOutput runs any git command with returning output +func GenericGitCommandWithErrorOutput(repoPath string, args []string) (string, error) { + out, err := helpers.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return helpers.TrimTrailingNewline(out), err + } + return helpers.TrimTrailingNewline(out), nil +} + +// GitShow is conventional git show command without any argument +func GitShow(repoPath, hash string) string { + args := []string{"show", hash} + diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return "?" + } + return diff +} + +// GitShowEmail gets author's e-mail with git show command +func GitShowEmail(repoPath, hash string) string { + args := []string{"show", "--quiet", "--pretty=format:%ae", hash} + diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return "?" + } + return diff +} + +// GitShowBody gets body of the commit with git show +func GitShowBody(repoPath, hash string) string { + args := []string{"show", "--quiet", "--pretty=format:%B", hash} + diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return err.Error() + } + return diff +} + +// GitShowDate gets commit's date with git show as string +func GitShowDate(repoPath, hash string) string { + args := []string{"show", "--quiet", "--pretty=format:%ai", hash} + diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return "?" + } + return diff +} + +// StatusWithGit returns the plaintext short status of the repo +func (entity *RepoEntity) StatusWithGit() string { + args := []string{"status"} + status, err := helpers.RunCommandWithOutput(entity.AbsPath, "git", args) + if err != nil { + return "?" + } + return status +} diff --git a/pkg/git/commands.go b/pkg/git/commands.go deleted file mode 100644 index b78b501..0000000 --- a/pkg/git/commands.go +++ /dev/null @@ -1,82 +0,0 @@ -package git - -import ( - "github.com/isacikgoz/gitbatch/pkg/helpers" -) - -// GenericGitCommand runs any git command without expecting output -func GenericGitCommand(repoPath string, args []string) error { - _, err := helpers.RunCommandWithOutput(repoPath, "git", args) - if err != nil { - return err - } - return nil -} - -// GenericGitCommandWithOutput runs any git command with returning output -func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) { - out, err := helpers.RunCommandWithOutput(repoPath, "git", args) - if err != nil { - return "?", err - } - return helpers.TrimTrailingNewline(out), nil -} - -// GenericGitCommandWithErrorOutput runs any git command with returning output -func GenericGitCommandWithErrorOutput(repoPath string, args []string) (string, error) { - out, err := helpers.RunCommandWithOutput(repoPath, "git", args) - if err != nil { - return helpers.TrimTrailingNewline(out), err - } - return helpers.TrimTrailingNewline(out), nil -} - -// GitShow is conventional git show command without any argument -func GitShow(repoPath, hash string) string { - args := []string{"show", hash} - diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) - if err != nil { - return "?" - } - return diff -} - -// GitShowEmail gets author's e-mail with git show command -func GitShowEmail(repoPath, hash string) string { - args := []string{"show", "--quiet", "--pretty=format:%ae", hash} - diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) - if err != nil { - return "?" - } - return diff -} - -// GitShowBody gets body of the commit with git show -func GitShowBody(repoPath, hash string) string { - args := []string{"show", "--quiet", "--pretty=format:%B", hash} - diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) - if err != nil { - return err.Error() - } - return diff -} - -// GitShowDate gets commit's date with git show as string -func GitShowDate(repoPath, hash string) string { - args := []string{"show", "--quiet", "--pretty=format:%ai", hash} - diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) - if err != nil { - return "?" - } - return diff -} - -// StatusWithGit returns the plaintext short status of the repo -func (entity *RepoEntity) StatusWithGit() string { - args := []string{"status"} - status, err := helpers.RunCommandWithOutput(entity.AbsPath, "git", args) - if err != nil { - return "?" - } - return status -} diff --git a/pkg/git/errors.go b/pkg/git/errors.go deleted file mode 100644 index 8621cbc..0000000 --- a/pkg/git/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package git - -import ( - "errors" -) - -var ( - // ErrGitCommand is thrown when git command returned an error code - ErrGitCommand = errors.New("Git command returned error code") - // ErrAuthenticationRequired is thrown when an authentication required on - // a remote operation - ErrAuthenticationRequired = errors.New("Authentication required") - // ErrAuthorizationFailed is thrown when authorization failed while trying - // to authenticate with remote - ErrAuthorizationFailed = errors.New("Authorization failed") - // ErrInvalidAuthMethod is thrown when invalid auth method is invoked - ErrInvalidAuthMethod = errors.New("invalid auth method") - // ErrAlreadyUpToDate is thrown when a repository is already up to date - // with its src on merge/fetch/pull - ErrAlreadyUpToDate = errors.New("Already up to date") - // ErrCouldNotFindRemoteRef is thrown when trying to fetch/pull cannot - // find suitable remote reference - ErrCouldNotFindRemoteRef = errors.New("Could not find remote ref") -) diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go deleted file mode 100644 index 39a0277..0000000 --- a/pkg/git/fetch.go +++ /dev/null @@ -1,142 +0,0 @@ -package git - -import ( - "strings" - - log "github.com/sirupsen/logrus" - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/config" - "gopkg.in/src-d/go-git.v4/plumbing/transport" - "gopkg.in/src-d/go-git.v4/plumbing/transport/http" -) - -var ( - fetchCmdMode string - fetchTryCount int - - fetchCommand = "fetch" - fetchCmdModeLegacy = "git" - fetchCmdModeNative = "go-git" - fetchMaxTry = 1 -) - -// FetchOptions defines the rules for fetch operation -type FetchOptions struct { - // Name of the remote to fetch from. Defaults to origin. - RemoteName string - // Credentials holds the user and pswd information - Credentials Credentials - // Before fetching, remove any remote-tracking references that no longer - // exist on the remote. - Prune bool - // Show what would be done, without making any changes. - DryRun bool - // Force allows the fetch to update a local branch even when the remote - // branch does not descend from it. - Force bool - // There should be more room for authentication, tags and progress -} - -// Fetch branches refs from one or more other repositories, along with the -// objects necessary to complete their histories -func Fetch(entity *RepoEntity, options FetchOptions) (err error) { - // here we configure fetch operation - // default mode is go-git (this may be configured) - fetchCmdMode = fetchCmdModeNative - fetchTryCount = 0 - // prune and dry run is not supported from go-git yet, rely on old friend - if options.Prune || options.DryRun { - fetchCmdMode = fetchCmdModeLegacy - } - switch fetchCmdMode { - case fetchCmdModeLegacy: - err = fetchWithGit(entity.AbsPath, options) - return err - case fetchCmdModeNative: - // this should be the refspec as default, let's give it a try - refspec := "+" + "refs/heads/" + entity.Branch.Name + ":" + "/refs/remotes/" + entity.Remote.Branch.Name - err = fetchWithGoGit(entity, options, refspec) - return err - } - // till this step everything should be ok - err = entity.Refresh() - return err -} - -// fetchWithGit is simply a bare git fetch command which is flexible -// for complex operations, but on the other hand, it ties the app to another -// tool. To avoid that, using native implementation is preferred. -func fetchWithGit(abspath string, options FetchOptions) (err error) { - args := make([]string, 0) - args = append(args, fetchCommand) - // parse options to command line arguments - if len(options.RemoteName) > 0 { - args = append(args, options.RemoteName) - } - if options.Prune { - args = append(args, "-p") - } - if options.Force { - args = append(args, "-f") - } - if options.DryRun { - args = append(args, "--dry-run") - } - if err := GenericGitCommand(abspath, args); err != nil { - log.Warn("Error at git command (fetch)") - } - return err -} - -// fetchWithGoGit is the primary fetch method and refspec is the main feature. -// RefSpec is a mapping from local branches to remote references The format of -// the refspec is an optional +, followed by :, where is the -// pattern for references on the remote side and is where those references -// will be written locally. The + tells Git to update the reference even if it -// isn’t a fast-forward. -func fetchWithGoGit(entity *RepoEntity, options FetchOptions, refspec string) (err error) { - opt := &git.FetchOptions{ - RemoteName: options.RemoteName, - RefSpecs: []config.RefSpec{config.RefSpec(refspec)}, - Force: options.Force, - } - if len(options.Credentials.User) > 0 { - protocol, err := entity.authProtocol(entity.Remote) - if err != nil { - return err - } - if protocol == authProtocolHttp || protocol == authProtocolHttps { - opt.Auth = &http.BasicAuth{ - Username: options.Credentials.User, - Password: options.Credentials.Password, - } - } else { - return ErrInvalidAuthMethod - } - } - - err = entity.Repository.Fetch(opt) - if err != nil { - if err == git.NoErrAlreadyUpToDate { - // Already up-to-date - log.Warn(err.Error()) - // TODO: submit a PR for this kind of error, this type of catch is lame - } else if strings.Contains(err.Error(), "couldn't find remote ref") { - // we dont have remote ref, so lets pull other things.. maybe it'd be useful - rp := entity.Remote.RefSpecs[0] - if fetchTryCount < fetchMaxTry { - fetchTryCount++ - fetchWithGoGit(entity, options, rp) - } else { - return err - } - } else if err == transport.ErrAuthenticationRequired { - log.Warn(err.Error()) - return ErrAuthenticationRequired - } else { - log.Warn(err.Error()) - return err - } - } - return nil -} diff --git a/pkg/git/job-queue.go b/pkg/git/job-queue.go new file mode 100644 index 0000000..e95250a --- /dev/null +++ b/pkg/git/job-queue.go @@ -0,0 +1,78 @@ +package git + +import ( + "errors" +) + +// JobQueue holds the slice of Jobs +type JobQueue struct { + series []*Job +} + +// CreateJobQueue creates a jobqueue struct and initialize its slice then return +// its pointer +func CreateJobQueue() (jobQueue *JobQueue) { + s := make([]*Job, 0) + return &JobQueue{ + series: s, + } +} + +// AddJob adds a job to the queue +func (jobQueue *JobQueue) AddJob(j *Job) error { + for _, job := range jobQueue.series { + if job.Entity.RepoID == j.Entity.RepoID && job.JobType == j.JobType { + return errors.New("Same job already is in the queue") + } + } + jobQueue.series = append(jobQueue.series, nil) + copy(jobQueue.series[1:], jobQueue.series[0:]) + jobQueue.series[0] = j + return nil +} + +// StartNext starts the next job in the queue +func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) { + finished = false + if len(jobQueue.series) < 1 { + finished = true + return nil, finished, nil + } + i := len(jobQueue.series) - 1 + lastJob := jobQueue.series[i] + jobQueue.series = jobQueue.series[:i] + if err = lastJob.start(); err != nil { + return lastJob, finished, err + } + return lastJob, finished, nil +} + +// RemoveFromQueue deletes the given entity and its job from the queue +// TODO: it is not safe if the job has been started +func (jobQueue *JobQueue) RemoveFromQueue(entity *RepoEntity) error { + removed := false + for i, job := range jobQueue.series { + if job.Entity.RepoID == entity.RepoID { + jobQueue.series = append(jobQueue.series[:i], jobQueue.series[i+1:]...) + removed = true + } + } + if !removed { + return errors.New("There is no job with given repoID") + } + return nil +} + +// IsInTheQueue function; since the job and entity is not tied with its own +// struct, this function returns true if that entity is in the queue along with +// the jobs type +func (jobQueue *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) { + inTheQueue = false + for _, job := range jobQueue.series { + if job.Entity.RepoID == entity.RepoID { + inTheQueue = true + jt = job.JobType + } + } + return inTheQueue, jt +} diff --git a/pkg/git/job.go b/pkg/git/job.go index 13ef566..7dc4126 100644 --- a/pkg/git/job.go +++ b/pkg/git/job.go @@ -1,13 +1,14 @@ package git -import ( - "time" -) +import () // Job relates the type of the operation and the entity type Job struct { + // JobType is to select operation type that will be applied to repository JobType JobType - Entity *RepoEntity + // Entity points to the repository that will be used for operation + Entity *RepoEntity + // Options is a placeholder for operation options Options interface{} } @@ -26,9 +27,8 @@ const ( // starts the job func (job *Job) start() error { job.Entity.State = Working - // added for testing, TODO: remove - time.Sleep(time.Second) // TODO: Handle errors? + // TOOD: Better implementation required switch mode := job.JobType; mode { case FetchJob: var opts FetchOptions @@ -44,11 +44,17 @@ func (job *Job) start() error { return err } case PullJob: - if err := Fetch(job.Entity, FetchOptions{ - RemoteName: job.Entity.Remote.Name, - }); err != nil { + var opts FetchOptions + if job.Options != nil { + opts = job.Options.(FetchOptions) + } else { + opts = FetchOptions{ + RemoteName: job.Entity.Remote.Name, + } + } + if err := Fetch(job.Entity, opts); err != nil { job.Entity.State = Fail - return nil + return err } if err := Merge(job.Entity, MergeOptions{ BranchName: job.Entity.Remote.Branch.Name, diff --git a/pkg/git/load.go b/pkg/git/load.go deleted file mode 100644 index e35e1c1..0000000 --- a/pkg/git/load.go +++ /dev/null @@ -1,48 +0,0 @@ -package git - -import ( - log "github.com/sirupsen/logrus" - - "errors" - "sync" -) - -// LoadRepositoryEntities initializes the go-git's repository obejcts with given -// slice of paths. since this job is done parallel, the order of the directories -// is not kept -func LoadRepositoryEntities(directories []string) (entities []*RepoEntity, err error) { - entities = make([]*RepoEntity, 0) - - var wg sync.WaitGroup - var mu sync.Mutex - - for _, dir := range directories { - // increment wait counter by one because we run a single goroutine - // below - wg.Add(1) - go func(d string) { - // decrement the wait counter by one, we call it in a defer so it's - // called at the end of this goroutine - defer wg.Done() - entity, err := InitializeRepository(d) - if err != nil { - log.WithFields(log.Fields{ - "directory": d, - }).Trace("Cannot load git repository.") - return - } - // lock so we don't get a race if multiple go routines try to add - // to the same entities - mu.Lock() - entities = append(entities, entity) - mu.Unlock() - }(dir) - } - // wait until the wait counter is zero, this happens if all goroutines have - // finished - wg.Wait() - if len(entities) == 0 { - return entities, errors.New("There are no git repositories at given path(s)") - } - return entities, nil -} diff --git a/pkg/git/merge.go b/pkg/git/merge.go deleted file mode 100644 index 984bc4b..0000000 --- a/pkg/git/merge.go +++ /dev/null @@ -1,39 +0,0 @@ -package git - -import ( - log "github.com/sirupsen/logrus" -) - -var mergeCommand = "merge" - -// MergeOptions defines the rules of a merge operation -type MergeOptions struct { - // Name of the branch to merge with. - BranchName string - // Be verbose. - Verbose bool - // With true do not show a diffstat at the end of the merge. - NoStat bool -} - -// Merge incorporates changes from the named commits or branches into the -// current branch -func Merge(entity *RepoEntity, options MergeOptions) error { - args := make([]string, 0) - args = append(args, mergeCommand) - if len(options.BranchName) > 0 { - args = append(args, options.BranchName) - } - if options.Verbose { - args = append(args, "-v") - } - if options.NoStat { - args = append(args, "-n") - } - if err := GenericGitCommand(entity.AbsPath, args); err != nil { - log.Warn("Error while merging") - return err - } - entity.Refresh() - return nil -} diff --git a/pkg/git/queue.go b/pkg/git/queue.go deleted file mode 100644 index e95250a..0000000 --- a/pkg/git/queue.go +++ /dev/null @@ -1,78 +0,0 @@ -package git - -import ( - "errors" -) - -// JobQueue holds the slice of Jobs -type JobQueue struct { - series []*Job -} - -// CreateJobQueue creates a jobqueue struct and initialize its slice then return -// its pointer -func CreateJobQueue() (jobQueue *JobQueue) { - s := make([]*Job, 0) - return &JobQueue{ - series: s, - } -} - -// AddJob adds a job to the queue -func (jobQueue *JobQueue) AddJob(j *Job) error { - for _, job := range jobQueue.series { - if job.Entity.RepoID == j.Entity.RepoID && job.JobType == j.JobType { - return errors.New("Same job already is in the queue") - } - } - jobQueue.series = append(jobQueue.series, nil) - copy(jobQueue.series[1:], jobQueue.series[0:]) - jobQueue.series[0] = j - return nil -} - -// StartNext starts the next job in the queue -func (jobQueue *JobQueue) StartNext() (j *Job, finished bool, err error) { - finished = false - if len(jobQueue.series) < 1 { - finished = true - return nil, finished, nil - } - i := len(jobQueue.series) - 1 - lastJob := jobQueue.series[i] - jobQueue.series = jobQueue.series[:i] - if err = lastJob.start(); err != nil { - return lastJob, finished, err - } - return lastJob, finished, nil -} - -// RemoveFromQueue deletes the given entity and its job from the queue -// TODO: it is not safe if the job has been started -func (jobQueue *JobQueue) RemoveFromQueue(entity *RepoEntity) error { - removed := false - for i, job := range jobQueue.series { - if job.Entity.RepoID == entity.RepoID { - jobQueue.series = append(jobQueue.series[:i], jobQueue.series[i+1:]...) - removed = true - } - } - if !removed { - return errors.New("There is no job with given repoID") - } - return nil -} - -// IsInTheQueue function; since the job and entity is not tied with its own -// struct, this function returns true if that entity is in the queue along with -// the jobs type -func (jobQueue *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) { - inTheQueue = false - for _, job := range jobQueue.series { - if job.Entity.RepoID == entity.RepoID { - inTheQueue = true - jt = job.JobType - } - } - return inTheQueue, jt -} diff --git a/pkg/git/repository-sort.go b/pkg/git/repository-sort.go deleted file mode 100644 index de5f454..0000000 --- a/pkg/git/repository-sort.go +++ /dev/null @@ -1,59 +0,0 @@ -package git - -import ( - "unicode" -) - -// Alphabetical slice is the re-ordered *RepoEntity slice that sorted according -// to alphabetical order (A-Z) -type Alphabetical []*RepoEntity - -// Len is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Len() int { return len(s) } - -// Swap is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Less(i, j int) bool { - iRunes := []rune(s[i].Name) - jRunes := []rune(s[j].Name) - - max := len(iRunes) - if max > len(jRunes) { - max = len(jRunes) - } - - for idx := 0; idx < max; idx++ { - ir := iRunes[idx] - jr := jRunes[idx] - - lir := unicode.ToLower(ir) - ljr := unicode.ToLower(jr) - - if lir != ljr { - return lir < ljr - } - - // the lowercase runes are the same, so compare the original - if ir != jr { - return ir < jr - } - } - return false -} - -// LastModified slice is the re-ordered *RepoEntity slice that sorted according -// to last modified date of the repository directory -type LastModified []*RepoEntity - -// Len is the interface implementation for LastModified sorting function -func (s LastModified) Len() int { return len(s) } - -// Swap is the interface implementation for LastModified sorting function -func (s LastModified) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less is the interface implementation for LastModified sorting function -func (s LastModified) Less(i, j int) bool { - return s[i].ModTime.Unix() > s[j].ModTime.Unix() -} diff --git a/pkg/git/repository.go b/pkg/git/repository.go index 8d0160b..ff67611 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -39,10 +39,12 @@ const ( Queued RepoState = 1 // Working means an operation is just started for this repository Working RepoState = 2 + // Paused is expected when a user interaction is required + Paused RepoState = 3 // Success is the expected outcome of the operation - Success RepoState = 3 + Success RepoState = 4 // Fail is the unexpected outcome of the operation - Fail RepoState = 4 + Fail RepoState = 5 ) // InitializeRepository initializes a RepoEntity struct with its belongings. diff --git a/pkg/git/reset.go b/pkg/git/reset.go deleted file mode 100644 index f93225c..0000000 --- a/pkg/git/reset.go +++ /dev/null @@ -1,55 +0,0 @@ -package git - -import ( - "errors" - "strings" - - log "github.com/sirupsen/logrus" -) - -var resetCommand = "reset" - -// ResetOptions defines the rules of git reset command -type ResetOptions struct { - Hard bool - Merge bool - Keep bool -} - -// Reset is the wrapper of "git reset" command -func (file *File) Reset(option ResetOptions) error { - args := make([]string, 0) - args = append(args, resetCommand) - args = append(args, "--") - args = append(args, file.Name) - if option.Hard { - args = append(args, "--hard") - } - if option.Merge { - args = append(args, "--merge") - } - if option.Keep { - args = append(args, "--keep") - } - out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) - if err != nil { - log.Warn("Error while add command") - return errors.New(out + "\n" + err.Error()) - } - return nil -} - -// ResetAll resets the changes in a repository, should be used wise -func (entity *RepoEntity) ResetAll(option ResetOptions) error { - args := make([]string, 0) - args = append(args, resetCommand) - if option.Hard { - args = append(args, "--hard") - } - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) - if err != nil { - log.Warn("Error while add command") - return errors.New(out + "\n" + err.Error()) - } - return nil -} diff --git a/pkg/git/rev-list.go b/pkg/git/rev-list.go deleted file mode 100644 index 664d69f..0000000 --- a/pkg/git/rev-list.go +++ /dev/null @@ -1,42 +0,0 @@ -package git - -import ( - "strings" - - log "github.com/sirupsen/logrus" -) - -var revlistCommand = "rev-list" -var hashLength = 40 - -// RevListOptions defines the rules of rev-list func -type RevListOptions struct { - // Ref1 is the first reference hash to link - Ref1 string - // Ref2 is the second reference hash to link - Ref2 string -} - -// RevList returns the commit hashes that are links from the given commit(s). -// The output is given in reverse chronological order by default. -func RevList(entity *RepoEntity, options RevListOptions) ([]string, error) { - args := make([]string, 0) - args = append(args, revlistCommand) - if len(options.Ref1) > 0 && len(options.Ref2) > 0 { - arg1 := options.Ref1 + ".." + options.Ref2 - args = append(args, arg1) - } - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) - if err != nil { - log.Warn("Error while rev-list command") - return []string{out}, err - } - hashes := strings.Split(out, "\n") - for _, hash := range hashes { - if len(hash) != hashLength { - return make([]string, 0), nil - } - break - } - return hashes, nil -} diff --git a/pkg/git/stash.go b/pkg/git/stash.go deleted file mode 100644 index 9d813a6..0000000 --- a/pkg/git/stash.go +++ /dev/null @@ -1,106 +0,0 @@ -package git - -import ( - "regexp" - "strconv" - "strings" - - log "github.com/sirupsen/logrus" -) - -var stashCommand = "stash" - -// StashedItem holds the required fields for a stashed change -type StashedItem struct { - StashID int - BranchName string - Hash string - Description string - EntityPath string -} - -func stashGet(entity *RepoEntity, option string) string { - args := make([]string, 0) - args = append(args, stashCommand) - args = append(args, option) - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) - if err != nil { - log.Warn("Error while stash command") - return "?" - } - return out -} - -func (entity *RepoEntity) loadStashedItems() error { - entity.Stasheds = make([]*StashedItem, 0) - output := stashGet(entity, "list") - stashIDRegex := regexp.MustCompile(`stash@{[\d]+}:`) - stashIDRegexInt := regexp.MustCompile(`[\d]+`) - stashBranchRegex := regexp.MustCompile(`[\w]+: `) - stashHashRegex := regexp.MustCompile(`[\w]{7}`) - - stashlist := strings.Split(output, "\n") - for _, stashitem := range stashlist { - // find id - id := stashIDRegexInt.FindString(stashIDRegex.FindString(stashitem)) - i, err := strconv.Atoi(id) - if err != nil { - // probably something isn't right let's continue over this iteration - log.Trace("cannot initiate stashed item") - continue - } - // trim id section - trimmed := stashIDRegex.Split(stashitem, 2)[1] - - // find branch - stashBranchRegexMatch := stashBranchRegex.FindString(trimmed) - branchName := stashBranchRegexMatch[:len(stashBranchRegexMatch)-2] - - // trim branch section - trimmed = stashBranchRegex.Split(trimmed, 2)[1] - hash := stashHashRegex.FindString(trimmed) - - // trim hash - desc := stashHashRegex.Split(trimmed, 2)[1][1:] - - entity.Stasheds = append(entity.Stasheds, &StashedItem{ - StashID: i, - BranchName: branchName, - Hash: hash, - Description: desc, - EntityPath: entity.AbsPath, - }) - } - return nil -} - -// Stash is the wrapper of convetional "git stash" command -func (entity *RepoEntity) Stash() (output string, err error) { - args := make([]string, 0) - args = append(args, stashCommand) - - output, err = GenericGitCommandWithErrorOutput(entity.AbsPath, args) - entity.Refresh() - return output, err -} - -// Pop is the wrapper of "git stash pop" command that used for a file -func (stashedItem *StashedItem) Pop() (output string, err error) { - args := make([]string, 0) - args = append(args, stashCommand) - args = append(args, "pop") - args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}") - output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args) - return output, err -} - -// Show is the wrapper of "git stash show -p " command -func (stashedItem *StashedItem) Show() (output string, err error) { - args := make([]string, 0) - args = append(args, stashCommand) - args = append(args, "show") - args = append(args, "-p") - args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}") - output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args) - return output, err -} diff --git a/pkg/git/status.go b/pkg/git/status.go deleted file mode 100644 index 4cca604..0000000 --- a/pkg/git/status.go +++ /dev/null @@ -1,91 +0,0 @@ -package git - -import ( - "os" - "regexp" - "strings" - - log "github.com/sirupsen/logrus" -) - -var statusCommand = "status" - -// File represents the status of a file in an index or work tree -type File struct { - Name string - AbsPath string - X FileStatus - Y FileStatus -} - -// FileStatus is the short representation of state of a file -type FileStatus rune - -var ( - // StatusNotupdated says file not updated - StatusNotupdated FileStatus = ' ' - // StatusModified says file is modifed - StatusModified FileStatus = 'M' - // StatusAdded says file is added to index - StatusAdded FileStatus = 'A' - // StatusDeleted says file is deleted - StatusDeleted FileStatus = 'D' - // StatusRenamed says file is renamed - StatusRenamed FileStatus = 'R' - // StatusCopied says file is copied - StatusCopied FileStatus = 'C' - // StatusUpdated says file is updated - StatusUpdated FileStatus = 'U' - // StatusUntracked says file is untraced - StatusUntracked FileStatus = '?' - // StatusIgnored says file is ignored - StatusIgnored FileStatus = '!' -) - -func shortStatus(entity *RepoEntity, option string) string { - args := make([]string, 0) - args = append(args, statusCommand) - args = append(args, option) - args = append(args, "--short") - out, err := GenericGitCommandWithOutput(entity.AbsPath, args) - if err != nil { - log.Warn("Error while status command") - return "?" - } - return out -} - -// LoadFiles function simply commands a git status and collects output in a -// structured way -func (entity *RepoEntity) LoadFiles() ([]*File, error) { - files := make([]*File, 0) - output := shortStatus(entity, "--untracked-files=all") - if len(output) == 0 { - return files, nil - } - fileslist := strings.Split(output, "\n") - for _, file := range fileslist { - x := rune(file[0]) - y := rune(file[1]) - relativePathRegex := regexp.MustCompile(`[(\w|/|.|\-)]+`) - path := relativePathRegex.FindString(file[2:]) - - files = append(files, &File{ - Name: path, - AbsPath: entity.AbsPath + string(os.PathSeparator) + path, - X: FileStatus(x), - Y: FileStatus(y), - }) - } - return files, nil -} - -// Diff is a wrapper of "git diff" command for a file to compare with HEAD rev -func (file *File) Diff() (output string, err error) { - args := make([]string, 0) - args = append(args, "diff") - args = append(args, "HEAD") - args = append(args, file.Name) - output, err = GenericGitCommandWithErrorOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) - return output, err -} diff --git a/pkg/git/util-errors.go b/pkg/git/util-errors.go new file mode 100644 index 0000000..8621cbc --- /dev/null +++ b/pkg/git/util-errors.go @@ -0,0 +1,24 @@ +package git + +import ( + "errors" +) + +var ( + // ErrGitCommand is thrown when git command returned an error code + ErrGitCommand = errors.New("Git command returned error code") + // ErrAuthenticationRequired is thrown when an authentication required on + // a remote operation + ErrAuthenticationRequired = errors.New("Authentication required") + // ErrAuthorizationFailed is thrown when authorization failed while trying + // to authenticate with remote + ErrAuthorizationFailed = errors.New("Authorization failed") + // ErrInvalidAuthMethod is thrown when invalid auth method is invoked + ErrInvalidAuthMethod = errors.New("invalid auth method") + // ErrAlreadyUpToDate is thrown when a repository is already up to date + // with its src on merge/fetch/pull + ErrAlreadyUpToDate = errors.New("Already up to date") + // ErrCouldNotFindRemoteRef is thrown when trying to fetch/pull cannot + // find suitable remote reference + ErrCouldNotFindRemoteRef = errors.New("Could not find remote ref") +) diff --git a/pkg/git/util-load.go b/pkg/git/util-load.go new file mode 100644 index 0000000..e35e1c1 --- /dev/null +++ b/pkg/git/util-load.go @@ -0,0 +1,48 @@ +package git + +import ( + log "github.com/sirupsen/logrus" + + "errors" + "sync" +) + +// LoadRepositoryEntities initializes the go-git's repository obejcts with given +// slice of paths. since this job is done parallel, the order of the directories +// is not kept +func LoadRepositoryEntities(directories []string) (entities []*RepoEntity, err error) { + entities = make([]*RepoEntity, 0) + + var wg sync.WaitGroup + var mu sync.Mutex + + for _, dir := range directories { + // increment wait counter by one because we run a single goroutine + // below + wg.Add(1) + go func(d string) { + // decrement the wait counter by one, we call it in a defer so it's + // called at the end of this goroutine + defer wg.Done() + entity, err := InitializeRepository(d) + if err != nil { + log.WithFields(log.Fields{ + "directory": d, + }).Trace("Cannot load git repository.") + return + } + // lock so we don't get a race if multiple go routines try to add + // to the same entities + mu.Lock() + entities = append(entities, entity) + mu.Unlock() + }(dir) + } + // wait until the wait counter is zero, this happens if all goroutines have + // finished + wg.Wait() + if len(entities) == 0 { + return entities, errors.New("There are no git repositories at given path(s)") + } + return entities, nil +} diff --git a/pkg/git/util-repository-sort.go b/pkg/git/util-repository-sort.go new file mode 100644 index 0000000..de5f454 --- /dev/null +++ b/pkg/git/util-repository-sort.go @@ -0,0 +1,59 @@ +package git + +import ( + "unicode" +) + +// Alphabetical slice is the re-ordered *RepoEntity slice that sorted according +// to alphabetical order (A-Z) +type Alphabetical []*RepoEntity + +// Len is the interface implementation for Alphabetical sorting function +func (s Alphabetical) Len() int { return len(s) } + +// Swap is the interface implementation for Alphabetical sorting function +func (s Alphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less is the interface implementation for Alphabetical sorting function +func (s Alphabetical) Less(i, j int) bool { + iRunes := []rune(s[i].Name) + jRunes := []rune(s[j].Name) + + max := len(iRunes) + if max > len(jRunes) { + max = len(jRunes) + } + + for idx := 0; idx < max; idx++ { + ir := iRunes[idx] + jr := jRunes[idx] + + lir := unicode.ToLower(ir) + ljr := unicode.ToLower(jr) + + if lir != ljr { + return lir < ljr + } + + // the lowercase runes are the same, so compare the original + if ir != jr { + return ir < jr + } + } + return false +} + +// LastModified slice is the re-ordered *RepoEntity slice that sorted according +// to last modified date of the repository directory +type LastModified []*RepoEntity + +// Len is the interface implementation for LastModified sorting function +func (s LastModified) Len() int { return len(s) } + +// Swap is the interface implementation for LastModified sorting function +func (s LastModified) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less is the interface implementation for LastModified sorting function +func (s LastModified) Less(i, j int) bool { + return s[i].ModTime.Unix() > s[j].ModTime.Unix() +} diff --git a/pkg/gui/authenticationview.go b/pkg/gui/authenticationview.go index 240b46c..9c830cd 100644 --- a/pkg/gui/authenticationview.go +++ b/pkg/gui/authenticationview.go @@ -2,30 +2,40 @@ package gui import ( "fmt" + "regexp" + "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" log "github.com/sirupsen/logrus" ) var ( - authenticationReturnView string + // this is required so we can know where we can return + authenticationReturnView string + // these views used as a label for git repository address and credential views authenticationViewFeature = viewFeature{Name: "authentication", Title: " Authentication "} - authUserFeature = viewFeature{Name: "authuser", Title: " User "} - authPasswordViewFeature = viewFeature{Name: "authpasswd", Title: " Password "} authUserLabelFeature = viewFeature{Name: "authuserlabel", Title: " User: "} authPswdLabelViewFeature = viewFeature{Name: "authpasswdlabel", Title: " Password: "} + // these views used as a input for the credentials + authUserFeature = viewFeature{Name: "authuser", Title: " User "} + authPasswordViewFeature = viewFeature{Name: "authpasswd", Title: " Password "} + // these are the view groups, so that we can assign common keybindings authViews = []viewFeature{authUserFeature, authPasswordViewFeature} authLabels = []viewFeature{authenticationViewFeature, authUserLabelFeature, authPswdLabelViewFeature} + // we can hold the job that is required to authenticate jobRequiresAuth *git.Job ) -// open an error view to inform user with a message and a useful note +// open an auth view to get user credentials func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job *git.Job, returnViewName string) error { maxX, maxY := g.Size() - // lets remove this job from the queue so that it won't block anything + // lets add this job since it is removed from the queue // also it is already unsuccessfully finished + if err := jobQueue.AddJob(job); err != nil { + return err + } jobRequiresAuth = job if job.Entity.State != git.Fail { if err := jobQueue.RemoveFromQueue(job.Entity); err != nil { @@ -33,26 +43,22 @@ func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *git.JobQueue, job return err } } - authenticationReturnView = returnViewName v, err := g.SetView(authenticationViewFeature.Name, maxX/2-30, maxY/2-2, maxX/2+30, maxY/2+2) if err != nil { if err != gocui.ErrUnknownView { return err } - fmt.Fprintln(v, " Enter credentials for: "+red.Sprint(job.Entity.Remote.URL[0])) + fmt.Fprintln(v, keySymbol+selectionIndicator+red.Sprint(jobRequiresAuth.Entity.Remote.URL[0])) } g.Cursor = true if err := gui.openUserView(g); err != nil { - return nil - } - if err := gui.openPasswordView(g); err != nil { - return nil + return err } - return nil + return gui.openPasswordView(g) } -// close the opened error view +// close the opened auth views func (gui *Gui) closeAuthenticationView(g *gocui.Gui, v *gocui.View) error { g.Cursor = false for _, vf := range authLabels { @@ -65,21 +71,20 @@ func (gui *Gui) closeAuthenticationView(g *gocui.Gui, v *gocui.View) error { return nil } } - if _, err := g.SetCurrentView(authenticationReturnView); err != nil { - return err - } - gui.updateKeyBindingsView(g, authenticationReturnView) - return nil + return gui.closeViewCleanup(authenticationReturnView) } -// close the opened error view +// close the opened auth views and submit the credentials func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error { g.Cursor = false + // in order to read buffer of the views, first we need to find'em v_user, err := g.View(authUserFeature.Name) v_pswd, err := g.View(authPasswordViewFeature.Name) - creduser := v_user.ViewBuffer() - credpswd := v_pswd.ViewBuffer() - // Maybe pause implementation can be added + // the return string of the views contain trailing new lines + re := regexp.MustCompile(`\r?\n`) + creduser := re.ReplaceAllString(v_user.ViewBuffer(), "") + credpswd := re.ReplaceAllString(v_pswd.ViewBuffer(), "") + // since the git ops require different types of options we better switch switch mode := jobRequiresAuth.JobType; mode { case git.FetchJob: jobRequiresAuth.Options = git.FetchOptions{ @@ -89,21 +94,34 @@ func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error { Password: credpswd, }, } + case git.PullJob: + // we handle pull as fetch&merge so same rule applies + jobRequiresAuth.Options = git.FetchOptions{ + RemoteName: jobRequiresAuth.Entity.Remote.Name, + Credentials: git.Credentials{ + User: creduser, + Password: credpswd, + }, + } } + jobRequiresAuth.Entity.State = git.Queued + // add this job to the last of the queue err = gui.State.Queue.AddJob(jobRequiresAuth) if err != nil { return err } - jobRequiresAuth.Entity.State = git.Queued + // refresh views with the updates gui.refreshMain(g) - gui.refreshViews(g, jobRequiresAuth.Entity) gui.closeAuthenticationView(g, v) + v_return, err := g.View(authenticationReturnView) + gui.startQueue(g, v_return) return nil } // open an error view to inform user with a message and a useful note func (gui *Gui) openUserView(g *gocui.Gui) error { maxX, maxY := g.Size() + // first, create the label for user vlabel, err := g.SetView(authUserLabelFeature.Name, maxX/2-30, maxY/2-1, maxX/2-19, maxY/2+1) if err != nil { if err != gocui.ErrUnknownView { @@ -112,6 +130,7 @@ func (gui *Gui) openUserView(g *gocui.Gui) error { fmt.Fprintln(vlabel, authUserLabelFeature.Title) vlabel.Frame = false } + // second, crete the user input v, err := g.SetView(authUserFeature.Name, maxX/2-18, maxY/2-1, maxX/2+29, maxY/2+1) if err != nil { if err != gocui.ErrUnknownView { @@ -121,16 +140,13 @@ func (gui *Gui) openUserView(g *gocui.Gui) error { v.Editable = true v.Frame = false } - gui.updateKeyBindingsView(g, authUserFeature.Name) - if _, err := g.SetCurrentView(authUserFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(authUserFeature.Name) } // open an error view to inform user with a message and a useful note func (gui *Gui) openPasswordView(g *gocui.Gui) error { maxX, maxY := g.Size() + // first, create the label for password vlabel, err := g.SetView(authPswdLabelViewFeature.Name, maxX/2-30, maxY/2, maxX/2-19, maxY/2+2) if err != nil { if err != gocui.ErrUnknownView { @@ -139,6 +155,7 @@ func (gui *Gui) openPasswordView(g *gocui.Gui) error { fmt.Fprintln(vlabel, authPswdLabelViewFeature.Title) vlabel.Frame = false } + // second, crete the masked password input v, err := g.SetView(authPasswordViewFeature.Name, maxX/2-18, maxY/2, maxX/2+29, maxY/2+2) if err != nil { if err != gocui.ErrUnknownView { @@ -157,9 +174,3 @@ func (gui *Gui) nextAuthView(g *gocui.Gui, v *gocui.View) error { err := gui.nextViewOfGroup(g, v, authViews) return err } - -// focus to previous view -func (gui *Gui) previousAuthView(g *gocui.Gui, v *gocui.View) error { - err := gui.previousViewOfGroup(g, v, authViews) - return err -} diff --git a/pkg/gui/cheatsheet.go b/pkg/gui/cheatsheet.go index 43020a5..4b2f97e 100644 --- a/pkg/gui/cheatsheet.go +++ b/pkg/gui/cheatsheet.go @@ -24,11 +24,7 @@ func (gui *Gui) openCheatSheetView(g *gocui.Gui, v *gocui.View) error { } } } - gui.updateKeyBindingsView(g, cheatSheetViewFeature.Name) - if _, err := g.SetCurrentView(cheatSheetViewFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(cheatSheetViewFeature.Name) } // close the application controls and do the clean job @@ -36,9 +32,5 @@ func (gui *Gui) closeCheatSheetView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(v.Name()); err != nil { return nil } - if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { - return err - } - gui.updateKeyBindingsView(g, mainViewFeature.Name) - return nil + return gui.closeViewCleanup(mainViewFeature.Name) } diff --git a/pkg/gui/diffview.go b/pkg/gui/diffview.go index f99d7f2..04c9e10 100644 --- a/pkg/gui/diffview.go +++ b/pkg/gui/diffview.go @@ -22,8 +22,7 @@ func (gui *Gui) prepareDiffView(g *gocui.Gui, v *gocui.View, display []string) ( out.Title = diffViewFeature.Title out.Overwrite = true out.Wrap = true - gui.updateKeyBindingsView(g, diffViewFeature.Name) - if _, err = g.SetCurrentView(diffViewFeature.Name); err != nil { + if err = gui.focusToView(diffViewFeature.Name); err != nil { return out, err } for _, line := range display { @@ -126,9 +125,5 @@ func (gui *Gui) closeCommitDiffView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(v.Name()); err != nil { return nil } - if _, err := g.SetCurrentView(diffReturnView); err != nil { - return err - } - gui.updateKeyBindingsView(g, diffReturnView) - return nil + return gui.closeViewCleanup(diffReturnView) } diff --git a/pkg/gui/errorview.go b/pkg/gui/errorview.go index b4be162..f679b54 100644 --- a/pkg/gui/errorview.go +++ b/pkg/gui/errorview.go @@ -23,11 +23,7 @@ func (gui *Gui) openErrorView(g *gocui.Gui, message, note, returnViewName string fmt.Fprintln(v, message) fmt.Fprintln(v, ps) } - gui.updateKeyBindingsView(g, errorViewFeature.Name) - if _, err := g.SetCurrentView(errorViewFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(errorViewFeature.Name) } // close the opened error view @@ -36,9 +32,5 @@ func (gui *Gui) closeErrorView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(v.Name()); err != nil { return nil } - if _, err := g.SetCurrentView(errorReturnView); err != nil { - return err - } - gui.updateKeyBindingsView(g, errorReturnView) - return nil + return gui.closeViewCleanup(errorReturnView) } diff --git a/pkg/gui/gui-util.go b/pkg/gui/gui-util.go index bd02f7c..343e293 100644 --- a/pkg/gui/gui-util.go +++ b/pkg/gui/gui-util.go @@ -193,3 +193,17 @@ func (gui *Gui) fastCursorUp(g *gocui.Gui, v *gocui.View) error { } return nil } + +// closeViewCleanup both updates the keybidings view and focuses to returning view +func (gui *Gui) closeViewCleanup(returningViewName string) (err error) { + if _, err = gui.g.SetCurrentView(returningViewName); err != nil { + return err + } + err = gui.updateKeyBindingsView(gui.g, returningViewName) + return err +} + +// focus to view same as closeViewCleanup but its just a wrapper for easy reading +func (gui *Gui) focusToView(viewName string) (err error) { + return gui.closeViewCleanup(viewName) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 9b23fa2..1d4dd6d 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -191,7 +191,7 @@ func (gui *Gui) generateKeybindings() error { Handler: gui.nextAuthView, Display: "tab", Description: "Next Panel", - Vital: false, + Vital: true, }, { View: view.Name, Key: gocui.KeyEnter, @@ -199,7 +199,7 @@ func (gui *Gui) generateKeybindings() error { Handler: gui.submitAuthenticationView, Display: "enter", Description: "Submit", - Vital: false, + Vital: true, }, } for _, binding := range authKeybindings { diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index fe8931b..389d66d 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -147,7 +147,8 @@ func (gui *Gui) removeFromQueue(entity *git.RepoEntity) error { // function does take its current state into account before adding it func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error { r := gui.getSelectedRepository() - if r.State == git.Available || r.State == git.Success { + // maybe, failed entities may be added to queue again + if r.State == git.Available || r.State == git.Success || r.State == git.Paused { if err := gui.addToQueue(r); err != nil { return err } diff --git a/pkg/gui/queuehandler.go b/pkg/gui/queuehandler.go index cec6f8b..a814c47 100644 --- a/pkg/gui/queuehandler.go +++ b/pkg/gui/queuehandler.go @@ -12,13 +12,17 @@ func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error { go func(gui_go *Gui, g_go *gocui.Gui) { for { job, finished, err := gui_go.State.Queue.StartNext() + // for each job execution we better refresh the main + // it would be nice if we can also refresh side views g_go.Update(func(gu *gocui.Gui) error { gui_go.refreshMain(gu) return nil }) - defer gui.updateKeyBindingsView(g, mainViewFeature.Name) + if err != nil { if err == git.ErrAuthenticationRequired { + // pause the job, so it will be indicated to being blocking + job.Entity.State = git.Paused err := gui_go.openAuthenticationView(g, gui_go.State.Queue, job, v.Name()) if err != nil { log.Warn(err.Error()) @@ -26,7 +30,9 @@ func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error { } } return + // with not returning here, we simply ignore and continue } + // if queue is finished simply return from this goroutine if finished { return } diff --git a/pkg/gui/stagedview.go b/pkg/gui/stagedview.go index e27d35d..f12c06d 100644 --- a/pkg/gui/stagedview.go +++ b/pkg/gui/stagedview.go @@ -22,11 +22,7 @@ func (gui *Gui) openStageView(g *gocui.Gui) error { if err := refreshStagedView(g, entity); err != nil { return err } - gui.updateKeyBindingsView(g, stageViewFeature.Name) - if _, err := g.SetCurrentView(stageViewFeature.Name); err != nil { - return err - } - return nil + return gui.focusToView(stageViewFeature.Name) } func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go index 494e07a..b241ed6 100644 --- a/pkg/gui/statusview.go +++ b/pkg/gui/statusview.go @@ -11,8 +11,7 @@ import ( ) var ( - statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "} - // statusViewFeature = viewFeature{Name: "status", Title: " Status "} + statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "} stageViewFeature = viewFeature{Name: "staged", Title: " Staged "} unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "} stashViewFeature = viewFeature{Name: "stash", Title: " Stash "} @@ -105,21 +104,14 @@ func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error { // close the opened stat views func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { - if err := g.DeleteView(stashViewFeature.Name); err != nil { - return err - } - if err := g.DeleteView(unstageViewFeature.Name); err != nil { - return err - } - if err := g.DeleteView(stageViewFeature.Name); err != nil { - return err + for _, view := range statusViews { + if err := g.DeleteView(view.Name); err != nil { + return err + } } if err := g.DeleteView(statusHeaderViewFeature.Name); err != nil { return err } - if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { - return err - } entity := gui.getSelectedRepository() if err := gui.refreshMain(g); err != nil { return err @@ -127,8 +119,7 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { if err := gui.refreshViews(g, entity); err != nil { return err } - gui.updateKeyBindingsView(g, mainViewFeature.Name) - return nil + return gui.closeViewCleanup(mainViewFeature.Name) } func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) { @@ -228,9 +219,6 @@ func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(commitMessageViewFeature.Name); err != nil { return err } - if _, err := g.SetCurrentView(commitMesageReturnView); err != nil { - return err - } if err := gui.refreshMain(g); err != nil { return err } @@ -240,6 +228,5 @@ func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { if err := refreshAllStatusView(g, entity); err != nil { return err } - gui.updateKeyBindingsView(g, commitMesageReturnView) - return nil + return gui.closeViewCleanup(commitMesageReturnView) } diff --git a/pkg/gui/textstyle.go b/pkg/gui/textstyle.go index efbbceb..7c36263 100644 --- a/pkg/gui/textstyle.go +++ b/pkg/gui/textstyle.go @@ -35,12 +35,15 @@ var ( queuedSymbol = "•" workingSymbol = "•" successSymbol = "✔" + pauseSymbol = "॥" failSymbol = "✗" fetchSymbol = "↓" pullSymbol = "↓↳" mergeSymbol = "↳" + keySymbol = ws + yellow.Sprint("🔑") + ws + modeSeperator = "" keyBindingSeperator = "░" @@ -102,6 +105,8 @@ func (gui *Gui) displayString(entity *git.RepoEntity) string { return prefix + repoName + ws + green.Sprint(workingSymbol) } else if entity.State == git.Success { return prefix + repoName + ws + green.Sprint(successSymbol) + } else if entity.State == git.Paused { + return prefix + repoName + ws + yellow.Sprint(pauseSymbol) } else if entity.State == git.Fail { return prefix + repoName + ws + red.Sprint(failSymbol) } else { -- cgit v1.2.3 From af7b9a486062d85223a16af1e08660aab8c4b260 Mon Sep 17 00:00:00 2001 From: İbrahim Serdar Açıkgöz Date: Thu, 13 Dec 2018 18:33:19 +0300 Subject: migrating commands to go-git --- pkg/git/cmd-add.go | 63 ++++++++++++++++++++------- pkg/git/cmd-commit.go | 93 +++++++++++++++++++++++++++++++++++++++ pkg/git/cmd-config.go | 107 +++++++++++++++++++++++++++++++++++++++++++++ pkg/git/cmd-reset.go | 113 ++++++++++++++++++++++++++++++++++++++++-------- pkg/git/cmd-status.go | 44 ++++++++++++++++++- pkg/gui/stagedview.go | 17 +++++--- pkg/gui/statusview.go | 18 +++++--- pkg/gui/unstagedview.go | 4 +- 8 files changed, 410 insertions(+), 49 deletions(-) create mode 100644 pkg/git/cmd-commit.go create mode 100644 pkg/git/cmd-config.go diff --git a/pkg/git/cmd-add.go b/pkg/git/cmd-add.go index 61fd587..649bad2 100644 --- a/pkg/git/cmd-add.go +++ b/pkg/git/cmd-add.go @@ -2,35 +2,54 @@ package git import ( "errors" - "strings" log "github.com/sirupsen/logrus" ) -var addCommand = "add" +var ( + addCmdMode string + + addCommand = "add" + addCmdModeLegacy = "git" + addCmdModeNative = "go-git" +) // AddOptions defines the rules for "git add" command type AddOptions struct { + // Update Update bool - Force bool + // Force + Force bool + // DryRun DryRun bool } // Add is a wrapper function for "git add" command -func (file *File) Add(option AddOptions) error { - args := make([]string, 0) - args = append(args, addCommand) - args = append(args, file.Name) - if option.Update { - args = append(args, "--update") +func Add(entity *RepoEntity, file *File, option AddOptions) error { + addCmdMode = addCmdModeNative + if option.Update || option.Force || option.DryRun { + addCmdMode = addCmdModeLegacy } - if option.Force { - args = append(args, "--force") + switch addCmdMode { + case addCmdModeLegacy: + err := addWithGit(entity, file, option) + return err + case addCmdModeNative: + err := addWithGoGit(entity, file) + return err } + return errors.New("Unhandled add operation") +} + +// AddAll function is the wrapper of "git add ." command +func AddAll(entity *RepoEntity, option AddOptions) error { + args := make([]string, 0) + args = append(args, addCommand) if option.DryRun { args = append(args, "--dry-run") } - out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + args = append(args, ".") + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) if err != nil { log.Warn("Error while add command") return errors.New(out + "\n" + err.Error()) @@ -38,14 +57,19 @@ func (file *File) Add(option AddOptions) error { return nil } -// AddAll function is the wrapper of "git add ." command -func (entity *RepoEntity) AddAll(option AddOptions) error { +func addWithGit(entity *RepoEntity, file *File, option AddOptions) error { args := make([]string, 0) args = append(args, addCommand) + args = append(args, file.Name) + if option.Update { + args = append(args, "--update") + } + if option.Force { + args = append(args, "--force") + } if option.DryRun { args = append(args, "--dry-run") } - args = append(args, ".") out, err := GenericGitCommandWithOutput(entity.AbsPath, args) if err != nil { log.Warn("Error while add command") @@ -53,3 +77,12 @@ func (entity *RepoEntity) AddAll(option AddOptions) error { } return nil } + +func addWithGoGit(entity *RepoEntity, file *File) error { + w, err := entity.Repository.Worktree() + if err != nil { + return err + } + _, err = w.Add(file.Name) + return nil +} diff --git a/pkg/git/cmd-commit.go b/pkg/git/cmd-commit.go new file mode 100644 index 0000000..8ea5bfe --- /dev/null +++ b/pkg/git/cmd-commit.go @@ -0,0 +1,93 @@ +package git + +import ( + "errors" + "time" + + log "github.com/sirupsen/logrus" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing/object" +) + +var ( + commitCmdMode string + + commitCommand = "commit" + commitCmdModeLegacy = "git" + commitCmdModeNative = "go-git" +) + +// CommitOptions defines the rules for commit operation +type CommitOptions struct { + // CommitMsg + CommitMsg string + // User + User string + // Email + Email string +} + +// CommitCommand +func CommitCommand(entity *RepoEntity, options CommitOptions) (err error) { + // here we configure commit operation + // default mode is go-git (this may be configured) + commitCmdMode = commitCmdModeNative + + switch commitCmdMode { + case commitCmdModeLegacy: + err = commitWithGit(entity, options) + return err + case commitCmdModeNative: + err = commitWithGoGit(entity, options) + return err + } + return errors.New("Unhandled commit operation") +} + +// commitWithGit is simply a bare git commit -m command which is flexible +func commitWithGit(entity *RepoEntity, options CommitOptions) (err error) { + args := make([]string, 0) + args = append(args, commitCommand) + args = append(args, "-m") + // parse options to command line arguments + if len(options.CommitMsg) > 0 { + args = append(args, options.CommitMsg) + } + if err := GenericGitCommand(entity.AbsPath, args); err != nil { + log.Warn("Error at git command (commit)") + return err + } + // till this step everything should be ok + err = entity.Refresh() + return err +} + +// commitWithGoGit is the primary commit method +func commitWithGoGit(entity *RepoEntity, options CommitOptions) (err error) { + config, err := entity.Repository.Config() + if err != nil { + return err + } + name := config.Raw.Section("user").Option("name") + email := config.Raw.Section("user").Option("email") + opt := &git.CommitOptions{ + Author: &object.Signature{ + Name: name, + Email: email, + When: time.Now(), + }, + } + + w, err := entity.Repository.Worktree() + if err != nil { + return err + } + + _, err = w.Commit(options.CommitMsg, opt) + if err != nil { + return err + } + // till this step everything should be ok + err = entity.Refresh() + return err +} diff --git a/pkg/git/cmd-config.go b/pkg/git/cmd-config.go new file mode 100644 index 0000000..94efe33 --- /dev/null +++ b/pkg/git/cmd-config.go @@ -0,0 +1,107 @@ +package git + +import ( + "errors" + + log "github.com/sirupsen/logrus" +) + +var ( + configCmdMode string + + configCommand = "config" + configCmdModeLegacy = "git" + configCmdModeNative = "go-git" +) + +// CommitOptions defines the rules for commit operation +type ConfigOptions struct { + // Section + Section string + // Option + Option string + // Site should be Global or Local + Site ConfigSite +} + +type ConfigSite string + +const ( + // ConfigStieLocal + ConfigStieLocal ConfigSite = "local" + // ConfgiSiteGlobal + ConfgiSiteGlobal ConfigSite = "global" +) + +// Config +func Config(entity *RepoEntity, options ConfigOptions) (value string, err error) { + // here we configure config operation + // default mode is go-git (this may be configured) + configCmdMode = configCmdModeNative + + switch configCmdMode { + case configCmdModeLegacy: + value, err = configWithGit(entity, options) + return value, err + case configCmdModeNative: + value, err = configWithGoGit(entity, options) + return value, err + } + return value, errors.New("Unhandled config operation") +} + +// configWithGit is simply a bare git commit -m command which is flexible +func configWithGit(entity *RepoEntity, options ConfigOptions) (value string, err error) { + args := make([]string, 0) + args = append(args, commitCommand) + if len(string(options.Site)) > 0 { + args = append(args, "--"+string(options.Site)) + } + args = append(args, "--get") + args = append(args, options.Section+"."+options.Option) + // parse options to command line arguments + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error at git command (commit)") + return out, err + } + // till this step everything should be ok + return out, entity.Refresh() +} + +// commitWithGoGit is the primary commit method +func configWithGoGit(entity *RepoEntity, options ConfigOptions) (value string, err error) { + config, err := entity.Repository.Config() + if err != nil { + return value, err + } + value = config.Raw.Section(options.Section).Option(options.Option) + return value, entity.Refresh() +} + +// AddConfig +func AddConfig(entity *RepoEntity, options ConfigOptions, value string) (err error) { + err = addConfigWithGit(entity, options, value) + return err + +} + +// addConfigWithGit is simply a bare git commit -m command which is flexible +func addConfigWithGit(entity *RepoEntity, options ConfigOptions, value string) (err error) { + args := make([]string, 0) + args = append(args, commitCommand) + if len(string(options.Site)) > 0 { + args = append(args, "--"+string(options.Site)) + } + args = append(args, "--add") + args = append(args, options.Section+"."+options.Option) + if len(value) > 0 { + args = append(args, value) + } + if err := GenericGitCommand(entity.AbsPath, args); err != nil { + log.Warn("Error at git command (commit)") + return err + } + // till this step everything should be ok + return entity.Refresh() +} diff --git a/pkg/git/cmd-reset.go b/pkg/git/cmd-reset.go index f93225c..2e6ed91 100644 --- a/pkg/git/cmd-reset.go +++ b/pkg/git/cmd-reset.go @@ -2,49 +2,101 @@ package git import ( "errors" - "strings" log "github.com/sirupsen/logrus" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" ) -var resetCommand = "reset" +var ( + resetCmdMode string + + resetCommand = "reset" + resetCmdModeLegacy = "git" + resetCmdModeNative = "go-git" +) // ResetOptions defines the rules of git reset command type ResetOptions struct { - Hard bool - Merge bool - Keep bool + // Hash is the reference to be resetted + Hash string + // Type is the mode of a reset operation + Rtype ResetType } +type ResetType string + +const ( + // ResetHard Resets the index and working tree. Any changes to tracked + // files in the working tree since are discarded. + ResetHard ResetType = "hard" + // ResetMixed Resets the index but not the working tree (i.e., the changed + // files are preserved but not marked for commit) and reports what has not + // been updated. This is the default action. + ResetMixed ResetType = "mixed" + // ResetMerge Resets the index and updates the files in the working tree + // that are different between and HEAD, but keeps those which are + // different between the index and working tree + ResetMerge ResetType = "merge" + // ResetSoft Does not touch the index file or the working tree at all + // (but resets the head to + ResetSoft ResetType = "soft" + // ResetKeep Resets index entries and updates files in the working tree + // that are different between and HEAD + ResetKeep ResetType = "keep" +) + // Reset is the wrapper of "git reset" command -func (file *File) Reset(option ResetOptions) error { +func Reset(entity *RepoEntity, file *File, option ResetOptions) error { + resetCmdMode = addCmdModeLegacy + + switch resetCmdMode { + case resetCmdModeLegacy: + err := resetWithGit(entity, file, option) + return err + case resetCmdModeNative: + + } + return errors.New("Unhandled reset operation") +} + +func resetWithGit(entity *RepoEntity, file *File, option ResetOptions) error { args := make([]string, 0) args = append(args, resetCommand) + args = append(args, "--") args = append(args, file.Name) - if option.Hard { - args = append(args, "--hard") - } - if option.Merge { - args = append(args, "--merge") + if len(option.Rtype) > 0 { + args = append(args, "--"+string(option.Rtype)) } - if option.Keep { - args = append(args, "--keep") - } - out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) if err != nil { - log.Warn("Error while add command") + log.Warn("Error while reset command") return errors.New(out + "\n" + err.Error()) } return nil } // ResetAll resets the changes in a repository, should be used wise -func (entity *RepoEntity) ResetAll(option ResetOptions) error { +func ResetAll(entity *RepoEntity, option ResetOptions) error { + resetCmdMode = addCmdModeNative + + switch resetCmdMode { + case resetCmdModeLegacy: + err := resetAllWithGit(entity, option) + return err + case resetCmdModeNative: + err := resetAllWithGoGit(entity, option) + return err + } + return errors.New("Unhandled reset operation") +} + +func resetAllWithGit(entity *RepoEntity, option ResetOptions) error { args := make([]string, 0) args = append(args, resetCommand) - if option.Hard { - args = append(args, "--hard") + if len(option.Rtype) > 0 { + args = append(args, "--"+string(option.Rtype)) } out, err := GenericGitCommandWithOutput(entity.AbsPath, args) if err != nil { @@ -53,3 +105,26 @@ func (entity *RepoEntity) ResetAll(option ResetOptions) error { } return nil } + +func resetAllWithGoGit(entity *RepoEntity, option ResetOptions) error { + w, err := entity.Repository.Worktree() + if err != nil { + return err + } + var mode git.ResetMode + switch option.Rtype { + case ResetHard: + mode = git.HardReset + case ResetMixed: + mode = git.MixedReset + case ResetMerge: + mode = git.MergeReset + case ResetSoft: + mode = git.SoftReset + } + err = w.Reset(&git.ResetOptions{ + Commit: plumbing.NewHash(option.Hash), + Mode: mode, + }) + return err +} diff --git a/pkg/git/cmd-status.go b/pkg/git/cmd-status.go index 4cca604..d3d1918 100644 --- a/pkg/git/cmd-status.go +++ b/pkg/git/cmd-status.go @@ -1,6 +1,7 @@ package git import ( + "errors" "os" "regexp" "strings" @@ -8,7 +9,13 @@ import ( log "github.com/sirupsen/logrus" ) -var statusCommand = "status" +var ( + statusCmdMode string + + statusCommand = "status" + statusCmdModeLegacy = "git" + statusCmdModeNative = "go-git" +) // File represents the status of a file in an index or work tree type File struct { @@ -55,9 +62,21 @@ func shortStatus(entity *RepoEntity, option string) string { return out } +func Status(entity *RepoEntity) ([]*File, error) { + statusCmdMode = statusCmdModeNative + + switch statusCmdMode { + case statusCmdModeLegacy: + return statusWithGit(entity) + case statusCmdModeNative: + return statusWithGoGit(entity) + } + return nil, errors.New("Unhandled status operation") +} + // LoadFiles function simply commands a git status and collects output in a // structured way -func (entity *RepoEntity) LoadFiles() ([]*File, error) { +func statusWithGit(entity *RepoEntity) ([]*File, error) { files := make([]*File, 0) output := shortStatus(entity, "--untracked-files=all") if len(output) == 0 { @@ -80,6 +99,27 @@ func (entity *RepoEntity) LoadFiles() ([]*File, error) { return files, nil } +func statusWithGoGit(entity *RepoEntity) ([]*File, error) { + files := make([]*File, 0) + w, err := entity.Repository.Worktree() + if err != nil { + return files, err + } + s, err := w.Status() + if err != nil { + return files, err + } + for k, v := range s { + files = append(files, &File{ + Name: k, + AbsPath: entity.AbsPath + string(os.PathSeparator) + k, + X: FileStatus(v.Staging), + Y: FileStatus(v.Worktree), + }) + } + 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) diff --git a/pkg/gui/stagedview.go b/pkg/gui/stagedview.go index f12c06d..294c0f7 100644 --- a/pkg/gui/stagedview.go +++ b/pkg/gui/stagedview.go @@ -36,20 +36,25 @@ func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error { } _, cy := v.Cursor() _, oy := v.Origin() - if err := files[cy+oy].Reset(git.ResetOptions{}); err != nil { + if err := git.Reset(entity, files[cy+oy], git.ResetOptions{}); err != nil { return err } - err = refreshAllStatusView(g, entity) - return err + return refreshAllStatusView(g, entity) } func (gui *Gui) resetAllChanges(g *gocui.Gui, v *gocui.View) error { entity := gui.getSelectedRepository() - if err := entity.ResetAll(git.ResetOptions{}); err != nil { + ref, err := entity.Repository.Head() + if err != nil { + return err + } + if err := git.ResetAll(entity, git.ResetOptions{ + Hash: ref.Hash().String(), + Rtype: git.ResetMixed, + }); err != nil { return err } - err := refreshAllStatusView(g, entity) - return err + return refreshAllStatusView(g, entity) } // refresh the main view and re-render the repository representations diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go index b241ed6..c0a9c65 100644 --- a/pkg/gui/statusview.go +++ b/pkg/gui/statusview.go @@ -1,6 +1,7 @@ package gui import ( + "errors" "fmt" "time" @@ -123,7 +124,7 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { } func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) { - files, err := entity.LoadFiles() + files, err := git.Status(entity) if err != nil { return nil, nil, err } @@ -195,12 +196,19 @@ func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error { if err != nil { return err } - // WIP: This better be removed to git pkg - // TODO: read config and get name & e-mail + config, err := entity.Repository.Config() + if err != nil { + return err + } + name := config.Raw.Section("user").Option("name") + email := config.Raw.Section("user").Option("email") + if len(email) <= 0 { + return errors.New("User email needs to be provided") + } _, err = w.Commit(v.ViewBuffer(), &ggt.CommitOptions{ Author: &object.Signature{ - Name: "İbrahim Serdar Açıkgöz", - Email: "serdaracikgoz86@gmail.com", + Name: name, + Email: email, When: time.Now(), }, }) diff --git a/pkg/gui/unstagedview.go b/pkg/gui/unstagedview.go index ef4866f..01b16de 100644 --- a/pkg/gui/unstagedview.go +++ b/pkg/gui/unstagedview.go @@ -34,7 +34,7 @@ func (gui *Gui) addChanges(g *gocui.Gui, v *gocui.View) error { } _, cy := v.Cursor() _, oy := v.Origin() - if err := files[cy+oy].Add(git.AddOptions{}); err != nil { + if err := git.Add(entity, files[cy+oy], git.AddOptions{}); err != nil { return err } err = refreshAllStatusView(g, entity) @@ -43,7 +43,7 @@ func (gui *Gui) addChanges(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) addAllChanges(g *gocui.Gui, v *gocui.View) error { entity := gui.getSelectedRepository() - if err := entity.AddAll(git.AddOptions{}); err != nil { + if err := git.AddAll(entity, git.AddOptions{}); err != nil { return err } err := refreshAllStatusView(g, entity) -- cgit v1.2.3 From 4051d3ee15d6db397938957a952dc7749a32341e Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Thu, 13 Dec 2018 23:45:58 +0300 Subject: files sort added --- pkg/git/util-repository-sort.go | 59 ------------------------- pkg/git/util-sort.go | 98 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 59 deletions(-) delete mode 100644 pkg/git/util-repository-sort.go create mode 100644 pkg/git/util-sort.go diff --git a/pkg/git/util-repository-sort.go b/pkg/git/util-repository-sort.go deleted file mode 100644 index de5f454..0000000 --- a/pkg/git/util-repository-sort.go +++ /dev/null @@ -1,59 +0,0 @@ -package git - -import ( - "unicode" -) - -// Alphabetical slice is the re-ordered *RepoEntity slice that sorted according -// to alphabetical order (A-Z) -type Alphabetical []*RepoEntity - -// Len is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Len() int { return len(s) } - -// Swap is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Less(i, j int) bool { - iRunes := []rune(s[i].Name) - jRunes := []rune(s[j].Name) - - max := len(iRunes) - if max > len(jRunes) { - max = len(jRunes) - } - - for idx := 0; idx < max; idx++ { - ir := iRunes[idx] - jr := jRunes[idx] - - lir := unicode.ToLower(ir) - ljr := unicode.ToLower(jr) - - if lir != ljr { - return lir < ljr - } - - // the lowercase runes are the same, so compare the original - if ir != jr { - return ir < jr - } - } - return false -} - -// LastModified slice is the re-ordered *RepoEntity slice that sorted according -// to last modified date of the repository directory -type LastModified []*RepoEntity - -// Len is the interface implementation for LastModified sorting function -func (s LastModified) Len() int { return len(s) } - -// Swap is the interface implementation for LastModified sorting function -func (s LastModified) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less is the interface implementation for LastModified sorting function -func (s LastModified) Less(i, j int) bool { - return s[i].ModTime.Unix() > s[j].ModTime.Unix() -} diff --git a/pkg/git/util-sort.go b/pkg/git/util-sort.go new file mode 100644 index 0000000..e3067bc --- /dev/null +++ b/pkg/git/util-sort.go @@ -0,0 +1,98 @@ +package git + +import ( + "unicode" +) + +// Alphabetical slice is the re-ordered *RepoEntity slice that sorted according +// to alphabetical order (A-Z) +type Alphabetical []*RepoEntity + +// Len is the interface implementation for Alphabetical sorting function +func (s Alphabetical) Len() int { return len(s) } + +// Swap is the interface implementation for Alphabetical sorting function +func (s Alphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less is the interface implementation for Alphabetical sorting function +func (s Alphabetical) Less(i, j int) bool { + iRunes := []rune(s[i].Name) + jRunes := []rune(s[j].Name) + + max := len(iRunes) + if max > len(jRunes) { + max = len(jRunes) + } + + for idx := 0; idx < max; idx++ { + ir := iRunes[idx] + jr := jRunes[idx] + + lir := unicode.ToLower(ir) + ljr := unicode.ToLower(jr) + + if lir != ljr { + return lir < ljr + } + + // the lowercase runes are the same, so compare the original + if ir != jr { + return ir < jr + } + } + return false +} + +// LastModified slice is the re-ordered *RepoEntity slice that sorted according +// to last modified date of the repository directory +type LastModified []*RepoEntity + +// Len is the interface implementation for LastModified sorting function +func (s LastModified) Len() int { return len(s) } + +// Swap is the interface implementation for LastModified sorting function +func (s LastModified) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less is the interface implementation for LastModified sorting function +func (s LastModified) Less(i, j int) bool { + return s[i].ModTime.Unix() > s[j].ModTime.Unix() +} + +// filesAlphabetical slice is the re-ordered *File slice that sorted according +// to alphabetical order (A-Z) +type filesAlphabetical []*File + +// Len is the interface implementation for Alphabetical sorting function +func (s filesAlphabetical) Len() int { return len(s) } + +// Swap is the interface implementation for Alphabetical sorting function +func (s filesAlphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less is the interface implementation for Alphabetical sorting function +func (s filesAlphabetical) Less(i, j int) bool { + iRunes := []rune(s[i].Name) + jRunes := []rune(s[j].Name) + + max := len(iRunes) + if max > len(jRunes) { + max = len(jRunes) + } + + for idx := 0; idx < max; idx++ { + ir := iRunes[idx] + jr := jRunes[idx] + + lir := unicode.ToLower(ir) + ljr := unicode.ToLower(jr) + + if lir != ljr { + return lir < ljr + } + + // the lowercase runes are the same, so compare the original + if ir != jr { + return ir < jr + } + } + return false +} -- cgit v1.2.3 From 3de3e0da39d82b9ac928890a186e0044fd84b3a5 Mon Sep 17 00:00:00 2001 From: <> Date: Fri, 14 Dec 2018 02:00:48 +0300 Subject: improved commit feature --- README.md | 4 +- pkg/git/cmd-config.go | 10 +-- pkg/git/cmd-status.go | 9 ++- pkg/gui/commitview.go | 187 ++++++++++++++++++++++++++++++++++++++++++++++++ pkg/gui/diffview.go | 4 +- pkg/gui/keybindings.go | 50 ++++++++----- pkg/gui/stagedview.go | 23 +++--- pkg/gui/stashview.go | 4 +- pkg/gui/statusview.go | 118 ++++++++---------------------- pkg/gui/unstagedview.go | 28 +++----- 10 files changed, 283 insertions(+), 154 deletions(-) create mode 100644 pkg/gui/commitview.go diff --git a/README.md b/README.md index 7719c88..ed085d0 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ For more information; ## Further goals - add testing - full src-d/go-git integration (*having some performance issues*) -- add commit and maybe push? + - fetch, config, add, reset, commit, status, diff ✔ + - merge, rev-list, stash ✗ +- add push ## Known issues Please refer to [Known issues page](https://github.com/isacikgoz/gitbatch/wiki/Known-issues) diff --git a/pkg/git/cmd-config.go b/pkg/git/cmd-config.go index 94efe33..541b0be 100644 --- a/pkg/git/cmd-config.go +++ b/pkg/git/cmd-config.go @@ -37,7 +37,7 @@ const ( func Config(entity *RepoEntity, options ConfigOptions) (value string, err error) { // here we configure config operation // default mode is go-git (this may be configured) - configCmdMode = configCmdModeNative + configCmdMode = configCmdModeLegacy switch configCmdMode { case configCmdModeLegacy: @@ -53,7 +53,7 @@ func Config(entity *RepoEntity, options ConfigOptions) (value string, err error) // configWithGit is simply a bare git commit -m command which is flexible func configWithGit(entity *RepoEntity, options ConfigOptions) (value string, err error) { args := make([]string, 0) - args = append(args, commitCommand) + args = append(args, configCommand) if len(string(options.Site)) > 0 { args = append(args, "--"+string(options.Site)) } @@ -62,21 +62,21 @@ func configWithGit(entity *RepoEntity, options ConfigOptions) (value string, err // parse options to command line arguments out, err := GenericGitCommandWithOutput(entity.AbsPath, args) if err != nil { - log.Warn("Error at git command (commit)") return out, err } // till this step everything should be ok - return out, entity.Refresh() + return out, nil } // commitWithGoGit is the primary commit method func configWithGoGit(entity *RepoEntity, options ConfigOptions) (value string, err error) { + // TODO: add global search config, err := entity.Repository.Config() if err != nil { return value, err } value = config.Raw.Section(options.Section).Option(options.Option) - return value, entity.Refresh() + return value, nil } // AddConfig diff --git a/pkg/git/cmd-status.go b/pkg/git/cmd-status.go index d3d1918..508a27c 100644 --- a/pkg/git/cmd-status.go +++ b/pkg/git/cmd-status.go @@ -4,6 +4,7 @@ import ( "errors" "os" "regexp" + "sort" "strings" log "github.com/sirupsen/logrus" @@ -26,7 +27,7 @@ type File struct { } // FileStatus is the short representation of state of a file -type FileStatus rune +type FileStatus byte var ( // StatusNotupdated says file not updated @@ -84,8 +85,8 @@ func statusWithGit(entity *RepoEntity) ([]*File, error) { } fileslist := strings.Split(output, "\n") for _, file := range fileslist { - x := rune(file[0]) - y := rune(file[1]) + x := byte(file[0]) + y := byte(file[1]) relativePathRegex := regexp.MustCompile(`[(\w|/|.|\-)]+`) path := relativePathRegex.FindString(file[2:]) @@ -96,6 +97,7 @@ func statusWithGit(entity *RepoEntity) ([]*File, error) { Y: FileStatus(y), }) } + sort.Sort(filesAlphabetical(files)) return files, nil } @@ -117,6 +119,7 @@ func statusWithGoGit(entity *RepoEntity) ([]*File, error) { Y: FileStatus(v.Worktree), }) } + sort.Sort(filesAlphabetical(files)) return files, nil } diff --git a/pkg/gui/commitview.go b/pkg/gui/commitview.go new file mode 100644 index 0000000..8fdd63f --- /dev/null +++ b/pkg/gui/commitview.go @@ -0,0 +1,187 @@ +package gui + +import ( + "errors" + "fmt" + "regexp" + + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" +) + +var ( + commitFrameViewFeature = viewFeature{Name: "commitframe", Title: " Frame "} + commitUserNameLabelFeature = viewFeature{Name: "commitusername", Title: " Name: "} + commitUserEmailLabelViewFeature = viewFeature{Name: "commituseremail", Title: " E-Mail: "} + + // these views used as a input for the credentials + commitMessageViewFeature = viewFeature{Name: "commitmessage", Title: " Commit Mesage "} + commitUserUserViewFeature = viewFeature{Name: "authuser", Title: " Name "} + commitUserEmailViewFeature = viewFeature{Name: "authpasswd", Title: " E-Mail "} + + commitViews = []viewFeature{commitMessageViewFeature, commitUserUserViewFeature, commitUserEmailViewFeature} + commitLabelViews = []viewFeature{commitFrameViewFeature, commitUserNameLabelFeature, commitUserEmailLabelViewFeature} +) + +// open the commit message views +func (gui *Gui) openCommitMessageView(g *gocui.Gui, v *gocui.View) error { + maxX, maxY := g.Size() + commitMesageReturnView = v.Name() + v_frame, err := g.SetView(commitFrameViewFeature.Name, maxX/2-30, maxY/2-4, maxX/2+30, maxY/2+3) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v_frame.Frame = true + fmt.Fprintln(v_frame, " Enter your commit message:") + } + v, err = g.SetView(commitMessageViewFeature.Name, maxX/2-29, maxY/2-3, maxX/2+29, maxY/2) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Frame = false + v.Wrap = true + v.Editable = true + v.Editor = gocui.DefaultEditor + g.Cursor = true + } + if err := gui.openCommitUserNameView(g); err != nil { + return err + } + if err := gui.openCommitUserEmailView(g); err != nil { + return err + } + gui.updateKeyBindingsView(g, commitMessageViewFeature.Name) + if _, err := g.SetCurrentView(commitMessageViewFeature.Name); err != nil { + return err + } + return nil +} + +// open an error view to inform user with a message and a useful note +func (gui *Gui) openCommitUserNameView(g *gocui.Gui) error { + entity := gui.getSelectedRepository() + maxX, maxY := g.Size() + // first, create the label for user + vlabel, err := g.SetView(commitUserNameLabelFeature.Name, maxX/2-30, maxY/2, maxX/2-19, maxY/2+2) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + fmt.Fprintln(vlabel, commitUserNameLabelFeature.Title) + vlabel.Frame = false + } + // second, crete the user input + v, err := g.SetView(commitUserUserViewFeature.Name, maxX/2-18, maxY/2, maxX/2+29, maxY/2+2) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + name, err := git.Config(entity, git.ConfigOptions{ + Section: "user", + Option: "name", + }) + if err != nil { + return err + } + fmt.Fprintln(v, name) + v.Editable = true + v.Frame = false + } + return nil +} + +// open an error view to inform user with a message and a useful note +func (gui *Gui) openCommitUserEmailView(g *gocui.Gui) error { + entity := gui.getSelectedRepository() + maxX, maxY := g.Size() + // first, create the label for password + vlabel, err := g.SetView(commitUserEmailLabelViewFeature.Name, maxX/2-30, maxY/2+1, maxX/2-19, maxY/2+3) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + fmt.Fprintln(vlabel, commitUserEmailLabelViewFeature.Title) + vlabel.Frame = false + } + // second, crete the masked password input + v, err := g.SetView(commitUserEmailViewFeature.Name, maxX/2-18, maxY/2+1, maxX/2+29, maxY/2+3) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + email, err := git.Config(entity, git.ConfigOptions{ + Section: "user", + Option: "email", + }) + if err != nil { + return err + } + fmt.Fprintln(v, email) + v.Editable = true + v.Frame = false + } + return nil +} + +// close the opened commite mesage view +func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + // in order to read buffer of the views, first we need to find'em + v_msg, err := g.View(commitMessageViewFeature.Name) + v_name, err := g.View(commitUserUserViewFeature.Name) + v_email, err := g.View(commitUserEmailViewFeature.Name) + // the return string of the views contain trailing new lines + re := regexp.MustCompile(`\r?\n`) + // TODO: maybe intentionally added new lines? + msg := re.ReplaceAllString(v_msg.ViewBuffer(), "") + name := re.ReplaceAllString(v_name.ViewBuffer(), "") + email := re.ReplaceAllString(v_email.ViewBuffer(), "") + if len(email) <= 0 { + return errors.New("User email needs to be provided") + } + err = git.CommitCommand(entity, git.CommitOptions{ + CommitMsg: msg, + User: name, + Email: email, + }) + if err != nil { + return err + } + entity.Refresh() + err = gui.closeCommitMessageView(g, v) + return err +} + +// focus to next view +func (gui *Gui) nextCommitView(g *gocui.Gui, v *gocui.View) error { + err := gui.nextViewOfGroup(g, v, commitViews) + return err +} + +// close the opened commite mesage view +func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + g.Cursor = false + for _, view := range commitViews { + if err := g.DeleteView(view.Name); err != nil { + return err + } + } + for _, view := range commitLabelViews { + if err := g.DeleteView(view.Name); err != nil { + return err + } + } + if err := gui.refreshMain(g); err != nil { + return err + } + if err := gui.refreshViews(g, entity); err != nil { + return err + } + if err := refreshAllStatusView(g, entity, true); err != nil { + return err + } + return gui.closeViewCleanup(commitMesageReturnView) +} diff --git a/pkg/gui/diffview.go b/pkg/gui/diffview.go index 04c9e10..55286e9 100644 --- a/pkg/gui/diffview.go +++ b/pkg/gui/diffview.go @@ -62,9 +62,9 @@ func (gui *Gui) openFileDiffView(g *gocui.Gui, v *gocui.View) (err error) { var files []*git.File switch v.Name() { case unstageViewFeature.Name: - _, files, err = generateFileLists(entity) + _, files, err = populateFileLists(entity) case stageViewFeature.Name: - files, _, err = generateFileLists(entity) + files, _, err = populateFileLists(entity) } if err != nil { return err diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 1d4dd6d..4917cd4 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -206,6 +206,38 @@ func (gui *Gui) generateKeybindings() error { gui.KeyBindings = append(gui.KeyBindings, binding) } } + for _, view := range commitViews { + commitKeybindings := []*KeyBinding{ + { + View: view.Name, + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.closeCommitMessageView, + Display: "esc", + Description: "close/cancel", + Vital: true, + }, { + View: view.Name, + Key: gocui.KeyTab, + Modifier: gocui.ModNone, + Handler: gui.nextCommitView, + Display: "tab", + Description: "Next Panel", + Vital: true, + }, { + View: view.Name, + Key: gocui.KeyEnter, //TODO: enter should be replaced with a better option + Modifier: gocui.ModNone, + Handler: gui.submitCommitMessageView, + Display: "ctrl + enter", + Description: "Submit", + Vital: true, + }, + } + for _, binding := range commitKeybindings { + gui.KeyBindings = append(gui.KeyBindings, binding) + } + } individualKeybindings := []*KeyBinding{ // stash view { @@ -277,24 +309,6 @@ func (gui *Gui) generateKeybindings() error { Description: "Show diff", Vital: true, }, - // commit message view - { - View: commitMessageViewFeature.Name, - Key: gocui.KeyEsc, - Modifier: gocui.ModNone, - Handler: gui.closeCommitMessageView, - Display: "esc", - Description: "Close / Cancel", - Vital: true, - }, { - View: commitMessageViewFeature.Name, - Key: gocui.KeyEnter, //TODO: enter should be replaced with a better option - Modifier: gocui.ModNone, - Handler: gui.submitCommitMessageView, - Display: "ctrl + enter", - Description: "Submit", - Vital: true, - }, // Main view controls { View: mainViewFeature.Name, diff --git a/pkg/gui/stagedview.go b/pkg/gui/stagedview.go index 294c0f7..4a6beb9 100644 --- a/pkg/gui/stagedview.go +++ b/pkg/gui/stagedview.go @@ -27,19 +27,16 @@ func (gui *Gui) openStageView(g *gocui.Gui) error { func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error { entity := gui.getSelectedRepository() - files, _, err := generateFileLists(entity) - if err != nil { - return err - } - if len(files) <= 0 { - return nil - } + _, cy := v.Cursor() _, oy := v.Origin() - if err := git.Reset(entity, files[cy+oy], git.ResetOptions{}); err != nil { + if len(stagedFiles) <= 0 || len(stagedFiles) <= cy+oy { + return nil + } + if err := git.Reset(entity, stagedFiles[cy+oy], git.ResetOptions{}); err != nil { return err } - return refreshAllStatusView(g, entity) + return refreshAllStatusView(g, entity, true) } func (gui *Gui) resetAllChanges(g *gocui.Gui, v *gocui.View) error { @@ -54,7 +51,7 @@ func (gui *Gui) resetAllChanges(g *gocui.Gui, v *gocui.View) error { }); err != nil { return err } - return refreshAllStatusView(g, entity) + return refreshAllStatusView(g, entity, true) } // refresh the main view and re-render the repository representations @@ -66,11 +63,7 @@ func refreshStagedView(g *gocui.Gui, entity *git.RepoEntity) error { stageView.Clear() _, cy := stageView.Cursor() _, oy := stageView.Origin() - files, _, err := generateFileLists(entity) - if err != nil { - return err - } - for i, file := range files { + for i, file := range stagedFiles { var prefix string if i == cy+oy { prefix = prefix + selectionIndicator diff --git a/pkg/gui/stashview.go b/pkg/gui/stashview.go index 54a05db..24b4f3c 100644 --- a/pkg/gui/stashview.go +++ b/pkg/gui/stashview.go @@ -34,7 +34,7 @@ func (gui *Gui) stashChanges(g *gocui.Gui, v *gocui.View) error { return err } } - err = refreshAllStatusView(g, entity) + err = refreshAllStatusView(g, entity, true) return err } @@ -58,7 +58,7 @@ func (gui *Gui) popStash(g *gocui.Gui, v *gocui.View) error { if err := entity.Refresh(); err != nil { return err } - err = refreshAllStatusView(g, entity) + err = refreshAllStatusView(g, entity, true) return err } diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go index c0a9c65..82df459 100644 --- a/pkg/gui/statusview.go +++ b/pkg/gui/statusview.go @@ -1,29 +1,30 @@ package gui import ( - "errors" "fmt" - "time" "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" - ggt "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing/object" ) var ( - statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "} - stageViewFeature = viewFeature{Name: "staged", Title: " Staged "} - unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "} - stashViewFeature = viewFeature{Name: "stash", Title: " Stash "} - commitMessageViewFeature = viewFeature{Name: "commitmessage", Title: " Commit Mesage "} + statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "} + stageViewFeature = viewFeature{Name: "staged", Title: " Staged "} + unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "} + stashViewFeature = viewFeature{Name: "stash", Title: " Stash "} + + statusViews = []viewFeature{stageViewFeature, unstageViewFeature, stashViewFeature} - statusViews = []viewFeature{stageViewFeature, unstageViewFeature, stashViewFeature} commitMesageReturnView string + stagedFiles []*git.File + unstagedFiles []*git.File ) // open the status layout func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error { + if err := reloadFiles(gui.getSelectedRepository()); err != nil { + return err + } gui.openStatusHeaderView(g) gui.openStageView(g) gui.openUnStagedView(g) @@ -31,6 +32,11 @@ func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error { return nil } +func reloadFiles(entity *git.RepoEntity) (err error) { + stagedFiles, unstagedFiles, err = populateFileLists(entity) + return err +} + // focus to next view func (gui *Gui) nextStatusView(g *gocui.Gui, v *gocui.View) error { err := gui.nextViewOfGroup(g, v, statusViews) @@ -62,7 +68,7 @@ func (gui *Gui) statusCursorDown(g *gocui.Gui, v *gocui.View) error { } } entity := gui.getSelectedRepository() - if err := refreshStatusView(v.Name(), g, entity); err != nil { + if err := refreshStatusView(v.Name(), g, entity, false); err != nil { return err } } @@ -80,7 +86,7 @@ func (gui *Gui) statusCursorUp(g *gocui.Gui, v *gocui.View) error { } } entity := gui.getSelectedRepository() - if err := refreshStatusView(v.Name(), g, entity); err != nil { + if err := refreshStatusView(v.Name(), g, entity, false); err != nil { return err } } @@ -113,6 +119,8 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { if err := g.DeleteView(statusHeaderViewFeature.Name); err != nil { return err } + stagedFiles = make([]*git.File, 0) + unstagedFiles = make([]*git.File, 0) entity := gui.getSelectedRepository() if err := gui.refreshMain(g); err != nil { return err @@ -123,7 +131,7 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { return gui.closeViewCleanup(mainViewFeature.Name) } -func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) { +func populateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) { files, err := git.Status(entity) if err != nil { return nil, nil, err @@ -139,14 +147,17 @@ func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, er return staged, unstaged, err } -func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity) error { +func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity, reload bool) error { + if reload { + reloadFiles(entity) + } switch viewName { case stageViewFeature.Name: if err := refreshStagedView(g, entity); err != nil { return err } case unstageViewFeature.Name: - if err := refreshUnstagedView(g, entity); err != nil { + if err := refreshUnstagedView(g); err != nil { return err } case stashViewFeature.Name: @@ -157,84 +168,11 @@ func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity) er return nil } -func refreshAllStatusView(g *gocui.Gui, entity *git.RepoEntity) error { +func refreshAllStatusView(g *gocui.Gui, entity *git.RepoEntity, reload bool) error { for _, v := range statusViews { - if err := refreshStatusView(v.Name, g, entity); err != nil { + if err := refreshStatusView(v.Name, g, entity, reload); err != nil { return err } } return nil } - -// open the commit message views -func (gui *Gui) openCommitMessageView(g *gocui.Gui, v *gocui.View) error { - maxX, maxY := g.Size() - commitMesageReturnView = v.Name() - v, err := g.SetView(commitMessageViewFeature.Name, maxX/2-30, maxY/2-3, maxX/2+30, maxY/2+3) - if err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = commitMessageViewFeature.Title - v.Wrap = true - v.Editable = true - v.Editor = gocui.DefaultEditor - v.Highlight = true - g.Cursor = true - } - gui.updateKeyBindingsView(g, commitMessageViewFeature.Name) - if _, err := g.SetCurrentView(commitMessageViewFeature.Name); err != nil { - return err - } - return nil -} - -// close the opened commite mesage view -func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() - w, err := entity.Repository.Worktree() - if err != nil { - return err - } - config, err := entity.Repository.Config() - if err != nil { - return err - } - name := config.Raw.Section("user").Option("name") - email := config.Raw.Section("user").Option("email") - if len(email) <= 0 { - return errors.New("User email needs to be provided") - } - _, err = w.Commit(v.ViewBuffer(), &ggt.CommitOptions{ - Author: &object.Signature{ - Name: name, - Email: email, - When: time.Now(), - }, - }) - if err != nil { - return err - } - entity.Refresh() - err = gui.closeCommitMessageView(g, v) - return err -} - -// close the opened commite mesage view -func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error { - entity := gui.getSelectedRepository() - g.Cursor = false - if err := g.DeleteView(commitMessageViewFeature.Name); err != nil { - return err - } - if err := gui.refreshMain(g); err != nil { - return err - } - if err := gui.refreshViews(g, entity); err != nil { - return err - } - if err := refreshAllStatusView(g, entity); err != nil { - return err - } - return gui.closeViewCleanup(commitMesageReturnView) -} diff --git a/pkg/gui/unstagedview.go b/pkg/gui/unstagedview.go index 01b16de..1a33c12 100644 --- a/pkg/gui/unstagedview.go +++ b/pkg/gui/unstagedview.go @@ -18,26 +18,22 @@ func (gui *Gui) openUnStagedView(g *gocui.Gui) error { } v.Title = unstageViewFeature.Title } - entity := gui.getSelectedRepository() - err = refreshUnstagedView(g, entity) + err = refreshUnstagedView(g) return err } func (gui *Gui) addChanges(g *gocui.Gui, v *gocui.View) error { entity := gui.getSelectedRepository() - _, files, err := generateFileLists(entity) - if err != nil { - return err - } - if len(files) <= 0 { - return nil - } + _, cy := v.Cursor() _, oy := v.Origin() - if err := git.Add(entity, files[cy+oy], git.AddOptions{}); err != nil { + if len(unstagedFiles) <= 0 || len(unstagedFiles) < cy+oy { + return nil + } + if err := git.Add(entity, unstagedFiles[cy+oy], git.AddOptions{}); err != nil { return err } - err = refreshAllStatusView(g, entity) + err := refreshAllStatusView(g, entity, true) return err } @@ -46,12 +42,12 @@ func (gui *Gui) addAllChanges(g *gocui.Gui, v *gocui.View) error { if err := git.AddAll(entity, git.AddOptions{}); err != nil { return err } - err := refreshAllStatusView(g, entity) + err := refreshAllStatusView(g, entity, true) return err } // refresh the main view and re-render the repository representations -func refreshUnstagedView(g *gocui.Gui, entity *git.RepoEntity) error { +func refreshUnstagedView(g *gocui.Gui) error { stageView, err := g.View(unstageViewFeature.Name) if err != nil { return err @@ -59,11 +55,7 @@ func refreshUnstagedView(g *gocui.Gui, entity *git.RepoEntity) error { stageView.Clear() _, cy := stageView.Cursor() _, oy := stageView.Origin() - _, files, err := generateFileLists(entity) - if err != nil { - return err - } - for i, file := range files { + for i, file := range unstagedFiles { var prefix string if i == cy+oy { prefix = prefix + selectionIndicator -- cgit v1.2.3 From 734514a61c00b325137b0242adbfc8c788ae66c5 Mon Sep 17 00:00:00 2001 From: <> Date: Fri, 14 Dec 2018 02:25:50 +0300 Subject: reorganize keybindings --- pkg/gui/gui-util.go | 22 +++++++++++- pkg/gui/keybindings.go | 96 +++++++++++++++++--------------------------------- 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/pkg/gui/gui-util.go b/pkg/gui/gui-util.go index 343e293..f203bbf 100644 --- a/pkg/gui/gui-util.go +++ b/pkg/gui/gui-util.go @@ -78,7 +78,6 @@ func (gui *Gui) previousViewOfGroup(g *gocui.Gui, v *gocui.View, group []viewFea } // siwtch the app mode -// TODO: switching can be made with conventional iteration func (gui *Gui) switchMode(g *gocui.Gui, v *gocui.View) error { for i, mode := range modes { if mode == gui.State.Mode { @@ -94,6 +93,27 @@ func (gui *Gui) switchMode(g *gocui.Gui, v *gocui.View) error { return nil } +// siwtch the app's mode to fetch +func (gui *Gui) switchToFetchMode(g *gocui.Gui, v *gocui.View) error { + gui.State.Mode = fetchMode + gui.updateKeyBindingsView(g, mainViewFeature.Name) + return nil +} + +// siwtch the app's mode to pull +func (gui *Gui) switchToPullMode(g *gocui.Gui, v *gocui.View) error { + gui.State.Mode = pullMode + gui.updateKeyBindingsView(g, mainViewFeature.Name) + return nil +} + +// siwtch the app's mode to merge +func (gui *Gui) switchToMergeMode(g *gocui.Gui, v *gocui.View) error { + gui.State.Mode = mergeMode + gui.updateKeyBindingsView(g, mainViewFeature.Name) + return nil +} + // bring the view on the top by its name func (gui *Gui) setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { if _, err := g.SetCurrentView(name); err != nil { diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 4917cd4..53acf60 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -33,42 +33,34 @@ func (gui *Gui) generateKeybindings() error { Vital: true, }, { View: view.Name, - Key: gocui.KeyTab, - Modifier: gocui.ModNone, - Handler: gui.switchMode, - Display: "tab", - Description: "Switch mode", - Vital: true, - }, { - View: view.Name, - Key: gocui.KeyArrowLeft, + Key: 'f', Modifier: gocui.ModNone, - Handler: gui.previousMainView, - Display: "←", - Description: "Previous Panel", + Handler: gui.switchToFetchMode, + Display: "f", + Description: "Fetch mode", Vital: false, }, { View: view.Name, - Key: gocui.KeyArrowRight, + Key: 'p', Modifier: gocui.ModNone, - Handler: gui.nextMainView, - Display: "→", - Description: "Next Panel", + Handler: gui.switchToPullMode, + Display: "p", + Description: "Pull mode", Vital: false, }, { View: view.Name, - Key: 'l', + Key: 'm', Modifier: gocui.ModNone, - Handler: gui.nextMainView, - Display: "l", - Description: "Previous Panel", + Handler: gui.switchToMergeMode, + Display: "m", + Description: "Merge mode", Vital: false, }, { View: view.Name, - Key: 'h', + Key: gocui.KeyTab, Modifier: gocui.ModNone, - Handler: gui.previousMainView, - Display: "h", + Handler: gui.nextMainView, + Display: "tab", Description: "Next Panel", Vital: false, }, @@ -82,44 +74,20 @@ func (gui *Gui) generateKeybindings() error { statusKeybindings := []*KeyBinding{ { View: view.Name, - Key: 'c', + Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.closeStatusView, - Display: "c", + Display: "esc", Description: "Close/Cancel", Vital: true, }, { View: view.Name, - Key: gocui.KeyArrowLeft, - Modifier: gocui.ModNone, - Handler: gui.previousStatusView, - Display: "←", - Description: "Previous Panel", - Vital: false, - }, { - View: view.Name, - Key: gocui.KeyArrowRight, - Modifier: gocui.ModNone, - Handler: gui.nextStatusView, - Display: "→", - Description: "Next Panel", - Vital: false, - }, { - View: view.Name, - Key: 'l', + Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextStatusView, - Display: "l", - Description: "Previous Panel", - Vital: false, - }, { - View: view.Name, - Key: 'h', - Modifier: gocui.ModNone, - Handler: gui.previousStatusView, - Display: "h", + Display: "tab", Description: "Next Panel", - Vital: false, + Vital: true, }, { View: view.Name, Key: gocui.KeyArrowUp, @@ -348,7 +316,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.markRepository, Display: "space", - Description: "Add to queue", + Description: "Select", Vital: true, }, { View: mainViewFeature.Name, @@ -356,7 +324,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.startQueue, Display: "enter", - Description: "Start queue", + Description: "Start", Vital: true, }, { View: mainViewFeature.Name, @@ -364,7 +332,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.markAllRepositories, Display: "ctrl + space", - Description: "Add all to queue", + Description: "Select All", Vital: false, }, { View: mainViewFeature.Name, @@ -372,7 +340,7 @@ func (gui *Gui) generateKeybindings() error { Modifier: gocui.ModNone, Handler: gui.unmarkAllRepositories, Display: "backspace", - Description: "Remove all from queue", + Description: "Deselect All", Vital: false, }, { View: mainViewFeature.Name, @@ -392,10 +360,10 @@ func (gui *Gui) generateKeybindings() error { Vital: false, }, { View: mainViewFeature.Name, - Key: 'm', + Key: 'r', Modifier: gocui.ModNone, Handler: gui.sortByMod, - Display: "m", + Display: "r", Description: "Sort repositories by Modification date", Vital: false, }, { @@ -570,10 +538,10 @@ func (gui *Gui) generateKeybindings() error { // Diff View Controls { View: diffViewFeature.Name, - Key: 'c', + Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.closeCommitDiffView, - Display: "c", + Display: "esc", Description: "close/cancel", Vital: true, }, { @@ -612,10 +580,10 @@ func (gui *Gui) generateKeybindings() error { // Application Controls { View: cheatSheetViewFeature.Name, - Key: 'c', + Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.closeCheatSheetView, - Display: "c", + Display: "esc", Description: "close/cancel", Vital: true, }, { @@ -654,10 +622,10 @@ func (gui *Gui) generateKeybindings() error { // Error View { View: errorViewFeature.Name, - Key: 'c', + Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.closeErrorView, - Display: "c", + Display: "esc", Description: "close/cancel", Vital: true, }, { -- cgit v1.2.3 From 7d287a1920d26f75757df37a0754b514074b2f96 Mon Sep 17 00:00:00 2001 From: <> Date: Fri, 14 Dec 2018 02:26:52 +0300 Subject: minor fix --- pkg/gui/keybindings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 53acf60..63c0c8b 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -197,7 +197,7 @@ func (gui *Gui) generateKeybindings() error { Key: gocui.KeyEnter, //TODO: enter should be replaced with a better option Modifier: gocui.ModNone, Handler: gui.submitCommitMessageView, - Display: "ctrl + enter", + Display: "enter", Description: "Submit", Vital: true, }, -- cgit v1.2.3 From 1fb01f7086f0b508b30904cf283d3c3a65578c79 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Fri, 14 Dec 2018 02:52:17 +0300 Subject: added new screen cast and minor bugfixes --- README.md | 2 +- pkg/gui/commitview.go | 8 ++++---- pkg/gui/keybindings.go | 22 ++++++---------------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ed085d0..7e6f592 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This tool is beening built to make your local repositories synchronized with remotes easily. Although the focus is batch jobs, you can still do de facto micro management of your git repositories (e.g *add/reset, stash, commit etc.*) Here is the screencast of the app: -[![asciicast](https://asciinema.org/a/eXgXpzZfuHxMpZqGMVODUipyc.svg)](https://asciinema.org/a/eXgXpzZfuHxMpZqGMVODUipyc) +[![asciicast](https://asciinema.org/a/QQPVDWVxUR3bvJhIZY3c4PTuG.svg)](https://asciinema.org/a/QQPVDWVxUR3bvJhIZY3c4PTuG) ## Installation For now, installation requires golang compiler and minimum golang 1.10 is recommended. (binary distribution will be provided on minimum viable product) diff --git a/pkg/gui/commitview.go b/pkg/gui/commitview.go index 8fdd63f..94ef574 100644 --- a/pkg/gui/commitview.go +++ b/pkg/gui/commitview.go @@ -11,13 +11,13 @@ import ( var ( commitFrameViewFeature = viewFeature{Name: "commitframe", Title: " Frame "} - commitUserNameLabelFeature = viewFeature{Name: "commitusername", Title: " Name: "} - commitUserEmailLabelViewFeature = viewFeature{Name: "commituseremail", Title: " E-Mail: "} + commitUserNameLabelFeature = viewFeature{Name: "commitusernamelabel", Title: " Name: "} + commitUserEmailLabelViewFeature = viewFeature{Name: "commituseremaillabel", Title: " E-Mail: "} // these views used as a input for the credentials commitMessageViewFeature = viewFeature{Name: "commitmessage", Title: " Commit Mesage "} - commitUserUserViewFeature = viewFeature{Name: "authuser", Title: " Name "} - commitUserEmailViewFeature = viewFeature{Name: "authpasswd", Title: " E-Mail "} + commitUserUserViewFeature = viewFeature{Name: "commitusername", Title: " Name "} + commitUserEmailViewFeature = viewFeature{Name: "commituseremail", Title: " E-Mail "} commitViews = []viewFeature{commitMessageViewFeature, commitUserUserViewFeature, commitUserEmailViewFeature} commitLabelViews = []viewFeature{commitFrameViewFeature, commitUserNameLabelFeature, commitUserEmailLabelViewFeature} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 63c0c8b..936c7c6 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -65,9 +65,7 @@ func (gui *Gui) generateKeybindings() error { Vital: false, }, } - for _, binding := range mainKeybindings { - gui.KeyBindings = append(gui.KeyBindings, binding) - } + gui.KeyBindings = append(gui.KeyBindings, mainKeybindings...) } // Statusviews common keybindings for _, view := range statusViews { @@ -138,9 +136,7 @@ func (gui *Gui) generateKeybindings() error { Vital: true, }, } - for _, binding := range statusKeybindings { - gui.KeyBindings = append(gui.KeyBindings, binding) - } + gui.KeyBindings = append(gui.KeyBindings, statusKeybindings...) } for _, view := range authViews { authKeybindings := []*KeyBinding{ @@ -170,9 +166,7 @@ func (gui *Gui) generateKeybindings() error { Vital: true, }, } - for _, binding := range authKeybindings { - gui.KeyBindings = append(gui.KeyBindings, binding) - } + gui.KeyBindings = append(gui.KeyBindings, authKeybindings...) } for _, view := range commitViews { commitKeybindings := []*KeyBinding{ @@ -194,7 +188,7 @@ func (gui *Gui) generateKeybindings() error { Vital: true, }, { View: view.Name, - Key: gocui.KeyEnter, //TODO: enter should be replaced with a better option + Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.submitCommitMessageView, Display: "enter", @@ -202,9 +196,7 @@ func (gui *Gui) generateKeybindings() error { Vital: true, }, } - for _, binding := range commitKeybindings { - gui.KeyBindings = append(gui.KeyBindings, binding) - } + gui.KeyBindings = append(gui.KeyBindings, commitKeybindings...) } individualKeybindings := []*KeyBinding{ // stash view @@ -662,9 +654,7 @@ func (gui *Gui) generateKeybindings() error { Vital: false, }, } - for _, binding := range individualKeybindings { - gui.KeyBindings = append(gui.KeyBindings, binding) - } + gui.KeyBindings = append(gui.KeyBindings, individualKeybindings...) return nil } -- cgit v1.2.3