diff --git a/README.md b/README.md index 12f4cb8..bb19d78 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ [![Build Status](https://travis-ci.com/isacikgoz/gitbatch.svg?branch=master)](https://travis-ci.com/isacikgoz/gitbatch) [![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/isacikgoz/gitbatch)](https://goreportcard.com/report/github.com/isacikgoz/gitbatch) ## gitbatch -This tool is being built to make your local repositories synchronized with remotes easily. Although the focus is batch jobs, you can still do de facto micro management of your git repositories (e.g *add/reset, stash, commit etc.*) +I like to use polyrepos. I (*was*) often end up walking on many directories and manually pulling updates etc. To make this routine faster, I created a simple tool to handle this job. Although the focus is batch jobs, you can still do de facto micro management of your git repositories (e.g *add/reset, stash, commit etc.*) Here is the screencast of the app: -[![asciicast](https://asciinema.org/a/QQPVDWVxUR3bvJhIZY3c4PTuG.svg)](https://asciinema.org/a/QQPVDWVxUR3bvJhIZY3c4PTuG) +[![asciicast](https://asciinema.org/a/AiH2y2gwr8sLce40epnIQxRAH.svg)](https://asciinema.org/a/AiH2y2gwr8sLce40epnIQxRAH) ## Installation To install with go, run the following command; @@ -19,7 +19,7 @@ run the `gitbatch` command from the parent of your git repositories. For start-u For more information see the [wiki pages](https://github.com/isacikgoz/gitbatch/wiki) ## Further goals -- add testing +- **add testing** - add push - full src-d/go-git integration (*having some performance issues in such cases*) - fetch, config, add, reset, commit, status and diff commands are supported but not fully utilized, still using git sometimes @@ -34,7 +34,6 @@ Please refer to [Known issues page](https://github.com/isacikgoz/gitbatch/wiki/K - [logrus](https://github.com/sirupsen/logrus) for logging - [viper](https://github.com/spf13/viper) for configuration management - [color](https://github.com/fatih/color) for colored text -- [lazygit](https://github.com/jesseduffield/lazygit) as app template and reference +- [lazygit](https://github.com/jesseduffield/lazygit) for inspiration - [kingpin](https://github.com/alecthomas/kingpin) for command-line flag&options -I love [lazygit](https://github.com/jesseduffield/lazygit), with that inspiration, decided to build this project to be even more lazy. The rationale was; my daily work is tied to many repositories and I often end up walking on many directories and manually pulling updates etc. To make this routine faster, I created a simple tool to handle this job. I really enjoy working on this project and I hope it will be a useful tool. diff --git a/pkg/app/app.go b/app/app.go similarity index 92% rename from pkg/app/app.go rename to app/app.go index 9c9e9a7..b8eebc8 100644 --- a/pkg/app/app.go +++ b/app/app.go @@ -3,7 +3,7 @@ package app import ( "os" - "github.com/isacikgoz/gitbatch/pkg/gui" + "github.com/isacikgoz/gitbatch/gui" log "github.com/sirupsen/logrus" ) @@ -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/config.go b/app/config.go similarity index 100% rename from pkg/app/config.go rename to app/config.go diff --git a/pkg/app/files.go b/app/files.go similarity index 65% rename from pkg/app/files.go rename to app/files.go index 25e5074..1ec05cd 100644 --- a/pkg/app/files.go +++ b/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/app/quick.go similarity index 77% rename from pkg/app/quick.go rename to app/quick.go index 488e1d6..005c033 100644 --- a/pkg/app/quick.go +++ b/app/quick.go @@ -5,7 +5,8 @@ import ( "sync" "time" - "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/isacikgoz/gitbatch/core/command" + "github.com/isacikgoz/gitbatch/core/git" ) func quick(directories []string, depth int, mode string) { @@ -35,12 +36,14 @@ func operate(directory, mode string) error { } switch mode { case "fetch": - return git.Fetch(r, git.FetchOptions{ + return command.Fetch(r, command.FetchOptions{ RemoteName: "origin", + Progress: true, }) case "pull": - return git.Pull(r, git.PullOptions{ + return command.Pull(r, command.PullOptions{ RemoteName: "origin", + Progress: true, }) } return nil diff --git a/pkg/git/cmd-add.go b/core/command/add.go similarity index 72% rename from pkg/git/cmd-add.go rename to core/command/add.go index e7faff2..5ad8774 100644 --- a/pkg/git/cmd-add.go +++ b/core/command/add.go @@ -1,8 +1,9 @@ -package git +package command import ( "errors" + "github.com/isacikgoz/gitbatch/core/git" log "github.com/sirupsen/logrus" ) @@ -25,31 +26,31 @@ type AddOptions struct { } // Add is a wrapper function for "git add" command -func Add(e *RepoEntity, file *File, option AddOptions) error { +func Add(r *git.Repository, 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) + err := addWithGit(r, file, option) return err case addCmdModeNative: - err := addWithGoGit(e, file) + err := addWithGoGit(r, file) return err } return errors.New("Unhandled add operation") } // AddAll function is the wrapper of "git add ." command -func AddAll(e *RepoEntity, option AddOptions) error { +func AddAll(r *git.Repository, 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) + out, err := GenericGitCommandWithOutput(r.AbsPath, args) if err != nil { log.Warn("Error while add command") return errors.New(out + "\n" + err.Error()) @@ -57,7 +58,7 @@ func AddAll(e *RepoEntity, option AddOptions) error { return nil } -func addWithGit(e *RepoEntity, file *File, option AddOptions) error { +func addWithGit(r *git.Repository, file *File, option AddOptions) error { args := make([]string, 0) args = append(args, addCommand) args = append(args, file.Name) @@ -70,7 +71,7 @@ func addWithGit(e *RepoEntity, file *File, option AddOptions) error { if option.DryRun { args = append(args, "--dry-run") } - out, err := GenericGitCommandWithOutput(e.AbsPath, args) + out, err := GenericGitCommandWithOutput(r.AbsPath, args) if err != nil { log.Warn("Error while add command") return errors.New(out + "\n" + err.Error()) @@ -78,8 +79,8 @@ func addWithGit(e *RepoEntity, file *File, option AddOptions) error { return nil } -func addWithGoGit(e *RepoEntity, file *File) error { - w, err := e.Repository.Worktree() +func addWithGoGit(r *git.Repository, file *File) error { + w, err := r.Repo.Worktree() if err != nil { return err } diff --git a/pkg/helpers/command.go b/core/command/cmd.go similarity index 51% rename from pkg/helpers/command.go rename to core/command/cmd.go index b861812..536495b 100644 --- a/pkg/helpers/command.go +++ b/core/command/cmd.go @@ -1,8 +1,9 @@ -package helpers +package command import ( "log" "os/exec" + "strings" "syscall" ) @@ -14,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 } @@ -49,3 +50,49 @@ func GetCommandStatus(dir string, command string, args []string) (int, error) { } 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 := RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return err + } + return nil +} + +// GenericGitCommandWithOutput runs any git command with returning output +func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) { + out, err := RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return out, err + } + return TrimTrailingNewline(out), nil +} + +// GenericGitCommandWithErrorOutput runs any git command with returning output +func GenericGitCommandWithErrorOutput(repoPath string, args []string) (string, error) { + out, err := RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return TrimTrailingNewline(out), err + } + 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 := RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return "?" + } + return diff +} diff --git a/pkg/git/cmd-commit.go b/core/command/commit.go similarity index 71% rename from pkg/git/cmd-commit.go rename to core/command/commit.go index 23c1de1..bf81a25 100644 --- a/pkg/git/cmd-commit.go +++ b/core/command/commit.go @@ -1,11 +1,12 @@ -package git +package command import ( "errors" "time" + "github.com/isacikgoz/gitbatch/core/git" log "github.com/sirupsen/logrus" - "gopkg.in/src-d/go-git.v4" + gogit "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing/object" ) @@ -28,22 +29,22 @@ type CommitOptions struct { } // CommitCommand defines which commit command to use. -func CommitCommand(e *RepoEntity, options CommitOptions) (err error) { +func CommitCommand(r *git.Repository, 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) + return commitWithGit(r, options) case commitCmdModeNative: - return commitWithGoGit(e, options) + return commitWithGoGit(r, options) } return errors.New("Unhandled commit operation") } // commitWithGit is simply a bare git commit -m command which is flexible -func commitWithGit(e *RepoEntity, options CommitOptions) (err error) { +func commitWithGit(r *git.Repository, options CommitOptions) (err error) { args := make([]string, 0) args = append(args, commitCommand) args = append(args, "-m") @@ -51,24 +52,24 @@ func commitWithGit(e *RepoEntity, options CommitOptions) (err error) { if len(options.CommitMsg) > 0 { args = append(args, options.CommitMsg) } - if err := GenericGitCommand(e.AbsPath, args); err != nil { + if err := GenericGitCommand(r.AbsPath, args); err != nil { log.Warn("Error at git command (commit)") - e.Refresh() + r.Refresh() return err } // till this step everything should be ok - return e.Refresh() + return r.Refresh() } // commitWithGoGit is the primary commit method -func commitWithGoGit(e *RepoEntity, options CommitOptions) (err error) { - config, err := e.Repository.Config() +func commitWithGoGit(r *git.Repository, options CommitOptions) (err error) { + config, err := r.Repo.Config() if err != nil { return err } name := config.Raw.Section("user").Option("name") email := config.Raw.Section("user").Option("email") - opt := &git.CommitOptions{ + opt := &gogit.CommitOptions{ Author: &object.Signature{ Name: name, Email: email, @@ -76,16 +77,16 @@ func commitWithGoGit(e *RepoEntity, options CommitOptions) (err error) { }, } - w, err := e.Repository.Worktree() + w, err := r.Repo.Worktree() if err != nil { return err } _, err = w.Commit(options.CommitMsg, opt) if err != nil { - e.Refresh() + r.Refresh() return err } // till this step everything should be ok - return e.Refresh() + return r.Refresh() } diff --git a/pkg/git/cmd-config.go b/core/command/config.go similarity index 71% rename from pkg/git/cmd-config.go rename to core/command/config.go index e4bacf0..35eff23 100644 --- a/pkg/git/cmd-config.go +++ b/core/command/config.go @@ -1,8 +1,9 @@ -package git +package command import ( "errors" + "github.com/isacikgoz/gitbatch/core/git" log "github.com/sirupsen/logrus" ) @@ -35,23 +36,23 @@ const ( ConfgiSiteGlobal ConfigSite = "global" ) -// Config -func Config(e *RepoEntity, options ConfigOptions) (value string, err error) { +// Config adds or reads config of a repository +func Config(r *git.Repository, 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) + return configWithGit(r, options) case configCmdModeNative: - return configWithGoGit(e, options) + return configWithGoGit(r, options) } return value, errors.New("Unhandled config operation") } // configWithGit is simply a bare git commit -m command which is flexible -func configWithGit(e *RepoEntity, options ConfigOptions) (value string, err error) { +func configWithGit(r *git.Repository, options ConfigOptions) (value string, err error) { args := make([]string, 0) args = append(args, configCommand) if len(string(options.Site)) > 0 { @@ -60,7 +61,7 @@ func configWithGit(e *RepoEntity, options ConfigOptions) (value string, err erro args = append(args, "--get") args = append(args, options.Section+"."+options.Option) // parse options to command line arguments - out, err := GenericGitCommandWithOutput(e.AbsPath, args) + out, err := GenericGitCommandWithOutput(r.AbsPath, args) if err != nil { return out, err } @@ -69,9 +70,9 @@ func configWithGit(e *RepoEntity, options ConfigOptions) (value string, err erro } // commitWithGoGit is the primary commit method -func configWithGoGit(e *RepoEntity, options ConfigOptions) (value string, err error) { +func configWithGoGit(r *git.Repository, options ConfigOptions) (value string, err error) { // TODO: add global search - config, err := e.Repository.Config() + config, err := r.Repo.Config() if err != nil { return value, err } @@ -79,13 +80,13 @@ func configWithGoGit(e *RepoEntity, options ConfigOptions) (value string, err er } // AddConfig adds an entry on the ConfigOptions field. -func AddConfig(e *RepoEntity, options ConfigOptions, value string) (err error) { - return addConfigWithGit(e, options, value) +func AddConfig(r *git.Repository, options ConfigOptions, value string) (err error) { + return addConfigWithGit(r, options, value) } // addConfigWithGit is simply a bare git config --add