aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bridge/bridges.go4
-rw-r--r--bridge/core/auth/credential.go232
-rw-r--r--bridge/core/auth/credential_test.go109
-rw-r--r--bridge/core/auth/options.go62
-rw-r--r--bridge/core/auth/token.go95
-rw-r--r--bridge/core/bridge.go32
-rw-r--r--bridge/core/interfaces.go7
-rw-r--r--bridge/core/token.go296
-rw-r--r--bridge/github/config.go135
-rw-r--r--bridge/github/config_test.go24
-rw-r--r--bridge/github/export.go108
-rw-r--r--bridge/github/export_test.go43
-rw-r--r--bridge/github/github.go5
-rw-r--r--bridge/github/import.go38
-rw-r--r--bridge/github/import_test.go15
-rw-r--r--bridge/github/iterator.go4
-rw-r--r--bridge/gitlab/config.go138
-rw-r--r--bridge/gitlab/export.go72
-rw-r--r--bridge/gitlab/export_test.go23
-rw-r--r--bridge/gitlab/gitlab.go6
-rw-r--r--bridge/gitlab/import.go36
-rw-r--r--bridge/gitlab/import_test.go15
-rw-r--r--bridge/gitlab/iterator.go4
-rw-r--r--bridge/launchpad/config.go39
-rw-r--r--bridge/launchpad/import.go4
-rw-r--r--commands/add.go2
-rw-r--r--commands/bridge_auth.go52
-rw-r--r--commands/bridge_auth_addtoken.go (renamed from commands/bridge_auth_add.go)15
-rw-r--r--commands/bridge_auth_rm.go8
-rw-r--r--commands/bridge_auth_show.go18
-rw-r--r--commands/bridge_configure.go50
-rw-r--r--commands/bridge_pull.go2
-rw-r--r--commands/bridge_push.go2
-rw-r--r--commands/comment_add.go2
-rw-r--r--commands/label_add.go2
-rw-r--r--commands/root.go8
-rw-r--r--commands/status_close.go2
-rw-r--r--commands/status_open.go2
-rw-r--r--commands/title_edit.go2
-rw-r--r--commands/user.go5
-rw-r--r--commands/user_create.go4
-rw-r--r--commands/webui.go2
-rw-r--r--doc/man/git-bug-bridge-configure.18
-rw-r--r--doc/md/git-bug_bridge_configure.md18
-rw-r--r--entity/err.go5
-rw-r--r--identity/identity.go20
-rw-r--r--misc/bash_completion/git-bug9
-rw-r--r--misc/powershell_completion/git-bug7
-rw-r--r--misc/zsh_completion/git-bug4
-rw-r--r--repository/config_mem.go26
-rw-r--r--repository/mock_repo.go23
-rw-r--r--repository/repo.go16
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