aboutsummaryrefslogtreecommitdiffstats
path: root/bridge
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2020-02-09 20:08:12 +0100
committerMichael Muré <batolettre@gmail.com>2020-02-09 20:23:38 +0100
commitb3d3612393387c83fa43f908dbb8e2a71068c834 (patch)
treee4609e21dc74e535d45b38cd7d0504681c544160 /bridge
parentdca85b309a0a82e9993a345964d0831ab2876fb4 (diff)
parent3caffeef4d2ed25d4eb5d4bfd262f4fc3b92561f (diff)
downloadgit-bug-b3d3612393387c83fa43f908dbb8e2a71068c834.tar.gz
Merge remote-tracking branch 'origin/master' into cheshirekow-jira
Diffstat (limited to 'bridge')
-rw-r--r--bridge/bridges.go7
-rw-r--r--bridge/core/auth/credential.go46
-rw-r--r--bridge/core/auth/credential_test.go41
-rw-r--r--bridge/core/auth/options.go32
-rw-r--r--bridge/core/auth/token.go29
-rw-r--r--bridge/core/bridge.go73
-rw-r--r--bridge/core/config.go46
-rw-r--r--bridge/core/export.go5
-rw-r--r--bridge/core/import.go5
-rw-r--r--bridge/core/interfaces.go5
-rw-r--r--bridge/github/config.go221
-rw-r--r--bridge/github/config_test.go5
-rw-r--r--bridge/github/export.go29
-rw-r--r--bridge/github/export_test.go15
-rw-r--r--bridge/github/github.go21
-rw-r--r--bridge/github/import.go30
-rw-r--r--bridge/github/import_test.go8
-rw-r--r--bridge/gitlab/config.go178
-rw-r--r--bridge/gitlab/config_test.go2
-rw-r--r--bridge/gitlab/export.go26
-rw-r--r--bridge/gitlab/export_test.go13
-rw-r--r--bridge/gitlab/gitlab.go6
-rw-r--r--bridge/gitlab/import.go13
-rw-r--r--bridge/gitlab/import_test.go8
-rw-r--r--bridge/jira/client.go2
-rw-r--r--bridge/jira/config.go23
-rw-r--r--bridge/jira/import.go1
-rw-r--r--bridge/jira/jira.go29
-rw-r--r--bridge/launchpad/config.go33
-rw-r--r--bridge/launchpad/import.go6
-rw-r--r--bridge/launchpad/launchpad.go19
31 files changed, 542 insertions, 435 deletions
diff --git a/bridge/bridges.go b/bridge/bridges.go
index 84ca9abe..d74a58fa 100644
--- a/bridge/bridges.go
+++ b/bridge/bridges.go
@@ -23,6 +23,13 @@ func Targets() []string {
return core.Targets()
}
+// LoginMetaKey return the metadata key used to store the remote bug-tracker login
+// on the user identity. The corresponding value is used to match identities and
+// credentials.
+func LoginMetaKey(target string) (string, error) {
+ return core.LoginMetaKey(target)
+}
+
// Instantiate a new Bridge for a repo, from the given target and name
func NewBridge(repo *cache.RepoCache, target string, name string) (*core.Bridge, error) {
return core.NewBridge(repo, target, name)
diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go
index a462a116..c1255aa6 100644
--- a/bridge/core/auth/credential.go
+++ b/bridge/core/auth/credential.go
@@ -14,9 +14,11 @@ import (
const (
configKeyPrefix = "git-bug.auth"
configKeyKind = "kind"
- configKeyUserId = "userid"
configKeyTarget = "target"
configKeyCreateTime = "createtime"
+ configKeyPrefixMeta = "meta."
+
+ MetaKeyLogin = "login"
)
type CredentialKind string
@@ -34,15 +36,18 @@ func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatc
type Credential interface {
ID() entity.Id
- UserId() entity.Id
Target() string
Kind() CredentialKind
CreateTime() time.Time
Validate() error
+ Metadata() map[string]string
+ GetMetadata(key string) (string, bool)
+ SetMetadata(key string, value string)
+
// Return all the specific properties of the credential that need to be saved into the configuration.
- // This does not include Target, User, Kind and CreateTime.
- ToConfig() map[string]string
+ // This does not include Target, Kind, CreateTime and Metadata.
+ toConfig() map[string]string
}
// Load loads a credential from the repo config
@@ -90,6 +95,7 @@ func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, erro
return matching[0], nil
}
+// loadFromConfig is a helper to construct a Credential from the set of git configs
func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
@@ -113,6 +119,20 @@ func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, err
return cred, nil
}
+func metaFromConfig(configs map[string]string) map[string]string {
+ result := make(map[string]string)
+ for key, val := range configs {
+ if strings.HasPrefix(key, configKeyPrefixMeta) {
+ key = strings.TrimPrefix(key, configKeyPrefixMeta)
+ result[key] = val
+ }
+ }
+ if len(result) == 0 {
+ return nil
+ }
+ return result
+}
+
// List load all existing credentials
func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
@@ -120,7 +140,7 @@ func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
return nil, err
}
- re, err := regexp.Compile(configKeyPrefix + `.([^.]+).([^.]+)`)
+ re, err := regexp.Compile(`^` + configKeyPrefix + `\.([^.]+)\.([^.]+(?:\.[^.]+)*)$`)
if err != nil {
panic(err)
}
@@ -168,7 +188,7 @@ func PrefixExist(repo repository.RepoConfig, prefix string) bool {
// Store stores a credential in the global git config
func Store(repo repository.RepoConfig, cred Credential) error {
- confs := cred.ToConfig()
+ confs := cred.toConfig()
prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
@@ -178,12 +198,6 @@ func Store(repo repository.RepoConfig, cred Credential) error {
return err
}
- // UserId
- err = repo.GlobalConfig().StoreString(prefix+configKeyUserId, cred.UserId().String())
- if err != nil {
- return err
- }
-
// Target
err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
if err != nil {
@@ -196,6 +210,14 @@ func Store(repo repository.RepoConfig, cred Credential) error {
return err
}
+ // Metadata
+ for key, val := range cred.Metadata() {
+ err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
+ if err != nil {
+ return err
+ }
+ }
+
// Custom
for key, val := range confs {
err := repo.GlobalConfig().StoreString(prefix+key, val)
diff --git a/bridge/core/auth/credential_test.go b/bridge/core/auth/credential_test.go
index f91d273d..2f8806c9 100644
--- a/bridge/core/auth/credential_test.go
+++ b/bridge/core/auth/credential_test.go
@@ -7,32 +7,23 @@ import (
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
func TestCredential(t *testing.T) {
repo := repository.NewMockRepoForTest()
- user1 := identity.NewIdentity("user1", "email")
- err := user1.Commit(repo)
- assert.NoError(t, err)
-
- user2 := identity.NewIdentity("user2", "email")
- err = user2.Commit(repo)
- assert.NoError(t, err)
-
- storeToken := func(user identity.Interface, val string, target string) *Token {
- token := NewToken(user.Id(), val, target)
- err = Store(repo, token)
+ storeToken := func(val string, target string) *Token {
+ token := NewToken(val, target)
+ err := Store(repo, token)
require.NoError(t, err)
return token
}
- token := storeToken(user1, "foobar", "github")
+ token := storeToken("foobar", "github")
// Store + Load
- err = Store(repo, token)
+ err := Store(repo, token)
assert.NoError(t, err)
token2, err := LoadWithId(repo, token.ID())
@@ -50,8 +41,8 @@ func TestCredential(t *testing.T) {
token.createTime = token3.CreateTime()
assert.Equal(t, token, token3)
- token4 := storeToken(user1, "foo", "gitlab")
- token5 := storeToken(user2, "bar", "github")
+ token4 := storeToken("foo", "gitlab")
+ token5 := storeToken("bar", "github")
// List + options
creds, err := List(repo, WithTarget("github"))
@@ -62,14 +53,6 @@ func TestCredential(t *testing.T) {
assert.NoError(t, err)
sameIds(t, creds, []Credential{token4})
- creds, err = List(repo, WithUser(user1))
- assert.NoError(t, err)
- sameIds(t, creds, []Credential{token, token4})
-
- creds, err = List(repo, WithUserId(user1.Id()))
- assert.NoError(t, err)
- sameIds(t, creds, []Credential{token, token4})
-
creds, err = List(repo, WithKind(KindToken))
assert.NoError(t, err)
sameIds(t, creds, []Credential{token, token4, token5})
@@ -78,6 +61,16 @@ func TestCredential(t *testing.T) {
assert.NoError(t, err)
sameIds(t, creds, []Credential{})
+ // Metadata
+
+ token4.SetMetadata("key", "value")
+ err = Store(repo, token4)
+ assert.NoError(t, err)
+
+ creds, err = List(repo, WithMeta("key", "value"))
+ assert.NoError(t, err)
+ sameIds(t, creds, []Credential{token4})
+
// Exist
exist := IdExist(repo, token.ID())
assert.True(t, exist)
diff --git a/bridge/core/auth/options.go b/bridge/core/auth/options.go
index 7bcda68e..74189874 100644
--- a/bridge/core/auth/options.go
+++ b/bridge/core/auth/options.go
@@ -1,14 +1,9 @@
package auth
-import (
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/identity"
-)
-
type options struct {
target string
- userId entity.Id
kind CredentialKind
+ meta map[string]string
}
type Option func(opts *options)
@@ -26,12 +21,14 @@ func (opts *options) Match(cred Credential) bool {
return false
}
- if opts.userId != "" && cred.UserId() != opts.userId {
+ if opts.kind != "" && cred.Kind() != opts.kind {
return false
}
- if opts.kind != "" && cred.Kind() != opts.kind {
- return false
+ for key, val := range opts.meta {
+ if v, ok := cred.GetMetadata(key); !ok || v != val {
+ return false
+ }
}
return true
@@ -43,20 +40,17 @@ func WithTarget(target string) Option {
}
}
-func WithUser(user identity.Interface) Option {
- return func(opts *options) {
- opts.userId = user.Id()
- }
-}
-
-func WithUserId(userId entity.Id) Option {
+func WithKind(kind CredentialKind) Option {
return func(opts *options) {
- opts.userId = userId
+ opts.kind = kind
}
}
-func WithKind(kind CredentialKind) Option {
+func WithMeta(key string, val string) Option {
return func(opts *options) {
- opts.kind = kind
+ if opts.meta == nil {
+ opts.meta = make(map[string]string)
+ }
+ opts.meta[key] = val
}
}
diff --git a/bridge/core/auth/token.go b/bridge/core/auth/token.go
index 12a3bfc0..42f960bf 100644
--- a/bridge/core/auth/token.go
+++ b/bridge/core/auth/token.go
@@ -18,16 +18,15 @@ var _ Credential = &Token{}
// Token holds an API access token data
type Token struct {
- userId entity.Id
target string
createTime time.Time
Value string
+ meta map[string]string
}
// NewToken instantiate a new token
-func NewToken(userId entity.Id, value, target string) *Token {
+func NewToken(value, target string) *Token {
return &Token{
- userId: userId,
target: target,
createTime: time.Now(),
Value: value,
@@ -37,7 +36,6 @@ func NewToken(userId entity.Id, value, target string) *Token {
func NewTokenFromConfig(conf map[string]string) *Token {
token := &Token{}
- token.userId = entity.Id(conf[configKeyUserId])
token.target = conf[configKeyTarget]
if createTime, ok := conf[configKeyCreateTime]; ok {
if t, err := repository.ParseTimestamp(createTime); err == nil {
@@ -46,6 +44,7 @@ func NewTokenFromConfig(conf map[string]string) *Token {
}
token.Value = conf[tokenValueKey]
+ token.meta = metaFromConfig(conf)
return token
}
@@ -55,10 +54,6 @@ func (t *Token) ID() entity.Id {
return entity.Id(fmt.Sprintf("%x", sum))
}
-func (t *Token) UserId() entity.Id {
- return t.userId
-}
-
func (t *Token) Target() string {
return t.target
}
@@ -88,7 +83,23 @@ func (t *Token) Validate() error {
return nil
}
-func (t *Token) ToConfig() map[string]string {
+func (t *Token) Metadata() map[string]string {
+ return t.meta
+}
+
+func (t *Token) GetMetadata(key string) (string, bool) {
+ val, ok := t.meta[key]
+ return val, ok
+}
+
+func (t *Token) SetMetadata(key string, value string) {
+ if t.meta == nil {
+ t.meta = make(map[string]string)
+ }
+ t.meta[key] = value
+}
+
+func (t *Token) toConfig() map[string]string {
return map[string]string{
tokenValueKey: t.Value,
}
diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go
index 95c0c2c4..ac0d47d7 100644
--- a/bridge/core/bridge.go
+++ b/bridge/core/bridge.go
@@ -28,28 +28,31 @@ const (
)
var bridgeImpl map[string]reflect.Type
+var bridgeLoginMetaKey map[string]string
// BridgeParams holds parameters to simplify the bridge configuration without
// having to make terminal prompts.
type BridgeParams struct {
- Owner string
- Project string
- URL string
- BaseURL string
- CredPrefix string
- TokenRaw string
+ Owner string // owner of the repo (Github)
+ Project string // name of the repo (Github, Launchpad)
+ URL string // complete URL of a repo (Github, Gitlab, Launchpad)
+ BaseURL string // base URL for self-hosted instance ( Gitlab)
+ CredPrefix string // ID prefix of the credential to use (Github, Gitlab)
+ TokenRaw string // pre-existing token to use (Github, Gitlab)
+ Login string // username for the passed credential (Github, Gitlab)
}
// Bridge is a wrapper around a BridgeImpl that will bind low-level
// implementation with utility code to provide high-level functions.
type Bridge struct {
- Name string
- repo *cache.RepoCache
- impl BridgeImpl
- importer Importer
- exporter Exporter
- conf Configuration
- initDone bool
+ Name string
+ repo *cache.RepoCache
+ impl BridgeImpl
+ importer Importer
+ exporter Exporter
+ conf Configuration
+ initImportDone bool
+ initExportDone bool
}
// Register will register a new BridgeImpl
@@ -57,7 +60,11 @@ func Register(impl BridgeImpl) {
if bridgeImpl == nil {
bridgeImpl = make(map[string]reflect.Type)
}
+ if bridgeLoginMetaKey == nil {
+ bridgeLoginMetaKey = make(map[string]string)
+ }
bridgeImpl[impl.Target()] = reflect.TypeOf(impl)
+ bridgeLoginMetaKey[impl.Target()] = impl.LoginMetaKey()
}
// Targets return all known bridge implementation target
@@ -79,6 +86,18 @@ func TargetExist(target string) bool {
return ok
}
+// LoginMetaKey return the metadata key used to store the remote bug-tracker login
+// on the user identity. The corresponding value is used to match identities and
+// credentials.
+func LoginMetaKey(target string) (string, error) {
+ metaKey, ok := bridgeLoginMetaKey[target]
+ if !ok {
+ return "", fmt.Errorf("unknown bridge target %v", target)
+ }
+
+ return metaKey, nil
+}
+
// Instantiate a new Bridge for a repo, from the given target and name
func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
implType, ok := bridgeImpl[target]
@@ -273,8 +292,25 @@ func (b *Bridge) getExporter() Exporter {
return b.exporter
}
-func (b *Bridge) ensureInit() error {
- if b.initDone {
+func (b *Bridge) ensureImportInit() error {
+ if b.initImportDone {
+ return nil
+ }
+
+ importer := b.getImporter()
+ if importer != nil {
+ err := importer.Init(b.repo, b.conf)
+ if err != nil {
+ return err
+ }
+ }
+
+ b.initImportDone = true
+ return nil
+}
+
+func (b *Bridge) ensureExportInit() error {
+ if b.initExportDone {
return nil
}
@@ -294,8 +330,7 @@ func (b *Bridge) ensureInit() error {
}
}
- b.initDone = true
-
+ b.initExportDone = true
return nil
}
@@ -313,7 +348,7 @@ func (b *Bridge) ImportAllSince(ctx context.Context, since time.Time) (<-chan Im
return nil, err
}
- err = b.ensureInit()
+ err = b.ensureImportInit()
if err != nil {
return nil, err
}
@@ -367,7 +402,7 @@ func (b *Bridge) ExportAll(ctx context.Context, since time.Time) (<-chan ExportR
return nil, err
}
- err = b.ensureInit()
+ err = b.ensureExportInit()
if err != nil {
return nil, err
}
diff --git a/bridge/core/config.go b/bridge/core/config.go
new file mode 100644
index 00000000..afcda560
--- /dev/null
+++ b/bridge/core/config.go
@@ -0,0 +1,46 @@
+package core
+
+import (
+ "github.com/MichaelMure/git-bug/cache"
+ "github.com/MichaelMure/git-bug/identity"
+)
+
+func FinishConfig(repo *cache.RepoCache, metaKey string, login string) error {
+ // if no user exist with the given login metadata
+ _, err := repo.ResolveIdentityImmutableMetadata(metaKey, login)
+ if err != nil && err != identity.ErrIdentityNotExist {
+ // real error
+ return err
+ }
+ if err == nil {
+ // found an already valid user, all good
+ return nil
+ }
+
+ // if a default user exist, tag it with the login
+ user, err := repo.GetUserIdentity()
+ if err != nil && err != identity.ErrNoIdentitySet {
+ // real error
+ return err
+ }
+ if err == nil {
+ // found one
+ user.SetMetadata(metaKey, login)
+ return user.CommitAsNeeded()
+ }
+
+ // otherwise create a user with that metadata
+ i, err := repo.NewIdentityFromGitUserRaw(map[string]string{
+ metaKey: login,
+ })
+ if err != nil {
+ return err
+ }
+
+ err = repo.SetUserIdentity(i)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/bridge/core/export.go b/bridge/core/export.go
index ef7a2e57..fa531c5f 100644
--- a/bridge/core/export.go
+++ b/bridge/core/export.go
@@ -27,9 +27,12 @@ const (
// Nothing changed on the bug
ExportEventNothing
+ // Something wrong happened during export that is worth notifying to the user
+ // but not severe enough to consider the export a failure.
+ ExportEventWarning
+
// Error happened during export
ExportEventError
- ExportEventWarning
)
// ExportResult is an event that is emitted during the export process, to
diff --git a/bridge/core/import.go b/bridge/core/import.go
index 3b1f3ac3..0b0b4c68 100644
--- a/bridge/core/import.go
+++ b/bridge/core/import.go
@@ -30,9 +30,12 @@ const (
// Identity has been created
ImportEventIdentity
+ // Something wrong happened during import that is worth notifying to the user
+ // but not severe enough to consider the import a failure.
+ ImportEventWarning
+
// Error happened during import
ImportEventError
- ImportEventWarning
)
// ImportResult is an event that is emitted during the import process, to
diff --git a/bridge/core/interfaces.go b/bridge/core/interfaces.go
index 77e0a7b9..ab2f3977 100644
--- a/bridge/core/interfaces.go
+++ b/bridge/core/interfaces.go
@@ -13,6 +13,11 @@ type BridgeImpl interface {
// Target return the target of the bridge (e.g.: "github")
Target() string
+ // LoginMetaKey return the metadata key used to store the remote bug-tracker login
+ // on the user identity. The corresponding value is used to match identities and
+ // credentials.
+ LoginMetaKey() string
+
// Configure handle the user interaction and return a key/value configuration
// for future use
Configure(repo *cache.RepoCache, params BridgeParams) (Configuration, error)
diff --git a/bridge/github/config.go b/bridge/github/config.go
index bc26a2fc..9477801d 100644
--- a/bridge/github/config.go
+++ b/bridge/github/config.go
@@ -14,29 +14,17 @@ import (
"sort"
"strconv"
"strings"
- "syscall"
"time"
text "github.com/MichaelMure/go-term-text"
"github.com/pkg/errors"
- "golang.org/x/crypto/ssh/terminal"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/cache"
- "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/input"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/colors"
- "github.com/MichaelMure/git-bug/util/interrupt"
-)
-
-const (
- target = "github"
- githubV3Url = "https://api.github.com"
- keyOwner = "owner"
- keyProject = "project"
-
- defaultTimeout = 60 * time.Second
)
var (
@@ -50,12 +38,6 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
conf := make(core.Configuration)
var err error
-
- if (params.CredPrefix != "" || params.TokenRaw != "") &&
- (params.URL == "" && (params.Project == "" || params.Owner == "")) {
- return nil, fmt.Errorf("you must provide a project URL or Owner/Name to configure this bridge with a token")
- }
-
var owner string
var project string
@@ -88,9 +70,23 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
return nil, fmt.Errorf("invalid parameter owner: %v", owner)
}
- user, err := repo.GetUserIdentity()
- if err != nil {
- return nil, err
+ login := params.Login
+ if login == "" {
+ validator := func(name string, value string) (string, error) {
+ ok, err := validateUsername(value)
+ if err != nil {
+ return "", err
+ }
+ if !ok {
+ return "invalid login", nil
+ }
+ return "", nil
+ }
+
+ login, err = input.Prompt("Github login", "login", input.Required, validator)
+ if err != nil {
+ return nil, err
+ }
}
var cred auth.Credential
@@ -101,13 +97,11 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
if err != nil {
return nil, err
}
- 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)
+ cred = auth.NewToken(params.TokenRaw, target)
+ cred.SetMetadata(auth.MetaKeyLogin, login)
default:
- cred, err = promptTokenOptions(repo, user.Id(), owner, project)
+ cred, err = promptTokenOptions(repo, login, owner, project)
if err != nil {
return nil, err
}
@@ -144,7 +138,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
}
}
- return conf, nil
+ return conf, core.FinishConfig(repo, metaKeyGithubLogin, login)
}
func (*Github) ValidateConfig(conf core.Configuration) error {
@@ -165,11 +159,11 @@ func (*Github) ValidateConfig(conf core.Configuration) error {
return nil
}
-func requestToken(note, username, password string, scope string) (*http.Response, error) {
- return requestTokenWith2FA(note, username, password, "", scope)
+func requestToken(note, login, password string, scope string) (*http.Response, error) {
+ return requestTokenWith2FA(note, login, password, "", scope)
}
-func requestTokenWith2FA(note, username, password, otpCode string, scope string) (*http.Response, error) {
+func requestTokenWith2FA(note, login, password, otpCode string, scope string) (*http.Response, error) {
url := fmt.Sprintf("%s/authorizations", githubV3Url)
params := struct {
Scopes []string `json:"scopes"`
@@ -191,7 +185,7 @@ func requestTokenWith2FA(note, username, password, otpCode string, scope string)
return nil, err
}
- req.SetBasicAuth(username, password)
+ req.SetBasicAuth(login, password)
req.Header.Set("Content-Type", "application/json")
if otpCode != "" {
@@ -235,9 +229,9 @@ func randomFingerprint() string {
return string(b)
}
-func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, project string) (auth.Credential, error) {
+func promptTokenOptions(repo repository.RepoConfig, login, owner, project string) (auth.Credential, error) {
for {
- creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target))
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithMeta(auth.MetaKeyLogin, login))
if err != nil {
return nil, err
}
@@ -253,10 +247,11 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, pro
fmt.Println("Existing tokens for Github:")
for i, cred := range creds {
token := cred.(*auth.Token)
- fmt.Printf("[%d]: %s => %s (%s)\n",
+ fmt.Printf("[%d]: %s => %s (login: %s, %s)\n",
i+3,
colors.Cyan(token.ID().Human()),
colors.Red(text.TruncateMax(token.Value, 10)),
+ token.Metadata()[auth.MetaKeyLogin],
token.CreateTime().Format(time.RFC822),
)
}
@@ -284,13 +279,17 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, pro
if err != nil {
return nil, err
}
- return auth.NewToken(userId, value, target), nil
+ token := auth.NewToken(value, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ return token, nil
case 2:
- value, err := loginAndRequestToken(owner, project)
+ value, err := loginAndRequestToken(login, owner, project)
if err != nil {
return nil, err
}
- return auth.NewToken(userId, value, target), nil
+ token := auth.NewToken(value, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ return token, nil
default:
return creds[index-3], nil
}
@@ -308,29 +307,22 @@ func promptToken() (string, error) {
fmt.Println(" - 'repo' : to be able to read private repositories")
fmt.Println()
- re, err := regexp.Compile(`^[a-zA-Z0-9]{40}`)
+ re, err := regexp.Compile(`^[a-zA-Z0-9]{40}$`)
if err != nil {
panic("regexp compile:" + err.Error())
}
- for {
- fmt.Print("Enter token: ")
-
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
- if err != nil {
- return "", err
+ validator := func(name string, value string) (complaint string, err error) {
+ if re.MatchString(value) {
+ return "", nil
}
-
- token := strings.TrimSpace(line)
- if re.MatchString(token) {
- return token, nil
- }
-
- fmt.Println("token is invalid")
+ return "token has incorrect format", nil
}
+
+ return input.Prompt("Enter token", "token", input.Required, validator)
}
-func loginAndRequestToken(owner, project string) (string, error) {
+func loginAndRequestToken(login, owner, project string) (string, error) {
fmt.Println("git-bug will now generate an access token in your Github profile. Your credential are not stored and are only used to generate the token. The token is stored in the global git config.")
fmt.Println()
fmt.Println("The access scope depend on the type of repository.")
@@ -341,17 +333,13 @@ func loginAndRequestToken(owner, project string) (string, error) {
fmt.Println()
// prompt project visibility to know the token scope needed for the repository
- isPublic, err := promptProjectVisibility()
- if err != nil {
- return "", err
- }
-
- username, err := promptUsername()
+ i, err := input.PromptChoice("repository visibility", []string{"public", "private"})
if err != nil {
return "", err
}
+ isPublic := i == 0
- password, err := promptPassword()
+ password, err := input.PromptPassword("Password", "password", input.Required)
if err != nil {
return "", err
}
@@ -370,7 +358,7 @@ func loginAndRequestToken(owner, project string) (string, error) {
note := fmt.Sprintf("git-bug - %s/%s", owner, project)
- resp, err := requestToken(note, username, password, scope)
+ resp, err := requestToken(note, login, password, scope)
if err != nil {
return "", err
}
@@ -380,12 +368,12 @@ func loginAndRequestToken(owner, project string) (string, error) {
// Handle 2FA is needed
OTPHeader := resp.Header.Get("X-GitHub-OTP")
if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
- otpCode, err := prompt2FA()
+ otpCode, err := input.PromptPassword("Two-factor authentication code", "code", input.Required)
if err != nil {
return "", err
}
- resp, err = requestTokenWith2FA(note, username, password, otpCode, scope)
+ resp, err = requestTokenWith2FA(note, login, password, otpCode, scope)
if err != nil {
return "", err
}
@@ -401,29 +389,6 @@ func loginAndRequestToken(owner, project string) (string, error) {
return "", fmt.Errorf("error creating token %v: %v", resp.StatusCode, string(b))
}
-func promptUsername() (string, error) {
- for {
- fmt.Print("username: ")
-
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
- if err != nil {
- return "", err
- }
-
- line = strings.TrimSpace(line)
-
- ok, err := validateUsername(line)
- if err != nil {
- return "", err
- }
- if ok {
- return line, nil
- }
-
- fmt.Println("invalid username")
- }
-}
-
func promptURL(repo repository.RepoCommon) (string, string, error) {
// remote suggestions
remotes, err := repo.GetRemotes()
@@ -578,87 +543,3 @@ func validateProject(owner, project string, token *auth.Token) (bool, error) {
return resp.StatusCode == http.StatusOK, nil
}
-
-func promptPassword() (string, error) {
- termState, err := terminal.GetState(int(syscall.Stdin))
- if err != nil {
- return "", err
- }
-
- cancel := interrupt.RegisterCleaner(func() error {
- return terminal.Restore(int(syscall.Stdin), termState)
- })
- defer cancel()
-
- for {
- fmt.Print("password: ")
-
- bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
- // new line for coherent formatting, ReadPassword clip the normal new line
- // entered by the user
- fmt.Println()
-
- if err != nil {
- return "", err
- }
-
- if len(bytePassword) > 0 {
- return string(bytePassword), nil
- }
-
- fmt.Println("password is empty")
- }
-}
-
-func prompt2FA() (string, error) {
- termState, err := terminal.GetState(int(syscall.Stdin))
- if err != nil {
- return "", err
- }
-
- cancel := interrupt.RegisterCleaner(func() error {
- return terminal.Restore(int(syscall.Stdin), termState)
- })
- defer cancel()
-
- for {
- fmt.Print("two-factor authentication code: ")
-
- byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
- fmt.Println()
- if err != nil {
- return "", err
- }
-
- if len(byte2fa) > 0 {
- return string(byte2fa), nil
- }
-
- fmt.Println("code is empty")
- }
-}
-
-func promptProjectVisibility() (bool, error) {
- for {
- fmt.Println("[1]: public")
- fmt.Println("[2]: private")
- fmt.Print("repository visibility: ")
-
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
- fmt.Println()
- if err != nil {
- return false, err
- }
-
- line = strings.TrimSpace(line)
-
- index, err := strconv.Atoi(line)
- if err != nil || (index != 1 && index != 2) {
- fmt.Println("invalid input")
- continue
- }
-
- // return true for public repositories, false for private
- return index == 1, nil
- }
-}
diff --git a/bridge/github/config_test.go b/bridge/github/config_test.go
index 9798d26b..d7b1b38d 100644
--- a/bridge/github/config_test.go
+++ b/bridge/github/config_test.go
@@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/MichaelMure/git-bug/bridge/core/auth"
- "github.com/MichaelMure/git-bug/entity"
)
func TestSplitURL(t *testing.T) {
@@ -155,8 +154,8 @@ func TestValidateProject(t *testing.T) {
t.Skip("Env var GITHUB_TOKEN_PUBLIC missing")
}
- tokenPrivate := auth.NewToken(entity.UnsetId, envPrivate, target)
- tokenPublic := auth.NewToken(entity.UnsetId, envPublic, target)
+ tokenPrivate := auth.NewToken(envPrivate, target)
+ tokenPublic := auth.NewToken(envPublic, target)
type args struct {
owner string
diff --git a/bridge/github/export.go b/bridge/github/export.go
index 6c089a47..c363e188 100644
--- a/bridge/github/export.go
+++ b/bridge/github/export.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
+ "os"
"strings"
"time"
@@ -19,7 +20,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/repository"
+ "github.com/MichaelMure/git-bug/identity"
)
var (
@@ -74,7 +75,8 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
return err
}
- creds, err := auth.List(repo, auth.WithUserId(user.Id()), auth.WithTarget(target), auth.WithKind(auth.KindToken))
+ login := user.ImmutableMetadata()[metaKeyGithubLogin]
+ creds, err := auth.List(repo, auth.WithMeta(auth.MetaKeyLogin, login), auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
@@ -88,16 +90,30 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
return nil
}
-func (ge *githubExporter) cacheAllClient(repo repository.RepoConfig) error {
+func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error {
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
for _, cred := range creds {
- if _, ok := ge.identityClient[cred.UserId()]; !ok {
+ login, ok := cred.GetMetadata(auth.MetaKeyLogin)
+ if !ok {
+ _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Github login\n", cred.ID().Human())
+ continue
+ }
+
+ user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGithubLogin, login)
+ if err == identity.ErrIdentityNotExist {
+ continue
+ }
+ if err != nil {
+ return nil
+ }
+
+ if _, ok := ge.identityClient[user.Id()]; !ok {
client := buildClient(creds[0].(*auth.Token))
- ge.identityClient[cred.UserId()] = client
+ ge.identityClient[user.Id()] = client
}
}
@@ -477,11 +493,12 @@ func (ge *githubExporter) cacheGithubLabels(ctx context.Context, gc *githubv4.Cl
for hasNextPage {
// create a new timeout context at each iteration
ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
- defer cancel()
if err := gc.Query(ctx, &q, variables); err != nil {
+ cancel()
return err
}
+ cancel()
for _, label := range q.Repository.Labels.Nodes {
ge.cachedLabels[label.Name] = label.ID
diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go
index 5a0bc653..7d6e6fb1 100644
--- a/bridge/github/export_test.go
+++ b/bridge/github/export_test.go
@@ -144,8 +144,12 @@ func TestPushPull(t *testing.T) {
require.NoError(t, err)
// set author identity
+ login := "identity-test"
author, err := backend.NewIdentity("test identity", "test@test.org")
require.NoError(t, err)
+ author.SetMetadata(metaKeyGithubLogin, login)
+ err = author.Commit()
+ require.NoError(t, err)
err = backend.SetUserIdentity(author)
require.NoError(t, err)
@@ -153,6 +157,11 @@ func TestPushPull(t *testing.T) {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
+ token := auth.NewToken(envToken, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ err = auth.Store(repo, token)
+ require.NoError(t, err)
+
tests := testCases(t, backend)
// generate project name
@@ -176,10 +185,6 @@ func TestPushPull(t *testing.T) {
return deleteRepository(projectName, envUser, envToken)
})
- token := auth.NewToken(author.Id(), envToken, target)
- err = auth.Store(repo, token)
- require.NoError(t, err)
-
// initialize exporter
exporter := &githubExporter{}
err = exporter.Init(backend, core.Configuration{
@@ -255,7 +260,7 @@ func TestPushPull(t *testing.T) {
// verify bug have same number of original operations
require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp)
- // verify bugs are taged with origin=github
+ // verify bugs are tagged with origin=github
issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin)
require.True(t, ok)
require.Equal(t, issueOrigin, target)
diff --git a/bridge/github/github.go b/bridge/github/github.go
index 874c2d11..19dc8a08 100644
--- a/bridge/github/github.go
+++ b/bridge/github/github.go
@@ -3,6 +3,7 @@ package github
import (
"context"
+ "time"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
@@ -11,12 +12,32 @@ import (
"github.com/MichaelMure/git-bug/bridge/core/auth"
)
+const (
+ target = "github"
+
+ metaKeyGithubId = "github-id"
+ metaKeyGithubUrl = "github-url"
+ metaKeyGithubLogin = "github-login"
+
+ keyOwner = "owner"
+ keyProject = "project"
+
+ githubV3Url = "https://api.github.com"
+ defaultTimeout = 60 * time.Second
+)
+
+var _ core.BridgeImpl = &Github{}
+
type Github struct{}
func (*Github) Target() string {
return target
}
+func (g *Github) LoginMetaKey() string {
+ return metaKeyGithubLogin
+}
+
func (*Github) NewImporter() core.Importer {
return &githubImporter{}
}
diff --git a/bridge/github/import.go b/bridge/github/import.go
index 092e3e71..ea0ccba3 100644
--- a/bridge/github/import.go
+++ b/bridge/github/import.go
@@ -15,12 +15,6 @@ import (
"github.com/MichaelMure/git-bug/util/text"
)
-const (
- metaKeyGithubId = "github-id"
- metaKeyGithubUrl = "github-url"
- metaKeyGithubLogin = "github-login"
-)
-
// githubImporter implement the Importer interface
type githubImporter struct {
conf core.Configuration
@@ -38,17 +32,7 @@ type githubImporter struct {
func (gi *githubImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
gi.conf = conf
- opts := []auth.Option{
- auth.WithTarget(target),
- auth.WithKind(auth.KindToken),
- }
-
- user, err := repo.GetUserIdentity()
- if err == nil {
- opts = append(opts, auth.WithUserId(user.Id()))
- }
-
- creds, err := auth.List(repo, opts...)
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
@@ -197,6 +181,11 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
// other edits will be added as CommentEdit operations
target, err := b.ResolveOperationWithMetadata(metaKeyGithubId, parseId(issue.Id))
+ if err == cache.ErrNoMatchingOp {
+ // original comment is missing somehow, issuing a warning
+ gi.out <- core.NewImportWarning(fmt.Errorf("comment ID %s to edit is missing", parseId(issue.Id)), b.Id())
+ continue
+ }
if err != nil {
return nil, err
}
@@ -545,10 +534,14 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca
case "Bot":
}
+ // Name is not necessarily set, fallback to login as a name is required in the identity
+ if name == "" {
+ name = string(actor.Login)
+ }
+
i, err = repo.NewIdentityRaw(
name,
email,
- string(actor.Login),
string(actor.AvatarUrl),
map[string]string{
metaKeyGithubLogin: string(actor.Login),
@@ -595,7 +588,6 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache,
return repo.NewIdentityRaw(
name,
"",
- string(q.User.Login),
string(q.User.AvatarUrl),
map[string]string{
metaKeyGithubLogin: string(q.User.Login),
diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go
index 304229a0..a8f8e346 100644
--- a/bridge/github/import_test.go
+++ b/bridge/github/import_test.go
@@ -21,6 +21,7 @@ import (
func Test_Importer(t *testing.T) {
author := identity.NewIdentity("Michael Muré", "batolettre@gmail.com")
+
tests := []struct {
name string
url string
@@ -140,10 +141,11 @@ func Test_Importer(t *testing.T) {
t.Skip("Env var GITHUB_TOKEN_PRIVATE missing")
}
- err = author.Commit(repo)
- require.NoError(t, err)
+ login := "test-identity"
+ author.SetMetadata(metaKeyGithubLogin, login)
- token := auth.NewToken(author.Id(), envToken, target)
+ token := auth.NewToken(envToken, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
err = auth.Store(repo, token)
require.NoError(t, err)
diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go
index 99c27836..fb593819 100644
--- a/bridge/gitlab/config.go
+++ b/bridge/gitlab/config.go
@@ -5,6 +5,7 @@ import (
"fmt"
"net/url"
"os"
+ "path"
"regexp"
"sort"
"strconv"
@@ -18,7 +19,7 @@ import (
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/cache"
- "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/input"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/colors"
)
@@ -34,16 +35,22 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
if params.Owner != "" {
fmt.Println("warning: --owner is ineffective for a gitlab bridge")
}
+ if params.Login != "" {
+ fmt.Println("warning: --login is ineffective for a gitlab bridge")
+ }
conf := make(core.Configuration)
var err error
+ var baseUrl string
- if (params.CredPrefix != "" || params.TokenRaw != "") && params.URL == "" {
- return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
- }
-
- if params.URL == "" {
- params.URL = defaultBaseURL
+ switch {
+ case params.BaseURL != "":
+ baseUrl = params.BaseURL
+ default:
+ baseUrl, err = promptBaseUrlOptions()
+ if err != nil {
+ return nil, errors.Wrap(err, "base url prompt")
+ }
}
var url string
@@ -54,7 +61,7 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
url = params.URL
default:
// terminal prompt
- url, err = promptURL(repo)
+ url, err = promptURL(repo, baseUrl)
if err != nil {
return nil, errors.Wrap(err, "url prompt")
}
@@ -64,11 +71,6 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, url)
}
- user, err := repo.GetUserIdentity()
- if err != nil {
- return nil, err
- }
-
var cred auth.Credential
switch {
@@ -77,13 +79,16 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
if err != nil {
return nil, err
}
- 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)
+ token := auth.NewToken(params.TokenRaw, target)
+ login, err := getLoginFromToken(baseUrl, token)
+ if err != nil {
+ return nil, err
+ }
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ cred = token
default:
- cred, err = promptTokenOptions(repo, user.Id())
+ cred, err = promptTokenOptions(repo, baseUrl)
if err != nil {
return nil, err
}
@@ -95,14 +100,14 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
}
// validate project url and get its ID
- id, err := validateProjectURL(params.BaseURL, url, token)
+ id, err := validateProjectURL(baseUrl, url, token)
if err != nil {
return nil, errors.Wrap(err, "project validation")
}
conf[core.ConfigKeyTarget] = target
conf[keyProjectID] = strconv.Itoa(id)
- conf[keyGitlabBaseUrl] = params.BaseURL
+ conf[keyGitlabBaseUrl] = baseUrl
err = g.ValidateConfig(conf)
if err != nil {
@@ -126,7 +131,9 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
} else if v != target {
return fmt.Errorf("unexpected target name: %v", v)
}
-
+ if _, ok := conf[keyGitlabBaseUrl]; !ok {
+ return fmt.Errorf("missing %s key", keyGitlabBaseUrl)
+ }
if _, ok := conf[keyProjectID]; !ok {
return fmt.Errorf("missing %s key", keyProjectID)
}
@@ -134,20 +141,51 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
return nil
}
-func promptTokenOptions(repo repository.RepoConfig, userId entity.Id) (auth.Credential, error) {
+func promptBaseUrlOptions() (string, error) {
+ index, err := input.PromptChoice("Gitlab base url", []string{
+ "https://gitlab.com",
+ "enter your own base url",
+ })
+
+ if err != nil {
+ return "", err
+ }
+
+ if index == 0 {
+ return defaultBaseURL, nil
+ } else {
+ return promptBaseUrl()
+ }
+}
+
+func promptBaseUrl() (string, error) {
+ validator := func(name string, value string) (string, error) {
+ u, err := url.Parse(value)
+ if err != nil {
+ return err.Error(), nil
+ }
+ if u.Scheme == "" {
+ return "missing scheme", nil
+ }
+ if u.Host == "" {
+ return "missing host", nil
+ }
+ return "", nil
+ }
+
+ return input.Prompt("Base url", "url", input.Required, validator)
+}
+
+func promptTokenOptions(repo repository.RepoConfig, baseUrl string) (auth.Credential, error) {
for {
- creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target), auth.WithKind(auth.KindToken))
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return nil, err
}
// if we don't have existing token, fast-track to the token prompt
if len(creds) == 0 {
- value, err := promptToken()
- if err != nil {
- return nil, err
- }
- return auth.NewToken(userId, value, target), nil
+ return promptToken(baseUrl)
}
fmt.Println()
@@ -185,54 +223,57 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id) (auth.Cred
switch index {
case 1:
- value, err := promptToken()
- if err != nil {
- return nil, err
- }
- return auth.NewToken(userId, value, target), nil
+ return promptToken(baseUrl)
default:
return creds[index-2], nil
}
}
}
-func promptToken() (string, error) {
- fmt.Println("You can generate a new token by visiting https://gitlab.com/profile/personal_access_tokens.")
+func promptToken(baseUrl string) (*auth.Token, error) {
+ fmt.Printf("You can generate a new token by visiting %s.\n", path.Join(baseUrl, "profile/personal_access_tokens"))
fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
fmt.Println()
fmt.Println("'api' access scope: to be able to make api calls")
fmt.Println()
- re, err := regexp.Compile(`^[a-zA-Z0-9\-]{20}`)
+ re, err := regexp.Compile(`^[a-zA-Z0-9\-\_]{20}$`)
if err != nil {
panic("regexp compile:" + err.Error())
}
- for {
- fmt.Print("Enter token: ")
+ var login string
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
- if err != nil {
- return "", err
+ validator := func(name string, value string) (complaint string, err error) {
+ if !re.MatchString(value) {
+ return "token has incorrect format", nil
}
-
- token := strings.TrimSpace(line)
- if re.MatchString(token) {
- return token, nil
+ login, err = getLoginFromToken(baseUrl, auth.NewToken(value, target))
+ if err != nil {
+ return fmt.Sprintf("token is invalid: %v", err), nil
}
+ return "", nil
+ }
- fmt.Println("token format is invalid")
+ rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
+ if err != nil {
+ return nil, err
}
+
+ token := auth.NewToken(rawToken, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
+
+ return token, nil
}
-func promptURL(repo repository.RepoCommon) (string, error) {
+func promptURL(repo repository.RepoCommon, baseUrl string) (string, error) {
// remote suggestions
remotes, err := repo.GetRemotes()
if err != nil {
return "", errors.Wrap(err, "getting remotes")
}
- validRemotes := getValidGitlabRemoteURLs(remotes)
+ validRemotes := getValidGitlabRemoteURLs(baseUrl, remotes)
if len(validRemotes) > 0 {
for {
fmt.Println("\nDetected projects:")
@@ -286,7 +327,7 @@ func promptURL(repo repository.RepoCommon) (string, error) {
}
}
-func getProjectPath(projectUrl string) (string, error) {
+func getProjectPath(baseUrl, projectUrl string) (string, error) {
cleanUrl := strings.TrimSuffix(projectUrl, ".git")
cleanUrl = strings.Replace(cleanUrl, "git@", "https://", 1)
objectUrl, err := url.Parse(cleanUrl)
@@ -294,38 +335,63 @@ func getProjectPath(projectUrl string) (string, error) {
return "", ErrBadProjectURL
}
+ objectBaseUrl, err := url.Parse(baseUrl)
+ if err != nil {
+ return "", ErrBadProjectURL
+ }
+
+ if objectUrl.Hostname() != objectBaseUrl.Hostname() {
+ return "", fmt.Errorf("base url and project url hostnames doesn't match")
+ }
return objectUrl.Path[1:], nil
}
-func getValidGitlabRemoteURLs(remotes map[string]string) []string {
+func getValidGitlabRemoteURLs(baseUrl string, remotes map[string]string) []string {
urls := make([]string, 0, len(remotes))
for _, u := range remotes {
- path, err := getProjectPath(u)
+ path, err := getProjectPath(baseUrl, u)
if err != nil {
continue
}
- urls = append(urls, fmt.Sprintf("%s%s", "gitlab.com", path))
+ urls = append(urls, fmt.Sprintf("%s/%s", baseUrl, path))
}
return urls
}
-func validateProjectURL(baseURL, url string, token *auth.Token) (int, error) {
- projectPath, err := getProjectPath(url)
+func validateProjectURL(baseUrl, url string, token *auth.Token) (int, error) {
+ projectPath, err := getProjectPath(baseUrl, url)
if err != nil {
return 0, err
}
- client, err := buildClient(baseURL, token)
+ client, err := buildClient(baseUrl, token)
if err != nil {
return 0, err
}
project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
if err != nil {
- return 0, err
+ return 0, errors.Wrap(err, "wrong token scope ou non-existent project")
}
return project.ID, nil
}
+
+func getLoginFromToken(baseUrl string, token *auth.Token) (string, error) {
+ client, err := buildClient(baseUrl, token)
+ if err != nil {
+ return "", err
+ }
+
+ user, _, err := client.Users.CurrentUser()
+ if err != nil {
+ return "", err
+ }
+ if user.Username == "" {
+ return "", fmt.Errorf("gitlab say username is empty")
+ }
+
+ return user.Username, nil
+}
diff --git a/bridge/gitlab/config_test.go b/bridge/gitlab/config_test.go
index 87469796..43ed649a 100644
--- a/bridge/gitlab/config_test.go
+++ b/bridge/gitlab/config_test.go
@@ -82,7 +82,7 @@ func TestProjectPath(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- path, err := getProjectPath(tt.args.url)
+ path, err := getProjectPath(defaultBaseURL, tt.args.url)
assert.Equal(t, tt.want.path, path)
assert.Equal(t, tt.want.err, err)
})
diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go
index d42ef1cd..c5323da4 100644
--- a/bridge/gitlab/export.go
+++ b/bridge/gitlab/export.go
@@ -3,6 +3,7 @@ package gitlab
import (
"context"
"fmt"
+ "os"
"strconv"
"time"
@@ -14,7 +15,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/repository"
+ "github.com/MichaelMure/git-bug/identity"
)
var (
@@ -54,20 +55,33 @@ func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
return nil
}
-func (ge *gitlabExporter) cacheAllClient(repo repository.RepoConfig) error {
+func (ge *gitlabExporter) cacheAllClient(repo *cache.RepoCache) error {
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
for _, cred := range creds {
- if _, ok := ge.identityClient[cred.UserId()]; !ok {
+ login, ok := cred.GetMetadata(auth.MetaKeyLogin)
+ if !ok {
+ _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Gitlab login\n", cred.ID().Human())
+ continue
+ }
+
+ user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabLogin, login)
+ if err == identity.ErrIdentityNotExist {
+ continue
+ }
+ if err != nil {
+ return nil
+ }
+
+ if _, ok := ge.identityClient[user.Id()]; !ok {
client, err := buildClient(ge.conf[keyGitlabBaseUrl], creds[0].(*auth.Token))
if err != nil {
return err
}
-
- ge.identityClient[cred.UserId()] = client
+ ge.identityClient[user.Id()] = client
}
}
@@ -159,7 +173,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc
gitlabID, ok := snapshot.GetCreateMetadata(metaKeyGitlabId)
if ok {
gitlabBaseUrl, ok := snapshot.GetCreateMetadata(metaKeyGitlabBaseUrl)
- if ok && gitlabBaseUrl != ge.conf[gitlabBaseUrl] {
+ if ok && gitlabBaseUrl != ge.conf[keyGitlabBaseUrl] {
out <- core.NewExportNothing(b.Id(), "skipping issue imported from another Gitlab instance")
return
}
diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go
index d16defd0..1d387655 100644
--- a/bridge/gitlab/export_test.go
+++ b/bridge/gitlab/export_test.go
@@ -149,8 +149,12 @@ func TestPushPull(t *testing.T) {
require.NoError(t, err)
// set author identity
+ login := "test-identity"
author, err := backend.NewIdentity("test identity", "test@test.org")
require.NoError(t, err)
+ author.SetMetadata(metaKeyGitlabLogin, login)
+ err = author.Commit()
+ require.NoError(t, err)
err = backend.SetUserIdentity(author)
require.NoError(t, err)
@@ -158,12 +162,13 @@ func TestPushPull(t *testing.T) {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
- tests := testCases(t, backend)
-
- token := auth.NewToken(author.Id(), envToken, target)
+ token := auth.NewToken(envToken, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
err = auth.Store(repo, token)
require.NoError(t, err)
+ tests := testCases(t, backend)
+
// generate project name
projectName := generateRepoName()
@@ -260,7 +265,7 @@ func TestPushPull(t *testing.T) {
// verify bug have same number of original operations
require.Len(t, importedBug.Snapshot().Operations, tt.numOpImp)
- // verify bugs are taged with origin=gitlab
+ // verify bugs are tagged with origin=gitlab
issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin)
require.True(t, ok)
require.Equal(t, issueOrigin, target)
diff --git a/bridge/gitlab/gitlab.go b/bridge/gitlab/gitlab.go
index 9298dc8e..8512379c 100644
--- a/bridge/gitlab/gitlab.go
+++ b/bridge/gitlab/gitlab.go
@@ -26,12 +26,18 @@ const (
defaultTimeout = 60 * time.Second
)
+var _ core.BridgeImpl = &Gitlab{}
+
type Gitlab struct{}
func (*Gitlab) Target() string {
return target
}
+func (g *Gitlab) LoginMetaKey() string {
+ return metaKeyGitlabLogin
+}
+
func (*Gitlab) NewImporter() core.Importer {
return &gitlabImporter{}
}
diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go
index 4fa37505..d699554b 100644
--- a/bridge/gitlab/import.go
+++ b/bridge/gitlab/import.go
@@ -33,17 +33,7 @@ type gitlabImporter struct {
func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
gi.conf = conf
- opts := []auth.Option{
- auth.WithTarget(target),
- auth.WithKind(auth.KindToken),
- }
-
- user, err := repo.GetUserIdentity()
- if err == nil {
- opts = append(opts, auth.WithUserId(user.Id()))
- }
-
- creds, err := auth.List(repo, opts...)
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
@@ -399,7 +389,6 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id
i, err = repo.NewIdentityRaw(
user.Name,
user.PublicEmail,
- user.Username,
user.AvatarURL,
map[string]string{
// because Gitlab
diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go
index 6e378b07..3c0caa55 100644
--- a/bridge/gitlab/import_test.go
+++ b/bridge/gitlab/import_test.go
@@ -21,6 +21,7 @@ import (
func TestImport(t *testing.T) {
author := identity.NewIdentity("Amine Hilaly", "hilalyamine@gmail.com")
+
tests := []struct {
name string
url string
@@ -94,10 +95,11 @@ func TestImport(t *testing.T) {
t.Skip("Env var GITLAB_PROJECT_ID missing")
}
- err = author.Commit(repo)
- require.NoError(t, err)
+ login := "test-identity"
+ author.SetMetadata(metaKeyGitlabLogin, login)
- token := auth.NewToken(author.Id(), envToken, target)
+ token := auth.NewToken(envToken, target)
+ token.SetMetadata(metaKeyGitlabLogin, login)
err = auth.Store(repo, token)
require.NoError(t, err)
diff --git a/bridge/jira/client.go b/bridge/jira/client.go
index 040c988e..6ec1c9dd 100644
--- a/bridge/jira/client.go
+++ b/bridge/jira/client.go
@@ -386,7 +386,7 @@ func (client *Client) Login(conf core.Configuration) error {
password := conf[keyPassword]
if password == "" {
var err error
- password, err = input.PromptPassword()
+ password, err = input.PromptPassword("Password", "password", input.Required)
if err != nil {
return err
}
diff --git a/bridge/jira/config.go b/bridge/jira/config.go
index 59076564..406bed31 100644
--- a/bridge/jira/config.go
+++ b/bridge/jira/config.go
@@ -8,7 +8,6 @@ import (
"os"
"strconv"
"strings"
- "time"
"github.com/pkg/errors"
@@ -17,22 +16,6 @@ import (
"github.com/MichaelMure/git-bug/input"
)
-const (
- target = "jira"
- keyServer = "server"
- keyProject = "project"
- keyCredentialsType = "credentials-type"
- keyCredentialsFile = "credentials-file"
- keyUsername = "username"
- keyPassword = "password"
- keyIDMap = "bug-id-map"
- keyIDRevMap = "bug-id-revmap"
- keyCreateDefaults = "create-issue-defaults"
- keyCreateGitBug = "create-issue-gitbug-id"
-
- defaultTimeout = 60 * time.Second
-)
-
const moreConfigText = `
NOTE: There are a few optional configuration values that you can additionally
set in your git configuration to influence the behavior of the bridge. Please
@@ -65,9 +48,7 @@ How would you like to store your JIRA login credentials?
`
// Configure sets up the bridge configuration
-func (g *Jira) Configure(
- repo *cache.RepoCache, params core.BridgeParams) (
- core.Configuration, error) {
+func (g *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
conf := make(core.Configuration)
var err error
var url string
@@ -126,7 +107,7 @@ func (g *Jira) Configure(
return nil, err
}
- password, err = input.PromptPassword()
+ password, err = input.PromptPassword("Password", "password", input.Required)
if err != nil {
return nil, err
}
diff --git a/bridge/jira/import.go b/bridge/jira/import.go
index 2337d8bd..bc1bf428 100644
--- a/bridge/jira/import.go
+++ b/bridge/jira/import.go
@@ -175,7 +175,6 @@ func (self *jiraImporter) ensurePerson(
user.DisplayName,
user.EmailAddress,
user.Key,
- "",
map[string]string{
keyJiraUser: string(user.Key),
},
diff --git a/bridge/jira/jira.go b/bridge/jira/jira.go
index accb9e7c..43a11c05 100644
--- a/bridge/jira/jira.go
+++ b/bridge/jira/jira.go
@@ -3,10 +3,32 @@ package jira
import (
"sort"
+ "time"
"github.com/MichaelMure/git-bug/bridge/core"
)
+const (
+ target = "jira"
+
+ metaKeyJiraLogin = "jira-login"
+
+ keyServer = "server"
+ keyProject = "project"
+ keyCredentialsType = "credentials-type"
+ keyCredentialsFile = "credentials-file"
+ keyUsername = "username"
+ keyPassword = "password"
+ keyIDMap = "bug-id-map"
+ keyIDRevMap = "bug-id-revmap"
+ keyCreateDefaults = "create-issue-defaults"
+ keyCreateGitBug = "create-issue-gitbug-id"
+
+ defaultTimeout = 60 * time.Second
+)
+
+var _ core.BridgeImpl = &Jira{}
+
// Jira Main object for the bridge
type Jira struct{}
@@ -15,6 +37,10 @@ func (*Jira) Target() string {
return target
}
+func (*Jira) LoginMetaKey() string {
+ return metaKeyJiraLogin
+}
+
// NewImporter returns the jira importer
func (*Jira) NewImporter() core.Importer {
return &jiraImporter{}
@@ -39,8 +65,7 @@ func stringInSlice(needle string, haystack []string) bool {
// 1. elements found only in the first input list
// 2. elements found only in the second input list
// 3. elements found in both input lists
-func setSymmetricDifference(
- setA, setB []string) ([]string, []string, []string) {
+func setSymmetricDifference(setA, setB []string) ([]string, []string, []string) {
sort.Strings(setA)
sort.Strings(setB)
diff --git a/bridge/launchpad/config.go b/bridge/launchpad/config.go
index edbd941d..e029fad3 100644
--- a/bridge/launchpad/config.go
+++ b/bridge/launchpad/config.go
@@ -1,27 +1,18 @@
package launchpad
import (
- "bufio"
"errors"
"fmt"
"net/http"
- "os"
"regexp"
- "strings"
- "time"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/cache"
+ "github.com/MichaelMure/git-bug/input"
)
var ErrBadProjectURL = errors.New("bad Launchpad project URL")
-const (
- target = "launchpad-preview"
- keyProject = "project"
- defaultTimeout = 60 * time.Second
-)
-
func (l *Launchpad) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
if params.TokenRaw != "" {
fmt.Println("warning: token params are ineffective for a Launchpad bridge")
@@ -45,7 +36,7 @@ func (l *Launchpad) Configure(repo *cache.RepoCache, params core.BridgeParams) (
project, err = splitURL(params.URL)
default:
// get project name from terminal prompt
- project, err = promptProjectName()
+ project, err = input.Prompt("Launchpad project name", "project name", input.Required)
}
if err != nil {
@@ -86,26 +77,6 @@ func (*Launchpad) ValidateConfig(conf core.Configuration) error {
return nil
}
-func promptProjectName() (string, error) {
- for {
- fmt.Print("Launchpad project name: ")
-
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
- if err != nil {
- return "", err
- }
-
- line = strings.TrimRight(line, "\n")
-
- if line == "" {
- fmt.Println("Project name is empty")
- continue
- }
-
- return line, nil
- }
-}
-
func validateProject(project string) (bool, error) {
url := fmt.Sprintf("%s/%s", apiRoot, project)
diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go
index 619631b3..5bca8e63 100644
--- a/bridge/launchpad/import.go
+++ b/bridge/launchpad/import.go
@@ -20,11 +20,6 @@ func (li *launchpadImporter) Init(repo *cache.RepoCache, conf core.Configuration
return nil
}
-const (
- metaKeyLaunchpadID = "launchpad-id"
- metaKeyLaunchpadLogin = "launchpad-login"
-)
-
func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson) (*cache.IdentityCache, error) {
// Look first in the cache
i, err := repo.ResolveIdentityImmutableMetadata(metaKeyLaunchpadLogin, owner.Login)
@@ -38,7 +33,6 @@ func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson)
return repo.NewIdentityRaw(
owner.Name,
"",
- owner.Login,
"",
map[string]string{
metaKeyLaunchpadLogin: owner.Login,
diff --git a/bridge/launchpad/launchpad.go b/bridge/launchpad/launchpad.go
index 030d9169..b4fcdd00 100644
--- a/bridge/launchpad/launchpad.go
+++ b/bridge/launchpad/launchpad.go
@@ -2,15 +2,34 @@
package launchpad
import (
+ "time"
+
"github.com/MichaelMure/git-bug/bridge/core"
)
+const (
+ target = "launchpad-preview"
+
+ metaKeyLaunchpadID = "launchpad-id"
+ metaKeyLaunchpadLogin = "launchpad-login"
+
+ keyProject = "project"
+
+ defaultTimeout = 60 * time.Second
+)
+
+var _ core.BridgeImpl = &Launchpad{}
+
type Launchpad struct{}
func (*Launchpad) Target() string {
return "launchpad-preview"
}
+func (l *Launchpad) LoginMetaKey() string {
+ return metaKeyLaunchpadLogin
+}
+
func (*Launchpad) NewImporter() core.Importer {
return &launchpadImporter{}
}