mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-15 10:12:06 +03:00
commit
9e1a987b4d
@ -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)
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
||||
|
@ -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,13 +21,15 @@ 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 {
|
||||
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 {
|
||||
return func(opts *options) {
|
||||
opts.userId = userId
|
||||
}
|
||||
}
|
||||
|
||||
func WithKind(kind CredentialKind) Option {
|
||||
return func(opts *options) {
|
||||
opts.kind = kind
|
||||
}
|
||||
}
|
||||
|
||||
func WithMeta(key string, val string) Option {
|
||||
return func(opts *options) {
|
||||
if opts.meta == nil {
|
||||
opts.meta = make(map[string]string)
|
||||
}
|
||||
opts.meta[key] = val
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
46
bridge/core/config.go
Normal file
46
bridge/core/config.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
validator := func(name string, value string) (complaint string, err error) {
|
||||
if re.MatchString(value) {
|
||||
return "", nil
|
||||
}
|
||||
return "token has incorrect format", nil
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(line)
|
||||
if re.MatchString(token) {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
fmt.Println("token has incorrect format")
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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{}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
login := "test-identity"
|
||||
author.SetMetadata(metaKeyGithubLogin, login)
|
||||
|
||||
err = identity.SetUserIdentity(repo, author)
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -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: ")
|
||||
index, err := input.PromptChoice("Gitlab base url", []string{
|
||||
"https://gitlab.com",
|
||||
"enter your own base url",
|
||||
})
|
||||
|
||||
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
index, err := strconv.Atoi(line)
|
||||
if err != nil || index < 0 || index > 1 {
|
||||
fmt.Println("invalid input")
|
||||
continue
|
||||
}
|
||||
|
||||
switch index {
|
||||
case 0:
|
||||
if index == 0 {
|
||||
return defaultBaseURL, nil
|
||||
case 1:
|
||||
} 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
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
return "missing scheme", nil
|
||||
}
|
||||
if u.Host == "" {
|
||||
return "missing host", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
ok, err := validateBaseUrl(line)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ok {
|
||||
return line, nil
|
||||
}
|
||||
}
|
||||
return input.Prompt("Base url", "url", input.Required, validator)
|
||||
}
|
||||
|
||||
func validateBaseUrl(baseUrl string) (bool, error) {
|
||||
u, err := url.Parse(baseUrl)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return u.Scheme != "" && u.Host != "", nil
|
||||
}
|
||||
|
||||
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')
|
||||
validator := func(name string, value string) (complaint string, err error) {
|
||||
if !re.MatchString(value) {
|
||||
return "token has incorrect format", nil
|
||||
}
|
||||
login, err = getLoginFromToken(baseUrl, auth.NewToken(value, target))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return fmt.Sprintf("token is invalid: %v", err), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(line)
|
||||
if re.MatchString(token) {
|
||||
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
|
||||
}
|
||||
|
||||
fmt.Println("token has incorrect format")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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{}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
login := "test-identity"
|
||||
author.SetMetadata(metaKeyGitlabLogin, login)
|
||||
|
||||
err = identity.SetUserIdentity(repo, author)
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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{}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
12
cache/bug_excerpt.go
vendored
12
cache/bug_excerpt.go
vendored
@ -2,7 +2,6 @@ package cache
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
@ -44,20 +43,10 @@ type BugExcerpt struct {
|
||||
// identity.Bare data are directly embedded in the bug excerpt
|
||||
type LegacyAuthorExcerpt struct {
|
||||
Name string
|
||||
Login 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")
|
||||
}
|
||||
|
||||
func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
|
||||
@ -95,7 +84,6 @@ 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(),
|
||||
}
|
||||
default:
|
||||
|
3
cache/filter.go
vendored
3
cache/filter.go
vendored
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
4
cache/identity_cache.go
vendored
4
cache/identity_cache.go
vendored
@ -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()
|
||||
}
|
||||
|
||||
|
15
cache/identity_excerpt.go
vendored
15
cache/identity_excerpt.go
vendored
@ -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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
/*
|
||||
|
4
cache/query.go
vendored
4
cache/query.go
vendored
@ -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
|
||||
|
91
cache/repo_cache.go
vendored
91
cache/repo_cache.go
vendored
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -4,11 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
)
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -23,6 +23,14 @@ Store a new token
|
||||
\fB\-t\fP, \fB\-\-target\fP=""
|
||||
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
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
2
go.mod
2
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
|
||||
|
2
go.sum
2
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=
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@ -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",
|
||||
})
|
||||
|
@ -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()")
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
})
|
||||
|
@ -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.
|
||||
|
@ -11,3 +11,8 @@ func (k *Key) Validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Key) Clone() *Key {
|
||||
clone := *k
|
||||
return &clone
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
106
input/prompt.go
106
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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=()
|
||||
|
@ -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' {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user