├── .github └── workflows │ └── go.yml ├── README.md ├── config └── config.go ├── gitusrgif.gif ├── go.mod ├── go.sum ├── main.go ├── models └── user.go └── utils └── utils.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: '1.23' 19 | 20 | - name: Build 21 | run: go build -v ./... 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitusr 2 | 3 | _gitusr_ is a fast and simple CLI tool built in **Go** that allows you to effortlessly switch between multiple global Git users. 4 | 5 | This tool was inspired by [kubectx](https://github.com/ahmetb/kubectx) and [kubens](https://github.com/ahmetb/kubectx). 6 | 7 | ![terminal example](https://github.com/surbytes/gitusr/raw/refs/heads/main/gitusrgif.gif) 8 | 9 | ## Installation 10 | 11 | You can install using the go command: 12 | 13 | ```shell 14 | go install github.com/surbytes/gitusr 15 | ``` 16 | 17 | or clone the project 18 | 19 | ```shell 20 | git clone https://github.com/surbytes/gitusr.git 21 | cd gitusr; go build -o gitusr main.go 22 | ./gitusr 23 | ``` 24 | 25 | ## Configuration 26 | 27 | To ensure _gitusr_ works correctly, users should configure their .gitconfig file so that each `[users ""]` section represents a global Git user, as follows: 28 | 29 | ![image](https://github.com/user-attachments/assets/9f6f073d-acaa-4b96-bcad-5dc40423ab5b) 30 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | -------------------------------------------------------------------------------- /gitusrgif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surbytes/gitusr/d3963552fa318fb75f83e364a8f39ce833d4273e/gitusrgif.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/surbytes/gitusr 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/manifoldco/promptui v0.9.0 8 | gopkg.in/ini.v1 v1.67.0 9 | ) 10 | 11 | require ( 12 | github.com/chzyer/readline v1.5.1 // indirect 13 | github.com/mattn/go-colorable v0.1.14 // indirect 14 | github.com/mattn/go-isatty v0.0.20 // indirect 15 | github.com/stretchr/testify v1.10.0 // indirect 16 | golang.org/x/sys v0.31.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 3 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 6 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 8 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 9 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 13 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 14 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 15 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 16 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 17 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 18 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 19 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 23 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 25 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 28 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 29 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 30 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/surbytes/gitusr/utils" 5 | ) 6 | 7 | func main() { 8 | 9 | utils.RenderUsers() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | type User struct { 10 | Name string 11 | Email string 12 | } 13 | 14 | // add new user into git config file 15 | func AddUsr(name string, email string) (User, error) { 16 | sectionName := "users." + name + ".name" 17 | sectionEmail := "users." + name + ".email" 18 | if err := exec.Command("git", "config", "--global", "--add", sectionEmail, email).Run(); err != nil { 19 | return User{}, err 20 | } 21 | 22 | if err := exec.Command("git", "config", "--global", "--add", sectionName, name).Run(); err != nil { 23 | return User{}, err 24 | } 25 | 26 | return User{ 27 | Name: name, 28 | Email: email, 29 | }, nil 30 | } 31 | 32 | func GetUsr(name string) (User, error) { 33 | sectionName := "users." + name + ".name" 34 | sectionEmail := "users." + name + ".email" 35 | 36 | n, err := exec.Command("git", "config", "--global", sectionName).Output() 37 | if err != nil { 38 | return User{}, err 39 | } 40 | 41 | email, err := exec.Command("git", "config", "--global", sectionEmail).Output() 42 | if err != nil { 43 | return User{}, err 44 | } 45 | nemail := strings.Trim(string(email), "\f\t\r\n ") 46 | nname := strings.Trim(string(n), "\f\t\r\n ") 47 | 48 | return User{ 49 | Name: nname, 50 | Email: nemail, 51 | }, nil 52 | } 53 | 54 | func SetUsr(currentUser, targetUser User) { 55 | sectionName := "users." + targetUser.Name + ".name" 56 | sectionEmail := "users." + targetUser.Name + ".email" 57 | 58 | _, err := AddUsr(currentUser.Name, currentUser.Email) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | if err := exec.Command("git", "config", "--global", "user.name", targetUser.Name).Run(); err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | if err := exec.Command("git", "config", "--global", "user.email", targetUser.Email).Run(); err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | if err := exec.Command("git", "config", "--global", "--unset", sectionName).Run(); err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | if err := exec.Command("git", "config", "--global", "--unset", sectionEmail).Run(); err != nil { 76 | log.Fatal(err) 77 | } 78 | } 79 | 80 | // get the current user on git config file 81 | func GetCurrentUsr() User { 82 | name, err := exec.Command("git", "config", "--global", "user.name").Output() 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | 87 | email, err := exec.Command("git", "config", "--global", "user.email").Output() 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | nname := strings.Trim(string(name), "\f\t\r\n ") 93 | nemail := strings.Trim(string(email), "\f\t\r\n ") 94 | 95 | return User{ 96 | Name: nname, 97 | Email: nemail, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "slices" 10 | "strings" 11 | 12 | "github.com/fatih/color" 13 | "github.com/manifoldco/promptui" 14 | "github.com/surbytes/gitusr/models" 15 | "gopkg.in/ini.v1" 16 | ) 17 | 18 | func PrintInfo(format string, args ...interface{}) { 19 | fmt.Printf("\x1b[34;1m%s\x1b[0m\n", fmt.Sprintf(format, args...)) 20 | } 21 | 22 | // global .gitconfigfile path 23 | func globalConfigFile() string { 24 | home, err := os.UserHomeDir() 25 | CheckErr(err) 26 | return filepath.Join(home, ".gitconfig") 27 | } 28 | 29 | // load config file 30 | func loadGlobalConfigFile() *ini.File { 31 | cfg, err := ini.Load(globalConfigFile()) 32 | CheckErr(err) 33 | return cfg 34 | } 35 | 36 | // get users section keys 37 | func getGlobalUsersKeys() []string { 38 | file, err := os.Open(globalConfigFile()) 39 | CheckErr(err) 40 | defer file.Close() 41 | 42 | re := regexp.MustCompile(`\[users\s+"(.*?)"\]`) 43 | scanner := bufio.NewScanner(file) 44 | var usersKeys []string 45 | 46 | for scanner.Scan() { 47 | line := scanner.Text() 48 | match := re.FindStringSubmatch(line) 49 | if len(match) > 1 { 50 | usersKeys = append(usersKeys, match[1]) 51 | } 52 | } 53 | 54 | err = scanner.Err() 55 | CheckErr(err) 56 | 57 | return usersKeys 58 | } 59 | 60 | // prepare users section 61 | func prepareUsers(usersKeys []string) []models.User { 62 | var users []models.User 63 | for _, v := range usersKeys { 64 | u := "users \"" + v + "\"" 65 | section := loadGlobalConfigFile().Section(u) 66 | name, err := section.GetKey("name") 67 | CheckErr(err) 68 | email, err := section.GetKey("email") 69 | CheckErr(err) 70 | user := models.User{ 71 | Name: name.String(), 72 | Email: email.String(), 73 | } 74 | users = append(users, user) 75 | } 76 | 77 | return append(users, models.GetCurrentUsr()) 78 | } 79 | 80 | // render users 81 | func RenderUsers() { 82 | var users []string 83 | currentUser := models.GetCurrentUsr() 84 | for _, usr := range prepareUsers(getGlobalUsersKeys()) { 85 | if usr.Name == currentUser.Name && usr.Email == currentUser.Email { 86 | users = append(users, color.YellowString("%s <%s> *", usr.Name, usr.Email)) 87 | } else { 88 | users = append(users, fmt.Sprintf("%s <%s>", usr.Name, usr.Email)) 89 | } 90 | } 91 | slices.Reverse(users) 92 | prompt := promptui.Select{ 93 | Label: "Select User", 94 | Items: users, 95 | } 96 | 97 | _, result, err := prompt.Run() 98 | if err != nil { 99 | fmt.Printf("Prompt failed %v\n", err) 100 | return 101 | } 102 | 103 | if len(result) > 0 { 104 | selectedUser := sanitizeResult(result) 105 | if currentUser.Name == selectedUser.Name && currentUser.Email == selectedUser.Email { 106 | color.Red("Selected user is already the active Git user. No changes made") 107 | } else { 108 | models.SetUsr(currentUser, selectedUser) 109 | } 110 | } 111 | } 112 | 113 | // Sanitize result with regex 114 | func sanitizeResult(result string) models.User { 115 | ansiReg := regexp.MustCompile(`\x1b\[[0-9;]*m`) 116 | reg := regexp.MustCompile(`\<(.*?)\>`) 117 | email := reg.FindStringSubmatch(result)[1] 118 | 119 | name := reg.ReplaceAllString(result, "") 120 | if ansiReg.MatchString(name) { 121 | name = strings.ReplaceAll(name, " *", "") 122 | name = ansiReg.ReplaceAllString(name, "") 123 | } 124 | name = strings.TrimSpace(name) 125 | 126 | return models.User{ 127 | Email: email, 128 | Name: name, 129 | } 130 | } 131 | 132 | // handle error 133 | func CheckErr(err error) { 134 | if err == nil { 135 | return 136 | } 137 | fmt.Printf("\x1b[31;1m%s\x1b[0m\n", fmt.Sprintf("error: %s", err)) 138 | os.Exit(1) 139 | } 140 | --------------------------------------------------------------------------------