mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-14 17:51:44 +03:00
Merge pull request #271 from MichaelMure/bridge-credentials
bridge: huge refactor to accept multiple kind of credentials
This commit is contained in:
commit
f1ed857cbd
@ -39,11 +39,11 @@ func DefaultBridge(repo *cache.RepoCache) (*core.Bridge, error) {
|
||||
|
||||
// ConfiguredBridges return the list of bridge that are configured for the given
|
||||
// repo
|
||||
func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
|
||||
func ConfiguredBridges(repo repository.RepoConfig) ([]string, error) {
|
||||
return core.ConfiguredBridges(repo)
|
||||
}
|
||||
|
||||
// Remove a configured bridge
|
||||
func RemoveBridge(repo repository.RepoCommon, name string) error {
|
||||
func RemoveBridge(repo repository.RepoConfig, name string) error {
|
||||
return core.RemoveBridge(repo, name)
|
||||
}
|
||||
|
232
bridge/core/auth/credential.go
Normal file
232
bridge/core/auth/credential.go
Normal file
@ -0,0 +1,232 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
configKeyPrefix = "git-bug.auth"
|
||||
configKeyKind = "kind"
|
||||
configKeyUserId = "userid"
|
||||
configKeyTarget = "target"
|
||||
configKeyCreateTime = "createtime"
|
||||
)
|
||||
|
||||
type CredentialKind string
|
||||
|
||||
const (
|
||||
KindToken CredentialKind = "token"
|
||||
KindLoginPassword CredentialKind = "login-password"
|
||||
)
|
||||
|
||||
var ErrCredentialNotExist = errors.New("credential doesn't exist")
|
||||
|
||||
func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatch {
|
||||
return entity.NewErrMultipleMatch("credential", matching)
|
||||
}
|
||||
|
||||
type Credential interface {
|
||||
ID() entity.Id
|
||||
UserId() entity.Id
|
||||
Target() string
|
||||
Kind() CredentialKind
|
||||
CreateTime() time.Time
|
||||
Validate() error
|
||||
|
||||
// Return all the specific properties of the credential that need to be saved into the configuration.
|
||||
// This does not include Target, User, Kind and CreateTime.
|
||||
ToConfig() map[string]string
|
||||
}
|
||||
|
||||
// Load loads a credential from the repo config
|
||||
func LoadWithId(repo repository.RepoConfig, id entity.Id) (Credential, error) {
|
||||
keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
|
||||
|
||||
// read token config pairs
|
||||
rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
|
||||
if err != nil {
|
||||
// Not exactly right due to the limitation of ReadAll()
|
||||
return nil, ErrCredentialNotExist
|
||||
}
|
||||
|
||||
return loadFromConfig(rawconfigs, id)
|
||||
}
|
||||
|
||||
// LoadWithPrefix load a credential from the repo config with a prefix
|
||||
func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, error) {
|
||||
creds, err := List(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// preallocate but empty
|
||||
matching := make([]Credential, 0, 5)
|
||||
|
||||
for _, cred := range creds {
|
||||
if cred.ID().HasPrefix(prefix) {
|
||||
matching = append(matching, cred)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matching) > 1 {
|
||||
ids := make([]entity.Id, len(matching))
|
||||
for i, cred := range matching {
|
||||
ids[i] = cred.ID()
|
||||
}
|
||||
return nil, NewErrMultipleMatchCredential(ids)
|
||||
}
|
||||
|
||||
if len(matching) == 0 {
|
||||
return nil, ErrCredentialNotExist
|
||||
}
|
||||
|
||||
return matching[0], nil
|
||||
}
|
||||
|
||||
func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
|
||||
keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
|
||||
|
||||
// trim key prefix
|
||||
configs := make(map[string]string)
|
||||
for key, value := range rawConfigs {
|
||||
newKey := strings.TrimPrefix(key, keyPrefix)
|
||||
configs[newKey] = value
|
||||
}
|
||||
|
||||
var cred Credential
|
||||
|
||||
switch CredentialKind(configs[configKeyKind]) {
|
||||
case KindToken:
|
||||
cred = NewTokenFromConfig(configs)
|
||||
case KindLoginPassword:
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
|
||||
}
|
||||
|
||||
return cred, nil
|
||||
}
|
||||
|
||||
// List load all existing credentials
|
||||
func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
|
||||
rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(configKeyPrefix + `.([^.]+).([^.]+)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mapped := make(map[string]map[string]string)
|
||||
|
||||
for key, val := range rawConfigs {
|
||||
res := re.FindStringSubmatch(key)
|
||||
if res == nil {
|
||||
continue
|
||||
}
|
||||
if mapped[res[1]] == nil {
|
||||
mapped[res[1]] = make(map[string]string)
|
||||
}
|
||||
mapped[res[1]][res[2]] = val
|
||||
}
|
||||
|
||||
matcher := matcher(opts)
|
||||
|
||||
var credentials []Credential
|
||||
for id, kvs := range mapped {
|
||||
cred, err := loadFromConfig(kvs, entity.Id(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matcher.Match(cred) {
|
||||
credentials = append(credentials, cred)
|
||||
}
|
||||
}
|
||||
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
// IdExist return whether a credential id exist or not
|
||||
func IdExist(repo repository.RepoConfig, id entity.Id) bool {
|
||||
_, err := LoadWithId(repo, id)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// PrefixExist return whether a credential id prefix exist or not
|
||||
func PrefixExist(repo repository.RepoConfig, prefix string) bool {
|
||||
_, err := LoadWithPrefix(repo, prefix)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Store stores a credential in the global git config
|
||||
func Store(repo repository.RepoConfig, cred Credential) error {
|
||||
confs := cred.ToConfig()
|
||||
|
||||
prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
|
||||
|
||||
// Kind
|
||||
err := repo.GlobalConfig().StoreString(prefix+configKeyKind, string(cred.Kind()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// UserId
|
||||
err = repo.GlobalConfig().StoreString(prefix+configKeyUserId, cred.UserId().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Target
|
||||
err = repo.GlobalConfig().StoreString(prefix+configKeyTarget, cred.Target())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateTime
|
||||
err = repo.GlobalConfig().StoreTimestamp(prefix+configKeyCreateTime, cred.CreateTime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Custom
|
||||
for key, val := range confs {
|
||||
err := repo.GlobalConfig().StoreString(prefix+key, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a credential from the global git config
|
||||
func Remove(repo repository.RepoConfig, id entity.Id) error {
|
||||
keyPrefix := fmt.Sprintf("%s.%s", configKeyPrefix, id)
|
||||
return repo.GlobalConfig().RemoveAll(keyPrefix)
|
||||
}
|
||||
|
||||
/*
|
||||
* Sorting
|
||||
*/
|
||||
|
||||
type ById []Credential
|
||||
|
||||
func (b ById) Len() int {
|
||||
return len(b)
|
||||
}
|
||||
|
||||
func (b ById) Less(i, j int) bool {
|
||||
return b[i].ID() < b[j].ID()
|
||||
}
|
||||
|
||||
func (b ById) Swap(i, j int) {
|
||||
b[i], b[j] = b[j], b[i]
|
||||
}
|
109
bridge/core/auth/credential_test.go
Normal file
109
bridge/core/auth/credential_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
func TestCredential(t *testing.T) {
|
||||
repo := repository.NewMockRepoForTest()
|
||||
|
||||
user1 := identity.NewIdentity("user1", "email")
|
||||
err := user1.Commit(repo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user2 := identity.NewIdentity("user2", "email")
|
||||
err = user2.Commit(repo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
storeToken := func(user identity.Interface, val string, target string) *Token {
|
||||
token := NewToken(user.Id(), val, target)
|
||||
err = Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
return token
|
||||
}
|
||||
|
||||
token := storeToken(user1, "foobar", "github")
|
||||
|
||||
// Store + Load
|
||||
err = Store(repo, token)
|
||||
assert.NoError(t, err)
|
||||
|
||||
token2, err := LoadWithId(repo, token.ID())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, token.createTime.Unix(), token2.CreateTime().Unix())
|
||||
token.createTime = token2.CreateTime()
|
||||
assert.Equal(t, token, token2)
|
||||
|
||||
prefix := string(token.ID())[:10]
|
||||
|
||||
// LoadWithPrefix
|
||||
token3, err := LoadWithPrefix(repo, prefix)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, token.createTime.Unix(), token3.CreateTime().Unix())
|
||||
token.createTime = token3.CreateTime()
|
||||
assert.Equal(t, token, token3)
|
||||
|
||||
token4 := storeToken(user1, "foo", "gitlab")
|
||||
token5 := storeToken(user2, "bar", "github")
|
||||
|
||||
// List + options
|
||||
creds, err := List(repo, WithTarget("github"))
|
||||
assert.NoError(t, err)
|
||||
sameIds(t, creds, []Credential{token, token5})
|
||||
|
||||
creds, err = List(repo, WithTarget("gitlab"))
|
||||
assert.NoError(t, err)
|
||||
sameIds(t, creds, []Credential{token4})
|
||||
|
||||
creds, err = List(repo, WithUser(user1))
|
||||
assert.NoError(t, err)
|
||||
sameIds(t, creds, []Credential{token, token4})
|
||||
|
||||
creds, err = List(repo, WithUserId(user1.Id()))
|
||||
assert.NoError(t, err)
|
||||
sameIds(t, creds, []Credential{token, token4})
|
||||
|
||||
creds, err = List(repo, WithKind(KindToken))
|
||||
assert.NoError(t, err)
|
||||
sameIds(t, creds, []Credential{token, token4, token5})
|
||||
|
||||
creds, err = List(repo, WithKind(KindLoginPassword))
|
||||
assert.NoError(t, err)
|
||||
sameIds(t, creds, []Credential{})
|
||||
|
||||
// Exist
|
||||
exist := IdExist(repo, token.ID())
|
||||
assert.True(t, exist)
|
||||
|
||||
exist = PrefixExist(repo, prefix)
|
||||
assert.True(t, exist)
|
||||
|
||||
// Remove
|
||||
err = Remove(repo, token.ID())
|
||||
assert.NoError(t, err)
|
||||
|
||||
creds, err = List(repo)
|
||||
assert.NoError(t, err)
|
||||
sameIds(t, creds, []Credential{token4, token5})
|
||||
}
|
||||
|
||||
func sameIds(t *testing.T, a []Credential, b []Credential) {
|
||||
t.Helper()
|
||||
|
||||
ids := func(creds []Credential) []entity.Id {
|
||||
result := make([]entity.Id, len(creds))
|
||||
for i, cred := range creds {
|
||||
result[i] = cred.ID()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, ids(a), ids(b))
|
||||
}
|
62
bridge/core/auth/options.go
Normal file
62
bridge/core/auth/options.go
Normal file
@ -0,0 +1,62 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
target string
|
||||
userId entity.Id
|
||||
kind CredentialKind
|
||||
}
|
||||
|
||||
type Option func(opts *options)
|
||||
|
||||
func matcher(opts []Option) *options {
|
||||
result := &options{}
|
||||
for _, opt := range opts {
|
||||
opt(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (opts *options) Match(cred Credential) bool {
|
||||
if opts.target != "" && cred.Target() != opts.target {
|
||||
return false
|
||||
}
|
||||
|
||||
if opts.userId != "" && cred.UserId() != opts.userId {
|
||||
return false
|
||||
}
|
||||
|
||||
if opts.kind != "" && cred.Kind() != opts.kind {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func WithTarget(target string) Option {
|
||||
return func(opts *options) {
|
||||
opts.target = target
|
||||
}
|
||||
}
|
||||
|
||||
func WithUser(user identity.Interface) Option {
|
||||
return func(opts *options) {
|
||||
opts.userId = user.Id()
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserId(userId entity.Id) Option {
|
||||
return func(opts *options) {
|
||||
opts.userId = userId
|
||||
}
|
||||
}
|
||||
|
||||
func WithKind(kind CredentialKind) Option {
|
||||
return func(opts *options) {
|
||||
opts.kind = kind
|
||||
}
|
||||
}
|
95
bridge/core/auth/token.go
Normal file
95
bridge/core/auth/token.go
Normal file
@ -0,0 +1,95 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenValueKey = "value"
|
||||
)
|
||||
|
||||
var _ Credential = &Token{}
|
||||
|
||||
// Token holds an API access token data
|
||||
type Token struct {
|
||||
userId entity.Id
|
||||
target string
|
||||
createTime time.Time
|
||||
Value string
|
||||
}
|
||||
|
||||
// NewToken instantiate a new token
|
||||
func NewToken(userId entity.Id, value, target string) *Token {
|
||||
return &Token{
|
||||
userId: userId,
|
||||
target: target,
|
||||
createTime: time.Now(),
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTokenFromConfig(conf map[string]string) *Token {
|
||||
token := &Token{}
|
||||
|
||||
token.userId = entity.Id(conf[configKeyUserId])
|
||||
token.target = conf[configKeyTarget]
|
||||
if createTime, ok := conf[configKeyCreateTime]; ok {
|
||||
if t, err := repository.ParseTimestamp(createTime); err == nil {
|
||||
token.createTime = t
|
||||
}
|
||||
}
|
||||
|
||||
token.Value = conf[tokenValueKey]
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func (t *Token) ID() entity.Id {
|
||||
sum := sha256.Sum256([]byte(t.target + t.Value))
|
||||
return entity.Id(fmt.Sprintf("%x", sum))
|
||||
}
|
||||
|
||||
func (t *Token) UserId() entity.Id {
|
||||
return t.userId
|
||||
}
|
||||
|
||||
func (t *Token) Target() string {
|
||||
return t.target
|
||||
}
|
||||
|
||||
func (t *Token) Kind() CredentialKind {
|
||||
return KindToken
|
||||
}
|
||||
|
||||
func (t *Token) CreateTime() time.Time {
|
||||
return t.createTime
|
||||
}
|
||||
|
||||
// Validate ensure token important fields are valid
|
||||
func (t *Token) Validate() error {
|
||||
if t.Value == "" {
|
||||
return fmt.Errorf("missing value")
|
||||
}
|
||||
if t.target == "" {
|
||||
return fmt.Errorf("missing target")
|
||||
}
|
||||
if t.createTime.IsZero() || t.createTime.Equal(time.Time{}) {
|
||||
return fmt.Errorf("missing creation time")
|
||||
}
|
||||
if !core.TargetExist(t.target) {
|
||||
return fmt.Errorf("unknown target")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Token) ToConfig() map[string]string {
|
||||
return map[string]string{
|
||||
tokenValueKey: t.Value,
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
@ -21,10 +20,9 @@ var ErrImportNotSupported = errors.New("import is not supported")
|
||||
var ErrExportNotSupported = errors.New("export is not supported")
|
||||
|
||||
const (
|
||||
ConfigKeyTarget = "target"
|
||||
ConfigKeyToken = "token"
|
||||
ConfigKeyTokenId = "token-id"
|
||||
MetaKeyOrigin = "origin"
|
||||
ConfigKeyTarget = "target"
|
||||
|
||||
MetaKeyOrigin = "origin"
|
||||
|
||||
bridgeConfigKeyPrefix = "git-bug.bridge"
|
||||
)
|
||||
@ -37,9 +35,8 @@ type BridgeParams struct {
|
||||
Owner string
|
||||
Project string
|
||||
URL string
|
||||
Token string
|
||||
TokenId string
|
||||
TokenStdin bool
|
||||
CredPrefix string
|
||||
TokenRaw string
|
||||
}
|
||||
|
||||
// Bridge is a wrapper around a BridgeImpl that will bind low-level
|
||||
@ -143,7 +140,7 @@ func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
|
||||
|
||||
// ConfiguredBridges return the list of bridge that are configured for the given
|
||||
// repo
|
||||
func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
|
||||
func ConfiguredBridges(repo repository.RepoConfig) ([]string, error) {
|
||||
configs, err := repo.LocalConfig().ReadAll(bridgeConfigKeyPrefix + ".")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't read configured bridges")
|
||||
@ -178,7 +175,7 @@ func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
|
||||
}
|
||||
|
||||
// Check if a bridge exist
|
||||
func BridgeExist(repo repository.RepoCommon, name string) bool {
|
||||
func BridgeExist(repo repository.RepoConfig, name string) bool {
|
||||
keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
|
||||
|
||||
conf, err := repo.LocalConfig().ReadAll(keyPrefix)
|
||||
@ -187,7 +184,7 @@ func BridgeExist(repo repository.RepoCommon, name string) bool {
|
||||
}
|
||||
|
||||
// Remove a configured bridge
|
||||
func RemoveBridge(repo repository.RepoCommon, name string) error {
|
||||
func RemoveBridge(repo repository.RepoConfig, name string) error {
|
||||
re, err := regexp.Compile(`^[a-zA-Z0-9]+`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -242,7 +239,7 @@ func (b *Bridge) ensureConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadConfig(repo repository.RepoCommon, name string) (Configuration, error) {
|
||||
func loadConfig(repo repository.RepoConfig, name string) (Configuration, error) {
|
||||
keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
|
||||
|
||||
pairs, err := repo.LocalConfig().ReadAll(keyPrefix)
|
||||
@ -280,16 +277,9 @@ func (b *Bridge) ensureInit() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
token, err := LoadToken(b.repo, entity.Id(b.conf[ConfigKeyTokenId]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.conf[ConfigKeyToken] = token.Value
|
||||
|
||||
importer := b.getImporter()
|
||||
if importer != nil {
|
||||
err := importer.Init(b.conf)
|
||||
err := importer.Init(b.repo, b.conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -297,7 +287,7 @@ func (b *Bridge) ensureInit() error {
|
||||
|
||||
exporter := b.getExporter()
|
||||
if exporter != nil {
|
||||
err := exporter.Init(b.conf)
|
||||
err := exporter.Init(b.repo, b.conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
type Configuration map[string]string
|
||||
@ -16,7 +15,7 @@ type BridgeImpl interface {
|
||||
|
||||
// Configure handle the user interaction and return a key/value configuration
|
||||
// for future use
|
||||
Configure(repo repository.RepoCommon, params BridgeParams) (Configuration, error)
|
||||
Configure(repo *cache.RepoCache, params BridgeParams) (Configuration, error)
|
||||
|
||||
// ValidateConfig check the configuration for error
|
||||
ValidateConfig(conf Configuration) error
|
||||
@ -29,11 +28,11 @@ type BridgeImpl interface {
|
||||
}
|
||||
|
||||
type Importer interface {
|
||||
Init(conf Configuration) error
|
||||
Init(repo *cache.RepoCache, conf Configuration) error
|
||||
ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan ImportResult, error)
|
||||
}
|
||||
|
||||
type Exporter interface {
|
||||
Init(conf Configuration) error
|
||||
Init(repo *cache.RepoCache, conf Configuration) error
|
||||
ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan ExportResult, error)
|
||||
}
|
||||
|
@ -1,296 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenConfigKeyPrefix = "git-bug.token"
|
||||
tokenValueKey = "value"
|
||||
tokenTargetKey = "target"
|
||||
tokenCreateTimeKey = "createtime"
|
||||
)
|
||||
|
||||
var ErrTokenNotExist = errors.New("token doesn't exist")
|
||||
|
||||
func NewErrMultipleMatchToken(matching []entity.Id) *entity.ErrMultipleMatch {
|
||||
return entity.NewErrMultipleMatch("token", matching)
|
||||
}
|
||||
|
||||
// Token holds an API access token data
|
||||
type Token struct {
|
||||
Value string
|
||||
Target string
|
||||
CreateTime time.Time
|
||||
}
|
||||
|
||||
// NewToken instantiate a new token
|
||||
func NewToken(value, target string) *Token {
|
||||
return &Token{
|
||||
Value: value,
|
||||
Target: target,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Token) ID() entity.Id {
|
||||
sum := sha256.Sum256([]byte(t.Target + t.Value))
|
||||
return entity.Id(fmt.Sprintf("%x", sum))
|
||||
}
|
||||
|
||||
// Validate ensure token important fields are valid
|
||||
func (t *Token) Validate() error {
|
||||
if t.Value == "" {
|
||||
return fmt.Errorf("missing value")
|
||||
}
|
||||
if t.Target == "" {
|
||||
return fmt.Errorf("missing target")
|
||||
}
|
||||
if t.CreateTime.IsZero() || t.CreateTime.Equal(time.Time{}) {
|
||||
return fmt.Errorf("missing creation time")
|
||||
}
|
||||
if !TargetExist(t.Target) {
|
||||
return fmt.Errorf("unknown target")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadToken loads a token from the repo config
|
||||
func LoadToken(repo repository.RepoCommon, id entity.Id) (*Token, error) {
|
||||
keyPrefix := fmt.Sprintf("git-bug.token.%s.", id)
|
||||
|
||||
// read token config pairs
|
||||
rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
|
||||
if err != nil {
|
||||
// Not exactly right due to the limitation of ReadAll()
|
||||
return nil, ErrTokenNotExist
|
||||
}
|
||||
|
||||
// trim key prefix
|
||||
configs := make(map[string]string)
|
||||
for key, value := range rawconfigs {
|
||||
newKey := strings.TrimPrefix(key, keyPrefix)
|
||||
configs[newKey] = value
|
||||
}
|
||||
|
||||
token := &Token{}
|
||||
|
||||
token.Value = configs[tokenValueKey]
|
||||
token.Target = configs[tokenTargetKey]
|
||||
if createTime, ok := configs[tokenCreateTimeKey]; ok {
|
||||
if t, err := repository.ParseTimestamp(createTime); err == nil {
|
||||
token.CreateTime = t
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// LoadTokenPrefix load a token from the repo config with a prefix
|
||||
func LoadTokenPrefix(repo repository.RepoCommon, prefix string) (*Token, error) {
|
||||
tokens, err := ListTokens(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// preallocate but empty
|
||||
matching := make([]entity.Id, 0, 5)
|
||||
|
||||
for _, id := range tokens {
|
||||
if id.HasPrefix(prefix) {
|
||||
matching = append(matching, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matching) > 1 {
|
||||
return nil, NewErrMultipleMatchToken(matching)
|
||||
}
|
||||
|
||||
if len(matching) == 0 {
|
||||
return nil, ErrTokenNotExist
|
||||
}
|
||||
|
||||
return LoadToken(repo, matching[0])
|
||||
}
|
||||
|
||||
// ListTokens list all existing token ids
|
||||
func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) {
|
||||
configs, err := repo.GlobalConfig().ReadAll(tokenConfigKeyPrefix + ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(tokenConfigKeyPrefix + `.([^.]+)`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
set := make(map[string]interface{})
|
||||
|
||||
for key := range configs {
|
||||
res := re.FindStringSubmatch(key)
|
||||
|
||||
if res == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
set[res[1]] = nil
|
||||
}
|
||||
|
||||
result := make([]entity.Id, 0, len(set))
|
||||
for key := range set {
|
||||
result = append(result, entity.Id(key))
|
||||
}
|
||||
|
||||
sort.Sort(entity.Alphabetical(result))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListTokensWithTarget list all token ids associated with the target
|
||||
func ListTokensWithTarget(repo repository.RepoCommon, target string) ([]entity.Id, error) {
|
||||
var ids []entity.Id
|
||||
tokensIds, err := ListTokens(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, tokenId := range tokensIds {
|
||||
token, err := LoadToken(repo, tokenId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if token.Target == target {
|
||||
ids = append(ids, tokenId)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// LoadTokens load all existing tokens
|
||||
func LoadTokens(repo repository.RepoCommon) ([]*Token, error) {
|
||||
tokensIds, err := ListTokens(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tokens []*Token
|
||||
for _, id := range tokensIds {
|
||||
token, err := LoadToken(repo, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// LoadTokensWithTarget load all existing tokens for a given target
|
||||
func LoadTokensWithTarget(repo repository.RepoCommon, target string) ([]*Token, error) {
|
||||
tokensIds, err := ListTokens(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tokens []*Token
|
||||
for _, id := range tokensIds {
|
||||
token, err := LoadToken(repo, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token.Target == target {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// TokenIdExist return wether token id exist or not
|
||||
func TokenIdExist(repo repository.RepoCommon, id entity.Id) bool {
|
||||
_, err := LoadToken(repo, id)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// TokenExist return wether there is a token with a certain value or not
|
||||
func TokenExist(repo repository.RepoCommon, value string) bool {
|
||||
tokens, err := LoadTokens(repo)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, token := range tokens {
|
||||
if token.Value == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TokenExistWithTarget same as TokenExist but restrict search for a given target
|
||||
func TokenExistWithTarget(repo repository.RepoCommon, value string, target string) bool {
|
||||
tokens, err := LoadTokensWithTarget(repo, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, token := range tokens {
|
||||
if token.Value == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StoreToken stores a token in the repo config
|
||||
func StoreToken(repo repository.RepoCommon, token *Token) error {
|
||||
storeValueKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenValueKey)
|
||||
err := repo.GlobalConfig().StoreString(storeValueKey, token.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storeTargetKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenTargetKey)
|
||||
err = repo.GlobalConfig().StoreString(storeTargetKey, token.Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createTimeKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenCreateTimeKey)
|
||||
return repo.GlobalConfig().StoreTimestamp(createTimeKey, token.CreateTime)
|
||||
}
|
||||
|
||||
// RemoveToken removes a token from the repo config
|
||||
func RemoveToken(repo repository.RepoCommon, id entity.Id) error {
|
||||
keyPrefix := fmt.Sprintf("git-bug.token.%s", id)
|
||||
return repo.GlobalConfig().RemoveAll(keyPrefix)
|
||||
}
|
||||
|
||||
// LoadOrCreateToken will try to load a token matching the same value or create it
|
||||
func LoadOrCreateToken(repo repository.RepoCommon, target, tokenValue string) (*Token, error) {
|
||||
tokens, err := LoadTokensWithTarget(repo, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
if token.Value == tokenValue {
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
token := NewToken(tokenValue, target)
|
||||
err = StoreToken(repo, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
@ -22,6 +22,8 @@ import (
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/colors"
|
||||
@ -33,7 +35,6 @@ const (
|
||||
githubV3Url = "https://api.github.com"
|
||||
keyOwner = "owner"
|
||||
keyProject = "project"
|
||||
keyToken = "token"
|
||||
|
||||
defaultTimeout = 60 * time.Second
|
||||
)
|
||||
@ -42,40 +43,33 @@ var (
|
||||
ErrBadProjectURL = errors.New("bad project url")
|
||||
)
|
||||
|
||||
func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
|
||||
func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
|
||||
conf := make(core.Configuration)
|
||||
var err error
|
||||
|
||||
if (params.Token != "" || params.TokenId != "" || params.TokenStdin) &&
|
||||
if (params.CredPrefix != "" || params.TokenRaw != "") &&
|
||||
(params.URL == "" && (params.Project == "" || params.Owner == "")) {
|
||||
return nil, fmt.Errorf("you must provide a project URL or Owner/Name to configure this bridge with a token")
|
||||
}
|
||||
|
||||
var owner string
|
||||
var project string
|
||||
|
||||
// getting owner and project name
|
||||
switch {
|
||||
case params.Owner != "" && params.Project != "":
|
||||
// first try to use params if both or project and owner are provided
|
||||
owner = params.Owner
|
||||
project = params.Project
|
||||
|
||||
case params.URL != "":
|
||||
// try to parse params URL and extract owner and project
|
||||
owner, project, err = splitURL(params.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
// remote suggestions
|
||||
remotes, err := repo.GetRemotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// terminal prompt
|
||||
owner, project, err = promptURL(remotes)
|
||||
owner, project, err = promptURL(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -90,49 +84,38 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams)
|
||||
return nil, fmt.Errorf("invalid parameter owner: %v", owner)
|
||||
}
|
||||
|
||||
var token string
|
||||
var tokenId entity.Id
|
||||
var tokenObj *core.Token
|
||||
user, err := repo.GetUserIdentity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// try to get token from params if provided, else use terminal prompt
|
||||
// to either enter a token or login and generate a new one, or choose
|
||||
// an existing token
|
||||
if params.Token != "" {
|
||||
token = params.Token
|
||||
} else if params.TokenStdin {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
token, err = reader.ReadString('\n')
|
||||
var cred auth.Credential
|
||||
|
||||
switch {
|
||||
case params.CredPrefix != "":
|
||||
cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading from stdin: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
token = strings.TrimSpace(token)
|
||||
} else if params.TokenId != "" {
|
||||
tokenId = entity.Id(params.TokenId)
|
||||
} else {
|
||||
tokenObj, err = promptTokenOptions(repo, owner, project)
|
||||
if cred.UserId() != user.Id() {
|
||||
return nil, fmt.Errorf("selected credential don't match the user")
|
||||
}
|
||||
case params.TokenRaw != "":
|
||||
cred = auth.NewToken(user.Id(), params.TokenRaw, target)
|
||||
default:
|
||||
cred, err = promptTokenOptions(repo, user.Id(), owner, project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// at this point, we check if the token already exist or we create a new one
|
||||
if token != "" {
|
||||
tokenObj, err = core.LoadOrCreateToken(repo, target, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if tokenId != "" {
|
||||
tokenObj, err = core.LoadToken(repo, tokenId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tokenObj.Target != target {
|
||||
return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target)
|
||||
}
|
||||
token, ok := cred.(*auth.Token)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the Github bridge only handle token credentials")
|
||||
}
|
||||
|
||||
// verify access to the repository with token
|
||||
ok, err = validateProject(owner, project, tokenObj.Value)
|
||||
ok, err = validateProject(owner, project, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -141,7 +124,6 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams)
|
||||
}
|
||||
|
||||
conf[core.ConfigKeyTarget] = target
|
||||
conf[core.ConfigKeyTokenId] = tokenObj.ID().String()
|
||||
conf[keyOwner] = owner
|
||||
conf[keyProject] = project
|
||||
|
||||
@ -150,6 +132,14 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// don't forget to store the now known valid token
|
||||
if !auth.IdExist(repo, cred.ID()) {
|
||||
err = auth.Store(repo, cred)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
@ -160,10 +150,6 @@ func (*Github) ValidateConfig(conf core.Configuration) error {
|
||||
return fmt.Errorf("unexpected target name: %v", v)
|
||||
}
|
||||
|
||||
if _, ok := conf[core.ConfigKeyTokenId]; !ok {
|
||||
return fmt.Errorf("missing %s key", core.ConfigKeyTokenId)
|
||||
}
|
||||
|
||||
if _, ok := conf[keyOwner]; !ok {
|
||||
return fmt.Errorf("missing %s key", keyOwner)
|
||||
}
|
||||
@ -245,9 +231,9 @@ func randomFingerprint() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func promptTokenOptions(repo repository.RepoCommon, owner, project string) (*core.Token, error) {
|
||||
func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, project string) (auth.Credential, error) {
|
||||
for {
|
||||
tokens, err := core.LoadTokensWithTarget(repo, target)
|
||||
creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -256,18 +242,19 @@ func promptTokenOptions(repo repository.RepoCommon, owner, project string) (*cor
|
||||
fmt.Println("[1]: enter my token")
|
||||
fmt.Println("[2]: interactive token creation")
|
||||
|
||||
if len(tokens) > 0 {
|
||||
if len(creds) > 0 {
|
||||
sort.Sort(auth.ById(creds))
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Existing tokens for Github:")
|
||||
for i, token := range tokens {
|
||||
if token.Target == target {
|
||||
fmt.Printf("[%d]: %s => %s (%s)\n",
|
||||
i+3,
|
||||
colors.Cyan(token.ID().Human()),
|
||||
text.TruncateMax(token.Value, 10),
|
||||
token.CreateTime.Format(time.RFC822),
|
||||
)
|
||||
}
|
||||
for i, cred := range creds {
|
||||
token := cred.(*auth.Token)
|
||||
fmt.Printf("[%d]: %s => %s (%s)\n",
|
||||
i+3,
|
||||
colors.Cyan(token.ID().Human()),
|
||||
colors.Red(text.TruncateMax(token.Value, 10)),
|
||||
token.CreateTime().Format(time.RFC822),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,30 +268,28 @@ func promptTokenOptions(repo repository.RepoCommon, owner, project string) (*cor
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
index, err := strconv.Atoi(line)
|
||||
if err != nil || index < 1 || index > len(tokens)+2 {
|
||||
if err != nil || index < 1 || index > len(creds)+2 {
|
||||
fmt.Println("invalid input")
|
||||
continue
|
||||
}
|
||||
|
||||
var token string
|
||||
switch index {
|
||||
case 1:
|
||||
token, err = promptToken()
|
||||
value, err := promptToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return auth.NewToken(userId, value, target), nil
|
||||
case 2:
|
||||
token, err = loginAndRequestToken(owner, project)
|
||||
value, err := loginAndRequestToken(owner, project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return auth.NewToken(userId, value, target), nil
|
||||
default:
|
||||
return tokens[index-3], nil
|
||||
return creds[index-3], nil
|
||||
}
|
||||
|
||||
return core.LoadOrCreateToken(repo, target, token)
|
||||
}
|
||||
}
|
||||
|
||||
@ -435,7 +420,13 @@ func promptUsername() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func promptURL(remotes map[string]string) (string, string, error) {
|
||||
func promptURL(repo repository.RepoCommon) (string, string, error) {
|
||||
// remote suggestions
|
||||
remotes, err := repo.GetRemotes()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
validRemotes := getValidGithubRemoteURLs(remotes)
|
||||
if len(validRemotes) > 0 {
|
||||
for {
|
||||
@ -556,7 +547,7 @@ func validateUsername(username string) (bool, error) {
|
||||
return resp.StatusCode == http.StatusOK, nil
|
||||
}
|
||||
|
||||
func validateProject(owner, project, token string) (bool, error) {
|
||||
func validateProject(owner, project string, token *auth.Token) (bool, error) {
|
||||
url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
@ -565,7 +556,7 @@ func validateProject(owner, project, token string) (bool, error) {
|
||||
}
|
||||
|
||||
// need the token for private repositories
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token.Value))
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
|
@ -5,6 +5,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
)
|
||||
|
||||
func TestSplitURL(t *testing.T) {
|
||||
@ -142,20 +145,23 @@ func TestValidateUsername(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateProject(t *testing.T) {
|
||||
tokenPrivateScope := os.Getenv("GITHUB_TOKEN_PRIVATE")
|
||||
if tokenPrivateScope == "" {
|
||||
envPrivate := os.Getenv("GITHUB_TOKEN_PRIVATE")
|
||||
if envPrivate == "" {
|
||||
t.Skip("Env var GITHUB_TOKEN_PRIVATE missing")
|
||||
}
|
||||
|
||||
tokenPublicScope := os.Getenv("GITHUB_TOKEN_PUBLIC")
|
||||
if tokenPublicScope == "" {
|
||||
envPublic := os.Getenv("GITHUB_TOKEN_PUBLIC")
|
||||
if envPublic == "" {
|
||||
t.Skip("Env var GITHUB_TOKEN_PUBLIC missing")
|
||||
}
|
||||
|
||||
tokenPrivate := auth.NewToken(entity.UnsetId, envPrivate, target)
|
||||
tokenPublic := auth.NewToken(entity.UnsetId, envPublic, target)
|
||||
|
||||
type args struct {
|
||||
owner string
|
||||
project string
|
||||
token string
|
||||
token *auth.Token
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -167,7 +173,7 @@ func TestValidateProject(t *testing.T) {
|
||||
args: args{
|
||||
project: "git-bug",
|
||||
owner: "MichaelMure",
|
||||
token: tokenPublicScope,
|
||||
token: tokenPublic,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
@ -176,7 +182,7 @@ func TestValidateProject(t *testing.T) {
|
||||
args: args{
|
||||
project: "git-bug-test-github-bridge",
|
||||
owner: "MichaelMure",
|
||||
token: tokenPrivateScope,
|
||||
token: tokenPrivate,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
@ -185,7 +191,7 @@ func TestValidateProject(t *testing.T) {
|
||||
args: args{
|
||||
project: "git-bug-test-github-bridge",
|
||||
owner: "MichaelMure",
|
||||
token: tokenPublicScope,
|
||||
token: tokenPublic,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
@ -194,7 +200,7 @@ func TestValidateProject(t *testing.T) {
|
||||
args: args{
|
||||
project: "cant-find-this",
|
||||
owner: "organisation-not-found",
|
||||
token: tokenPublicScope,
|
||||
token: tokenPublic,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
|
@ -15,9 +15,11 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -31,8 +33,12 @@ type githubExporter struct {
|
||||
// cache identities clients
|
||||
identityClient map[entity.Id]*githubv4.Client
|
||||
|
||||
// map identities with their tokens
|
||||
identityToken map[entity.Id]string
|
||||
// the client to use for non user-specific queries
|
||||
// should be the client of the default user
|
||||
defaultClient *githubv4.Client
|
||||
|
||||
// the token of the default user
|
||||
defaultToken *auth.Token
|
||||
|
||||
// github repository ID
|
||||
repositoryID string
|
||||
@ -46,68 +52,86 @@ type githubExporter struct {
|
||||
}
|
||||
|
||||
// Init .
|
||||
func (ge *githubExporter) Init(conf core.Configuration) error {
|
||||
func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
|
||||
ge.conf = conf
|
||||
//TODO: initialize with multiple tokens
|
||||
ge.identityToken = make(map[entity.Id]string)
|
||||
ge.identityClient = make(map[entity.Id]*githubv4.Client)
|
||||
ge.cachedOperationIDs = make(map[entity.Id]string)
|
||||
ge.cachedLabels = make(map[string]string)
|
||||
|
||||
user, err := repo.GetUserIdentity()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// preload all clients
|
||||
err = ge.cacheAllClient(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ge.defaultClient, err = ge.getClientForIdentity(user.Id())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
creds, err := auth.List(repo, auth.WithUserId(user.Id()), auth.WithTarget(target), auth.WithKind(auth.KindToken))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(creds) == 0 {
|
||||
return ErrMissingIdentityToken
|
||||
}
|
||||
|
||||
ge.defaultToken = creds[0].(*auth.Token)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getIdentityClient return a githubv4 API client configured with the access token of the given identity.
|
||||
// if no client were found it will initialize it from the known tokens map and cache it for next use
|
||||
func (ge *githubExporter) getIdentityClient(id entity.Id) (*githubv4.Client, error) {
|
||||
client, ok := ge.identityClient[id]
|
||||
func (ge *githubExporter) cacheAllClient(repo repository.RepoConfig) error {
|
||||
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cred := range creds {
|
||||
if _, ok := ge.identityClient[cred.UserId()]; !ok {
|
||||
client := buildClient(creds[0].(*auth.Token))
|
||||
ge.identityClient[cred.UserId()] = client
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClientForIdentity return a githubv4 API client configured with the access token of the given identity.
|
||||
func (ge *githubExporter) getClientForIdentity(userId entity.Id) (*githubv4.Client, error) {
|
||||
client, ok := ge.identityClient[userId]
|
||||
if ok {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// get token
|
||||
token, ok := ge.identityToken[id]
|
||||
if !ok {
|
||||
return nil, ErrMissingIdentityToken
|
||||
}
|
||||
|
||||
// create client
|
||||
client = buildClient(token)
|
||||
// cache client
|
||||
ge.identityClient[id] = client
|
||||
|
||||
return client, nil
|
||||
return nil, ErrMissingIdentityToken
|
||||
}
|
||||
|
||||
// ExportAll export all event made by the current user to Github
|
||||
func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
|
||||
out := make(chan core.ExportResult)
|
||||
|
||||
user, err := repo.GetUserIdentity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ge.identityToken[user.Id()] = ge.conf[core.ConfigKeyToken]
|
||||
|
||||
var err error
|
||||
// get repository node id
|
||||
ge.repositoryID, err = getRepositoryNodeID(
|
||||
ctx,
|
||||
ge.defaultToken,
|
||||
ge.conf[keyOwner],
|
||||
ge.conf[keyProject],
|
||||
ge.conf[core.ConfigKeyToken],
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := ge.getIdentityClient(user.Id())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// query all labels
|
||||
err = ge.cacheGithubLabels(ctx, client)
|
||||
err = ge.cacheGithubLabels(ctx, ge.defaultClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -115,8 +139,8 @@ func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache,
|
||||
go func() {
|
||||
defer close(out)
|
||||
|
||||
var allIdentitiesIds []entity.Id
|
||||
for id := range ge.identityToken {
|
||||
allIdentitiesIds := make([]entity.Id, 0, len(ge.identityClient))
|
||||
for id := range ge.identityClient {
|
||||
allIdentitiesIds = append(allIdentitiesIds, id)
|
||||
}
|
||||
|
||||
@ -209,7 +233,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc
|
||||
|
||||
} else {
|
||||
// check that we have a token for operation author
|
||||
client, err := ge.getIdentityClient(author.Id())
|
||||
client, err := ge.getClientForIdentity(author.Id())
|
||||
if err != nil {
|
||||
// if bug is still not exported and we do not have the author stop the execution
|
||||
out <- core.NewExportNothing(b.Id(), fmt.Sprintf("missing author token"))
|
||||
@ -262,7 +286,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc
|
||||
}
|
||||
|
||||
opAuthor := op.GetAuthor()
|
||||
client, err := ge.getIdentityClient(opAuthor.Id())
|
||||
client, err := ge.getClientForIdentity(opAuthor.Id())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -384,7 +408,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc
|
||||
}
|
||||
|
||||
// getRepositoryNodeID request github api v3 to get repository node id
|
||||
func getRepositoryNodeID(ctx context.Context, owner, project, token string) (string, error) {
|
||||
func getRepositoryNodeID(ctx context.Context, token *auth.Token, owner, project string) (string, error) {
|
||||
url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
|
||||
client := &http.Client{}
|
||||
|
||||
@ -394,7 +418,7 @@ func getRepositoryNodeID(ctx context.Context, owner, project, token string) (str
|
||||
}
|
||||
|
||||
// need the token for private repositories
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token.Value))
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
|
||||
defer cancel()
|
||||
@ -512,7 +536,7 @@ func (ge *githubExporter) createGithubLabel(ctx context.Context, label, color st
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
// need the token for private repositories
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[core.ConfigKeyToken]))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.defaultToken.Value))
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
@ -30,7 +31,7 @@ type testCase struct {
|
||||
numOrOp int // number of original operations
|
||||
}
|
||||
|
||||
func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCache) []*testCase {
|
||||
func testCases(t *testing.T, repo *cache.RepoCache) []*testCase {
|
||||
// simple bug
|
||||
simpleBug, _, err := repo.NewBug("simple bug", "new bug")
|
||||
require.NoError(t, err)
|
||||
@ -92,32 +93,32 @@ func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCach
|
||||
require.NoError(t, err)
|
||||
|
||||
return []*testCase{
|
||||
&testCase{
|
||||
{
|
||||
name: "simple bug",
|
||||
bug: simpleBug,
|
||||
numOrOp: 1,
|
||||
},
|
||||
&testCase{
|
||||
{
|
||||
name: "bug with comments",
|
||||
bug: bugWithComments,
|
||||
numOrOp: 2,
|
||||
},
|
||||
&testCase{
|
||||
{
|
||||
name: "bug label change",
|
||||
bug: bugLabelChange,
|
||||
numOrOp: 6,
|
||||
},
|
||||
&testCase{
|
||||
{
|
||||
name: "bug with comment editions",
|
||||
bug: bugWithCommentEditions,
|
||||
numOrOp: 4,
|
||||
},
|
||||
&testCase{
|
||||
{
|
||||
name: "bug changed status",
|
||||
bug: bugStatusChanged,
|
||||
numOrOp: 3,
|
||||
},
|
||||
&testCase{
|
||||
{
|
||||
name: "bug title edited",
|
||||
bug: bugTitleEdited,
|
||||
numOrOp: 2,
|
||||
@ -127,11 +128,11 @@ func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCach
|
||||
|
||||
func TestPushPull(t *testing.T) {
|
||||
// repo owner
|
||||
user := os.Getenv("GITHUB_TEST_USER")
|
||||
envUser := os.Getenv("GITHUB_TEST_USER")
|
||||
|
||||
// token must have 'repo' and 'delete_repo' scopes
|
||||
token := os.Getenv("GITHUB_TOKEN_ADMIN")
|
||||
if token == "" {
|
||||
envToken := os.Getenv("GITHUB_TOKEN_ADMIN")
|
||||
if envToken == "" {
|
||||
t.Skip("Env var GITHUB_TOKEN_ADMIN missing")
|
||||
}
|
||||
|
||||
@ -152,35 +153,38 @@ func TestPushPull(t *testing.T) {
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
tests := testCases(t, backend, author)
|
||||
tests := testCases(t, backend)
|
||||
|
||||
// generate project name
|
||||
projectName := generateRepoName()
|
||||
|
||||
// create target Github repository
|
||||
err = createRepository(projectName, token)
|
||||
err = createRepository(projectName, envToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Println("created repository", projectName)
|
||||
|
||||
// Make sure to remove the Github repository when the test end
|
||||
defer func(t *testing.T) {
|
||||
if err := deleteRepository(projectName, user, token); err != nil {
|
||||
if err := deleteRepository(projectName, envUser, envToken); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println("deleted repository:", projectName)
|
||||
}(t)
|
||||
|
||||
interrupt.RegisterCleaner(func() error {
|
||||
return deleteRepository(projectName, user, token)
|
||||
return deleteRepository(projectName, envUser, envToken)
|
||||
})
|
||||
|
||||
token := auth.NewToken(author.Id(), envToken, target)
|
||||
err = auth.Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
|
||||
// initialize exporter
|
||||
exporter := &githubExporter{}
|
||||
err = exporter.Init(core.Configuration{
|
||||
keyOwner: user,
|
||||
err = exporter.Init(backend, core.Configuration{
|
||||
keyOwner: envUser,
|
||||
keyProject: projectName,
|
||||
keyToken: token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -206,10 +210,9 @@ func TestPushPull(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
importer := &githubImporter{}
|
||||
err = importer.Init(core.Configuration{
|
||||
keyOwner: user,
|
||||
err = importer.Init(backend, core.Configuration{
|
||||
keyOwner: envUser,
|
||||
keyProject: projectName,
|
||||
keyToken: token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
)
|
||||
|
||||
type Github struct{}
|
||||
@ -24,9 +25,9 @@ func (*Github) NewExporter() core.Exporter {
|
||||
return &githubExporter{}
|
||||
}
|
||||
|
||||
func buildClient(token string) *githubv4.Client {
|
||||
func buildClient(token *auth.Token) *githubv4.Client {
|
||||
src := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
&oauth2.Token{AccessToken: token.Value},
|
||||
)
|
||||
httpClient := oauth2.NewClient(context.TODO(), src)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/shurcooL/githubv4"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
@ -24,6 +25,9 @@ const (
|
||||
type githubImporter struct {
|
||||
conf core.Configuration
|
||||
|
||||
// default user client
|
||||
client *githubv4.Client
|
||||
|
||||
// iterator
|
||||
iterator *iterator
|
||||
|
||||
@ -31,15 +35,37 @@ type githubImporter struct {
|
||||
out chan<- core.ImportResult
|
||||
}
|
||||
|
||||
func (gi *githubImporter) Init(conf core.Configuration) error {
|
||||
func (gi *githubImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
|
||||
gi.conf = conf
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithTarget(target),
|
||||
auth.WithKind(auth.KindToken),
|
||||
}
|
||||
|
||||
user, err := repo.GetUserIdentity()
|
||||
if err == nil {
|
||||
opts = append(opts, auth.WithUserId(user.Id()))
|
||||
}
|
||||
|
||||
creds, err := auth.List(repo, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(creds) == 0 {
|
||||
return ErrMissingIdentityToken
|
||||
}
|
||||
|
||||
gi.client = buildClient(creds[0].(*auth.Token))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportAll iterate over all the configured repository issues and ensure the creation of the
|
||||
// missing issues / timeline items / edits / label events ...
|
||||
func (gi *githubImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
|
||||
gi.iterator = NewIterator(ctx, 10, gi.conf[keyOwner], gi.conf[keyProject], gi.conf[core.ConfigKeyToken], since)
|
||||
gi.iterator = NewIterator(ctx, gi.client, 10, gi.conf[keyOwner], gi.conf[keyProject], since)
|
||||
out := make(chan core.ImportResult)
|
||||
gi.out = out
|
||||
|
||||
@ -494,7 +520,7 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca
|
||||
if err == nil {
|
||||
return i, nil
|
||||
}
|
||||
if _, ok := err.(entity.ErrMultipleMatch); ok {
|
||||
if entity.IsErrMultipleMatch(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -543,7 +569,7 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache,
|
||||
if err == nil {
|
||||
return i, nil
|
||||
}
|
||||
if _, ok := err.(entity.ErrMultipleMatch); ok {
|
||||
if entity.IsErrMultipleMatch(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -553,12 +579,10 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache,
|
||||
"login": githubv4.String("ghost"),
|
||||
}
|
||||
|
||||
gc := buildClient(gi.conf[core.ConfigKeyToken])
|
||||
|
||||
ctx, cancel := context.WithTimeout(gi.iterator.ctx, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = gc.Query(ctx, &q, variables)
|
||||
err = gi.client.Query(ctx, &q, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
@ -134,16 +135,22 @@ func Test_Importer(t *testing.T) {
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
token := os.Getenv("GITHUB_TOKEN_PRIVATE")
|
||||
if token == "" {
|
||||
envToken := os.Getenv("GITHUB_TOKEN_PRIVATE")
|
||||
if envToken == "" {
|
||||
t.Skip("Env var GITHUB_TOKEN_PRIVATE missing")
|
||||
}
|
||||
|
||||
err = author.Commit(repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
token := auth.NewToken(author.Id(), envToken, target)
|
||||
err = auth.Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
|
||||
importer := &githubImporter{}
|
||||
err = importer.Init(core.Configuration{
|
||||
err = importer.Init(backend, core.Configuration{
|
||||
keyOwner: "MichaelMure",
|
||||
keyProject: "git-bug-test-github-bridge",
|
||||
keyToken: token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -63,9 +63,9 @@ type iterator struct {
|
||||
}
|
||||
|
||||
// NewIterator create and initialize a new iterator
|
||||
func NewIterator(ctx context.Context, capacity int, owner, project, token string, since time.Time) *iterator {
|
||||
func NewIterator(ctx context.Context, client *githubv4.Client, capacity int, owner, project string, since time.Time) *iterator {
|
||||
i := &iterator{
|
||||
gc: buildClient(token),
|
||||
gc: client,
|
||||
since: since,
|
||||
capacity: capacity,
|
||||
ctx: ctx,
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -15,6 +16,8 @@ import (
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/colors"
|
||||
@ -24,7 +27,7 @@ var (
|
||||
ErrBadProjectURL = errors.New("bad project url")
|
||||
)
|
||||
|
||||
func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
|
||||
func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
|
||||
if params.Project != "" {
|
||||
fmt.Println("warning: --project is ineffective for a gitlab bridge")
|
||||
}
|
||||
@ -34,82 +37,77 @@ func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams)
|
||||
|
||||
conf := make(core.Configuration)
|
||||
var err error
|
||||
var url string
|
||||
var token string
|
||||
var tokenId entity.Id
|
||||
var tokenObj *core.Token
|
||||
|
||||
if (params.Token != "" || params.TokenStdin) && params.URL == "" {
|
||||
if (params.CredPrefix != "" || params.TokenRaw != "") && params.URL == "" {
|
||||
return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
|
||||
}
|
||||
|
||||
var url string
|
||||
|
||||
// get project url
|
||||
if params.URL != "" {
|
||||
switch {
|
||||
case params.URL != "":
|
||||
url = params.URL
|
||||
|
||||
} else {
|
||||
// remote suggestions
|
||||
remotes, err := repo.GetRemotes()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting remotes")
|
||||
}
|
||||
|
||||
default:
|
||||
// terminal prompt
|
||||
url, err = promptURL(remotes)
|
||||
url, err = promptURL(repo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "url prompt")
|
||||
}
|
||||
}
|
||||
|
||||
// get user token
|
||||
if params.Token != "" {
|
||||
token = params.Token
|
||||
} else if params.TokenStdin {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
token, err = reader.ReadString('\n')
|
||||
user, err := repo.GetUserIdentity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cred auth.Credential
|
||||
|
||||
switch {
|
||||
case params.CredPrefix != "":
|
||||
cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading from stdin: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
token = strings.TrimSpace(token)
|
||||
} else if params.TokenId != "" {
|
||||
tokenId = entity.Id(params.TokenId)
|
||||
} else {
|
||||
tokenObj, err = promptTokenOptions(repo)
|
||||
if cred.UserId() != user.Id() {
|
||||
return nil, fmt.Errorf("selected credential don't match the user")
|
||||
}
|
||||
case params.TokenRaw != "":
|
||||
cred = auth.NewToken(user.Id(), params.TokenRaw, target)
|
||||
default:
|
||||
cred, err = promptTokenOptions(repo, user.Id())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "token prompt")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
tokenObj, err = core.LoadOrCreateToken(repo, target, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if tokenId != "" {
|
||||
tokenObj, err = core.LoadToken(repo, entity.Id(tokenId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tokenObj.Target != target {
|
||||
return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target)
|
||||
}
|
||||
token, ok := cred.(*auth.Token)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the Gitlab bridge only handle token credentials")
|
||||
}
|
||||
|
||||
// validate project url and get its ID
|
||||
id, err := validateProjectURL(url, tokenObj.Value)
|
||||
id, err := validateProjectURL(url, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "project validation")
|
||||
}
|
||||
|
||||
conf[keyProjectID] = strconv.Itoa(id)
|
||||
conf[core.ConfigKeyTokenId] = tokenObj.ID().String()
|
||||
conf[core.ConfigKeyTarget] = target
|
||||
conf[keyProjectID] = strconv.Itoa(id)
|
||||
|
||||
err = g.ValidateConfig(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// don't forget to store the now known valid token
|
||||
if !auth.IdExist(repo, cred.ID()) {
|
||||
err = auth.Store(repo, cred)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
@ -120,10 +118,6 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
|
||||
return fmt.Errorf("unexpected target name: %v", v)
|
||||
}
|
||||
|
||||
if _, ok := conf[keyToken]; !ok {
|
||||
return fmt.Errorf("missing %s key", keyToken)
|
||||
}
|
||||
|
||||
if _, ok := conf[keyProjectID]; !ok {
|
||||
return fmt.Errorf("missing %s key", keyProjectID)
|
||||
}
|
||||
@ -131,19 +125,20 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) {
|
||||
func promptTokenOptions(repo repository.RepoConfig, userId entity.Id) (auth.Credential, error) {
|
||||
for {
|
||||
tokens, err := core.LoadTokensWithTarget(repo, target)
|
||||
creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target), auth.WithKind(auth.KindToken))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tokens) == 0 {
|
||||
token, err := promptToken()
|
||||
// if we don't have existing token, fast-track to the token prompt
|
||||
if len(creds) == 0 {
|
||||
value, err := promptToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return core.LoadOrCreateToken(repo, target, token)
|
||||
return auth.NewToken(userId, value, target), nil
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
@ -151,15 +146,16 @@ func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) {
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Existing tokens for Gitlab:")
|
||||
for i, token := range tokens {
|
||||
if token.Target == target {
|
||||
fmt.Printf("[%d]: %s => %s (%s)\n",
|
||||
i+2,
|
||||
colors.Cyan(token.ID().Human()),
|
||||
text.TruncateMax(token.Value, 10),
|
||||
token.CreateTime.Format(time.RFC822),
|
||||
)
|
||||
}
|
||||
|
||||
sort.Sort(auth.ById(creds))
|
||||
for i, cred := range creds {
|
||||
token := cred.(*auth.Token)
|
||||
fmt.Printf("[%d]: %s => %s (%s)\n",
|
||||
i+2,
|
||||
colors.Cyan(token.ID().Human()),
|
||||
colors.Red(text.TruncateMax(token.Value, 10)),
|
||||
token.CreateTime().Format(time.RFC822),
|
||||
)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
@ -173,23 +169,21 @@ func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) {
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
index, err := strconv.Atoi(line)
|
||||
if err != nil || index < 1 || index > len(tokens)+1 {
|
||||
if err != nil || index < 1 || index > len(creds)+1 {
|
||||
fmt.Println("invalid input")
|
||||
continue
|
||||
}
|
||||
|
||||
var token string
|
||||
switch index {
|
||||
case 1:
|
||||
token, err = promptToken()
|
||||
value, err := promptToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return auth.NewToken(userId, value, target), nil
|
||||
default:
|
||||
return tokens[index-2], nil
|
||||
return creds[index-2], nil
|
||||
}
|
||||
|
||||
return core.LoadOrCreateToken(repo, target, token)
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,7 +216,13 @@ func promptToken() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func promptURL(remotes map[string]string) (string, error) {
|
||||
func promptURL(repo repository.RepoCommon) (string, error) {
|
||||
// remote suggestions
|
||||
remotes, err := repo.GetRemotes()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "getting remotes")
|
||||
}
|
||||
|
||||
validRemotes := getValidGitlabRemoteURLs(remotes)
|
||||
if len(validRemotes) > 0 {
|
||||
for {
|
||||
@ -302,7 +302,7 @@ func getValidGitlabRemoteURLs(remotes map[string]string) []string {
|
||||
return urls
|
||||
}
|
||||
|
||||
func validateProjectURL(url, token string) (int, error) {
|
||||
func validateProjectURL(url string, token *auth.Token) (int, error) {
|
||||
projectPath, err := getProjectPath(url)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -10,9 +10,11 @@ import (
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -24,10 +26,7 @@ type gitlabExporter struct {
|
||||
conf core.Configuration
|
||||
|
||||
// cache identities clients
|
||||
identityClient map[string]*gitlab.Client
|
||||
|
||||
// map identities with their tokens
|
||||
identityToken map[string]string
|
||||
identityClient map[entity.Id]*gitlab.Client
|
||||
|
||||
// gitlab repository ID
|
||||
repositoryID string
|
||||
@ -38,58 +37,59 @@ type gitlabExporter struct {
|
||||
}
|
||||
|
||||
// Init .
|
||||
func (ge *gitlabExporter) Init(conf core.Configuration) error {
|
||||
func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
|
||||
ge.conf = conf
|
||||
//TODO: initialize with multiple tokens
|
||||
ge.identityToken = make(map[string]string)
|
||||
ge.identityClient = make(map[string]*gitlab.Client)
|
||||
ge.identityClient = make(map[entity.Id]*gitlab.Client)
|
||||
ge.cachedOperationIDs = make(map[string]string)
|
||||
|
||||
// get repository node id
|
||||
ge.repositoryID = ge.conf[keyProjectID]
|
||||
|
||||
// preload all clients
|
||||
err := ge.cacheAllClient(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ge *gitlabExporter) cacheAllClient(repo repository.RepoConfig) error {
|
||||
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cred := range creds {
|
||||
if _, ok := ge.identityClient[cred.UserId()]; !ok {
|
||||
client := buildClient(creds[0].(*auth.Token))
|
||||
ge.identityClient[cred.UserId()] = client
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getIdentityClient return a gitlab v4 API client configured with the access token of the given identity.
|
||||
// if no client were found it will initialize it from the known tokens map and cache it for next use
|
||||
func (ge *gitlabExporter) getIdentityClient(id entity.Id) (*gitlab.Client, error) {
|
||||
client, ok := ge.identityClient[id.String()]
|
||||
func (ge *gitlabExporter) getIdentityClient(userId entity.Id) (*gitlab.Client, error) {
|
||||
client, ok := ge.identityClient[userId]
|
||||
if ok {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// get token
|
||||
token, ok := ge.identityToken[id.String()]
|
||||
if !ok {
|
||||
return nil, ErrMissingIdentityToken
|
||||
}
|
||||
|
||||
// create client
|
||||
client = buildClient(token)
|
||||
// cache client
|
||||
ge.identityClient[id.String()] = client
|
||||
|
||||
return client, nil
|
||||
return nil, ErrMissingIdentityToken
|
||||
}
|
||||
|
||||
// ExportAll export all event made by the current user to Gitlab
|
||||
func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
|
||||
out := make(chan core.ExportResult)
|
||||
|
||||
user, err := repo.GetUserIdentity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ge.identityToken[user.Id().String()] = ge.conf[core.ConfigKeyToken]
|
||||
|
||||
// get repository node id
|
||||
ge.repositoryID = ge.conf[keyProjectID]
|
||||
|
||||
go func() {
|
||||
defer close(out)
|
||||
|
||||
allIdentitiesIds := make([]entity.Id, 0, len(ge.identityToken))
|
||||
for id := range ge.identityToken {
|
||||
allIdentitiesIds = append(allIdentitiesIds, entity.Id(id))
|
||||
allIdentitiesIds := make([]entity.Id, 0, len(ge.identityClient))
|
||||
for id := range ge.identityClient {
|
||||
allIdentitiesIds = append(allIdentitiesIds, id)
|
||||
}
|
||||
|
||||
allBugsIds := repo.AllBugsIds()
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
@ -32,7 +33,7 @@ type testCase struct {
|
||||
numOpImp int // number of operations after import
|
||||
}
|
||||
|
||||
func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCache) []*testCase {
|
||||
func testCases(t *testing.T, repo *cache.RepoCache) []*testCase {
|
||||
// simple bug
|
||||
simpleBug, _, err := repo.NewBug("simple bug", "new bug")
|
||||
require.NoError(t, err)
|
||||
@ -135,8 +136,8 @@ func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCach
|
||||
|
||||
func TestPushPull(t *testing.T) {
|
||||
// token must have 'repo' and 'delete_repo' scopes
|
||||
token := os.Getenv("GITLAB_API_TOKEN")
|
||||
if token == "" {
|
||||
envToken := os.Getenv("GITLAB_API_TOKEN")
|
||||
if envToken == "" {
|
||||
t.Skip("Env var GITLAB_API_TOKEN missing")
|
||||
}
|
||||
|
||||
@ -157,7 +158,11 @@ func TestPushPull(t *testing.T) {
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
tests := testCases(t, backend, author)
|
||||
tests := testCases(t, backend)
|
||||
|
||||
token := auth.NewToken(author.Id(), envToken, target)
|
||||
err = auth.Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
|
||||
// generate project name
|
||||
projectName := generateRepoName()
|
||||
@ -182,9 +187,8 @@ func TestPushPull(t *testing.T) {
|
||||
|
||||
// initialize exporter
|
||||
exporter := &gitlabExporter{}
|
||||
err = exporter.Init(core.Configuration{
|
||||
err = exporter.Init(backend, core.Configuration{
|
||||
keyProjectID: strconv.Itoa(projectID),
|
||||
keyToken: token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -210,9 +214,8 @@ func TestPushPull(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
importer := &gitlabImporter{}
|
||||
err = importer.Init(core.Configuration{
|
||||
err = importer.Init(backend, core.Configuration{
|
||||
keyProjectID: strconv.Itoa(projectID),
|
||||
keyToken: token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -276,7 +279,7 @@ func generateRepoName() string {
|
||||
}
|
||||
|
||||
// create repository need a token with scope 'repo'
|
||||
func createRepository(ctx context.Context, name, token string) (int, error) {
|
||||
func createRepository(ctx context.Context, name string, token *auth.Token) (int, error) {
|
||||
client := buildClient(token)
|
||||
project, _, err := client.Projects.CreateProject(
|
||||
&gitlab.CreateProjectOptions{
|
||||
@ -292,7 +295,7 @@ func createRepository(ctx context.Context, name, token string) (int, error) {
|
||||
}
|
||||
|
||||
// delete repository need a token with scope 'delete_repo'
|
||||
func deleteRepository(ctx context.Context, project int, token string) error {
|
||||
func deleteRepository(ctx context.Context, project int, token *auth.Token) error {
|
||||
client := buildClient(token)
|
||||
_, err := client.Projects.DeleteProject(project, gitlab.WithContext(ctx))
|
||||
return err
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -18,7 +19,6 @@ const (
|
||||
metaKeyGitlabProject = "gitlab-project-id"
|
||||
|
||||
keyProjectID = "project-id"
|
||||
keyToken = "token"
|
||||
|
||||
defaultTimeout = 60 * time.Second
|
||||
)
|
||||
@ -37,10 +37,10 @@ func (*Gitlab) NewExporter() core.Exporter {
|
||||
return &gitlabExporter{}
|
||||
}
|
||||
|
||||
func buildClient(token string) *gitlab.Client {
|
||||
func buildClient(token *auth.Token) *gitlab.Client {
|
||||
client := &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
|
||||
return gitlab.NewClient(client, token)
|
||||
return gitlab.NewClient(client, token.Value)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
@ -19,6 +20,9 @@ import (
|
||||
type gitlabImporter struct {
|
||||
conf core.Configuration
|
||||
|
||||
// default user client
|
||||
client *gitlab.Client
|
||||
|
||||
// iterator
|
||||
iterator *iterator
|
||||
|
||||
@ -26,15 +30,37 @@ type gitlabImporter struct {
|
||||
out chan<- core.ImportResult
|
||||
}
|
||||
|
||||
func (gi *gitlabImporter) Init(conf core.Configuration) error {
|
||||
func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
|
||||
gi.conf = conf
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithTarget(target),
|
||||
auth.WithKind(auth.KindToken),
|
||||
}
|
||||
|
||||
user, err := repo.GetUserIdentity()
|
||||
if err == nil {
|
||||
opts = append(opts, auth.WithUserId(user.Id()))
|
||||
}
|
||||
|
||||
creds, err := auth.List(repo, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(creds) == 0 {
|
||||
return ErrMissingIdentityToken
|
||||
}
|
||||
|
||||
gi.client = buildClient(creds[0].(*auth.Token))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
|
||||
// of the missing issues / comments / label events / title changes ...
|
||||
func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
|
||||
gi.iterator = NewIterator(ctx, 10, gi.conf[keyProjectID], gi.conf[core.ConfigKeyToken], since)
|
||||
gi.iterator = NewIterator(ctx, gi.client, 10, gi.conf[keyProjectID], since)
|
||||
out := make(chan core.ImportResult)
|
||||
gi.out = out
|
||||
|
||||
@ -357,13 +383,11 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id
|
||||
if err == nil {
|
||||
return i, nil
|
||||
}
|
||||
if _, ok := err.(entity.ErrMultipleMatch); ok {
|
||||
if entity.IsErrMultipleMatch(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := buildClient(gi.conf["token"])
|
||||
|
||||
user, _, err := client.Users.GetUser(id)
|
||||
user, _, err := gi.client.Users.GetUser(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
@ -83,8 +84,8 @@ func TestImport(t *testing.T) {
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
token := os.Getenv("GITLAB_API_TOKEN")
|
||||
if token == "" {
|
||||
envToken := os.Getenv("GITLAB_API_TOKEN")
|
||||
if envToken == "" {
|
||||
t.Skip("Env var GITLAB_API_TOKEN missing")
|
||||
}
|
||||
|
||||
@ -93,10 +94,16 @@ func TestImport(t *testing.T) {
|
||||
t.Skip("Env var GITLAB_PROJECT_ID missing")
|
||||
}
|
||||
|
||||
err = author.Commit(repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
token := auth.NewToken(author.Id(), envToken, target)
|
||||
err = auth.Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
|
||||
importer := &gitlabImporter{}
|
||||
err = importer.Init(core.Configuration{
|
||||
err = importer.Init(backend, core.Configuration{
|
||||
keyProjectID: projectID,
|
||||
keyToken: token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -71,9 +71,9 @@ type iterator struct {
|
||||
}
|
||||
|
||||
// NewIterator create a new iterator
|
||||
func NewIterator(ctx context.Context, capacity int, projectID, token string, since time.Time) *iterator {
|
||||
func NewIterator(ctx context.Context, client *gitlab.Client, capacity int, projectID string, since time.Time) *iterator {
|
||||
return &iterator{
|
||||
gc: buildClient(token),
|
||||
gc: client,
|
||||
project: projectID,
|
||||
since: since,
|
||||
capacity: capacity,
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
)
|
||||
|
||||
var ErrBadProjectURL = errors.New("bad Launchpad project URL")
|
||||
@ -22,9 +22,9 @@ const (
|
||||
defaultTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
func (l *Launchpad) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
|
||||
if params.Token != "" {
|
||||
fmt.Println("warning: --token is ineffective for a Launchpad bridge")
|
||||
func (l *Launchpad) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
|
||||
if params.TokenRaw != "" {
|
||||
fmt.Println("warning: token params are ineffective for a Launchpad bridge")
|
||||
}
|
||||
if params.Owner != "" {
|
||||
fmt.Println("warning: --owner is ineffective for a Launchpad bridge")
|
||||
@ -34,22 +34,19 @@ func (l *Launchpad) Configure(repo repository.RepoCommon, params core.BridgePara
|
||||
var err error
|
||||
var project string
|
||||
|
||||
if params.Project != "" {
|
||||
switch {
|
||||
case params.Project != "":
|
||||
project = params.Project
|
||||
|
||||
} else if params.URL != "" {
|
||||
case params.URL != "":
|
||||
// get project name from url
|
||||
project, err = splitURL(params.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else {
|
||||
default:
|
||||
// get project name from terminal prompt
|
||||
project, err = promptProjectName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// verify project
|
||||
@ -61,8 +58,8 @@ func (l *Launchpad) Configure(repo repository.RepoCommon, params core.BridgePara
|
||||
return nil, fmt.Errorf("project doesn't exist")
|
||||
}
|
||||
|
||||
conf[keyProject] = project
|
||||
conf[core.ConfigKeyTarget] = target
|
||||
conf[keyProject] = project
|
||||
|
||||
err = l.ValidateConfig(conf)
|
||||
if err != nil {
|
||||
@ -73,12 +70,14 @@ func (l *Launchpad) Configure(repo repository.RepoCommon, params core.BridgePara
|
||||
}
|
||||
|
||||
func (*Launchpad) ValidateConfig(conf core.Configuration) error {
|
||||
if _, ok := conf[keyProject]; !ok {
|
||||
return fmt.Errorf("missing %s key", keyProject)
|
||||
if v, ok := conf[core.ConfigKeyTarget]; !ok {
|
||||
return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
|
||||
} else if v != target {
|
||||
return fmt.Errorf("unexpected target name: %v", v)
|
||||
}
|
||||
|
||||
if _, ok := conf[core.ConfigKeyTarget]; !ok {
|
||||
return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
|
||||
if _, ok := conf[keyProject]; !ok {
|
||||
return fmt.Errorf("missing %s key", keyProject)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -15,7 +15,7 @@ type launchpadImporter struct {
|
||||
conf core.Configuration
|
||||
}
|
||||
|
||||
func (li *launchpadImporter) Init(conf core.Configuration) error {
|
||||
func (li *launchpadImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
|
||||
li.conf = conf
|
||||
return nil
|
||||
}
|
||||
@ -31,7 +31,7 @@ func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson)
|
||||
if err == nil {
|
||||
return i, nil
|
||||
}
|
||||
if _, ok := err.(entity.ErrMultipleMatch); ok {
|
||||
if entity.IsErrMultipleMatch(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ func runAddBug(cmd *cobra.Command, args []string) error {
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Create a new bug.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runAddBug,
|
||||
}
|
||||
|
||||
|
@ -7,38 +7,58 @@ import (
|
||||
|
||||
text "github.com/MichaelMure/go-term-text"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/util/colors"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
)
|
||||
|
||||
func runBridgeAuth(cmd *cobra.Command, args []string) error {
|
||||
tokens, err := core.ListTokens(repo)
|
||||
backend, err := cache.NewRepoCache(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
creds, err := auth.List(backend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
token, err := core.LoadToken(repo, token)
|
||||
defaultUser, _ := backend.GetUserIdentity()
|
||||
|
||||
for _, cred := range creds {
|
||||
targetFmt := text.LeftPadMaxLine(cred.Target(), 10, 0)
|
||||
|
||||
var value string
|
||||
switch cred := cred.(type) {
|
||||
case *auth.Token:
|
||||
value = cred.Value
|
||||
}
|
||||
|
||||
user, err := backend.ResolveIdentity(cred.UserId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printToken(token)
|
||||
userFmt := user.DisplayName()
|
||||
|
||||
if cred.UserId() == defaultUser.Id() {
|
||||
userFmt = colors.Red(userFmt)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s %s %s %s\n",
|
||||
colors.Cyan(cred.ID().Human()),
|
||||
colors.Yellow(targetFmt),
|
||||
colors.Magenta(cred.Kind()),
|
||||
userFmt,
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printToken(token *core.Token) {
|
||||
targetFmt := text.LeftPadMaxLine(token.Target, 10, 0)
|
||||
|
||||
fmt.Printf("%s %s %s %s\n",
|
||||
colors.Cyan(token.ID().Human()),
|
||||
colors.Yellow(targetFmt),
|
||||
colors.Magenta("token"),
|
||||
token.Value,
|
||||
)
|
||||
}
|
||||
|
||||
var bridgeAuthCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "List all known bridge authentication credentials.",
|
||||
|
@ -12,6 +12,8 @@ import (
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge"
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -22,7 +24,7 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
|
||||
var value string
|
||||
|
||||
if bridgeAuthAddTokenTarget == "" {
|
||||
return fmt.Errorf("auth target is required")
|
||||
return fmt.Errorf("flag --target is required")
|
||||
}
|
||||
|
||||
if !core.TargetExist(bridgeAuthAddTokenTarget) {
|
||||
@ -44,12 +46,17 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
|
||||
value = strings.TrimSuffix(raw, "\n")
|
||||
}
|
||||
|
||||
token := core.NewToken(value, bridgeAuthAddTokenTarget)
|
||||
user, err := identity.GetUserIdentity(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token := auth.NewToken(user.Id(), value, bridgeAuthAddTokenTarget)
|
||||
if err := token.Validate(); err != nil {
|
||||
return errors.Wrap(err, "invalid token")
|
||||
}
|
||||
|
||||
err := core.StoreToken(repo, token)
|
||||
err = auth.Store(repo, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -61,7 +68,7 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
|
||||
var bridgeAuthAddTokenCmd = &cobra.Command{
|
||||
Use: "add-token [<token>]",
|
||||
Short: "Store a new token",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runBridgeTokenAdd,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
}
|
@ -5,21 +5,21 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
)
|
||||
|
||||
func runBridgeAuthRm(cmd *cobra.Command, args []string) error {
|
||||
token, err := core.LoadTokenPrefix(repo, args[0])
|
||||
cred, err := auth.LoadWithPrefix(repo, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = core.RemoveToken(repo, token.ID())
|
||||
err = auth.Remove(repo, cred.ID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("token %s removed\n", token.ID())
|
||||
fmt.Printf("credential %s removed\n", cred.ID())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -6,20 +6,24 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
)
|
||||
|
||||
func runBridgeAuthShow(cmd *cobra.Command, args []string) error {
|
||||
token, err := core.LoadTokenPrefix(repo, args[0])
|
||||
cred, err := auth.LoadWithPrefix(repo, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Id: %s\n", token.ID())
|
||||
fmt.Printf("Target: %s\n", token.Target)
|
||||
fmt.Printf("Type: token\n")
|
||||
fmt.Printf("Value: %s\n", token.Value)
|
||||
fmt.Printf("Creation: %s\n", token.CreateTime.Format(time.RFC822))
|
||||
fmt.Printf("Id: %s\n", cred.ID())
|
||||
fmt.Printf("Target: %s\n", cred.Target())
|
||||
fmt.Printf("Kind: %s\n", cred.Kind())
|
||||
fmt.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822))
|
||||
|
||||
switch cred := cred.(type) {
|
||||
case *auth.Token:
|
||||
fmt.Printf("Value: %s\n", cred.Value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge"
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
@ -21,9 +22,11 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
bridgeConfigureName string
|
||||
bridgeConfigureTarget string
|
||||
bridgeParams core.BridgeParams
|
||||
bridgeConfigureName string
|
||||
bridgeConfigureTarget string
|
||||
bridgeConfigureParams core.BridgeParams
|
||||
bridgeConfigureToken string
|
||||
bridgeConfigureTokenStdin bool
|
||||
)
|
||||
|
||||
func runBridgeConfigure(cmd *cobra.Command, args []string) error {
|
||||
@ -34,9 +37,28 @@ func runBridgeConfigure(cmd *cobra.Command, args []string) error {
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
if (bridgeParams.TokenStdin || bridgeParams.Token != "" || bridgeParams.TokenId != "") &&
|
||||
if (bridgeConfigureTokenStdin || bridgeConfigureToken != "" || bridgeConfigureParams.CredPrefix != "") &&
|
||||
(bridgeConfigureName == "" || bridgeConfigureTarget == "") {
|
||||
return fmt.Errorf("you must provide a bridge name and target to configure a bridge with a token")
|
||||
return fmt.Errorf("you must provide a bridge name and target to configure a bridge with a credential")
|
||||
}
|
||||
|
||||
// early fail
|
||||
if bridgeConfigureParams.CredPrefix != "" {
|
||||
if _, err := auth.LoadWithPrefix(repo, bridgeConfigureParams.CredPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case bridgeConfigureTokenStdin:
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
token, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading from stdin: %v", err)
|
||||
}
|
||||
bridgeConfigureParams.TokenRaw = strings.TrimSpace(token)
|
||||
case bridgeConfigureToken != "":
|
||||
bridgeConfigureParams.TokenRaw = bridgeConfigureToken
|
||||
}
|
||||
|
||||
if bridgeConfigureTarget == "" {
|
||||
@ -58,7 +80,7 @@ func runBridgeConfigure(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.Configure(bridgeParams)
|
||||
err = b.Configure(bridgeConfigureParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -94,7 +116,7 @@ func promptTarget() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func promptName(repo repository.RepoCommon) (string, error) {
|
||||
func promptName(repo repository.RepoConfig) (string, error) {
|
||||
defaultExist := core.BridgeExist(repo, defaultName)
|
||||
|
||||
for {
|
||||
@ -184,7 +206,7 @@ git bug bridge configure \
|
||||
--target=github \
|
||||
--url=https://github.com/michaelmure/git-bug \
|
||||
--token=$(TOKEN)`,
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runBridgeConfigure,
|
||||
}
|
||||
|
||||
@ -193,11 +215,11 @@ func init() {
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureName, "name", "n", "", "A distinctive name to identify the bridge")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureTarget, "target", "t", "",
|
||||
fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.URL, "url", "u", "", "The URL of the target repository")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Owner, "owner", "o", "", "The owner of the target repository")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Token, "token", "T", "", "The authentication token for the API")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.TokenId, "token-id", "i", "", "The authentication token identifier for the API")
|
||||
bridgeConfigureCmd.Flags().BoolVar(&bridgeParams.TokenStdin, "token-stdin", false, "Will read the token from stdin and ignore --token")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeParams.Project, "project", "p", "", "The name of the target repository")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.URL, "url", "u", "", "The URL of the target repository")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.Owner, "owner", "o", "", "The owner of the target repository")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.CredPrefix, "credential", "c", "", "The identifier or prefix of an already known credential for the API (see \"git-bug bridge auth\")")
|
||||
bridgeConfigureCmd.Flags().StringVar(&bridgeConfigureToken, "token", "", "A raw authentication token for the API")
|
||||
bridgeConfigureCmd.Flags().BoolVar(&bridgeConfigureTokenStdin, "token-stdin", false, "Will read the token from stdin and ignore --token")
|
||||
bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.Project, "project", "p", "", "The name of the target repository")
|
||||
bridgeConfigureCmd.Flags().SortFlags = false
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ func parseSince(since string) (time.Time, error) {
|
||||
var bridgePullCmd = &cobra.Command{
|
||||
Use: "pull [<name>]",
|
||||
Short: "Pull updates.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runBridgePull,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func runBridgePush(cmd *cobra.Command, args []string) error {
|
||||
var bridgePushCmd = &cobra.Command{
|
||||
Use: "push [<name>]",
|
||||
Short: "Push updates.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runBridgePush,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func runCommentAdd(cmd *cobra.Command, args []string) error {
|
||||
var commentAddCmd = &cobra.Command{
|
||||
Use: "add [<id>]",
|
||||
Short: "Add a new comment to a bug.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runCommentAdd,
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ func runLabelAdd(cmd *cobra.Command, args []string) error {
|
||||
var labelAddCmd = &cobra.Command{
|
||||
Use: "add [<id>] <label>[...]",
|
||||
Short: "Add a label to a bug.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runLabelAdd,
|
||||
}
|
||||
|
||||
|
@ -84,16 +84,10 @@ func loadRepoEnsureUser(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
set, err := identity.IsUserIdentitySet(repo)
|
||||
_, err = identity.GetUserIdentity(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !set {
|
||||
// Print the error directly to not confuse a user
|
||||
_, _ = fmt.Fprintln(os.Stderr, identity.ErrNoIdentitySet.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ func runStatusClose(cmd *cobra.Command, args []string) error {
|
||||
var closeCmd = &cobra.Command{
|
||||
Use: "close [<id>]",
|
||||
Short: "Mark a bug as closed.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runStatusClose,
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ func runStatusOpen(cmd *cobra.Command, args []string) error {
|
||||
var openCmd = &cobra.Command{
|
||||
Use: "open [<id>]",
|
||||
Short: "Mark a bug as open.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runStatusOpen,
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ func runTitleEdit(cmd *cobra.Command, args []string) error {
|
||||
var titleEditCmd = &cobra.Command{
|
||||
Use: "edit [<id>]",
|
||||
Short: "Edit a title of a bug.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runTitleEdit,
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -84,7 +85,7 @@ func runUser(cmd *cobra.Command, args []string) error {
|
||||
var userCmd = &cobra.Command{
|
||||
Use: "user [<user-id>]",
|
||||
Short: "Display or change the user identity.",
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runUser,
|
||||
}
|
||||
|
||||
|
@ -18,10 +18,6 @@ func runUserCreate(cmd *cobra.Command, args []string) error {
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Before creating a new identity, please be aware that "+
|
||||
"you can also use an already existing one using \"git bug user adopt\". As an example, "+
|
||||
"you can do that if your identity has already been created by an importer.\n\n")
|
||||
|
||||
preName, err := backend.GetUserName()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -249,7 +249,7 @@ var webUICmd = &cobra.Command{
|
||||
Available git config:
|
||||
git-bug.webui.open [bool]: control the automatic opening of the web UI in the default browser
|
||||
`,
|
||||
PreRunE: loadRepo,
|
||||
PreRunE: loadRepoEnsureUser,
|
||||
RunE: runWebUI,
|
||||
}
|
||||
|
||||
|
@ -44,12 +44,12 @@ Token configuration can be directly passed with the \-\-token flag or in the ter
|
||||
The owner of the target repository
|
||||
|
||||
.PP
|
||||
\fB\-T\fP, \fB\-\-token\fP=""
|
||||
The authentication token for the API
|
||||
\fB\-c\fP, \fB\-\-credential\fP=""
|
||||
The identifier or prefix of an already known credential for the API (see "git\-bug bridge auth")
|
||||
|
||||
.PP
|
||||
\fB\-i\fP, \fB\-\-token\-id\fP=""
|
||||
The authentication token identifier for the API
|
||||
\fB\-\-token\fP=""
|
||||
A raw authentication token for the API
|
||||
|
||||
.PP
|
||||
\fB\-\-token\-stdin\fP[=false]
|
||||
|
@ -70,15 +70,15 @@ git bug bridge configure \
|
||||
### Options
|
||||
|
||||
```
|
||||
-n, --name string A distinctive name to identify the bridge
|
||||
-t, --target string The target of the bridge. Valid values are [github,gitlab,launchpad-preview]
|
||||
-u, --url string The URL of the target repository
|
||||
-o, --owner string The owner of the target repository
|
||||
-T, --token string The authentication token for the API
|
||||
-i, --token-id string The authentication token identifier for the API
|
||||
--token-stdin Will read the token from stdin and ignore --token
|
||||
-p, --project string The name of the target repository
|
||||
-h, --help help for configure
|
||||
-n, --name string A distinctive name to identify the bridge
|
||||
-t, --target string The target of the bridge. Valid values are [github,gitlab,launchpad-preview]
|
||||
-u, --url string The URL of the target repository
|
||||
-o, --owner string The owner of the target repository
|
||||
-c, --credential string The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")
|
||||
--token string A raw authentication token for the API
|
||||
--token-stdin Will read the token from stdin and ignore --token
|
||||
-p, --project string The name of the target repository
|
||||
-h, --help help for configure
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
@ -25,3 +25,8 @@ func (e ErrMultipleMatch) Error() string {
|
||||
e.entityType,
|
||||
strings.Join(matching, "\n"))
|
||||
}
|
||||
|
||||
func IsErrMultipleMatch(err error) bool {
|
||||
_, ok := err.(*ErrMultipleMatch)
|
||||
return ok
|
||||
}
|
||||
|
@ -23,7 +23,9 @@ const versionEntryName = "version"
|
||||
const identityConfigKey = "git-bug.identity"
|
||||
|
||||
var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge")
|
||||
var ErrNoIdentitySet = errors.New("to interact with bugs, an identity first needs to be created using \"git bug user create\" or \"git bug user adopt\"")
|
||||
var ErrNoIdentitySet = errors.New("No identity is set.\n" +
|
||||
"To interact with bugs, an identity first needs to be created using " +
|
||||
"\"git bug user create\"")
|
||||
var ErrMultipleIdentitiesSet = errors.New("multiple user identities set")
|
||||
|
||||
var _ Interface = &Identity{}
|
||||
@ -218,22 +220,8 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) {
|
||||
return NewIdentity(name, email), nil
|
||||
}
|
||||
|
||||
// IsUserIdentitySet tell if the user identity is correctly set.
|
||||
func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) {
|
||||
configs, err := repo.LocalConfig().ReadAll(identityConfigKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(configs) > 1 {
|
||||
return false, ErrMultipleIdentitiesSet
|
||||
}
|
||||
|
||||
return len(configs) == 1, nil
|
||||
}
|
||||
|
||||
// SetUserIdentity store the user identity's id in the git config
|
||||
func SetUserIdentity(repo repository.RepoCommon, identity *Identity) error {
|
||||
func SetUserIdentity(repo repository.RepoConfig, identity *Identity) error {
|
||||
return repo.LocalConfig().StoreString(identityConfigKey, identity.Id().String())
|
||||
}
|
||||
|
||||
|
@ -404,14 +404,13 @@ _git-bug_bridge_configure()
|
||||
two_word_flags+=("--owner")
|
||||
two_word_flags+=("-o")
|
||||
local_nonpersistent_flags+=("--owner=")
|
||||
flags+=("--credential=")
|
||||
two_word_flags+=("--credential")
|
||||
two_word_flags+=("-c")
|
||||
local_nonpersistent_flags+=("--credential=")
|
||||
flags+=("--token=")
|
||||
two_word_flags+=("--token")
|
||||
two_word_flags+=("-T")
|
||||
local_nonpersistent_flags+=("--token=")
|
||||
flags+=("--token-id=")
|
||||
two_word_flags+=("--token-id")
|
||||
two_word_flags+=("-i")
|
||||
local_nonpersistent_flags+=("--token-id=")
|
||||
flags+=("--token-stdin")
|
||||
local_nonpersistent_flags+=("--token-stdin")
|
||||
flags+=("--project=")
|
||||
|
@ -81,10 +81,9 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock {
|
||||
[CompletionResult]::new('--url', 'url', [CompletionResultType]::ParameterName, 'The URL of the target repository')
|
||||
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'The owner of the target repository')
|
||||
[CompletionResult]::new('--owner', 'owner', [CompletionResultType]::ParameterName, 'The owner of the target repository')
|
||||
[CompletionResult]::new('-T', 'T', [CompletionResultType]::ParameterName, 'The authentication token for the API')
|
||||
[CompletionResult]::new('--token', 'token', [CompletionResultType]::ParameterName, 'The authentication token for the API')
|
||||
[CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'The authentication token identifier for the API')
|
||||
[CompletionResult]::new('--token-id', 'token-id', [CompletionResultType]::ParameterName, 'The authentication token identifier for the API')
|
||||
[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")')
|
||||
[CompletionResult]::new('--credential', 'credential', [CompletionResultType]::ParameterName, 'The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")')
|
||||
[CompletionResult]::new('--token', 'token', [CompletionResultType]::ParameterName, 'A raw authentication token for the API')
|
||||
[CompletionResult]::new('--token-stdin', 'token-stdin', [CompletionResultType]::ParameterName, 'Will read the token from stdin and ignore --token')
|
||||
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'The name of the target repository')
|
||||
[CompletionResult]::new('--project', 'project', [CompletionResultType]::ParameterName, 'The name of the target repository')
|
||||
|
@ -194,8 +194,8 @@ function _git-bug_bridge_configure {
|
||||
'(-t --target)'{-t,--target}'[The target of the bridge. Valid values are [github,gitlab,launchpad-preview]]:' \
|
||||
'(-u --url)'{-u,--url}'[The URL of the target repository]:' \
|
||||
'(-o --owner)'{-o,--owner}'[The owner of the target repository]:' \
|
||||
'(-T --token)'{-T,--token}'[The authentication token for the API]:' \
|
||||
'(-i --token-id)'{-i,--token-id}'[The authentication token identifier for the API]:' \
|
||||
'(-c --credential)'{-c,--credential}'[The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")]:' \
|
||||
'--token[A raw authentication token for the API]:' \
|
||||
'--token-stdin[Will read the token from stdin and ignore --token]' \
|
||||
'(-p --project)'{-p,--project}'[The name of the target repository]:'
|
||||
}
|
||||
|
@ -6,30 +6,32 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ Config = &memConfig{}
|
||||
var _ Config = &MemConfig{}
|
||||
|
||||
type memConfig struct {
|
||||
type MemConfig struct {
|
||||
config map[string]string
|
||||
}
|
||||
|
||||
func newMemConfig(config map[string]string) *memConfig {
|
||||
return &memConfig{config: config}
|
||||
func NewMemConfig() *MemConfig {
|
||||
return &MemConfig{
|
||||
config: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *memConfig) StoreString(key, value string) error {
|
||||
func (mc *MemConfig) StoreString(key, value string) error {
|
||||
mc.config[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *memConfig) StoreBool(key string, value bool) error {
|
||||
func (mc *MemConfig) StoreBool(key string, value bool) error {
|
||||
return mc.StoreString(key, strconv.FormatBool(value))
|
||||
}
|
||||
|
||||
func (mc *memConfig) StoreTimestamp(key string, value time.Time) error {
|
||||
func (mc *MemConfig) StoreTimestamp(key string, value time.Time) error {
|
||||
return mc.StoreString(key, strconv.Itoa(int(value.Unix())))
|
||||
}
|
||||
|
||||
func (mc *memConfig) ReadAll(keyPrefix string) (map[string]string, error) {
|
||||
func (mc *MemConfig) ReadAll(keyPrefix string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
for key, val := range mc.config {
|
||||
if strings.HasPrefix(key, keyPrefix) {
|
||||
@ -39,7 +41,7 @@ func (mc *memConfig) ReadAll(keyPrefix string) (map[string]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (mc *memConfig) ReadString(key string) (string, error) {
|
||||
func (mc *MemConfig) ReadString(key string) (string, error) {
|
||||
// unlike git, the mock can only store one value for the same key
|
||||
val, ok := mc.config[key]
|
||||
if !ok {
|
||||
@ -49,7 +51,7 @@ func (mc *memConfig) ReadString(key string) (string, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (mc *memConfig) ReadBool(key string) (bool, error) {
|
||||
func (mc *MemConfig) ReadBool(key string) (bool, error) {
|
||||
// unlike git, the mock can only store one value for the same key
|
||||
val, ok := mc.config[key]
|
||||
if !ok {
|
||||
@ -59,7 +61,7 @@ func (mc *memConfig) ReadBool(key string) (bool, error) {
|
||||
return strconv.ParseBool(val)
|
||||
}
|
||||
|
||||
func (mc *memConfig) ReadTimestamp(key string) (time.Time, error) {
|
||||
func (mc *MemConfig) ReadTimestamp(key string) (time.Time, error) {
|
||||
value, err := mc.ReadString(key)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
@ -74,7 +76,7 @@ func (mc *memConfig) ReadTimestamp(key string) (time.Time, error) {
|
||||
}
|
||||
|
||||
// RmConfigs remove all key/value pair matching the key prefix
|
||||
func (mc *memConfig) RemoveAll(keyPrefix string) error {
|
||||
func (mc *MemConfig) RemoveAll(keyPrefix string) error {
|
||||
for key := range mc.config {
|
||||
if strings.HasPrefix(key, keyPrefix) {
|
||||
delete(mc.config, key)
|
||||
|
@ -12,8 +12,8 @@ var _ ClockedRepo = &mockRepoForTest{}
|
||||
|
||||
// mockRepoForTest defines an instance of Repo that can be used for testing.
|
||||
type mockRepoForTest struct {
|
||||
config map[string]string
|
||||
globalConfig map[string]string
|
||||
config *MemConfig
|
||||
globalConfig *MemConfig
|
||||
blobs map[git.Hash][]byte
|
||||
trees map[git.Hash]string
|
||||
commits map[git.Hash]commit
|
||||
@ -29,24 +29,25 @@ type commit struct {
|
||||
|
||||
func NewMockRepoForTest() *mockRepoForTest {
|
||||
return &mockRepoForTest{
|
||||
config: make(map[string]string),
|
||||
blobs: make(map[git.Hash][]byte),
|
||||
trees: make(map[git.Hash]string),
|
||||
commits: make(map[git.Hash]commit),
|
||||
refs: make(map[string]git.Hash),
|
||||
createClock: lamport.NewClock(),
|
||||
editClock: lamport.NewClock(),
|
||||
config: NewMemConfig(),
|
||||
globalConfig: NewMemConfig(),
|
||||
blobs: make(map[git.Hash][]byte),
|
||||
trees: make(map[git.Hash]string),
|
||||
commits: make(map[git.Hash]commit),
|
||||
refs: make(map[string]git.Hash),
|
||||
createClock: lamport.NewClock(),
|
||||
editClock: lamport.NewClock(),
|
||||
}
|
||||
}
|
||||
|
||||
// LocalConfig give access to the repository scoped configuration
|
||||
func (r *mockRepoForTest) LocalConfig() Config {
|
||||
return newMemConfig(r.config)
|
||||
return r.config
|
||||
}
|
||||
|
||||
// GlobalConfig give access to the git global configuration
|
||||
func (r *mockRepoForTest) GlobalConfig() Config {
|
||||
return newMemConfig(r.globalConfig)
|
||||
return r.globalConfig
|
||||
}
|
||||
|
||||
// GetPath returns the path to the repo.
|
||||
|
@ -15,6 +15,15 @@ var (
|
||||
ErrMultipleConfigEntry = errors.New("multiple config entry for the given key")
|
||||
)
|
||||
|
||||
// RepoConfig access the configuration of a repository
|
||||
type RepoConfig interface {
|
||||
// LocalConfig give access to the repository scoped configuration
|
||||
LocalConfig() Config
|
||||
|
||||
// GlobalConfig give access to the git global configuration
|
||||
GlobalConfig() Config
|
||||
}
|
||||
|
||||
// RepoCommon represent the common function the we want all the repo to implement
|
||||
type RepoCommon interface {
|
||||
// GetPath returns the path to the repo.
|
||||
@ -31,16 +40,11 @@ type RepoCommon interface {
|
||||
|
||||
// GetRemotes returns the configured remotes repositories.
|
||||
GetRemotes() (map[string]string, error)
|
||||
|
||||
// LocalConfig give access to the repository scoped configuration
|
||||
LocalConfig() Config
|
||||
|
||||
// GlobalConfig give access to the git global configuration
|
||||
GlobalConfig() Config
|
||||
}
|
||||
|
||||
// Repo represents a source code repository.
|
||||
type Repo interface {
|
||||
RepoConfig
|
||||
RepoCommon
|
||||
|
||||
// FetchRefs fetch git refs from a remote
|
||||
|
Loading…
Reference in New Issue
Block a user