Merge pull request #294 from MichaelMure/cred-metadata

Cred metadata
This commit is contained in:
Michael Muré 2020-02-09 01:31:00 +01:00 committed by GitHub
commit 9e1a987b4d
60 changed files with 791 additions and 801 deletions

View File

@ -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)

View File

@ -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
*/

View File

@ -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)

View File

@ -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
}
}

View File

@ -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,

View File

@ -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
View 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
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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{}
}

View File

@ -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),

View File

@ -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)

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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{}
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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{}
}

View File

@ -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
View File

@ -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
View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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
View File

@ -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
View File

@ -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)
}

View File

@ -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,
)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"),

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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")
}

View File

@ -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",

View File

@ -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
}

View File

@ -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)
}

View File

@ -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",
})

View File

@ -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()")
}

View File

@ -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",
})

View File

@ -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.

View File

@ -11,3 +11,8 @@ func (k *Key) Validate() error {
return nil
}
func (k *Key) Clone() *Key {
clone := *k
return &clone
}

View File

@ -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
}

View File

@ -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",

View File

@ -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
}
}

View File

@ -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=()

View File

@ -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' {

View File

@ -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 {