From 44ddb04e0bfe395565ace08e34d1d84332c48bc1 Mon Sep 17 00:00:00 2001 From: İbrahim Serdar Açıkgöz Date: Fri, 28 Dec 2018 10:59:28 +0300 Subject: fixes a semantic error while loading repositories --- main.go | 2 +- pkg/gui/gui.go | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index 8edebeb..7ef48b8 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ var ( ) func main() { - kingpin.Version("gitbatch version 0.2.0") + kingpin.Version("gitbatch version 0.2.1") // parse the command line flag and options kingpin.Parse() diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2e1f2b2..c839860 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -101,6 +101,16 @@ func (gui *Gui) Run() error { return err } + defer g.Close() + 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) + // start an async view apart from this loop to show loading screen go func(g_ui *Gui) { maxX, maxY := g.Size() @@ -131,16 +141,6 @@ func (gui *Gui) Run() error { gui.fillMain(g) }(gui) - defer g.Close() - 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 { log.Error("Keybindings could not be created.") return err -- cgit v1.2.3 From f96d29826072337d7e4498e6ccea3a05e014465b Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Mon, 31 Dec 2018 17:08:58 +0300 Subject: switch esc keybinding to q, add home/end buttons to repository nav and fic a bug preventing app to release sysin --- main.go | 4 ++-- pkg/gui/gui.go | 1 - pkg/gui/keybindings.go | 36 ++++++++++++++++++++++++++---------- pkg/gui/mainview.go | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index 7ef48b8..573f1ae 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,7 @@ package main import ( "github.com/isacikgoz/gitbatch/pkg/app" log "github.com/sirupsen/logrus" - "gopkg.in/alecthomas/kingpin.v2" + kingpin "gopkg.in/alecthomas/kingpin.v2" ) var ( @@ -15,7 +15,7 @@ var ( ) func main() { - kingpin.Version("gitbatch version 0.2.1") + kingpin.Version("gitbatch version 0.2.2") // parse the command line flag and options kingpin.Parse() diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 0c050bd..b19772b 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -111,7 +111,6 @@ func (gui *Gui) Run() error { g.InputEsc = true g.SetManagerFunc(gui.layout) - defer g.Close() gui.g = g g.Highlight = true g.SelFgColor = gocui.ColorGreen diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 3c5db5f..330c67a 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -110,10 +110,10 @@ func (gui *Gui) generateKeybindings() error { statusKeybindings := []*KeyBinding{ { View: view.Name, - Key: gocui.KeyEsc, + Key: 'q', Modifier: gocui.ModNone, Handler: gui.closeStatusView, - Display: "esc", + Display: "q", Description: "Close/Cancel", Vital: true, }, { @@ -316,6 +316,22 @@ func (gui *Gui) generateKeybindings() error { Display: "↑", Description: "Up", Vital: false, + }, { + View: mainViewFeature.Name, + Key: gocui.KeyHome, + Modifier: gocui.ModNone, + Handler: gui.cursorTop, + Display: "home", + Description: "Home", + Vital: false, + }, { + View: mainViewFeature.Name, + Key: gocui.KeyEnd, + Modifier: gocui.ModNone, + Handler: gui.cursorEnd, + Display: "end", + Description: "End", + Vital: false, }, { View: mainViewFeature.Name, Key: gocui.KeyArrowDown, @@ -440,10 +456,10 @@ func (gui *Gui) generateKeybindings() error { // upstream confirmation { View: confirmationViewFeature.Name, - Key: gocui.KeyEsc, + Key: 'q', Modifier: gocui.ModNone, Handler: gui.closeConfirmationView, - Display: "esc", + Display: "q", Description: "Close/Cancel", Vital: true, }, { @@ -458,10 +474,10 @@ func (gui *Gui) generateKeybindings() error { // Diff View Controls { View: diffViewFeature.Name, - Key: gocui.KeyEsc, + Key: 'q', Modifier: gocui.ModNone, Handler: gui.closeCommitDiffView, - Display: "esc", + Display: "q", Description: "Close/Cancel", Vital: true, }, { @@ -500,10 +516,10 @@ func (gui *Gui) generateKeybindings() error { // Application Controls { View: cheatSheetViewFeature.Name, - Key: gocui.KeyEsc, + Key: 'q', Modifier: gocui.ModNone, Handler: gui.closeCheatSheetView, - Display: "esc", + Display: "q", Description: "Close/Cancel", Vital: true, }, { @@ -542,10 +558,10 @@ func (gui *Gui) generateKeybindings() error { // Error View { View: errorViewFeature.Name, - Key: gocui.KeyEsc, + Key: 'q', Modifier: gocui.ModNone, Handler: gui.closeErrorView, - Display: "esc", + Display: "q", Description: "Close/Cancel", Vital: true, }, { diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index d07a742..109d066 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -94,6 +94,44 @@ func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error { return gui.renderMain() } +// moves cursor to the top +func (gui *Gui) cursorTop(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, _ := v.Origin() + cx, _ := v.Cursor() + if err := v.SetOrigin(ox, 0); err != nil { + return err + } + if err := v.SetCursor(cx, 0); err != nil { + return err + } + } + return gui.renderMain() +} + +// moves cursor to the end +func (gui *Gui) cursorEnd(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, _ := v.Origin() + cx, _ := v.Cursor() + _, vy := v.Size() + lr := len(gui.State.Repositories) + if lr <= vy { + if err := v.SetCursor(cx, lr-1); err != nil { + return err + } + return gui.renderMain() + } + if err := v.SetOrigin(ox, lr-vy); err != nil { + return err + } + if err := v.SetCursor(cx, vy-1); err != nil { + return err + } + } + return gui.renderMain() +} + // returns the entity at cursors position by taking its position in the gui's // slice of repositories. Since it is not a %100 percent safe methodology it may // rrequire a better implementation or the slice's order must be synchronized -- cgit v1.2.3 From f5bc05b6af765a72bbaa6cc72854681718221c5e Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Tue, 1 Jan 2019 14:32:26 +0300 Subject: add page up page down controls on the repo view --- pkg/gui/keybindings.go | 16 ++++++++++++++++ pkg/gui/mainview.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 330c67a..7aaee56 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -316,6 +316,14 @@ func (gui *Gui) generateKeybindings() error { Display: "↑", Description: "Up", Vital: false, + }, { + View: mainViewFeature.Name, + Key: gocui.KeyPgup, + Modifier: gocui.ModNone, + Handler: gui.pageUp, + Display: "page up", + Description: "Page up", + Vital: false, }, { View: mainViewFeature.Name, Key: gocui.KeyHome, @@ -324,6 +332,14 @@ func (gui *Gui) generateKeybindings() error { Display: "home", Description: "Home", Vital: false, + }, { + View: mainViewFeature.Name, + Key: gocui.KeyPgdn, + Modifier: gocui.ModNone, + Handler: gui.pageDown, + Display: "page down", + Description: "Page Down", + Vital: false, }, { View: mainViewFeature.Name, Key: gocui.KeyEnd, diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index 109d066..869b334 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -132,6 +132,51 @@ func (gui *Gui) cursorEnd(g *gocui.Gui, v *gocui.View) error { return gui.renderMain() } +// moves cursor down for a page size +func (gui *Gui) pageDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + cx, _ := v.Cursor() + _, vy := v.Size() + lr := len(gui.State.Repositories) + if oy+vy >= lr-vy { + if err := v.SetOrigin(ox, lr-vy); err != nil { + return err + } + } else if err := v.SetOrigin(ox, oy+vy); err != nil { + return err + } + if err := v.SetCursor(cx, 0); err != nil { + return err + } + } + return gui.renderMain() +} + +// moves cursor up for a page size +func (gui *Gui) pageUp(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + _, vy := v.Size() + if oy == 0 || oy+cy < vy { + if err := v.SetOrigin(ox, 0); err != nil { + return err + } + } else if oy <= vy { + if err := v.SetOrigin(ox, oy+cy-vy); err != nil { + return err + } + } else if err := v.SetOrigin(ox, oy-vy); err != nil { + return err + } + if err := v.SetCursor(cx, 0); err != nil { + return err + } + } + return gui.renderMain() +} + // returns the entity at cursors position by taking its position in the gui's // slice of repositories. Since it is not a %100 percent safe methodology it may // rrequire a better implementation or the slice's order must be synchronized -- cgit v1.2.3 From 457d39a46dc891d18b0b199c2295f38e5538ca8a Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Tue, 1 Jan 2019 15:04:04 +0300 Subject: remove utility package --- main.go | 22 +++++++++---- pkg/git/branch.go | 5 ++- pkg/git/cmd.go | 80 ++++++++++++++++++++++++++++++++++++++------- pkg/git/repository.go | 5 ++- pkg/git/util-random.go | 18 ++++++++++ pkg/git/util-random_test.go | 11 +++++++ pkg/gui/mainview.go | 3 ++ pkg/gui/util-common.go | 11 +++++-- pkg/helpers/command.go | 51 ----------------------------- pkg/helpers/utils.go | 36 -------------------- pkg/helpers/utils_test.go | 11 ------- 11 files changed, 128 insertions(+), 125 deletions(-) create mode 100644 pkg/git/util-random.go create mode 100644 pkg/git/util-random_test.go delete mode 100644 pkg/helpers/command.go delete mode 100644 pkg/helpers/utils.go delete mode 100644 pkg/helpers/utils_test.go diff --git a/main.go b/main.go index 573f1ae..d904e8d 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "os" + "github.com/isacikgoz/gitbatch/pkg/app" log "github.com/sirupsen/logrus" kingpin "gopkg.in/alecthomas/kingpin.v2" @@ -19,6 +21,15 @@ func main() { // parse the command line flag and options kingpin.Parse() + if err := run(); err != nil { + log.WithFields(log.Fields{ + "error": err.Error(), + }).Error("Application quitted with an unhandled error.") + os.Exit(1) + } +} + +func run() error { // set the app app, err := app.Setup(&app.SetupConfig{ Directories: *dirs, @@ -28,15 +39,12 @@ func main() { Mode: *mode, }) if err != nil { - log.Fatal(err) - } - - // execute the app and wait its routine - err = app.Gui.Run() - if err != nil { - log.Fatal(err) + return err } // good citizens always clean up their mess defer app.Close() + + // execute the app and wait its routine + return app.Gui.Run() } diff --git a/pkg/git/branch.go b/pkg/git/branch.go index d3b2425..94ad578 100644 --- a/pkg/git/branch.go +++ b/pkg/git/branch.go @@ -4,9 +4,8 @@ import ( "strconv" "strings" - "github.com/isacikgoz/gitbatch/pkg/helpers" log "github.com/sirupsen/logrus" - "gopkg.in/src-d/go-git.v4" + git "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" ) @@ -142,7 +141,7 @@ func (e *RepoEntity) Checkout(branch *Branch) error { // an issue about it: https://github.com/src-d/go-git/issues/844 func (e *RepoEntity) isClean() bool { s := e.StatusWithGit() - s = helpers.TrimTrailingNewline(s) + s = TrimTrailingNewline(s) if s != "?" { vs := strings.Split(s, "\n") line := vs[len(vs)-1] diff --git a/pkg/git/cmd.go b/pkg/git/cmd.go index a84f489..05d030c 100644 --- a/pkg/git/cmd.go +++ b/pkg/git/cmd.go @@ -1,12 +1,68 @@ package git import ( - "github.com/isacikgoz/gitbatch/pkg/helpers" + "log" + "os/exec" + "strings" + "syscall" ) +// RunCommandWithOutput runs the OS command and return its output. If the output +// returns error it also encapsulates it as a golang.error which is a return code +// of the command except zero +func RunCommandWithOutput(dir string, command string, args []string) (string, error) { + cmd := exec.Command(command, args...) + if dir != "" { + cmd.Dir = dir + } + output, err := cmd.Output() + return string(output), err +} + +// GetCommandStatus returns if we supposed to get return value as an int of a command +// this method can be used. It is practical when you use a command and process a +// failover acoording to a soecific return code +func GetCommandStatus(dir string, command string, args []string) (int, error) { + cmd := exec.Command(command, args...) + if dir != "" { + cmd.Dir = dir + } + var err error + // this time the execution is a little different + if err := cmd.Start(); err != nil { + return -1, err + } + if err := cmd.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + statusCode := status.ExitStatus() + return statusCode, err + } + } else { + log.Fatalf("cmd.Wait: %v", err) + } + } + return -1, err +} + +// TrimTrailingNewline removes the trailing new line form a string. this method +// is used mostly on outputs of a command +func TrimTrailingNewline(str string) string { + if strings.HasSuffix(str, "\n") { + return str[:len(str)-1] + } + return str +} + // GenericGitCommand runs any git command without expecting output func GenericGitCommand(repoPath string, args []string) error { - _, err := helpers.RunCommandWithOutput(repoPath, "git", args) + _, err := RunCommandWithOutput(repoPath, "git", args) if err != nil { return err } @@ -15,26 +71,26 @@ func GenericGitCommand(repoPath string, args []string) error { // GenericGitCommandWithOutput runs any git command with returning output func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) { - out, err := helpers.RunCommandWithOutput(repoPath, "git", args) + out, err := RunCommandWithOutput(repoPath, "git", args) if err != nil { return "?", err } - return helpers.TrimTrailingNewline(out), nil + return 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) + out, err := RunCommandWithOutput(repoPath, "git", args) if err != nil { - return helpers.TrimTrailingNewline(out), err + return TrimTrailingNewline(out), err } - return helpers.TrimTrailingNewline(out), nil + return TrimTrailingNewline(out), nil } // GitShow is conventional git show command without any argument func GitShow(repoPath, hash string) string { args := []string{"show", hash} - diff, err := helpers.RunCommandWithOutput(repoPath, "git", args) + diff, err := RunCommandWithOutput(repoPath, "git", args) if err != nil { return "?" } @@ -44,7 +100,7 @@ func GitShow(repoPath, hash string) string { // 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) + diff, err := RunCommandWithOutput(repoPath, "git", args) if err != nil { return "?" } @@ -54,7 +110,7 @@ func GitShowEmail(repoPath, hash string) string { // 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) + diff, err := RunCommandWithOutput(repoPath, "git", args) if err != nil { return err.Error() } @@ -64,7 +120,7 @@ func GitShowBody(repoPath, hash string) string { // 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) + diff, err := RunCommandWithOutput(repoPath, "git", args) if err != nil { return "?" } @@ -74,7 +130,7 @@ func GitShowDate(repoPath, hash string) string { // StatusWithGit returns the plaintext short status of the repo func (e *RepoEntity) StatusWithGit() string { args := []string{"status"} - status, err := helpers.RunCommandWithOutput(e.AbsPath, "git", args) + status, err := RunCommandWithOutput(e.AbsPath, "git", args) if err != nil { return "?" } diff --git a/pkg/git/repository.go b/pkg/git/repository.go index 0e57482..0c02f26 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -6,9 +6,8 @@ import ( "sync" "time" - "github.com/isacikgoz/gitbatch/pkg/helpers" log "github.com/sirupsen/logrus" - "gopkg.in/src-d/go-git.v4" + git "gopkg.in/src-d/go-git.v4" ) // RepoEntity is the main entity of the application. The repository name is @@ -82,7 +81,7 @@ func FastInitializeRepo(dir string) (e *RepoEntity, err error) { return nil, err } // initialize RepoEntity with minimum viable fields - e = &RepoEntity{RepoID: helpers.RandomString(8), + e = &RepoEntity{RepoID: RandomString(8), Name: fstat.Name(), AbsPath: dir, ModTime: fstat.ModTime(), diff --git a/pkg/git/util-random.go b/pkg/git/util-random.go new file mode 100644 index 0000000..c388613 --- /dev/null +++ b/pkg/git/util-random.go @@ -0,0 +1,18 @@ +package git + +import ( + "math/rand" + "time" +) + +var characterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") +var r = rand.New(rand.NewSource(time.Now().UnixNano())) + +// RandomString generates a random string of n length +func RandomString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = characterRunes[r.Intn(len(characterRunes))] + } + return string(b) +} diff --git a/pkg/git/util-random_test.go b/pkg/git/util-random_test.go new file mode 100644 index 0000000..2eeb00c --- /dev/null +++ b/pkg/git/util-random_test.go @@ -0,0 +1,11 @@ +package git + +import "testing" + +func TestRandomString(t *testing.T) { + stringLength := 8 + randString := RandomString(stringLength) + if len(randString) != stringLength { + t.Errorf("The length of the string should be equal.") + } +} diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index 869b334..6cd43bb 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -139,6 +139,9 @@ func (gui *Gui) pageDown(g *gocui.Gui, v *gocui.View) error { cx, _ := v.Cursor() _, vy := v.Size() lr := len(gui.State.Repositories) + if lr < vy { + return nil + } if oy+vy >= lr-vy { if err := v.SetOrigin(ox, lr-vy); err != nil { return err diff --git a/pkg/gui/util-common.go b/pkg/gui/util-common.go index 92c49b9..b25848c 100644 --- a/pkg/gui/util-common.go +++ b/pkg/gui/util-common.go @@ -1,7 +1,6 @@ package gui import ( - "github.com/isacikgoz/gitbatch/pkg/helpers" "github.com/jroimartin/gocui" log "github.com/sirupsen/logrus" ) @@ -94,7 +93,7 @@ func (gui *Gui) correctCursor(v *gocui.View) error { if oy+cy <= ly { return nil } - newCy := helpers.Min(ly, maxY) + newCy := min(ly, maxY) if err := v.SetCursor(cx, newCy); err != nil { return err } @@ -102,6 +101,14 @@ func (gui *Gui) correctCursor(v *gocui.View) error { return err } +// min finds the minimum value of two int +func min(x, y int) int { + if x < y { + return x + } + return y +} + // this function handles the iteration of a side view and set its origin point // so that the selected line can be in the middle of the view func (gui *Gui) smartAnchorRelativeToLine(v *gocui.View, currentindex, totallines int) error { diff --git a/pkg/helpers/command.go b/pkg/helpers/command.go deleted file mode 100644 index b861812..0000000 --- a/pkg/helpers/command.go +++ /dev/null @@ -1,51 +0,0 @@ -package helpers - -import ( - "log" - "os/exec" - "syscall" -) - -// RunCommandWithOutput runs the OS command and return its output. If the output -// returns error it also encapsulates it as a golang.error which is a return code -// of the command except zero -func RunCommandWithOutput(dir string, command string, args []string) (string, error) { - cmd := exec.Command(command, args...) - if dir != "" { - cmd.Dir = dir - } - output, err := cmd.Output() - return string(output), err -} - -// GetCommandStatus returns if we supposed to get return value as an int of a command -// this method can be used. It is practical when you use a command and process a -// failover acoording to a soecific return code -func GetCommandStatus(dir string, command string, args []string) (int, error) { - cmd := exec.Command(command, args...) - if dir != "" { - cmd.Dir = dir - } - var err error - // this time the execution is a little different - if err := cmd.Start(); err != nil { - return -1, err - } - if err := cmd.Wait(); err != nil { - if exiterr, ok := err.(*exec.ExitError); ok { - // The program has exited with an exit code != 0 - - // This works on both Unix and Windows. Although package - // syscall is generally platform dependent, WaitStatus is - // defined for both Unix and Windows and in both cases has - // an ExitStatus() method with the same signature. - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - statusCode := status.ExitStatus() - return statusCode, err - } - } else { - log.Fatalf("cmd.Wait: %v", err) - } - } - return -1, err -} diff --git a/pkg/helpers/utils.go b/pkg/helpers/utils.go deleted file mode 100644 index 35de2c3..0000000 --- a/pkg/helpers/utils.go +++ /dev/null @@ -1,36 +0,0 @@ -package helpers - -import ( - "math/rand" - "strings" - "time" -) - -var characterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") -var r = rand.New(rand.NewSource(time.Now().UnixNano())) - -// TrimTrailingNewline removes the trailing new line form a string. this method -// is used mostly on outputs of a command -func TrimTrailingNewline(str string) string { - if strings.HasSuffix(str, "\n") { - return str[:len(str)-1] - } - return str -} - -// Min finds the minimum value of two int -func Min(x, y int) int { - if x < y { - return x - } - return y -} - -// RandomString generates a random string of n length -func RandomString(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = characterRunes[r.Intn(len(characterRunes))] - } - return string(b) -} diff --git a/pkg/helpers/utils_test.go b/pkg/helpers/utils_test.go deleted file mode 100644 index a6a56a4..0000000 --- a/pkg/helpers/utils_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package helpers - -import "testing" - -func TestRandomString(t *testing.T) { - stringLength := 8 - randString := RandomString(stringLength) - if len(randString) != stringLength { - t.Errorf("The length of the string should be equal.") - } -} -- cgit v1.2.3 From 1ec4ef1e6a4a8f1f5a1f8bbb14381ea9e794f6fb Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Tue, 1 Jan 2019 16:12:35 +0300 Subject: refactor according to good practices --- pkg/app/app.go | 10 +++---- pkg/app/files.go | 47 ++++++++++++-------------------- pkg/app/quick.go | 2 ++ pkg/git/branch.go | 77 +++++++++++++++++++++++++++------------------------- pkg/git/cmd-fetch.go | 12 +++++++- pkg/git/cmd-pull.go | 14 +++++++++- 6 files changed, 89 insertions(+), 73 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 9c9e9a7..7d51abb 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -45,18 +45,18 @@ func Setup(setupConfig *SetupConfig) (*App, error) { x := appConfig.Mode == "fetch" y := appConfig.Mode == "pull" if x == y { - log.Fatal("Unrecognized quick mode: " + appConfig.Mode) + log.Error("Unrecognized quick mode: " + appConfig.Mode) + os.Exit(1) } quick(directories, appConfig.Depth, appConfig.Mode) - log.Fatal("Finished") + os.Exit(0) } // create a gui.Gui struct and set it as App's gui app.Gui, err = gui.NewGui(appConfig.Mode, directories) if err != nil { - // the error types and handling is not considered yer - log.Error(err) - return app, err + // the error types and handling is not considered yet + return nil, err } // hopefull everything went smooth as butter log.Trace("App configuration completed") diff --git a/pkg/app/files.go b/pkg/app/files.go index 25e5074..1ec05cd 100644 --- a/pkg/app/files.go +++ b/pkg/app/files.go @@ -4,20 +4,20 @@ import ( "io/ioutil" "os" "path/filepath" - "strings" log "github.com/sirupsen/logrus" ) // generateDirectories returns poosible git repositories to pipe into git pkg's // load function -func generateDirectories(directories []string, depth int) (gitDirectories []string) { +func generateDirectories(dirs []string, depth int) []string { + gitDirs := make([]string, 0) for i := 0; i <= depth; i++ { - nonrepos, repos := walkRecursive(directories, gitDirectories) - directories = nonrepos - gitDirectories = repos + nonrepos, repos := walkRecursive(dirs, gitDirs) + dirs = nonrepos + gitDirs = repos } - return gitDirectories + return gitDirs } // returns given values, first search directories and second stands for possible @@ -33,7 +33,7 @@ func walkRecursive(search, appendant []string) ([]string, []string) { if err != nil { log.WithFields(log.Fields{ "directory": search[i], - }).Trace("Can't read directory") + }).WithError(err).Trace("Can't read directory") continue } // since we started to search let's get rid of it and remove from search @@ -49,14 +49,16 @@ func walkRecursive(search, appendant []string) ([]string, []string) { // seperateDirectories is to find all the files in given path. This method // does not check if the given file is a valid git repositories -func seperateDirectories(directory string) (directories, gitDirectories []string, err error) { +func seperateDirectories(directory string) ([]string, []string, error) { + dirs := make([]string, 0) + gitDirs := make([]string, 0) files, err := ioutil.ReadDir(directory) // can we read the directory? if err != nil { log.WithFields(log.Fields{ "directory": directory, }).Trace("Can't read directory") - return directories, gitDirectories, nil + return nil, nil, nil } for _, f := range files { repo := directory + string(os.PathSeparator) + f.Name() @@ -66,38 +68,25 @@ func seperateDirectories(directory string) (directories, gitDirectories []string log.WithFields(log.Fields{ "file": file, "directory": repo, - }).Trace("Failed to open file in the directory") + }).WithError(err).Trace("Failed to open file in the directory") + file.Close() continue } dir, err := filepath.Abs(file.Name()) if err != nil { - return nil, nil, err + file.Close() + continue } // with this approach, we ignore submodule or sub repositoreis in a git repository ff, err := os.Open(dir + string(os.PathSeparator) + ".git") if err != nil { - directories = append(directories, dir) + dirs = append(dirs, dir) } else { - gitDirectories = append(gitDirectories, dir) + gitDirs = append(gitDirs, dir) } ff.Close() file.Close() } - return directories, gitDirectories, nil -} - -// takes a fileInfo slice and returns it with the ones matches with the -// pattern string -func filterDirectories(files []os.FileInfo, pattern string) []os.FileInfo { - var filteredRepos []os.FileInfo - for _, f := range files { - // it is just a simple filter - if strings.Contains(f.Name(), pattern) && f.Name() != ".git" { - filteredRepos = append(filteredRepos, f) - } else { - continue - } - } - return filteredRepos + return dirs, gitDirs, nil } diff --git a/pkg/app/quick.go b/pkg/app/quick.go index 488e1d6..90a2283 100644 --- a/pkg/app/quick.go +++ b/pkg/app/quick.go @@ -37,10 +37,12 @@ func operate(directory, mode string) error { case "fetch": return git.Fetch(r, git.FetchOptions{ RemoteName: "origin", + Progress: true, }) case "pull": return git.Pull(r, git.PullOptions{ RemoteName: "origin", + Progress: true, }) } return nil diff --git a/pkg/git/branch.go b/pkg/git/branch.go index 94ad578..084288a 100644 --- a/pkg/git/branch.go +++ b/pkg/git/branch.go @@ -38,40 +38,43 @@ func (e *RepoEntity) loadLocalBranches() error { } var branchFound bool bs.ForEach(func(b *plumbing.Reference) error { - if b.Type() == plumbing.HashReference { - var push, pull string - pushables, err := RevList(e, RevListOptions{ - Ref1: "@{u}", - Ref2: "HEAD", - }) - if err != nil { - push = pushables[0] - } else { - push = strconv.Itoa(len(pushables)) - } - pullables, err := RevList(e, RevListOptions{ - Ref1: "HEAD", - Ref2: "@{u}", - }) - if err != nil { - pull = pullables[0] - } else { - pull = strconv.Itoa(len(pullables)) - } - clean := e.isClean() - branch := &Branch{ - Name: b.Name().Short(), - Reference: b, - Pushables: push, - Pullables: pull, - Clean: clean, - } - if b.Name() == headRef.Name() { - e.Branch = branch - branchFound = true - } - lbs = append(lbs, branch) + if b.Type() != plumbing.HashReference { + return nil } + + var push, pull string + pushables, err := RevList(e, RevListOptions{ + Ref1: "@{u}", + Ref2: "HEAD", + }) + if err != nil { + push = pushables[0] + } else { + push = strconv.Itoa(len(pushables)) + } + pullables, err := RevList(e, RevListOptions{ + Ref1: "HEAD", + Ref2: "@{u}", + }) + if err != nil { + pull = pullables[0] + } else { + pull = strconv.Itoa(len(pullables)) + } + clean := e.isClean() + branch := &Branch{ + Name: b.Name().Short(), + Reference: b, + Pushables: push, + Pullables: pull, + Clean: clean, + } + if b.Name() == headRef.Name() { + e.Branch = branch + branchFound = true + } + lbs = append(lbs, branch) + return nil }) if !branchFound { @@ -112,8 +115,8 @@ func (e *RepoEntity) currentBranchIndex() int { // Checkout to given branch. If any errors occur, the method returns it instead // of returning nil -func (e *RepoEntity) Checkout(branch *Branch) error { - if branch.Name == e.Branch.Name { +func (e *RepoEntity) Checkout(b *Branch) error { + if b.Name == e.Branch.Name { return nil } @@ -123,7 +126,7 @@ func (e *RepoEntity) Checkout(branch *Branch) error { return err } if err = w.Checkout(&git.CheckoutOptions{ - Branch: branch.Reference.Name(), + Branch: b.Reference.Name(), }); err != nil { log.Warn("Cannot checkout " + err.Error()) return err @@ -131,7 +134,7 @@ func (e *RepoEntity) Checkout(branch *Branch) error { // make this conditional on global scale // we don't care if this function returns an error - e.Remote.SyncBranches(branch.Name) + e.Remote.SyncBranches(b.Name) return e.Refresh() } diff --git a/pkg/git/cmd-fetch.go b/pkg/git/cmd-fetch.go index 0830105..2c66c1c 100644 --- a/pkg/git/cmd-fetch.go +++ b/pkg/git/cmd-fetch.go @@ -1,10 +1,11 @@ package git import ( + "os" "strings" log "github.com/sirupsen/logrus" - "gopkg.in/src-d/go-git.v4" + git "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/plumbing/transport" "gopkg.in/src-d/go-git.v4/plumbing/transport/http" @@ -31,6 +32,8 @@ type FetchOptions struct { Prune bool // Show what would be done, without making any changes. DryRun bool + // Process logs the output to stdout + Progress bool // Force allows the fetch to update a local branch even when the remote // branch does not descend from it. Force bool @@ -122,6 +125,9 @@ func fetchWithGoGit(e *RepoEntity, options FetchOptions, refspec string) (err er return ErrInvalidAuthMethod } } + if options.Progress { + opt.Progress = os.Stdout + } if err := e.Repository.Fetch(opt); err != nil { if err == git.NoErrAlreadyUpToDate { @@ -137,6 +143,10 @@ func fetchWithGoGit(e *RepoEntity, options FetchOptions, refspec string) (err er } else { return err } + // TODO: submit a PR for this kind of error, this type of catch is lame + } else if strings.Contains(err.Error(), "SSH_AUTH_SOCK") { + // The env variable SSH_AUTH_SOCK is not defined, maybe git can handle this + return fetchWithGit(e, options) } else if err == transport.ErrAuthenticationRequired { log.Warn(err.Error()) return ErrAuthenticationRequired diff --git a/pkg/git/cmd-pull.go b/pkg/git/cmd-pull.go index f620925..8f2cf92 100644 --- a/pkg/git/cmd-pull.go +++ b/pkg/git/cmd-pull.go @@ -1,8 +1,11 @@ package git import ( + "os" + "strings" + log "github.com/sirupsen/logrus" - "gopkg.in/src-d/go-git.v4" + git "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/transport" "gopkg.in/src-d/go-git.v4/plumbing/transport/http" @@ -28,6 +31,8 @@ type PullOptions struct { SingleBranch bool // Credentials holds the user and pswd information Credentials Credentials + // Process logs the output to stdout + Progress bool // Force allows the pull to update a local branch even when the remote // branch does not descend from it. Force bool @@ -94,6 +99,9 @@ func pullWithGoGit(e *RepoEntity, options PullOptions) (err error) { return ErrInvalidAuthMethod } } + if options.Progress { + opt.Progress = os.Stdout + } w, err := e.Repository.Worktree() if err != nil { return err @@ -103,6 +111,10 @@ func pullWithGoGit(e *RepoEntity, options PullOptions) (err error) { 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(), "SSH_AUTH_SOCK") { + // The env variable SSH_AUTH_SOCK is not defined, maybe git can handle this + return pullWithGit(e, options) } else if err == transport.ErrAuthenticationRequired { log.Warn(err.Error()) return ErrAuthenticationRequired -- cgit v1.2.3 From 6bbe6c3968040153f08ea9835889e151971466d7 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 2 Jan 2019 22:34:19 +0300 Subject: add failover to pull/fetch --- pkg/git/cmd-fetch.go | 2 +- pkg/git/cmd-pull.go | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/git/cmd-fetch.go b/pkg/git/cmd-fetch.go index 2c66c1c..6090f7c 100644 --- a/pkg/git/cmd-fetch.go +++ b/pkg/git/cmd-fetch.go @@ -152,7 +152,7 @@ func fetchWithGoGit(e *RepoEntity, options FetchOptions, refspec string) (err er return ErrAuthenticationRequired } else { log.Warn(err.Error()) - return err + return fetchWithGit(e, options) } } diff --git a/pkg/git/cmd-pull.go b/pkg/git/cmd-pull.go index 8f2cf92..0c3954e 100644 --- a/pkg/git/cmd-pull.go +++ b/pkg/git/cmd-pull.go @@ -9,6 +9,7 @@ import ( "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/transport" "gopkg.in/src-d/go-git.v4/plumbing/transport/http" + "gopkg.in/src-d/go-git.v4/storage/memory" ) var ( @@ -109,9 +110,19 @@ func pullWithGoGit(e *RepoEntity, options PullOptions) (err error) { if err = w.Pull(opt); err != nil { if err == git.NoErrAlreadyUpToDate { + // log.Error("error: " + err.Error()) // Already up-to-date log.Warn(err.Error()) // TODO: submit a PR for this kind of error, this type of catch is lame + } else if err == memory.ErrRefHasChanged && pullTryCount < pullMaxTry { + pullTryCount++ + log.Error("trying to fetch") + if err := Fetch(e, FetchOptions{ + RemoteName: options.RemoteName, + }); err != nil { + return err + } + return Pull(e, options) } else if strings.Contains(err.Error(), "SSH_AUTH_SOCK") { // The env variable SSH_AUTH_SOCK is not defined, maybe git can handle this return pullWithGit(e, options) @@ -120,9 +131,10 @@ func pullWithGoGit(e *RepoEntity, options PullOptions) (err error) { return ErrAuthenticationRequired } else { log.Warn(err.Error()) - return err + return pullWithGit(e, options) } } + e.SetState(Success) return e.Refresh() } -- cgit v1.2.3 From 5123680dcee84beed8b1d98735a1ec7135c60949 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Thu, 3 Jan 2019 00:44:23 +0300 Subject: superfast job handling and failover for auth added --- pkg/git/job-queue.go | 28 +++++++++++++++++++++++++--- pkg/gui/authenticationview.go | 11 +---------- pkg/gui/gui.go | 16 +++++++++------- pkg/gui/keybindings.go | 8 ++++++++ pkg/gui/mainview.go | 40 ++++++++++++++++++++++------------------ pkg/gui/util-textstyle.go | 9 ++++----- 6 files changed, 69 insertions(+), 43 deletions(-) diff --git a/pkg/git/job-queue.go b/pkg/git/job-queue.go index 3eee605..e6e6f0c 100644 --- a/pkg/git/job-queue.go +++ b/pkg/git/job-queue.go @@ -2,6 +2,7 @@ package git import ( "errors" + "sync" ) // JobQueue holds the slice of Jobs @@ -66,13 +67,34 @@ func (jq *JobQueue) RemoveFromQueue(entity *RepoEntity) error { // IsInTheQueue function; since the job and entity is not tied with its own // struct, this function returns true if that entity is in the queue along with // the jobs type -func (jq *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, jt JobType) { +func (jq *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, j *Job) { inTheQueue = false for _, job := range jq.series { if job.Entity.RepoID == entity.RepoID { inTheQueue = true - jt = job.JobType + j = job } } - return inTheQueue, jt + return inTheQueue, j +} + +// StartJobsAsync start he jobs in the queue asynchronously +func (jq *JobQueue) StartJobsAsync() map[*Job]error { + fails := make(map[*Job]error) + var wg sync.WaitGroup + var mx sync.Mutex + for range jq.series { + wg.Add(1) + go func() { + defer wg.Done() + j, _, err := jq.StartNext() + if err != nil { + mx.Lock() + fails[j] = err + mx.Unlock() + } + }() + } + wg.Wait() + return fails } diff --git a/pkg/gui/authenticationview.go b/pkg/gui/authenticationview.go index a274425..7d4861f 100644 --- a/pkg/gui/authenticationview.go +++ b/pkg/gui/authenticationview.go @@ -125,16 +125,7 @@ func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error { return err } - if err := gui.closeAuthenticationView(g, v); err != nil { - return err // should return?? - } - - vReturn, err := g.View(authenticationReturnView) - if err != nil { - return err // should return?? - } - - return gui.startQueue(g, vReturn) + return gui.closeAuthenticationView(g, v) } // open an error view to inform user with a message and a useful note diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index b19772b..6d5de19 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -21,10 +21,11 @@ type Gui struct { // guiState struct holds the repositories, directiories, mode and queue of the // gui object. These values are not static type guiState struct { - Repositories []*git.RepoEntity - Directories []string - Mode mode - Queue *git.JobQueue + Repositories []*git.RepoEntity + Directories []string + Mode mode + Queue *git.JobQueue + FailoverQueue *git.JobQueue } // this struct encapsulates the name and title of a view. the name of a view is @@ -77,9 +78,10 @@ var ( // NewGui creates a Gui opject and fill it's state related entites func NewGui(mode string, directoies []string) (*Gui, error) { initialState := guiState{ - Directories: directoies, - Mode: fetchMode, - Queue: git.CreateJobQueue(), + Directories: directoies, + Mode: fetchMode, + Queue: git.CreateJobQueue(), + FailoverQueue: git.CreateJobQueue(), } gui := &Gui{ State: initialState, diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 7aaee56..668fe90 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -309,6 +309,14 @@ func (gui *Gui) generateKeybindings() error { }, // Main view controls { + View: mainViewFeature.Name, + Key: 'u', + Modifier: gocui.ModNone, + Handler: gui.submitCredentials, + Display: "u", + Description: "Submit Credentials", + Vital: false, + }, { View: mainViewFeature.Name, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index 6cd43bb..8ee31ba 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -231,29 +231,33 @@ func (gui *Gui) removeFromQueue(entity *git.RepoEntity) error { // this function starts the queue and updates the gui with the result of an // operation func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error { - go func(gui_go *Gui, g_go *gocui.Gui) { - for { - job, finished, err := gui_go.State.Queue.StartNext() + go func(gui_go *Gui) { + fails := gui_go.State.Queue.StartJobsAsync() + gui_go.State.Queue = git.CreateJobQueue() + for j, err := range fails { + if err == git.ErrAuthenticationRequired { + j.Entity.SetState(git.Paused) + gui_go.State.FailoverQueue.AddJob(j) + } + } + }(gui) + return nil +} +func (gui *Gui) submitCredentials(g *gocui.Gui, v *gocui.View) error { + if is, j := gui.State.FailoverQueue.IsInTheQueue(gui.getSelectedRepository()); is { + if j.Entity.State() == git.Paused { + gui.State.FailoverQueue.RemoveFromQueue(j.Entity) + err := gui.openAuthenticationView(g, gui.State.Queue, j, v.Name()) if err != nil { - if err == git.ErrAuthenticationRequired { - // pause the job, so it will be indicated to being blocking - job.Entity.SetState(git.Paused) - err := gui_go.openAuthenticationView(g, gui_go.State.Queue, job, v.Name()) - if err != nil { - log.Warn(err.Error()) - return - } - } - return - // with not returning here, we simply ignore and continue + log.Warn(err.Error()) + return err } - // if queue is finished simply return from this goroutine - if finished { - return + if isnt, _ := gui.State.Queue.IsInTheQueue(j.Entity); !isnt { + gui.State.FailoverQueue.AddJob(j) } } - }(gui, g) + } return nil } diff --git a/pkg/gui/util-textstyle.go b/pkg/gui/util-textstyle.go index 95c8de7..1bb3635 100644 --- a/pkg/gui/util-textstyle.go +++ b/pkg/gui/util-textstyle.go @@ -84,8 +84,8 @@ func (gui *Gui) repositoryLabel(e *git.RepoEntity) string { var suffix string // rendering the satus according to repository's state if e.State() == git.Queued { - if inQueue, ty := gui.State.Queue.IsInTheQueue(e); inQueue { - switch mode := ty; mode { + if inQueue, j := gui.State.Queue.IsInTheQueue(e); inQueue { + switch mode := j.JobType; mode { case git.FetchJob: suffix = blue.Sprint(queuedSymbol) case git.PullJob: @@ -103,12 +103,11 @@ func (gui *Gui) repositoryLabel(e *git.RepoEntity) string { } else if e.State() == git.Success { return prefix + repoName + ws + green.Sprint(successSymbol) } else if e.State() == git.Paused { - return prefix + repoName + ws + yellow.Sprint(pauseSymbol) + return prefix + repoName + ws + yellow.Sprint("auth required (u)") } else if e.State() == git.Fail { return prefix + repoName + ws + red.Sprint(failSymbol) - } else { - return prefix + repoName } + return prefix + repoName } func commitLabel(c *git.Commit) string { -- cgit v1.2.3 From f2f2c1bb18b2f7236f5ede917b1eacdd422cbd5b Mon Sep 17 00:00:00 2001 From: İbrahim Serdar Açıkgöz Date: Thu, 3 Jan 2019 18:04:25 +0300 Subject: error messages and semaphores added --- pkg/git/cmd-pull.go | 5 ++--- pkg/git/cmd-rev-list.go | 2 +- pkg/git/cmd.go | 4 ++-- pkg/git/job-queue.go | 36 +++++++++++++++++++++++++++++++----- pkg/git/job.go | 3 +++ pkg/git/repository.go | 10 ++++++++++ pkg/git/util-errors.go | 45 ++++++++++++++++++++++++++++++++++++++++----- pkg/gui/util-textstyle.go | 4 ++-- 8 files changed, 91 insertions(+), 18 deletions(-) diff --git a/pkg/git/cmd-pull.go b/pkg/git/cmd-pull.go index 0c3954e..769d6b5 100644 --- a/pkg/git/cmd-pull.go +++ b/pkg/git/cmd-pull.go @@ -67,9 +67,8 @@ func pullWithGit(e *RepoEntity, options PullOptions) (err error) { if options.Force { args = append(args, "-f") } - if err := GenericGitCommand(e.AbsPath, args); err != nil { - log.Warn("Error at git command (pull)") - return err + if out, err := GenericGitCommandWithOutput(e.AbsPath, args); err != nil { + return parseGitError(out, err) } e.SetState(Success) return e.Refresh() diff --git a/pkg/git/cmd-rev-list.go b/pkg/git/cmd-rev-list.go index e68e82a..624e290 100644 --- a/pkg/git/cmd-rev-list.go +++ b/pkg/git/cmd-rev-list.go @@ -29,7 +29,7 @@ func RevList(e *RepoEntity, options RevListOptions) ([]string, error) { out, err := GenericGitCommandWithOutput(e.AbsPath, args) if err != nil { log.Warn("Error while rev-list command") - return []string{out}, err + return []string{"?"}, err } hashes := strings.Split(out, "\n") for _, hash := range hashes { diff --git a/pkg/git/cmd.go b/pkg/git/cmd.go index 05d030c..b177b88 100644 --- a/pkg/git/cmd.go +++ b/pkg/git/cmd.go @@ -15,7 +15,7 @@ func RunCommandWithOutput(dir string, command string, args []string) (string, er if dir != "" { cmd.Dir = dir } - output, err := cmd.Output() + output, err := cmd.CombinedOutput() return string(output), err } @@ -73,7 +73,7 @@ func GenericGitCommand(repoPath string, args []string) error { func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) { out, err := RunCommandWithOutput(repoPath, "git", args) if err != nil { - return "?", err + return out, err } return TrimTrailingNewline(out), nil } diff --git a/pkg/git/job-queue.go b/pkg/git/job-queue.go index e6e6f0c..975b8af 100644 --- a/pkg/git/job-queue.go +++ b/pkg/git/job-queue.go @@ -1,8 +1,13 @@ package git import ( + "context" "errors" + "runtime" "sync" + + log "github.com/sirupsen/logrus" + "golang.org/x/sync/semaphore" ) // JobQueue holds the slice of Jobs @@ -80,13 +85,26 @@ func (jq *JobQueue) IsInTheQueue(entity *RepoEntity) (inTheQueue bool, j *Job) { // StartJobsAsync start he jobs in the queue asynchronously func (jq *JobQueue) StartJobsAsync() map[*Job]error { - fails := make(map[*Job]error) - var wg sync.WaitGroup + + ctx := context.TODO() + + var ( + maxWorkers = runtime.GOMAXPROCS(0) + sem = semaphore.NewWeighted(int64(maxWorkers)) + fails = make(map[*Job]error) + ) + var mx sync.Mutex for range jq.series { - wg.Add(1) + + if err := sem.Acquire(ctx, 1); err != nil { + log.Errorf("Failed to acquire semaphore: %v", err) + break + } + go func() { - defer wg.Done() + + defer sem.Release(1) j, _, err := jq.StartNext() if err != nil { mx.Lock() @@ -95,6 +113,14 @@ func (jq *JobQueue) StartJobsAsync() map[*Job]error { } }() } - wg.Wait() + + // Acquire all of the tokens to wait for any remaining workers to finish. + // + // If you are already waiting for the workers by some other means (such as an + // errgroup.Group), you can omit this final Acquire call. + if err := sem.Acquire(ctx, int64(maxWorkers)); err != nil { + log.Errorf("Failed to acquire semaphore: %v", err) + } + return fails } diff --git a/pkg/git/job.go b/pkg/git/job.go index 85c28f3..82a14f6 100644 --- a/pkg/git/job.go +++ b/pkg/git/job.go @@ -41,6 +41,7 @@ func (j *Job) start() error { } if err := Fetch(j.Entity, opts); err != nil { j.Entity.SetState(Fail) + j.Entity.SetStateMessage(err.Error()) return err } case PullJob: @@ -54,6 +55,7 @@ func (j *Job) start() error { } if err := Pull(j.Entity, opts); err != nil { j.Entity.SetState(Fail) + j.Entity.SetStateMessage(err.Error()) return err } case MergeJob: @@ -61,6 +63,7 @@ func (j *Job) start() error { BranchName: j.Entity.Remote.Branch.Name, }); err != nil { j.Entity.SetState(Fail) + j.Entity.SetStateMessage(err.Error()) return nil } default: diff --git a/pkg/git/repository.go b/pkg/git/repository.go index 0c02f26..31b926d 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -28,6 +28,9 @@ type RepoEntity struct { Stasheds []*StashedItem state RepoState + // TODO: move this into state + Message string + mutex *sync.RWMutex listeners map[string][]RepositoryListener } @@ -210,3 +213,10 @@ func (e *RepoEntity) SetState(state RepoState) { log.Warnf("Cannot publish on %s topic.\n", RepositoryUpdated) } } + +// SetMessage sets the message of status, it is used if state is Fail +func (e *RepoEntity) SetStateMessage(msg string) { + if e.State() == Fail { + e.Message = msg + } +} diff --git a/pkg/git/util-errors.go b/pkg/git/util-errors.go index 8621cbc..33eb15f 100644 --- a/pkg/git/util-errors.go +++ b/pkg/git/util-errors.go @@ -2,23 +2,58 @@ package git import ( "errors" + "strings" ) var ( // ErrGitCommand is thrown when git command returned an error code - ErrGitCommand = errors.New("Git command returned 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") + ErrAuthenticationRequired = errors.New("authentication required") // ErrAuthorizationFailed is thrown when authorization failed while trying // to authenticate with remote - ErrAuthorizationFailed = errors.New("Authorization failed") + 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") + 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") + ErrCouldNotFindRemoteRef = errors.New("could not find remote ref") + // ErrPullAbortedTryCommit indicates that the repositort is not clean and + // some changes may conflict with the merge + ErrMergeAbortedTryCommit = errors.New("stash/commit changes. aborted") + // ErrRemoteBranchNotSpecified means that default remote branch is not set + // for the current branch. can be setted with "git config --local --add + // branch..remote= " + ErrRemoteBranchNotSpecified = errors.New("upstream not set") + // ErrRemoteNotFound is thrown when the remote is not reachable. It may be + // caused by the deletion of the remote or coneectivty problems + ErrRemoteNotFound = errors.New("remote not found") + // ErrConflictAfterMerge is thrown when a conflict occurs at merging two + // references + ErrConflictAfterMerge = errors.New("conflict while merging") + // ErrUnmergedFiles possibly occurs after a conflict + ErrUnmergedFiles = errors.New("unmerged files detected") + // ErrUnclassified is unconsidered error type + ErrUnclassified = errors.New("unclassified error") ) + +// parseGitError takes git output as an input and tries to find some meaningful +// errors can be used by the app +func parseGitError(out string, err error) error { + if strings.Contains(out, "error: Your local changes to the following files would be overwritten by merge") { + return ErrMergeAbortedTryCommit + } else if strings.Contains(out, "ERROR: Repository not found") { + return ErrRemoteNotFound + } else if strings.Contains(out, "for your current branch, you must specify a branch on the command line") { + return ErrRemoteBranchNotSpecified + } else if strings.Contains(out, "Automatic merge failed; fix conflicts and then commit the result") { + return ErrConflictAfterMerge + } else if strings.Contains(out, "error: Pulling is not possible because you have unmerged files.") { + return ErrUnmergedFiles + } + return ErrUnclassified +} diff --git a/pkg/gui/util-textstyle.go b/pkg/gui/util-textstyle.go index 1bb3635..c42857b 100644 --- a/pkg/gui/util-textstyle.go +++ b/pkg/gui/util-textstyle.go @@ -103,9 +103,9 @@ func (gui *Gui) repositoryLabel(e *git.RepoEntity) string { } else if e.State() == git.Success { return prefix + repoName + ws + green.Sprint(successSymbol) } else if e.State() == git.Paused { - return prefix + repoName + ws + yellow.Sprint("auth required (u)") + return prefix + repoName + ws + yellow.Sprint("authentication required (u)") } else if e.State() == git.Fail { - return prefix + repoName + ws + red.Sprint(failSymbol) + return prefix + repoName + ws + red.Sprint(failSymbol) + ws + red.Sprint(e.Message) } return prefix + repoName } -- cgit v1.2.3 From 4778c038850a98751401b55a042f2d3a0ff94d0b Mon Sep 17 00:00:00 2001 From: İbrahim Serdar Açıkgöz Date: Thu, 3 Jan 2019 18:30:01 +0300 Subject: async loading --- pkg/git/util-load.go | 41 +++++++++++++++++++++++++++++++++++++++++ pkg/gui/gui.go | 36 +++++++++++++++++------------------- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/pkg/git/util-load.go b/pkg/git/util-load.go index bf19de8..091716a 100644 --- a/pkg/git/util-load.go +++ b/pkg/git/util-load.go @@ -3,10 +3,16 @@ package git import ( log "github.com/sirupsen/logrus" + "context" "errors" + "runtime" "sync" + + "golang.org/x/sync/semaphore" ) +type AsyncAdd func(e *RepoEntity) + // 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 @@ -46,3 +52,38 @@ func LoadRepositoryEntities(directories []string) (entities []*RepoEntity, err e } return entities, nil } + +func LoadRepositoryEntitiesAsync(directories []string, add AsyncAdd) error { + ctx := context.TODO() + + var ( + maxWorkers = runtime.GOMAXPROCS(0) + sem = semaphore.NewWeighted(int64(maxWorkers)) + ) + + var mx sync.Mutex + for _, dir := range directories { + if err := sem.Acquire(ctx, 1); err != nil { + log.Errorf("Failed to acquire semaphore: %v", err) + break + } + + go func(d string) { + + defer sem.Release(1) + entity, err := InitializeRepo(d) + if err != nil { + log.WithFields(log.Fields{ + "directory": d, + }).Trace("Cannot load git repository.") + return + } + // lock so we don't get a race if multiple go routines try to add + // to the same entities + mx.Lock() + add(entity) + mx.Unlock() + }(dir) + } + return nil +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 6d5de19..120054f 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -113,15 +113,6 @@ func (gui *Gui) Run() error { g.InputEsc = true g.SetManagerFunc(gui.layout) - 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) - // start an async view apart from this loop to show loading screen go func(g_ui *Gui) { maxX, maxY := g.Size() @@ -138,17 +129,18 @@ func (gui *Gui) Run() error { log.Warn("Loading view cannot be focused.") return } - rs, err := git.LoadRepositoryEntities(g_ui.State.Directories) - if err != nil { - g.Close() - log.Fatal(err) - return - } - g_ui.State.Repositories = rs + go git.LoadRepositoryEntitiesAsync(g_ui.State.Directories, gui.addRepository) + // rs, err := git.LoadRepositoryEntities(g_ui.State.Directories) + // if err != nil { + // g.Close() + // log.Fatal(err) + // return + // } + // g_ui.State.Repositories = rs // add gui's repositoryUpdated func as an observer to repositories - for _, repo := range rs { - repo.On(git.RepositoryUpdated, gui.repositoryUpdated) - } + // for _, repo := range rs { + // repo.On(git.RepositoryUpdated, gui.repositoryUpdated) + // } gui.fillMain(g) }(gui) @@ -167,6 +159,12 @@ func (gui *Gui) Run() error { return nil } +func (gui *Gui) addRepository(e *git.RepoEntity) { + gui.State.Repositories = append(gui.State.Repositories, e) + e.On(git.RepositoryUpdated, gui.repositoryUpdated) + e.Refresh() +} + // set the layout and create views with their default size, name etc. values // TODO: window sizes can be handled better func (gui *Gui) layout(g *gocui.Gui) error { -- cgit v1.2.3 From 9ce00feb165a537799f0172aa3486d27863cd7ed Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Fri, 4 Jan 2019 00:41:15 +0300 Subject: remove unnecessary code --- pkg/gui/gui.go | 37 +++++-------------------------------- pkg/gui/mainview.go | 24 ------------------------ 2 files changed, 5 insertions(+), 56 deletions(-) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 120054f..2a9fb8b 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -1,7 +1,6 @@ package gui import ( - "fmt" "sync" "github.com/isacikgoz/gitbatch/pkg/git" @@ -113,36 +112,7 @@ func (gui *Gui) Run() error { g.InputEsc = true g.SetManagerFunc(gui.layout) - // start an async view apart from this loop to show loading screen - go func(g_ui *Gui) { - maxX, maxY := g.Size() - // TODO: view size can be handled in a more smart way - v, err := g.SetView(loadingViewFeature.Name, maxX/2-10, maxY/2-1, maxX/2+10, maxY/2+1) - if err != nil { - if err != gocui.ErrUnknownView { - log.Warn("Loading view cannot be created.") - return - } - fmt.Fprintln(v, "Loading...") - } - if _, err := g.SetCurrentView(loadingViewFeature.Name); err != nil { - log.Warn("Loading view cannot be focused.") - return - } - go git.LoadRepositoryEntitiesAsync(g_ui.State.Directories, gui.addRepository) - // rs, err := git.LoadRepositoryEntities(g_ui.State.Directories) - // if err != nil { - // g.Close() - // log.Fatal(err) - // return - // } - // g_ui.State.Repositories = rs - // add gui's repositoryUpdated func as an observer to repositories - // for _, repo := range rs { - // repo.On(git.RepositoryUpdated, gui.repositoryUpdated) - // } - gui.fillMain(g) - }(gui) + go git.LoadRepositoryEntitiesAsync(gui.State.Directories, gui.addRepository) if err := gui.generateKeybindings(); err != nil { log.Error("Keybindings could not be created.") @@ -162,7 +132,7 @@ func (gui *Gui) Run() error { func (gui *Gui) addRepository(e *git.RepoEntity) { gui.State.Repositories = append(gui.State.Repositories, e) e.On(git.RepositoryUpdated, gui.repositoryUpdated) - e.Refresh() + gui.repositoryUpdated(nil) } // set the layout and create views with their default size, name etc. values @@ -175,6 +145,9 @@ func (gui *Gui) layout(g *gocui.Gui) error { } v.Title = mainViewFeature.Title v.Overwrite = true + if _, err := gui.setCurrentViewOnTop(g, mainViewFeature.Name); err != nil { + return err + } } if v, err := g.SetView(remoteViewFeature.Name, int(0.55*float32(maxX)), 0, maxX-1, int(0.10*float32(maxY))); err != nil { if err != gocui.ErrUnknownView { diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index 8ee31ba..bcf58ed 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -9,30 +9,6 @@ import ( log "github.com/sirupsen/logrus" ) -// this is the initial function for filling the values for the main view. the -// function waits a separate routine to fill the gui's repository slice -func (gui *Gui) fillMain(g *gocui.Gui) error { - g.Update(func(g *gocui.Gui) error { - v, err := g.View(mainViewFeature.Name) - if err != nil { - return err - } - - // if there is still a loading screen we better get rid of it - if err := g.DeleteView(loadingViewFeature.Name); err != nil { - return err - } - if _, err := gui.setCurrentViewOnTop(g, mainViewFeature.Name); err != nil { - return err - } - - // Sort by name is default behavior as expected, so it handles initial - // rendering of the main view - return gui.sortByName(g, v) - }) - return nil -} - // refresh the main view and re-render the repository representations func (gui *Gui) renderMain() error { gui.mutex.Lock() -- cgit v1.2.3 From b4547c56f3c28f0d03ee9437ec93c0e6fb6e0331 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Fri, 4 Jan 2019 03:24:46 +0300 Subject: huge refactor, package layour re-organized --- app/app.go | 111 +++++++ app/config.go | 123 ++++++++ app/files.go | 92 ++++++ app/quick.go | 50 ++++ core/command/cmd-add.go | 89 ++++++ core/command/cmd-commit.go | 92 ++++++ core/command/cmd-config.go | 106 +++++++ core/command/cmd-diff.go | 90 ++++++ core/command/cmd-fetch.go | 163 ++++++++++ core/command/cmd-merge.go | 39 +++ core/command/cmd-pull.go | 141 +++++++++ core/command/cmd-reset.go | 136 +++++++++ core/command/cmd-status.go | 94 ++++++ core/command/cmd.go | 98 ++++++ core/command/file.go | 92 ++++++ core/errors/util-errors.go | 59 ++++ core/git/authentication.go | 30 ++ core/git/branch.go | 209 +++++++++++++ core/git/commit.go | 179 +++++++++++ core/git/random.go | 18 ++ core/git/random_test.go | 11 + core/git/remote.go | 80 +++++ core/git/remotebranch.go | 90 ++++++ core/git/repository.go | 222 ++++++++++++++ core/git/sort.go | 59 ++++ core/git/stash.go | 127 ++++++++ core/job/job-queue.go | 127 ++++++++ core/job/job.go | 79 +++++ core/load/load.go | 92 ++++++ gui/authenticationview.go | 188 ++++++++++++ gui/commitview.go | 189 ++++++++++++ gui/controlsview.go | 36 +++ gui/diffview.go | 127 ++++++++ gui/errorview.go | 36 +++ gui/gui.go | 211 +++++++++++++ gui/keybindings.go | 675 ++++++++++++++++++++++++++++++++++++++++++ gui/mainview.go | 302 +++++++++++++++++++ gui/sideviews.go | 232 +++++++++++++++ gui/stagedview.go | 73 +++++ gui/stashview.go | 84 ++++++ gui/statusview.go | 162 ++++++++++ gui/unstagedview.go | 66 +++++ gui/util-common.go | 198 +++++++++++++ gui/util-textstyle.go | 186 ++++++++++++ main.go | 2 +- pkg/app/app.go | 111 ------- pkg/app/config.go | 123 -------- pkg/app/files.go | 92 ------ pkg/app/quick.go | 49 --- pkg/git/authentication.go | 30 -- pkg/git/branch.go | 159 ---------- pkg/git/cmd-add.go | 88 ------ pkg/git/cmd-commit.go | 91 ------ pkg/git/cmd-config.go | 105 ------- pkg/git/cmd-diff.go | 89 ------ pkg/git/cmd-fetch.go | 162 ---------- pkg/git/cmd-merge.go | 39 --- pkg/git/cmd-pull.go | 139 --------- pkg/git/cmd-reset.go | 135 --------- pkg/git/cmd-rev-list.go | 42 --- pkg/git/cmd-stash.go | 118 -------- pkg/git/cmd-status.go | 92 ------ pkg/git/cmd.go | 138 --------- pkg/git/commit.go | 142 --------- pkg/git/file.go | 52 ---- pkg/git/job-queue.go | 126 -------- pkg/git/job.go | 74 ----- pkg/git/remote.go | 80 ----- pkg/git/remotebranch.go | 90 ------ pkg/git/repository.go | 222 -------------- pkg/git/util-errors.go | 59 ---- pkg/git/util-load.go | 89 ------ pkg/git/util-random.go | 18 -- pkg/git/util-random_test.go | 11 - pkg/git/util-sort.go | 98 ------ pkg/gui/authenticationview.go | 186 ------------ pkg/gui/commitview.go | 189 ------------ pkg/gui/controlsview.go | 36 --- pkg/gui/diffview.go | 129 -------- pkg/gui/errorview.go | 36 --- pkg/gui/gui.go | 209 ------------- pkg/gui/keybindings.go | 675 ------------------------------------------ pkg/gui/mainview.go | 300 ------------------- pkg/gui/sideviews.go | 231 --------------- pkg/gui/stagedview.go | 73 ----- pkg/gui/stashview.go | 84 ------ pkg/gui/statusview.go | 164 ---------- pkg/gui/unstagedview.go | 66 ----- pkg/gui/util-common.go | 198 ------------- pkg/gui/util-textstyle.go | 185 ------------ 90 files changed, 5664 insertions(+), 5625 deletions(-) create mode 100644 app/app.go create mode 100644 app/config.go create mode 100644 app/files.go create mode 100644 app/quick.go create mode 100644 core/command/cmd-add.go create mode 100644 core/command/cmd-commit.go create mode 100644 core/command/cmd-config.go create mode 100644 core/command/cmd-diff.go create mode 100644 core/command/cmd-fetch.go create mode 100644 core/command/cmd-merge.go create mode 100644 core/command/cmd-pull.go create mode 100644 core/command/cmd-reset.go create mode 100644 core/command/cmd-status.go create mode 100644 core/command/cmd.go create mode 100644 core/command/file.go create mode 100644 core/errors/util-errors.go create mode 100644 core/git/authentication.go create mode 100644 core/git/branch.go create mode 100644 core/git/commit.go create mode 100644 core/git/random.go create mode 100644 core/git/random_test.go create mode 100644 core/git/remote.go create mode 100644 core/git/remotebranch.go create mode 100644 core/git/repository.go create mode 100644 core/git/sort.go create mode 100644 core/git/stash.go create mode 100644 core/job/job-queue.go create mode 100644 core/job/job.go create mode 100644 core/load/load.go create mode 100644 gui/authenticationview.go create mode 100644 gui/commitview.go create mode 100644 gui/controlsview.go create mode 100644 gui/diffview.go create mode 100644 gui/errorview.go create mode 100644 gui/gui.go create mode 100644 gui/keybindings.go create mode 100644 gui/mainview.go create mode 100644 gui/sideviews.go create mode 100644 gui/stagedview.go create mode 100644 gui/stashview.go create mode 100644 gui/statusview.go create mode 100644 gui/unstagedview.go create mode 100644 gui/util-common.go create mode 100644 gui/util-textstyle.go delete mode 100644 pkg/app/app.go delete mode 100644 pkg/app/config.go delete mode 100644 pkg/app/files.go delete mode 100644 pkg/app/quick.go delete mode 100644 pkg/git/authentication.go delete mode 100644 pkg/git/branch.go delete mode 100644 pkg/git/cmd-add.go delete mode 100644 pkg/git/cmd-commit.go delete mode 100644 pkg/git/cmd-config.go delete mode 100644 pkg/git/cmd-diff.go delete mode 100644 pkg/git/cmd-fetch.go delete mode 100644 pkg/git/cmd-merge.go delete mode 100644 pkg/git/cmd-pull.go delete mode 100644 pkg/git/cmd-reset.go delete mode 100644 pkg/git/cmd-rev-list.go delete mode 100644 pkg/git/cmd-stash.go delete mode 100644 pkg/git/cmd-status.go delete mode 100644 pkg/git/cmd.go delete mode 100644 pkg/git/commit.go delete mode 100644 pkg/git/file.go delete mode 100644 pkg/git/job-queue.go delete mode 100644 pkg/git/job.go delete mode 100644 pkg/git/remote.go delete mode 100644 pkg/git/remotebranch.go delete mode 100644 pkg/git/repository.go delete mode 100644 pkg/git/util-errors.go delete mode 100644 pkg/git/util-load.go delete mode 100644 pkg/git/util-random.go delete mode 100644 pkg/git/util-random_test.go delete mode 100644 pkg/git/util-sort.go delete mode 100644 pkg/gui/authenticationview.go delete mode 100644 pkg/gui/commitview.go delete mode 100644 pkg/gui/controlsview.go delete mode 100644 pkg/gui/diffview.go delete mode 100644 pkg/gui/errorview.go delete mode 100644 pkg/gui/gui.go delete mode 100644 pkg/gui/keybindings.go delete mode 100644 pkg/gui/mainview.go delete mode 100644 pkg/gui/sideviews.go delete mode 100644 pkg/gui/stagedview.go delete mode 100644 pkg/gui/stashview.go delete mode 100644 pkg/gui/statusview.go delete mode 100644 pkg/gui/unstagedview.go delete mode 100644 pkg/gui/util-common.go delete mode 100644 pkg/gui/util-textstyle.go diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..b8eebc8 --- /dev/null +++ b/app/app.go @@ -0,0 +1,111 @@ +package app + +import ( + "os" + + "github.com/isacikgoz/gitbatch/gui" + log "github.com/sirupsen/logrus" +) + +// The App struct is responsible to hold app-wide related entities. Currently +// it has only the gui.Gui pointer for interface entity. +type App struct { + Gui *gui.Gui + Config *SetupConfig +} + +// SetupConfig is an assembler data to initiate a setup +type SetupConfig struct { + Directories []string + LogLevel string + Depth int + QuickMode bool + Mode string +} + +// Setup will handle pre-required operations. It is designed to be a wrapper for +// main method right now. +func Setup(setupConfig *SetupConfig) (*App, error) { + // initiate the app and give it initial values + app := &App{} + if len(setupConfig.Directories) <= 0 { + d, _ := os.Getwd() + setupConfig.Directories = []string{d} + } + + appConfig, err := overrideDefaults(setupConfig) + if err != nil { + return nil, err + } + + setLogLevel(appConfig.LogLevel) + directories := generateDirectories(appConfig.Directories, appConfig.Depth) + + if appConfig.QuickMode { + x := appConfig.Mode == "fetch" + y := appConfig.Mode == "pull" + if x == y { + log.Error("Unrecognized quick mode: " + appConfig.Mode) + os.Exit(1) + } + quick(directories, appConfig.Depth, appConfig.Mode) + os.Exit(0) + } + + // create a gui.Gui struct and set it as App's gui + app.Gui, err = gui.NewGui(appConfig.Mode, directories) + if err != nil { + // the error types and handling is not considered yet + return nil, err + } + // hopefull everything went smooth as butter + log.Trace("App configuration completed") + return app, nil +} + +// Close function will handle if any cleanup is required. e.g. closing streams +// or cleaning temproray files so on and so forth +func (app *App) Close() error { + return nil +} + +// set the level of logging it is fatal by default +func setLogLevel(logLevel string) { + switch logLevel { + case "trace": + log.SetLevel(log.TraceLevel) + case "debug": + log.SetLevel(log.DebugLevel) + case "info": + log.SetLevel(log.InfoLevel) + case "warn": + log.SetLevel(log.WarnLevel) + case "error": + log.SetLevel(log.ErrorLevel) + default: + log.SetLevel(log.FatalLevel) + } + log.WithFields(log.Fields{ + "level": logLevel, + }).Trace("logging level has been set") +} + +func overrideDefaults(setupConfig *SetupConfig) (appConfig *SetupConfig, err error) { + appConfig, err = LoadConfiguration() + if len(setupConfig.Directories) > 0 { + appConfig.Directories = setupConfig.Directories + } + if len(setupConfig.LogLevel) > 0 { + appConfig.LogLevel = setupConfig.LogLevel + } + if setupConfig.Depth > 0 { + appConfig.Depth = setupConfig.Depth + } + if setupConfig.QuickMode { + appConfig.QuickMode = setupConfig.QuickMode + } + if len(setupConfig.Mode) > 0 { + appConfig.Mode = setupConfig.Mode + } + return appConfig, err +} diff --git a/app/config.go b/app/config.go new file mode 100644 index 0000000..57c2e6e --- /dev/null +++ b/app/config.go @@ -0,0 +1,123 @@ +package app + +import ( + "os" + "path/filepath" + "runtime" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +// config file stuff +var ( + configFileName = "config" + configFileExt = ".yml" + configType = "yaml" + appName = "gitbatch" + + configurationDirectory = filepath.Join(osConfigDirectory(), appName) + configFileAbsPath = filepath.Join(configurationDirectory, configFileName) +) + +// configuration items +var ( + modeKey = "mode" + modeKeyDefault = "fetch" + pathsKey = "paths" + pathsKeyDefault = []string{"."} + logLevelKey = "loglevel" + logLevelKeyDefault = "error" + qucikKey = "quick" + qucikKeyDefault = false + recursionKey = "recursion" + recursionKeyDefault = 1 +) + +// LoadConfiguration returns a Config struct is filled +func LoadConfiguration() (*SetupConfig, error) { + if err := initializeConfigurationManager(); err != nil { + return nil, err + } + if err := setDefaults(); err != nil { + return nil, err + } + if err := readConfiguration(); err != nil { + return nil, err + } + var directories []string + if len(viper.GetStringSlice(pathsKey)) <= 0 { + d, _ := os.Getwd() + directories = []string{d} + } else { + directories = viper.GetStringSlice(pathsKey) + } + config := &SetupConfig{ + Directories: directories, + LogLevel: viper.GetString(logLevelKey), + Depth: viper.GetInt(recursionKey), + QuickMode: viper.GetBool(qucikKey), + Mode: viper.GetString(modeKey), + } + return config, nil +} + +// set default configuration parameters +func setDefaults() error { + viper.SetDefault(logLevelKey, logLevelKeyDefault) + viper.SetDefault(qucikKey, qucikKeyDefault) + viper.SetDefault(recursionKey, recursionKeyDefault) + viper.SetDefault(modeKey, modeKeyDefault) + // viper.SetDefault(pathsKey, pathsKeyDefault) + return nil +} + +// read configuration from file +func readConfiguration() error { + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + // if file does not exist, simply create one + if _, err := os.Stat(configFileAbsPath + configFileExt); os.IsNotExist(err) { + os.MkdirAll(configurationDirectory, 0755) + os.Create(configFileAbsPath + configFileExt) + } else { + return err + } + // let's write defaults + if err := viper.WriteConfig(); err != nil { + return err + } + } + return nil +} + +// write configuration to a file +func writeConfiguration() error { + err := viper.WriteConfig() + return err +} + +// initialize the configuration manager +func initializeConfigurationManager() error { + // config viper + viper.AddConfigPath(configurationDirectory) + viper.SetConfigName(configFileName) + viper.SetConfigType(configType) + + return nil +} + +// returns OS dependent config directory +func osConfigDirectory() (osConfigDirectory string) { + switch osname := runtime.GOOS; osname { + case "windows": + osConfigDirectory = os.Getenv("APPDATA") + case "darwin": + osConfigDirectory = os.Getenv("HOME") + "/Library/Application Support" + case "linux": + osConfigDirectory = os.Getenv("HOME") + "/.config" + default: + log.Warn("Operating system couldn't be recognized") + } + return osConfigDirectory +} diff --git a/app/files.go b/app/files.go new file mode 100644 index 0000000..1ec05cd --- /dev/null +++ b/app/files.go @@ -0,0 +1,92 @@ +package app + +import ( + "io/ioutil" + "os" + "path/filepath" + + log "github.com/sirupsen/logrus" +) + +// generateDirectories returns poosible git repositories to pipe into git pkg's +// load function +func generateDirectories(dirs []string, depth int) []string { + gitDirs := make([]string, 0) + for i := 0; i <= depth; i++ { + nonrepos, repos := walkRecursive(dirs, gitDirs) + dirs = nonrepos + gitDirs = repos + } + return gitDirs +} + +// returns given values, first search directories and second stands for possible +// git repositories. Call this func from a "for i := 0; i= len(search) { + continue + } + // find possible repositories and remaining ones, b slice is possible ones + a, b, err := seperateDirectories(search[i]) + if err != nil { + log.WithFields(log.Fields{ + "directory": search[i], + }).WithError(err).Trace("Can't read directory") + continue + } + // since we started to search let's get rid of it and remove from search + // array + search[i] = search[len(search)-1] + search = search[:len(search)-1] + // lets append what we have found to continue recursion + search = append(search, a...) + appendant = append(appendant, b...) + } + return search, appendant +} + +// seperateDirectories is to find all the files in given path. This method +// does not check if the given file is a valid git repositories +func seperateDirectories(directory string) ([]string, []string, error) { + dirs := make([]string, 0) + gitDirs := make([]string, 0) + files, err := ioutil.ReadDir(directory) + // can we read the directory? + if err != nil { + log.WithFields(log.Fields{ + "directory": directory, + }).Trace("Can't read directory") + return nil, nil, nil + } + for _, f := range files { + repo := directory + string(os.PathSeparator) + f.Name() + file, err := os.Open(repo) + // if we cannot open it, simply continue to iteration and don't consider + if err != nil { + log.WithFields(log.Fields{ + "file": file, + "directory": repo, + }).WithError(err).Trace("Failed to open file in the directory") + file.Close() + continue + } + dir, err := filepath.Abs(file.Name()) + if err != nil { + file.Close() + continue + } + // with this approach, we ignore submodule or sub repositoreis in a git repository + ff, err := os.Open(dir + string(os.PathSeparator) + ".git") + if err != nil { + dirs = append(dirs, dir) + } else { + gitDirs = append(gitDirs, dir) + } + ff.Close() + file.Close() + + } + return dirs, gitDirs, nil +} diff --git a/app/quick.go b/app/quick.go new file mode 100644 index 0000000..005c033 --- /dev/null +++ b/app/quick.go @@ -0,0 +1,50 @@ +package app + +import ( + "fmt" + "sync" + "time" + + "github.com/isacikgoz/gitbatch/core/command" + "github.com/isacikgoz/gitbatch/core/git" +) + +func quick(directories []string, depth int, mode string) { + var wg sync.WaitGroup + start := time.Now() + for _, dir := range directories { + wg.Add(1) + go func(d string, mode string) { + defer wg.Done() + err := operate(d, mode) + if err != nil { + fmt.Printf("%s: %s\n", d, err.Error()) + } else { + fmt.Printf("%s: successful\n", d) + } + }(dir, mode) + } + wg.Wait() + elapsed := time.Since(start) + fmt.Printf("%d repositories finished in: %s\n", len(directories), elapsed) +} + +func operate(directory, mode string) error { + r, err := git.FastInitializeRepo(directory) + if err != nil { + return err + } + switch mode { + case "fetch": + return command.Fetch(r, command.FetchOptions{ + RemoteName: "origin", + Progress: true, + }) + case "pull": + return command.Pull(r, command.PullOptions{ + RemoteName: "origin", + Progress: true, + }) + } + return nil +} diff --git a/core/command/cmd-add.go b/core/command/cmd-add.go new file mode 100644 index 0000000..2addb23 --- /dev/null +++ b/core/command/cmd-add.go @@ -0,0 +1,89 @@ +package command + +import ( + "errors" + + "github.com/isacikgoz/gitbatch/core/git" + log "github.com/sirupsen/logrus" +) + +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 + Force bool + // DryRun + DryRun bool +} + +// Add is a wrapper function for "git add" command +func Add(e *git.RepoEntity, file *File, option AddOptions) error { + addCmdMode = addCmdModeNative + if option.Update || option.Force || option.DryRun { + addCmdMode = addCmdModeLegacy + } + switch addCmdMode { + case addCmdModeLegacy: + err := addWithGit(e, file, option) + return err + case addCmdModeNative: + err := addWithGoGit(e, file) + return err + } + return errors.New("Unhandled add operation") +} + +// AddAll function is the wrapper of "git add ." command +func AddAll(e *git.RepoEntity, option AddOptions) error { + args := make([]string, 0) + args = append(args, addCommand) + if option.DryRun { + args = append(args, "--dry-run") + } + args = append(args, ".") + out, err := GenericGitCommandWithOutput(e.AbsPath, args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} + +func addWithGit(e *git.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") + } + out, err := GenericGitCommandWithOutput(e.AbsPath, args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} + +func addWithGoGit(e *git.RepoEntity, file *File) error { + w, err := e.Repository.Worktree() + if err != nil { + return err + } + _, err = w.Add(file.Name) + return nil +} diff --git a/core/command/cmd-commit.go b/core/command/cmd-commit.go new file mode 100644 index 0000000..d81942e --- /dev/null +++ b/core/command/cmd-commit.go @@ -0,0 +1,92 @@ +package command + +import ( + "errors" + "time" + + "github.com/isacikgoz/gitbatch/core/git" + log "github.com/sirupsen/logrus" + gogit "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 defines which commit command to use. +func CommitCommand(e *git.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: + return commitWithGit(e, options) + case commitCmdModeNative: + return commitWithGoGit(e, options) + } + return errors.New("Unhandled commit operation") +} + +// commitWithGit is simply a bare git commit -m command which is flexible +func commitWithGit(e *git.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(e.AbsPath, args); err != nil { + log.Warn("Error at git command (commit)") + e.Refresh() + return err + } + // till this step everything should be ok + return e.Refresh() +} + +// commitWithGoGit is the primary commit method +func commitWithGoGit(e *git.RepoEntity, options CommitOptions) (err error) { + config, err := e.Repository.Config() + if err != nil { + return err + } + name := config.Raw.Section("user").Option("name") + email := config.Raw.Section("user").Option("email") + opt := &gogit.CommitOptions{ + Author: &object.Signature{ + Name: name, + Email: email, + When: time.Now(), + }, + } + + w, err := e.Repository.Worktree() + if err != nil { + return err + } + + _, err = w.Commit(options.CommitMsg, opt) + if err != nil { + e.Refresh() + return err + } + // till this step everything should be ok + return e.Refresh() +} diff --git a/core/command/cmd-config.go b/core/command/cmd-config.go new file mode 100644 index 0000000..c580ff6 --- /dev/null +++ b/core/command/cmd-config.go @@ -0,0 +1,106 @@ +package command + +import ( + "errors" + + "github.com/isacikgoz/gitbatch/core/git" + log "github.com/sirupsen/logrus" +) + +var ( + configCmdMode string + + configCommand = "config" + configCmdModeLegacy = "git" + configCmdModeNative = "go-git" +) + +// ConfigOptions defines the rules for commit operation +type ConfigOptions struct { + // Section + Section string + // Option + Option string + // Site should be Global or Local + Site ConfigSite +} + +// ConfigSite defines a string type for the site. +type ConfigSite string + +const ( + // ConfigSiteLocal defines a local config. + ConfigSiteLocal ConfigSite = "local" + + // ConfgiSiteGlobal defines a global config. + ConfgiSiteGlobal ConfigSite = "global" +) + +// Config adds or reads config of a repository +func Config(e *git.RepoEntity, options ConfigOptions) (value string, err error) { + // here we configure config operation + // default mode is go-git (this may be configured) + configCmdMode = configCmdModeLegacy + + switch configCmdMode { + case configCmdModeLegacy: + return configWithGit(e, options) + case configCmdModeNative: + return configWithGoGit(e, options) + } + return value, errors.New("Unhandled config operation") +} + +// configWithGit is simply a bare git commit -m command which is flexible +func configWithGit(e *git.RepoEntity, options ConfigOptions) (value string, err error) { + args := make([]string, 0) + args = append(args, configCommand) + 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(e.AbsPath, args) + if err != nil { + return out, err + } + // till this step everything should be ok + return out, nil +} + +// commitWithGoGit is the primary commit method +func configWithGoGit(e *git.RepoEntity, options ConfigOptions) (value string, err error) { + // TODO: add global search + config, err := e.Repository.Config() + if err != nil { + return value, err + } + return config.Raw.Section(options.Section).Option(options.Option), nil +} + +// AddConfig adds an entry on the ConfigOptions field. +func AddConfig(e *git.RepoEntity, options ConfigOptions, value string) (err error) { + return addConfigWithGit(e, options, value) + +} + +// addConfigWithGit is simply a bare git config --add