From 46e22a8968f72895622c56b1e7fa554bb501371c Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Sat, 8 Dec 2018 03:12:52 +0300 Subject: status implementation cntd.. added display and navigation --- pkg/git/status.go | 67 +++++++++++++++++++++++++++ pkg/gui/keybindings.go | 58 ++++++++++++++++------- pkg/gui/stagedview.go | 54 ++++++++++++++++++++++ pkg/gui/stashview.go | 47 +++++++++++++++++++ pkg/gui/statusview.go | 119 +++++++++++++++++++++++++++++------------------- pkg/gui/unstagedview.go | 50 ++++++++++++++++++++ 6 files changed, 332 insertions(+), 63 deletions(-) create mode 100644 pkg/git/status.go create mode 100644 pkg/gui/stagedview.go create mode 100644 pkg/gui/stashview.go create mode 100644 pkg/gui/unstagedview.go diff --git a/pkg/git/status.go b/pkg/git/status.go new file mode 100644 index 0000000..e1207ee --- /dev/null +++ b/pkg/git/status.go @@ -0,0 +1,67 @@ +package git + +import ( + "os" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" +) + +var statusCommand = "status" + +type File struct { + Name string + AbsPath string + X FileStatus + Y FileStatus +} + +type FileStatus rune + +var ( + StatusNotupdated FileStatus = ' ' + StatusModified FileStatus = 'M' + StatusAdded FileStatus = 'A' + StatusDeleted FileStatus = 'D' + StatusRenamed FileStatus = 'R' + StatusCopied FileStatus = 'C' + StatusUntracked FileStatus = '?' + 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 +} + +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 --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 26df3fa..55c86bd 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -77,6 +77,7 @@ func (gui *Gui) generateKeybindings() error { gui.KeyBindings = append(gui.KeyBindings, binding) } } + // Statusviews common keybindings for _, view := range statusViews { statusKeybindings := []*KeyBinding{ { @@ -119,6 +120,38 @@ func (gui *Gui) generateKeybindings() error { Display: "h", Description: "Next Panel", Vital: false, + }, { + View: view.Name, + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + Handler: gui.statusCursorUp, + Display: "↑", + Description: "Up", + Vital: false, + }, { + View: view.Name, + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + Handler: gui.statusCursorDown, + Display: "↓", + Description: "Down", + Vital: false, + }, { + View: view.Name, + Key: 'k', + Modifier: gocui.ModNone, + Handler: gui.statusCursorUp, + Display: "k", + Description: "Up", + Vital: false, + }, { + View: view.Name, + Key: 'j', + Modifier: gocui.ModNone, + Handler: gui.statusCursorDown, + Display: "j", + Description: "Down", + Vital: false, }, } for _, binding := range statusKeybindings { @@ -126,6 +159,7 @@ func (gui *Gui) generateKeybindings() error { } } individualKeybindings := []*KeyBinding{ + // Main view controls { View: mainViewFeature.Name, Key: gocui.KeyArrowUp, @@ -214,6 +248,14 @@ func (gui *Gui) generateKeybindings() error { Display: "m", Description: "Sort repositories by Modification date", Vital: false, + }, { + View: mainViewFeature.Name, + Key: 's', + Modifier: gocui.ModNone, + Handler: gui.openStatusView, + Display: "s", + Description: "Open Status", + Vital: true, }, { View: "", Key: gocui.KeyCtrlC, @@ -460,22 +502,6 @@ func (gui *Gui) generateKeybindings() error { Display: "c", Description: "close/cancel", Vital: true, - }, { - View: statusHeaderViewFeature.Name, - Key: 'c', - Modifier: gocui.ModNone, - Handler: gui.closeStatusView, - Display: "c", - Description: "close/cancel", - Vital: true, - }, { - View: mainViewFeature.Name, - Key: 's', - Modifier: gocui.ModNone, - Handler: gui.openStatusView, - Display: "s", - Description: "Open Status", - Vital: true, }, } for _, binding := range individualKeybindings { diff --git a/pkg/gui/stagedview.go b/pkg/gui/stagedview.go new file mode 100644 index 0000000..e3684f0 --- /dev/null +++ b/pkg/gui/stagedview.go @@ -0,0 +1,54 @@ +package gui + +import ( + "fmt" + + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" +) + +// staged view +func (gui *Gui) openStageView(g *gocui.Gui) error { + maxX, maxY := g.Size() + + v, err := g.SetView(stageViewFeature.Name, 6, 5, maxX/2-1, int(0.75*float32(maxY))-1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = stageViewFeature.Title + v.Wrap = true + } + entity := gui.getSelectedRepository() + 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 +} + +// refresh the main view and re-render the repository representations +func refreshStagedView(g *gocui.Gui, entity *git.RepoEntity) error { + stageView, err := g.View(stageViewFeature.Name) + if err != nil { + return err + } + stageView.Clear() + _, cy := stageView.Cursor() + _, oy := stageView.Origin() + files, _, err := generateFileLists(entity) + if err != nil { + return err + } + for i, file := range files { + var prefix string + if i == cy+oy { + prefix = prefix + selectionIndicator + } + fmt.Fprintf(stageView, "%s%s%s %s\n", prefix, green.Sprint(string(file.X)), red.Sprint(string(file.Y)), file.Name) + } + return nil +} diff --git a/pkg/gui/stashview.go b/pkg/gui/stashview.go new file mode 100644 index 0000000..8cdc46c --- /dev/null +++ b/pkg/gui/stashview.go @@ -0,0 +1,47 @@ +package gui + +import ( + "fmt" + + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" +) + +// stash view +func (gui *Gui) openStashView(g *gocui.Gui) error { + maxX, maxY := g.Size() + + v, err := g.SetView(stashViewFeature.Name, 6, int(0.75*float32(maxY)), maxX-6, maxY-3) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = stashViewFeature.Title + v.Wrap = true + } + entity := gui.getSelectedRepository() + if err := refreshStashView(g, entity); err != nil { + return err + } + return nil +} + +// refresh the main view and re-render the repository representations +func refreshStashView(g *gocui.Gui, entity *git.RepoEntity) error { + stashView, err := g.View(stashViewFeature.Name) + if err != nil { + return err + } + stashView.Clear() + _, cy := stashView.Cursor() + _, oy := stashView.Origin() + stashedItems := entity.Stasheds + for i, stashedItem := range stashedItems { + var prefix string + if i == cy+oy { + prefix = prefix + selectionIndicator + } + fmt.Fprintf(stashView, "%s%d %s: %s (%s)\n", prefix, stashedItem.StashID, cyan.Sprint(stashedItem.BranchName), stashedItem.Description, stashedItem.Hash) + } + return nil +} \ No newline at end of file diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go index f9ab29d..b1873e8 100644 --- a/pkg/gui/statusview.go +++ b/pkg/gui/statusview.go @@ -3,6 +3,7 @@ package gui import ( "fmt" + "github.com/isacikgoz/gitbatch/pkg/git" "github.com/jroimartin/gocui" ) @@ -41,72 +42,62 @@ func (gui *Gui) previousStatusView(g *gocui.Gui, v *gocui.View) error { return nil } -// header og the status layout -func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error { - maxX, _ := g.Size() - entity := gui.getSelectedRepository() - v, err := g.SetView(statusHeaderViewFeature.Name, 6, 2, maxX-6, 4) - if err != nil { - if err != gocui.ErrUnknownView { - return err - } - fmt.Fprintln(v, entity.AbsPath) - // v.Frame = false - v.Wrap = true - } - return nil -} +// moves the cursor downwards for the main view and if it goes to bottom it +// prevents from going further +func (gui *Gui) statusCursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + cx, cy := v.Cursor() + ox, oy := v.Origin() + ly := len(v.BufferLines()) - 2 // why magic number? have no idea -// staged view -func (gui *Gui) openStageView(g *gocui.Gui) error { - maxX, maxY := g.Size() + // if we are at the end we just return + if cy+oy == ly { + return nil + } + if err := v.SetCursor(cx, cy+1); err != nil { - v, err := g.SetView(stageViewFeature.Name, 6, 5, maxX/2-1, int(0.75*float32(maxY))-1) - if err != nil { - if err != gocui.ErrUnknownView { + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + entity := gui.getSelectedRepository() + if err := refreshStatusView(v.Name(), g, entity); err != nil { return err } - v.Title = stageViewFeature.Title - v.Wrap = true - } - gui.updateKeyBindingsView(g, stageViewFeature.Name) - if _, err := g.SetCurrentView(stageViewFeature.Name); err != nil { - return err } return nil } -// not staged view -func (gui *Gui) openUnStagedView(g *gocui.Gui) error { - maxX, maxY := g.Size() - - v, err := g.SetView(unstageViewFeature.Name, maxX/2+1, 5, maxX-6, int(0.75*float32(maxY))-1) - if err != nil { - if err != gocui.ErrUnknownView { +// moves the cursor upwards for the main view +func (gui *Gui) statusCursorUp(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + entity := gui.getSelectedRepository() + if err := refreshStatusView(v.Name(), g, entity); err != nil { return err } - v.Title = unstageViewFeature.Title - v.Wrap = true } return nil } -// stash view -func (gui *Gui) openStashView(g *gocui.Gui) error { - maxX, maxY := g.Size() - - v, err := g.SetView(stashViewFeature.Name, 6, int(0.75*float32(maxY)), maxX-6, maxY-3) +// header og the status layout +func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error { + maxX, _ := g.Size() + entity := gui.getSelectedRepository() + v, err := g.SetView(statusHeaderViewFeature.Name, 6, 2, maxX-6, 4) if err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = stashViewFeature.Title + fmt.Fprintln(v, entity.AbsPath) + // v.Frame = false v.Wrap = true - entity := gui.getSelectedRepository() - stashedItems := entity.Stasheds - for _, stashedItem := range stashedItems { - fmt.Fprintln(v, stashedItem) - } } return nil } @@ -131,3 +122,37 @@ func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { gui.updateKeyBindingsView(g, mainViewFeature.Name) return nil } + +func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) { + files, err := entity.LoadFiles() + if err != nil { + return nil, nil, err + } + for _, file := range files { + if file.X != git.StatusNotupdated && file.X != git.StatusUntracked && file.X != git.StatusIgnored { + staged = append(staged, file) + } + if file.Y != git.StatusNotupdated { + unstaged = append(unstaged, file) + } + } + return staged, unstaged, err +} + +func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity) error { + switch viewName { + case stageViewFeature.Name: + if err := refreshStagedView(g, entity); err != nil { + return err + } + case unstageViewFeature.Name: + if err := refreshUnstagedView(g, entity); err != nil { + return err + } + case stashViewFeature.Name: + if err := refreshStashView(g, entity); err != nil { + return err + } + } + return nil +} diff --git a/pkg/gui/unstagedview.go b/pkg/gui/unstagedview.go new file mode 100644 index 0000000..5ee25a7 --- /dev/null +++ b/pkg/gui/unstagedview.go @@ -0,0 +1,50 @@ +package gui + +import ( + "fmt" + + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" +) + +// not staged view +func (gui *Gui) openUnStagedView(g *gocui.Gui) error { + maxX, maxY := g.Size() + + v, err := g.SetView(unstageViewFeature.Name, maxX/2+1, 5, maxX-6, int(0.75*float32(maxY))-1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = unstageViewFeature.Title + v.Wrap = true + } + entity := gui.getSelectedRepository() + if err := refreshUnstagedView(g, entity); err != nil { + return err + } + return nil +} + +// refresh the main view and re-render the repository representations +func refreshUnstagedView(g *gocui.Gui, entity *git.RepoEntity) error { + stageView, err := g.View(unstageViewFeature.Name) + if err != nil { + return err + } + stageView.Clear() + _, cy := stageView.Cursor() + _, oy := stageView.Origin() + _, files, err := generateFileLists(entity) + if err != nil { + return err + } + for i, file := range files { + var prefix string + if i == cy+oy { + prefix = prefix + selectionIndicator + } + fmt.Fprintf(stageView, "%s%s%s %s\n", prefix, green.Sprint(string(file.X)), red.Sprint(string(file.Y)), file.Name) + } + return nil +} \ No newline at end of file -- cgit v1.2.3