diff options
| author | Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> | 2018-11-18 16:44:24 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-11-18 16:44:24 +0300 |
| commit | 4e2ddf090fbd067cd7cc565d6bceba5d0d069f05 (patch) | |
| tree | 4145a3d75a7768dc7efb210a29610656d8754f9a | |
| parent | Update README.md (diff) | |
| parent | update readme. (diff) | |
| download | gitbatch-4e2ddf090fbd067cd7cc565d6bceba5d0d069f05.tar.gz | |
Merge pull request #2 from isacikgoz/develop
Develop
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | main.go | 57 | ||||
| -rw-r--r-- | pkg/app/app.go | 38 | ||||
| -rw-r--r-- | pkg/command/command.go | 44 | ||||
| -rw-r--r-- | pkg/git/git-commands.go | 38 | ||||
| -rw-r--r-- | pkg/git/git.go | 41 | ||||
| -rw-r--r-- | pkg/gui/gui.go | 196 |
7 files changed, 386 insertions, 34 deletions
@@ -3,9 +3,13 @@ ## gitbatch Aim of this simple application to make your local repositories syncrhonized with remotes easily. This is still a work in progress application. +clone the repository: +```bash +git clone https://github.com/isacikgoz/gitbatch +``` use it like: ```bash -go run main.go -d '/Users/ibrahim/git' -p gitbatch -r origin -b master +go run main.go -d '/Users/ibrahim/git' -p gitbatch ``` to get help: ```bash @@ -1,53 +1,51 @@ package main import ( + "github.com/isacikgoz/gitbatch/pkg/app" + "github.com/isacikgoz/gitbatch/pkg/git" + "gopkg.in/alecthomas/kingpin.v2" "io/ioutil" "log" - "strings" "os" - "gopkg.in/alecthomas/kingpin.v2" - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing" + "strings" ) var ( - currentDir, err = os.Getwd() - dir = kingpin.Flag("directory", "Directory to roam for git repositories.").Default(currentDir).Short('d').String() - repoPattern = kingpin.Flag("pattern", "Pattern to filter repositories").Short('p').String() - branch = kingpin.Flag("branch", "branch to be pulled").Default("master").Short('b').String() - remote = kingpin.Flag("remote", "remote name te be pulled").Default("origin").Short('r').String() + currentDir, err = os.Getwd() + dir = kingpin.Flag("directory", "Directory to roam for git repositories.").Default(currentDir).Short('d').String() + repoPattern = kingpin.Flag("pattern", "Pattern to filter repositories").Short('p').String() + repositories []git.RepoEntity ) func main() { kingpin.Parse() - FindRepos(*dir) + repositories = FindRepos(*dir) + + app, err := app.Setup(repositories) + if err != nil { + log.Fatal(err) + } + + defer app.Close() } -func FindRepos(directory string) []string { - var gitRepositories []string +func FindRepos(directory string) []git.RepoEntity { + var gitRepositories []git.RepoEntity files, err := ioutil.ReadDir(directory) if err != nil { log.Fatal(err) } + filteredFiles := FilterRepos(files) for _, f := range filteredFiles { repo := directory + "/" + f.Name() - r, err := git.PlainOpen(repo) - if err !=nil { + + entity, err := git.InitializeRepository(repo) + if err != nil { continue } - // Get the working directory for the repository - w, err := r.Worktree() - CheckIfError(err) - - ref := plumbing.ReferenceName("refs/heads/" + *branch) - err = w.Pull(&git.PullOptions{ - RemoteName: *remote, - Progress: os.Stdout, - ReferenceName: ref, - }) - CheckIfError(err) + gitRepositories = append(gitRepositories, entity) } return gitRepositories } @@ -64,11 +62,4 @@ func FilterRepos(files []os.FileInfo) []os.FileInfo { return filteredRepos } -// CheckIfError should be used to naively panics if an error is not nil. -func CheckIfError(err error) { - if err == nil { - return - } - log.Fatal(err) - os.Exit(1) -} + diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 0000000..9d6a0e6 --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,38 @@ +package app + +import ( + "github.com/isacikgoz/gitbatch/pkg/gui" + "github.com/isacikgoz/gitbatch/pkg/git" + "io" +) + +// App struct +type App struct { + closers []io.Closer +} + +// Setup bootstrap a new application +func Setup(repositories []git.RepoEntity) (*App, error) { + app := &App{ + closers: []io.Closer{}, + } + + var err error + + err = gui.Run(repositories) + if err != nil { + return app, err + } + return app, nil +} + +// Close closes any resources +func (app *App) Close() error { + for _, closer := range app.closers { + err := closer.Close() + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/command/command.go b/pkg/command/command.go new file mode 100644 index 0000000..2c5a0c3 --- /dev/null +++ b/pkg/command/command.go @@ -0,0 +1,44 @@ +package command + +import ( + "os/exec" + "syscall" + "log" +) + +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 +} + +func GetCommandStatus(dir string, command string, args []string) (int, error) { + cmd := exec.Command(command, args...) + if dir != "" { + cmd.Dir = dir + } + var err error + 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/git/git-commands.go b/pkg/git/git-commands.go new file mode 100644 index 0000000..beb6936 --- /dev/null +++ b/pkg/git/git-commands.go @@ -0,0 +1,38 @@ +package git + +import ( + "strings" + "github.com/isacikgoz/gitbatch/pkg/command" +) + +// UpstreamDifferenceCount checks how many pushables/pullables there are for the +// current branch +func UpstreamDifferenceCount(repoPath string) (string, string) { + args := []string{"rev-list", "@{u}..HEAD", "--count"} + pushableCount, err := command.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return "?", "?" + } + args = []string{"rev-list", "HEAD..@{u}", "--count"} + pullableCount, err := command.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return "?", "?" + } + return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) +} + +func CurrentBranchName(repoPath string) (string, error) { + args := []string{"symbolic-ref", "--short", "HEAD"} + branchName, err := command.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + args = []string{"rev-parse", "--short", "HEAD"} + branchName, err = command.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return "", err + } + } + return branchName, nil +} + + + diff --git a/pkg/git/git.go b/pkg/git/git.go new file mode 100644 index 0000000..c96fc96 --- /dev/null +++ b/pkg/git/git.go @@ -0,0 +1,41 @@ +package git + +import ( + "gopkg.in/src-d/go-git.v4" +) + +type RepoEntity struct { + Name string + Repository git.Repository + Pushables string + Pullables string + Branch string +} + +func InitializeRepository(directory string) (RepoEntity, error) { + var entity RepoEntity + + r, err := git.PlainOpen(directory) + if err != nil { + return entity, err + } + entity = RepoEntity{directory, *r, "", "", ""} + + return entity, nil +} + +func InitializeRepositories(directories []string) []RepoEntity { + var gitRepositories []RepoEntity + for _, f := range directories { + r, err := git.PlainOpen(f) + if err != nil { + continue + } + entity := RepoEntity{f, *r, "", "", ""} + gitRepositories = append(gitRepositories, entity) + } + return gitRepositories +} + + + diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go new file mode 100644 index 0000000..1d32012 --- /dev/null +++ b/pkg/gui/gui.go @@ -0,0 +1,196 @@ +package gui + +import ( + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" + "fmt" +) + +// Gui wraps the gocui Gui object which handles rendering and events +var ( + repositories []git.RepoEntity +) + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + + if v, err := g.SetView("main", 0, 0, maxX-1, int(0.65*float32(maxY))-3); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = " Matched Repositories " + v.Highlight = true + v.SelBgColor = gocui.ColorGreen + v.SelFgColor = gocui.ColorBlack + + for _, r := range repositories { + fmt.Fprintln(v, r.Name) + } + + if _, err = setCurrentViewOnTop(g, "main"); err != nil { + return err + } + } + + if v, err := g.SetView("detail", 0, int(0.65*float32(maxY))-2, maxX-1, maxY-2); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = " Repository Detail " + v.Wrap = true + v.Autoscroll = true + } + + if v, err := g.SetView("keybindings", -1, maxY-2, maxX, maxY); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.BgColor = gocui.ColorBlue + v.FgColor = gocui.ColorYellow + v.Frame = false + fmt.Fprintln(v, "q: quit ↑ ↓: navigate space: select") + } + return nil +} + +func keybindings(g *gocui.Gui) error { + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", 'q', gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeySpace, gocui.ModNone, markRepository); err != nil { + return err + } + return nil +} + +func cursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy+1); err != nil { + ox, oy := v.Origin() + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + updateDetail(g, v) + } + return nil +} + +func cursorUp(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 + } + } + updateDetail(g, v) + } + return nil +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { + if _, err := g.SetCurrentView(name); err != nil { + return nil, err + } + return g.SetViewOnTop(name) +} + +func getSelectedRepository(g *gocui.Gui, v *gocui.View) (git.RepoEntity, error) { + var l string + var err error + var r git.RepoEntity + + _, cy := v.Cursor() + if l, err = v.Line(cy); err != nil { + return r, err + } + + for _, sr := range repositories { + if l == sr.Name { + return sr, nil + } + } + return r, err +} + +func updateDetail(g *gocui.Gui, v *gocui.View) error { + var err error + + _, cy := v.Cursor() + if _, err = v.Line(cy); err != nil { + return err + } + + out, err := g.View("detail") + if err != nil { + return err + } + + out.Clear() + + if repo, err := getSelectedRepository(g, v); err != nil { + return err + } else { + if list, err := repo.Repository.Remotes(); err != nil { + return err + } else { + fmt.Fprintln(out, repo.Name) + for _, r := range list { + fmt.Fprintln(out, r) + } + } + } + return nil +} + +func markRepository(g *gocui.Gui, v *gocui.View) error { + var l string + var err error + + _, cy := v.Cursor() + if l, err = v.Line(cy); err != nil { + return err + } else { + l = l + " X" + } + + return nil +} + +// Run setup the gui with keybindings and start the mainloop +func Run(repos []git.RepoEntity) error { + repositories = repos + g, err := gocui.NewGui(gocui.OutputNormal) + if err != nil { + return err + } + defer g.Close() + + g.SetManagerFunc(layout) + + if err := keybindings(g); err != nil { + return err + } + + if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + return err + } + return nil +} + |
