bugtaker 5 years ago
commit
7d821a98c2
16 changed files with 1163 additions and 0 deletions
  1. 19 0
      .editorconfig
  2. 23 0
      .gitignore
  3. 21 0
      .goreleaser.yml
  4. 4 0
      README.md
  5. 83 0
      cmd/add.go
  6. 49 0
      cmd/delete.go
  7. 27 0
      cmd/list.go
  8. 97 0
      cmd/update.go
  9. 65 0
      cmd/use.go
  10. 106 0
      git/git.go
  11. 15 0
      go.mod
  12. 68 0
      go.sum
  13. 51 0
      main.go
  14. 103 0
      ssh/ssh.go
  15. 240 0
      store/store.go
  16. 192 0
      util/util.go

+ 19 - 0
.editorconfig

@@ -0,0 +1,19 @@
+# http://editorconfig.org
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# Tab indentation (no size specified)
+[Makefile]
+indent_style = tab
+
+# Golang uses tabs by default
+[*.go]
+indent_style = tab

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+*.iml
+.idea/
+.ipr
+.iws
+*~
+~*
+*.diff
+*.patch
+*.bak
+.DS_Store
+Thumbs.db
+.project
+.*proj
+.svn/
+*.swp
+*.swo
+*.log
+*.sublime-project
+*.sublime-workspace
+.buildpath
+.settings
+vendor/
+giter

+ 21 - 0
.goreleaser.yml

@@ -0,0 +1,21 @@
+builds:
+  -
+    main: ./main.go
+    binary: giter
+    ldflags: -s -w -X main.version={{.Version}}
+    goos:
+      - darwin
+      - linux
+      - windows
+      - freebsd
+      - netbsd
+      - openbsd
+    goarch:
+      - amd64
+      - 386
+changelog:
+  sort: asc
+  filters:
+    exclude:
+    - '^docs:'
+    - '^test:'

+ 4 - 0
README.md

@@ -0,0 +1,4 @@
+Giter - Git users manager
+======
+
+> Giter is a git users manager.

+ 83 - 0
cmd/add.go

@@ -0,0 +1,83 @@
+package cmd
+
+import (
+	"fmt"
+	"github.com/jsmartx/giter/git"
+	"github.com/jsmartx/giter/store"
+	"github.com/jsmartx/giter/util"
+	"github.com/urfave/cli"
+)
+
+func getUser() *git.User {
+	g, err := git.New(".")
+	if err != nil {
+		return git.GlobalUser()
+	}
+	return g.GetUser()
+}
+
+func getHost() string {
+	g, err := git.New(".")
+	if err != nil {
+		return "ssh://github.com"
+	}
+	urls, err := g.Remotes()
+	if err != nil || len(urls) == 0 {
+		return "ssh://github.com"
+	}
+	return fmt.Sprintf("%s://%s", urls[0].Scheme, urls[0].Host)
+}
+
+func Add(c *cli.Context) error {
+	u := getUser()
+	nameCfg := &util.PromptConfig{
+		Prompt: "user name: ",
+	}
+	emailCfg := &util.PromptConfig{
+		Prompt: "user email: ",
+	}
+	urlCfg := &util.PromptConfig{
+		Prompt:  "git server: ",
+		Default: getHost(),
+	}
+	if u != nil {
+		nameCfg.Default = u.Name
+		emailCfg.Default = u.Email
+	}
+	name := util.Prompt(nameCfg)
+	email := util.Prompt(emailCfg)
+	urlStr := util.Prompt(urlCfg)
+
+	url, err := util.ParseURL(urlStr)
+	util.CheckError(err)
+
+	host, port := util.SplitHostPort(url.Host)
+	user := &store.User{
+		Name:   name,
+		Email:  email,
+		Scheme: url.Scheme,
+		Host:   host,
+		Port:   port,
+	}
+	options := &store.Options{}
+	if user.IsSSH() {
+		pvtPath, err := util.SysSSHConfig()
+		if err == nil {
+			fmt.Printf("There is a SSH key: %s\nYou can use this key or generate a new SSH key.\n", pvtPath)
+			txt := util.Prompt(&util.PromptConfig{
+				Prompt: fmt.Sprintf("Do you want to use '%s' [Y/n]? ", pvtPath),
+			})
+			if txt == "y" || txt == "Y" {
+				options.KeyPath = pvtPath
+			}
+		}
+	} else {
+		pwd := util.Prompt(&util.PromptConfig{
+			Prompt: "user password: ",
+			Silent: true,
+		})
+		options.Password = pwd
+	}
+	s := store.New()
+	return s.Add(user, options)
+}

+ 49 - 0
cmd/delete.go

@@ -0,0 +1,49 @@
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"github.com/jsmartx/giter/store"
+	"github.com/jsmartx/giter/util"
+	"github.com/urfave/cli"
+	"strconv"
+)
+
+func Delete(c *cli.Context) error {
+	name := c.Args().First()
+	if name == "" {
+		name = util.Prompt(&util.PromptConfig{
+			Prompt: "user name: ",
+		})
+	}
+	s := store.New()
+	users := s.List(name, true)
+	if len(users) == 0 {
+		return errors.New("User not found!")
+	}
+	u := users[0]
+	if len(users) > 1 {
+		fmt.Printf("There are %d users:\n", len(users))
+		for i, item := range users {
+			fmt.Printf("%4d) %s\n", i+1, item.String())
+		}
+		str := util.Prompt(&util.PromptConfig{
+			Prompt: "Enter number to select user: ",
+		})
+		i, err := strconv.Atoi(str)
+		if err != nil {
+			return err
+		}
+		if i < 1 || i > len(users) {
+			return errors.New("Out of range")
+		}
+		u = users[i-1]
+	}
+	txt := util.Prompt(&util.PromptConfig{
+		Prompt: fmt.Sprintf("Are you sure to delete '%s' [Y/n]? ", u.String()),
+	})
+	if txt != "y" && txt != "Y" {
+		return nil
+	}
+	return s.Delete(u)
+}

+ 27 - 0
cmd/list.go

@@ -0,0 +1,27 @@
+package cmd
+
+import (
+	"fmt"
+	"github.com/jsmartx/giter/git"
+	"github.com/jsmartx/giter/store"
+	"github.com/urfave/cli"
+)
+
+func List(c *cli.Context) error {
+	g, err := git.New(".")
+	var cur *git.User
+	if err == nil {
+		cur = g.GetUser()
+	}
+	filter := c.Args().First()
+	s := store.New()
+	users := s.List(filter, false)
+	for _, u := range users {
+		if cur != nil && cur.Name == u.Name && cur.Email == u.Email {
+			fmt.Printf(" * %s\n", u.String())
+		} else {
+			fmt.Printf("   %s\n", u.String())
+		}
+	}
+	return nil
+}

+ 97 - 0
cmd/update.go

@@ -0,0 +1,97 @@
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"github.com/jsmartx/giter/store"
+	"github.com/jsmartx/giter/util"
+	"github.com/urfave/cli"
+	"strconv"
+)
+
+func Update(c *cli.Context) error {
+	name := c.Args().First()
+	if name == "" {
+		name = util.Prompt(&util.PromptConfig{
+			Prompt: "user name: ",
+		})
+	}
+	s := store.New()
+	users := s.List(name, true)
+	if len(users) == 0 {
+		return errors.New("User not found!")
+	}
+	u := users[0]
+	if len(users) > 1 {
+		fmt.Printf("There are %d users:\n", len(users))
+		for i, item := range users {
+			fmt.Printf("%4d) %s\n", i+1, item.String())
+		}
+		str := util.Prompt(&util.PromptConfig{
+			Prompt: "Enter number to select user: ",
+		})
+		i, err := strconv.Atoi(str)
+		if err != nil {
+			return err
+		}
+		if i < 1 || i > len(users) {
+			return errors.New("Out of range")
+		}
+		u = users[i-1]
+	}
+	email := util.Prompt(&util.PromptConfig{
+		Prompt:  "user email: ",
+		Default: u.Email,
+	})
+	urlStr := util.Prompt(&util.PromptConfig{
+		Prompt:  "git server: ",
+		Default: u.FullHost(),
+	})
+
+	url, err := util.ParseURL(urlStr)
+	util.CheckError(err)
+
+	host, port := util.SplitHostPort(url.Host)
+	user := &store.User{
+		Name:   name,
+		Email:  email,
+		Scheme: url.Scheme,
+		Host:   host,
+		Port:   port,
+	}
+	options := &store.Options{}
+	if user.IsSSH() {
+		if u.IsSSH() {
+			txt := util.Prompt(&util.PromptConfig{
+				Prompt: fmt.Sprintf("Regenerate the SSH key [Y/n]? "),
+			})
+			if txt != "y" && txt != "Y" {
+				p, err := u.KeyPath()
+				if err != nil {
+					return err
+				}
+				options.KeyPath = p
+			}
+		}
+	} else {
+		if !u.IsSSH() {
+			txt := util.Prompt(&util.PromptConfig{
+				Prompt: fmt.Sprintf("Reset password [Y/n]? "),
+			})
+			if txt == "y" || txt == "Y" {
+				pwd := util.Prompt(&util.PromptConfig{
+					Prompt: "user password: ",
+					Silent: true,
+				})
+				options.Password = pwd
+			}
+		} else {
+			pwd := util.Prompt(&util.PromptConfig{
+				Prompt: "user password: ",
+				Silent: true,
+			})
+			options.Password = pwd
+		}
+	}
+	return s.Update(u.Hash(), user, options)
+}

+ 65 - 0
cmd/use.go

@@ -0,0 +1,65 @@
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"github.com/jsmartx/giter/git"
+	"github.com/jsmartx/giter/ssh"
+	"github.com/jsmartx/giter/store"
+	"github.com/jsmartx/giter/util"
+	"github.com/urfave/cli"
+	"strconv"
+)
+
+func Use(c *cli.Context) error {
+	g, err := git.New(".")
+	if err != nil {
+		return err
+	}
+	name := c.Args().First()
+	if name == "" {
+		name = util.Prompt(&util.PromptConfig{
+			Prompt: "user name: ",
+		})
+	}
+	s := store.New()
+	users := s.List(name, true)
+	if len(users) == 0 {
+		return errors.New("User not found!")
+	}
+	u := users[0]
+	if len(users) > 1 {
+		fmt.Printf("There are %d users:\n", len(users))
+		for i, item := range users {
+			fmt.Printf("%4d) %s\n", i+1, item.String())
+		}
+		str := util.Prompt(&util.PromptConfig{
+			Prompt: "Enter number to select user: ",
+		})
+		i, err := strconv.Atoi(str)
+		if err != nil {
+			return err
+		}
+		if i < 1 || i > len(users) {
+			return errors.New("Out of range")
+		}
+		u = users[i-1]
+	}
+	if u.IsSSH() {
+		keyPath, err := u.KeyPath()
+		if err != nil {
+			fmt.Println(err)
+		}
+		s := ssh.New()
+		err = s.SetHost(&ssh.Host{
+			Key:          u.Host,
+			Hostname:     u.Host,
+			Port:         u.Port,
+			IdentityFile: keyPath,
+		})
+		if err != nil {
+			fmt.Println(err)
+		}
+	}
+	return g.SetUser(u.Name, u.Email)
+}

+ 106 - 0
git/git.go

@@ -0,0 +1,106 @@
+package git
+
+import (
+	"github.com/jsmartx/giter/util"
+	"github.com/mitchellh/go-homedir"
+	"gopkg.in/src-d/go-git.v4"
+	"gopkg.in/src-d/go-git.v4/config"
+	fs "io/ioutil"
+	"net/url"
+)
+
+type Git struct {
+	r *git.Repository
+}
+
+type User struct {
+	Name  string
+	Email string
+}
+
+func New(path string) (*Git, error) {
+	r, err := git.PlainOpen(path)
+	if err != nil {
+		return nil, err
+	}
+	g := &Git{r: r}
+	return g, nil
+}
+
+func GlobalUser() *User {
+	p, err := homedir.Expand("~/.gitconfig")
+	if err != nil {
+		return nil
+	}
+
+	data, err := fs.ReadFile(p)
+	if err != nil {
+		return nil
+	}
+
+	cfg := config.NewConfig()
+	if err = cfg.Unmarshal(data); err != nil {
+		return nil
+	}
+
+	u := cfg.Raw.Section("user")
+	name := u.Option("name")
+	email := u.Option("email")
+	if name == "" || email == "" {
+		return nil
+	}
+	return &User{Name: name, Email: email}
+}
+
+func (g *Git) Remotes() ([]*url.URL, error) {
+	cfg, err := g.r.Config()
+
+	if err != nil {
+		return nil, err
+	}
+
+	urls := make([]*url.URL, 0)
+
+	for _, remote := range cfg.Remotes {
+		for _, repo := range remote.URLs {
+			repoURL, err := util.ParseURL(repo)
+			if err != nil {
+				continue
+			}
+			if remote.Name == "origin" {
+				urls = append([]*url.URL{repoURL}, urls...)
+			} else {
+				urls = append(urls, repoURL)
+			}
+		}
+	}
+	return urls, nil
+}
+
+func (g *Git) GetUser() *User {
+	cfg, err := g.r.Config()
+	if err != nil {
+		return nil
+	}
+
+	u := cfg.Raw.Section("user")
+	name := u.Option("name")
+	email := u.Option("email")
+	if name == "" || email == "" {
+		return nil
+	}
+	return &User{Name: name, Email: email}
+}
+
+func (g *Git) SetUser(name, email string) error {
+	cfg, err := g.r.Config()
+
+	if err != nil {
+		return err
+	}
+
+	u := cfg.Raw.Section("user")
+	u.SetOption("name", name)
+	u.SetOption("email", email)
+	return g.r.Storer.SetConfig(cfg)
+}

+ 15 - 0
go.mod

@@ -0,0 +1,15 @@
+module github.com/jsmartx/giter
+
+require (
+	github.com/chzyer/logex v1.1.10 // indirect
+	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
+	github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
+	github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e
+	github.com/mitchellh/go-homedir v1.0.0
+	github.com/urfave/cli v1.20.0
+	golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
+	golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb // indirect
+	gopkg.in/src-d/go-git.v4 v4.8.1
+)
+
+replace golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 => github.com/golang/crypto v0.0.0-20181203042331-505ab145d0a9

+ 68 - 0
go.sum

@@ -0,0 +1,68 @@
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo=
+github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/golang/crypto v0.0.0-20181203042331-505ab145d0a9 h1:2VPKynXZCEf8bI6hOvmbwFg1m0b7ah3m/Yg4JzBsIRQ=
+github.com/golang/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:uZvAcrsnNaCxlh1HorK5dUQHGmEKPh2H/Rl1kehswPo=
+github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8=
+github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
+github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
+github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
+github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro=
+github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb h1:pf3XwC90UUdNPYWZdFjhGBE7DUFuK3Ct1zWmZ65QN30=
+golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/src-d/go-billy.v4 v4.2.1 h1:omN5CrMrMcQ+4I8bJ0wEhOBPanIRWzFC953IiXKdYzo=
+gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
+gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs=
+gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
+gopkg.in/src-d/go-git.v4 v4.8.1 h1:aAyBmkdE1QUUEHcP4YFCGKmsMQRAuRmUcPEQR7lOAa0=
+gopkg.in/src-d/go-git.v4 v4.8.1/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=

+ 51 - 0
main.go

@@ -0,0 +1,51 @@
+package main
+
+import (
+	"github.com/jsmartx/giter/cmd"
+	"github.com/urfave/cli"
+	"log"
+	"os"
+)
+
+const version = "0.0.1"
+
+func main() {
+	app := cli.NewApp()
+	app.Usage = "Git users manager"
+	app.Version = version
+	app.Commands = []cli.Command{
+		{
+			Name:    "list",
+			Aliases: []string{"ls"},
+			Usage:   "List all the git user config",
+			Action:  cmd.List,
+		},
+		{
+			Name:   "use",
+			Usage:  "Change git user config to username",
+			Action: cmd.Use,
+		},
+		{
+			Name:    "add",
+			Aliases: []string{"new"},
+			Usage:   "Add one custom user config",
+			Action:  cmd.Add,
+		},
+		{
+			Name:   "update",
+			Usage:  "Update one custom user config",
+			Action: cmd.Update,
+		},
+		{
+			Name:    "del",
+			Aliases: []string{"rm"},
+			Usage:   "Delete one custom user config",
+			Action:  cmd.Delete,
+		},
+	}
+
+	err := app.Run(os.Args)
+	if err != nil {
+		log.Fatal(err)
+	}
+}

+ 103 - 0
ssh/ssh.go

@@ -0,0 +1,103 @@
+package ssh
+
+import (
+	"github.com/jsmartx/giter/util"
+	"github.com/kevinburke/ssh_config"
+	"os"
+	fs "io/ioutil"
+	"path/filepath"
+)
+
+type Config struct {
+	cfg *ssh_config.Config
+}
+
+func loadConfig() *ssh_config.Config {
+	p, err := util.JoinPath("~/.ssh/", "config")
+	if err != nil {
+		return nil
+	}
+	f, err := os.Open(p)
+	defer f.Close()
+	if err != nil {
+		return nil
+	}
+	cfg, err := ssh_config.Decode(f)
+	if err != nil {
+		return nil
+	}
+	return cfg
+}
+
+func saveConfig(cfg *ssh_config.Config) error {
+	root, err := util.Mkdir("~/.ssh/")
+	if err != nil {
+		return err
+	}
+	b, err := cfg.MarshalText()
+	if err != nil {
+		return err
+	}
+	cfgPath := filepath.Join(root, "config")
+	// write marshaled data to the file
+	return fs.WriteFile(cfgPath, b, 0755)
+}
+
+func New() *Config {
+	cfg := loadConfig()
+	if cfg != nil {
+		return &Config{cfg: cfg}
+	}
+	cfg = &ssh_config.Config{
+		Hosts: make([]*ssh_config.Host, 0),
+	}
+	saveConfig(cfg)
+	return &Config{cfg: cfg}
+}
+
+type Host struct {
+	Key          string
+	Hostname     string
+	Port         string
+	IdentityFile string
+}
+
+func (h *Host) Transform() (*ssh_config.Host, error) {
+	pattern, err := ssh_config.NewPattern(h.Key)
+	if err != nil {
+		return nil, err
+	}
+	nodes := make([]ssh_config.Node, 0)
+	if h.Hostname != "" {
+		nodes = append(nodes, &ssh_config.KV{Key: "  HostName", Value: h.Hostname})
+	}
+	if h.Port != "" {
+		nodes = append(nodes, &ssh_config.KV{Key: "  Port", Value: h.Port})
+	}
+	if h.IdentityFile != "" {
+		nodes = append(nodes, &ssh_config.KV{Key: "  IdentityFile", Value: h.IdentityFile})
+	}
+	nodes = append(nodes, &ssh_config.Empty{})
+	return &ssh_config.Host{
+		Patterns:   []*ssh_config.Pattern{pattern},
+		Nodes:      nodes,
+		EOLComment: " -- added by giter",
+	}, nil
+}
+
+func (c *Config) SetHost(h *Host) error {
+	host, err := h.Transform()
+	if err != nil {
+		return err
+	}
+	for i, v := range c.cfg.Hosts {
+		for _, pattern := range v.Patterns {
+			if pattern.String() == h.Key {
+				c.cfg.Hosts[i] = host
+				return saveConfig(c.cfg)
+			}
+		}
+	}
+	c.cfg.Hosts = append(c.cfg.Hosts, host)
+	return saveConfig(c.cfg)
+}

+ 240 - 0
store/store.go

@@ -0,0 +1,240 @@
+package store
+
+import (
+	"crypto/md5"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/jsmartx/giter/util"
+	fs "io/ioutil"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+const ROOT = "~/.giter/"
+
+type User struct {
+	Name   string `json:"name"`
+	Email  string `json:"email"`
+	Scheme string `json:"scheme"`
+	Host   string `json:"host"`
+	Port   string `json:"port"`
+}
+
+type Options struct {
+	KeyPath  string
+	Password string
+}
+
+func (u *User) Hash() string {
+	host := util.JoinHostPort(u.Host, u.Port)
+	url := fmt.Sprintf("%s://%s@%s", u.Scheme, u.Name, host)
+	hash := md5.New()
+	hash.Write([]byte(url))
+	return fmt.Sprintf("%x", hash.Sum(nil))
+}
+
+func (u *User) Url(pwd string) string {
+	fullUrl := &url.URL{
+		Scheme: u.Scheme,
+		User:   url.UserPassword(u.Name, pwd),
+		Host:   util.JoinHostPort(u.Host, u.Port),
+	}
+	return fullUrl.String()
+}
+
+func (u *User) KeyPath() (string, error) {
+	return util.JoinPath(ROOT, "keys", u.Hash())
+}
+
+func (u *User) FullHost() string {
+	host := util.JoinHostPort(u.Host, u.Port)
+	return fmt.Sprintf("%s://%s", u.Scheme, host)
+}
+
+func (u *User) IsSSH() bool {
+	if u.Scheme == "http" || u.Scheme == "https" {
+		return false
+	}
+	return true
+}
+
+func (u *User) String() string {
+	return fmt.Sprintf("%s - %s", u.Name, u.FullHost())
+}
+
+type Config struct {
+	Users []*User `json:"users"`
+}
+
+type Store struct {
+	c *Config
+}
+
+func loadConfig() *Config {
+	cfgPath, err := util.JoinPath(ROOT, "config.json")
+	if err != nil {
+		return nil
+	}
+	f, err := os.Open(cfgPath)
+	defer f.Close()
+	if err != nil {
+		return nil
+	}
+	var cfg Config
+	parser := json.NewDecoder(f)
+	parser.Decode(&cfg)
+	return &cfg
+}
+
+func saveConfig(cfg *Config) error {
+	root, err := util.Mkdir(ROOT)
+	if err != nil {
+		return err
+	}
+	b, err := json.MarshalIndent(cfg, "", "  ")
+	if err != nil {
+		return err
+	}
+	cfgPath := filepath.Join(root, "config.json")
+	// write marshaled data to the file
+	return fs.WriteFile(cfgPath, b, 0755)
+}
+
+func New() *Store {
+	cfg := loadConfig()
+	if cfg != nil {
+		return &Store{c: cfg}
+	}
+	cfg = &Config{
+		Users: make([]*User, 0),
+	}
+	saveConfig(cfg)
+	return &Store{c: cfg}
+}
+
+func (s *Store) check(user *User) error {
+	for _, u := range s.c.Users {
+		if u.Hash() == user.Hash() {
+			return errors.New("User already exist!")
+		}
+	}
+	return nil
+}
+
+func (s *Store) Add(u *User, opts *Options) error {
+	if err := s.check(u); err != nil {
+		return err
+	}
+	keysDir, err := util.Mkdir(ROOT, "keys")
+	if err != nil {
+		return err
+	}
+	if u.IsSSH() {
+		pvtPath := filepath.Join(keysDir, u.Hash())
+		pubPath := filepath.Join(keysDir, u.Hash()+".pub")
+		if opts.KeyPath == "" {
+			if err := util.Keygen(pubPath, pvtPath, 4096); err != nil {
+				return err
+			}
+		} else {
+			if err := util.Copy(opts.KeyPath, pvtPath); err != nil {
+				return err
+			}
+			if err := util.Copy(opts.KeyPath+".pub", pubPath); err != nil {
+				return err
+			}
+		}
+	} else {
+		pwdPath := filepath.Join(keysDir, u.Hash()+".credential")
+		data := []byte(u.Url(opts.Password))
+		if err := fs.WriteFile(pwdPath, data, 0755); err != nil {
+			return err
+		}
+	}
+	s.c.Users = append(s.c.Users, u)
+	return saveConfig(s.c)
+}
+
+func (s *Store) Update(hash string, u *User, opts *Options) error {
+	if hash != u.Hash() {
+		if err := s.check(u); err != nil {
+			return err
+		}
+	}
+	keysDir, err := util.Mkdir(ROOT, "keys")
+	if err != nil {
+		return err
+	}
+	if u.IsSSH() {
+		pvtPath := filepath.Join(keysDir, u.Hash())
+		pubPath := filepath.Join(keysDir, u.Hash()+".pub")
+		if opts.KeyPath == "" {
+			if err := util.Keygen(pubPath, pvtPath, 4096); err != nil {
+				return err
+			}
+		} else if opts.KeyPath != pvtPath {
+			if err := util.Copy(opts.KeyPath, pvtPath); err != nil {
+				return err
+			}
+			if err := util.Copy(opts.KeyPath+".pub", pubPath); err != nil {
+				return err
+			}
+		}
+	} else {
+		pwdPath := filepath.Join(keysDir, u.Hash()+".credential")
+		if opts.Password != "" {
+			data := []byte(u.Url(opts.Password))
+			if err := fs.WriteFile(pwdPath, data, 0755); err != nil {
+				return err
+			}
+		} else if hash != u.Hash() {
+			oldPath := filepath.Join(keysDir, hash+".credential")
+			if err := util.Copy(oldPath, pwdPath); err != nil {
+				return err
+			}
+		}
+	}
+	for i := 0; i < len(s.c.Users); i++ {
+		if s.c.Users[i].Hash() == hash {
+			s.c.Users[i] = u
+		}
+	}
+	return saveConfig(s.c)
+}
+
+func (s *Store) Delete(u *User) error {
+	p, err := u.KeyPath()
+	if err != nil {
+		return err
+	}
+	if u.IsSSH() {
+		os.Remove(p)
+		os.Remove(p + ".pub")
+	} else {
+		os.Remove(p + ".credential")
+	}
+	users := make([]*User, 0)
+	for _, v := range s.c.Users {
+		if v.Hash() != u.Hash() {
+			users = append(users, v)
+		}
+	}
+	s.c.Users = users
+	return saveConfig(s.c)
+}
+
+func (s *Store) List(filter string, strict bool) []*User {
+	users := make([]*User, 0)
+	for _, u := range s.c.Users {
+		if !strict && strings.Contains(u.Name, filter) {
+			users = append(users, u)
+		}
+		if strict && u.Name == filter {
+			users = append(users, u)
+		}
+	}
+	return users
+}

+ 192 - 0
util/util.go

@@ -0,0 +1,192 @@
+package util
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"github.com/chzyer/readline"
+	"github.com/mitchellh/go-homedir"
+	"golang.org/x/crypto/ssh"
+	"io"
+	fs "io/ioutil"
+	"net"
+	"net/url"
+	"os"
+	"path/filepath"
+	"regexp"
+)
+
+func Keygen(publicPath, privatePath string, bits int) error {
+	privateKey, err := rsa.GenerateKey(rand.Reader, bits)
+	if err != nil {
+		return err
+	}
+	// generate and write private key as PEM
+	privateFile, err := os.Create(privatePath)
+	defer privateFile.Close()
+	if err != nil {
+		return err
+	}
+	privatePEM := &pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
+	}
+	if err := pem.Encode(privateFile, privatePEM); err != nil {
+		return err
+	}
+	// generate and write public key
+	publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
+	if err != nil {
+		return err
+	}
+	return fs.WriteFile(publicPath, ssh.MarshalAuthorizedKey(publicKey), 0755)
+}
+
+func JoinHostPort(host, port string) string {
+	if port != "" {
+		return fmt.Sprintf("%s:%s", host, port)
+	} else {
+		return host
+	}
+}
+
+func SplitHostPort(host string) (string, string) {
+	h, p, err := net.SplitHostPort(host)
+	if err != nil {
+		return host, ""
+	} else {
+		return h, p
+	}
+}
+
+var ScpRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)
+var Schemes = []string{"git", "https", "http", "git+ssh", "ssh"}
+
+func ParseURL(repo string) (*url.URL, error) {
+	var err error
+	var repoURL *url.URL
+	if m := ScpRe.FindStringSubmatch(repo); m != nil {
+		repoURL = &url.URL{
+			Scheme: "ssh",
+			User:   url.User(m[1]),
+			Host:   m[2],
+			Path:   m[3],
+		}
+	} else {
+		repoURL, err = url.Parse(repo)
+		if err != nil {
+			return nil, err
+		}
+	}
+	for _, scheme := range Schemes {
+		if repoURL.Scheme == scheme {
+			return repoURL, nil
+		}
+	}
+	return nil, errors.New(repoURL.Scheme + " is not supported")
+}
+
+type PromptConfig struct {
+	Prompt  string
+	Default string
+	Silent  bool
+}
+
+func (c *PromptConfig) String() string {
+	if c.Default != "" {
+		return fmt.Sprintf("%s(%s) ", c.Prompt, c.Default)
+	} else {
+		return c.Prompt
+	}
+}
+
+func CheckError(err error) {
+	if err == nil {
+		return
+	}
+	fmt.Printf("\x1b[31;1m%s\x1b[0m\n", fmt.Sprintf("Error: %s", err))
+	os.Exit(1)
+}
+
+func Prompt(cfg *PromptConfig) string {
+	r, err := readline.NewEx(&readline.Config{
+		Prompt:     cfg.String(),
+		EnableMask: cfg.Silent,
+		MaskRune:   42,
+	})
+	CheckError(err)
+	defer r.Close()
+	for {
+		line, err := r.Readline()
+		CheckError(err)
+		if line != "" {
+			return line
+		}
+		if cfg.Default != "" {
+			return cfg.Default
+		}
+	}
+}
+
+func IsExist(path string) bool {
+	p, err := homedir.Expand(path)
+	if err != nil {
+		return false
+	}
+	if _, err := os.Stat(p); os.IsNotExist(err) {
+		return false
+	}
+	return true
+}
+
+func Mkdir(paths ...string) (string, error) {
+	p, err := homedir.Expand(filepath.Join(paths...))
+	if err != nil {
+		return "", err
+	}
+	if err := os.MkdirAll(p, 0755); err != nil {
+		return "", err
+	}
+	return p, nil
+}
+
+func JoinPath(paths ...string) (string, error) {
+	p, err := homedir.Expand(filepath.Join(paths...))
+	if err != nil {
+		return "", err
+	}
+	return p, nil
+}
+
+func Copy(src, dst string) error {
+	in, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer in.Close()
+
+	out, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	_, err = io.Copy(out, in)
+	if err != nil {
+		return err
+	}
+	return out.Close()
+}
+
+func SysSSHConfig() (string, error) {
+	keypairs := []string{"~/.ssh/id_rsa", "~/.ssh/id_ed25519"}
+	for _, k := range keypairs {
+		if IsExist(k) && IsExist(k+".pub") {
+			return homedir.Expand(k)
+		}
+	}
+	return "", errors.New("System ssh config not found")
+}