mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-14 17:51:44 +03:00
Merge pull request #260 from MichaelMure/bridge
Support bridge configuration with global tokens
This commit is contained in:
commit
44f648a931
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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.")
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user