diff options
35 files changed, 1972 insertions, 541 deletions
diff --git a/.travis.yml b/.travis.yml index cb86428d..a64ed083 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ matrix: include: - language: go - go: 1.12.x - - language: go go: 1.13.x - language: go go: 1.14.x + - language: go + go: 1.15.x - language: node_js node_js: node before_install: @@ -41,5 +41,5 @@ deploy: file: dist/**/* on: repo: MichaelMure/git-bug - go: 1.13.x + go: 1.14.x tags: true diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go index d95b23c7..2327a6fc 100644 --- a/bridge/core/auth/credential.go +++ b/bridge/core/auth/credential.go @@ -1,11 +1,11 @@ package auth import ( - "crypto/rand" "encoding/base64" + "encoding/json" "errors" "fmt" - "regexp" + "strconv" "strings" "time" @@ -14,12 +14,12 @@ import ( ) const ( - configKeyPrefix = "git-bug.auth" - configKeyKind = "kind" - configKeyTarget = "target" - configKeyCreateTime = "createtime" - configKeySalt = "salt" - configKeyPrefixMeta = "meta." + keyringKeyPrefix = "auth-" + keyringKeyKind = "kind" + keyringKeyTarget = "target" + keyringKeyCreateTime = "createtime" + keyringKeySalt = "salt" + keyringKeyPrefixMeta = "meta." MetaKeyLogin = "login" MetaKeyBaseURL = "base-url" @@ -57,22 +57,23 @@ type Credential interface { } // Load loads a credential from the repo config -func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) { - keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id) +func LoadWithId(repo repository.RepoKeyring, id entity.Id) (Credential, error) { + key := fmt.Sprintf("%s%s", keyringKeyPrefix, id) - // read token config pairs - rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix) - if err != nil { - // Not exactly right due to the limitation of ReadAll() + item, err := repo.Keyring().Get(key) + if err == repository.ErrKeyringKeyNotFound { return nil, ErrCredentialNotExist } + if err != nil { + return nil, err + } - return loadFromConfig(rawconfigs, id) + return decode(item) } // LoadWithPrefix load a credential from the repo config with a prefix -func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) { - creds, err := List(repo) +func LoadWithPrefix(repo repository.RepoKeyring, prefix string) (Credential, error) { + keys, err := repo.Keyring().Keys() if err != nil { return nil, err } @@ -80,10 +81,22 @@ func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, erro // preallocate but empty matching := make([]Credential, 0, 5) - for _, cred := range creds { - if cred.ID().HasPrefix(prefix) { - matching = append(matching, cred) + for _, key := range keys { + if !strings.HasPrefix(key, keyringKeyPrefix+prefix) { + continue + } + + item, err := repo.Keyring().Get(key) + if err != nil { + return nil, err } + + cred, err := decode(item) + if err != nil { + return nil, err + } + + matching = append(matching, cred) } if len(matching) > 1 { @@ -101,29 +114,25 @@ func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, erro return matching[0], nil } -// loadFromConfig is a helper to construct a Credential from the set of git configs -func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) { - keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id) +// decode is a helper to construct a Credential from the keyring Item +func decode(item repository.Item) (Credential, error) { + data := make(map[string]string) - // trim key prefix - configs := make(map[string]string) - for key, value := range rawConfigs { - newKey := strings.TrimPrefix(key, keyPrefix) - configs[newKey] = value + err := json.Unmarshal(item.Data, &data) + if err != nil { + return nil, err } var cred Credential - var err error - - switch CredentialKind(configs[configKeyKind]) { + switch CredentialKind(data[keyringKeyKind]) { case KindToken: - cred, err = NewTokenFromConfig(configs) + cred, err = NewTokenFromConfig(data) case KindLogin: - cred, err = NewLoginFromConfig(configs) + cred, err = NewLoginFromConfig(data) case KindLoginPassword: - cred, err = NewLoginPasswordFromConfig(configs) + cred, err = NewLoginPasswordFromConfig(data) default: - return nil, fmt.Errorf("unknown credential type \"%s\"", configs[configKeyKind]) + return nil, fmt.Errorf("unknown credential type \"%s\"", data[keyringKeyKind]) } if err != nil { @@ -133,64 +142,27 @@ func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, err return cred, nil } -func metaFromConfig(configs map[string]string) map[string]string { - result := make(map[string]string) - for key, val := range configs { - if strings.HasPrefix(key, configKeyPrefixMeta) { - key = strings.TrimPrefix(key, configKeyPrefixMeta) - result[key] = val - } - } - if len(result) == 0 { - return nil - } - return result -} - -func makeSalt() []byte { - result := make([]byte, 16) - _, err := rand.Read(result) - if err != nil { - panic(err) - } - return result -} - -func saltFromConfig(configs map[string]string) ([]byte, error) { - val, ok := configs[configKeySalt] - if !ok { - return nil, fmt.Errorf("no credential salt found") - } - return base64.StdEncoding.DecodeString(val) -} - // List load all existing credentials -func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) { - rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".") +func List(repo repository.RepoKeyring, opts ...ListOption) ([]Credential, error) { + keys, err := repo.Keyring().Keys() if err != nil { return nil, err } - re := regexp.MustCompile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`) - - mapped := make(map[string]map[string]string) + matcher := matcher(opts) - for key, val := range rawConfigs { - res := re.FindStringSubmatch(key) - if res == nil { + var credentials []Credential + for _, key := range keys { + if !strings.HasPrefix(key, keyringKeyPrefix) { continue } - if mapped[res[1]] == nil { - mapped[res[1]] = make(map[string]string) - } - mapped[res[1]][res[2]] = val - } - matcher := matcher(opts) + item, err := repo.Keyring().Get(key) + if err != nil { + return nil, err + } - var credentials []Credential - for id, kvs := range mapped { - cred, err := loadFromConfig(kvs, entity.Id(id)) + cred, err := decode(item) if err != nil { return nil, err } @@ -203,74 +175,48 @@ func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) { } // IdExist return whether a credential id exist or not -func IdExist(repo repository.RepoConfig, id entity.Id) bool { +func IdExist(repo repository.RepoKeyring, id entity.Id) bool { _, err := LoadWithId(repo, id) return err == nil } // PrefixExist return whether a credential id prefix exist or not -func PrefixExist(repo repository.RepoConfig, prefix string) bool { +func PrefixExist(repo repository.RepoKeyring, prefix string) bool { _, err := LoadWithPrefix(repo, prefix) return err == nil } // Store stores a credential in the global git config -func Store(repo repository.RepoConfig, cred Credential) error { - confs := cred.toConfig() - - prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID()) - - // Kind - err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind())) - if err != nil { - return err - } - - // Target - err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target()) - if err != nil { - return err - } - - // CreateTime - err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime()) - if err != nil { - return err - } - - // Salt +func Store(repo repository.RepoKeyring, cred Credential) error { if len(cred.Salt()) != 16 { panic("credentials need to be salted") } - encoded := base64.StdEncoding.EncodeToString(cred.Salt()) - err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded) - if err != nil { - return err - } - // Metadata + confs := cred.toConfig() + + confs[keyringKeyKind] = string(cred.Kind()) + confs[keyringKeyTarget] = cred.Target() + confs[keyringKeyCreateTime] = strconv.Itoa(int(cred.CreateTime().Unix())) + confs[keyringKeySalt] = base64.StdEncoding.EncodeToString(cred.Salt()) + for key, val := range cred.Metadata() { - err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val) - if err != nil { - return err - } + confs[keyringKeyPrefixMeta+key] = val } - // Custom - for key, val := range confs { - err := repo.GlobalConfig().StoreString(prefix+key, val) - if err != nil { - return err - } + data, err := json.Marshal(confs) + if err != nil { + return err } - return nil + return repo.Keyring().Set(repository.Item{ + Key: keyringKeyPrefix + cred.ID().String(), + Data: data, + }) } // Remove removes a credential from the global git config -func Remove(repo repository.RepoConfig, id entity.Id) error { - keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id) - return repo.GlobalConfig().RemoveAll(keyPrefix) +func Remove(repo repository.RepoKeyring, id entity.Id) error { + return repo.Keyring().Remove(keyringKeyPrefix + id.String()) } /* diff --git a/bridge/core/auth/credential_base.go b/bridge/core/auth/credential_base.go index 488c223c..f9d1bf67 100644 --- a/bridge/core/auth/credential_base.go +++ b/bridge/core/auth/credential_base.go @@ -1,7 +1,10 @@ package auth import ( + "crypto/rand" + "encoding/base64" "fmt" + "strings" "time" "github.com/MichaelMure/git-bug/bridge/core" @@ -23,13 +26,22 @@ func newCredentialBase(target string) *credentialBase { } } -func newCredentialBaseFromConfig(conf map[string]string) (*credentialBase, error) { +func makeSalt() []byte { + result := make([]byte, 16) + _, err := rand.Read(result) + if err != nil { + panic(err) + } + return result +} + +func newCredentialBaseFromData(data map[string]string) (*credentialBase, error) { base := &credentialBase{ - target: conf[configKeyTarget], - meta: metaFromConfig(conf), + target: data[keyringKeyTarget], + meta: metaFromData(data), } - if createTime, ok := conf[configKeyCreateTime]; ok { + if createTime, ok := data[keyringKeyCreateTime]; ok { t, err := repository.ParseTimestamp(createTime) if err != nil { return nil, err @@ -39,7 +51,7 @@ func newCredentialBaseFromConfig(conf map[string]string) (*credentialBase, error return nil, fmt.Errorf("missing create time") } - salt, err := saltFromConfig(conf) + salt, err := saltFromData(data) if err != nil { return nil, err } @@ -48,6 +60,28 @@ func newCredentialBaseFromConfig(conf map[string]string) (*credentialBase, error return base, nil } +func metaFromData(data map[string]string) map[string]string { + result := make(map[string]string) + for key, val := range data { + if strings.HasPrefix(key, keyringKeyPrefixMeta) { + key = strings.TrimPrefix(key, keyringKeyPrefixMeta) + result[key] = val + } + } + if len(result) == 0 { + return nil + } + return result +} + +func saltFromData(data map[string]string) ([]byte, error) { + val, ok := data[keyringKeySalt] + if !ok { + return nil, fmt.Errorf("no credential salt found") + } + return base64.StdEncoding.DecodeString(val) +} + func (cb *credentialBase) Target() string { return cb.target } diff --git a/bridge/core/auth/login.go b/bridge/core/auth/login.go index ea74835a..496f2412 100644 --- a/bridge/core/auth/login.go +++ b/bridge/core/auth/login.go @@ -8,7 +8,7 @@ import ( ) const ( - configKeyLoginLogin = "login" + keyringKeyLoginLogin = "login" ) var _ Credential = &Login{} @@ -26,14 +26,14 @@ func NewLogin(target, login string) *Login { } func NewLoginFromConfig(conf map[string]string) (*Login, error) { - base, err := newCredentialBaseFromConfig(conf) + base, err := newCredentialBaseFromData(conf) if err != nil { return nil, err } return &Login{ credentialBase: base, - Login: conf[configKeyLoginLogin], + Login: conf[keyringKeyLoginLogin], }, nil } @@ -62,6 +62,6 @@ func (lp *Login) Validate() error { func (lp *Login) toConfig() map[string]string { return map[string]string{ - configKeyLoginLogin: lp.Login, + keyringKeyLoginLogin: lp.Login, } } diff --git a/bridge/core/auth/login_password.go b/bridge/core/auth/login_password.go index 1981026a..166e37fb 100644 --- a/bridge/core/auth/login_password.go +++ b/bridge/core/auth/login_password.go @@ -8,8 +8,8 @@ import ( ) const ( - configKeyLoginPasswordLogin = "login" - configKeyLoginPasswordPassword = "password" + keyringKeyLoginPasswordLogin = "login" + keyringKeyLoginPasswordPassword = "password" ) var _ Credential = &LoginPassword{} @@ -29,15 +29,15 @@ func NewLoginPassword(target, login, password string) *LoginPassword { } func NewLoginPasswordFromConfig(conf map[string]string) (*LoginPassword, error) { - base, err := newCredentialBaseFromConfig(conf) + base, err := newCredentialBaseFromData(conf) if err != nil { return nil, err } return &LoginPassword{ credentialBase: base, - Login: conf[configKeyLoginPasswordLogin], - Password: conf[configKeyLoginPasswordPassword], + Login: conf[keyringKeyLoginPasswordLogin], + Password: conf[keyringKeyLoginPasswordPassword], }, nil } @@ -70,7 +70,7 @@ func (lp *LoginPassword) Validate() error { func (lp *LoginPassword) toConfig() map[string]string { return map[string]string{ - configKeyLoginPasswordLogin: lp.Login, - configKeyLoginPasswordPassword: lp.Password, + keyringKeyLoginPasswordLogin: lp.Login, + keyringKeyLoginPasswordPassword: lp.Password, } } diff --git a/bridge/core/auth/options.go b/bridge/core/auth/options.go index 1d8c44d1..00c6e3ec 100644 --- a/bridge/core/auth/options.go +++ b/bridge/core/auth/options.go @@ -1,22 +1,22 @@ package auth -type options struct { +type listOptions struct { target string kind map[CredentialKind]interface{} meta map[string]string } -type Option func(opts *options) +type ListOption func(opts *listOptions) -func matcher(opts []Option) *options { - result := &options{} +func matcher(opts []ListOption) *listOptions { + result := &listOptions{} for _, opt := range opts { opt(result) } return result } -func (opts *options) Match(cred Credential) bool { +func (opts *listOptions) Match(cred Credential) bool { if opts.target != "" && cred.Target() != opts.target { return false } @@ -35,15 +35,15 @@ func (opts *options) Match(cred Credential) bool { return true } -func WithTarget(target string) Option { - return func(opts *options) { +func WithTarget(target string) ListOption { + return func(opts *listOptions) { opts.target = target } } // WithKind match credentials with the given kind. Can be specified multiple times. -func WithKind(kind CredentialKind) Option { - return func(opts *options) { +func WithKind(kind CredentialKind) ListOption { + return func(opts *listOptions) { if opts.kind == nil { opts.kind = make(map[CredentialKind]interface{}) } @@ -51,8 +51,8 @@ func WithKind(kind CredentialKind) Option { } } -func WithMeta(key string, val string) Option { - return func(opts *options) { +func WithMeta(key string, val string) ListOption { + return func(opts *listOptions) { if opts.meta == nil { opts.meta = make(map[string]string) } diff --git a/bridge/core/auth/token.go b/bridge/core/auth/token.go index 1f019f44..84d6ac13 100644 --- a/bridge/core/auth/token.go +++ b/bridge/core/auth/token.go @@ -8,7 +8,7 @@ import ( ) const ( - configKeyTokenValue = "value" + keyringKeyTokenValue = "value" ) var _ Credential = &Token{} @@ -28,14 +28,14 @@ func NewToken(target, value string) *Token { } func NewTokenFromConfig(conf map[string]string) (*Token, error) { - base, err := newCredentialBaseFromConfig(conf) + base, err := newCredentialBaseFromData(conf) if err != nil { return nil, err } return &Token{ credentialBase: base, - Value: conf[configKeyTokenValue], + Value: conf[keyringKeyTokenValue], }, nil } @@ -65,6 +65,6 @@ func (t *Token) Validate() error { func (t *Token) toConfig() map[string]string { return map[string]string{ - configKeyTokenValue: t.Value, + keyringKeyTokenValue: t.Value, } } diff --git a/bridge/github/config.go b/bridge/github/config.go index 61d641a6..130b0ad1 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -239,7 +239,7 @@ func randomFingerprint() string { return string(b) } -func promptTokenOptions(repo repository.RepoConfig, login, owner, project string) (auth.Credential, error) { +func promptTokenOptions(repo repository.RepoKeyring, login, owner, project string) (auth.Credential, error) { creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken), diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go index e4e3d8e3..dfac4c54 100644 --- a/bridge/gitlab/config.go +++ b/bridge/gitlab/config.go @@ -156,7 +156,7 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error { return nil } -func promptTokenOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) { +func promptTokenOptions(repo repository.RepoKeyring, login, baseUrl string) (auth.Credential, error) { creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken), diff --git a/bridge/jira/config.go b/bridge/jira/config.go index ffd3bdc1..717046e2 100644 --- a/bridge/jira/config.go +++ b/bridge/jira/config.go @@ -163,7 +163,7 @@ func (*Jira) ValidateConfig(conf core.Configuration) error { return nil } -func promptCredOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) { +func promptCredOptions(repo repository.RepoKeyring, login, baseUrl string) (auth.Credential, error) { creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken), diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 563fac6b..eeb7fb90 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -25,6 +25,8 @@ const formatVersion = 2 const defaultMaxLoadedBugs = 1000 var _ repository.RepoCommon = &RepoCache{} +var _ repository.RepoConfig = &RepoCache{} +var _ repository.RepoKeyring = &RepoCache{} // RepoCache is a cache for a Repository. This cache has multiple functions: // diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go index a931f2be..95e2f7bb 100644 --- a/cache/repo_cache_common.go +++ b/cache/repo_cache_common.go @@ -20,11 +20,20 @@ func (c *RepoCache) LocalConfig() repository.Config { return c.repo.LocalConfig() } -// GlobalConfig give access to the git global configuration +// GlobalConfig give access to the global scoped configuration func (c *RepoCache) GlobalConfig() repository.Config { return c.repo.GlobalConfig() } +// AnyConfig give access to a merged local/global configuration +func (c *RepoCache) AnyConfig() repository.ConfigRead { + return c.repo.AnyConfig() +} + +func (c *RepoCache) Keyring() repository.Keyring { + return c.repo.Keyring() +} + // GetPath returns the path to the repo. func (c *RepoCache) GetPath() string { return c.repo.GetPath() diff --git a/commands/env.go b/commands/env.go index 59c5d33b..5658342d 100644 --- a/commands/env.go +++ b/commands/env.go @@ -54,7 +54,7 @@ func loadRepo(env *Env) func(*cobra.Command, []string) error { return fmt.Errorf("unable to get the current working directory: %q", err) } - env.repo, err = repository.NewGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader}) + env.repo, err = repository.NewGoGitRepo(cwd, []repository.ClockLoader{bug.ClockLoader}) if err == repository.ErrNotARepo { return fmt.Errorf("%s must be run from within a git repo", rootCommandName) } diff --git a/commands/webui.go b/commands/webui.go index 4d87a303..7e5fc752 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -139,7 +139,7 @@ func runWebUI(env *Env, opts webUIOptions, args []string) error { env.out.Printf("Graphql Playground: http://%s/playground\n", addr) env.out.Println("Press Ctrl+c to quit") - configOpen, err := env.repo.LocalConfig().ReadBool(webUIOpenConfigKey) + configOpen, err := env.repo.AnyConfig().ReadBool(webUIOpenConfigKey) if err == repository.ErrNoConfigEntry { // default to true configOpen = true @@ -1,9 +1,10 @@ module github.com/MichaelMure/git-bug -go 1.12 +go 1.13 require ( github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b + github.com/99designs/keyring v1.1.5 github.com/MichaelMure/go-term-text v0.2.9 github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 @@ -12,6 +13,7 @@ require ( github.com/corpix/uarand v0.1.1 // indirect github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.9.0 + github.com/go-git/go-git/v5 v5.1.0 github.com/gorilla/mux v1.8.0 github.com/hashicorp/golang-lru v0.5.4 github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 @@ -25,8 +27,12 @@ require ( github.com/stretchr/testify v1.6.1 github.com/vektah/gqlparser v1.3.1 github.com/xanzy/go-gitlab v0.33.0 - golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 + golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/text v0.3.3 ) + +// Use a forked go-git for now until https://github.com/go-git/go-git/pull/112 is merged +// and released. +replace github.com/go-git/go-git/v5 => github.com/MichaelMure/go-git/v5 v5.1.1-0.20200827115354-b40ca794fe33 @@ -1,12 +1,14 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/99designs/gqlgen v0.10.3-0.20200208093655-ab8d62b67dd0 h1:ADy3XJwhOYg6Pb90XeXazWvO+9gpOsgLuaM1buZUZOY= -github.com/99designs/gqlgen v0.10.3-0.20200208093655-ab8d62b67dd0/go.mod h1:dfBhwZKMcSYiYRMTs8qWF+Oha6782e1xPfgRmVal9I8= github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b h1:510xa84qGbDemwTHNio4cLWkdKFxxJgVtsIOH+Ku8bo= github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b/go.mod h1:dfBhwZKMcSYiYRMTs8qWF+Oha6782e1xPfgRmVal9I8= +github.com/99designs/keyring v1.1.5 h1:wLv7QyzYpFIyMSwOADq1CLTF9KbjbBfcnfmOGJ64aO4= +github.com/99designs/keyring v1.1.5/go.mod h1:7hsVvt2qXgtadGevGJ4ujg+u8m6SpJ5TpHqTozIPqf0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/MichaelMure/go-git/v5 v5.1.1-0.20200827115354-b40ca794fe33 h1:QFzkZPUMm0HRZ0dZ+GgDKHPUrgUrH3CbcyuzQlhBeww= +github.com/MichaelMure/go-git/v5 v5.1.1-0.20200827115354-b40ca794fe33/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= github.com/MichaelMure/go-term-text v0.2.6 h1:dSmJSzk2iI5xWymSMrMbdVM1bxYWu3DjDFhdcJvAuqA= github.com/MichaelMure/go-term-text v0.2.6/go.mod h1:o2Z5T3b28F4kwAojGvvNdbzjHf9t18vbQ7E2pmTe2Ww= github.com/MichaelMure/go-term-text v0.2.7 h1:nSYvYGwXxJoiQu6kdGSErpxZ6ah/4WlJyp/niqQor6g= @@ -18,15 +20,21 @@ github.com/MichaelMure/go-term-text v0.2.9/go.mod h1:2QSU/Nn2u41Tqoar+90RlYuhjng github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +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/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM= github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 h1:QvIfX96O11qjX1Zr3hKkG0dI12JBRBGABWffyZ1GI60= github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0= @@ -45,7 +53,6 @@ github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -53,33 +60,48 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbp github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/corpix/uarand v0.1.1 h1:RMr1TWc9F4n5jiPDzFHtmaUXLKLNUFK0SgCLo4BhX/U= github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= +github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU= +github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4= -github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= -github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg= -github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= +github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk= +github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -92,14 +114,12 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= @@ -109,6 +129,8 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= @@ -122,12 +144,19 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 h1:Mo9W14pwbO9VfRe+ygqZ8dFbPpoIK1HFrG/zjTuQ+nc= github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= +github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= +github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jondot/goweight v1.0.5 h1:aRpnyj1G8BLLNhem8xezuuV0GlFz4G11e3/UtBU/FlQ= -github.com/jondot/goweight v1.0.5/go.mod h1:3PRcpOwkyspe1t4+KCNgauas+aNDTSSCwZ6AQ4kDD/A= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM= +github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -137,6 +166,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= @@ -148,22 +179,21 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= -github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53 h1:tGfIHhDghvEnneeRhODvGYOt305TPwingKt6p90F4MU= -github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -192,12 +222,12 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 h1:Vk3RiBQpF0Ja+OqbFG7lYTk79+l8Cm2QESLXB0x6u6U= github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= @@ -215,24 +245,17 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= -github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= -github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -242,35 +265,18 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0= -github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g= -github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713 h1:knaxjm6QMbUMNvuaSnJZmw0gRX4V/79JVUQiziJGM84= -github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= -github.com/vektah/gqlparser v1.2.1 h1:C+L7Go/eUbN0w6Y0kaiq2W6p2wN5j8wU82EdDXxDivc= -github.com/vektah/gqlparser v1.2.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU= github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU= github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= -github.com/xanzy/go-gitlab v0.22.1 h1:TVxgHmoa35jQL+9FCkG0nwPDxU9dQZXknBTDtGaSFno= -github.com/xanzy/go-gitlab v0.22.1/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.24.0 h1:zP1zC4K76Gha0coN5GhygOLhsHTCvUjrnqGL3kHXkVU= -github.com/xanzy/go-gitlab v0.24.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.25.0 h1:G5aTZeqZd66Q6qMVieBfmHBsPpF0jY92zCLAMpULe3I= -github.com/xanzy/go-gitlab v0.25.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.26.0 h1:eAnJRBUC+GDJSy8OoGCZBqBMpXsGOOT235TFm/F8C0Q= -github.com/xanzy/go-gitlab v0.26.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.27.0 h1:zy7xBB8+PID6izH07ZArtkEisJ192dtQajRaeo4+glg= -github.com/xanzy/go-gitlab v0.27.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og= -github.com/xanzy/go-gitlab v0.29.0 h1:9tMvAkG746eIlzcdpnRgpcKPA1woUDmldMIjR/E5OWM= -github.com/xanzy/go-gitlab v0.29.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.33.0 h1:MUJZknbLhVXSFzBA5eqGGhQ2yHSu8tPbGBPeB3sN4B0= github.com/xanzy/go-gitlab v0.33.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -278,11 +284,14 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -297,6 +306,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -309,16 +320,19 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -348,11 +362,18 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/identity/identity_actions.go b/identity/identity_actions.go index aa6a2a91..e33b75f9 100644 --- a/identity/identity_actions.go +++ b/identity/identity_actions.go @@ -4,9 +4,10 @@ import ( "fmt" "strings" + "github.com/pkg/errors" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" - "github.com/pkg/errors" ) // Fetch retrieve updates from a remote diff --git a/identity/identity_user.go b/identity/identity_user.go index 60622c12..cd67459e 100644 --- a/identity/identity_user.go +++ b/identity/identity_user.go @@ -35,24 +35,19 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { } func GetUserIdentityId(repo repository.Repo) (entity.Id, error) { - configs, err := repo.LocalConfig().ReadAll(identityConfigKey) - if err != nil { - return entity.UnsetId, err - } - - if len(configs) == 0 { + val, err := repo.LocalConfig().ReadString(identityConfigKey) + if err == repository.ErrNoConfigEntry { return entity.UnsetId, ErrNoIdentitySet } - - if len(configs) > 1 { + if err == repository.ErrMultipleConfigEntry { return entity.UnsetId, ErrMultipleIdentitiesSet } - - var id entity.Id - for _, val := range configs { - id = entity.Id(val) + if err != nil { + return entity.UnsetId, err } + var id = entity.Id(val) + if err := id.Validate(); err != nil { return entity.UnsetId, err } @@ -62,10 +57,12 @@ func GetUserIdentityId(repo repository.Repo) (entity.Id, error) { // IsUserIdentitySet say if the user has set his identity func IsUserIdentitySet(repo repository.Repo) (bool, error) { - configs, err := repo.LocalConfig().ReadAll(identityConfigKey) + _, err := repo.LocalConfig().ReadString(identityConfigKey) + if err == repository.ErrNoConfigEntry { + return false, nil + } if err != nil { return false, err } - - return len(configs) == 1, nil + return true, nil } diff --git a/misc/random_bugs/cmd/main.go b/misc/random_bugs/cmd/main.go index ec62b6ed..3127b4aa 100644 --- a/misc/random_bugs/cmd/main.go +++ b/misc/random_bugs/cmd/main.go @@ -20,7 +20,7 @@ func main() { bug.ClockLoader, } - repo, err := repository.NewGitRepo(dir, loaders) + repo, err := repository.NewGoGitRepo(dir, loaders) if err != nil { panic(err) } diff --git a/repository/config.go b/repository/config.go index 4fa5c69b..4db8d4be 100644 --- a/repository/config.go +++ b/repository/config.go @@ -1,21 +1,23 @@ package repository import ( + "errors" "strconv" "time" ) +var ( + ErrNoConfigEntry = errors.New("no config entry for the given key") + ErrMultipleConfigEntry = errors.New("multiple config entry for the given key") +) + // Config represent the common function interacting with the repository config storage type Config interface { - // Store writes a single key/value pair in the config - StoreString(key, value string) error - - // Store writes a key and timestamp value to the config - StoreTimestamp(key string, value time.Time) error - - // Store writes a key and boolean value to the config - StoreBool(key string, value bool) error + ConfigRead + ConfigWrite +} +type ConfigRead interface { // ReadAll reads all key/value pair matching the key prefix ReadAll(keyPrefix string) (map[string]string, error) @@ -33,6 +35,17 @@ type Config interface { // Return ErrNoConfigEntry or ErrMultipleConfigEntry if // there is zero or more than one entry for this key ReadTimestamp(key string) (time.Time, error) +} + +type ConfigWrite interface { + // Store writes a single key/value pair in the config + StoreString(key, value string) error + + // Store writes a key and timestamp value to the config + StoreTimestamp(key string, value time.Time) error + + // Store writes a key and boolean value to the config + StoreBool(key string, value bool) error // RemoveAll removes all key/value pair matching the key prefix RemoveAll(keyPrefix string) error @@ -46,3 +59,87 @@ func ParseTimestamp(s string) (time.Time, error) { return time.Unix(int64(timestamp), 0), nil } + +// mergeConfig is a helper to easily support RepoConfig.AnyConfig() +// from two separate local and global Config +func mergeConfig(local ConfigRead, global ConfigRead) *mergedConfig { + return &mergedConfig{ + local: local, + global: global, + } +} + +var _ ConfigRead = &mergedConfig{} + +type mergedConfig struct { + local ConfigRead + global ConfigRead +} + +func (m *mergedConfig) ReadAll(keyPrefix string) (map[string]string, error) { + values, err := m.global.ReadAll(keyPrefix) + if err != nil { + return nil, err + } + locals, err := m.local.ReadAll(keyPrefix) + if err != nil { + return nil, err + } + for k, val := range locals { + values[k] = val + } + return values, nil +} + +func (m *mergedConfig) ReadBool(key string) (bool, error) { + v, err := m.local.ReadBool(key) + if err == nil { + return v, nil + } + if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry { + return false, err + } + return m.global.ReadBool(key) +} + +func (m *mergedConfig) ReadString(key string) (string, error) { + val, err := m.local.ReadString(key) + if err == nil { + return val, nil + } + if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry { + return "", err + } + return m.global.ReadString(key) +} + +func (m *mergedConfig) ReadTimestamp(key string) (time.Time, error) { + val, err := m.local.ReadTimestamp(key) + if err == nil { + return val, nil + } + if err != ErrNoConfigEntry && err != ErrMultipleConfigEntry { + return time.Time{}, err + } + return m.global.ReadTimestamp(key) +} + +var _ ConfigWrite = &configPanicWriter{} + +type configPanicWriter struct{} + +func (c configPanicWriter) StoreString(key, value string) error { + panic("not implemented") +} + +func (c configPanicWriter) StoreTimestamp(key string, value time.Time) error { + panic("not implemented") +} + +func (c configPanicWriter) StoreBool(key string, value bool) error { + panic("not implemented") +} + +func (c configPanicWriter) RemoveAll(keyPrefix string) error { + panic("not implemented") +} diff --git a/repository/config_test.go b/repository/config_test.go new file mode 100644 index 00000000..2a763540 --- /dev/null +++ b/repository/config_test.go @@ -0,0 +1,54 @@ +package repository + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestMergedConfig(t *testing.T) { + local := NewMemConfig() + global := NewMemConfig() + merged := mergeConfig(local, global) + + require.NoError(t, global.StoreBool("bool", true)) + require.NoError(t, global.StoreString("string", "foo")) + require.NoError(t, global.StoreTimestamp("timestamp", time.Unix(1234, 0))) + + val1, err := merged.ReadBool("bool") + require.NoError(t, err) + require.Equal(t, val1, true) + + val2, err := merged.ReadString("string") + require.NoError(t, err) + require.Equal(t, val2, "foo") + + val3, err := merged.ReadTimestamp("timestamp") + require.NoError(t, err) + require.Equal(t, val3, time.Unix(1234, 0)) + + require.NoError(t, local.StoreBool("bool", false)) + require.NoError(t, local.StoreString("string", "bar")) + require.NoError(t, local.StoreTimestamp("timestamp", time.Unix(5678, 0))) + + val1, err = merged.ReadBool("bool") + require.NoError(t, err) + require.Equal(t, val1, false) + + val2, err = merged.ReadString("string") + require.NoError(t, err) + require.Equal(t, val2, "bar") + + val3, err = merged.ReadTimestamp("timestamp") + require.NoError(t, err) + require.Equal(t, val3, time.Unix(5678, 0)) + + all, err := merged.ReadAll("") + require.NoError(t, err) + require.Equal(t, all, map[string]string{ + "bool": "false", + "string": "bar", + "timestamp": "5678", + }) +} diff --git a/repository/config_testing.go b/repository/config_testing.go index 25639d59..445f8721 100644 --- a/repository/config_testing.go +++ b/repository/config_testing.go @@ -2,62 +2,115 @@ package repository import ( "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testConfig(t *testing.T, config Config) { + // string err := config.StoreString("section.key", "value") - assert.NoError(t, err) + require.NoError(t, err) val, err := config.ReadString("section.key") - assert.NoError(t, err) - assert.Equal(t, "value", val) + require.NoError(t, err) + require.Equal(t, "value", val) - err = config.StoreString("section.true", "true") - assert.NoError(t, err) + // bool + err = config.StoreBool("section.true", true) + require.NoError(t, err) val2, err := config.ReadBool("section.true") - assert.NoError(t, err) - assert.Equal(t, true, val2) + require.NoError(t, err) + require.Equal(t, true, val2) + // timestamp + err = config.StoreTimestamp("section.time", time.Unix(1234, 0)) + require.NoError(t, err) + + val3, err := config.ReadTimestamp("section.time") + require.NoError(t, err) + require.Equal(t, time.Unix(1234, 0), val3) + + // ReadAll configs, err := config.ReadAll("section") - assert.NoError(t, err) - assert.Equal(t, map[string]string{ + require.NoError(t, err) + require.Equal(t, map[string]string{ "section.key": "value", "section.true": "true", + "section.time": "1234", }, configs) + // RemoveAll err = config.RemoveAll("section.true") - assert.NoError(t, err) + require.NoError(t, err) configs, err = config.ReadAll("section") - assert.NoError(t, err) - assert.Equal(t, map[string]string{ - "section.key": "value", + require.NoError(t, err) + require.Equal(t, map[string]string{ + "section.key": "value", + "section.time": "1234", }, configs) _, err = config.ReadBool("section.true") - assert.Equal(t, ErrNoConfigEntry, err) + require.Equal(t, ErrNoConfigEntry, err) err = config.RemoveAll("section.nonexistingkey") - assert.Error(t, err) + require.Error(t, err) err = config.RemoveAll("section.key") - assert.NoError(t, err) + require.NoError(t, err) _, err = config.ReadString("section.key") - assert.Equal(t, ErrNoConfigEntry, err) + require.Equal(t, ErrNoConfigEntry, err) err = config.RemoveAll("nonexistingsection") - assert.Error(t, err) + require.Error(t, err) + + err = config.RemoveAll("section.time") + require.NoError(t, err) err = config.RemoveAll("section") - assert.Error(t, err) + require.Error(t, err) _, err = config.ReadString("section.key") - assert.Error(t, err) + require.Error(t, err) err = config.RemoveAll("section.key") - assert.Error(t, err) + require.Error(t, err) + + // section + subsections + require.NoError(t, config.StoreString("section.opt1", "foo")) + require.NoError(t, config.StoreString("section.opt2", "foo2")) + require.NoError(t, config.StoreString("section.subsection.opt1", "foo3")) + require.NoError(t, config.StoreString("section.subsection.opt2", "foo4")) + require.NoError(t, config.StoreString("section.subsection.subsection.opt1", "foo5")) + require.NoError(t, config.StoreString("section.subsection.subsection.opt2", "foo6")) + + all, err := config.ReadAll("section") + require.NoError(t, err) + require.Equal(t, map[string]string{ + "section.opt1": "foo", + "section.opt2": "foo2", + "section.subsection.opt1": "foo3", + "section.subsection.opt2": "foo4", + "section.subsection.subsection.opt1": "foo5", + "section.subsection.subsection.opt2": "foo6", + }, all) + + all, err = config.ReadAll("section.subsection") + require.NoError(t, err) + require.Equal(t, map[string]string{ + "section.subsection.opt1": "foo3", + "section.subsection.opt2": "foo4", + "section.subsection.subsection.opt1": "foo5", + "section.subsection.subsection.opt2": "foo6", + }, all) + + all, err = config.ReadAll("section.subsection.subsection") + require.NoError(t, err) + require.Equal(t, map[string]string{ + "section.subsection.subsection.opt1": "foo5", + "section.subsection.subsection.opt2": "foo6", + }, all) } diff --git a/repository/git.go b/repository/git.go index 3d756324..dba2d29d 100644 --- a/repository/git.go +++ b/repository/git.go @@ -4,8 +4,6 @@ package repository import ( "bytes" "fmt" - "io" - "os/exec" "path" "strings" "sync" @@ -22,70 +20,28 @@ var _ TestedRepo = &GitRepo{} // GitRepo represents an instance of a (local) git repository. type GitRepo struct { + gitCli path string clocksMutex sync.Mutex clocks map[string]lamport.Clock -} - -// LocalConfig give access to the repository scoped configuration -func (repo *GitRepo) LocalConfig() Config { - return newGitConfig(repo, false) -} - -// GlobalConfig give access to the git global configuration -func (repo *GitRepo) GlobalConfig() Config { - return newGitConfig(repo, true) -} - -// Run the given git command with the given I/O reader/writers, returning an error if it fails. -func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { - // make sure that the working directory for the command - // always exist, in particular when running "git init". - path := strings.TrimSuffix(repo.path, ".git") - - // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " ")) - - cmd := exec.Command("git", args...) - cmd.Dir = path - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - - return cmd.Run() -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) { - var stdout bytes.Buffer - var stderr bytes.Buffer - err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...) - return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) { - stdout, stderr, err := repo.runGitCommandRaw(stdin, args...) - if err != nil { - if stderr == "" { - stderr = "Error running git command: " + strings.Join(args, " ") - } - err = fmt.Errorf(stderr) - } - return stdout, err -} -// Run the given git command and return its stdout, or an error if the command fails. -func (repo *GitRepo) runGitCommand(args ...string) (string, error) { - return repo.runGitCommandWithStdin(nil, args...) + keyring Keyring } // NewGitRepo determines if the given working directory is inside of a git repository, // and returns the corresponding GitRepo instance if it is. func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { + k, err := defaultKeyring() + if err != nil { + return nil, err + } + repo := &GitRepo{ - path: path, - clocks: make(map[string]lamport.Clock), + gitCli: gitCli{path: path}, + path: path, + clocks: make(map[string]lamport.Clock), + keyring: k, } // Check the repo and retrieve the root path @@ -100,6 +56,7 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { // Fix the path to be sure we are at the root repo.path = stdout + repo.gitCli.path = stdout for _, loader := range clockLoaders { allExist := true @@ -123,6 +80,7 @@ func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { // InitGitRepo create a new empty git repo at the given path func InitGitRepo(path string) (*GitRepo, error) { repo := &GitRepo{ + gitCli: gitCli{path: path}, path: path + "/.git", clocks: make(map[string]lamport.Clock), } @@ -138,6 +96,7 @@ func InitGitRepo(path string) (*GitRepo, error) { // InitBareGitRepo create a new --bare empty git repo at the given path func InitBareGitRepo(path string) (*GitRepo, error) { repo := &GitRepo{ + gitCli: gitCli{path: path}, path: path, clocks: make(map[string]lamport.Clock), } @@ -150,6 +109,26 @@ func InitBareGitRepo(path string) (*GitRepo, error) { return repo, nil } +// LocalConfig give access to the repository scoped configuration +func (repo *GitRepo) LocalConfig() Config { + return newGitConfig(repo.gitCli, false) +} + +// GlobalConfig give access to the global scoped configuration +func (repo *GitRepo) GlobalConfig() Config { + return newGitConfig(repo.gitCli, true) +} + +// AnyConfig give access to a merged local/global configuration +func (repo *GitRepo) AnyConfig() ConfigRead { + return mergeConfig(repo.LocalConfig(), repo.GlobalConfig()) +} + +// Keyring give access to a user-wide storage for secrets +func (repo *GitRepo) Keyring() Keyring { + return repo.keyring +} + // GetPath returns the path to the repo. func (repo *GitRepo) GetPath() string { return repo.path @@ -290,8 +269,8 @@ func (repo *GitRepo) RemoveRef(ref string) error { } // ListRefs will return a list of Git ref matching the given refspec -func (repo *GitRepo) ListRefs(refspec string) ([]string, error) { - stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refspec) +func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) { + stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix) if err != nil { return nil, err diff --git a/repository/git_cli.go b/repository/git_cli.go new file mode 100644 index 00000000..085b1cda --- /dev/null +++ b/repository/git_cli.go @@ -0,0 +1,56 @@ +package repository + +import ( + "bytes" + "fmt" + "io" + "os/exec" + "strings" +) + +// gitCli is a helper to launch CLI git commands +type gitCli struct { + path string +} + +// Run the given git command with the given I/O reader/writers, returning an error if it fails. +func (cli gitCli) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { + // make sure that the working directory for the command + // always exist, in particular when running "git init". + path := strings.TrimSuffix(cli.path, ".git") + + // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " ")) + + cmd := exec.Command("git", args...) + cmd.Dir = path + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + + return cmd.Run() +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (cli gitCli) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + err := cli.runGitCommandWithIO(stdin, &stdout, &stderr, args...) + return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (cli gitCli) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) { + stdout, stderr, err := cli.runGitCommandRaw(stdin, args...) + if err != nil { + if stderr == "" { + stderr = "Error running git command: " + strings.Join(args, " ") + } + err = fmt.Errorf(stderr) + } + return stdout, err +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (cli gitCli) runGitCommand(args ...string) (string, error) { + return cli.runGitCommandWithStdin(nil, args...) +} diff --git a/repository/git_config.go b/repository/git_config.go index 987cf195..b46cc69b 100644 --- a/repository/git_config.go +++ b/repository/git_config.go @@ -14,24 +14,24 @@ import ( var _ Config = &gitConfig{} type gitConfig struct { - repo *GitRepo + cli gitCli localityFlag string } -func newGitConfig(repo *GitRepo, global bool) *gitConfig { +func newGitConfig(cli gitCli, global bool) *gitConfig { localityFlag := "--local" if global { localityFlag = "--global" } return &gitConfig{ - repo: repo, + cli: cli, localityFlag: localityFlag, } } // StoreString store a single key/value pair in the config of the repo func (gc *gitConfig) StoreString(key string, value string) error { - _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--replace-all", key, value) + _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--replace-all", key, value) return err } @@ -45,7 +45,7 @@ func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error { // ReadAll read all key/value pair matching the key prefix func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) { - stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix) + stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix) // / \ // / ! \ @@ -74,7 +74,7 @@ func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) { } func (gc *gitConfig) ReadString(key string) (string, error) { - stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key) + stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key) // / \ // / ! \ @@ -116,12 +116,12 @@ func (gc *gitConfig) ReadTimestamp(key string) (time.Time, error) { } func (gc *gitConfig) rmSection(keyPrefix string) error { - _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix) + _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix) return err } func (gc *gitConfig) unsetAll(keyPrefix string) error { - _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix) + _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix) return err } @@ -180,7 +180,7 @@ func (gc *gitConfig) RemoveAll(keyPrefix string) error { } func (gc *gitConfig) gitVersion() (*semver.Version, error) { - versionOut, err := gc.repo.runGitCommand("version") + versionOut, err := gc.cli.runGitCommand("version") if err != nil { return nil, err } diff --git a/repository/git_testing.go b/repository/git_testing.go index 5ae4ccc9..7d40bf1f 100644 --- a/repository/git_testing.go +++ b/repository/git_testing.go @@ -3,6 +3,8 @@ package repository import ( "io/ioutil" "log" + + "github.com/99designs/keyring" ) // This is intended for testing only @@ -34,7 +36,11 @@ func CreateTestRepo(bare bool) TestedRepo { log.Fatal("failed to set user.email for test repository: ", err) } - return repo + // make sure we use a mock keyring for testing to not interact with the global system + return &replaceKeyring{ + TestedRepo: repo, + keyring: keyring.NewArrayKeyring(nil), + } } func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) { @@ -56,3 +62,13 @@ func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) { return repoA, repoB, remote } + +// replaceKeyring allow to replace the Keyring of the underlying repo +type replaceKeyring struct { + TestedRepo + keyring Keyring +} + +func (rk replaceKeyring) Keyring() Keyring { + return rk.keyring +} diff --git a/repository/gogit.go b/repository/gogit.go new file mode 100644 index 00000000..09f714ea --- /dev/null +++ b/repository/gogit.go @@ -0,0 +1,615 @@ +package repository + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + stdpath "path" + "path/filepath" + "strings" + "sync" + "time" + + gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/object" + + "github.com/MichaelMure/git-bug/util/lamport" +) + +var _ ClockedRepo = &GoGitRepo{} + +type GoGitRepo struct { + r *gogit.Repository + path string + + clocksMutex sync.Mutex + clocks map[string]lamport.Clock + + keyring Keyring +} + +func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) { + path, err := detectGitPath(path) + if err != nil { + return nil, err + } + + r, err := gogit.PlainOpen(path) + if err != nil { + return nil, err + } + + k, err := defaultKeyring() + if err != nil { + return nil, err + } + + repo := &GoGitRepo{ + r: r, + path: path, + clocks: make(map[string]lamport.Clock), + keyring: k, + } + + for _, loader := range clockLoaders { + allExist := true + for _, name := range loader.Clocks { + if _, err := repo.getClock(name); err != nil { + allExist = false + } + } + + if !allExist { + err = loader.Witnesser(repo) + if err != nil { + return nil, err + } + } + } + + return repo, nil +} + +func detectGitPath(path string) (string, error) { + // normalize the path + path, err := filepath.Abs(path) + if err != nil { + return "", err + } + + for { + fi, err := os.Stat(stdpath.Join(path, ".git")) + if err == nil { + if !fi.IsDir() { + return "", fmt.Errorf(".git exist but is not a directory") + } + return stdpath.Join(path, ".git"), nil + } + if !os.IsNotExist(err) { + // unknown error + return "", err + } + + // detect bare repo + ok, err := isGitDir(path) + if err != nil { + return "", err + } + if ok { + return path, nil + } + + if parent := filepath.Dir(path); parent == path { + return "", fmt.Errorf(".git not found") + } else { + path = parent + } + } +} + +func isGitDir(path string) (bool, error) { + markers := []string{"HEAD", "objects", "refs"} + + for _, marker := range markers { + _, err := os.Stat(stdpath.Join(path, marker)) + if err == nil { + continue + } + if !os.IsNotExist(err) { + // unknown error + return false, err + } else { + return false, nil + } + } + + return true, nil +} + +// InitGoGitRepo create a new empty git repo at the given path +func InitGoGitRepo(path string) (*GoGitRepo, error) { + r, err := gogit.PlainInit(path, false) + if err != nil { + return nil, err + } + + return &GoGitRepo{ + r: r, + path: path + "/.git", + clocks: make(map[string]lamport.Clock), + }, nil +} + +// InitBareGoGitRepo create a new --bare empty git repo at the given path +func InitBareGoGitRepo(path string) (*GoGitRepo, error) { + r, err := gogit.PlainInit(path, true) + if err != nil { + return nil, err + } + + return &GoGitRepo{ + r: r, + path: path, + clocks: make(map[string]lamport.Clock), + }, nil +} + +// LocalConfig give access to the repository scoped configuration +func (repo *GoGitRepo) LocalConfig() Config { + return newGoGitLocalConfig(repo.r) +} + +// GlobalConfig give access to the global scoped configuration +func (repo *GoGitRepo) GlobalConfig() Config { + // TODO: replace that with go-git native implementation once it's supported + // see: https://github.com/go-git/go-git + // see: https://github.com/src-d/go-git/issues/760 + return newGoGitGlobalConfig(repo.r) +} + +// AnyConfig give access to a merged local/global configuration +func (repo *GoGitRepo) AnyConfig() ConfigRead { + return mergeConfig(repo.LocalConfig(), repo.GlobalConfig()) +} + +// Keyring give access to a user-wide storage for secrets +func (repo *GoGitRepo) Keyring() Keyring { + return repo.keyring +} + +// GetPath returns the path to the repo. +func (repo *GoGitRepo) GetPath() string { + return repo.path +} + +// GetUserName returns the name the the user has used to configure git +func (repo *GoGitRepo) GetUserName() (string, error) { + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + return cfg.User.Name, nil +} + +// GetUserEmail returns the email address that the user has used to configure git. +func (repo *GoGitRepo) GetUserEmail() (string, error) { + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + return cfg.User.Email, nil +} + +// GetCoreEditor returns the name of the editor that the user has used to configure git. +func (repo *GoGitRepo) GetCoreEditor() (string, error) { + // See https://git-scm.com/docs/git-var + // The order of preference is the $GIT_EDITOR environment variable, then core.editor configuration, then $VISUAL, then $EDITOR, and then the default chosen at compile time, which is usually vi. + + if val, ok := os.LookupEnv("GIT_EDITOR"); ok { + return val, nil + } + + val, err := repo.AnyConfig().ReadString("core.editor") + if err == nil && val != "" { + return val, nil + } + if err != nil && err != ErrNoConfigEntry { + return "", err + } + + if val, ok := os.LookupEnv("VISUAL"); ok { + return val, nil + } + + if val, ok := os.LookupEnv("EDITOR"); ok { + return val, nil + } + + return "vi", nil +} + +// GetRemotes returns the configured remotes repositories. +func (repo *GoGitRepo) GetRemotes() (map[string]string, error) { + cfg, err := repo.r.Config() + if err != nil { + return nil, err + } + + result := make(map[string]string, len(cfg.Remotes)) + for name, remote := range cfg.Remotes { + if len(remote.URLs) > 0 { + result[name] = remote.URLs[0] + } + } + + return result, nil +} + +// FetchRefs fetch git refs from a remote +func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) { + buf := bytes.NewBuffer(nil) + + err := repo.r.Fetch(&gogit.FetchOptions{ + RemoteName: remote, + RefSpecs: []config.RefSpec{config.RefSpec(refSpec)}, + Progress: buf, + }) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// PushRefs push git refs to a remote +func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) { + buf := bytes.NewBuffer(nil) + + err := repo.r.Push(&gogit.PushOptions{ + RemoteName: remote, + RefSpecs: []config.RefSpec{config.RefSpec(refSpec)}, + Progress: buf, + }) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// StoreData will store arbitrary data and return the corresponding hash +func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) { + obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.BlobObject) + + w, err := obj.Writer() + if err != nil { + return "", err + } + + _, err = w.Write(data) + if err != nil { + return "", err + } + + h, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(h.String()), nil +} + +// ReadData will attempt to read arbitrary data from the given hash +func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) { + obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String())) + if err != nil { + return nil, err + } + + r, err := obj.Reader() + if err != nil { + return nil, err + } + + return ioutil.ReadAll(r) +} + +// StoreTree will store a mapping key-->Hash as a Git tree +func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { + var tree object.Tree + + for _, entry := range mapping { + mode := filemode.Regular + if entry.ObjectType == Tree { + mode = filemode.Dir + } + + tree.Entries = append(tree.Entries, object.TreeEntry{ + Name: entry.Name, + Mode: mode, + Hash: plumbing.NewHash(entry.Hash.String()), + }) + } + + obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.TreeObject) + err := tree.Encode(obj) + if err != nil { + return "", err + } + + hash, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(hash.String()), nil +} + +// ReadTree will return the list of entries in a Git tree +func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { + h := plumbing.NewHash(hash.String()) + + // the given hash could be a tree or a commit + obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h) + if err != nil { + return nil, err + } + + var tree *object.Tree + switch obj.Type() { + case plumbing.TreeObject: + tree, err = object.DecodeTree(repo.r.Storer, obj) + case plumbing.CommitObject: + var commit *object.Commit + commit, err = object.DecodeCommit(repo.r.Storer, obj) + if err != nil { + return nil, err + } + tree, err = commit.Tree() + default: + return nil, fmt.Errorf("given hash is not a tree") + } + if err != nil { + return nil, err + } + + treeEntries := make([]TreeEntry, len(tree.Entries)) + for i, entry := range tree.Entries { + objType := Blob + if entry.Mode == filemode.Dir { + objType = Tree + } + + treeEntries[i] = TreeEntry{ + ObjectType: objType, + Hash: Hash(entry.Hash.String()), + Name: entry.Name, + } + } + + return treeEntries, nil +} + +// StoreCommit will store a Git commit with the given Git tree +func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) { + return repo.StoreCommitWithParent(treeHash, "") +} + +// StoreCommit will store a Git commit with the given Git tree +func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + commit := object.Commit{ + Author: object.Signature{ + cfg.Author.Name, + cfg.Author.Email, + time.Now(), + }, + Committer: object.Signature{ + cfg.Committer.Name, + cfg.Committer.Email, + time.Now(), + }, + Message: "", + TreeHash: plumbing.NewHash(treeHash.String()), + } + + if parent != "" { + commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())} + } + + obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.CommitObject) + err = commit.Encode(obj) + if err != nil { + return "", err + } + + hash, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(hash.String()), nil +} + +// GetTreeHash return the git tree hash referenced in a commit +func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) { + obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String())) + if err != nil { + return "", err + } + + return Hash(obj.TreeHash.String()), nil +} + +// FindCommonAncestor will return the last common ancestor of two chain of commit +func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) { + obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String())) + if err != nil { + return "", err + } + obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String())) + if err != nil { + return "", err + } + + commits, err := obj1.MergeBase(obj2) + if err != nil { + return "", err + } + + return Hash(commits[0].Hash.String()), nil +} + +// UpdateRef will create or update a Git reference +func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { + return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String()))) +} + +// RemoveRef will remove a Git reference +func (repo *GoGitRepo) RemoveRef(ref string) error { + return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref)) +} + +// ListRefs will return a list of Git ref matching the given refspec +func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) { + refIter, err := repo.r.References() + if err != nil { + return nil, err + } + + refs := make([]string, 0) + + err = refIter.ForEach(func(ref *plumbing.Reference) error { + if strings.HasPrefix(ref.Name().String(), refPrefix) { + refs = append(refs, ref.Name().String()) + } + return nil + }) + if err != nil { + return nil, err + } + + return refs, nil +} + +// RefExist will check if a reference exist in Git +func (repo *GoGitRepo) RefExist(ref string) (bool, error) { + _, err := repo.r.Reference(plumbing.ReferenceName(ref), false) + if err == nil { + return true, nil + } else if err == plumbing.ErrReferenceNotFound { + return false, nil + } + return false, err +} + +// CopyRef will create a new reference with the same value as another one +func (repo *GoGitRepo) CopyRef(source string, dest string) error { + r, err := repo.r.Reference(plumbing.ReferenceName(source), false) + if err != nil { + return err + } + return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash())) +} + +// ListCommits will return the list of tree hashes of a ref, in chronological order +func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { + r, err := repo.r.Reference(plumbing.ReferenceName(ref), false) + if err != nil { + return nil, err + } + + commit, err := repo.r.CommitObject(r.Hash()) + if err != nil { + return nil, err + } + hashes := []Hash{Hash(commit.Hash.String())} + + for { + commit, err = commit.Parent(0) + if err == object.ErrParentNotFound { + break + } + if err != nil { + return nil, err + } + + if commit.NumParents() > 1 { + return nil, fmt.Errorf("multiple parents") + } + + hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...) + } + + return hashes, nil +} + +// GetOrCreateClock return a Lamport clock stored in the Repo. +// If the clock doesn't exist, it's created. +func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { + c, err := repo.getClock(name) + if err == nil { + return c, nil + } + if err != ErrClockNotExist { + return nil, err + } + + repo.clocksMutex.Lock() + defer repo.clocksMutex.Unlock() + + p := stdpath.Join(repo.path, clockPath, name+"-clock") + + c, err = lamport.NewPersistedClock(p) + if err != nil { + return nil, err + } + + repo.clocks[name] = c + return c, nil +} + +func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) { + repo.clocksMutex.Lock() + defer repo.clocksMutex.Unlock() + + if c, ok := repo.clocks[name]; ok { + return c, nil + } + + p := stdpath.Join(repo.path, clockPath, name+"-clock") + + c, err := lamport.LoadPersistedClock(p) + if err == nil { + repo.clocks[name] = c + return c, nil + } + if err == lamport.ErrClockNotExist { + return nil, ErrClockNotExist + } + return nil, err +} + +// AddRemote add a new remote to the repository +// Not in the interface because it's only used for testing +func (repo *GoGitRepo) AddRemote(name string, url string) error { + _, err := repo.r.CreateRemote(&config.RemoteConfig{ + Name: name, + URLs: []string{url}, + }) + + return err +} diff --git a/repository/gogit_config.go b/repository/gogit_config.go new file mode 100644 index 00000000..7812de76 --- /dev/null +++ b/repository/gogit_config.go @@ -0,0 +1,235 @@ +package repository + +import ( + "fmt" + "strconv" + "strings" + "time" + + gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" +) + +var _ Config = &goGitConfig{} + +type goGitConfig struct { + ConfigRead + ConfigWrite +} + +func newGoGitLocalConfig(repo *gogit.Repository) *goGitConfig { + return &goGitConfig{ + ConfigRead: &goGitConfigReader{getConfig: repo.Config}, + ConfigWrite: &goGitConfigWriter{repo: repo}, + } +} + +func newGoGitGlobalConfig(repo *gogit.Repository) *goGitConfig { + return &goGitConfig{ + ConfigRead: &goGitConfigReader{getConfig: func() (*config.Config, error) { + return config.LoadConfig(config.GlobalScope) + }}, + ConfigWrite: &configPanicWriter{}, + } +} + +var _ ConfigRead = &goGitConfigReader{} + +type goGitConfigReader struct { + getConfig func() (*config.Config, error) +} + +func (cr *goGitConfigReader) ReadAll(keyPrefix string) (map[string]string, error) { + cfg, err := cr.getConfig() + if err != nil { + return nil, err + } + + split := strings.Split(keyPrefix, ".") + result := make(map[string]string) + + switch { + case keyPrefix == "": + for _, section := range cfg.Raw.Sections { + for _, option := range section.Options { + result[fmt.Sprintf("%s.%s", section.Name, option.Key)] = option.Value + } + for _, subsection := range section.Subsections { + for _, option := range subsection.Options { + result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value + } + } + } + case len(split) == 1: + if !cfg.Raw.HasSection(split[0]) { + return nil, nil + } + section := cfg.Raw.Section(split[0]) + for _, option := range section.Options { + result[fmt.Sprintf("%s.%s", section.Name, option.Key)] = option.Value + } + for _, subsection := range section.Subsections { + for _, option := range subsection.Options { + result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value + } + } + default: + if !cfg.Raw.HasSection(split[0]) { + return nil, nil + } + section := cfg.Raw.Section(split[0]) + rest := strings.Join(split[1:], ".") + for _, subsection := range section.Subsections { + if strings.HasPrefix(subsection.Name, rest) { + for _, option := range subsection.Options { + result[fmt.Sprintf("%s.%s.%s", section.Name, subsection.Name, option.Key)] = option.Value + } + } + } + } + + return result, nil +} + +func (cr *goGitConfigReader) ReadBool(key string) (bool, error) { + val, err := cr.ReadString(key) + if err != nil { + return false, err + } + + return strconv.ParseBool(val) +} + +func (cr *goGitConfigReader) ReadString(key string) (string, error) { + cfg, err := cr.getConfig() + if err != nil { + return "", err + } + + split := strings.Split(key, ".") + + if len(split) <= 1 { + return "", fmt.Errorf("invalid key") + } + + sectionName := split[0] + if !cfg.Raw.HasSection(sectionName) { + return "", ErrNoConfigEntry + } + section := cfg.Raw.Section(sectionName) + + switch { + case len(split) == 2: + optionName := split[1] + if !section.HasOption(optionName) { + return "", ErrNoConfigEntry + } + if len(section.OptionAll(optionName)) > 1 { + return "", ErrMultipleConfigEntry + } + return section.Option(optionName), nil + default: + subsectionName := strings.Join(split[1:len(split)-2], ".") + optionName := split[len(split)-1] + if !section.HasSubsection(subsectionName) { + return "", ErrNoConfigEntry + } + subsection := section.Subsection(subsectionName) + if !subsection.HasOption(optionName) { + return "", ErrNoConfigEntry + } + if len(subsection.OptionAll(optionName)) > 1 { + return "", ErrMultipleConfigEntry + } + return subsection.Option(optionName), nil + } +} + +func (cr *goGitConfigReader) ReadTimestamp(key string) (time.Time, error) { + value, err := cr.ReadString(key) + if err != nil { + return time.Time{}, err + } + return ParseTimestamp(value) +} + +var _ ConfigWrite = &goGitConfigWriter{} + +// Only works for the local config as go-git only support that +type goGitConfigWriter struct { + repo *gogit.Repository +} + +func (cw *goGitConfigWriter) StoreString(key, value string) error { + cfg, err := cw.repo.Config() + if err != nil { + return err + } + + split := strings.Split(key, ".") + + switch { + case len(split) <= 1: + return fmt.Errorf("invalid key") + case len(split) == 2: + cfg.Raw.Section(split[0]).SetOption(split[1], value) + default: + section := split[0] + subsection := strings.Join(split[1:len(split)-1], ".") + option := split[len(split)-1] + cfg.Raw.Section(section).Subsection(subsection).SetOption(option, value) + } + + return cw.repo.SetConfig(cfg) +} + +func (cw *goGitConfigWriter) StoreTimestamp(key string, value time.Time) error { + return cw.StoreString(key, strconv.Itoa(int(value.Unix()))) +} + +func (cw *goGitConfigWriter) StoreBool(key string, value bool) error { + return cw.StoreString(key, strconv.FormatBool(value)) +} + +func (cw *goGitConfigWriter) RemoveAll(keyPrefix string) error { + cfg, err := cw.repo.Config() + if err != nil { + return err + } + + split := strings.Split(keyPrefix, ".") + + switch { + case keyPrefix == "": + cfg.Raw.Sections = nil + // warning: this does not actually remove everything as go-git config hold + // some entries in multiple places (cfg.User ...) + case len(split) == 1: + if cfg.Raw.HasSection(split[0]) { + cfg.Raw.RemoveSection(split[0]) + } else { + return fmt.Errorf("invalid key prefix") + } + default: + if !cfg.Raw.HasSection(split[0]) { + return fmt.Errorf("invalid key prefix") + } + section := cfg.Raw.Section(split[0]) + rest := strings.Join(split[1:], ".") + + ok := false + if section.HasSubsection(rest) { + section.RemoveSubsection(rest) + ok = true + } + if section.HasOption(rest) { + section.RemoveOption(rest) + ok = true + } + if !ok { + return fmt.Errorf("invalid key prefix") + } + } + + return cw.repo.SetConfig(cfg) +} diff --git a/repository/gogit_test.go b/repository/gogit_test.go new file mode 100644 index 00000000..fba990d3 --- /dev/null +++ b/repository/gogit_test.go @@ -0,0 +1,68 @@ +package repository + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewGoGitRepo(t *testing.T) { + // Plain + plainRoot, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(plainRoot) + + _, err = InitGoGitRepo(plainRoot) + require.NoError(t, err) + plainGitDir := path.Join(plainRoot, ".git") + + // Bare + bareRoot, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(bareRoot) + + _, err = InitBareGoGitRepo(bareRoot) + require.NoError(t, err) + bareGitDir := bareRoot + + tests := []struct { + inPath string + outPath string + err bool + }{ + // errors + {"/", "", true}, + // parent dir of a repo + {filepath.Dir(plainRoot), "", true}, + + // Plain repo + {plainRoot, plainGitDir, false}, + {plainGitDir, plainGitDir, false}, + {path.Join(plainGitDir, "objects"), plainGitDir, false}, + + // Bare repo + {bareRoot, bareGitDir, false}, + {bareGitDir, bareGitDir, false}, + {path.Join(bareGitDir, "objects"), bareGitDir, false}, + } + + for i, tc := range tests { + r, err := NewGoGitRepo(tc.inPath, nil) + + if tc.err { + require.Error(t, err, i) + } else { + require.NoError(t, err, i) + assert.Equal(t, filepath.ToSlash(tc.outPath), filepath.ToSlash(r.GetPath()), i) + } + } +} + +func TestGoGitRepo(t *testing.T) { + RepoTest(t, CreateGoGitTestRepo, CleanupTestRepos) +} diff --git a/repository/gogit_testing.go b/repository/gogit_testing.go new file mode 100644 index 00000000..f20ff6be --- /dev/null +++ b/repository/gogit_testing.go @@ -0,0 +1,58 @@ +package repository + +import ( + "io/ioutil" + "log" +) + +// This is intended for testing only + +func CreateGoGitTestRepo(bare bool) TestedRepo { + dir, err := ioutil.TempDir("", "") + if err != nil { + log.Fatal(err) + } + + var creator func(string) (*GoGitRepo, error) + + if bare { + creator = InitBareGoGitRepo + } else { + creator = InitGoGitRepo + } + + repo, err := creator(dir) + if err != nil { + log.Fatal(err) + } + + config := repo.LocalConfig() + if err := config.StoreString("user.name", "testuser"); err != nil { + log.Fatal("failed to set user.name for test repository: ", err) + } + if err := config.StoreString("user.email", "testuser@example.com"); err != nil { + log.Fatal("failed to set user.email for test repository: ", err) + } + + return repo +} + +func SetupGoGitReposAndRemote() (repoA, repoB, remote TestedRepo) { + repoA = CreateGoGitTestRepo(false) + repoB = CreateGoGitTestRepo(false) + remote = CreateGoGitTestRepo(true) + + remoteAddr := "file://" + remote.GetPath() + + err := repoA.AddRemote("origin", remoteAddr) + if err != nil { + log.Fatal(err) + } + + err = repoB.AddRemote("origin", remoteAddr) + if err != nil { + log.Fatal(err) + } + + return repoA, repoB, remote +} diff --git a/repository/keyring.go b/repository/keyring.go new file mode 100644 index 00000000..f690b0b3 --- /dev/null +++ b/repository/keyring.go @@ -0,0 +1,50 @@ +package repository + +import ( + "os" + "path" + + "github.com/99designs/keyring" +) + +type Item = keyring.Item + +var ErrKeyringKeyNotFound = keyring.ErrKeyNotFound + +// Keyring provides the uniform interface over the underlying backends +type Keyring interface { + // Returns an Item matching the key or ErrKeyringKeyNotFound + Get(key string) (Item, error) + // Stores an Item on the keyring + Set(item Item) error + // Removes the item with matching key + Remove(key string) error + // Provides a slice of all keys stored on the keyring + Keys() ([]string, error) +} + +func defaultKeyring() (Keyring, error) { + ucd, err := os.UserConfigDir() + if err != nil { + return nil, err + } + + return keyring.Open(keyring.Config{ + // only use the file backend until https://github.com/99designs/keyring/issues/74 is resolved + AllowedBackends: []keyring.BackendType{ + keyring.FileBackend, + }, + + ServiceName: "git-bug", + + // Fallback encrypted file + FileDir: path.Join(ucd, "git-bug", "keyring"), + // As we write the file in the user's config directory, this file should already be protected by the OS against + // other user's access. We actually don't terribly need to protect it further and a password prompt across all + // UI's would be a pain. Therefore we use here a constant password so the file will be unreadable by generic file + // scanners if the user's machine get compromised. + FilePasswordFunc: func(string) (string, error) { + return "git-bug", nil + }, + }) +} diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 576e984e..628939aa 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/99designs/keyring" + "github.com/MichaelMure/git-bug/util/lamport" ) @@ -13,85 +15,143 @@ var _ TestedRepo = &mockRepoForTest{} // mockRepoForTest defines an instance of Repo that can be used for testing. type mockRepoForTest struct { - config *MemConfig - globalConfig *MemConfig - blobs map[Hash][]byte - trees map[Hash]string - commits map[Hash]commit - refs map[string]Hash - clocks map[string]lamport.Clock -} - -type commit struct { - treeHash Hash - parent Hash + *mockRepoConfig + *mockRepoKeyring + *mockRepoCommon + *mockRepoData + *mockRepoClock } func NewMockRepoForTest() *mockRepoForTest { return &mockRepoForTest{ - config: NewMemConfig(), + mockRepoConfig: NewMockRepoConfig(), + mockRepoKeyring: NewMockRepoKeyring(), + mockRepoCommon: NewMockRepoCommon(), + mockRepoData: NewMockRepoData(), + mockRepoClock: NewMockRepoClock(), + } +} + +var _ RepoConfig = &mockRepoConfig{} + +type mockRepoConfig struct { + localConfig *MemConfig + globalConfig *MemConfig +} + +func NewMockRepoConfig() *mockRepoConfig { + return &mockRepoConfig{ + localConfig: NewMemConfig(), globalConfig: NewMemConfig(), - blobs: make(map[Hash][]byte), - trees: make(map[Hash]string), - commits: make(map[Hash]commit), - refs: make(map[string]Hash), - clocks: make(map[string]lamport.Clock), } } // LocalConfig give access to the repository scoped configuration -func (r *mockRepoForTest) LocalConfig() Config { - return r.config +func (r *mockRepoConfig) LocalConfig() Config { + return r.localConfig } // GlobalConfig give access to the git global configuration -func (r *mockRepoForTest) GlobalConfig() Config { +func (r *mockRepoConfig) GlobalConfig() Config { return r.globalConfig } +// AnyConfig give access to a merged local/global configuration +func (r *mockRepoConfig) AnyConfig() ConfigRead { + return mergeConfig(r.localConfig, r.globalConfig) +} + +var _ RepoKeyring = &mockRepoKeyring{} + +type mockRepoKeyring struct { + keyring *keyring.ArrayKeyring +} + +func NewMockRepoKeyring() *mockRepoKeyring { + return &mockRepoKeyring{ + keyring: keyring.NewArrayKeyring(nil), + } +} + +// Keyring give access to a user-wide storage for secrets +func (r *mockRepoKeyring) Keyring() Keyring { + return r.keyring +} + +var _ RepoCommon = &mockRepoCommon{} + +type mockRepoCommon struct{} + +func NewMockRepoCommon() *mockRepoCommon { + return &mockRepoCommon{} +} + // GetPath returns the path to the repo. -func (r *mockRepoForTest) GetPath() string { +func (r *mockRepoCommon) GetPath() string { return "~/mockRepo/" } -func (r *mockRepoForTest) GetUserName() (string, error) { +func (r *mockRepoCommon) GetUserName() (string, error) { return "René Descartes", nil } // GetUserEmail returns the email address that the user has used to configure git. -func (r *mockRepoForTest) GetUserEmail() (string, error) { +func (r *mockRepoCommon) GetUserEmail() (string, error) { return "user@example.com", nil } // GetCoreEditor returns the name of the editor that the user has used to configure git. -func (r *mockRepoForTest) GetCoreEditor() (string, error) { +func (r *mockRepoCommon) GetCoreEditor() (string, error) { return "vi", nil } // GetRemotes returns the configured remotes repositories. -func (r *mockRepoForTest) GetRemotes() (map[string]string, error) { +func (r *mockRepoCommon) GetRemotes() (map[string]string, error) { return map[string]string{ "origin": "git://github.com/MichaelMure/git-bug", }, nil } +var _ RepoData = &mockRepoData{} + +type commit struct { + treeHash Hash + parent Hash +} + +type mockRepoData struct { + blobs map[Hash][]byte + trees map[Hash]string + commits map[Hash]commit + refs map[string]Hash +} + +func NewMockRepoData() *mockRepoData { + return &mockRepoData{ + blobs: make(map[Hash][]byte), + trees: make(map[Hash]string), + commits: make(map[Hash]commit), + refs: make(map[string]Hash), + } +} + // PushRefs push git refs to a remote -func (r *mockRepoForTest) PushRefs(remote string, refSpec string) (string, error) { +func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) { return "", nil } -func (r *mockRepoForTest) FetchRefs(remote string, refSpec string) (string, error) { +func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) { return "", nil } -func (r *mockRepoForTest) StoreData(data []byte) (Hash, error) { +func (r *mockRepoData) StoreData(data []byte) (Hash, error) { rawHash := sha1.Sum(data) hash := Hash(fmt.Sprintf("%x", rawHash)) r.blobs[hash] = data return hash, nil } -func (r *mockRepoForTest) ReadData(hash Hash) ([]byte, error) { +func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) { data, ok := r.blobs[hash] if !ok { @@ -101,7 +161,7 @@ func (r *mockRepoForTest) ReadData(hash Hash) ([]byte, error) { return data, nil } -func (r *mockRepoForTest) StoreTree(entries []TreeEntry) (Hash, error) { +func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) { buffer := prepareTreeEntries(entries) rawHash := sha1.Sum(buffer.Bytes()) hash := Hash(fmt.Sprintf("%x", rawHash)) @@ -110,7 +170,7 @@ func (r *mockRepoForTest) StoreTree(entries []TreeEntry) (Hash, error) { return hash, nil } -func (r *mockRepoForTest) StoreCommit(treeHash Hash) (Hash, error) { +func (r *mockRepoData) StoreCommit(treeHash Hash) (Hash, error) { rawHash := sha1.Sum([]byte(treeHash)) hash := Hash(fmt.Sprintf("%x", rawHash)) r.commits[hash] = commit{ @@ -119,7 +179,7 @@ func (r *mockRepoForTest) StoreCommit(treeHash Hash) (Hash, error) { return hash, nil } -func (r *mockRepoForTest) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { +func (r *mockRepoData) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { rawHash := sha1.Sum([]byte(treeHash + parent)) hash := Hash(fmt.Sprintf("%x", rawHash)) r.commits[hash] = commit{ @@ -129,22 +189,22 @@ func (r *mockRepoForTest) StoreCommitWithParent(treeHash Hash, parent Hash) (Has return hash, nil } -func (r *mockRepoForTest) UpdateRef(ref string, hash Hash) error { +func (r *mockRepoData) UpdateRef(ref string, hash Hash) error { r.refs[ref] = hash return nil } -func (r *mockRepoForTest) RemoveRef(ref string) error { +func (r *mockRepoData) RemoveRef(ref string) error { delete(r.refs, ref) return nil } -func (r *mockRepoForTest) RefExist(ref string) (bool, error) { +func (r *mockRepoData) RefExist(ref string) (bool, error) { _, exist := r.refs[ref] return exist, nil } -func (r *mockRepoForTest) CopyRef(source string, dest string) error { +func (r *mockRepoData) CopyRef(source string, dest string) error { hash, exist := r.refs[source] if !exist { @@ -155,11 +215,11 @@ func (r *mockRepoForTest) CopyRef(source string, dest string) error { return nil } -func (r *mockRepoForTest) ListRefs(refspec string) ([]string, error) { +func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) { var keys []string for k := range r.refs { - if strings.HasPrefix(k, refspec) { + if strings.HasPrefix(k, refPrefix) { keys = append(keys, k) } } @@ -167,7 +227,7 @@ func (r *mockRepoForTest) ListRefs(refspec string) ([]string, error) { return keys, nil } -func (r *mockRepoForTest) ListCommits(ref string) ([]Hash, error) { +func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) { var hashes []Hash hash := r.refs[ref] @@ -186,7 +246,7 @@ func (r *mockRepoForTest) ListCommits(ref string) ([]Hash, error) { return hashes, nil } -func (r *mockRepoForTest) ReadTree(hash Hash) ([]TreeEntry, error) { +func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) { var data string data, ok := r.trees[hash] @@ -209,7 +269,7 @@ func (r *mockRepoForTest) ReadTree(hash Hash) ([]TreeEntry, error) { return readTreeEntries(data) } -func (r *mockRepoForTest) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) { +func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) { ancestor1 := []Hash{hash1} for hash1 != "" { @@ -241,7 +301,7 @@ func (r *mockRepoForTest) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, erro } } -func (r *mockRepoForTest) GetTreeHash(commit Hash) (Hash, error) { +func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) { c, ok := r.commits[commit] if !ok { return "", fmt.Errorf("unknown commit") @@ -250,7 +310,21 @@ func (r *mockRepoForTest) GetTreeHash(commit Hash) (Hash, error) { return c.treeHash, nil } -func (r *mockRepoForTest) GetOrCreateClock(name string) (lamport.Clock, error) { +func (r *mockRepoData) AddRemote(name string, url string) error { + panic("implement me") +} + +type mockRepoClock struct { + clocks map[string]lamport.Clock +} + +func NewMockRepoClock() *mockRepoClock { + return &mockRepoClock{ + clocks: make(map[string]lamport.Clock), + } +} + +func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) { if c, ok := r.clocks[name]; ok { return c, nil } @@ -259,7 +333,3 @@ func (r *mockRepoForTest) GetOrCreateClock(name string) (lamport.Clock, error) { r.clocks[name] = c return c, nil } - -func (r *mockRepoForTest) AddRemote(name string, url string) error { - panic("implement me") -} diff --git a/repository/repo.go b/repository/repo.go index 30d95806..4b45a1c5 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -2,29 +2,48 @@ package repository import ( - "bytes" "errors" - "strings" "github.com/MichaelMure/git-bug/util/lamport" ) var ( - ErrNoConfigEntry = errors.New("no config entry for the given key") - ErrMultipleConfigEntry = errors.New("multiple config entry for the given key") // ErrNotARepo is the error returned when the git repo root wan't be found ErrNotARepo = errors.New("not a git repository") // ErrClockNotExist is the error returned when a clock can't be found ErrClockNotExist = errors.New("clock doesn't exist") ) +// Repo represents a source code repository. +type Repo interface { + RepoConfig + RepoKeyring + RepoCommon + RepoData +} + +// ClockedRepo is a Repo that also has Lamport clocks +type ClockedRepo interface { + Repo + RepoClock +} + // RepoConfig access the configuration of a repository type RepoConfig interface { // LocalConfig give access to the repository scoped configuration LocalConfig() Config - // GlobalConfig give access to the git global configuration + // GlobalConfig give access to the global scoped configuration GlobalConfig() Config + + // AnyConfig give access to a merged local/global configuration + AnyConfig() ConfigRead +} + +// RepoKeyring give access to a user-wide storage for secrets +type RepoKeyring interface { + // Keyring give access to a user-wide storage for secrets + Keyring() Keyring } // RepoCommon represent the common function the we want all the repo to implement @@ -45,11 +64,8 @@ type RepoCommon interface { GetRemotes() (map[string]string, error) } -// Repo represents a source code repository. -type Repo interface { - RepoConfig - RepoCommon - +// RepoData give access to the git data storage +type RepoData interface { // FetchRefs fetch git refs from a remote FetchRefs(remote string, refSpec string) (string, error) @@ -66,6 +82,7 @@ type Repo interface { StoreTree(mapping []TreeEntry) (Hash, error) // ReadTree will return the list of entries in a Git tree + // The given hash could be from either a commit or a tree ReadTree(hash Hash) ([]TreeEntry, error) // StoreCommit will store a Git commit with the given Git tree @@ -87,7 +104,7 @@ type Repo interface { RemoveRef(ref string) error // ListRefs will return a list of Git ref matching the given refspec - ListRefs(refspec string) ([]string, error) + ListRefs(refPrefix string) ([]string, error) // RefExist will check if a reference exist in Git RefExist(ref string) (bool, error) @@ -99,10 +116,8 @@ type Repo interface { ListCommits(ref string) ([]Hash, error) } -// ClockedRepo is a Repo that also has Lamport clocks -type ClockedRepo interface { - Repo - +// RepoClock give access to Lamport clocks +type RepoClock interface { // GetOrCreateClock return a Lamport clock stored in the Repo. // If the clock doesn't exist, it's created. GetOrCreateClock(name string) (lamport.Clock, error) @@ -120,41 +135,14 @@ type ClockLoader struct { Witnesser func(repo ClockedRepo) error } -func prepareTreeEntries(entries []TreeEntry) bytes.Buffer { - var buffer bytes.Buffer - - for _, entry := range entries { - buffer.WriteString(entry.Format()) - } - - return buffer -} - -func readTreeEntries(s string) ([]TreeEntry, error) { - split := strings.Split(strings.TrimSpace(s), "\n") - - casted := make([]TreeEntry, len(split)) - for i, line := range split { - if line == "" { - continue - } - - entry, err := ParseTreeEntry(line) - - if err != nil { - return nil, err - } - - casted[i] = entry - } - - return casted, nil -} - // TestedRepo is an extended ClockedRepo with function for testing only type TestedRepo interface { ClockedRepo + repoTest +} +// repoTest give access to test only functions +type repoTest interface { // AddRemote add a new remote to the repository AddRemote(name string, url string) error } diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 28eb9a21..41b3609e 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -7,8 +7,9 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/util/lamport" ) func CleanupTestRepos(repos ...Repo) { @@ -47,136 +48,179 @@ type RepoCleaner func(repos ...Repo) // Test suite for a Repo implementation func RepoTest(t *testing.T, creator RepoCreator, cleaner RepoCleaner) { - t.Run("Blob-Tree-Commit-Ref", func(t *testing.T) { - repo := creator(false) - defer cleaner(repo) - - // Blob - - data := randomData() - - blobHash1, err := repo.StoreData(data) - require.NoError(t, err) - assert.True(t, blobHash1.IsValid()) - - blob1Read, err := repo.ReadData(blobHash1) - require.NoError(t, err) - assert.Equal(t, data, blob1Read) - - // Tree - - blobHash2, err := repo.StoreData(randomData()) - require.NoError(t, err) - blobHash3, err := repo.StoreData(randomData()) - require.NoError(t, err) - - tree1 := []TreeEntry{ - { - ObjectType: Blob, - Hash: blobHash1, - Name: "blob1", - }, - { - ObjectType: Blob, - Hash: blobHash2, - Name: "blob2", - }, - } + for bare, name := range map[bool]string{ + false: "Plain", + true: "Bare", + } { + t.Run(name, func(t *testing.T) { + repo := creator(bare) + defer cleaner(repo) + + t.Run("Data", func(t *testing.T) { + RepoDataTest(t, repo) + }) + + t.Run("Config", func(t *testing.T) { + RepoConfigTest(t, repo) + }) + + t.Run("Clocks", func(t *testing.T) { + RepoClockTest(t, repo) + }) + }) + } +} - treeHash1, err := repo.StoreTree(tree1) - require.NoError(t, err) - assert.True(t, treeHash1.IsValid()) - - tree1Read, err := repo.ReadTree(treeHash1) - require.NoError(t, err) - assert.ElementsMatch(t, tree1, tree1Read) - - tree2 := []TreeEntry{ - { - ObjectType: Tree, - Hash: treeHash1, - Name: "tree1", - }, - { - ObjectType: Blob, - Hash: blobHash3, - Name: "blob3", - }, - } +// helper to test a RepoConfig +func RepoConfigTest(t *testing.T, repo RepoConfig) { + testConfig(t, repo.LocalConfig()) +} + +// helper to test a RepoData +func RepoDataTest(t *testing.T, repo RepoData) { + // Blob + + data := randomData() + + blobHash1, err := repo.StoreData(data) + require.NoError(t, err) + require.True(t, blobHash1.IsValid()) + + blob1Read, err := repo.ReadData(blobHash1) + require.NoError(t, err) + require.Equal(t, data, blob1Read) + + // Tree + + blobHash2, err := repo.StoreData(randomData()) + require.NoError(t, err) + blobHash3, err := repo.StoreData(randomData()) + require.NoError(t, err) + + tree1 := []TreeEntry{ + { + ObjectType: Blob, + Hash: blobHash1, + Name: "blob1", + }, + { + ObjectType: Blob, + Hash: blobHash2, + Name: "blob2", + }, + } + + treeHash1, err := repo.StoreTree(tree1) + require.NoError(t, err) + require.True(t, treeHash1.IsValid()) + + tree1Read, err := repo.ReadTree(treeHash1) + require.NoError(t, err) + require.ElementsMatch(t, tree1, tree1Read) + + tree2 := []TreeEntry{ + { + ObjectType: Tree, + Hash: treeHash1, + Name: "tree1", + }, + { + ObjectType: Blob, + Hash: blobHash3, + Name: "blob3", + }, + } + + treeHash2, err := repo.StoreTree(tree2) + require.NoError(t, err) + require.True(t, treeHash2.IsValid()) - treeHash2, err := repo.StoreTree(tree2) - require.NoError(t, err) - assert.True(t, treeHash2.IsValid()) + tree2Read, err := repo.ReadTree(treeHash2) + require.NoError(t, err) + require.ElementsMatch(t, tree2, tree2Read) - tree2Read, err := repo.ReadTree(treeHash2) - require.NoError(t, err) - assert.ElementsMatch(t, tree2, tree2Read) + // Commit - // Commit + commit1, err := repo.StoreCommit(treeHash1) + require.NoError(t, err) + require.True(t, commit1.IsValid()) - commit1, err := repo.StoreCommit(treeHash1) - require.NoError(t, err) - assert.True(t, commit1.IsValid()) + treeHash1Read, err := repo.GetTreeHash(commit1) + require.NoError(t, err) + require.Equal(t, treeHash1, treeHash1Read) - treeHash1Read, err := repo.GetTreeHash(commit1) - require.NoError(t, err) - assert.Equal(t, treeHash1, treeHash1Read) + commit2, err := repo.StoreCommitWithParent(treeHash2, commit1) + require.NoError(t, err) + require.True(t, commit2.IsValid()) - commit2, err := repo.StoreCommitWithParent(treeHash2, commit1) - require.NoError(t, err) - assert.True(t, commit2.IsValid()) + treeHash2Read, err := repo.GetTreeHash(commit2) + require.NoError(t, err) + require.Equal(t, treeHash2, treeHash2Read) - treeHash2Read, err := repo.GetTreeHash(commit2) - require.NoError(t, err) - assert.Equal(t, treeHash2, treeHash2Read) + // ReadTree should accept tree and commit hashes + tree1read, err := repo.ReadTree(commit1) + require.NoError(t, err) + require.Equal(t, tree1read, tree1) - // Ref + // Ref - exist1, err := repo.RefExist("refs/bugs/ref1") - require.NoError(t, err) - assert.False(t, exist1) + exist1, err := repo.RefExist("refs/bugs/ref1") + require.NoError(t, err) + require.False(t, exist1) - err = repo.UpdateRef("refs/bugs/ref1", commit2) - require.NoError(t, err) + err = repo.UpdateRef("refs/bugs/ref1", commit2) + require.NoError(t, err) - exist1, err = repo.RefExist("refs/bugs/ref1") - require.NoError(t, err) - assert.True(t, exist1) + exist1, err = repo.RefExist("refs/bugs/ref1") + require.NoError(t, err) + require.True(t, exist1) - ls, err := repo.ListRefs("refs/bugs") - require.NoError(t, err) - assert.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls) + ls, err := repo.ListRefs("refs/bugs") + require.NoError(t, err) + require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls) - err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2") - require.NoError(t, err) + err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2") + require.NoError(t, err) - ls, err = repo.ListRefs("refs/bugs") - require.NoError(t, err) - assert.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls) + ls, err = repo.ListRefs("refs/bugs") + require.NoError(t, err) + require.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls) - commits, err := repo.ListCommits("refs/bugs/ref2") - require.NoError(t, err) - assert.ElementsMatch(t, []Hash{commit1, commit2}, commits) + commits, err := repo.ListCommits("refs/bugs/ref2") + require.NoError(t, err) + require.Equal(t, []Hash{commit1, commit2}, commits) - // Graph + // Graph - commit3, err := repo.StoreCommitWithParent(treeHash1, commit1) - require.NoError(t, err) + commit3, err := repo.StoreCommitWithParent(treeHash1, commit1) + require.NoError(t, err) + + ancestorHash, err := repo.FindCommonAncestor(commit2, commit3) + require.NoError(t, err) + require.Equal(t, commit1, ancestorHash) + + err = repo.RemoveRef("refs/bugs/ref1") + require.NoError(t, err) +} - ancestorHash, err := repo.FindCommonAncestor(commit2, commit3) - require.NoError(t, err) - assert.Equal(t, commit1, ancestorHash) +// helper to test a RepoClock +func RepoClockTest(t *testing.T, repo RepoClock) { + clock, err := repo.GetOrCreateClock("foo") + require.NoError(t, err) + require.Equal(t, lamport.Time(1), clock.Time()) - err = repo.RemoveRef("refs/bugs/ref1") - require.NoError(t, err) - }) + time, err := clock.Increment() + require.NoError(t, err) + require.Equal(t, lamport.Time(1), time) + require.Equal(t, lamport.Time(2), clock.Time()) - t.Run("Local config", func(t *testing.T) { - repo := creator(false) - defer cleaner(repo) + clock2, err := repo.GetOrCreateClock("foo") + require.NoError(t, err) + require.Equal(t, lamport.Time(2), clock2.Time()) - testConfig(t, repo.LocalConfig()) - }) + clock3, err := repo.GetOrCreateClock("bar") + require.NoError(t, err) + require.Equal(t, lamport.Time(1), clock3.Time()) } func randomData() []byte { diff --git a/repository/tree_entry.go b/repository/tree_entry.go index 8b3de8e2..6c5ec1a5 100644 --- a/repository/tree_entry.go +++ b/repository/tree_entry.go @@ -1,6 +1,7 @@ package repository import ( + "bytes" "fmt" "strings" ) @@ -68,3 +69,34 @@ func ParseObjectType(mode, objType string) (ObjectType, error) { return Unknown, fmt.Errorf("Unknown git object type %s %s", mode, objType) } } + +func prepareTreeEntries(entries []TreeEntry) bytes.Buffer { + var buffer bytes.Buffer + + for _, entry := range entries { + buffer.WriteString(entry.Format()) + } + + return buffer +} + +func readTreeEntries(s string) ([]TreeEntry, error) { + split := strings.Split(strings.TrimSpace(s), "\n") + + casted := make([]TreeEntry, len(split)) + for i, line := range split { + if line == "" { + continue + } + + entry, err := ParseTreeEntry(line) + + if err != nil { + return nil, err + } + + casted[i] = entry + } + + return casted, nil +} |