aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bridge/bridges.go7
-rw-r--r--bridge/core/auth/credential.go66
-rw-r--r--bridge/core/auth/credential_test.go41
-rw-r--r--bridge/core/auth/options.go32
-rw-r--r--bridge/core/auth/token.go31
-rw-r--r--bridge/core/bridge.go30
-rw-r--r--bridge/core/config.go46
-rw-r--r--bridge/core/interfaces.go5
-rw-r--r--bridge/github/config.go226
-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.go29
-rw-r--r--bridge/github/import_test.go11
-rw-r--r--bridge/gitlab/config.go164
-rw-r--r--bridge/gitlab/export.go24
-rw-r--r--bridge/gitlab/export_test.go13
-rw-r--r--bridge/gitlab/gitlab.go6
-rw-r--r--bridge/gitlab/import.go17
-rw-r--r--bridge/gitlab/import_test.go11
-rw-r--r--bridge/launchpad/config.go33
-rw-r--r--bridge/launchpad/import.go6
-rw-r--r--bridge/launchpad/launchpad.go19
-rw-r--r--bug/status.go2
-rw-r--r--cache/bug_excerpt.go18
-rw-r--r--cache/filter.go3
-rw-r--r--cache/identity_cache.go4
-rw-r--r--cache/identity_excerpt.go17
-rw-r--r--cache/query.go4
-rw-r--r--cache/repo_cache.go91
-rw-r--r--commands/bridge_auth.go26
-rw-r--r--commands/bridge_auth_addtoken.go51
-rw-r--r--commands/bridge_auth_show.go32
-rw-r--r--commands/user.go3
-rw-r--r--commands/user_adopt.go15
-rw-r--r--commands/user_create.go8
-rw-r--r--doc/man/git-bug-bridge-auth-add-token.18
-rw-r--r--doc/md/git-bug_bridge_auth_add-token.md2
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--graphql/graph/gen_graph.go47
-rw-r--r--graphql/resolvers/identity.go3
-rw-r--r--graphql/schema/identity.graphql4
-rw-r--r--graphql/schema/root.graphql2
-rw-r--r--identity/bare.go45
-rw-r--r--identity/bare_test.go1
-rw-r--r--identity/common.go2
-rw-r--r--identity/identity.go67
-rw-r--r--identity/identity_actions_test.go8
-rw-r--r--identity/identity_stub.go4
-rw-r--r--identity/identity_test.go45
-rw-r--r--identity/interface.go7
-rw-r--r--identity/key.go5
-rw-r--r--identity/version.go40
-rw-r--r--identity/version_test.go3
-rw-r--r--input/prompt.go108
-rw-r--r--misc/bash_completion/git-bug8
-rw-r--r--misc/powershell_completion/git-bug4
-rw-r--r--misc/zsh_completion/git-bug4
60 files changed, 786 insertions, 796 deletions
diff --git a/bridge/bridges.go b/bridge/bridges.go
index a306fe5d..5d3066f9 100644
--- a/bridge/bridges.go
+++ b/bridge/bridges.go
@@ -21,6 +21,13 @@ func Targets() []string {
return core.Targets()
}
+// LoginMetaKey return the metadata key used to store the remote bug-tracker login
+// on the user identity. The corresponding value is used to match identities and
+// credentials.
+func LoginMetaKey(target string) (string, error) {
+ return core.LoginMetaKey(target)
+}
+
// Instantiate a new Bridge for a repo, from the given target and name
func NewBridge(repo *cache.RepoCache, target string, name string) (*core.Bridge, error) {
return core.NewBridge(repo, target, name)
diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go
index fd026c5d..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
@@ -32,22 +34,19 @@ func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatc
return entity.NewErrMultipleMatch("credential", matching)
}
-// Special Id to mark a credential as being associated to the default user, whoever it might be.
-// The intended use is for the bridge configuration, to be able to create and store a credential
-// with no identities created yet, and then select one with `git-bug user adopt`
-const DefaultUserId = entity.Id("default-user")
-
type Credential interface {
ID() entity.Id
- UserId() entity.Id
- updateUserId(id entity.Id)
Target() string
Kind() CredentialKind
CreateTime() time.Time
Validate() error
+ Metadata() map[string]string
+ 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.
+ // This does not include Target, Kind, CreateTime and Metadata.
toConfig() map[string]string
}
@@ -120,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 + ".")
@@ -127,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)
}
@@ -185,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 {
@@ -203,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)
@@ -220,25 +235,6 @@ func Remove(repo repository.RepoConfig, id entity.Id) error {
return repo.GlobalConfig().RemoveAll(keyPrefix)
}
-// ReplaceDefaultUser update all the credential attributed to the temporary "default user"
-// with a real user Id
-func ReplaceDefaultUser(repo repository.RepoConfig, id entity.Id) error {
- list, err := List(repo, WithUserId(DefaultUserId))
- if err != nil {
- return err
- }
-
- for _, cred := range list {
- cred.updateUserId(id)
- err = Store(repo, cred)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
/*
* Sorting
*/
diff --git a/bridge/core/auth/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 8333ef12..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,14 +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) updateUserId(id entity.Id) {
- t.userId = id
-}
-
func (t *Token) Target() string {
return t.target
}
@@ -92,6 +83,22 @@ func (t *Token) Validate() error {
return nil
}
+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 f606d2da..ac0d47d7 100644
--- a/bridge/core/bridge.go
+++ b/bridge/core/bridge.go
@@ -28,16 +28,18 @@ 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
@@ -58,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
@@ -80,6 +86,18 @@ func TargetExist(target string) bool {
return ok
}
+// LoginMetaKey return the metadata key used to store the remote bug-tracker login
+// on the user identity. The corresponding value is used to match identities and
+// credentials.
+func LoginMetaKey(target string) (string, error) {
+ metaKey, ok := bridgeLoginMetaKey[target]
+ if !ok {
+ return "", fmt.Errorf("unknown bridge target %v", target)
+ }
+
+ return metaKey, nil
+}
+
// Instantiate a new Bridge for a repo, from the given target and name
func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
implType, ok := bridgeImpl[target]
diff --git a/bridge/core/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/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 ea4b622f..9477801d 100644
--- a/bridge/github/config.go
+++ b/bridge/github/config.go
@@ -14,30 +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/identity"
+ "github.com/MichaelMure/git-bug/input"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/colors"
- "github.com/MichaelMure/git-bug/util/interrupt"
-)
-
-const (
- target = "github"
- githubV3Url = "https://api.github.com"
- keyOwner = "owner"
- keyProject = "project"
-
- defaultTimeout = 60 * time.Second
)
var (
@@ -51,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
@@ -89,15 +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 && err != identity.ErrNoIdentitySet {
- 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
+ }
- // default to a "to be filled" user Id if we don't have a valid one yet
- userId := auth.DefaultUserId
- if user != nil {
- userId = user.Id()
+ login, err = input.Prompt("Github login", "login", input.Required, validator)
+ if err != nil {
+ return nil, err
+ }
}
var cred auth.Credential
@@ -108,13 +97,11 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
if err != nil {
return nil, err
}
- if user != nil && cred.UserId() != user.Id() {
- return nil, fmt.Errorf("selected credential don't match the user")
- }
case params.TokenRaw != "":
- cred = auth.NewToken(userId, params.TokenRaw, target)
+ cred = auth.NewToken(params.TokenRaw, target)
+ cred.SetMetadata(auth.MetaKeyLogin, login)
default:
- cred, err = promptTokenOptions(repo, userId, owner, project)
+ cred, err = promptTokenOptions(repo, login, owner, project)
if err != nil {
return nil, err
}
@@ -151,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 {
@@ -172,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"`
@@ -198,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 != "" {
@@ -242,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
}
@@ -260,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),
)
}
@@ -291,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
}
@@ -315,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
- }
-
- token := strings.TrimSpace(line)
- if re.MatchString(token) {
- return token, nil
+ validator := func(name string, value string) (complaint string, err error) {
+ if re.MatchString(value) {
+ return "", nil
}
-
- fmt.Println("token has incorrect format")
+ return "token has incorrect format", nil
}
+
+ return input.Prompt("Enter token", "token", input.Required, validator)
}
-func loginAndRequestToken(owner, project string) (string, error) {
+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.")
@@ -348,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()
+ i, err := input.PromptChoice("repository visibility", []string{"public", "private"})
if err != nil {
return "", err
}
+ isPublic := i == 0
- username, err := promptUsername()
- if err != nil {
- return "", err
- }
-
- password, err := promptPassword()
+ password, err := input.PromptPassword("Password", "password", input.Required)
if err != nil {
return "", err
}
@@ -377,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
}
@@ -387,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
}
@@ -408,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()
@@ -585,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 39aebccb..ea0ccba3 100644
--- a/bridge/github/import.go
+++ b/bridge/github/import.go
@@ -12,16 +12,9 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/text"
)
-const (
- metaKeyGithubId = "github-id"
- metaKeyGithubUrl = "github-url"
- metaKeyGithubLogin = "github-login"
-)
-
// githubImporter implement the Importer interface
type githubImporter struct {
conf core.Configuration
@@ -39,20 +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()))
- }
- if err == identity.ErrNoIdentitySet {
- opts = append(opts, auth.WithUserId(auth.DefaultUserId))
- }
-
- creds, err := auth.List(repo, opts...)
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
@@ -554,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),
@@ -604,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 57bab61e..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,13 +141,11 @@ func Test_Importer(t *testing.T) {
t.Skip("Env var GITHUB_TOKEN_PRIVATE missing")
}
- err = author.Commit(repo)
- require.NoError(t, err)
-
- err = identity.SetUserIdentity(repo, author)
- require.NoError(t, err)
+ 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 5e345b31..fb593819 100644
--- a/bridge/gitlab/config.go
+++ b/bridge/gitlab/config.go
@@ -19,8 +19,7 @@ import (
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/cache"
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/input"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/colors"
)
@@ -36,14 +35,12 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
if params.Owner != "" {
fmt.Println("warning: --owner is ineffective for a gitlab bridge")
}
+ if params.Login != "" {
+ fmt.Println("warning: --login is ineffective for a gitlab bridge")
+ }
conf := make(core.Configuration)
var err error
-
- if (params.CredPrefix != "" || params.TokenRaw != "") && params.URL == "" {
- return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
- }
-
var baseUrl string
switch {
@@ -74,17 +71,6 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, url)
}
- user, err := repo.GetUserIdentity()
- if err != nil && err != identity.ErrNoIdentitySet {
- return nil, err
- }
-
- // default to a "to be filled" user Id if we don't have a valid one yet
- userId := auth.DefaultUserId
- if user != nil {
- userId = user.Id()
- }
-
var cred auth.Credential
switch {
@@ -93,13 +79,16 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
if err != nil {
return nil, err
}
- if user != nil && cred.UserId() != user.Id() {
- return nil, fmt.Errorf("selected credential don't match the user")
- }
case params.TokenRaw != "":
- cred = auth.NewToken(userId, params.TokenRaw, target)
+ token := auth.NewToken(params.TokenRaw, target)
+ login, err := getLoginFromToken(baseUrl, token)
+ if err != nil {
+ return nil, err
+ }
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ cred = token
default:
- cred, err = promptTokenOptions(repo, userId, baseUrl)
+ cred, err = promptTokenOptions(repo, baseUrl)
if err != nil {
return nil, err
}
@@ -153,77 +142,50 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
}
func promptBaseUrlOptions() (string, error) {
- for {
- fmt.Printf("Gitlab base url:\n")
- fmt.Printf("[0]: https://gitlab.com\n")
- fmt.Printf("[1]: enter your own base url\n")
- fmt.Printf("Select option: ")
-
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
- if err != nil {
- return "", err
- }
+ index, err := input.PromptChoice("Gitlab base url", []string{
+ "https://gitlab.com",
+ "enter your own base url",
+ })
- line = strings.TrimSpace(line)
-
- index, err := strconv.Atoi(line)
- if err != nil || index < 0 || index > 1 {
- fmt.Println("invalid input")
- continue
- }
+ if err != nil {
+ return "", err
+ }
- switch index {
- case 0:
- return defaultBaseURL, nil
- case 1:
- return promptBaseUrl()
- }
+ if index == 0 {
+ return defaultBaseURL, nil
+ } else {
+ return promptBaseUrl()
}
}
func promptBaseUrl() (string, error) {
- for {
- fmt.Print("Base url: ")
-
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+ validator := func(name string, value string) (string, error) {
+ u, err := url.Parse(value)
if err != nil {
- return "", err
+ return err.Error(), nil
}
-
- line = strings.TrimSpace(line)
-
- ok, err := validateBaseUrl(line)
- if err != nil {
- return "", err
+ if u.Scheme == "" {
+ return "missing scheme", nil
}
- if ok {
- return line, nil
+ if u.Host == "" {
+ return "missing host", nil
}
+ return "", nil
}
-}
-func validateBaseUrl(baseUrl string) (bool, error) {
- u, err := url.Parse(baseUrl)
- if err != nil {
- return false, err
- }
- return u.Scheme != "" && u.Host != "", nil
+ return input.Prompt("Base url", "url", input.Required, validator)
}
-func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, baseUrl string) (auth.Credential, error) {
+func promptTokenOptions(repo repository.RepoConfig, baseUrl string) (auth.Credential, error) {
for {
- creds, err := auth.List(repo, auth.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(baseUrl)
- if err != nil {
- return nil, err
- }
- return auth.NewToken(userId, value, target), nil
+ return promptToken(baseUrl)
}
fmt.Println()
@@ -261,44 +223,47 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, baseUrl st
switch index {
case 1:
- value, err := promptToken(baseUrl)
- if err != nil {
- return nil, err
- }
- return auth.NewToken(userId, value, target), nil
+ return promptToken(baseUrl)
default:
return creds[index-2], nil
}
}
}
-func promptToken(baseUrl string) (string, error) {
+func promptToken(baseUrl string) (*auth.Token, error) {
fmt.Printf("You can generate a new token by visiting %s.\n", path.Join(baseUrl, "profile/personal_access_tokens"))
fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
fmt.Println()
fmt.Println("'api' access scope: to be able to make api calls")
fmt.Println()
- re, err := regexp.Compile(`^[a-zA-Z0-9\-\_]{20}`)
+ re, err := regexp.Compile(`^[a-zA-Z0-9\-\_]{20}$`)
if err != nil {
panic("regexp compile:" + err.Error())
}
- for {
- fmt.Print("Enter token: ")
+ var login string
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
- 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 has incorrect format")
+ 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, baseUrl string) (string, error) {
@@ -408,8 +373,25 @@ func validateProjectURL(baseUrl, url string, token *auth.Token) (int, error) {
project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
if err != nil {
- return 0, errors.Wrap(err, "wrong token scope ou inexistent project")
+ return 0, errors.Wrap(err, "wrong token scope ou non-existent project")
}
return project.ID, nil
}
+
+func getLoginFromToken(baseUrl string, token *auth.Token) (string, error) {
+ client, err := buildClient(baseUrl, token)
+ if err != nil {
+ return "", err
+ }
+
+ user, _, err := client.Users.CurrentUser()
+ if err != nil {
+ return "", err
+ }
+ if user.Username == "" {
+ return "", fmt.Errorf("gitlab say username is empty")
+ }
+
+ return user.Username, nil
+}
diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go
index 2ba149a2..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
}
}
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 fa6bbfb6..d699554b 100644
--- a/bridge/gitlab/import.go
+++ b/bridge/gitlab/import.go
@@ -13,7 +13,6 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/text"
)
@@ -34,20 +33,7 @@ type gitlabImporter struct {
func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
gi.conf = conf
- opts := []auth.Option{
- auth.WithTarget(target),
- auth.WithKind(auth.KindToken),
- }
-
- user, err := repo.GetUserIdentity()
- if err == nil {
- opts = append(opts, auth.WithUserId(user.Id()))
- }
- if err == identity.ErrNoIdentitySet {
- opts = append(opts, auth.WithUserId(auth.DefaultUserId))
- }
-
- creds, err := auth.List(repo, opts...)
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
@@ -403,7 +389,6 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id
i, err = repo.NewIdentityRaw(
user.Name,
user.PublicEmail,
- user.Username,
user.AvatarURL,
map[string]string{
// because Gitlab
diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go
index 1e2f5d50..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,13 +95,11 @@ func TestImport(t *testing.T) {
t.Skip("Env var GITLAB_PROJECT_ID missing")
}
- err = author.Commit(repo)
- require.NoError(t, err)
-
- err = identity.SetUserIdentity(repo, author)
- require.NoError(t, err)
+ login := "test-identity"
+ author.SetMetadata(metaKeyGitlabLogin, login)
- token := auth.NewToken(author.Id(), envToken, target)
+ token := auth.NewToken(envToken, target)
+ token.SetMetadata(metaKeyGitlabLogin, login)
err = auth.Store(repo, token)
require.NoError(t, err)
diff --git a/bridge/launchpad/config.go b/bridge/launchpad/config.go
index 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{}
}
diff --git a/bug/status.go b/bug/status.go
index 737c8d31..9e998034 100644
--- a/bug/status.go
+++ b/bug/status.go
@@ -44,7 +44,7 @@ func StatusFromString(str string) (Status, error) {
case "closed":
return ClosedStatus, nil
default:
- return 0, fmt.Errorf("unknow status")
+ return 0, fmt.Errorf("unknown status")
}
}
diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go
index 36c7dcfe..10e522f9 100644
--- a/cache/bug_excerpt.go
+++ b/cache/bug_excerpt.go
@@ -2,7 +2,6 @@ package cache
import (
"encoding/gob"
- "fmt"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity"
@@ -43,21 +42,11 @@ type BugExcerpt struct {
// identity.Bare data are directly embedded in the bug excerpt
type LegacyAuthorExcerpt struct {
- Name string
- Login string
+ Name string
}
func (l LegacyAuthorExcerpt) DisplayName() string {
- switch {
- case l.Name == "" && l.Login != "":
- return l.Login
- case l.Name != "" && l.Login == "":
- return l.Name
- case l.Name != "" && l.Login != "":
- return fmt.Sprintf("%s (%s)", l.Name, l.Login)
- }
-
- panic("invalid person data")
+ return l.Name
}
func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
@@ -95,8 +84,7 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
e.AuthorId = snap.Author.Id()
case *identity.Bare:
e.LegacyAuthor = LegacyAuthorExcerpt{
- Login: snap.Author.Login(),
- Name: snap.Author.Name(),
+ Name: snap.Author.Name(),
}
default:
panic("unhandled identity type")
diff --git a/cache/filter.go b/cache/filter.go
index 27e92cf3..9b1de1d5 100644
--- a/cache/filter.go
+++ b/cache/filter.go
@@ -37,8 +37,7 @@ func AuthorFilter(query string) Filter {
}
// Legacy identity support
- return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query) ||
- strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Login), query)
+ return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query)
}
}
diff --git a/cache/identity_cache.go b/cache/identity_cache.go
index 2ae55f2d..eb5ee183 100644
--- a/cache/identity_cache.go
+++ b/cache/identity_cache.go
@@ -21,8 +21,8 @@ func (i *IdentityCache) notifyUpdated() error {
return i.repoCache.identityUpdated(i.Identity.Id())
}
-func (i *IdentityCache) AddVersion(version *identity.Version) error {
- i.Identity.AddVersion(version)
+func (i *IdentityCache) Mutate(f func(identity.Mutator) identity.Mutator) error {
+ i.Identity.Mutate(f)
return i.notifyUpdated()
}
diff --git a/cache/identity_excerpt.go b/cache/identity_excerpt.go
index 18514e9a..06788aa5 100644
--- a/cache/identity_excerpt.go
+++ b/cache/identity_excerpt.go
@@ -2,7 +2,6 @@ package cache
import (
"encoding/gob"
- "fmt"
"strings"
"github.com/MichaelMure/git-bug/entity"
@@ -21,7 +20,6 @@ type IdentityExcerpt struct {
Id entity.Id
Name string
- Login string
ImmutableMetadata map[string]string
}
@@ -29,7 +27,6 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt {
return &IdentityExcerpt{
Id: i.Id(),
Name: i.Name(),
- Login: i.Login(),
ImmutableMetadata: i.ImmutableMetadata(),
}
}
@@ -37,23 +34,13 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt {
// DisplayName return a non-empty string to display, representing the
// identity, based on the non-empty values.
func (i *IdentityExcerpt) DisplayName() string {
- switch {
- case i.Name == "" && i.Login != "":
- return i.Login
- case i.Name != "" && i.Login == "":
- return i.Name
- case i.Name != "" && i.Login != "":
- return fmt.Sprintf("%s (%s)", i.Name, i.Login)
- }
-
- panic("invalid person data")
+ return i.Name
}
// Match matches a query with the identity name, login and ID prefixes
func (i *IdentityExcerpt) Match(query string) bool {
return i.Id.HasPrefix(query) ||
- strings.Contains(strings.ToLower(i.Name), query) ||
- strings.Contains(strings.ToLower(i.Login), query)
+ strings.Contains(strings.ToLower(i.Name), query)
}
/*
diff --git a/cache/query.go b/cache/query.go
index 633ef1c2..967c18d6 100644
--- a/cache/query.go
+++ b/cache/query.go
@@ -91,7 +91,7 @@ func ParseQuery(query string) (*Query, error) {
sortingDone = true
default:
- return nil, fmt.Errorf("unknow qualifier name %s", qualifierName)
+ return nil, fmt.Errorf("unknown qualifier name %s", qualifierName)
}
}
@@ -165,7 +165,7 @@ func (q *Query) parseSorting(query string) error {
q.OrderDirection = OrderAscending
default:
- return fmt.Errorf("unknow sorting %s", query)
+ return fmt.Errorf("unknown sorting %s", query)
}
return nil
diff --git a/cache/repo_cache.go b/cache/repo_cache.go
index 90a489c8..18be9b5a 100644
--- a/cache/repo_cache.go
+++ b/cache/repo_cache.go
@@ -409,36 +409,27 @@ func (c *RepoCache) ResolveBugExcerpt(id entity.Id) (*BugExcerpt, error) {
// ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
// bugs match.
func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
- // preallocate but empty
- matching := make([]entity.Id, 0, 5)
-
- for id := range c.bugExcerpts {
- if id.HasPrefix(prefix) {
- matching = append(matching, id)
- }
- }
-
- if len(matching) > 1 {
- return nil, bug.NewErrMultipleMatchBug(matching)
- }
-
- if len(matching) == 0 {
- return nil, bug.ErrBugNotExist
- }
-
- return c.ResolveBug(matching[0])
+ return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
+ return excerpt.Id.HasPrefix(prefix)
+ })
}
// ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
// its Create operation, that is, the first operation. It fails if multiple bugs
// match.
func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
+ return c.ResolveBugMatcher(func(excerpt *BugExcerpt) bool {
+ return excerpt.CreateMetadata[key] == value
+ })
+}
+
+func (c *RepoCache) ResolveBugMatcher(f func(*BugExcerpt) bool) (*BugCache, error) {
// preallocate but empty
matching := make([]entity.Id, 0, 5)
- for id, excerpt := range c.bugExcerpts {
- if excerpt.CreateMetadata[key] == value {
- matching = append(matching, id)
+ for _, excerpt := range c.bugExcerpts {
+ if f(excerpt) {
+ matching = append(matching, excerpt.Id)
}
}
@@ -785,35 +776,26 @@ func (c *RepoCache) ResolveIdentityExcerpt(id entity.Id) (*IdentityExcerpt, erro
// ResolveIdentityPrefix retrieve an Identity matching an id prefix.
// It fails if multiple identities match.
func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) {
- // preallocate but empty
- matching := make([]entity.Id, 0, 5)
-
- for id := range c.identitiesExcerpts {
- if id.HasPrefix(prefix) {
- matching = append(matching, id)
- }
- }
-
- if len(matching) > 1 {
- return nil, identity.NewErrMultipleMatch(matching)
- }
-
- if len(matching) == 0 {
- return nil, identity.ErrIdentityNotExist
- }
-
- return c.ResolveIdentity(matching[0])
+ return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
+ return excerpt.Id.HasPrefix(prefix)
+ })
}
// ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
// one of it's version. If multiple version have the same key, the first defined take precedence.
func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) {
+ return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
+ return excerpt.ImmutableMetadata[key] == value
+ })
+}
+
+func (c *RepoCache) ResolveIdentityMatcher(f func(*IdentityExcerpt) bool) (*IdentityCache, error) {
// preallocate but empty
matching := make([]entity.Id, 0, 5)
- for id, i := range c.identitiesExcerpts {
- if i.ImmutableMetadata[key] == value {
- matching = append(matching, id)
+ for _, excerpt := range c.identitiesExcerpts {
+ if f(excerpt) {
+ matching = append(matching, excerpt.Id)
}
}
@@ -881,21 +863,36 @@ func (c *RepoCache) IsUserIdentitySet() (bool, error) {
return identity.IsUserIdentitySet(c.repo)
}
+func (c *RepoCache) NewIdentityFromGitUser() (*IdentityCache, error) {
+ return c.NewIdentityFromGitUserRaw(nil)
+}
+
+func (c *RepoCache) NewIdentityFromGitUserRaw(metadata map[string]string) (*IdentityCache, error) {
+ i, err := identity.NewFromGitUser(c.repo)
+ if err != nil {
+ return nil, err
+ }
+ return c.finishIdentity(i, metadata)
+}
+
// NewIdentity create a new identity
// The new identity is written in the repository (commit)
func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) {
- return c.NewIdentityRaw(name, email, "", "", nil)
+ return c.NewIdentityRaw(name, email, "", nil)
}
// NewIdentityFull create a new identity
// The new identity is written in the repository (commit)
-func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*IdentityCache, error) {
- return c.NewIdentityRaw(name, email, login, avatarUrl, nil)
+func (c *RepoCache) NewIdentityFull(name string, email string, avatarUrl string) (*IdentityCache, error) {
+ return c.NewIdentityRaw(name, email, avatarUrl, nil)
}
-func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) {
- i := identity.NewIdentityFull(name, email, login, avatarUrl)
+func (c *RepoCache) NewIdentityRaw(name string, email string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) {
+ i := identity.NewIdentityFull(name, email, avatarUrl)
+ return c.finishIdentity(i, metadata)
+}
+func (c *RepoCache) finishIdentity(i *identity.Identity, metadata map[string]string) (*IdentityCache, error) {
for key, value := range metadata {
i.SetMetadata(key, value)
}
diff --git a/commands/bridge_auth.go b/commands/bridge_auth.go
index bfbab33c..3a0e0c29 100644
--- a/commands/bridge_auth.go
+++ b/commands/bridge_auth.go
@@ -2,6 +2,8 @@ package commands
import (
"fmt"
+ "sort"
+ "strings"
"github.com/spf13/cobra"
@@ -26,8 +28,6 @@ func runBridgeAuth(cmd *cobra.Command, args []string) error {
return err
}
- defaultUser, _ := backend.GetUserIdentity()
-
for _, cred := range creds {
targetFmt := text.LeftPadMaxLine(cred.Target(), 10, 0)
@@ -37,29 +37,19 @@ func runBridgeAuth(cmd *cobra.Command, args []string) error {
value = cred.Value
}
- var userFmt string
-
- switch cred.UserId() {
- case auth.DefaultUserId:
- userFmt = colors.Red("default user")
- default:
- user, err := backend.ResolveIdentity(cred.UserId())
- if err != nil {
- return err
- }
- userFmt = user.DisplayName()
-
- if cred.UserId() == defaultUser.Id() {
- userFmt = colors.Red(userFmt)
- }
+ meta := make([]string, 0, len(cred.Metadata()))
+ for k, v := range cred.Metadata() {
+ meta = append(meta, k+":"+v)
}
+ sort.Strings(meta)
+ metaFmt := strings.Join(meta, ",")
fmt.Printf("%s %s %s %s %s\n",
colors.Cyan(cred.ID().Human()),
colors.Yellow(targetFmt),
colors.Magenta(cred.Kind()),
- userFmt,
value,
+ metaFmt,
)
}
diff --git a/commands/bridge_auth_addtoken.go b/commands/bridge_auth_addtoken.go
index 018015e4..9a937f4d 100644
--- a/commands/bridge_auth_addtoken.go
+++ b/commands/bridge_auth_addtoken.go
@@ -13,24 +13,37 @@ import (
"github.com/MichaelMure/git-bug/bridge"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth"
- "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/cache"
+ "github.com/MichaelMure/git-bug/util/interrupt"
)
var (
bridgeAuthAddTokenTarget string
+ bridgeAuthAddTokenLogin string
+ bridgeAuthAddTokenUser string
)
func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
- var value string
-
if bridgeAuthAddTokenTarget == "" {
return fmt.Errorf("flag --target is required")
}
+ if bridgeAuthAddTokenLogin == "" {
+ return fmt.Errorf("flag --login is required")
+ }
+
+ backend, err := cache.NewRepoCache(repo)
+ if err != nil {
+ return err
+ }
+ defer backend.Close()
+ interrupt.RegisterCleaner(backend.Close)
if !core.TargetExist(bridgeAuthAddTokenTarget) {
return fmt.Errorf("unknown target")
}
+ var value string
+
if len(args) == 1 {
value = args[0]
} else {
@@ -46,12 +59,36 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
value = strings.TrimSuffix(raw, "\n")
}
- user, err := identity.GetUserIdentity(repo)
+ var user *cache.IdentityCache
+
+ if bridgeAuthAddTokenUser == "" {
+ user, err = backend.GetUserIdentity()
+ } else {
+ user, err = backend.ResolveIdentityPrefix(bridgeAuthAddTokenUser)
+ }
if err != nil {
return err
}
- token := auth.NewToken(user.Id(), value, bridgeAuthAddTokenTarget)
+ metaKey, _ := bridge.LoginMetaKey(bridgeAuthAddTokenTarget)
+ login, ok := user.ImmutableMetadata()[metaKey]
+
+ switch {
+ case ok && login == bridgeAuthAddTokenLogin:
+ // nothing to do
+ case ok && login != bridgeAuthAddTokenLogin:
+ return fmt.Errorf("this user is already tagged with a different %s login", bridgeAuthAddTokenTarget)
+ default:
+ user.SetMetadata(metaKey, bridgeAuthAddTokenLogin)
+ err = user.Commit()
+ if err != nil {
+ return err
+ }
+ }
+
+ token := auth.NewToken(value, bridgeAuthAddTokenTarget)
+ token.SetMetadata(auth.MetaKeyLogin, bridgeAuthAddTokenLogin)
+
if err := token.Validate(); err != nil {
return errors.Wrap(err, "invalid token")
}
@@ -77,5 +114,9 @@ func init() {
bridgeAuthCmd.AddCommand(bridgeAuthAddTokenCmd)
bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenTarget, "target", "t", "",
fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
+ bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenLogin,
+ "login", "l", "", "The login in the remote bug-tracker")
+ bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenUser,
+ "user", "u", "", "The user to add the token to. Default is the current user")
bridgeAuthAddTokenCmd.Flags().SortFlags = false
}
diff --git a/commands/bridge_auth_show.go b/commands/bridge_auth_show.go
index 02c56806..fbbf60a7 100644
--- a/commands/bridge_auth_show.go
+++ b/commands/bridge_auth_show.go
@@ -2,13 +2,14 @@ package commands
import (
"fmt"
+ "sort"
+ "strings"
"time"
"github.com/spf13/cobra"
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/cache"
- "github.com/MichaelMure/git-bug/util/colors"
"github.com/MichaelMure/git-bug/util/interrupt"
)
@@ -25,28 +26,9 @@ func runBridgeAuthShow(cmd *cobra.Command, args []string) error {
return err
}
- var userFmt string
-
- switch cred.UserId() {
- case auth.DefaultUserId:
- userFmt = colors.Red("default user")
- default:
- user, err := backend.ResolveIdentity(cred.UserId())
- if err != nil {
- return err
- }
- userFmt = user.DisplayName()
-
- defaultUser, _ := backend.GetUserIdentity()
- if cred.UserId() == defaultUser.Id() {
- userFmt = colors.Red(userFmt)
- }
- }
-
fmt.Printf("Id: %s\n", cred.ID())
fmt.Printf("Target: %s\n", cred.Target())
fmt.Printf("Kind: %s\n", cred.Kind())
- fmt.Printf("User: %s\n", userFmt)
fmt.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822))
switch cred := cred.(type) {
@@ -54,6 +36,16 @@ func runBridgeAuthShow(cmd *cobra.Command, args []string) error {
fmt.Printf("Value: %s\n", cred.Value)
}
+ fmt.Println("Metadata:")
+
+ meta := make([]string, 0, len(cred.Metadata()))
+ for key, value := range cred.Metadata() {
+ meta = append(meta, fmt.Sprintf(" %s --> %s\n", key, value))
+ }
+ sort.Strings(meta)
+
+ fmt.Print(strings.Join(meta, ""))
+
return nil
}
diff --git a/commands/user.go b/commands/user.go
index f669c73f..5cf40cf0 100644
--- a/commands/user.go
+++ b/commands/user.go
@@ -50,8 +50,6 @@ func runUser(cmd *cobra.Command, args []string) error {
Time().Format("Mon Jan 2 15:04:05 2006 +0200"))
case "lastModificationLamport":
fmt.Printf("%d\n", id.LastModificationLamport())
- case "login":
- fmt.Printf("%s\n", id.Login())
case "metadata":
for key, value := range id.ImmutableMetadata() {
fmt.Printf("%s\n%s\n", key, value)
@@ -68,7 +66,6 @@ func runUser(cmd *cobra.Command, args []string) error {
fmt.Printf("Id: %s\n", id.Id())
fmt.Printf("Name: %s\n", id.Name())
- fmt.Printf("Login: %s\n", id.Login())
fmt.Printf("Email: %s\n", id.Email())
fmt.Printf("Last modification: %s (lamport %d)\n",
id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200"),
diff --git a/commands/user_adopt.go b/commands/user_adopt.go
index a7de54d9..7054f1f7 100644
--- a/commands/user_adopt.go
+++ b/commands/user_adopt.go
@@ -4,11 +4,10 @@ import (
"fmt"
"os"
- "github.com/MichaelMure/git-bug/bridge/core/auth"
+ "github.com/spf13/cobra"
+
"github.com/MichaelMure/git-bug/cache"
- "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/interrupt"
- "github.com/spf13/cobra"
)
func runUserAdopt(cmd *cobra.Command, args []string) error {
@@ -26,16 +25,6 @@ func runUserAdopt(cmd *cobra.Command, args []string) error {
return err
}
- _, err = backend.GetUserIdentity()
- if err == identity.ErrNoIdentitySet {
- err = auth.ReplaceDefaultUser(repo, i.Id())
- if err != nil {
- return err
- }
- } else if err != nil {
- return err
- }
-
err = backend.SetUserIdentity(i)
if err != nil {
return err
diff --git a/commands/user_create.go b/commands/user_create.go
index 15b9767e..95e09050 100644
--- a/commands/user_create.go
+++ b/commands/user_create.go
@@ -23,7 +23,7 @@ func runUserCreate(cmd *cobra.Command, args []string) error {
return err
}
- name, err := input.PromptValueRequired("Name", preName)
+ name, err := input.PromptDefault("Name", "name", preName, input.Required)
if err != nil {
return err
}
@@ -33,17 +33,17 @@ func runUserCreate(cmd *cobra.Command, args []string) error {
return err
}
- email, err := input.PromptValueRequired("Email", preEmail)
+ email, err := input.PromptDefault("Email", "email", preEmail, input.Required)
if err != nil {
return err
}
- login, err := input.PromptValue("Avatar URL", "")
+ avatarURL, err := input.Prompt("Avatar URL", "avatar")
if err != nil {
return err
}
- id, err := backend.NewIdentityRaw(name, email, "", login, nil)
+ id, err := backend.NewIdentityRaw(name, email, avatarURL, nil)
if err != nil {
return err
}
diff --git a/doc/man/git-bug-bridge-auth-add-token.1 b/doc/man/git-bug-bridge-auth-add-token.1
index a76ed793..c9ca55d6 100644
--- a/doc/man/git-bug-bridge-auth-add-token.1
+++ b/doc/man/git-bug-bridge-auth-add-token.1
@@ -24,6 +24,14 @@ Store a new token
The target of the bridge. Valid values are [github,gitlab,launchpad\-preview]
.PP
+\fB\-l\fP, \fB\-\-login\fP=""
+ The login in the remote bug\-tracker
+
+.PP
+\fB\-u\fP, \fB\-\-user\fP=""
+ The user to add the token to. Default is the current user
+
+.PP
\fB\-h\fP, \fB\-\-help\fP[=false]
help for add\-token
diff --git a/doc/md/git-bug_bridge_auth_add-token.md b/doc/md/git-bug_bridge_auth_add-token.md
index 7067c3ca..496455a0 100644
--- a/doc/md/git-bug_bridge_auth_add-token.md
+++ b/doc/md/git-bug_bridge_auth_add-token.md
@@ -14,6 +14,8 @@ git-bug bridge auth add-token [<token>] [flags]
```
-t, --target string The target of the bridge. Valid values are [github,gitlab,launchpad-preview]
+ -l, --login string The login in the remote bug-tracker
+ -u, --user string The user to add the token to. Default is the current user
-h, --help help for add-token
```
diff --git a/go.mod b/go.mod
index 56e91373..2c5d5e47 100644
--- a/go.mod
+++ b/go.mod
@@ -25,7 +25,7 @@ require (
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.0
github.com/theckman/goconstraint v1.11.0
- github.com/vektah/gqlparser v1.2.1
+ github.com/vektah/gqlparser v1.3.1
github.com/xanzy/go-gitlab v0.24.0
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288
diff --git a/go.sum b/go.sum
index b5771cee..8fc1e290 100644
--- a/go.sum
+++ b/go.sum
@@ -131,6 +131,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser v1.2.1 h1:C+L7Go/eUbN0w6Y0kaiq2W6p2wN5j8wU82EdDXxDivc=
github.com/vektah/gqlparser v1.2.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
+github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU=
+github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
github.com/xanzy/go-gitlab v0.22.1 h1:TVxgHmoa35jQL+9FCkG0nwPDxU9dQZXknBTDtGaSFno=
github.com/xanzy/go-gitlab v0.22.1/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
github.com/xanzy/go-gitlab v0.24.0 h1:zP1zC4K76Gha0coN5GhygOLhsHTCvUjrnqGL3kHXkVU=
diff --git a/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go
index 215603cb..5e882142 100644
--- a/graphql/graph/gen_graph.go
+++ b/graphql/graph/gen_graph.go
@@ -210,7 +210,6 @@ type ComplexityRoot struct {
HumanID func(childComplexity int) int
ID func(childComplexity int) int
IsProtected func(childComplexity int) int
- Login func(childComplexity int) int
Name func(childComplexity int) int
}
@@ -1139,13 +1138,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Identity.IsProtected(childComplexity), true
- case "Identity.login":
- if e.complexity.Identity.Login == nil {
- break
- }
-
- return e.complexity.Identity.Login(childComplexity), true
-
case "Identity.name":
if e.complexity.Identity.Name == nil {
break
@@ -2319,11 +2311,7 @@ type Identity {
"""
email: String
"""
- The login of the person, if known.
- """
- login: String
- """
- A string containing the either the name of the person, its login or both
+ A non-empty string to display, representing the identity, based on the non-empty values.
"""
displayName: String!
"""
@@ -6215,37 +6203,6 @@ func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.C
return ec.marshalOString2string(ctx, field.Selections, res)
}
-func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj identity.Interface) (ret graphql.Marshaler) {
- defer func() {
- if r := recover(); r != nil {
- ec.Error(ctx, ec.Recover(ctx, r))
- ret = graphql.Null
- }
- }()
- fc := &graphql.FieldContext{
- Object: "Identity",
- Field: field,
- Args: nil,
- IsMethod: true,
- }
-
- ctx = graphql.WithFieldContext(ctx, fc)
- resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
- ctx = rctx // use context from middleware stack in children
- return obj.Login(), nil
- })
- if err != nil {
- ec.Error(ctx, err)
- return graphql.Null
- }
- if resTmp == nil {
- return graphql.Null
- }
- res := resTmp.(string)
- fc.Result = res
- return ec.marshalOString2string(ctx, field.Selections, res)
-}
-
func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj identity.Interface) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -11946,8 +11903,6 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet,
out.Values[i] = ec._Identity_name(ctx, field, obj)
case "email":
out.Values[i] = ec._Identity_email(ctx, field, obj)
- case "login":
- out.Values[i] = ec._Identity_login(ctx, field, obj)
case "displayName":
out.Values[i] = ec._Identity_displayName(ctx, field, obj)
if out.Values[i] == graphql.Null {
diff --git a/graphql/resolvers/identity.go b/graphql/resolvers/identity.go
index da8e7b08..d36669d0 100644
--- a/graphql/resolvers/identity.go
+++ b/graphql/resolvers/identity.go
@@ -15,6 +15,7 @@ func (identityResolver) ID(ctx context.Context, obj identity.Interface) (string,
return obj.Id().String(), nil
}
-func (identityResolver) HumanID(ctx context.Context, obj identity.Interface) (string, error) {
+func (r identityResolver) HumanID(ctx context.Context, obj identity.Interface) (string, error) {
return obj.Id().Human(), nil
+
}
diff --git a/graphql/schema/identity.graphql b/graphql/schema/identity.graphql
index 6872ecb9..6490d538 100644
--- a/graphql/schema/identity.graphql
+++ b/graphql/schema/identity.graphql
@@ -8,9 +8,7 @@ type Identity {
name: String
"""The email of the person, if known."""
email: String
- """The login of the person, if known."""
- login: String
- """A string containing the either the name of the person, its login or both"""
+ """A non-empty string to display, representing the identity, based on the non-empty values."""
displayName: String!
"""An url to an avatar"""
avatarUrl: String
diff --git a/graphql/schema/root.graphql b/graphql/schema/root.graphql
index f66272ca..2a12cc37 100644
--- a/graphql/schema/root.graphql
+++ b/graphql/schema/root.graphql
@@ -3,6 +3,8 @@ type Query {
defaultRepository: Repository
"""Access a repository by reference/name."""
repository(ref: String!): Repository
+
+ #TODO: connection for all repositories
}
type Mutation {
diff --git a/identity/bare.go b/identity/bare.go
index a243f074..a02ec790 100644
--- a/identity/bare.go
+++ b/identity/bare.go
@@ -25,7 +25,6 @@ type Bare struct {
id entity.Id
name string
email string
- login string
avatarUrl string
}
@@ -33,8 +32,8 @@ func NewBare(name string, email string) *Bare {
return &Bare{id: entity.UnsetId, name: name, email: email}
}
-func NewBareFull(name string, email string, login string, avatarUrl string) *Bare {
- return &Bare{id: entity.UnsetId, name: name, email: email, login: login, avatarUrl: avatarUrl}
+func NewBareFull(name string, email string, avatarUrl string) *Bare {
+ return &Bare{id: entity.UnsetId, name: name, email: email, avatarUrl: avatarUrl}
}
func deriveId(data []byte) entity.Id {
@@ -45,7 +44,7 @@ func deriveId(data []byte) entity.Id {
type bareIdentityJSON struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
- Login string `json:"login,omitempty"`
+ Login string `json:"login,omitempty"` // Deprecated, only kept to have the same ID when reading an old value
AvatarUrl string `json:"avatar_url,omitempty"`
}
@@ -53,7 +52,6 @@ func (i *Bare) MarshalJSON() ([]byte, error) {
return json.Marshal(bareIdentityJSON{
Name: i.name,
Email: i.email,
- Login: i.login,
AvatarUrl: i.avatarUrl,
})
}
@@ -70,7 +68,6 @@ func (i *Bare) UnmarshalJSON(data []byte) error {
i.name = aux.Name
i.email = aux.Email
- i.login = aux.Login
i.avatarUrl = aux.AvatarUrl
return nil
@@ -109,45 +106,31 @@ func (i *Bare) Email() string {
return i.email
}
-// Login return the last version of the login
-func (i *Bare) Login() string {
- return i.login
-}
-
// AvatarUrl return the last version of the Avatar URL
func (i *Bare) AvatarUrl() string {
return i.avatarUrl
}
// Keys return the last version of the valid keys
-func (i *Bare) Keys() []Key {
- return []Key{}
+func (i *Bare) Keys() []*Key {
+ return nil
}
// ValidKeysAtTime return the set of keys valid at a given lamport time
-func (i *Bare) ValidKeysAtTime(time lamport.Time) []Key {
- return []Key{}
+func (i *Bare) ValidKeysAtTime(_ lamport.Time) []*Key {
+ return nil
}
// DisplayName return a non-empty string to display, representing the
// identity, based on the non-empty values.
func (i *Bare) DisplayName() string {
- switch {
- case i.name == "" && i.login != "":
- return i.login
- case i.name != "" && i.login == "":
- return i.name
- case i.name != "" && i.login != "":
- return fmt.Sprintf("%s (%s)", i.name, i.login)
- }
-
- panic("invalid person data")
+ return i.name
}
// Validate check if the Identity data is valid
func (i *Bare) Validate() error {
- if text.Empty(i.name) && text.Empty(i.login) {
- return fmt.Errorf("either name or login should be set")
+ if text.Empty(i.name) {
+ return fmt.Errorf("name is not set")
}
if strings.Contains(i.name, "\n") {
@@ -158,14 +141,6 @@ func (i *Bare) Validate() error {
return fmt.Errorf("name is not fully printable")
}
- if strings.Contains(i.login, "\n") {
- return fmt.Errorf("login should be a single line")
- }
-
- if !text.Safe(i.login) {
- return fmt.Errorf("login is not fully printable")
- }
-
if strings.Contains(i.email, "\n") {
return fmt.Errorf("email should be a single line")
}
diff --git a/identity/bare_test.go b/identity/bare_test.go
index 335c8d37..5aa50e40 100644
--- a/identity/bare_test.go
+++ b/identity/bare_test.go
@@ -18,7 +18,6 @@ func TestBare_Id(t *testing.T) {
func TestBareSerialize(t *testing.T) {
before := &Bare{
- login: "login",
email: "email",
name: "name",
avatarUrl: "avatar",
diff --git a/identity/common.go b/identity/common.go
index 007e10d6..0fd2b274 100644
--- a/identity/common.go
+++ b/identity/common.go
@@ -37,7 +37,7 @@ func UnmarshalJSON(raw json.RawMessage) (Interface, error) {
b := &Bare{}
err = json.Unmarshal(raw, b)
- if err == nil && (b.name != "" || b.login != "") {
+ if err == nil && b.name != "" {
return b, nil
}
diff --git a/identity/identity.go b/identity/identity.go
index cd47c1b7..c33a8818 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"os"
+ "reflect"
"strings"
"time"
@@ -55,14 +56,13 @@ func NewIdentity(name string, email string) *Identity {
}
}
-func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity {
+func NewIdentityFull(name string, email string, avatarUrl string) *Identity {
return &Identity{
id: entity.UnsetId,
versions: []*Version{
{
name: name,
email: email,
- login: login,
avatarURL: avatarUrl,
nonce: makeNonce(20),
},
@@ -271,8 +271,31 @@ func IsUserIdentitySet(repo repository.Repo) (bool, error) {
return len(configs) == 1, nil
}
-func (i *Identity) AddVersion(version *Version) {
- i.versions = append(i.versions, version)
+type Mutator struct {
+ Name string
+ Email string
+ AvatarUrl string
+ Keys []*Key
+}
+
+// Mutate allow to create a new version of the Identity
+func (i *Identity) Mutate(f func(orig Mutator) Mutator) {
+ orig := Mutator{
+ Name: i.Name(),
+ Email: i.Email(),
+ AvatarUrl: i.AvatarUrl(),
+ Keys: i.Keys(),
+ }
+ mutated := f(orig)
+ if reflect.DeepEqual(orig, mutated) {
+ return
+ }
+ i.versions = append(i.versions, &Version{
+ name: mutated.Name,
+ email: mutated.Email,
+ avatarURL: mutated.AvatarUrl,
+ keys: mutated.Keys,
+ })
}
// Write the identity into the Repository. In particular, this ensure that
@@ -478,24 +501,19 @@ func (i *Identity) Email() string {
return i.lastVersion().email
}
-// Login return the last version of the login
-func (i *Identity) Login() string {
- return i.lastVersion().login
-}
-
// AvatarUrl return the last version of the Avatar URL
func (i *Identity) AvatarUrl() string {
return i.lastVersion().avatarURL
}
// Keys return the last version of the valid keys
-func (i *Identity) Keys() []Key {
+func (i *Identity) Keys() []*Key {
return i.lastVersion().keys
}
// ValidKeysAtTime return the set of keys valid at a given lamport time
-func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key {
- var result []Key
+func (i *Identity) ValidKeysAtTime(time lamport.Time) []*Key {
+ var result []*Key
for _, v := range i.versions {
if v.time > time {
@@ -511,16 +529,7 @@ func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key {
// DisplayName return a non-empty string to display, representing the
// identity, based on the non-empty values.
func (i *Identity) DisplayName() string {
- switch {
- case i.Name() == "" && i.Login() != "":
- return i.Login()
- case i.Name() != "" && i.Login() == "":
- return i.Name()
- case i.Name() != "" && i.Login() != "":
- return fmt.Sprintf("%s (%s)", i.Name(), i.Login())
- }
-
- panic("invalid person data")
+ return i.Name()
}
// IsProtected return true if the chain of git commits started to be signed.
@@ -540,9 +549,13 @@ func (i *Identity) LastModification() timestamp.Timestamp {
return timestamp.Timestamp(i.lastVersion().unixTime)
}
-// SetMetadata store arbitrary metadata along the last defined Version.
-// If the Version has been commit to git already, it won't be overwritten.
+// SetMetadata store arbitrary metadata along the last not-commit Version.
+// If the Version has been commit to git already, a new identical version is added and will need to be
+// commit.
func (i *Identity) SetMetadata(key string, value string) {
+ if i.lastVersion().commitHash != "" {
+ i.versions = append(i.versions, i.lastVersion().Clone())
+ }
i.lastVersion().SetMetadata(key, value)
}
@@ -575,3 +588,9 @@ func (i *Identity) MutableMetadata() map[string]string {
return metadata
}
+
+// addVersionForTest add a new version to the identity
+// Only for testing !
+func (i *Identity) addVersionForTest(version *Version) {
+ i.versions = append(i.versions, version)
+}
diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go
index 142ffaa6..713b3246 100644
--- a/identity/identity_actions_test.go
+++ b/identity/identity_actions_test.go
@@ -48,14 +48,14 @@ func TestPushPull(t *testing.T) {
// Update both
- identity1.AddVersion(&Version{
+ identity1.addVersionForTest(&Version{
name: "name1b",
email: "email1b",
})
err = identity1.Commit(repoA)
require.NoError(t, err)
- identity2.AddVersion(&Version{
+ identity2.addVersionForTest(&Version{
name: "name2b",
email: "email2b",
})
@@ -92,7 +92,7 @@ func TestPushPull(t *testing.T) {
// Concurrent update
- identity1.AddVersion(&Version{
+ identity1.addVersionForTest(&Version{
name: "name1c",
email: "email1c",
})
@@ -102,7 +102,7 @@ func TestPushPull(t *testing.T) {
identity1B, err := ReadLocal(repoB, identity1.Id())
require.NoError(t, err)
- identity1B.AddVersion(&Version{
+ identity1B.addVersionForTest(&Version{
name: "name1concurrent",
email: "email1concurrent",
})
diff --git a/identity/identity_stub.go b/identity/identity_stub.go
index be52ffc0..7e2fcd94 100644
--- a/identity/identity_stub.go
+++ b/identity/identity_stub.go
@@ -64,11 +64,11 @@ func (IdentityStub) AvatarUrl() string {
panic("identities needs to be properly loaded with identity.ReadLocal()")
}
-func (IdentityStub) Keys() []Key {
+func (IdentityStub) Keys() []*Key {
panic("identities needs to be properly loaded with identity.ReadLocal()")
}
-func (IdentityStub) ValidKeysAtTime(time lamport.Time) []Key {
+func (IdentityStub) ValidKeysAtTime(_ lamport.Time) []*Key {
panic("identities needs to be properly loaded with identity.ReadLocal()")
}
diff --git a/identity/identity_test.go b/identity/identity_test.go
index f91c548f..ee6ccdf7 100644
--- a/identity/identity_test.go
+++ b/identity/identity_test.go
@@ -44,7 +44,7 @@ func TestIdentityCommitLoad(t *testing.T) {
time: 100,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyA"},
},
},
@@ -52,7 +52,7 @@ func TestIdentityCommitLoad(t *testing.T) {
time: 200,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyB"},
},
},
@@ -60,7 +60,7 @@ func TestIdentityCommitLoad(t *testing.T) {
time: 201,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyC"},
},
},
@@ -79,20 +79,25 @@ func TestIdentityCommitLoad(t *testing.T) {
// add more version
- identity.AddVersion(&Version{
+ identity.Mutate(func(orig Mutator) Mutator {
+
+ return orig
+ })
+
+ identity.addVersionForTest(&Version{
time: 201,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyD"},
},
})
- identity.AddVersion(&Version{
+ identity.addVersionForTest(&Version{
time: 300,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyE"},
},
})
@@ -123,7 +128,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
time: 100,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyA"},
},
},
@@ -131,7 +136,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
time: 200,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyB"},
},
},
@@ -139,7 +144,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
time: 201,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyC"},
},
},
@@ -147,7 +152,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
time: 201,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyD"},
},
},
@@ -155,7 +160,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
time: 300,
name: "René Descartes",
email: "rene.descartes@example.com",
- keys: []Key{
+ keys: []*Key{
{PubKey: "pubkeyE"},
},
},
@@ -163,13 +168,13 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
}
assert.Nil(t, identity.ValidKeysAtTime(10))
- assert.Equal(t, identity.ValidKeysAtTime(100), []Key{{PubKey: "pubkeyA"}})
- assert.Equal(t, identity.ValidKeysAtTime(140), []Key{{PubKey: "pubkeyA"}})
- assert.Equal(t, identity.ValidKeysAtTime(200), []Key{{PubKey: "pubkeyB"}})
- assert.Equal(t, identity.ValidKeysAtTime(201), []Key{{PubKey: "pubkeyD"}})
- assert.Equal(t, identity.ValidKeysAtTime(202), []Key{{PubKey: "pubkeyD"}})
- assert.Equal(t, identity.ValidKeysAtTime(300), []Key{{PubKey: "pubkeyE"}})
- assert.Equal(t, identity.ValidKeysAtTime(3000), []Key{{PubKey: "pubkeyE"}})
+ assert.Equal(t, identity.ValidKeysAtTime(100), []*Key{{PubKey: "pubkeyA"}})
+ assert.Equal(t, identity.ValidKeysAtTime(140), []*Key{{PubKey: "pubkeyA"}})
+ assert.Equal(t, identity.ValidKeysAtTime(200), []*Key{{PubKey: "pubkeyB"}})
+ assert.Equal(t, identity.ValidKeysAtTime(201), []*Key{{PubKey: "pubkeyD"}})
+ assert.Equal(t, identity.ValidKeysAtTime(202), []*Key{{PubKey: "pubkeyD"}})
+ assert.Equal(t, identity.ValidKeysAtTime(300), []*Key{{PubKey: "pubkeyE"}})
+ assert.Equal(t, identity.ValidKeysAtTime(3000), []*Key{{PubKey: "pubkeyE"}})
}
// Test the immutable or mutable metadata search
@@ -189,7 +194,7 @@ func TestMetadata(t *testing.T) {
assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1")
// try override
- identity.AddVersion(&Version{
+ identity.addVersionForTest(&Version{
name: "René Descartes",
email: "rene.descartes@example.com",
})
diff --git a/identity/interface.go b/identity/interface.go
index 54a9da78..d138362d 100644
--- a/identity/interface.go
+++ b/identity/interface.go
@@ -17,17 +17,14 @@ type Interface interface {
// Email return the last version of the email
Email() string
- // Login return the last version of the login
- Login() string
-
// AvatarUrl return the last version of the Avatar URL
AvatarUrl() string
// Keys return the last version of the valid keys
- Keys() []Key
+ Keys() []*Key
// ValidKeysAtTime return the set of keys valid at a given lamport time
- ValidKeysAtTime(time lamport.Time) []Key
+ ValidKeysAtTime(time lamport.Time) []*Key
// DisplayName return a non-empty string to display, representing the
// identity, based on the non-empty values.
diff --git a/identity/key.go b/identity/key.go
index 90edfb60..cc948394 100644
--- a/identity/key.go
+++ b/identity/key.go
@@ -11,3 +11,8 @@ func (k *Key) Validate() error {
return nil
}
+
+func (k *Key) Clone() *Key {
+ clone := *k
+ return &clone
+}
diff --git a/identity/version.go b/identity/version.go
index 95530767..f9c7b262 100644
--- a/identity/version.go
+++ b/identity/version.go
@@ -24,14 +24,13 @@ type Version struct {
unixTime int64
name string
- email string
- login string
+ email string // as defined in git, not for bridges
avatarURL string
// The set of keys valid at that time, from this version onward, until they get removed
// in a new version. This allow to have multiple key for the same identity (e.g. one per
// device) as well as revoke key.
- keys []Key
+ keys []*Key
// This optional array is here to ensure a better randomness of the identity id to avoid collisions.
// It has no functional purpose and should be ignored.
@@ -53,13 +52,28 @@ type VersionJSON struct {
UnixTime int64 `json:"unix_time"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
- Login string `json:"login,omitempty"`
AvatarUrl string `json:"avatar_url,omitempty"`
- Keys []Key `json:"pub_keys,omitempty"`
+ Keys []*Key `json:"pub_keys,omitempty"`
Nonce []byte `json:"nonce,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
+// Make a deep copy
+func (v *Version) Clone() *Version {
+ clone := &Version{
+ name: v.name,
+ email: v.email,
+ avatarURL: v.avatarURL,
+ keys: make([]*Key, len(v.keys)),
+ }
+
+ for i, key := range v.keys {
+ clone.keys[i] = key.Clone()
+ }
+
+ return clone
+}
+
func (v *Version) MarshalJSON() ([]byte, error) {
return json.Marshal(VersionJSON{
FormatVersion: formatVersion,
@@ -67,7 +81,6 @@ func (v *Version) MarshalJSON() ([]byte, error) {
UnixTime: v.unixTime,
Name: v.name,
Email: v.email,
- Login: v.login,
AvatarUrl: v.avatarURL,
Keys: v.keys,
Nonce: v.nonce,
@@ -90,7 +103,6 @@ func (v *Version) UnmarshalJSON(data []byte) error {
v.unixTime = aux.UnixTime
v.name = aux.Name
v.email = aux.Email
- v.login = aux.Login
v.avatarURL = aux.AvatarUrl
v.keys = aux.Keys
v.nonce = aux.Nonce
@@ -108,8 +120,8 @@ func (v *Version) Validate() error {
return fmt.Errorf("lamport time not set")
}
- if text.Empty(v.name) && text.Empty(v.login) {
- return fmt.Errorf("either name or login should be set")
+ if text.Empty(v.name) {
+ return fmt.Errorf("name not set")
}
if strings.Contains(v.name, "\n") {
@@ -120,14 +132,6 @@ func (v *Version) Validate() error {
return fmt.Errorf("name is not fully printable")
}
- if strings.Contains(v.login, "\n") {
- return fmt.Errorf("login should be a single line")
- }
-
- if !text.Safe(v.login) {
- return fmt.Errorf("login is not fully printable")
- }
-
if strings.Contains(v.email, "\n") {
return fmt.Errorf("email should be a single line")
}
@@ -202,7 +206,7 @@ func (v *Version) GetMetadata(key string) (string, bool) {
return val, ok
}
-// AllMetadata return all metadata for this Identity
+// AllMetadata return all metadata for this Version
func (v *Version) AllMetadata() map[string]string {
return v.metadata
}
diff --git a/identity/version_test.go b/identity/version_test.go
index 8c4c8d99..25848eb5 100644
--- a/identity/version_test.go
+++ b/identity/version_test.go
@@ -9,11 +9,10 @@ import (
func TestVersionSerialize(t *testing.T) {
before := &Version{
- login: "login",
name: "name",
email: "email",
avatarURL: "avatarUrl",
- keys: []Key{
+ keys: []*Key{
{
Fingerprint: "fingerprint1",
PubKey: "pubkey1",
diff --git a/input/prompt.go b/input/prompt.go
index 6036c062..960ecd62 100644
--- a/input/prompt.go
+++ b/input/prompt.go
@@ -4,23 +4,38 @@ import (
"bufio"
"fmt"
"os"
+ "strconv"
"strings"
+ "syscall"
+
+ "golang.org/x/crypto/ssh/terminal"
+
+ "github.com/MichaelMure/git-bug/util/interrupt"
)
-func PromptValue(name string, preValue string) (string, error) {
- return promptValue(name, preValue, false)
+// PromptValidator is a validator for a user entry
+// If complaint is "", value is considered valid, otherwise it's the error reported to the user
+// If err != nil, a terminal error happened
+type PromptValidator func(name string, value string) (complaint string, err error)
+
+// Required is a validator preventing a "" value
+func Required(name string, value string) (string, error) {
+ if value == "" {
+ return fmt.Sprintf("%s is empty", name), nil
+ }
+ return "", nil
}
-func PromptValueRequired(name string, preValue string) (string, error) {
- return promptValue(name, preValue, true)
+func Prompt(prompt, name string, validators ...PromptValidator) (string, error) {
+ return PromptDefault(prompt, name, "", validators...)
}
-func promptValue(name string, preValue string, required bool) (string, error) {
+func PromptDefault(prompt, name, preValue string, validators ...PromptValidator) (string, error) {
for {
if preValue != "" {
- _, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", name, preValue)
+ _, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", prompt, preValue)
} else {
- _, _ = fmt.Fprintf(os.Stderr, "%s: ", name)
+ _, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
}
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
@@ -31,14 +46,85 @@ func promptValue(name string, preValue string, required bool) (string, error) {
line = strings.TrimSpace(line)
if preValue != "" && line == "" {
- return preValue, nil
+ line = preValue
}
- if required && line == "" {
- _, _ = fmt.Fprintf(os.Stderr, "%s is empty\n", name)
- continue
+ for _, validator := range validators {
+ complaint, err := validator(name, line)
+ if err != nil {
+ return "", err
+ }
+ if complaint != "" {
+ _, _ = fmt.Fprintln(os.Stderr, complaint)
+ continue
+ }
}
return line, nil
}
}
+
+func PromptPassword(prompt, name string, validators ...PromptValidator) (string, error) {
+ termState, err := terminal.GetState(syscall.Stdin)
+ if err != nil {
+ return "", err
+ }
+
+ cancel := interrupt.RegisterCleaner(func() error {
+ return terminal.Restore(syscall.Stdin, termState)
+ })
+ defer cancel()
+
+ for {
+ _, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
+
+ bytePassword, err := terminal.ReadPassword(syscall.Stdin)
+ // new line for coherent formatting, ReadPassword clip the normal new line
+ // entered by the user
+ fmt.Println()
+
+ if err != nil {
+ return "", err
+ }
+
+ pass := string(bytePassword)
+
+ for _, validator := range validators {
+ complaint, err := validator(name, pass)
+ if err != nil {
+ return "", err
+ }
+ if complaint != "" {
+ _, _ = fmt.Fprintln(os.Stderr, complaint)
+ continue
+ }
+ }
+
+ return pass, nil
+ }
+}
+
+func PromptChoice(prompt string, choices []string) (int, error) {
+ for {
+ for i, choice := range choices {
+ _, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice)
+ }
+ _, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
+
+ line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+ fmt.Println()
+ if err != nil {
+ return 0, err
+ }
+
+ line = strings.TrimSpace(line)
+
+ index, err := strconv.Atoi(line)
+ if err != nil || index < 1 || index > len(choices) {
+ fmt.Println("invalid input")
+ continue
+ }
+
+ return index, nil
+ }
+}
diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug
index 557bbf20..a062bfe8 100644
--- a/misc/bash_completion/git-bug
+++ b/misc/bash_completion/git-bug
@@ -305,6 +305,14 @@ _git-bug_bridge_auth_add-token()
two_word_flags+=("--target")
two_word_flags+=("-t")
local_nonpersistent_flags+=("--target=")
+ flags+=("--login=")
+ two_word_flags+=("--login")
+ two_word_flags+=("-l")
+ local_nonpersistent_flags+=("--login=")
+ flags+=("--user=")
+ two_word_flags+=("--user")
+ two_word_flags+=("-u")
+ local_nonpersistent_flags+=("--user=")
must_have_one_flag=()
must_have_one_noun=()
diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug
index d52113e4..b15e6398 100644
--- a/misc/powershell_completion/git-bug
+++ b/misc/powershell_completion/git-bug
@@ -64,6 +64,10 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock {
'git-bug;bridge;auth;add-token' {
[CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'The target of the bridge. Valid values are [github,gitlab,launchpad-preview]')
[CompletionResult]::new('--target', 'target', [CompletionResultType]::ParameterName, 'The target of the bridge. Valid values are [github,gitlab,launchpad-preview]')
+ [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'The login in the remote bug-tracker')
+ [CompletionResult]::new('--login', 'login', [CompletionResultType]::ParameterName, 'The login in the remote bug-tracker')
+ [CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'The user to add the token to. Default is the current user')
+ [CompletionResult]::new('--user', 'user', [CompletionResultType]::ParameterName, 'The user to add the token to. Default is the current user')
break
}
'git-bug;bridge;auth;rm' {
diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug
index 3b06a396..f6d50e08 100644
--- a/misc/zsh_completion/git-bug
+++ b/misc/zsh_completion/git-bug
@@ -177,7 +177,9 @@ function _git-bug_bridge_auth {
function _git-bug_bridge_auth_add-token {
_arguments \
- '(-t --target)'{-t,--target}'[The target of the bridge. Valid values are [github,gitlab,launchpad-preview]]:'
+ '(-t --target)'{-t,--target}'[The target of the bridge. Valid values are [github,gitlab,launchpad-preview]]:' \
+ '(-l --login)'{-l,--login}'[The login in the remote bug-tracker]:' \
+ '(-u --user)'{-u,--user}'[The user to add the token to. Default is the current user]:'
}
function _git-bug_bridge_auth_rm {