aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bridge/core/bridge.go15
-rw-r--r--bridge/core/token.go118
-rw-r--r--bridge/github/config.go77
-rw-r--r--bridge/github/export.go6
-rw-r--r--bridge/github/import.go4
-rw-r--r--bridge/gitlab/config.go74
-rw-r--r--bridge/gitlab/export.go2
-rw-r--r--bridge/gitlab/import.go2
-rw-r--r--commands/bridge_configure.go4
9 files changed, 272 insertions, 30 deletions
diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go
index a3133b9c..3a36dfaa 100644
--- a/bridge/core/bridge.go
+++ b/bridge/core/bridge.go
@@ -13,6 +13,7 @@ import (
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/cache"
+ "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)
@@ -20,8 +21,10 @@ var ErrImportNotSupported = errors.New("import is not supported")
var ErrExportNotSupported = errors.New("export is not supported")
const (
- ConfigKeyTarget = "target"
- MetaKeyOrigin = "origin"
+ ConfigKeyTarget = "target"
+ ConfigKeyToken = "token"
+ ConfigKeyTokenId = "token-id"
+ MetaKeyOrigin = "origin"
bridgeConfigKeyPrefix = "git-bug.bridge"
)
@@ -35,6 +38,7 @@ type BridgeParams struct {
Project string
URL string
Token string
+ TokenId string
TokenStdin bool
}
@@ -276,6 +280,13 @@ 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)
diff --git a/bridge/core/token.go b/bridge/core/token.go
index 449ebbb5..28c64f5c 100644
--- a/bridge/core/token.go
+++ b/bridge/core/token.go
@@ -122,8 +122,7 @@ func LoadTokenPrefix(repo repository.RepoCommon, prefix string) (*Token, error)
return LoadToken(repo, matching[0])
}
-// ListTokens return a map representing the stored tokens in the repo config and global config
-// along with their type (global: true, local:false)
+// ListTokens list all existing token ids
func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) {
configs, err := repo.GlobalConfig().ReadAll(tokenConfigKeyPrefix + ".")
if err != nil {
@@ -157,6 +156,99 @@ func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) {
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)
@@ -180,3 +272,25 @@ 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
+}
diff --git a/bridge/github/config.go b/bridge/github/config.go
index e76a14f4..0fbbd5aa 100644
--- a/bridge/github/config.go
+++ b/bridge/github/config.go
@@ -21,6 +21,7 @@ import (
"golang.org/x/crypto/ssh/terminal"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/interrupt"
)
@@ -43,10 +44,12 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams)
conf := make(core.Configuration)
var err error
var token string
+ var tokenId entity.Id
+ var tokenObj *core.Token
var owner string
var project string
- if (params.Token != "" || params.TokenStdin) &&
+ if (params.Token != "" || params.TokenId != "" || params.TokenStdin) &&
(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")
}
@@ -87,11 +90,11 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams)
return nil, fmt.Errorf("invalid parameter owner: %v", owner)
}
- // try to get token from params if provided, else use terminal prompt to either
- // enter a token or login and generate a new one
+ // 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')
@@ -99,15 +102,33 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams)
return nil, fmt.Errorf("reading from stdin: %v", err)
}
token = strings.TrimSuffix(token, "\n")
+ } else if params.TokenId != "" {
+ tokenId = entity.Id(params.TokenId)
} else {
- token, err = promptTokenOptions(owner, project)
+ tokenObj, err = promptTokenOptions(repo, 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, entity.Id(tokenId))
+ if err != nil {
+ return nil, err
+ }
+ if tokenObj.Target != target {
+ return nil, fmt.Errorf("token target is incompatible %s", tokenObj.Target)
+ }
}
// verify access to the repository with token
- ok, err = validateProject(owner, project, token)
+ ok, err = validateProject(owner, project, tokenObj.Value)
if err != nil {
return nil, err
}
@@ -116,7 +137,7 @@ func (g *Github) Configure(repo repository.RepoCommon, params core.BridgeParams)
}
conf[core.ConfigKeyTarget] = target
- conf[keyToken] = token
+ conf[core.ConfigKeyTokenId] = tokenObj.ID().String()
conf[keyOwner] = owner
conf[keyProject] = project
@@ -135,8 +156,8 @@ func (*Github) 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[core.ConfigKeyTokenId]; !ok {
+ return fmt.Errorf("missing %s key", core.ConfigKeyTokenId)
}
if _, ok := conf[keyOwner]; !ok {
@@ -220,32 +241,58 @@ func randomFingerprint() string {
return string(b)
}
-func promptTokenOptions(owner, project string) (string, error) {
+func promptTokenOptions(repo repository.RepoCommon, owner, project string) (*core.Token, error) {
for {
+ tokens, err := core.LoadTokensWithTarget(repo, target)
+ if err != nil {
+ return nil, err
+ }
+
fmt.Println()
fmt.Println("[1]: user provided token")
fmt.Println("[2]: interactive token creation")
+
+ if len(tokens) > 0 {
+ fmt.Println("known tokens for Github:")
+ for i, token := range tokens {
+ if token.Target == target {
+ fmt.Printf("[%d]: %s\n", i+3, token.ID())
+ }
+ }
+ }
fmt.Print("Select option: ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
fmt.Println()
if err != nil {
- return "", err
+ return nil, err
}
line = strings.TrimRight(line, "\n")
index, err := strconv.Atoi(line)
- if err != nil || (index != 1 && index != 2) {
+ if err != nil || index < 1 || index > len(tokens)+2 {
fmt.Println("invalid input")
continue
}
- if index == 1 {
- return promptToken()
+ var token string
+ switch index {
+ case 1:
+ token, err = promptToken()
+ if err != nil {
+ return nil, err
+ }
+ case 2:
+ token, err = loginAndRequestToken(owner, project)
+ if err != nil {
+ return nil, err
+ }
+ default:
+ return tokens[index-3], nil
}
- return loginAndRequestToken(owner, project)
+ return core.LoadOrCreateToken(repo, target, token)
}
}
diff --git a/bridge/github/export.go b/bridge/github/export.go
index 2fb92636..8d515802 100644
--- a/bridge/github/export.go
+++ b/bridge/github/export.go
@@ -87,14 +87,14 @@ func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache,
return nil, err
}
- ge.identityToken[user.Id()] = ge.conf[keyToken]
+ ge.identityToken[user.Id()] = ge.conf[core.ConfigKeyToken]
// get repository node id
ge.repositoryID, err = getRepositoryNodeID(
ctx,
ge.conf[keyOwner],
ge.conf[keyProject],
- ge.conf[keyToken],
+ ge.conf[core.ConfigKeyToken],
)
if err != nil {
@@ -512,7 +512,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[keyToken]))
+ req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[core.ConfigKeyToken]))
resp, err := client.Do(req)
if err != nil {
diff --git a/bridge/github/import.go b/bridge/github/import.go
index 86444057..c0fb3d6c 100644
--- a/bridge/github/import.go
+++ b/bridge/github/import.go
@@ -39,7 +39,7 @@ func (gi *githubImporter) Init(conf core.Configuration) error {
// 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[keyToken], since)
+ gi.iterator = NewIterator(ctx, 10, gi.conf[keyOwner], gi.conf[keyProject], gi.conf[core.ConfigKeyToken], since)
out := make(chan core.ImportResult)
gi.out = out
@@ -553,7 +553,7 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache,
"login": githubv4.String("ghost"),
}
- gc := buildClient(gi.conf[keyToken])
+ gc := buildClient(gi.conf[core.ConfigKeyToken])
ctx, cancel := context.WithTimeout(gi.iterator.ctx, defaultTimeout)
defer cancel()
diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go
index f2e667a8..88ba7db8 100644
--- a/bridge/gitlab/config.go
+++ b/bridge/gitlab/config.go
@@ -13,6 +13,7 @@ import (
"github.com/xanzy/go-gitlab"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)
@@ -32,6 +33,8 @@ func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams)
var err error
var url string
var token string
+ var tokenId entity.Id
+ var tokenObj *core.Token
if (params.Token != "" || params.TokenStdin) && params.URL == "" {
return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
@@ -65,21 +68,38 @@ func (g *Gitlab) Configure(repo repository.RepoCommon, params core.BridgeParams)
return nil, fmt.Errorf("reading from stdin: %v", err)
}
token = strings.TrimSuffix(token, "\n")
+ } else if params.TokenId != "" {
+ tokenId = entity.Id(params.TokenId)
} else {
- token, err = promptToken()
+ tokenObj, err = promptTokenOptions(repo)
if err != nil {
return nil, errors.Wrap(err, "token prompt")
}
}
+ 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)
+ }
+ }
+
// validate project url and get its ID
- id, err := validateProjectURL(url, token)
+ id, err := validateProjectURL(url, tokenObj.Value)
if err != nil {
return nil, errors.Wrap(err, "project validation")
}
conf[keyProjectID] = strconv.Itoa(id)
- conf[keyToken] = token
+ conf[core.ConfigKeyTokenId] = tokenObj.ID().String()
conf[core.ConfigKeyTarget] = target
err = g.ValidateConfig(conf)
@@ -108,6 +128,54 @@ func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
return nil
}
+func promptTokenOptions(repo repository.RepoCommon) (*core.Token, error) {
+ for {
+ tokens, err := core.LoadTokensWithTarget(repo, target)
+ if err != nil {
+ return nil, err
+ }
+
+ fmt.Println()
+ fmt.Println("[1]: user provided token")
+
+ if len(tokens) > 0 {
+ fmt.Println("known tokens for Gitlab:")
+ for i, token := range tokens {
+ if token.Target == target {
+ fmt.Printf("[%d]: %s\n", i+2, token.ID())
+ }
+ }
+ }
+ fmt.Print("Select option: ")
+
+ line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+ fmt.Println()
+ if err != nil {
+ return nil, err
+ }
+
+ line = strings.TrimRight(line, "\n")
+ index, err := strconv.Atoi(line)
+ if err != nil || index < 1 || index > len(tokens)+1 {
+ fmt.Println("invalid input")
+ continue
+ }
+
+ var token string
+ switch index {
+ case 1:
+ token, err = promptToken()
+ if err != nil {
+ return nil, err
+ }
+ default:
+ return tokens[index-2], nil
+ }
+
+ return core.LoadOrCreateToken(repo, target, token)
+ }
+}
+
func promptToken() (string, error) {
fmt.Println("You can generate a new token by visiting https://gitlab.com/profile/personal_access_tokens.")
fmt.Println("Choose 'Create personal access token' and set the necessary access scope for your repository.")
diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go
index 7c00e39d..092434a5 100644
--- a/bridge/gitlab/export.go
+++ b/bridge/gitlab/export.go
@@ -79,7 +79,7 @@ func (ge *gitlabExporter) ExportAll(ctx context.Context, repo *cache.RepoCache,
return nil, err
}
- ge.identityToken[user.Id().String()] = ge.conf[keyToken]
+ ge.identityToken[user.Id().String()] = ge.conf[core.ConfigKeyToken]
// get repository node id
ge.repositoryID = ge.conf[keyProjectID]
diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go
index 92e9952e..4fcf8568 100644
--- a/bridge/gitlab/import.go
+++ b/bridge/gitlab/import.go
@@ -34,7 +34,7 @@ func (gi *gitlabImporter) Init(conf core.Configuration) error {
// 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[keyToken], since)
+ gi.iterator = NewIterator(ctx, 10, gi.conf[keyProjectID], gi.conf[core.ConfigKeyToken], since)
out := make(chan core.ImportResult)
gi.out = out
diff --git a/commands/bridge_configure.go b/commands/bridge_configure.go
index 3562af17..6f314135 100644
--- a/commands/bridge_configure.go
+++ b/commands/bridge_configure.go
@@ -34,7 +34,8 @@ func runBridgeConfigure(cmd *cobra.Command, args []string) error {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
- if (bridgeParams.TokenStdin || bridgeParams.Token != "") && (bridgeConfigureName == "" || bridgeConfigureTarget == "") {
+ if (bridgeParams.TokenStdin || bridgeParams.Token != "" || bridgeParams.TokenId != "") &&
+ (bridgeConfigureName == "" || bridgeConfigureTarget == "") {
return fmt.Errorf("you must provide a bridge name and target to configure a bridge with a token")
}
@@ -195,6 +196,7 @@ func init() {
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().SortFlags = false