pax_global_header00006660000000000000000000000064135317256130014520gustar00rootroot0000000000000052 comment=16184dc1b1a0bbffdfacaf1b442ce859b345c3ba gitbatch-0.5.0/000077500000000000000000000000001353172561300133075ustar00rootroot00000000000000gitbatch-0.5.0/.gitignore000066400000000000000000000000721353172561300152760ustar00rootroot00000000000000exec.go.test build.sh test.go .vscode build/ coverage.txt gitbatch-0.5.0/.gitmodules000066400000000000000000000000001353172561300154520ustar00rootroot00000000000000gitbatch-0.5.0/.travis.yml000066400000000000000000000003211353172561300154140ustar00rootroot00000000000000language: go go: - "1.10" - tip before_install: - go get -t -v ./... script: - go test ./... -coverprofile=coverage.txt -covermode=atomic after_success: - bash <(curl -s https://codecov.io/bash) gitbatch-0.5.0/LICENSE000066400000000000000000000020671353172561300143210ustar00rootroot00000000000000MIT License Copyright (c) 2018 Ibrahim Serdar Acikgoz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gitbatch-0.5.0/README.md000066400000000000000000000041441353172561300145710ustar00rootroot00000000000000[![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 Managing multiple git repositories is easier than ever. I (*was*) often end up working 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.*) Check out the screencast of the app: [![asciicast](https://asciinema.org/a/lxoZT6Z8fSliIEebWSPVIY8ct.svg)](https://asciinema.org/a/lxoZT6Z8fSliIEebWSPVIY8ct) ## Installation To install with go, run the following command; ```bash go get -u github.com/isacikgoz/gitbatch ``` ### MacOS using homebrew ```bash brew tap isacikgoz/taps brew install gitbatch ``` For other options see [installation page](https://github.com/isacikgoz/gitbatch/wiki/Installation) ## Use run the `gitbatch` command from the parent of your git repositories. For start-up options simply `gitbatch --help` For more information see the [wiki pages](https://github.com/isacikgoz/gitbatch/wiki) ## Further goals - improve testing - add push - add batch checkout - full src-d/go-git integration (*having some performance issues in large repos*) - fetch, config, rev-list, add, reset, commit, status and diff commands are supported but not fully utilized, still using git occasionally - merge, stash are not supported yet by go-git ## Credits - [go-git](https://github.com/src-d/go-git) for git interface (partially) - [gocui](https://github.com/jroimartin/gocui) for user interface - [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 - [kingpin](https://github.com/alecthomas/kingpin) for command-line flag&options gitbatch-0.5.0/app/000077500000000000000000000000001353172561300140675ustar00rootroot00000000000000gitbatch-0.5.0/app/builder.go000066400000000000000000000054551353172561300160550ustar00rootroot00000000000000package app import ( "errors" "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 *Config } // Config is an assembler data to initiate a setup type Config 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(argConfig *Config) (*App, error) { // initiate the app and give it initial values app := &App{} if len(argConfig.Directories) <= 0 { d, _ := os.Getwd() argConfig.Directories = []string{d} } presetConfig, err := LoadConfiguration() if err != nil { return nil, err } appConfig := overrideConfig(presetConfig, argConfig) setLogLevel(appConfig.LogLevel) // hopefull everything went smooth as butter log.Trace("App configuration completed") dirs := generateDirectories(appConfig.Directories, appConfig.Depth) if appConfig.QuickMode { if err := execQuickMode(dirs, appConfig); err != nil { return nil, err } // we are done here and no need for an app to be configured return nil, nil } // create a gui.Gui struct and set it as App's gui app.Gui, err = gui.NewGui(appConfig.Mode, dirs) if err != nil { // the error types and handling is not considered yet return nil, err } 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 overrideConfig(appConfig, setupConfig *Config) *Config { 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 } func execQuickMode(dirs []string, cfg *Config) error { x := cfg.Mode == "fetch" y := cfg.Mode == "pull" if x == y { return errors.New("unrecognized quick mode: " + cfg.Mode) } quick(dirs, cfg.Mode) return nil } gitbatch-0.5.0/app/builder_test.go000066400000000000000000000060131353172561300171030ustar00rootroot00000000000000package app import ( "io/ioutil" "os" "testing" "time" "github.com/isacikgoz/gitbatch/core/git" log "github.com/sirupsen/logrus" ggit "gopkg.in/src-d/go-git.v4" ) var ( config1 = &Config{ Directories: []string{}, LogLevel: "info", Depth: 1, QuickMode: false, Mode: "fetch", } config2 = &Config{ Directories: []string{string(os.PathSeparator) + "tmp"}, LogLevel: "error", Depth: 1, QuickMode: true, Mode: "pull", } testRepoDir, _ = ioutil.TempDir("", "test-data") ) func TestSetup(t *testing.T) { mockApp := &App{Config: config1} var tests = []struct { input *Config expected *App }{ {config2, nil}, {config1, mockApp}, } for _, test := range tests { app, err := Setup(test.input) if err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } q := test.input.QuickMode if q && app != nil { t.Errorf("Test Failed.") } else if !q && app == nil { t.Errorf("Test Failed.") } } } func TestClose(t *testing.T) { mockApp := &App{} if err := mockApp.Close(); err != nil { t.Errorf("Test") } } func TestSetLogLevel(t *testing.T) { var tests = []struct { input string }{ {"debug"}, {"info"}, } for _, test := range tests { setLogLevel(test.input) if test.input != log.GetLevel().String() { t.Errorf("Test Failed: %s inputted, actual: %s", test.input, log.GetLevel().String()) } } } func TestOverrideConfig(t *testing.T) { var tests = []struct { inp1 *Config inp2 *Config expected *Config }{ {config1, config2, config1}, } for _, test := range tests { if output := overrideConfig(test.inp1, test.inp2); output != test.expected || test.inp2.Mode != output.Mode { t.Errorf("Test Failed: {%s, %s} inputted, output: %s, expected: %s", test.inp1.Directories, test.inp2.Directories, output.Directories, test.expected.Directories) } } } func TestExecQuickMode(t *testing.T) { defer cleanRepo() _, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 []string inp2 *Config }{ {[]string{basic}, config1}, } for _, test := range tests { if err := execQuickMode(test.inp1, test.inp2); err != nil { t.Errorf("Test Failed: %s", err.Error()) } } } func testRepo() (*git.Repository, error) { testRepoURL := "https://gitlab.com/isacikgoz/test-data.git" _, err := ggit.PlainClone(testRepoDir, false, &ggit.CloneOptions{ URL: testRepoURL, RecurseSubmodules: ggit.DefaultSubmoduleRecursionDepth, }) time.Sleep(time.Second) if err != nil && err != ggit.NoErrAlreadyUpToDate { return nil, err } return git.InitializeRepo(testRepoDir) } func testFile(name string) (*git.File, error) { _, err := os.Create(testRepoDir + string(os.PathSeparator) + name) if err != nil { return nil, err } f := &git.File{ Name: name, AbsPath: testRepoDir + string(os.PathSeparator) + name, X: git.StatusUntracked, Y: git.StatusUntracked, } return f, nil } func cleanRepo() error { return os.RemoveAll(testRepoDir) } gitbatch-0.5.0/app/config.go000066400000000000000000000061171353172561300156700ustar00rootroot00000000000000package 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(runtime.GOOS), 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() (*Config, 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 := &Config{ 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(osname string) (osConfigDirectory string) { switch 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 } gitbatch-0.5.0/app/config_test.go000066400000000000000000000016141353172561300167240ustar00rootroot00000000000000package app import ( "strings" "testing" ) func TestLoadConfiguration(t *testing.T) { if _, err := LoadConfiguration(); err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } } func TestReadConfiguration(t *testing.T) { if err := readConfiguration(); err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } } func TestInitializeConfigurationManager(t *testing.T) { if err := initializeConfigurationManager(); err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } } func TestOsConfigDirectory(t *testing.T) { var tests = []struct { input string expected string }{ {"linux", ".config"}, {"darwin", "Application Support"}, } for _, test := range tests { if output := osConfigDirectory(test.input); !strings.Contains(output, test.expected) { t.Errorf("Test Failed. %s inputted, output: %s, expected %s", test.input, output, test.expected) } } } gitbatch-0.5.0/app/files.go000066400000000000000000000050121353172561300155160ustar00rootroot00000000000000package 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 } gitbatch-0.5.0/app/files_test.go000066400000000000000000000054141353172561300165630ustar00rootroot00000000000000package app import ( "os" "strings" "testing" ) var ( wd, _ = os.Getwd() sp = string(os.PathSeparator) d = strings.TrimSuffix(wd, sp+"app") relparent = ".." + sp + "test" parent = d + sp + "test" // data = parent + sp + "test-data" basic = testRepoDir + sp + "basic-repo" dirty = testRepoDir + sp + "dirty-repo" non = testRepoDir + sp + "non-repo" subbasic = non + sp + "basic-repo" ) func TestGenerateDirectories(t *testing.T) { defer cleanRepo() _, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 []string inp2 int expected []string }{ // {[]string{relparent}, 0, []string{testRepoDir}}, {[]string{testRepoDir}, 0, []string{basic, dirty}}, {[]string{testRepoDir}, 2, []string{basic, dirty, subbasic}}, } for _, test := range tests { if output := generateDirectories(test.inp1, test.inp2); !testEq(output, test.expected) { t.Errorf("Test Failed: {%s, %d} inputted, recieved: %s, expected: %s", test.inp1, test.inp2, output, test.expected) } } } func TestWalkRecursive(t *testing.T) { defer cleanRepo() _, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 []string inp2 []string exp1 []string exp2 []string }{ { []string{testRepoDir}, []string{""}, []string{testRepoDir + sp + ".git", testRepoDir + sp + ".gitmodules", non}, []string{"", basic, dirty}, }, } for _, test := range tests { if out1, out2 := walkRecursive(test.inp1, test.inp2); !testEq(out1, test.exp1) || !testEq(out2, test.exp2) { t.Errorf("Test Failed: {%s, %s} inputted, recieved: {%s, %s}, expected: {%s, %s}", test.inp1, test.inp2, out1, out2, test.exp1, test.exp2) } } } func TestSeperateDirectories(t *testing.T) { defer cleanRepo() _, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } var tests = []struct { input string exp1 []string exp2 []string }{ { "", nil, nil, }, { testRepoDir, []string{testRepoDir + sp + ".git", testRepoDir + sp + ".gitmodules", non}, []string{basic, dirty}, }, } for _, test := range tests { if out1, out2, err := seperateDirectories(test.input); !testEq(out1, test.exp1) || !testEq(out2, test.exp2) || err != nil { if err != nil { t.Errorf("Test failed with error: %s ", err.Error()) return } t.Errorf("Test Failed: %s inputted, recieved: {%s, %s}, expected: {%s, %s}", test.input, out1, out2, test.exp1, test.exp2) } } } func testEq(a, b []string) bool { // If one is nil, the other must also be nil. if (a == nil) != (b == nil) { return false } if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } gitbatch-0.5.0/app/quick.go000066400000000000000000000017421353172561300155360ustar00rootroot00000000000000package app import ( "fmt" "sync" "time" "github.com/isacikgoz/gitbatch/core/command" "github.com/isacikgoz/gitbatch/core/git" ) func quick(directories []string, 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 } gitbatch-0.5.0/app/quick_test.go000066400000000000000000000005771353172561300166020ustar00rootroot00000000000000package app import ( "testing" ) func TestQuick(t *testing.T) { defer cleanRepo() _, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 []string inp2 string }{ { []string{dirty}, "fetch", }, { []string{dirty}, "pull", }, } for _, test := range tests { quick(test.inp1, test.inp2) } } gitbatch-0.5.0/core/000077500000000000000000000000001353172561300142375ustar00rootroot00000000000000gitbatch-0.5.0/core/command/000077500000000000000000000000001353172561300156555ustar00rootroot00000000000000gitbatch-0.5.0/core/command/add.go000066400000000000000000000033051353172561300167350ustar00rootroot00000000000000package command import ( "errors" "github.com/isacikgoz/gitbatch/core/git" log "github.com/sirupsen/logrus" ) // AddOptions defines the rules for "git add" command type AddOptions struct { // Update Update bool // Force Force bool // DryRun DryRun bool // Mode is the command mode CommandMode Mode } // Add is a wrapper function for "git add" command func Add(r *git.Repository, f *git.File, o *AddOptions) error { mode := o.CommandMode if o.Update || o.Force || o.DryRun { mode = ModeLegacy } switch mode { case ModeLegacy: err := addWithGit(r, f, o) return err case ModeNative: err := addWithGoGit(r, f) return err } return errors.New("Unhandled add operation") } // AddAll function is the wrapper of "git add ." command func AddAll(r *git.Repository, o *AddOptions) error { args := make([]string, 0) args = append(args, "add") if o.DryRun { args = append(args, "--dry-run") } args = append(args, ".") out, err := Run(r.AbsPath, "git", args) if err != nil { log.Warn("Error while add command") return errors.New(out + "\n" + err.Error()) } return nil } func addWithGit(r *git.Repository, f *git.File, o *AddOptions) error { args := make([]string, 0) args = append(args, "add") args = append(args, f.Name) if o.Update { args = append(args, "--update") } if o.Force { args = append(args, "--force") } if o.DryRun { args = append(args, "--dry-run") } out, err := Run(r.AbsPath, "git", args) if err != nil { log.Warn("Error while add command") return errors.New(out + "\n" + err.Error()) } return nil } func addWithGoGit(r *git.Repository, f *git.File) error { w, err := r.Repo.Worktree() if err != nil { return err } _, err = w.Add(f.Name) return err } gitbatch-0.5.0/core/command/add_test.go000066400000000000000000000030761353172561300200010ustar00rootroot00000000000000package command import ( "testing" "github.com/isacikgoz/gitbatch/core/git" ) var ( testAddopt1 = &AddOptions{} ) func TestAddAll(t *testing.T) { defer cleanRepo() r, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } _, err = testFile("file") if err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 *git.Repository inp2 *AddOptions }{ {r, testAddopt1}, } for _, test := range tests { if err := AddAll(test.inp1, test.inp2); err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } } } func TestAddWithGit(t *testing.T) { defer cleanRepo() r, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } f, err := testFile("file") if err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 *git.Repository inp2 *git.File inp3 *AddOptions }{ {r, f, testAddopt1}, } for _, test := range tests { if err := addWithGit(test.inp1, test.inp2, test.inp3); err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } } } func TestAddWithGoGit(t *testing.T) { defer cleanRepo() r, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } f, err := testFile("file") if err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 *git.Repository inp2 *git.File }{ {r, f}, } for _, test := range tests { if err := addWithGoGit(test.inp1, test.inp2); err != nil { t.Errorf("Test Failed. error: %s", err.Error()) } } } gitbatch-0.5.0/core/command/checkout.go000066400000000000000000000020711353172561300200110ustar00rootroot00000000000000package command import ( "os/exec" "github.com/isacikgoz/gitbatch/core/git" ) // CheckoutOptions defines the rules of checkout command type CheckoutOptions struct { TargetRef string CreateIfAbsent bool CommandMode Mode } // Checkout is a wrapper function for "git checkout" command. func Checkout(r *git.Repository, o *CheckoutOptions) error { var branch *git.Branch for _, b := range r.Branches { if b.Name == o.TargetRef { branch = b break } } msg := "checkout in progress" if branch != nil { if err := r.Checkout(branch); err != nil { r.SetWorkStatus(git.Fail) msg = err.Error() } else { r.SetWorkStatus(git.Success) msg = "switched to " + o.TargetRef } } else if o.CreateIfAbsent { args := []string{"checkout", "-b", o.TargetRef} cmd := exec.Command("git", args...) cmd.Dir = r.AbsPath _, err := cmd.CombinedOutput() if err != nil { r.SetWorkStatus(git.Fail) msg = err.Error() } else { r.SetWorkStatus(git.Success) msg = "switched to " + o.TargetRef } } r.State.Message = msg return r.Refresh() } gitbatch-0.5.0/core/command/cmd.go000066400000000000000000000037661353172561300167630ustar00rootroot00000000000000package command import ( "log" "os/exec" "strings" "syscall" ) // Mode indicates that wheter command should run native code or use git // command to operate. type Mode uint8 const ( // ModeLegacy uses traditional git command line tool to operate ModeLegacy = iota // ModeNative uses native implementation of given git command ModeNative ) // Run 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 Run(d string, c string, args []string) (string, error) { cmd := exec.Command(c, args...) if d != "" { cmd.Dir = d } output, err := cmd.CombinedOutput() return trimTrailingNewline(string(output)), err } // Return 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 Return(d string, c string, args []string) (int, error) { cmd := exec.Command(c, args...) if d != "" { cmd.Dir = d } 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(s string) string { if strings.HasSuffix(s, "\n") || strings.HasSuffix(s, "\r") { return s[:len(s)-1] } return s } gitbatch-0.5.0/core/command/cmd_test.go000066400000000000000000000044261353172561300200140ustar00rootroot00000000000000package command import ( "io/ioutil" "os" "testing" "time" "github.com/isacikgoz/gitbatch/core/git" ggit "gopkg.in/src-d/go-git.v4" ) var ( testRepoDir, _ = ioutil.TempDir("", "dirty-repo") ) func TestRun(t *testing.T) { wd, err := os.Getwd() if err != nil { t.Fatalf("Test Failed.") } var tests = []struct { inp1 string inp2 string inp3 []string }{ {wd, "git", []string{"status"}}, } for _, test := range tests { if output, err := Run(test.inp1, test.inp2, test.inp3); err != nil || len(output) <= 0 { t.Errorf("Test Failed. {%s, %s, %s} inputted, output: %s", test.inp1, test.inp2, test.inp3, output) } } } func TestReturn(t *testing.T) { wd, err := os.Getwd() if err != nil { t.Fatalf("Test Failed.") } var tests = []struct { inp1 string inp2 string inp3 []string expected int }{ {wd, "foo", []string{}, -1}, } for _, test := range tests { if output, _ := Return(test.inp1, test.inp2, test.inp3); output != test.expected { t.Errorf("Test Failed. {%s, %s, %s} inputted, output: %d, expected : %d", test.inp1, test.inp2, test.inp3, output, test.expected) } } } func TestTrimTrailingNewline(t *testing.T) { var tests = []struct { input string expected string }{ {"foo", "foo"}, {"foo\n", "foo"}, {"foo\r", "foo"}, } for _, test := range tests { if output := trimTrailingNewline(test.input); output != test.expected { t.Errorf("Test Failed. %s inputted, output: %s, expected: %s", test.input, output, test.expected) } } } func testRepo() (*git.Repository, error) { testRepoURL := "https://gitlab.com/isacikgoz/dirty-repo.git" _, err := ggit.PlainClone(testRepoDir, false, &ggit.CloneOptions{ URL: testRepoURL, RecurseSubmodules: ggit.DefaultSubmoduleRecursionDepth, }) time.Sleep(time.Second) if err != nil && err != ggit.NoErrAlreadyUpToDate { return nil, err } return git.InitializeRepo(testRepoDir) } func testFile(name string) (*git.File, error) { _, err := os.Create(testRepoDir + string(os.PathSeparator) + name) if err != nil { return nil, err } f := &git.File{ Name: name, AbsPath: testRepoDir + string(os.PathSeparator) + name, X: git.StatusUntracked, Y: git.StatusUntracked, } return f, nil } func cleanRepo() error { return os.RemoveAll(testRepoDir) } gitbatch-0.5.0/core/command/commit.go000066400000000000000000000036451353172561300175040ustar00rootroot00000000000000package command import ( "errors" "time" giterr "github.com/isacikgoz/gitbatch/core/errors" "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" ) // CommitOptions defines the rules for commit operation type CommitOptions struct { // CommitMsg CommitMsg string // User User string // Email Email string // Mode is the command mode CommandMode Mode } // Commit defines which commit command to use. func Commit(r *git.Repository, o *CommitOptions) (err error) { // here we configure commit operation switch o.CommandMode { case ModeLegacy: return commitWithGit(r, o) case ModeNative: return commitWithGoGit(r, o) } return errors.New("Unhandled commit operation") } // commitWithGit is simply a bare git commit -m command which is flexible func commitWithGit(r *git.Repository, opt *CommitOptions) (err error) { args := make([]string, 0) args = append(args, "commit") args = append(args, "-m") // parse options to command line arguments if len(opt.CommitMsg) > 0 { args = append(args, opt.CommitMsg) } if out, err := Run(r.AbsPath, "git", args); err != nil { log.Warn("Error at git command (commit)") r.Refresh() return giterr.ParseGitError(out, err) } // till this step everything should be ok return r.Refresh() } // commitWithGoGit is the primary commit method func commitWithGoGit(r *git.Repository, options *CommitOptions) (err error) { opt := &gogit.CommitOptions{ Author: &object.Signature{ Name: options.User, Email: options.Email, When: time.Now(), }, Committer: &object.Signature{ Name: options.User, Email: options.Email, When: time.Now(), }, } w, err := r.Repo.Worktree() if err != nil { return err } _, err = w.Commit(options.CommitMsg, opt) if err != nil { r.Refresh() return err } // till this step everything should be ok return r.Refresh() } gitbatch-0.5.0/core/command/commit_test.go000066400000000000000000000026631353172561300205420ustar00rootroot00000000000000package command import ( "testing" giterr "github.com/isacikgoz/gitbatch/core/errors" "github.com/isacikgoz/gitbatch/core/git" ) var ( testCommitopt1 = &CommitOptions{ CommitMsg: "test", User: "foo", Email: "foo@bar.com", } ) func TestCommitWithGit(t *testing.T) { defer cleanRepo() r, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } f, err := testFile("file") if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } if err := addWithGit(r, f, testAddopt1); err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 *git.Repository inp2 *CommitOptions }{ {r, testCommitopt1}, } for _, test := range tests { if err := commitWithGit(test.inp1, test.inp2); err != nil && err == giterr.ErrUserEmailNotSet { t.Errorf("Test Failed.") } } } func TestCommitWithGoGit(t *testing.T) { defer cleanRepo() r, err := testRepo() if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } f, err := testFile("file") if err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } if err := addWithGit(r, f, testAddopt1); err != nil { t.Fatalf("Test Failed. error: %s", err.Error()) } var tests = []struct { inp1 *git.Repository inp2 *CommitOptions }{ {r, testCommitopt1}, } for _, test := range tests { if err := commitWithGoGit(test.inp1, test.inp2); err != nil { t.Errorf("Test Failed.") } } } gitbatch-0.5.0/core/command/config.go000066400000000000000000000052331353172561300174540ustar00rootroot00000000000000package command import ( "errors" "github.com/isacikgoz/gitbatch/core/git" log "github.com/sirupsen/logrus" ) // ConfigOptions defines the rules for commit operation type ConfigOptions struct { // Section Section string // Option Option string // Site should be Global or Local Site ConfigSite // Mode is the command mode CommandMode Mode } // 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(r *git.Repository, o *ConfigOptions) (value string, err error) { // here we configure config operation switch o.CommandMode { case ModeLegacy: return configWithGit(r, o) case ModeNative: return configWithGoGit(r, o) } return value, errors.New("Unhandled config operation") } // configWithGit is simply a bare git config --site