diff options
Diffstat (limited to 'bridge')
-rw-r--r-- | bridge/bridges.go | 4 | ||||
-rw-r--r-- | bridge/core/auth/credential.go | 232 | ||||
-rw-r--r-- | bridge/core/auth/credential_test.go | 109 | ||||
-rw-r--r-- | bridge/core/auth/options.go | 62 | ||||
-rw-r--r-- | bridge/core/auth/token.go | 95 | ||||
-rw-r--r-- | bridge/core/bridge.go | 32 | ||||
-rw-r--r-- | bridge/core/interfaces.go | 7 | ||||
-rw-r--r-- | bridge/core/token.go | 296 | ||||
-rw-r--r-- | bridge/github/config.go | 135 | ||||
-rw-r--r-- | bridge/github/config_test.go | 24 | ||||
-rw-r--r-- | bridge/github/export.go | 108 | ||||
-rw-r--r-- | bridge/github/export_test.go | 43 | ||||
-rw-r--r-- | bridge/github/github.go | 5 | ||||
-rw-r--r-- | bridge/github/import.go | 38 | ||||
-rw-r--r-- | bridge/github/import_test.go | 15 | ||||
-rw-r--r-- | bridge/github/iterator.go | 4 | ||||
-rw-r--r-- | bridge/gitlab/config.go | 138 | ||||
-rw-r--r-- | bridge/gitlab/export.go | 72 | ||||
-rw-r--r-- | bridge/gitlab/export_test.go | 23 | ||||
-rw-r--r-- | bridge/gitlab/gitlab.go | 6 | ||||
-rw-r--r-- | bridge/gitlab/import.go | 36 | ||||
-rw-r--r-- | bridge/gitlab/import_test.go | 15 | ||||
-rw-r--r-- | bridge/gitlab/iterator.go | 4 | ||||
-rw-r--r-- | bridge/launchpad/config.go | 39 | ||||
-rw-r--r-- | bridge/launchpad/import.go | 4 |
25 files changed, 913 insertions, 633 deletions
diff --git a/bridge/bridges.go b/bridge/bridges.go index 9bbf3941..a306fe5d 100644 --- a/bridge/bridges.go +++ b/bridge/bridges.go @@ -39,11 +39,11 @@ func DefaultBridge(repo *cache.RepoCache) (*core.Bridge, error) { // ConfiguredBridges return the list of bridge that are configured for the given // repo -func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) { +func ConfiguredBridges(repo repository.RepoConfig) ([]string, error) { return core.ConfiguredBridges(repo) } // Remove a configured bridge -func RemoveBridge(repo repository.RepoCommon, name string) error { +func RemoveBridge(repo repository.RepoConfig, name string) error { return core.RemoveBridge(repo, name) } diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go new file mode 100644 index 00000000..a462a116 --- /dev/null +++ b/bridge/core/auth/credential.go @@ -0,0 +1,232 @@ +package auth + +import ( + "errors" + "fmt" + "regexp" + "strings" + "time" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" +) + +const ( + configKeyPrefix = "git-bug.auth" + configKeyKind = "kind" + configKeyUserId = "userid" + configKeyTarget = "target" + configKeyCreateTime = "createtime" +) + +type CredentialKind string + +const ( + KindToken CredentialKind = "token" + KindLoginPassword CredentialKind = "login-password" +) + +var ErrCredentialNotExist = errors.New("credential doesn't exist") + +func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch { + return entity.NewErrMultipleMatch("credential", matching) +} + +type Credential interface { + ID() entity.Id + UserId() entity.Id + Target() string + Kind() CredentialKind + CreateTime() time.Time + Validate() error + + // 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. + ToConfig() map[string]string +} + +// Load loads a credential from the repo config +func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) { + keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id) + + // read token config pairs + rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix) + if err != nil { + // Not exactly right due to the limitation of ReadAll() + return nil, ErrCredentialNotExist + } + + return loadFromConfig(rawconfigs, id) +} + +// LoadWithPrefix load a credential from the repo config with a prefix +func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) { + creds, err := List(repo) + if err != nil { + return nil, err + } + + // preallocate but empty + matching := make([]Credential, 0, 5) + + for _, cred := range creds { + if cred.ID().HasPrefix(prefix) { + matching = append(matching, cred) + } + } + + if len(matching) > 1 { + ids := make([]entity.Id, len(matching)) + for i, cred := range matching { + ids[i] = cred.ID() + } + return nil, NewErrMultipleMatchCredential(ids) + } + + if len(matching) == 0 { + return nil, ErrCredentialNotExist + } + + return matching[0], nil +} + +func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) { + keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id) + + // trim key prefix + configs := make(map[string]string) + for key, value := range rawConfigs { + newKey := strings.TrimPrefix(key, keyPrefix) + configs[newKey] = value + } + + var cred Credential + + switch CredentialKind(configs[configKeyKind]) { + case KindToken: + cred = NewTokenFromConfig(configs) + case KindLoginPassword: + default: + return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind]) + } + + return cred, nil +} + +// List load all existing credentials +func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) { + rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".") + if err != nil { + return nil, err + } + + re, err := regexp.Compile(configKeyPrefix + `.([^.]+).([^.]+)`) + if err != nil { + panic(err) + } + + mapped := make(map[string]map[string]string) + + for key, val := range rawConfigs { + res := re.FindStringSubmatch(key) + if res == nil { + continue + } + if mapped[res[1]] == nil { + mapped[res[1]] = make(map[string]string) + } + mapped[res[1]][res[2]] = val + } + + matcher := matcher(opts) + + var credentials []Credential + for id, kvs := range mapped { + cred, err := loadFromConfig(kvs, entity.Id(id)) + if err != nil { + return nil, err + } + if matcher.Match(cred) { + credentials = append(credentials, cred) + } + } + + return credentials, nil +} + +// IdExist return whether a credential id exist or not +func IdExist(repo repository.RepoConfig, id entity.Id) bool { + _, err := LoadWithId(repo, id) + return err == nil +} + +// PrefixExist return whether a credential id prefix exist or not +func PrefixExist(repo repository.RepoConfig, prefix string) bool { + _, err := LoadWithPrefix(repo, prefix) + return err == nil +} + +// Store stores a credential in the global git config +func Store(repo repository.RepoConfig, cred Credential) error { + confs := cred.ToConfig() + + prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID()) + + // Kind + err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind())) + if err != nil { + return err + } + + // 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 { + return err + } + + // CreateTime + err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime()) + if err != nil { + return err + } + + // Custom + for key, val := range confs { + err := repo.GlobalConfig().StoreString(prefix+key, val) + if err != nil { + return err + } + } + + return nil +} + +// Remove removes a credential from the global git config +func Remove(repo repository.RepoConfig, id entity.Id) error { + keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id) + return repo.GlobalConfig().RemoveAll(keyPrefix) +} + +/* + * Sorting + */ + +type ById []Credential + +func (b ById) Len() int { + return len(b) +} + +func (b ById) Less(i, j int) bool { + return b[i].ID() < b[j].ID() +} + +func (b ById) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} diff --git a/bridge/core/auth/credential_test.go b/bridge/core/auth/credential_test.go new file mode 100644 index 00000000..f91d273d --- /dev/null +++ b/bridge/core/auth/credential_test.go @@ -0,0 +1,109 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "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) + require.NoError(t, err) + return token + } + + token := storeToken(user1, "foobar", "github") + + // Store + Load + err = Store(repo, token) + assert.NoError(t, err) + + token2, err := LoadWithId(repo, token.ID()) + assert.NoError(t, err) + assert.Equal(t, token.createTime.Unix(), token2.CreateTime().Unix()) + token.createTime = token2.CreateTime() + assert.Equal(t, token, token2) + + prefix := string(token.ID())[:10] + + // LoadWithPrefix + token3, err := LoadWithPrefix(repo, prefix) + assert.NoError(t, err) + assert.Equal(t, token.createTime.Unix(), token3.CreateTime().Unix()) + token.createTime = token3.CreateTime() + assert.Equal(t, token, token3) + + token4 := storeToken(user1, "foo", "gitlab") + token5 := storeToken(user2, "bar", "github") + + // List + options + creds, err := List(repo, WithTarget("github")) + assert.NoError(t, err) + sameIds(t, creds, []Credential{token, token5}) + + creds, err = List(repo, WithTarget("gitlab")) + 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}) + + creds, err = List(repo, WithKind(KindLoginPassword)) + assert.NoError(t, err) + sameIds(t, creds, []Credential{}) + + // Exist + exist := IdExist(repo, token.ID()) + assert.True(t, exist) + + exist = PrefixExist(repo, prefix) + assert.True(t, exist) + + // Remove + err = Remove(repo, token.ID()) + assert.NoError(t, err) + + creds, err = List(repo) + assert.NoError(t, err) + sameIds(t, creds, []Credential{token4, token5}) +} + +func sameIds(t *testing.T, a []Credential, b []Credential) { + t.Helper() + + ids := func(creds []Credential) []entity.Id { + result := make([]entity.Id, len(creds)) + for i, cred := range creds { + result[i] = cred.ID() + } + return result + } + + assert.ElementsMatch(t, ids(a), ids(b)) +} diff --git a/bridge/core/auth/options.go b/bridge/core/auth/options.go new file mode 100644 index 00000000..7bcda68e --- /dev/null +++ b/bridge/core/auth/options.go @@ -0,0 +1,62 @@ +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 +} + +type Option func(opts *options) + +func matcher(opts []Option) *options { + result := &options{} + for _, opt := range opts { + opt(result) + } + return result +} + +func (opts *options) Match(cred Credential) bool { + if opts.target != "" && cred.Target() != opts.target { + return false + } + + if opts.userId != "" && cred.UserId() != opts.userId { + return false + } + + if opts.kind != "" && cred.Kind() != opts.kind { + return false + } + + return true +} + +func WithTarget(target string) Option { + return func(opts *options) { + opts.target = target + } +} + +func WithUser(user identity.Interface) Option { + return func(opts *options) { + opts.userId = user.Id() + } +} + +func WithUserId(userId entity.Id) Option { + return func(opts *options) { + opts.userId = userId + } +} + +func WithKind(kind CredentialKind) Option { + return func(opts *options) { + opts.kind = kind + } +} diff --git a/bridge/core/auth/token.go b/bridge/core/auth/token.go new file mode 100644 index 00000000..12a3bfc0 --- /dev/null +++ b/bridge/core/auth/token.go @@ -0,0 +1,95 @@ +package auth + +import ( + "crypto/sha256" + "fmt" + "time" + + "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" +) + +const ( + tokenValueKey = "value" +) + +var _ Credential = &Token{} + +// Token holds an API access token data +type Token struct { + userId entity.Id + target string + createTime time.Time + Value string +} + +// NewToken instantiate a new token +func NewToken(userId entity.Id, value, target string) *Token { + return &Token{ + userId: userId, + target: target, + createTime: time.Now(), + Value: value, + } +} + +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 { + token.createTime = t + } + } + + token.Value = conf[tokenValueKey] + + return token +} + +func (t *Token) ID() entity.Id { + sum := sha256.Sum256([]byte(t.target + t.Value)) + return entity.Id(fmt.Sprintf("%x", sum)) +} + +func (t *Token) UserId() entity.Id { + return t.userId +} + +func (t *Token) Target() string { + return t.target +} + +func (t *Token) Kind() CredentialKind { + return KindToken +} + +func (t *Token) CreateTime() time.Time { + return t.createTime +} + +// Validate ensure token important fields are valid +func (t *Token) Validate() error { + if t.Value == "" { + return fmt.Errorf("missing value") + } + if t.target == "" { + return fmt.Errorf("missing target") + } + if t.createTime.IsZero() || t.createTime.Equal(time.Time{}) { + return fmt.Errorf("missing creation time") + } + if !core.TargetExist(t.target) { + return fmt.Errorf("unknown target") + } + return nil +} + +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 1cad10e9..9a46e7b1 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -13,7 +13,6 @@ import ( "github.com/pkg/errors" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -21,10 +20,9 @@ var ErrImportNotSupported = errors.New("import is not supported") var ErrExportNotSupported = errors.New("export is not supported") const ( - ConfigKeyTarget = "target" - ConfigKeyToken = "token" - ConfigKeyTokenId = "token-id" - MetaKeyOrigin = "origin" + ConfigKeyTarget = "target" + + MetaKeyOrigin = "origin" bridgeConfigKeyPrefix = "git-bug.bridge" ) @@ -37,9 +35,8 @@ type BridgeParams struct { Owner string Project string URL string - Token string - TokenId string - TokenStdin bool + CredPrefix string + TokenRaw string } // Bridge is a wrapper around a BridgeImpl that will bind low-level @@ -143,7 +140,7 @@ func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) { // ConfiguredBridges return the list of bridge that are configured for the given // repo -func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) { +func ConfiguredBridges(repo repository.RepoConfig) ([]string, error) { configs, err := repo.LocalConfig().ReadAll(bridgeConfigKeyPrefix + ".") if err != nil { return nil, errors.Wrap(err, "can't read configured bridges") @@ -178,7 +175,7 @@ func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) { } // Check if a bridge exist -func BridgeExist(repo repository.RepoCommon, name string) bool { +func BridgeExist(repo repository.RepoConfig, name string) bool { keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name) conf, err := repo.LocalConfig().ReadAll(keyPrefix) @@ -187,7 +184,7 @@ func BridgeExist(repo repository.RepoCommon, name string) bool { } // Remove a configured bridge -func RemoveBridge(repo repository.RepoCommon, name string) error { +func RemoveBridge(repo repository.RepoConfig, name string) error { re, err := regexp.Compile(`^[a-zA-Z0-9]+`) if err != nil { panic(err) @@ -242,7 +239,7 @@ func (b *Bridge) ensureConfig() error { return nil } -func loadConfig(repo repository.RepoCommon, name string) (Configuration, error) { +func loadConfig(repo repository.RepoConfig, name string) (Configuration, error) { keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name) pairs, err := repo.LocalConfig().ReadAll(keyPrefix) @@ -280,16 +277,9 @@ func (b *Bridge) ensureInit() error { return nil } - token, err := LoadToken(b.repo, entity.Id(b.conf[ConfigKeyTokenId])) - if err != nil { - return err - } - - b.conf[ConfigKeyToken] = token.Value - importer := b.getImporter() if importer != nil { - err := importer.Init(b.conf) + err := importer.Init(b.repo, b.conf) if err != nil { return err } @@ -297,7 +287,7 @@ func (b *Bridge) ensureInit() error { exporter := b.getExporter() if exporter != nil { - err := exporter.Init(b.conf) + err := exporter.Init(b.repo, b.conf) if err != nil { return err } diff --git a/bridge/core/interfaces.go b/bridge/core/interfaces.go index 047f3880..77e0a7b9 100644 --- a/bridge/core/interfaces.go +++ b/bridge/core/interfaces.go @@ -5,7 +5,6 @@ import ( "time" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/repository" ) type Configuration map[string]string @@ -16,7 +15,7 @@ type BridgeImpl interface { // Configure handle the user interaction and return a key/value configuration // for future use - Configure(repo repository.RepoCommon, params BridgeParams) (Configuration, error) + Configure(repo *cache.RepoCache, params BridgeParams) (Configuration, error) // ValidateConfig check the configuration for error ValidateConfig(conf Configuration) error @@ -29,11 +28,11 @@ type BridgeImpl interface { } type Importer interface { - Init(conf Configuration) error + Init(repo *cache.RepoCache, conf Configuration) error ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan ImportResult, error) } type Exporter interface { - Init(conf Configuration) error + Init(repo *cache.RepoCache, conf Configuration) error ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan ExportResult, error) } diff --git a/bridge/core/token.go b/bridge/core/token.go deleted file mode 100644 index 28c64f5c..00000000 --- a/bridge/core/token.go +++ /dev/null @@ -1,296 +0,0 @@ -package core - -import ( - "crypto/sha256" - "errors" - "fmt" - "regexp" - "sort" - "strings" - "time" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" -) - -const ( - tokenConfigKeyPrefix = "git-bug.token" - tokenValueKey = "value" - tokenTargetKey = "target" - tokenCreateTimeKey = "createtime" -) - -var ErrTokenNotExist = errors.New("token doesn't exist") - -func NewErrMultipleMatchToken(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("token", matching) -} - -// Token holds an API access token data -type Token struct { - Value string - Target string - CreateTime time.Time -} - -// NewToken instantiate a new token -func NewToken(value, target string) *Token { - return &Token{ - Value: value, - Target: target, - CreateTime: time.Now(), - } -} - -func (t *Token) ID() entity.Id { - sum := sha256.Sum256([]byte(t.Target + t.Value)) - return entity.Id(fmt.Sprintf("%x", sum)) -} - -// Validate ensure token important fields are valid -func (t *Token) Validate() error { - if t.Value == "" { - return fmt.Errorf("missing value") - } - if t.Target == "" { - return fmt.Errorf("missing target") - } - if t.CreateTime.IsZero() || t.CreateTime.Equal(time.Time{}) { - return fmt.Errorf("missing creation time") - } - if !TargetExist(t.Target) { - return fmt.Errorf("unknown target") - } - return nil -} - -// LoadToken loads a token from the repo config -func LoadToken(repo repository.RepoCommon, id entity.Id) (*Token, error) { - keyPrefix := fmt.Sprintf("git-bug.token.%s.", id) - - // read token config pairs - rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix) - if err != nil { - // Not exactly right due to the limitation of ReadAll() - return nil, ErrTokenNotExist - } - - // trim key prefix - configs := make(map[string]string) - for key, value := range rawconfigs { - newKey := strings.TrimPrefix(key, keyPrefix) - configs[newKey] = value - } - - token := &Token{} - - token.Value = configs[tokenValueKey] - token.Target = configs[tokenTargetKey] - if createTime, ok := configs[tokenCreateTimeKey]; ok { - if t, err := repository.ParseTimestamp(createTime); err == nil { - token.CreateTime = t - } - } - - return token, nil -} - -// LoadTokenPrefix load a token from the repo config with a prefix -func LoadTokenPrefix(repo repository.RepoCommon, prefix string) (*Token, error) { - tokens, err := ListTokens(repo) - if err != nil { - return nil, err - } - - // preallocate but empty - matching := make([]entity.Id, 0, 5) - - for _, id := range tokens { - if id.HasPrefix(prefix) { - matching = append(matching, id) - } - } - - if len(matching) > 1 { - return nil, NewErrMultipleMatchToken(matching) - } - - if len(matching) == 0 { - return nil, ErrTokenNotExist - } - - return LoadToken(repo, matching[0]) -} - -// ListTokens list all existing token ids -func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) { - configs, err := repo.GlobalConfig().ReadAll(tokenConfigKeyPrefix + ".") - if err != nil { - return nil, err - } - - re, err := regexp.Compile(tokenConfigKeyPrefix + `.([^.]+)`) - if err != nil { - panic(err) - } - - set := make(map[string]interface{}) - - for key := range configs { - res := re.FindStringSubmatch(key) - - if res == nil { - continue - } - - set[res[1]] = nil - } - - result := make([]entity.Id, 0, len(set)) - for key := range set { - result = append(result, entity.Id(key)) - } - - sort.Sort(entity.Alphabetical(result)) - - return result, nil -} - -// ListTokensWithTarget list all token ids associated with the target -func ListTokensWithTarget(repo repository.RepoCommon, target string) ([]entity.Id, error) { - var ids []entity.Id - tokensIds, err := ListTokens(repo) - if err != nil { - return nil, err - } - - for _, tokenId := range tokensIds { - token, err := LoadToken(repo, tokenId) - if err != nil { - return nil, err - } - - if token.Target == target { - ids = append(ids, tokenId) - } - } - return ids, nil -} - -// LoadTokens load all existing tokens -func LoadTokens(repo repository.RepoCommon) ([]*Token, error) { - tokensIds, err := ListTokens(repo) - if err != nil { - return nil, err - } - - var tokens []*Token - for _, id := range tokensIds { - token, err := LoadToken(repo, id) - if err != nil { - return nil, err - } - tokens = append(tokens, token) - } - return tokens, nil -} - -// LoadTokensWithTarget load all existing tokens for a given target -func LoadTokensWithTarget(repo repository.RepoCommon, target string) ([]*Token, error) { - tokensIds, err := ListTokens(repo) - if err != nil { - return nil, err - } - - var tokens []*Token - for _, id := range tokensIds { - token, err := LoadToken(repo, id) - if err != nil { - return nil, err - } - if token.Target == target { - tokens = append(tokens, token) - } - } - return tokens, nil -} - -// TokenIdExist return wether token id exist or not -func TokenIdExist(repo repository.RepoCommon, id entity.Id) bool { - _, err := LoadToken(repo, id) - return err == nil -} - -// TokenExist return wether there is a token with a certain value or not -func TokenExist(repo repository.RepoCommon, value string) bool { - tokens, err := LoadTokens(repo) - if err != nil { - return false - } - for _, token := range tokens { - if token.Value == value { - return true - } - } - return false -} - -// TokenExistWithTarget same as TokenExist but restrict search for a given target -func TokenExistWithTarget(repo repository.RepoCommon, value string, target string) bool { - tokens, err := LoadTokensWithTarget(repo, target) - if err != nil { - return false - } - for _, token := range tokens { - if token.Value == value { - return true - } - } - return false -} - -// StoreToken stores a token in the repo config -func StoreToken(repo repository.RepoCommon, token *Token) error { - storeValueKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenValueKey) - err := repo.GlobalConfig().StoreString(storeValueKey, token.Value) - if err != nil { - return err - } - - storeTargetKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenTargetKey) - err = repo.GlobalConfig().StoreString(storeTargetKey, token.Target) - if err != nil { - return err - } - - createTimeKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenCreateTimeKey) - return repo.GlobalConfig().StoreTimestamp(createTimeKey, token.CreateTime) -} - -// RemoveToken removes a token from the repo config -func RemoveToken(repo repository.RepoCommon, id entity.Id) error { - keyPrefix := fmt.Sprintf("git-bug.token.%s", id) - return repo.GlobalConfig().RemoveAll(keyPrefix) -} - -// LoadOrCreateToken will try to load a token matching the same value or create it -func LoadOrCreateToken(repo repository.RepoCommon, target, tokenValue string) (*Token, error) { - tokens, err := LoadTokensWithTarget(repo, target) - if err != nil { - return nil, err - } - - for _, token := range tokens { - if token.Value == tokenValue { - return token, nil - } - } - - token := NewToken(tokenValue, target) - err = StoreToken(repo, token) - if err != nil { - return nil, err - } - - return token, nil -} diff --git a/bridge/github/config.go b/bridge/github/config.go index 9d059b00..a74e7f6b 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -22,6 +22,8 @@ import ( "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/repository" "github.com/MichaelMure/git-bug/util/colors" @@ -33,7 +35,6 @@ const ( githubV3Url = "https://api.github.com" keyOwner = "owner" keyProject = "project" - keyToken = "token" defaultTimeout = 60 * time.Second ) @@ -42,40 +43,33 @@ var ( ErrBadProjectURL = errors.New("bad project url") ) -func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) { +func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) { conf := make(core.Configuration) var err error - if (params.Token != "" || params.TokenId != "" || params.TokenStdin) && + 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 + // getting owner and project name switch { case params.Owner != "" && params.Project != "": // first try to use params if both or project and owner are provided owner = params.Owner project = params.Project - case params.URL != "": // try to parse params URL and extract owner and project owner, project, err = splitURL(params.URL) if err != nil { return nil, err } - default: - // remote suggestions - remotes, err := repo.GetRemotes() - if err != nil { - return nil, err - } - // terminal prompt - owner, project, err = promptURL(remotes) + owner, project, err = promptURL(repo) if err != nil { return nil, err } @@ -90,49 +84,38 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) return nil, fmt.Errorf("invalid parameter owner: %v", owner) } - var token string - var tokenId entity.Id - var tokenObj *core.Token - - // try to get token from params if provided, else use terminal prompt - // to either enter a token or login and generate a new one, or choose - // an existing token - if params.Token != "" { - token = params.Token - } else if params.TokenStdin { - reader := bufio.NewReader(os.Stdin) - token, err = reader.ReadString('\n') - if err != nil { - return nil, fmt.Errorf("reading from stdin: %v", err) - } - token = strings.TrimSpace(token) - } else if params.TokenId != "" { - tokenId = entity.Id(params.TokenId) - } else { - tokenObj, err = promptTokenOptions(repo, owner, project) - if err != nil { - return nil, err - } + user, err := repo.GetUserIdentity() + if err != nil { + return nil, err } - // at this point, we check if the token already exist or we create a new one - if token != "" { - tokenObj, err = core.LoadOrCreateToken(repo, target, token) + var cred auth.Credential + + switch { + case params.CredPrefix != "": + cred, err = auth.LoadWithPrefix(repo, params.CredPrefix) if err != nil { return nil, err } - } else if tokenId != "" { - tokenObj, err = core.LoadToken(repo, tokenId) + if cred.UserId() != user.Id() { + return nil, fmt.Errorf("selected credential don't match the user") + } + case params.TokenRaw != "": + cred = auth.NewToken(user.Id(), params.TokenRaw, target) + default: + cred, err = promptTokenOptions(repo, user.Id(), owner, project) if err != nil { return nil, err } - if tokenObj.Target != target { - return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target) - } + } + + token, ok := cred.(*auth.Token) + if !ok { + return nil, fmt.Errorf("the Github bridge only handle token credentials") } // verify access to the repository with token - ok, err = validateProject(owner, project, tokenObj.Value) + ok, err = validateProject(owner, project, token) if err != nil { return nil, err } @@ -141,7 +124,6 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) } conf[core.ConfigKeyTarget] = target - conf[core.ConfigKeyTokenId] = tokenObj.ID().String() conf[keyOwner] = owner conf[keyProject] = project @@ -150,6 +132,14 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) return nil, err } + // don't forget to store the now known valid token + if !auth.IdExist(repo, cred.ID()) { + err = auth.Store(repo, cred) + if err != nil { + return nil, err + } + } + return conf, nil } @@ -160,10 +150,6 @@ func (*Github) ValidateConfig(conf core.Configuration) error { return fmt.Errorf("unexpected target name: %v", v) } - if _, ok := conf[core.ConfigKeyTokenId]; !ok { - return fmt.Errorf("missing %s key", core.ConfigKeyTokenId) - } - if _, ok := conf[keyOwner]; !ok { return fmt.Errorf("missing %s key", keyOwner) } @@ -245,9 +231,9 @@ func randomFingerprint() string { return string(b) } -func promptTokenOptions(repo repository.RepoCommon, owner, project string) (*core.Token, error) { +func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, project string) (auth.Credential, error) { for { - tokens, err := core.LoadTokensWithTarget(repo, target) + creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target)) if err != nil { return nil, err } @@ -256,18 +242,19 @@ func promptTokenOptions(repo repository.RepoCommon, owner, project string) (*cor fmt.Println("[1]: enter my token") fmt.Println("[2]: interactive token creation") - if len(tokens) > 0 { + if len(creds) > 0 { + sort.Sort(auth.ById(creds)) + fmt.Println() fmt.Println("Existing tokens for Github:") - for i, token := range tokens { - if token.Target == target { - fmt.Printf("[%d]: %s => %s (%s)\n", - i+3, - colors.Cyan(token.ID().Human()), - text.TruncateMax(token.Value, 10), - token.CreateTime.Format(time.RFC822), - ) - } + for i, cred := range creds { + token := cred.(*auth.Token) + fmt.Printf("[%d]: %s => %s (%s)\n", + i+3, + colors.Cyan(token.ID().Human()), + colors.Red(text.TruncateMax(token.Value, 10)), + token.CreateTime().Format(time.RFC822), + ) } } @@ -281,30 +268,28 @@ func promptTokenOptions(repo repository.RepoCommon, owner, project string) (*cor } line = strings.TrimSpace(line) - index, err := strconv.Atoi(line) - if err != nil || index < 1 || index > len(tokens)+2 { + if err != nil || index < 1 || index > len(creds)+2 { fmt.Println("invalid input") continue } - var token string switch index { case 1: - token, err = promptToken() + value, err := promptToken() if err != nil { return nil, err } + return auth.NewToken(userId, value, target), nil case 2: - token, err = loginAndRequestToken(owner, project) + value, err := loginAndRequestToken(owner, project) if err != nil { return nil, err } + return auth.NewToken(userId, value, target), nil default: - return tokens[index-3], nil + return creds[index-3], nil } - - return core.LoadOrCreateToken(repo, target, token) } } @@ -435,7 +420,13 @@ func promptUsername() (string, error) { } } -func promptURL(remotes map[string]string) (string, string, error) { +func promptURL(repo repository.RepoCommon) (string, string, error) { + // remote suggestions + remotes, err := repo.GetRemotes() + if err != nil { + return "", "", err + } + validRemotes := getValidGithubRemoteURLs(remotes) if len(validRemotes) > 0 { for { @@ -556,7 +547,7 @@ func validateUsername(username string) (bool, error) { return resp.StatusCode == http.StatusOK, nil } -func validateProject(owner, project, token string) (bool, error) { +func validateProject(owner, project string, token *auth.Token) (bool, error) { url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project) req, err := http.NewRequest("GET", url, nil) @@ -565,7 +556,7 @@ func validateProject(owner, project, token string) (bool, error) { } // need the token for private repositories - req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) + req.Header.Set("Authorization", fmt.Sprintf("token %s", token.Value)) client := &http.Client{ Timeout: defaultTimeout, diff --git a/bridge/github/config_test.go b/bridge/github/config_test.go index 4feeaa74..9798d26b 100644 --- a/bridge/github/config_test.go +++ b/bridge/github/config_test.go @@ -5,6 +5,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/MichaelMure/git-bug/bridge/core/auth" + "github.com/MichaelMure/git-bug/entity" ) func TestSplitURL(t *testing.T) { @@ -142,20 +145,23 @@ func TestValidateUsername(t *testing.T) { } func TestValidateProject(t *testing.T) { - tokenPrivateScope := os.Getenv("GITHUB_TOKEN_PRIVATE") - if tokenPrivateScope == "" { + envPrivate := os.Getenv("GITHUB_TOKEN_PRIVATE") + if envPrivate == "" { t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") } - tokenPublicScope := os.Getenv("GITHUB_TOKEN_PUBLIC") - if tokenPublicScope == "" { + envPublic := os.Getenv("GITHUB_TOKEN_PUBLIC") + if envPublic == "" { t.Skip("Env var GITHUB_TOKEN_PUBLIC missing") } + tokenPrivate := auth.NewToken(entity.UnsetId, envPrivate, target) + tokenPublic := auth.NewToken(entity.UnsetId, envPublic, target) + type args struct { owner string project string - token string + token *auth.Token } tests := []struct { name string @@ -167,7 +173,7 @@ func TestValidateProject(t *testing.T) { args: args{ project: "git-bug", owner: "MichaelMure", - token: tokenPublicScope, + token: tokenPublic, }, want: true, }, @@ -176,7 +182,7 @@ func TestValidateProject(t *testing.T) { args: args{ project: "git-bug-test-github-bridge", owner: "MichaelMure", - token: tokenPrivateScope, + token: tokenPrivate, }, want: true, }, @@ -185,7 +191,7 @@ func TestValidateProject(t *testing.T) { args: args{ project: "git-bug-test-github-bridge", owner: "MichaelMure", - token: tokenPublicScope, + token: tokenPublic, }, want: false, }, @@ -194,7 +200,7 @@ func TestValidateProject(t *testing.T) { args: args{ project: "cant-find-this", owner: "organisation-not-found", - token: tokenPublicScope, + token: tokenPublic, }, want: false, }, diff --git a/bridge/github/export.go b/bridge/github/export.go index 8d515802..6c089a47 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -15,9 +15,11 @@ import ( "golang.org/x/sync/errgroup" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" ) var ( @@ -31,8 +33,12 @@ type githubExporter struct { // cache identities clients identityClient map[entity.Id]*githubv4.Client - // map identities with their tokens - identityToken map[entity.Id]string + // 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 @@ -46,68 +52,86 @@ type githubExporter struct { } // Init . -func (ge *githubExporter) Init(conf core.Configuration) error { +func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) error { ge.conf = conf - //TODO: initialize with multiple tokens - ge.identityToken = make(map[entity.Id]string) ge.identityClient = make(map[entity.Id]*githubv4.Client) 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)) + if err != nil { + return err + } + + if len(creds) == 0 { + return ErrMissingIdentityToken + } + + ge.defaultToken = creds[0].(*auth.Token) + return nil } -// getIdentityClient return a githubv4 API client configured with the access token of the given identity. -// if no client were found it will initialize it from the known tokens map and cache it for next use -func (ge *githubExporter) getIdentityClient(id entity.Id) (*githubv4.Client, error) { - client, ok := ge.identityClient[id] - if ok { - return client, nil +func (ge *githubExporter) cacheAllClient(repo repository.RepoConfig) error { + creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken)) + if err != nil { + return err } - // get token - token, ok := ge.identityToken[id] - if !ok { - return nil, ErrMissingIdentityToken + for _, cred := range creds { + if _, ok := ge.identityClient[cred.UserId()]; !ok { + client := buildClient(creds[0].(*auth.Token)) + ge.identityClient[cred.UserId()] = client + } } - // create client - client = buildClient(token) - // cache client - ge.identityClient[id] = client + return nil +} - return client, nil +// getClientForIdentity return a githubv4 API client configured with the access token of the given identity. +func (ge *githubExporter) getClientForIdentity(userId entity.Id) (*githubv4.Client, error) { + client, ok := ge.identityClient[userId] + if ok { + return client, nil + } + + return nil, ErrMissingIdentityToken } // ExportAll export all event made by the current user to Github func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) { out := make(chan core.ExportResult) - user, err := repo.GetUserIdentity() - if err != nil { - return nil, err - } - - ge.identityToken[user.Id()] = ge.conf[core.ConfigKeyToken] - + var err error // get repository node id ge.repositoryID, err = getRepositoryNodeID( ctx, + ge.defaultToken, ge.conf[keyOwner], ge.conf[keyProject], - ge.conf[core.ConfigKeyToken], ) - - if err != nil { - return nil, err - } - - client, err := ge.getIdentityClient(user.Id()) if err != nil { return nil, err } // query all labels - err = ge.cacheGithubLabels(ctx, client) + err = ge.cacheGithubLabels(ctx, ge.defaultClient) if err != nil { return nil, err } @@ -115,8 +139,8 @@ func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, go func() { defer close(out) - var allIdentitiesIds []entity.Id - for id := range ge.identityToken { + allIdentitiesIds := make([]entity.Id, 0, len(ge.identityClient)) + for id := range ge.identityClient { allIdentitiesIds = append(allIdentitiesIds, id) } @@ -209,7 +233,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc } else { // check that we have a token for operation author - client, err := ge.getIdentityClient(author.Id()) + client, err := ge.getClientForIdentity(author.Id()) if err != nil { // if bug is still not exported and we do not have the author stop the execution out <- core.NewExportNothing(b.Id(), fmt.Sprintf("missing author token")) @@ -262,7 +286,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc } opAuthor := op.GetAuthor() - client, err := ge.getIdentityClient(opAuthor.Id()) + client, err := ge.getClientForIdentity(opAuthor.Id()) if err != nil { continue } @@ -384,7 +408,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc } // getRepositoryNodeID request github api v3 to get repository node id -func getRepositoryNodeID(ctx context.Context, owner, project, token string) (string, error) { +func getRepositoryNodeID(ctx context.Context, token *auth.Token, owner, project string) (string, error) { url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project) client := &http.Client{} @@ -394,7 +418,7 @@ func getRepositoryNodeID(ctx context.Context, owner, project, token string) (str } // need the token for private repositories - req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) + req.Header.Set("Authorization", fmt.Sprintf("token %s", token.Value)) ctx, cancel := context.WithTimeout(ctx, defaultTimeout) defer cancel() @@ -512,7 +536,7 @@ func (ge *githubExporter) createGithubLabel(ctx context.Context, label, color st req = req.WithContext(ctx) // need the token for private repositories - req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[core.ConfigKeyToken])) + req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.defaultToken.Value)) resp, err := client.Do(req) if err != nil { diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index dba72f3f..5a0bc653 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/repository" @@ -30,7 +31,7 @@ type testCase struct { numOrOp int // number of original operations } -func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCache) []*testCase { +func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { // simple bug simpleBug, _, err := repo.NewBug("simple bug", "new bug") require.NoError(t, err) @@ -92,32 +93,32 @@ func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCach require.NoError(t, err) return []*testCase{ - &testCase{ + { name: "simple bug", bug: simpleBug, numOrOp: 1, }, - &testCase{ + { name: "bug with comments", bug: bugWithComments, numOrOp: 2, }, - &testCase{ + { name: "bug label change", bug: bugLabelChange, numOrOp: 6, }, - &testCase{ + { name: "bug with comment editions", bug: bugWithCommentEditions, numOrOp: 4, }, - &testCase{ + { name: "bug changed status", bug: bugStatusChanged, numOrOp: 3, }, - &testCase{ + { name: "bug title edited", bug: bugTitleEdited, numOrOp: 2, @@ -127,11 +128,11 @@ func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCach func TestPushPull(t *testing.T) { // repo owner - user := os.Getenv("GITHUB_TEST_USER") + envUser := os.Getenv("GITHUB_TEST_USER") // token must have 'repo' and 'delete_repo' scopes - token := os.Getenv("GITHUB_TOKEN_ADMIN") - if token == "" { + envToken := os.Getenv("GITHUB_TOKEN_ADMIN") + if envToken == "" { t.Skip("Env var GITHUB_TOKEN_ADMIN missing") } @@ -152,35 +153,38 @@ func TestPushPull(t *testing.T) { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - tests := testCases(t, backend, author) + tests := testCases(t, backend) // generate project name projectName := generateRepoName() // create target Github repository - err = createRepository(projectName, token) + err = createRepository(projectName, envToken) require.NoError(t, err) fmt.Println("created repository", projectName) // Make sure to remove the Github repository when the test end defer func(t *testing.T) { - if err := deleteRepository(projectName, user, token); err != nil { + if err := deleteRepository(projectName, envUser, envToken); err != nil { t.Fatal(err) } fmt.Println("deleted repository:", projectName) }(t) interrupt.RegisterCleaner(func() error { - return deleteRepository(projectName, user, token) + return deleteRepository(projectName, envUser, envToken) }) + token := auth.NewToken(author.Id(), envToken, target) + err = auth.Store(repo, token) + require.NoError(t, err) + // initialize exporter exporter := &githubExporter{} - err = exporter.Init(core.Configuration{ - keyOwner: user, + err = exporter.Init(backend, core.Configuration{ + keyOwner: envUser, keyProject: projectName, - keyToken: token, }) require.NoError(t, err) @@ -206,10 +210,9 @@ func TestPushPull(t *testing.T) { require.NoError(t, err) importer := &githubImporter{} - err = importer.Init(core.Configuration{ - keyOwner: user, + err = importer.Init(backend, core.Configuration{ + keyOwner: envUser, keyProject: projectName, - keyToken: token, }) require.NoError(t, err) diff --git a/bridge/github/github.go b/bridge/github/github.go index e4fb03dd..874c2d11 100644 --- a/bridge/github/github.go +++ b/bridge/github/github.go @@ -8,6 +8,7 @@ import ( "golang.org/x/oauth2" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" ) type Github struct{} @@ -24,9 +25,9 @@ func (*Github) NewExporter() core.Exporter { return &githubExporter{} } -func buildClient(token string) *githubv4.Client { +func buildClient(token *auth.Token) *githubv4.Client { src := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, + &oauth2.Token{AccessToken: token.Value}, ) httpClient := oauth2.NewClient(context.TODO(), src) diff --git a/bridge/github/import.go b/bridge/github/import.go index 67ab9351..092e3e71 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -8,6 +8,7 @@ import ( "github.com/shurcooL/githubv4" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" @@ -24,6 +25,9 @@ const ( type githubImporter struct { conf core.Configuration + // default user client + client *githubv4.Client + // iterator iterator *iterator @@ -31,15 +35,37 @@ type githubImporter struct { out chan<- core.ImportResult } -func (gi *githubImporter) Init(conf core.Configuration) error { +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())) + } + + creds, err := auth.List(repo, opts...) + if err != nil { + return err + } + + if len(creds) == 0 { + return ErrMissingIdentityToken + } + + gi.client = buildClient(creds[0].(*auth.Token)) + return nil } // ImportAll iterate over all the configured repository issues and ensure the creation of the // missing issues / timeline items / edits / label events ... func (gi *githubImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) { - gi.iterator = NewIterator(ctx, 10, gi.conf[keyOwner], gi.conf[keyProject], gi.conf[core.ConfigKeyToken], since) + gi.iterator = NewIterator(ctx, gi.client, 10, gi.conf[keyOwner], gi.conf[keyProject], since) out := make(chan core.ImportResult) gi.out = out @@ -494,7 +520,7 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca if err == nil { return i, nil } - if _, ok := err.(entity.ErrMultipleMatch); ok { + if entity.IsErrMultipleMatch(err) { return nil, err } @@ -543,7 +569,7 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, if err == nil { return i, nil } - if _, ok := err.(entity.ErrMultipleMatch); ok { + if entity.IsErrMultipleMatch(err) { return nil, err } @@ -553,12 +579,10 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, "login": githubv4.String("ghost"), } - gc := buildClient(gi.conf[core.ConfigKeyToken]) - ctx, cancel := context.WithTimeout(gi.iterator.ctx, defaultTimeout) defer cancel() - err = gc.Query(ctx, &q, variables) + err = gi.client.Query(ctx, &q, variables) if err != nil { return nil, err } diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index f1558831..304229a0 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/identity" @@ -134,16 +135,22 @@ func Test_Importer(t *testing.T) { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - token := os.Getenv("GITHUB_TOKEN_PRIVATE") - if token == "" { + envToken := os.Getenv("GITHUB_TOKEN_PRIVATE") + if envToken == "" { t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") } + err = author.Commit(repo) + require.NoError(t, err) + + token := auth.NewToken(author.Id(), envToken, target) + err = auth.Store(repo, token) + require.NoError(t, err) + importer := &githubImporter{} - err = importer.Init(core.Configuration{ + err = importer.Init(backend, core.Configuration{ keyOwner: "MichaelMure", keyProject: "git-bug-test-github-bridge", - keyToken: token, }) require.NoError(t, err) diff --git a/bridge/github/iterator.go b/bridge/github/iterator.go index d1d7900f..40b00292 100644 --- a/bridge/github/iterator.go +++ b/bridge/github/iterator.go @@ -63,9 +63,9 @@ type iterator struct { } // NewIterator create and initialize a new iterator -func NewIterator(ctx context.Context, capacity int, owner, project, token string, since time.Time) *iterator { +func NewIterator(ctx context.Context, client *githubv4.Client, capacity int, owner, project string, since time.Time) *iterator { i := &iterator{ - gc: buildClient(token), + gc: client, since: since, capacity: capacity, ctx: ctx, diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go index 6b85e8cb..7bc2e577 100644 --- a/bridge/gitlab/config.go +++ b/bridge/gitlab/config.go @@ -6,6 +6,7 @@ import ( "net/url" "os" "regexp" + "sort" "strconv" "strings" "time" @@ -15,6 +16,8 @@ import ( "github.com/xanzy/go-gitlab" "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/repository" "github.com/MichaelMure/git-bug/util/colors" @@ -24,7 +27,7 @@ var ( ErrBadProjectURL = errors.New("bad project url") ) -func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) { +func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) { if params.Project != "" { fmt.Println("warning: --project is ineffective for a gitlab bridge") } @@ -34,82 +37,77 @@ func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) conf := make(core.Configuration) var err error - var url string - var token string - var tokenId entity.Id - var tokenObj *core.Token - if (params.Token != "" || params.TokenStdin) && params.URL == "" { + 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 url string + // get project url - if params.URL != "" { + switch { + case params.URL != "": url = params.URL - - } else { - // remote suggestions - remotes, err := repo.GetRemotes() - if err != nil { - return nil, errors.Wrap(err, "getting remotes") - } - + default: // terminal prompt - url, err = promptURL(remotes) + url, err = promptURL(repo) if err != nil { return nil, errors.Wrap(err, "url prompt") } } - // get user token - if params.Token != "" { - token = params.Token - } else if params.TokenStdin { - reader := bufio.NewReader(os.Stdin) - token, err = reader.ReadString('\n') - if err != nil { - return nil, fmt.Errorf("reading from stdin: %v", err) - } - token = strings.TrimSpace(token) - } else if params.TokenId != "" { - tokenId = entity.Id(params.TokenId) - } else { - tokenObj, err = promptTokenOptions(repo) - if err != nil { - return nil, errors.Wrap(err, "token prompt") - } + user, err := repo.GetUserIdentity() + if err != nil { + return nil, err } - if token != "" { - tokenObj, err = core.LoadOrCreateToken(repo, target, token) + var cred auth.Credential + + switch { + case params.CredPrefix != "": + cred, err = auth.LoadWithPrefix(repo, params.CredPrefix) if err != nil { return nil, err } - } else if tokenId != "" { - tokenObj, err = core.LoadToken(repo, entity.Id(tokenId)) + if cred.UserId() != user.Id() { + return nil, fmt.Errorf("selected credential don't match the user") + } + case params.TokenRaw != "": + cred = auth.NewToken(user.Id(), params.TokenRaw, target) + default: + cred, err = promptTokenOptions(repo, user.Id()) if err != nil { return nil, err } - if tokenObj.Target != target { - return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target) - } + } + + token, ok := cred.(*auth.Token) + if !ok { + return nil, fmt.Errorf("the Gitlab bridge only handle token credentials") } // validate project url and get its ID - id, err := validateProjectURL(url, tokenObj.Value) + id, err := validateProjectURL(url, token) if err != nil { return nil, errors.Wrap(err, "project validation") } - conf[keyProjectID] = strconv.Itoa(id) - conf[core.ConfigKeyTokenId] = tokenObj.ID().String() conf[core.ConfigKeyTarget] = target + conf[keyProjectID] = strconv.Itoa(id) err = g.ValidateConfig(conf) if err != nil { return nil, err } + // don't forget to store the now known valid token + if !auth.IdExist(repo, cred.ID()) { + err = auth.Store(repo, cred) + if err != nil { + return nil, err + } + } + return conf, nil } @@ -120,10 +118,6 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error { return fmt.Errorf("unexpected target name: %v", v) } - if _, ok := conf[keyToken]; !ok { - return fmt.Errorf("missing %s key", keyToken) - } - if _, ok := conf[keyProjectID]; !ok { return fmt.Errorf("missing %s key", keyProjectID) } @@ -131,19 +125,20 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error { return nil } -func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) { +func promptTokenOptions(repo repository.RepoConfig, userId entity.Id) (auth.Credential, error) { for { - tokens, err := core.LoadTokensWithTarget(repo, target) + creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target), auth.WithKind(auth.KindToken)) if err != nil { return nil, err } - if len(tokens) == 0 { - token, err := promptToken() + // if we don't have existing token, fast-track to the token prompt + if len(creds) == 0 { + value, err := promptToken() if err != nil { return nil, err } - return core.LoadOrCreateToken(repo, target, token) + return auth.NewToken(userId, value, target), nil } fmt.Println() @@ -151,15 +146,16 @@ func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) { fmt.Println() fmt.Println("Existing tokens for Gitlab:") - for i, token := range tokens { - if token.Target == target { - fmt.Printf("[%d]: %s => %s (%s)\n", - i+2, - colors.Cyan(token.ID().Human()), - text.TruncateMax(token.Value, 10), - token.CreateTime.Format(time.RFC822), - ) - } + + sort.Sort(auth.ById(creds)) + for i, cred := range creds { + token := cred.(*auth.Token) + fmt.Printf("[%d]: %s => %s (%s)\n", + i+2, + colors.Cyan(token.ID().Human()), + colors.Red(text.TruncateMax(token.Value, 10)), + token.CreateTime().Format(time.RFC822), + ) } fmt.Println() @@ -173,23 +169,21 @@ func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) { line = strings.TrimSpace(line) index, err := strconv.Atoi(line) - if err != nil || index < 1 || index > len(tokens)+1 { + if err != nil || index < 1 || index > len(creds)+1 { fmt.Println("invalid input") continue } - var token string switch index { case 1: - token, err = promptToken() + value, err := promptToken() if err != nil { return nil, err } + return auth.NewToken(userId, value, target), nil default: - return tokens[index-2], nil + return creds[index-2], nil } - - return core.LoadOrCreateToken(repo, target, token) } } @@ -222,7 +216,13 @@ func promptToken() (string, error) { } } -func promptURL(remotes map[string]string) (string, error) { +func promptURL(repo repository.RepoCommon) (string, error) { + // remote suggestions + remotes, err := repo.GetRemotes() + if err != nil { + return "", errors.Wrap(err, "getting remotes") + } + validRemotes := getValidGitlabRemoteURLs(remotes) if len(validRemotes) > 0 { for { @@ -302,7 +302,7 @@ func getValidGitlabRemoteURLs(remotes map[string]string) []string { return urls } -func validateProjectURL(url, token string) (int, error) { +func validateProjectURL(url string, token *auth.Token) (int, error) { projectPath, err := getProjectPath(url) if err != nil { return 0, err diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go index 092434a5..373cf637 100644 --- a/bridge/gitlab/export.go +++ b/bridge/gitlab/export.go @@ -10,9 +10,11 @@ import ( "github.com/xanzy/go-gitlab" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" ) var ( @@ -24,10 +26,7 @@ type gitlabExporter struct { conf core.Configuration // cache identities clients - identityClient map[string]*gitlab.Client - - // map identities with their tokens - identityToken map[string]string + identityClient map[entity.Id]*gitlab.Client // gitlab repository ID repositoryID string @@ -38,58 +37,59 @@ type gitlabExporter struct { } // Init . -func (ge *gitlabExporter) Init(conf core.Configuration) error { +func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) error { ge.conf = conf - //TODO: initialize with multiple tokens - ge.identityToken = make(map[string]string) - ge.identityClient = make(map[string]*gitlab.Client) + ge.identityClient = make(map[entity.Id]*gitlab.Client) ge.cachedOperationIDs = make(map[string]string) + // get repository node id + ge.repositoryID = ge.conf[keyProjectID] + + // preload all clients + err := ge.cacheAllClient(repo) + if err != nil { + return err + } + return nil } -// getIdentityClient return a gitlab v4 API client configured with the access token of the given identity. -// if no client were found it will initialize it from the known tokens map and cache it for next use -func (ge *gitlabExporter) getIdentityClient(id entity.Id) (*gitlab.Client, error) { - client, ok := ge.identityClient[id.String()] - if ok { - return client, nil +func (ge *gitlabExporter) cacheAllClient(repo repository.RepoConfig) error { + creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken)) + if err != nil { + return err } - // get token - token, ok := ge.identityToken[id.String()] - if !ok { - return nil, ErrMissingIdentityToken + for _, cred := range creds { + if _, ok := ge.identityClient[cred.UserId()]; !ok { + client := buildClient(creds[0].(*auth.Token)) + ge.identityClient[cred.UserId()] = client + } } - // create client - client = buildClient(token) - // cache client - ge.identityClient[id.String()] = client + return nil +} + +// getIdentityClient return a gitlab v4 API client configured with the access token of the given identity. +func (ge *gitlabExporter) getIdentityClient(userId entity.Id) (*gitlab.Client, error) { + client, ok := ge.identityClient[userId] + if ok { + return client, nil + } - return client, nil + return nil, ErrMissingIdentityToken } // ExportAll export all event made by the current user to Gitlab func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) { out := make(chan core.ExportResult) - user, err := repo.GetUserIdentity() - if err != nil { - return nil, err - } - - ge.identityToken[user.Id().String()] = ge.conf[core.ConfigKeyToken] - - // get repository node id - ge.repositoryID = ge.conf[keyProjectID] - go func() { defer close(out) - allIdentitiesIds := make([]entity.Id, 0, len(ge.identityToken)) - for id := range ge.identityToken { - allIdentitiesIds = append(allIdentitiesIds, entity.Id(id)) + allIdentitiesIds := make([]entity.Id, 0, len(ge.identityClient)) + for id := range ge.identityClient { + allIdentitiesIds = append(allIdentitiesIds, id) } allBugsIds := repo.AllBugsIds() diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go index 26b47bfb..645e2d76 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/repository" @@ -32,7 +33,7 @@ type testCase struct { numOpImp int // number of operations after import } -func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCache) []*testCase { +func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { // simple bug simpleBug, _, err := repo.NewBug("simple bug", "new bug") require.NoError(t, err) @@ -135,8 +136,8 @@ func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCach func TestPushPull(t *testing.T) { // token must have 'repo' and 'delete_repo' scopes - token := os.Getenv("GITLAB_API_TOKEN") - if token == "" { + envToken := os.Getenv("GITLAB_API_TOKEN") + if envToken == "" { t.Skip("Env var GITLAB_API_TOKEN missing") } @@ -157,7 +158,11 @@ func TestPushPull(t *testing.T) { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - tests := testCases(t, backend, author) + tests := testCases(t, backend) + + token := auth.NewToken(author.Id(), envToken, target) + err = auth.Store(repo, token) + require.NoError(t, err) // generate project name projectName := generateRepoName() @@ -182,9 +187,8 @@ func TestPushPull(t *testing.T) { // initialize exporter exporter := &gitlabExporter{} - err = exporter.Init(core.Configuration{ + err = exporter.Init(backend, core.Configuration{ keyProjectID: strconv.Itoa(projectID), - keyToken: token, }) require.NoError(t, err) @@ -210,9 +214,8 @@ func TestPushPull(t *testing.T) { require.NoError(t, err) importer := &gitlabImporter{} - err = importer.Init(core.Configuration{ + err = importer.Init(backend, core.Configuration{ keyProjectID: strconv.Itoa(projectID), - keyToken: token, }) require.NoError(t, err) @@ -276,7 +279,7 @@ func generateRepoName() string { } // create repository need a token with scope 'repo' -func createRepository(ctx context.Context, name, token string) (int, error) { +func createRepository(ctx context.Context, name string, token *auth.Token) (int, error) { client := buildClient(token) project, _, err := client.Projects.CreateProject( &gitlab.CreateProjectOptions{ @@ -292,7 +295,7 @@ func createRepository(ctx context.Context, name, token string) (int, error) { } // delete repository need a token with scope 'delete_repo' -func deleteRepository(ctx context.Context, project int, token string) error { +func deleteRepository(ctx context.Context, project int, token *auth.Token) error { client := buildClient(token) _, err := client.Projects.DeleteProject(project, gitlab.WithContext(ctx)) return err diff --git a/bridge/gitlab/gitlab.go b/bridge/gitlab/gitlab.go index d976d813..bcc50e4c 100644 --- a/bridge/gitlab/gitlab.go +++ b/bridge/gitlab/gitlab.go @@ -7,6 +7,7 @@ import ( "github.com/xanzy/go-gitlab" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" ) const ( @@ -18,7 +19,6 @@ const ( metaKeyGitlabProject = "gitlab-project-id" keyProjectID = "project-id" - keyToken = "token" defaultTimeout = 60 * time.Second ) @@ -37,10 +37,10 @@ func (*Gitlab) NewExporter() core.Exporter { return &gitlabExporter{} } -func buildClient(token string) *gitlab.Client { +func buildClient(token *auth.Token) *gitlab.Client { client := &http.Client{ Timeout: defaultTimeout, } - return gitlab.NewClient(client, token) + return gitlab.NewClient(client, token.Value) } diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index 4fcf8568..00dee252 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -9,6 +9,7 @@ import ( "github.com/xanzy/go-gitlab" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" @@ -19,6 +20,9 @@ import ( type gitlabImporter struct { conf core.Configuration + // default user client + client *gitlab.Client + // iterator iterator *iterator @@ -26,15 +30,37 @@ type gitlabImporter struct { out chan<- core.ImportResult } -func (gi *gitlabImporter) Init(conf core.Configuration) error { +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())) + } + + creds, err := auth.List(repo, opts...) + if err != nil { + return err + } + + if len(creds) == 0 { + return ErrMissingIdentityToken + } + + gi.client = buildClient(creds[0].(*auth.Token)) + return nil } // ImportAll iterate over all the configured repository issues (notes) and ensure the creation // of the missing issues / comments / label events / title changes ... func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) { - gi.iterator = NewIterator(ctx, 10, gi.conf[keyProjectID], gi.conf[core.ConfigKeyToken], since) + gi.iterator = NewIterator(ctx, gi.client, 10, gi.conf[keyProjectID], since) out := make(chan core.ImportResult) gi.out = out @@ -357,13 +383,11 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id if err == nil { return i, nil } - if _, ok := err.(entity.ErrMultipleMatch); ok { + if entity.IsErrMultipleMatch(err) { return nil, err } - client := buildClient(gi.conf["token"]) - - user, _, err := client.Users.GetUser(id) + user, _, err := gi.client.Users.GetUser(id) if err != nil { return nil, err } diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index 8e596349..1676bdf3 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/identity" @@ -83,8 +84,8 @@ func TestImport(t *testing.T) { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - token := os.Getenv("GITLAB_API_TOKEN") - if token == "" { + envToken := os.Getenv("GITLAB_API_TOKEN") + if envToken == "" { t.Skip("Env var GITLAB_API_TOKEN missing") } @@ -93,10 +94,16 @@ func TestImport(t *testing.T) { t.Skip("Env var GITLAB_PROJECT_ID missing") } + err = author.Commit(repo) + require.NoError(t, err) + + token := auth.NewToken(author.Id(), envToken, target) + err = auth.Store(repo, token) + require.NoError(t, err) + importer := &gitlabImporter{} - err = importer.Init(core.Configuration{ + err = importer.Init(backend, core.Configuration{ keyProjectID: projectID, - keyToken: token, }) require.NoError(t, err) diff --git a/bridge/gitlab/iterator.go b/bridge/gitlab/iterator.go index 902dc9f1..07f9cce9 100644 --- a/bridge/gitlab/iterator.go +++ b/bridge/gitlab/iterator.go @@ -71,9 +71,9 @@ type iterator struct { } // NewIterator create a new iterator -func NewIterator(ctx context.Context, capacity int, projectID, token string, since time.Time) *iterator { +func NewIterator(ctx context.Context, client *gitlab.Client, capacity int, projectID string, since time.Time) *iterator { return &iterator{ - gc: buildClient(token), + gc: client, project: projectID, since: since, capacity: capacity, diff --git a/bridge/launchpad/config.go b/bridge/launchpad/config.go index be81c0ac..8db39100 100644 --- a/bridge/launchpad/config.go +++ b/bridge/launchpad/config.go @@ -11,7 +11,7 @@ import ( "time" "github.com/MichaelMure/git-bug/bridge/core" - "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/cache" ) var ErrBadProjectURL = errors.New("bad Launchpad project URL") @@ -22,9 +22,9 @@ const ( defaultTimeout = 60 * time.Second ) -func (l *Launchpad) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) { - if params.Token != "" { - fmt.Println("warning: --token is ineffective for a Launchpad bridge") +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") } if params.Owner != "" { fmt.Println("warning: --owner is ineffective for a Launchpad bridge") @@ -34,22 +34,19 @@ func (l *Launchpad) Configure(repo repository.RepoCommon, params core.BridgePara var err error var project string - if params.Project != "" { + switch { + case params.Project != "": project = params.Project - - } else if params.URL != "" { + case params.URL != "": // get project name from url project, err = splitURL(params.URL) - if err != nil { - return nil, err - } - - } else { + default: // get project name from terminal prompt project, err = promptProjectName() - if err != nil { - return nil, err - } + } + + if err != nil { + return nil, err } // verify project @@ -61,8 +58,8 @@ func (l *Launchpad) Configure(repo repository.RepoCommon, params core.BridgePara return nil, fmt.Errorf("project doesn't exist") } - conf[keyProject] = project conf[core.ConfigKeyTarget] = target + conf[keyProject] = project err = l.ValidateConfig(conf) if err != nil { @@ -73,12 +70,14 @@ func (l *Launchpad) Configure(repo repository.RepoCommon, params core.BridgePara } func (*Launchpad) ValidateConfig(conf core.Configuration) error { - if _, ok := conf[keyProject]; !ok { - return fmt.Errorf("missing %s key", keyProject) + if v, ok := conf[core.ConfigKeyTarget]; !ok { + return fmt.Errorf("missing %s key", core.ConfigKeyTarget) + } else if v != target { + return fmt.Errorf("unexpected target name: %v", v) } - if _, ok := conf[core.ConfigKeyTarget]; !ok { - return fmt.Errorf("missing %s key", core.ConfigKeyTarget) + if _, ok := conf[keyProject]; !ok { + return fmt.Errorf("missing %s key", keyProject) } return nil diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index 59fc5c5f..619631b3 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -15,7 +15,7 @@ type launchpadImporter struct { conf core.Configuration } -func (li *launchpadImporter) Init(conf core.Configuration) error { +func (li *launchpadImporter) Init(repo *cache.RepoCache, conf core.Configuration) error { li.conf = conf return nil } @@ -31,7 +31,7 @@ func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson) if err == nil { return i, nil } - if _, ok := err.(entity.ErrMultipleMatch); ok { + if entity.IsErrMultipleMatch(err) { return nil, err } |