├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd └── gitbatch │ └── main.go ├── go.mod ├── go.sum └── internal ├── app ├── builder.go ├── builder_test.go ├── config.go ├── config_test.go ├── files.go ├── files_test.go ├── quick.go └── quick_test.go ├── command ├── add.go ├── add_test.go ├── checkout.go ├── checkout_test.go ├── cmd.go ├── cmd_test.go ├── commit.go ├── commit_test.go ├── config.go ├── config_test.go ├── diff.go ├── diff_test.go ├── fetch.go ├── fetch_test.go ├── merge.go ├── merge_test.go ├── pull.go ├── pull_test.go ├── reset.go ├── reset_test.go ├── status.go └── status_test.go ├── errors ├── errors.go └── errors_test.go ├── git ├── authentication.go ├── authentication_test.go ├── branch.go ├── branch_test.go ├── commit.go ├── file.go ├── helper.go ├── random.go ├── random_test.go ├── remote.go ├── remotebranch.go ├── repository.go ├── sort.go └── stash.go ├── gui ├── authenticationview.go ├── batchbranchesview.go ├── commitview.go ├── controlsview.go ├── dynamickeybindings.go ├── dynamicview.go ├── errorview.go ├── extensions.go ├── focus.go ├── focusviews.go ├── gui.go ├── keybindings.go ├── overview.go ├── repositoriesview.go ├── sideviews.go ├── stashview.go ├── statusview.go └── text-renderer.go ├── job ├── job.go ├── job_test.go ├── queue.go └── queue_test.go ├── load ├── load.go └── load_test.go └── testlib ├── data.go └── test-data-master.zip /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | name: CI 3 | jobs: 4 | test: 5 | env: 6 | GOPATH: ${{ github.workspace }} 7 | 8 | defaults: 9 | run: 10 | working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Checkout Code 20 | uses: actions/checkout@v2 21 | with: 22 | path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} 23 | - name: Execute Tests 24 | run: | 25 | go get -d -t ./... 26 | go test ./... -coverprofile=coverage.txt -covermode=atomic 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v3 29 | with: 30 | directory: . 31 | fail_ci_if_error: true 32 | files: coverage.txt 33 | flags: unittests 34 | verbose: true 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code into $GITHUB_WORKSPACE directory 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.18 23 | id: go 24 | 25 | - name: GoReleaser 26 | uses: goreleaser/goreleaser-action@v2 27 | with: 28 | version: latest 29 | args: release --rm-dist 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | exec.go.test 2 | build.sh 3 | test.go 4 | .vscode 5 | build/ 6 | coverage.txt 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | # You may remove this if you don't use go modules. 4 | - go mod download 5 | builds: 6 | - 7 | # Path to main.go file or main package. Default is `.`. 8 | main: ./cmd/gitbatch/main.go 9 | env: 10 | - CGO_ENABLED=0 11 | # GOOS list to build for. Defaults are darwin and linux. 12 | # For more info: https://golang.org/doc/install/source#environment 13 | goos: 14 | - darwin 15 | - linux 16 | - windows 17 | 18 | brews: 19 | - 20 | tap: 21 | owner: isacikgoz 22 | name: homebrew-taps 23 | homepage: "{{ .GitURL }}" 24 | description: Manage your git repositories in one place 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ibrahim Serdar Acikgoz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://img.shields.io/github/actions/workflow/status/isacikgoz/gitbatch/ci.yml) [![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) 2 | 3 | ## gitbatch 4 | 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.*) 5 | 6 | Check out the screencast of the app: 7 | [![asciicast](https://asciinema.org/a/lxoZT6Z8fSliIEebWSPVIY8ct.svg)](https://asciinema.org/a/lxoZT6Z8fSliIEebWSPVIY8ct) 8 | 9 | ## Installation 10 | 11 | Install [latest](https://golang.org/dl/) Golang release. 12 | 13 | To install with go, run the following command; 14 | ```bash 15 | go get github.com/isacikgoz/gitbatch/cmd/gitbatch 16 | ``` 17 | or, in Windows 10: 18 | ```bash 19 | go install github.com/isacikgoz/gitbatch/cmd/gitbatch@latest 20 | ``` 21 | 22 | ### MacOS using homebrew 23 | ```bash 24 | brew install gitbatch 25 | ``` 26 | For other options see [installation page](https://github.com/isacikgoz/gitbatch/wiki/Installation) 27 | 28 | ## Use 29 | run the `gitbatch` command from the parent of your git repositories. For start-up options simply `gitbatch --help` 30 | 31 | For more information see the [wiki pages](https://github.com/isacikgoz/gitbatch/wiki) 32 | 33 | ## Further goals 34 | - improve testing 35 | - add push 36 | - full src-d/go-git integration (*having some performance issues in large repos*) 37 | - fetch, config, rev-list, add, reset, commit, status and diff commands are supported but not fully utilized, still using git occasionally 38 | - merge, stash are not supported yet by go-git 39 | 40 | ## Credits 41 | - [go-git](https://github.com/src-d/go-git) for git interface (partially) 42 | - [gocui](https://github.com/jroimartin/gocui) for user interface 43 | - [viper](https://github.com/spf13/viper) for configuration management 44 | - [color](https://github.com/fatih/color) for colored text 45 | - [kingpin](https://github.com/alecthomas/kingpin) for command-line flag&options 46 | 47 | -------------------------------------------------------------------------------- /cmd/gitbatch/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/alecthomas/kingpin" 8 | "github.com/isacikgoz/gitbatch/internal/app" 9 | ) 10 | 11 | func main() { 12 | kingpin.Version("gitbatch version 0.6.1") 13 | 14 | dirs := kingpin.Flag("directory", "Directory(s) to roam for git repositories.").Short('d').Strings() 15 | mode := kingpin.Flag("mode", "Application start mode, more sensible with quick run.").Short('m').String() 16 | recursionDepth := kingpin.Flag("recursive-depth", "Find directories recursively.").Default("0").Short('r').Int() 17 | logLevel := kingpin.Flag("log-level", "Logging level; trace,debug,info,warn,error").Default("error").Short('l').String() 18 | quick := kingpin.Flag("quick", "runs without gui and fetches/pull remote upstream.").Short('q').Bool() 19 | 20 | kingpin.Parse() 21 | 22 | if err := run(*dirs, *logLevel, *recursionDepth, *quick, *mode); err != nil { 23 | fmt.Fprintf(os.Stderr, "application quitted with an unhandled error: %v", err) 24 | os.Exit(1) 25 | } 26 | } 27 | 28 | func run(dirs []string, log string, depth int, quick bool, mode string) error { 29 | app, err := app.New(&app.Config{ 30 | Directories: dirs, 31 | LogLevel: log, 32 | Depth: depth, 33 | QuickMode: quick, 34 | Mode: mode, 35 | }) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | return app.Run() 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/isacikgoz/gitbatch 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alecthomas/kingpin v2.2.6+incompatible 7 | github.com/fatih/color v1.13.0 8 | github.com/go-git/go-git/v5 v5.5.2 9 | github.com/jroimartin/gocui v0.5.0 10 | github.com/spf13/viper v1.14.0 11 | github.com/stretchr/testify v1.8.1 12 | golang.org/x/sync v0.1.0 13 | ) 14 | 15 | require ( 16 | github.com/Microsoft/go-winio v0.6.0 // indirect 17 | github.com/ProtonMail/go-crypto v0.0.0-20230113180642-068501e20d67 // indirect 18 | github.com/acomagu/bufpipe v1.0.3 // indirect 19 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 20 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 21 | github.com/cloudflare/circl v1.3.1 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/emirpasic/gods v1.18.1 // indirect 24 | github.com/fsnotify/fsnotify v1.6.0 // indirect 25 | github.com/go-git/gcfg v1.5.0 // indirect 26 | github.com/go-git/go-billy/v5 v5.4.0 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/imdario/mergo v0.3.13 // indirect 29 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 30 | github.com/kevinburke/ssh_config v1.2.0 // indirect 31 | github.com/magiconair/properties v1.8.7 // indirect 32 | github.com/mattn/go-colorable v0.1.13 // indirect 33 | github.com/mattn/go-isatty v0.0.17 // indirect 34 | github.com/mattn/go-runewidth v0.0.14 // indirect 35 | github.com/mitchellh/mapstructure v1.5.0 // indirect 36 | github.com/nsf/termbox-go v1.1.1 // indirect 37 | github.com/pelletier/go-toml v1.9.5 // indirect 38 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 39 | github.com/pjbgf/sha1cd v0.2.3 // indirect 40 | github.com/pmezard/go-difflib v1.0.0 // indirect 41 | github.com/rivo/uniseg v0.4.3 // indirect 42 | github.com/sergi/go-diff v1.3.1 // indirect 43 | github.com/skeema/knownhosts v1.1.0 // indirect 44 | github.com/spf13/afero v1.9.3 // indirect 45 | github.com/spf13/cast v1.5.0 // indirect 46 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 47 | github.com/spf13/pflag v1.0.5 // indirect 48 | github.com/subosito/gotenv v1.4.2 // indirect 49 | github.com/xanzy/ssh-agent v0.3.3 // indirect 50 | golang.org/x/crypto v0.5.0 // indirect 51 | golang.org/x/mod v0.7.0 // indirect 52 | golang.org/x/net v0.5.0 // indirect 53 | golang.org/x/sys v0.4.0 // indirect 54 | golang.org/x/text v0.6.0 // indirect 55 | golang.org/x/tools v0.5.0 // indirect 56 | gopkg.in/ini.v1 v1.67.0 // indirect 57 | gopkg.in/warnings.v0 v0.1.2 // indirect 58 | gopkg.in/yaml.v2 v2.4.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /internal/app/builder.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/isacikgoz/gitbatch/internal/gui" 8 | ) 9 | 10 | // The App struct is responsible to hold app-wide related entities. Currently 11 | // it has only the gui.Gui pointer for interface entity. 12 | type App struct { 13 | Config *Config 14 | } 15 | 16 | // Config is an assembler data to initiate a setup 17 | type Config struct { 18 | Directories []string 19 | LogLevel string 20 | Depth int 21 | QuickMode bool 22 | Mode string 23 | } 24 | 25 | // New will handle pre-required operations. It is designed to be a wrapper for 26 | // main method right now. 27 | func New(argConfig *Config) (*App, error) { 28 | // initiate the app and give it initial values 29 | app := &App{} 30 | if len(argConfig.Directories) <= 0 { 31 | d, _ := os.Getwd() 32 | argConfig.Directories = []string{d} 33 | } 34 | presetConfig, err := loadConfiguration() 35 | if err != nil { 36 | return nil, err 37 | } 38 | app.Config = overrideConfig(presetConfig, argConfig) 39 | 40 | return app, nil 41 | } 42 | 43 | // Run starts the application. 44 | func (a *App) Run() error { 45 | dirs := generateDirectories(a.Config.Directories, a.Config.Depth) 46 | if a.Config.QuickMode { 47 | return a.execQuickMode(dirs) 48 | } 49 | // create a gui.Gui struct and run the gui 50 | gui, err := gui.New(a.Config.Mode, dirs) 51 | if err != nil { 52 | return err 53 | } 54 | return gui.Run() 55 | } 56 | 57 | func overrideConfig(appConfig, setupConfig *Config) *Config { 58 | if len(setupConfig.Directories) > 0 { 59 | appConfig.Directories = setupConfig.Directories 60 | } 61 | if len(setupConfig.LogLevel) > 0 { 62 | appConfig.LogLevel = setupConfig.LogLevel 63 | } 64 | if setupConfig.Depth > 0 { 65 | appConfig.Depth = setupConfig.Depth 66 | } 67 | if setupConfig.QuickMode { 68 | appConfig.QuickMode = setupConfig.QuickMode 69 | } 70 | if len(setupConfig.Mode) > 0 { 71 | appConfig.Mode = setupConfig.Mode 72 | } 73 | return appConfig 74 | } 75 | 76 | func (a *App) execQuickMode(directories []string) error { 77 | if a.Config.Mode != "fetch" && a.Config.Mode != "pull" { 78 | return fmt.Errorf("unrecognized quick mode: " + a.Config.Mode) 79 | } 80 | 81 | return quick(directories, a.Config.Mode) 82 | } 83 | -------------------------------------------------------------------------------- /internal/app/builder_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/isacikgoz/gitbatch/internal/git" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestOverrideConfig(t *testing.T) { 12 | config1 := &Config{ 13 | Directories: []string{}, 14 | LogLevel: "info", 15 | Depth: 1, 16 | QuickMode: false, 17 | Mode: "fetch", 18 | } 19 | config2 := &Config{ 20 | Directories: []string{string(os.PathSeparator) + "tmp"}, 21 | LogLevel: "error", 22 | Depth: 1, 23 | QuickMode: true, 24 | Mode: "pull", 25 | } 26 | 27 | var tests = []struct { 28 | inp1 *Config 29 | inp2 *Config 30 | expected *Config 31 | }{ 32 | {config1, config2, config1}, 33 | } 34 | for _, test := range tests { 35 | output := overrideConfig(test.inp1, test.inp2) 36 | require.Equal(t, test.expected, output) 37 | require.Equal(t, test.inp2.Mode, output.Mode) 38 | } 39 | } 40 | 41 | func TestExecQuickMode(t *testing.T) { 42 | th := git.InitTestRepositoryFromLocal(t) 43 | defer th.CleanUp(t) 44 | 45 | var tests = []struct { 46 | inp1 []string 47 | }{ 48 | {[]string{th.BasicRepoPath()}}, 49 | } 50 | a := App{ 51 | Config: &Config{ 52 | Mode: "fetch", 53 | }, 54 | } 55 | for _, test := range tests { 56 | err := a.execQuickMode(test.inp1) 57 | require.NoError(t, err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/app/config.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // config file stuff 12 | var ( 13 | configFileName = "config" 14 | configFileExt = ".yml" 15 | configType = "yaml" 16 | appName = "gitbatch" 17 | 18 | configurationDirectory = filepath.Join(osConfigDirectory(runtime.GOOS), appName) 19 | configFileAbsPath = filepath.Join(configurationDirectory, configFileName) 20 | ) 21 | 22 | // configuration items 23 | var ( 24 | modeKey = "mode" 25 | modeKeyDefault = "fetch" 26 | pathsKey = "paths" 27 | quickKey = "quick" 28 | quickKeyDefault = false 29 | recursionKey = "recursion" 30 | recursionKeyDefault = 1 31 | ) 32 | 33 | // loadConfiguration returns a Config struct is filled 34 | func loadConfiguration() (*Config, error) { 35 | if err := initializeConfigurationManager(); err != nil { 36 | return nil, err 37 | } 38 | if err := setDefaults(); err != nil { 39 | return nil, err 40 | } 41 | if err := readConfiguration(); err != nil { 42 | return nil, err 43 | } 44 | var directories []string 45 | if len(viper.GetStringSlice(pathsKey)) <= 0 { 46 | d, _ := os.Getwd() 47 | directories = []string{d} 48 | } else { 49 | directories = viper.GetStringSlice(pathsKey) 50 | } 51 | config := &Config{ 52 | Directories: directories, 53 | Depth: viper.GetInt(recursionKey), 54 | QuickMode: viper.GetBool(quickKey), 55 | Mode: viper.GetString(modeKey), 56 | } 57 | return config, nil 58 | } 59 | 60 | // set default configuration parameters 61 | func setDefaults() error { 62 | viper.SetDefault(quickKey, quickKeyDefault) 63 | viper.SetDefault(recursionKey, recursionKeyDefault) 64 | viper.SetDefault(modeKey, modeKeyDefault) 65 | // viper.SetDefault(pathsKey, pathsKeyDefault) 66 | return nil 67 | } 68 | 69 | // read configuration from file 70 | func readConfiguration() error { 71 | err := viper.ReadInConfig() // Find and read the config file 72 | if err != nil { // Handle errors reading the config file 73 | // if file does not exist, simply create one 74 | if _, err := os.Stat(configFileAbsPath + configFileExt); os.IsNotExist(err) { 75 | if err = os.MkdirAll(configurationDirectory, 0755); err != nil { 76 | return err 77 | } 78 | f, err := os.Create(configFileAbsPath + configFileExt) 79 | if err != nil { 80 | return err 81 | } 82 | defer f.Close() 83 | } else { 84 | return err 85 | } 86 | // let's write defaults 87 | if err := viper.WriteConfig(); err != nil { 88 | return err 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | // initialize the configuration manager 95 | func initializeConfigurationManager() error { 96 | // config viper 97 | viper.AddConfigPath(configurationDirectory) 98 | viper.SetConfigName(configFileName) 99 | viper.SetConfigType(configType) 100 | 101 | return nil 102 | } 103 | 104 | // returns OS dependent config directory 105 | func osConfigDirectory(osName string) (osConfigDirectory string) { 106 | switch osName { 107 | case "windows": 108 | osConfigDirectory = os.Getenv("APPDATA") 109 | case "darwin": 110 | osConfigDirectory = os.Getenv("HOME") + "/Library/Application Support" 111 | case "linux": 112 | osConfigDirectory = os.Getenv("HOME") + "/.config" 113 | } 114 | return osConfigDirectory 115 | } 116 | -------------------------------------------------------------------------------- /internal/app/config_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestLoadConfiguration(t *testing.T) { 10 | cfg, err := loadConfiguration() 11 | require.NoError(t, err) 12 | require.NotNil(t, cfg) 13 | } 14 | 15 | func TestReadConfiguration(t *testing.T) { 16 | err := readConfiguration() 17 | require.NoError(t, err) 18 | } 19 | 20 | func TestInitializeConfigurationManager(t *testing.T) { 21 | err := initializeConfigurationManager() 22 | require.NoError(t, err) 23 | } 24 | 25 | func TestOsConfigDirectory(t *testing.T) { 26 | var tests = []struct { 27 | input string 28 | expected string 29 | }{ 30 | {"linux", ".config"}, 31 | {"darwin", "Application Support"}, 32 | } 33 | for _, test := range tests { 34 | output := osConfigDirectory(test.input) 35 | require.Contains(t, output, test.expected) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/files.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // generateDirectories returns possible git repositories to pipe into git pkg 10 | // load function 11 | func generateDirectories(dirs []string, depth int) []string { 12 | gitDirs := make([]string, 0) 13 | for i := 0; i < depth; i++ { 14 | directories, repositories := walkRecursive(dirs, gitDirs) 15 | dirs = directories 16 | gitDirs = repositories 17 | } 18 | return gitDirs 19 | } 20 | 21 | // returns given values, first search directories and second stands for possible 22 | // git repositories. Call this func from a "for i := 0; i= len(search) { 27 | continue 28 | } 29 | // find possible repositories and remaining ones, b slice is possible ones 30 | a, b, err := separateDirectories(search[i]) 31 | if err != nil { 32 | continue 33 | } 34 | // since we started to search let's get rid of it and remove from search 35 | // array 36 | search[i] = search[len(search)-1] 37 | search = search[:len(search)-1] 38 | // lets append what we have found to continue recursion 39 | search = append(search, a...) 40 | appendant = append(appendant, b...) 41 | } 42 | return search, appendant 43 | } 44 | 45 | // separateDirectories is to find all the files in given path. This method 46 | // does not check if the given file is a valid git repositories 47 | func separateDirectories(directory string) ([]string, []string, error) { 48 | dirs := make([]string, 0) 49 | gitDirs := make([]string, 0) 50 | files, err := ioutil.ReadDir(directory) 51 | // can we read the directory? 52 | if err != nil { 53 | return nil, nil, nil 54 | } 55 | for _, f := range files { 56 | repo := directory + string(os.PathSeparator) + f.Name() 57 | file, err := os.Open(repo) 58 | // if we cannot open it, simply continue to iteration and don't consider 59 | if err != nil { 60 | file.Close() 61 | continue 62 | } 63 | dir, err := filepath.Abs(file.Name()) 64 | if err != nil { 65 | file.Close() 66 | continue 67 | } 68 | // with this approach, we ignore submodule or sub repositories in a git repository 69 | ff, err := os.Open(dir + string(os.PathSeparator) + ".git") 70 | if err != nil { 71 | dirs = append(dirs, dir) 72 | } else { 73 | gitDirs = append(gitDirs, dir) 74 | } 75 | ff.Close() 76 | file.Close() 77 | 78 | } 79 | return dirs, gitDirs, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/app/files_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/isacikgoz/gitbatch/internal/git" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestGenerateDirectories(t *testing.T) { 12 | th := git.InitTestRepositoryFromLocal(t) 13 | defer th.CleanUp(t) 14 | 15 | var tests = []struct { 16 | inp1 []string 17 | inp2 int 18 | expected []string 19 | }{ 20 | {[]string{th.RepoPath}, 1, []string{th.BasicRepoPath(), th.DirtyRepoPath()}}, 21 | {[]string{th.RepoPath}, 2, []string{th.BasicRepoPath(), th.DirtyRepoPath()}}, // maybe move one repo to a sub folder 22 | } 23 | for _, test := range tests { 24 | output := generateDirectories(test.inp1, test.inp2) 25 | require.ElementsMatch(t, output, test.expected) 26 | } 27 | } 28 | 29 | func TestWalkRecursive(t *testing.T) { 30 | th := git.InitTestRepositoryFromLocal(t) 31 | defer th.CleanUp(t) 32 | 33 | var tests = []struct { 34 | inp1 []string 35 | inp2 []string 36 | exp1 []string 37 | exp2 []string 38 | }{ 39 | { 40 | []string{th.RepoPath}, 41 | []string{""}, 42 | []string{filepath.Join(th.RepoPath, ".git"), filepath.Join(th.RepoPath, ".gitmodules"), th.NonRepoPath()}, 43 | []string{"", th.BasicRepoPath(), th.DirtyRepoPath()}, 44 | }, 45 | } 46 | for _, test := range tests { 47 | out1, out2 := walkRecursive(test.inp1, test.inp2) 48 | require.ElementsMatch(t, out1, test.exp1) 49 | require.ElementsMatch(t, out2, test.exp2) 50 | } 51 | } 52 | 53 | func TestSeparateDirectories(t *testing.T) { 54 | th := git.InitTestRepositoryFromLocal(t) 55 | defer th.CleanUp(t) 56 | 57 | var tests = []struct { 58 | input string 59 | exp1 []string 60 | exp2 []string 61 | }{ 62 | { 63 | "", 64 | nil, 65 | nil, 66 | }, 67 | { 68 | th.RepoPath, 69 | []string{filepath.Join(th.RepoPath, ".git"), filepath.Join(th.RepoPath, ".gitmodules"), th.NonRepoPath()}, 70 | []string{th.BasicRepoPath(), th.DirtyRepoPath()}, 71 | }, 72 | } 73 | for _, test := range tests { 74 | out1, out2, err := separateDirectories(test.input) 75 | require.NoError(t, err) 76 | require.ElementsMatch(t, out1, test.exp1) 77 | require.ElementsMatch(t, out2, test.exp2) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/app/quick.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/isacikgoz/gitbatch/internal/command" 9 | "github.com/isacikgoz/gitbatch/internal/git" 10 | ) 11 | 12 | func quick(directories []string, mode string) error { 13 | var wg sync.WaitGroup 14 | start := time.Now() 15 | for _, dir := range directories { 16 | wg.Add(1) 17 | go func(d string, mode string) { 18 | defer wg.Done() 19 | if err := operate(d, mode); err != nil { 20 | fmt.Printf("could not perform %s on %s: %s", mode, d, err) 21 | } 22 | fmt.Printf("%s: successful\n", d) 23 | }(dir, mode) 24 | } 25 | wg.Wait() 26 | elapsed := time.Since(start) 27 | fmt.Printf("%d repositories finished in: %s\n", len(directories), elapsed) 28 | return nil 29 | } 30 | 31 | func operate(directory, mode string) error { 32 | r, err := git.FastInitializeRepo(directory) 33 | if err != nil { 34 | return err 35 | } 36 | switch mode { 37 | case "fetch": 38 | return command.Fetch(r, &command.FetchOptions{ 39 | RemoteName: "origin", 40 | Progress: true, 41 | }) 42 | case "pull": 43 | return command.Pull(r, &command.PullOptions{ 44 | RemoteName: "origin", 45 | Progress: true, 46 | }) 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/app/quick_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/isacikgoz/gitbatch/internal/git" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestQuick(t *testing.T) { 11 | th := git.InitTestRepositoryFromLocal(t) 12 | defer th.CleanUp(t) 13 | 14 | var tests = []struct { 15 | inp1 []string 16 | inp2 string 17 | }{ 18 | { 19 | []string{th.DirtyRepoPath()}, 20 | "fetch", 21 | }, { 22 | []string{th.DirtyRepoPath()}, 23 | "pull", 24 | }, 25 | } 26 | for _, test := range tests { 27 | err := quick(test.inp1, test.inp2) 28 | require.NoError(t, err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/command/add.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/isacikgoz/gitbatch/internal/git" 7 | ) 8 | 9 | // AddOptions defines the rules for "git add" command 10 | type AddOptions struct { 11 | // Update 12 | Update bool 13 | // Force 14 | Force bool 15 | // DryRun 16 | DryRun bool 17 | // Mode is the command mode 18 | CommandMode Mode 19 | } 20 | 21 | // Add is a wrapper function for "git add" command 22 | func Add(r *git.Repository, f *git.File, o *AddOptions) error { 23 | mode := o.CommandMode 24 | if o.Update || o.Force || o.DryRun { 25 | mode = ModeLegacy 26 | } 27 | switch mode { 28 | case ModeLegacy: 29 | err := addWithGit(r, f, o) 30 | return err 31 | case ModeNative: 32 | err := addWithGoGit(r, f) 33 | return err 34 | } 35 | return fmt.Errorf("unhandled add operation") 36 | } 37 | 38 | // AddAll function is the wrapper of "git add ." command 39 | func AddAll(r *git.Repository, o *AddOptions) error { 40 | args := make([]string, 0) 41 | args = append(args, "add") 42 | if o.DryRun { 43 | args = append(args, "--dry-run") 44 | } 45 | args = append(args, ".") 46 | _, err := Run(r.AbsPath, "git", args) 47 | if err != nil { 48 | return fmt.Errorf("could not run add function: %v", err) 49 | } 50 | return nil 51 | } 52 | 53 | func addWithGit(r *git.Repository, f *git.File, o *AddOptions) error { 54 | args := make([]string, 0) 55 | args = append(args, "add") 56 | args = append(args, f.Name) 57 | if o.Update { 58 | args = append(args, "--update") 59 | } 60 | if o.Force { 61 | args = append(args, "--force") 62 | } 63 | if o.DryRun { 64 | args = append(args, "--dry-run") 65 | } 66 | _, err := Run(r.AbsPath, "git", args) 67 | if err != nil { 68 | return fmt.Errorf("could not add %s: %v", f.AbsPath, err) 69 | } 70 | return nil 71 | } 72 | 73 | func addWithGoGit(r *git.Repository, f *git.File) error { 74 | w, err := r.Repo.Worktree() 75 | if err != nil { 76 | return err 77 | } 78 | _, err = w.Add(f.Name) 79 | return err 80 | } 81 | -------------------------------------------------------------------------------- /internal/command/add_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/isacikgoz/gitbatch/internal/git" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var ( 11 | testAddopt1 = &AddOptions{} 12 | ) 13 | 14 | func TestAddAll(t *testing.T) { 15 | th := git.InitTestRepositoryFromLocal(t) 16 | defer th.CleanUp(t) 17 | 18 | _, err := testFile(th.RepoPath, "file") 19 | require.NoError(t, err) 20 | 21 | var tests = []struct { 22 | inp1 *git.Repository 23 | inp2 *AddOptions 24 | }{ 25 | {th.Repository, testAddopt1}, 26 | } 27 | for _, test := range tests { 28 | err := AddAll(test.inp1, test.inp2) 29 | require.NoError(t, err) 30 | } 31 | } 32 | 33 | func TestAddWithGit(t *testing.T) { 34 | th := git.InitTestRepositoryFromLocal(t) 35 | defer th.CleanUp(t) 36 | 37 | f, err := testFile(th.RepoPath, "file") 38 | require.NoError(t, err) 39 | 40 | var tests = []struct { 41 | inp1 *git.Repository 42 | inp2 *git.File 43 | inp3 *AddOptions 44 | }{ 45 | {th.Repository, f, testAddopt1}, 46 | } 47 | for _, test := range tests { 48 | err := addWithGit(test.inp1, test.inp2, test.inp3) 49 | require.NoError(t, err) 50 | } 51 | } 52 | 53 | func TestAddWithGoGit(t *testing.T) { 54 | th := git.InitTestRepositoryFromLocal(t) 55 | defer th.CleanUp(t) 56 | 57 | f, err := testFile(th.RepoPath, "file") 58 | require.NoError(t, err) 59 | 60 | var tests = []struct { 61 | inp1 *git.Repository 62 | inp2 *git.File 63 | }{ 64 | {th.Repository, f}, 65 | } 66 | for _, test := range tests { 67 | err := addWithGoGit(test.inp1, test.inp2) 68 | require.NoError(t, err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/command/checkout.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/isacikgoz/gitbatch/internal/git" 7 | ) 8 | 9 | // CheckoutOptions defines the rules of checkout command 10 | type CheckoutOptions struct { 11 | TargetRef string 12 | CreateIfAbsent bool 13 | CommandMode Mode 14 | } 15 | 16 | // Checkout is a wrapper function for "git checkout" command. 17 | func Checkout(r *git.Repository, o *CheckoutOptions) error { 18 | var branch *git.Branch 19 | for _, b := range r.Branches { 20 | if b.Name == o.TargetRef { 21 | branch = b 22 | break 23 | } 24 | } 25 | msg := "checkout in progress" 26 | if branch != nil { 27 | if err := r.Checkout(branch); err != nil { 28 | r.SetWorkStatus(git.Fail) 29 | msg = err.Error() 30 | } else { 31 | r.SetWorkStatus(git.Success) 32 | msg = "switched to " + o.TargetRef 33 | } 34 | } else if o.CreateIfAbsent { 35 | args := []string{"checkout", "-b", o.TargetRef} 36 | cmd := exec.Command("git", args...) 37 | cmd.Dir = r.AbsPath 38 | _, err := cmd.CombinedOutput() 39 | if err != nil { 40 | r.SetWorkStatus(git.Fail) 41 | msg = err.Error() 42 | } else { 43 | r.SetWorkStatus(git.Success) 44 | msg = "switched to " + o.TargetRef 45 | } 46 | } 47 | r.State.Message = msg 48 | return r.Refresh() 49 | } 50 | -------------------------------------------------------------------------------- /internal/command/checkout_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/isacikgoz/gitbatch/internal/git" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCheckout(t *testing.T) { 11 | opts1 := &CheckoutOptions{ 12 | TargetRef: "master", 13 | CreateIfAbsent: false, 14 | CommandMode: ModeLegacy, 15 | } 16 | opts2 := &CheckoutOptions{ 17 | TargetRef: "develop", 18 | CreateIfAbsent: true, 19 | CommandMode: ModeLegacy, 20 | } 21 | 22 | th := git.InitTestRepositoryFromLocal(t) 23 | defer th.CleanUp(t) 24 | 25 | var tests = []struct { 26 | inp1 *git.Repository 27 | inp2 *CheckoutOptions 28 | }{ 29 | {th.Repository, opts1}, 30 | {th.Repository, opts2}, 31 | } 32 | for _, test := range tests { 33 | err := Checkout(test.inp1, test.inp2) 34 | require.NoError(t, err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/command/cmd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | "strings" 7 | "syscall" 8 | ) 9 | 10 | // Mode indicates that whether command should run native code or use git 11 | // command to operate. 12 | type Mode uint8 13 | 14 | const ( 15 | // ModeLegacy uses traditional git command line tool to operate 16 | ModeLegacy = iota 17 | // ModeNative uses native implementation of given git command 18 | ModeNative 19 | ) 20 | 21 | // Run runs the OS command and return its output. If the output 22 | // returns error it also encapsulates it as a golang.error which is a return code 23 | // of the command except zero 24 | func Run(d string, c string, args []string) (string, error) { 25 | cmd := exec.Command(c, args...) 26 | if d != "" { 27 | cmd.Dir = d 28 | } 29 | output, err := cmd.CombinedOutput() 30 | return trimTrailingNewline(string(output)), err 31 | } 32 | 33 | // Return returns if we supposed to get return value as an int of a command 34 | // this method can be used. It is practical when you use a command and process a 35 | // failover according to a specific return code 36 | func Return(d string, c string, args []string) (int, error) { 37 | cmd := exec.Command(c, args...) 38 | if d != "" { 39 | cmd.Dir = d 40 | } 41 | var err error 42 | // this time the execution is a little different 43 | if err := cmd.Start(); err != nil { 44 | return -1, err 45 | } 46 | if err := cmd.Wait(); err != nil { 47 | if exiterr, ok := err.(*exec.ExitError); ok { 48 | // The program has exited with an exit code != 0 49 | 50 | // This works on both Unix and Windows. Although package 51 | // syscall is generally platform dependent, WaitStatus is 52 | // defined for both Unix and Windows and in both cases has 53 | // an ExitStatus() method with the same signature. 54 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 55 | statusCode := status.ExitStatus() 56 | return statusCode, err 57 | } 58 | } else { 59 | log.Fatalf("cmd.Wait: %v", err) 60 | } 61 | } 62 | return -1, err 63 | } 64 | 65 | // trimTrailingNewline removes the trailing new line form a string. this method 66 | // is used mostly on outputs of a command 67 | func trimTrailingNewline(s string) string { 68 | if strings.HasSuffix(s, "\n") || strings.HasSuffix(s, "\r") { 69 | return s[:len(s)-1] 70 | } 71 | return s 72 | } 73 | -------------------------------------------------------------------------------- /internal/command/cmd_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/isacikgoz/gitbatch/internal/git" 8 | ) 9 | 10 | func TestRun(t *testing.T) { 11 | wd, err := os.Getwd() 12 | if err != nil { 13 | t.Fatalf("Test Failed.") 14 | } 15 | var tests = []struct { 16 | inp1 string 17 | inp2 string 18 | inp3 []string 19 | }{ 20 | {wd, "git", []string{"status"}}, 21 | } 22 | for _, test := range tests { 23 | if output, err := Run(test.inp1, test.inp2, test.inp3); err != nil || len(output) <= 0 { 24 | t.Errorf("Test Failed. {%s, %s, %s} inputted, output: %s", test.inp1, test.inp2, test.inp3, output) 25 | } 26 | } 27 | } 28 | 29 | func TestReturn(t *testing.T) { 30 | wd, err := os.Getwd() 31 | if err != nil { 32 | t.Fatalf("Test Failed.") 33 | } 34 | var tests = []struct { 35 | inp1 string 36 | inp2 string 37 | inp3 []string 38 | expected int 39 | }{ 40 | {wd, "foo", []string{}, -1}, 41 | } 42 | for _, test := range tests { 43 | if output, _ := Return(test.inp1, test.inp2, test.inp3); output != test.expected { 44 | t.Errorf("Test Failed. {%s, %s, %s} inputted, output: %d, expected : %d", test.inp1, test.inp2, test.inp3, output, test.expected) 45 | } 46 | } 47 | } 48 | 49 | func TestTrimTrailingNewline(t *testing.T) { 50 | var tests = []struct { 51 | input string 52 | expected string 53 | }{ 54 | {"foo", "foo"}, 55 | {"foo\n", "foo"}, 56 | {"foo\r", "foo"}, 57 | } 58 | for _, test := range tests { 59 | if output := trimTrailingNewline(test.input); output != test.expected { 60 | t.Errorf("Test Failed. %s inputted, output: %s, expected: %s", test.input, output, test.expected) 61 | } 62 | } 63 | } 64 | 65 | func testFile(testRepoDir, name string) (*git.File, error) { 66 | _, err := os.Create(testRepoDir + string(os.PathSeparator) + name) 67 | if err != nil { 68 | return nil, err 69 | } 70 | f := &git.File{ 71 | Name: name, 72 | AbsPath: testRepoDir + string(os.PathSeparator) + name, 73 | X: git.StatusUntracked, 74 | Y: git.StatusUntracked, 75 | } 76 | return f, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/command/commit.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | gogit "github.com/go-git/go-git/v5" 8 | "github.com/go-git/go-git/v5/plumbing/object" 9 | giterr "github.com/isacikgoz/gitbatch/internal/errors" 10 | "github.com/isacikgoz/gitbatch/internal/git" 11 | ) 12 | 13 | // CommitOptions defines the rules for commit operation 14 | type CommitOptions struct { 15 | // CommitMsg 16 | CommitMsg string 17 | // User 18 | User string 19 | // Email 20 | Email string 21 | // Mode is the command mode 22 | CommandMode Mode 23 | } 24 | 25 | // Commit defines which commit command to use. 26 | func Commit(r *git.Repository, o *CommitOptions) (err error) { 27 | // here we configure commit operation 28 | 29 | switch o.CommandMode { 30 | case ModeLegacy: 31 | return commitWithGit(r, o) 32 | case ModeNative: 33 | return commitWithGoGit(r, o) 34 | } 35 | return fmt.Errorf("unhandled commit operation") 36 | } 37 | 38 | // commitWithGit is simply a bare git commit -m command which is flexible 39 | func commitWithGit(r *git.Repository, opt *CommitOptions) (err error) { 40 | args := make([]string, 0) 41 | args = append(args, "commit") 42 | args = append(args, "-m") 43 | // parse options to command line arguments 44 | if len(opt.CommitMsg) > 0 { 45 | args = append(args, opt.CommitMsg) 46 | } 47 | if out, err := Run(r.AbsPath, "git", args); err != nil { 48 | _ = r.Refresh() 49 | return giterr.ParseGitError(out, err) 50 | } 51 | // till this step everything should be ok 52 | return r.Refresh() 53 | } 54 | 55 | // commitWithGoGit is the primary commit method 56 | func commitWithGoGit(r *git.Repository, options *CommitOptions) (err error) { 57 | opt := &gogit.CommitOptions{ 58 | Author: &object.Signature{ 59 | Name: options.User, 60 | Email: options.Email, 61 | When: time.Now(), 62 | }, 63 | Committer: &object.Signature{ 64 | Name: options.User, 65 | Email: options.Email, 66 | When: time.Now(), 67 | }, 68 | } 69 | 70 | w, err := r.Repo.Worktree() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | _, err = w.Commit(options.CommitMsg, opt) 76 | if err != nil { 77 | _ = r.Refresh() 78 | return err 79 | } 80 | // till this step everything should be ok 81 | return r.Refresh() 82 | } 83 | -------------------------------------------------------------------------------- /internal/command/commit_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | giterr "github.com/isacikgoz/gitbatch/internal/errors" 7 | "github.com/isacikgoz/gitbatch/internal/git" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCommitWithGit(t *testing.T) { 12 | th := git.InitTestRepositoryFromLocal(t) 13 | defer th.CleanUp(t) 14 | 15 | f, err := testFile(th.RepoPath, "file") 16 | require.NoError(t, err) 17 | 18 | err = addWithGit(th.Repository, f, testAddopt1) 19 | require.NoError(t, err) 20 | 21 | testCommitopt1 := &CommitOptions{ 22 | CommitMsg: "test", 23 | User: "foo", 24 | Email: "foo@bar.com", 25 | } 26 | 27 | var tests = []struct { 28 | inp1 *git.Repository 29 | inp2 *CommitOptions 30 | }{ 31 | {th.Repository, testCommitopt1}, 32 | } 33 | for _, test := range tests { 34 | err = commitWithGit(test.inp1, test.inp2) 35 | require.False(t, err != nil && err == giterr.ErrUserEmailNotSet) 36 | } 37 | } 38 | 39 | func TestCommitWithGoGit(t *testing.T) { 40 | th := git.InitTestRepositoryFromLocal(t) 41 | defer th.CleanUp(t) 42 | 43 | f, err := testFile(th.RepoPath, "file") 44 | require.NoError(t, err) 45 | 46 | err = addWithGit(th.Repository, f, testAddopt1) 47 | require.NoError(t, err) 48 | 49 | testCommitopt1 := &CommitOptions{ 50 | CommitMsg: "test", 51 | User: "foo", 52 | Email: "foo@bar.com", 53 | } 54 | 55 | var tests = []struct { 56 | inp1 *git.Repository 57 | inp2 *CommitOptions 58 | }{ 59 | {th.Repository, testCommitopt1}, 60 | } 61 | for _, test := range tests { 62 | err = commitWithGoGit(test.inp1, test.inp2) 63 | require.NoError(t, err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/command/config.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/isacikgoz/gitbatch/internal/git" 7 | ) 8 | 9 | // ConfigOptions defines the rules for commit operation 10 | type ConfigOptions struct { 11 | // Section 12 | Section string 13 | // Option 14 | Option string 15 | // Site should be Global or Local 16 | Site ConfigSite 17 | // Mode is the command mode 18 | CommandMode Mode 19 | } 20 | 21 | // ConfigSite defines a string type for the site. 22 | type ConfigSite string 23 | 24 | const ( 25 | // ConfigSiteLocal defines a local config. 26 | ConfigSiteLocal ConfigSite = "local" 27 | 28 | // ConfigSiteGlobal defines a global config. 29 | ConfigSiteGlobal ConfigSite = "global" 30 | ) 31 | 32 | // Config adds or reads config of a repository 33 | func Config(r *git.Repository, o *ConfigOptions) (value string, err error) { 34 | // here we configure config operation 35 | 36 | switch o.CommandMode { 37 | case ModeLegacy: 38 | return configWithGit(r, o) 39 | case ModeNative: 40 | return configWithGoGit(r, o) 41 | } 42 | return value, fmt.Errorf("unhandled config operation") 43 | } 44 | 45 | // configWithGit is simply a bare git config --site