From db893494bb1492a3d9e32787a5ada1dd8f639ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 7 Jan 2020 22:06:42 +0100 Subject: [PATCH 01/16] input: better reusable prompt functions --- bridge/github/config.go | 139 ++++------------------------------------ commands/user_create.go | 6 +- input/prompt.go | 106 ++++++++++++++++++++++++++---- 3 files changed, 110 insertions(+), 141 deletions(-) diff --git a/bridge/github/config.go b/bridge/github/config.go index ea4b622f5f29f46631f82b2135195c3ff3d65189..8c4bf7c5b54c638bbffd8ea027efb614091fe0b4 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -14,21 +14,19 @@ 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 ( @@ -320,21 +318,14 @@ func promptToken() (string, error) { panic("regexp compile:" + err.Error()) } - for { - fmt.Print("Enter token: ") - - 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 "", nil } - - token := strings.TrimSpace(line) - if re.MatchString(token) { - return token, 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) { @@ -348,17 +339,18 @@ 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 } @@ -387,12 +379,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 +400,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 +554,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/commands/user_create.go b/commands/user_create.go index 15b9767e87b9e7d88dba6d424c045ef733c8f525..dab8a879426b5aeb4c4b2e92ee9e8bab07c645fc 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.Prompt("Name", "name", preName, input.Required) if err != nil { return err } @@ -33,12 +33,12 @@ func runUserCreate(cmd *cobra.Command, args []string) error { return err } - email, err := input.PromptValueRequired("Email", preEmail) + email, err := input.Prompt("Email", "email", preEmail, input.Required) if err != nil { return err } - login, err := input.PromptValue("Avatar URL", "") + login, err := input.Prompt("Avatar URL", "avatar", "") if err != nil { return err } diff --git a/input/prompt.go b/input/prompt.go index 6036c0626ff8f44e2800a076f409b98e1526679b..c7887abb7860696d3c52005bcf354dfdcdf6d723 100644 --- a/input/prompt.go +++ b/input/prompt.go @@ -4,23 +4,36 @@ 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 +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 +44,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 + } +} From 26f0152384f77d2bfd16c6762f5618bc966809a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 7 Jan 2020 22:07:25 +0100 Subject: [PATCH 02/16] WIP --- bridge/core/auth/credential.go | 56 ++++++++++++----------------- bridge/core/auth/options.go | 32 +++++++---------- bridge/core/auth/token.go | 20 +++++------ bridge/core/bridge.go | 13 +++---- bridge/github/config.go | 65 ++++++++++++++-------------------- bridge/gitlab/config.go | 2 +- 6 files changed, 78 insertions(+), 110 deletions(-) diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go index fd026c5d967354620b80f95b76a172899c038442..228eb006ba984fbd67e4e4ee02d5585c9fe4f894 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,19 +34,13 @@ 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 // 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. @@ -120,6 +116,17 @@ 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 + } + } + return result +} + // List load all existing credentials func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) { rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".") @@ -185,12 +192,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 +204,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 +229,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/options.go b/bridge/core/auth/options.go index 7bcda68e7a7fe25c2781e3b5f2939da963457009..0c780dc1a3d1a1b1f4d5669ed69a35804caf4e56 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.Metadata()[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..60137cd9961286c4b5caeca1ae3985e41495f8ba 100644 --- a/bridge/core/auth/token.go +++ b/bridge/core/auth/token.go @@ -18,26 +18,25 @@ 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, + meta: make(map[string]string), } } 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 +45,7 @@ func NewTokenFromConfig(conf map[string]string) *Token { } token.Value = conf[tokenValueKey] + token.meta = metaFromConfig(conf) return token } @@ -55,14 +55,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 +84,10 @@ func (t *Token) Validate() error { return nil } +func (t *Token) Metadata() map[string]string { + return t.meta +} + 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..7891763fd95c6290aeb72137519a8bbcaa303f2b 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -32,12 +32,13 @@ var bridgeImpl map[string]reflect.Type // 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 diff --git a/bridge/github/config.go b/bridge/github/config.go index 8c4bf7c5b54c638bbffd8ea027efb614091fe0b4..e51f244b79ce894bedf0eb0f1d0a1100a918c4df 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -22,8 +22,6 @@ 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" @@ -49,12 +47,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 @@ -87,15 +79,12 @@ 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 - } - - // 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 := params.Login + if login == "" { + login, err = input.Prompt("Github login", "", true, validateUsername) + if err != nil { + return nil, err + } } var cred auth.Credential @@ -106,13 +95,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.Metadata()[auth.MetaKeyLogin] = login default: - cred, err = promptTokenOptions(repo, userId, owner, project) + cred, err = promptTokenOptions(repo, login, owner, project) if err != nil { return nil, err } @@ -170,11 +157,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"` @@ -196,7 +183,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 != "" { @@ -240,9 +227,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 } @@ -258,10 +245,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), ) } @@ -289,13 +277,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.Metadata()[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.Metadata()[auth.MetaKeyLogin] = login + return token, nil default: return creds[index-3], nil } @@ -328,7 +320,7 @@ func promptToken() (string, error) { 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.") @@ -345,11 +337,6 @@ func loginAndRequestToken(owner, project string) (string, error) { } isPublic := i == 0 - username, err := promptUsername() - if err != nil { - return "", err - } - password, err := input.PromptPassword("Password", "password", input.Required) if err != nil { return "", err @@ -369,7 +356,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 } diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go index 5e345b314d3114f9454c361c4335e2eea477cb95..0758074c298d507e88864ec442a9b9516a3fbdde 100644 --- a/bridge/gitlab/config.go +++ b/bridge/gitlab/config.go @@ -212,7 +212,7 @@ func validateBaseUrl(baseUrl string) (bool, error) { func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, 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 } From ae2f942ef907161af0aba5f3511db72cf9801dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 12 Jan 2020 16:38:16 +0100 Subject: [PATCH 03/16] more wip --- bridge/core/auth/credential.go | 4 +-- bridge/core/auth/credential_test.go | 41 +++++++++++--------------- bridge/core/config.go | 1 + bridge/github/config.go | 8 +++-- bridge/github/config_test.go | 5 ++-- bridge/github/export.go | 45 ++++++++++------------------- bridge/github/import.go | 16 +--------- bridge/github/import_test.go | 8 +---- input/prompt.go | 2 ++ 9 files changed, 47 insertions(+), 83 deletions(-) create mode 100644 bridge/core/config.go diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go index 228eb006ba984fbd67e4e4ee02d5585c9fe4f894..e843ede7a331ecc213caffff514060b2562963a5 100644 --- a/bridge/core/auth/credential.go +++ b/bridge/core/auth/credential.go @@ -43,7 +43,7 @@ type Credential interface { Metadata() map[string]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 } @@ -134,7 +134,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) } diff --git a/bridge/core/auth/credential_test.go b/bridge/core/auth/credential_test.go index f91d273dca631c20315c6226b929d3f91e45372e..49c138cf7983a61af08409879fc2dceedd31bed9 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.Metadata()["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/config.go b/bridge/core/config.go new file mode 100644 index 0000000000000000000000000000000000000000..9a8bc9592b0bcef7115b5159d4f426ba4fbd39f8 --- /dev/null +++ b/bridge/core/config.go @@ -0,0 +1 @@ +package core diff --git a/bridge/github/config.go b/bridge/github/config.go index e51f244b79ce894bedf0eb0f1d0a1100a918c4df..fcb9407919d2df123dc95b4c36ab357ad4075ae9 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -81,7 +81,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor login := params.Login if login == "" { - login, err = input.Prompt("Github login", "", true, validateUsername) + login, err = input.Prompt("Github login", "login", input.Required, validateUsername) if err != nil { return nil, err } @@ -128,6 +128,10 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor return nil, err } + // Todo: if no user exist with the given login + // - tag the default user with the github login + // - add a command to manually tag a user ? + // don't forget to store the now known valid token if !auth.IdExist(repo, cred.ID()) { err = auth.Store(repo, cred) @@ -317,7 +321,7 @@ func promptToken() (string, error) { return "token has incorrect format", nil } - return input.Prompt("Enter token", "token", "", input.Required, validator) + return input.Prompt("Enter token", "token", input.Required, validator) } func loginAndRequestToken(login, owner, project string) (string, error) { 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..1cc66dee7f5c918734229bfe4836c7b92b491e73 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,6 +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/identity" "github.com/MichaelMure/git-bug/repository" ) @@ -33,13 +35,6 @@ type githubExporter struct { // cache identities clients identityClient map[entity.Id]*githubv4.Client - // the client to use for non user-specific queries - // should be the client of the default user - defaultClient *githubv4.Client - - // the token of the default user - defaultToken *auth.Token - // github repository ID repositoryID string @@ -58,43 +53,33 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e ge.cachedOperationIDs = make(map[entity.Id]string) ge.cachedLabels = make(map[string]string) - user, err := repo.GetUserIdentity() - if err != nil { - return err - } - // preload all clients - err = ge.cacheAllClient(repo) - if err != nil { - return err - } - - ge.defaultClient, err = ge.getClientForIdentity(user.Id()) - if err != nil { - return err - } - - creds, err := auth.List(repo, auth.WithUserId(user.Id()), auth.WithTarget(target), auth.WithKind(auth.KindToken)) + err := ge.cacheAllClient(repo) if err != nil { return err } - if len(creds) == 0 { - return ErrMissingIdentityToken - } - - ge.defaultToken = creds[0].(*auth.Token) - 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 { + login, ok := cred.Metadata()[auth.MetaKeyLogin] + if !ok { + _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with Github login\n", cred.ID().Human()) + continue + } + + user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGithubLogin, login) + if err == identity.ErrIdentityNotExist { + continue + } + if _, ok := ge.identityClient[cred.UserId()]; !ok { client := buildClient(creds[0].(*auth.Token)) ge.identityClient[cred.UserId()] = client diff --git a/bridge/github/import.go b/bridge/github/import.go index 39aebccb9a0c6d246d4be633b4902cf222da4932..aac4f119fcb9a080afb2fd256dda627e0ad94bd1 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -12,7 +12,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" ) @@ -39,20 +38,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 } diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 57bab61ecb18ece6a14424b672b0f69d8e62cfaf..75310ab3abd6771c3a3a8fdc8781dbfe7ec8efb4 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -140,13 +140,7 @@ 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) - - token := auth.NewToken(author.Id(), envToken, target) + token := auth.NewToken(envToken, target) err = auth.Store(repo, token) require.NoError(t, err) diff --git a/input/prompt.go b/input/prompt.go index c7887abb7860696d3c52005bcf354dfdcdf6d723..960ecd62f2a7c5072e11e8da7f6cdaace4330e98 100644 --- a/input/prompt.go +++ b/input/prompt.go @@ -14,6 +14,8 @@ import ( ) // 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 From 390b13c9ff69f3d42860b5f8109eb134459c40b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 14 Jan 2020 22:01:44 +0100 Subject: [PATCH 04/16] identity: rework mutation --- cache/identity_cache.go | 4 ++-- identity/identity.go | 23 +++++++++++++++++++++-- identity/version.go | 12 ++++++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/cache/identity_cache.go b/cache/identity_cache.go index 2ae55f2d7c4841115aecfcb52c17e535a934f94a..f9c928fe269d11da8d22decec3dfa79a7b355d8d 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.VersionMutator) identity.VersionMutator) error { + i.Identity.Mutate(f) return i.notifyUpdated() } diff --git a/identity/identity.go b/identity/identity.go index cd47c1b74e05a29be7b1242e15a20e3d673689d2..a07bf8580ca4132a892ad358eacf168e07b525de 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "reflect" "strings" "time" @@ -271,8 +272,26 @@ func IsUserIdentitySet(repo repository.Repo) (bool, error) { return len(configs) == 1, nil } -func (i *Identity) AddVersion(version *Version) { - i.versions = append(i.versions, version) +// Mutate allow to create a new version of the Identity +func (i *Identity) Mutate(f func(orig VersionMutator) VersionMutator) { + orig := VersionMutator{ + Name: i.Name(), + Email: i.Email(), + Login: i.Login(), + 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, + login: mutated.Login, + avatarURL: mutated.AvatarUrl, + keys: mutated.Keys, + }) } // Write the identity into the Repository. In particular, this ensure that diff --git a/identity/version.go b/identity/version.go index 955307678a4ad2a0e8cb434a15859eac34d9165c..b5b8dae343e1c85436a8e729daf612e1bf9506e8 100644 --- a/identity/version.go +++ b/identity/version.go @@ -24,8 +24,8 @@ type Version struct { unixTime int64 name string - email string - login string + email string // TODO: remove ? keep and use metadata for bridges ? + login string // TODO: remove avatarURL string // The set of keys valid at that time, from this version onward, until they get removed @@ -60,6 +60,14 @@ type VersionJSON struct { Metadata map[string]string `json:"metadata,omitempty"` } +type VersionMutator struct { + Name string + Email string + Login string + AvatarUrl string + Keys []Key +} + func (v *Version) MarshalJSON() ([]byte, error) { return json.Marshal(VersionJSON{ FormatVersion: formatVersion, From da0904d23826ebe578305b51e38f49fdf330f992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 14 Jan 2020 22:02:03 +0100 Subject: [PATCH 05/16] cache: rework resolving of bugs, identity --- cache/repo_cache.go | 72 +++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 90a489c857c5d23febc4df5b5919660019a13c48..99afeb4179f1b1027f573f9518428954233061a9 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,32 @@ 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) ResolveIdentityLogin(login string) (*IdentityCache, error) { + return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool { + return excerpt.Login == login + }) +} + +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) } } From b950c2580dfbf7672ee9e5e1e3f5b45c8cbc2788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 14 Jan 2020 22:02:35 +0100 Subject: [PATCH 06/16] more wip --- bridge/github/config.go | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/bridge/github/config.go b/bridge/github/config.go index fcb9407919d2df123dc95b4c36ab357ad4075ae9..30e28d0d8c8e8e14cc04b9a2c53567bc04ab6026 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -22,6 +22,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/identity" "github.com/MichaelMure/git-bug/input" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/colors" @@ -81,7 +82,18 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor login := params.Login if login == "" { - login, err = input.Prompt("Github login", "login", input.Required, validateUsername) + 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 + } + + login, err = input.Prompt("Github login", "login", input.Required, validator) if err != nil { return nil, err } @@ -128,6 +140,28 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor return nil, err } + // TODO + func(login string) error { + // if no user exist with the given login + _, err := repo.ResolveIdentityLogin(login) + if err != nil && err != identity.ErrIdentityNotExist { + return err + } + + // tag the default user with the github login, if any + // if not, + user, err := repo.GetUserIdentity() + if err == identity.ErrNoIdentitySet { + return nil + } + if err != nil { + return err + } + + repo.GetUserIdentity() + + }(login) + // Todo: if no user exist with the given login // - tag the default user with the github login // - add a command to manually tag a user ? From 8da522d97af3dcaca8a8424e3541705c69779d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 21 Jan 2020 18:49:33 +0100 Subject: [PATCH 07/16] wip --- bridge/github/config.go | 6 ++++-- cache/identity_cache.go | 2 +- identity/bare.go | 37 ++++++------------------------------ identity/bare_test.go | 1 - identity/common.go | 2 +- identity/identity.go | 40 +++++++++++++++++---------------------- identity/interface.go | 3 --- identity/version.go | 42 ++++++++++++++++++++--------------------- 8 files changed, 49 insertions(+), 84 deletions(-) diff --git a/bridge/github/config.go b/bridge/github/config.go index 30e28d0d8c8e8e14cc04b9a2c53567bc04ab6026..40653afa793d3c2588daf820f56683b71e432626 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -149,7 +149,6 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor } // tag the default user with the github login, if any - // if not, user, err := repo.GetUserIdentity() if err == identity.ErrNoIdentitySet { return nil @@ -158,7 +157,10 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor return err } - repo.GetUserIdentity() + userLogin, ok := user.ImmutableMetadata()[metaKeyGithubLogin] + if !ok { + user.SetMetadata() + } }(login) diff --git a/cache/identity_cache.go b/cache/identity_cache.go index f9c928fe269d11da8d22decec3dfa79a7b355d8d..e2129f8452fc810cfdd2374568d5815010670af3 100644 --- a/cache/identity_cache.go +++ b/cache/identity_cache.go @@ -21,7 +21,7 @@ func (i *IdentityCache) notifyUpdated() error { return i.repoCache.identityUpdated(i.Identity.Id()) } -func (i *IdentityCache) Mutate(f func(identity.VersionMutator) identity.VersionMutator) error { +func (i *IdentityCache) Mutate(f func(identity.IdentityMutator) identity.IdentityMutator) error { i.Identity.Mutate(f) return i.notifyUpdated() } diff --git a/identity/bare.go b/identity/bare.go index a243f0746356c14e9d9d631757e59c0caff3b892..26ecdf030083246b57f4b751e3778931e3532395 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,11 +106,6 @@ 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 @@ -132,22 +124,13 @@ func (i *Bare) ValidKeysAtTime(time lamport.Time) []Key { // 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 a07bf8580ca4132a892ad358eacf168e07b525de..655afd316cde4159223b82b5ba9c93c8f50886d5 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -56,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), }, @@ -272,12 +271,18 @@ func IsUserIdentitySet(repo repository.Repo) (bool, error) { return len(configs) == 1, nil } +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 VersionMutator) VersionMutator) { - orig := VersionMutator{ +func (i *Identity) Mutate(f func(orig Mutator) Mutator) { + orig := Mutator{ Name: i.Name(), Email: i.Email(), - Login: i.Login(), AvatarUrl: i.AvatarUrl(), Keys: i.Keys(), } @@ -288,7 +293,6 @@ func (i *Identity) Mutate(f func(orig VersionMutator) VersionMutator) { i.versions = append(i.versions, &Version{ name: mutated.Name, email: mutated.Email, - login: mutated.Login, avatarURL: mutated.AvatarUrl, keys: mutated.Keys, }) @@ -497,11 +501,6 @@ 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 @@ -530,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. @@ -559,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 version is added and will need to be +// commit. func (i *Identity) SetMetadata(key string, value string) { + if i.lastVersion().commitHash != "" { + + } i.lastVersion().SetMetadata(key, value) } diff --git a/identity/interface.go b/identity/interface.go index 54a9da78b193eaf2ef5761cf35360792efbfbe70..3407d7ab6c1ab8fc1e761441c61f12e7ac65e57e 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -17,9 +17,6 @@ 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 diff --git a/identity/version.go b/identity/version.go index b5b8dae343e1c85436a8e729daf612e1bf9506e8..85195049b7771127eb19a41434ff084d1bf3b23f 100644 --- a/identity/version.go +++ b/identity/version.go @@ -24,8 +24,7 @@ type Version struct { unixTime int64 name string - email string // TODO: remove ? keep and use metadata for bridges ? - login string // TODO: remove + 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 @@ -53,19 +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"` Nonce []byte `json:"nonce,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` } -type VersionMutator struct { - Name string - Email string - Login string - AvatarUrl string - Keys []Key +// 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)), + metadata: make(map[string]string), + } + + for i, op := range opp.Operations { + clone.Operations[i] = op + } + + return clone } func (v *Version) MarshalJSON() ([]byte, error) { @@ -75,7 +83,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, @@ -98,7 +105,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 @@ -116,8 +122,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") { @@ -128,14 +134,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") } @@ -210,7 +208,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 } From 74e91144105790cc997c1d79a7f638e1e3a1f3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 24 Jan 2020 00:30:13 +0100 Subject: [PATCH 08/16] more more wip --- bridge/core/auth/credential.go | 6 ++ bridge/core/auth/credential_test.go | 2 +- bridge/core/auth/options.go | 2 +- bridge/core/auth/token.go | 13 ++- bridge/core/config.go | 42 ++++++++++ bridge/github/config.go | 37 +------- bridge/github/export.go | 41 +++++++-- bridge/github/export_test.go | 2 +- bridge/github/import.go | 2 - bridge/launchpad/import.go | 1 - bug/status.go | 2 +- cache/bug_excerpt.go | 18 +--- cache/filter.go | 3 +- cache/identity_cache.go | 2 +- cache/identity_excerpt.go | 17 +--- cache/query.go | 4 +- cache/repo_cache.go | 31 ++++--- graphql/graph/gen_graph.go | 126 ++++++++++++++++++++++++++-- graphql/resolvers/identity.go | 4 - graphql/schema/identity.graphql | 4 +- graphql/schema/root.graphql | 2 + identity/bare.go | 8 +- identity/identity.go | 18 ++-- identity/identity_actions_test.go | 8 +- identity/identity_stub.go | 4 +- identity/identity_test.go | 45 +++++----- identity/interface.go | 4 +- identity/key.go | 5 ++ identity/version.go | 12 ++- identity/version_test.go | 3 +- 30 files changed, 313 insertions(+), 155 deletions(-) diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go index e843ede7a331ecc213caffff514060b2562963a5..c1255aa6e31cf0918d0b4643ce671fc8d0bf7433 100644 --- a/bridge/core/auth/credential.go +++ b/bridge/core/auth/credential.go @@ -40,7 +40,10 @@ type Credential interface { 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, Kind, CreateTime and Metadata. @@ -124,6 +127,9 @@ func metaFromConfig(configs map[string]string) map[string]string { result[key] = val } } + if len(result) == 0 { + return nil + } return result } diff --git a/bridge/core/auth/credential_test.go b/bridge/core/auth/credential_test.go index 49c138cf7983a61af08409879fc2dceedd31bed9..2f8806c951d4e268bebee97b86c4412327f6017c 100644 --- a/bridge/core/auth/credential_test.go +++ b/bridge/core/auth/credential_test.go @@ -63,7 +63,7 @@ func TestCredential(t *testing.T) { // Metadata - token4.Metadata()["key"] = "value" + token4.SetMetadata("key", "value") err = Store(repo, token4) assert.NoError(t, err) diff --git a/bridge/core/auth/options.go b/bridge/core/auth/options.go index 0c780dc1a3d1a1b1f4d5669ed69a35804caf4e56..741898743b08f6052a72f9cf5caeee782812a036 100644 --- a/bridge/core/auth/options.go +++ b/bridge/core/auth/options.go @@ -26,7 +26,7 @@ func (opts *options) Match(cred Credential) bool { } for key, val := range opts.meta { - if v, ok := cred.Metadata()[key]; !ok || v != val { + if v, ok := cred.GetMetadata(key); !ok || v != val { return false } } diff --git a/bridge/core/auth/token.go b/bridge/core/auth/token.go index 60137cd9961286c4b5caeca1ae3985e41495f8ba..42f960bfc9d72ae979f2ae34e86fbbcd11e1bdb6 100644 --- a/bridge/core/auth/token.go +++ b/bridge/core/auth/token.go @@ -30,7 +30,6 @@ func NewToken(value, target string) *Token { target: target, createTime: time.Now(), Value: value, - meta: make(map[string]string), } } @@ -88,6 +87,18 @@ 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/config.go b/bridge/core/config.go index 9a8bc9592b0bcef7115b5159d4f426ba4fbd39f8..adee5f08e9b8ea145d79ebc24346913903ea7534 100644 --- a/bridge/core/config.go +++ b/bridge/core/config.go @@ -1 +1,43 @@ 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.ErrIdentityNotExist { + // 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, + }) + + err = repo.SetUserIdentity(i) + if err != nil { + return err + } + + return nil +} diff --git a/bridge/github/config.go b/bridge/github/config.go index 40653afa793d3c2588daf820f56683b71e432626..9ede72d495ca73efcc83923a28399a7341f52f23 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -22,7 +22,6 @@ 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/identity" "github.com/MichaelMure/git-bug/input" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/colors" @@ -109,7 +108,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor } case params.TokenRaw != "": cred = auth.NewToken(params.TokenRaw, target) - cred.Metadata()[auth.MetaKeyLogin] = login + cred.SetMetadata(auth.MetaKeyLogin, login) default: cred, err = promptTokenOptions(repo, login, owner, project) if err != nil { @@ -140,34 +139,6 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor return nil, err } - // TODO - func(login string) error { - // if no user exist with the given login - _, err := repo.ResolveIdentityLogin(login) - if err != nil && err != identity.ErrIdentityNotExist { - return err - } - - // tag the default user with the github login, if any - user, err := repo.GetUserIdentity() - if err == identity.ErrNoIdentitySet { - return nil - } - if err != nil { - return err - } - - userLogin, ok := user.ImmutableMetadata()[metaKeyGithubLogin] - if !ok { - user.SetMetadata() - } - - }(login) - - // Todo: if no user exist with the given login - // - tag the default user with the github login - // - add a command to manually tag a user ? - // don't forget to store the now known valid token if !auth.IdExist(repo, cred.ID()) { err = auth.Store(repo, cred) @@ -176,7 +147,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 { @@ -318,7 +289,7 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string return nil, err } token := auth.NewToken(value, target) - token.Metadata()[auth.MetaKeyLogin] = login + token.SetMetadata(auth.MetaKeyLogin, login) return token, nil case 2: value, err := loginAndRequestToken(login, owner, project) @@ -326,7 +297,7 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string return nil, err } token := auth.NewToken(value, target) - token.Metadata()[auth.MetaKeyLogin] = login + token.SetMetadata(auth.MetaKeyLogin, login) return token, nil default: return creds[index-3], nil diff --git a/bridge/github/export.go b/bridge/github/export.go index 1cc66dee7f5c918734229bfe4836c7b92b491e73..663361f59f589d3b44cd18daff4c259c2e10fd9f 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -21,7 +21,6 @@ import ( "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" ) var ( @@ -35,6 +34,13 @@ type githubExporter struct { // cache identities clients identityClient map[entity.Id]*githubv4.Client + // the client to use for non user-specific queries + // should be the client of the default user + defaultClient *githubv4.Client + + // the token of the default user + defaultToken *auth.Token + // github repository ID repositoryID string @@ -53,12 +59,34 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e ge.cachedOperationIDs = make(map[entity.Id]string) ge.cachedLabels = make(map[string]string) + user, err := repo.GetUserIdentity() + if err != nil { + return err + } + // preload all clients - err := ge.cacheAllClient(repo) + err = ge.cacheAllClient(repo) if err != nil { return err } + ge.defaultClient, err = ge.getClientForIdentity(user.Id()) + if err != nil { + return err + } + + login := user.ImmutableMetadata()[metaKeyGithubLogin] + creds, err := auth.List(repo, auth.WithMeta(metaKeyGithubLogin, login), auth.WithTarget(target), auth.WithKind(auth.KindToken)) + if err != nil { + return err + } + + if len(creds) == 0 { + return ErrMissingIdentityToken + } + + ge.defaultToken = creds[0].(*auth.Token) + return nil } @@ -69,7 +97,7 @@ func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error { } for _, cred := range creds { - login, ok := cred.Metadata()[auth.MetaKeyLogin] + login, ok := cred.GetMetadata(auth.MetaKeyLogin) if !ok { _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with Github login\n", cred.ID().Human()) continue @@ -80,9 +108,9 @@ func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error { continue } - if _, ok := ge.identityClient[cred.UserId()]; !ok { + if _, ok := ge.identityClient[user.Id()]; !ok { client := buildClient(creds[0].(*auth.Token)) - ge.identityClient[cred.UserId()] = client + ge.identityClient[user.Id()] = client } } @@ -462,11 +490,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..d2cfb1f996c3662014637a30ec1fc28fde2a441f 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -176,7 +176,7 @@ func TestPushPull(t *testing.T) { return deleteRepository(projectName, envUser, envToken) }) - token := auth.NewToken(author.Id(), envToken, target) + token := auth.NewToken(envToken, target) err = auth.Store(repo, token) require.NoError(t, err) diff --git a/bridge/github/import.go b/bridge/github/import.go index aac4f119fcb9a080afb2fd256dda627e0ad94bd1..f2c9a53df8696e91821b10436d64f314380893bc 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -543,7 +543,6 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca i, err = repo.NewIdentityRaw( name, email, - string(actor.Login), string(actor.AvatarUrl), map[string]string{ metaKeyGithubLogin: string(actor.Login), @@ -590,7 +589,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/launchpad/import.go b/bridge/launchpad/import.go index 619631b3257493d3e2875ff23270f95fad171612..ecbf74f806c9085460bb87b84ab5c9c128738879 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -38,7 +38,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/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 e2129f8452fc810cfdd2374568d5815010670af3..eb5ee18305a74fba1c235eeccdc2c703d8085980 100644 --- a/cache/identity_cache.go +++ b/cache/identity_cache.go @@ -21,7 +21,7 @@ func (i *IdentityCache) notifyUpdated() error { return i.repoCache.identityUpdated(i.Identity.Id()) } -func (i *IdentityCache) Mutate(f func(identity.IdentityMutator) identity.IdentityMutator) error { +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 99afeb4179f1b1027f573f9518428954233061a9..18be9b5a59651aa6d0bff69a62cc6de32659b1d9 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -789,12 +789,6 @@ func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) ( }) } -func (c *RepoCache) ResolveIdentityLogin(login string) (*IdentityCache, error) { - return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool { - return excerpt.Login == login - }) -} - func (c *RepoCache) ResolveIdentityMatcher(f func(*IdentityExcerpt) bool) (*IdentityCache, error) { // preallocate but empty matching := make([]entity.Id, 0, 5) @@ -869,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/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go index 215603cbd0e1891f7c8f36805a744bc71219e4f1..ee170eccf3b81c31269e3bcb3d80342da8144b6d 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 @@ -2070,6 +2062,7 @@ type BugConnection { An edge in a connection. """ type BugEdge { +<<<<<<< HEAD """ A cursor for use in pagination. """ @@ -2078,6 +2071,105 @@ type BugEdge { The item at the end of the edge. """ node: Bug! +======= + """A cursor for use in pagination.""" + cursor: String! + """The item at the end of the edge.""" + node: Bug! +} +`}, + &ast.Source{Name: "schema/identity.graphql", Input: `"""Represents an identity""" +type Identity { + """The identifier for this identity""" + id: String! + """The human version (truncated) identifier for this identity""" + humanId: String! + """The name of the person, if known.""" + name: String + """The email of the person, if known.""" + email: String + """A non-empty string to display, representing the identity, based on the non-empty values.""" + displayName: String! + """An url to an avatar""" + avatarUrl: String + """isProtected is true if the chain of git commits started to be signed. + If that's the case, only signed commit with a valid key for this identity can be added.""" + isProtected: Boolean! +} + +type IdentityConnection { + edges: [IdentityEdge!]! + nodes: [Identity!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type IdentityEdge { + cursor: String! + node: Identity! +}`}, + &ast.Source{Name: "schema/label.graphql", Input: `"""Label for a bug.""" +type Label { + """The name of the label.""" + name: String! + """Color of the label.""" + color: Color! +} + +type LabelConnection { + edges: [LabelEdge!]! + nodes: [Label!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type LabelEdge { + cursor: String! + node: Label! +}`}, + &ast.Source{Name: "schema/mutations.graphql", Input: `input NewBugInput { + """A unique identifier for the client performing the mutation.""" + clientMutationId: String + """"The name of the repository. If not set, the default repository is used.""" + repoRef: String + """The title of the new bug.""" + title: String! + """The first message of the new bug.""" + message: String! + """The collection of file's hash required for the first message.""" + files: [Hash!] +} + +type NewBugPayload { + """A unique identifier for the client performing the mutation.""" + clientMutationId: String + """The created bug.""" + bug: Bug! + """The resulting operation.""" + operation: CreateOperation! +} + +input AddCommentInput { + """A unique identifier for the client performing the mutation.""" + clientMutationId: String + """"The name of the repository. If not set, the default repository is used.""" + repoRef: String + """The bug ID's prefix.""" + prefix: String! + """The first message of the new bug.""" + message: String! + """The collection of file's hash required for the first message.""" + files: [Hash!] +} + +type AddCommentPayload { + """A unique identifier for the client performing the mutation.""" + clientMutationId: String + """The affected bug.""" + bug: Bug! + """The resulting operation.""" + operation: AddCommentOperation! +>>>>>>> more more wip } input ChangeLabelInput { """ @@ -6215,6 +6307,7 @@ func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.C return ec.marshalOString2string(ctx, field.Selections, res) } +<<<<<<< HEAD func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj identity.Interface) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6247,6 +6340,10 @@ func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.C } func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj identity.Interface) (ret graphql.Marshaler) { +======= +func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) (ret graphql.Marshaler) { + ctx = ec.Tracer.StartFieldExecution(ctx, field) +>>>>>>> more more wip defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -11945,9 +12042,22 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, case "name": out.Values[i] = ec._Identity_name(ctx, field, obj) case "email": +<<<<<<< HEAD out.Values[i] = ec._Identity_email(ctx, field, obj) case "login": out.Values[i] = ec._Identity_login(ctx, field, obj) +======= + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Identity_email(ctx, field, obj) + return res + }) +>>>>>>> more more wip 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..2d1e909bc0456d9a5f09ac0363fe0ae3516afb2b 100644 --- a/graphql/resolvers/identity.go +++ b/graphql/resolvers/identity.go @@ -14,7 +14,3 @@ type identityResolver struct{} func (identityResolver) ID(ctx context.Context, obj identity.Interface) (string, error) { return obj.Id().String(), nil } - -func (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 26ecdf030083246b57f4b751e3778931e3532395..a02ec790bb29e1825eb237b96f2a69a8c8050ec5 100644 --- a/identity/bare.go +++ b/identity/bare.go @@ -112,13 +112,13 @@ func (i *Bare) AvatarUrl() string { } // 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 diff --git a/identity/identity.go b/identity/identity.go index 655afd316cde4159223b82b5ba9c93c8f50886d5..c33a8818b3fd2ea1e189f26c1e11ed7f906d18fd 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -275,7 +275,7 @@ type Mutator struct { Name string Email string AvatarUrl string - Keys []Key + Keys []*Key } // Mutate allow to create a new version of the Identity @@ -507,13 +507,13 @@ func (i *Identity) AvatarUrl() string { } // 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 { @@ -550,11 +550,11 @@ func (i *Identity) LastModification() timestamp.Timestamp { } // SetMetadata store arbitrary metadata along the last not-commit Version. -// If the Version has been commit to git already, a new version is added and will need to be +// 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) } @@ -588,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 3407d7ab6c1ab8fc1e761441c61f12e7ac65e57e..d138362dfb8aa1afbae7d2102cf8ca539147dd21 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -21,10 +21,10 @@ type Interface interface { 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 85195049b7771127eb19a41434ff084d1bf3b23f..f9c7b262956060204749933957c14fbe6b1fc089 100644 --- a/identity/version.go +++ b/identity/version.go @@ -30,7 +30,7 @@ type Version struct { // 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,24 +53,22 @@ type VersionJSON struct { Name string `json:"name,omitempty"` Email string `json:"email,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)), - metadata: make(map[string]string), + keys: make([]*Key, len(v.keys)), } - for i, op := range opp.Operations { - clone.Operations[i] = op + for i, key := range v.keys { + clone.keys[i] = key.Clone() } return clone 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", From f515b9a1291ddd3e4fe1b43bf5891ab19569e33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 4 Feb 2020 00:25:27 +0100 Subject: [PATCH 09/16] gitlab also compile --- bridge/github/config.go | 2 +- bridge/github/export.go | 5 +- bridge/github/export_test.go | 3 + bridge/github/import_test.go | 5 ++ bridge/gitlab/config.go | 159 +++++++++++++++-------------------- bridge/gitlab/export.go | 24 ++++-- bridge/gitlab/export_test.go | 5 +- bridge/gitlab/import.go | 17 +--- bridge/gitlab/import_test.go | 11 ++- bridge/launchpad/config.go | 26 +----- 10 files changed, 113 insertions(+), 144 deletions(-) diff --git a/bridge/github/config.go b/bridge/github/config.go index 9ede72d495ca73efcc83923a28399a7341f52f23..ed32f3986aba74639e8028114a24a13986fafe40 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -316,7 +316,7 @@ 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()) } diff --git a/bridge/github/export.go b/bridge/github/export.go index 663361f59f589d3b44cd18daff4c259c2e10fd9f..1e27be4a9b48bf4dc5a1fc707f02d2710440147f 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -99,7 +99,7 @@ func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error { for _, cred := range creds { login, ok := cred.GetMetadata(auth.MetaKeyLogin) if !ok { - _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with Github login\n", cred.ID().Human()) + _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Github login\n", cred.ID().Human()) continue } @@ -107,6 +107,9 @@ func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error { if err == identity.ErrIdentityNotExist { continue } + if err != nil { + return nil + } if _, ok := ge.identityClient[user.Id()]; !ok { client := buildClient(creds[0].(*auth.Token)) diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index d2cfb1f996c3662014637a30ec1fc28fde2a441f..cb6bacc1ab1052e23b345ce1a7c7e715af612cce 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -144,8 +144,10 @@ 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 = backend.SetUserIdentity(author) require.NoError(t, err) @@ -177,6 +179,7 @@ func TestPushPull(t *testing.T) { }) token := auth.NewToken(envToken, target) + token.SetMetadata(metaKeyGithubLogin, login) err = auth.Store(repo, token) require.NoError(t, err) diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 75310ab3abd6771c3a3a8fdc8781dbfe7ec8efb4..73eaa0967e957d6f535b15cf0cb1666389773104 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,7 +141,11 @@ func Test_Importer(t *testing.T) { t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") } + login := "test-identity" + author.SetMetadata(metaKeyGithubLogin, login) + token := auth.NewToken(envToken, target) + token.SetMetadata(metaKeyGithubLogin, login) err = auth.Store(repo, token) require.NoError(t, err) diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go index 0758074c298d507e88864ec442a9b9516a3fbdde..36daaa68d2ffb7662dd3f3438fcede1a1ab7d50d 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,64 +142,41 @@ 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 - } - - line = strings.TrimSpace(line) + index, err := input.PromptChoice("Gitlab base url", []string{ + "https://gitlab.com", + "enter your own base url", + }) - 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.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { @@ -219,11 +185,7 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, baseUrl st // 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,44 @@ 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') + validator := func(name string, value string) (complaint string, err error) { + if !re.MatchString(value) { + return "token has incorrect format", nil + } + login, err = getLoginFromToken(baseUrl, auth.NewToken(value, target)) if err != nil { - return "", err + return fmt.Sprintf("token is invalid: %v", err), nil } + return "", nil + } - token := strings.TrimSpace(line) - if re.MatchString(token) { - return token, nil - } + rawToken, err := input.Prompt("Enter token", "token", input.Required, validator) - fmt.Println("token has incorrect format") - } + token := auth.NewToken(rawToken, target) + token.SetMetadata(auth.MetaKeyLogin, login) + + return token, nil } func promptURL(repo repository.RepoCommon, baseUrl string) (string, error) { @@ -408,8 +370,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..50dbd04aac0216d9cd327bf0a40bbd662fede865 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -149,8 +149,10 @@ 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 = backend.SetUserIdentity(author) require.NoError(t, err) @@ -160,7 +162,8 @@ func TestPushPull(t *testing.T) { tests := testCases(t, backend) - 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/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..674aff008c4263218946f9020b70af89f22947bd 100644 --- a/bridge/launchpad/config.go +++ b/bridge/launchpad/config.go @@ -1,17 +1,15 @@ 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") @@ -45,7 +43,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 +84,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) From 646fd681ffba59d4a74ec3d99558d2ed70b0106b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 4 Feb 2020 22:05:34 +0100 Subject: [PATCH 10/16] it compiles \o/ --- bridge/bridges.go | 7 ++++ bridge/core/bridge.go | 17 +++++++++ bridge/core/interfaces.go | 5 +++ bridge/github/config.go | 9 ----- bridge/github/export.go | 2 +- bridge/github/export_test.go | 2 +- bridge/github/github.go | 21 ++++++++++ bridge/github/import.go | 6 --- bridge/github/import_test.go | 2 +- bridge/gitlab/gitlab.go | 6 +++ bridge/launchpad/config.go | 7 ---- bridge/launchpad/import.go | 5 --- bridge/launchpad/launchpad.go | 19 +++++++++ commands/bridge_auth.go | 26 ++++--------- commands/bridge_auth_addtoken.go | 51 ++++++++++++++++++++++--- commands/bridge_auth_show.go | 32 ++++++---------- commands/user.go | 3 -- commands/user_adopt.go | 15 +------- commands/user_create.go | 8 ++-- doc/man/git-bug-bridge-auth-add-token.1 | 8 ++++ doc/md/git-bug_bridge_auth_add-token.md | 2 + misc/bash_completion/git-bug | 8 ++++ misc/powershell_completion/git-bug | 4 ++ misc/zsh_completion/git-bug | 4 +- 24 files changed, 175 insertions(+), 94 deletions(-) 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/bridge.go b/bridge/core/bridge.go index 7891763fd95c6290aeb72137519a8bbcaa303f2b..ac0d47d74b840ff0670730b0ac5bf6ffa2db2a0d 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -28,6 +28,7 @@ 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. @@ -59,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 @@ -81,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/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 ed32f3986aba74639e8028114a24a13986fafe40..9477801d394058c439423f545a5d848b4056bee3 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -27,15 +27,6 @@ import ( "github.com/MichaelMure/git-bug/util/colors" ) -const ( - target = "github" - githubV3Url = "https://api.github.com" - keyOwner = "owner" - keyProject = "project" - - defaultTimeout = 60 * time.Second -) - var ( ErrBadProjectURL = errors.New("bad project url") ) diff --git a/bridge/github/export.go b/bridge/github/export.go index 1e27be4a9b48bf4dc5a1fc707f02d2710440147f..c363e188fac9a7fdeb562879b7cdd545d5286e97 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -76,7 +76,7 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e } login := user.ImmutableMetadata()[metaKeyGithubLogin] - creds, err := auth.List(repo, auth.WithMeta(metaKeyGithubLogin, login), auth.WithTarget(target), auth.WithKind(auth.KindToken)) + creds, err := auth.List(repo, auth.WithMeta(auth.MetaKeyLogin, login), auth.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { return err } diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index cb6bacc1ab1052e23b345ce1a7c7e715af612cce..89040d8f2e5f4463c5cff7da150a97e437c7d305 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -179,7 +179,7 @@ func TestPushPull(t *testing.T) { }) token := auth.NewToken(envToken, target) - token.SetMetadata(metaKeyGithubLogin, login) + token.SetMetadata(auth.MetaKeyLogin, login) err = auth.Store(repo, token) require.NoError(t, err) 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 f2c9a53df8696e91821b10436d64f314380893bc..6a4c0110c6146c821780590057f01e465baeea40 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -15,12 +15,6 @@ import ( "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 diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 73eaa0967e957d6f535b15cf0cb1666389773104..a8f8e3468037b755fa8a9a18faea63a5d9fc7357 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -145,7 +145,7 @@ func Test_Importer(t *testing.T) { author.SetMetadata(metaKeyGithubLogin, login) token := auth.NewToken(envToken, target) - token.SetMetadata(metaKeyGithubLogin, login) + token.SetMetadata(auth.MetaKeyLogin, login) err = auth.Store(repo, token) require.NoError(t, err) 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/launchpad/config.go b/bridge/launchpad/config.go index 674aff008c4263218946f9020b70af89f22947bd..e029fad3907a34844e8a0fc9a2df4104baf5a9db 100644 --- a/bridge/launchpad/config.go +++ b/bridge/launchpad/config.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "regexp" - "time" "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/cache" @@ -14,12 +13,6 @@ import ( 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") diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index ecbf74f806c9085460bb87b84ab5c9c128738879..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) 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/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 dab8a879426b5aeb4c4b2e92ee9e8bab07c645fc..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.Prompt("Name", "name", preName, input.Required) + 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.Prompt("Email", "email", preEmail, input.Required) + email, err := input.PromptDefault("Email", "email", preEmail, input.Required) if err != nil { return err } - login, err := input.Prompt("Avatar URL", "avatar", "") + 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/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 { From 20ca2bc0984f089a45286002a429d51746b0febb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 8 Feb 2020 17:22:45 +0100 Subject: [PATCH 11/16] fix merge --- go.mod | 2 +- go.sum | 2 + graphql/graph/gen_graph.go | 157 +--------------------------------- graphql/resolvers/identity.go | 5 ++ 4 files changed, 9 insertions(+), 157 deletions(-) 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 ee170eccf3b81c31269e3bcb3d80342da8144b6d..5e882142548ea63ae20a52115715158bbab83aea 100644 --- a/graphql/graph/gen_graph.go +++ b/graphql/graph/gen_graph.go @@ -2062,7 +2062,6 @@ type BugConnection { An edge in a connection. """ type BugEdge { -<<<<<<< HEAD """ A cursor for use in pagination. """ @@ -2071,105 +2070,6 @@ type BugEdge { The item at the end of the edge. """ node: Bug! -======= - """A cursor for use in pagination.""" - cursor: String! - """The item at the end of the edge.""" - node: Bug! -} -`}, - &ast.Source{Name: "schema/identity.graphql", Input: `"""Represents an identity""" -type Identity { - """The identifier for this identity""" - id: String! - """The human version (truncated) identifier for this identity""" - humanId: String! - """The name of the person, if known.""" - name: String - """The email of the person, if known.""" - email: String - """A non-empty string to display, representing the identity, based on the non-empty values.""" - displayName: String! - """An url to an avatar""" - avatarUrl: String - """isProtected is true if the chain of git commits started to be signed. - If that's the case, only signed commit with a valid key for this identity can be added.""" - isProtected: Boolean! -} - -type IdentityConnection { - edges: [IdentityEdge!]! - nodes: [Identity!]! - pageInfo: PageInfo! - totalCount: Int! -} - -type IdentityEdge { - cursor: String! - node: Identity! -}`}, - &ast.Source{Name: "schema/label.graphql", Input: `"""Label for a bug.""" -type Label { - """The name of the label.""" - name: String! - """Color of the label.""" - color: Color! -} - -type LabelConnection { - edges: [LabelEdge!]! - nodes: [Label!]! - pageInfo: PageInfo! - totalCount: Int! -} - -type LabelEdge { - cursor: String! - node: Label! -}`}, - &ast.Source{Name: "schema/mutations.graphql", Input: `input NewBugInput { - """A unique identifier for the client performing the mutation.""" - clientMutationId: String - """"The name of the repository. If not set, the default repository is used.""" - repoRef: String - """The title of the new bug.""" - title: String! - """The first message of the new bug.""" - message: String! - """The collection of file's hash required for the first message.""" - files: [Hash!] -} - -type NewBugPayload { - """A unique identifier for the client performing the mutation.""" - clientMutationId: String - """The created bug.""" - bug: Bug! - """The resulting operation.""" - operation: CreateOperation! -} - -input AddCommentInput { - """A unique identifier for the client performing the mutation.""" - clientMutationId: String - """"The name of the repository. If not set, the default repository is used.""" - repoRef: String - """The bug ID's prefix.""" - prefix: String! - """The first message of the new bug.""" - message: String! - """The collection of file's hash required for the first message.""" - files: [Hash!] -} - -type AddCommentPayload { - """A unique identifier for the client performing the mutation.""" - clientMutationId: String - """The affected bug.""" - bug: Bug! - """The resulting operation.""" - operation: AddCommentOperation! ->>>>>>> more more wip } input ChangeLabelInput { """ @@ -2411,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! """ @@ -6307,43 +6203,7 @@ func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.C return ec.marshalOString2string(ctx, field.Selections, res) } -<<<<<<< HEAD -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) { -======= -func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) (ret graphql.Marshaler) { - ctx = ec.Tracer.StartFieldExecution(ctx, field) ->>>>>>> more more wip defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -12042,22 +11902,7 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, case "name": out.Values[i] = ec._Identity_name(ctx, field, obj) case "email": -<<<<<<< HEAD out.Values[i] = ec._Identity_email(ctx, field, obj) - case "login": - out.Values[i] = ec._Identity_login(ctx, field, obj) -======= - field := field - out.Concurrently(i, func() (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Identity_email(ctx, field, obj) - return res - }) ->>>>>>> more more wip 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 2d1e909bc0456d9a5f09ac0363fe0ae3516afb2b..d36669d0a3abd5a615517f2ce060dba7c7eb878f 100644 --- a/graphql/resolvers/identity.go +++ b/graphql/resolvers/identity.go @@ -14,3 +14,8 @@ type identityResolver struct{} func (identityResolver) ID(ctx context.Context, obj identity.Interface) (string, error) { return obj.Id().String(), nil } + +func (r identityResolver) HumanID(ctx context.Context, obj identity.Interface) (string, error) { + return obj.Id().Human(), nil + +} From 2e7ac569ad3146e5fa22907d8a4e63d2a524533b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 8 Feb 2020 17:35:35 +0100 Subject: [PATCH 12/16] fix tests ? --- bridge/github/export_test.go | 12 ++++++------ bridge/gitlab/export_test.go | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 89040d8f2e5f4463c5cff7da150a97e437c7d305..36d43173718a74aef12fc8faba374bd6370fa498 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -155,6 +155,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 @@ -178,11 +183,6 @@ func TestPushPull(t *testing.T) { return deleteRepository(projectName, envUser, envToken) }) - token := auth.NewToken(envToken, target) - token.SetMetadata(auth.MetaKeyLogin, login) - err = auth.Store(repo, token) - require.NoError(t, err) - // initialize exporter exporter := &githubExporter{} err = exporter.Init(backend, core.Configuration{ @@ -258,7 +258,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/gitlab/export_test.go b/bridge/gitlab/export_test.go index 50dbd04aac0216d9cd327bf0a40bbd662fede865..8410ebba370dfe215eb39146d9ba2404c6727f92 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -160,13 +160,13 @@ func TestPushPull(t *testing.T) { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - tests := testCases(t, backend) - token := auth.NewToken(envToken, target) - token.SetMetadata(metaKeyGitlabLogin, login) + token.SetMetadata(auth.MetaKeyLogin, login) err = auth.Store(repo, token) require.NoError(t, err) + tests := testCases(t, backend) + // generate project name projectName := generateRepoName() @@ -263,7 +263,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) From 8773929f963b847841fac0d3c42d6278de69b79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 8 Feb 2020 22:04:25 +0100 Subject: [PATCH 13/16] github: make sure to have a name --- bridge/github/import.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bridge/github/import.go b/bridge/github/import.go index 6a4c0110c6146c821780590057f01e465baeea40..ea0ccba32b9dc17fdbc1658c944af3fc2fd73e84 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -534,6 +534,11 @@ 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, From a335725cc5f712f3f3089154afa96881284d4853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 8 Feb 2020 22:04:51 +0100 Subject: [PATCH 14/16] bridge: fix wrong error used --- bridge/core/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bridge/core/config.go b/bridge/core/config.go index adee5f08e9b8ea145d79ebc24346913903ea7534..8d306ef679045079c2786fe9407e49001ce931dc 100644 --- a/bridge/core/config.go +++ b/bridge/core/config.go @@ -19,7 +19,7 @@ func FinishConfig(repo *cache.RepoCache, metaKey string, login string) error { // if a default user exist, tag it with the login user, err := repo.GetUserIdentity() - if err != nil && err != identity.ErrIdentityNotExist { + if err != nil && err != identity.ErrNoIdentitySet { // real error return err } From bef35d4c679de81a214c99a30916fa7ebfc3f6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 8 Feb 2020 22:05:13 +0100 Subject: [PATCH 15/16] bridge: hopefully fix tests --- bridge/github/export_test.go | 2 ++ bridge/gitlab/export_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 36d43173718a74aef12fc8faba374bd6370fa498..7d6e6fb191b7123f3bacf0be9a9d39410021d010 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -148,6 +148,8 @@ func TestPushPull(t *testing.T) { 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) diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go index 8410ebba370dfe215eb39146d9ba2404c6727f92..1d387655c1067f8275ff8362b2f6d07e997393c1 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -153,6 +153,8 @@ func TestPushPull(t *testing.T) { 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) From 9b1aaa032d36e1ac05504916e359f767d1622d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 8 Feb 2020 22:08:35 +0100 Subject: [PATCH 16/16] bridge: fix 2 uncatched errors --- bridge/core/config.go | 3 +++ bridge/gitlab/config.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/bridge/core/config.go b/bridge/core/config.go index 8d306ef679045079c2786fe9407e49001ce931dc..afcda56058d62a4193abd30ccc03ad7c3dee7b48 100644 --- a/bridge/core/config.go +++ b/bridge/core/config.go @@ -33,6 +33,9 @@ func FinishConfig(repo *cache.RepoCache, metaKey string, login string) error { i, err := repo.NewIdentityFromGitUserRaw(map[string]string{ metaKey: login, }) + if err != nil { + return err + } err = repo.SetUserIdentity(i) if err != nil { diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go index 36daaa68d2ffb7662dd3f3438fcede1a1ab7d50d..fb59381904310d6bcfb7eac10ca0d83e39537b36 100644 --- a/bridge/gitlab/config.go +++ b/bridge/gitlab/config.go @@ -256,6 +256,9 @@ func promptToken(baseUrl string) (*auth.Token, error) { } 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)