diff --git a/bridge/bridges.go b/bridge/bridges.go index a306fe5d583736e86b905bbdd39ce572bd3e36e7..5d3066f9d7cfcfbec200e0cdaed3d8b01b2d1891 100644 --- a/bridge/bridges.go +++ b/bridge/bridges.go @@ -21,6 +21,13 @@ func Targets() []string { return core.Targets() } +// LoginMetaKey return the metadata key used to store the remote bug-tracker login +// on the user identity. The corresponding value is used to match identities and +// credentials. +func LoginMetaKey(target string) (string, error) { + return core.LoginMetaKey(target) +} + // Instantiate a new Bridge for a repo, from the given target and name func NewBridge(repo *cache.RepoCache, target string, name string) (*core.Bridge, error) { return core.NewBridge(repo, target, name) diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go index fd026c5d967354620b80f95b76a172899c038442..c1255aa6e31cf0918d0b4643ce671fc8d0bf7433 100644 --- a/bridge/core/auth/credential.go +++ b/bridge/core/auth/credential.go @@ -14,9 +14,11 @@ import ( const ( configKeyPrefix = "git-bug.auth" configKeyKind = "kind" - configKeyUserId = "userid" configKeyTarget = "target" configKeyCreateTime = "createtime" + configKeyPrefixMeta = "meta." + + MetaKeyLogin = "login" ) type CredentialKind string @@ -32,22 +34,19 @@ func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatc return entity.NewErrMultipleMatch("credential", matching) } -// Special Id to mark a credential as being associated to the default user, whoever it might be. -// The intended use is for the bridge configuration, to be able to create and store a credential -// with no identities created yet, and then select one with `git-bug user adopt` -const DefaultUserId = entity.Id("default-user") - type Credential interface { ID() entity.Id - UserId() entity.Id - updateUserId(id entity.Id) Target() string Kind() CredentialKind CreateTime() time.Time Validate() error + Metadata() map[string]string + GetMetadata(key string) (string, bool) + SetMetadata(key string, value string) + // Return all the specific properties of the credential that need to be saved into the configuration. - // This does not include Target, User, Kind and CreateTime. + // This does not include Target, Kind, CreateTime and Metadata. toConfig() map[string]string } @@ -120,6 +119,20 @@ 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 +} + // List load all existing credentials func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) { rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".") @@ -127,7 +140,7 @@ func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) { return nil, err } - re, err := regexp.Compile(configKeyPrefix + `.([^.]+).([^.]+)`) + re, err := regexp.Compile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`) if err != nil { panic(err) } @@ -185,12 +198,6 @@ func Store(repo repository.RepoConfig, cred Credential) error { return err } - // UserId - err = repo.GlobalConfig().StoreString(prefix+configKeyUserId, cred.UserId().String()) - if err != nil { - return err - } - // Target err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target()) if err != nil { @@ -203,6 +210,14 @@ func Store(repo repository.RepoConfig, cred Credential) error { return err } + // Metadata + for key, val := range cred.Metadata() { + err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val) + if err != nil { + return err + } + } + // Custom for key, val := range confs { err := repo.GlobalConfig().StoreString(prefix+key, val) @@ -220,25 +235,6 @@ func Remove(repo repository.RepoConfig, id entity.Id) error { return repo.GlobalConfig().RemoveAll(keyPrefix) } -// ReplaceDefaultUser update all the credential attributed to the temporary "default user" -// with a real user Id -func ReplaceDefaultUser(repo repository.RepoConfig, id entity.Id) error { - list, err := List(repo, WithUserId(DefaultUserId)) - if err != nil { - return err - } - - for _, cred := range list { - cred.updateUserId(id) - err = Store(repo, cred) - if err != nil { - return err - } - } - - return nil -} - /* * Sorting */ diff --git a/bridge/core/auth/credential_test.go b/bridge/core/auth/credential_test.go index f91d273dca631c20315c6226b929d3f91e45372e..2f8806c951d4e268bebee97b86c4412327f6017c 100644 --- a/bridge/core/auth/credential_test.go +++ b/bridge/core/auth/credential_test.go @@ -7,32 +7,23 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" ) func TestCredential(t *testing.T) { repo := repository.NewMockRepoForTest() - user1 := identity.NewIdentity("user1", "email") - err := user1.Commit(repo) - assert.NoError(t, err) - - user2 := identity.NewIdentity("user2", "email") - err = user2.Commit(repo) - assert.NoError(t, err) - - storeToken := func(user identity.Interface, val string, target string) *Token { - token := NewToken(user.Id(), val, target) - err = Store(repo, token) + storeToken := func(val string, target string) *Token { + token := NewToken(val, target) + err := Store(repo, token) require.NoError(t, err) return token } - token := storeToken(user1, "foobar", "github") + token := storeToken("foobar", "github") // Store + Load - err = Store(repo, token) + err := Store(repo, token) assert.NoError(t, err) token2, err := LoadWithId(repo, token.ID()) @@ -50,8 +41,8 @@ func TestCredential(t *testing.T) { token.createTime = token3.CreateTime() assert.Equal(t, token, token3) - token4 := storeToken(user1, "foo", "gitlab") - token5 := storeToken(user2, "bar", "github") + token4 := storeToken("foo", "gitlab") + token5 := storeToken("bar", "github") // List + options creds, err := List(repo, WithTarget("github")) @@ -62,14 +53,6 @@ func TestCredential(t *testing.T) { assert.NoError(t, err) sameIds(t, creds, []Credential{token4}) - creds, err = List(repo, WithUser(user1)) - assert.NoError(t, err) - sameIds(t, creds, []Credential{token, token4}) - - creds, err = List(repo, WithUserId(user1.Id())) - assert.NoError(t, err) - sameIds(t, creds, []Credential{token, token4}) - creds, err = List(repo, WithKind(KindToken)) assert.NoError(t, err) sameIds(t, creds, []Credential{token, token4, token5}) @@ -78,6 +61,16 @@ func TestCredential(t *testing.T) { assert.NoError(t, err) sameIds(t, creds, []Credential{}) + // Metadata + + token4.SetMetadata("key", "value") + err = Store(repo, token4) + assert.NoError(t, err) + + creds, err = List(repo, WithMeta("key", "value")) + assert.NoError(t, err) + sameIds(t, creds, []Credential{token4}) + // Exist exist := IdExist(repo, token.ID()) assert.True(t, exist) diff --git a/bridge/core/auth/options.go b/bridge/core/auth/options.go index 7bcda68e7a7fe25c2781e3b5f2939da963457009..741898743b08f6052a72f9cf5caeee782812a036 100644 --- a/bridge/core/auth/options.go +++ b/bridge/core/auth/options.go @@ -1,14 +1,9 @@ package auth -import ( - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/identity" -) - type options struct { target string - userId entity.Id kind CredentialKind + meta map[string]string } type Option func(opts *options) @@ -26,12 +21,14 @@ func (opts *options) Match(cred Credential) bool { return false } - if opts.userId != "" && cred.UserId() != opts.userId { + if opts.kind != "" && cred.Kind() != opts.kind { return false } - if opts.kind != "" && cred.Kind() != opts.kind { - return false + for key, val := range opts.meta { + if v, ok := cred.GetMetadata(key); !ok || v != val { + return false + } } return true @@ -43,20 +40,17 @@ func WithTarget(target string) Option { } } -func WithUser(user identity.Interface) Option { - return func(opts *options) { - opts.userId = user.Id() - } -} - -func WithUserId(userId entity.Id) Option { +func WithKind(kind CredentialKind) Option { return func(opts *options) { - opts.userId = userId + opts.kind = kind } } -func WithKind(kind CredentialKind) Option { +func WithMeta(key string, val string) Option { return func(opts *options) { - opts.kind = kind + if opts.meta == nil { + opts.meta = make(map[string]string) + } + opts.meta[key] = val } } diff --git a/bridge/core/auth/token.go b/bridge/core/auth/token.go index 8333ef129035494557e681dc075f4c7539b7cfe0..42f960bfc9d72ae979f2ae34e86fbbcd11e1bdb6 100644 --- a/bridge/core/auth/token.go +++ b/bridge/core/auth/token.go @@ -18,16 +18,15 @@ var _ Credential = &Token{} // Token holds an API access token data type Token struct { - userId entity.Id target string createTime time.Time Value string + meta map[string]string } // NewToken instantiate a new token -func NewToken(userId entity.Id, value, target string) *Token { +func NewToken(value, target string) *Token { return &Token{ - userId: userId, target: target, createTime: time.Now(), Value: value, @@ -37,7 +36,6 @@ func NewToken(userId entity.Id, value, target string) *Token { func NewTokenFromConfig(conf map[string]string) *Token { token := &Token{} - token.userId = entity.Id(conf[configKeyUserId]) token.target = conf[configKeyTarget] if createTime, ok := conf[configKeyCreateTime]; ok { if t, err := repository.ParseTimestamp(createTime); err == nil { @@ -46,6 +44,7 @@ func NewTokenFromConfig(conf map[string]string) *Token { } token.Value = conf[tokenValueKey] + token.meta = metaFromConfig(conf) return token } @@ -55,14 +54,6 @@ func (t *Token) ID() entity.Id { return entity.Id(fmt.Sprintf("%x", sum)) } -func (t *Token) UserId() entity.Id { - return t.userId -} - -func (t *Token) updateUserId(id entity.Id) { - t.userId = id -} - func (t *Token) Target() string { return t.target } @@ -92,6 +83,22 @@ func (t *Token) Validate() error { return nil } +func (t *Token) Metadata() map[string]string { + return t.meta +} + +func (t *Token) GetMetadata(key string) (string, bool) { + val, ok := t.meta[key] + return val, ok +} + +func (t *Token) SetMetadata(key string, value string) { + if t.meta == nil { + t.meta = make(map[string]string) + } + t.meta[key] = value +} + func (t *Token) toConfig() map[string]string { return map[string]string{ tokenValueKey: t.Value, diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go index f606d2da092156139eb3878aed9838745828b16f..ac0d47d74b840ff0670730b0ac5bf6ffa2db2a0d 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -28,16 +28,18 @@ const ( ) var bridgeImpl map[string]reflect.Type +var bridgeLoginMetaKey map[string]string // BridgeParams holds parameters to simplify the bridge configuration without // having to make terminal prompts. type BridgeParams struct { - Owner string - Project string - URL string - BaseURL string - CredPrefix string - TokenRaw string + Owner string // owner of the repo (Github) + Project string // name of the repo (Github, Launchpad) + URL string // complete URL of a repo (Github, Gitlab, Launchpad) + BaseURL string // base URL for self-hosted instance ( Gitlab) + CredPrefix string // ID prefix of the credential to use (Github, Gitlab) + TokenRaw string // pre-existing token to use (Github, Gitlab) + Login string // username for the passed credential (Github, Gitlab) } // Bridge is a wrapper around a BridgeImpl that will bind low-level @@ -58,7 +60,11 @@ func Register(impl BridgeImpl) { if bridgeImpl == nil { bridgeImpl = make(map[string]reflect.Type) } + if bridgeLoginMetaKey == nil { + bridgeLoginMetaKey = make(map[string]string) + } bridgeImpl[impl.Target()] = reflect.TypeOf(impl) + bridgeLoginMetaKey[impl.Target()] = impl.LoginMetaKey() } // Targets return all known bridge implementation target @@ -80,6 +86,18 @@ func TargetExist(target string) bool { return ok } +// LoginMetaKey return the metadata key used to store the remote bug-tracker login +// on the user identity. The corresponding value is used to match identities and +// credentials. +func LoginMetaKey(target string) (string, error) { + metaKey, ok := bridgeLoginMetaKey[target] + if !ok { + return "", fmt.Errorf("unknown bridge target %v", target) + } + + return metaKey, nil +} + // Instantiate a new Bridge for a repo, from the given target and name func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) { implType, ok := bridgeImpl[target] diff --git a/bridge/core/config.go b/bridge/core/config.go new file mode 100644 index 0000000000000000000000000000000000000000..afcda56058d62a4193abd30ccc03ad7c3dee7b48 --- /dev/null +++ b/bridge/core/config.go @@ -0,0 +1,46 @@ +package core + +import ( + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/identity" +) + +func FinishConfig(repo *cache.RepoCache, metaKey string, login string) error { + // if no user exist with the given login metadata + _, err := repo.ResolveIdentityImmutableMetadata(metaKey, login) + if err != nil && err != identity.ErrIdentityNotExist { + // real error + return err + } + if err == nil { + // found an already valid user, all good + return nil + } + + // if a default user exist, tag it with the login + user, err := repo.GetUserIdentity() + if err != nil && err != identity.ErrNoIdentitySet { + // real error + return err + } + if err == nil { + // found one + user.SetMetadata(metaKey, login) + return user.CommitAsNeeded() + } + + // otherwise create a user with that metadata + i, err := repo.NewIdentityFromGitUserRaw(map[string]string{ + metaKey: login, + }) + if err != nil { + return err + } + + err = repo.SetUserIdentity(i) + if err != nil { + return err + } + + return nil +} diff --git a/bridge/core/interfaces.go b/bridge/core/interfaces.go index 77e0a7b96003d4c87413a586b7e8bf7249174b23..ab2f3977e00f0e50005b8febd188b212b09cde79 100644 --- a/bridge/core/interfaces.go +++ b/bridge/core/interfaces.go @@ -13,6 +13,11 @@ type BridgeImpl interface { // Target return the target of the bridge (e.g.: "github") Target() string + // LoginMetaKey return the metadata key used to store the remote bug-tracker login + // on the user identity. The corresponding value is used to match identities and + // credentials. + LoginMetaKey() string + // Configure handle the user interaction and return a key/value configuration // for future use Configure(repo *cache.RepoCache, params BridgeParams) (Configuration, error) diff --git a/bridge/github/config.go b/bridge/github/config.go index ea4b622f5f29f46631f82b2135195c3ff3d65189..9477801d394058c439423f545a5d848b4056bee3 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -14,30 +14,17 @@ import ( "sort" "strconv" "strings" - "syscall" "time" text "github.com/MichaelMure/go-term-text" "github.com/pkg/errors" - "golang.org/x/crypto/ssh/terminal" "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/input" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/colors" - "github.com/MichaelMure/git-bug/util/interrupt" -) - -const ( - target = "github" - githubV3Url = "https://api.github.com" - keyOwner = "owner" - keyProject = "project" - - defaultTimeout = 60 * time.Second ) var ( @@ -51,12 +38,6 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor conf := make(core.Configuration) var err error - - if (params.CredPrefix != "" || params.TokenRaw != "") && - (params.URL == "" && (params.Project == "" || params.Owner == "")) { - return nil, fmt.Errorf("you must provide a project URL or Owner/Name to configure this bridge with a token") - } - var owner string var project string @@ -89,15 +70,23 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor return nil, fmt.Errorf("invalid parameter owner: %v", owner) } - user, err := repo.GetUserIdentity() - if err != nil && err != identity.ErrNoIdentitySet { - return nil, err - } + login := params.Login + if login == "" { + validator := func(name string, value string) (string, error) { + ok, err := validateUsername(value) + if err != nil { + return "", err + } + if !ok { + return "invalid login", nil + } + return "", nil + } - // default to a "to be filled" user Id if we don't have a valid one yet - userId := auth.DefaultUserId - if user != nil { - userId = user.Id() + login, err = input.Prompt("Github login", "login", input.Required, validator) + if err != nil { + return nil, err + } } var cred auth.Credential @@ -108,13 +97,11 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor if err != nil { return nil, err } - if user != nil && cred.UserId() != user.Id() { - return nil, fmt.Errorf("selected credential don't match the user") - } case params.TokenRaw != "": - cred = auth.NewToken(userId, params.TokenRaw, target) + cred = auth.NewToken(params.TokenRaw, target) + cred.SetMetadata(auth.MetaKeyLogin, login) default: - cred, err = promptTokenOptions(repo, userId, owner, project) + cred, err = promptTokenOptions(repo, login, owner, project) if err != nil { return nil, err } @@ -151,7 +138,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor } } - return conf, nil + return conf, core.FinishConfig(repo, metaKeyGithubLogin, login) } func (*Github) ValidateConfig(conf core.Configuration) error { @@ -172,11 +159,11 @@ func (*Github) ValidateConfig(conf core.Configuration) error { return nil } -func requestToken(note, username, password string, scope string) (*http.Response, error) { - return requestTokenWith2FA(note, username, password, "", scope) +func requestToken(note, login, password string, scope string) (*http.Response, error) { + return requestTokenWith2FA(note, login, password, "", scope) } -func requestTokenWith2FA(note, username, password, otpCode string, scope string) (*http.Response, error) { +func requestTokenWith2FA(note, login, password, otpCode string, scope string) (*http.Response, error) { url := fmt.Sprintf("%s/authorizations", githubV3Url) params := struct { Scopes []string `json:"scopes"` @@ -198,7 +185,7 @@ func requestTokenWith2FA(note, username, password, otpCode string, scope string) return nil, err } - req.SetBasicAuth(username, password) + req.SetBasicAuth(login, password) req.Header.Set("Content-Type", "application/json") if otpCode != "" { @@ -242,9 +229,9 @@ func randomFingerprint() string { return string(b) } -func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, project string) (auth.Credential, error) { +func promptTokenOptions(repo repository.RepoConfig, login, owner, project string) (auth.Credential, error) { for { - creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target)) + creds, err := auth.List(repo, auth.WithTarget(target), auth.WithMeta(auth.MetaKeyLogin, login)) if err != nil { return nil, err } @@ -260,10 +247,11 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, pro fmt.Println("Existing tokens for Github:") for i, cred := range creds { token := cred.(*auth.Token) - fmt.Printf("[%d]: %s => %s (%s)\n", + fmt.Printf("[%d]: %s => %s (login: %s, %s)\n", i+3, colors.Cyan(token.ID().Human()), colors.Red(text.TruncateMax(token.Value, 10)), + token.Metadata()[auth.MetaKeyLogin], token.CreateTime().Format(time.RFC822), ) } @@ -291,13 +279,17 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, pro if err != nil { return nil, err } - return auth.NewToken(userId, value, target), nil + token := auth.NewToken(value, target) + token.SetMetadata(auth.MetaKeyLogin, login) + return token, nil case 2: - value, err := loginAndRequestToken(owner, project) + value, err := loginAndRequestToken(login, owner, project) if err != nil { return nil, err } - return auth.NewToken(userId, value, target), nil + token := auth.NewToken(value, target) + token.SetMetadata(auth.MetaKeyLogin, login) + return token, nil default: return creds[index-3], nil } @@ -315,29 +307,22 @@ func promptToken() (string, error) { fmt.Println(" - 'repo' : to be able to read private repositories") fmt.Println() - re, err := regexp.Compile(`^[a-zA-Z0-9]{40}`) + re, err := regexp.Compile(`^[a-zA-Z0-9]{40}$`) if err != nil { panic("regexp compile:" + err.Error()) } - for { - fmt.Print("Enter token: ") - - line, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "", err - } - - token := strings.TrimSpace(line) - if re.MatchString(token) { - return token, nil + validator := func(name string, value string) (complaint string, err error) { + if re.MatchString(value) { + return "", nil } - - fmt.Println("token has incorrect format") + return "token has incorrect format", nil } + + return input.Prompt("Enter token", "token", input.Required, validator) } -func loginAndRequestToken(owner, project string) (string, error) { +func loginAndRequestToken(login, owner, project string) (string, error) { fmt.Println("git-bug will now generate an access token in your Github profile. Your credential are not stored and are only used to generate the token. The token is stored in the global git config.") fmt.Println() fmt.Println("The access scope depend on the type of repository.") @@ -348,17 +333,13 @@ func loginAndRequestToken(owner, project string) (string, error) { fmt.Println() // prompt project visibility to know the token scope needed for the repository - isPublic, err := promptProjectVisibility() + i, err := input.PromptChoice("repository visibility", []string{"public", "private"}) if err != nil { return "", err } + isPublic := i == 0 - username, err := promptUsername() - if err != nil { - return "", err - } - - password, err := promptPassword() + password, err := input.PromptPassword("Password", "password", input.Required) if err != nil { return "", err } @@ -377,7 +358,7 @@ func loginAndRequestToken(owner, project string) (string, error) { note := fmt.Sprintf("git-bug - %s/%s", owner, project) - resp, err := requestToken(note, username, password, scope) + resp, err := requestToken(note, login, password, scope) if err != nil { return "", err } @@ -387,12 +368,12 @@ func loginAndRequestToken(owner, project string) (string, error) { // Handle 2FA is needed OTPHeader := resp.Header.Get("X-GitHub-OTP") if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" { - otpCode, err := prompt2FA() + otpCode, err := input.PromptPassword("Two-factor authentication code", "code", input.Required) if err != nil { return "", err } - resp, err = requestTokenWith2FA(note, username, password, otpCode, scope) + resp, err = requestTokenWith2FA(note, login, password, otpCode, scope) if err != nil { return "", err } @@ -408,29 +389,6 @@ func loginAndRequestToken(owner, project string) (string, error) { return "", fmt.Errorf("error creating token %v: %v", resp.StatusCode, string(b)) } -func promptUsername() (string, error) { - for { - fmt.Print("username: ") - - line, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "", err - } - - line = strings.TrimSpace(line) - - ok, err := validateUsername(line) - if err != nil { - return "", err - } - if ok { - return line, nil - } - - fmt.Println("invalid username") - } -} - func promptURL(repo repository.RepoCommon) (string, string, error) { // remote suggestions remotes, err := repo.GetRemotes() @@ -585,87 +543,3 @@ func validateProject(owner, project string, token *auth.Token) (bool, error) { return resp.StatusCode == http.StatusOK, nil } - -func promptPassword() (string, error) { - termState, err := terminal.GetState(int(syscall.Stdin)) - if err != nil { - return "", err - } - - cancel := interrupt.RegisterCleaner(func() error { - return terminal.Restore(int(syscall.Stdin), termState) - }) - defer cancel() - - for { - fmt.Print("password: ") - - bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) - // new line for coherent formatting, ReadPassword clip the normal new line - // entered by the user - fmt.Println() - - if err != nil { - return "", err - } - - if len(bytePassword) > 0 { - return string(bytePassword), nil - } - - fmt.Println("password is empty") - } -} - -func prompt2FA() (string, error) { - termState, err := terminal.GetState(int(syscall.Stdin)) - if err != nil { - return "", err - } - - cancel := interrupt.RegisterCleaner(func() error { - return terminal.Restore(int(syscall.Stdin), termState) - }) - defer cancel() - - for { - fmt.Print("two-factor authentication code: ") - - byte2fa, err := terminal.ReadPassword(int(syscall.Stdin)) - fmt.Println() - if err != nil { - return "", err - } - - if len(byte2fa) > 0 { - return string(byte2fa), nil - } - - fmt.Println("code is empty") - } -} - -func promptProjectVisibility() (bool, error) { - for { - fmt.Println("[1]: public") - fmt.Println("[2]: private") - fmt.Print("repository visibility: ") - - line, err := bufio.NewReader(os.Stdin).ReadString('\n') - fmt.Println() - if err != nil { - return false, err - } - - line = strings.TrimSpace(line) - - index, err := strconv.Atoi(line) - if err != nil || (index != 1 && index != 2) { - fmt.Println("invalid input") - continue - } - - // return true for public repositories, false for private - return index == 1, nil - } -} diff --git a/bridge/github/config_test.go b/bridge/github/config_test.go index 9798d26b7fdea968ea834ee778d9fcd3c6d77689..d7b1b38d2b46e786b980020b600baa53b3078fef 100644 --- a/bridge/github/config_test.go +++ b/bridge/github/config_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/MichaelMure/git-bug/bridge/core/auth" - "github.com/MichaelMure/git-bug/entity" ) func TestSplitURL(t *testing.T) { @@ -155,8 +154,8 @@ func TestValidateProject(t *testing.T) { t.Skip("Env var GITHUB_TOKEN_PUBLIC missing") } - tokenPrivate := auth.NewToken(entity.UnsetId, envPrivate, target) - tokenPublic := auth.NewToken(entity.UnsetId, envPublic, target) + tokenPrivate := auth.NewToken(envPrivate, target) + tokenPublic := auth.NewToken(envPublic, target) type args struct { owner string diff --git a/bridge/github/export.go b/bridge/github/export.go index 6c089a474069217c61f962d17183784bab2c25e2..c363e188fac9a7fdeb562879b7cdd545d5286e97 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "net/http" + "os" "strings" "time" @@ -19,7 +20,7 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/identity" ) var ( @@ -74,7 +75,8 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e return err } - creds, err := auth.List(repo, auth.WithUserId(user.Id()), auth.WithTarget(target), auth.WithKind(auth.KindToken)) + login := user.ImmutableMetadata()[metaKeyGithubLogin] + creds, err := auth.List(repo, auth.WithMeta(auth.MetaKeyLogin, login), auth.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { return err } @@ -88,16 +90,30 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e return nil } -func (ge *githubExporter) cacheAllClient(repo repository.RepoConfig) error { +func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error { creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { return err } for _, cred := range creds { - if _, ok := ge.identityClient[cred.UserId()]; !ok { + login, ok := cred.GetMetadata(auth.MetaKeyLogin) + if !ok { + _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Github login\n", cred.ID().Human()) + continue + } + + user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGithubLogin, login) + if err == identity.ErrIdentityNotExist { + continue + } + if err != nil { + return nil + } + + if _, ok := ge.identityClient[user.Id()]; !ok { client := buildClient(creds[0].(*auth.Token)) - ge.identityClient[cred.UserId()] = client + ge.identityClient[user.Id()] = client } } @@ -477,11 +493,12 @@ func (ge *githubExporter) cacheGithubLabels(ctx context.Context, gc *githubv4.Cl for hasNextPage { // create a new timeout context at each iteration ctx, cancel := context.WithTimeout(ctx, defaultTimeout) - defer cancel() if err := gc.Query(ctx, &q, variables); err != nil { + cancel() return err } + cancel() for _, label := range q.Repository.Labels.Nodes { ge.cachedLabels[label.Name] = label.ID diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 5a0bc6530b0aadca639ba36e6548d25e289b10ed..7d6e6fb191b7123f3bacf0be9a9d39410021d010 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -144,8 +144,12 @@ func TestPushPull(t *testing.T) { require.NoError(t, err) // set author identity + login := "identity-test" author, err := backend.NewIdentity("test identity", "test@test.org") require.NoError(t, err) + author.SetMetadata(metaKeyGithubLogin, login) + err = author.Commit() + require.NoError(t, err) err = backend.SetUserIdentity(author) require.NoError(t, err) @@ -153,6 +157,11 @@ func TestPushPull(t *testing.T) { defer backend.Close() interrupt.RegisterCleaner(backend.Close) + token := auth.NewToken(envToken, target) + token.SetMetadata(auth.MetaKeyLogin, login) + err = auth.Store(repo, token) + require.NoError(t, err) + tests := testCases(t, backend) // generate project name @@ -176,10 +185,6 @@ func TestPushPull(t *testing.T) { return deleteRepository(projectName, envUser, envToken) }) - token := auth.NewToken(author.Id(), envToken, target) - err = auth.Store(repo, token) - require.NoError(t, err) - // initialize exporter exporter := &githubExporter{} err = exporter.Init(backend, core.Configuration{ @@ -255,7 +260,7 @@ func TestPushPull(t *testing.T) { // verify bug have same number of original operations require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp) - // verify bugs are taged with origin=github + // verify bugs are tagged with origin=github issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin) require.True(t, ok) require.Equal(t, issueOrigin, target) diff --git a/bridge/github/github.go b/bridge/github/github.go index 874c2d11cd044bfef4931db9980253cd859359d1..19dc8a08cbfd7168a6243bd5449e8d8d72291d9e 100644 --- a/bridge/github/github.go +++ b/bridge/github/github.go @@ -3,6 +3,7 @@ package github import ( "context" + "time" "github.com/shurcooL/githubv4" "golang.org/x/oauth2" @@ -11,12 +12,32 @@ import ( "github.com/MichaelMure/git-bug/bridge/core/auth" ) +const ( + target = "github" + + metaKeyGithubId = "github-id" + metaKeyGithubUrl = "github-url" + metaKeyGithubLogin = "github-login" + + keyOwner = "owner" + keyProject = "project" + + githubV3Url = "https://api.github.com" + defaultTimeout = 60 * time.Second +) + +var _ core.BridgeImpl = &Github{} + type Github struct{} func (*Github) Target() string { return target } +func (g *Github) LoginMetaKey() string { + return metaKeyGithubLogin +} + func (*Github) NewImporter() core.Importer { return &githubImporter{} } diff --git a/bridge/github/import.go b/bridge/github/import.go index 39aebccb9a0c6d246d4be633b4902cf222da4932..ea0ccba32b9dc17fdbc1658c944af3fc2fd73e84 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -12,16 +12,9 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/text" ) -const ( - metaKeyGithubId = "github-id" - metaKeyGithubUrl = "github-url" - metaKeyGithubLogin = "github-login" -) - // githubImporter implement the Importer interface type githubImporter struct { conf core.Configuration @@ -39,20 +32,7 @@ type githubImporter struct { func (gi *githubImporter) Init(repo *cache.RepoCache, conf core.Configuration) error { gi.conf = conf - opts := []auth.Option{ - auth.WithTarget(target), - auth.WithKind(auth.KindToken), - } - - user, err := repo.GetUserIdentity() - if err == nil { - opts = append(opts, auth.WithUserId(user.Id())) - } - if err == identity.ErrNoIdentitySet { - opts = append(opts, auth.WithUserId(auth.DefaultUserId)) - } - - creds, err := auth.List(repo, opts...) + creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { return err } @@ -554,10 +534,14 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca case "Bot": } + // Name is not necessarily set, fallback to login as a name is required in the identity + if name == "" { + name = string(actor.Login) + } + i, err = repo.NewIdentityRaw( name, email, - string(actor.Login), string(actor.AvatarUrl), map[string]string{ metaKeyGithubLogin: string(actor.Login), @@ -604,7 +588,6 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, return repo.NewIdentityRaw( name, "", - string(q.User.Login), string(q.User.AvatarUrl), map[string]string{ metaKeyGithubLogin: string(q.User.Login), diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 57bab61ecb18ece6a14424b672b0f69d8e62cfaf..a8f8e3468037b755fa8a9a18faea63a5d9fc7357 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -21,6 +21,7 @@ import ( func Test_Importer(t *testing.T) { author := identity.NewIdentity("Michael Muré", "batolettre@gmail.com") + tests := []struct { name string url string @@ -140,13 +141,11 @@ func Test_Importer(t *testing.T) { t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") } - err = author.Commit(repo) - require.NoError(t, err) - - err = identity.SetUserIdentity(repo, author) - require.NoError(t, err) + login := "test-identity" + author.SetMetadata(metaKeyGithubLogin, login) - token := auth.NewToken(author.Id(), envToken, target) + token := auth.NewToken(envToken, target) + token.SetMetadata(auth.MetaKeyLogin, login) err = auth.Store(repo, token) require.NoError(t, err) diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go index 5e345b314d3114f9454c361c4335e2eea477cb95..fb59381904310d6bcfb7eac10ca0d83e39537b36 100644 --- a/bridge/gitlab/config.go +++ b/bridge/gitlab/config.go @@ -19,8 +19,7 @@ import ( "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/input" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/colors" ) @@ -36,14 +35,12 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor if params.Owner != "" { fmt.Println("warning: --owner is ineffective for a gitlab bridge") } + if params.Login != "" { + fmt.Println("warning: --login is ineffective for a gitlab bridge") + } conf := make(core.Configuration) var err error - - if (params.CredPrefix != "" || params.TokenRaw != "") && params.URL == "" { - return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token") - } - var baseUrl string switch { @@ -74,17 +71,6 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, url) } - user, err := repo.GetUserIdentity() - if err != nil && err != identity.ErrNoIdentitySet { - return nil, err - } - - // default to a "to be filled" user Id if we don't have a valid one yet - userId := auth.DefaultUserId - if user != nil { - userId = user.Id() - } - var cred auth.Credential switch { @@ -93,13 +79,16 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor if err != nil { return nil, err } - if user != nil && cred.UserId() != user.Id() { - return nil, fmt.Errorf("selected credential don't match the user") - } case params.TokenRaw != "": - cred = auth.NewToken(userId, params.TokenRaw, target) + token := auth.NewToken(params.TokenRaw, target) + login, err := getLoginFromToken(baseUrl, token) + if err != nil { + return nil, err + } + token.SetMetadata(auth.MetaKeyLogin, login) + cred = token default: - cred, err = promptTokenOptions(repo, userId, baseUrl) + cred, err = promptTokenOptions(repo, baseUrl) if err != nil { return nil, err } @@ -153,77 +142,50 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error { } func promptBaseUrlOptions() (string, error) { - for { - fmt.Printf("Gitlab base url:\n") - fmt.Printf("[0]: https://gitlab.com\n") - fmt.Printf("[1]: enter your own base url\n") - fmt.Printf("Select option: ") - - line, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "", err - } + index, err := input.PromptChoice("Gitlab base url", []string{ + "https://gitlab.com", + "enter your own base url", + }) - line = strings.TrimSpace(line) - - index, err := strconv.Atoi(line) - if err != nil || index < 0 || index > 1 { - fmt.Println("invalid input") - continue - } + if err != nil { + return "", err + } - switch index { - case 0: - return defaultBaseURL, nil - case 1: - return promptBaseUrl() - } + if index == 0 { + return defaultBaseURL, nil + } else { + return promptBaseUrl() } } func promptBaseUrl() (string, error) { - for { - fmt.Print("Base url: ") - - line, err := bufio.NewReader(os.Stdin).ReadString('\n') + validator := func(name string, value string) (string, error) { + u, err := url.Parse(value) if err != nil { - return "", err + return err.Error(), nil } - - line = strings.TrimSpace(line) - - ok, err := validateBaseUrl(line) - if err != nil { - return "", err + if u.Scheme == "" { + return "missing scheme", nil } - if ok { - return line, nil + if u.Host == "" { + return "missing host", nil } + return "", nil } -} -func validateBaseUrl(baseUrl string) (bool, error) { - u, err := url.Parse(baseUrl) - if err != nil { - return false, err - } - return u.Scheme != "" && u.Host != "", nil + return input.Prompt("Base url", "url", input.Required, validator) } -func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, baseUrl string) (auth.Credential, error) { +func promptTokenOptions(repo repository.RepoConfig, baseUrl string) (auth.Credential, error) { for { - creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target), auth.WithKind(auth.KindToken)) + creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { return nil, err } // if we don't have existing token, fast-track to the token prompt if len(creds) == 0 { - value, err := promptToken(baseUrl) - if err != nil { - return nil, err - } - return auth.NewToken(userId, value, target), nil + return promptToken(baseUrl) } fmt.Println() @@ -261,44 +223,47 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, baseUrl st switch index { case 1: - value, err := promptToken(baseUrl) - if err != nil { - return nil, err - } - return auth.NewToken(userId, value, target), nil + return promptToken(baseUrl) default: return creds[index-2], nil } } } -func promptToken(baseUrl string) (string, error) { +func promptToken(baseUrl string) (*auth.Token, error) { fmt.Printf("You can generate a new token by visiting %s.\n", path.Join(baseUrl, "profile/personal_access_tokens")) fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.") fmt.Println() fmt.Println("'api' access scope: to be able to make api calls") fmt.Println() - re, err := regexp.Compile(`^[a-zA-Z0-9\-\_]{20}`) + re, err := regexp.Compile(`^[a-zA-Z0-9\-\_]{20}$`) if err != nil { panic("regexp compile:" + err.Error()) } - for { - fmt.Print("Enter token: ") + var login string - line, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "", err + validator := func(name string, value string) (complaint string, err error) { + if !re.MatchString(value) { + return "token has incorrect format", nil } - - token := strings.TrimSpace(line) - if re.MatchString(token) { - return token, nil + login, err = getLoginFromToken(baseUrl, auth.NewToken(value, target)) + if err != nil { + return fmt.Sprintf("token is invalid: %v", err), nil } + return "", nil + } - fmt.Println("token has incorrect format") + rawToken, err := input.Prompt("Enter token", "token", input.Required, validator) + if err != nil { + return nil, err } + + token := auth.NewToken(rawToken, target) + token.SetMetadata(auth.MetaKeyLogin, login) + + return token, nil } func promptURL(repo repository.RepoCommon, baseUrl string) (string, error) { @@ -408,8 +373,25 @@ func validateProjectURL(baseUrl, url string, token *auth.Token) (int, error) { project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{}) if err != nil { - return 0, errors.Wrap(err, "wrong token scope ou inexistent project") + return 0, errors.Wrap(err, "wrong token scope ou non-existent project") } return project.ID, nil } + +func getLoginFromToken(baseUrl string, token *auth.Token) (string, error) { + client, err := buildClient(baseUrl, token) + if err != nil { + return "", err + } + + user, _, err := client.Users.CurrentUser() + if err != nil { + return "", err + } + if user.Username == "" { + return "", fmt.Errorf("gitlab say username is empty") + } + + return user.Username, nil +} diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go index 2ba149a299057cd2c8fc0377c9a3184c6970fde0..c5323da4b164979429933cc26b1a680ad9d63343 100644 --- a/bridge/gitlab/export.go +++ b/bridge/gitlab/export.go @@ -3,6 +3,7 @@ package gitlab import ( "context" "fmt" + "os" "strconv" "time" @@ -14,7 +15,7 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/identity" ) var ( @@ -54,20 +55,33 @@ func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) e return nil } -func (ge *gitlabExporter) cacheAllClient(repo repository.RepoConfig) error { +func (ge *gitlabExporter) cacheAllClient(repo *cache.RepoCache) error { creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { return err } for _, cred := range creds { - if _, ok := ge.identityClient[cred.UserId()]; !ok { + login, ok := cred.GetMetadata(auth.MetaKeyLogin) + if !ok { + _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Gitlab login\n", cred.ID().Human()) + continue + } + + user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabLogin, login) + if err == identity.ErrIdentityNotExist { + continue + } + if err != nil { + return nil + } + + if _, ok := ge.identityClient[user.Id()]; !ok { client, err := buildClient(ge.conf[keyGitlabBaseUrl], creds[0].(*auth.Token)) if err != nil { return err } - - ge.identityClient[cred.UserId()] = client + ge.identityClient[user.Id()] = client } } diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go index d16defd007468fb454d2d6ff3fbc75618fb6ea0c..1d387655c1067f8275ff8362b2f6d07e997393c1 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -149,8 +149,12 @@ func TestPushPull(t *testing.T) { require.NoError(t, err) // set author identity + login := "test-identity" author, err := backend.NewIdentity("test identity", "test@test.org") require.NoError(t, err) + author.SetMetadata(metaKeyGitlabLogin, login) + err = author.Commit() + require.NoError(t, err) err = backend.SetUserIdentity(author) require.NoError(t, err) @@ -158,12 +162,13 @@ func TestPushPull(t *testing.T) { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - tests := testCases(t, backend) - - token := auth.NewToken(author.Id(), envToken, target) + token := auth.NewToken(envToken, target) + token.SetMetadata(auth.MetaKeyLogin, login) err = auth.Store(repo, token) require.NoError(t, err) + tests := testCases(t, backend) + // generate project name projectName := generateRepoName() @@ -260,7 +265,7 @@ func TestPushPull(t *testing.T) { // verify bug have same number of original operations require.Len(t, importedBug.Snapshot().Operations, tt.numOpImp) - // verify bugs are taged with origin=gitlab + // verify bugs are tagged with origin=gitlab issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin) require.True(t, ok) require.Equal(t, issueOrigin, target) diff --git a/bridge/gitlab/gitlab.go b/bridge/gitlab/gitlab.go index 9298dc8edbb7dae90c1e8048c21fa29b20596f8e..8512379cf83de9c90dc37c95ac25db903a97094c 100644 --- a/bridge/gitlab/gitlab.go +++ b/bridge/gitlab/gitlab.go @@ -26,12 +26,18 @@ const ( defaultTimeout = 60 * time.Second ) +var _ core.BridgeImpl = &Gitlab{} + type Gitlab struct{} func (*Gitlab) Target() string { return target } +func (g *Gitlab) LoginMetaKey() string { + return metaKeyGitlabLogin +} + func (*Gitlab) NewImporter() core.Importer { return &gitlabImporter{} } diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index fa6bbfb66aa662a54bd11247388b1aa47f4e173d..d699554beab6958a4f38c52a2429f765bffa3201 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -13,7 +13,6 @@ import ( "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/text" ) @@ -34,20 +33,7 @@ type gitlabImporter struct { func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) error { gi.conf = conf - opts := []auth.Option{ - auth.WithTarget(target), - auth.WithKind(auth.KindToken), - } - - user, err := repo.GetUserIdentity() - if err == nil { - opts = append(opts, auth.WithUserId(user.Id())) - } - if err == identity.ErrNoIdentitySet { - opts = append(opts, auth.WithUserId(auth.DefaultUserId)) - } - - creds, err := auth.List(repo, opts...) + creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { return err } @@ -403,7 +389,6 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id i, err = repo.NewIdentityRaw( user.Name, user.PublicEmail, - user.Username, user.AvatarURL, map[string]string{ // because Gitlab diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index 1e2f5d502eb6fc52aefaa5d58ff77f974e347865..3c0caa55794d4fed871728d1baa46670b77c396f 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -21,6 +21,7 @@ import ( func TestImport(t *testing.T) { author := identity.NewIdentity("Amine Hilaly", "hilalyamine@gmail.com") + tests := []struct { name string url string @@ -94,13 +95,11 @@ func TestImport(t *testing.T) { t.Skip("Env var GITLAB_PROJECT_ID missing") } - err = author.Commit(repo) - require.NoError(t, err) - - err = identity.SetUserIdentity(repo, author) - require.NoError(t, err) + login := "test-identity" + author.SetMetadata(metaKeyGitlabLogin, login) - token := auth.NewToken(author.Id(), envToken, target) + token := auth.NewToken(envToken, target) + token.SetMetadata(metaKeyGitlabLogin, login) err = auth.Store(repo, token) require.NoError(t, err) diff --git a/bridge/launchpad/config.go b/bridge/launchpad/config.go index edbd941d9a44971bceb0ccf8c3d9ca55ab795445..e029fad3907a34844e8a0fc9a2df4104baf5a9db 100644 --- a/bridge/launchpad/config.go +++ b/bridge/launchpad/config.go @@ -1,27 +1,18 @@ package launchpad import ( - "bufio" "errors" "fmt" "net/http" - "os" "regexp" - "strings" - "time" "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/input" ) var ErrBadProjectURL = errors.New("bad Launchpad project URL") -const ( - target = "launchpad-preview" - keyProject = "project" - defaultTimeout = 60 * time.Second -) - func (l *Launchpad) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) { if params.TokenRaw != "" { fmt.Println("warning: token params are ineffective for a Launchpad bridge") @@ -45,7 +36,7 @@ func (l *Launchpad) Configure(repo *cache.RepoCache, params core.BridgeParams) ( project, err = splitURL(params.URL) default: // get project name from terminal prompt - project, err = promptProjectName() + project, err = input.Prompt("Launchpad project name", "project name", input.Required) } if err != nil { @@ -86,26 +77,6 @@ func (*Launchpad) ValidateConfig(conf core.Configuration) error { return nil } -func promptProjectName() (string, error) { - for { - fmt.Print("Launchpad project name: ") - - line, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "", err - } - - line = strings.TrimRight(line, "\n") - - if line == "" { - fmt.Println("Project name is empty") - continue - } - - return line, nil - } -} - func validateProject(project string) (bool, error) { url := fmt.Sprintf("%s/%s", apiRoot, project) diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index 619631b3257493d3e2875ff23270f95fad171612..5bca8e63db73c7bf5a44a1c887bb921f414c0ffb 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -20,11 +20,6 @@ func (li *launchpadImporter) Init(repo *cache.RepoCache, conf core.Configuration return nil } -const ( - metaKeyLaunchpadID = "launchpad-id" - metaKeyLaunchpadLogin = "launchpad-login" -) - func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson) (*cache.IdentityCache, error) { // Look first in the cache i, err := repo.ResolveIdentityImmutableMetadata(metaKeyLaunchpadLogin, owner.Login) @@ -38,7 +33,6 @@ func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson) return repo.NewIdentityRaw( owner.Name, "", - owner.Login, "", map[string]string{ metaKeyLaunchpadLogin: owner.Login, diff --git a/bridge/launchpad/launchpad.go b/bridge/launchpad/launchpad.go index 030d916957323640adfffa4c7a15b0c35dcb4505..b4fcdd008fa2787a23a6802f878d088f5a94aa48 100644 --- a/bridge/launchpad/launchpad.go +++ b/bridge/launchpad/launchpad.go @@ -2,15 +2,34 @@ package launchpad import ( + "time" + "github.com/MichaelMure/git-bug/bridge/core" ) +const ( + target = "launchpad-preview" + + metaKeyLaunchpadID = "launchpad-id" + metaKeyLaunchpadLogin = "launchpad-login" + + keyProject = "project" + + defaultTimeout = 60 * time.Second +) + +var _ core.BridgeImpl = &Launchpad{} + type Launchpad struct{} func (*Launchpad) Target() string { return "launchpad-preview" } +func (l *Launchpad) LoginMetaKey() string { + return metaKeyLaunchpadLogin +} + func (*Launchpad) NewImporter() core.Importer { return &launchpadImporter{} } diff --git a/bug/status.go b/bug/status.go index 737c8d31daa933164e363fcb00de7208b52cd858..9e998034c5d7e196e301836f68d60a845e6776ca 100644 --- a/bug/status.go +++ b/bug/status.go @@ -44,7 +44,7 @@ func StatusFromString(str string) (Status, error) { case "closed": return ClosedStatus, nil default: - return 0, fmt.Errorf("unknow status") + return 0, fmt.Errorf("unknown status") } } diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 36c7dcfe13c6ff04d35af62c145f1675fd6648d6..10e522f91bd57c0db5008daae003bbc7c8e4e369 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -2,7 +2,6 @@ package cache import ( "encoding/gob" - "fmt" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/entity" @@ -43,21 +42,11 @@ type BugExcerpt struct { // identity.Bare data are directly embedded in the bug excerpt type LegacyAuthorExcerpt struct { - Name string - Login string + Name string } func (l LegacyAuthorExcerpt) DisplayName() string { - switch { - case l.Name == "" && l.Login != "": - return l.Login - case l.Name != "" && l.Login == "": - return l.Name - case l.Name != "" && l.Login != "": - return fmt.Sprintf("%s (%s)", l.Name, l.Login) - } - - panic("invalid person data") + return l.Name } func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { @@ -95,8 +84,7 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { e.AuthorId = snap.Author.Id() case *identity.Bare: e.LegacyAuthor = LegacyAuthorExcerpt{ - Login: snap.Author.Login(), - Name: snap.Author.Name(), + Name: snap.Author.Name(), } default: panic("unhandled identity type") diff --git a/cache/filter.go b/cache/filter.go index 27e92cf33c24a588a4b5829e45afaf842d431f6d..9b1de1d58e2972d56dce8b809cc1b50f81b4aedf 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -37,8 +37,7 @@ func AuthorFilter(query string) Filter { } // Legacy identity support - return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query) || - strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Login), query) + return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query) } } diff --git a/cache/identity_cache.go b/cache/identity_cache.go index 2ae55f2d7c4841115aecfcb52c17e535a934f94a..eb5ee18305a74fba1c235eeccdc2c703d8085980 100644 --- a/cache/identity_cache.go +++ b/cache/identity_cache.go @@ -21,8 +21,8 @@ func (i *IdentityCache) notifyUpdated() error { return i.repoCache.identityUpdated(i.Identity.Id()) } -func (i *IdentityCache) AddVersion(version *identity.Version) error { - i.Identity.AddVersion(version) +func (i *IdentityCache) Mutate(f func(identity.Mutator) identity.Mutator) error { + i.Identity.Mutate(f) return i.notifyUpdated() } diff --git a/cache/identity_excerpt.go b/cache/identity_excerpt.go index 18514e9ab08f562bef06d7aa680e6c1832828c52..06788aa5351219ef30e072f88206dff00f3242bf 100644 --- a/cache/identity_excerpt.go +++ b/cache/identity_excerpt.go @@ -2,7 +2,6 @@ package cache import ( "encoding/gob" - "fmt" "strings" "github.com/MichaelMure/git-bug/entity" @@ -21,7 +20,6 @@ type IdentityExcerpt struct { Id entity.Id Name string - Login string ImmutableMetadata map[string]string } @@ -29,7 +27,6 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt { return &IdentityExcerpt{ Id: i.Id(), Name: i.Name(), - Login: i.Login(), ImmutableMetadata: i.ImmutableMetadata(), } } @@ -37,23 +34,13 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt { // DisplayName return a non-empty string to display, representing the // identity, based on the non-empty values. func (i *IdentityExcerpt) DisplayName() string { - switch { - case i.Name == "" && i.Login != "": - return i.Login - case i.Name != "" && i.Login == "": - return i.Name - case i.Name != "" && i.Login != "": - return fmt.Sprintf("%s (%s)", i.Name, i.Login) - } - - panic("invalid person data") + return i.Name } // Match matches a query with the identity name, login and ID prefixes func (i *IdentityExcerpt) Match(query string) bool { return i.Id.HasPrefix(query) || - strings.Contains(strings.ToLower(i.Name), query) || - strings.Contains(strings.ToLower(i.Login), query) + strings.Contains(strings.ToLower(i.Name), query) } /* diff --git a/cache/query.go b/cache/query.go index 633ef1c2fb4fe4c694d85233b56c700b716b8104..967c18d66e1a491000b118742523df9c98ee8e25 100644 --- a/cache/query.go +++ b/cache/query.go @@ -91,7 +91,7 @@ func ParseQuery(query string) (*Query, error) { sortingDone = true default: - return nil, fmt.Errorf("unknow qualifier name %s", qualifierName) + return nil, fmt.Errorf("unknown qualifier name %s", qualifierName) } } @@ -165,7 +165,7 @@ func (q *Query) parseSorting(query string) error { q.OrderDirection = OrderAscending default: - return fmt.Errorf("unknow sorting %s", query) + return fmt.Errorf("unknown sorting %s", query) } return nil diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 90a489c857c5d23febc4df5b5919660019a13c48..18be9b5a59651aa6d0bff69a62cc6de32659b1d9 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -409,36 +409,27 @@ func (c *RepoCache) ResolveBugExcerpt(id entity.Id) (*BugExcerpt, error) { // ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple // bugs match. func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) { - // preallocate but empty - matching := make([]entity.Id, 0, 5) - - for id := range c.bugExcerpts { - if id.HasPrefix(prefix) { - matching = append(matching, id) - } - } - - if len(matching) > 1 { - return nil, bug.NewErrMultipleMatchBug(matching) - } - - if len(matching) == 0 { - return nil, bug.ErrBugNotExist - } - - return c.ResolveBug(matching[0]) + return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool { + return excerpt.Id.HasPrefix(prefix) + }) } // ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on // its Create operation, that is, the first operation. It fails if multiple bugs // match. func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) { + return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool { + return excerpt.CreateMetadata[key] == value + }) +} + +func (c *RepoCache) ResolveBugMatcher(f func(*BugExcerpt) bool) (*BugCache, error) { // preallocate but empty matching := make([]entity.Id, 0, 5) - for id, excerpt := range c.bugExcerpts { - if excerpt.CreateMetadata[key] == value { - matching = append(matching, id) + for _, excerpt := range c.bugExcerpts { + if f(excerpt) { + matching = append(matching, excerpt.Id) } } @@ -785,35 +776,26 @@ func (c *RepoCache) ResolveIdentityExcerpt(id entity.Id) (*IdentityExcerpt, erro // ResolveIdentityPrefix retrieve an Identity matching an id prefix. // It fails if multiple identities match. func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) { - // preallocate but empty - matching := make([]entity.Id, 0, 5) - - for id := range c.identitiesExcerpts { - if id.HasPrefix(prefix) { - matching = append(matching, id) - } - } - - if len(matching) > 1 { - return nil, identity.NewErrMultipleMatch(matching) - } - - if len(matching) == 0 { - return nil, identity.ErrIdentityNotExist - } - - return c.ResolveIdentity(matching[0]) + return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool { + return excerpt.Id.HasPrefix(prefix) + }) } // ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on // one of it's version. If multiple version have the same key, the first defined take precedence. func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) { + return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool { + return excerpt.ImmutableMetadata[key] == value + }) +} + +func (c *RepoCache) ResolveIdentityMatcher(f func(*IdentityExcerpt) bool) (*IdentityCache, error) { // preallocate but empty matching := make([]entity.Id, 0, 5) - for id, i := range c.identitiesExcerpts { - if i.ImmutableMetadata[key] == value { - matching = append(matching, id) + for _, excerpt := range c.identitiesExcerpts { + if f(excerpt) { + matching = append(matching, excerpt.Id) } } @@ -881,21 +863,36 @@ func (c *RepoCache) IsUserIdentitySet() (bool, error) { return identity.IsUserIdentitySet(c.repo) } +func (c *RepoCache) NewIdentityFromGitUser() (*IdentityCache, error) { + return c.NewIdentityFromGitUserRaw(nil) +} + +func (c *RepoCache) NewIdentityFromGitUserRaw(metadata map[string]string) (*IdentityCache, error) { + i, err := identity.NewFromGitUser(c.repo) + if err != nil { + return nil, err + } + return c.finishIdentity(i, metadata) +} + // NewIdentity create a new identity // The new identity is written in the repository (commit) func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) { - return c.NewIdentityRaw(name, email, "", "", nil) + return c.NewIdentityRaw(name, email, "", nil) } // NewIdentityFull create a new identity // The new identity is written in the repository (commit) -func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*IdentityCache, error) { - return c.NewIdentityRaw(name, email, login, avatarUrl, nil) +func (c *RepoCache) NewIdentityFull(name string, email string, avatarUrl string) (*IdentityCache, error) { + return c.NewIdentityRaw(name, email, avatarUrl, nil) } -func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) { - i := identity.NewIdentityFull(name, email, login, avatarUrl) +func (c *RepoCache) NewIdentityRaw(name string, email string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) { + i := identity.NewIdentityFull(name, email, avatarUrl) + return c.finishIdentity(i, metadata) +} +func (c *RepoCache) finishIdentity(i *identity.Identity, metadata map[string]string) (*IdentityCache, error) { for key, value := range metadata { i.SetMetadata(key, value) } diff --git a/commands/bridge_auth.go b/commands/bridge_auth.go index bfbab33cb362953f9dda793b567ee6765d03730d..3a0e0c294d0e7770756eeecae9deb316d81b09cf 100644 --- a/commands/bridge_auth.go +++ b/commands/bridge_auth.go @@ -2,6 +2,8 @@ package commands import ( "fmt" + "sort" + "strings" "github.com/spf13/cobra" @@ -26,8 +28,6 @@ func runBridgeAuth(cmd *cobra.Command, args []string) error { return err } - defaultUser, _ := backend.GetUserIdentity() - for _, cred := range creds { targetFmt := text.LeftPadMaxLine(cred.Target(), 10, 0) @@ -37,29 +37,19 @@ func runBridgeAuth(cmd *cobra.Command, args []string) error { value = cred.Value } - var userFmt string - - switch cred.UserId() { - case auth.DefaultUserId: - userFmt = colors.Red("default user") - default: - user, err := backend.ResolveIdentity(cred.UserId()) - if err != nil { - return err - } - userFmt = user.DisplayName() - - if cred.UserId() == defaultUser.Id() { - userFmt = colors.Red(userFmt) - } + meta := make([]string, 0, len(cred.Metadata())) + for k, v := range cred.Metadata() { + meta = append(meta, k+":"+v) } + sort.Strings(meta) + metaFmt := strings.Join(meta, ",") fmt.Printf("%s %s %s %s %s\n", colors.Cyan(cred.ID().Human()), colors.Yellow(targetFmt), colors.Magenta(cred.Kind()), - userFmt, value, + metaFmt, ) } diff --git a/commands/bridge_auth_addtoken.go b/commands/bridge_auth_addtoken.go index 018015e4d7e2a6a9267a93c1b8adb84bc06d0d0f..9a937f4d52b10320b9b8c09afe60679ad4372611 100644 --- a/commands/bridge_auth_addtoken.go +++ b/commands/bridge_auth_addtoken.go @@ -13,24 +13,37 @@ import ( "github.com/MichaelMure/git-bug/bridge" "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bridge/core/auth" - "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/util/interrupt" ) var ( bridgeAuthAddTokenTarget string + bridgeAuthAddTokenLogin string + bridgeAuthAddTokenUser string ) func runBridgeTokenAdd(cmd *cobra.Command, args []string) error { - var value string - if bridgeAuthAddTokenTarget == "" { return fmt.Errorf("flag --target is required") } + if bridgeAuthAddTokenLogin == "" { + return fmt.Errorf("flag --login is required") + } + + backend, err := cache.NewRepoCache(repo) + if err != nil { + return err + } + defer backend.Close() + interrupt.RegisterCleaner(backend.Close) if !core.TargetExist(bridgeAuthAddTokenTarget) { return fmt.Errorf("unknown target") } + var value string + if len(args) == 1 { value = args[0] } else { @@ -46,12 +59,36 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error { value = strings.TrimSuffix(raw, "\n") } - user, err := identity.GetUserIdentity(repo) + var user *cache.IdentityCache + + if bridgeAuthAddTokenUser == "" { + user, err = backend.GetUserIdentity() + } else { + user, err = backend.ResolveIdentityPrefix(bridgeAuthAddTokenUser) + } if err != nil { return err } - token := auth.NewToken(user.Id(), value, bridgeAuthAddTokenTarget) + metaKey, _ := bridge.LoginMetaKey(bridgeAuthAddTokenTarget) + login, ok := user.ImmutableMetadata()[metaKey] + + switch { + case ok && login == bridgeAuthAddTokenLogin: + // nothing to do + case ok && login != bridgeAuthAddTokenLogin: + return fmt.Errorf("this user is already tagged with a different %s login", bridgeAuthAddTokenTarget) + default: + user.SetMetadata(metaKey, bridgeAuthAddTokenLogin) + err = user.Commit() + if err != nil { + return err + } + } + + token := auth.NewToken(value, bridgeAuthAddTokenTarget) + token.SetMetadata(auth.MetaKeyLogin, bridgeAuthAddTokenLogin) + if err := token.Validate(); err != nil { return errors.Wrap(err, "invalid token") } @@ -77,5 +114,9 @@ func init() { bridgeAuthCmd.AddCommand(bridgeAuthAddTokenCmd) bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenTarget, "target", "t", "", fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ","))) + bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenLogin, + "login", "l", "", "The login in the remote bug-tracker") + bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenUser, + "user", "u", "", "The user to add the token to. Default is the current user") bridgeAuthAddTokenCmd.Flags().SortFlags = false } diff --git a/commands/bridge_auth_show.go b/commands/bridge_auth_show.go index 02c568064f7b3cfc5b768dbb16b594893183fccf..fbbf60a72b5e30b095f73c4779020fc2e177f3af 100644 --- a/commands/bridge_auth_show.go +++ b/commands/bridge_auth_show.go @@ -2,13 +2,14 @@ package commands import ( "fmt" + "sort" + "strings" "time" "github.com/spf13/cobra" "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/util/colors" "github.com/MichaelMure/git-bug/util/interrupt" ) @@ -25,28 +26,9 @@ func runBridgeAuthShow(cmd *cobra.Command, args []string) error { return err } - var userFmt string - - switch cred.UserId() { - case auth.DefaultUserId: - userFmt = colors.Red("default user") - default: - user, err := backend.ResolveIdentity(cred.UserId()) - if err != nil { - return err - } - userFmt = user.DisplayName() - - defaultUser, _ := backend.GetUserIdentity() - if cred.UserId() == defaultUser.Id() { - userFmt = colors.Red(userFmt) - } - } - fmt.Printf("Id: %s\n", cred.ID()) fmt.Printf("Target: %s\n", cred.Target()) fmt.Printf("Kind: %s\n", cred.Kind()) - fmt.Printf("User: %s\n", userFmt) fmt.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822)) switch cred := cred.(type) { @@ -54,6 +36,16 @@ func runBridgeAuthShow(cmd *cobra.Command, args []string) error { fmt.Printf("Value: %s\n", cred.Value) } + fmt.Println("Metadata:") + + meta := make([]string, 0, len(cred.Metadata())) + for key, value := range cred.Metadata() { + meta = append(meta, fmt.Sprintf(" %s --> %s\n", key, value)) + } + sort.Strings(meta) + + fmt.Print(strings.Join(meta, "")) + return nil } diff --git a/commands/user.go b/commands/user.go index f669c73f6064d13febf558ade5a7fa20d483323c..5cf40cf0e77fa3bb38d303465f785959c7c60434 100644 --- a/commands/user.go +++ b/commands/user.go @@ -50,8 +50,6 @@ func runUser(cmd *cobra.Command, args []string) error { Time().Format("Mon Jan 2 15:04:05 2006 +0200")) case "lastModificationLamport": fmt.Printf("%d\n", id.LastModificationLamport()) - case "login": - fmt.Printf("%s\n", id.Login()) case "metadata": for key, value := range id.ImmutableMetadata() { fmt.Printf("%s\n%s\n", key, value) @@ -68,7 +66,6 @@ func runUser(cmd *cobra.Command, args []string) error { fmt.Printf("Id: %s\n", id.Id()) fmt.Printf("Name: %s\n", id.Name()) - fmt.Printf("Login: %s\n", id.Login()) fmt.Printf("Email: %s\n", id.Email()) fmt.Printf("Last modification: %s (lamport %d)\n", id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200"), diff --git a/commands/user_adopt.go b/commands/user_adopt.go index a7de54d9bfd31b39caf7b1c31f0c1fb80a614f3d..7054f1f7470be69603e74d2a6bfddb39bb2a05ff 100644 --- a/commands/user_adopt.go +++ b/commands/user_adopt.go @@ -4,11 +4,10 @@ import ( "fmt" "os" - "github.com/MichaelMure/git-bug/bridge/core/auth" + "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/interrupt" - "github.com/spf13/cobra" ) func runUserAdopt(cmd *cobra.Command, args []string) error { @@ -26,16 +25,6 @@ func runUserAdopt(cmd *cobra.Command, args []string) error { return err } - _, err = backend.GetUserIdentity() - if err == identity.ErrNoIdentitySet { - err = auth.ReplaceDefaultUser(repo, i.Id()) - if err != nil { - return err - } - } else if err != nil { - return err - } - err = backend.SetUserIdentity(i) if err != nil { return err diff --git a/commands/user_create.go b/commands/user_create.go index 15b9767e87b9e7d88dba6d424c045ef733c8f525..95e090509b0d2f053f6f5472635e3b9151ca0d78 100644 --- a/commands/user_create.go +++ b/commands/user_create.go @@ -23,7 +23,7 @@ func runUserCreate(cmd *cobra.Command, args []string) error { return err } - name, err := input.PromptValueRequired("Name", preName) + name, err := input.PromptDefault("Name", "name", preName, input.Required) if err != nil { return err } @@ -33,17 +33,17 @@ func runUserCreate(cmd *cobra.Command, args []string) error { return err } - email, err := input.PromptValueRequired("Email", preEmail) + email, err := input.PromptDefault("Email", "email", preEmail, input.Required) if err != nil { return err } - login, err := input.PromptValue("Avatar URL", "") + avatarURL, err := input.Prompt("Avatar URL", "avatar") if err != nil { return err } - id, err := backend.NewIdentityRaw(name, email, "", login, nil) + id, err := backend.NewIdentityRaw(name, email, avatarURL, nil) if err != nil { return err } diff --git a/doc/man/git-bug-bridge-auth-add-token.1 b/doc/man/git-bug-bridge-auth-add-token.1 index a76ed793ccad1a67b03ef6ebbac5212b2b6b789e..c9ca55d65ed10d0ca7d9d1bbc658bf938a8449a9 100644 --- a/doc/man/git-bug-bridge-auth-add-token.1 +++ b/doc/man/git-bug-bridge-auth-add-token.1 @@ -23,6 +23,14 @@ Store a new token \fB\-t\fP, \fB\-\-target\fP="" The target of the bridge. Valid values are [github,gitlab,launchpad\-preview] +.PP +\fB\-l\fP, \fB\-\-login\fP="" + The login in the remote bug\-tracker + +.PP +\fB\-u\fP, \fB\-\-user\fP="" + The user to add the token to. Default is the current user + .PP \fB\-h\fP, \fB\-\-help\fP[=false] help for add\-token diff --git a/doc/md/git-bug_bridge_auth_add-token.md b/doc/md/git-bug_bridge_auth_add-token.md index 7067c3caa076802c61429f9027ee287b685de2bb..496455a060a3c6be4db2e881d3c4a43fe1ff91b1 100644 --- a/doc/md/git-bug_bridge_auth_add-token.md +++ b/doc/md/git-bug_bridge_auth_add-token.md @@ -14,6 +14,8 @@ git-bug bridge auth add-token [] [flags] ``` -t, --target string The target of the bridge. Valid values are [github,gitlab,launchpad-preview] + -l, --login string The login in the remote bug-tracker + -u, --user string The user to add the token to. Default is the current user -h, --help help for add-token ``` diff --git a/go.mod b/go.mod index 56e913734b2c5f0ef773fb347859ef0c89b3fe49..2c5d5e474e41df922feba130e09bd90dc445e58b 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.4.0 github.com/theckman/goconstraint v1.11.0 - github.com/vektah/gqlparser v1.2.1 + github.com/vektah/gqlparser v1.3.1 github.com/xanzy/go-gitlab v0.24.0 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 diff --git a/go.sum b/go.sum index b5771ceea864c622b64824245bbf42e76b2f5cd1..8fc1e2909d92d657c69a58787782343a5456c24f 100644 --- a/go.sum +++ b/go.sum @@ -131,6 +131,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb 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/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= diff --git a/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go index 215603cbd0e1891f7c8f36805a744bc71219e4f1..5e882142548ea63ae20a52115715158bbab83aea 100644 --- a/graphql/graph/gen_graph.go +++ b/graphql/graph/gen_graph.go @@ -210,7 +210,6 @@ type ComplexityRoot struct { HumanID func(childComplexity int) int ID func(childComplexity int) int IsProtected func(childComplexity int) int - Login func(childComplexity int) int Name func(childComplexity int) int } @@ -1139,13 +1138,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Identity.IsProtected(childComplexity), true - case "Identity.login": - if e.complexity.Identity.Login == nil { - break - } - - return e.complexity.Identity.Login(childComplexity), true - case "Identity.name": if e.complexity.Identity.Name == nil { break @@ -2319,11 +2311,7 @@ type Identity { """ email: String """ - The login of the person, if known. - """ - login: String - """ - A string containing the either the name of the person, its login or both + A non-empty string to display, representing the identity, based on the non-empty values. """ displayName: String! """ @@ -6215,37 +6203,6 @@ func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.C return ec.marshalOString2string(ctx, field.Selections, res) } -func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj identity.Interface) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Identity", - Field: field, - Args: nil, - IsMethod: true, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Login(), nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalOString2string(ctx, field.Selections, res) -} - func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj identity.Interface) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -11946,8 +11903,6 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, out.Values[i] = ec._Identity_name(ctx, field, obj) case "email": out.Values[i] = ec._Identity_email(ctx, field, obj) - case "login": - out.Values[i] = ec._Identity_login(ctx, field, obj) case "displayName": out.Values[i] = ec._Identity_displayName(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/graphql/resolvers/identity.go b/graphql/resolvers/identity.go index da8e7b08b9e0114a401e6595135a13e42bd7cc58..d36669d0a3abd5a615517f2ce060dba7c7eb878f 100644 --- a/graphql/resolvers/identity.go +++ b/graphql/resolvers/identity.go @@ -15,6 +15,7 @@ func (identityResolver) ID(ctx context.Context, obj identity.Interface) (string, return obj.Id().String(), nil } -func (identityResolver) HumanID(ctx context.Context, obj identity.Interface) (string, error) { +func (r identityResolver) HumanID(ctx context.Context, obj identity.Interface) (string, error) { return obj.Id().Human(), nil + } diff --git a/graphql/schema/identity.graphql b/graphql/schema/identity.graphql index 6872ecb9b83c0a2f9e86543eaa3c9610e7616a12..6490d5387eda344482f4edebdb545ddd990135bc 100644 --- a/graphql/schema/identity.graphql +++ b/graphql/schema/identity.graphql @@ -8,9 +8,7 @@ type Identity { name: String """The email of the person, if known.""" email: String - """The login of the person, if known.""" - login: String - """A string containing the either the name of the person, its login or both""" + """A non-empty string to display, representing the identity, based on the non-empty values.""" displayName: String! """An url to an avatar""" avatarUrl: String diff --git a/graphql/schema/root.graphql b/graphql/schema/root.graphql index f66272ca3681ef46f42ad5f5ee144ed53fdae163..2a12cc375cc5aea2c7d606d5cd989215110695d0 100644 --- a/graphql/schema/root.graphql +++ b/graphql/schema/root.graphql @@ -3,6 +3,8 @@ type Query { defaultRepository: Repository """Access a repository by reference/name.""" repository(ref: String!): Repository + + #TODO: connection for all repositories } type Mutation { diff --git a/identity/bare.go b/identity/bare.go index a243f0746356c14e9d9d631757e59c0caff3b892..a02ec790bb29e1825eb237b96f2a69a8c8050ec5 100644 --- a/identity/bare.go +++ b/identity/bare.go @@ -25,7 +25,6 @@ type Bare struct { id entity.Id name string email string - login string avatarUrl string } @@ -33,8 +32,8 @@ func NewBare(name string, email string) *Bare { return &Bare{id: entity.UnsetId, name: name, email: email} } -func NewBareFull(name string, email string, login string, avatarUrl string) *Bare { - return &Bare{id: entity.UnsetId, name: name, email: email, login: login, avatarUrl: avatarUrl} +func NewBareFull(name string, email string, avatarUrl string) *Bare { + return &Bare{id: entity.UnsetId, name: name, email: email, avatarUrl: avatarUrl} } func deriveId(data []byte) entity.Id { @@ -45,7 +44,7 @@ func deriveId(data []byte) entity.Id { type bareIdentityJSON struct { Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` - Login string `json:"login,omitempty"` + Login string `json:"login,omitempty"` // Deprecated, only kept to have the same ID when reading an old value AvatarUrl string `json:"avatar_url,omitempty"` } @@ -53,7 +52,6 @@ func (i *Bare) MarshalJSON() ([]byte, error) { return json.Marshal(bareIdentityJSON{ Name: i.name, Email: i.email, - Login: i.login, AvatarUrl: i.avatarUrl, }) } @@ -70,7 +68,6 @@ func (i *Bare) UnmarshalJSON(data []byte) error { i.name = aux.Name i.email = aux.Email - i.login = aux.Login i.avatarUrl = aux.AvatarUrl return nil @@ -109,45 +106,31 @@ func (i *Bare) Email() string { return i.email } -// Login return the last version of the login -func (i *Bare) Login() string { - return i.login -} - // AvatarUrl return the last version of the Avatar URL func (i *Bare) AvatarUrl() string { return i.avatarUrl } // Keys return the last version of the valid keys -func (i *Bare) Keys() []Key { - return []Key{} +func (i *Bare) Keys() []*Key { + return nil } // ValidKeysAtTime return the set of keys valid at a given lamport time -func (i *Bare) ValidKeysAtTime(time lamport.Time) []Key { - return []Key{} +func (i *Bare) ValidKeysAtTime(_ lamport.Time) []*Key { + return nil } // DisplayName return a non-empty string to display, representing the // identity, based on the non-empty values. func (i *Bare) DisplayName() string { - switch { - case i.name == "" && i.login != "": - return i.login - case i.name != "" && i.login == "": - return i.name - case i.name != "" && i.login != "": - return fmt.Sprintf("%s (%s)", i.name, i.login) - } - - panic("invalid person data") + return i.name } // Validate check if the Identity data is valid func (i *Bare) Validate() error { - if text.Empty(i.name) && text.Empty(i.login) { - return fmt.Errorf("either name or login should be set") + if text.Empty(i.name) { + return fmt.Errorf("name is not set") } if strings.Contains(i.name, "\n") { @@ -158,14 +141,6 @@ func (i *Bare) Validate() error { return fmt.Errorf("name is not fully printable") } - if strings.Contains(i.login, "\n") { - return fmt.Errorf("login should be a single line") - } - - if !text.Safe(i.login) { - return fmt.Errorf("login is not fully printable") - } - if strings.Contains(i.email, "\n") { return fmt.Errorf("email should be a single line") } diff --git a/identity/bare_test.go b/identity/bare_test.go index 335c8d37ef22bbb81fa32f2ffb2f74514d752e97..5aa50e40cba961577b2152c7e0d78aa6860149ad 100644 --- a/identity/bare_test.go +++ b/identity/bare_test.go @@ -18,7 +18,6 @@ func TestBare_Id(t *testing.T) { func TestBareSerialize(t *testing.T) { before := &Bare{ - login: "login", email: "email", name: "name", avatarUrl: "avatar", diff --git a/identity/common.go b/identity/common.go index 007e10d609e9801161a64361fe89f96de443dc51..0fd2b274081fb33510ebb436170ca98ed00c4693 100644 --- a/identity/common.go +++ b/identity/common.go @@ -37,7 +37,7 @@ func UnmarshalJSON(raw json.RawMessage) (Interface, error) { b := &Bare{} err = json.Unmarshal(raw, b) - if err == nil && (b.name != "" || b.login != "") { + if err == nil && b.name != "" { return b, nil } diff --git a/identity/identity.go b/identity/identity.go index cd47c1b74e05a29be7b1242e15a20e3d673689d2..c33a8818b3fd2ea1e189f26c1e11ed7f906d18fd 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "reflect" "strings" "time" @@ -55,14 +56,13 @@ func NewIdentity(name string, email string) *Identity { } } -func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity { +func NewIdentityFull(name string, email string, avatarUrl string) *Identity { return &Identity{ id: entity.UnsetId, versions: []*Version{ { name: name, email: email, - login: login, avatarURL: avatarUrl, nonce: makeNonce(20), }, @@ -271,8 +271,31 @@ func IsUserIdentitySet(repo repository.Repo) (bool, error) { return len(configs) == 1, nil } -func (i *Identity) AddVersion(version *Version) { - i.versions = append(i.versions, version) +type Mutator struct { + Name string + Email string + AvatarUrl string + Keys []*Key +} + +// Mutate allow to create a new version of the Identity +func (i *Identity) Mutate(f func(orig Mutator) Mutator) { + orig := Mutator{ + Name: i.Name(), + Email: i.Email(), + AvatarUrl: i.AvatarUrl(), + Keys: i.Keys(), + } + mutated := f(orig) + if reflect.DeepEqual(orig, mutated) { + return + } + i.versions = append(i.versions, &Version{ + name: mutated.Name, + email: mutated.Email, + avatarURL: mutated.AvatarUrl, + keys: mutated.Keys, + }) } // Write the identity into the Repository. In particular, this ensure that @@ -478,24 +501,19 @@ func (i *Identity) Email() string { return i.lastVersion().email } -// Login return the last version of the login -func (i *Identity) Login() string { - return i.lastVersion().login -} - // AvatarUrl return the last version of the Avatar URL func (i *Identity) AvatarUrl() string { return i.lastVersion().avatarURL } // Keys return the last version of the valid keys -func (i *Identity) Keys() []Key { +func (i *Identity) Keys() []*Key { return i.lastVersion().keys } // ValidKeysAtTime return the set of keys valid at a given lamport time -func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key { - var result []Key +func (i *Identity) ValidKeysAtTime(time lamport.Time) []*Key { + var result []*Key for _, v := range i.versions { if v.time > time { @@ -511,16 +529,7 @@ func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key { // DisplayName return a non-empty string to display, representing the // identity, based on the non-empty values. func (i *Identity) DisplayName() string { - switch { - case i.Name() == "" && i.Login() != "": - return i.Login() - case i.Name() != "" && i.Login() == "": - return i.Name() - case i.Name() != "" && i.Login() != "": - return fmt.Sprintf("%s (%s)", i.Name(), i.Login()) - } - - panic("invalid person data") + return i.Name() } // IsProtected return true if the chain of git commits started to be signed. @@ -540,9 +549,13 @@ func (i *Identity) LastModification() timestamp.Timestamp { return timestamp.Timestamp(i.lastVersion().unixTime) } -// SetMetadata store arbitrary metadata along the last defined Version. -// If the Version has been commit to git already, it won't be overwritten. +// SetMetadata store arbitrary metadata along the last not-commit Version. +// If the Version has been commit to git already, a new identical version is added and will need to be +// commit. func (i *Identity) SetMetadata(key string, value string) { + if i.lastVersion().commitHash != "" { + i.versions = append(i.versions, i.lastVersion().Clone()) + } i.lastVersion().SetMetadata(key, value) } @@ -575,3 +588,9 @@ func (i *Identity) MutableMetadata() map[string]string { return metadata } + +// addVersionForTest add a new version to the identity +// Only for testing ! +func (i *Identity) addVersionForTest(version *Version) { + i.versions = append(i.versions, version) +} diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go index 142ffaa65f41061bd8c103ae2805fc25c90a429d..713b3246c4accfb38f9b284aa22d19601823a931 100644 --- a/identity/identity_actions_test.go +++ b/identity/identity_actions_test.go @@ -48,14 +48,14 @@ func TestPushPull(t *testing.T) { // Update both - identity1.AddVersion(&Version{ + identity1.addVersionForTest(&Version{ name: "name1b", email: "email1b", }) err = identity1.Commit(repoA) require.NoError(t, err) - identity2.AddVersion(&Version{ + identity2.addVersionForTest(&Version{ name: "name2b", email: "email2b", }) @@ -92,7 +92,7 @@ func TestPushPull(t *testing.T) { // Concurrent update - identity1.AddVersion(&Version{ + identity1.addVersionForTest(&Version{ name: "name1c", email: "email1c", }) @@ -102,7 +102,7 @@ func TestPushPull(t *testing.T) { identity1B, err := ReadLocal(repoB, identity1.Id()) require.NoError(t, err) - identity1B.AddVersion(&Version{ + identity1B.addVersionForTest(&Version{ name: "name1concurrent", email: "email1concurrent", }) diff --git a/identity/identity_stub.go b/identity/identity_stub.go index be52ffc0ae730b38ca07094c16a49e31981229af..7e2fcd94f0d2d6bc93899fc60d3e11affac1c642 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -64,11 +64,11 @@ func (IdentityStub) AvatarUrl() string { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (IdentityStub) Keys() []Key { +func (IdentityStub) Keys() []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (IdentityStub) ValidKeysAtTime(time lamport.Time) []Key { +func (IdentityStub) ValidKeysAtTime(_ lamport.Time) []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/identity_test.go b/identity/identity_test.go index f91c548f7423513e9682fa8980d924137dce836e..ee6ccdf7dfd2140bbedb31a276137f51630a970a 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -44,7 +44,7 @@ func TestIdentityCommitLoad(t *testing.T) { time: 100, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyA"}, }, }, @@ -52,7 +52,7 @@ func TestIdentityCommitLoad(t *testing.T) { time: 200, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyB"}, }, }, @@ -60,7 +60,7 @@ func TestIdentityCommitLoad(t *testing.T) { time: 201, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyC"}, }, }, @@ -79,20 +79,25 @@ func TestIdentityCommitLoad(t *testing.T) { // add more version - identity.AddVersion(&Version{ + identity.Mutate(func(orig Mutator) Mutator { + + return orig + }) + + identity.addVersionForTest(&Version{ time: 201, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyD"}, }, }) - identity.AddVersion(&Version{ + identity.addVersionForTest(&Version{ time: 300, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyE"}, }, }) @@ -123,7 +128,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { time: 100, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyA"}, }, }, @@ -131,7 +136,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { time: 200, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyB"}, }, }, @@ -139,7 +144,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { time: 201, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyC"}, }, }, @@ -147,7 +152,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { time: 201, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyD"}, }, }, @@ -155,7 +160,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { time: 300, name: "René Descartes", email: "rene.descartes@example.com", - keys: []Key{ + keys: []*Key{ {PubKey: "pubkeyE"}, }, }, @@ -163,13 +168,13 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { } assert.Nil(t, identity.ValidKeysAtTime(10)) - assert.Equal(t, identity.ValidKeysAtTime(100), []Key{{PubKey: "pubkeyA"}}) - assert.Equal(t, identity.ValidKeysAtTime(140), []Key{{PubKey: "pubkeyA"}}) - assert.Equal(t, identity.ValidKeysAtTime(200), []Key{{PubKey: "pubkeyB"}}) - assert.Equal(t, identity.ValidKeysAtTime(201), []Key{{PubKey: "pubkeyD"}}) - assert.Equal(t, identity.ValidKeysAtTime(202), []Key{{PubKey: "pubkeyD"}}) - assert.Equal(t, identity.ValidKeysAtTime(300), []Key{{PubKey: "pubkeyE"}}) - assert.Equal(t, identity.ValidKeysAtTime(3000), []Key{{PubKey: "pubkeyE"}}) + assert.Equal(t, identity.ValidKeysAtTime(100), []*Key{{PubKey: "pubkeyA"}}) + assert.Equal(t, identity.ValidKeysAtTime(140), []*Key{{PubKey: "pubkeyA"}}) + assert.Equal(t, identity.ValidKeysAtTime(200), []*Key{{PubKey: "pubkeyB"}}) + assert.Equal(t, identity.ValidKeysAtTime(201), []*Key{{PubKey: "pubkeyD"}}) + assert.Equal(t, identity.ValidKeysAtTime(202), []*Key{{PubKey: "pubkeyD"}}) + assert.Equal(t, identity.ValidKeysAtTime(300), []*Key{{PubKey: "pubkeyE"}}) + assert.Equal(t, identity.ValidKeysAtTime(3000), []*Key{{PubKey: "pubkeyE"}}) } // Test the immutable or mutable metadata search @@ -189,7 +194,7 @@ func TestMetadata(t *testing.T) { assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1") // try override - identity.AddVersion(&Version{ + identity.addVersionForTest(&Version{ name: "René Descartes", email: "rene.descartes@example.com", }) diff --git a/identity/interface.go b/identity/interface.go index 54a9da78b193eaf2ef5761cf35360792efbfbe70..d138362dfb8aa1afbae7d2102cf8ca539147dd21 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -17,17 +17,14 @@ type Interface interface { // Email return the last version of the email Email() string - // Login return the last version of the login - Login() string - // AvatarUrl return the last version of the Avatar URL AvatarUrl() string // Keys return the last version of the valid keys - Keys() []Key + Keys() []*Key // ValidKeysAtTime return the set of keys valid at a given lamport time - ValidKeysAtTime(time lamport.Time) []Key + ValidKeysAtTime(time lamport.Time) []*Key // DisplayName return a non-empty string to display, representing the // identity, based on the non-empty values. diff --git a/identity/key.go b/identity/key.go index 90edfb607eaefbe92989023f964eebad0ee16e8f..cc948394aeddc438e1eb7961036b53259459fd92 100644 --- a/identity/key.go +++ b/identity/key.go @@ -11,3 +11,8 @@ func (k *Key) Validate() error { return nil } + +func (k *Key) Clone() *Key { + clone := *k + return &clone +} diff --git a/identity/version.go b/identity/version.go index 955307678a4ad2a0e8cb434a15859eac34d9165c..f9c7b262956060204749933957c14fbe6b1fc089 100644 --- a/identity/version.go +++ b/identity/version.go @@ -24,14 +24,13 @@ type Version struct { unixTime int64 name string - email string - login string + email string // as defined in git, not for bridges avatarURL string // The set of keys valid at that time, from this version onward, until they get removed // in a new version. This allow to have multiple key for the same identity (e.g. one per // device) as well as revoke key. - keys []Key + keys []*Key // This optional array is here to ensure a better randomness of the identity id to avoid collisions. // It has no functional purpose and should be ignored. @@ -53,13 +52,28 @@ type VersionJSON struct { UnixTime int64 `json:"unix_time"` Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` - Login string `json:"login,omitempty"` AvatarUrl string `json:"avatar_url,omitempty"` - Keys []Key `json:"pub_keys,omitempty"` + Keys []*Key `json:"pub_keys,omitempty"` Nonce []byte `json:"nonce,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` } +// Make a deep copy +func (v *Version) Clone() *Version { + clone := &Version{ + name: v.name, + email: v.email, + avatarURL: v.avatarURL, + keys: make([]*Key, len(v.keys)), + } + + for i, key := range v.keys { + clone.keys[i] = key.Clone() + } + + return clone +} + func (v *Version) MarshalJSON() ([]byte, error) { return json.Marshal(VersionJSON{ FormatVersion: formatVersion, @@ -67,7 +81,6 @@ func (v *Version) MarshalJSON() ([]byte, error) { UnixTime: v.unixTime, Name: v.name, Email: v.email, - Login: v.login, AvatarUrl: v.avatarURL, Keys: v.keys, Nonce: v.nonce, @@ -90,7 +103,6 @@ func (v *Version) UnmarshalJSON(data []byte) error { v.unixTime = aux.UnixTime v.name = aux.Name v.email = aux.Email - v.login = aux.Login v.avatarURL = aux.AvatarUrl v.keys = aux.Keys v.nonce = aux.Nonce @@ -108,8 +120,8 @@ func (v *Version) Validate() error { return fmt.Errorf("lamport time not set") } - if text.Empty(v.name) && text.Empty(v.login) { - return fmt.Errorf("either name or login should be set") + if text.Empty(v.name) { + return fmt.Errorf("name not set") } if strings.Contains(v.name, "\n") { @@ -120,14 +132,6 @@ func (v *Version) Validate() error { return fmt.Errorf("name is not fully printable") } - if strings.Contains(v.login, "\n") { - return fmt.Errorf("login should be a single line") - } - - if !text.Safe(v.login) { - return fmt.Errorf("login is not fully printable") - } - if strings.Contains(v.email, "\n") { return fmt.Errorf("email should be a single line") } @@ -202,7 +206,7 @@ func (v *Version) GetMetadata(key string) (string, bool) { return val, ok } -// AllMetadata return all metadata for this Identity +// AllMetadata return all metadata for this Version func (v *Version) AllMetadata() map[string]string { return v.metadata } diff --git a/identity/version_test.go b/identity/version_test.go index 8c4c8d9951c8ca3c20f32eb7c55ee06c57f70e88..25848eb5e6ede496e4f5ed942596314854e45334 100644 --- a/identity/version_test.go +++ b/identity/version_test.go @@ -9,11 +9,10 @@ import ( func TestVersionSerialize(t *testing.T) { before := &Version{ - login: "login", name: "name", email: "email", avatarURL: "avatarUrl", - keys: []Key{ + keys: []*Key{ { Fingerprint: "fingerprint1", PubKey: "pubkey1", diff --git a/input/prompt.go b/input/prompt.go index 6036c0626ff8f44e2800a076f409b98e1526679b..960ecd62f2a7c5072e11e8da7f6cdaace4330e98 100644 --- a/input/prompt.go +++ b/input/prompt.go @@ -4,23 +4,38 @@ import ( "bufio" "fmt" "os" + "strconv" "strings" + "syscall" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/MichaelMure/git-bug/util/interrupt" ) -func PromptValue(name string, preValue string) (string, error) { - return promptValue(name, preValue, false) +// PromptValidator is a validator for a user entry +// If complaint is "", value is considered valid, otherwise it's the error reported to the user +// If err != nil, a terminal error happened +type PromptValidator func(name string, value string) (complaint string, err error) + +// Required is a validator preventing a "" value +func Required(name string, value string) (string, error) { + if value == "" { + return fmt.Sprintf("%s is empty", name), nil + } + return "", nil } -func PromptValueRequired(name string, preValue string) (string, error) { - return promptValue(name, preValue, true) +func Prompt(prompt, name string, validators ...PromptValidator) (string, error) { + return PromptDefault(prompt, name, "", validators...) } -func promptValue(name string, preValue string, required bool) (string, error) { +func PromptDefault(prompt, name, preValue string, validators ...PromptValidator) (string, error) { for { if preValue != "" { - _, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", name, preValue) + _, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", prompt, preValue) } else { - _, _ = fmt.Fprintf(os.Stderr, "%s: ", name) + _, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt) } line, err := bufio.NewReader(os.Stdin).ReadString('\n') @@ -31,14 +46,85 @@ func promptValue(name string, preValue string, required bool) (string, error) { line = strings.TrimSpace(line) if preValue != "" && line == "" { - return preValue, nil + line = preValue } - if required && line == "" { - _, _ = fmt.Fprintf(os.Stderr, "%s is empty\n", name) - continue + for _, validator := range validators { + complaint, err := validator(name, line) + if err != nil { + return "", err + } + if complaint != "" { + _, _ = fmt.Fprintln(os.Stderr, complaint) + continue + } } return line, nil } } + +func PromptPassword(prompt, name string, validators ...PromptValidator) (string, error) { + termState, err := terminal.GetState(syscall.Stdin) + if err != nil { + return "", err + } + + cancel := interrupt.RegisterCleaner(func() error { + return terminal.Restore(syscall.Stdin, termState) + }) + defer cancel() + + for { + _, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt) + + bytePassword, err := terminal.ReadPassword(syscall.Stdin) + // new line for coherent formatting, ReadPassword clip the normal new line + // entered by the user + fmt.Println() + + if err != nil { + return "", err + } + + pass := string(bytePassword) + + for _, validator := range validators { + complaint, err := validator(name, pass) + if err != nil { + return "", err + } + if complaint != "" { + _, _ = fmt.Fprintln(os.Stderr, complaint) + continue + } + } + + return pass, nil + } +} + +func PromptChoice(prompt string, choices []string) (int, error) { + for { + for i, choice := range choices { + _, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice) + } + _, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt) + + line, err := bufio.NewReader(os.Stdin).ReadString('\n') + fmt.Println() + if err != nil { + return 0, err + } + + line = strings.TrimSpace(line) + + index, err := strconv.Atoi(line) + if err != nil || index < 1 || index > len(choices) { + fmt.Println("invalid input") + continue + } + + return index, nil + } +} diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 557bbf2094454ca8049a47c98971480699716214..a062bfe86a2ec5315cb6e85e23a79c313ae5bbbd 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -305,6 +305,14 @@ _git-bug_bridge_auth_add-token() two_word_flags+=("--target") two_word_flags+=("-t") local_nonpersistent_flags+=("--target=") + flags+=("--login=") + two_word_flags+=("--login") + two_word_flags+=("-l") + local_nonpersistent_flags+=("--login=") + flags+=("--user=") + two_word_flags+=("--user") + two_word_flags+=("-u") + local_nonpersistent_flags+=("--user=") must_have_one_flag=() must_have_one_noun=() diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug index d52113e4b75fffc4d91a9c9cf3a87a8b7e47d9a6..b15e6398dee0bad40d87fb93ad67f5a800426129 100644 --- a/misc/powershell_completion/git-bug +++ b/misc/powershell_completion/git-bug @@ -64,6 +64,10 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { 'git-bug;bridge;auth;add-token' { [CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'The target of the bridge. Valid values are [github,gitlab,launchpad-preview]') [CompletionResult]::new('--target', 'target', [CompletionResultType]::ParameterName, 'The target of the bridge. Valid values are [github,gitlab,launchpad-preview]') + [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'The login in the remote bug-tracker') + [CompletionResult]::new('--login', 'login', [CompletionResultType]::ParameterName, 'The login in the remote bug-tracker') + [CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'The user to add the token to. Default is the current user') + [CompletionResult]::new('--user', 'user', [CompletionResultType]::ParameterName, 'The user to add the token to. Default is the current user') break } 'git-bug;bridge;auth;rm' { diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index 3b06a3963af63e72262828efddbe4a0f2303b8a4..f6d50e082549da8f6c819fd1ea08825e743af217 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -177,7 +177,9 @@ function _git-bug_bridge_auth { function _git-bug_bridge_auth_add-token { _arguments \ - '(-t --target)'{-t,--target}'[The target of the bridge. Valid values are [github,gitlab,launchpad-preview]]:' + '(-t --target)'{-t,--target}'[The target of the bridge. Valid values are [github,gitlab,launchpad-preview]]:' \ + '(-l --login)'{-l,--login}'[The login in the remote bug-tracker]:' \ + '(-u --user)'{-u,--user}'[The user to add the token to. Default is the current user]:' } function _git-bug_bridge_auth_rm {