diff options
52 files changed, 1091 insertions, 769 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 } diff --git a/commands/add.go b/commands/add.go index ff4f9529..e656a262 100644 --- a/commands/add.go +++ b/commands/add.go @@ -57,7 +57,7 @@ func runAddBug(cmd *cobra.Command, args []string) error { var addCmd = &cobra.Command{ Use: "add", Short: "Create a new bug.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runAddBug, } diff --git a/commands/bridge_auth.go b/commands/bridge_auth.go index e7fce1bd..4e8b50c4 100644 --- a/commands/bridge_auth.go +++ b/commands/bridge_auth.go @@ -7,36 +7,56 @@ import ( text "github.com/MichaelMure/go-term-text" - "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/util/colors" + "github.com/MichaelMure/git-bug/util/interrupt" ) func runBridgeAuth(cmd *cobra.Command, args []string) error { - tokens, err := core.ListTokens(repo) + backend, err := cache.NewRepoCache(repo) if err != nil { return err } + defer backend.Close() + interrupt.RegisterCleaner(backend.Close) - for _, token := range tokens { - token, err := core.LoadToken(repo, token) + creds, err := auth.List(backend) + if err != nil { + return err + } + + defaultUser, _ := backend.GetUserIdentity() + + for _, cred := range creds { + targetFmt := text.LeftPadMaxLine(cred.Target(), 10, 0) + + var value string + switch cred := cred.(type) { + case *auth.Token: + value = cred.Value + } + + user, err := backend.ResolveIdentity(cred.UserId()) if err != nil { return err } - printToken(token) - } + userFmt := user.DisplayName() - return nil -} + if cred.UserId() == defaultUser.Id() { + userFmt = colors.Red(userFmt) + } -func printToken(token *core.Token) { - targetFmt := text.LeftPadMaxLine(token.Target, 10, 0) + fmt.Printf("%s %s %s %s %s\n", + colors.Cyan(cred.ID().Human()), + colors.Yellow(targetFmt), + colors.Magenta(cred.Kind()), + userFmt, + value, + ) + } - fmt.Printf("%s %s %s %s\n", - colors.Cyan(token.ID().Human()), - colors.Yellow(targetFmt), - colors.Magenta("token"), - token.Value, - ) + return nil } var bridgeAuthCmd = &cobra.Command{ diff --git a/commands/bridge_auth_add.go b/commands/bridge_auth_addtoken.go index ae2c4dbc..018015e4 100644 --- a/commands/bridge_auth_add.go +++ b/commands/bridge_auth_addtoken.go @@ -12,6 +12,8 @@ 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" ) var ( @@ -22,7 +24,7 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error { var value string if bridgeAuthAddTokenTarget == "" { - return fmt.Errorf("auth target is required") + return fmt.Errorf("flag --target is required") } if !core.TargetExist(bridgeAuthAddTokenTarget) { @@ -44,12 +46,17 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error { value = strings.TrimSuffix(raw, "\n") } - token := core.NewToken(value, bridgeAuthAddTokenTarget) + user, err := identity.GetUserIdentity(repo) + if err != nil { + return err + } + + token := auth.NewToken(user.Id(), value, bridgeAuthAddTokenTarget) if err := token.Validate(); err != nil { return errors.Wrap(err, "invalid token") } - err := core.StoreToken(repo, token) + err = auth.Store(repo, token) if err != nil { return err } @@ -61,7 +68,7 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error { var bridgeAuthAddTokenCmd = &cobra.Command{ Use: "add-token [<token>]", Short: "Store a new token", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runBridgeTokenAdd, Args: cobra.MaximumNArgs(1), } diff --git a/commands/bridge_auth_rm.go b/commands/bridge_auth_rm.go index b0b4d437..17e70625 100644 --- a/commands/bridge_auth_rm.go +++ b/commands/bridge_auth_rm.go @@ -5,21 +5,21 @@ import ( "github.com/spf13/cobra" - "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" ) func runBridgeAuthRm(cmd *cobra.Command, args []string) error { - token, err := core.LoadTokenPrefix(repo, args[0]) + cred, err := auth.LoadWithPrefix(repo, args[0]) if err != nil { return err } - err = core.RemoveToken(repo, token.ID()) + err = auth.Remove(repo, cred.ID()) if err != nil { return err } - fmt.Printf("token %s removed\n", token.ID()) + fmt.Printf("credential %s removed\n", cred.ID()) return nil } diff --git a/commands/bridge_auth_show.go b/commands/bridge_auth_show.go index 94141b93..5352957d 100644 --- a/commands/bridge_auth_show.go +++ b/commands/bridge_auth_show.go @@ -6,20 +6,24 @@ import ( "github.com/spf13/cobra" - "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bridge/core/auth" ) func runBridgeAuthShow(cmd *cobra.Command, args []string) error { - token, err := core.LoadTokenPrefix(repo, args[0]) + cred, err := auth.LoadWithPrefix(repo, args[0]) if err != nil { return err } - fmt.Printf("Id: %s\n", token.ID()) - fmt.Printf("Target: %s\n", token.Target) - fmt.Printf("Type: token\n") - fmt.Printf("Value: %s\n", token.Value) - fmt.Printf("Creation: %s\n", token.CreateTime.Format(time.RFC822)) + fmt.Printf("Id: %s\n", cred.ID()) + fmt.Printf("Target: %s\n", cred.Target()) + fmt.Printf("Kind: %s\n", cred.Kind()) + fmt.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822)) + + switch cred := cred.(type) { + case *auth.Token: + fmt.Printf("Value: %s\n", cred.Value) + } return nil } diff --git a/commands/bridge_configure.go b/commands/bridge_configure.go index 6c24568c..00634b28 100644 --- a/commands/bridge_configure.go +++ b/commands/bridge_configure.go @@ -11,6 +11,7 @@ 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/cache" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/interrupt" @@ -21,9 +22,11 @@ const ( ) var ( - bridgeConfigureName string - bridgeConfigureTarget string - bridgeParams core.BridgeParams + bridgeConfigureName string + bridgeConfigureTarget string + bridgeConfigureParams core.BridgeParams + bridgeConfigureToken string + bridgeConfigureTokenStdin bool ) func runBridgeConfigure(cmd *cobra.Command, args []string) error { @@ -34,9 +37,28 @@ func runBridgeConfigure(cmd *cobra.Command, args []string) error { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - if (bridgeParams.TokenStdin || bridgeParams.Token != "" || bridgeParams.TokenId != "") && + if (bridgeConfigureTokenStdin || bridgeConfigureToken != "" || bridgeConfigureParams.CredPrefix != "") && (bridgeConfigureName == "" || bridgeConfigureTarget == "") { - return fmt.Errorf("you must provide a bridge name and target to configure a bridge with a token") + return fmt.Errorf("you must provide a bridge name and target to configure a bridge with a credential") + } + + // early fail + if bridgeConfigureParams.CredPrefix != "" { + if _, err := auth.LoadWithPrefix(repo, bridgeConfigureParams.CredPrefix); err != nil { + return err + } + } + + switch { + case bridgeConfigureTokenStdin: + reader := bufio.NewReader(os.Stdin) + token, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("reading from stdin: %v", err) + } + bridgeConfigureParams.TokenRaw = strings.TrimSpace(token) + case bridgeConfigureToken != "": + bridgeConfigureParams.TokenRaw = bridgeConfigureToken } if bridgeConfigureTarget == "" { @@ -58,7 +80,7 @@ func runBridgeConfigure(cmd *cobra.Command, args []string) error { return err } - err = b.Configure(bridgeParams) + err = b.Configure(bridgeConfigureParams) if err != nil { return err } @@ -94,7 +116,7 @@ func promptTarget() (string, error) { } } -func promptName(repo repository.RepoCommon) (string, error) { +func promptName(repo repository.RepoConfig) (string, error) { defaultExist := core.BridgeExist(repo, defaultName) for { @@ -184,7 +206,7 @@ git bug bridge configure \ --target=github \ --url=https://github.com/michaelmure/git-bug \ --token=$(TOKEN)`, - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runBridgeConfigure, } @@ -193,11 +215,11 @@ func init() { bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureName, "name", "n", "", "A distinctive name to identify the bridge") bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureTarget, "target", "t", "", fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ","))) - bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.URL, "url", "u", "", "The URL of the target repository") - bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Owner, "owner", "o", "", "The owner of the target repository") - bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Token, "token", "T", "", "The authentication token for the API") - bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.TokenId, "token-id", "i", "", "The authentication token identifier for the API") - bridgeConfigureCmd.Flags().BoolVar(&bridgeParams.TokenStdin, "token-stdin", false, "Will read the token from stdin and ignore --token") - bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Project, "project", "p", "", "The name of the target repository") + bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.URL, "url", "u", "", "The URL of the target repository") + bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.Owner, "owner", "o", "", "The owner of the target repository") + bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.CredPrefix, "credential", "c", "", "The identifier or prefix of an already known credential for the API (see \"git-bug bridge auth\")") + bridgeConfigureCmd.Flags().StringVar(&bridgeConfigureToken, "token", "", "A raw authentication token for the API") + bridgeConfigureCmd.Flags().BoolVar(&bridgeConfigureTokenStdin, "token-stdin", false, "Will read the token from stdin and ignore --token") + bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.Project, "project", "p", "", "The name of the target repository") bridgeConfigureCmd.Flags().SortFlags = false } diff --git a/commands/bridge_pull.go b/commands/bridge_pull.go index 2dd3d93e..692ec5e9 100644 --- a/commands/bridge_pull.go +++ b/commands/bridge_pull.go @@ -138,7 +138,7 @@ func parseSince(since string) (time.Time, error) { var bridgePullCmd = &cobra.Command{ Use: "pull [<name>]", Short: "Pull updates.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runBridgePull, Args: cobra.MaximumNArgs(1), } diff --git a/commands/bridge_push.go b/commands/bridge_push.go index 95ad5f5e..52d23a97 100644 --- a/commands/bridge_push.go +++ b/commands/bridge_push.go @@ -90,7 +90,7 @@ func runBridgePush(cmd *cobra.Command, args []string) error { var bridgePushCmd = &cobra.Command{ Use: "push [<name>]", Short: "Push updates.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runBridgePush, Args: cobra.MaximumNArgs(1), } diff --git a/commands/comment_add.go b/commands/comment_add.go index 3e153009..dfd63e38 100644 --- a/commands/comment_add.go +++ b/commands/comment_add.go @@ -57,7 +57,7 @@ func runCommentAdd(cmd *cobra.Command, args []string) error { var commentAddCmd = &cobra.Command{ Use: "add [<id>]", Short: "Add a new comment to a bug.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runCommentAdd, } diff --git a/commands/label_add.go b/commands/label_add.go index 6e2679d9..39dfb085 100644 --- a/commands/label_add.go +++ b/commands/label_add.go @@ -38,7 +38,7 @@ func runLabelAdd(cmd *cobra.Command, args []string) error { var labelAddCmd = &cobra.Command{ Use: "add [<id>] <label>[...]", Short: "Add a label to a bug.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runLabelAdd, } diff --git a/commands/root.go b/commands/root.go index 387c6342..1a424d32 100644 --- a/commands/root.go +++ b/commands/root.go @@ -84,16 +84,10 @@ func loadRepoEnsureUser(cmd *cobra.Command, args []string) error { return err } - set, err := identity.IsUserIdentitySet(repo) + _, err = identity.GetUserIdentity(repo) if err != nil { return err } - if !set { - // Print the error directly to not confuse a user - _, _ = fmt.Fprintln(os.Stderr, identity.ErrNoIdentitySet.Error()) - os.Exit(-1) - } - return nil } diff --git a/commands/status_close.go b/commands/status_close.go index 94d05ddf..08c67e87 100644 --- a/commands/status_close.go +++ b/commands/status_close.go @@ -31,7 +31,7 @@ func runStatusClose(cmd *cobra.Command, args []string) error { var closeCmd = &cobra.Command{ Use: "close [<id>]", Short: "Mark a bug as closed.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runStatusClose, } diff --git a/commands/status_open.go b/commands/status_open.go index 9a2b76ab..1b1c426e 100644 --- a/commands/status_open.go +++ b/commands/status_open.go @@ -31,7 +31,7 @@ func runStatusOpen(cmd *cobra.Command, args []string) error { var openCmd = &cobra.Command{ Use: "open [<id>]", Short: "Mark a bug as open.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runStatusOpen, } diff --git a/commands/title_edit.go b/commands/title_edit.go index 385dbdc9..3e40bd9e 100644 --- a/commands/title_edit.go +++ b/commands/title_edit.go @@ -55,7 +55,7 @@ func runTitleEdit(cmd *cobra.Command, args []string) error { var titleEditCmd = &cobra.Command{ Use: "edit [<id>]", Short: "Edit a title of a bug.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runTitleEdit, } diff --git a/commands/user.go b/commands/user.go index 254abf2f..f669c73f 100644 --- a/commands/user.go +++ b/commands/user.go @@ -4,9 +4,10 @@ import ( "errors" "fmt" + "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/util/interrupt" - "github.com/spf13/cobra" ) var ( @@ -84,7 +85,7 @@ func runUser(cmd *cobra.Command, args []string) error { var userCmd = &cobra.Command{ Use: "user [<user-id>]", Short: "Display or change the user identity.", - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runUser, } diff --git a/commands/user_create.go b/commands/user_create.go index 037d79b2..88cc94de 100644 --- a/commands/user_create.go +++ b/commands/user_create.go @@ -18,10 +18,6 @@ func runUserCreate(cmd *cobra.Command, args []string) error { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - _, _ = fmt.Fprintf(os.Stderr, "Before creating a new identity, please be aware that "+ - "you can also use an already existing one using \"git bug user adopt\". As an example, "+ - "you can do that if your identity has already been created by an importer.\n\n") - preName, err := backend.GetUserName() if err != nil { return err diff --git a/commands/webui.go b/commands/webui.go index 8e735e55..2e1a1bc0 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -249,7 +249,7 @@ var webUICmd = &cobra.Command{ Available git config: git-bug.webui.open [bool]: control the automatic opening of the web UI in the default browser `, - PreRunE: loadRepo, + PreRunE: loadRepoEnsureUser, RunE: runWebUI, } diff --git a/doc/man/git-bug-bridge-configure.1 b/doc/man/git-bug-bridge-configure.1 index 6ccd44cb..14b773a6 100644 --- a/doc/man/git-bug-bridge-configure.1 +++ b/doc/man/git-bug-bridge-configure.1 @@ -44,12 +44,12 @@ Token configuration can be directly passed with the \-\-token flag or in the ter The owner of the target repository .PP -\fB\-T\fP, \fB\-\-token\fP="" - The authentication token for the API +\fB\-c\fP, \fB\-\-credential\fP="" + The identifier or prefix of an already known credential for the API (see "git\-bug bridge auth") .PP -\fB\-i\fP, \fB\-\-token\-id\fP="" - The authentication token identifier for the API +\fB\-\-token\fP="" + A raw authentication token for the API .PP \fB\-\-token\-stdin\fP[=false] diff --git a/doc/md/git-bug_bridge_configure.md b/doc/md/git-bug_bridge_configure.md index 66b72a94..73121072 100644 --- a/doc/md/git-bug_bridge_configure.md +++ b/doc/md/git-bug_bridge_configure.md @@ -70,15 +70,15 @@ git bug bridge configure \ ### Options ``` - -n, --name string A distinctive name to identify the bridge - -t, --target string The target of the bridge. Valid values are [github,gitlab,launchpad-preview] - -u, --url string The URL of the target repository - -o, --owner string The owner of the target repository - -T, --token string The authentication token for the API - -i, --token-id string The authentication token identifier for the API - --token-stdin Will read the token from stdin and ignore --token - -p, --project string The name of the target repository - -h, --help help for configure + -n, --name string A distinctive name to identify the bridge + -t, --target string The target of the bridge. Valid values are [github,gitlab,launchpad-preview] + -u, --url string The URL of the target repository + -o, --owner string The owner of the target repository + -c, --credential string The identifier or prefix of an already known credential for the API (see "git-bug bridge auth") + --token string A raw authentication token for the API + --token-stdin Will read the token from stdin and ignore --token + -p, --project string The name of the target repository + -h, --help help for configure ``` ### SEE ALSO diff --git a/entity/err.go b/entity/err.go index 7022305c..7d6c662e 100644 --- a/entity/err.go +++ b/entity/err.go @@ -25,3 +25,8 @@ func (e ErrMultipleMatch) Error() string { e.entityType, strings.Join(matching, "\n")) } + +func IsErrMultipleMatch(err error) bool { + _, ok := err.(*ErrMultipleMatch) + return ok +} diff --git a/identity/identity.go b/identity/identity.go index b7d44a4b..ed8e39e0 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -23,7 +23,9 @@ const versionEntryName = "version" const identityConfigKey = "git-bug.identity" var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge") -var ErrNoIdentitySet = errors.New("to interact with bugs, an identity first needs to be created using \"git bug user create\" or \"git bug user adopt\"") +var ErrNoIdentitySet = errors.New("No identity is set.\n" + + "To interact with bugs, an identity first needs to be created using " + + "\"git bug user create\"") var ErrMultipleIdentitiesSet = errors.New("multiple user identities set") var _ Interface = &Identity{} @@ -218,22 +220,8 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) { return NewIdentity(name, email), nil } -// IsUserIdentitySet tell if the user identity is correctly set. -func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) { - configs, err := repo.LocalConfig().ReadAll(identityConfigKey) - if err != nil { - return false, err - } - - if len(configs) > 1 { - return false, ErrMultipleIdentitiesSet - } - - return len(configs) == 1, nil -} - // SetUserIdentity store the user identity's id in the git config -func SetUserIdentity(repo repository.RepoCommon, identity *Identity) error { +func SetUserIdentity(repo repository.RepoConfig, identity *Identity) error { return repo.LocalConfig().StoreString(identityConfigKey, identity.Id().String()) } diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 8b850201..707369c5 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -404,14 +404,13 @@ _git-bug_bridge_configure() two_word_flags+=("--owner") two_word_flags+=("-o") local_nonpersistent_flags+=("--owner=") + flags+=("--credential=") + two_word_flags+=("--credential") + two_word_flags+=("-c") + local_nonpersistent_flags+=("--credential=") flags+=("--token=") two_word_flags+=("--token") - two_word_flags+=("-T") local_nonpersistent_flags+=("--token=") - flags+=("--token-id=") - two_word_flags+=("--token-id") - two_word_flags+=("-i") - local_nonpersistent_flags+=("--token-id=") flags+=("--token-stdin") local_nonpersistent_flags+=("--token-stdin") flags+=("--project=") diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug index bdf08f42..b6086f27 100644 --- a/misc/powershell_completion/git-bug +++ b/misc/powershell_completion/git-bug @@ -81,10 +81,9 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { [CompletionResult]::new('--url', 'url', [CompletionResultType]::ParameterName, 'The URL of the target repository') [CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'The owner of the target repository') [CompletionResult]::new('--owner', 'owner', [CompletionResultType]::ParameterName, 'The owner of the target repository') - [CompletionResult]::new('-T', 'T', [CompletionResultType]::ParameterName, 'The authentication token for the API') - [CompletionResult]::new('--token', 'token', [CompletionResultType]::ParameterName, 'The authentication token for the API') - [CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'The authentication token identifier for the API') - [CompletionResult]::new('--token-id', 'token-id', [CompletionResultType]::ParameterName, 'The authentication token identifier for the API') + [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")') + [CompletionResult]::new('--credential', 'credential', [CompletionResultType]::ParameterName, 'The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")') + [CompletionResult]::new('--token', 'token', [CompletionResultType]::ParameterName, 'A raw authentication token for the API') [CompletionResult]::new('--token-stdin', 'token-stdin', [CompletionResultType]::ParameterName, 'Will read the token from stdin and ignore --token') [CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'The name of the target repository') [CompletionResult]::new('--project', 'project', [CompletionResultType]::ParameterName, 'The name of the target repository') diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index 7a4580c0..a0b4840b 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -194,8 +194,8 @@ function _git-bug_bridge_configure { '(-t --target)'{-t,--target}'[The target of the bridge. Valid values are [github,gitlab,launchpad-preview]]:' \ '(-u --url)'{-u,--url}'[The URL of the target repository]:' \ '(-o --owner)'{-o,--owner}'[The owner of the target repository]:' \ - '(-T --token)'{-T,--token}'[The authentication token for the API]:' \ - '(-i --token-id)'{-i,--token-id}'[The authentication token identifier for the API]:' \ + '(-c --credential)'{-c,--credential}'[The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")]:' \ + '--token[A raw authentication token for the API]:' \ '--token-stdin[Will read the token from stdin and ignore --token]' \ '(-p --project)'{-p,--project}'[The name of the target repository]:' } diff --git a/repository/config_mem.go b/repository/config_mem.go index bd680d03..5ce577ac 100644 --- a/repository/config_mem.go +++ b/repository/config_mem.go @@ -6,30 +6,32 @@ import ( "time" ) -var _ Config = &memConfig{} +var _ Config = &MemConfig{} -type memConfig struct { +type MemConfig struct { config map[string]string } -func newMemConfig(config map[string]string) *memConfig { - return &memConfig{config: config} +func NewMemConfig() *MemConfig { + return &MemConfig{ + config: make(map[string]string), + } } -func (mc *memConfig) StoreString(key, value string) error { +func (mc *MemConfig) StoreString(key, value string) error { mc.config[key] = value return nil } -func (mc *memConfig) StoreBool(key string, value bool) error { +func (mc *MemConfig) StoreBool(key string, value bool) error { return mc.StoreString(key, strconv.FormatBool(value)) } -func (mc *memConfig) StoreTimestamp(key string, value time.Time) error { +func (mc *MemConfig) StoreTimestamp(key string, value time.Time) error { return mc.StoreString(key, strconv.Itoa(int(value.Unix()))) } -func (mc *memConfig) ReadAll(keyPrefix string) (map[string]string, error) { +func (mc *MemConfig) ReadAll(keyPrefix string) (map[string]string, error) { result := make(map[string]string) for key, val := range mc.config { if strings.HasPrefix(key, keyPrefix) { @@ -39,7 +41,7 @@ func (mc *memConfig) ReadAll(keyPrefix string) (map[string]string, error) { return result, nil } -func (mc *memConfig) ReadString(key string) (string, error) { +func (mc *MemConfig) ReadString(key string) (string, error) { // unlike git, the mock can only store one value for the same key val, ok := mc.config[key] if !ok { @@ -49,7 +51,7 @@ func (mc *memConfig) ReadString(key string) (string, error) { return val, nil } -func (mc *memConfig) ReadBool(key string) (bool, error) { +func (mc *MemConfig) ReadBool(key string) (bool, error) { // unlike git, the mock can only store one value for the same key val, ok := mc.config[key] if !ok { @@ -59,7 +61,7 @@ func (mc *memConfig) ReadBool(key string) (bool, error) { return strconv.ParseBool(val) } -func (mc *memConfig) ReadTimestamp(key string) (time.Time, error) { +func (mc *MemConfig) ReadTimestamp(key string) (time.Time, error) { value, err := mc.ReadString(key) if err != nil { return time.Time{}, err @@ -74,7 +76,7 @@ func (mc *memConfig) ReadTimestamp(key string) (time.Time, error) { } // RmConfigs remove all key/value pair matching the key prefix -func (mc *memConfig) RemoveAll(keyPrefix string) error { +func (mc *MemConfig) RemoveAll(keyPrefix string) error { for key := range mc.config { if strings.HasPrefix(key, keyPrefix) { delete(mc.config, key) diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 88c5a132..89d0f395 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -12,8 +12,8 @@ var _ ClockedRepo = &mockRepoForTest{} // mockRepoForTest defines an instance of Repo that can be used for testing. type mockRepoForTest struct { - config map[string]string - globalConfig map[string]string + config *MemConfig + globalConfig *MemConfig blobs map[git.Hash][]byte trees map[git.Hash]string commits map[git.Hash]commit @@ -29,24 +29,25 @@ type commit struct { func NewMockRepoForTest() *mockRepoForTest { return &mockRepoForTest{ - config: make(map[string]string), - blobs: make(map[git.Hash][]byte), - trees: make(map[git.Hash]string), - commits: make(map[git.Hash]commit), - refs: make(map[string]git.Hash), - createClock: lamport.NewClock(), - editClock: lamport.NewClock(), + config: NewMemConfig(), + globalConfig: NewMemConfig(), + blobs: make(map[git.Hash][]byte), + trees: make(map[git.Hash]string), + commits: make(map[git.Hash]commit), + refs: make(map[string]git.Hash), + createClock: lamport.NewClock(), + editClock: lamport.NewClock(), } } // LocalConfig give access to the repository scoped configuration func (r *mockRepoForTest) LocalConfig() Config { - return newMemConfig(r.config) + return r.config } // GlobalConfig give access to the git global configuration func (r *mockRepoForTest) GlobalConfig() Config { - return newMemConfig(r.globalConfig) + return r.globalConfig } // GetPath returns the path to the repo. diff --git a/repository/repo.go b/repository/repo.go index 71bd7a8e..e8517508 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -15,6 +15,15 @@ var ( ErrMultipleConfigEntry = errors.New("multiple config entry for the given key") ) +// RepoConfig access the configuration of a repository +type RepoConfig interface { + // LocalConfig give access to the repository scoped configuration + LocalConfig() Config + + // GlobalConfig give access to the git global configuration + GlobalConfig() Config +} + // RepoCommon represent the common function the we want all the repo to implement type RepoCommon interface { // GetPath returns the path to the repo. @@ -31,16 +40,11 @@ type RepoCommon interface { // GetRemotes returns the configured remotes repositories. GetRemotes() (map[string]string, error) - - // LocalConfig give access to the repository scoped configuration - LocalConfig() Config - - // GlobalConfig give access to the git global configuration - GlobalConfig() Config } // Repo represents a source code repository. type Repo interface { + RepoConfig RepoCommon // FetchRefs fetch git refs from a remote |